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:
.include "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 #<66
sta G+5
lda #>66
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–959. 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
This caller is NROM-256 with CHR-RAM, not a CHR-ROM cartridge. Its explicit iNES header declares zero CHR-ROM banks. The component owns background patterns at PPU $0000–$0FFF and names at $2000–$23BF. Attribute bytes at $23C0–$23FF, palettes, the other pattern bank and the other physical nametable remain separate.
Turn rendering off for the entire call. $2001 is write-only; the routine cannot read it to validate that policy. Own NMI/mapper state and the $2005/$2006 latch, use increment-one addressing and restore scroll/address/control before display resumes. A full upload does not fit by assumption into VBlank. This is a blanked-transfer component; a live queue would need its own budget and bank contract.
The pinned ca65 linker emits an extra empty CHARS segment. The builder verifies the header and exact allowed extents, rejects any nonempty undeclared data, then writes the declared 32,784-byte cartridge. Removing assets does not make a CHR-ROM cartridge writable.
Turn a sketch into stored pixels
Each tile has 16 bytes: eight low-plane rows followed by eight high-plane rows. For a dot in row r, combine bit b from offsets r and r+8 as low + 2*high. Bit 7 is the leftmost dot. Tile 255 begins at $0FF0 and ends at $0FFF.
Cell (2,2) has index 2*32+2 = 66. A tile chooses two-bit pixel values; attribute data chooses its background palette. Replacing tile data does not change those attributes. The caller makes all four background palettes equivalent so the checked attribute guards can vary without changing the example’s colours.
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
; NROM CHR-RAM background 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.
; RENDERING OFF is a caller precondition. PPUMASK is write-only; this routine does not pretend to read it. Own PPU address latch, increment=1 and restore scroll/address before display.
glyph_init:
jsr glyph_reset_result
lda #<0
sta G+12
lda #>0
sta G+13
lda #<4096
sta G+16
lda #>4096
sta G+17
jsr glyph_zero_range
lda #<8192
sta G+12
lda #>8192
sta G+13
lda #<960
sta G+16
lda #>960
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
asl G+12
rol G+13
lda G+13
clc
adc #>0
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
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 #>960
bcc cell_ok
bne glyph_reject
lda G+5
cmp #<960
bcs glyph_reject
cell_ok:
lda G+5
sta G+12
lda G+6
clc
adc #>8192
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:
bit $2002
lda G+13
sta $2006
lda G+12
sta $2006
rts
glyph_store:
sta $2007
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 16. Full init writes 5,056 bytes. The assembled component takes 258 code bytes and 24 context bytes; each source picture takes 16 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. PPU address writes also cost 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 logical PPU memory, 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. The diagnostic reads $3E00 through its physical $2E00 mirror to avoid palette reads bypassing the read buffer at $3F00.
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.