Skip to content

Draw Pixels, Spans and Boxes in CPC MODE 1

Reusable four-pen plotting, readback, spans and rectangles, with CPC packing, partial-byte masks and explicit video ownership.

Four pixels share each MODE 1 byte, but their colour bits are not adjacent. This backend handles that packing so your game can set, clear, XOR or read a pixel, then reuse the same work for spans and boxes.

CPC native output with green and blue filled shapes, white outlines and a clipped line
The geometry component uses this backend for every dot and span. Pen selection and palette colour are separate.

Include it and make a first call

Copy cpc.inc and define BP at 48 private RAM bytes. The complete geometry caller places it at $4300. Initialise a 320×200 MODE 1 screen at $C000 using the caller’s Gate Array/CRTC setup, clear it, and choose your palette. The include does not perform video setup or change the palette.

To set pixel (1,20) to pen 3, store little-endian words 1 at BP+0 and 20 at BP+2, set BP+8 to 0 and BP+11 to 3, then call bp_pixel. BP+9 returns 1 when accepted. Set BP+8 to 0 and call bp_read at the same coordinates; BP+10 returns pen index 3. The native checks exercise those entry points through the complete caller. Its mailbox is verification transport; a game calls them directly.

The actual checked pixel entry, not a browser substitute

cpc.inc, excerpt
bp_pixel:
    ld a,2
    jr bp_enter

The calling contract

Words are unsigned 16-bit, little-endian. Origin is top left; Y increases downwards.

BP offset Supply or read
0,2 x0,y0
4,6 x1,y1, exclusive ends for spans/rectangles
8 0 replace with pen, 1 clear to pen 0, 2 XOR pen bits
9 Output: accepted 1, rejected 0
10 Output: read pen 0–3, zero for other calls/rejection
11 Input pen 0–3
12–47 Private scratch; word at 40 counts destination-byte writes

bp_pixel/bp_read use x0,y0; set operation 0 for a read. bp_hspan uses x0,x1,y0 and bp_vspan uses x0,y0,y1. bp_fill and bp_box use all four coordinates. Unused ends are ignored. Inputs 0–8 and 11 survive. Invalid pen, operation or used coordinates reject before writes. Empty/reversed spans and boxes reject; nothing is silently clipped. A 1×1 outlined box writes once under XOR. Geometry supplies clipping above this backend.

Replace really means replace: setting pen 2 over pen 1 must clear one pen bit and set the other. OR alone would produce pen 3. Clear ignores the selected pen and writes pen 0; XOR with pen 0 changes no bits. Read returns the stored pen index, not an RGB colour.

Find the byte, then the two bits

address = $C000 + (y & 7)*2048 + (y >> 3)*80 + (x >> 2)

At (13,20), scanline phase is 4, character row is 2 and byte column is 3: the byte is $E0A3. 13 mod 4 = 1 selects pixel phase 1. Its low pen bit uses bit 6 ($40), its high pen bit bit 2 ($04). Pen 2 inserts $04 into mask $44, keeping the byte’s other six bits.

Pixel phase Pen-bit mask Pen 1 bits Pen 2 bits Pen 3 bits
0 $88 $80 $08 $88
1 $44 $40 $04 $44
2 $22 $20 $02 $22
3 $11 $10 $01 $11

A span [1,5) covers four pixels but touches two bytes. The first selected mask is $77; the second is $88. Each edge must preserve its neighbours. Interior bytes can be handled together instead of recalculating each pixel address. The backend still uses checked read/modify/write work; a specialised full-byte store path can avoid reads for set/clear at the cost of another path.

Each 2 KiB scanline bank contains 2,000 visible bytes and 48 unused bytes. The backend leaves those 384 unused bytes unchanged. It supports this standard CRTC layout only; another start address, stride or MODE needs its own configuration and checks. Native map regions demonstrates another use of this physical display layout.

Costs and ownership

The backend is 578 assembled bytes, plus BP’s 48 RAM bytes. That excludes setup, stack, display and assets. A full clear writes 16,000 visible bytes. A four-pixel span can cost two byte writes when unaligned and one when aligned. These are destination-write counts, not cycles or a frame-budget guarantee.

AF/BC/DE/HL and flags are clobbered. Reserve at least ten stack bytes including the direct caller, plus other calls and interrupts. BP is non-re-entrant. The routines neither change interrupt state nor coordinate another video user. Own the CRTC, Gate Array, palette and bitmap; a firmware-managed display needs a deliberate coexistence/loading path. The original reset fixture takes over the machine.

