Skip to content

Clip Masked Sprites on the Spectrum

A byte-aligned 16×16 sprite can cross any screen edge safely. Reusable signed clipping and masked drawing, with source cropping and a complete 48K tape example.

Original spectrum scene with masked objects cropped at the four screen corners and drawn inside the viewport

Original Code198x program and artwork captured in Emu198x.

Code status: maintained routines and an emulator-executed caller. Execution record.

The original masked renderer expects a complete on-screen rectangle. This wrapper lets an object begin outside the bitmap, selects the surviving source bytes and rows, then draws only that part. Its sample scene shows four clipped corners and three interior objects; two further calls are fully invisible.

Include the routines and give them a context

Include clip.inc, draw.inc and the existing bitmap-addressing helper once. draw_clipped takes IX pointing at 32 caller-owned bytes. Put the signed X position at +0, signed Y at +14, and source pointer at +28. X is measured in eight-pixel byte columns, Y in pixel rows. X = −1 crops one column from the left; X = 32 is fully outside.

The source is fixed at 16×16: 64 interleaved keep-mask/image bytes, four bytes per original row. Keep-mask 1 preserves the old bitmap bit; the corresponding image bit must be zero. Source, context, stack and code must be separate from the bitmap and one another.

The wrapper fills in its two clipping blocks. Within each 14-byte block, destination is +6, source skip +8 and visible count +10. +12..13 is scratch; context +30 counts drawn rows. Positions and source pointer survive, but other workspace fields are outputs or scratch. AF/BC/DE/HL change; IX/IY survive. The wrapper does not touch attributes or set up interrupts.

Crop the source and draw the visible mask/image pairs

draw.inc
; SPDX-License-Identifier: MIT
; draw_clipped: IX -> 32 bytes. +0 signed X in 8-pixel byte columns,
; +14 signed Y in pixel rows, +28 pointer to 16x16 interleaved KEEP-mask/image.
; Other fields are work/output: two clip_axis blocks, +30 row counter.
; 64 source bytes remain caller-owned, separate from bitmap/code/stack/context.
; AF/BC/DE/HL change, IX/IY survive. Attributes untouched. Include clip.inc and
; bitmap-addressing.inc once. Exclusive workspace; no timing/IRQ setup here.
draw_clipped:
    ld (ix+2),2
    ld (ix+3),0
    ld (ix+4),32
    ld (ix+5),0
    call clip_axis
    ld a,(ix+10)
    or a
    ret z
    push ix
    pop hl
    ld de,14
    add hl,de
    push ix
    push hl
    pop ix
    ld (ix+2),16
    ld (ix+3),0
    ld (ix+4),192
    ld (ix+5),0
    call clip_axis
    pop ix
    ld a,(ix+24)
    or a
    ret z
    ld (ix+30),a
    ld l,(ix+22)
    ld h,0
    add hl,hl
    add hl,hl
    ld e,(ix+8)
    ld d,0
    add hl,de
    add hl,de
    ld e,(ix+28)
    ld d,(ix+29)
    add hl,de
    push hl
    ld d,(ix+20)
    ld a,(ix+6)
    add a,a
    add a,a
    add a,a
    ld e,a
    call pixel_addr
    ex de,hl
    pop hl
draw_clipped_row:
    push hl
    push de
    ld b,(ix+10)
draw_clipped_byte:
    ld c,(hl)
    inc hl
    ld a,(de)
    and c
    or (hl)
    ld (de),a
    inc hl
    inc de
    djnz draw_clipped_byte
    pop de
    pop hl
    dec (ix+30)
    ret z
    ld bc,4
    add hl,bc
    ex de,hl
    call pixel_down
    ex de,hl
    jr draw_clipped_row

The axis helper is also usable on its own: IX selects a 14-byte block containing signed position at +0, extent at +2 and viewport limit at +4. Extent is 0..256, limit 1..256. All three results are zero when empty. Temporary two-byte pushes preserve comparisons; the drawing loop pushes four row-pointer bytes, in addition to call returns and interrupt needs.

Intersect a signed interval with the viewport

