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.

ANTIC decides where a line begins
ANTIC F gives this example a 320×192 monochrome picture: forty 8×8 cells across, twenty-four down. Each picture takes eight bytes, with the leftmost pixel in bit 7. The CPU can copy those bytes directly, but the display must read the same addresses.
ANTIC’s screen-memory counter wraps at a 4 KB boundary. Simply placing all 7,680 display bytes in one consecutive block would let the CPU and display disagree. Here the first block holds 102 complete forty-byte scanlines; line 102 starts at $5000. The tables use $4000 + (y/102)*4096 + (y mod 102)*40.
The diagnostic uses an explicit LMS address for every scanline. That is deliberately readable rather than a minimal display list. Its active 582 bytes fit within a single 1 KB display-list page at $3000; the driver reserves and copies 768 bytes. The bitmap allocation is $4000–$5FFF, with 7,680 used bytes and 512 gap/tail bytes which drawing must preserve.
Make it part of your project
Reserve all 32 bytes of NR in zero page, clear decimal mode, define the map, tile and screen constants, and include the shared 6502 routine once. CELL_BYTES=1 and X_STEP=1. Supply low/high scanline tables matching your actual display list. A new mode, width or buffer address needs matching tables and ANTIC setup.
The example map has twelve byte IDs per row and eight rows. Four tile pictures take eight bytes each; ID zero is an opaque blank. No scratch initialisation call is needed. A/X/Y and flags are clobbered; allow four stack bytes for the direct JSR and paint helper, beyond your caller and interrupt needs.
The address tables cost 384 bytes. One cell copies eight bytes, or 120 for the 5×3 patch, plus ID preflight and address work. Cached source rows or span copying can save work in a larger scene. Character-mode rendering offers a different memory/copy trade-off; it is not a drop-in change of screen address.
The original reset driver owns DMA, interrupts, display list and colours. An OS-loaded game must arrange ownership and memory with the OS rather than treating this reset setup as a loader.
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
NR=112
RESULT=768
MAP=1264
MAP_W=12
MAP_H=8
TILE_COUNT=4
CELL_BYTES=1
SCREEN_COLS=40
SCREEN_ROWS=24
X_STEP=1Supply 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)
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 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
; Caller defines NR (32 zero-page bytes), MAP/MAP_W/MAP_H, TILES,
; TILE_COUNT (1..16), CELL_BYTES (1/2), SCREEN_COLS/ROWS, X_STEP (1/2/8),
; and bitmap_lo/bitmap_hi (one address per pixel scanline).
; Decimal mode must be clear. A/X/Y/flags and NR+6..31 clobbered.
region_draw:
lda #0
sta NR+6
lda NR+4
bne bounds_nonzero_0
rts
bounds_nonzero_0:
lda NR+0
cmp #MAP_W
bcc bounds_start_0
rts
bounds_start_0:
lda #MAP_W
sec
sbc NR+0
cmp NR+4
bcs bounds_fit_0
rts
bounds_fit_0:
lda NR+5
bne bounds_nonzero_1
rts
bounds_nonzero_1:
lda NR+1
cmp #MAP_H
bcc bounds_start_1
rts
bounds_start_1:
lda #MAP_H
sec
sbc NR+1
cmp NR+5
bcs bounds_fit_1
rts
bounds_fit_1:
lda NR+4
bne bounds_nonzero_2
rts
bounds_nonzero_2:
lda NR+2
cmp #SCREEN_COLS
bcc bounds_start_2
rts
bounds_start_2:
lda #SCREEN_COLS
sec
sbc NR+2
cmp NR+4
bcs bounds_fit_2
rts
bounds_fit_2:
lda NR+5
bne bounds_nonzero_3
rts
bounds_nonzero_3:
lda NR+3
cmp #SCREEN_ROWS
bcc bounds_start_3
rts
bounds_start_3:
lda #SCREEN_ROWS
sec
sbc NR+3
cmp NR+5
bcs bounds_fit_3
rts
bounds_fit_3:
lda #0
sta NR+29
region_pass:
lda #0
sta NR+16
region_row:
lda #0
sta NR+17
region_cell:
lda #<MAP
sta NR+20
lda #>MAP
sta NR+21
lda NR+1
clc
adc NR+16
tax
beq source_column
source_row:
clc
lda NR+20
adc #MAP_W
sta NR+20
bcc source_no_carry
inc NR+21
source_no_carry:
dex
bne source_row
source_column:
lda NR
clc
adc NR+17
tay
lda (NR+20),y
sta NR+19
cmp #TILE_COUNT
bcc region_id_ok
rts
region_id_ok:
lda NR+29
beq region_next
jsr region_paint
region_next:
inc NR+17
lda NR+17
cmp NR+4
beq region_row_done
jmp region_cell
region_row_done:
inc NR+16
lda NR+16
cmp NR+5
beq region_pass_done
jmp region_row
region_pass_done:
lda NR+29
bne region_success
inc NR+29
jmp region_pass
region_success:
lda #1
sta NR+6
rts
region_paint:
lda NR+19
asl
asl
asl
!if CELL_BYTES = 2 {
asl
}
clc
adc #<TILES
sta NR+24
lda #>TILES
adc #0
sta NR+25
lda #0
sta NR+18
region_scanline:
lda NR+3
clc
adc NR+16
asl
asl
asl
clc
adc NR+18
tay
lda bitmap_lo,y
sta NR+22
lda bitmap_hi,y
sta NR+23
lda #0
sta NR+28
lda NR+2
clc
adc NR+17
!if X_STEP >= 2 {
asl
rol NR+28
}
!if X_STEP = 8 {
asl
rol NR+28
asl
rol NR+28
}
clc
adc NR+22
sta NR+22
lda NR+23
adc NR+28
sta NR+23
ldy #0
lda (NR+24),y
sta (NR+22),y
!if CELL_BYTES = 2 {
iny
lda (NR+24),y
sta (NR+22),y
}
clc
lda NR+24
adc #CELL_BYTES
sta NR+24
bcc paint_no_carry
inc NR+25
paint_no_carry:
inc NR+18
lda NR+18
cmp #8
bne region_scanline
rtsThe 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 18 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 source selection crosses a CPU page boundary. Reserved attributes and allocation gaps are included in the memory comparison.
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.