
Space spends one turn. Wait and nothing changes. T enables a deadline; after 240 BIOS ticks (about four seconds on the named NTSC configuration), a turn arrives automatically. T disables or starts a fresh duration; Escape returns to BASIC after release. The three hexadecimal bytes show turn, expiry count and enabled flag.
Build, load and reserve
Run make -C msx/patterns/assembly/framework/turn-deadline ASM=/path/to/asm198x. Enter the generated build/load.bas into the recorded stock MSX1 BIOS/BASIC. CLEAR 200,49151 reserves the upper area before READ/POKE writes the actual emitted bytes. RUN defines USR at $C000; allow the loader to finish, then SCREEN 0:WIDTH 40:CLS:A=USR(0) launches it. The verifier requires exact loaded-byte equality before entry. This is a keyboard loader, not cassette transfer.
This qualification uses a 64 KiB MSX1 with its normal BIOS/BASIC slots and default NTSC VDP timing. The 361-byte image occupies $C000–$C168, including 22 data bytes: four game/input/reserved bytes, ten deadline bytes and eight display-buffer bytes. Keep that area out of BASIC and other owners. The native arithmetic fixture is 519 bytes. The post-return observer additionally borrows $D000–$D3C0 for actual VPEEK text cells and a completion marker. It is diagnostic only, inside the CLEAR reservation; no game routine needs it. No slot, mapper, interrupt vector or private stack is installed.
Take the shared helper
demo.asm
; SPDX-License-Identifier: MIT
org $C000
DURATION equ 240
include "caller.inc"
include "clock.inc"
include "input.inc"
include "../../../../../cross-platform/patterns/framework/elapsed-deadline/z80.inc"
include "dispatch.inc"
TURN: db 0
EXPIRIES: db 0
KEY: db 0
UNUSED: db 0
DEADLINE: defs 10,0
DISPLAY: db "00 00 00"
code_end:z80.inc
; DEADLINE owns 10 bytes. HL=low 16 clock bits. AF/DE/HL clobbered.
; Poll at least once per 65536 clock ticks. BC/IX/IY preserved.
deadline_init:
ld (DEADLINE+8),hl
ld hl,0
ld (DEADLINE+2),hl
ld (DEADLINE+6),hl
xor a
ld (DEADLINE+4),a
ld (DEADLINE+5),a
ret
deadline_start:
ld hl,(DEADLINE)
ld (DEADLINE+2),hl
ld a,h
or l
jr z,deadline_off
ld a,1
deadline_off:
ld (DEADLINE+4),a
xor a
ld (DEADLINE+5),a
ret
deadline_poll:
ld de,(DEADLINE+8)
ld (DEADLINE+8),hl
or a
sbc hl,de
ld (DEADLINE+6),hl
xor a
ld (DEADLINE+5),a
ld a,(DEADLINE+4)
or a
ret z
ld hl,(DEADLINE+2)
ld de,(DEADLINE+6)
or a
sbc hl,de
jr c,deadline_expired
ld a,h
or l
jr z,deadline_expired
ld (DEADLINE+2),hl
ret
deadline_expired:
ld hl,0
ld (DEADLINE+2),hl
xor a
ld (DEADLINE+4),a
inc a
ld (DEADLINE+5),a
retInclude cross-platform/patterns/framework/elapsed-deadline/z80.inc once and supply DEADLINE: ten writable bytes. Little-endian words: duration +0, remaining +2, active +4, one-poll expiry +5, elapsed +6, previous +8.
Set duration to unsigned 0..65535; pass a clock sample in HL and call deadline_init once. It records the baseline, preserves duration and starts disabled. deadline_start copies duration into remaining and enables when nonzero, clearing the event without taking a new sample. Poll immediately before starting if the exact baseline matters. deadline_poll takes the next clock in HL, including while disabled. It updates the baseline, clears the old event and subtracts elapsed time from an active countdown. Expiry clamps remaining to zero, disables active and sets +5. Consume the event before the next poll clears it.
The helper clobbers AF/DE/HL, preserves BC/IX/IY, uses the caller’s return stack and performs no I/O or interrupt manipulation. One labelled context has one owner; this is not a re-entrant or implicit multi-timer interface.
Unsigned subtraction wraps modulo 65536: 65526 → 10 is twenty ticks. Poll before a full cycle passes (roughly 18 minutes 12 seconds at 60 Hz). An extra missed cycle looks identical. Zero duration means off. Reinitialise after a global clock reset; it is not a wrap. Late observation gives one event without catch-up; restarting at observation time introduces drift. Units, ranges and wrap connect directly to Maths for Games.
Machine adapters and policy
clock.inc
; JIFFY is owned by the normal BIOS VBlank ISR. HL out, no flags changed.
; One Z80 instruction loads the word; maskable IRQs occur between instructions.
clock_read:
ld hl,($FC9E)
retinput.inc
; Mainline, normal BIOS mapping, sole consumer of buffered characters.
; CHSNS says Z=empty; CHGET is called only after availability was established.
; Return A: 0 none/ignored, 1 Space, 2 T, 3 Escape. BIOS registers clobbered.
read_command:
call $009C
jr z,input_none
call $009F
cp 32
jr z,input_action
cp 'T'
jr z,input_toggle
cp 't'
jr z,input_toggle
cp 27
jr z,input_quit
input_none:
xor a
ret
input_action:
ld a,1
ret
input_toggle:
ld a,2
ret
input_quit:
ld a,3
retdispatch.inc
; Fixed labelled game state. AF/DE/HL clobbered; BC/IX/IY preserved.
dispatch:
ld a,(DEADLINE+5)
or a
jr z,dispatch_key
xor a
ld (KEY),a
ld hl,EXPIRIES
inc (hl)
ld hl,TURN
inc (hl)
jp deadline_start
dispatch_key:
ld a,(KEY)
cp 2
jr nz,dispatch_action
ld a,(DEADLINE+4)
or a
jp z,deadline_start
xor a
ld (DEADLINE+4),a
ld hl,0
ld (DEADLINE+2),hl
ret
dispatch_action:
cp 1
ret nz
ld hl,TURN
inc (hl)
ld a,(DEADLINE+4)
or a
ret z
jp deadline_startcaller.inc
; BASIC USR entry, normal BIOS slots and firmware IRQs retained.
start:
push af
push bc
push de
push hl
push ix
push iy
xor a
ld (TURN),a
ld (EXPIRIES),a
ld hl,DURATION
ld (DEADLINE),hl
call clock_read
call deadline_init
main:
call clock_read
call deadline_poll
call read_command
ld (KEY),a
call dispatch
call show_state
ld a,(KEY)
cp 3
jr nz,main
quit_release:
ld a,7
call $0141
bit 2,a
jr z,quit_release
call $0156
pop iy
pop ix
pop hl
pop de
pop bc
pop af
ret
show_state:
ld a,(TURN)
ld de,DISPLAY
call show_byte
ld a,(EXPIRIES)
ld de,DISPLAY+3
call show_byte
ld a,(DEADLINE+4)
ld de,DISPLAY+6
call show_byte
ld hl,DISPLAY
ld de,0
ld bc,8
call $005C
ret
show_byte:
push af
rrca
rrca
rrca
rrca
and 15
call hex_digit
ld (de),a
inc de
pop af
and 15
call hex_digit
ld (de),a
ret
hex_digit:
cp 10
jr c,hex_number
add a,7
hex_number:
add a,48
retclock.inc reads BIOS JIFFY at $FC9E–$FC9F into HL. One Z80 LD instruction reads the word; maskable IRQs occur between instructions. Other registers/flags and interrupt state are unchanged. Keep the normal VBlank interrupt service running; it supplies the clock and keyboard scan. The default NTSC configuration counts about sixty ticks per second; PAL timing needs a different seconds conversion. Never reset TIME/JIFFY behind an active context.
input.inc calls CHSNS ($009C) and calls CHGET ($009F) only after a character is available. It returns A=0 for none/ignored, 1 for Space, 2 for T/t and 3 for Escape. Treat BIOS/main registers as clobbered. This must be the sole mainline consumer of that buffer; availability followed by a blocking reader is safe only while another owner cannot drain it in between. Delivered characters can include typeahead and repeat. This is not physical press detection or simultaneous-key scanning. The native taps span ten fields so the firmware scanner can observe them; a very short press may be missed.
dispatch.inc handles expiry first and discards that iteration’s command, including Escape/T. Otherwise Space advances once and restarts only an already active deadline. Untimed play stays untimed. T discards remaining on disable and starts a full duration on enable. Byte turn/expiry counters wrap. Replace their effects with your own rules; pause preservation, queued commands, carried lateness and multiple contexts are deliberate extensions with different costs.
Presentation formats the three status bytes into DISPLAY, then uses BIOS LDIRVM ($005C) to copy eight cells to VRAM address zero in SCREEN 0, width 40. Other modes/table bases need a different adapter. Treat main registers/flags as clobbered; the whole USR session saves/restores AF/BC/DE/HL/IX/IY on BASIC’s existing balanced stack. Escape release is checked with SNSMAT ($0141), row 7 bit 2; KILBUF ($0156) clears remaining firmware repeats before RET. BIOS and normal IRQ workspace remain firmware-owned.
Customise and diagnose
customise.asm
; SPDX-License-Identifier: MIT
org $C000
DURATION equ 120
include "caller.inc"
include "clock.inc"
include "input.inc"
include "../../../../../cross-platform/patterns/framework/elapsed-deadline/z80.inc"
include "dispatch.inc"
TURN: db 0
EXPIRIES: db 0
KEY: db 0
UNUSED: db 0
DEADLINE: defs 10,0
DISPLAY: db "00 00 00"
code_end:Deliberately broken: reset-baseline.inc
; JIFFY is owned by the normal BIOS VBlank ISR. HL out, no flags changed.
; One Z80 instruction loads the word; maskable IRQs occur between instructions.
clock_read:
ld hl,($FC9E)
ld (DEADLINE+8),hl
retcustomise.asm changes DURATION from 240 to 120: roughly four seconds becomes two in this configuration, with identical code/state size.
mistakes/reset-baseline.asm deliberately chooses a clock adapter that overwrites previous before polling. The clock advances, but elapsed always becomes zero, remaining stays at 240 and the automatic turn never arrives. The faulty screen stays at 01 00 01 rather than reaching 03 01 01. Restore clock.inc and rebuild; the separate fresh repaired run verifies the fix. The faulty adapter adds three bytes. Keep it out of working builds.

