Draw a door once and show it in several rooms. Change its glyph, and every cell using that code changes together. Change one cell’s code, and only that instance changes. These routines give you both operations without a whole renderer.
Include, initialise and call
Define G at 24 private zero-page bytes and include glyphs.inc. Clear decimal mode and establish the caller’s unexpanded PAL hi-res configuration first. It sets $9005=$FC and $9002=$96: RAM glyphs at $1000, screen at $1E00. Set each cell’s colour at $9600 with bit 3 clear. The component does not set colours.
Call glyph_init once to blank the owned glyph and name ranges. It destroys their previous contents. Then install the original sketches and select code 1 in two cells:
Install two pictures; display the first twice
; Install two original glyphs in slots 1 and 2, then select slot 1 twice.
lda #<original_glyphs
sta G
lda #>original_glyphs
sta G+1
lda #1
sta G+2
lda #2
sta G+3
jsr glyph_install
lda #1
sta G+4
lda #0
sta G+6
lda #46
sta G+5
jsr glyph_select
inc G+5
jsr glyph_select
lda #0
sta COMMAND
lda #239
sta COMMAND+1The complete caller includes video setup and boot structure. Its mailbox is verification transport; your project calls these entries directly.
The calling contract
All three glyph backends use little-endian words in a 24-byte context at G.
| Offset | Supply or read |
|---|---|
| 0 | Word: readable source address |
| 2, 3 | First slot, nonzero count |
| 4 | Code to select in a cell |
| 5 | Word: linear cell index |
| 7 | Output: 1 accepted, 0 rejected |
| 8 | Word: destination bytes written |
glyph_install uploads a range; glyph_replace uploads one picture and ignores count. glyph_select writes one name entry. glyph_init blanks all owned patterns and cells. Inputs 0–6 survive. Counts reset on rejection. The source must be readable, long enough and non-overlapping with destination/context; it is a trusted pointer, not an arbitrary-memory validator.
Valid slots are 0–127; cell indices are 0–505. Counts run 1–128. Zero counts, ranges past the final slot and invalid cells reject before writing. Codes 128–255 reject in this allocation too.
Make the picture into bytes
Row 10000001 lights the outer two dots: bit weights 128+1 give byte 129. Sketch eight rows like that and you have an 8×8 picture. The source sketches are original Code198x assets under MIT, not ripped game art or a copied ROM font.
One byte stores each row. Row r in slot n is base + n*8 + r; slot 1 begins eight bytes after slot 0.
The Crates opening lesson builds larger artwork from four 8×8 sketches. Its method of turning dots into byte weights applies here too, even though the upload and colour rules differ.
Respect this machine’s display
The font allocation is $1000–$13FF (128 slots); the screen is $1E00–$1FF9 (22×23 cells). The hardware can address more codes: 128 is this routine’s RAM reservation, leaving room for other data on an unexpanded VIC. A 256-code variant needs more reserved RAM and adapted bounds/init. Character codes are screen codes, not PETSCII. Colour bit 3 enables multicolour, changing the interpretation of row bits; this is the high-resolution variant.
Replace a picture; select a different instance
After the example, point G+0 at a new sketch, set G+2=1 and call glyph_replace. Both cells still contain code 1, so both show the new picture. To change only the first, put code 2 in G+4 and its cell index in G+5, then call glyph_select. The second cell keeps code 1.
For a cell at column 2, row 2, the row-major index is 2*22+2 = 46. Local dot (3,5) in that cell appears at screen pixel (2*8+3, 2*8+5) = (19,21). Place a point explores origin and coordinates; here the top-left screen origin makes Y increase downwards. Grid placement does not imply arbitrary-position or scaled text support.

