Code status: maintained Z80 routines, executed in a bounded CPU fixture. The checks compare actual memory or emitted characters with independent expectations. They establish the stated routine contracts in uncontended 48K RAM; ROM startup, display timing and original hardware are separate checks.
Audit execution record · Scope and remaining checks
Overview
An Amiga draws a shaped object over scenery with the blitter’s cookie-cut; the Spectrum has no such hardware, so it does the same job by hand, one byte at a time. The technique is the same in spirit: a mask decides pixel by pixel whether to keep the background or the sprite. For each screen byte the object touches, you AND a mask to punch a clean hole in the background, then OR the image into that hole — screen = (screen AND mask) OR image. Combined with the screen addressing routine to walk down the object’s lines, this gives us a shaped object while preserving scenery around it.
Calling the routines
Include masked.inc and the maintained bitmap addressing include once. Define SPRITE_W as a constant from 1 to 32. Supply DE = the left screen-byte address, HL = readable interleaved mask/image pairs and C = height. The entire rectangle must fit the 256 × 192 bitmap. The routine does not clip, shift or set attributes.
A zero height does nothing. draw_row accepts a byte width in B; zero is a no-op. Each screen byte becomes (old_byte AND mask) OR image. Supply zero image bits wherever the background should survive. The source needs two bytes for every destination byte.
AF, BC, DE and HL are working registers. HL advances through the sprite data; the row’s left edge is saved before drawing and restored before stepping down. pixel_down must preserve BC/DE. It is never called after the last row, so a sprite ending on pixel row 191 stays within its contract.
Masking combines a new image with the current background. Moving it still requires restoring the old area or redrawing the scene. Applying a mask over the previous sprite is not an erase operation.
For example, after establishing the documented stack, state and data:
ld d,63 ; pixel row
ld e,16 ; pixel X, byte-aligned
call pixel_addr ; HL is the screen byte
ex de,hl ; DE is the destination
ld hl,my_pairs
ld c,3 ; three pixel rows, 2*SPRITE_W*3 source bytes
call draw_sprite
; Include masked.inc and bitmap-addressing.inc once.Masked rows and sprites
; Maintained masked renderer, Pasmo syntax. Includes pixel_down separately.
; SPRITE_W=1.masked_32 bytes; DE=left screen byte, HL=interleaved mask/image pairs.
; C=height (zero is a no-op); the whole rectangle must fit 256x192 display.
; Caller owns bitmap; routine neither clips nor restores the old background.
; AF/BC/DE/HL change; HL advances by 2*SPRITE_W*height. C returns zero.
; pixel_down must preserve BC/DE. No call below pixel row191 is permitted.
; =============================================================================
; MASKED SPRITES - ZX SPECTRUM
; Draw one sprite row over the background: screen = (screen AND mask) OR image
; Sprite data is stored as interleaved (mask, image) byte pairs.
; DE -> screen address for this row, HL -> sprite data, B = width in bytes
; =============================================================================
draw_row:
ld a,b
or a
ret z ; zero width is a no-op
masked_byte:
ld a,(de) ; the background byte
and (hl) ; AND mask: 1s keep background, 0s clear for the sprite
inc hl
or (hl) ; OR image: the sprite's own pixels
inc hl
ld (de),a ; write it back
inc de ; next byte across
djnz masked_byte
ret
; A full sprite: for each of its rows, draw a row then step the screen down.
; (pixel_down is the one-line-down routine from Bitmap Addressing.)
draw_sprite:
ld a,c
or a
ret z ; zero height is a no-op
; DE -> top-left screen byte, HL -> sprite data, C = height in rows
masked_row:
push bc
push de ; retain this row's LEFT edge
ld b,SPRITE_W
call draw_row
pop de
pop bc
dec c
ret z ; do not step below the final display row
ex de,hl
call pixel_down ; preserves DE: the sprite-data pointer survives
ex de,hl
jr masked_rowTrade-offs
| Aspect | Cost |
|---|---|
| CPU | Work grows with the destination bytes touched; measure it alongside restoration and the rest of the frame |
| Memory | Mask and image (double the sprite data); pre-shifting multiplies it further |
| Limitation | Colour is still per-cell — a moving sprite risks attribute clash |
When to use: Any moving object over a non-blank background — the standard Spectrum sprite.
When to avoid: A sprite over solid paper with no detail behind it — a plain OR (or XOR for reversible draw) is faster with no mask.
The mask, bit for bit
The two bytes do complementary jobs. The mask is 1 wherever the background should survive and 0 over the sprite’s solid pixels; AND-ing it leaves the scenery intact and clears a precise silhouette-shaped hole. The image is the sprite’s pixels, 0 everywhere else; OR-ing it drops the object into the hole without disturbing a pixel outside it. A bare OR can add set pixels but cannot clear scenery where the object needs an unset pixel. A straight rectangular copy has the opposite problem: its background pixels overwrite the scenery too.
Pre-shifting for any X
A byte holds eight pixels, so a sprite whose X isn’t a multiple of 8 straddles two byte columns — its pixels have to be shifted into place. Shifting mask and image at runtime (rotating through the carry, byte by byte) works but is slow, and it’s the same shift every frame. So fast games pre-shift: store the sprite in all eight horizontal alignments, precomputed, and pick the copy matching x AND 7. It costs eight times the sprite memory and makes each shifted copy one byte wider (the pixels spill right), but the draw loop becomes a plain copy with no rotation — a useful trade when the available memory and frame budget support it.
Movement means erase
A sprite that moves must leave the background as it found it. Two ways: save-under — copy the screen bytes before drawing, restore them next frame before drawing at the new spot — or redraw the background tiles the sprite covered. Either way the order each frame is erase-old, then draw-new, and doing it during the frame the eye doesn’t see is what keeps the motion clean. The moment two sprites share an 8×8 cell, though, the colour question arrives — which is attribute clash.
Related
Patterns: Bitmap Addressing, Attribute Clash, Cookie-Cut Blit (Amiga)
Vault: ULA | ZX Spectrum
Explore the cross-platform sprite family for software drawing, hardware limits, restoration and overload policies.
For a complete caller that removes old objects and handles overlapping saved backgrounds, follow the software-object lifecycle.