Game loop spins at full CPU and reports nonsense frame times when the swapchain isn't acquirable
Symptom
A shipped game's own FPS log, vsync disabled, on the Bistro sample:
FPS: 174.4 <- plausible
FPS: 179.4
FPS: 23916.2 <- not plausible
FPS: 25871.6
FPS: 4783.5
FPS: 27072.9
FPS: 213.4 <- plausible againCause
The generated main loop treats a failed swapchain acquire as "skip rendering" and carries on:
const _frame = win.beginFrame(); // null when the swapchain isn't acquirable
// ... input, scripts, scene sync, UI all run unconditionally ...
if (_frame) |fr| {
render.renderScene(...);
render.runPostProcess(...);
fr.submit(); // the loop's ONLY pacing
}gpu.Window.beginFrame returns null when SDL_AcquireGPUSwapchainTexture yields no image — an occluded or minimized window, or a moment when no image is free. On that path nothing is presented, so nothing paces the loop: it spins as fast as the CPU allows, running scripts every iteration.
Two consequences:
- Frame time is garbage.
deltais computed unconditionally from the wall clock, so a spin iteration produces a near-zero delta.FpsCounterreports1/delta, hence 23,916 FPS. Any script integratingdelta(movement, timers, animation) is equally affected. - 100 % CPU while nothing is visible. A minimized game burns a core.
The generated loop contains no sleep, no frame limiter and no event wait, so the present is genuinely the only thing that ever blocks it.
Fix
When beginFrame returns null, the loop should yield rather than spin — a short sleep, or waiting on events. Whether scripts should still tick on a non-rendered frame is a separate call worth making explicitly: ticking them keeps game time advancing while occluded, skipping them keeps delta honest. Ticking with a clamped delta is probably the right compromise.
Also worth a maximum-delta clamp regardless: a frame that takes a second (a hitch, a breakpoint, a scene load) currently hands scripts a delta of 1.0, which teleports anything integrating it.
Related
- #169 wants a runtime frame cap in this same loop, which is the natural place for the limiter
editor/build/codegen/MainZigCodegen.zigis where the loop is generated