A door opens, and one map cell changes. This routine redraws its picture without rebuilding the whole screen. The same call can paint a room or the strip of scenery revealed by a camera: choose a source rectangle and a destination, then let the machine’s renderer do the copying.

The capture uses an original diagnostic font ROM, supplied by the test fixture for repeatable glyph checks. It is not the stock font and does not show software-redefinable characters on this machine. Set your game’s descriptors to codes in its installed ROM.
Give the map a screen vocabulary
On the 40-column PET, one screen byte selects a picture from the installed character ROM. A logical wall ID can select any appropriate screen code through glyph_codes; it need not equal that code. The example deliberately maps IDs 0,1,2,3 to codes 0,3,1,2.
The active screen is 40×25 cells at $8000–$83E7. The checked allocation includes all 1,024 bytes through $83FF, including its unused 24-byte tail. This configuration has no per-cell colour RAM: define HAS_COLOUR=0 so the shared 6502 include omits colour writes.
A screen code is not necessarily the byte you would print through the editor. Choose descriptors for the installed ROM and character set. This component does not make the fixed character ROM writable. Build scenery from its available glyphs, perhaps combining several cells into a larger object.
Reserve your workspace
Define NR, MAP, MAP_W, MAP_H, TILE_COUNT, SCREEN, SCREEN_COLS=40, SCREEN_ROWS=25, HAS_COLOUR=0, and a glyph_codes byte per ID. Include character-regions/6502.inc once. All 32 bytes of context must be in zero page; clear decimal mode before calling. No scratch initialisation call is needed.
The source map has twelve byte IDs per row and eight rows. Source width is 1–128 and height 1–255; allocations must not wrap or overlap the destination. A/X/Y and flags are clobbered. Four stack bytes cover the direct JSR and paint helper, beyond your own caller/interrupt use.
A 5×3 patch writes fifteen screen bytes plus preflight/address work. There is no eight-byte picture copy per cell. The installed font saves picture RAM and copy work, while limiting which shapes can appear. The original reset caller disables the cursor and owns the display; an editor-sharing program needs its own ownership agreement.
Draw the rectangle
The constants belong to the original caller’s working memory plan. Choose allocations which fit your own project, keeping setup and drawing separate.
Caller-owned memory and display constants
NR=$0070
RESULT=$0300
MAP=$04f0
MAP_W=12
MAP_H=8
TILE_COUNT=4
SCREEN_COLS=40
SCREEN_ROWS=25
SCREEN=$8000
HAS_COLOUR=0
COLOUR=$0000Translate logical IDs into native screen codes and colours
glyph_codes:
!byte 0,3,1,2
tile_colours:
!byte 1,1,2,5Supply six unsigned bytes at NR+0..5: source column, source row, destination column, destination row, width and height. Call region_draw; NR+6 is one for completion 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, read the completion flag and carry on with your own logic. The routine rejects zero sizes, invalid starts, source/destination overruns and IDs outside the configured table. Every selected ID is checked before painting begins, so a bad tile cannot leave half a patch behind. This variant rejects rather than clips.
Keep the routine
Checked character and colour region drawing
; Checked character/colour region. See README for symbols, state and ownership.
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 #<SCREEN
sta NR+22
lda #>SCREEN
sta NR+23
lda NR+3
clc
adc NR+16
tax
beq char_column
char_row:
clc
lda NR+22
adc #SCREEN_COLS
sta NR+22
bcc char_row_no_carry
inc NR+23
char_row_no_carry:
dex
bne char_row
char_column:
lda NR+2
clc
adc NR+17
tay
ldx NR+19
lda glyph_codes,x
sta (NR+22),y
!if HAS_COLOUR = 1 {
clc
lda NR+22
adc #<(COLOUR-SCREEN)
sta NR+22
lda NR+23
adc #>(COLOUR-SCREEN)
sta NR+23
lda tile_colours,x
sta (NR+22),y
}
rtsThe context is private and non-re-entrant. Keep the request, map, descriptors or picture data, and display configuration stable throughout the call. Your caller owns video setup, allocations and interrupt policy; the routine does not disable interrupts or negotiate with an OS. Interrupt handlers must preserve the used registers and must not change those resources halfway through drawing.
What was checked
All 18 calls completed: three accepted draws and fifteen map/bounds/ID rejections. Each rejected request also ran in isolation after the same three accepted draws, preserving complete destination allocations and both mapped guards. The source selection crosses a CPU page boundary.
The observer compares every destination byte, unused allocation tails and stored colour bits where present, with an independent model. It compares every native capture pixel with a separate composition, including borders and scaling. Completed wrong-layout and unsafe-destination controls fail the same memory checks; wrong-layout pictures also fail the pixel check.
These results qualify the selected Emu198x 0.39.0 configuration with Asm198x 0.0.71 and the original reset fixture. They do not establish stock-OS loading, original-hardware timing, interrupt coexistence, tear-free updates or throughput.