Skip to content

Draw Lines, Circles and Ellipses on Dragon 32 PMODE 4

Reusable clipped integer geometry, span-filled curves and explicit endpoint ownership for Dragon 32 PMODE 4.

Draw a sight line, a circular scanner or a filled oval without building a renderer first. These are five callable assembly routines over the existing pixel/span interface. You choose the bitmap, own the memory and call only the pieces your game needs.

Native Dragon 32 PMODE 4 output with filled and outlined ellipses, a circle, and a clipped diagonal line
Original caller in Emu198x 0.39.0. This is the machine’s native output; the diagnostic scene contains no third-party art.

Include it and make a call

The caller places BP at $0380 and G at $0400, outside the $0600–$1DFF bitmap. Scanlines are linear: address=$0600+y*32+(x div 8). The complete caller configures the SAM and PIA for this exact PMODE 4 display. Do not reuse that setup blindly on a different Dragon/CoCo mode.

Define GEO_WIDTH and GEO_HEIGHT as 256 and 192. Set GEO_PEN_MAX to 255 (pen ignored). Include the geometry 6809.inc and the backend exactly once, as shown at the bottom of the complete caller. Reserve 160 bytes at G plus 48 at BP, separately from code, stack, tables and display. Scratch needs no initialisation; public inputs do.

After the caller’s video setup, this is an ordinary direct call. Your game does not need the verification mailbox that follows it.

Outline an ellipse centred at (80,70), radii (48,24)

demo.asm, excerpt
; A normal game call: an outlined ellipse. Setup and mailbox are separate.
    ldd #$0050
    std G+0
    ldd #$0046
    std G+2
    ldd #$0030
    std G+4
    ldd #$0018
    std G+6
    ldd #$0000
    std G+12
    ldd #$0000
    std G+14
    ldd #$0100
    std G+16
    ldd #$00c0
    std G+18
    lda #0
    sta G+8
    lda #0
    sta G+9
    lda #1
    sta G+11
    jsr geo_ellipse
    lda #0
    sta COMMAND
    lda #239
    sta COMMAND+1

Read G+10 after return: 1 means accepted, 0 means rejected. An accepted shape can be completely outside the scissor and draw nothing. Change the inputs and call again; G holds no hidden scene or list of shapes.

The calling contract

All words use big-endian byte order. Screen origin is top left; Y increases downwards. Pen is ignored by this monochrome backend; initialise it to 1 for a caller that also ports to CPC.

G offset Supply or read
0,2 Signed x0,y0 for a line; centre cx,cy for curves
4,6 Signed x1,y1 for a line; radii rx,ry for an ellipse; a circle uses rx and ignores ry
8 Operation: 0 set/replace, 1 clear, 2 XOR
9 Line: 0 includes both ends, 1 omits the original caller’s final end; curves require 0
10 Output: 1 accepted, 0 rejected
11 Pen index (ignored on mono backends)
12,14,16,18 Unsigned scissor left,top,right,bottom; right/bottom exclusive
20 Output: 32-bit destination-byte writes
24 Output: 16-bit backend calls
26–159 Private scratch

Call geo_line, geo_circle, geo_ellipse, geo_fill_circle or geo_fill_ellipse. Coordinates must be −512..511, radii 0..255. The scissor must be nonempty and inside the 256×192 display. Invalid bounds, radii, operation, flags or pen reject before any destination write, with zero counters. Inputs at 0–9 and 11–19 survive. Unused circle ry is ignored. A zero radius becomes a point or straight span rather than an error.

Keep the line you asked for

The line uses accumulated integer error in all eight octants. It orders endpoints consistently before choosing half-way pixels, so reversing a line produces the same pixels. A horizontal or vertical line uses a span fast path. A zero-length inclusive line draws one pixel.

For a closed XOR polyline, give every edge flag 1. Each omits its original final endpoint, so the next edge owns that shared vertex. Leaving both endpoints on both edges would toggle the vertex twice and erase it. A zero-length omit-final line draws nothing.

The scissor discards outside pixels from the original raster, preserving its rounding phase. It does not replace the endpoints with rounded intersections. Offscreen steps still cost work: the accepted coordinate range bounds the major-axis delta at 1023. Filled curves instead intersect each row span with the scissor before calling the backend.

Draw with integer error works through a short line, symmetry, span filling and overflow. Place a point explains the origin difference; for integer screen rows here, convert an upwards world Y with screenY=height-1-worldY.

Fill rows, not thousands of point calls

Outline ellipses update an integer decision and reflect points into four quadrants. Axis points are emitted once. Filled ellipses select the widest interval for each row and emit that span once; they do not repeatedly XOR the same row as the edge advances. Circles use equal ellipse radii through this same kernel. A specialised eight-way circle implementation could reduce work further.

An ellipse with radii (37,15) covers 1,833 logical pixels here but needs only 31 span calls when filled. Those calls made 258 byte writes on this backend. That is actual destination work, not elapsed cycles. Four-pixel CPC packing and eight-pixel mono packing produce different counts, and partial edge bytes still need protection.

The geometry occupies 4,573 assembled bytes, the backend 470, plus the 208 RAM bytes for G and BP. Reset code, stack, display and assets are additional. The checked 32-bit curve arithmetic buys a clear range contract but has a code/time cost. Cache static shapes or specialise a hot path when measurement shows a need. No frame-budget claim has been measured here.

A logical circle has equal X/Y radii. Its displayed shape still depends on the display mode, pixel aspect and capture presentation. Choose the presentation deliberately; changing radius to compensate is a separate mapping decision, not a change to the circle equation.

Own the resources

Clobbered: D/X/Y/CC; U and DP survive. Reserve a conservative 64-byte stack budget, plus your other calls and interrupts. The component is non-re-entrant. It does not change interrupt state or negotiate display access. Keep G/BP private and coordinate another renderer or interrupt user yourself. Reset fixtures own the machine; integration under a live OS needs its own loading and coexistence checks.

