A single door picture can appear in many places. Replacing it changes every cell using its number. Changing one cell’s number affects only that instance. Here is reusable code for both jobs.

Include, initialise and call
Reserve 24 private zero-page bytes at G. Clear decimal mode, include the machine’s component and establish the caller-owned video configuration before calling glyph_init.
Include the maintained component
kernel_start:
!source "glyphs.inc"Init clears the owned patterns and names. It destroys their old contents; call it during your setup, not before every tile change. The complete caller establishes the video state. Then install two original sketches and put the first in two cells:
Initialise, install pictures, then select the first twice
jsr glyph_init
example_install:
lda #<original_patterns
sta G
lda #>original_patterns
sta G+1
lda #1
sta G+2
lda #2
sta G+3
jsr glyph_install
lda #1
sta G+4
lda #<82
sta G+5
lda #>82
sta G+6
jsr glyph_select
inc G+5
jsr glyph_selectTo replace both instances, point the source at a new sketch, set first=1 and call glyph_replace. To change only one cell, select code 2 there. Games call these entries directly; the later mailbox and readback buffer are optional verification transport.
The calling contract
Words are little-endian. glyph_install uploads a range, glyph_replace uploads one picture and ignores count, glyph_select changes one name. Status and data-write count reset on every call, including rejection.
| Offset | Supply or read |
|---|---|
| 0 | CPU source address, word |
| 2, 3 | First slot and nonzero count, bytes |
| 4 | Selected code, byte |
| 5 | Linear cell, word |
| 7 | Output: 1 accepted, 0 rejected |
| 8 | Output: destination bytes written, word |
| 10–23 | Private scratch |
Valid slots/codes are 0–255, cells 0–999. Counts are bytes, 1–255; an exclusive range end of 256 is legal. Zero counts, overflowing ranges and invalid cells reject without destination writes. Use multiple calls to install all 256 pictures.
Keep the source readable, stable, long enough, non-wrapping and separate from the context and destination. These are trusted pointers, not a memory sandbox. The context is non-re-entrant. Inputs 0–6 survive; A/X/Y and flags are clobbered. Reserve six stack bytes including the direct call. Add space for your own calls and interrupts.
Respect the display
The caller selects VIC bank 0 with CIA2, $D018=$18, $D011=$1B and $D016=$08. Pictures occupy $2000–$27FF, names $0400–$07E7. A character number is a screen code, not PETSCII. The component preserves the 24 padding bytes after the names and the separately owned colour RAM at $D800–$DBFF. Set high-resolution colours yourself; multicolour changes what the bits mean.
The original reset caller uses CPU mapping $37 and blank RAM. It supplies its own pictures; it does not copy a ROM font or keep BASIC/KERNAL running. The stock-ROM wrapper below keeps the standard letters while adding private pictures. Your project must reserve the same RAM and avoid another renderer, OS or interrupt using it.
Turn a sketch into stored pixels
Each picture has eight bytes, one row per byte, bit 7 on the left. Row 10000001 is 128+1 = 129. Slot n, row r lives at $2000+n*8+r. Slot 255 begins at $27F8 and ends at $27FF.
For cell (2,2), the index is 2*40+2 = 82. Dot (3,5) within it appears at screen pixel (2*8+3, 2*8+5) = (19,21). The colour byte belongs to that cell, while the shared picture belongs to its slot.
Move a glyph connects these bit weights and cell indices with text drawing. Place a point covers local-to-screen coordinates. These routines place fixed-grid pictures; they do not provide proportional text or arbitrary-size letters.
Read the complete reusable 6502 component
Initialise, install, replace and select
; SPDX-License-Identifier: MIT
; C64 standard hi-res character RAM component. G = 24 private zero-page bytes.
; +0 CPU source word; +2 first byte; +3 count byte (1..255); +4 code byte;
; +5 cell word; +7 status; +8 data-write count word; +10..23 scratch. LE words.
; Inputs 0..6 preserved. A/X/Y/flags clobbered, decimal clear; non-reentrant.
; Source must remain readable without wrapping/overlap. No bank/IRQ changes.
; Caller owns VIC bank, character/screen bases, colour RAM and CPU mapping.
glyph_init:
jsr glyph_reset_result
lda #<8192
sta G+12
lda #>8192
sta G+13
lda #<2048
sta G+16
lda #>2048
sta G+17
jsr glyph_zero_range
lda #<1024
sta G+12
lda #>1024
sta G+13
lda #<1000
sta G+16
lda #>1000
sta G+17
jsr glyph_zero_range
jmp glyph_accept
glyph_zero_range:
jsr glyph_address
zero_next:
lda #0
jsr glyph_store
jsr glyph_decrement
bne zero_next
rts
glyph_replace:
lda #1
bne glyph_begin
glyph_install:
lda G+3
glyph_begin:
sta G+10
jsr glyph_reset_result
lda G+10
beq glyph_reject
clc
adc G+2
bcc glyph_range_ok
bne glyph_reject ; carry plus zero is the legal exclusive end 256
glyph_range_ok:
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 #>8192
sta G+13
jsr glyph_address
lda G
sta G+14
lda G+1
sta G+15
lda G+10
sta G+16
lda #0
sta G+17
asl G+16
rol G+17
asl G+16
rol G+17
asl G+16
rol G+17
ldy #0
copy_next:
lda (G+14),y
jsr glyph_store
inc G+14
bne source_ok
inc G+15
source_ok:
jsr glyph_decrement
bne copy_next
glyph_accept:
lda #1
sta G+7
glyph_reject:
rts
glyph_select:
jsr glyph_reset_result
lda G+6
cmp #>1000
bcc cell_ok
bne glyph_reject
lda G+5
cmp #<1000
bcs glyph_reject
cell_ok:
lda G+5
sta G+12
lda G+6
clc
adc #>1024
sta G+13
jsr glyph_address
lda G+4
jsr glyph_store
jmp glyph_accept
glyph_reset_result:
lda #0
sta G+7
sta G+8
sta G+9
rts
glyph_decrement:
lda G+16
bne length_low
dec G+17
length_low:
dec G+16
lda G+16
ora G+17
rts
glyph_address:
rts
glyph_store:
ldy #0
sta (G+12),y
inc G+12
bne destination_ok
inc G+13
destination_ok:
inc G+8
bne count_ok
inc G+9
count_ok:
rtsPay for what changes
A single name costs 1 destination byte; replacing a picture costs 8. Full init writes 3,048 bytes. The assembled component takes 238 code bytes and 24 context bytes; each source picture takes 8 bytes. These exclude the reset caller, video setup and assets.
If many cells use one picture and all should change, a replacement can save writes. If one door should open, change its name. These are storage and data-write counts, not elapsed time. Colour updates are separate work. Measure inside your actual game before promising a frame rate. Upload queues, caches and alternate allocations are useful further components.
What was verified
20 calls completed per build with Asm198x 0.0.71 and Emu198x 0.39.0. Checks compare complete font, names, padding and colour RAM, guards, inputs, status and actual destination-write counts. They cover shared replacement, single-cell selection, last slots/cells, maximum uploads, zero/overflow requests and reset.
Selected whole native frames match a separate compositor. Completed wrong-slot and wrong-name builds fail both memory and visible-output comparisons. Retained checks independently rederive the expectations; corruption tests reject rehashed wrong memory and pictures. Colour RAM is read by CPU code into a private buffer; debugger reads intentionally show underlying RAM.
This is an original reset fixture, not proof of stock-OS loading, interrupt coexistence, hardware timing or tear-free live updates. Palette RGB values describe the pinned emulator release. Code and source pictures are MIT licensed.
- Complete caller and execution evidence
- Full contracts, costs and hardware references
- Compare writable character systems
- Map storage and drawing
Keep the standard letters and add your own pictures
You do not have to redraw an alphabet to add a door or a character. glyph_init_rom copies one 2 KiB half of the machine’s character ROM into the existing $2000 font allocation. Then use the same glyph_install, glyph_replace and glyph_select routines above. Unchanged letters retain their familiar shapes; every cell using a replaced slot changes together.
The new rom-demo.asm is a stock-ROM-loaded PRG, separate from the original reset-ROM fixture. Load it and RUN. The first Fire press and release installs an original sketch in slot 1 and selects it in cells 82 and 83. The next Fire press replaces that sketch in both cells; holding the button adds no further replacement. rom-custom.asm chooses the lowercase ROM half. This is a standalone takeover program and stays in its own loop.
The stock-ROM PRG includes and chosen half
HALF=0
ROM_WRONG=0
!source "rom-caller.inc"Include and call the ROM initializer
Define G as 24 private zero-page bytes, ROM_WRONG=0 for the working initializer, and include glyphs.inc followed by rom-init.inc. Keep the code, context and stack below $A000. Supply A=0 or 1 for the ROM half and X=your known $01 output-latch shadow. After the call, G+7 is accepted/rejected and G+8/9 is the destination-byte count, as with the other glyph routines. Inputs G through G+6 survive; A/X/Y change. The caller’s processor flags, including decimal and interrupt state, are restored.
The wrapper requires the three banking DDR bits to be outputs and KERNAL to be already banked out. It checks those conditions and that the shadow matches the port’s output bits before making any destination writes. It cannot discover an incorrect shadow for a pin configured as an input: keep the real latch shadow yourself. Reading $01 is not a substitute for that shadow.
Own NMI handling too. Disable CIA2 NMI sources and provide a safe RAM NMI vector and handler before calling. The handler must tolerate I/O being hidden. SEI only masks IRQ; it does not mask NMI. This wrapper keeps KERNAL out so the RAM vector stays visible. It is not an OS-friendly routine that can borrow arbitrary BASIC/KERNAL state and return safely.
The temporary mapping selects character ROM rather than the VIC/SID/CIA registers. The exact caller-supplied latch value is restored afterwards; $00 is untouched. The initializer writes 2,048 font bytes, without clearing the screen or colour RAM. Configure VIC bank 0 and the $2000 font as in the complete caller. See the Commodore Programmer’s Reference Guide, memory management for the bank-control port.
Copy a ROM half and restore the caller's mapping
; SPDX-License-Identifier: MIT
; Extend glyphs.inc, G=24 private ZP bytes, code/context below $a000.
; A=ROM half 0/1. X=caller's known $01 output-latch shadow (not a port read).
; Standard no-cartridge mapping; KERNAL already out, banking DDR bits outputs.
; Safe RAM NMI vector/handler and CIA2 NMI ownership are caller prerequisites.
; Preserves inputs G..G+6 and caller P, including I/D; A/X/Y clobbered.
; G+7 accepted, G+8/9 data writes. Leaves screen/colour RAM/DDR untouched.
glyph_init_rom:
sta G+11
stx G+10
php
sei
cld
jsr glyph_reset_result
lda G+11
cmp #2
bcs rom_reject
lda $00
and #7
cmp #7
bne rom_reject
lda G+10
and #2
bne rom_reject
lda G+10
eor $01
and $00
and #$3f
bne rom_reject
lda G+10
and #$f8
!if ROM_WRONG {
ora #5 ; Deliberate error: leave I/O visible instead of character ROM.
} else {
ora #1
}
sta $01
lda #0
sta G+12
sta G+14
sta G+16
lda #$20
sta G+13
lda #8
sta G+17
lda G+11
asl
asl
asl
clc
adc #$d0
sta G+15
rom_copy:
ldy #0
lda (G+14),y
jsr glyph_store
inc G+14
bne rom_source_ok
inc G+15
rom_source_ok:
jsr glyph_decrement
bne rom_copy
lda G+10
sta $01
lda #1
sta G+7
rom_reject:
plp
rts
rom_init_end:Why the screen must survive
The old blank-font initializer fills names with code zero. Doing that with a copied ROM font would fill the screen with @ characters. This variant deliberately leaves every name alone. The caller fills its screen with screen-code 32 spaces, adds its caption and later selects the two original sketches. Screen codes and PETSCII still have different jobs.
Common error: copying I/O instead of ROM
The wrong build changes one immediate mask from 1 to 5, leaving CHAREN set. The bounded copy completes, but reads I/O-register values rather than the alphabet. Its status and 2,048-byte write count alone cannot prove success. Comparing the full private native font with the selected ROM half catches the error; the native image shows damaged letters. The repaired PRG is byte-for-byte identical to the working PRG.
Restoring an all-RAM CPU mapping also hides I/O. The boundary fixture checks exact restoration, then explicitly selects its I/O-visible map before reading the next joystick handshake. Restoring ownership is not the same as choosing a mapping suitable for the caller’s next operation.
Complete input-driven caller with private screen ownership
; Standalone stock-ROM PRG. Takes ownership; does not return to BASIC.
*=$0801
!byte $0b,$08,$0a,$00,$9e
!text "2064"
!byte 0,0,0
*=$0810
G=$70
CPU_MAP=$35
start:
sei
cld
lda #$7f
sta $dc0d
sta $dd0d
lda $dc0d
lda $dd0d
lda #0
sta $d01a
sta $d015
sta $d020
sta $d021
lda #<safe_interrupt
sta $fffa
sta $fffe
lda #>safe_interrupt
sta $fffb
sta $ffff
lda #CPU_MAP
sta $01
lda $dd02
ora #3
sta $dd02
lda $dd00
ora #3
sta $dd00
lda #$1b
sta $d011
lda #8
sta $d016
lda #$18
sta $d018
ldx #0
lda #32
clear_screen:
sta $0400,x
sta $0500,x
sta $0600,x
sta $0700,x
inx
bne clear_screen
lda #1
colours:
sta $d800,x
sta $d900,x
sta $da00,x
sta $db00,x
inx
bne colours
lda #$a5
font_guards:
sta $1ff0,x
sta $2800,x
inx
cpx #16
bne font_guards
ldx #0
banner_copy:
lda banner,x
sta $0400,x
inx
cpx #13
bne banner_copy
lda #<patterns
sta G
lda #>patterns
sta G+1
lda #1
sta G+2
sta G+3
sta G+4
lda #82
sta G+5
lda #0
sta G+6
lda #HALF
ldx #CPU_MAP
jsr glyph_init_rom
lda G+7
sta init_status
lda G+8
sta init_writes
lda G+9
sta init_writes+1
lda $01
sta restored_port
jsr joy_init
lda #$a5
sta ready
init_done:
jsr joy_poll
lda joy2
and #16
beq init_done
initial_release:
jsr joy_poll
lda joy2
and #16
bne initial_release
; The second caller and observer can stop here before any glyph is patched.
jsr glyph_install
jsr glyph_select
inc G+5
jsr glyph_select
lda #0
sta pressed_before
main_loop:
jsr joy_poll
lda joy2
and #16
beq no_fire
lda pressed_before
bne frame_done
lda #<patterns+8
sta G
lda #>patterns+8
sta G+1
jsr glyph_replace
inc changes
no_fire:
lda joy2
and #16
sta pressed_before
frame_done:
jmp main_loop
safe_interrupt:
rti
ready: !byte 0
init_status: !byte 0
init_writes: !word 0
restored_port: !byte 0
pressed_before: !byte 0
changes: !byte 0
banner: !byte 18,15,13,32,6,15,14,20,32,11,5,16,20
patterns: !byte 129,66,36,24,24,36,66,129
!byte 255,129,129,129,129,129,129,255
!source "glyphs.inc"
!source "rom-init.inc"
!source "../../input/joystick-reading/joystick.inc"
program_end:Customisation, cost and evidence
Choose ROM half 0 or 1; change the installed slot and eight source bytes to suit your picture. Reserve slots carefully: replacing a letter also changes every occurrence of that letter. Move the fixed RAM font allocation only by adapting the existing component and VIC configuration together. Proportional text, arbitrary positions, multicolour and a different VIC bank need different allocation/rendering choices.
The wrapper adds 102 code bytes, uses the existing 24-byte context and copies 2,048 bytes during setup. The complete stock-ROM PRG is 778 bytes including its two-byte load address. One later shared replacement still writes only eight bytes. This is startup work, not a claim that a whole font fits your VBlank budget. The wrapper needs eight stack bytes including its direct call and nested copy helper; allow more for the surrounding caller and any interrupts.
Five native runs on Emu198x 0.44.2 with Asm198x 0.0.72 verify stock-PRG identity before RUN, both ROM halves, untouched screen, complete private font comparison, shared install/replacement, held input, guards and native pixels. A separate six-case guest caller checks invalid half, banking DDR, shadow mismatch, KERNAL visibility, all-RAM restoration and the lowercase half, including preserved decimal/interrupt/carry flags. Nineteen corruption controls reject altered evidence, including a coupled wrong displayed glyph. Complete licensed ROM/font dumps remain private; the retained evidence records hashes and only the few glyphs visible in each capture. This does not establish arbitrary interrupt coexistence or physical-hardware timing.


Stock-ROM code and native evidence. Bit weights, eight-byte row storage and cell indexing connect this extension with Maths for Games.