A life bar, a room outline or a tiny drawing tool needs a few reliable drawing calls. These routines let you change a bit, read it back, or paint a box without borrowing a game’s renderer.

Include it and make a first call
Define BP at 48 private RAM bytes, include z80.inc, then include the existing bitmap-addressing.inc. Set SP to a valid stack. The complete caller uses BP=$7000 and SP=$FF00; those are allocations to change deliberately in your game.
Initialise and clear the display once. This example then sets pixel (0,0); the context’s unused end fields need no initialisation for a point call. The complete reset caller also contains a mailbox used by the verifier. Your game calls the routine directly.
Set one pixel using the reusable component
; Example: set pixel (0,0). Attributes belong to the caller.
ld hl,0
ld (BP),hl
ld (BP+2),hl
xor a
ld (BP+8),a
call bp_pixelThe screen origin is the top left. X increases rightwards and Y downwards. This mode is 256×192; the last pixel is (255,191).
The calling contract
Reserve 48 context bytes at BP. The word fields are little-endian on this CPU.
| Offset | What to supply or read |
|---|---|
| 0, 2 | x0, y0: unsigned 16-bit coordinates |
| 4, 6 | x1, y1: unsigned 16-bit exclusive ends |
| 8 | Operation: 0 set, 1 clear, 2 XOR |
| 9 | Output: 1 accepted, 0 rejected |
| 10 | Output: the bit read, as 0 or 1 |
| 12–47 | Private scratch; word at 40 counts destination-byte writes |
bp_fill paints [x0,x1)×[y0,y1); bp_box outlines it. bp_hspan uses x0/x1/y0, bp_vspan uses x0/y0/y1, and bp_pixel/bp_read use x0/y0. Set operation to zero for reads. Public inputs survive. Unused end fields are ignored.
A box from (5,20) to (13,24) is eight pixels wide and four high. A 1×1 outline touches its pixel once, including with XOR. Empty, reversed, invalid-operation or out-of-bounds requests reject before any destination write. The routine does not silently clip.
From a coordinate to a byte
The Spectrum is three pictures of 64 scanlines each, with rows shuffled again inside each third. At (13,20), pixel_addr returns $4441, and the mask is $04. The helper finds a byte; the new routine decides which bits may change. It leaves all 768 colour attributes untouched. Set INK/PAPER separately; changing a bit alone does not choose its colour.
Integer division and remainder tell us two different things: 13 div 8 = 1 chooses a byte column, while 13 mod 8 = 5 chooses a bit within it. The leftmost pixel uses bit 7, so 128 >> 5 = 4. Set ORs that mask into the old byte; clear ANDs its inverse; XOR toggles it. Writing $04 straight over the byte would erase seven neighbours.
Place a point explores position and origin. Its world Y points upwards; these screen coordinates point downwards. For integer points in a bottom-left-origin world of this height, convert with screenY = 191 - worldY. The width calculation needs no sign reversal: 13-5=8, height 24-20=4, area 8*4=32 pixels.
The maintained implementation
Open the complete reusable Z80 include
Include this backend once in your program
; SPDX-License-Identifier: MIT
; Spectrum 48K. BP reserves 48 bytes of RAM. Native little-endian public words:
; +0 x0,+2 y0,+4 x1,+6 y1 (exclusive ends); +8 op 0=set/1=clear/2=XOR.
; +9 accepted,+10 read value,+40 word destination-write count. +12..47 scratch.
; Inputs survive; destroys AF/BC/DE/HL, flags; stack <=8 bytes including caller.
; Requires existing pixel_addr (D=y,E=x), no attribute writes, not re-entrant.
bp_fill:
xor a
jr bp_enter
bp_box:
ld a,1
jr bp_enter
bp_pixel:
ld a,2
jr bp_enter
bp_read:
ld a,3
jr bp_enter
bp_hspan:
ld a,4
jr bp_enter
bp_vspan:
ld a,5
bp_enter:
ld (BP+12),a
xor a
ld (BP+9),a
ld (BP+10),a
ld (BP+40),a
ld (BP+41),a
ld hl,BP
ld de,BP+16
ld bc,8
ldir
ld a,(BP+12)
cp 2
jr c,bp_validate
cp 5
jr z,bp_unit_x
ld hl,(BP+18)
inc hl
ld (BP+22),hl
cp 4
jr z,bp_validate
bp_unit_x:
ld hl,(BP+16)
inc hl
ld (BP+20),hl
bp_validate:
ld a,(BP+8)
cp 3
ret nc
ld hl,(BP+20)
ld de,257
or a
sbc hl,de
ret nc
ld hl,(BP+16)
ld de,(BP+20)
or a
sbc hl,de
ret nc
ld hl,(BP+22)
ld de,193
or a
sbc hl,de
ret nc
ld hl,(BP+18)
ld de,(BP+22)
or a
sbc hl,de
ret nc
ld a,1
ld (BP+9),a
ld a,(BP+16)
and 7
ld e,a
ld d,0
ld hl,bp_right_masks
add hl,de
ld a,(hl)
ld (BP+24),a
ld hl,bp_bit_masks
add hl,de
ld a,(hl)
ld (BP+26),a
ld a,(BP+20)
and 7
ld e,a
ld hl,bp_left_masks
add hl,de
ld a,(hl)
ld (BP+25),a
ld hl,(BP+20)
dec hl
ld a,l
and 7
ld e,a
ld hl,bp_bit_masks
add hl,de
ld a,(hl)
ld (BP+27),a
ld a,(BP+16)
srl a
srl a
srl a
ld (BP+28),a
ld hl,(BP+20)
dec hl
ld a,l
srl a
srl a
srl a
ld (BP+29),a
ld a,(BP+18)
ld (BP+32),a
ld a,(BP+22)
dec a
ld (BP+42),a
bp_row:
xor a
ld (BP+39),a
ld a,(BP+32)
ld d,a
ld a,(BP+16)
ld e,a
call pixel_addr
ld (BP+34),hl
ld a,(BP+28)
ld (BP+30),a
ld a,(BP+24)
ld (BP+36),a
ld a,(BP+25)
ld (BP+37),a
ld a,(BP+12)
cp 1
jr nz,bp_bytes
ld a,(BP+18)
ld b,a
ld a,(BP+32)
cp b
jr z,bp_bytes
ld a,(BP+42)
ld b,a
ld a,(BP+32)
cp b
jr z,bp_bytes
ld a,1
ld (BP+39),a
ld a,(BP+26)
ld (BP+36),a
ld a,(BP+27)
ld (BP+37),a
bp_bytes:
ld c,255
ld a,(BP+28)
ld b,a
ld a,(BP+30)
cp b
jr nz,bp_not_first
ld a,(BP+36)
ld c,a
bp_not_first:
ld a,(BP+29)
ld b,a
ld a,(BP+30)
cp b
jr nz,bp_not_last
ld a,(BP+37)
and c
ld c,a
ld a,(BP+39)
or a
jr z,bp_not_last
ld a,(BP+28)
cp b
jr nz,bp_not_last
ld a,(BP+36)
ld b,a
ld a,(BP+37)
or b
ld c,a
bp_not_last:
ld a,(BP+39)
or a
jr z,bp_apply
ld a,(BP+30)
ld b,a
ld a,(BP+28)
cp b
jr z,bp_apply
ld a,(BP+29)
cp b
jr nz,bp_advance
bp_apply:
ld hl,(BP+34)
ld a,(BP+12)
cp 3
jr nz,bp_write
ld a,(hl)
and c
ret z
ld a,1
ld (BP+10),a
ret
bp_write:
ld a,(BP+8)
or a
jr z,bp_set
cp 1
jr z,bp_clear
ld a,(hl)
xor c
jr bp_store
bp_clear:
ld a,c
cpl
and (hl)
jr bp_store
bp_set:
ld a,c
or (hl)
bp_store:
ld (hl),a
ld hl,(BP+40)
inc hl
ld (BP+40),hl
bp_advance:
ld a,(BP+29)
ld b,a
ld a,(BP+30)
cp b
jr z,bp_next_row
inc a
ld (BP+30),a
ld hl,(BP+34)
inc hl
ld (BP+34),hl
jp bp_bytes
bp_next_row:
ld a,(BP+22)
ld b,a
ld a,(BP+32)
inc a
cp b
ret z
ld (BP+32),a
jp bp_row
bp_right_masks:
db 255,127,63,31,15,7,3,1
bp_left_masks:
db 255,128,192,224,240,248,252,254
bp_bit_masks:
db 128,64,32,16,8,4,2,1Choose a path for the work
The 32×8 aligned fill in the native check makes 32 byte writes; shifting it one pixel makes 40. The eight-pixel-wide example above makes eight writes for its 32 pixels. Calling a plot routine for each pixel would repeat addressing and edge work. This backend works a byte at a time, preserving the first and last byte’s unrelated bits.
A full-display clear makes 6,144 writes. The kernel, including its masks, takes 489 assembled bytes, plus the 44-byte existing addressing helpers; BP needs 48 RAM bytes. Assets/display memory and the reset caller are additional. These are measured storage and write counts, not a frame-budget or cross-CPU speed ranking. The write counter itself also costs work.
An unchecked inner loop, cached row address or specialised full-byte store can be smaller/faster for a particular caller. This version still reads a byte before a full-byte set/clear; an outline skips stores to middle columns but still visits them. Clipped lines, circles and ellipses add a reusable geometry layer over this unchanged backend.
Own the resources
AF/BC/DE/HL and flags are clobbered. The context is not re-entrant. Reserve at least eight stack bytes for the routines, plus the rest of your program and interrupts. Keep context, code and any address tables out of the bitmap; do not let another renderer or interrupt handler change them during a call. The routines do not change interrupt state. Colour, palette and video setup remain caller-owned.
What was verified
All 94 calls completed independently on this Spectrum 48K configuration using Asm198x 0.0.71 and Emu198x 0.39.0. Every call checks the complete bitmap, guards or attribute neighbours, status, read value, unchanged public inputs and byte-write count. Cases cover all bit phases, set/clear/XOR/read, spans, one-pixel and full-size boxes, empty/reversed extents and invalid bounds. Selected whole native frames match a separate composition. A completed wrong-mask version fails both memory and picture comparisons.
This is an original reset fixture. It establishes these calls in this mode; stock-OS loading, physical-machine timing, interrupt coexistence and tear-free animation need their own checks.
- Complete caller and execution evidence
- Reusable include
- Full contracts, measured space/work costs and primary references
- Compare the three bitmap layouts
- Understand the address helper
Draw bitmap text reuses these spans for glyphs at arbitrary pixel positions, with an explicit font, advance and scissor.