Open the complete reusable 6809 geometry include

Include this CPU component once

6809.inc
; SPDX-License-Identifier: MIT
; Copyright (c) 2020 zingl; assembly adaptation Code198x (2026).
; Integer geometry over the existing bp_pixel/bp_hspan/bp_vspan backend.
; G is 160 caller-owned RAM bytes. BP is a separate 48-byte workspace.
; Read README.md for signed inputs, scissor, endpoint flags and ownership.
; Bresenham/ellipse recurrences adapted from Alois Zingl; see LICENSE-Zingl.txt.
g_a equ G+0
g_b equ G+2
g_c equ G+4
g_d equ G+6
g_op equ G+8
g_flags equ G+9
g_status equ G+10
g_pen equ G+11
g_clip_l equ G+12
g_clip_t equ G+14
g_clip_r equ G+16
g_clip_b equ G+18
g_writes equ G+20
g_calls equ G+24
g_mode equ G+32
g_first equ G+33
g_swap equ G+34
g_fill equ G+35
g_x equ G+40
g_y equ G+42
g_end_x equ G+44
g_end_y equ G+46
g_dx equ G+48
g_dy equ G+50
g_sy equ G+54
g_err equ G+56
g_e2 equ G+58
g_temp equ G+60
g_last_y equ G+62
g_rx equ G+64
g_ry equ G+66
g_px equ G+68
g_py equ G+70
g_right equ G+72
g_bottom equ G+74
g_a2 equ G+80
g_b2 equ G+84
g_curve_dx equ G+88
g_curve_dy equ G+92
g_curve_err equ G+96
g_curve_e2 equ G+100
g_step_x equ G+104
g_step_y equ G+108
g_work equ G+112
g_mul equ G+158
g_cmp equ G+159
bp_writes equ BP+40
bp_status equ BP+9
bp_op equ BP+8
bp_pen equ BP+11
bp_x0 equ BP+0
bp_y0 equ BP+2
bp_x1 equ BP+4
bp_y1 equ BP+6
geo_line
    lda #$00
    sta g_mode
    jmp geo_enter
geo_circle
    lda #$01
    sta g_mode
    jmp geo_enter
geo_ellipse
    lda #$02
    sta g_mode
    jmp geo_enter
geo_fill_circle
    lda #$03
    sta g_mode
    jmp geo_enter
geo_fill_ellipse
    lda #$04
    sta g_mode
    jmp geo_enter
geo_enter
    lda #$00
    sta g_status
    lda #$00
    sta g_writes+3
    lda #$00
    sta g_writes+2
    lda #$00
    sta g_writes+1
    lda #$00
    sta g_writes
    ldd #$0000
    std g_calls
; Validate the entire request before the first backend write.
    lda #$02
    sta g_cmp
    lda g_op
    cmpa g_cmp
    lblo geo_branch_1
    lbne geo_done
geo_branch_1
    lda g_pen
    cmpa #GEO_PEN_MAX
    lblo geo_branch_2
    lbeq geo_branch_2
    jmp geo_done
geo_branch_2
    lda #$fe
    eora #$80
    sta g_cmp
    lda g_a
    eora #$80
    cmpa g_cmp
    lblo geo_done
    lbne geo_branch_3
    lda #$00
    sta g_cmp
    lda g_a+1
    cmpa g_cmp
    lblo geo_done
geo_branch_3
    lda #$01
    eora #$80
    sta g_cmp
    lda g_a
    eora #$80
    cmpa g_cmp
    lblo geo_branch_4
    lbne geo_done
    lda #$ff
    sta g_cmp
    lda g_a+1
    cmpa g_cmp
    lblo geo_branch_4
    lbne geo_done
geo_branch_4
    lda #$fe
    eora #$80
    sta g_cmp
    lda g_b
    eora #$80
    cmpa g_cmp
    lblo geo_done
    lbne geo_branch_5
    lda #$00
    sta g_cmp
    lda g_b+1
    cmpa g_cmp
    lblo geo_done
geo_branch_5
    lda #$01
    eora #$80
    sta g_cmp
    lda g_b
    eora #$80
    cmpa g_cmp
    lblo geo_branch_6
    lbne geo_done
    lda #$ff
    sta g_cmp
    lda g_b+1
    cmpa g_cmp
    lblo geo_branch_6
    lbne geo_done
geo_branch_6
    lda #$00
    eora #$80
    sta g_cmp
    lda g_clip_l
    eora #$80
    cmpa g_cmp
    lblo geo_done
    lbne geo_branch_7
    lda #$00
    sta g_cmp
    lda g_clip_l+1
    cmpa g_cmp
    lblo geo_done
geo_branch_7
    lda #$00
    eora #$80
    sta g_cmp
    lda g_clip_t
    eora #$80
    cmpa g_cmp
    lblo geo_done
    lbne geo_branch_8
    lda #$00
    sta g_cmp
    lda g_clip_t+1
    cmpa g_cmp
    lblo geo_done
geo_branch_8
    lda g_clip_r
    eora #$80
    sta g_cmp
    lda g_clip_l
    eora #$80
    cmpa g_cmp
    lblo geo_branch_9
    lbne geo_done
    lda g_clip_r+1
    sta g_cmp
    lda g_clip_l+1
    cmpa g_cmp
    lblo geo_branch_9
    jmp geo_done
geo_branch_9
    lda g_clip_b
    eora #$80
    sta g_cmp
    lda g_clip_t
    eora #$80
    cmpa g_cmp
    lblo geo_branch_10
    lbne geo_done
    lda g_clip_b+1
    sta g_cmp
    lda g_clip_t+1
    cmpa g_cmp
    lblo geo_branch_10
    jmp geo_done
geo_branch_10
    ldd #GEO_WIDTH
    std g_temp
    lda g_temp
    eora #$80
    sta g_cmp
    lda g_clip_r
    eora #$80
    cmpa g_cmp
    lblo geo_branch_11
    lbne geo_done
    lda g_temp+1
    sta g_cmp
    lda g_clip_r+1
    cmpa g_cmp
    lblo geo_branch_11
    lbne geo_done
