A door opens. Its map cell changes, and now its picture needs to change too. This routine takes a rectangle of byte-map IDs and paints their 8×8 pictures into the bitmap. Use it for one tile, a whole room or the strip of scenery revealed by a camera.

Make room for the renderer
Define the map, tile pictures, two scanline address tables and a 32-byte context, NR, in your own memory layout. The allocation constants shown here belong to the reset diagnostic; choose addresses which fit your project. The context can live in ordinary writable RAM.
The caller supplies the map, bitmap dimensions and private context
NR equ 17280
RESULT equ 17152
MAP equ 17648
MAP_W equ 12
MAP_H equ 8
TILE_COUNT equ 4
CELL_BYTES equ 2
SCREEN_COLS equ 40
SCREEN_ROWS equ 25
X_STEP equ 2MAP is a row-major byte map, twelve cells wide and eight high in this example. TILES contains four pictures, each with eight scanlines and 2 byte(s) per scanline. ID zero is an opaque empty picture, so drawing it erases a cell. It does not mean “skip this tile”.
The two tables, bitmap_lo and bitmap_hi, hold the low and high address byte for every pixel scanline. Keep the tables, tiles and map stable throughout a call. Include the shared z80.inc routine once, after defining its symbols. The complete caller linked below supplies working tables and tile data as well as video setup.
Draw a rectangle
Set NR+0..5 to source column, source row, destination column, destination row, width and height. Call region_draw; NR+6 is one when it completes and zero when it rejects the request. All six request bytes survive.
Copy a 5×3 patch from map cell (2,1) to display cell (4,3)
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),aThe extra result store belongs to the diagnostic. In a game, inspect NR+6 and continue with your own logic. No separate initialisation call is needed for the context scratch.
The routine rejects zero sizes, out-of-range starts, rectangles which overrun either edge and IDs outside the tile table. It checks every selected ID before painting the first byte. Bounds are checked by comparing the requested size with the space remaining, so byte arithmetic cannot wrap a large request into an apparently small one. This variant rejects rather than clips.
How this bitmap is laid out
MODE 1 gives you 320×200 pixels with four pens: forty cells across and twenty-five down. Each 8-pixel tile scanline needs two packed bytes. Set CELL_BYTES=2 and X_STEP=2 for this routine.
The bitmap occupies the $C000–$FFFF allocation, but successive scanlines are not adjacent. Within a character row they begin $800 bytes apart. After eight scanlines, the next character row starts eighty bytes after the preceding one. The tables encode that layout, including the unused gaps which the tests require to remain unchanged.
Pixel colour bits are interleaved too. Bits 7–4 contain the low colour bit of four pixels; bits 3–0 contain their high colour bit. An ordinary sequence of adjacent two-bit colours will produce the wrong picture. The maintained tile data shows the actual packed bytes.
The reset diagnostic configures the CRTC, Gate Array mode and four pens. It disables the upper ROM so CPU readback sees the bitmap, while retaining its original reset code in the lower ROM. An ordinary game will need its own firmware/ROM and interrupt policy.
The reusable routine
Checked bitmap-region drawing
; Caller-owned NR (32 bytes); same compile-time contract as the 6502 version.
; AF/BC/DE/HL/flags and NR+6..31 clobbered; inputs NR+0..5 preserved.
region_draw:
xor a
ld (NR+6),a
ld a,(NR+4)
or 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)
or 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)
or 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)
or 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
or a
jr z,source_column
ld b,a
ld de,MAP_W
source_row:
add hl,de
djnz 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)
or 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)
or 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+19)
ld l,a
ld h,0
add hl,hl
add hl,hl
add hl,hl
if CELL_BYTES = 2
add hl,hl
endif
ld de,TILES
add hl,de
ld (NR+24),hl
xor a
ld (NR+18),a
region_scanline:
ld a,(NR+3)
ld b,a
ld a,(NR+16)
add a,b
add a,a
add a,a
add a,a
ld b,a
ld a,(NR+18)
add a,b
ld e,a
ld d,0
ld hl,bitmap_lo
add hl,de
ld c,(hl)
ld hl,bitmap_hi
add hl,de
ld b,(hl)
ld a,(NR+2)
ld e,a
ld a,(NR+17)
add a,e
ld l,a
ld h,0
add hl,hl
add hl,bc
ex de,hl
ld hl,(NR+24)
ld a,(hl)
ld (de),a
inc hl
inc de
ld a,(hl)
ld (de),a
inc hl
ld (NR+24),hl
ld hl,NR+18
inc (hl)
ld a,(hl)
cp 8
jr nz,region_scanline
retAF, BC, DE, HL and flags are clobbered. The context scratch may change; only the six request bytes are preserved. Allow four stack bytes for the direct call and its paint helper, plus whatever your caller and interrupt handlers need. The routine is not re-entrant.
Your renderer owns the bitmap, display configuration, palette and interrupt policy. This routine neither sets video registers nor changes interrupt state. An interrupt may run only if it preserves the registers and cannot change the request, map, tables, tiles or affected display memory during the call.
Choose the right cost
One accepted cell copies 16 bitmap bytes. A 5×3 patch copies 240; there is also a full ID preflight and source address work. The two scanline tables cost 400 bytes, trading space for a readable address lookup. These are storage and copy counts, not measured cycle or VBlank budgets.
For a large moving scene, cache source-row pointers, copy longer spans or queue a limited amount of work per frame. For a sprite, consider masking and background restoration instead of opaque tile replacement. A camera at a map edge needs clipping of source and destination together. Those are useful variants; they each change the contract.
What was checked
All 18 calls completed in this machine’s reset diagnostic: three accepted draws and fifteen rejected requests. Each rejection was also executed in isolation after the accepted draws, and left the entire allocated bitmap and guard unchanged. The non-square source selection crosses a CPU page boundary.
The observer compares complete bitmap memory with an independent expected layout, then compares every native capture pixel with a separate composition. Both completed wrong-layout and unsafe-destination controls fail. The captures include the emulator’s borders and scaling; they are not photographs of a CRT.
This qualifies the selected Emu198x 0.39.0 configuration with Asm198x 0.0.71. The publishing assembler, 0.0.65, produces byte-identical fixtures. It does not establish stock-OS loading, interrupt coexistence, original-hardware timing, tearing-free animation or a whole-game frame budget.