A quarter-pixel step should eventually move the sprite a whole pixel. Keep the fraction between updates and four small steps add up; throw it away each time and the sprite never gets there.

Include, initialise and call
Reserve POS_LO, POS_HI, VEL_LO, VEL_HI as 4 contiguous private bytes in that order. Include the helper once. Call fractional_position_init at the intended reset boundary: it clears every named byte, including input fields. Set the starting values afterwards, then call fractional_position_step for each intended service.
Initialise once, then set both position bytes and both velocity bytes. Call once per simulation update. Use POS_HI as the integer coordinate only where that coordinate range suits the renderer; retain POS_LO in the game state. The normal client adds 0.25 four times to position 100 and reaches exactly 101.
POS_LO/POS_HI hold an unsigned 8.8 position: the low byte counts 1/256 units and the high byte is the integer part. VEL_LO/VEL_HI hold a signed two’s-complement 8.8 displacement. Step adds the whole two-byte displacement to the whole position, preserving both velocity bytes. Position wraps modulo 65,536 raw units by design; this helper does not clamp a world boundary.
Reusable initialisation and service
; SPDX-License-Identifier: MIT
; Fractional Position Updates. Contiguous private bytes: POS_LO, POS_HI, VEL_LO, VEL_HI.
; init clears all fields; set inputs afterwards. No I/O, waits or IRQ ownership.
; AF/BC/DE/HL change; IX/IY survive.
fractional_position_init:
xor a
ld (POS_LO),a
ld (POS_HI),a
ld (VEL_LO),a
ld (VEL_HI),a
ret
fractional_position_step:
ld hl,(POS_LO)
ld de,(VEL_LO)
add hl,de
ld (POS_LO),hl
ret
component_end:State, registers and ownership
Four persistent/input bytes. A displacement of −0.5 is FF80 in hexadecimal, stored low byte 80 then high byte FF. One raw unit is 1/256 of your chosen world unit; these are units per service call, not per second.
AF/BC/DE/HL may change; IX/IY survive. A direct CALL needs two 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 standalone tape program enters at $8000, disables interrupts and owns bitmap/attribute RAM. It does not restore BASIC. Those choices belong to the test program; the helper itself never takes over the display.
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 fractional_position_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
org $8000
CUSTOM equ 0
BROKEN equ 0
COYOTE_STEPS equ 4
BUFFER_STEPS equ 4
TEXT_CAP equ 8
PROPERTY_COUNT equ 16
STREAM_BYTES equ 13
CRC_POLY equ 7
include "caller.inc"
include "exercise.inc"
component_start:
include "component.inc"
include "data.inc"
include "cases.inc"
include "../../physics/rectangle-overlap/display.inc"
code_end:
STATE:
POS_LO: defb 0
POS_HI: defb 0
VEL_LO: defb 0
VEL_HI: defb 0
input_ptr: defw 0
output_ptr: defw 0
cases_left: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1300,0
end startA separate client retaining state between calls
preview_rules:
if CUSTOM
ld a,0
ld (POS_LO),a
ld a,100
ld (POS_HI),a
ld a,128
ld (VEL_LO),a
ld a,0
ld (VEL_HI),a
if BROKEN
ld a,0
ld (POS_LO),a
call checked_step
else
call checked_step
endif
call emit_state
if BROKEN
ld a,0
ld (POS_LO),a
call checked_step
else
call checked_step
endif
call emit_state
if BROKEN
ld a,0
ld (POS_LO),a
call checked_step
else
call checked_step
endif
call emit_state
if BROKEN
ld a,0
ld (POS_LO),a
call checked_step
else
call checked_step
endif
call emit_state
else
ld a,0
ld (POS_LO),a
ld a,100
ld (POS_HI),a
ld a,64
ld (VEL_LO),a
ld a,0
ld (VEL_HI),a
if BROKEN
ld a,0
ld (POS_LO),a
call checked_step
else
call checked_step
endif
call emit_state
if BROKEN
ld a,0
ld (POS_LO),a
call checked_step
else
call checked_step
endif
call emit_state
if BROKEN
ld a,0
ld (POS_LO),a
call checked_step
else
call checked_step
endif
call emit_state
if BROKEN
ld a,0
ld (POS_LO),a
call checked_step
else
call checked_step
endif
call emit_state
endif
retThe display fields are POS_LO, POS_HI, VEL_LO, VEL_HI; remaining display fields are zero. Each is one hexadecimal byte. The normal row is 00 65 40 00 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 client uses 0.5 per update instead of 0.25. Four calls reach 102. The representation and helper code are unchanged.
Maintained custom configuration or caller
org $8000
CUSTOM equ 1
BROKEN equ 0
COYOTE_STEPS equ 2
BUFFER_STEPS equ 2
TEXT_CAP equ 4
PROPERTY_COUNT equ 16
STREAM_BYTES equ 13
CRC_POLY equ 49
include "caller.inc"
include "exercise.inc"
component_start:
include "component.inc"
include "data.inc"
include "cases.inc"
include "../../physics/rectangle-overlap/display.inc"
code_end:
STATE:
POS_LO: defb 0
POS_HI: defb 0
VEL_LO: defb 0
VEL_HI: defb 0
input_ptr: defw 0
output_ptr: defw 0
cases_left: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1300,0
end startThe custom row is 00 66 80 00 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 clears POS_LO before every call. The quarter-step is always restarted from a whole unit, so four calls leave position 100.25 instead of 101. Retain the fractional byte between calls; discarding it only for drawing is harmless, discarding it from state is not.
Select the deliberately wrong client
org $8000
CUSTOM equ 0
BROKEN equ 1
COYOTE_STEPS equ 4
BUFFER_STEPS equ 4
TEXT_CAP equ 8
PROPERTY_COUNT equ 16
STREAM_BYTES equ 13
CRC_POLY equ 7
include "caller.inc"
include "exercise.inc"
component_start:
include "component.inc"
include "data.inc"
include "cases.inc"
include "../../physics/rectangle-overlap/display.inc"
code_end:
STATE:
POS_LO: defb 0
POS_HI: defb 0
VEL_LO: defb 0
VEL_HI: defb 0
input_ptr: defw 0
output_ptr: defw 0
cases_left: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1300,0
end start
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 26 code bytes, 0 immutable data bytes and 4 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.
One two-byte addition, with no rendering or wait. The 6502 carries from the low byte; Z80 adds a word; 68000 packs the byte binding into a word. Integer movement is smaller when fractional motion is unnecessary, but cannot accumulate these sub-unit steps.
An 8.8 unsigned position covers 0–255.996 units. C64 and Amiga displays can extend beyond 255 horizontal pixels, so do not mistake this for a complete screen-coordinate format. Wider worlds need a wider integer part. The Amiga Flock lessons already use 16.16 positions; that is a useful wider alternative rather than a reason to squeeze those games into this byte-oriented interface. Clamp or wrap at the game’s actual bounds as a separate policy.
Four native runs use Spectrum 48K with stock ROM tape loading and USR entry; Asm198x 0.0.72 and Emu198x 0.44.2. 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.