geo_branch_11
    ldd #GEO_HEIGHT
    std g_temp
    lda g_temp
    eora #$80
    sta g_cmp
    lda g_clip_b
    eora #$80
    cmpa g_cmp
    lblo geo_branch_12
    lbne geo_done
    lda g_temp+1
    sta g_cmp
    lda g_clip_b+1
    cmpa g_cmp
    lblo geo_branch_12
    lbne geo_done
geo_branch_12
    lda #$00
    sta g_cmp
    lda g_mode
    cmpa g_cmp
    lbne geo_curve_validate
geo_branch_13
    lda #$01
    sta g_cmp
    lda g_flags
    cmpa g_cmp
    lblo geo_branch_14
    lbne geo_done
geo_branch_14
    lda #$fe
    eora #$80
    sta g_cmp
    lda g_c
    eora #$80
    cmpa g_cmp
    lblo geo_done
    lbne geo_branch_15
    lda #$00
    sta g_cmp
    lda g_c+1
    cmpa g_cmp
    lblo geo_done
geo_branch_15
    lda #$01
    eora #$80
    sta g_cmp
    lda g_c
    eora #$80
    cmpa g_cmp
    lblo geo_branch_16
    lbne geo_done
    lda #$ff
    sta g_cmp
    lda g_c+1
    cmpa g_cmp
    lblo geo_branch_16
    lbne geo_done
geo_branch_16
    lda #$fe
    eora #$80
    sta g_cmp
    lda g_d
    eora #$80
    cmpa g_cmp
    lblo geo_done
    lbne geo_branch_17
    lda #$00
    sta g_cmp
    lda g_d+1
    cmpa g_cmp
    lblo geo_done
geo_branch_17
    lda #$01
    eora #$80
    sta g_cmp
    lda g_d
    eora #$80
    cmpa g_cmp
    lblo geo_branch_18
    lbne geo_done
    lda #$ff
    sta g_cmp
    lda g_d+1
    cmpa g_cmp
    lblo geo_branch_18
    lbne geo_done
geo_branch_18
    lda #$01
    sta g_status
    jmp geo_line_start
geo_curve_validate
    lda #$00
    sta g_cmp
    lda g_flags
    cmpa g_cmp
    lbne geo_done
geo_branch_19
    lda #$00
    eora #$80
    sta g_cmp
    lda g_c
    eora #$80
    cmpa g_cmp
    lblo geo_done
    lbne geo_branch_20
    lda #$00
    sta g_cmp
    lda g_c+1
    cmpa g_cmp
    lblo geo_done
geo_branch_20
    lda #$00
    eora #$80
    sta g_cmp
    lda g_c
    eora #$80
    cmpa g_cmp
    lblo geo_branch_21
    lbne geo_done
    lda #$ff
    sta g_cmp
    lda g_c+1
    cmpa g_cmp
    lblo geo_branch_21
    lbne geo_done
geo_branch_21
    ldd g_c
    std g_rx
    lda #$01
    sta g_cmp
    lda g_mode
    cmpa g_cmp
    lbne geo_branch_22
    jmp geo_equal_axes
geo_branch_22
    lda #$03
    sta g_cmp
    lda g_mode
    cmpa g_cmp
    lbne geo_branch_23
    jmp geo_equal_axes
geo_branch_23
    lda #$00
    eora #$80
    sta g_cmp
    lda g_d
    eora #$80
    cmpa g_cmp
    lblo geo_done
    lbne geo_branch_24
    lda #$00
    sta g_cmp
    lda g_d+1
    cmpa g_cmp
    lblo geo_done
geo_branch_24
    lda #$00
    eora #$80
    sta g_cmp
    lda g_d
    eora #$80
    cmpa g_cmp
    lblo geo_branch_25
    lbne geo_done
    lda #$ff
    sta g_cmp
    lda g_d+1
    cmpa g_cmp
    lblo geo_branch_25
    lbne geo_done
geo_branch_25
    ldd g_d
    std g_ry
    jmp geo_axes_ready
geo_equal_axes
    ldd g_c
    std g_ry
geo_axes_ready
    lda #$01
    sta g_status
    lda #$00
    sta g_fill
    lda #$03
    sta g_cmp
    lda g_mode
    cmpa g_cmp
    lblo geo_curve_start
geo_branch_26
    lda #$01
    sta g_fill
    jmp geo_curve_start
geo_done
    rts
geo_line_start
; Canonical ordering gives the same tie pixels when endpoints are reversed.
    ldd g_a
    std g_x
    ldd g_b
    std g_y
    ldd g_c
    std g_end_x
    ldd g_d
    std g_end_y
    lda #$00
    sta g_swap
    lda g_end_x
    eora #$80
    sta g_cmp
    lda g_x
    eora #$80
    cmpa g_cmp
    lblo geo_branch_27
    lbne geo_swap_endpoints
    lda g_end_x+1
    sta g_cmp
    lda g_x+1
    cmpa g_cmp
    lblo geo_branch_27
    lbne geo_swap_endpoints
geo_branch_27
    lda g_end_x
    eora #$80
    sta g_cmp
    lda g_x
    eora #$80
    cmpa g_cmp
    lbne geo_line_ordered
    lda g_end_x+1
    sta g_cmp
    lda g_x+1
    cmpa g_cmp
    lbne geo_line_ordered
geo_branch_28
    lda g_end_y
    eora #$80
    sta g_cmp
    lda g_y
    eora #$80
    cmpa g_cmp
    lblo geo_line_ordered
    lbne geo_branch_29
    lda g_end_y+1
    sta g_cmp
    lda g_y+1
    cmpa g_cmp
    lblo geo_line_ordered
    lbeq geo_line_ordered
