Code status: executed sample. The maintained listing and complete caller were assembled and executed in Emu198x on a 48K PAL configuration. The check covers every valid pixel coordinate and next-row byte address; it does not qualify physical hardware or contended execution.
Overview
The Spectrum has no hardware sprites and no drawing chip — every pixel is placed by the CPU, straight into the screen bitmap at $4000. And that bitmap is laid out unlike any other: consecutive screen lines are not consecutive addresses. The ULA stores pixels interleaved by third-of-screen and character row, so moving one pixel down the screen can jump the address by 256 bytes or by thousands. Every Spectrum graphics routine begins by taming this — turning an (x, y) into the right byte, and then stepping between lines without recomputing from scratch. Get this right and the rest of Spectrum drawing follows; get it wrong and your sprite scatters itself across the screen.
The routine’s contract
pixel_addr takes Y in D (0–191) and X in E (0–255), and returns the byte address in HL. It changes A, H, L and flags; B, C, D and E are preserved. The pixel mask is separate: $80 >> (x AND 7).
pixel_down takes an address in $4000–$57FF belonging to a bitmap row 0–190, and returns the same byte column on the next row. It changes A, H, L and flags, preserving B, C, D and E. Neither routine checks its inputs. Stop before stepping from row 191: that step reaches attribute memory at $5800, not another bitmap row.
Code
; SPDX-License-Identifier: MIT
; Maintained Code198x Bitmap Addressing pattern; no third-party derivation claimed.
; =============================================================================
; BITMAP ADDRESSING - ZX SPECTRUM
; Pixel (D = y 0..191, E = x 0..255) -> HL = screen byte address
; High byte: 0 1 0 y7 y6 y2 y1 y0 Low byte: y5 y4 y3 x4 x3 x2 x1 x0
; =============================================================================
pixel_addr:
ld a,d
and %00000111 ; y2 y1 y0
ld h,a
ld a,d
and %11000000 ; y7 y6 (which third)
rrca
rrca
rrca ; -> bits 4,3
or h
or %01000000 ; the $40 base
ld h,a ; H = 010 y7 y6 y2 y1 y0
ld a,d
and %00111000 ; y5 y4 y3 (char row within third)
rlca
rlca ; -> bits 7,6,5
ld l,a
ld a,e
rrca
rrca
rrca
and %00011111 ; x / 8 -> column 0..31
or l
ld l,a ; L = y5 y4 y3 x4 x3 x2 x1 x0
; the pixel within the byte is bit (7 - (x AND 7)); mask = $80 >> (x AND 7)
ret
; Step HL down exactly one pixel line — the interleave made cheap
end_pixel_addr:
pixel_down:
inc h ; within a cell, the next line is simply H+1
ld a,h
and %00000111
ret nz ; still inside this character cell — done
ld a,l
add a,32 ; crossed a cell: move to the next character row
ld l,a
ret c ; carried into the next third — H is already right
ld a,h
sub 8 ; else undo the cell carry that inc h left in H
ld h,a
ret
end_pixel_down:Trade-offs
| Aspect | Cost |
|---|---|
| CPU | pixel_addr: 117 T-states; pixel_down: 26, 46 or 65, depending on the row boundary |
| Memory | No lookup data; an optional 192×2-byte line-address table needs 384 bytes |
| Limitation | The layout resists straight-line arithmetic; plan routines around it |
These are uncontended Z80 instruction totals, including each routine’s RET and excluding the caller’s CALL (17 more T-states). pixel_down takes 26 within a character cell, 46 across a 64-line third, and 65 across another character-row boundary. Memory contention and interrupts can increase elapsed time; the retained verification matches these totals in Emu198x with code at $8000, stack at $FF00 and interrupts disabled. The routines compute addresses without reading or writing screen RAM. These measurements do not cover contention or physical hardware.
When to use: Every pixel-level draw — sprites, lines, plotting, scrolling.
When to avoid: Character-cell work; printing and colour move a whole 8×8 block at a time and dodge the pixel maths.
Why the layout is this way
The address decomposes into three pieces of the y coordinate scattered across the two bytes: the third of the screen (y7 y6), the character row within that third (y5 y4 y3), and the pixel row within the character (y2 y1 y0). It reads as chaos, but it exactly matches the order the ULA fetches bytes to build the display, which let Ferranti shave gates off the address logic. The cost landed on the programmer: there’s no base + y*32, so serious code either computes the address with the bit-shuffle above or, more often, looks it up in a precomputed 192-entry table of line addresses.
Stepping down is the common case
You rarely need a fresh address per line — you need the line below the one you have, which is what pixel_down gives cheaply. Inside a character cell, y2 y1 y0 live in the low three bits of the high byte, so the next line down is just inc h. Only when those bits overflow — every eighth line, at a cell boundary — do you move to the next character row (L += 32) and, unless you’ve crossed into a new third, undo the carry the inc h left behind. That asymmetry — seven cheap steps then one expensive one — is the shape of every Spectrum draw loop.
Related
Patterns: Masked Sprites, Attribute Writing
Vault: ULA — the screen layout | ZX Spectrum
Sources
Zilog, Z80 CPU User Manual, UM008011-0816: instruction descriptions for LD, AND, OR, RLCA, RRCA, INC, ADD, SUB, RET and conditional RET. The timing totals above account for the instructions in these listings.