Skip to content

Draw a Tile Region on Master System

Copy a checked rectangle of map cells into Master System's Mode 4 name table, with reusable Z80 code, explicit video ownership and verified native output.

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.

Master System native output: a small coloured map region, a single-cell redraw and isolated edge tiles
Original diagnostic in Emu198x 0.39.0. The rectangle is copied from a larger map; separate edge cells and a changed tile are checked too.

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=2 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

demo.asm, excerpt
initialise_context:
    ld a,3
    ld (TM+14),a
    ld a,$be
    ld (NR+12),a
    ld a,$bf
    ld (NR+13),a
    ld hl,$3800
    ld (NR+14),hl
    ld hl,descriptors
    ld (NR+16),hl
    ret

This caller uses data-write port $BE and control-write port $BF. Its diagnostic reads data from $BE. These are the tested target’s hexadecimal ports.

Each cell contains a low pattern byte and a high attribute byte. The latter includes the ninth pattern bit and flip, palette and priority flags. The example gives ID 3 the horizontal-flip flag, so its arrow faces the opposite direction to the TMS example. Game logic still sees ID 3.

The configured name table is at $3800 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

demo.asm, excerpt
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),a

The 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

region.inc
; 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
    ret

The 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 Master System 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.

For shared pictures, install, replace and select characters separates pattern storage from the names this region routine writes.