Skip to content

Draw a Native Region on Game Boy

Reusable Game Boy code for drawing a room, changed tile or strip of scenery, with explicit display ownership.

Open a door, break a wall, reveal the next stretch of scenery. The map changes first; this routine makes the picture catch up. Give it a rectangle of byte-map IDs and a destination, and it draws just that part of the world.

Game Boy native output showing a patterned map patch and a single-cell redraw
Original diagnostic in Emu198x 0.39.0. A 5×3 patch and a single-cell erase are visible; a separate logical bottom-right write is also checked in memory.

Load a room while the LCD is off

This SM83 routine copies logical IDs into the Game Boy’s background map. The caller uploads four original 2bpp patterns separately at $8000–$803F; map IDs 0–3 select those same hardware pattern numbers. A scanline in a pattern is two bytes: low colour plane, then high colour plane.

The selected background map is $9800–$9BFF, 32×32 cells. At zero scroll the screen shows only 20×18 of them. A valid write to the bottom-right map cell is therefore outside the current view. Map coordinates, camera position and visible pixels are different things.

This variant has a firm rule: LCD-off calls only. It checks LCDC bit 7 and rejects the request if the LCD is running. The reset caller waits until LY is at least 145 before disabling a running LCD, following Nintendo’s early-DMG caution, then uploads assets and draws. It enables the LCD afterwards with zero scroll, unsigned pattern addressing, and window/sprites off.

Make it part of your project

Reserve a 32-byte writable NR context in WRAM, define MAP, MAP_W, MAP_H, TILE_COUNT, BG_MAP and the 32×32 screen dimensions, then include sm83.inc once. All allocations must fit their accessible bank. The twelve-by-eight source map uses one byte per logical ID; ID zero is an opaque blank pattern. No scratch initialisation call is needed.

AF/BC/DE/HL and flags are clobbered. Four stack bytes cover the direct CALL and paint helper, beyond caller and interrupt work. Keep the request, map, assets and LCD policy stable throughout the call; an interrupt must not enable the LCD halfway through copying.

Each accepted cell writes one map byte: fifteen for a 5×3 patch, plus the preflight. The four separately loaded pictures cost 64 bytes. This suits room loading or a blanked transition. A live scrolling game needs a bounded VRAM update queue or another access-safe transfer policy. CGB banked attributes and logical-to-physical pattern translation are separate variants, not capabilities hidden in this one.

Draw the patch

These are the example caller’s allocations and constants. Use addresses which fit your own memory plan.

The caller owns its memory and display configuration

demo.asm, excerpt
DEF NR EQU 49152
DEF RESULT EQU 49216
DEF MAP EQU 49392
DEF MAP_W EQU 12
DEF MAP_H EQU 8
DEF TILE_COUNT EQU 4
DEF SCREEN_COLS EQU 32
DEF SCREEN_ROWS EQU 32
DEF BG_MAP EQU 38912

Supply six unsigned bytes: source column, source row, destination column, destination row, width and height. Then call region_draw. NR+6 is one for a completed draw or zero for rejection. The six request bytes survive.

Draw a 5×3 patch from map cell (2,1) at display cell (4,3)

demo.asm, excerpt
case_0:
    ld a,2
    ld [NR+0],a
    ld a,1
    ld [NR+1],a
    ld a,4
    ld [NR+2],a
    ld a,3
    ld [NR+3],a
    ld a,5
    ld [NR+4],a
    ld a,3
    ld [NR+5],a
    call region_draw
    ld a,[NR+6]
    or 128
    ld [RESULT+1],a

The result store after the call belongs to the diagnostic. In a game, inspect the completion flag and continue with your own logic. The routine rejects zero sizes, invalid starts, source/destination overruns and IDs outside the configured tile set. It checks every selected ID before writing any destination bytes, so a bad ID cannot leave half a patch behind. It rejects rather than clips.

Keep the reusable code

Checked native region drawing

