Skip to content

Install and Replace Characters on Game Boy

Reusable glyph uploads and cell selection on Game Boy; keep pictures, character numbers and placement separate.

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.

An eight-row cross in slot one, shared by two screen cells containing code one
The picture lives in a slot; screen cells hold its number. Replacement and placement have different effects and different costs.

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

demo.asm, excerpt
; 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_select

The 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.

Game Boy native output with both shared cells showing the replacement pattern
The first replacement in the native check changes both cells. A later single-cell selection changes only one. Complete pattern/name memory and selected native frames are checked separately.
Open the complete reusable SM83 include

Upload, replace and select without a whole game renderer

glyphs.inc
; 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
    ret

Pay 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.