Cost and qualification
The shared helper emits 97 bytes plus its ten-byte context. The clock adapter emits four bytes. The complete caller is 361 bytes including its 22 data bytes. Calls use the existing BASIC stack plus saved registers, BIOS and IRQ needs; no exhaustive peak-stack or cycle count is claimed. Drawing, BIOS work and your own game determine polling latency. This is continuous feedback, not frame pacing.
Emu198x 0.44.2, Asm198x 0.0.72 and the hashed stock BIOS/BASIC. Six independent loaded callers cover normal, half duration, wrong baseline, repair, twelve synthetic arithmetic pairs/three policy cases and a wrong-equality control. Loaded bytes must match assembly before USR. The wrong equality mutation completes but fails the unsigned oracle. Supplied samples do not claim an observed 18-minute rollover.
After quit, BASIC computes 2+3. A guest VPEEK observer copies the actual name table and writes its completion marker before the host accepts that return check. Captures, source/tool/firmware hashes, loaded bytes and state samples are retained. Run scripts/verify-msx-assembly-deadlines.py with released tools and your own firmware; run python3 scripts/check-msx-assembly-deadlines.py and python3 -m unittest discover -s scripts -p test_msx_assembly_deadlines.py for retained checks. Ten deliberately corrupted records must fail. This is configuration-specific execution, not original hardware, every BIOS or whole-emulator qualification.
Primary references: MSX BIOS source listing, fixed entries CHSNS/CHGET, LDIRVM, SNSMAT and KILBUF; TIMI/JIFFY service and BASIC TIME workspace. The adapters and policy are this implementation, not an assertion that a manufacturer supplied this exact game loop. See the existing MSX command-loop entry for the independent untimed BIOS route.
Maintained source and related patterns
Maintained source · Native execution record · Untimed command loop · Choosing progression and clocks