diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp index 860dc4d7..b92a62df 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp @@ -108,14 +108,18 @@ bool BackendCursorCapture(ScreenCapturer* capturer, bool requested) { // as if it had none until a usbmmidd monitor is plugged, so the virtual // display path can be exercised without unplugging cables. Once the virtual // monitor is attached the real probe takes over again. -bool ForcedHeadlessForTesting() { +bool ForceHeadlessEnabled() { static const bool forced = [] { wchar_t value[8] = {0}; const DWORD length = GetEnvironmentVariableW(L"CROSSDESK_FORCE_HEADLESS", value, 8); return length > 0 && length < 8 && value[0] == L'1'; }(); - return forced && CountUsbmmiddMonitors() == 0; + return forced; +} + +bool ForcedHeadlessForTesting() { + return ForceHeadlessEnabled() && CountUsbmmiddMonitors() == 0; } // Detect phantom/headless topology for virtual-display provisioning. @@ -421,6 +425,12 @@ void ScreenCapturerWin::UpdateNativeCursor(ScreenCapturer* backend) { } bool ScreenCapturerWin::PrepareUnattendedDisplay() { + if (!ForceHeadlessEnabled() && unattended_host_ && virtual_display_.active() && + QueryAttachedPhysicalDisplayMode().width > 0) { + // The monitor may have returned between controller connections. + virtual_display_.Release(); + return false; + } if (!unattended_host_ || (!virtual_display_.active() && !DetectHeadlessDesktop())) return false; virtual_display_.PrepareAcquire(); @@ -431,8 +441,8 @@ bool ScreenCapturerWin::PrepareUnattendedDisplay() { } const auto name = GetDisplayLabel(FindUsbmmiddDisplayDeviceName()); std::lock_guard lock(alias_mutex_); - virtual_display_name_ = name; - virtual_display_slot_ = name.empty() ? -1 : 0; + replacement_display_name_ = name; + replacement_display_slot_ = name.empty() ? -1 : 0; return !name.empty(); } @@ -494,7 +504,7 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { } else { // During IDD takeover, never relabel a late frame from the old output // as the virtual display's stream. - if (!virtual_display_name_.empty()) return; + if (!replacement_display_name_.empty()) return; // Unknown backend labels are presentation data, not wire IDs. // Resolve them through the selected logical display instead. mapped_stream_id.clear(); @@ -566,7 +576,7 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { BuildCanonicalFromImpl(); monitor_index_.store(0, std::memory_order_relaxed); if (prepared_display) { - const auto name = virtual_display_name_; + const auto name = replacement_display_name_; const auto displays = impl_->GetDisplayInfoList(); if (std::none_of(displays.begin(), displays.end(), [&name](const DisplayInfo& display) { return display.name == name && display.width > 0 && display.height > 0; @@ -702,7 +712,7 @@ bool ScreenCapturerWin::TryStartBackend( std::string required_display; { std::lock_guard lock(alias_mutex_); - required_display = virtual_display_name_; + required_display = replacement_display_name_; } if (ret == 0 && !required_display.empty()) { const auto displays = candidate->GetDisplayInfoList(); @@ -738,7 +748,7 @@ bool ScreenCapturerWin::TryStartBackend( committed->store(true, std::memory_order_release); LOG_INFO("Windows capturer: started {}", name); if (!required_display.empty()) - LOG_INFO("Windows capturer: virtual display '{}' captured via {}", + LOG_INFO("Windows capturer: replacement display '{}' captured via {}", required_display, name); return true; } @@ -768,6 +778,7 @@ int ScreenCapturerWin::Start(bool show_cursor) { if (running_.load(std::memory_order_relaxed)) { return 0; } + ResolveVirtualDisplayMode(); native_cursor_enabled_.store(native_cursor_default_.load()); NotifyPrivacyCapture(false); @@ -790,8 +801,8 @@ int ScreenCapturerWin::Start(bool show_cursor) { // Reconcile the live target, not the display name cached at process startup. if (!PrepareUnattendedDisplay()) { std::lock_guard lock(alias_mutex_); - virtual_display_name_.clear(); - virtual_display_slot_ = -1; + replacement_display_name_.clear(); + replacement_display_slot_ = -1; } DetectHeadlessDesktop(false, true); virtual_display_ready_.store(false, std::memory_order_relaxed); @@ -799,6 +810,7 @@ int ScreenCapturerWin::Start(bool show_cursor) { last_display_probe_tick_ = 0; last_capture_retry_tick_ = 0; last_virtual_adoption_tick_ = 0; + physical_display_since_ = 0; headless_recovery_.Reset(); impl_->Destroy(); if (!StartPreferredBackend(requested_monitor)) { @@ -903,8 +915,8 @@ void ScreenCapturerWin::MaybeAdoptVirtualDisplay() { if (previous) previous->Destroy(); { std::lock_guard lock(alias_mutex_); - virtual_display_name_ = virtual_name; - virtual_display_slot_ = current_monitor; + replacement_display_name_ = virtual_name; + replacement_display_slot_ = current_monitor; } if (StartPreferredBackend(current_monitor)) { if (!IsCurrentSessionUserDesktopActive()) impl_->Stop(); @@ -915,8 +927,8 @@ void ScreenCapturerWin::MaybeAdoptVirtualDisplay() { LOG_ERROR("Windows capturer: no backend could start on usbmmidd; will retry"); { std::lock_guard lock(alias_mutex_); - virtual_display_name_.clear(); - virtual_display_slot_ = -1; + replacement_display_name_.clear(); + replacement_display_slot_ = -1; } { std::lock_guard lock(impl_mutex_); @@ -1016,21 +1028,20 @@ void ScreenCapturerWin::RebuildAliasesFromImpl(bool preserve_backend_slots) { stream_id_alias_.clear(); auto current = impl_->GetDisplayInfoList(); if (current.empty() || canonical_displays_.empty()) return; - if (!virtual_display_name_.empty() && virtual_display_slot_ >= 0 && - virtual_display_slot_ < static_cast(canonical_displays_.size())) { - // A headless session deliberately replaces its selected output with IDD. - // Bind by device name, not primary status/enumeration order: the phantom - // NVIDIA output can remain first even after usbmmidd is ready. + if (!replacement_display_name_.empty() && replacement_display_slot_ >= 0 && + replacement_display_slot_ < static_cast(canonical_displays_.size())) { + // Bind IDD (or the returning physical screen) to the same wire slot. + // The first enumerated output may still be a phantom NVIDIA desktop. handle_to_canonical_index_.clear(); - const int i = BindVirtualDisplayToStream(current, virtual_display_name_, - virtual_display_slot_, &canonical_displays_, &backend_to_canonical_); + const int i = BindVirtualDisplayToStream(current, replacement_display_name_, + replacement_display_slot_, &canonical_displays_, &backend_to_canonical_); if (i >= 0) { - const auto stream = MakeDisplayStreamId(virtual_display_slot_); + const auto stream = MakeDisplayStreamId(replacement_display_slot_); stream_id_alias_[MakeDisplayStreamId(i)] = stream; stream_id_alias_[current[i].name] = stream; if (current[i].handle) - handle_to_canonical_index_[current[i].handle] = virtual_display_slot_; - monitor_index_.store(virtual_display_slot_, std::memory_order_relaxed); + handle_to_canonical_index_[current[i].handle] = replacement_display_slot_; + monitor_index_.store(replacement_display_slot_, std::memory_order_relaxed); } return; } @@ -1158,15 +1169,42 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) { const bool headless = DetectHeadlessDesktop(stalled, topology_changed); const bool owns_display = virtual_display_.active(); + const bool provisioning = + virtual_display_busy_.load(std::memory_order_acquire); + if (!provisioning) { + std::wstring physical_device; + const bool physical_present = !ForceHeadlessEnabled() && + QueryAttachedPhysicalDisplayMode(&physical_device).width > 0; + bool physical_binding_changed = false; + if (!owns_display && physical_present) { + std::lock_guard lock(alias_mutex_); + physical_binding_changed = !replacement_display_name_.empty() && + replacement_display_name_ != GetDisplayLabel(physical_device); + } + if (physical_present && (owns_display || physical_binding_changed)) { + // 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; + if (now - physical_display_since_ >= 1000) { + RestorePhysicalDisplay(); + physical_display_since_ = 0; + } + return; + } + physical_display_since_ = 0; + // Track user resolution changes during capture, not only process startup. + // Resolve ignores phantom, RDP and virtual-display clone modes. + ResolveVirtualDisplayMode(); + } bool target_ready = false; if (owns_display) { const auto current = GetDisplayLabel(FindUsbmmiddDisplayDeviceName()); std::lock_guard lock(alias_mutex_); - target_ready = !current.empty() && current == virtual_display_name_; + target_ready = !current.empty() && current == replacement_display_name_; } if (!headless_recovery_.ShouldProvision( now, headless || (owns_display && !target_ready), - virtual_display_busy_.load(std::memory_order_acquire), target_ready)) return; + provisioning, target_ready)) return; if (!running_.load(std::memory_order_relaxed)) return; headless_recovery_.Attempted(now); // The current backend has lost its usable target. Continue the same order @@ -1184,6 +1222,33 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) { BeginVirtualDisplayProvisioning(); } +void ScreenCapturerWin::RestorePhysicalDisplay() { + JoinVirtualDisplayThread(); + if (!running_.load(std::memory_order_relaxed)) return; + LOG_INFO("Windows capturer: physical monitor returned; restoring physical capture"); + NotifyPrivacyCapture(false); + if (impl_) impl_->Stop(); + StopSecureDesktopSharedCapture(secure_shared_session_id_); + virtual_display_ready_.store(false, std::memory_order_release); + virtual_display_.Release(); + // 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; + QueryAttachedPhysicalDisplayMode(&device); + const int slot = monitor_index_.load(std::memory_order_relaxed); + { + std::lock_guard lock(alias_mutex_); + replacement_display_name_ = GetDisplayLabel(device); + replacement_display_slot_ = replacement_display_name_.empty() ? -1 : slot; + } + headless_recovery_.Reset(); + if (!running_.load(std::memory_order_relaxed)) return; + const bool started = StartPreferredBackend(slot); + if (started && !IsCurrentSessionUserDesktopActive()) impl_->Stop(); + NotifyPrivacyCapture(started); + if (!started) ReleaseNativeCursor(); +} + void ScreenCapturerWin::CheckCaptureProgress(ULONGLONG now) { if (!running_.load() || !impl_) return; if (dynamic_cast(impl_.get()) && backend_frame_received_.load() && diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h index 25b490b8..561a11f8 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h @@ -108,7 +108,7 @@ class ScreenCapturerWin : public ScreenCapturer { bool applied_show_cursor_ = true; // GUI sessions provision on a worker and release after Stop(). Machine // hosts prepare before signaling, revalidate before capture, and retain - // the monitor until Destroy(). Recovery also handles lost login targets. + // the monitor until a physical screen returns or Destroy() runs. // The management thread adopts the output once virtual_display_ready_ is // set, trying the same ordered backends on the replacement target. VirtualDisplayProvisioner virtual_display_; @@ -116,10 +116,11 @@ class ScreenCapturerWin : public ScreenCapturer { std::atomic virtual_display_ready_{false}; std::atomic virtual_display_busy_{false}; ULONGLONG last_virtual_adoption_tick_ = 0; - // Protected by alias_mutex_. A session-created display replaces the active - // wire slot explicitly, even if Windows retains the old phantom output. - std::string virtual_display_name_; - int virtual_display_slot_ = -1; + ULONGLONG physical_display_since_ = 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_; + int replacement_display_slot_ = -1; std::atomic monitor_index_{0}; std::atomic secure_desktop_capture_active_{false}; std::atomic post_secure_desktop_waiting_for_frame_{false}; @@ -152,6 +153,7 @@ class ScreenCapturerWin : public ScreenCapturer { bool RestartCaptureBackendAfterSecureDesktop(); void CheckCaptureProgress(ULONGLONG now); void CheckDisplayPresence(ULONGLONG now); + void RestorePhysicalDisplay(); // Virtual display provisioning runs off the caller's thread. void JoinVirtualDisplayThread(); void BeginVirtualDisplayProvisioning(); diff --git a/apps/desktop/src/platform/windows/screen_capturer/virtual_display_binding.h b/apps/desktop/src/platform/windows/screen_capturer/virtual_display_binding.h index 6db79d89..14d25331 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/virtual_display_binding.h +++ b/apps/desktop/src/platform/windows/screen_capturer/virtual_display_binding.h @@ -14,8 +14,8 @@ namespace crossdesk { -// Only for headless takeover. Preserve the registered wire slot while choosing -// IDD by identity; primary status and the old phantom's slot are irrelevant. +// Preserve the registered wire slot across headless takeover and physical +// recovery. Select the replacement by identity, not the phantom's position. inline int BindVirtualDisplayToStream(const std::vector& current, const std::string& name, int slot, std::vector* canonical, @@ -41,4 +41,4 @@ inline int BindVirtualDisplayToStream(const std::vector& current, } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp b/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp index b6d975ee..8755950c 100644 --- a/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp +++ b/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp @@ -14,10 +14,12 @@ // clang-format on #include +#include #include #include #include "rd_log.h" +#include "../screen_capturer/display_presence.h" namespace crossdesk { @@ -189,30 +191,120 @@ std::vector EnumerateModes(const std::wstring& device) { return modes; } +bool ValidDisplayMode(VirtualDisplayMode mode) { + return mode.width > 0 && mode.height > 0 && mode.width <= 16384 && + mode.height <= 16384; +} + +// One atomic registry value keeps the mode and its observation time together. +// HKCU works for a normal GUI; HKLM makes the same record available before +// login to the SYSTEM host. Pick the newest, not an arbitrarily preferred hive. +struct RememberedDisplayMode { + VirtualDisplayMode mode; + ULONGLONG observed_at = 0; +}; +constexpr wchar_t kRememberedModeValue[] = L"LastAttachedDisplayModeV2"; +std::mutex remembered_mode_mutex; +RememberedDisplayMode last_observed_mode; + +RememberedDisplayMode ReadRememberedMode(HKEY root) { + RememberedDisplayMode saved; + DWORD size = sizeof(saved); + if (RegGetValueW(root, kUsbmmiddInstallMarkerKey, kRememberedModeValue, + RRF_RT_REG_BINARY | RRF_SUBKEY_WOW6464KEY, nullptr, &saved, + &size) == ERROR_SUCCESS && size == sizeof(saved) && + ValidDisplayMode(saved.mode)) return saved; + // Migrate the packed value written by earlier versions. + DWORD packed = 0; + size = sizeof(packed); + if (RegGetValueW(root, kUsbmmiddInstallMarkerKey, L"LastAttachedDisplayMode", + RRF_RT_REG_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr, &packed, + &size) == ERROR_SUCCESS) { + VirtualDisplayMode mode{static_cast(packed >> 16), + static_cast(packed & 0xffff)}; + if (ValidDisplayMode(mode)) return {mode, 0}; + } + return {}; +} + +void SaveRememberedMode(HKEY root, const RememberedDisplayMode& record) { + HKEY key = nullptr; + if (RegCreateKeyExW(root, kUsbmmiddInstallMarkerKey, 0, nullptr, 0, + KEY_SET_VALUE | KEY_WOW64_64KEY, nullptr, &key, + nullptr) != ERROR_SUCCESS) return; + RegSetValueExW(key, kRememberedModeValue, 0, REG_BINARY, + reinterpret_cast(&record), sizeof(record)); + RegCloseKey(key); +} + +std::vector ActivePhysicalDisplaySources(bool* queried) { + *queried = false; + for (int attempt = 0; attempt < 4; ++attempt) { + UINT32 path_count = 0, mode_count = 0; + if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &path_count, + &mode_count) != ERROR_SUCCESS) break; + // Once CCD is accessible, a racing/failed topology query is inconclusive. + // Do not fall back to stale EnumDisplayDevices children in that window. + *queried = true; + std::vector paths((std::max)(path_count, 1u)); + std::vector modes((std::max)(mode_count, 1u)); + const LONG result = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &path_count, + paths.data(), &mode_count, modes.data(), nullptr); + if (result == ERROR_INSUFFICIENT_BUFFER) continue; + if (result != ERROR_SUCCESS) break; + std::vector sources; + for (UINT32 i = 0; i < path_count; ++i) { + const auto& path = paths[i]; + if (!(path.flags & DISPLAYCONFIG_PATH_ACTIVE) || + !path.targetInfo.targetAvailable || + path.targetInfo.outputTechnology == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INDIRECT_WIRED || + path.targetInfo.outputTechnology == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INDIRECT_VIRTUAL) + continue; + DISPLAYCONFIG_TARGET_DEVICE_NAME target{}; + target.header = {DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME, sizeof(target), + path.targetInfo.adapterId, path.targetInfo.id}; + if (DisplayConfigGetDeviceInfo(&target.header) != ERROR_SUCCESS || + !HasDisplayTargetIdentity(target)) continue; + 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); + } + return sources; + } + return {}; +} + } // namespace -VirtualDisplayMode ResolveVirtualDisplayMode() { - constexpr wchar_t value_name[] = L"LastAttachedDisplayMode"; - DWORD saved = 0, size = sizeof(saved); - if (RegGetValueW(HKEY_LOCAL_MACHINE, kUsbmmiddInstallMarkerKey, value_name, - RRF_RT_REG_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr, &saved, &size) != ERROR_SUCCESS) - saved = 0; - const auto valid = [](VirtualDisplayMode mode) { - return mode.width > 0 && mode.height > 0 && mode.width <= 16384 && mode.height <= 16384; - }; +VirtualDisplayMode QueryAttachedPhysicalDisplayMode(std::wstring* device_name) { + if (device_name) device_name->clear(); VirtualDisplayMode attached; // 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); for (DWORD index = 0;; ++index) { DISPLAY_DEVICEW adapter{sizeof(adapter)}; if (!EnumDisplayDevicesW(nullptr, index, &adapter, 0)) break; if (!(adapter.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) || + (adapter.StateFlags & (DISPLAY_DEVICE_MIRRORING_DRIVER | + DISPLAY_DEVICE_REMOTE)) || IsUsbmmiddAdapter(adapter.DeviceString, adapter.DeviceID)) continue; + // A converter can retain an active EnumDisplayDevices monitor after its + // 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; + })) continue; bool present = false; for (DWORD child = 0; !present; ++child) { DISPLAY_DEVICEW monitor{sizeof(monitor)}; if (!EnumDisplayDevicesW(adapter.DeviceName, child, &monitor, 0)) break; present = (monitor.StateFlags & DISPLAY_DEVICE_ACTIVE) != 0 && + monitor.DeviceID[0] != L'\0' && !ContainsCaseInsensitive(monitor.DeviceID, L"Default_Monitor"); } DEVMODEW mode{}; @@ -221,26 +313,51 @@ VirtualDisplayMode ResolveVirtualDisplayMode() { continue; const VirtualDisplayMode current{static_cast(mode.dmPelsWidth), static_cast(mode.dmPelsHeight)}; - if (!valid(current)) continue; - if (!valid(attached) || (adapter.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE)) attached = current; + if (!ValidDisplayMode(current)) continue; + if (!ValidDisplayMode(attached) || + (adapter.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE)) { + attached = current; + if (device_name) *device_name = adapter.DeviceName; + } if (adapter.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) break; } } - if (valid(attached)) { - const DWORD packed = (static_cast(attached.width) << 16) | - static_cast(attached.height); - HKEY key = nullptr; - if (packed != saved && RegCreateKeyExW(HKEY_LOCAL_MACHINE, kUsbmmiddInstallMarkerKey, - 0, nullptr, 0, KEY_SET_VALUE | KEY_WOW64_64KEY, nullptr, &key, nullptr) == ERROR_SUCCESS) { - if (RegSetValueExW(key, value_name, 0, REG_DWORD, - reinterpret_cast(&packed), sizeof(packed)) == ERROR_SUCCESS) - LOG_INFO("Remembered attached display mode: {}x{}", attached.width, attached.height); - RegCloseKey(key); - } - return attached; + return attached; +} + +VirtualDisplayMode ResolveVirtualDisplayMode() { + std::lock_guard lock(remembered_mode_mutex); + RememberedDisplayMode saved = last_observed_mode; + for (HKEY root : {HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER}) { + const auto record = ReadRememberedMode(root); + if (ValidDisplayMode(record.mode) && + (!ValidDisplayMode(saved.mode) || record.observed_at > saved.observed_at)) + saved = record; } - const VirtualDisplayMode previous{static_cast(saved >> 16), static_cast(saved & 0xffff)}; - return valid(previous) ? previous : kDefaultVirtualDisplayMode; + // 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 + ? QueryAttachedPhysicalDisplayMode() + : VirtualDisplayMode{}; + if (ValidDisplayMode(attached)) { + if (attached != saved.mode) { + FILETIME now{}; + GetSystemTimeAsFileTime(&now); + saved = {attached, (static_cast(now.dwHighDateTime) << 32) | + now.dwLowDateTime}; + LOG_INFO("Remembered attached display mode: {}x{}", attached.width, + attached.height); + } + // Also propagate a record first observed by another process/hive. Avoid + // repeated writes on the display probe's steady-state path. + for (HKEY root : {HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE}) { + const auto record = ReadRememberedMode(root); + if (record.mode != saved.mode || record.observed_at != saved.observed_at) + SaveRememberedMode(root, saved); + } + } + last_observed_mode = saved; // Still usable when neither hive is writable. + return ValidDisplayMode(saved.mode) ? saved.mode : kDefaultVirtualDisplayMode; } UsbmmiddPackage LocateUsbmmiddPackage() { @@ -384,6 +501,13 @@ bool ApplyUsbmmiddDisplayMode(VirtualDisplayMode requested, SetError(error, "usbmmidd_monitor_not_attached"); return false; } + // Recheck immediately before changing a mode: a physical monitor can return + // while the driver is being installed/plugged. A cloned topology could resize + // that monitor too; the capture owner will retire its fallback instead. + if (ValidDisplayMode(QueryAttachedPhysicalDisplayMode())) { + SetError(error, "physical_display_present"); + return false; + } VirtualDisplayMode chosen; if (!ChooseVirtualDisplayMode(EnumerateModes(device), requested, &chosen)) { SetError(error, "usbmmidd_no_display_modes"); @@ -404,7 +528,7 @@ bool ApplyUsbmmiddDisplayMode(VirtualDisplayMode requested, mode.dmPelsHeight = static_cast(chosen.height); mode.dmBitsPerPel = 32; const LONG result = ChangeDisplaySettingsExW(device.c_str(), &mode, nullptr, - CDS_UPDATEREGISTRY, nullptr); + 0, nullptr); if (result != DISP_CHANGE_SUCCESSFUL) { SetError(error, "usbmmidd_mode_change_failed:" + std::to_string(result)); return false; diff --git a/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.h b/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.h index a59f5c98..e24cd868 100644 --- a/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.h +++ b/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.h @@ -42,6 +42,10 @@ 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(); +// 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); // Adapters attached to the desktop, i.e. monitors currently plugged in. int CountUsbmmiddMonitors(); @@ -57,7 +61,8 @@ bool PlugUsbmmiddMonitor(const UsbmmiddPackage& package, DWORD timeout_ms, std::string* error); bool UnplugUsbmmiddMonitor(const UsbmmiddPackage& package, DWORD timeout_ms, std::string* error); -// Applies the closest supported mode to the first plugged usbmmidd monitor. +// Applies a temporary mode to usbmmidd only while no physical screen is active. +// Never persists the virtual resolution in Windows display configuration. bool ApplyUsbmmiddDisplayMode(VirtualDisplayMode requested, std::string* error); // Installs the driver on first use, plugs one monitor when none is attached diff --git a/apps/desktop/src/platform/windows/virtual_display/virtual_display_provisioner.cpp b/apps/desktop/src/platform/windows/virtual_display/virtual_display_provisioner.cpp index a60e72c5..c54c3a3d 100644 --- a/apps/desktop/src/platform/windows/virtual_display/virtual_display_provisioner.cpp +++ b/apps/desktop/src/platform/windows/virtual_display/virtual_display_provisioner.cpp @@ -281,6 +281,8 @@ void VirtualDisplayProvisioner::Release() { LOG_INFO("Virtual display: usbmmidd monitor released"); } else { LOG_WARN("Virtual display: release failed, error={}", error); + // Keep the owner eligible to retry after a physical-monitor hotplug. + active_.store(was_active, std::memory_order_relaxed); } } diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 7e97675c..55512f58 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -291,7 +291,7 @@ foreach ($serviceAction in '--service-stop', '--service-install', '--service-sta 登录前提供画面、键鼠、多屏切换和安全桌面控制。登录界面支持由原生控制端独立绘制光标;采集期间暂时隐藏本机系统光标,停止采集、采集进程退出或连接保活超时后自动恢复。网页控制端仍使用视频内光标。启用自动隐私模式时,后台会在恢复虚拟屏桌面采集前接续光标控制,GUI 随后复用该状态,避免初始化期间短暂出现视频内鼠标;手动关闭隐私遮罩仍会恢复本机光标。GUI 附加后,声音、文件传输、剪贴板和隐私遮罩由登录用户进程处理,通过本机通道复用后台的连接;文件操作不会以 SYSTEM 身份执行。GUI 的主动远控和局域网直连仍使用原有实现。密码、共享设置和可信设备记录由后台统一保存并同步给 GUI。GUI 退出时用户功能暂时不可用,重新打开后恢复。便携版和 Debug 不能启用此模式。共享身份位于 `%ProgramData%\CrossDeskUnattended`,只允许 SYSTEM 和管理员访问;GUI 与 SYSTEM 各自的运行缓存仍保持用户私有权限,日志位于机器目录的 `logs` 子目录。 -无实体显示器或切换器未提供显示器信息时,无人值守端会在接受连接前尝试准备虚拟显示器,优先恢复此前记录的显示器分辨率(包括 3840×2160);没有有效记录时才使用 1920×1080。记录在启用开机自启和初始化抓屏时更新,CrossDesk 自己的虚拟屏与临时无显示器模式不会覆盖它。若驱动初始化失败,程序会记录原因并继续使用 Windows 提供的显示模式。 +无实体显示器或切换器未提供显示器信息时,无人值守端会在接受连接前尝试准备虚拟显示器,优先恢复此前记录的显示器分辨率(包括 3840×2160);没有有效记录时才使用 1920×1080。记录在启用开机自启、初始化抓屏、开始连接和采集期间更新,普通权限启动也能保存到当前用户配置。CrossDesk 自己的虚拟屏、RDP、切换器失去有效显示目标后的临时模式,以及实体屏与虚拟屏共存时的临时复制模式不会覆盖记录。虚拟分辨率只临时应用,不写入 Windows 的持久显示配置;实体显示器稳定接入后,会释放本次创建的虚拟屏并切回实体屏,保持远程视频流编号不变。若驱动初始化失败,程序会记录原因并继续使用 Windows 提供的显示模式。 ```powershell # 停止无人值守被控端,并恢复服务按需启动