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, in zero page. 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 PET caller
case_0:
lda #<pictures
sta FC+0
lda #>pictures
sta FC+1
lda #2
sta FC+2
lda #0
sta FC+3
lda #6
sta FC+4
jsr character_span
lda FC+5
ora #128
sta $0090Put 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. All 256 byte codes are accepted as names; their pictures depend on the installed character ROM and selected set. 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. A/X/Y/flags are clobbered; decimal mode must be clear. 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
PET stores one screen code per cell at $8000–$83E7. Cell (2,3) is $8000+3*40+2 = $807A. The remaining 24 bytes in the 1 KiB span are padding. There is no per-cell colour RAM here.
PETSCII is the language of PRINT; a direct screen code selects a ROM picture. Use the installed character set when choosing codes. This caller writes codes 1–6 to display A–F. Changing those name bytes does not change the font.
This is the 40-column configuration. Its green display treatment is monochrome; it does not turn green into a programmable cell colour. An 80-column machine or graphics board needs a separate configuration.
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 zero-page bytes). Caller owns the 40-column PET screen.
; +0 source word, +2 x, +3 y, +4 count, +5 accepted, +6 writes.
; Inputs +0..4 survive. Scratch +5..23 may change. A/X/Y/flags clobbered.
; No colour RAM, charset/mode changes or interrupt changes.
character_span:
lda #0
sta FC+5
sta FC+6
lda FC+2
cmp #40
bcs fc_reject
lda FC+3
cmp #25
bcs fc_reject
lda FC+4
beq fc_reject
lda #40
sec
sbc FC+2
cmp FC+4
bcc fc_reject
lda #0
sta FC+10
lda #$80
sta FC+11
ldx FC+3
beq fc_row_ready
fc_next_row:
clc
lda FC+10
adc #40
sta FC+10
bcc fc_row_no_carry
inc FC+11
fc_row_no_carry:
dex
bne fc_next_row
fc_row_ready:
clc
lda FC+10
adc FC+2
sta FC+10
bcc fc_start_ready
inc FC+11
fc_start_ready:
ldy #0
fc_copy:
lda (FC),y
sta (FC+10),y
iny
cpy FC+4
bne fc_copy
sty FC+6
lda #1
sta FC+5
fc_reject:
rts
character_span_end:Complete caller and configuration
*=$f000
FC=$0070
start:
sei
cld
ldx #$ff
txs
lda #10
sta $e880
lda #$20
sta $e881
ldx #0
lda #32
clear_screen:
sta $8000,x
sta $8100,x
sta $8200,x
sta $8300,x
inx
bne clear_screen
case_0:
lda #<pictures
sta FC+0
lda #>pictures
sta FC+1
lda #2
sta FC+2
lda #0
sta FC+3
lda #6
sta FC+4
jsr character_span
lda FC+5
ora #128
sta $0090
case_1:
lda #<pictures
sta FC+0
lda #>pictures
sta FC+1
lda #0
sta FC+2
lda #1
sta FC+3
lda #40
sta FC+4
jsr character_span
lda FC+5
ora #128
sta $0091
case_2:
lda #<pictures
sta FC+0
lda #>pictures
sta FC+1
lda #39
sta FC+2
lda #2
sta FC+3
lda #1
sta FC+4
jsr character_span
lda FC+5
ora #128
sta $0092
case_3:
lda #<pictures
sta FC+0
lda #>pictures
sta FC+1
lda #0
sta FC+2
lda #24
sta FC+3
lda #1
sta FC+4
jsr character_span
lda FC+5
ora #128
sta $0093
case_4:
lda #<pictures
sta FC+0
lda #>pictures
sta FC+1
lda #40
sta FC+2
lda #0
sta FC+3
lda #1
sta FC+4
jsr character_span
lda FC+5
ora #128
sta $0094
case_5:
lda #<pictures
sta FC+0
lda #>pictures
sta FC+1
lda #0
sta FC+2
lda #25
sta FC+3
lda #1
sta FC+4
jsr character_span
lda FC+5
ora #128
sta $0095
case_6:
lda #<pictures
sta FC+0
lda #>pictures
sta FC+1
lda #0
sta FC+2
lda #0
sta FC+3
lda #0
sta FC+4
jsr character_span
lda FC+5
ora #128
sta $0096
case_7:
lda #<pictures
sta FC+0
lda #>pictures
sta FC+1
lda #39
sta FC+2
lda #0
sta FC+3
lda #2
sta FC+4
jsr character_span
lda FC+5
ora #128
sta $0097
case_8:
lda #<pictures
sta FC+0
lda #>pictures
sta FC+1
lda #255
sta FC+2
lda #0
sta FC+3
lda #2
sta FC+4
jsr character_span
lda FC+5
ora #128
sta $0098
case_9:
lda #<pictures
sta FC+0
lda #>pictures
sta FC+1
lda #0
sta FC+2
lda #255
sta FC+3
lda #1
sta FC+4
jsr character_span
lda FC+5
ora #128
sta $0099
case_10:
lda #<pictures
sta FC+0
lda #>pictures
sta FC+1
lda #0
sta FC+2
lda #0
sta FC+3
lda #255
sta FC+4
jsr character_span
lda FC+5
ora #128
sta $009a
lda #239
sta $00af
done:
jmp done
!source "characters.inc"
pictures:
!byte 1,2,3,4,5,6,1,2,3,4,5,6,1,2,3,4,5,6,1,2,3,4,5,6,1,2,3,4,5,6,1,2,3,4,5,6,1,2,3,4,5,6
!fill $fffc-*,0
!word start,startPay for the work
The component is 86 code bytes, excluding caller and source, with 24 reserved context bytes. A six-character label needs six source bytes and six destination writes. PET writes names only. 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. It disables the cursor and owns the screen; it does not demonstrate loading under the PET 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.