geo_branch_29
geo_swap_endpoints
    ldd g_x
    std g_temp
    ldd g_end_x
    std g_x
    ldd g_temp
    std g_end_x
    ldd g_y
    std g_temp
    ldd g_end_y
    std g_y
    ldd g_temp
    std g_end_y
    lda #$01
    sta g_swap
geo_line_ordered
    lda g_end_y
    eora #$80
    sta g_cmp
    lda g_y
    eora #$80
    cmpa g_cmp
    lbne geo_branch_30
    lda g_end_y+1
    sta g_cmp
    lda g_y+1
    cmpa g_cmp
    lbne geo_branch_30
    jmp geo_horizontal
geo_branch_30
    lda g_end_x
    eora #$80
    sta g_cmp
    lda g_x
    eora #$80
    cmpa g_cmp
    lbne geo_branch_31
    lda g_end_x+1
    sta g_cmp
    lda g_x+1
    cmpa g_cmp
    lbne geo_branch_31
    jmp geo_vertical_line
geo_branch_31
    ldd g_end_x
    std g_dx
    ldd g_dx
    subd g_x
    std g_dx
    ldd g_end_y
    std g_dy
    ldd g_dy
    subd g_y
    std g_dy
    ldd #$0001
    std g_sy
    lda #$00
    eora #$80
    sta g_cmp
    lda g_dy
    eora #$80
    cmpa g_cmp
    lblo geo_branch_33
    lbne geo_branch_32
    lda #$00
    sta g_cmp
    lda g_dy+1
    cmpa g_cmp
    lblo geo_branch_33
    jmp geo_branch_32
geo_branch_33
    ldd #$ffff
    std g_sy
    lda g_dy+1
    eora #$ff
    sta g_dy+1
    lda g_dy
    eora #$ff
    sta g_dy
    ldd g_dy
    addd #$0001
    std g_dy
geo_branch_32
    lda g_dy+1
    eora #$ff
    sta g_dy+1
    lda g_dy
    eora #$ff
    sta g_dy
    ldd g_dy
    addd #$0001
    std g_dy
    ldd g_dx
    std g_err
    ldd g_err
    addd g_dy
    std g_err
    lda #$01
    sta g_first
geo_line_loop
    lda #$00
    sta g_cmp
    lda g_flags
    cmpa g_cmp
    lbne geo_branch_36
    jmp geo_branch_34
geo_branch_36
    lda #$00
    sta g_cmp
    lda g_swap
    cmpa g_cmp
    lbne geo_branch_38
    jmp geo_branch_37
geo_branch_38
    lda #$00
    sta g_cmp
    lda g_first
    cmpa g_cmp
    lbne geo_branch_35
geo_branch_39
    jmp geo_branch_34
geo_branch_37
    lda g_end_x
    eora #$80
    sta g_cmp
    lda g_x
    eora #$80
    cmpa g_cmp
    lbne geo_branch_34
    lda g_end_x+1
    sta g_cmp
    lda g_x+1
    cmpa g_cmp
    lbne geo_branch_34
geo_branch_40
    lda g_end_y
    eora #$80
    sta g_cmp
    lda g_y
    eora #$80
    cmpa g_cmp
    lbne geo_branch_41
    lda g_end_y+1
    sta g_cmp
    lda g_y+1
    cmpa g_cmp
    lbne geo_branch_41
    jmp geo_done
geo_branch_41
geo_branch_34
    ldd g_x
    std g_px
    ldd g_y
    std g_py
    jsr geo_point
geo_branch_35
    lda #$00
    sta g_first
    lda g_end_x
    eora #$80
    sta g_cmp
    lda g_x
    eora #$80
    cmpa g_cmp
    lbne geo_branch_42
    lda g_end_x+1
    sta g_cmp
    lda g_x+1
    cmpa g_cmp
    lbne geo_branch_42
geo_branch_43
    lda g_end_y
    eora #$80
    sta g_cmp
    lda g_y
    eora #$80
    cmpa g_cmp
    lbne geo_branch_44
    lda g_end_y+1
    sta g_cmp
    lda g_y+1
    cmpa g_cmp
    lbne geo_branch_44
    jmp geo_done
geo_branch_44
geo_branch_42
    ldd g_err
    std g_e2
    lsl g_e2+1
    rol g_e2
    lda g_dy
    eora #$80
    sta g_cmp
    lda g_e2
    eora #$80
    cmpa g_cmp
    lblo geo_branch_45
    lbne geo_branch_46
    lda g_dy+1
    sta g_cmp
    lda g_e2+1
    cmpa g_cmp
    lblo geo_branch_45
geo_branch_46
    ldd g_err
    addd g_dy
    std g_err
    ldd g_x
    addd #$0001
    std g_x
geo_branch_45
    lda g_dx
    eora #$80
    sta g_cmp
    lda g_e2
    eora #$80
    cmpa g_cmp
    lblo geo_branch_48
    lbne geo_branch_47
    lda g_dx+1
    sta g_cmp
    lda g_e2+1
    cmpa g_cmp
    lblo geo_branch_48
    lbne geo_branch_47
geo_branch_48
    ldd g_err
    addd g_dx
    std g_err
    ldd g_y
    addd g_sy
    std g_y
geo_branch_47
    jmp geo_line_loop
geo_horizontal
    lda #$00
    sta g_cmp
    lda g_flags
    cmpa g_cmp
    lbne geo_branch_50
    jmp geo_branch_49
geo_branch_50
    lda #$00
    sta g_cmp
    lda g_swap
    cmpa g_cmp
    lbne geo_branch_52
    jmp geo_branch_51
geo_branch_52
    ldd g_x
    addd #$0001
    std g_x
    jmp geo_branch_49
geo_branch_51
    ldd g_end_x
    addd #$ffff
    std g_end_x
geo_branch_49
    lda g_end_x
    eora #$80
    sta g_cmp
    lda g_x
    eora #$80
    cmpa g_cmp
    lblo geo_branch_53
    lbne geo_done
    lda g_end_x+1
    sta g_cmp
    lda g_x+1
    cmpa g_cmp
    lblo geo_branch_53
    lbne geo_done
