fix(windows): preserve Dynamic Refresh Rate when restoring display mode

On a Dynamic Refresh Rate display (e.g. Surface 120Hz panels), stopping
playback with Match Refresh Rate enabled left Windows pinned to a fixed
60Hz with DRR disabled: the legacy EnumDisplaySettingsW capture only sees
the DRR base rate, and a DEVMODE ChangeDisplaySettingsExW restore cannot
express the CCD-level DISPLAYCONFIG_PATH_BOOST_REFRESH_RATE selection.

Capture a virtual-refresh-rate-aware QueryDisplayConfig snapshot of the
topology before the first display mutation and restore it through
SetDisplayConfig (saving to the database to repair the 24/48/60Hz
registry-workaround write-back), keeping the DEVMODE path as fallback.
The HDR toggle mode dances re-apply the same kind of snapshot, and crash
recovery returns to the persisted display configuration via
SDC_USE_DATABASE_CURRENT, verified against the recorded mode before the
legacy restore takes over.

close #2055
This commit is contained in:
edde746
2026-08-23 07:34:56 +02:00
parent 1a9d2686c9
commit 802d47e980
3 changed files with 183 additions and 37 deletions
+148 -34
View File
@@ -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<DISPLAYCONFIG_PATH_INFO>& paths, std::vector<DISPLAYCONFIG_MODE_INFO>& 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<DISPLAYCONFIG_PATH_INFO> paths = snapshot.paths;
std::vector<DISPLAYCONFIG_MODE_INFO> 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<UINT32>(paths.size()), paths.data(), static_cast<UINT32>(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<DISPLAYCONFIG_PATH_INFO> DisplayModeManager::GetDisplayConfigPaths() {
UINT32 path_count = 0;
UINT32 mode_count = 0;
std::vector<DISPLAYCONFIG_PATH_INFO> paths;
std::vector<DISPLAYCONFIG_MODE_INFO> 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, &current) &&
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) !=
+23 -3
View File
@@ -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<DISPLAYCONFIG_PATH_INFO> paths;
std::vector<DISPLAYCONFIG_MODE_INFO> 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.
+12
View File
@@ -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_