mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-13 20:25:53 +08:00
[fix] avoid blocking Windows shutdown and restore display modes
This commit is contained in:
1 parent
e47a3ac65c
commit
3b60a32ba7
13 files changed
+273
-221
No files matched your search
@@ -0,0 +1,72 @@
|
||||
#include "session_end_observer.h"
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
constexpr wchar_t kSessionWindowClass[] = L"CrossDesk.SessionEndObserver";
|
||||
}
|
||||
|
||||
SessionEndObserver::~SessionEndObserver() {
|
||||
if (window_) DestroyWindow(window_);
|
||||
}
|
||||
|
||||
bool SessionEndObserver::ReportError(const char* operation) {
|
||||
const DWORD error = GetLastError();
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
if (last_error_tick_ == 0 || now - last_error_tick_ >= 5000) {
|
||||
LOG_WARN("Windows session end observer {} failed, error={}", operation, error);
|
||||
last_error_tick_ = now;
|
||||
}
|
||||
SetLastError(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SessionEndObserver::Start() {
|
||||
if (window_) return true;
|
||||
WNDCLASSW klass{};
|
||||
klass.lpfnWndProc = WindowProc;
|
||||
klass.hInstance = GetModuleHandleW(nullptr);
|
||||
klass.lpszClassName = kSessionWindowClass;
|
||||
if (!RegisterClassW(&klass) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS)
|
||||
return ReportError("register window");
|
||||
// A hidden top-level window receives session-end broadcasts even when the
|
||||
// Slint windows are in the tray. HWND_MESSAGE does not receive them.
|
||||
window_ = CreateWindowExW(WS_EX_TOOLWINDOW, kSessionWindowClass, L"CrossDesk",
|
||||
WS_OVERLAPPED, 0, 0, 0, 0, nullptr, nullptr, klass.hInstance, this);
|
||||
if (!window_) return ReportError("create window");
|
||||
return true;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK SessionEndObserver::WindowProc(
|
||||
HWND window, UINT message, WPARAM wparam, LPARAM lparam) {
|
||||
if (message == WM_NCCREATE) {
|
||||
const auto* create = reinterpret_cast<CREATESTRUCTW*>(lparam);
|
||||
auto* observer = static_cast<SessionEndObserver*>(create->lpCreateParams);
|
||||
observer->window_ = window;
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(observer));
|
||||
}
|
||||
auto* observer = reinterpret_cast<SessionEndObserver*>(
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA));
|
||||
if (observer && message == WM_QUERYENDSESSION) {
|
||||
// Never stop work or veto shutdown here, including logoff/Restart Manager.
|
||||
LOG_INFO("Windows shutdown query accepted: flags=0x{:x}",
|
||||
static_cast<DWORD>(lparam));
|
||||
return TRUE;
|
||||
}
|
||||
if (observer && message == WM_ENDSESSION) {
|
||||
LOG_INFO("Windows session end: confirmed={}, flags=0x{:x}",
|
||||
wparam != FALSE, static_cast<DWORD>(lparam));
|
||||
// Let the GUI timer begin its normal cleanup only after confirmation.
|
||||
// Do not wait for network/device threads inside a Windows message handler.
|
||||
if (wparam != FALSE) observer->ending_ = true;
|
||||
return 0;
|
||||
}
|
||||
if (observer && message == WM_NCDESTROY) {
|
||||
observer->window_ = nullptr;
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
||||
}
|
||||
return DefWindowProcW(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-11-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SESSION_END_OBSERVER_H_
|
||||
#define _SESSION_END_OBSERVER_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Owned and pumped by the UI thread. A query is permission to end the session,
|
||||
// not a request to tear down transport/capture: another application may still
|
||||
// cancel shutdown. This observer never registers a shutdown-blocking reason.
|
||||
class SessionEndObserver {
|
||||
public:
|
||||
SessionEndObserver() = default;
|
||||
~SessionEndObserver();
|
||||
SessionEndObserver(const SessionEndObserver&) = delete;
|
||||
SessionEndObserver& operator=(const SessionEndObserver&) = delete;
|
||||
|
||||
bool Start();
|
||||
bool Ending() const { return ending_; }
|
||||
|
||||
private:
|
||||
bool ReportError(const char* operation);
|
||||
static LRESULT CALLBACK WindowProc(HWND window, UINT message,
|
||||
WPARAM wparam, LPARAM lparam);
|
||||
|
||||
HWND window_ = nullptr;
|
||||
bool ending_ = false;
|
||||
ULONGLONG last_error_tick_ = 0;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,121 +0,0 @@
|
||||
#include "shutdown_session_guard.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
constexpr wchar_t kShutdownWindowClass[] = L"CrossDesk.ShutdownSessionGuard";
|
||||
}
|
||||
|
||||
ShutdownSessionGuard::ShutdownSessionGuard(
|
||||
std::shared_ptr<std::atomic<bool>> connected)
|
||||
: connected_(std::move(connected)) {}
|
||||
|
||||
ShutdownSessionGuard::~ShutdownSessionGuard() {
|
||||
ClearReason();
|
||||
if (window_) DestroyWindow(window_);
|
||||
}
|
||||
|
||||
bool ShutdownSessionGuard::ReportError(const char* operation) {
|
||||
const DWORD error = GetLastError();
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
if (last_error_tick_ == 0 || now - last_error_tick_ >= 5000) {
|
||||
LOG_WARN("Windows shutdown session guard {} failed, error={}", operation, error);
|
||||
last_error_tick_ = now;
|
||||
}
|
||||
SetLastError(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ShutdownSessionGuard::Start() {
|
||||
if (window_) return true;
|
||||
WNDCLASSW klass{};
|
||||
klass.lpfnWndProc = WindowProc;
|
||||
klass.hInstance = GetModuleHandleW(nullptr);
|
||||
klass.lpszClassName = kShutdownWindowClass;
|
||||
if (!RegisterClassW(&klass) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS)
|
||||
return ReportError("register window");
|
||||
// A top-level window receives session-end broadcasts even when the Slint
|
||||
// windows are hidden in the tray. HWND_MESSAGE does not receive them.
|
||||
window_ = CreateWindowExW(WS_EX_TOOLWINDOW, kShutdownWindowClass, L"CrossDesk",
|
||||
WS_OVERLAPPED, 0, 0, 0, 0, nullptr, nullptr, klass.hInstance, this);
|
||||
if (!window_) return ReportError("create window");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShutdownSessionGuard::ClearReason() {
|
||||
if (!reason_registered_) return true;
|
||||
if (window_ && !ShutdownBlockReasonDestroy(window_))
|
||||
return ReportError("clear reason");
|
||||
reason_registered_ = false;
|
||||
LOG_INFO("Windows shutdown session guard released");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShutdownSessionGuard::SyncReason() {
|
||||
if (shutdown_allowed_ || !connected_ ||
|
||||
!connected_->load(std::memory_order_relaxed)) {
|
||||
return ClearReason();
|
||||
}
|
||||
if (reason_registered_) return true;
|
||||
if (!ShutdownBlockReasonCreate(window_, reason_.c_str()))
|
||||
return ReportError("register reason");
|
||||
reason_registered_ = true;
|
||||
LOG_INFO("Windows shutdown session guard armed for connected controller");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShutdownSessionGuard::Update(std::wstring_view reason) {
|
||||
if (!Start()) return false;
|
||||
if (std::wstring_view(reason_) != reason) {
|
||||
// Keep the old registration and text on failure so the next tick retries
|
||||
// removal before registering the newly selected language.
|
||||
if (!ClearReason()) return false;
|
||||
reason_.assign(reason);
|
||||
}
|
||||
return SyncReason();
|
||||
}
|
||||
|
||||
LRESULT CALLBACK ShutdownSessionGuard::WindowProc(
|
||||
HWND window, UINT message, WPARAM wparam, LPARAM lparam) {
|
||||
if (message == WM_NCCREATE) {
|
||||
const auto* create = reinterpret_cast<CREATESTRUCTW*>(lparam);
|
||||
auto* guard = static_cast<ShutdownSessionGuard*>(create->lpCreateParams);
|
||||
guard->window_ = window;
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(guard));
|
||||
}
|
||||
auto* guard = reinterpret_cast<ShutdownSessionGuard*>(
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA));
|
||||
if (guard && message == WM_QUERYENDSESSION) {
|
||||
const auto flags = static_cast<DWORD>(lparam);
|
||||
// "Shut down anyway", logoff and Restart Manager must remain available.
|
||||
if ((flags & (ENDSESSION_CRITICAL | ENDSESSION_LOGOFF | ENDSESSION_CLOSEAPP)) != 0)
|
||||
guard->shutdown_allowed_ = true;
|
||||
guard->SyncReason();
|
||||
const bool retain = !guard->shutdown_allowed_ && guard->reason_registered_ &&
|
||||
guard->connected_ && guard->connected_->load(std::memory_order_relaxed);
|
||||
LOG_INFO("Windows shutdown query: flags=0x{:x}, retain_remote_session={}", flags, retain);
|
||||
return retain ? FALSE : TRUE;
|
||||
}
|
||||
if (guard && message == WM_ENDSESSION) {
|
||||
LOG_INFO("Windows session end: confirmed={}, flags=0x{:x}",
|
||||
wparam != FALSE, static_cast<DWORD>(lparam));
|
||||
guard->shutdown_allowed_ = wparam != FALSE;
|
||||
guard->SyncReason();
|
||||
// A cancelled shutdown must leave transport, capture and input running.
|
||||
return 0;
|
||||
}
|
||||
if (guard && message == WM_NCDESTROY) {
|
||||
guard->ClearReason();
|
||||
// Destruction removes the native registration even if explicit removal
|
||||
// failed. A later window must not inherit the destroyed window's state.
|
||||
guard->reason_registered_ = false;
|
||||
guard->window_ = nullptr;
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
||||
}
|
||||
return DefWindowProcW(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-22
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SHUTDOWN_SESSION_GUARD_H_
|
||||
#define _SHUTDOWN_SESSION_GUARD_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Owned and pumped by the UI thread. The transport may revoke the shared
|
||||
// connection flag from another thread, including between UI ticks.
|
||||
class ShutdownSessionGuard {
|
||||
public:
|
||||
explicit ShutdownSessionGuard(std::shared_ptr<std::atomic<bool>> connected);
|
||||
~ShutdownSessionGuard();
|
||||
ShutdownSessionGuard(const ShutdownSessionGuard&) = delete;
|
||||
ShutdownSessionGuard& operator=(const ShutdownSessionGuard&) = delete;
|
||||
|
||||
bool Update(std::wstring_view reason);
|
||||
|
||||
private:
|
||||
bool Start();
|
||||
bool SyncReason();
|
||||
bool ClearReason();
|
||||
bool ReportError(const char* operation);
|
||||
static LRESULT CALLBACK WindowProc(HWND window, UINT message,
|
||||
WPARAM wparam, LPARAM lparam);
|
||||
|
||||
std::shared_ptr<std::atomic<bool>> connected_;
|
||||
HWND window_ = nullptr;
|
||||
std::wstring reason_;
|
||||
bool reason_registered_ = false;
|
||||
bool shutdown_allowed_ = false;
|
||||
ULONGLONG last_error_tick_ = 0;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -425,17 +425,22 @@ void ScreenCapturerWin::UpdateNativeCursor(ScreenCapturer* backend) {
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::PrepareUnattendedDisplay() {
|
||||
if (virtual_display_.active() || DetectHeadlessDesktop()) {
|
||||
physical_mode_recovery_pending_ = true;
|
||||
physical_mode_restore_attempts_ = 0;
|
||||
}
|
||||
if (!ForceHeadlessEnabled() && unattended_host_ && virtual_display_.active() &&
|
||||
QueryAttachedPhysicalDisplayMode().width > 0) {
|
||||
// The monitor may have returned between controller connections.
|
||||
virtual_display_.Release();
|
||||
RestorePhysicalDisplayMode();
|
||||
return false;
|
||||
}
|
||||
if (!unattended_host_ ||
|
||||
(!virtual_display_.active() && !DetectHeadlessDesktop())) return false;
|
||||
virtual_display_.PrepareAcquire();
|
||||
std::string error;
|
||||
if (!virtual_display_.Acquire(ResolveVirtualDisplayMode(), &error)) {
|
||||
if (!virtual_display_.Acquire(ResolveVirtualDisplayMode(false), &error)) {
|
||||
LOG_WARN("Cannot prepare unattended capture display: {}", error);
|
||||
return false;
|
||||
}
|
||||
@@ -778,7 +783,7 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
if (running_.load(std::memory_order_relaxed)) {
|
||||
return 0;
|
||||
}
|
||||
ResolveVirtualDisplayMode();
|
||||
ResolveVirtualDisplayMode(!physical_mode_recovery_pending_ && physical_mode_restore_attempts_ == 0);
|
||||
native_cursor_enabled_.store(native_cursor_default_.load());
|
||||
NotifyPrivacyCapture(false);
|
||||
|
||||
@@ -870,7 +875,7 @@ void ScreenCapturerWin::BeginVirtualDisplayProvisioning() {
|
||||
// A usbmmidd virtual monitor turns the phantom desktop into a real
|
||||
// display target, so the GPU backends and a chosen resolution apply.
|
||||
std::string error;
|
||||
if (virtual_display_.Acquire(ResolveVirtualDisplayMode(), &error)) {
|
||||
if (virtual_display_.Acquire(ResolveVirtualDisplayMode(false), &error)) {
|
||||
virtual_display_ready_.store(true, std::memory_order_release);
|
||||
} else if (error != "cancelled") {
|
||||
LOG_WARN("Windows capturer: no usbmmidd virtual display ({}); "
|
||||
@@ -1169,6 +1174,10 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) {
|
||||
const bool headless = DetectHeadlessDesktop(stalled,
|
||||
topology_changed);
|
||||
const bool owns_display = virtual_display_.active();
|
||||
if (headless || owns_display) {
|
||||
physical_mode_recovery_pending_ = true;
|
||||
physical_mode_restore_attempts_ = 0;
|
||||
}
|
||||
const bool provisioning =
|
||||
virtual_display_busy_.load(std::memory_order_acquire);
|
||||
if (!provisioning) {
|
||||
@@ -1181,7 +1190,8 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) {
|
||||
physical_binding_changed = !replacement_display_name_.empty() &&
|
||||
replacement_display_name_ != GetDisplayLabel(physical_device);
|
||||
}
|
||||
if (physical_present && (owns_display || physical_binding_changed)) {
|
||||
if (physical_present && (owns_display || physical_binding_changed ||
|
||||
physical_mode_recovery_pending_)) {
|
||||
// Debounce a hotplug before removing the fallback that is still carrying
|
||||
// the stream. This runs on the management thread, never a frame callback.
|
||||
if (physical_display_since_ == 0) physical_display_since_ = now;
|
||||
@@ -1194,7 +1204,7 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) {
|
||||
physical_display_since_ = 0;
|
||||
// Track user resolution changes during capture, not only process startup.
|
||||
// Resolve ignores phantom, RDP and virtual-display clone modes.
|
||||
ResolveVirtualDisplayMode();
|
||||
ResolveVirtualDisplayMode(!physical_mode_recovery_pending_ && physical_mode_restore_attempts_ == 0);
|
||||
}
|
||||
bool target_ready = false;
|
||||
if (owns_display) {
|
||||
@@ -1222,6 +1232,23 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) {
|
||||
BeginVirtualDisplayProvisioning();
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::RestorePhysicalDisplayMode() {
|
||||
if (!physical_mode_recovery_pending_ || ForceHeadlessEnabled()) return;
|
||||
std::string error;
|
||||
if (RestoreAttachedPhysicalDisplayModes(&error)) {
|
||||
physical_mode_recovery_pending_ = false;
|
||||
physical_mode_restore_attempts_ = 0;
|
||||
} else {
|
||||
++physical_mode_restore_attempts_;
|
||||
LOG_WARN("Physical display mode recovery attempt {} failed: {}",
|
||||
physical_mode_restore_attempts_, error);
|
||||
// Retry transient EDID/topology failures, but do not continually interrupt
|
||||
// capture or fight a user setting if a driver cannot apply its native mode.
|
||||
if (physical_mode_restore_attempts_ >= 3)
|
||||
physical_mode_recovery_pending_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::RestorePhysicalDisplay() {
|
||||
JoinVirtualDisplayThread();
|
||||
if (!running_.load(std::memory_order_relaxed)) return;
|
||||
@@ -1231,6 +1258,7 @@ void ScreenCapturerWin::RestorePhysicalDisplay() {
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
virtual_display_ready_.store(false, std::memory_order_release);
|
||||
virtual_display_.Release();
|
||||
RestorePhysicalDisplayMode();
|
||||
// Windows may renumber outputs after the unplug. Requery and bind the real
|
||||
// screen to the same registered wire slot, including multi-monitor hotplugs.
|
||||
std::wstring device;
|
||||
|
||||
@@ -117,6 +117,10 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
std::atomic<bool> virtual_display_busy_{false};
|
||||
ULONGLONG last_virtual_adoption_tick_ = 0;
|
||||
ULONGLONG physical_display_since_ = 0;
|
||||
// A phantom desktop can return on the same DISPLAYn without ever acquiring
|
||||
// usbmmidd. Remember that transition independently of virtual ownership.
|
||||
bool physical_mode_recovery_pending_ = false;
|
||||
unsigned physical_mode_restore_attempts_ = 0;
|
||||
// Protected by alias_mutex_. The virtual screen and then the returning
|
||||
// physical screen replace the same active wire slot explicitly.
|
||||
std::string replacement_display_name_;
|
||||
@@ -154,6 +158,7 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
void CheckCaptureProgress(ULONGLONG now);
|
||||
void CheckDisplayPresence(ULONGLONG now);
|
||||
void RestorePhysicalDisplay();
|
||||
void RestorePhysicalDisplayMode();
|
||||
// Virtual display provisioning runs off the caller's thread.
|
||||
void JoinVirtualDisplayThread();
|
||||
void BeginVirtualDisplayProvisioning();
|
||||
|
||||
@@ -22,8 +22,9 @@ inline constexpr DWORD WindowsShutdownLevel(WindowsShutdownRole role) {
|
||||
}
|
||||
|
||||
inline DWORD ConfigureWindowsShutdownOrder(WindowsShutdownRole role) {
|
||||
// Change only the order. The GUI's session guard separately defers shutdown
|
||||
// while a controller is connected; a confirmed shutdown still ends CrossDesk.
|
||||
// Change only the order, without vetoing shutdown or extending its timeout.
|
||||
// SCM shutdown notifications still govern the service-owned host/helpers;
|
||||
// these levels do not override an explicit stop from their owner.
|
||||
if (SetProcessShutdownParameters(WindowsShutdownLevel(role), 0))
|
||||
return ERROR_SUCCESS;
|
||||
const DWORD error = GetLastError();
|
||||
|
||||
@@ -237,7 +237,14 @@ void SaveRememberedMode(HKEY root, const RememberedDisplayMode& record) {
|
||||
RegCloseKey(key);
|
||||
}
|
||||
|
||||
std::vector<std::wstring> ActivePhysicalDisplaySources(bool* queried) {
|
||||
struct PhysicalDisplayTarget {
|
||||
std::wstring device;
|
||||
std::wstring monitor_path;
|
||||
DISPLAYCONFIG_PATH_TARGET_INFO target;
|
||||
bool cloned = false;
|
||||
};
|
||||
|
||||
std::vector<PhysicalDisplayTarget> ActivePhysicalDisplays(bool* queried) {
|
||||
*queried = false;
|
||||
for (int attempt = 0; attempt < 4; ++attempt) {
|
||||
UINT32 path_count = 0, mode_count = 0;
|
||||
@@ -252,7 +259,7 @@ std::vector<std::wstring> ActivePhysicalDisplaySources(bool* queried) {
|
||||
paths.data(), &mode_count, modes.data(), nullptr);
|
||||
if (result == ERROR_INSUFFICIENT_BUFFER) continue;
|
||||
if (result != ERROR_SUCCESS) break;
|
||||
std::vector<std::wstring> sources;
|
||||
std::vector<PhysicalDisplayTarget> displays;
|
||||
for (UINT32 i = 0; i < path_count; ++i) {
|
||||
const auto& path = paths[i];
|
||||
if (!(path.flags & DISPLAYCONFIG_PATH_ACTIVE) ||
|
||||
@@ -268,10 +275,18 @@ std::vector<std::wstring> ActivePhysicalDisplaySources(bool* queried) {
|
||||
DISPLAYCONFIG_SOURCE_DEVICE_NAME source{};
|
||||
source.header = {DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, sizeof(source),
|
||||
path.sourceInfo.adapterId, path.sourceInfo.id};
|
||||
if (DisplayConfigGetDeviceInfo(&source.header) == ERROR_SUCCESS)
|
||||
sources.emplace_back(source.viewGdiDeviceName);
|
||||
if (DisplayConfigGetDeviceInfo(&source.header) != ERROR_SUCCESS) continue;
|
||||
const bool cloned = std::count_if(paths.begin(), paths.begin() + path_count,
|
||||
[&](const DISPLAYCONFIG_PATH_INFO& other) {
|
||||
return (other.flags & DISPLAYCONFIG_PATH_ACTIVE) &&
|
||||
other.sourceInfo.id == path.sourceInfo.id &&
|
||||
other.sourceInfo.adapterId.LowPart == path.sourceInfo.adapterId.LowPart &&
|
||||
other.sourceInfo.adapterId.HighPart == path.sourceInfo.adapterId.HighPart;
|
||||
}) > 1;
|
||||
displays.push_back({source.viewGdiDeviceName, target.monitorDevicePath,
|
||||
path.targetInfo, cloned});
|
||||
}
|
||||
return sources;
|
||||
return displays;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -284,7 +299,7 @@ VirtualDisplayMode QueryAttachedPhysicalDisplayMode(std::wstring* device_name) {
|
||||
// RDP creates a display mode for that client, not the console's next boot.
|
||||
if (!GetSystemMetrics(SM_REMOTESESSION)) {
|
||||
bool queried = false;
|
||||
const auto sources = ActivePhysicalDisplaySources(&queried);
|
||||
const auto sources = ActivePhysicalDisplays(&queried);
|
||||
for (DWORD index = 0;; ++index) {
|
||||
DISPLAY_DEVICEW adapter{sizeof(adapter)};
|
||||
if (!EnumDisplayDevicesW(nullptr, index, &adapter, 0)) break;
|
||||
@@ -296,8 +311,8 @@ VirtualDisplayMode QueryAttachedPhysicalDisplayMode(std::wstring* device_name) {
|
||||
// real target disappears. Require the corresponding available CCD path
|
||||
// when CCD is accessible; an IDD path must not validate a phantom GPU.
|
||||
if (queried && std::none_of(sources.begin(), sources.end(),
|
||||
[&](const std::wstring& source) {
|
||||
return _wcsicmp(source.c_str(), adapter.DeviceName) == 0;
|
||||
[&](const PhysicalDisplayTarget& source) {
|
||||
return _wcsicmp(source.device.c_str(), adapter.DeviceName) == 0;
|
||||
})) continue;
|
||||
bool present = false;
|
||||
for (DWORD child = 0; !present; ++child) {
|
||||
@@ -325,7 +340,88 @@ VirtualDisplayMode QueryAttachedPhysicalDisplayMode(std::wstring* device_name) {
|
||||
return attached;
|
||||
}
|
||||
|
||||
VirtualDisplayMode ResolveVirtualDisplayMode() {
|
||||
bool RestoreAttachedPhysicalDisplayModes(std::string* error) {
|
||||
if (GetSystemMetrics(SM_REMOTESESSION) || CountUsbmmiddMonitors() != 0) {
|
||||
SetError(error, "physical_display_topology_not_ready");
|
||||
return false;
|
||||
}
|
||||
bool queried = false;
|
||||
const auto displays = ActivePhysicalDisplays(&queried);
|
||||
if (displays.empty()) {
|
||||
SetError(error, "physical_display_not_ready");
|
||||
return false;
|
||||
}
|
||||
bool restored = true;
|
||||
for (const auto& display : displays) {
|
||||
// One source mode affects every cloned target. Do not force a native 4K
|
||||
// mode onto another real/third-party screen sharing a 1080p clone source.
|
||||
if (display.cloned) {
|
||||
LOG_INFO("Physical display '{}': keeping the shared clone mode", Narrow(display.device));
|
||||
continue;
|
||||
}
|
||||
DISPLAYCONFIG_TARGET_PREFERRED_MODE preferred{};
|
||||
preferred.header = {DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_PREFERRED_MODE,
|
||||
sizeof(preferred), display.target.adapterId, display.target.id};
|
||||
const LONG preferred_error = DisplayConfigGetDeviceInfo(&preferred.header);
|
||||
DEVMODEW mode{};
|
||||
mode.dmSize = sizeof(mode);
|
||||
if (preferred_error != ERROR_SUCCESS ||
|
||||
!ValidDisplayMode({static_cast<int>(preferred.width), static_cast<int>(preferred.height)}) ||
|
||||
!EnumDisplaySettingsExW(display.device.c_str(), ENUM_CURRENT_SETTINGS, &mode, 0)) {
|
||||
SetError(error, "physical_display_preferred_mode_unavailable:" + std::to_string(preferred_error));
|
||||
restored = false;
|
||||
continue;
|
||||
}
|
||||
DWORD width = preferred.width, height = preferred.height;
|
||||
if (mode.dmDisplayOrientation == DMDO_90 || mode.dmDisplayOrientation == DMDO_270)
|
||||
std::swap(width, height);
|
||||
if (mode.dmPelsWidth == width && mode.dmPelsHeight == height) continue;
|
||||
const DWORD old_width = mode.dmPelsWidth, old_height = mode.dmPelsHeight;
|
||||
mode.dmPelsWidth = width;
|
||||
mode.dmPelsHeight = height;
|
||||
// Do not reuse the phantom's timing, change the primary screen/position,
|
||||
// or write this temporary recovery into the user's persistent settings.
|
||||
mode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL | DM_DISPLAYORIENTATION;
|
||||
LONG result = ChangeDisplaySettingsExW(display.device.c_str(), &mode, nullptr, CDS_TEST, nullptr);
|
||||
if (result == DISP_CHANGE_SUCCESSFUL) {
|
||||
// Display numbers may be recycled during hotplug. Validate the target
|
||||
// identity again after CDS_TEST before changing this source's mode.
|
||||
const auto current = ActivePhysicalDisplays(&queried);
|
||||
const bool same_target = std::any_of(current.begin(), current.end(),
|
||||
[&](const PhysicalDisplayTarget& target) {
|
||||
return !target.cloned && target.device == display.device &&
|
||||
target.monitor_path == display.monitor_path &&
|
||||
target.target.id == display.target.id &&
|
||||
target.target.adapterId.LowPart == display.target.adapterId.LowPart &&
|
||||
target.target.adapterId.HighPart == display.target.adapterId.HighPart;
|
||||
});
|
||||
if (!same_target || CountUsbmmiddMonitors() != 0) {
|
||||
SetError(error, "physical_display_changed_during_restore");
|
||||
restored = false;
|
||||
continue;
|
||||
}
|
||||
result = ChangeDisplaySettingsExW(display.device.c_str(), &mode, nullptr, 0, nullptr);
|
||||
}
|
||||
if (result != DISP_CHANGE_SUCCESSFUL) {
|
||||
SetError(error, "physical_display_mode_change_failed:" + std::to_string(result));
|
||||
restored = false;
|
||||
continue;
|
||||
}
|
||||
DEVMODEW applied{};
|
||||
applied.dmSize = sizeof(applied);
|
||||
if (!EnumDisplaySettingsExW(display.device.c_str(), ENUM_CURRENT_SETTINGS, &applied, 0) ||
|
||||
applied.dmPelsWidth != width || applied.dmPelsHeight != height) {
|
||||
SetError(error, "physical_display_mode_not_applied");
|
||||
restored = false;
|
||||
continue;
|
||||
}
|
||||
LOG_INFO("Physical display '{}' restored from {}x{} to preferred {}x{}",
|
||||
Narrow(display.device), old_width, old_height, width, height);
|
||||
}
|
||||
return restored;
|
||||
}
|
||||
|
||||
VirtualDisplayMode ResolveVirtualDisplayMode(bool remember_attached) {
|
||||
std::lock_guard lock(remembered_mode_mutex);
|
||||
RememberedDisplayMode saved = last_observed_mode;
|
||||
for (HKEY root : {HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER}) {
|
||||
@@ -336,7 +432,7 @@ VirtualDisplayMode ResolveVirtualDisplayMode() {
|
||||
}
|
||||
// A reattached physical screen may temporarily clone the fallback monitor.
|
||||
// Do not let that temporary desktop mode replace the original observation.
|
||||
const auto attached = CountUsbmmiddMonitors() == 0
|
||||
const auto attached = remember_attached && CountUsbmmiddMonitors() == 0
|
||||
? QueryAttachedPhysicalDisplayMode()
|
||||
: VirtualDisplayMode{};
|
||||
if (ValidDisplayMode(attached)) {
|
||||
|
||||
@@ -41,11 +41,15 @@ UsbmmiddStatus QueryUsbmmiddStatus();
|
||||
|
||||
// Remember an attached display's current pixel mode for GUI/SYSTEM startup.
|
||||
// Phantom targets and our own fallback monitor must not overwrite that mode.
|
||||
VirtualDisplayMode ResolveVirtualDisplayMode();
|
||||
VirtualDisplayMode ResolveVirtualDisplayMode(bool remember_attached = true);
|
||||
// A real active monitor's current mode, or {0, 0} for headless/RDP desktops.
|
||||
// This probe does not update the remembered mode or change display settings.
|
||||
VirtualDisplayMode QueryAttachedPhysicalDisplayMode(
|
||||
std::wstring* device_name = nullptr);
|
||||
// After a headless/virtual-display handoff, recover each real screen's native
|
||||
// preferred pixel mode. Keep rotation and layout; never persist fallback modes.
|
||||
// Returns false while topology/EDID is settling, so the owner can retry.
|
||||
bool RestoreAttachedPhysicalDisplayModes(std::string* error = nullptr);
|
||||
|
||||
// Adapters attached to the desktop, i.e. monitors currently plugged in.
|
||||
int CountUsbmmiddMonitors();
|
||||
|
||||
Reference in new issue
Block a user