Press fire and the background changes colour. Keep holding it: nothing more happens. Release and press again, and the first colour returns. The game has accepted two actions, while the display has kept running throughout.
That small difference is worth getting right. A player waiting for their next move should not accidentally stop the picture, and a held button should not become hundreds of moves.

Build and try it
make -C atari-7800/patterns/assembly/framework/turn-loopStart build/demo.bin in Emu198x for Atari 7800. The qualified configuration is NTSC 48 KiB direct reset cartridge, bypassing BIOS authentication. Reset to leave. The caller is a deliberately small backdrop demonstration, not a complete display engine.
Include and initialise the reusable state
Copy turn.inc and its neighbouring edge-filter.inc together, and include the routine once after your main code. Define T as seven private, consecutive RAM bytes. The caller uses $1900–$1906; choose another valid allocation if your game owns that memory already.
Call init_turn with the starting progress counter in A. It clears the previous/current samples, accepted flag and 16-bit service count, and sets the step to 1. Change T+5 after initialisation when you want another step. These routines require decimal mode clear; the complete reset caller uses CLD.
Rule progression and display service are separate routines
; Caller defines T as the first of seven disjoint private RAM bytes.
; T+0 turns, +1 previous sample, +2/+3 serviced fields, +4 current,
; +5 step size, +6 accepted flag. Decimal mode must be clear.
; init_turn: A = initial counter; initialises all seven bytes, step=1.
init_turn:
sta T
lda #0
sta T+1
sta T+2
sta T+3
sta T+4
sta T+6
lda #1
sta T+5
rts
; sample_turn: A = active-high button mask, same bit convention each call.
; Changes A/flags, preserves X/Y. Counter wraps modulo 256.
; No hardware access or waiting. 2 stack bytes for a direct JSR.
sample_turn:
sta T+4
!source "edge-filter.inc"
beq held_or_released
lda T
clc
adc T+5
sta T
lda #1
sta T+6
bne remember_sample
held_or_released:
lda #0
sta T+6
remember_sample:
lda T+4
sta T+1
rts
; One accepted display-service tick, independent of rule progression.
; Changes flags only; wraps at 65536. Call exactly once per serviced field.
service_tick:
inc T+2
bne tick_done
inc T+3
tick_done:
rts
turn_end:Keep only bits that became pressed since the last sample
; Preserve current bits that were absent in the previous sample.
lda T+1
eor #$ff
and T+4| Entry or state | Contract |
|---|---|
init_turn |
A is the initial 8-bit counter; initialises all seven bytes |
sample_turn |
A is the active-high button mask; accepts newly pressed bits |
T+0 |
Progress counter, increased by T+5 on acceptance; wraps modulo 256 |
T+1, T+4 |
Previous/current samples; keep the same bit convention each call |
T+2, T+3 |
Little-endian 16-bit service count, independent of turns |
T+5, T+6 |
Step size and the current call’s accepted flag (1 or 0) |
service_tick |
Increment the service count once per accepted display field |
The init/sample routines change A and flags; service_tick changes flags. All preserve X/Y. A direct JSR needs two stack bytes, plus your surrounding caller and interrupt depth. The routines have no nested calls, hardware access, frame wait, private stack or interrupt masking. Their shared context is not re-entrant.
Use one primary-button bit as the caller does. With a multi-bit mask, any newly pressed bit commits one update, even if another bit remains held; this routine does not queue separate actions for each button. A zero step can still report an accepted action without changing progress. Neither helper decides your game rules or imposes a deadline.
Fit it into the machine’s caller
Reset, input conversion, rule update and machine-specific display service
; atari-7800: rule state and display servicing have separate counters.
T = $1900
!source "settings.inc"
* = $4000
start:
sei
cld
ldx #$ff
txs
lda #7
sta $01 ; enter/lock 7800 mode; BIOS authentication is external
lda #0
sta $3c ; MARIA DMA off during pointer setup
lda #>dll
sta $2c
lda #<dll
sta $30
wait_enable:
bit $28 ; enable DMA only during VBlank
bpl wait_enable
lda #$40
sta $3c ; 160A, empty display lists, background visible
lda #INITIAL_TURN
jsr init_turn
lda #$a5
sta T-1
sta T+7
lda #STEP_SIZE
sta T+5
main:
jsr wait_frame
jsr service_tick
lda $000c
and #$80
eor #$80
jsr sample_turn
lda T
and #FEEDBACK_BIT
beq even_turn
lda #$44
bne show
even_turn:
lda #$84
show:
sta $0020
jmp main
!source "frame.inc"
!source "../../../../../cross-platform/patterns/framework/atari-turn-loops/turn.inc"
irq:
rti
!align 255,0
dll:
!for i,1,16 { !byte $0f,>empty_list,<empty_list }
empty_list:
!byte 0,0 ; no objects; MARIA still renders the background
!fill $fffa-*, $ff
!word irq,start,irqsample_turn reports acceptance only for that call. Read T+6 before another sample replaces it, then let your own rules respond. service_tick does not change that flag. The two counters have different meanings: one counts accepted progress, the other counts serviced fields.
The guards immediately outside the seven-byte allocation are diagnostic caller state, not part of the reusable routine. The reset caller takes over the stack and display interfaces. Keep its ownership separate from other routines that scan input, alter interrupt enables or own the same video registers.
Wait on MARIA, not on a TIA kernel
The 7800’s picture comes from MARIA display lists. The caller points MARIA at empty lists, enables DMA during blanking and uses the background colour as feedback. It keeps servicing the loop while the turn state waits.
Accept a fresh MARIA blanking interval
wait_frame:
bit $28 ; MSTAT bit 7: leave any current VBlank
bmi wait_frame
wait_new:
bit $28
bpl wait_new
rtsMSTAT at $28, bit 7, is high during vertical blank. The wait leaves any current blanking interval before accepting the next one. Calling only the second half would allow several iterations during the same blank. This is different from both the 5200’s VCOUNT test and the 2600’s scanline production.
The caller locks 7800 mode through INPTCTRL, installs the display-list-list pointer and enables MARIA’s DMA in blanking. The empty lists deliberately keep graphics demand small. sample_turn itself does none of that and never accesses a device register. The primary fire read is the active-low bit 7 at $0C; this caller does not supply dual-button/controller-mode handling.
$20 selects MARIA’s background colour. CPU speed and available work change with hardware access and DMA; a similarly sized 2600 routine is not a useful timing budget. A game adding objects must account for MARIA’s DMA stealing and list lifetime. The MARIA object routines have their own allocation and publication contract.
The service counter advances 30 times in 30 native frames with these empty lists. A missed blanking interval is lost. This direct reset cartridge bypasses stock BIOS authentication; the run does not prove an authenticated retail-console startup path.
Make it your own
The default caller reads these settings:
One progress unit per new press
INITIAL_TURN = 0
STEP_SIZE = 1
FEEDBACK_BIT = 1Replace settings.inc with this maintained alternative and rebuild:
Two progress units, with feedback using the next counter bit
INITIAL_TURN = 0
STEP_SIZE = 2
FEEDBACK_BIT = 2The counter now goes 0 → 2 → 4, still with one accepted action per press. We also select bit 1 for colour feedback: bit 0 never becomes set when adding two, so leaving the original feedback mask would hide the progress. This is a useful distinction between changing the rules and choosing how to show them.
The routine code and seven-byte context stay the same size; only caller constants change. The compiled routine occupies 77 bytes for this allocation, excluding the reset/display caller, stack and cartridge padding. The absolute addressing of this allocation costs more code bytes than the 2600’s zero-page allocation. Moving it can change both size and execution cost; preserve the machine’s timing contract when relocating it. The customisation is separately executed, not just assembled.
Common mistake: held means new
The next fragment is deliberately broken. It replaces edge-filter.inc and treats every held sample as another press.
Broken filter: the current state is not an edge
; Deliberately wrong: a held bit is treated as a new press every sample.
lda T+4In the native run, holding fire for a further 30 frames changes the faulty counter again. The working counter remains at 1. Its picture may alternate too quickly for a still to explain the error: the measured state transition is the evidence. Restore the previous-sample filter above. Keep the update of T+1 on every call, including releases, so another press can be recognised.
Common mistake: a plausible picture, the wrong clock
Replacing the field wait with RTS makes the service counter a count of CPU iterations, even though its name still says “fields”. The background may look unchanged.
Deliberately wrong: return without accepting a field boundary
; Deliberately wrong: no field boundary is accepted.
wait_frame:
rtsThe native observation is 8,442 service ticks in 30 frames, against 30 for the working wait. The counter is 16-bit and wraps; this measured interval is not a universal maximum speed. Restore frame.inc above and the original rate check passes again.
What was checked
Code status: assembled and emulator-executed with Asm198x 0.0.71 and Emu198x 0.44.0. Seven independent builds/runs cover the working caller, step-two customisation, 254 → 255 → 0 wrap, both deliberate faults and both repairs. The checks use real button input, 120 idle frames, a 30-frame hold, a second press, protected state boundaries and native captures. The repairs restore the working executable and captured results.
The records retain source, tool and executable identities. The CI checker rejects missing bindings, omitted variants and altered behavioural evidence; it checks retained evidence and does not claim a new emulator run. No PAL, arbitrary game workload, original-hardware or whole-emulator qualification follows from this example.
- Complete callers, routine and reproduction instructions
- Native execution record
- Choose progression, clocks and presentation
Hardware references
Atari, 7800 Software Guide, “Starting up the 7800”, MSTAT, display-list-list pointers and DMA control.