From 6d564c8eafe4d6ec660d5433ef6ac0b96cd7f4e5 Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sat, 29 Aug 2026 15:01:47 -0400 Subject: [PATCH] runtime: pace the frame even when driver vsync should own the cadence present_should_wall_pace() skipped the wall-clock pacer entirely whenever present_vsync_owns_cadence() was true, trusting the GL swap to block. A swap that does not block then leaves the guest with NO speed limit at all and the game simply free-runs -- indistinguishable, to a player, from a stuck fast-forward. Measured on FF7 (NVIDIA GL under a compositing WM), 45 s, nothing held, telemetry off: driver vsync "owns cadence" 4771 frames ~106 fps 1.77x PSX_VSYNC=0 (pacer forced on) 2651 frames ~58.9 fps 1.00x this change, default settings 2643 frames ~58.7 fps 0.98x Running the pacer underneath vsync is safe because frame_pacer_wait() is deadline-based rather than a fixed sleep: when the swap has already consumed the period, now >= next_deadline and it returns immediately after advancing the deadline. So it costs nothing where vsync works -- and in particular adds no latency to the healthy path, which is why vsync was given sole ownership in the first place -- while supplying the cap where vsync does not block. The frame-interpolation exclusion is kept: that path subdivides this same interval and would genuinely double-pace. The startup line is corrected with it. "wall-clock pacer skipped" is how a bring-up session decides pacing is accounted for, and it is what sent this investigation hunting a phantom stuck fast-forward instead of an unpaced present. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ --- runtime/src/main.cpp | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index c505bccd..8ff88ff9 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -2853,7 +2853,23 @@ static int present_should_wall_pace(void) { if (g_frame_interpolation && g_gl_active && gl_renderer_interpolation_owns_cadence()) return 0; - return g_frame_period_ms > 0.0 && !present_vsync_owns_cadence(); + /* Run the pacer even when driver vsync is SUPPOSED to own the cadence. + * + * Skipping it here trusts the swap to block, and a swap that does not + * block leaves the guest with NO speed limit at all -- the game simply + * free-runs. Measured on NVIDIA GL under a compositing WM: 45 s of + * gameplay produced 4771 guest frames (~106 fps, 1.77x) with vsync + * "owning" cadence, against 2651 (~58.9 fps, 1.00x) with PSX_VSYNC=0 + * forcing this pacer on. Nothing was held; it looked exactly like a stuck + * fast-forward. + * + * This is safe to run alongside a vsync that DOES block, because + * frame_pacer_wait() is deadline-based rather than a fixed sleep: if the + * swap already consumed the period, `now >= next_deadline` and it returns + * immediately after advancing the deadline. So it costs nothing where + * vsync works and supplies the cap where it does not, which also means it + * cannot add latency to the healthy path. */ + return g_frame_period_ms > 0.0; } static void apply_present_cadence(void) { @@ -2871,9 +2887,15 @@ static void apply_present_cadence(void) { static void log_present_cadence(void) { if (present_vsync_owns_cadence()) { - std::printf("psxrecomp: present cadence: driver vsync (%.1f Hz panel, " - "wall-clock pacer skipped)\n", - g_host_refresh_hz); + /* The pacer still runs underneath as a deadline cap (see + * present_should_wall_pace) -- a no-op whenever the swap actually + * blocks, and the only speed limit when it does not. Say so: this + * line is how a bring-up session decides whether pacing is accounted + * for, and "skipped" sent this one hunting a phantom stuck + * fast-forward instead of an unpaced present. */ + std::printf("psxrecomp: present cadence: driver vsync (%.1f Hz panel; " + "wall-clock cap %.4f ms/frame)\n", + g_host_refresh_hz, g_frame_period_ms); } else if (g_frame_period_ms > 0.0) { if (g_video_vsync != 0 && !g_frame_interpolation && !g_netplay_vsync_forced_off) {