A door opens. Redraw its cell with a small, deliberate update. This routine takes a rectangle from your level map and writes the matching native cells at a chosen screen position. The same call can draw a room, a changed patch of scenery or one tile.

Include and initialise
The existing byte-map context, TM, holds the map pointer, width, height and highest logical ID. Add an 18-byte region context, NR, for the rectangle, VDP ports, name-table base and descriptor pointer. Define CELL_BYTES=1 and NT_STRIDE=32; include the existing Z80 map lookup and the region routine. Reserve your own CPU memory and VRAM before using the sample’s addresses.
Initialise this machine's drawing context
initialise_context:
ld a,3
ld (TM+14),a
ld a,$08
ld (NR+12),a
ld a,$09
ld (NR+13),a
ld hl,$1800
ld (NR+14),hl
ld hl,descriptors
ld (NR+16),hl
retThis caller uses data-write port $08 and control-write port $09. Its diagnostic reads data from $08. These are the tested target’s hexadecimal ports.
One name-table byte selects a pattern. This Graphics I example translates IDs 0–3 to patterns 0, 8, 16 and 24, putting the coloured patterns in separate eight-pattern colour groups. Pattern and colour upload are separate from the region copy.
The configured name table is at $1800 in this caller, with 32×24 logical cells. The VDP registers must select that table; the table, patterns, colours/palette and sprites need separate, non-overlapping allocations. The diagnostic configures the display itself. A BIOS or another renderer will need an explicit agreement about those resources.
Draw a rectangle
Set NR+0..5 to source column, source row, destination column, destination row, width and height. Call region_draw, then check NR+6: one means the rectangle completed; zero means it was rejected before any VDP write.
Draw the 7×5 selection and record its result
example_call:
ld hl,main_map
ld (TM),hl
ld a,12
ld (TM+2),a
ld a,8
ld (TM+3),a
ld hl,main_rectangle
ld de,NR
ld bc,6
ldir
call region_draw
ld a,(NR+6)
or $80 ; nonzero diagnostic marker, including rejected calls
ld (RESULT+1),aThe sample’s main_rectangle is 2,1,8,5,7,5: take seven columns and five rows starting at map cell (2,1), then place them at logical screen cell (8,5). Source rows are twelve cells wide; destination rows are thirty-two native cells wide. Those strides have different jobs.
Zero extents, source/destination overflow and IDs above the configured maximum are rejected. Coordinates are unsigned bytes. This variant checks bounds; it does not clip. Clip source and destination together before calling if your camera can reach a map edge.
The reusable routine
Checked native region drawing
; TM: existing 20-byte byte-map context. NR: caller-owned 18-byte region.
; NR: srcC,srcR,dstC,dstR,width,height,valid,row,col,scratch,srcPtrLo,Hi,
; dataWritePort,controlWritePort,nameBaseLo,Hi,descriptorPtrLo,Hi.
; CELL_BYTES is 1 (Graphics I) or 2 (Sega Mode 4). NT_STRIDE is 32.
; Caller owns a stable map/table, VDP latch/ports and name-table rectangle.
; No interrupt changes: exclude concurrent VDP users. See README for timing.
region_draw:
xor a
ld (NR+6),a
ld a,(NR+4)
or a
ret z
ld a,(NR+5)
or a
ret z
; Compare extent with remaining space, rather than overflowing origin+size.
ld a,(TM+2)
ld b,a
ld a,(NR)
cp b
ret nc
ld c,a
ld a,b
sub c
ld b,a
ld a,(NR+4)
cp b
jr c,region_source_width_ok
ret nz
region_source_width_ok:
ld a,(TM+3)
ld b,a
ld a,(NR+1)
cp b
ret nc
ld c,a
ld a,b
sub c
ld b,a
ld a,(NR+5)
cp b
jr c,region_source_height_ok
ret nz
region_source_height_ok:
ld a,(NR+2)
cp 32
ret nc
ld b,a
ld a,32
sub b
ld b,a
ld a,(NR+4)
cp b
jr c,region_dest_width_ok
ret nz
region_dest_width_ok:
ld a,(NR+3)
cp 24
ret nc
ld b,a
ld a,24
sub b
ld b,a
ld a,(NR+5)
cp b
jr c,region_dest_height_ok
ret nz
region_dest_height_ok:
; Preflight every ID before the first OUT: rejection cannot partly draw.
xor a
ld (NR+7),a
region_preflight_row:
call region_source_row
xor a
ld (NR+8),a
region_preflight_cell:
ld a,(TM+14)
ld b,a
ld a,(hl)
cp b
jr c,region_id_ok
ret nz
region_id_ok:
inc hl
ld a,(NR+8)
inc a
ld (NR+8),a
ld b,a
ld a,(NR+4)
cp b
jr nz,region_preflight_cell
ld a,(NR+7)
inc a
ld (NR+7),a
ld b,a
ld a,(NR+5)
cp b
jr nz,region_preflight_row
xor a
ld (NR+7),a
region_draw_row:
call region_source_row
ld (NR+10),hl
; Destination and source have independent row strides.
ld hl,(NR+14)
ld a,(NR+3)
ld b,a
ld a,(NR+7)
add a,b
ld b,a
ld de,NT_STRIDE*CELL_BYTES
or a
jr z,region_dest_column
region_dest_rows:
add hl,de
djnz region_dest_rows
region_dest_column:
ld a,(NR+2)
ld e,a
ld d,0
IF CELL_BYTES=2
sla e
ENDIF
add hl,de
call region_write_address
ld hl,(NR+10)
xor a
ld (NR+8),a
region_draw_cell:
ld a,(hl)
push hl
ld l,a
ld h,0
IF CELL_BYTES=2
add hl,hl
ENDIF
ld de,(NR+16)
add hl,de
ld a,(NR+12)
ld c,a
ld a,(hl)
call region_out
IF CELL_BYTES=2
inc hl
ld a,(hl)
call region_out
ENDIF
pop hl
inc hl
ld a,(NR+8)
inc a
ld (NR+8),a
ld b,a
ld a,(NR+4)
cp b
jr nz,region_draw_cell
ld a,(NR+7)
inc a
ld (NR+7),a
ld b,a
ld a,(NR+5)
cp b
jp nz,region_draw_row
ld a,1
ld (NR+6),a
ret
region_source_row:
ld a,(NR)
ld (TM+4),a
ld a,(NR+1)
ld b,a
ld a,(NR+7)
add a,b
ld (TM+5),a
call tile_locate
; tile_locate leaves the computed pointer in TM+8/9, on every CPU port.
ld hl,(TM+8)
ret
region_write_address:
ld a,(NR+13)
ld c,a
ld a,l
call region_out
ld a,h
or $40
call region_out
ret
region_out:
ld b,0
out (c),a
nop
nop
nop
nop
retThe preflight reads every selected ID before the first output instruction. That costs another pass, but an invalid tile near the end cannot leave half a rectangle drawn. The map and descriptor table must stay unchanged throughout the call.
AF, BC, DE and HL are clobbered, along with region scratch and the map-query column/row, valid flag and address scratch. The six rectangle inputs and region configuration survive. The 20-byte map context and 18-byte region context are caller-owned; the deepest call uses six stack bytes including the direct CALL and saved pointer. Add space for the rest of your program.
Fit it into your renderer
The caller owns the VDP address latch and ports during the call. Exclude interrupts or other code that can use them. The routine neither installs handlers nor changes interrupt state. These examples disable interrupts and blank the display during updates, then enable it to inspect the result.
The output helper deliberately spaces VDP accesses. At the tested clocks its minimum 62-T-state interval exceeds the TI manual’s worst-case eight-microsecond access delay. This does not mean every rectangle fits VBlank. A running game can queue a bounded number of cells per frame, or use cached row pointers and pretranslated entries to trade memory for speed. Other display modes need their own asset and table contracts.
What was checked
All 22 calls completed on this Einstein TC01 target: a non-square selection, redraw, source/destination corners, zero and oversized extents, byte-coordinate extremes and an invalid ID after a valid one. The map crosses a CPU page boundary. The observer checks the complete name table and following guard through actual VDP readback, then compares every raw framebuffer pixel with an independent composition. Completed wrong-stride and unsafe-destination versions are rejected.
The qualification is this original reset diagnostic in the selected Emu198x 0.39.0 default machine/video configuration, built with Asm198x 0.0.71. It establishes these VRAM writes and this native picture. Original hardware, stock-OS loading, concurrent interrupt use and frame throughput remain separate checks.
- Calling contract, ports, sources and alternatives
- Complete caller and retained execution evidence
- Native region family and other machines
- Separate map/query routines
For reusable picture installation and shared-instance changes, custom characters supplies the independently checked upload and cell-selection calls.