Skip to content

Resource Counters on Amiga

An empty magazine should refuse a shot, not turn into 255 rounds.

An empty magazine should refuse a shot, not turn into 255 rounds. A full energy meter should stay full when you collect another recharge. This small counter gives those two operations different rules: refill saturates at 255; spending succeeds in full or leaves the balance alone.

Amiga shows the working result 05 06 01 00 00 00 00 00.
The completed native caller shows hexadecimal state bytes. The field order below explains the result; the full verification packet retains every call.

Include, initialise and call

Bind BALANCE, AMOUNT, MODE, OK as 4 contiguous private bytes in that order. Include the helper once. Call resource_wallet_init at the deliberate reset boundary: it clears all named bytes, including inputs. Set your starting values afterwards. Call resource_wallet_step when this service is needed.

Initialise once, then set the starting balance. Try a spend before creating the effect; consume OK immediately. A rejected request leaves all the resource available for a later affordable action. The helper does not spawn a projectile, reserve an object slot or coordinate another writer.

Set BALANCE to the current unsigned amount, AMOUNT to the requested change and MODE to 0 for refill or 1 for spend. Call step, then read OK: 1 means the operation was accepted, 0 means an unaffordable spend or an unknown mode. A zero-amount refill or spend is valid and succeeds. Other MODE values leave BALANCE alone and clear OK. AMOUNT and MODE survive.

Reusable initialisation and service

component.inc
; SPDX-License-Identifier: MIT
; Resource Counters. Bind these contiguous private bytes: BALANCE, AMOUNT, MODE, OK.
; init clears the whole block; step uses current inputs. No I/O, waits or IRQ owner.
; D0-D3/A0-A1/CCR may change; D4-D7/A2-A6 survive.
resource_wallet_init:
    clr.b BALANCE
    clr.b AMOUNT
    clr.b MODE
    clr.b OK
    rts
resource_wallet_step:
    clr.b OK
    move.b MODE,d0
    beq.s wallet_add
    cmp.b #1,d0
    bne.s wallet_done
    move.b BALANCE,d0
    cmp.b AMOUNT,d0
    blo.s wallet_done
    sub.b AMOUNT,d0
    bra.s wallet_store
wallet_add:
    move.b BALANCE,d0
    add.b AMOUNT,d0
    bcc.s wallet_store
    move.b #255,d0
wallet_store:
    move.b d0,BALANCE
    move.b #1,OK
wallet_done:
    rts
component_end:

State and ownership

BALANCE is persistent resource state; AMOUNT and MODE are caller inputs; OK is replaced on every call.

D0–D3/A0–A1/CCR may change; D4–D7/A2–A6 survive. A direct BSR/JSR uses four return-address stack bytes. The program is a 68000 implementation with no division instruction requirement.

This implementation has fixed symbol bindings and one mutation owner. Independent users need distinct state blocks and scoped/renamed bindings; including another copy with the same global labels is not sufficient. Sequential queries may reuse a scratch block only after the previous result has been consumed. Do not let an interrupt and the main loop mutate the same block without a separately designed ownership policy.

The standalone boot program takes over Copper/bitplane/interrupt/DMA resources, explicitly disarms all eight sprites, and does not restore Workbench. Those are diagnostic presentation choices; the helper itself owns no chipset registers.

A small caller you can adapt

The standalone program includes the helper and a separate client. Its read-only table exercises 96 valid inputs, then this client initialises once and retains state between calls. Replace the supplied input changes with your own game values; the display and packet machinery are diagnostics rather than dependencies of the component.

Buildable caller, bindings and include order

demo.asm
    section code,code_c
CUSTOM = 0
BROKEN = 0
SCORE_CAP = 65535
QUEUE_CAP = 4
REPEAT_DELAY = 4
REPEAT_RATE = 2
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "cases.inc"
    include "display.inc"
    include "../turn-deadline/font.inc"
code_end:
STATE:
BALANCE: dc.b 0
AMOUNT: dc.b 0
MODE: dc.b 0
OK: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
    even
signature: dc.b "198XGAMEPLAY000",0
    even
copperlist:
    dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
    dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
    dc.w $180,$012,$182,$fff
copbpl:
    dc.w $0e0,0,$0e2,0
    dc.w $ffff,$fffe
plane: dcb.b 10240,0

A separate client retaining state between calls

