Skip to content

Change Shared Character Pictures on Atari 800XL

Install a picture, share it across cells, then change every instance or just one. Reusable Atari 800XL routines with explicit character, colour and memory rules.

A wall can appear in twenty places while its picture is stored once. Change that picture and every wall changes. Open one door instead, and you probably want to change just that cell’s number. These routines let you choose.

Atari 800XL native output showing two cells using the same replaced character picture
The first replacement changes both shared instances. Later calls change a single cell and exercise normal/inverse forms. Original Code198x pictures, independently captured on this target.

Include, initialise and call

Reserve 24 private zero-page bytes at G, clear decimal mode and include glyphs.inc. The complete reset caller sets up the video and interrupt policy before the component runs. It supplies its own firmware image; it does not use the manufacturer’s BASIC or operating system.

Include the maintained character component

demo.asm, excerpt
kernel_start:
    !source "glyphs.inc"

glyph_init clears the owned pictures and names. That destroys their old contents, so use it during setup. Then install two original sketches and select the first twice:

Initialise, install and select shared pictures

demo.asm, excerpt
    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_select

Your game calls these entries directly. The later mailbox and debugger readbacks are optional verification transport.

Supply a source, slot and cell

All words are little-endian. glyph_install uploads a range; glyph_replace uploads one shape and ignores count; glyph_select writes one cell’s screen code. Init clears the owned allocations. Inputs 0–6 survive, and status/write count reset even when a request rejects.

Offset Supply or read
0–1 Readable CPU source address
2, 3 First stored glyph code and nonzero count
4 Screen code to select
5–6 Logical cell index
7 Output: 1 accepted, 0 rejected
8–9 Output: destination data bytes written
10–23 Private scratch

Stored shapes are 0–127, counts 1–128 with first+count <= 128. Screen codes are 0–255: the low seven bits select a shape and bit 7 uses the caller’s selected inverse policy. These are screen codes, not ATASCII. Logical cells are 0–959.

Zero counts, overflowing ranges and invalid slots/cells reject before a destination write. Keep the source long enough, readable, non-wrapping, stable and separate from the context/destination. These are trusted pointers, not an arbitrary-memory validator.

The context is non-re-entrant. A/X/Y and flags are clobbered; clear decimal mode and reserve six stack bytes including the direct call. Add space for your own calls and interrupts. The routines do not change interrupt state or banks.

Respect the machine’s characters

The caller selects ANTIC mode 2, with 40×24 eight-dot characters. It reserves a 1 KiB aligned font at $4000–$43FF, names at $5000–$53BF, and a display list at $3000. CHBASE=$40 selects the font; CHACTL=$02 supplies this variant’s inverse policy. GTIA colours, DMACTL, display-list setup, NMI and CPU mapping remain caller-owned.

The display list supplies three blank mode lines, a load-memory-scan character instruction, 23 further character lines and a jump/wait-for-vertical-blank. The original OS-socket fixture runs at $E000. It is our own reset code, not an Atari OS ROM. An operating-system VBlank handler could restore its own video settings if mixed into this takeover example.

Hardware CHBASE is $D409. The OS shadow CHBAS at $02F4 has a different role: writing the shadow relies on the OS to apply it. The reusable component installs pictures and names; it does not silently change either register.

Each row is eight bits, bit 7 leftmost. Slot 127 starts at $4000+127*8 = $43F8. Screen code 255 selects its inverse under this CHACTL policy. Other ANTIC character modes can change size, colours and alignment; they need separately checked implementations.

Count a shape and its place

Cell (2,2) has index 2*40+2 = 82, at screen address $5000+82 = $5052. Local dot (3,5) reaches active pixel (19,21). The last cell is 23*40+39 = 959; cell 960 lies beyond this allocation.

The font’s 1 KiB alignment matters because the hardware interprets CHBASE as a font base, not a general byte pointer. Keep that constraint visible when laying out memory or adding font banks.

Move a glyph connects bit weights, stored shapes, codes and cell placement. Place a point explains local and screen coordinates. These routines place fixed-grid pictures; they do not implement proportional text, arbitrary placement or scaled letters.

Replace the shared picture, or one instance

After the example, point G+0 at a new eight-row sketch, set G+2 to 1 and call glyph_replace. Both cells still use that slot, so both change. To change only the first, put code 2 in G+4 and its logical cell index in G+5, then call glyph_select. The second keeps the old code.

Read the complete reusable 6502 component

Initialise, install, replace and select

glyphs.inc
; SPDX-License-Identifier: MIT
; atari-800xl: selected character-RAM allocation, G=24 private zero-page bytes.
; +0 CPU source word; +2 first slot byte; +3 count byte; +4 screen-code byte;
; +5 logical cell word; +7 output accepted; +8 output data-write word; +10..23 scratch.
; Inputs +0..6 survive. A/X/Y/flags clobbered, clear decimal; non-reentrant.
; Trusted readable source must not wrap/overlap; caller owns video/banks/IRQs.
; Allocations, bounds, character-code and colour rules are in the family README.
glyph_init:
    jsr glyph_reset_result
    lda #<16384
    sta G+12
    lda #>16384
    sta G+13
    lda #<1024
    sta G+16
    lda #>1024
    sta G+17
    jsr glyph_zero_range
    lda #<20480
    sta G+12
    lda #>20480
    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
    lda G+2
    cmp #0
    bcc glyph_reject
    cmp #128
    bcs glyph_reject
    lda G+10
    clc
    adc G+2
    bcs glyph_reject
    cmp #129
    bcs glyph_reject
    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 #>16384
    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 #>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 #>20480
    sta G+13
    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:
    rts

Pay for what changes

One cell selection writes one name byte. Replacing one picture writes eight bytes, regardless of how many cells use it. Full init writes 1,984 bytes. The assembled component uses 249 code bytes, 24 context bytes and eight source bytes per picture, excluding reset/video/diagnostic code.

Choose the operation by what should change. Eight writes can update many shared walls, but one write can open one door. These are storage and destination-write counts, not elapsed time. A font bank or cache can spend memory to avoid regenerating pictures. Live upload scheduling and physical contention need their own measurements before claiming a frame budget.

What was verified

21 calls completed per build, using Asm198x 0.0.71. Qualification uses the released Atari 800XL Emu198x 0.41.0 binary with the original OS-socket fixture and BASIC disabled. The whole font/name memory, display list and surrounding guards are checked. The capture describes the pinned default video/palette configuration, not every PAL/NTSC or ANTIC mode.

Three builds on each of the three targets produce 210 executed calls in total. Tests include shared replacement, one-cell selection, inverse forms, full legal uploads, final slots/cells, zero/overflow requests and reset. Completed wrong-slot and wrong-name controls fail both memory and visible-output checks. Selected whole native frames match an independent compositor; retained checks reject missing calls and rehashed wrong observations.

These original reset callers do not establish stock-OS loading, interrupt coexistence, physical timing or tear-free updates. Code and original pictures are MIT licensed.