diff --git a/windows/runner/mpv/display_mode_manager.cpp b/windows/runner/mpv/display_mode_manager.cpp index 3c28c212a..e941d03dc 100644 --- a/windows/runner/mpv/display_mode_manager.cpp +++ b/windows/runner/mpv/display_mode_manager.cpp @@ -58,6 +58,93 @@ class RecoveryRunGuard { bool acquired_; }; +// Query display paths and modes with the given QDC flags, retrying on +// ERROR_INSUFFICIENT_BUFFER (Kodi pattern). Clears the outputs on failure. +bool QueryDisplayConfigPathsAndModes( + UINT32 flags, std::vector& paths, std::vector& modes) { + UINT32 path_count = 0; + UINT32 mode_count = 0; + LONG result; + + do { + if (GetDisplayConfigBufferSizes(flags, &path_count, &mode_count) != ERROR_SUCCESS) { + paths.clear(); + modes.clear(); + return false; + } + + paths.resize(path_count); + modes.resize(mode_count); + + result = QueryDisplayConfig(flags, &path_count, paths.data(), &mode_count, modes.data(), nullptr); + } while (result == ERROR_INSUFFICIENT_BUFFER); + + if (result != ERROR_SUCCESS) { + paths.clear(); + modes.clear(); + return false; + } + + paths.resize(path_count); + modes.resize(mode_count); + return true; +} + +// Capture the complete active topology. Prefer a virtual-refresh-rate-aware +// snapshot so a Dynamic Refresh Rate configuration +// (DISPLAYCONFIG_PATH_BOOST_REFRESH_RATE) survives the round trip; retry +// without that flag for OS versions that reject it (pre-Win11-22H2). +DisplayConfigSnapshot CaptureDisplayConfigSnapshot() { + DisplayConfigSnapshot snapshot; + constexpr UINT32 base_flags = QDC_ONLY_ACTIVE_PATHS | QDC_VIRTUAL_MODE_AWARE; + + snapshot.vrr_aware = true; + if (QueryDisplayConfigPathsAndModes(base_flags | QDC_VIRTUAL_REFRESH_RATE_AWARE, snapshot.paths, snapshot.modes)) { + return snapshot; + } + + snapshot.vrr_aware = false; + QueryDisplayConfigPathsAndModes(base_flags, snapshot.paths, snapshot.modes); + return snapshot; +} + +// Re-apply a captured topology through SetDisplayConfig. Unlike the legacy +// ChangeDisplaySettingsExW restore, this preserves CCD-level path state such +// as the Dynamic Refresh Rate boost flag, which a DEVMODEW cannot express: an +// explicit DEVMODE restore turns a "Dynamic" refresh-rate selection into a +// fixed rate pinned at the DRR base rate. save_to_database additionally +// repairs the persisted display database in case the 24/48/60Hz registry +// workaround in SetDisplayMode overwrote it. +bool ApplyDisplayConfigSnapshot(const DisplayConfigSnapshot& snapshot, bool save_to_database) { + if (!snapshot.valid()) return false; + + // SetDisplayConfig may adjust the supplied arrays (SDC_ALLOW_CHANGES); keep + // the caller's snapshot intact for later retries. + std::vector paths = snapshot.paths; + std::vector modes = snapshot.modes; + + UINT32 flags = SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES | SDC_VIRTUAL_MODE_AWARE; + if (snapshot.vrr_aware) flags |= SDC_VIRTUAL_REFRESH_RATE_AWARE; + if (save_to_database) flags |= SDC_SAVE_TO_DATABASE; + + return SetDisplayConfig( + static_cast(paths.size()), paths.data(), static_cast(modes.size()), modes.data(), + flags) == ERROR_SUCCESS; +} + +// Re-apply the user's persisted display configuration for the current +// topology -- the documented "return from a temporary mode to the last saved +// display configuration" SetDisplayConfig scenario. The database entry keeps +// CCD-level state such as a Dynamic Refresh Rate selection that the recovery +// record's DEVMODE cannot express, and Plezy's overrides never replace it +// (restores only ever save the pre-override snapshot). The awareness flags +// are documented modifiers to supplied-config calls only, so they are +// deliberately absent here. Used by crash recovery, which has no in-memory +// snapshot. +bool ApplyDatabaseCurrentConfig() { + return SetDisplayConfig(0, nullptr, 0, nullptr, SDC_APPLY | SDC_USE_DATABASE_CURRENT) == ERROR_SUCCESS; +} + bool PrepareModeRecoveryAtRegistry(const std::wstring& device_name, DWORD width, DWORD height, DWORD refresh_rate); bool PrepareHDRRecoveryAtRegistry(const std::wstring& device_name, bool enabled); bool CompleteRecoveryOperationAtRegistry( @@ -84,27 +171,9 @@ std::wstring DisplayModeManager::GetMonitorDeviceName(HWND window) { } std::vector DisplayModeManager::GetDisplayConfigPaths() { - UINT32 path_count = 0; - UINT32 mode_count = 0; std::vector paths; std::vector modes; - - constexpr UINT32 flags = QDC_ONLY_ACTIVE_PATHS; - LONG result; - - // Retry loop for ERROR_INSUFFICIENT_BUFFER (Kodi pattern). - do { - if (GetDisplayConfigBufferSizes(flags, &path_count, &mode_count) != ERROR_SUCCESS) return {}; - - paths.resize(path_count); - modes.resize(mode_count); - - result = QueryDisplayConfig(flags, &path_count, paths.data(), &mode_count, modes.data(), nullptr); - } while (result == ERROR_INSUFFICIENT_BUFFER); - - if (result != ERROR_SUCCESS) return {}; - - paths.resize(path_count); + QueryDisplayConfigPathsAndModes(QDC_ONLY_ACTIVE_PATHS, paths, modes); return paths; } @@ -187,6 +256,12 @@ DisplayMode DisplayModeManager::GetCurrentMode(HWND window) { } void DisplayModeManager::SaveOriginalMode(HWND window) { + // Snapshot the CCD topology first, before any Windows mutation, so restores + // can put back state the legacy DEVMODE cannot express (Dynamic Refresh + // Rate). On a DRR display ENUM_CURRENT_SETTINGS reports only the fixed DRR + // base rate (issue #2055). + original_config_ = CaptureDisplayConfigSnapshot(); + original_device_name_ = GetMonitorDeviceName(window); if (original_device_name_.empty()) return; @@ -275,15 +350,23 @@ bool DisplayModeManager::RestoreOriginalMode(HWND) { return false; } - original_devmode_.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; - LONG rc = - ChangeDisplaySettingsExW(original_device_name_.c_str(), &original_devmode_, nullptr, CDS_FULLSCREEN, nullptr); + // Prefer re-applying the pre-override topology through SetDisplayConfig: it + // restores a "Dynamic" refresh-rate selection that the DEVMODE fallback + // would pin to a fixed rate at the DRR base frequency (issue #2055). + bool restored = ApplyDisplayConfigSnapshot(original_config_, /*save_to_database=*/true); - if (rc != DISP_CHANGE_SUCCESSFUL) { - // Fallback: restore registry defaults. - rc = ChangeDisplaySettingsExW(original_device_name_.c_str(), nullptr, nullptr, 0, nullptr); + if (!restored) { + original_devmode_.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; + LONG rc = + ChangeDisplaySettingsExW(original_device_name_.c_str(), &original_devmode_, nullptr, CDS_FULLSCREEN, nullptr); + + if (rc != DISP_CHANGE_SUCCESSFUL) { + // Fallback: restore registry defaults. + rc = ChangeDisplaySettingsExW(original_device_name_.c_str(), nullptr, nullptr, 0, nullptr); + } + restored = rc == DISP_CHANGE_SUCCESSFUL; } - if (rc != DISP_CHANGE_SUCCESSFUL) { + if (!restored) { // The explicit owner has given up. Keep the durable marker, but release it // so a later topology notification can restore a reconnected target or a // conflicting mode request can consume the failed recovery disposition. @@ -415,8 +498,10 @@ bool DisplayModeManager::SetHDREnabled(HWND window, bool enabled) { // this process's just-persisted marker as crash recovery. g_live_hdr_recovery_record = true; - // Save DEVMODEW before toggle — Windows changes display mode on HDR state change. - // Source: Kodi WIN32Util.cpp:1252-1257. + // Save the display topology before the toggle — Windows changes display + // mode on HDR state change (Kodi WIN32Util.cpp:1252-1257). The DEVMODEW is + // the fallback when no CCD snapshot is available. + const DisplayConfigSnapshot pre_toggle_config = CaptureDisplayConfigSnapshot(); DEVMODEW pre_toggle_dm = {}; pre_toggle_dm.dmSize = sizeof(pre_toggle_dm); EnumDisplaySettingsW(device_name.c_str(), ENUM_CURRENT_SETTINGS, &pre_toggle_dm); @@ -431,9 +516,12 @@ bool DisplayModeManager::SetHDREnabled(HWND window, bool enabled) { return false; } - // Restore DEVMODEW after toggle — Windows may have changed the display mode. - // Source: Kodi WIN32Util.cpp:1276-1288. - if (pre_toggle_dm.dmDisplayFrequency != 0) { + // Restore the display mode after the toggle — Windows may have changed it + // (Kodi WIN32Util.cpp:1276-1288). Re-applying the CCD snapshot keeps a + // Dynamic Refresh Rate selection intact; the DEVMODE re-apply would pin the + // DRR base rate (issue #2055). + if (!ApplyDisplayConfigSnapshot(pre_toggle_config, /*save_to_database=*/false) && + pre_toggle_dm.dmDisplayFrequency != 0) { pre_toggle_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; ChangeDisplaySettingsExW(device_name.c_str(), &pre_toggle_dm, nullptr, CDS_FULLSCREEN, nullptr); } @@ -466,7 +554,8 @@ bool DisplayModeManager::RestoreOriginalHDRState(HWND) { // state already matches, so a disconnected target cannot be mistaken for // a successful restore. - // Save DEVMODEW before restore toggle. + // Save the display topology before the restore toggle; see SetHDREnabled. + const DisplayConfigSnapshot pre_toggle_config = CaptureDisplayConfigSnapshot(); DEVMODEW pre_toggle_dm = {}; pre_toggle_dm.dmSize = sizeof(pre_toggle_dm); EnumDisplaySettingsW(original_hdr_device_name_.c_str(), ENUM_CURRENT_SETTINGS, &pre_toggle_dm); @@ -477,8 +566,9 @@ bool DisplayModeManager::RestoreOriginalHDRState(HWND) { return false; } - // Restore DEVMODEW after toggle. - if (pre_toggle_dm.dmDisplayFrequency != 0) { + // Restore the display mode after the toggle; see SetHDREnabled. + if (!ApplyDisplayConfigSnapshot(pre_toggle_config, /*save_to_database=*/false) && + pre_toggle_dm.dmDisplayFrequency != 0) { pre_toggle_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; ChangeDisplaySettingsExW(original_hdr_device_name_.c_str(), &pre_toggle_dm, nullptr, CDS_FULLSCREEN, nullptr); } @@ -636,6 +726,24 @@ class Win32DisplayRecoveryBackend final : public DisplayRecoveryBackend { } bool RestoreMode(const std::wstring& device_name, DWORD width, DWORD height, DWORD refresh_rate) override { + // The recorded DEVMODE cannot express a "Dynamic" refresh-rate selection, + // so prefer re-applying the persisted display configuration, which the + // overrides never replace. On a DRR display the recorded rate is the DRR + // base rate — exactly what the restored configuration reports through the + // legacy API — so the verification below accepts it. A mismatch means the + // database no longer holds the recorded original (e.g. a crash inside the + // 24/48/60Hz registry-workaround window) and the explicit legacy restore + // takes over. + if (ApplyDatabaseCurrentConfig()) { + DEVMODEW current = {}; + current.dmSize = sizeof(current); + if (EnumDisplaySettingsW(device_name.c_str(), ENUM_CURRENT_SETTINGS, ¤t) && + current.dmPelsWidth == width && current.dmPelsHeight == height && + current.dmDisplayFrequency == refresh_rate) { + return true; + } + } + DEVMODEW dm = {}; dm.dmSize = sizeof(dm); dm.dmPelsWidth = width; @@ -650,12 +758,18 @@ class Win32DisplayRecoveryBackend final : public DisplayRecoveryBackend { const auto target_id = DisplayModeManager::GetDisplayTargetId(device_name); if (!target_id) return false; + // Save the display topology before the toggle; the CCD re-apply keeps a + // Dynamic Refresh Rate selection intact where the DEVMODE re-apply would + // pin the DRR base rate (issue #2055). + const DisplayConfigSnapshot pre_toggle_config = CaptureDisplayConfigSnapshot(); DEVMODEW pre_toggle_mode = {}; pre_toggle_mode.dmSize = sizeof(pre_toggle_mode); EnumDisplaySettingsW(device_name.c_str(), ENUM_CURRENT_SETTINGS, &pre_toggle_mode); if (DisplayModeManager::SetHDRStateForTarget(*target_id, enabled) != ERROR_SUCCESS) return false; + if (ApplyDisplayConfigSnapshot(pre_toggle_config, /*save_to_database=*/false)) return true; + if (pre_toggle_mode.dmDisplayFrequency != 0) { pre_toggle_mode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; if (ChangeDisplaySettingsExW(device_name.c_str(), &pre_toggle_mode, nullptr, CDS_FULLSCREEN, nullptr) != diff --git a/windows/runner/mpv/display_mode_manager.h b/windows/runner/mpv/display_mode_manager.h index 2c9ccc693..9285f33b2 100644 --- a/windows/runner/mpv/display_mode_manager.h +++ b/windows/runner/mpv/display_mode_manager.h @@ -21,6 +21,21 @@ struct DisplayConfigId { UINT32 id; }; +// Complete active display topology captured with QueryDisplayConfig before a +// display mutation. Restoring it through SetDisplayConfig preserves CCD-level +// path state -- notably the Dynamic Refresh Rate boost flag +// (DISPLAYCONFIG_PATH_BOOST_REFRESH_RATE) -- that a DEVMODEW restore cannot +// express. +struct DisplayConfigSnapshot { + std::vector paths; + std::vector modes; + // Whether the snapshot was captured virtual-refresh-rate-aware; the apply + // flags must match the query flags. + bool vrr_aware = false; + + bool valid() const { return !paths.empty(); } +}; + // Windows-runner-internal boundary for deterministic crash-recovery tests. // Production uses the Win32/registry implementation in display_mode_manager.cpp. class DisplayRecoveryBackend { @@ -65,7 +80,8 @@ class DisplayModeManager { // Get the current display mode. DisplayMode GetCurrentMode(HWND window); - // Save the current mode for later restoration. + // Save the current mode (legacy DEVMODE plus a CCD topology snapshot) for + // later restoration. void SaveOriginalMode(HWND window); // Change the display mode (refresh rate and/or resolution). @@ -73,7 +89,9 @@ class DisplayModeManager { // Returns true on success. bool SetDisplayMode(HWND window, DWORD width, DWORD height, DWORD refresh_rate); - // Restore the previously saved display mode. + // Restore the previously saved display mode. Prefers re-applying the saved + // CCD snapshot (which keeps a Dynamic Refresh Rate selection intact); falls + // back to the legacy DEVMODE restore. bool RestoreOriginalMode(HWND window); // Returns true if a mode change has been applied (and not yet restored). @@ -159,9 +177,11 @@ class DisplayModeManager { // the recorded display rather than the window's current monitor. static bool IsHDREnabledForTarget(const DisplayConfigId& target); - // Stored original mode for restoration. + // Stored original mode for restoration. The snapshot is the preferred + // restore source; the DEVMODE is the fallback. std::wstring original_device_name_; DEVMODEW original_devmode_ = {}; + DisplayConfigSnapshot original_config_; bool mode_changed_ = false; // Stored original HDR state for restoration. diff --git a/windows/runner/mpv/sdk_26100.h b/windows/runner/mpv/sdk_26100.h index ececc142e..8c49fa872 100644 --- a/windows/runner/mpv/sdk_26100.h +++ b/windows/runner/mpv/sdk_26100.h @@ -57,4 +57,16 @@ typedef struct _DISPLAYCONFIG_SET_HDR_STATE { #endif // NTDDI_WIN11_GE +// DisplayConfig virtual-refresh-rate awareness flags for Dynamic Refresh Rate +// (Win11 22H2+, SDK 10.0.22621.0+). Guarded individually because, unlike the +// 24H2 structs above, older SDKs (10.0.19041.0) lack them while 22621 has +// them without NTDDI_WIN11_GE. +#ifndef QDC_VIRTUAL_REFRESH_RATE_AWARE +#define QDC_VIRTUAL_REFRESH_RATE_AWARE 0x00000040 +#endif + +#ifndef SDC_VIRTUAL_REFRESH_RATE_AWARE +#define SDC_VIRTUAL_REFRESH_RATE_AWARE 0x00020000 +#endif + #endif // SDK_26100_H_