Skip to content

Claim and Reuse Object Slots in Dragon 32 Assembly

Reserve fixed slots, report a full pool safely, release by index and optionally expire objects without moving their payloads.

A game can keep a small stock of enemies, projectiles or effects without allocating a new block of memory every time. A pool gives each live object a stable slot. Releasing one slot makes room for another object without shuffling every payload.

Dragon 32 completes the fixed-slot client with both reuse checks passing.
The working client claims, releases, reuses and expires fixed slots.

Include and initialise

Define POOL_CAPACITY as an integer from one through 255 and reserve that many private flag bytes. Zero means free and one occupied. Include pool.inc once, then call pool_init at an intended reset boundary. Initialisation clears flags only: payload arrays belong to the caller. Call it once before use, rather than resetting ownership during ordinary spawning.

The Dragon 32 example uses POOL_FLAGS for the flag base. A game chooses RAM it owns and allocates its payload arrays separately. A slot index can address the same position in each parallel array: position, direction, animation, kind or other chosen state.

Reusable slot ownership routines

pool.inc
; SPDX-License-Identifier: MIT
; POOL_FLAGS: capacity private bytes, compile-time capacity1..255.
; init clears flags only. acquire C=0/B=index; full C=1/B=capacity.
; release B: carry clear on live valid slot, carry set on invalid/already-free.
; A/B/X/CC clobbered; Y/U/DP preserved. No I/O, IRQ handler or hidden allocation.
; ABX adds unsigned B. Do not replace it with signed B,X indexing for IDs128+.
pool_init:
    ldx #POOL_FLAGS
    clrb
pool_init_loop:
    clr ,x+
    incb
    cmpb #POOL_CAPACITY
    bne pool_init_loop
    rts
pool_acquire:
    ldx #POOL_FLAGS
    clrb
pool_acquire_loop:
    lda ,x
    beq pool_claim
    leax 1,x
    incb
    cmpb #POOL_CAPACITY
    bne pool_acquire_loop
    orcc #1
    rts
pool_claim:
    lda #1
    sta ,x
    andcc #254
    rts
pool_release:
    cmpb #POOL_CAPACITY
    bhs pool_reject
    ldx #POOL_FLAGS
    abx
    tst ,x
    beq pool_reject
    clr ,x
    andcc #254
    rts
pool_reject:
    orcc #1
    rts
pool_code_end:

Acquire, check, then write

Call pool_acquire. Check carry before using the returned index. Carry clear means the lowest free slot has been claimed. Initialise its payload before making it visible to another part of the game. Carry set means full; the returned index equals capacity and lies outside every valid slot.

A/B/X/CC change. Y/U/DP survive. Acquisition returns carry clear with B=slot, or carry set with B=capacity. Release takes B=slot and reports success through carry.

Call pool_release with a valid occupied index to clear its flag. An invalid index or an already-free slot returns carry set and changes no flag. Release leaves payload intact. Those old bytes are not a live object: the ownership flag decides whether the game may use them.

Fill, release and reuse

The maintained caller claims slots zero, one and two, then fills the remaining slots. It observes a full-pool result, releases slot one and claims it again. A rejected release of index 255 leaves the pool unchanged in these eight- and four-slot examples.

Retained fill, release, reuse and expiry client

caller.inc, excerpt
preview_pool:
    jsr pool_init
    jsr pool_acquire
    stb preview
    jsr pool_acquire
    stb preview+1
    jsr pool_acquire
    stb preview+2
preview_fill:
    jsr pool_acquire
    bcc preview_fill
    lda #255
    sta preview+3
    lda #BROKEN
    bne preview_no_release
    ldb #1
    jsr pool_release
preview_no_release:
    jsr pool_acquire
    bcc preview_reused
    ldb #255
preview_reused:
    stb preview+4
    tfr cc,a
    anda #1
    sta preview+8
    ldb #255
    jsr pool_release
    tfr cc,a
    anda #1
    sta preview+5
    ldx #POOL_LIFE
    clrb
preview_life:
    tfr b,a
    anda #1
    inca
    sta ,x+
    incb
    cmpb #POOL_CAPACITY
    bne preview_life
    jsr pool_age
    jsr occupancy
    sta preview+6
    jsr pool_age
    jsr occupancy
    sta preview+7
    rts

The default pool has eight slots; custom.asm uses four. Both reuse slot one after its release. The supplied occupancy display is an eight-bit diagnostic, so that client needs capacity three through eight. The build rejects an unsuitable capacity for that display instead of silently truncating it. Separate native boundary callers exercise capacity one and 255, with complete flag arrays. The helper’s wider contract does not make the small display wider.

Expire objects without moving them

Optionally include age.inc and reserve a separate lifetime byte for each slot. Before the first age call, every occupied slot must have a lifetime from one through 255. One pool_age call is one chosen simulation step. It decrements occupied lifetimes and releases those reaching zero, keeping every other object at the same index.

Optional fixed-slot lifetime policy

age.inc
; Optional policy: occupied lifetimes are1..255; no spawning during pass.
; A/B/X/CC clobbered, Y/U/DP preserved. Nested release uses stack.
pool_age:
    clrb
