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 entire context must fit in zero page, and decimal mode must be clear.
The caller supplies the map, bitmap dimensions and private context
NR=112
RESULT=768
MAP=1264
MAP_W=12
MAP_H=8
TILE_COUNT=4
CELL_BYTES=1
SCREEN_COLS=32
SCREEN_ROWS=24
X_STEP=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 6502.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
The Atom’s highest monochrome mode gives you 256×192 pixels, or thirty-two by twenty-four 8×8 cells. It needs the full 6 KB graphics RAM at $8000–$97FF. The presence of the graphics commands in an unexpanded Atom does not supply that RAM.
Rows are linear: the next pixel scanline starts thirty-two bytes later. Each byte contains eight pixels, most significant bit on the left. The next cell is one byte across, so CELL_BYTES=1 and X_STEP=1.
The original driver configures the 8255 and selects mode 4 through $B000. That register also selects a keyboard row in its lower nibble. The drawing routine only writes bitmap RAM; your input and video setup must agree on the port value. The separate guard lives after the map because the address immediately below video RAM is unmapped.
The reusable routine
Checked bitmap-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
rtsA, X, Y 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 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.
- Complete caller, tables and execution evidence
- Shared calling contract, costs and primary references
- Other native region routines
- The separate map/query family
For individual dots and shapes rather than tile copies, pixels, spans and boxes supply separately verified reusable bitmap routines.