runtime: keep PAL cadence off 60 Hz host sync

This commit is contained in:
Matthew Stanley
2026-08-29 17:23:52 -07:00
parent c473fb1555
commit 3d1cb5013f
4 changed files with 68 additions and 10 deletions
+1
View File
@@ -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
+3
View File
@@ -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,
+15 -10
View File
@@ -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);
}
}
}
@@ -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")