geo_branch_53
    ldd g_x
    std g_px
    ldd g_end_x
    std g_right
    ldd g_y
    std g_py
    jsr geo_span
    jmp geo_done
geo_vertical_line
    lda #$00
    sta g_cmp
    lda g_flags
    cmpa g_cmp
    lbne geo_branch_55
    jmp geo_branch_54
geo_branch_55
    lda #$00
    sta g_cmp
    lda g_swap
    cmpa g_cmp
    lbne geo_branch_57
    jmp geo_branch_56
geo_branch_57
    ldd g_y
    addd #$0001
    std g_y
    jmp geo_branch_54
geo_branch_56
    ldd g_end_y
    addd #$ffff
    std g_end_y
geo_branch_54
    lda g_end_y
    eora #$80
    sta g_cmp
    lda g_y
    eora #$80
    cmpa g_cmp
    lblo geo_branch_58
    lbne geo_done
    lda g_end_y+1
    sta g_cmp
    lda g_y+1
    cmpa g_cmp
    lblo geo_branch_58
    lbne geo_done
geo_branch_58
    ldd g_x
    std g_px
    ldd g_y
    std g_py
    ldd g_end_y
    std g_bottom
    jsr geo_vspan
    jmp geo_done
geo_curve_start
; Collapsed axes are an inclusive point or straight span, never a division by zero.
    lda #$00
    eora #$80
    sta g_cmp
    lda g_rx
    eora #$80
    cmpa g_cmp
    lbne geo_branch_59
    lda #$00
    sta g_cmp
    lda g_rx+1
    cmpa g_cmp
    lbne geo_branch_59
    jmp geo_curve_vertical
geo_branch_59
    lda #$00
    eora #$80
    sta g_cmp
    lda g_ry
    eora #$80
    cmpa g_cmp
    lbne geo_branch_60
    lda #$00
    sta g_cmp
    lda g_ry+1
    cmpa g_cmp
    lbne geo_branch_60
    jmp geo_curve_horizontal
geo_branch_60
    ldd g_rx
    std g_x
    lda g_x+1
    eora #$ff
    sta g_x+1
    lda g_x
    eora #$ff
    sta g_x
    ldd g_x
    addd #$0001
    std g_x
    ldd #$0000
    std g_y
    ldd #$ffff
    std g_last_y
; Squared radii and signed error increments use 32-bit storage. Radii <=255.
    lda #$00
    sta g_a2+3
    lda #$00
    sta g_a2+2
    lda #$00
    sta g_a2+1
    lda #$00
    sta g_a2
    lda #$00
    sta g_work+3
    lda #$00
    sta g_work+2
    lda #$00
    sta g_work+1
    lda #$00
    sta g_work
    lda g_rx+1
    sta g_work+3
    lda g_rx
    sta g_work+2
    lda g_rx+1
    sta g_mul
geo_branch_61
    lda #$00
    sta g_cmp
    lda g_mul
    cmpa g_cmp
    lbne geo_branch_63
    jmp geo_branch_62
geo_branch_63
    lda g_mul
    anda #1
    lbeq geo_branch_64
    lda g_a2+3
    adda g_work+3
    sta g_a2+3
    lda g_a2+2
    adca g_work+2
    sta g_a2+2
    lda g_a2+1
    adca g_work+1
    sta g_a2+1
    lda g_a2
    adca g_work
    sta g_a2
geo_branch_64
    lsr g_mul
    lsl g_work+3
    rol g_work+2
    rol g_work+1
    rol g_work
    jmp geo_branch_61
geo_branch_62
    lda #$00
    sta g_b2+3
    lda #$00
    sta g_b2+2
    lda #$00
    sta g_b2+1
    lda #$00
    sta g_b2
    lda #$00
    sta g_work+3
    lda #$00
    sta g_work+2
    lda #$00
    sta g_work+1
    lda #$00
    sta g_work
    lda g_ry+1
    sta g_work+3
    lda g_ry
    sta g_work+2
    lda g_ry+1
    sta g_mul
geo_branch_65
    lda #$00
    sta g_cmp
    lda g_mul
    cmpa g_cmp
    lbne geo_branch_67
    jmp geo_branch_66
geo_branch_67
    lda g_mul
    anda #1
    lbeq geo_branch_68
    lda g_b2+3
    adda g_work+3
    sta g_b2+3
    lda g_b2+2
    adca g_work+2
    sta g_b2+2
    lda g_b2+1
    adca g_work+1
    sta g_b2+1
    lda g_b2
    adca g_work
    sta g_b2
geo_branch_68
    lsr g_mul
    lsl g_work+3
    rol g_work+2
    rol g_work+1
    rol g_work
    jmp geo_branch_65
geo_branch_66
    lda g_b2+3
    sta g_step_x+3
    lda g_b2+2
    sta g_step_x+2
    lda g_b2+1
    sta g_step_x+1
    lda g_b2
    sta g_step_x
    lsl g_step_x+3
    rol g_step_x+2
    rol g_step_x+1
    rol g_step_x
    lda g_a2+3
    sta g_step_y+3
    lda g_a2+2
    sta g_step_y+2
    lda g_a2+1
    sta g_step_y+1
    lda g_a2
    sta g_step_y
    lsl g_step_y+3
    rol g_step_y+2
    rol g_step_y+1
    rol g_step_y
    lda g_b2+3
    sta g_curve_dx+3
    lda g_b2+2
    sta g_curve_dx+2
    lda g_b2+1
    sta g_curve_dx+1
    lda g_b2
    sta g_curve_dx
    ldd g_rx
    std g_temp
geo_branch_69
    lda g_curve_dx+3
    suba g_step_x+3
    sta g_curve_dx+3
    lda g_curve_dx+2
    sbca g_step_x+2
    sta g_curve_dx+2
    lda g_curve_dx+1
    sbca g_step_x+1
    sta g_curve_dx+1
    lda g_curve_dx
    sbca g_step_x
    sta g_curve_dx
    ldd g_temp
    addd #$ffff
    std g_temp
    lda #$00
    eora #$80
    sta g_cmp
    lda g_temp
    eora #$80
    cmpa g_cmp
    lbne geo_branch_69
    lda #$00
    sta g_cmp
    lda g_temp+1
    cmpa g_cmp
    lbne geo_branch_69
