SCUMM v5 timing — the jiffy / frame split

The single most important timing fact in SCUMM v5: there are two clocks, and conflating them makes everything that moves run too fast.

 jiffies (1/60 s):   │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ …
                     └─────┬─────┘ └─────┬─────┘ └────┬────
 game frames           one frame      next frame        …
 (every VAR_TIMER_NEXT = 6 jiffies → ~10 fps)

 counted in JIFFIES:  delay · VAR_MUSIC_TIMER · the talk timer
                      — wall-time accurate at any frame rate
 advance per FRAME:   scripts (breakHere) · actor walking ·
                      costume animation — all at ~10 fps

The two clocks

So a script's breakHere yields until the next frame (not the next jiffy), an actor moves its walk-speed per frame, and a costume animation advances one cmd-stream byte per frame — all at ~10 fps — while a delay 120 waits a wall-accurate 120 jiffies = 2 s.

Why it matters (the bug this fixed)

The engine originally ran the whole main loop — scripts, walking, anim — every jiffy (60 Hz). Delay-gated cutscene timing stayed correct (delays count jiffies), so total cutscene wall-time matched ScummVM. But everything that moves ran ~6× too fast: the Mêlée clouds zoomed off-screen, the LucasArts sparkles and lookout fire flickered too fast, and Guybrush walked across a room in a fraction of a second. The user's exact report: "the cutscene takes about the same time, but things move too fast." That asymmetry — wall-time right, motion fast — is the signature of running frame work on the jiffy clock.

The model in this engine

A single per-jiffy tick is the canonical driver — the shell loop and the headless harnesses all advance time through it, so the model lives in one place:

tick():                       // one jiffy (1/60 s)
  beginTick()                 // input/cursor mirror, VAR_MUSIC_TIMER++,
                              //   talk timer, camera follow — every jiffy
  for each slot: delayRemaining--   // delay countdown — every jiffy
  frameAccumulator++
  if frameAccumulator < VAR_TIMER_NEXT: return   // not a frame yet
  frameAccumulator = 0
  // ── one game frame ──
  processSentence()
  resume yielded, non-frozen, delay==0 slots
  runScriptsUntilAllYield()   // scripts
  stepAllActorWalks()         // walking
  stepAnim() for each actor   // costume animation

Likely also fixed by this

The "Le tre prove" interstitial that played in under a second (it should hold for several) was almost certainly a breakHere-loop-gated cutscene running 6× too fast — the same root cause. Verify visually.

Pitfalls