Skip to content

Place ROM characters on Aquarius

Draw counted labels and character pictures with the machine's ROM, with explicit bounds and display ownership.

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.

Native Aquarius output from the reusable character-span caller.
Code198x native capture: 40×25 cells with paired colour bytes. 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. Character designs belong to the machine's manufacturer; no firmware is distributed.

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

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

Put 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

characters.inc
; 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

demo.asm
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 166

Pay 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.