geo_branch_70
    lda g_a2+3
    sta g_curve_dy+3
    lda g_a2+2
    sta g_curve_dy+2
    lda g_a2+1
    sta g_curve_dy+1
    lda g_a2
    sta g_curve_dy
    lda g_curve_dx+3
    sta g_curve_err+3
    lda g_curve_dx+2
    sta g_curve_err+2
    lda g_curve_dx+1
    sta g_curve_err+1
    lda g_curve_dx
    sta g_curve_err
    lda g_curve_err+3
    adda g_curve_dy+3
    sta g_curve_err+3
    lda g_curve_err+2
    adca g_curve_dy+2
    sta g_curve_err+2
    lda g_curve_err+1
    adca g_curve_dy+1
    sta g_curve_err+1
    lda g_curve_err
    adca g_curve_dy
    sta g_curve_err
geo_curve_loop
    jsr geo_emit_curve
    lda g_curve_err+3
    sta g_curve_e2+3
    lda g_curve_err+2
    sta g_curve_e2+2
    lda g_curve_err+1
    sta g_curve_e2+1
    lda g_curve_err
    sta g_curve_e2
    lsl g_curve_e2+3
    rol g_curve_e2+2
    rol g_curve_e2+1
    rol g_curve_e2
    lda g_curve_dx
    eora #$80
    sta g_cmp
    lda g_curve_e2
    eora #$80
    cmpa g_cmp
    lblo geo_branch_71
    lbne geo_branch_72
    lda g_curve_dx+1
    sta g_cmp
    lda g_curve_e2+1
    cmpa g_cmp
    lblo geo_branch_71
    lbne geo_branch_72
    lda g_curve_dx+2
    sta g_cmp
    lda g_curve_e2+2
    cmpa g_cmp
    lblo geo_branch_71
    lbne geo_branch_72
    lda g_curve_dx+3
    sta g_cmp
    lda g_curve_e2+3
    cmpa g_cmp
    lblo geo_branch_71
geo_branch_72
    ldd g_x
    addd #$0001
    std g_x
    lda g_curve_dx+3
    adda g_step_x+3
    sta g_curve_dx+3
    lda g_curve_dx+2
    adca g_step_x+2
    sta g_curve_dx+2
    lda g_curve_dx+1
    adca g_step_x+1
    sta g_curve_dx+1
    lda g_curve_dx
    adca g_step_x
    sta g_curve_dx
    lda g_curve_err+3
    adda g_curve_dx+3
    sta g_curve_err+3
    lda g_curve_err+2
    adca g_curve_dx+2
    sta g_curve_err+2
    lda g_curve_err+1
    adca g_curve_dx+1
    sta g_curve_err+1
    lda g_curve_err
    adca g_curve_dx
    sta g_curve_err
geo_branch_71
    lda g_curve_dy
    eora #$80
    sta g_cmp
    lda g_curve_e2
    eora #$80
    cmpa g_cmp
    lblo geo_branch_74
    lbne geo_branch_73
    lda g_curve_dy+1
    sta g_cmp
    lda g_curve_e2+1
    cmpa g_cmp
    lblo geo_branch_74
    lbne geo_branch_73
    lda g_curve_dy+2
    sta g_cmp
    lda g_curve_e2+2
    cmpa g_cmp
    lblo geo_branch_74
    lbne geo_branch_73
    lda g_curve_dy+3
    sta g_cmp
    lda g_curve_e2+3
    cmpa g_cmp
    lblo geo_branch_74
    lbne geo_branch_73
geo_branch_74
    ldd g_y
    addd #$0001
    std g_y
    lda g_curve_dy+3
    adda g_step_y+3
    sta g_curve_dy+3
    lda g_curve_dy+2
    adca g_step_y+2
    sta g_curve_dy+2
    lda g_curve_dy+1
    adca g_step_y+1
    sta g_curve_dy+1
    lda g_curve_dy
    adca g_step_y
    sta g_curve_dy
    lda g_curve_err+3
    adda g_curve_dy+3
    sta g_curve_err+3
    lda g_curve_err+2
    adca g_curve_dy+2
    sta g_curve_err+2
    lda g_curve_err+1
    adca g_curve_dy+1
    sta g_curve_err+1
    lda g_curve_err
    adca g_curve_dy
    sta g_curve_err
geo_branch_73
    lda #$00
    eora #$80
    sta g_cmp
    lda g_x
    eora #$80
    cmpa g_cmp
    lblo geo_curve_loop
    lbne geo_branch_75
    lda #$00
    sta g_cmp
    lda g_x+1
    cmpa g_cmp
    lblo geo_curve_loop
    lbeq geo_curve_loop
geo_branch_75
    ldd #$0000
    std g_x
geo_curve_tip
    lda g_ry
    eora #$80
    sta g_cmp
    lda g_y
    eora #$80
    cmpa g_cmp
    lblo geo_branch_76
    lbne geo_done
    lda g_ry+1
    sta g_cmp
    lda g_y+1
    cmpa g_cmp
    lblo geo_branch_76
    jmp geo_done
geo_branch_76
    ldd g_y
    addd #$0001
    std g_y
    jsr geo_emit_curve
    jmp geo_curve_tip
geo_curve_vertical
    ldd g_a
    std g_px
    ldd g_b
    std g_py
    ldd g_py
    subd g_ry
    std g_py
    ldd g_b
    std g_bottom
    ldd g_bottom
    addd g_ry
    std g_bottom
    jsr geo_vspan
    jmp geo_done
