The existing mask renderer draws a shaped object. These helpers supply the other half of movement: save the background, then restore it at the old address before drawing again. The caller uses two objects that can overlap.
Original Code198x 48K tape program captured in Emu198x; motion is byte aligned.
Code status: maintained rectangle helpers, independently CPU-executed, plus a ROM-loaded tape caller. Execution record. The shared lifecycle explains why overlapping backgrounds must be restored in reverse order.
Include the helpers beside the renderer
Include background.inc and bitmap-addressing.inc once. save_rect and restore_rect take DE=top-left bitmap byte, HL=packed buffer, B=width in bytes and C=height in pixel rows. Either zero dimension is a no-op. Otherwise the whole rectangle must fit: width≤32, left column+width≤32 and top row+height≤192.
The caller supplies width×height buffer bytes, without address wrap or overlap with the bitmap, code, stack or another live save. AF/BC/DE/HL change; IX/IY survive. HL advances by width×height. Four temporary stack bytes are needed beyond the return address and any interrupt use. There is no static save state; IRQs must preserve active registers and other renderers must leave the owned region alone.
Packed rectangle saves and restores across the Spectrum bitmap
; SPDX-License-Identifier: MIT
; B=width bytes (0..32), C=height rows, DE=top-left bitmap byte, HL=buffer.
; Zero width/height: no-op. Otherwise the rectangle must fit the 256x192 bitmap.
; Buffer has B*C readable/writable bytes and does not overlap bitmap/stack/code.
; Include bitmap-addressing.inc once for pixel_down (preserves BC/DE).
; AF/BC/DE/HL change; IX/IY preserved. HL advances by B*C. No static state.
; Four temporary stack bytes beyond the caller return and any interrupt use.
save_rect:
ld a,b
or a
ret z
ld a,c
or a
ret z
save_rect_row:
push bc
push de
save_rect_byte:
ld a,(de)
ld (hl),a
inc hl
inc de
djnz save_rect_byte
pop de
pop bc
dec c
ret z
ex de,hl
call pixel_down
ex de,hl
jr save_rect_row
restore_rect:
ld a,b
or a
ret z
ld a,c
or a
ret z
restore_rect_row:
push bc
push de
restore_rect_byte:
ld a,(hl)
ld (de),a
inc hl
inc de
djnz restore_rect_byte
pop de
pop bc
dec c
ret z
ex de,hl
call pixel_down
ex de,hl
jr restore_rect_rowThe existing pixel_down routine handles the interleaved screen layout. It is not called after the last row, so a rectangle ending on row 191 stays within its contract. Attributes are separate and are not saved here.
Keep each buffer with its old address
ld de,(old_b)
ld hl,under_b
ld bc,$0210 ; 2 bytes wide, 16 pixel rows
call restore_rect
; Restore old A next, then choose new positions and save/draw A followed by B.Drawing still uses masked.inc. In this caller SPRITE_W=2 configures that renderer; the rectangle helpers take their own width each call. Mask data consists of interleaved keep-mask/image byte pairs, with image bits zero where the mask preserves scenery.
make creates build/demo.tap; LOAD "" on a 48K Spectrum. The caller owns the bitmap, attributes, private stack and ROM IM1 wait. Two 16×16 objects each keep 32 saved bytes. One moves in eight-pixel steps; reset to leave.
Two objects, separate buffers and reverse restoration
org $8000
SPRITE_W equ 2
start:
di
ld sp,$ff00
ld hl,$4000
ld de,$4001
ld bc,6143
ld (hl),0
ldir
ld hl,$5800
ld de,$5801
ld bc,767
ld (hl),71
ldir
ld d,64
ld b,64
paint_line:
push bc
ld e,0
call pixel_addr
ld b,32
paint_byte:
ld a,d
and 7
jr nz,paint_ready
ld a,$55
paint_ready:
ld (hl),a
inc hl
djnz paint_byte
inc d
pop bc
djnz paint_line
call draw_objects
ld hl,$cafe
ld (ready),hl
im 1
ei
main:
halt
; Reverse the order in which the previous frame was saved/drawn.
ld de,(old_b)
ld hl,under_b
ld bc,$0210
call restore_rect
ld de,(old_a)
ld hl,under_a
ld bc,$0210
call restore_rect
ld a,(tick)
inc a
ld (tick),a
and 3
jr nz,no_move
ld a,(column)
add a,8
cp 176
jr c,column_ready
ld a,64
column_ready:
ld (column),a
no_move:
call draw_objects
ld a,(updates)
inc a
ld (updates),a
jr main
draw_objects:
ld d,80
ld a,(column)
ld e,a
call pixel_addr
ld (old_a),hl
ex de,hl
ld hl,under_a
ld bc,$0210
call save_rect
ld de,(old_a)
ld hl,shape_a
ld c,16
call draw_sprite
ld d,88
ld e,120
call pixel_addr
ld (old_b),hl
ex de,hl
ld hl,under_b
ld bc,$0210
call save_rect
ld de,(old_b)
ld hl,shape_b
ld c,16
call draw_sprite
ret
ready: defw 0
updates: defb 0
tick: defb 0
column: defb 64
old_a: defw 0
old_b: defw 0
under_a: defs 32,0
under_b: defs 32,0
include "background.inc"
include "shapes.inc"
include "../masked-sprites/masked.inc"
include "../bitmap-addressing/bitmap-addressing.inc"
end startGrow the scene deliberately
The independent checks cover seven rectangle cases, including zero sizes, layout boundaries, a full bitmap and the last scanline. Four overlap cases compare the composed and restored bitmap. Broken saving and wrong restoration order must fail. Complete tape startup and progression are separate checks; no tear-free display or whole-game frame budget is claimed.
Saving and restoring add two transfers of width×height bytes per object around drawing. More objects can repeatedly touch the same screen area. Before increasing their size or count, measure the entire update and consider dirty-tile redraw or a clean back buffer.
These routines do not clip, shift or organise object lists. If the background changes, saved bytes can become stale. Restore old objects before scrolling, then capture and draw at the new screen positions. The shared guide connects that ordering to the BBC and Amiga implementations.
When an object can leave the screen, clip its visible rectangle before saving or drawing it. Keep its old clipped address and dimensions for the next restoration.