Skip to content

Jump Buffering and Coyote Time on Amiga

Press Jump just before landing and it is frustrating when the press disappears.

Press Jump just before landing and it is frustrating when the press disappears. Walk a tiny distance off a ledge and a short forgiveness window can still let the jump happen. These two timers give the game those choices without hiding the rules inside the input reader.

Amiga shows the working diagnostic row 00 04 00 01 00 00 00 00.
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 JUMPED, COYOTE, BUFFER, GROUNDED, PRESSED as 5 contiguous private bytes in that order. Include the helper once. Call jump_windows_init at the intended reset boundary: it clears every named byte, including input fields. Set the starting values afterwards, then call jump_windows_step for each intended service.

Initialise at the session/reset boundary. Update grounded state from collision, supply the new-press bit, then service once per simulation update. On JUMPED, the caller applies the chosen vertical velocity and updates its movement state. The helper grants a jump; it does not move the player or decide whether a floor contact is real.

GROUNDED and PRESSED are caller inputs; any nonzero value is true, and PRESSED must mean a new press. Each step first decrements positive COYOTE and BUFFER timers, then reloads COYOTE from COYOTE_STEPS when grounded and BUFFER from BUFFER_STEPS on a press. If both timers are positive, it emits JUMPED = 1 and consumes both timers; otherwise JUMPED is zero. Both configured windows are 1–255 service calls. The maintained builds use four and two.

Reusable initialisation and service

component.inc
; SPDX-License-Identifier: MIT
; Jump Buffering and Coyote Time. Contiguous private bytes: JUMPED, COYOTE, BUFFER, GROUNDED, PRESSED.
; 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.
jump_windows_init:
    clr.b JUMPED
    clr.b COYOTE
    clr.b BUFFER
    clr.b GROUNDED
    clr.b PRESSED
    rts
jump_windows_step:
    clr.b JUMPED
    tst.b COYOTE
    beq.s jump_buffer
    subq.b #1,COYOTE
jump_buffer:
    tst.b BUFFER
    beq.s jump_ground
    subq.b #1,BUFFER
jump_ground:
    tst.b GROUNDED
    beq.s jump_press
    move.b #COYOTE_STEPS,COYOTE
jump_press:
    tst.b PRESSED
    beq.s jump_test
    move.b #BUFFER_STEPS,BUFFER
jump_test:
    tst.b COYOTE
    beq.s jump_done
    tst.b BUFFER
    beq.s jump_done
    move.b #1,JUMPED
    clr.b COYOTE
    clr.b BUFFER
jump_done:
    rts
component_end:

State, registers and ownership

Five bytes: JUMPED is a replaced pulse, COYOTE and BUFFER persist, GROUNDED and PRESSED are inputs. A timer of one expires at the start of this call unless its corresponding current input reloads it. A stationary grounded player with no buffered or new press does not jump.

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 jump_windows_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:
JUMPED: dc.b 0
COYOTE: dc.b 0
BUFFER: dc.b 0
GROUNDED: dc.b 0
PRESSED: 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 #1,PRESSED
    ifne BROKEN
    move.b #1,PRESSED
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    move.b #0,PRESSED
    ifne BROKEN
    move.b #1,PRESSED
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    ifne BROKEN
    move.b #1,PRESSED
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    move.b #1,GROUNDED
    ifne BROKEN
    move.b #1,PRESSED
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    ifne BROKEN
    move.b #1,PRESSED
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    else
    move.b #1,PRESSED
    ifne BROKEN
    move.b #1,PRESSED
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    move.b #0,PRESSED
    ifne BROKEN
    move.b #1,PRESSED
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    ifne BROKEN
    move.b #1,PRESSED
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    move.b #1,GROUNDED
    ifne BROKEN
    move.b #1,PRESSED
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    ifne BROKEN
    move.b #1,PRESSED
    bsr checked_step
    else
    bsr checked_step
    endif
    bsr emit_state
    endif
    rts

The display fields are JUMPED, COYOTE, BUFFER, GROUNDED, PRESSED; remaining display fields are zero. Each is one hexadecimal byte. The normal row is 00 04 00 01 00 00 00 00. 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 build shortens both windows from four calls to two. In the supplied trace, a press three calls before landing is accepted with the four-call buffer and has expired with the two-call buffer. The final row alone does not count earlier jump pulses; the complete packet retains every call.

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:
JUMPED: dc.b 0
COYOTE: dc.b 0
BUFFER: dc.b 0
GROUNDED: dc.b 0
PRESSED: 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 02 00 01 00 00 00 00. 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 supplies a held level as PRESSED on every call. The permission can be reissued on successive grounded updates even though the player never made another press. Feed a new-press edge and retain the timers; also update the caller’s grounded/movement state when it accepts a jump.

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:
JUMPED: dc.b 0
COYOTE: dc.b 0
BUFFER: dc.b 0
GROUNDED: dc.b 0
PRESSED: 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 01 00 00 01 01 00 00 00.
The wrong client completes with 01 00 00 01 01 00 00 00. 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 136 code bytes, 0 immutable data bytes and 5 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.

Two saturating decrements, input-dependent reloads and a pair of tests per update. There is no busy wait or keyboard scan. The same call-count settings represent different real durations under different service rates; choose a simulation clock before tuning them.

This is a logical permission service, not a complete platformer jump controller. Variable jump height, multiple air jumps, wall jumps and input consumption across menus need explicit additional policies. Freeze or advance the timers during pause deliberately. Respawn invulnerability is a different grace window and remains covered by damage/recovery.

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.