A label, a room wall, a face made from several characters: a fixed font can do useful work. Give this routine a row of native screen codes and a cell position. It checks the request, then replaces that span. Your game owns the choice of pictures; the ROM supplies their dots.

Include, set up, call
Copy characters.inc into your project and include it once. Reserve 24 private bytes at FC. Also define COLS=32, ROWS=22 and DF_END=$4010 as in the caller. The first maintained call places a six-character span; the full caller below shows allocations and inclusion:
Complete ZX81 caller
case_0:
ld hl,pictures
ld (FC),hl
ld a,2
ld (FC+2),a
ld a,0
ld (FC+3),a
ld a,6
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($6020),aStock-ROM USR bridge
10 CLS
20 PRINT " "
30 PRINT " "
40 PRINT " "
50 LET A=USR 24832
60 INPUT APut a source pointer at FC+0..1 (low byte first), column at +2, row at +3 and nonzero count at +4. Offset +7 is unused on this machine. Call character_span. Byte +5 returns 1 when accepted, 0 when rejected; +6 returns the number of character writes, or 0. Inputs +0..4 and +7 survive. Scratch/status +5..23 belong to the routine.
A request stays on one row. Zero counts, invalid starts and horizontal overruns reject before any display write. Sinclair requests also reject unallocated cells or unsafe source codes. This variant rejects rather than clips: shorten a label deliberately if that is the behaviour you want.
Keep the source long enough, readable, stable, non-wrapping and separate from anything the call can overwrite. Contexts are not re-entrant. AF/BC/DE/HL and flags are clobbered; IX/IY survive. Reserve four stack bytes for the direct call and its helpers, plus your caller and interrupts. The USR bridge adds eight saved-register bytes and returns BC=0 to BASIC. The routine changes no interrupt state or display mode.
The USR bridge assumes the assembled image is already at $6100. Reserve the context/results area at $6000 and the image through $63FF against BASIC growth before adapting it to a larger program. This small verification caller stays below that region; it does not install a RAMTOP policy or a tape loader for you.
Use the actual display
The ZX81 keeps a moving, newline-terminated display file. Its current address is held at $400C. VARS at $4010 supplies the following allocation boundary; that address means something different from ZX80 DF-END.
A is screen code 38. Codes 0–63 and inverse codes 128–191 are safe character names for this routine. Codes with bit 6 set reject before any write. In particular, 118 is a row terminator/HALT, not a drawable letter.
With the selected 16 KiB configuration the caller has full rows. It can replace a 32-cell row and the final cell of its owned area. The routine still walks row terminators rather than assuming that every future game has the same layout.
The caller uses 22 upper rows as a policy to leave input space; the display hardware has 24. A game that owns the entire valid display file can choose a different row limit. The include reads D_FILE afresh, walks to the row, preflights the whole span and then writes. PRINT, INPUT and editing can relocate or rebuild that file, so cached pointers need a clear lifetime.
The ZX80 normally blanks while computing and refreshes when the ROM waits for input. The ZX81 adds SLOW mode, which shares CPU time with display work; FAST suppresses that work. This component leaves that machinery alone. A standalone assembly game must provide its own refresh policy rather than assuming a free-running bitmap controller.
Connect cells, shapes and maths
At an 8×8 character grid, cell (2,3) begins at active pixel (16,24). That multiplication places a whole glyph; it does not let you change one of its dots. Border coordinates are a separate matter.
Block characters can supply a coarser drawing grid. With a genuine 2×2 block repertoire, coarse point (5,7) belongs to cell (2,3), quadrant (1,1): divide by two for the cell and take the remainders for the quadrant. You still need a lookup from the desired block mask to this ROM’s actual codes. This routine accepts those codes; it does not invent a universal semigraphics table.
Place a point develops local/grid coordinates. Move a glyph connects codes and stored shapes. A fixed font makes the distinction especially clear: a code can select a picture without giving the CPU writable picture memory. Use larger cell pictures or an approximation when the ROM lacks the shape; a high-resolution extension needs its own named configuration.
Read the reusable component and complete caller
Reusable character-span routine
; Define FC (24 private bytes). Source and request must not alias output.
; +0 source word, +2 x, +3 y, +4 count, +5 accepted, +6 writes,
; +7 Aquarius colour (ignored on Sinclair). Inputs +0..4 and +7 survive.
; AF/BC/DE/HL clobbered; IX/IY and interrupt state unchanged.
character_span:
xor a
ld (FC+5),a
ld (FC+6),a
ld a,(FC+2)
cp COLS
ret nc
ld b,a
ld a,COLS
sub b
ld b,a
ld a,(FC+4)
or a
ret z
cp b
jr c,fc_count_fit
ret nz
fc_count_fit:
ld a,(FC+3)
cp ROWS
ret nc
; A current, ROM-owned, well-formed display file. Never allocate or extend it.
ld hl,($400c)
ld de,(DF_END)
ld (FC+12),de
ld a,(hl)
cp $76
ret nz
inc hl
ld a,(FC+3)
ld b,a
or a
jr z,fc_row_ready
fc_skip_row:
ld c,33
fc_scan:
call fc_inside
ret nc
ld a,(hl)
inc hl
cp $76
jr z,fc_newline
dec c
jr nz,fc_scan
ret
fc_newline:
djnz fc_skip_row
fc_row_ready:
ld a,(FC+2)
ld b,a
or a
jr z,fc_start_ready
fc_skip_column:
call fc_inside
ret nc
ld a,(hl)
cp $76
ret z
inc hl
djnz fc_skip_column
fc_start_ready:
ld (FC+10),hl
ld a,(FC+4)
ld b,a
ld de,(FC)
fc_preflight:
call fc_inside
ret nc
ld a,(hl)
cp $76
ret z
ld a,(de)
and $40
ret nz
inc de
inc hl
djnz fc_preflight
ld hl,(FC+10)
ld de,(FC)
ld a,(FC+4)
ld b,a
fc_copy:
ld a,(de)
ld (hl),a
inc de
inc hl
djnz fc_copy
ld a,(FC+4)
ld (FC+6),a
ld a,1
ld (FC+5),a
ret
; Carry means HL is below the exclusive end. Does not clobber BC/DE/HL.
fc_inside:
ld a,(FC+13)
cp h
jr nz,fc_high
ld a,(FC+12)
cp l
fc_high:
jr z,fc_equal
ccf
ret
fc_equal:
or a
ret
character_span_end:Complete caller and configuration
ORG $6100
FC EQU $6000
COLS EQU 32
ROWS EQU 22
DF_END EQU $4010
start:
push af
push bc
push de
push hl
case_0:
ld hl,pictures
ld (FC),hl
ld a,2
ld (FC+2),a
ld a,0
ld (FC+3),a
ld a,6
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($6020),a
case_1:
ld hl,pictures
ld (FC),hl
ld a,0
ld (FC+2),a
ld a,1
ld (FC+3),a
ld a,32
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($6021),a
case_2:
ld hl,pictures
ld (FC),hl
ld a,31
ld (FC+2),a
ld a,2
ld (FC+3),a
ld a,1
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($6022),a
case_3:
ld hl,pictures
ld (FC),hl
ld a,0
ld (FC+2),a
ld a,21
ld (FC+3),a
ld a,1
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($6023),a
case_4:
ld hl,pictures
ld (FC),hl
ld a,32
ld (FC+2),a
ld a,0
ld (FC+3),a
ld a,1
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($6024),a
case_5:
ld hl,pictures
ld (FC),hl
ld a,0
ld (FC+2),a
ld a,22
ld (FC+3),a
ld a,1
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($6025),a
case_6:
ld hl,pictures
ld (FC),hl
ld a,0
ld (FC+2),a
ld a,0
ld (FC+3),a
ld a,0
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($6026),a
case_7:
ld hl,pictures
ld (FC),hl
ld a,31
ld (FC+2),a
ld a,0
ld (FC+3),a
ld a,2
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($6027),a
case_8:
ld hl,pictures
ld (FC),hl
ld a,255
ld (FC+2),a
ld a,0
ld (FC+3),a
ld a,2
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($6028),a
case_9:
ld hl,pictures
ld (FC),hl
ld a,0
ld (FC+2),a
ld a,255
ld (FC+3),a
ld a,1
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($6029),a
case_10:
ld hl,pictures
ld (FC),hl
ld a,0
ld (FC+2),a
ld a,0
ld (FC+3),a
ld a,255
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($602a),a
case_11:
ld hl,bad_picture
ld (FC),hl
ld a,0
ld (FC+2),a
ld a,2
ld (FC+3),a
ld a,2
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($602b),a
case_12:
ld hl,inverse_picture
ld (FC),hl
ld a,0
ld (FC+2),a
ld a,2
ld (FC+3),a
ld a,1
ld (FC+4),a
ld a,0
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($602c),a
ld a,239
ld ($603f),a
pop hl
pop de
pop bc
pop af
ld bc,0
ret
INCLUDE "characters.inc"
pictures:
db 38,39,40,41,42,43,38,39,40,41,42,43,38,39,40,41,42,43,38,39,40,41,42,43,38,39,40,41,42,43,38,39,40,41,42,43,38,39,40,41,42,43
bad_picture:
db 118,38
inverse_picture:
db 166Pay for the work
The component is 164 code bytes, excluding caller and source, with 24 reserved context bytes. A six-character label needs six source bytes and six destination writes. Sinclair also walks preceding rows and checks the target span before copying. Those are storage/work counts, not measured cycles or a promise to fit one frame.
PRINT is simpler when the ROM owns formatting, placement and scrolling. Direct spans suit a game that owns its cells. Cached row pointers or a trusted fast path can reduce work, but their validity depends on allocation lifetime and validated inputs. Clipping, transparent cells, proportional text and arbitrary-size lettering each change the contract.
What was verified
The stock PAL 8 KiB ROM runs the BASIC USR caller with 16 KiB RAM. Qualification places the image at $6100 using debugger writes. Every pixel of the first 22 rows is compared; the bottom two remain available to input. This is not a cassette loader or a 1 KiB footprint claim.
Three builds per machine complete every call, including boundary and rejection cases. Completed wrong-stride controls fail the same memory and visible-output checks. The second control bypasses source-token validation; the independent newline preflight still prevents a count-only overflow.
Native pixel comparisons cover the caller-owned rectangle, with one global alignment for native frame padding. They do not claim that moving input/cursor rows stay unchanged. Retained CI checks source/image/memory hashes, call markers and independent destinations. Fresh execution needs separately supplied firmware. These observations do not establish original-hardware timing, tear-free updates or OS coexistence.