diff --git a/apps/desktop/src/platform/windows/screen_capturer/capture_backend_policy.h b/apps/desktop/src/platform/windows/screen_capturer/capture_backend_policy.h new file mode 100644 index 00000000..5b213a08 --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/capture_backend_policy.h @@ -0,0 +1,31 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-23 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _CAPTURE_BACKEND_POLICY_H_ +#define _CAPTURE_BACKEND_POLICY_H_ + +#include + +#include "screen_capture_method.h" + +namespace crossdesk { + +inline std::vector WindowsCaptureBackendOrder( + ScreenCaptureMethod requested, bool headless, bool usbmmidd) { + if (requested != ScreenCaptureMethod::Auto) return {requested}; + if (headless) return {ScreenCaptureMethod::Gdi}; + // usbmmidd may put its software cursor directly into a DXGI/GDI image. + // WGC can exclude it at capture time. Keep WGC for both cursor modes so a + // native client joining/leaving never needs to inherit an embedded cursor. + // Do not silently fall back to a backend that cannot exclude that cursor. + if (usbmmidd) return {ScreenCaptureMethod::Wgc}; + return {ScreenCaptureMethod::Dxgi, ScreenCaptureMethod::Wgc, + ScreenCaptureMethod::Gdi}; +} + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/platform/windows/screen_capturer/display_presence.h b/apps/desktop/src/platform/windows/screen_capturer/display_presence.h index 6a84bdad..1aaf0db2 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/display_presence.h +++ b/apps/desktop/src/platform/windows/screen_capturer/display_presence.h @@ -13,6 +13,8 @@ #include #include +#include "../virtual_display/usbmmidd_policy.h" + namespace crossdesk { // Without any attached monitor, Windows keeps the desktop on a phantom display @@ -26,22 +28,65 @@ struct DisplayPresenceProbe { size_t available_targets = 0; // active paths whose monitor is present size_t desktop_devices = 0; // adapters attached to the desktop size_t attached_monitors = 0; // monitor devices reported active + size_t identified_targets = 0; // EDID, monitor interface or built-in/IDD target + size_t unidentified_targets = 0; // available targets with no monitor identity + size_t target_query_failures = 0; + size_t embedded_or_virtual_targets = 0; // independent of an external monitor + size_t edid_targets = 0; + size_t monitor_path_targets = 0; }; // Pure policy, kept separate from Win32 so it can be tested with fixtures. inline DisplayPresence ClassifyDisplayPresence( - const DisplayPresenceProbe& probe) { + const DisplayPresenceProbe& probe, bool capture_stalled = false) { if (probe.query_succeeded) { - if (probe.available_targets > 0) return DisplayPresence::present; - if (probe.active_paths > 0 || probe.desktop_devices > 0) - return DisplayPresence::headless; - return DisplayPresence::unknown; + if (probe.available_targets > 0) { + // NVIDIA can retain both availability AND EDID/interface identity after + // the last unplug. Those describe a target, not a connected monitor. + // Require stalled capture plus a desktop adapter with no active monitor, + // and exempt built-in/wireless/IDD targets and failed target queries. + // A remaining physical monitor (even without EDID) prevents takeover. + if (capture_stalled && probe.desktop_devices > 0 && + probe.attached_monitors == 0 && + probe.embedded_or_virtual_targets == 0 && + probe.target_query_failures == 0) { + return DisplayPresence::headless; + } + return DisplayPresence::present; + } + // A successful empty topology is also headless (e.g. hot unplug), not an + // API failure. The caller suppresses recovery on the secure desktop. + return DisplayPresence::headless; } if (probe.attached_monitors > 0) return DisplayPresence::present; if (probe.desktop_devices > 0) return DisplayPresence::headless; return DisplayPresence::unknown; } +inline bool IsEmbeddedOrVirtualTarget( + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY technology) { + switch (technology) { + case DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL: + case DISPLAYCONFIG_OUTPUT_TECHNOLOGY_LVDS: + case DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EMBEDDED: + case DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EMBEDDED: + case DISPLAYCONFIG_OUTPUT_TECHNOLOGY_MIRACAST: + case DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INDIRECT_WIRED: + case DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INDIRECT_VIRTUAL: + return true; + default: + return false; + } +} + +inline bool HasDisplayTargetIdentity( + const DISPLAYCONFIG_TARGET_DEVICE_NAME& target) { + return IsEmbeddedOrVirtualTarget(target.outputTechnology) || + target.flags.edidIdsValid || + (target.monitorDevicePath[0] != L'\0' && + !ContainsCaseInsensitive(target.monitorDevicePath, L"Default_Monitor")); +} + inline DisplayPresenceProbe ProbeDisplayPresence() { DisplayPresenceProbe probe; // The buffer size can change between the two calls during a topology @@ -63,7 +108,27 @@ inline DisplayPresenceProbe ProbeDisplayPresence() { if (!(paths[index].flags & DISPLAYCONFIG_PATH_ACTIVE)) continue; ++probe.active_paths; // A removed monitor can leave its path active while the target is gone. - if (paths[index].targetInfo.targetAvailable) ++probe.available_targets; + const auto& info = paths[index].targetInfo; + if (!info.targetAvailable) continue; + ++probe.available_targets; + if (IsEmbeddedOrVirtualTarget(info.outputTechnology)) + ++probe.embedded_or_virtual_targets; + DISPLAYCONFIG_TARGET_DEVICE_NAME target{}; + target.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + target.header.size = sizeof(target); + target.header.adapterId = info.adapterId; + target.header.id = info.id; + if (DisplayConfigGetDeviceInfo(&target.header) != ERROR_SUCCESS) { + ++probe.target_query_failures; + } else { + if (target.flags.edidIdsValid) ++probe.edid_targets; + if (target.monitorDevicePath[0] != L'\0') ++probe.monitor_path_targets; + if (HasDisplayTargetIdentity(target)) { + ++probe.identified_targets; + } else { + ++probe.unidentified_targets; + } + } } } for (DWORD index = 0;; ++index) { diff --git a/apps/desktop/src/platform/windows/screen_capturer/dxgi_cursor_state.h b/apps/desktop/src/platform/windows/screen_capturer/dxgi_cursor_state.h index 71bfa501..c5f8a938 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/dxgi_cursor_state.h +++ b/apps/desktop/src/platform/windows/screen_capturer/dxgi_cursor_state.h @@ -13,6 +13,15 @@ namespace crossdesk { // coordinates or the wire protocol. class DxgiCursorState { public: + void BeginOutput(void* monitor, bool software_cursor) { + std::lock_guard lock(mutex_); + // usbmmidd can composite the cursor into the desktop without ever sending + // a separate pointer update. Preserve this output-specific default across + // privacy restoration; an explicit DXGI update can still override it. + software_cursor_monitor_ = software_cursor ? monitor : nullptr; + embedded_monitor_ = privacy_cursor_hidden_ ? nullptr : software_cursor_monitor_; + } + void Update(int64_t last_mouse_update_time, bool separate_pointer_visible, void* monitor) { // PointerPosition is undefined on desktop-only updates. Retain the last @@ -26,15 +35,16 @@ class DxgiCursorState { void Reset() { std::lock_guard lock(mutex_); embedded_monitor_ = nullptr; + software_cursor_monitor_ = nullptr; } void SetPrivacyCursorHidden(bool hidden) { std::lock_guard lock(mutex_); if (privacy_cursor_hidden_ == hidden) return; privacy_cursor_hidden_ = hidden; - // Pointer updates from before/during suppression cannot describe whether - // the restored cursor is embedded. Wait for a fresh DXGI pointer update. - embedded_monitor_ = nullptr; + // Discard stale pointer-plane updates, then restore the output's default. + // A software-cursor IDD may never send a fresh pointer-plane update. + embedded_monitor_ = hidden ? nullptr : software_cursor_monitor_; } bool ShouldDrawCursor(bool system_cursor_visible, void* cursor_monitor) const { @@ -53,6 +63,7 @@ class DxgiCursorState { private: mutable std::mutex mutex_; void* embedded_monitor_ = nullptr; + void* software_cursor_monitor_ = nullptr; bool privacy_cursor_hidden_ = false; }; diff --git a/apps/desktop/src/platform/windows/screen_capturer/headless_recovery.h b/apps/desktop/src/platform/windows/screen_capturer/headless_recovery.h new file mode 100644 index 00000000..66994793 --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/headless_recovery.h @@ -0,0 +1,47 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-23 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _HEADLESS_RECOVERY_H_ +#define _HEADLESS_RECOVERY_H_ + +#include + +namespace crossdesk { + +// Management-thread policy. Debounce mode changes and bound retries after a +// failed install. Capture progress is intentionally separate from DXGI retries. +class HeadlessRecovery { + public: + void Reset() { *this = {}; } + + bool ShouldProvision(uint64_t now, bool headless, bool busy, bool active) { + if (!headless) { + suspected_ = false; + return false; + } + if (!suspected_) { + suspected_ = true; + since_ = now; + } + return !busy && !active && now - since_ >= 1000 && + (!attempted_ || now - last_attempt_ >= 15000); + } + + void Attempted(uint64_t now) { + attempted_ = true; + last_attempt_ = now; + } + + private: + bool suspected_ = false; + bool attempted_ = false; + uint64_t since_ = 0; + uint64_t last_attempt_ = 0; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp index 968cc6d3..dcf4a8a0 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp @@ -14,6 +14,7 @@ #include "libyuv.h" #include "rd_log.h" #include "windows_thread_dpi.h" +#include "../virtual_display/usbmmidd_virtual_display.h" namespace crossdesk { @@ -285,6 +286,13 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) { rotation_ = desc.Rotation; DXGI_ADAPTER_DESC adapter_desc{}; output_adapter->GetDesc(&adapter_desc); + DXGI_OUTPUT_DESC output_desc{}; + software_cursor_output_ = + SUCCEEDED(outputs_[monitor_index]->GetDesc(&output_desc)) && + IsUsbmmiddDisplayDevice(output_desc.DeviceName); + SharedDxgiCursorState().BeginOutput( + display_info_list_[monitor_index].handle, software_cursor_output_); + last_logged_cursor_embedding_ = -1; LOG_INFO("DXGI: duplication ready, monitor={}, rotation={}, adapter='{}'", monitor_index, static_cast(rotation_), WideToUtf8(adapter_desc.Description)); @@ -535,6 +543,21 @@ void ScreenCapturerDxgi::CaptureLoop() { const std::string stream_id = MakeDisplayStreamId(frame_monitor); cached_generation = duplication_generation_; + if (converted && frame_monitor == monitor_index_.load() && callback_) { + if (last_logged_cursor_embedding_ < 0 || + (software_cursor_output_ && + last_logged_cursor_embedding_ != static_cast(cursor_embedded))) { + LOG_INFO("DXGI cursor: monitor={} usbmmidd={} mouse_update={} " + "separate_pointer_visible={} pointer_shape_bytes={} " + "compose_cursor={} embedded={}", + frame_monitor, software_cursor_output_, + frame_info.LastMouseUpdateTime.QuadPart, + frame_info.PointerPosition.Visible != FALSE, + frame_info.PointerShapeBufferSize, + show_cursor_.load(std::memory_order_relaxed), cursor_embedded); + last_logged_cursor_embedding_ = static_cast(cursor_embedded); + } + } duplication_->ReleaseFrame(); capture_lock.unlock(); diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.h b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.h index 692e8ffd..e35ba543 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.h +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.h @@ -79,6 +79,8 @@ class ScreenCapturerDxgi : public ScreenCapturer { Microsoft::WRL::ComPtr staging_; DXGI_MODE_ROTATION rotation_ = DXGI_MODE_ROTATION_IDENTITY; uint64_t duplication_generation_ = 0; + bool software_cursor_output_ = false; + int last_logged_cursor_embedding_ = -1; std::atomic running_{false}; std::atomic paused_{false}; 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 965ad009..3b114a74 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 @@ -1,6 +1,7 @@ #include "screen_capturer_win.h" #include +#include #include #include @@ -18,7 +19,10 @@ #include #include "captured_nv12_frame.h" #include "captured_cursor_state.h" +#include "capture_backend_policy.h" #include "display_presence.h" +#include "display_label.h" +#include "virtual_display_binding.h" #include "dxgi_cursor_state.h" #include "interactive_desktop.h" #include "interactive_state.h" @@ -54,6 +58,21 @@ constexpr int kSecureDesktopCaptureMinFps = 30; constexpr int kSecureDesktopCaptureMaxIntervalMs = 1000 / kSecureDesktopCaptureMinFps; +class CaptureThreadApartment { + public: + CaptureThreadApartment() : result_(RoInitialize(RO_INIT_MULTITHREADED)) { + if (FAILED(result_) && result_ != RPC_E_CHANGED_MODE) + LOG_WARN("Windows capturer: WinRT initialization failed, hr={}", + static_cast(result_)); + } + ~CaptureThreadApartment() { + if (SUCCEEDED(result_)) RoUninitialize(); + } + + private: + HRESULT result_; +}; + class WgcPluginCapturer final : public ScreenCapturer { public: using CreateFn = ScreenCapturer* (*)(); @@ -182,6 +201,18 @@ const char* CaptureBackendName(const ScreenCapturer* capturer) { return "unavailable"; } +bool CapturesUsbmmiddOutput(ScreenCapturer* capturer) { + if (!capturer) return false; + const auto displays = capturer->GetDisplayInfoList(); + const int selected = capturer->GetCurrentMonitorIndex(); + if (selected < 0 || selected >= static_cast(displays.size())) return false; + MONITORINFOEXW info{}; + info.cbSize = sizeof(info); + return displays[selected].handle && + GetMonitorInfoW(static_cast(displays[selected].handle), &info) && + IsUsbmmiddDisplayDevice(info.szDevice); +} + // Debug aid: CROSSDESK_FORCE_HEADLESS=1 makes a host that has monitors behave // 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 @@ -201,7 +232,9 @@ bool ForcedHeadlessForTesting() { // WGC has no real monitor item; GDI reads the composed desktop. This is a // best-effort baseline that needs no driver. Only an explicit Auto selection // takes this shortcut; a pinned method keeps its own failure semantics. -bool DetectHeadlessDesktop(ScreenCaptureMethod method) { +bool DetectHeadlessDesktop(ScreenCaptureMethod method, + bool capture_stalled = false, + bool log_probe = false) { if (method != ScreenCaptureMethod::Auto) return false; if (ForcedHeadlessForTesting()) { static std::atomic logged{false}; @@ -212,29 +245,23 @@ bool DetectHeadlessDesktop(ScreenCaptureMethod method) { return true; } const auto probe = ProbeDisplayPresence(); - const auto presence = ClassifyDisplayPresence(probe); + const auto presence = ClassifyDisplayPresence(probe, capture_stalled); // Sessions probe repeatedly; report transitions rather than every probe. static std::atomic last_logged{-1}; if (last_logged.exchange(static_cast(presence)) != - static_cast(presence)) { - if (presence == DisplayPresence::headless) { - LOG_WARN( - "Windows capturer: no attached display (active_paths={}, " - "available_targets={}, desktop_devices={}, monitors={}); entering " - "headless compatibility mode. Resolution is fixed by Windows and " - "capture uses GDI. Attach a monitor or an HDMI dummy plug to " - "restore GPU capture.", - probe.active_paths, probe.available_targets, probe.desktop_devices, - probe.attached_monitors); - } else if (presence == DisplayPresence::unknown) { - LOG_WARN("Windows capturer: display presence unknown (query_ok={}, " - "desktop_devices={}); using the normal backend order", - probe.query_succeeded, probe.desktop_devices); - } else { - LOG_INFO("Windows capturer: attached display detected " - "(available_targets={}, monitors={})", - probe.available_targets, probe.attached_monitors); - } + static_cast(presence) || log_probe) { + LOG_INFO("Windows capturer: display probe presence={} query_ok={} " + "active_paths={} available_targets={} identified_targets={} " + "unidentified_targets={} target_query_failures={} " + "embedded_or_virtual_targets={} edid_targets={} monitor_paths={} " + "desktop_devices={} monitors={} capture_stalled={}", + presence == DisplayPresence::headless ? "headless" : + presence == DisplayPresence::present ? "present" : "unknown", + probe.query_succeeded, probe.active_paths, probe.available_targets, + probe.identified_targets, probe.unidentified_targets, + probe.target_query_failures, probe.embedded_or_virtual_targets, + probe.edid_targets, probe.monitor_path_targets, probe.desktop_devices, + probe.attached_monitors, capture_stalled); } return presence == DisplayPresence::headless; } @@ -485,6 +512,7 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { if (size == ScreenCapturer::kDisplayTopologyChanged) { SharedCapturedCursorState().Reset(); RebuildAliasesFromImpl(true); + display_probe_requested_.store(true, std::memory_order_relaxed); return; } if (size == ScreenCapturer::kBackendReset) { @@ -511,6 +539,9 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { if (it != stream_id_alias_.end()) { mapped_stream_id = it->second; } 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; // Unknown backend labels are presentation data, not wire IDs. // Resolve them through the selected logical display instead. mapped_stream_id.clear(); @@ -687,7 +718,30 @@ bool ScreenCapturerWin::TryStartBackend( } const char* name = CaptureBackendName(candidate.get()); const bool show_cursor = show_cursor_.load(std::memory_order_relaxed); - int ret = candidate->Init(fps_, cb_); + // A candidate may deliver immediately from Start(). Publish frames only + // after its display selection and wire aliases have been committed. + auto committed = std::make_shared>(false); + int ret = candidate->Init(fps_, [this, committed]( + unsigned char* data, int size, int width, int height, + const char* stream, const MiniRtcNativeVideoFrame* native) { + if (committed->load(std::memory_order_acquire)) + cb_(data, size, width, height, stream, native); + }); + std::string required_display; + { + std::lock_guard lock(alias_mutex_); + required_display = virtual_display_name_; + } + if (ret == 0 && !required_display.empty()) { + const auto displays = candidate->GetDisplayInfoList(); + const auto found = std::find_if(displays.begin(), displays.end(), + [&](const DisplayInfo& display) { + return display.name == required_display && + display.width > 0 && display.height > 0; + }); + ret = found == displays.end() ? -1 : + candidate->SwitchTo(static_cast(found - displays.begin())); + } if (ret == 0) { last_capture_progress_tick_.store(GetTickCount64(), std::memory_order_relaxed); ret = candidate->Start(show_cursor); @@ -706,30 +760,41 @@ bool ScreenCapturerWin::TryStartBackend( } applied_show_cursor_ = show_cursor; RestoreMonitor(monitor_index); + committed->store(true, std::memory_order_release); LOG_INFO("Windows capturer: started {}", name); return true; } std::vector -ScreenCapturerWin::PreferredBackends(bool headless) const { - // A headless desktop goes straight to GDI: DXGI has no output to duplicate - // and WGC has no monitor item. +ScreenCapturerWin::PreferredBackends(bool headless, bool usbmmidd) const { std::vector factories; - if (!headless && IsBackendEnabled(ScreenCaptureMethod::Dxgi)) { - factories.push_back([] { return std::make_unique(); }); - } - if (!headless && IsBackendEnabled(ScreenCaptureMethod::Wgc)) { - factories.push_back([] { return WgcPluginCapturer::Create(); }); - } - if (IsBackendEnabled(ScreenCaptureMethod::Gdi)) { - factories.push_back([] { return std::make_unique(); }); + for (const auto backend : + WindowsCaptureBackendOrder(capture_method_, headless, usbmmidd)) { + switch (backend) { + case ScreenCaptureMethod::Dxgi: + factories.push_back([] { return std::make_unique(); }); + break; + case ScreenCaptureMethod::Wgc: + factories.push_back([] { return WgcPluginCapturer::Create(); }); + break; + case ScreenCaptureMethod::Gdi: + factories.push_back([] { return std::make_unique(); }); + break; + default: + break; + } } return factories; } bool ScreenCapturerWin::StartPreferredBackend(bool headless, - int monitor_index) { - for (const auto& create : PreferredBackends(headless)) { + int monitor_index, + bool usbmmidd) { + if (usbmmidd && capture_method_ == ScreenCaptureMethod::Auto) { + LOG_INFO("Windows capturer: usbmmidd uses WGC with cursor capture={}", + show_cursor_.load(std::memory_order_relaxed)); + } + for (const auto& create : PreferredBackends(headless, usbmmidd)) { if (TryStartBackend(create(), monitor_index)) return true; } return false; @@ -776,19 +841,33 @@ int ScreenCapturerWin::Start(bool show_cursor) { monitor_index_.load(std::memory_order_relaxed); // A monitor may have been attached or removed since the last session. Pick // the backend for the desktop as it is now, not as it was at Init. - const bool headless = DetectHeadlessDesktop(capture_method_); + JoinVirtualDisplayThread(); + { + std::lock_guard lock(alias_mutex_); + virtual_display_name_.clear(); + virtual_display_slot_ = -1; + } + const bool headless = DetectHeadlessDesktop(capture_method_, false, true); + const bool usbmmidd = !headless && CapturesUsbmmiddOutput(impl_.get()); // The virtual display is provisioned off this thread (Start() runs on the // UI thread and a driver install can take minutes). Begin in compatibility // mode; MaybeAdoptVirtualDisplay() switches to a GPU backend once ready. - JoinVirtualDisplayThread(); virtual_display_ready_.store(false, std::memory_order_relaxed); + display_probe_requested_.store(true, std::memory_order_relaxed); + last_display_probe_tick_ = 0; + last_capture_retry_tick_ = 0; + last_virtual_adoption_tick_ = 0; + headless_recovery_.Reset(); const bool was_headless = headless_compat_.exchange(headless, std::memory_order_relaxed); const bool backend_is_gdi = dynamic_cast(impl_.get()) != nullptr; int ret = 0; - if (headless != was_headless || (headless && !backend_is_gdi)) { - LOG_INFO("Windows capturer: desktop presence changed (headless={}), " - "reselecting the capture backend", headless); + const bool needs_wgc = usbmmidd && + capture_method_ == ScreenCaptureMethod::Auto && + dynamic_cast(impl_.get()) == nullptr; + if (headless != was_headless || (headless && !backend_is_gdi) || needs_wgc) { + LOG_INFO("Windows capturer: reselecting capture backend " + "(headless={}, usbmmidd={})", headless, usbmmidd); impl_->Destroy(); // Keep an instance around on failure so the next Start can retry. std::unique_ptr previous; @@ -796,15 +875,13 @@ int ScreenCapturerWin::Start(bool show_cursor) { std::lock_guard lock(impl_mutex_); previous = std::move(impl_); } - if (!StartPreferredBackend(headless, requested_monitor)) { + if (!StartPreferredBackend(headless, requested_monitor, usbmmidd)) { std::lock_guard lock(impl_mutex_); impl_ = std::move(previous); LOG_ERROR("Windows capturer: no capture backend could start"); return -1; } - ret = MarkStarted(); - if (ret == 0 && headless) BeginVirtualDisplayProvisioning(); - return ret; + return MarkStarted(); } impl_->Destroy(); ret = impl_->Init(fps_, cb_); @@ -826,11 +903,13 @@ int ScreenCapturerWin::Start(bool show_cursor) { ret); bool fallback_started = false; - if (dynamic_cast(impl_.get()) && + if (usbmmidd) { + fallback_started = StartPreferredBackend(false, requested_monitor, true); + } else if (dynamic_cast(impl_.get()) && TryStartBackend(WgcPluginCapturer::Create(), requested_monitor)) { fallback_started = true; } - if (!fallback_started && (dynamic_cast(impl_.get()) || + if (!usbmmidd && !fallback_started && (dynamic_cast(impl_.get()) || dynamic_cast(impl_.get()))) { fallback_started = TryStartBackend(std::make_unique(), requested_monitor); @@ -842,9 +921,7 @@ int ScreenCapturerWin::Start(bool show_cursor) { } } - ret = MarkStarted(); - if (ret == 0 && headless) BeginVirtualDisplayProvisioning(); - return ret; + return MarkStarted(); } int ScreenCapturerWin::Stop() { @@ -873,7 +950,11 @@ void ScreenCapturerWin::JoinVirtualDisplayThread() { } void ScreenCapturerWin::BeginVirtualDisplayProvisioning() { + if (virtual_display_busy_.load(std::memory_order_acquire)) return; JoinVirtualDisplayThread(); + virtual_display_.PrepareAcquire(); + if (!running_.load(std::memory_order_relaxed)) return; + virtual_display_busy_.store(true, std::memory_order_release); virtual_display_thread_ = std::thread([this] { // A usbmmidd virtual monitor turns the phantom desktop into a real // display target, so the GPU backends and a chosen resolution apply. @@ -881,10 +962,11 @@ void ScreenCapturerWin::BeginVirtualDisplayProvisioning() { if (virtual_display_.Acquire(kDefaultVirtualDisplayMode, &error)) { virtual_display_ready_.store(true, std::memory_order_release); } else if (error != "cancelled") { - LOG_WARN("Windows capturer: no usbmmidd virtual display ({}); staying " - "in headless compatibility mode", + LOG_WARN("Windows capturer: no usbmmidd virtual display ({}); " + "will retry while headless", error); } + virtual_display_busy_.store(false, std::memory_order_release); }); } @@ -896,18 +978,22 @@ void ScreenCapturerWin::BeginVirtualDisplayRelease() { } void ScreenCapturerWin::MaybeAdoptVirtualDisplay() { - if (!virtual_display_ready_.exchange(false, std::memory_order_acq_rel)) + if (!virtual_display_ready_.load(std::memory_order_acquire)) return; if (!running_.load(std::memory_order_relaxed)) return; - if (DetectHeadlessDesktop(capture_method_)) { - LOG_WARN("Windows capturer: virtual display plugged but the desktop " - "still reports no display; staying in compatibility mode"); - return; - } + const auto now = GetTickCount64(); + if (last_virtual_adoption_tick_ != 0 && + now - last_virtual_adoption_tick_ < 3000) return; + last_virtual_adoption_tick_ = now; + const auto virtual_name = GetDisplayLabel(FindUsbmmiddDisplayDeviceName()); + if (virtual_name.empty()) return; const int current_monitor = monitor_index_.load(std::memory_order_relaxed); - LOG_INFO("Windows capturer: virtual display ready, switching from " - "compatibility mode to a GPU backend"); + LOG_INFO("Windows capturer: adopting usbmmidd display '{}' in stream slot {}", + virtual_name, current_monitor); NotifyPrivacyCapture(false); + // Drain callbacks while impl_ still names their backend. A topology callback + // can rebuild aliases and must not observe the temporary null during a swap. + if (impl_) impl_->Stop(); // Public entry points dereference impl_ under impl_mutex_; take the old // backend out under the lock, then tear it down without holding it. std::unique_ptr previous; @@ -916,22 +1002,30 @@ void ScreenCapturerWin::MaybeAdoptVirtualDisplay() { previous = std::move(impl_); } if (previous) previous->Destroy(); - if (StartPreferredBackend(false, current_monitor)) { + { + std::lock_guard lock(alias_mutex_); + virtual_display_name_ = virtual_name; + virtual_display_slot_ = current_monitor; + } + if (StartPreferredBackend(false, current_monitor, true)) { + virtual_display_ready_.store(false, std::memory_order_release); headless_compat_.store(false, std::memory_order_relaxed); // The owner refreshes its display list and input mapping every tick from // GetDisplayInfoList(), so the new geometry is picked up without a hint. } else { - LOG_ERROR("Windows capturer: no GPU backend started on the virtual " - "display; restoring compatibility capture"); + LOG_ERROR("Windows capturer: WGC could not start on usbmmidd; " + "will retry cursor-excluding capture"); + { + std::lock_guard lock(alias_mutex_); + virtual_display_name_.clear(); + virtual_display_slot_ = -1; + } { std::lock_guard lock(impl_mutex_); impl_ = std::move(previous); } - // RestoreMonitor takes impl_mutex_ itself (via SwitchTo). - if (impl_ && impl_->Init(fps_, cb_) == 0) { - RestoreMonitor(current_monitor); - impl_->Start(show_cursor_.load(std::memory_order_relaxed)); - } + // Retain the stopped instance for Stop()/the next retry, but do not start + // DXGI/GDI on the now-attached IDD: it can bake the cursor into the video. } NotifyPrivacyCapture(true); } @@ -1024,6 +1118,24 @@ 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. + handle_to_canonical_index_.clear(); + const int i = BindVirtualDisplayToStream(current, virtual_display_name_, + virtual_display_slot_, &canonical_displays_, &backend_to_canonical_); + if (i >= 0) { + const auto stream = MakeDisplayStreamId(virtual_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); + } + return; + } const bool stable_slots = preserve_backend_slots && dynamic_cast(impl_.get()); if (!stable_slots) backend_to_canonical_.assign(current.size(), -1); @@ -1118,6 +1230,7 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() { const bool show_cursor = show_cursor_.load(std::memory_order_relaxed); const int current_monitor = monitor_index_.load(std::memory_order_relaxed); + const bool usbmmidd = virtual_display_.active() || CapturesUsbmmiddOutput(impl_.get()); LOG_INFO("Windows capturer: restarting capture backend after secure desktop"); impl_->Stop(); @@ -1144,7 +1257,7 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() { } if (StartPreferredBackend(headless_compat_.load(std::memory_order_relaxed), - current_monitor)) { + current_monitor, usbmmidd)) { return true; } @@ -1155,6 +1268,39 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() { return false; } +void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) { + if (capture_method_ != ScreenCaptureMethod::Auto) return; + // Topology events accelerate the next probe, but driver rebuild callbacks + // must not flood QueryDisplayConfig. Polling also covers WGC/GDI and unplug + // events that NVIDIA does not expose as a changed DXGI output. + const auto elapsed = now - last_display_probe_tick_; + if (last_display_probe_tick_ != 0 && + (elapsed < 250 || (elapsed < 1000 && + !display_probe_requested_.load(std::memory_order_relaxed)))) return; + last_display_probe_tick_ = now; + const bool topology_changed = + display_probe_requested_.exchange(false, std::memory_order_relaxed); + if (!IsWindowsPrivacyDesktopAvailable()) { + headless_recovery_.ShouldProvision(now, false, false, false); + return; + } + const auto last_progress = + last_capture_progress_tick_.load(std::memory_order_relaxed); + const bool stalled = last_progress != 0 && now >= last_progress && + now - last_progress >= kCaptureStallTimeoutMs; + const bool headless = DetectHeadlessDesktop(capture_method_, stalled, + topology_changed); + if (!headless_recovery_.ShouldProvision( + now, headless, virtual_display_busy_.load(std::memory_order_acquire), + virtual_display_.active())) return; + if (!running_.load(std::memory_order_relaxed)) return; + headless_recovery_.Attempted(now); + headless_compat_.store(true, std::memory_order_relaxed); + LOG_WARN("Windows capturer: confirmed headless desktop during capture " + "(stalled={}); provisioning usbmmidd", stalled); + BeginVirtualDisplayProvisioning(); +} + void ScreenCapturerWin::CheckCaptureProgress(ULONGLONG now) { if (capture_method_ != ScreenCaptureMethod::Auto || !running_.load()) return; auto* dxgi = dynamic_cast(impl_.get()); @@ -1164,11 +1310,15 @@ void ScreenCapturerWin::CheckCaptureProgress(ULONGLONG now) { last_capture_progress_tick_.load(std::memory_order_relaxed); // A capture callback may have advanced the tick after this loop sampled now. if (last_progress == 0 || now < last_progress || - now - last_progress < kCaptureStallTimeoutMs) return; - // Retry only the pinned output. A missing target must never trigger a - // backend switch or move the session onto another display. + now - last_progress < kCaptureStallTimeoutMs) return; + if (last_capture_retry_tick_ != 0 && + now - last_capture_retry_tick_ < kCaptureStallTimeoutMs) return; + // Probe/recovery is handled separately. When another real monitor remains, + // retry only the pinned output instead of silently selecting that monitor. + LOG_WARN("Windows capturer: DXGI has delivered no frame for {} ms; " + "refreshing the selected output", now - last_progress); dxgi->RequestTopologyRefresh(); - last_capture_progress_tick_.store(now, std::memory_order_relaxed); + last_capture_retry_tick_ = now; } void ScreenCapturerWin::ApplyCursorCaptureSetting() { @@ -1462,6 +1612,9 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture( } void ScreenCapturerWin::SecureDesktopCaptureLoop() { + // Virtual-display adoption constructs WGC on this std::thread, which does + // not inherit the UI thread's WinRT apartment. + CaptureThreadApartment apartment; const int frame_interval_ms = fps_ > 0 ? (std::min)(kSecureDesktopCaptureMaxIntervalMs, 1000 / fps_) : kSecureDesktopCaptureMaxIntervalMs; @@ -1593,6 +1746,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) { StopSecureDesktopSharedCapture(secure_shared_session_id_); + CheckDisplayPresence(now); MaybeAdoptVirtualDisplay(); CheckCaptureProgress(now); if (post_secure_restart_pending) { 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 38b37c3c..ee8d0dbc 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 @@ -25,6 +25,7 @@ #include "privacy_controller.h" // Relative: platform include directories stay private to the capture target. #include "../virtual_display/virtual_display_provisioner.h" +#include "headless_recovery.h" namespace crossdesk { @@ -90,6 +91,10 @@ class ScreenCapturerWin : public ScreenCapturer { std::atomic native_output_error_logged_{false}; std::atomic running_{false}; std::atomic last_capture_progress_tick_{0}; + std::atomic display_probe_requested_{false}; + ULONGLONG last_display_probe_tick_ = 0; + ULONGLONG last_capture_retry_tick_ = 0; + HeadlessRecovery headless_recovery_; std::atomic paused_{false}; std::atomic show_cursor_{true}; bool applied_show_cursor_ = true; @@ -103,6 +108,12 @@ class ScreenCapturerWin : public ScreenCapturer { VirtualDisplayProvisioner virtual_display_; std::thread virtual_display_thread_; 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; std::atomic monitor_index_{0}; std::atomic secure_desktop_capture_active_{false}; std::atomic post_secure_desktop_waiting_for_frame_{false}; @@ -130,10 +141,12 @@ class ScreenCapturerWin : public ScreenCapturer { void RestoreMonitor(int monitor_index); // Backends in preference order for the current desktop; each factory is // invoked lazily so an unused WGC plugin is never loaded. - std::vector PreferredBackends(bool headless) const; + std::vector PreferredBackends(bool headless, + bool usbmmidd = false) const; bool TryStartBackend(std::unique_ptr candidate, int monitor_index); - bool StartPreferredBackend(bool headless, int monitor_index); + bool StartPreferredBackend(bool headless, int monitor_index, + bool usbmmidd = false); void ResetPostSecureDesktopState(); // Common tail of every successful Start(): publish running state and // launch the management thread. @@ -145,6 +158,7 @@ class ScreenCapturerWin : public ScreenCapturer { void StopSecureCaptureThread(); bool RestartCaptureBackendAfterSecureDesktop(); void CheckCaptureProgress(ULONGLONG now); + void CheckDisplayPresence(ULONGLONG now); // 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 new file mode 100644 index 00000000..6db79d89 --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/virtual_display_binding.h @@ -0,0 +1,44 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-23 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _VIRTUAL_DISPLAY_BINDING_H_ +#define _VIRTUAL_DISPLAY_BINDING_H_ + +#include +#include + +#include "display_info.h" + +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. +inline int BindVirtualDisplayToStream(const std::vector& current, + const std::string& name, int slot, + std::vector* canonical, + std::vector* backend_to_canonical) { + if (slot < 0 || slot >= static_cast(canonical->size())) return -1; + backend_to_canonical->assign(current.size(), -1); + for (auto& display : *canonical) { + display.handle = nullptr; + display.left = display.top = display.right = display.bottom = 0; + display.width = display.height = 0; + display.is_primary = false; + } + for (size_t i = 0; i < current.size(); ++i) { + if (current[i].name != name || current[i].width <= 0 || + current[i].height <= 0) + continue; + (*backend_to_canonical)[i] = slot; + (*canonical)[slot] = current[i]; + return static_cast(i); + } + return -1; +} + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/platform/windows/screen_capturer/wgc_session_impl.cpp b/apps/desktop/src/platform/windows/screen_capturer/wgc_session_impl.cpp index 3dc21c5f..2aaf3fd5 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/wgc_session_impl.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/wgc_session_impl.cpp @@ -188,6 +188,8 @@ int WgcSessionImpl::StartCaptureLocked(bool show_cursor) { } capture_session_.IsCursorCaptureEnabled(show_cursor); + LOG_INFO("WGC: session {} cursor capture enabled={}", id_, + capture_session_.IsCursorCaptureEnabled()); capture_session_.StartCapture(); is_running_ = true; return 0; 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 6b4895a5..3cf894dd 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 @@ -217,6 +217,22 @@ int CountUsbmmiddMonitors() { return count; } +std::wstring FindUsbmmiddDisplayDeviceName() { + for (const auto& adapter : EnumerateUsbmmiddAdapters()) { + if (adapter.attached_to_desktop) return adapter.device_name; + } + return {}; +} + +bool IsUsbmmiddDisplayDevice(const std::wstring& device_name) { + for (const auto& adapter : EnumerateUsbmmiddAdapters()) { + if (adapter.attached_to_desktop && + _wcsicmp(adapter.device_name.c_str(), device_name.c_str()) == 0) + return true; + } + return false; +} + bool IsUsbmmiddDriverInstalled() { const HDEVINFO device_info = SetupDiGetClassDevsW( &GUID_DEVCLASS_DISPLAY, nullptr, nullptr, DIGCF_PRESENT); 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 3c7963ef..1e0e6026 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,9 @@ UsbmmiddStatus QueryUsbmmiddStatus(); // Adapters attached to the desktop, i.e. monitors currently plugged in. int CountUsbmmiddMonitors(); +// GDI device name of an attached usbmmidd output, or empty while settling. +std::wstring FindUsbmmiddDisplayDeviceName(); +bool IsUsbmmiddDisplayDevice(const std::wstring& device_name); // Root-enumerated device node present, plugged or not. bool IsUsbmmiddDriverInstalled(); 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 5fc7e94d..d4334477 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 @@ -190,7 +190,10 @@ bool VirtualDisplayProvisioner::Acquire(VirtualDisplayMode mode, std::string local_error; if (error == nullptr) error = &local_error; if (active_.load(std::memory_order_relaxed)) return true; - cancel_.store(false, std::memory_order_relaxed); + if (Cancelled()) { + *error = "cancelled"; + return false; + } bool helper_reachable = false; const ULONGLONG started = GetTickCount64(); diff --git a/apps/desktop/src/platform/windows/virtual_display/virtual_display_provisioner.h b/apps/desktop/src/platform/windows/virtual_display/virtual_display_provisioner.h index 00260adf..f6e04291 100644 --- a/apps/desktop/src/platform/windows/virtual_display/virtual_display_provisioner.h +++ b/apps/desktop/src/platform/windows/virtual_display/virtual_display_provisioner.h @@ -29,6 +29,9 @@ class VirtualDisplayProvisioner { delete; bool Acquire(VirtualDisplayMode mode, std::string* error); + // Owner calls this before launching an Acquire worker. Clearing cancellation + // inside the worker would lose a Stop() racing with thread startup. + void PrepareAcquire() { cancel_.store(false, std::memory_order_relaxed); } void Release(); bool active() const { return active_.load(std::memory_order_relaxed); } // True when Release() has something to undo. Read on the owner's thread @@ -37,7 +40,7 @@ class VirtualDisplayProvisioner { return active() || pending_helper_release_ || plugged_locally_ > 0; } // Makes a blocking Acquire() return early with "cancelled" at its next poll - // (helper install/plug can take up to two minutes). Cleared by Acquire(). + // (helper install/plug can take up to two minutes). void RequestCancel() { cancel_.store(true, std::memory_order_relaxed); } private: diff --git a/apps/desktop/xmake/targets.lua b/apps/desktop/xmake/targets.lua index 45eedd69..a72b559b 100644 --- a/apps/desktop/xmake/targets.lua +++ b/apps/desktop/xmake/targets.lua @@ -251,7 +251,7 @@ function setup_targets() "apps/desktop/src/platform/windows/screen_capturer", "apps/desktop/src/platform/windows/service", "apps/desktop/src/platform/windows/virtual_display") - add_syslinks("Setupapi", {public = true}) + add_syslinks("Setupapi", "RuntimeObject", {public = true}) elseif is_os("macosx") then add_deps("privacy") add_mxxflags("-fobjc-arc")