Joystick-one fire takes a turn. Hold it and the counter stays put; release before the next action. Waiting takes no turn until SELECT enables timing. After 240 OS clock ticks, about four seconds in the named NTSC configuration, an automatic turn arrives. SELECT disables timing or starts a fresh duration. OPTION returns to BASIC with USR result zero. The first eight screen cells show turn, expiry count and enabled flag in hexadecimal.

Build, reserve and load
Run make ASM198X=/path/to/asm198x here. Enter the complete generated build/load.bas in stock Atari BASIC, one line at a time. Its numbered program loads DATA bytes; RUN performs the POKEs, wait for the new READY prompt, then X=USR(32768) starts the caller. The final POKE can finish before NEXT/END returns to the editor; complete bytes alone do not prove readiness. The loader is generated from the actual assembled image, not a manually maintained byte list. No ROM is supplied.
The 424-byte image occupies $8000–$81A7. Caller-owned state occupies $8300–$8313: TURN, EXPIRIES, KEY, HELD; twelve countdown bytes; two ready-marker bytes; two guards. The assembler refuses code that reaches $8300, and the generated loader checks its payload budget too. Relocate code, data labels and reservation together. No DOS, cartridge or other routine may own this RAM concurrently.
The complete caller is larger than the 256-byte page-six area used by the earlier loop. Its loader instead sets RAMTOP (106) to 124, then selects GRAPHICS 0. BASIC and the display remain below $7C00; code starts at $8000. The 1 KiB gap matters: the reference documents clear/scroll operations reaching above RAMTOP. Do not place a routine directly at an assumed display ceiling. Native qualification checks actual MEMTOP and SAVMSC as well as RAMTOP. This is an explicit layout for the named 64 KiB machine, not a general allocator. It leaves the reservation in place after return; reset or deliberately restore your own layout when finished.
The complete caller
; SPDX-License-Identifier: MIT
* = $8000
DURATION = 240
!source "caller.inc"
!source "clock.inc"
!source "../../../../../cross-platform/patterns/framework/elapsed-deadline/6502.inc"
!source "dispatch.inc"
!source "input.inc"
code_end:
!if code_end > $8300 { !error "Code overlaps deadline context" }Include and call the helper
The maintained shared countdown
; DEADLINE owns 12 bytes. A=clock low, X=clock high; decimal mode clear.
; Poll at least once per 65536 clock ticks. A/X/status clobbered; Y preserved.
deadline_init:
sta DEADLINE+8
stx DEADLINE+9
lda #0
sta DEADLINE+2
sta DEADLINE+3
sta DEADLINE+4
sta DEADLINE+5
sta DEADLINE+6
sta DEADLINE+7
rts
deadline_start:
lda DEADLINE
sta DEADLINE+2
lda DEADLINE+1
sta DEADLINE+3
ora DEADLINE
beq deadline_off
lda #1
deadline_off:
sta DEADLINE+4
lda #0
sta DEADLINE+5
rts
deadline_poll:
sta DEADLINE+10
stx DEADLINE+11
sec
sbc DEADLINE+8
sta DEADLINE+6
txa
sbc DEADLINE+9
sta DEADLINE+7
lda DEADLINE+10
sta DEADLINE+8
lda DEADLINE+11
sta DEADLINE+9
lda #0
sta DEADLINE+5
lda DEADLINE+4
beq deadline_done
sec
lda DEADLINE+2
sbc DEADLINE+6
sta DEADLINE+2
lda DEADLINE+3
sbc DEADLINE+7
sta DEADLINE+3
bcc deadline_expired
ora DEADLINE+2
bne deadline_done
deadline_expired:
lda #0
sta DEADLINE+2
sta DEADLINE+3
sta DEADLINE+4
lda #1
sta DEADLINE+5
deadline_done:
rtsProvide DEADLINE for twelve private writable bytes and include cross-platform/patterns/framework/elapsed-deadline/6502.inc once. Its little-endian words are duration +0, remaining +2, active +4, one-poll event +5, elapsed +6, previous +8 and scratch +10.
- Set duration to integer ticks 0..65535. Pass A=clock low, X=clock high to deadline_init once. It records the baseline and starts disabled; duration survives.
- Call deadline_start to copy duration to remaining and enable only when nonzero. It clears the event but retains the last sampled baseline. Poll immediately before starting if that instant matters.
- Pass the next sample in A/X to deadline_poll on every service iteration, including while disabled. Read +5 before another poll/start clears it. Expiry clamps remaining to zero, disables active and emits once. The caller decides what that means.
A/X/status are clobbered; Y survives. Decimal mode must be clear. The helper owns no zero page, input, display or interrupt handler. Ordinary JSR/RTS uses the existing stack. A context belongs to one caller; it is not re-entrant. The label is an assembly-time allocation, not a runtime pointer parameter.
Elapsed subtraction wraps modulo 65536: 65526 → 10 is twenty ticks. Poll less than a complete cycle apart, roughly 18 minutes 12 seconds at nominal 60 Hz. A missed extra cycle is ambiguous. Reinitialise after an external clock reset. Zero duration means off. A late poll emits one event without catch-up; restarting from observation introduces drift. A full 24-bit clock/helper would allow longer intervals with additional arithmetic/state; it is not a drop-in claim for this implementation.
Sample the OS clock without stopping NMI
Read the VBI clock coherently
; RTCLOK $12..$14 belongs to the normal OS VBI (NMI, not masked by SEI).
; A=low, X=high, Y preserved. Retry if the high byte changed around the low.
sample_clock:
ldx $13
lda $14
cpx $13
bne sample_clock
rtsclock.inc reads RTCLOK’s middle byte at $13, low byte at $14, then compares the middle byte again. If a carry changed it, the reader retries. A is the resulting low byte, X the high byte, Y is preserved and flags change. The ROM owns the full three-byte RTCLOK at $12–$14. This adapter uses only its low word and never resets it.
The normal VBlank service updates this clock under NMI. SEI does not mask NMI; it would not make a naive pair of loads atomic. The retry read avoids taking over the OS service. Leave normal VBI, display and input services running. Interrupt replacement, region changes or suspended polling need another qualification. The recorded NTSC configuration advances roughly one tick per field; PAL needs a different ticks-to-seconds conversion. A clock sample is not a display wait.
Separate controls, policy and drawing
Release-gated fire and console controls
; 1=joystick fire, 2=SELECT, 3=OPTION. All must release to re-arm.
; A/X/flags clobbered, Y preserved. No writes to hardware/OS input shadows.
read_command:
lda $d01f
and #4
beq input_quit
lda $d01f
and #2
beq input_toggle
lda $0284
beq input_action
lda #0
sta HELD
rts
input_quit:
ldx #3
bne input_gate
input_toggle:
ldx #2
bne input_gate
input_action:
ldx #1
input_gate:
lda HELD
bne input_none
inc HELD
txa
rts
input_none:
lda #0
rtsChoose what a turn means
; No I/O. Expiry beats command; byte turn and expiry counters wrap.
dispatch:
lda DEADLINE+5
beq dispatch_key
lda #0
sta KEY
inc EXPIRIES
inc TURN
jmp deadline_start
dispatch_key:
lda KEY
cmp #2
bne dispatch_action
lda DEADLINE+4
beq dispatch_enable
lda #0
sta DEADLINE+4
sta DEADLINE+2
sta DEADLINE+3
rts
dispatch_enable:
jmp deadline_start
dispatch_action:
cmp #1
bne dispatch_done
inc TURN
lda DEADLINE+4
beq dispatch_done
jmp deadline_start
dispatch_done:
rtsUSR entry, service and return
TURN = $8300
EXPIRIES = $8301
KEY = $8302
HELD = $8303
DEADLINE = $8304
READY = $8310
GUARDS = $8312
start:
pla ; stock BASIC USR zero-argument count, not a JSR helper
php
txa
pha
tya
pha
cld
lda #0
sta TURN
sta EXPIRIES
sta HELD
lda #<DURATION
sta DEADLINE
lda #>DURATION
sta DEADLINE+1
jsr sample_clock
jsr deadline_init
lda #165
sta GUARDS
lda #90
sta GUARDS+1
lda #$fe
sta READY
lda #$ca
sta READY+1
main:
jsr sample_clock
jsr deadline_poll
jsr read_command
sta KEY
jsr dispatch
jsr draw_state
lda KEY
cmp #3
bne main
quit_release:
jsr read_command
lda HELD
bne quit_release
pla
tay
pla
tax
lda #0
sta $d4 ; USR returns unsigned integer zero through FR0
sta $d5
plp
rts
draw_state:
ldy #0
lda TURN
jsr draw_byte
lda #0
sta ($58),y
iny
lda EXPIRIES
jsr draw_byte
lda #0
sta ($58),y
iny
lda DEADLINE+4
draw_byte:
pha
lsr
lsr
lsr
lsr
tax
lda digits,x
sta ($58),y
iny
pla
and #15
tax
lda digits,x
sta ($58),y
iny
rts
digits:
!byte 16,17,18,19,20,21,22,23,24,25,33,34,35,36,37,38input.inc reads the OS-maintained active-low STRIG0 shadow at $0284 and the GTIA CONSOL register at $D01F. OPTION has priority over SELECT over fire. A=0 means none, 1 action, 2 toggle, 3 quit. A/X/flags change; Y survives. HELD suppresses further commands until all three controls are observed released. A short tap between polls can be missed; moving directly between held commands does not re-arm. No keyboard buffer is consumed and no input shadow/register is written.
dispatch.inc has no I/O. An expiry wins over that iteration’s command, including quit/toggle. Otherwise fire advances once and restarts only active timing; untimed actions stay untimed. SELECT discards the remainder on disable and starts a full duration on enable. TURN/EXPIRIES wrap as bytes. Replace the increment with your own world update. Preserved pause, queued commands or carried lateness are separate useful policies, with additional state and different behaviour.
The BASIC USR wrapper consumes the zero-argument count byte before saving X/Y/status. Do not put that PLA inside an ordinary JSR helper. It sets decimal mode clear, retains the existing stack and restores the saved registers/status on return. OPTION waits for command release; FR0’s low two bytes at $D4/$D5 are set to zero for BASIC’s USR result. No private stack or interrupt handler is installed. BASIC, the OS and nested calls still need stack/workspace; the emitted-byte counts are not peak-stack bounds.
Drawing follows SAVMSC at $58/$59 in GRAPHICS 0, rather than assuming a fixed screen address. It writes only the first eight character cells using ANTIC screen codes, changes A/X/Y/flags and leaves the remaining BASIC screen visible. The caller redraws continuously for simple feedback. A game can redraw only on changes, or add independent display pacing while still servicing deadlines often enough.
Customise, break and repair
Halve the duration
; SPDX-License-Identifier: MIT
* = $8000
DURATION = 120
!source "caller.inc"
!source "clock.inc"
!source "../../../../../cross-platform/patterns/framework/elapsed-deadline/6502.inc"
!source "dispatch.inc"
!source "input.inc"
code_end:
!if code_end > $8300 { !error "Code overlaps deadline context" }Deliberately broken baseline
; RTCLOK $12..$14 belongs to the normal OS VBI (NMI, not masked by SEI).
; A=low, X=high, Y preserved. Retry if the high byte changed around the low.
sample_clock:
ldx $13
lda $14
cpx $13
bne sample_clock
sta DEADLINE+8
stx DEADLINE+9
rtscustomise.asm changes DURATION=240 to 120: roughly four seconds becomes two, with unchanged payload/state sizes. Choose units before choosing a number. Maths for Games explains the same units, ranges and modular arithmetic.
mistakes/reset-baseline.asm deliberately selects a clock adapter that stores now as previous immediately before polling. The clock moves, but elapsed is always zero, remaining stays at 240 and the screen remains 01 00 01. Restore clock.inc and rebuild; an independently run repair reaches 03 01 01. The broken adapter adds six bytes. Never include both adapters in a working image.

