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.

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
; 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+1Read 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
; 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,1Resources 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.