Open the complete reusable MODE 1 backend

Include once; all six drawing/read entries share BP

cpc.inc
; SPDX-License-Identifier: MIT
; CPC 464 MODE 1, 320x200, display base $C000. Caller owns Gate Array,
; CRTC, palette, ROM/RAM mapping and interrupt policy. BP reserves 48 RAM bytes.
; +0/+2 x0/y0, +4/+6 exclusive x1/y1 (little-endian); +8 op, +11 pen 0..3.
; Op 0 replaces selected pixels with pen; 1 clears to pen 0; 2 XORs pen bits.
; +9 accepted, +10 read pen, +40 word write count; inputs +0..8/+11 survive.
; Same bp_pixel/read/hspan/vspan/box/fill entries as the monochrome backends.
; Whole invalid requests reject before writes. AF/BC/DE/HL/flags clobbered.
; Non-re-entrant; stack <=10 bytes including direct caller, plus interrupts.
bp_fill:
    xor a
    jr bp_enter
bp_box:
    ld a,1
    jr bp_enter
bp_pixel:
    ld a,2
    jr bp_enter
bp_read:
    ld a,3
    jr bp_enter
bp_hspan:
    ld a,4
    jr bp_enter
bp_vspan:
    ld a,5
bp_enter:
    ld (BP+12),a
    xor a
    ld (BP+9),a
    ld (BP+10),a
    ld (BP+40),a
    ld (BP+41),a
    ld hl,BP
    ld de,BP+16
    ld bc,8
    ldir
    ld a,(BP+12)
    cp 2
    jr c,bp_validate
    cp 5
    jr z,bp_unit_x
    ld hl,(BP+18)
    inc hl
    ld (BP+22),hl
    cp 4
    jr z,bp_validate
bp_unit_x:
    ld hl,(BP+16)
    inc hl
    ld (BP+20),hl
bp_validate:
    ld a,(BP+11)
    cp 4
    ret nc
    ld a,(BP+8)
    cp 3
    ret nc
    ld hl,(BP+20)
    ld de,321
    or a
    sbc hl,de
    ret nc
    ld hl,(BP+16)
    ld de,(BP+20)
    or a
    sbc hl,de
    ret nc
    ld hl,(BP+22)
    ld de,201
    or a
    sbc hl,de
    ret nc
    ld hl,(BP+18)
    ld de,(BP+22)
    or a
    sbc hl,de
    ret nc
    ld a,(BP+11)
    ld e,a
    ld d,0
    ld hl,bp_pens
    add hl,de
    ld a,(hl)
    ld (BP+43),a
    ld a,1
    ld (BP+9),a
    ld a,(BP+16)
    and 3
    ld e,a
    ld d,0
    ld hl,bp_right_masks
    add hl,de
    ld a,(hl)
    ld (BP+24),a
    ld hl,bp_bit_masks
    add hl,de
    ld a,(hl)
    ld (BP+26),a
    ld a,(BP+20)
    and 3
    ld e,a
    ld hl,bp_left_masks
    add hl,de
    ld a,(hl)
    ld (BP+25),a
    ld hl,(BP+20)
    dec hl
    ld a,l
    and 3
    ld e,a
    ld hl,bp_bit_masks
    add hl,de
    ld a,(hl)
    ld (BP+27),a
    ld hl,(BP+16)
    srl h
    rr l
    srl h
    rr l
    ld a,l
    ld (BP+28),a
    ld hl,(BP+20)
    dec hl
    srl h
    rr l
    srl h
    rr l
    ld a,l
    ld (BP+29),a
    ld a,(BP+18)
    ld (BP+32),a
    ld a,(BP+22)
    dec a
    ld (BP+42),a
bp_row:
    xor a
    ld (BP+39),a
    call bp_cpc_address
    ld (BP+34),hl
    ld a,(BP+28)
    ld (BP+30),a
    ld a,(BP+24)
    ld (BP+36),a
    ld a,(BP+25)
    ld (BP+37),a
    ld a,(BP+12)
    cp 1
    jr nz,bp_bytes
    ld a,(BP+18)
    ld b,a
    ld a,(BP+32)
    cp b
    jr z,bp_bytes
    ld a,(BP+42)
    ld b,a
    ld a,(BP+32)
    cp b
    jr z,bp_bytes
    ld a,1
    ld (BP+39),a
    ld a,(BP+26)
    ld (BP+36),a
    ld a,(BP+27)
    ld (BP+37),a
