From fbf0427ba6e43caffcc6394a3dc8809b7f9ed7dd Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Mon, 31 Aug 2026 19:28:10 -0700 Subject: [PATCH] Fix display cadence refresh on monitor moves --- runtime/CMakeLists.txt | 3 + runtime/src/main.cpp | 96 ++++++++++++++----- .../test_mixed_refresh_display_cadence.py | 61 ++++++++++++ .../tests/test_pal_cadence_host_refresh.py | 2 +- 4 files changed, 135 insertions(+), 27 deletions(-) create mode 100644 runtime/tests/test_mixed_refresh_display_cadence.py diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 0dafa176..f5ce8142 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -518,6 +518,9 @@ if(BUILD_TESTING) add_test(NAME pal_cadence_host_refresh_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_pal_cadence_host_refresh.py) + add_test(NAME mixed_refresh_display_cadence_test + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_mixed_refresh_display_cadence.py) add_test(NAME cdrom_lid_integration_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_cdrom_lid_integration.py) diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 0ed4124f..7663ea7e 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1255,7 +1255,10 @@ static_assert((int)PSX_MOD_CONTROLLER_DIGITAL == (int)PSXRecompV4::PAD_MODE_DIGITAL); static double g_host_refresh_hz = 0.0; static constexpr double PSX_FRAME_PERIOD_MS = 1000.0 / 59.94; +static double g_guest_frame_period_ms = PSX_FRAME_PERIOD_MS; static double g_frame_period_ms = PSX_FRAME_PERIOD_MS; +static int g_host_refresh_display_idx = -2; +static uint64_t g_host_refresh_last_probe_ms = 0; static bool g_mod_native_vblank_rate = false; static uint32_t g_mod_native_vblank_fps = 0; /* Activation-time request. -1 means no enabled mod owns load acceleration. */ @@ -1268,6 +1271,7 @@ static int present_vsync_owns_cadence(void); static int present_effective_swap_interval(void); static int present_should_wall_pace(void); static void apply_present_cadence(void); +static void refresh_host_display_cadence(int force_log, int force_probe); /* Map the configured tri-state fullscreen mode (g_fullscreen) to the SDL * window-fullscreen flag: used both to open the window in that mode and to @@ -1369,9 +1373,10 @@ extern "C" int psx_mod_set_native_vblank_rate( } g_mod_native_vblank_rate = true; g_mod_native_vblank_fps = frames_per_second; - g_frame_period_ms = frames_per_second + g_guest_frame_period_ms = frames_per_second ? 1000.0 / (double)frames_per_second : 0.0; + g_frame_period_ms = g_guest_frame_period_ms; /* * Above the physical panel rate (and in uncapped mode), swap-interval * blocking would silently replace the requested guest cadence with the @@ -2809,12 +2814,66 @@ static void apply_netplay_local_viewport_aspect(bool netplay_enabled) { * 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_matches_guest_cadence(void) { - if (g_host_refresh_hz <= 0.0 || g_frame_period_ms <= 0.0) + if (g_host_refresh_hz <= 0.0 || g_guest_frame_period_ms <= 0.0) return 0; - const double guest_hz = 1000.0 / g_frame_period_ms; + const double guest_hz = 1000.0 / g_guest_frame_period_ms; return std::fabs(g_host_refresh_hz - guest_hz) <= guest_hz * 0.02; } +static void refresh_host_display_cadence(int force_log, int force_probe) { +#ifndef PSX_SDL_NO_RENDER + if (!sdl_window) + return; + + const uint64_t now_ms = SDL_GetTicks64(); + const int disp_idx = SDL_GetWindowDisplayIndex(sdl_window); + if (!force_probe && + disp_idx == g_host_refresh_display_idx && + g_host_refresh_last_probe_ms != 0 && + now_ms >= g_host_refresh_last_probe_ms && + now_ms - g_host_refresh_last_probe_ms < 1000ull) { + return; + } + g_host_refresh_last_probe_ms = now_ms ? now_ms : 1ull; + + double host_hz = 0.0; + if (disp_idx >= 0) { + SDL_DisplayMode dm; + if (SDL_GetCurrentDisplayMode(disp_idx, &dm) == 0 && + dm.refresh_rate > 0) { + host_hz = (double)dm.refresh_rate; + } + } + + const int display_changed = (disp_idx != g_host_refresh_display_idx); + const int refresh_changed = + std::fabs(host_hz - g_host_refresh_hz) > 0.05; + if (!force_log && !display_changed && !refresh_changed) + return; + + g_host_refresh_display_idx = disp_idx; + g_host_refresh_hz = host_hz; + g_frame_period_ms = g_guest_frame_period_ms; + 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 if (host_hz > 0.0) { + 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); + } else { + std::printf("psxrecomp: host refresh unknown; keeping %.2f Hz pacing\n", + g_frame_period_ms > 0.0 ? 1000.0 / g_frame_period_ms : 0.0); + } + apply_present_cadence(); +#else + (void)force_log; + (void)force_probe; +#endif +} + static int host_driver_vsync_unreliable(void) { #ifdef _WIN32 return 0; @@ -6746,6 +6805,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { return ep; } + refresh_host_display_cadence(0, 0); + /* TCP turbo is for automated validation and trace capture. It keeps the * simulation advancing and the debug server polling, but removes frontend * presentation and wall-clock pacing. */ @@ -13317,7 +13378,8 @@ session_reboot: /* We need to adjust the frame pacing for PAL games to run at the * correct speed */ vblank_cycles = 677376u; - g_frame_period_ms = 1000.0 / 50.0; /* 50hz refresh rate */ + g_guest_frame_period_ms = 1000.0 / 50.0; /* 50hz refresh rate */ + g_frame_period_ms = g_guest_frame_period_ms; } else if (ident.region == "NTSC-J") cdrom_set_disc_scex("SCEI"); else if (ident.region == "NTSC-U") cdrom_set_disc_scex("SCEA"); @@ -13547,28 +13609,10 @@ session_reboot: if (!g_fullscreen && !g_video_win_w_explicit) SDL_MaximizeWindow(sdl_window); - /* 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_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 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); - } - } - } + /* Host refresh: if the window's current panel matches the guest cadence, + * record it so driver vsync can own cadence. Re-probed while running so + * mixed-refresh multi-monitor moves cannot leave this latched at startup. */ + refresh_host_display_cadence(1, 1); /* OpenGL backend: create the GL context now. On failure, relabel the * facade back to software (rasterization already runs through software in diff --git a/runtime/tests/test_mixed_refresh_display_cadence.py b/runtime/tests/test_mixed_refresh_display_cadence.py new file mode 100644 index 00000000..728d0b97 --- /dev/null +++ b/runtime/tests/test_mixed_refresh_display_cadence.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Guard mixed-refresh monitor moves against startup-latched cadence.""" + +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 double g_guest_frame_period_ms = PSX_FRAME_PERIOD_MS;", + "guest cadence must be stored separately from host-synced pacing", +) +require( + "static int g_host_refresh_display_idx = -2;", + "host display identity must be tracked across runtime probes", +) +require( + "static void refresh_host_display_cadence(int force_log, int force_probe)", + "host display cadence must have a reusable refresh helper", +) +require( + "const double guest_hz = 1000.0 / g_guest_frame_period_ms;", + "host-refresh matching must compare against guest cadence, not effective host period", +) +require( + "g_frame_period_ms = g_guest_frame_period_ms;\n if (host_refresh_matches_guest_cadence())", + "mismatched display probes must restore guest pacing before deciding vsync ownership", +) +require( + "refresh_host_display_cadence(1, 1);", + "startup must use the shared host display cadence helper", +) +require( + "refresh_host_display_cadence(0, 0);", + "runtime must re-probe host display cadence after window moves", +) +require( + "disp_idx == g_host_refresh_display_idx", + "runtime probe must detect display index changes", +) +require( + "now_ms - g_host_refresh_last_probe_ms < 1000ull", + "runtime probe must still refresh same-display mode changes periodically", +) + +if MAIN.count("SDL_GetWindowDisplayIndex(sdl_window)") != 1: + raise AssertionError("window display probing should have one shared owner") + +startup = MAIN.index("refresh_host_display_cadence(1, 1);") +runtime = MAIN.index("refresh_host_display_cadence(0, 0);") +if startup == runtime: + raise AssertionError("startup and runtime probes must be distinct call sites") + +print("mixed-refresh display cadence guard passed") diff --git a/runtime/tests/test_pal_cadence_host_refresh.py b/runtime/tests/test_pal_cadence_host_refresh.py index 54311aa5..f88acb31 100644 --- a/runtime/tests/test_pal_cadence_host_refresh.py +++ b/runtime/tests/test_pal_cadence_host_refresh.py @@ -18,7 +18,7 @@ require( "host refresh must be compared against the active guest cadence", ) require( - "const double guest_hz = 1000.0 / g_frame_period_ms;", + "const double guest_hz = 1000.0 / g_guest_frame_period_ms;", "guest cadence must derive from the current frame period", ) require(