geo_curve_horizontal
    ldd g_b
    std g_py
    ldd g_a
    std g_px
    ldd g_px
    subd g_rx
    std g_px
    ldd g_a
    std g_right
    ldd g_right
    addd g_rx
    std g_right
    jsr geo_span
    jmp geo_done
geo_emit_curve
    lda #$00
    sta g_cmp
    lda g_fill
    cmpa g_cmp
    lbne geo_emit_fill
geo_branch_77
; Axis tests remove duplicate symmetric pixels, including XOR at the centre.
    ldd g_a
    std g_px
    ldd g_px
    subd g_x
    std g_px
    ldd g_b
    std g_py
    ldd g_py
    addd g_y
    std g_py
    jsr geo_point
    lda #$00
    eora #$80
    sta g_cmp
    lda g_x
    eora #$80
    cmpa g_cmp
    lbne geo_branch_79
    lda #$00
    sta g_cmp
    lda g_x+1
    cmpa g_cmp
    lbne geo_branch_79
    jmp geo_branch_78
geo_branch_79
    ldd g_a
    std g_px
    ldd g_px
    addd g_x
    std g_px
    ldd g_b
    std g_py
    ldd g_py
    addd g_y
    std g_py
    jsr geo_point
geo_branch_78
    lda #$00
    eora #$80
    sta g_cmp
    lda g_y
    eora #$80
    cmpa g_cmp
    lbne geo_branch_80
    lda #$00
    sta g_cmp
    lda g_y+1
    cmpa g_cmp
    lbne geo_branch_80
    jmp geo_emit_return
geo_branch_80
    ldd g_a
    std g_px
    ldd g_px
    subd g_x
    std g_px
    ldd g_b
    std g_py
    ldd g_py
    subd g_y
    std g_py
    jsr geo_point
    lda #$00
    eora #$80
    sta g_cmp
    lda g_x
    eora #$80
    cmpa g_cmp
    lbne geo_branch_81
    lda #$00
    sta g_cmp
    lda g_x+1
    cmpa g_cmp
    lbne geo_branch_81
    jmp geo_emit_return
geo_branch_81
    ldd g_a
    std g_px
    ldd g_px
    addd g_x
    std g_px
    ldd g_b
    std g_py
    ldd g_py
    subd g_y
    std g_py
    jsr geo_point
geo_emit_return
    rts
geo_emit_fill
; Only the first, widest extent for a row emits spans. Each row is written once.
    lda g_last_y
    eora #$80
    sta g_cmp
    lda g_y
    eora #$80
    cmpa g_cmp
    lbne geo_branch_82
    lda g_last_y+1
    sta g_cmp
    lda g_y+1
    cmpa g_cmp
    lbne geo_branch_82
    jmp geo_emit_return
geo_branch_82
    ldd g_y
    std g_last_y
    ldd g_a
    std g_px
    ldd g_px
    addd g_x
    std g_px
    ldd g_a
    std g_right
    ldd g_right
    subd g_x
    std g_right
    ldd g_b
    std g_py
    ldd g_py
    addd g_y
    std g_py
    jsr geo_span
    lda #$00
    eora #$80
    sta g_cmp
    lda g_y
    eora #$80
    cmpa g_cmp
    lbne geo_branch_83
    lda #$00
    sta g_cmp
    lda g_y+1
    cmpa g_cmp
    lbne geo_branch_83
    jmp geo_emit_return
geo_branch_83
    ldd g_a
    std g_px
    ldd g_px
    addd g_x
    std g_px
    ldd g_a
    std g_right
    ldd g_right
    subd g_x
    std g_right
    ldd g_b
    std g_py
    ldd g_py
    subd g_y
    std g_py
    jsr geo_span
    rts
geo_point
; Scissor the original raster, preserving its decision phase outside the window.
    lda g_clip_l
    eora #$80
    sta g_cmp
    lda g_px
    eora #$80
    cmpa g_cmp
    lblo geo_emit_return
    lbne geo_branch_84
    lda g_clip_l+1
    sta g_cmp
    lda g_px+1
    cmpa g_cmp
    lblo geo_emit_return
geo_branch_84
    lda g_clip_r
    eora #$80
    sta g_cmp
    lda g_px
    eora #$80
    cmpa g_cmp
    lblo geo_branch_85
    lbne geo_emit_return
    lda g_clip_r+1
    sta g_cmp
    lda g_px+1
    cmpa g_cmp
    lblo geo_branch_85
    jmp geo_emit_return
geo_branch_85
    lda g_clip_t
    eora #$80
    sta g_cmp
    lda g_py
    eora #$80
    cmpa g_cmp
    lblo geo_emit_return
    lbne geo_branch_86
    lda g_clip_t+1
    sta g_cmp
    lda g_py+1
    cmpa g_cmp
    lblo geo_emit_return
geo_branch_86
    lda g_clip_b
    eora #$80
    sta g_cmp
    lda g_py
    eora #$80
    cmpa g_cmp
    lblo geo_branch_87
    lbne geo_emit_return
    lda g_clip_b+1
    sta g_cmp
    lda g_py+1
    cmpa g_cmp
    lblo geo_branch_87
    jmp geo_emit_return
geo_branch_87
    ldd g_px
    std bp_x0
    ldd g_py
    std bp_y0
    jsr geo_backend_inputs
    jsr bp_pixel
    jmp geo_account
geo_span
    lda g_clip_t
    eora #$80
    sta g_cmp
    lda g_py
    eora #$80
    cmpa g_cmp
    lblo geo_emit_return
    lbne geo_branch_88
    lda g_clip_t+1
    sta g_cmp
    lda g_py+1
    cmpa g_cmp
    lblo geo_emit_return
geo_branch_88
    lda g_clip_b
    eora #$80
    sta g_cmp
    lda g_py
    eora #$80
    cmpa g_cmp
    lblo geo_branch_89
    lbne geo_emit_return
    lda g_clip_b+1
    sta g_cmp
    lda g_py+1
    cmpa g_cmp
    lblo geo_branch_89
    jmp geo_emit_return
