Skip to content

Install and Replace Characters on VIC-20

Reusable glyph uploads and cell selection on VIC-20; keep pictures, character numbers and placement separate.

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.

An eight-row cross in slot one, shared by two screen cells containing code one
The picture lives in a slot; screen cells hold its number. Replacement and placement have different effects and different costs.

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

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

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

VIC-20 native output with both shared cells showing the replacement pattern
The first replacement in the native check changes both cells. A later single-cell selection changes only one. Complete pattern/name memory and selected native frames are checked separately.
Open the complete reusable 6502 include

Upload, replace and select without a whole game renderer

glyphs.inc
; 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
    rts

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