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. The selected display layout is fixed in this machine-specific include. The first maintained call places a six-character span; the full caller below shows allocations and inclusion:
Complete Aquarius 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,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c20),aPut a source pointer at FC+0..1 (low byte first), column at +2, row at +3 and nonzero count at +4. Set the foreground/background byte at +7 too. 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. A start at (0,0) also rejects to protect the surround. 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 two stack bytes for the direct call and its helpers, plus your caller and interrupts. The routine changes no interrupt state or display mode.
Use the actual display
Aquarius stores character names at $3000–$33E7 and matching colours at $3400–$37E7. Cell (2,3) has index 122, so its addresses are $307A and $347A. Each plane has an unchanged 24-byte tail.
Attribute $1F means red foreground on white background: high nibble is foreground, low nibble background. A six-character label writes six names and six colours. The fixed ROM supplies the pictures; this is not character RAM.
Cell zero repeats into the surround. This routine rejects requests starting at (0,0), keeping a label from repainting the border. A game can deliberately own that cell outside the routine. BASIC normally uses rows 1–24; the reset driver owns all 25 rows and both planes.
Caller ownership matters: the OS cursor, scrolling code and an interrupt cannot independently redraw these allocations without coordination. Keep gameplay tiles as logical IDs and translate them into screen codes only when drawing.
For a room rectangle, reuse the existing native region renderer. This smaller span is useful for labels or a changed row. It does not upload a font or turn a character cell into arbitrary pixels.
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
; Cell zero repeats into the surround: preserve it as a caller-owned border.
ld a,(FC+2)
ld b,a
ld a,(FC+3)
or b
ret z
ld hl,$3000
ld a,(FC+3)
ld b,a
or a
jr z,fc_row_ready
ld de,40
fc_next_row:
add hl,de
djnz fc_next_row
fc_row_ready:
ld a,(FC+2)
ld e,a
ld d,0
add hl,de
ld de,(FC)
ld a,(FC+4)
ld b,a
fc_copy:
ld a,(de)
ld (hl),a
inc h
inc h
inc h
inc h
ld a,(FC+7)
ld (hl),a
dec h
dec h
dec h
dec h
inc de
inc hl
djnz fc_copy
ld a,(FC+4)
ld (FC+6),a
ld a,1
ld (FC+5),a
ret
character_span_end:Complete caller and configuration
ORG $0000
FC EQU $3c00
COLS EQU 40
ROWS EQU 25
start:
di
ld sp,$3ff0
ld hl,$3000
ld de,$3001
ld bc,1023
ld (hl),32
ldir
ld hl,$3400
ld de,$3401
ld bc,1023
ld (hl),$f0
ldir
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,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c20),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,40
ld (FC+4),a
ld a,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c21),a
case_2:
ld hl,pictures
ld (FC),hl
ld a,39
ld (FC+2),a
ld a,2
ld (FC+3),a
ld a,1
ld (FC+4),a
ld a,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c22),a
case_3:
ld hl,pictures
ld (FC),hl
ld a,0
ld (FC+2),a
ld a,24
ld (FC+3),a
ld a,1
ld (FC+4),a
ld a,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c23),a
case_4:
ld hl,pictures
ld (FC),hl
ld a,40
ld (FC+2),a
ld a,0
ld (FC+3),a
ld a,1
ld (FC+4),a
ld a,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c24),a
case_5:
ld hl,pictures
ld (FC),hl
ld a,0
ld (FC+2),a
ld a,25
ld (FC+3),a
ld a,1
ld (FC+4),a
ld a,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c25),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,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c26),a
case_7:
ld hl,pictures
ld (FC),hl
ld a,39
ld (FC+2),a
ld a,0
ld (FC+3),a
ld a,2
ld (FC+4),a
ld a,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c27),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,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c28),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,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c29),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,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c2a),a
case_11:
ld hl,pictures
ld (FC),hl
ld a,0
ld (FC+2),a
ld a,0
ld (FC+3),a
ld a,1
ld (FC+4),a
ld a,31
ld (FC+7),a
call character_span
ld a,(FC+5)
or 128
ld ($3c2b),a
ld a,239
ld ($3c3f),a
done:
jp done
INCLUDE "characters.inc"
pictures:
db 65,66,67,68,69,70,65,66,67,68,69,70,65,66,67,68,69,70,65,66,67,68,69,70,65,66,67,68,69,70,65,66,67,68,69,70,65,66,67,68,69,70
bad_picture:
db 118,38
inverse_picture:
db 166Pay for the work
The component is 103 code bytes, excluding caller and source, with 24 reserved context bytes. A six-character label needs six source bytes and twelve destination writes. Aquarius writes both the name and its colour. 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 caller is an original reset diagnostic with a local stock character ROM and no RAM expansion. Context, results and stack fit the mapped working RAM. It does not establish coexistence with the BASIC editor.
Three builds per machine complete every call, including boundary and rejection cases. Completed wrong-stride controls fail the same memory and visible-output checks. A second control bypasses the horizontal bound and is rejected by the independent destination and image checks.
The full native frame, padding and border are compared against a separate compositor using the local stock font. 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.