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:
@@ -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, ¤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) !=
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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_
|
||||
|
||||
Reference in New Issue
Block a user