Skip to content

A game loop on the vertical-blank request

Poll a fresh vertical-blank request while the main program owns the machine.

The background changes as updates advance. Hold Fire on physical joystick port 2 and a deliberately long CPU loop slows those changes. Release it and the caller returns to one update per display frame in the tested scene.

Use the wait in your project

Call wait_frame once per update with A5 unchanged. D6 and condition codes change; other registers survive. It clears the pending VERTB request, then polls for a newly asserted one. No interrupt handler may consume that same request. Clearing old pending work is deliberate: this helper waits for a new boundary rather than treating a stale request as another update.

frame.inc
; A5=$DFF000, caller owns custom VERTB request and disables its IRQ consumer.
; Drop pending work, then await a newly asserted VERTB request.
; D6/CCR change; other registers survive. This is a fixed-step wait, not a timer.
wait_frame:
    move.w #$0020,$09c(a5)
wait_vertical:
    move.w $01e(a5),d6
    and.w #$0020,d6
    beq.s wait_vertical
    rts

Start the complete example

Boot the ADF on the tested A500 OCS PAL/Kickstart 1.3 configuration. The caller disables custom interrupts and DMA, sets A5 to $DFF000, configures the Fire input and owns the background colour. It takes the machine over; it does not restore Workbench. Reset to leave.

With Asm198x available, keep the sample folders together and run:

Shell
make -C commodore-amiga/patterns/assembly/framework/game-loop

The 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.

demo.asm
; A500 OCS PAL, KS1.3, boot ADF. Physical port 2 Fire adds CPU work.
; Takes over custom IRQ/DMA and background colour; chip-memory executable.
; Reuses the input reader from the verified input/sound caller. Reset to leave.
    section code,code_c
start:
    lea $dff000,a5
    move.w #$7fff,$09a(a5)
    move.w #$7fff,$09c(a5)
    move.w #$7fff,$096(a5)
    bclr #7,$bfe201
    clr.w updates
    clr.w slow
    move.w #$cafe,ready
main_loop:
    bsr wait_frame
    bsr read_controls
    move.w controls,d0
    and.w #$0010,d0
    move.w d0,slow
    bsr update_game
    bsr draw_screen
    bsr expensive_work
frame_done:
    bra.s main_loop
update_game:
    addq.w #1,updates
    rts
draw_screen:
    move.w updates,d0
    lsr.w #3,d0
    and.w #7,d0
    lsl.w #8,d0
    or.w #$0030,d0
    move.w d0,$180(a5)
    rts
expensive_work:
    tst.w slow
    beq.s work_done
    move.w #60000,d0
work_inner:
    dbra d0,work_inner
work_done:
    rts
signature: dc.b "198XLOOPAMIGA!",0,0,0
    even
ready: dc.w 0
updates: dc.w 0
slow: dc.w 0
    include "frame.inc"
    include "../input-to-sound/input.inc"
program_end:

What happens when work runs late

The request is a latch, not a count of every missed frame. Slow work reduces update frequency and the loop does not catch up. This is one hardware-owning approach; Flock also explains a two-phase beam-position wait. Neither is an AmigaDOS-friendly scheduling API. Other models, regions, display arrangements and DMA loads need their own qualification.

Check this combination

Code status: assembled and emulator-executed. A500 OCS PAL, Kickstart 1.3. The recorded checks identify sources, firmware and observer. In three 30-frame windows the caller made 30 normal, 5 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.

See the idea inside a complete game.