A bullet needs somewhere to keep its position and remaining life. When it leaves the screen, that space should become available again. This small fixed pool gives you eight slots, refuses a ninth object, then shows slot 1 being released and reused. A lifetime pass removes the finished objects without shifting anyone else in memory.

Include, initialise and call
Reserve POOL_CAPACITY contiguous flag bytes at pool_flags and include pool.inc once. Call pool_init at startup; it clears the flags, leaving payload alone. Flag zero means free and one means occupied. The component’s capacity contract is 1–255; these maintained native callers qualify 4 and 8, and their compact occupancy renderer only represents eight slots.
Call pool_acquire: carry clear / A = slot means success; carry set / A = capacity means full. pool_release takes A = index, and returns carry clear on release or carry set for invalid/already free. It does not promise to preserve A. AF, BC and HL change; DE, IX and IY survive. No port, interrupt or hidden scratch ownership. A direct call uses a two-byte return address. The optional ageing pass also saves a two-byte loop counter while making a nested release call.
Reusable pool initialisation, acquire and release
; SPDX-License-Identifier: MIT
; pool_flags: POOL_CAPACITY private bytes, 1..255; 0 free, 1 occupied.
; init clears flags only. acquire: NC/A=index; full C/A=capacity.
; release A=index: NC on occupied valid slot; C on invalid/already free.
; A/flags, BC, HL change; DE, IX, IY survive. No I/O, IRQ or hidden state.
; Single owner; initialise payload after acquisition, before any reader.
pool_init:
ld hl,pool_flags
ld b,POOL_CAPACITY
xor a
pool_init_loop:
ld (hl),a
inc hl
djnz pool_init_loop
ret
pool_acquire:
ld hl,pool_flags
ld b,POOL_CAPACITY
ld c,0
pool_acquire_loop:
ld a,(hl)
or a
jr z,pool_claim
inc hl
inc c
djnz pool_acquire_loop
ld a,POOL_CAPACITY
scf
ret
pool_claim:
ld (hl),1
ld a,c
or a
ret
pool_release:
cp POOL_CAPACITY
jr nc,pool_reject
ld l,a
ld h,0
ld bc,pool_flags
add hl,bc
ld a,(hl)
or a
jr z,pool_reject
ld (hl),0
or a
ret
pool_reject:
scf
ret
component_end:On success the slot is already marked occupied. Initialise every payload field that its update or renderer will read—position, speed, type or lifetime—before another reader sees it. Keep one owner for pool mutations. Interrupts can record requests for that owner rather than looking at half-initialised objects. The helper is not re-entrant and supplies no locking.
Check the success result before indexing a payload array. A full result returns the capacity, just outside the valid index range. It is an ordinary game decision: refuse another shot, drop a cosmetic effect or deliberately choose an eviction policy. The allocator never silently replaces an object.
Buildable caller and include order
org $8000
POOL_CAPACITY equ 8
BROKEN equ 0
include "caller.inc"
include "exercise.inc"
component_start:
include "pool.inc"
include "age.inc"
include "../../physics/rectangle-overlap/display.inc"
code_end:
guard_a: defb 0
pool_flags: defs POOL_CAPACITY,0
guard_b: defb 0
pool_life: defs POOL_CAPACITY,0
mask_index: defb 0
selected: defb 0
status_bits: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1294,0
end startThe standalone 48K PAL caller is entered by the ROM tape loader and BASIC USR. It disables interrupts, owns the bitmap, attributes and border port, and uses ROM glyphs for its small diagnostic row. It stays running; it does not restore a BASIC session. Its code starts at $8000 and its private flags, payload, packet and output variables follow the code. The reusable routines touch none of those video or port resources.
Release during an update
pool_release clears one occupied flag. It changes neither position nor any other payload. Invalid and double releases return failure without a write. A reused index refers to a new object: clear references held by other objects when releasing it. Generation-tagged handles are a possible later extension when references must survive reuse; this routine has none.
The optional lifetime pass visits fixed indices in order. Each occupied slot needs a lifetime of 1–255. On each simulation step it decrements live lifetimes and releases those reaching zero. A zero initial lifetime is outside this pass’s contract and would wrap. Nothing moves when an object is released, so the loop cannot skip a neighbour that was shifted into its place.
Optional fixed-slot lifetime pass
; Optional lifetime policy. Live life must be 1..255. Spawn after the pass.
; AF/BC/HL change; DE/IX/IY survive. Fixed indices, no compaction.
; Saved loop counter plus nested release call use stack space on expiry.
pool_age:
ld b,POOL_CAPACITY
ld c,0
pool_age_loop:
ld l,c
ld h,0
push bc
ld bc,pool_flags
add hl,bc
ld a,(hl)
pop bc
or a
jr z,pool_age_next
ld l,c
ld h,0
push bc
ld bc,pool_life
add hl,bc
dec (hl)
pop bc
jr nz,pool_age_next
ld a,c
push bc
call pool_release
pop bc
pool_age_next:
inc c
djnz pool_age_loop
ret
age_end:Apply new spawn requests after this pass. Otherwise a new object in a later slot might update immediately while one reusing an earlier slot waits. This is simulation work, not a frame waiter: choose when your game’s loop calls it.
Fill, refuse overflow, reuse and age the collection
preview_pool:
call pool_init
call pool_acquire
ld (preview),a
call pool_acquire
ld (preview+1),a
call pool_acquire
ld (preview+2),a
preview_fill:
call pool_acquire
jr nc,preview_fill
ld a,255
ld (preview+3),a
ld a,1
if BROKEN=0
call pool_release
endif
call pool_acquire
jr nc,preview_reused
ld a,255
preview_reused:
ld (preview+4),a
ld a,255
call pool_release
ld a,0
adc a,0
ld (preview+5),a
ld hl,pool_life
ld b,POOL_CAPACITY
ld c,0
preview_life:
ld a,c
and 1
inc a
ld (hl),a
inc hl
inc c
djnz preview_life
call pool_age
call occupancy
ld (preview+6),a
call pool_age
call occupancy
ld (preview+7),a
retMake it your own
custom.asm changes capacity from 8 to 4. Component code size is unchanged in this layout; flags and lifetime storage each shrink by four bytes. The first six output fields stay the same; the first lifetime mask becomes 0A, meaning slots 1 and 3 survive, then 00. Both capacities run through the native ownership tests.
Use four slots with the same ownership rules
org $8000
POOL_CAPACITY equ 4
BROKEN equ 0
include "caller.inc"
include "exercise.inc"
component_start:
include "pool.inc"
include "age.inc"
include "../../physics/rectangle-overlap/display.inc"
code_end:
guard_a: defb 0
pool_flags: defs POOL_CAPACITY,0
guard_b: defb 0
pool_life: defs POOL_CAPACITY,0
mask_index: defb 0
selected: defb 0
status_bits: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1294,0
end startThe eight fields are the first three acquired indices, FF for the refused allocation, the reclaimed index, status from an invalid release, and occupied-slot masks after two updates. In a mask, bit i means slot i is live. AA means slots 1, 3, 5 and 7 remain. FF is the caller’s display sentinel, never a successful slot.
Give bullets position/velocity arrays or enemies a type/state array, without making a pool index into a hardware sprite number. A game object can be offscreen, use several sprites or have no visible sprite. The sprite lifecycle is a separate drawing concern.
Common mistake: forget to release
The labelled wrong caller omits the release of slot 1. All flags stay occupied, so the next acquire correctly fails and the fifth field becomes FF. The program completes its remaining lifetime passes: this is an observed exhaustion result, not a failed loader. Restore the release call shown in the working preview; the repaired executable is identical to the working executable. The Spectrum’s tape header can have a different filename even when its loaded CODE is identical.
Deliberate fault: omit release before reacquiring
org $8000
POOL_CAPACITY equ 8
BROKEN equ 1
include "caller.inc"
include "exercise.inc"
component_start:
include "pool.inc"
include "age.inc"
include "../../physics/rectangle-overlap/display.inc"
code_end:
guard_a: defb 0
pool_flags: defs POOL_CAPACITY,0
guard_b: defb 0
pool_life: defs POOL_CAPACITY,0
mask_index: defb 0
selected: defb 0
status_bits: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1294,0
end start
If a real game stops spawning, inspect every removal path: leaving the playfield, ending an effect and changing rooms all need an ownership decision. Releasing a slot does not erase stale payload or invalidate somebody else’s saved index automatically.
Cost and alternatives
On this layout, the helper uses 56 code bytes plus N flag bytes. The optional lifetime pass adds 39 code bytes plus N lifetime bytes. Those are component costs, separate from native packaging and the diagnostic client.
Init writes N flags. Acquisition scans through the first free flag, or all N when full. Release checks a valid slot and writes at most one flag. Ageing scans N flags, decrements each live lifetime and calls release for each expiry. These are work counts, not physical-hardware cycle measurements. Addressing choices and complete update/display work still need a budget.
A free list trades faster allocation for extra bookkeeping. Packed arrays avoid scanning holes but move objects, changing references and deletion rules. For a small bounded collection, the scan keeps ownership easy to inspect. Maths for Games connects indexing and bit masks; The Craft helps decide what exhaustion should mean to the player.
What ran
Four native executions with Asm198x 0.0.72 / Emu198x 0.44.2 cover eight slots, four slots, omitted release and exact executable repair. Amiga media is mastered by Build198x 0.2.9. A separate caller emits all 256 occupancy masks for each configured capacity, allocation/full/release results, a preserved-register sentinel, guards, cleared flags and untouched lifetime payload. The four-slot fixture ignores upper mask bits. A set-based oracle checks that output; the visible client checks reuse and safe lifetime removal.
The checks also bind actual loaded code, startup completion, component extents, capture hashes and native text pixels. Configuration records pin tools and applicable firmware; no firmware is distributed. These are named emulator workloads, not original-hardware timings or a claim about untested capacities.