Skip to content

Draw Pixels, Spans and Boxes on Oric Atmos HIRES

Reusable 6502 bitmap primitives with explicit serial-attribute and inverse-bit protection.

Draw a life bar, outline a room or inspect one pixel. These six callable routines change the bitmap you own, with set, clear and XOR operations. The hardware layout stays visible: it is part of using the routine safely.

Native Oric Atmos HIRES output showing filled and outlined boxes and spans
Original caller captured unchanged in Emu198x 0.39.0; the full bitmap and selected whole native images are independently checked.

Include it and make a call

Reserve 48 zero-page bytes at BP; the caller uses $70. Define PIXEL_WIDTH=228 and PIXEL_HEIGHT=200 and include oric.inc once. Keep the complete caller’s bitmap_lo/hi tables too. Code, tables, stack, bitmap and BP must not alias. Scratch needs no initialisation; public inputs do.

After video setup, this is a normal direct call. Your game does not need the diagnostic mailbox that follows it.

Outline [13,91) across and [27,53) down

demo.asm, excerpt
; A normal game call: an outlined box using exclusive right/bottom ends.
    lda #<13
    sta BP+0
    lda #>13
    sta BP+1
    lda #<27
    sta BP+2
    lda #>27
    sta BP+3
    lda #<91
    sta BP+4
    lda #>91
    sta BP+5
    lda #<53
    sta BP+6
    lda #>53
    sta BP+7
    lda #0
    sta BP+8
    jsr bp_box
    lda #0
    sta COMMAND
    lda #239
    sta COMMAND+1

Read BP+9 for acceptance. Call again with another request; no drawing list or hidden game loop is retained.

The calling contract

Words are unsigned little-endian. Origin is top left, Y downwards. Call bp_pixel, bp_read, bp_hspan, bp_vspan, bp_fill or bp_box.

BP offset Supply or read
0,2 x0,y0 words
4,6 Exclusive x1,y1 words
8 0 set, 1 clear, 2 XOR; read requires 0
9 Output: accepted 1, rejected 0
10 Output: stored bit for read; otherwise 0
11 Ignored and preserved
12–39,42–47 Private scratch
40 Output word: destination-byte writes

Pixel/read ignore both ends; hspan ignores y1; vspan ignores x1. Fill and box use [x0,x1)×[y0,y1). A box [5,13)×[20,24) is eight pixels wide and four high. The entire effective rectangle must be nonempty, ordered and within 228×200. Bad requests reject before destination writes, with zero read/write outputs. Public inputs 0–8 and 11 survive. This is strict rejection, not clipping; intersect with the display first when partial shapes are wanted.

Spans mask their edges and write full interior bytes. Outlines visit corners once, including a 1×1 box, so XOR does not erase a shared corner. Reading does not change memory.

Six pixels per byte, with room for control codes

HIRES is 240×200, but the first two bytes of every forty-byte row carry paper-black 16 and ink-white 7. Keep those attributes out of your drawing allocation. The routine therefore exposes 228×200 logical pixels; logical x=0 starts at physical x=12.

address = $A002 + 40*y + (x div 6); mask = 32 >> (x mod 6). At logical (13,20), that is $A324, mask $10. Six data bits hold the picture, bit 7 inverts its ink/paper presentation, and the component requires bit 6 set throughout the selected rectangle.

That last rule is a safe allocation policy, not a complete description of the ULA decoder. Effective byte values $20–$3F can carry pixel patterns even with bit 6 clear. This routine rejects them too: clearing/XORing such a byte could create a serial attribute ($00–$1F). Initialise drawable bytes to $40, or $C0 for inverse cells, and leave the two attribute columns alone.

Before any write, preflight checks every byte in the request’s bounding rectangle, even the interior of an outline. A bad byte on a later row cannot leave a partial box behind. The extra reads buy that guarantee. Set/clear/XOR preserve bits 6 and 7. Clear means zero stored pixels: inside an inverse cell those bits appear as ink, so “clear” does not universally mean black. Read returns the stored bit.

The caller owns HIRES memory $A000–$BF3F, its serial-attribute policy and the 50 Hz HIRES mode code $1E at $BFDF. A mixed attribute/text renderer needs a different explicit ownership policy.

The maths under the call

Division chooses a byte; remainder chooses a bit. Six pixels share that byte, so a point must preserve the other five. OR sets the mask, AND with its inverse clears it, and XOR toggles it. Writing only the mask over the old byte would erase neighbours.

Place a point connects the coordinates to position; Draw with integer error explains how pixels and spans build larger shapes. The current clipped geometry adapters have their own qualified backends; adding these primitive entries does not silently qualify geometry or text on these machines.

