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 896
RESULT equ 768
MAP equ 1264
MAP_W equ 12
MAP_H equ 8
TILE_COUNT equ 4
CELL_BYTES equ 1
SCREEN_COLS equ 32
SCREEN_ROWS equ 24
X_STEP equ 1MAP is a row-major byte map, twelve cells wide and eight high in this example. TILES contains four pictures, each with eight scanlines and 1 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 6809.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
lda #2
sta NR+0
lda #1
sta NR+1
lda #4
sta NR+2
lda #3
sta NR+3
lda #5
sta NR+4
lda #3
sta NR+5
jsr region_draw
lda NR+6
ora #128
sta RESULT+1The 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
PMODE 4 gives you 256×192 monochrome pixels: thirty-two cells across and twenty-four down. This example places its 6 KB bitmap at $0600–$1DFF. Rows are linear, thirty-two bytes apart; one byte holds eight pixels, most significant bit first. Set CELL_BYTES=1 and X_STEP=1.
The reset driver configures the PIA, VDG and SAM separately. SAM selects the display base in 512-byte units. Moving the bitmap therefore means changing both the video configuration and the address tables; changing a CPU pointer alone will not move the picture.
There is a useful 6809 trap in the table lookup. An offset in B is signed. Pixel scanline 192 would therefore address before the table if used directly as B,X. The routine clears A and uses unsigned D indexing for the scanline tables. Scratch pointers use the 6809’s big-endian byte order. The map itself remains ordinary byte IDs.
The reusable routine
Checked bitmap-region drawing
; NR is 32 caller-owned bytes. Same scalar/compile-time contract; 6809
; pointers in scratch NR+20..27 are big-endian. A/B/X/CC clobbered.
region_draw
clr NR+6
lda NR+4
bne bounds_nonzero_0
rts
bounds_nonzero_0
lda NR+0
cmpa #MAP_W
blo bounds_start_0
rts
bounds_start_0
lda #MAP_W
suba NR+0
cmpa NR+4
bhs bounds_fit_0
rts
bounds_fit_0
lda NR+5
bne bounds_nonzero_1
rts
bounds_nonzero_1
lda NR+1
cmpa #MAP_H
blo bounds_start_1
rts
bounds_start_1
lda #MAP_H
suba NR+1
cmpa NR+5
bhs bounds_fit_1
rts
bounds_fit_1
lda NR+4
bne bounds_nonzero_2
rts
bounds_nonzero_2
lda NR+2
cmpa #SCREEN_COLS
blo bounds_start_2
rts
bounds_start_2
lda #SCREEN_COLS
suba NR+2
cmpa NR+4
bhs bounds_fit_2
rts
bounds_fit_2
lda NR+5
bne bounds_nonzero_3
rts
bounds_nonzero_3
lda NR+3
cmpa #SCREEN_ROWS
blo bounds_start_3
rts
bounds_start_3
lda #SCREEN_ROWS
suba NR+3
cmpa NR+5
bhs bounds_fit_3
rts
bounds_fit_3
clr NR+29
region_pass
clr NR+16
region_row
clr NR+17
region_cell
ldx #MAP
lda NR+1
adda NR+16
beq source_column
source_row
leax MAP_W,x
deca
bne source_row
source_column
ldb NR
addb NR+17
lda b,x
sta NR+19
cmpa #TILE_COUNT
blo region_id_ok
rts
region_id_ok
tst NR+29
beq region_next
jsr region_paint
region_next
inc NR+17
lda NR+17
cmpa NR+4
beq region_row_done
jmp region_cell
region_row_done
inc NR+16
lda NR+16
cmpa NR+5
beq region_pass_done
jmp region_row
region_pass_done
tst NR+29
bne region_success
inc NR+29
jmp region_pass
region_success
lda #1
sta NR+6
rts
region_paint
clra
ldb NR+19
lslb
rola
lslb
rola
lslb
rola
addd #TILES
std NR+24
clr NR+18
region_scanline
ldb NR+3
addb NR+16
lslb
lslb
lslb
addb NR+18
; B-indexed addressing is signed on 6809: use unsigned D for rows >=128.
clra
ldx #bitmap_lo
lda d,x
sta NR+23
clra
ldx #bitmap_hi
lda d,x
sta NR+22
clra
ldb NR+2
addb NR+17
addd NR+22
std NR+22
ldx NR+24
lda ,x+
stx NR+24
ldx NR+22
sta ,x
inc NR+18
lda NR+18
cmpa #8
bne region_scanline
rtsA, B, X and condition codes are clobbered; Y, U and DP are preserved. 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 8 bitmap bytes. A 5×3 patch copies 120; there is also a full ID preflight and source address work. The two scanline tables cost 384 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.