The border changes as the update counter advances. Hold Fire on joystick port 2 and the colour changes slow down: we have deliberately given the CPU more work than fits in a frame. Release it and the normal pace returns.
Use the wait in your project
No clock initialisation is needed for the wait itself. Call wait_frame once per update. It changes flags and preserves A/X/Y. It first waits for raster bit 8 to be low, then for it to become high. Both phases matter: a short update cannot consume the same high interval repeatedly.
; Wait for the next low-to-high transition of raster bit 8.
; Own no IRQ source. Flags change; A/X/Y survive. I/O must be visible.
; Both phases matter: a quick caller cannot consume the same boundary twice.
wait_frame:
bit $d011
bmi wait_frame
wait_frame_high:
bit $d011
bpl wait_frame_high
rtsStart the complete example
This caller takes over CIA1 and the border, disables IRQs, banks out the ROMs and supplies a minimal NMI return. Keep I/O visible and keyboard keys released. Reset to leave. The joystick reader is the existing maintained routine, not another incompatible input format.
With Asm198x available, keep the sample folders together and run:
make -C commodore-64/patterns/assembly/framework/game-loopThe Amiga Makefile also needs Build198x. Open the generated file under build using the machine’s normal loading route.
The loop keeps four jobs visible: wait, read input, update state, draw. expensive_work is an optional CPU workload to expose missed deadlines; remove it when building your own game. The changing colour or digit is a small observable result, not a rendering engine.
; PAL C64. RUN. Port 2 Fire holds an intentionally excessive CPU workload.
; Exclusive CIA1/border ownership, interrupts disabled, ROM banked out.
; Reuses the maintained joystick reader. Reset to leave.
*=$0801
!byte $0b,$08,$0a,$00,$9e
!text "2064"
!byte 0,0,0
*=$0810
start:
sei
lda #$7f
sta $dc0d
sta $dd0d
lda $dc0d
lda $dd0d
lda #$35
sta $01
lda #<nmi_return
sta $fffa
lda #>nmi_return
sta $fffb
lda #0
sta $dc0e
sta $dc0f
sta updates
sta updates+1
sta slow
jsr joy_init
main_loop:
jsr wait_frame
jsr joy_poll
lda joy2
and #$10
sta slow
jsr update_game
jsr draw_screen
jsr expensive_work
frame_done:
jmp main_loop
update_game:
inc updates
bne update_done
inc updates+1
update_done:
rts
draw_screen:
lda updates
lsr
lsr
lsr
and #7
sta $d020
rts
expensive_work:
lda slow
beq work_done
ldy #48
work_outer:
ldx #0
work_inner:
dex
bne work_inner
dey
bne work_outer
work_done:
rts
nmi_return: rti
updates: !word 0
slow: !byte 0
!source "frame.inc"
!source "../../input/joystick-reading/joystick.inc"What happens when work runs late
This is a fixed-step loop. Work that overruns a display interval reduces the update rate; missed updates are not replayed. The sample checks PAL only. NTSC has different timing and needs its own measurements. Reaching the lower part of the screen does not guarantee that arbitrary drawing will avoid tearing.
Check this combination
Code status: assembled and emulator-executed. PAL C64. The recorded checks identify sources, firmware and observer. In three 30-frame windows the caller made 30 normal, 8 deliberately slowed and 30 recovered updates. An unpaced negative control is rejected by the same rate guard. This is configuration-specific emulator evidence, not an original-hardware or whole-emulator claim.
The loop comparison puts the ownership choices side by side. Each implementation keeps its own timing and resource contract.