Open the reusable 6502 backend

Include this backend once

oric.inc
; SPDX-License-Identifier: MIT
; Oric HIRES, six pixel bits. Content width 228 excludes two serial-attribute columns.
; bit 6 must already mark pixel data; bit 7 (inverse) survives every operation. BP reserves 48 zero-page bytes; decimal mode must be off.
; Public words: +0 x0, +2 y0, +4 x1, +6 y1 (little-endian, exclusive ends).
; +8 operation 0=set,1=clear,2=XOR. +9 accepted, +10 read value.
; +40 word counts destination-byte writes for the most recent call.
; Calls destroy A/X/Y/flags, +12..47; inputs +0..8 survive. Not re-entrant.
; bitmap_lo/hi[y] names the selected content viewport; horizontal byte step is 1.
; All entries reject the whole request before writing; empty/reversed is rejected.
bp_fill:
    lda #0
    beq bp_enter
bp_box:
    lda #1
    bne bp_enter
bp_pixel:
    lda #2
    bne bp_enter
bp_read:
    lda #3
    bne bp_enter
bp_hspan:
    lda #4
    bne bp_enter
bp_vspan:
    lda #5
bp_enter:
    sta BP+12
    lda #0
    sta BP+9
    sta BP+10
    sta BP+40
    sta BP+41
    ldx #7
bp_copy:
    lda BP,x
    sta BP+16,x
    dex
    bpl bp_copy
    lda BP+12
    cmp #2
    bcc bp_validate
    cmp #5
    beq bp_unit_x
    ; Pixel/read/hspan have height 1.
    lda BP+18
    clc
    adc #1
    sta BP+22
    lda BP+19
    adc #0
    sta BP+23
    lda BP+12
    cmp #4
    beq bp_validate
bp_unit_x:
    lda BP+16
    clc
    adc #1
    sta BP+20
    lda BP+17
    adc #0
    sta BP+21
bp_validate:
    lda BP+12
    cmp #3
    bne bp_operation_check
    lda BP+8
    beq bp_operation_check
    rts
bp_operation_check:
    lda BP+8
    cmp #3
    bcs bp_invalid
    ; x0 < x1 <= 320; y0 < y1 <= 256.
    lda BP+17
    cmp BP+21
    bcc bp_x_ordered
    bne bp_invalid
    lda BP+16
    cmp BP+20
    bcs bp_invalid
bp_x_ordered:
    lda BP+21
    cmp #>PIXEL_WIDTH
    bcc bp_y_check
    bne bp_invalid
    lda BP+20
    cmp #<PIXEL_WIDTH
    bcc bp_y_check
    bne bp_invalid
bp_y_check:
    lda BP+19
    cmp BP+23
    bcc bp_y_ordered
    bne bp_invalid
    lda BP+18
    cmp BP+22
    bcs bp_invalid
bp_y_ordered:
    lda BP+23
    cmp #>PIXEL_HEIGHT
    bcc bp_valid
    bne bp_invalid
    lda BP+22
    cmp #<PIXEL_HEIGHT
    bcc bp_valid
    beq bp_valid
bp_invalid:
    rts
bp_valid:
    lda #1
    sta BP+9
    ; x0/6 and x0 mod 6; bounds ensure x < 228 and one-byte division.
    lda BP+16
    ldx #0
bp_div_first:
    cmp #6
    bcc bp_first_remainder
    sec
    sbc #6
    inx
    bne bp_div_first
bp_first_remainder:
    stx BP+28
    tax
    lda bp_right_masks,x
    sta BP+24
    lda bp_bit_masks,x
    sta BP+26
    lda BP+20
    sec
    sbc #1
    ldx #0
bp_div_last:
    cmp #6
    bcc bp_last_remainder
    sec
    sbc #6
    inx
    bne bp_div_last
bp_last_remainder:
    stx BP+29
    tax
    lda bp_bit_masks,x
    sta BP+27
    inx
    cpx #6
    bne bp_last_phase
    ldx #0
bp_last_phase:
    lda bp_left_masks,x
    sta BP+25
    lda BP+18
    sta BP+32
    lda BP+19
    sta BP+33
    lda BP+22
    sec
    sbc #1
    sta BP+42
    lda BP+23
    sbc #0
    sta BP+43
    ; Validate every byte in the request's bounding rectangle before storing.
    ; A control/attribute byte inside the content window is not a drawable pixel.
    lda BP+18
    sta BP+44
bp_preflight_row:
    ldx BP+44
    lda bitmap_lo,x
    sta BP+46
    lda bitmap_hi,x
    sta BP+47
    ldy BP+28
