Skip to content

Draw Pixels, Spans and Boxes on Atari 5200

Reusable 6502 bitmap primitives with explicit ANTIC display-list and 4 KiB screen addressing.

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.

Native Atari 5200 output showing filled and outlined boxes and spans
Original caller captured unchanged in Emu198x 0.39.0; the full bitmap and selected whole native images are independently checked.

Include it and make a call

Reserve 48 zero-page bytes at BP; the caller uses $70. Define PIXEL_WIDTH=320 and PIXEL_HEIGHT=192 and include antic.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

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

Read 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 320×192. 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.

ANTIC must read the same bytes the CPU draws

ANTIC mode F is a 320×192 monochrome bitmap: forty eight-pixel bytes per scanline. Bit 7 is leftmost. The display’s screen counter wraps at a 4 KiB boundary, so a naive consecutive 7,680-byte CPU bitmap would eventually disagree with ANTIC.

Here 102 complete scanlines occupy 4,080 bytes. Skip sixteen bytes, then begin row 102 at the next 4 KiB boundary:

address = base + (y div 102)*4096 + (y mod 102)*40 + (x div 8)

The base is $2000. At (13,102), the address is $3001, mask $04. The caller reserves 8,192 bytes, of which 512 are padding. The routine preserves those gaps.

The display list begins with three eight-line blanks, then an explicit LMS ($4F plus address) for each scanline, ending with JVB. It uses 582 meaningful bytes in a 768-byte reservation at $1000, wholly inside a 1 KiB boundary. Per-row LMS is larger than a two-LMS list but keeps the layout easy to inspect. The 384-byte CPU row tables must agree with it.

The caller owns DMA, NMI policy, palette and stable RAM mappings. GTIA registers are at $C000, ANTIC at $D400. This separately executed 32 KiB cartridge uses an original generated 2 KiB reset stub, not the manufacturer BIOS. It does not establish the original authentication/boot path. A working XL example is not used as proof for the 5200.

The maths under the call

Division chooses a byte; remainder chooses a bit. Eight pixels share that byte, so a point must preserve the other seven. 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

antic.inc
; SPDX-License-Identifier: MIT
; Monochrome byte/span drawing with caller-supplied trusted scanline tables. 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
    ; Checked coordinates use PIXEL_WIDTH/HEIGHT, not an assumed OS mode.
    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 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 #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 #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 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

Resources and proof

The backend is 528 assembled bytes, plus 384 bytes of CPU row tables and 48 zero-page context bytes. Reset setup, bitmap and stack are additional. The display list adds 582 meaningful bytes, with 768 reserved. Full-byte spans save repeated per-pixel addressing. A smaller display list could trade construction simplicity for space. 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.

155 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. The display list, every padding byte and the row 101/102 split 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.