geo_branch_89
    lda g_clip_l
    eora #$80
    sta g_cmp
    lda g_right
    eora #$80
    cmpa g_cmp
    lblo geo_emit_return
    lbne geo_branch_90
    lda g_clip_l+1
    sta g_cmp
    lda g_right+1
    cmpa g_cmp
    lblo geo_emit_return
geo_branch_90
    lda g_clip_r
    eora #$80
    sta g_cmp
    lda g_px
    eora #$80
    cmpa g_cmp
    lblo geo_branch_91
    lbne geo_emit_return
    lda g_clip_r+1
    sta g_cmp
    lda g_px+1
    cmpa g_cmp
    lblo geo_branch_91
    jmp geo_emit_return
geo_branch_91
    lda g_clip_l
    eora #$80
    sta g_cmp
    lda g_px
    eora #$80
    cmpa g_cmp
    lblo geo_branch_93
    lbne geo_branch_92
    lda g_clip_l+1
    sta g_cmp
    lda g_px+1
    cmpa g_cmp
    lblo geo_branch_93
    jmp geo_branch_92
geo_branch_93
    ldd g_clip_l
    std g_px
geo_branch_92
    lda g_clip_r
    eora #$80
    sta g_cmp
    lda g_right
    eora #$80
    cmpa g_cmp
    lblo geo_branch_94
    lbne geo_branch_95
    lda g_clip_r+1
    sta g_cmp
    lda g_right+1
    cmpa g_cmp
    lblo geo_branch_94
geo_branch_95
    ldd g_clip_r
    std g_right
    ldd g_right
    addd #$ffff
    std g_right
geo_branch_94
    ldd g_px
    std bp_x0
    ldd g_py
    std bp_y0
    ldd g_right
    std bp_x1
    ldd bp_x1
    addd #$0001
    std bp_x1
    jsr geo_backend_inputs
    jsr bp_hspan
    jmp geo_account
geo_vspan
    lda g_clip_l
    eora #$80
    sta g_cmp
    lda g_px
    eora #$80
    cmpa g_cmp
    lblo geo_emit_return
    lbne geo_branch_96
    lda g_clip_l+1
    sta g_cmp
    lda g_px+1
    cmpa g_cmp
    lblo geo_emit_return
geo_branch_96
    lda g_clip_r
    eora #$80
    sta g_cmp
    lda g_px
    eora #$80
    cmpa g_cmp
    lblo geo_branch_97
    lbne geo_emit_return
    lda g_clip_r+1
    sta g_cmp
    lda g_px+1
    cmpa g_cmp
    lblo geo_branch_97
    jmp geo_emit_return
geo_branch_97
    lda g_clip_t
    eora #$80
    sta g_cmp
    lda g_bottom
    eora #$80
    cmpa g_cmp
    lblo geo_emit_return
    lbne geo_branch_98
    lda g_clip_t+1
    sta g_cmp
    lda g_bottom+1
    cmpa g_cmp
    lblo geo_emit_return
geo_branch_98
    lda g_clip_b
    eora #$80
    sta g_cmp
    lda g_py
    eora #$80
    cmpa g_cmp
    lblo geo_branch_99
    lbne geo_emit_return
    lda g_clip_b+1
    sta g_cmp
    lda g_py+1
    cmpa g_cmp
    lblo geo_branch_99
    jmp geo_emit_return
geo_branch_99
    lda g_clip_t
    eora #$80
    sta g_cmp
    lda g_py
    eora #$80
    cmpa g_cmp
    lblo geo_branch_101
    lbne geo_branch_100
    lda g_clip_t+1
    sta g_cmp
    lda g_py+1
    cmpa g_cmp
    lblo geo_branch_101
    jmp geo_branch_100
geo_branch_101
    ldd g_clip_t
    std g_py
geo_branch_100
    lda g_clip_b
    eora #$80
    sta g_cmp
    lda g_bottom
    eora #$80
    cmpa g_cmp
    lblo geo_branch_102
    lbne geo_branch_103
    lda g_clip_b+1
    sta g_cmp
    lda g_bottom+1
    cmpa g_cmp
    lblo geo_branch_102
geo_branch_103
    ldd g_clip_b
    std g_bottom
    ldd g_bottom
    addd #$ffff
    std g_bottom
geo_branch_102
    ldd g_px
    std bp_x0
    ldd g_py
    std bp_y0
    ldd g_bottom
    std bp_y1
    ldd bp_y1
    addd #$0001
    std bp_y1
    jsr geo_backend_inputs
    jsr bp_vspan
    jmp geo_account
geo_backend_inputs
    lda g_op
    sta bp_op
    lda g_pen
    sta bp_pen
    rts
geo_account
    lda #$00
    sta g_work+3
    lda #$00
    sta g_work+2
    lda #$00
    sta g_work+1
    lda #$00
    sta g_work
    lda bp_writes+1
    sta g_work+3
    lda bp_writes
    sta g_work+2
    lda g_writes+3
    adda g_work+3
    sta g_writes+3
    lda g_writes+2
    adca g_work+2
    sta g_writes+2
    lda g_writes+1
    adca g_work+1
    sta g_writes+1
    lda g_writes
    adca g_work
    sta g_writes
    ldd g_calls
    addd #$0001
    std g_calls
    rts
geometry_end

What was verified

215 calls completed on this exact configuration using Asm198x 0.0.71 and Emu198x 0.39.0. Every call compares complete bitmap memory, neighbouring guards, public inputs, acceptance, byte writes and backend calls with an independent model. Cases cover all octants, reversal/ties, endpoint ownership, axes, zero radii, clipping, XOR, range extremes and rejected requests. Selected whole native frames match independently composed pixels. A wrong ellipse recurrence completed accepted draws and failed both memory and picture checks.

The radius bound also has a separate exhaustive arithmetic check: all 65,025 nonzero radius pairs fit the chosen 32-bit error path. That host check does not claim native execution of every pair. Real-hardware timing, stock-OS loading and tear-free live drawing remain separate.