bp_preflight_byte:
    lda (BP+46),y
    and #64
    bne bp_preflight_next
    lda #0
    sta BP+9
    rts
bp_preflight_next:
    cpy BP+29
    beq bp_preflight_end_row
    iny
    bne bp_preflight_byte
bp_preflight_end_row:
    inc BP+44
    lda BP+44
    cmp BP+22
    bne bp_preflight_row
bp_row:
    lda #0
    sta BP+39
    ldx BP+32
    lda bitmap_lo,x
    sta BP+34
    lda bitmap_hi,x
    sta BP+35
    ; Add byte column to the configured row start.
    lda BP+28
    ldx #0
    stx BP+31
    clc
    adc BP+34
    sta BP+34
    lda BP+31
    adc BP+35
    sta BP+35
    lda BP+28
    sta BP+30
    lda BP+24
    sta BP+36
    lda BP+25
    sta BP+37
    lda BP+12
    cmp #1
    bne bp_bytes
    ; Middle rows of an outline contain only its left and right pixels.
    lda BP+32
    cmp BP+18
    beq bp_bytes
    cmp BP+42
    bne bp_sides
    lda BP+33
    cmp BP+43
    beq bp_bytes
bp_sides:
    lda #1
    sta BP+39
    lda BP+26
    sta BP+36
    lda BP+27
    sta BP+37
bp_bytes:
    lda #63
    sta BP+38
    lda BP+30
    cmp BP+28
    bne bp_not_first
    lda BP+36
    sta BP+38
bp_not_first:
    lda BP+30
    cmp BP+29
    bne bp_not_last
    lda BP+38
    and BP+37
    sta BP+38
    ; Same-byte outline sides need OR, not intersection.
    lda BP+39
    beq bp_not_last
    lda BP+30
    cmp BP+28
    bne bp_not_last
    lda BP+36
    ora BP+37
    sta BP+38
bp_not_last:
    ; For outline middle rows, skip bytes between the sides.
    lda BP+39
    beq bp_apply
    lda BP+30
    cmp BP+28
    beq bp_apply
    cmp BP+29
    bne bp_advance
bp_apply:
    ldy #0
    lda BP+12
    cmp #3
    bne bp_write
    lda (BP+34),y
    and BP+38
    beq bp_return
    lda #1
    sta BP+10
bp_return:
    rts
bp_write:
    lda BP+8
    beq bp_set
    cmp #1
    beq bp_clear
    lda (BP+34),y
    eor BP+38
    jmp bp_store
bp_clear:
    lda BP+38
    eor #255
    and (BP+34),y
    jmp bp_store
bp_set:
    lda BP+38
    ora (BP+34),y
bp_store:
    sta (BP+34),y
    inc BP+40
    bne bp_advance
    inc BP+41
bp_advance:
    lda BP+30
    cmp BP+29
    beq bp_next_row
    inc BP+30
    clc
    lda BP+34
    adc #1
    sta BP+34
    bcc bp_bytes_jump
    inc BP+35
bp_bytes_jump:
    jmp bp_bytes
bp_next_row:
    inc BP+32
    bne bp_row_test
    inc BP+33
bp_row_test:
    lda BP+32
    cmp BP+22
    bne bp_more_rows
    lda BP+33
    cmp BP+23
    beq bp_return
bp_more_rows:
    jmp bp_row
bp_right_masks:
    !byte 63,31,15,7,3,1
bp_left_masks:
    !byte 63,32,48,56,60,62
bp_bit_masks:
    !byte 32,16,8,4,2,1

Resources and proof

The backend is 547 assembled bytes, plus 400 bytes of CPU row tables and 48 zero-page context bytes. Reset setup, bitmap and stack are additional. The reserved attribute columns are within the 8,000-byte HIRES bitmap. Full-byte spans save repeated per-pixel addressing. Preflight also costs reads across the whole bounding rectangle. These are storage/write counts, not cycles or a frame-budget promise.

Clobbers A/X/Y/P. Decimal mode must be off before a call; the complete caller uses CLD during setup. Reserve a conservative 64-byte stack budget beyond your own calls/interrupts. BP is non-re-entrant; the routine does not change interrupt enables, arrange OS ownership or synchronise display access.

143 calls completed with Asm198x 0.0.71 and native Emu198x 0.39.0. Each compares full bitmap, guards, public inputs, acceptance, read and write counts against an independent selected-pixel model. Inverse cells, control codes and noncanonical $20 rejection are included. Selected whole native frames match independently composed pixels. Two faulty builds completed accepted draws and failed both memory and picture checks. Real-hardware timing, stock firmware loading and live-rendering coexistence remain separate checks.