Skip to content

Change Shared Character Pictures on Oric Atmos

Install a picture, share it across cells, then change every instance or just one. Reusable Oric Atmos 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.

Oric Atmos 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 #33
    sta G+2
    lda #2
    sta G+3
    jsr glyph_install
    lda #33
    sta G+4
    lda #<76
    sta G+5
    lda #>76
    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

The stored drawable shapes are ASCII 32–127, with counts 1–96 staying within that range. The selectable screen codes are 32–127 and 160–255: the latter are inverse forms of the same standard shapes. Codes 0–31 and 128–159 are attributes, so this picture API rejects them. Logical cells are 0–1035.

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

Oric TEXT has 40×28 physical cells, each 6×8 dots. This allocation reserves the first three cells of every row for the caller’s paper, ink and mode attributes: 16 selects black paper, 7 white ink, and 26 selects 50 Hz TEXT. Pictures use the remaining 37×28 rectangle. The routine skips the control columns rather than treating them as spare picture cells.

The standard character set starts at $B400. Drawable ASCII 32 begins at $B500; ASCII 127 ends at $B7FF. The low six bits of each row are dots, bit 5 on the left. Codes below 32 control display state instead of choosing a picture. The component preserves their unused font bytes, the alternate-font neighbour, reserved screen attributes and guards.

Bit 7 in a screen code selects inverse video. It does not independently select the alternate font: standard/alternate selection comes from the serial style attribute. This variant uses the standard set, normal height and no flash. HIRES and alternate-font versions need their own allocations and checks.

The complete caller owns the VIA/IRQ policy. The routines do not change video mode, colours, interrupt state or font style. Another renderer writing serial attributes can change the meaning of everything to their right, so coordinate ownership across the whole row.

Count a shape and its place

Logical cell (2,2) has index 2*37+2 = 76, but its physical screen address is $BB80+2*40+3+2 = $BBD5. The logical row width and physical stride differ. Using 37 for both puts later rows in the wrong place.

A six-dot row 100001 is 33, not 129. The two upper bits do not become extra visible dots. The example starts at physical column 5, so local dot (3,5) appears at pixel (5*6+3, 2*8+5) = (33,21). Row-transition checks exercise logical cells 36 and 37.

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 33 and call glyph_replace. Both cells still use that slot, so both change. To change only the first, put code 34 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
; oric: 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 #<46336
    sta G+12
    lda #>46336
    sta G+13
    lda #<768
    sta G+16
    lda #>768
    sta G+17
    jsr glyph_zero_range
    lda #<$bb83
    sta G+12
    lda #>$bb83
    sta G+13
    lda #28
    sta G+18
init_row:
    lda #37
    sta G+19
init_cell:
    lda #32
    jsr glyph_store
    dec G+19
    bne init_cell
    clc
    lda G+12
    adc #3
    sta G+12
    bcc init_next
    inc G+13
init_next:
    dec G+18
    bne init_row
    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 #32
    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 #>46080
    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+4
    and #127
    cmp #32
    bcc glyph_reject
    lda G+6
    cmp #>1036
    bcc cell_ok
    bne glyph_reject
    lda G+5
    cmp #<1036
    bcs glyph_reject
cell_ok:
    lda G+5
    sta G+20
    lda G+6
    sta G+21
    lda #<$bb83
    sta G+12
    lda #>$bb83
    sta G+13
find_row:
    lda G+21
    bne subtract_row
    lda G+20
    cmp #37
    bcc found_row
subtract_row:
    sec
    lda G+20
    sbc #37
    sta G+20
    lda G+21
    sbc #0
    sta G+21
    clc
    lda G+12
    adc #40
    sta G+12
    bcc find_row
    inc G+13
    jmp find_row
found_row:
    clc
    lda G+12
    adc G+20
    sta G+12
    bcc name_address
    inc G+13
name_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:
    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,804 bytes. The assembled component uses 331 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. The Oric row lookup uses repeated subtraction; a row-address table can save arithmetic by spending table memory. Live upload scheduling and physical contention need their own measurements before claiming a frame budget.

What was verified

28 calls completed per build, using Asm198x 0.0.71. Qualification uses the released Oric Atmos Emu198x 0.41.0 binary. It checks the complete standard font, screen, reserved attributes and neighbours, including rejected control codes and the logical row transition. It does not turn the existing HIRES routines into evidence for this TEXT mode; this caller is separately executed.

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.