Draw a door once and show it in several rooms. Change its glyph, and every cell using that code changes together. Change one cell’s code, and only that instance changes. These routines give you both operations without a whole renderer.
Include, initialise and call
Define G at 24 private WRAM bytes and include glyphs.inc in your ROM section. Configure unsigned background tiles at $8000 and the background map at $9800. Every entry requires the LCD off. The complete caller’s lcd_off helper waits until LY≥145 before stopping a running LCD; after updating it selects LCDC=$91 and BGP=$E4. Keep scrolling at zero for this example.
Call glyph_init once to blank the owned glyph and name ranges. It destroys their previous contents. Then install the original sketches and select code 1 in two cells:
Install two pictures; display the first twice
; The example source is 2bpp: low plane then high plane for each row.
ld hl,original_glyphs
ld a,l
ld [G],a
ld a,h
ld [G+1],a
ld a,1
ld [G+2],a
ld a,2
ld [G+3],a
call glyph_install
ld a,1
ld [G+4],a
xor a
ld [G+6],a
ld a,66
ld [G+5],a
call glyph_select
ld a,67
ld [G+5],a
call glyph_selectThe complete caller includes video setup and boot structure. Its mailbox is verification transport; your project calls these entries directly.
The calling contract
All three glyph backends use little-endian words in a 24-byte context at G.
| Offset | Supply or read |
|---|---|
| 0 | Word: readable source address |
| 2, 3 | First slot, nonzero count |
| 4 | Code to select in a cell |
| 5 | Word: linear cell index |
| 7 | Output: 1 accepted, 0 rejected |
| 8 | Word: destination bytes written |
glyph_install uploads a range; glyph_replace uploads one picture and ignores count. glyph_select writes one name entry. glyph_init blanks all owned patterns and cells. Inputs 0–6 survive. Counts reset on rejection. The source must be readable, long enough and non-overlapping with destination/context; it is a trusted pointer, not an arbitrary-memory validator.
Valid slots are 0–255; cell indices are 0–1023. Counts run 1–255; two calls can upload all 256 slots. Zero counts, ranges past the final slot and invalid cells reject before writing. All byte-sized codes are valid.
Make the picture into bytes
Row 10000001 lights the outer two dots: bit weights 128+1 give byte 129. Sketch eight rows like that and you have an 8×8 picture. The source sketches are original Code198x assets under MIT, not ripped game art or a copied ROM font.
Two bytes store each row. The low-plane row address is base + n*16 + r*2; the next byte is its high plane. Low=$81/high=$00 gives colour index 1 at the two outer dots. Setting both planes there gives index 3.
The Crates opening lesson builds larger artwork from four 8×8 sketches. Its method of turning dots into byte weights applies here too, even though the upload and colour rules differ.
Respect this machine’s display
Tiles occupy $8000–$8FFF and the 32×32 background map occupies $9800–$9BFF. Only 20×18 cells are visible with zero scroll; cell 1023 is legal but off screen. Each row has two bytes: low plane then high plane, both with the leftmost pixel in bit 7. Their bits produce palette indices 0–3. BGP then selects shades. LCD-on calls return rejection before touching VRAM. Stopping the LCD is a bulk construction baseline, not a recommended way to animate every frame; a bounded live upload queue is a later variant.
Replace a picture; select a different instance
After the example, point G+0 at a new sketch, set G+2=1 and call glyph_replace. Both cells still contain code 1, so both show the new picture. To change only the first, put code 2 in G+4 and its cell index in G+5, then call glyph_select. The second cell keeps code 1.
For a cell at column 2, row 2, the row-major index is 2*32+2 = 66. Local dot (3,5) in that cell appears at screen pixel (2*8+3, 2*8+5) = (19,21). Place a point explores origin and coordinates; here the top-left screen origin makes Y increase downwards. Grid placement does not imply arbitrary-position or scaled text support.