exercise.inc, excerpt
preview_rules:
    ifne CUSTOM
    move.b #3,BALANCE
    move.b #2,AMOUNT
    move.b #1,MODE
    ifne BROKEN
    move.b #0,MODE
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    move.b #255,AMOUNT
    move.b #0,MODE
    ifne BROKEN
    move.b #0,MODE
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    move.b #250,AMOUNT
    move.b #1,MODE
    ifne BROKEN
    move.b #0,MODE
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    move.b #6,AMOUNT
    move.b #1,MODE
    ifne BROKEN
    move.b #0,MODE
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    else
    move.b #3,BALANCE
    move.b #5,AMOUNT
    move.b #1,MODE
    ifne BROKEN
    move.b #0,MODE
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    move.b #255,AMOUNT
    move.b #0,MODE
    ifne BROKEN
    move.b #0,MODE
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    move.b #250,AMOUNT
    move.b #1,MODE
    ifne BROKEN
    move.b #0,MODE
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    move.b #6,AMOUNT
    move.b #1,MODE
    ifne BROKEN
    move.b #0,MODE
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    endif
    rts

The eight display fields follow BALANCE, AMOUNT, MODE, OK; unused display fields are zero. Each field is a hexadecimal byte. The working row is 05 06 01 00 00 00 00 00. The output is a completed diagnostic state, not a recording of movement, input or sound hardware.

Make it your own

The custom caller changes the first spend from five units to two. It succeeds from a balance of three; the later refill still saturates and the later unaffordable spend is still rejected.

Maintained custom configuration or caller

custom.asm
    section code,code_c
CUSTOM = 1
BROKEN = 0
SCORE_CAP = 999
QUEUE_CAP = 2
REPEAT_DELAY = 6
REPEAT_RATE = 3
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "cases.inc"
    include "display.inc"
    include "../turn-deadline/font.inc"
code_end:
STATE:
BALANCE: dc.b 0
AMOUNT: dc.b 0
MODE: dc.b 0
OK: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
    even
signature: dc.b "198XGAMEPLAY000",0
    even
copperlist:
    dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
    dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
    dc.w $180,$012,$182,$fff
copbpl:
    dc.w $0e0,0,$0e2,0
    dc.w $ffff,$fffe
plane: dcb.b 10240,0

The observed custom row is 05 06 01 00 00 00 00 00. The retained records qualify these named configurations; a new input domain or larger layout needs its own bounds and fresh execution checks.

Common mistake and repair

The wrong client labels every request as a refill by replacing MODE with zero. Spending now increases the balance, so an emptying sequence ends at 255. Preserve the requested operation and let the affordability check decide whether to spend.

Deliberately wrong client selection

wrong.asm
    section code,code_c
CUSTOM = 0
BROKEN = 1
SCORE_CAP = 65535
QUEUE_CAP = 4
REPEAT_DELAY = 4
REPEAT_RATE = 2
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "cases.inc"
    include "display.inc"
    include "../turn-deadline/font.inc"
code_end:
STATE:
BALANCE: dc.b 0
AMOUNT: dc.b 0
MODE: dc.b 0
OK: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
    even
signature: dc.b "198XGAMEPLAY000",0
    even
copperlist:
    dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
    dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
    dc.w $180,$012,$182,$fff
copbpl:
    dc.w $0e0,0,$0e2,0
    dc.w $ffff,$fffe
plane: dcb.b 10240,0
Amiga shows the deliberate fault result FF 06 00 01 00 00 00 00.
The wrong client completes and records FF 06 00 01 00 00 00 00. Compare its state with the working row and the explained failure.

The wrong branch is in the displayed client above, selected by BROKEN. repaired.asm selects the original client again. Its executable matches demo exactly; Spectrum tape filenames can differ while loaded CODE is identical. The bad caller is a labelled experiment, not an alternative recommended implementation.

Cost and verification

The measured helper occupies 90 code bytes plus 4 named state bytes in this layout. Return addresses, alignment, tables and display diagnostics are additional. The sizes cover init and step together, not the whole standalone program.

Refill and spend take bounded branch paths, with no search or loop. Two output bytes can change. A simple decrement is smaller when running out is impossible, but cannot enforce affordability.

For a smaller capacity, clamp refills to an explicit capacity instead of 255 and qualify both the new cap and the initial-balance invariant. Multiple resource types need separate state. Transactions involving both ammo and object slots need an explicit reservation/commit policy; OK is not a lock.

Four native runs use A500 OCS PAL with Kickstart 1.3 and an OFS boot disk; Asm198x 0.0.72 and Emu198x 0.44.2, with Build198x 0.2.9 for disk mastering. Every run checks 96 table results, dirty initialisation and every state in a separate sequential trace against an independent integer model. This is boundary/varied-input qualification, not exhaustive coverage of every byte combination. Source/tool/firmware hashes, native startup and completion, preserved-register sentinels, immutable loaded code, actual helper extent, capture bytes and pixels, and exact repair are checked. Deliberately changed observations must fail. Firmware is hashed, never distributed.

Related reusable technique · Maths for Games · The Craft.

Source and native evidence.