Skip to content

Draw Pixels, Spans and Boxes on Dragon 32

Callable set, clear, XOR, read, span and rectangle routines for Dragon 32, 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.

Dragon 32 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 private RAM bytes and include 6809.inc. The PMODE 4 bitmap is fixed at $0600 in this variant; establish the matching SAM/VDG configuration first. The caller shows the PIA direction/data setup as well as the SAM address bits. Supply a valid S stack.

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: pixel (0,0), native big-endian coordinates.
    clra
    clrb
    std BP
    std BP+2
    sta BP+8
    jsr bp_pixel

The screen origin is the top left. X increases rightwards and Y downwards. This mode is 256×192; the last pixel is (255,191).

The calling contract

Reserve 48 context bytes at BP. The word fields are big-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

PMODE 4 has 32 bytes per scanline. At (13,20), $0600 + 20*32 + 1 = $0881; mask $04 selects the pixel. Five shifts build y×32 without a row table. Unlike the Spectrum and BBC, the next row really is another 32 bytes. The caller owns $0600–$1DFF and the SAM/VDG/PIA configuration. Set/clear are bit values; the selected colour set determines their visible colours.

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 = 191 - 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 6809 include

Include this backend once in your program

6809.inc
; SPDX-License-Identifier: MIT
; Dragon PMODE 4 at $0600. BP reserves 48 bytes RAM, native big-endian words:
; +0 x0,+2 y0,+4 x1,+6 y1 (exclusive ends); +8 op 0=set/1=clear/2=XOR.
; +9 accepted,+10 read,+40 word write count. +12..47 scratch, inputs survive.
; D/X/Y/CC destroyed; stack <=6 bytes incl caller. Not re-entrant; no IRQ mask changes.
bp_fill
    clra
    bra bp_enter
bp_box
    lda #1
    bra bp_enter
bp_pixel
    lda #2
    bra bp_enter
bp_read
    lda #3
    bra bp_enter
bp_hspan
    lda #4
    bra bp_enter
bp_vspan
    lda #5
bp_enter
    sta BP+12
    clr BP+9
    clr BP+10
    clr BP+40
    clr BP+41
    ldx #BP
    ldy #BP+16
    ldb #8
bp_copy
    lda ,x+
    sta ,y+
    decb
    bne bp_copy
    lda BP+12
    cmpa #2
    blo bp_validate
    cmpa #5
    beq bp_unit_x
    ldd BP+18
    addd #1
    std BP+22
    lda BP+12
    cmpa #4
    beq bp_validate
bp_unit_x
    ldd BP+16
    addd #1
    std BP+20
bp_validate
    lda BP+8
    cmpa #3
    bhs bp_invalid
    ldd BP+20
    cmpd #256
    bhi bp_invalid
    ldd BP+16
    cmpd BP+20
    bhs bp_invalid
    ldd BP+22
    cmpd #192
    bhi bp_invalid
    ldd BP+18
    cmpd BP+22
    bhs bp_invalid
    bra bp_valid
bp_invalid
    rts
bp_valid
    lda #1
    sta BP+9
    ldb BP+17
    andb #7
    ldx #bp_right_masks
    lda b,x
    sta BP+24
    ldx #bp_bit_masks
    lda b,x
    sta BP+26
    ldb BP+21
    andb #7
    ldx #bp_left_masks
    lda b,x
    sta BP+25
    decb
    andb #7
    ldx #bp_bit_masks
    lda b,x
    sta BP+27
    ldb BP+17
    lsrb
    lsrb
    lsrb
    stb BP+28
    ldd BP+20
    subd #1
    lsrb
    lsrb
    lsrb
    stb BP+29
    ldb BP+19
    stb BP+32
    ldb BP+23
    decb
    stb BP+42
bp_row
    clr BP+39
    ; A 32-byte scanline: y*32 + x/8 + $0600. No row table.
    clra
    ldb BP+32
    lslb
    rola
    lslb
    rola
    lslb
    rola
    lslb
    rola
    lslb
    rola
    addd #$0600
    addb BP+28
    adca #0
    std BP+34
    lda BP+28
    sta BP+30
    lda BP+24
    sta BP+36
    lda BP+25
    sta BP+37
    lda BP+12
    cmpa #1
    bne bp_bytes
    lda BP+32
    cmpa BP+19
    beq bp_bytes
    cmpa BP+42
    beq bp_bytes
    lda #1
    sta BP+39
    lda BP+26
    sta BP+36
    lda BP+27
    sta BP+37
bp_bytes
    ldb #255
    lda BP+30
    cmpa BP+28
    bne bp_not_first
    ldb BP+36
bp_not_first
    lda BP+30
    cmpa BP+29
    bne bp_not_last
    andb BP+37
    tst BP+39
    beq bp_not_last
    cmpa BP+28
    bne bp_not_last
    ldb BP+36
    orb BP+37
bp_not_last
    tst BP+39
    beq bp_apply
    lda BP+30
    cmpa BP+28
    beq bp_apply
    cmpa BP+29
    bne bp_advance
bp_apply
    ldx BP+34
    lda BP+12
    cmpa #3
    bne bp_write
    andb ,x
    beq bp_return
    lda #1
    sta BP+10
bp_return
    rts
bp_write
    lda BP+8
    beq bp_set
    cmpa #1
    beq bp_clear
    eorb ,x
    bra bp_store
bp_clear
    comb
    andb ,x
    bra bp_store
bp_set
    orb ,x
bp_store
    stb ,x
    ldd BP+40
    addd #1
    std BP+40
bp_advance
    lda BP+30
    cmpa BP+29
    beq bp_next_row
    inc BP+30
    ldx BP+34
    leax 1,x
    stx BP+34
    jmp bp_bytes
bp_next_row
    inc BP+32
    lda BP+32
    cmpa BP+23
    beq bp_return
    jmp bp_row
bp_right_masks
    fcb 255,127,63,31,15,7,3,1
bp_left_masks
    fcb 255,128,192,224,240,248,252,254
bp_bit_masks
    fcb 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 6,144 writes. The kernel, including its masks, takes 470 assembled bytes; 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

D/X/Y and CC are clobbered; U and DP survive. 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 Dragon 32 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.