sm83.inc
; LCD-off background map transfer. Caller supplies byte-map and tile assets.
; Inputs NR+0..5: source X/Y, destination X/Y, width/height; NR+6=0/1.
; AF/BC/DE/HL and NR+6..31 clobbered. No interrupt or LCD ownership changes.
region_draw:
    xor a
    ld [NR+6],a
    ld a,[$ff40]
    bit 7,a
    ret nz
    ld a,[NR+4]
    and a
    ret z
    ld a,[NR+0]
    cp MAP_W
    ret nc
    ld b,a
    ld a,MAP_W
    sub b
    ld b,a
    ld a,[NR+4]
    cp b
    jr c,bounds_fit_0
    ret nz
bounds_fit_0:
    ld a,[NR+5]
    and a
    ret z
    ld a,[NR+1]
    cp MAP_H
    ret nc
    ld b,a
    ld a,MAP_H
    sub b
    ld b,a
    ld a,[NR+5]
    cp b
    jr c,bounds_fit_1
    ret nz
bounds_fit_1:
    ld a,[NR+4]
    and a
    ret z
    ld a,[NR+2]
    cp SCREEN_COLS
    ret nc
    ld b,a
    ld a,SCREEN_COLS
    sub b
    ld b,a
    ld a,[NR+4]
    cp b
    jr c,bounds_fit_2
    ret nz
bounds_fit_2:
    ld a,[NR+5]
    and a
    ret z
    ld a,[NR+3]
    cp SCREEN_ROWS
    ret nc
    ld b,a
    ld a,SCREEN_ROWS
    sub b
    ld b,a
    ld a,[NR+5]
    cp b
    jr c,bounds_fit_3
    ret nz
bounds_fit_3:
    xor a
    ld [NR+29],a
region_pass:
    xor a
    ld [NR+16],a
region_row:
    xor a
    ld [NR+17],a
region_cell:
    ld hl,MAP
    ld a,[NR+1]
    ld b,a
    ld a,[NR+16]
    add a,b
    and a
    jr z,source_column
    ld b,a
    ld de,MAP_W
source_row:
    add hl,de
    dec b
    jr nz,source_row
source_column:
    ld a,[NR]
    ld b,a
    ld a,[NR+17]
    add a,b
    ld e,a
    ld d,0
    add hl,de
    ld a,[hl]
    ld [NR+19],a
    cp TILE_COUNT
    ret nc
    ld a,[NR+29]
    and a
    call nz,region_paint
    ld hl,NR+17
    inc [hl]
    ld a,[NR+4]
    cp [hl]
    jp nz,region_cell
    ld hl,NR+16
    inc [hl]
    ld a,[NR+5]
    cp [hl]
    jp nz,region_row
    ld a,[NR+29]
    and a
    jr nz,region_success
    inc a
    ld [NR+29],a
    jp region_pass
region_success:
    ld a,1
    ld [NR+6],a
    ret
region_paint:
    ld a,[NR+3]
    ld b,a
    ld a,[NR+16]
    add a,b
    ld l,a
    ld h,0
    add hl,hl
    add hl,hl
    add hl,hl
    add hl,hl
    add hl,hl
    ld de,BG_MAP
    add hl,de
    ld a,[NR+2]
    ld b,a
    ld a,[NR+17]
    add a,b
    ld e,a
    ld d,0
    add hl,de
    ld a,[NR+19]
    ld [hl],a
    ret

The context is non-re-entrant. Your caller owns its memory, video setup and interrupt policy. Keep all inputs and display resources stable throughout the call. Drawing an opaque zero tile erases a cell; transparent sprites need masking or background restoration instead.

What was checked

All 19 calls completed on this machine in the original reset diagnostic. Fifteen rejected map/bounds/ID requests were also executed in isolation, each after the same three accepted draws. The complete destination and both mapped guards were compared with an independent model, and every native capture pixel was compared with a separate composition. Completed wrong-layout and unsafe-destination controls fail those checks.

The additional LCD-on request is rejected without a VRAM write. The full logical map is checked, including cells outside the visible viewport.

These results qualify the selected Emu198x 0.39.0 configuration and Asm198x 0.0.71 fixture. They do not establish original-hardware timing, stock-OS loading, interrupt coexistence or a whole-game frame budget. The complete caller keeps reset setup separate from reusable drawing. Copy counts above are not measured timing claims.