clip.inc
; SPDX-License-Identifier: MIT
; clip_axis: IX -> 14 caller-owned bytes, little-endian words:
; +0 signed position (-32768..32767), +2 extent (0..256), +4 limit (1..256).
; Outputs +6 destination, +8 source skip, +10 visible count; +12 scratch.
; Empty output has all three result words zero. Inputs survive.
; AF/BC/DE/HL change, IX/IY survive. No hardware or interrupt ownership.
clip_axis:
    xor a
    ld (ix+6),a
    ld (ix+7),a
    ld (ix+8),a
    ld (ix+9),a
    ld (ix+10),a
    ld (ix+11),a
    ld c,(ix+2)
    ld b,(ix+3)
    ld a,b
    or c
    ret z
    ld l,(ix+0)
    ld h,(ix+1)
    bit 7,h
    jr z,clip_positive
    ; Unsigned magnitude also handles -32768 without signed negation overflow.
    xor a
    sub l
    ld e,a
    sbc a,a
    sub h
    ld d,a
    ld h,b
    ld l,c
    or a
    sbc hl,de
    ret c
    ret z
    ld (ix+8),e
    ld (ix+9),d
    jr clip_capacity
clip_positive:
    ld e,(ix+4)
    ld d,(ix+5)
    push hl
    or a
    sbc hl,de
    pop hl
    ret nc
    ld (ix+6),l
    ld (ix+7),h
    ld h,b
    ld l,c
clip_capacity:
    ; HL remaining source count; DE = viewport capacity at destination.
    ld e,(ix+4)
    ld d,(ix+5)
    ld c,(ix+6)
    ld b,(ix+7)
    ex de,hl
    or a
    sbc hl,bc
    ex de,hl
    push hl
    or a
    sbc hl,de
    pop hl
    jr c,clip_store
    ex de,hl
clip_store:
    ld (ix+10),l
    ld (ix+11),h
    ret

Fit it into a running program

The complete caller initialises the display, sets context/source values and calls the wrapper for each object. It owns a private stack at $ff00 and uses ROM IM1/HALT afterwards. Build with make ASM198X=/path/to/asm198x and load build/demo.tap on a 48K PAL Spectrum. Reset to leave the caller.

Complete tape caller with cropped corners and invisible objects

demo.asm
org $8000
start:
    di
    ld sp,$ff00
    ld hl,$4000
    ld de,$4001
    ld bc,6143
    ld (hl),$55
    ldir
    ld hl,$5800
    ld de,$5801
    ld bc,767
    ld (hl),71
    ldir
    ld hl,-1
    ld (context),hl
    ld hl,-8
    ld (context+14),hl
    ld hl,shape_a
    ld (context+28),hl
    ld ix,context
    call draw_clipped
    ld hl,31
    ld (context),hl
    ld hl,-8
    ld (context+14),hl
    ld hl,shape_a
    ld (context+28),hl
    ld ix,context
    call draw_clipped
    ld hl,-1
    ld (context),hl
    ld hl,184
    ld (context+14),hl
    ld hl,shape_a
    ld (context+28),hl
    ld ix,context
    call draw_clipped
    ld hl,31
    ld (context),hl
    ld hl,184
    ld (context+14),hl
    ld hl,shape_a
    ld (context+28),hl
    ld ix,context
    call draw_clipped
    ld hl,15
    ld (context),hl
    ld hl,88
    ld (context+14),hl
    ld hl,shape_a
    ld (context+28),hl
    ld ix,context
    call draw_clipped
    ld hl,0
    ld (context),hl
    ld hl,80
    ld (context+14),hl
    ld hl,shape_a
    ld (context+28),hl
    ld ix,context
    call draw_clipped
    ld hl,30
    ld (context),hl
    ld hl,112
    ld (context+14),hl
    ld hl,shape_a
    ld (context+28),hl
    ld ix,context
    call draw_clipped
    ld hl,-2
    ld (context),hl
    ld hl,32
    ld (context+14),hl
    ld hl,shape_a
    ld (context+28),hl
    ld ix,context
    call draw_clipped
    ld hl,32
    ld (context),hl
    ld hl,32
    ld (context+14),hl
    ld hl,shape_a
    ld (context+28),hl
    ld ix,context
    call draw_clipped
    ld hl,$cafe
    ld (ready),hl
    im 1
    ei
main:
    halt
    ld hl,(updates)
    inc hl
    ld (updates),hl
    jr main
ready: defw 0
updates: defw 0
context: defs 32,0
include "clip.inc"
include "draw.inc"
include "shapes.inc"
include "../bitmap-addressing/bitmap-addressing.inc"
end start

Move it next

This draws at a position; it does not remove the previous image. Combine it with background restoration, retaining the previous clipped address and dimensions with the saved pixels. The shared clipping entry explains the order and the case where an object becomes fully invisible.

The sides are byte aligned, while top and bottom cropping work by pixel row. Arbitrary-X motion needs shifts or prepared image variants. There is no promised frame budget: the complete scene proves the stated drawing result and a running caller, while display scheduling belongs to the surrounding game.

For individual pixel X positions, select prepared source phases and keep the padded footprint and original source stride explicit.