Skip to content

Bounded Signed Acceleration on Amiga

Gravity should keep adding speed until the chosen terminal velocity, not turn a fast fall into an upward launch when an integer overflows.

Gravity should keep adding speed until the chosen terminal velocity, not turn a fast fall into an upward launch when an integer overflows. This helper adds signed acceleration and clamps the result to an explicit symmetric speed limit.

Amiga shows the working diagnostic row 00 FE 80 FF 00 02 00 01.
A native completed caller shows its state as hexadecimal bytes. The field order below explains the row; the full packet retains every call.

Include, initialise and call

Reserve VEL_LO, VEL_HI, ACC_LO, ACC_HI, LIMIT_LO, LIMIT_HI, CLIPPED, VALID as 8 contiguous private bytes in that order. Include the helper once. Call bounded_acceleration_init at the intended reset boundary: it clears every named byte, including input fields. Set the starting values afterwards, then call bounded_acceleration_step for each intended service.

Initialise once, set the starting velocity, acceleration and limit, then service before the position update if that is your chosen integrator order. Treat that order as part of the game’s behaviour. The normal four-call client accelerates by −0.5 per call to a limit of 2.0 and finishes at −2.0.

VEL_LO/VEL_HI and ACC_LO/ACC_HI are signed two’s-complement 8.8 words, stored low byte first. LIMIT_LO/LIMIT_HI are a nonnegative raw magnitude from 0 to 32,767. Step clears CLIPPED and VALID, rejects a limit with its high bit set, and otherwise adds velocity and acceleration in a logically wider signed range before clamping to −LIMIT…+LIMIT. VALID becomes 1; CLIPPED becomes 1 only when the wide sum was outside that interval. An invalid limit preserves velocity and leaves both flags zero. Acceleration and limit bytes survive.

Reusable initialisation and service

component.inc
; SPDX-License-Identifier: MIT
; Bounded Signed Acceleration. Contiguous private bytes: VEL_LO, VEL_HI, ACC_LO, ACC_HI, LIMIT_LO, LIMIT_HI, CLIPPED, VALID.
; init clears all fields; set inputs afterwards. No I/O, waits or IRQ ownership.
; D0-D3/A0-A1/CCR change; D4-D7/A2-A6 survive.
bounded_acceleration_init:
    clr.b VEL_LO
    clr.b VEL_HI
    clr.b ACC_LO
    clr.b ACC_HI
    clr.b LIMIT_LO
    clr.b LIMIT_HI
    clr.b CLIPPED
    clr.b VALID
    rts
bounded_acceleration_step:
    clr.b CLIPPED
    clr.b VALID
    tst.b LIMIT_HI
    bpl.s accel_valid
    rts
accel_valid:
    move.b #1,VALID
    moveq #0,d0
    move.b VEL_HI,d0
    lsl.w #8,d0
    move.b VEL_LO,d0
    ext.l d0
    moveq #0,d1
    move.b ACC_HI,d1
    lsl.w #8,d1
    move.b ACC_LO,d1
    ext.l d1
    add.l d1,d0
    moveq #0,d2
    move.b LIMIT_HI,d2
    lsl.w #8,d2
    move.b LIMIT_LO,d2
    cmp.l d2,d0
    bgt.s accel_positive
    neg.l d2
    cmp.l d2,d0
    bge.s accel_store
    move.l d2,d0
    bra.s accel_clip
accel_positive:
    move.l d2,d0
accel_clip:
    move.b #1,CLIPPED
accel_store:
    move.b d0,VEL_LO
    lsr.w #8,d0
    move.b d0,VEL_HI
accel_done:
    rts
component_end:

State, registers and ownership

Eight bytes: two for velocity, two for acceleration, two for a magnitude limit, and two replaced status flags. The symmetric limit cannot be 32,768 because the positive signed endpoint cannot represent it. Initial velocity may be anywhere in the signed 16-bit range; a valid call clamps the resulting sum.

D0–D3/A0–A1/CCR may change; D4–D7/A2–A6 survive. A direct BSR/JSR needs four return-address stack bytes.

The implementation binds fixed symbols and has one mutation owner. Independent users need separate state blocks and scoped/renamed labels, including routine labels; sharing the same bindings means sharing the state. Do not let an interrupt and the main loop mutate the same block without an explicit ownership policy.

The bootable caller takes over Copper/bitplane/interrupt/DMA resources and explicitly disarms all eight sprites. It does not restore Workbench. These are caller presentation choices, not dependencies of the reusable helper.

A caller you can adapt

The buildable program supplies bindings and include order. Its separate client retains state between calls. checked_step calls the public bounded_acceleration_step entry and checks register sentinels; emit_state records diagnostics. Your game can call the public entry directly, remove the diagnostics and consume the documented outputs.

Bindings, buildable caller and include order

demo.asm
    section code,code_c
CUSTOM = 0
BROKEN = 0
COYOTE_STEPS = 4
BUFFER_STEPS = 4
TEXT_CAP = 8
PROPERTY_COUNT = 16
STREAM_BYTES = 13
CRC_POLY = 7
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "data.inc"
    include "cases.inc"
    even
    include "display.inc"
    include "../../framework/turn-deadline/font.inc"
code_end:
STATE:
VEL_LO: dc.b 0
VEL_HI: dc.b 0
ACC_LO: dc.b 0
ACC_HI: dc.b 0
LIMIT_LO: dc.b 0
LIMIT_HI: dc.b 0
CLIPPED: dc.b 0
VALID: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
    even
