mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
[fix] recover headless capture and use WGC for usbmmidd displays
This commit is contained in:
@@ -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 <vector>
|
||||
|
||||
#include "screen_capture_method.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline std::vector<ScreenCaptureMethod> 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
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#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) {
|
||||
|
||||
@@ -13,6 +13,15 @@ namespace crossdesk {
|
||||
// coordinates or the wire protocol.
|
||||
class DxgiCursorState {
|
||||
public:
|
||||
void BeginOutput(void* monitor, bool software_cursor) {
|
||||
std::lock_guard<std::mutex> 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<std::mutex> lock(mutex_);
|
||||
embedded_monitor_ = nullptr;
|
||||
software_cursor_monitor_ = nullptr;
|
||||
}
|
||||
|
||||
void SetPrivacyCursorHidden(bool hidden) {
|
||||
std::lock_guard<std::mutex> 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 <cstdint>
|
||||
|
||||
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
|
||||
@@ -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<int>(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<int>(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<int>(cursor_embedded);
|
||||
}
|
||||
}
|
||||
duplication_->ReleaseFrame();
|
||||
capture_lock.unlock();
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> 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<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "screen_capturer_win.h"
|
||||
|
||||
#include <Windows.h>
|
||||
#include <roapi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -18,7 +19,10 @@
|
||||
#include <display_stream_id.h>
|
||||
#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<int>(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<int>(displays.size())) return false;
|
||||
MONITORINFOEXW info{};
|
||||
info.cbSize = sizeof(info);
|
||||
return displays[selected].handle &&
|
||||
GetMonitorInfoW(static_cast<HMONITOR>(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<bool> 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<int> last_logged{-1};
|
||||
if (last_logged.exchange(static_cast<int>(presence)) !=
|
||||
static_cast<int>(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<int>(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<std::atomic<bool>>(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<std::mutex> 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<int>(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::BackendFactory>
|
||||
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<BackendFactory> factories;
|
||||
if (!headless && IsBackendEnabled(ScreenCaptureMethod::Dxgi)) {
|
||||
factories.push_back([] { return std::make_unique<ScreenCapturerDxgi>(); });
|
||||
}
|
||||
if (!headless && IsBackendEnabled(ScreenCaptureMethod::Wgc)) {
|
||||
factories.push_back([] { return WgcPluginCapturer::Create(); });
|
||||
}
|
||||
if (IsBackendEnabled(ScreenCaptureMethod::Gdi)) {
|
||||
factories.push_back([] { return std::make_unique<ScreenCapturerGdi>(); });
|
||||
for (const auto backend :
|
||||
WindowsCaptureBackendOrder(capture_method_, headless, usbmmidd)) {
|
||||
switch (backend) {
|
||||
case ScreenCaptureMethod::Dxgi:
|
||||
factories.push_back([] { return std::make_unique<ScreenCapturerDxgi>(); });
|
||||
break;
|
||||
case ScreenCaptureMethod::Wgc:
|
||||
factories.push_back([] { return WgcPluginCapturer::Create(); });
|
||||
break;
|
||||
case ScreenCaptureMethod::Gdi:
|
||||
factories.push_back([] { return std::make_unique<ScreenCapturerGdi>(); });
|
||||
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<std::mutex> 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<ScreenCapturerGdi*>(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<WgcPluginCapturer*>(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<ScreenCapturer> previous;
|
||||
@@ -796,15 +875,13 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
std::lock_guard<std::mutex> lock(impl_mutex_);
|
||||
previous = std::move(impl_);
|
||||
}
|
||||
if (!StartPreferredBackend(headless, requested_monitor)) {
|
||||
if (!StartPreferredBackend(headless, requested_monitor, usbmmidd)) {
|
||||
std::lock_guard<std::mutex> 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<ScreenCapturerDxgi*>(impl_.get()) &&
|
||||
if (usbmmidd) {
|
||||
fallback_started = StartPreferredBackend(false, requested_monitor, true);
|
||||
} else if (dynamic_cast<ScreenCapturerDxgi*>(impl_.get()) &&
|
||||
TryStartBackend(WgcPluginCapturer::Create(), requested_monitor)) {
|
||||
fallback_started = true;
|
||||
}
|
||||
if (!fallback_started && (dynamic_cast<WgcPluginCapturer*>(impl_.get()) ||
|
||||
if (!usbmmidd && !fallback_started && (dynamic_cast<WgcPluginCapturer*>(impl_.get()) ||
|
||||
dynamic_cast<ScreenCapturerDxgi*>(impl_.get()))) {
|
||||
fallback_started = TryStartBackend(std::make_unique<ScreenCapturerGdi>(),
|
||||
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<ScreenCapturer> previous;
|
||||
@@ -916,22 +1002,30 @@ void ScreenCapturerWin::MaybeAdoptVirtualDisplay() {
|
||||
previous = std::move(impl_);
|
||||
}
|
||||
if (previous) previous->Destroy();
|
||||
if (StartPreferredBackend(false, current_monitor)) {
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::mutex> lock(alias_mutex_);
|
||||
virtual_display_name_.clear();
|
||||
virtual_display_slot_ = -1;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<int>(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<ScreenCapturerDxgi*>(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<ScreenCapturerDxgi*>(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) {
|
||||
|
||||
@@ -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<bool> native_output_error_logged_{false};
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<ULONGLONG> last_capture_progress_tick_{0};
|
||||
std::atomic<bool> display_probe_requested_{false};
|
||||
ULONGLONG last_display_probe_tick_ = 0;
|
||||
ULONGLONG last_capture_retry_tick_ = 0;
|
||||
HeadlessRecovery headless_recovery_;
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<bool> 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<bool> virtual_display_ready_{false};
|
||||
std::atomic<bool> 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<int> monitor_index_{0};
|
||||
std::atomic<bool> secure_desktop_capture_active_{false};
|
||||
std::atomic<bool> 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<BackendFactory> PreferredBackends(bool headless) const;
|
||||
std::vector<BackendFactory> PreferredBackends(bool headless,
|
||||
bool usbmmidd = false) const;
|
||||
bool TryStartBackend(std::unique_ptr<ScreenCapturer> 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();
|
||||
|
||||
@@ -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 <string>
|
||||
#include <vector>
|
||||
|
||||
#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<DisplayInfo>& current,
|
||||
const std::string& name, int slot,
|
||||
std::vector<DisplayInfo>* canonical,
|
||||
std::vector<int>* backend_to_canonical) {
|
||||
if (slot < 0 || slot >= static_cast<int>(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<int>(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user