From 3d1cb5013fa392a6831bc03b22669079b7690de7 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 29 Aug 2026 17:23:52 -0700 Subject: [PATCH] runtime: keep PAL cadence off 60 Hz host sync --- recompiler/CMakeLists.txt | 1 + runtime/CMakeLists.txt | 3 ++ runtime/src/main.cpp | 25 ++++++---- .../tests/test_pal_cadence_host_refresh.py | 49 +++++++++++++++++++ 4 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 runtime/tests/test_pal_cadence_host_refresh.py diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 4567626f..2cd74383 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -585,6 +585,7 @@ if(BUILD_TESTING) release_zip overlay_decodable_fallback overlay_init_guard + pal_cadence_host_refresh sdl3_main_single_include spu_sample_scheduler_default vk_build_default diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 3a13fd8e..2494923a 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -501,6 +501,9 @@ if(BUILD_TESTING) add_test(NAME gl_depth24_upload_order_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_depth24_upload_order.py) + add_test(NAME pal_cadence_host_refresh_test + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_pal_cadence_host_refresh.py) endif() # Full card protocol regression: read, write, address echo, per-slot state, diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index ccd06cd2..b0b44e0b 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -2702,8 +2702,11 @@ static void apply_netplay_local_viewport_aspect(bool netplay_enabled) { * wall-clock pacer double-blocks the vblank callback (present is before the * guest resumes), which shows up as MotK FMV ~30–40 FPS in netplay vs ~50+ * offline. Force immediate swaps for the session; restore on soft-exit. */ -static int host_refresh_is_approx_60hz(void) { - return g_host_refresh_hz >= 58.8 && g_host_refresh_hz <= 61.2; +static int host_refresh_matches_guest_cadence(void) { + if (g_host_refresh_hz <= 0.0 || g_frame_period_ms <= 0.0) + return 0; + const double guest_hz = 1000.0 / g_frame_period_ms; + return std::fabs(g_host_refresh_hz - guest_hz) <= guest_hz * 0.02; } static int host_driver_vsync_unreliable(void) { @@ -2747,7 +2750,7 @@ static int present_vsync_owns_cadence(void) { return 0; if (g_netplay_vsync_forced_off || psx_netplay_active()) return 0; - return host_refresh_is_approx_60hz(); + return host_refresh_matches_guest_cadence(); } static int present_effective_swap_interval(void) { @@ -13257,23 +13260,25 @@ session_reboot: if (!g_fullscreen && !g_video_win_w_explicit) SDL_MaximizeWindow(sdl_window); - /* Host refresh: if the panel is within ~2% of 60 Hz, record it so driver - * vsync can own cadence (pacer skipped). Non-~60 Hz and unknown refresh - * (common on Wayland) keep PSX 59.94 Hz pacing and force swap interval 0 - * — vsync as the clock would run the sim at the panel rate. */ + /* Host refresh: if the panel matches the current guest cadence, record it + * so driver vsync can own cadence (pacer skipped). Mismatched and unknown + * refresh rates keep guest pacing and force swap interval 0; vsync as the + * clock would otherwise run the sim at the panel rate. */ { SDL_DisplayMode dm; int disp_idx = SDL_GetWindowDisplayIndex(sdl_window); if (disp_idx >= 0 && SDL_GetCurrentDisplayMode(disp_idx, &dm) == 0 && dm.refresh_rate > 0) { double host_hz = (double)dm.refresh_rate; g_host_refresh_hz = host_hz; - if (host_hz >= 58.8 && host_hz <= 61.2) { + if (host_refresh_matches_guest_cadence()) { g_frame_period_ms = 1000.0 / host_hz; std::printf("psxrecomp: sync-to-host-refresh: pacing to %.1f Hz panel " "(%.4f ms/frame)\n", host_hz, g_frame_period_ms); } else { - std::printf("psxrecomp: host panel %.1f Hz not ~60 Hz; keeping PSX " - "59.94 Hz pacing\n", host_hz); + std::printf("psxrecomp: host panel %.1f Hz does not match guest " + "cadence; keeping %.2f Hz pacing\n", + host_hz, + g_frame_period_ms > 0.0 ? 1000.0 / g_frame_period_ms : 0.0); } } } diff --git a/runtime/tests/test_pal_cadence_host_refresh.py b/runtime/tests/test_pal_cadence_host_refresh.py new file mode 100644 index 00000000..54311aa5 --- /dev/null +++ b/runtime/tests/test_pal_cadence_host_refresh.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Guard PAL timing against 60 Hz host-refresh cadence leakage.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +MAIN = (ROOT / "runtime" / "src" / "main.cpp").read_text(encoding="utf-8") + + +def require(needle: str, message: str) -> None: + if needle not in MAIN: + raise AssertionError(message) + + +require( + "static int host_refresh_matches_guest_cadence(void)", + "host refresh must be compared against the active guest cadence", +) +require( + "const double guest_hz = 1000.0 / g_frame_period_ms;", + "guest cadence must derive from the current frame period", +) +require( + "return std::fabs(g_host_refresh_hz - guest_hz) <= guest_hz * 0.02;", + "host refresh tolerance must be relative to the current guest cadence", +) +require( + "if (host_refresh_matches_guest_cadence())", + "host-refresh sync must not overwrite PAL timing from a hard-coded 60 Hz check", +) +require( + "return host_refresh_matches_guest_cadence();", + "driver vsync cadence ownership must respect the active guest cadence", +) + +for forbidden in ( + "host_refresh_is_approx_60hz", + "host_hz >= 58.8", + "g_host_refresh_hz >= 58.8", + "host_hz <= 61.2", + "g_host_refresh_hz <= 61.2", +): + if forbidden in MAIN: + raise AssertionError( + f"PAL timing can leak back to the old 60 Hz-only cadence check: {forbidden}" + ) + +print("PAL host-refresh cadence guard passed")