Skip to content

Draw Pixels, Spans and Boxes on BBC Micro

Callable set, clear, XOR, read, span and rectangle routines for BBC Micro, with explicit bounds and byte ownership.

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.

BBC Micro output showing filled strips, thin and outlined boxes, bit-phase tests and a full-display border
Original caller running in Emu198x 0.39.0. The complete bitmap and selected native frames are checked independently.

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

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

The 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.

Four rows of sixteen dots, with columns five through twelve highlighted across a byte boundary
[5,13) spans two bytes. Keep the neighbours on both edges, then repeat the masks over four rows.

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

6502.inc
; 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,1

Choose 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.

Draw bitmap text reuses these spans for glyphs at arbitrary pixel positions, with an explicit font, advance and scissor.