pool_age_loop:
    ldx #POOL_FLAGS
    abx
    tst ,x
    beq pool_age_next
    ldx #POOL_LIFE
    abx
    dec ,x
    bne pool_age_next
    jsr pool_release
pool_age_next:
    incb
    cmpb #POOL_CAPACITY
    bne pool_age_loop
    rts
pool_age_code_end:

The example assigns alternating lifetimes one and two. After one step the odd slots remain; after the next, none remain. The maximum-capacity native caller also checks all 255 lifetimes changing from 255 to 254, then from one to zero with every slot released.

The caller owns the clock. An untimed game can omit this policy, and a timed-turn game can invoke it at its chosen boundary. Queue new spawns until after the pass: otherwise whether a new object ages immediately would depend on which slot it received. Passing zero as a live lifetime violates this policy’s contract and can wrap the decrement.

Keep one owner

The routines use ordinary call/return stack space and no device I/O. They are not re-entrant. Do not let an interrupt or another owner mutate the same flags during a traversal. Optional expiry adds a nested release call, and the Z80 policy saves a loop counter on the stack.

A released index can later name a different object. Long-lived references need a generation or another identity policy; a bare index does not solve that problem. Multiple pools need separate storage and renamed labels, or a deliberately designed context-pointer interface. Compacting objects is another approach with different reference and movement costs.

Mistake and repair

The wrong caller omits the release before trying to reuse slot one. The pool remains full. It returns carry set, and the caller records 255 as its diagnostic failure marker instead of a usable slot. Both saved reuse-index and success checks fail. Check the status and release ownership at the right boundary; do not treat stale payload bytes as spare capacity. Repaired source and emitted machine code match normal exactly.

Dragon 32 shows both reuse checks failing after the omitted release.
The caller reaches completion, but the still-full pool correctly refuses another allocation.

The other native callers check full pools, invalid and repeated releases, unchanged guards, preserved context and all 256 ownership masks for the eight- and four-slot configurations. Capacity-255 tests include the high-bit indices and the full result 255; that result must never be used as a table index.

Use the full byte index on the 6809

This port walks the flag table with X and adds an unsigned index with ABX for release and lifetime access. Accumulator-offset B,X uses a signed offset; replacing ABX with that form would send indices above 127 backwards. The capacity-255 native program exercises 127, 128 and 254 explicitly, alongside slot zero and a full pool.

Motorola documents unsigned ABX in the 1981 programming manual, Appendix A, ABX, and signed accumulator-offset indexing in the MC6809 data sheet, addressing modes. This is a concrete reason to preserve the CPU-specific implementation even when the game-facing idea is shared.

Machine layout and measured cost

The Dragon loader reserves memory with CLEAR 200,&H5FFF and installs code at $6000. Flags begin at $6800, lifetimes at $6A00 and the observation buffer at $6C00. The entry wrapper saves CC/DP/D/X/Y/U and returns through EXEC. The observer waits for this firmware’s editor and checks the exact stored BASIC line inventory before RUN.

These are explicit layouts for the named stock-firmware verification, not a general-purpose memory allocator. The complete diagnostic buffer reserves 4 KiB; it is not required pool state. A game needs capacity flag bytes and, if using expiry, capacity lifetime bytes, plus its own payload. The helper emits 57 bytes, with 26 additional bytes for expiry, on this CPU/tool configuration. Those sizes exclude the loader, exhaustive caller, observation buffers, BASIC runtime and payloads.

Initialisation, acquisition and a full expiry pass visit at most capacity slots. Release checks and changes at most one flag. A bitset can reduce flag storage at the cost of mask work. A free-list can avoid a scan but needs links and an explicit free-order policy. No measured cycle budget, original-hardware result or whole-game speed claim follows from these bounded examples.

Native evidence and Maths for Games

All six programs load through the stock BASIC interpreter’s generated READ/POKE source, with every actual machine-code byte compared before entry. Normal, custom, wrong, exact repair and the two capacity-boundary programs are independent runs. Full ownership/lifetime packets, actual emitted extent, completion, guards, register/context checks, BASIC arithmetic after return and independent screenshot indicators are required. Captures follow two complete frames after completion.

The set model chooses the lowest member of the free set and compares the complete ownership result. Public CI rechecks retained records and rejects altered source, producer, program, state, completion and coupled wrong captures; it does not execute restricted firmware. The retained cases name Emu198x 0.44.2 and Asm198x 0.0.72. Publishing separately checks the existing assembler pin. Host checks complement the native configurations; neither is claimed as every possible capacity, firmware or timing condition.

Run make with ASM198X pointing to the assembler. Each build produces the flat image and its generated stock loader under build/. Follow that loader’s reservation and wait for the editor before its CALL, EXEC or USR entry. The site includes maintained source and pins the complete repository; these payloads use the existing source-delivery path. A game normally includes the helper in its own assembly project and supplies its own layout.

Maths for Games connects finite sets, lowest-free selection, bounds and stable indices. The Craft connects object lifetime, spawn limits and feedback when no slot is available.

Complete source

Source, stock loaders and native evidence.