A life bar, a room outline or a tiny drawing tool needs a few reliable drawing calls. These routines let you change a bit, read it back, or paint a box without borrowing a game’s renderer.

Include it and make a first call
Define BP at 48 reserved zero-page bytes, PIXEL_WIDTH=320, PIXEL_HEIGHT=256, and include 6502.inc. Copy the complete caller’s bitmap_lo and bitmap_hi tables too. They describe each of the 256 scanlines, not character rows. Clear decimal mode before calls.
Initialise and clear the display once. This example then sets pixel (0,0); the context’s unused end fields need no initialisation for a point call. The complete reset caller also contains a mailbox used by the verifier. Your game calls the routine directly.
Set one pixel using the reusable component
; Example: set a single pixel. Public inputs use little-endian words.
lda #0
sta BP
sta BP+1
sta BP+2
sta BP+3
sta BP+8
jsr bp_pixelThe screen origin is the top left. X increases rightwards and Y downwards. This mode is 320×256; the last pixel is (319,255).
The calling contract
Reserve 48 context bytes at BP. The word fields are little-endian on this CPU.
| Offset | What to supply or read |
|---|---|
| 0, 2 | x0, y0: unsigned 16-bit coordinates |
| 4, 6 | x1, y1: unsigned 16-bit exclusive ends |
| 8 | Operation: 0 set, 1 clear, 2 XOR |
| 9 | Output: 1 accepted, 0 rejected |
| 10 | Output: the bit read, as 0 or 1 |
| 12–47 | Private scratch; word at 40 counts destination-byte writes |
bp_fill paints [x0,x1)×[y0,y1); bp_box outlines it. bp_hspan uses x0/x1/y0, bp_vspan uses x0/y0/y1, and bp_pixel/bp_read use x0/y0. Set operation to zero for reads. Public inputs survive. Unused end fields are ignored.
A box from (5,20) to (13,24) is eight pixels wide and four high. A 1×1 outline touches its pixel once, including with XOR. Empty, reversed, invalid-operation or out-of-bounds requests reject before any destination write. The routine does not silently clip.
From a coordinate to a byte
MODE 4 puts eight scanlines of one character next to each other. Moving one byte to the right on screen means adding 8, not 1. At (13,20), the byte is &5A8C and the mask is &04. The 512-byte row table buys a straightforward lookup; deriving rows or caching the last address saves table space with a different CPU cost. The caller owns the 6845, video ULA, palette and the &5800–&7FFF bitmap.
Integer division and remainder tell us two different things: 13 div 8 = 1 chooses a byte column, while 13 mod 8 = 5 chooses a bit within it. The leftmost pixel uses bit 7, so 128 >> 5 = 4. Set ORs that mask into the old byte; clear ANDs its inverse; XOR toggles it. Writing $04 straight over the byte would erase seven neighbours.
Place a point explores position and origin. Its world Y points upwards; these screen coordinates point downwards. For integer points in a bottom-left-origin world of this height, convert with screenY = 255 - worldY. The width calculation needs no sign reversal: 13-5=8, height 24-20=4, area 8*4=32 pixels.
The maintained implementation
Open the complete reusable 6502 include
Include this backend once in your program
; SPDX-License-Identifier: MIT
; MODE 4 bitmap drawing. 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] is the address of the leftmost byte; X_STEP=8 for MODE 4.
; 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+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
lda BP+16
and #7
tax
lda bp_right_masks,x
sta BP+24
lda bp_bit_masks,x
sta BP+26
lda BP+20
and #7
tax
lda bp_left_masks,x
sta BP+25
; last pixel phase = (x1-1)&7.
txa
sec
sbc #1
and #7
tax
lda bp_bit_masks,x
sta BP+27
; First and last byte column, allowing x >= 256.
lda BP+17
sta BP+31
lda BP+16
lsr BP+31
ror
lsr BP+31
ror
lsr BP+31
ror
sta BP+28
lda BP+20
sec
sbc #1
sta BP+30
lda BP+21
sbc #0
sta BP+31
lsr BP+31
ror BP+30
lsr BP+31
ror BP+30
lsr BP+31
ror BP+30
lda BP+30
sta BP+29
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
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 column*8 to row start.
lda BP+28
ldx #0
stx BP+31
asl
rol BP+31
asl
rol BP+31
asl
rol 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 #255
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 #8
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 255,127,63,31,15,7,3,1
bp_left_masks:
!byte 255,128,192,224,240,248,252,254
bp_bit_masks:
!byte 128,64,32,16,8,4,2,1Choose a path for the work
The 32×8 aligned fill in the native check makes 32 byte writes; shifting it one pixel makes 40. The eight-pixel-wide example above makes eight writes for its 32 pixels. Calling a plot routine for each pixel would repeat addressing and edge work. This backend works a byte at a time, preserving the first and last byte’s unrelated bits.
A full-display clear makes 10,240 writes. The kernel, including its masks, takes 526 assembled bytes, with a further 512-byte row table; BP needs 48 RAM bytes. Assets/display memory and the reset caller are additional. These are measured storage and write counts, not a frame-budget or cross-CPU speed ranking. The write counter itself also costs work.
An unchecked inner loop, cached row address or specialised full-byte store can be smaller/faster for a particular caller. This version still reads a byte before a full-byte set/clear; an outline skips stores to middle columns but still visits them. Clipped lines, circles and ellipses add a reusable geometry layer over this unchanged backend.
Own the resources
A/X/Y and flags are clobbered; keep decimal mode clear. The context is not re-entrant. Reserve at least eight stack bytes for the routines, plus the rest of your program and interrupts. Keep context, code and any address tables out of the bitmap; do not let another renderer or interrupt handler change them during a call. The routines do not change interrupt state. Colour, palette and video setup remain caller-owned.
What was verified
All 94 calls completed independently on this BBC Micro configuration using Asm198x 0.0.71 and Emu198x 0.39.0. Every call checks the complete bitmap, guards or attribute neighbours, status, read value, unchanged public inputs and byte-write count. Cases cover all bit phases, set/clear/XOR/read, spans, one-pixel and full-size boxes, empty/reversed extents and invalid bounds. Selected whole native frames match a separate composition. A completed wrong-mask version fails both memory and picture comparisons.
This is an original reset fixture. It establishes these calls in this mode; stock-OS loading, physical-machine timing, interrupt coexistence and tear-free animation need their own checks.
- Complete caller and execution evidence
- Reusable include
- Full contracts, measured space/work costs and primary references
- Compare the three bitmap layouts
- Draw a tile-map region
Draw bitmap text reuses these spans for glyphs at arbitrary pixel positions, with an explicit font, advance and scissor.