A loader that writes past its reservation can damage BASIC or the screen rather than producing a useful deadline example. Keep the emitted-size checks and actual display pointers; a convenient address from another machine or caller is not a memory-ownership contract.
Costs, verification and sources
The maintained shared helper is 140 emitted bytes plus twelve context bytes. The coherent clock reader adds nine bytes, input forty-eight, dispatcher sixty-two, and caller/presentation 165. The whole image is 424 bytes plus twenty separately reserved state/marker/guard bytes. These are emitted sizes and ownership, not cycle measurements or a whole-game frame budget.
Six independent keyboard-loaded native cases cover working/custom/wrong/repaired callers and a second arithmetic/policy caller with a wrong-equality control. The loaded code must match assembly before USR. The second caller supplies twelve clock pairs for zero/one/maximum duration, wrap and exact/overshot expiry, plus three controlled policies: expiry+action, expiry+toggle and untimed action. A reversed zero branch completes the guards but fails the independent unsigned oracle. Supplied samples do not establish an observed eighteen-minute rollover.
Real controls include a long held fire button. The native ready/guard check and debugger’s natural main-loop stop keep observations outside unfinished updates; the debugger does not set the PC or write game state. Return is followed by real BASIC arithmetic and a zero USR-result check. PNG pixels are compared with the native screen codes and ROM glyphs. Native sources, tools, firmware identities, output and captures are retained.
Run scripts/verify-atari-xl-assembly-deadlines.py with released tools and your own OS/BASIC. Recheck with scripts/check-atari-xl-assembly-deadlines.py and its unittest corruption controls. Qualification names Emu198x 0.44.2, Asm198x 0.0.72, 800XL NTSC, OS Rev 2 and the recorded BASIC ROM. No original-hardware, every region/ROM or whole-emulator claim follows.
Primary references: Atari, Atari BASIC Reference Manual, USR calling/return conventions and GRAPHICS; Ian Chadwick, Mapping the Atari, RTCLOK 18–20, SAVMSC 88–89, RAMTOP 106, MEMTOP 741–742, STRIG0 644, CONSOL 53279 and FR0 212–217. Public scan: https://www.atarimania.com/documents/Mapping-the-Atari.pdf . The countdown, retry adapter and policy are this implementation’s choices.
Compare the clock and game-loop choices before choosing timing for a game. The Spectrum Z80 version uses the same countdown policy with a different interrupt and calling contract.
Source and loading instructions. Native evidence and configuration.