signature: dc.b "198XPLAYTOOLS00",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 #128,ACC_LO
    move.b #255,ACC_HI
    move.b #0,LIMIT_LO
    move.b #1,LIMIT_HI
    ifne BROKEN
    move.b #0,ACC_HI
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    ifne BROKEN
    move.b #0,ACC_HI
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    ifne BROKEN
    move.b #0,ACC_HI
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    ifne BROKEN
    move.b #0,ACC_HI
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    else
    move.b #128,ACC_LO
    move.b #255,ACC_HI
    move.b #0,LIMIT_LO
    move.b #2,LIMIT_HI
    ifne BROKEN
    move.b #0,ACC_HI
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    ifne BROKEN
    move.b #0,ACC_HI
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    ifne BROKEN
    move.b #0,ACC_HI
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    ifne BROKEN
    move.b #0,ACC_HI
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    endif
    rts

The display fields are VEL_LO, VEL_HI, ACC_LO, ACC_HI, LIMIT_LO, LIMIT_HI, CLIPPED, VALID. Each is one hexadecimal byte. The normal row is 00 FE 80 FF 00 02 00 01. This is a completed state observation, not a recording of an actual character jumping, a camera scrolling or an asset being displayed.

Make it your own

The custom client lowers the limit from 2.0 to 1.0. Continued acceleration now saturates at −1.0 and sets CLIPPED on the later calls. A zero limit is also a valid request to hold velocity at zero.

Maintained custom configuration or caller

custom.asm
    section code,code_c
CUSTOM = 1
BROKEN = 0
COYOTE_STEPS = 2
BUFFER_STEPS = 2
TEXT_CAP = 4
PROPERTY_COUNT = 16
STREAM_BYTES = 13
CRC_POLY = 49
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "data.inc"
    include "cases.inc"
    even
    include "display.inc"
    include "../../framework/turn-deadline/font.inc"
code_end:
STATE:
VEL_LO: dc.b 0
VEL_HI: dc.b 0
ACC_LO: dc.b 0
ACC_HI: dc.b 0
LIMIT_LO: dc.b 0
LIMIT_HI: dc.b 0
CLIPPED: dc.b 0
VALID: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
    even
signature: dc.b "198XPLAYTOOLS00",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 custom row is 00 FF 80 FF 00 01 01 01. These records qualify the named builds and inputs; another layout, format or input domain needs explicit bounds and fresh execution evidence.

Common mistake and repair

The wrong client clears ACC_HI. The intended FF80 displacement (−0.5) becomes 0080 (+0.5), so the object accelerates the wrong way. Supply the signed high byte rather than zero-extending a negative value.

Select the deliberately wrong client

wrong.asm
    section code,code_c
CUSTOM = 0
BROKEN = 1
COYOTE_STEPS = 4
BUFFER_STEPS = 4
TEXT_CAP = 8
PROPERTY_COUNT = 16
STREAM_BYTES = 13
CRC_POLY = 7
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "data.inc"
    include "cases.inc"
    even
    include "display.inc"
    include "../../framework/turn-deadline/font.inc"
code_end:
STATE:
VEL_LO: dc.b 0
VEL_HI: dc.b 0
ACC_LO: dc.b 0
ACC_HI: dc.b 0
LIMIT_LO: dc.b 0
LIMIT_HI: dc.b 0
CLIPPED: dc.b 0
VALID: dc.b 0
    even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
    even
signature: dc.b "198XPLAYTOOLS00",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 row 00 02 80 00 00 02 00 01.
The wrong client completes with 00 02 80 00 00 02 00 01. Its code and the explanation above identify the cause; the capture observes the result.

The BROKEN branch is visible in the client. repaired.asm restores the normal selection; its executable matches demo exactly. Spectrum tape filenames may differ while loaded CODE remains identical. The fault is a labelled experiment, not a suggested alternative.

Cost, limits and verification

The measured layout has 162 code bytes, 0 immutable data bytes and 8 private state bytes. Alignment, stack usage, diagnostic tables and display machinery are additional. The extents come from the loaded program’s symbols, not from counting source lines.

The 6502 examines signed overflow after the high-byte ADC; the Z80 uses ADC HL,DE because ADD HL,DE does not provide the same signed-overflow result; the 68000 sign-extends words into a long intermediate. Comparisons and clamping take bounded paths. No floating-point unit is used.

This models acceleration over fixed simulation updates. A variable time step needs explicit scaling, wider intermediates and a rounding policy. Drag, collision response and different upward/downward limits are separate rules. If parameters are prevalidated, an unchecked internal variant may save work, but it is a different contract and is not the qualified implementation here.

Four native runs use A500 OCS PAL with Kickstart 1.3 and an OFS cold-boot disk; Asm198x 0.0.72 and Emu198x 0.44.2, with Build198x 0.2.9 for disk mastering. Each checks 64 boundary/varied/error cases, dirty initialisation and every state of a separate sequential client against an independent integer model. This is not exhaustive coverage of every possible byte combination. Source/tool/firmware hashes, immutable loaded code/data, native startup/completion, preserved-register sentinels, helper/data sizes, screenshot bytes/pixels and exact repair are checked. Deliberately altered observations must fail. Firmware is hashed, never distributed.

Related technique · Maths for Games · The Craft.

Source and native evidence.