Open the complete reusable SM83 include
Upload, replace and select without a whole game renderer
; SPDX-License-Identifier: MIT
; DMG, unsigned background tiles $8000..8fff, map $9800..9bff.
; G reserves 24 RAM bytes: +0 source LE word,+2 first,+3 count (1..255),
; +4 code,+5 cell LE word (0..1023),+7 status,+8 LE word bytes written.
; Inputs survive; AF/BC/DE/HL/flags destroyed. Source readable/non-overlapping.
; ALL entries require LCD off (LCDC bit 7=0) and reject before writing if on.
; Caller disables LCD safely, owns VRAM, banks/IRQs. No interrupts changed here.
; Rows are low-plane byte then high-plane byte; 16 bytes per 8x8 glyph.
glyph_init:
call glyph_reset
ret nz
ld hl,$8000
ld bc,4096
call glyph_zero
ld hl,$9800
ld bc,1024
call glyph_zero
ld hl,5120
ld a,l
ld [G+8],a
ld a,h
ld [G+9],a
ld a,1
ld [G+7],a
ret
glyph_zero:
xor a
ld [hl],a
inc hl
dec bc
ld a,b
or c
jr nz,glyph_zero
ret
glyph_replace:
ld b,1
jr glyph_begin
glyph_install:
ld a,[G+3]
ld b,a
glyph_begin:
call glyph_reset
ret nz
ld a,b
or a
ret z
ld e,a
ld d,0
ld a,[G+2]
ld l,a
ld h,0
add hl,de
ld de,257
ld a,h
cp d
jr c,glyph_range_ok
ret nz
ld a,l
cp e
ret nc
glyph_range_ok:
ld a,[G+2]
ld l,a
ld h,0
add hl,hl
add hl,hl
add hl,hl
add hl,hl
ld de,$8000
add hl,de
push hl
ld l,b
ld h,0
add hl,hl
add hl,hl
add hl,hl
add hl,hl
ld a,l
ld [G+8],a
ld c,a
ld a,h
ld [G+9],a
ld b,a
ld a,[G]
ld l,a
ld a,[G+1]
ld h,a
pop de
glyph_copy:
ld a,[hl]
inc hl
ld [de],a
inc de
dec bc
ld a,b
or c
jr nz,glyph_copy
ld a,1
ld [G+7],a
ret
glyph_select:
call glyph_reset
ret nz
ld a,[G+6]
cp 4
ret nc
add a,$98
ld h,a
ld a,[G+5]
ld l,a
ld a,[G+4]
ld [hl],a
ld a,1
ld [G+7],a
ld [G+8],a
ret
glyph_reset:
xor a
ld [G+7],a
ld [G+8],a
ld [G+9],a
ld a,[$ff40]
and $80
retPay for what changes
Changing one name costs one destination byte; replacing one picture costs 16. A full init writes 5,120 bytes; that clears the font plus the entire owned name table. Installing every glyph just to change one is wasted work.
The assembled component takes 186 code bytes and 24 context bytes; glyph assets take 16 bytes each. These counts exclude the reset caller, video setup and the LCD-stop helper. They are storage and write counts, not measured frame budgets. The complete pattern allocation is 4 KB.
AF/BC/DE/HL and flags are clobbered; allow eight stack bytes including nested calls. Add space for your other code and interrupts. The context is non-re-entrant. The routines do not change interrupt state or banks, and must not race a font cache, other renderer or OS using the same resources.
A font cache, proportional layout, arbitrary-position text and live transfer queue are useful further components. PET and Aquarius ROM characters need a different strategy; an upload routine cannot make fixed ROM writable.
What was verified
All 17 calls completed on this Game Boy configuration with Asm198x 0.0.71 and Emu198x 0.39.0. Checks compare full pattern/name memory, nonzero neighbours, public inputs, accepted/rejected status and destination-write counts after each request. They include shared replacement, single-cell selection, first/last slots, largest legal uploads, zero/overflow counts, invalid cells and reset plus LCD-on rejection. Selected whole native frames match a separate compositor. A completed wrong-slot build fails both memory and visible-output checks.
This is an original reset caller, qualified independently on this machine. It does not establish stock-OS loading, interrupt coexistence, hardware timing or live LCD-on uploads.