Commit Graph

4 Commits

Author SHA1 Message Date
Matthew Stanley ee9e464fd0 Preserve stock-frame VSync on faster displays 2026-10-19 18:02:47 -07:00
Kevin a91884a4bb runtime: pace frames the GL renderer elides a swap for
The GL renderer may skip SDL_GL_SwapWindow when the displayed rect is
unchanged, so a double-buffered 30 Hz game does not swap an identical
front buffer on the intervening vblank. The frontend calls present once
per guest vblank, so eliding a swap also elides whatever that swap was
blocking on.

That is only safe while the wall-clock pacer owns cadence, because the
pacer runs per present regardless of what the renderer does. When driver
vsync owns cadence instead (~60 Hz panel, present_vsync_owns_cadence()),
the pacer is deliberately not run -- the two are XOR to avoid
double-blocking -- and the swap block becomes the only throttle. An
elided frame then blocks on nothing, and a game presenting every other
vblank advances two guest vblanks per block: exactly 2x real time.

Measured on BreathOfFire3Recomp (SLPS-00990, 60 Hz panel, OpenGL):
guest vblank counter sampled against wall clock ran 120.4 fps / 2.01x
before, 59.8 fps / 0.997x after. Swaps per second are unchanged at 60
either way -- the fix removes a free-running guest frame, not a swap --
and per-frame GPU cost is unchanged (present_gpu_ms_avg 12.2 -> 12.6,
scene_gpu_ms_avg 4.5 -> 4.1).

Gate both skip sites (VRAM and wide-FBO paths) on the cadence owner,
alongside the existing OSD and interpolation-cadence guards. Only the GL
renderer has this optimisation; the software and Vulkan paths always
present and were never affected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:20:09 -04:00
Matthew Stanley 2bebd29629 Harden audio reserve and transient frame catch-up 2026-07-13 05:23:33 -07:00
Matthew Stanley ffc15219fd fix: Bug B hard freeze — TOCTOU underflow in frame pacing slept ~24.7 days
Root cause (freeze dump 1781046069, addr2line-verified): the wall-clock
pacing loop in sdl_vblank_present read the performance counter twice per
iteration — once in the while-condition, once to compute remaining time.
If the frame deadline was crossed between the two reads (a preemption
window), the unsigned subtraction underflowed, *1000 overflowed, and
SDL_Delay received ~2^64/10^4 ms mod 2^32 ≈ 24.7 days at the standard
10 MHz QPC. The app parks in one SDL_Delay with every counter frozen,
wedged between 'i_stat |= VBLANK' (interrupts.c:286) and exception
delivery — which is why dumps showed i_stat=1 pinned. VBLANK was being
ACK'd every frame right up to the wedge in every dump examined; the
'VBLANK-ACK reentry storm' framing was a symptom misread.

- frame_pacing.c/h: shared race-free pacer (single counter read per
  decision, compare-before-subtract, sleep clamped to one period).
  Both call sites switched: main.cpp sdl_vblank_present and
  beetle_main.cpp. Unit test pins the underflow case.
- Vsync self-heal: the 'recovered storm' dump (1781045865) was 8/8
  main-thread samples inside wglSwapBuffers — NV driver blocking ~1.5s
  per present. After 3 presents >250ms, drop driver vsync (we pace to
  59.94 Hz ourselves). Counters surfaced in the freeze dump.
- Watchdog classifier: 'reentry_storm' threshold (5K/sec) was below the
  healthy baseline (~2K reentry blocks/frame × 30-60 fps), mislabeling
  normal play and host stalls — that label misdirected two prior
  investigations. Now normalized per frame (>20K/frame), plus a new
  slow_frames kind (<5 fps) with multi-sample stack capture.

Verified: test_frame_pacing ALL PASS; live Tomba paced at 59.962 fps
over 20s (single-process measurement).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 20:09:25 -07:00