In September 1962 the students playing Spacewar! on MIT’s PDP-1 had a problem that every game programmer since has faced: some frames had more to do than others. Two ships, a star, torpedoes and explosions all had to be moved and drawn, and the work varied from moment to moment. The program’s answer, in the listing dated 24 September 1962, was to count. Each routine added its instruction count to a running total, and, as Norbert Landsteiner’s analysis of the code explains, “any remaining number of instruction would be spent in a little loop at the end of each frame” before a final jump back to the top. Every frame took the same time, so the ships flew at the same speed however busy the screen was.
That loop is the shape of almost every game since. Read the controls, move everything, draw the result, wait, and go round again. What changed on the home computers and consoles of the 1980s is who decided when to go round. The television did. Its picture was redrawn fifty times a second in PAL countries and sixty in NTSC ones, the video chip could tell the processor when each new frame began, and a game that kept in step with it got smooth, steady movement. Steve Bress’s 1985 guide to C64 arcade games put it plainly: because the display gives “a known minimum update time of 1/60 second, this tends to become the time period by which most aspects of the game are measured.”
The loop
initialise the game
repeat for ever:
read the joystick or keys
move the player, the enemies and the bullets
check for collisions, update the score
draw the changes
wait for the next frameThe last line does two jobs. It keeps the speed constant: a sprite that moves two pixels a frame moves 100 pixels a second on a PAL machine and 120 on an NTSC one, whatever else is going on. And it tells the program when the video chip has finished reading the screen, so that changes to the picture can be made without the player seeing them half done (see VSync and Double Buffering).
The frame budget
Waiting for the next frame also sets a hard limit. Everything in the loop has to fit into the processor time between one frame and the next.
| Machine | Processor clock | Frame | Cycles per frame |
|---|---|---|---|
| ZX Spectrum 48K | 3.5 MHz Z80 | 50 Hz | 69,888 T-states |
| Commodore 64, PAL | 0.985 MHz 6510 | 312 lines × 63 cycles | 19,656 |
| Commodore 64, NTSC | 1.023 MHz 6510 | 263 lines × 65 cycles | 17,095 |
| NES, NTSC | 1.79 MHz 2A03 | 262 lines | about 29,781 |
| NES, PAL | 1.66 MHz 2A07 | 312 lines | about 33,248 |
| Amiga, PAL | 7.09 MHz 68000 | 312 lines × 227 colour clocks | about 141,650 |
The C64 figures come from Christian Bauer’s description of the VIC-II: a PAL 6569 draws 312 lines of 63 cycles, and the NTSC 6567R8 263 lines of 65 (the earliest NTSC chips, 262 lines of 64). The Amiga’s comes from its hardware manual, which gives a PAL line as 227 colour clocks, each worth two cycles of the 68000’s clock. The Spectrum’s 69,888 is Chris Smith’s figure from his study of the ULA.
These are clock cycles, not free time. The video hardware takes its share first. The VIC-II stops the C64’s processor for 40 to 43 cycles on every eighth line of the picture to fetch character data, and more for each sprite; the Spectrum’s ULA holds the Z80 back when it touches screen memory while the picture is being drawn; the Amiga’s display and Blitter compete with the 68000 for chip memory. The Cycle Accuracy entry describes how each machine shares its bus.
The window when the screen can safely be changed is smaller again. On an NTSC NES the vertical blank lasts 20 lines, about 2,273 CPU cycles, and a PAL one 70 lines, about 7,459; the sprite copy alone takes 513 or 514 of them. On a PAL C64, 112 of the 312 lines lie outside the 200-line display window (lines 51 to 250), about 7,000 cycles before sprite and other fetches.
A Spectrum reader asking Your Sinclair about sprite flicker in 1991 was reminded of the limit in one line: “Remember - you only have one fiftieth of a second to do everything!”
Waiting for the frame
ZX Spectrum: HALT
The Spectrum’s ULA interrupts the Z80 once a frame, 64 lines before it starts drawing the picture. The Z80’s HALT instruction, in Zilog’s words, “suspends CPU operation until a subsequent interrupt or reset is received”, so a program that halts wakes up at the start of the next frame. Your Sinclair’s Spec Tec gave exactly this loop to the reader with the flickering sprites:
loop: halt ; wait for the frame interrupt
call erase ; rub out the sprites at their old positions
call sprite ; draw them at their new ones
; ... rest of the game ...
jp loop“Just make sure the interrupts are enabled,” the magazine added, “or your Speccy will be waiting forever!”
In the Spectrum’s normal interrupt mode 1, the interrupt also runs the ROM’s routine at $0038, which adds one to the three-byte frame counter FRAMES at 23672 and scans the keyboard; the manual explains that the counter “increases by 1” every fiftieth of a second. A game that wants its own routine to run on every frame, to play music for instance, switches the Z80 to interrupt mode 2. The processor then builds an address from the I register and a byte read from the data bus, and fetches the address of the handler from there. On a Spectrum that byte is not defined. A Your Spectrum reader wrote in 1984 that with no peripheral attached it reads as $FF, but programmers who wanted to be safe filled a whole page and one byte more with the same value, so the handler’s address came out the same whatever the bus held:
; interrupt mode 2 with a 257-byte table: every vector reads $FDFD
di
ld hl,$FE00
ld de,$FE01
ld bc,256
ld (hl),$FD
ldir ; $FE00-$FF00 all hold $FD
ld a,$FE
ld i,a ; table page
im 2
ei
; at $FDFD: jp frame_handlerDavid Jones’s routines for 128K programmers, printed in Your Sinclair in December 1987, do the same with a table of $FF bytes, so every interrupt vectors to $FFFF; a relative jump there sends it on to a JP to the handler, which plays the tune. The handler must end with EI and RET like the ROM’s.
Commodore 64: the raster interrupt
The C64 has no HALT. A program can poll the VIC-II’s raster register until the beam reaches a line below the picture:
; wait until the raster reaches line 251, just below the display window
wait_frame:
lda #251
wait_line:
cmp $d012
bne wait_lineThe low byte 251 can only mean line 251 on both PAL and NTSC machines, so the ninth bit in $D011 need not be checked. If the rest of the loop could finish within the same raster line, the program should first wait for the line to change, or it will run twice in one frame.
Polling wastes the time spent waiting, so most C64 games let the VIC-II interrupt them instead (see Raster Interrupts for the registers and set-up). Bress’s advice was to use interrupts “for the part of your program that needs the highest priority in terms of microprocessor time”, and to take control “at least once every other screen. Otherwise, the animation will appear jerky.”
Lasse Öörni, who wrote C64 games as Cadaver, described the arrangement most C64 games settled on. The raster interrupts do only what must happen at a particular line, “the immediate setting of VIC registers (like screen-splits and sprite multiplexing) and playing music/sound”. “All the time-consuming things like movement, AI & scrolling are done in the main program.” The two halves meet through a frame counter:
; raster interrupt at the bottom of the screen, entered through the
; KERNAL vector at $0314: VIC register writes and music only
irq: lda #$01
sta $d019 ; acknowledge the raster interrupt
; ... set sprite and scroll registers for the next frame ...
; ... play one frame of music ...
inc frame_count
jmp $ea81 ; restore A, X and Y and return
; main program
main: jsr read_joystick
jsr move_objects
jsr update_screen
lda frame_count
wait_irq:
cmp frame_count ; wait until the interrupt has run again
beq wait_irq
jmp mainThe pay-off, in Öörni’s words, is that if the main program runs over its budget it “doesn’t involve ugly graphical effects like flickering/jerking of screen, but it’s just visible as an overall slowdown”. The interrupts still fire on time, so the screen splits hold and the music keeps its tempo; only the game gets slower.
Martin Walker’s development diary for Citadel, printed in ZZAP! in February 1989, shows a programmer working the same boundary. Interrupt time had “run out in the worst possible case”, with every sprite on screen moving and firing. His rule: “some routines absolutely MUST occur every frame at an exact scanline position (moving sprites will flicker unless moved during off screen time), but with a rethink some routines may be able to run during mainline time as long as they can occur at about once per frame”. Moving two routines out of the interrupt stopped the overrun.
NES: the NMI
The NES’s PPU can raise a non-maskable interrupt (NMI) as vertical blank begins, once bit 7 of PPUCTRL ($2000) is set. Because the PPU’s memory can only be written safely during vertical blank or with the picture switched off, NES games split the frame the other way round from C64 games. The NMI handler does the screen updates, from buffers the main loop filled in during the previous frame, and the main loop does the game.
Nintendo’s Metroid (1986) is a clear example. Its main loop runs one frame of game logic, adds one to a frame counter, clears a flag and spins until the NMI sets it again. The NMI handler starts the sprite DMA copy, writes the palette, the queued tiles, PPUCTRL and the scroll, reads the joypads and runs the sound engine. If the main loop had not finished its frame when the NMI arrived, the handler still copies the sprites and plays the music but skips the other PPU writes and the joypad read, so a half-finished update never reaches the screen; the game simply slows down.
; NMI handler: screen updates only
nmi: pha
txa
pha
tya
pha
lda #$00
sta $2003 ; OAMADDR
lda #$02
sta $4014 ; copy the sprite list at $0200 to the PPU
; ... palette and nametable writes queued by the main loop ...
; ... PPUCTRL and scroll ...
inc nmi_count
pla
tay
pla
tax
pla
rti
; main loop: game logic, then wait for the NMI
main: jsr read_pads
jsr update_game ; also fills the sprite list and the PPU queue
lda nmi_count
wait_nmi:
cmp nmi_count
beq wait_nmi
jmp mainAmiga: the vertical blank and Copper interrupts
The Amiga’s custom chips raise a level 3 interrupt at the start of every vertical blank. The Amiga Hardware Reference Manual describes it as bit 5, VERTB, of the interrupt registers, occurring “at line 0 (start of vertical blank)”, and lists the jobs it is meant for: “updating of various pointer registers, rewriting lists of Copper tasks when necessary, and other system-control operations”. Jorgo Schimanski’s German book on Amiga games programming, published in 1991, set it up as games did once they had taken over the machine: point the level 3 vector at $6C to the game’s own routine, and enable only that interrupt.
move.l #vblank,$6c.w ; level 3 interrupt vector
move.w #$7fff,$dff09a ; INTENA: switch every interrupt off
move.w #$c020,$dff09a ; INTENA: SET + INTEN + VERTB
main: move.w frame_count,d0
wait: cmp.w frame_count,d0 ; wait for the next vertical blank
beq.s wait
; ... one frame of game logic ...
bra.s main
vblank: move.w #$0020,$dff09c ; INTREQ: clear the VERTB request
movem.l d0-d7/a0-a6,-(sp)
addq.w #1,frame_count ; tell the main loop a frame has passed
movem.l (sp)+,d0-d7/a0-a6
rte
frame_count:
dc.w 0“For jerk- and flicker-free animation,” Schimanski wrote, “an interrupt simply has to be generated.” He also recommended a refinement. Vertical blank comes before the picture is drawn, and it is better to start animating when the beam reaches the bottom of the screen, which gives more time. The Copper can wait for any line and then write to the interrupt request register, so one instruction at the right place in the Copper list raises bit 4, COPER, on the same level 3:
dc.w $009c,$8010 ; Copper MOVE: INTREQ = SET + COPERThe hardware manual suggests the same thing for a program that needs more time than vertical blank gives: a Copper interrupt “timed to occur just after the last line of display on the screen”. VERTB, COPER and the Blitter’s finished interrupt all share level 3, so a handler that enables more than one of them reads INTREQR ($DFF01E) to see which it has been given.
Fixed steps and slowdown
Home computer and console games almost always moved everything a fixed amount each frame. It was simple, it kept the game in step with the music and the display, and it made the game’s behaviour repeatable. The cost showed when a frame had too much to do: the loop missed a frame, and everything, player included, ran slower. Players of the period knew this as slowdown.
Programmers had several ways round it. They spread slow jobs, such as enemy decisions, over several frames. They updated only what had changed (Dirty Rectangles). And on the C64 some ran the game logic less often than the screen: Öörni suggests running movement and logic “each 2nd or each 4th frame” and filling the gaps by interpolation, noting that “some games like Gauntlet 3 or Myth scroll at 50Hz, while updating the movement of sprites at a lower rate” (Frameskipping and Interpolation).
PC games could not rely on a fixed frame rate, because PCs varied so widely in speed. Doom (1993) separated the two halves. Its source code, released by id Software in 1997, advances the game world in fixed steps it calls tics: “State updates, number of tics / second. #define TICRATE 35”. Each pass of the main loop runs as many tics as the clock says are due, at least one, and then draws a single frame, so the game runs at the same speed on a fast machine and a slow one while the picture updates as often as the machine can manage. Games that instead scale every movement by the time since the last frame, a variable timestep, came later and are common today.
Game states
The same loop usually runs several different games: a title screen, the game itself, a pause, a game-over sequence. A single variable records which is active, and the loop jumps to that state’s routine each frame (see State Machine). Metroid’s main routine is chosen through a table of code addresses indexed by a byte holding the current mode.
Sources
- Norbert Landsteiner, “Inside Spacewar! Part 3: Objects!”, masswerk.at (web, secondary), reproducing and explaining the Spacewar! 3.1 listing of 24 September 1962: the main loop and its instruction counting. https://www.masswerk.at/spacewar/inside/insidespacewar-pt3-objects.html
- Steve Bress, Commodore 64 Assembly Language Arcade Game Programming (TAB Books, 1985): chapter 1, p. 2 (the 1/60-second “screen”); chapter 6, “Working with Interrupts”, pp. 19–20.
- Your Sinclair issue 68, August 1991, p. 69: Spec Tec’s reply on sprite flicker and
HALT. - Your Sinclair issue 24, December 1987, p. 94: David Jones’s interrupt mode 2 routines for the 128.
- Your Spectrum issue 2, March 1984, letters: the mode 2 vector with no peripheral attached.
- Zilog, Z80 CPU User Manual (UM008011-0816):
HALT; interrupt mode 2. - Steven Vickers, Sinclair ZX Spectrum BASIC Programming (Sinclair Research, 1982), chapter 18: the frame counter at 23672–23674; Ian Logan and Frank O’Hara, The Complete Spectrum ROM Disassembly (Melbourne House, 1983): the maskable interrupt routine at
$0038. - Chris Smith, The ZX Spectrum ULA: How to Design a Microcomputer (ZX Design and Media, 2010): 69,888 T-states per frame (p. 225); the 56 lines of top border and 8 of flyback before the picture (ch. 9).
- Christian Bauer, The MOS 6567/6569 video controller (VIC-II) and its application in the Commodore 64 (1996): lines and cycles per line for each VIC-II type (section 3.4); Bad Lines (section 3.5).
- Lasse Öörni (Cadaver), “Frameskipping, interpolation and re-entrant IRQ code” (web, secondary): https://cadaver.github.io/rants/interp.html
- ZZAP! issue 46, February 1989, p. 42: Martin Walker’s Citadel diary.
- NESdev Wiki, “Cycle reference chart” and “PPU registers” (web, secondary): NES clock rates, cycles per frame, vertical blank length, PPUCTRL bit 7, OAM DMA. https://www.nesdev.org/wiki/Cycle_reference_chart
- Metroid (Nintendo, 1986), disassembly by Nick Mikstas,
Source_Files/Bank07.asm(web, secondary): the main loop, the NMI handler and the frame-ready flag. https://github.com/nmikstas/metroid-disassembly - Commodore-Amiga, Amiga Hardware Reference Manual (Addison-Wesley, 1989): the 7.09379 MHz PAL clock (chapter 1); 227 colour clocks per PAL line (chapter 2); the VERTB and COPER interrupts (chapter 7, pp. 214–215).
- Jorgo Schimanski, Spieleprogrammierung in Assembler auf dem Amiga (Heim-Verlag, 1991), chapter 2, pp. 8–13: interrupt registers, the level 3 vector, the VERTB handler and the Copper interrupt.
- id Software, DOOM source code, released 23 December 1997:
doomdef.h(TICRATE) andd_main.c(D_DoomLoop) (web, secondary). https://github.com/id-Software/DOOM
The C64 listings were assembled with ACME and run in the VICE emulator (x64sc) as PAL and NTSC machines. The Spectrum listings were assembled with sjasmplus and Pasmo, the NES listing with ca65 and the Amiga listings with vasm, and each was run in the Emu198x emulator on a Spectrum 48K, an NES and an Amiga 500.