Open the complete reusable 6502 include
Upload, replace and select without a whole game renderer
; SPDX-License-Identifier: MIT
; Unexpanded VIC-20, hi-res 8x8 chars, CPU glyphs $1000..13ff, screen $1e00.
; G reserves 24 zero-page bytes. +0 source word, +2 first slot, +3 count (1..128),
; +4 code, +5 cell word (0..505), +7 status, +8 word bytes written (all LE).
; Inputs preserved. +10..23 scratch. A/X/Y/flags destroyed; decimal mode off.
; glyph_init blanks owned glyphs and screen; caller owns video/colour setup.
; glyph_replace ignores +3, replaces one slot. select never writes glyph RAM.
; Trusted readable source range, no overlap with destination/context; IRQ owners
; must not share G or these resources. No bank or interrupt changes here.
glyph_init:
lda #0
sta G+7
sta G+8
sta G+9
ldx #0
glyph_blank:
sta $1000,x
sta $1100,x
sta $1200,x
sta $1300,x
sta $1e00,x
inx
bne glyph_blank
ldx #0
glyph_blank_tail:
sta $1f00,x
inx
cpx #250
bne glyph_blank_tail
lda #250
sta G+8
lda #5
sta G+9
lda #1
sta G+7
rts
glyph_replace:
lda #1
bne glyph_begin
glyph_install:
lda G+3
glyph_begin:
sta G+10
lda #0
sta G+7
sta G+8
sta G+9
lda G+10
beq glyph_invalid
clc
adc G+2
bcs glyph_invalid
cmp #129
bcs glyph_invalid
lda G+2
asl
sta G+12
lda #0
rol
sta G+13
asl G+12
rol G+13
asl G+12
rol G+13
lda G+13
clc
adc #$10
sta G+13
lda G
sta G+14
lda G+1
sta G+15
ldy #0
glyph_next:
ldx #8
glyph_row:
lda (G+14),y
sta (G+12),y
inc G+14
bne glyph_src_ok
inc G+15
glyph_src_ok:
inc G+12
bne glyph_dst_ok
inc G+13
glyph_dst_ok:
inc G+8
bne glyph_count_ok
inc G+9
glyph_count_ok:
dex
bne glyph_row
dec G+10
bne glyph_next
lda #1
sta G+7
glyph_invalid:
rts
glyph_select:
lda #0
sta G+7
sta G+8
sta G+9
lda G+4
cmp #128
bcs glyph_invalid
lda G+6
cmp #1
bcc glyph_cell_ok
bne glyph_invalid
lda G+5
cmp #250
bcs glyph_invalid
glyph_cell_ok:
lda G+5
sta G+12
lda G+6
clc
adc #$1e
sta G+13
ldy #0
lda G+4
sta (G+12),y
lda #1
sta G+7
sta G+8
rtsPay for what changes
Changing one name costs one destination byte; replacing one picture costs 8. A full init writes 1,530 bytes; that clears the font plus the entire owned name table. Installing every glyph just to change one is wasted work.
The assembled component takes 203 code bytes and 24 context bytes; glyph assets take 8 bytes each. These counts exclude the reset caller, video setup. They are storage and write counts, not measured frame budgets. The font reservation itself is 1 KB.
A/X/Y and flags are clobbered; clear decimal mode and allow four stack bytes including the direct call. Add space for your other code and interrupts. The context is non-re-entrant. The routines do not change interrupt state or banks, and must not race a font cache, other renderer or OS using the same resources.
A font cache, proportional layout, arbitrary-position text and live transfer queue are useful further components. PET and Aquarius ROM characters need a different strategy; an upload routine cannot make fixed ROM writable.
What was verified
All 17 calls completed on this VIC-20 configuration with Asm198x 0.0.71 and Emu198x 0.39.0. Checks compare full pattern/name memory, nonzero neighbours, public inputs, accepted/rejected status and destination-write counts after each request. They include shared replacement, single-cell selection, first/last slots, largest legal uploads, zero/overflow counts, invalid cells and reset. Selected whole native frames match a separate compositor. A completed wrong-slot build fails both memory and visible-output checks.
This is an original reset caller, qualified independently on this machine. It does not establish stock-OS loading, interrupt coexistence, hardware timing.