bp_bytes:
    ld c,255
    ld a,(BP+28)
    ld b,a
    ld a,(BP+30)
    cp b
    jr nz,bp_not_first
    ld a,(BP+36)
    ld c,a
bp_not_first:
    ld a,(BP+29)
    ld b,a
    ld a,(BP+30)
    cp b
    jr nz,bp_not_last
    ld a,(BP+37)
    and c
    ld c,a
    ld a,(BP+39)
    or a
    jr z,bp_not_last
    ld a,(BP+28)
    cp b
    jr nz,bp_not_last
    ld a,(BP+36)
    ld b,a
    ld a,(BP+37)
    or b
    ld c,a
bp_not_last:
    ld a,(BP+39)
    or a
    jr z,bp_apply
    ld a,(BP+30)
    ld b,a
    ld a,(BP+28)
    cp b
    jr z,bp_apply
    ld a,(BP+29)
    cp b
    jr nz,bp_advance
bp_apply:
    ld hl,(BP+34)
    ld a,(BP+12)
    cp 3
    jr nz,bp_write
    ; Align the selected two-bit pixel with bits 7 and 3.
    ld a,(BP+16)
    and 3
    ld b,a
    inc b
    ld a,(hl)
    jr bp_read_test
bp_read_shift:
    add a,a
bp_read_test:
    djnz bp_read_shift
    ld c,a
    and $80
    ld a,0
    jr z,bp_read_low
    inc a
bp_read_low:
    ld b,a
    ld a,c
    and 8
    ld a,b
    jr z,bp_read_done
    or 2
bp_read_done:
    ld (BP+10),a
    ret
bp_write:
    ld a,(BP+8)
    or a
    jr z,bp_set
    cp 1
    jr z,bp_clear
    ld a,(BP+43)
    and c
    xor (hl)
    jr bp_store
bp_clear:
    ld a,c
    cpl
    and (hl)
    jr bp_store
bp_set:
    ld a,c
    cpl
    and (hl)
    ld b,a
    ld a,(BP+43)
    and c
    or b
bp_store:
    ld (hl),a
    ld hl,(BP+40)
    inc hl
    ld (BP+40),hl
bp_advance:
    ld a,(BP+29)
    ld b,a
    ld a,(BP+30)
    cp b
    jr z,bp_next_row
    inc a
    ld (BP+30),a
    ld hl,(BP+34)
    inc hl
    ld (BP+34),hl
    jp bp_bytes
bp_next_row:
    ld a,(BP+22)
    ld b,a
    ld a,(BP+32)
    inc a
    cp b
    ret z
    ld (BP+32),a
    jp bp_row
bp_cpc_address:
    ; $C000 + (y AND 7)*2048 + (y DIV 8)*80 + byte column.
    ld a,(BP+32)
    and 7
    add a,a
    add a,a
    add a,a
    or $c0
    ld h,a
    ld l,0
    push hl
    ld a,(BP+32)
    srl a
    srl a
    srl a
    ld l,a
    ld h,0
    add hl,hl
    add hl,hl
    add hl,hl
    add hl,hl
    ld d,h
    ld e,l
    add hl,hl
    add hl,hl
    add hl,de
    pop de
    add hl,de
    ld a,(BP+28)
    ld e,a
    ld d,0
    add hl,de
    ret
bp_right_masks:
    db $ff,$77,$33,$11
bp_left_masks:
    db $ff,$88,$cc,$ee
bp_bit_masks:
    db $88,$44,$22,$11
bp_pens:
    db $00,$f0,$0f,$ff

What was verified

166 native backend calls cover all pen/phase combinations, replacement/clear/XOR/readback, partial-byte spans, boxes, full-screen bounds and rejected requests. Each checks the entire 16 KiB allocation with patterned neighbours and unused bytes, public inputs, status and write count. Another 216 calls include geometry and the direct startup ellipse. Selected whole native frames match independent composition. A completed wrong partial-byte mask and a wrong ellipse recurrence fail both memory and pixel comparisons.

The tools are released Asm198x 0.0.71 and Emu198x 0.39.0, CPC 464. This verifies the original reset configuration, not stock-firmware loading, physical-machine timing or tear-free animation.

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