[feat] add headless compatibility capture and usbmmidd virtual display plugging

This commit is contained in:
dijunkun
2026-09-21 01:10:30 +08:00
parent fb556a763d
commit 6387f2e555
11 changed files with 1510 additions and 59 deletions
@@ -0,0 +1,85 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_DISPLAY_PRESENCE_H_
#define CROSSDESK_DISPLAY_PRESENCE_H_
#include <Windows.h>
#include <algorithm>
#include <cstddef>
#include <vector>
namespace crossdesk {
// Without any attached monitor, Windows keeps the desktop on a phantom display
// target with an OS-chosen resolution. DXGI has no output to duplicate there,
// but GDI still reads the composed desktop and input injection keeps working.
enum class DisplayPresence { present, headless, unknown };
struct DisplayPresenceProbe {
bool query_succeeded = false; // QueryDisplayConfig returned a topology
size_t active_paths = 0; // active display paths, phantom included
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
};
// Pure policy, kept separate from Win32 so it can be tested with fixtures.
inline DisplayPresence ClassifyDisplayPresence(
const DisplayPresenceProbe& probe) {
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.attached_monitors > 0) return DisplayPresence::present;
if (probe.desktop_devices > 0) return DisplayPresence::headless;
return DisplayPresence::unknown;
}
inline DisplayPresenceProbe ProbeDisplayPresence() {
DisplayPresenceProbe probe;
// The buffer size can change between the two calls during a topology
// change; a few retries cover that window.
for (int attempt = 0; attempt < 4 && !probe.query_succeeded; ++attempt) {
UINT32 path_count = 0;
UINT32 mode_count = 0;
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &path_count,
&mode_count) != ERROR_SUCCESS) break;
std::vector<DISPLAYCONFIG_PATH_INFO> paths((std::max)(path_count, 1u));
std::vector<DISPLAYCONFIG_MODE_INFO> modes((std::max)(mode_count, 1u));
const LONG status = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &path_count,
paths.data(), &mode_count,
modes.data(), nullptr);
if (status == ERROR_INSUFFICIENT_BUFFER) continue;
if (status != ERROR_SUCCESS) break;
probe.query_succeeded = true;
for (UINT32 index = 0; index < path_count; ++index) {
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;
}
}
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)) continue;
++probe.desktop_devices;
for (DWORD child = 0;; ++child) {
DISPLAY_DEVICEW monitor{sizeof(monitor)};
if (!EnumDisplayDevicesW(adapter.DeviceName, child, &monitor, 0)) break;
if (monitor.StateFlags & DISPLAY_DEVICE_ACTIVE) ++probe.attached_monitors;
}
}
return probe;
}
} // namespace crossdesk
#endif // CROSSDESK_DISPLAY_PRESENCE_H_
@@ -122,15 +122,25 @@ int ScreenCapturerGdi::ResetToInitialMonitor() {
void ScreenCapturerGdi::CaptureLoop() {
int interval_ms = fps_ > 0 ? (1000 / fps_) : 16;
HDC screen_dc = GetDC(nullptr);
bool dc_error_logged = false;
while (running_) {
if (paused_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (!screen_dc) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// The screen DC can be unavailable during a desktop switch; retry so
// capture resumes once the desktop is reachable again.
if (!dc_error_logged) {
LOG_WARN("GDI: GetDC(nullptr) failed, error={}; retrying",
GetLastError());
dc_error_logged = true;
}
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
screen_dc = GetDC(nullptr);
continue;
}
dc_error_logged = false;
int idx = monitor_index_.load();
if (idx < 0 || idx >= static_cast<int>(display_info_list_.size())) {
@@ -157,11 +167,22 @@ void ScreenCapturerGdi::CaptureLoop() {
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
// GDI is the only backend on a headless desktop, where odd DIB sizes and
// exhausted GDI handles are plausible; never dereference a failed alloc.
void* bits = nullptr;
HDC mem_dc = CreateCompatibleDC(screen_dc);
HBITMAP dib =
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
HGDIOBJ old = SelectObject(mem_dc, dib);
HBITMAP dib = mem_dc ? CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS,
&bits, nullptr, 0)
: nullptr;
HGDIOBJ old = (dib && bits) ? SelectObject(mem_dc, dib) : nullptr;
if (!old) {
LOG_WARN("GDI: frame buffer allocation failed ({}x{}), error={}", width,
height, GetLastError());
if (dib) DeleteObject(dib);
if (mem_dc) DeleteDC(mem_dc);
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
continue;
}
BitBlt(mem_dc, 0, 0, width, height, screen_dc, left, top,
SRCCOPY | CAPTUREBLT);
@@ -202,7 +223,7 @@ void ScreenCapturerGdi::CaptureLoop() {
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
}
ReleaseDC(nullptr, screen_dc);
if (screen_dc) ReleaseDC(nullptr, screen_dc);
}
} // namespace crossdesk
@@ -18,6 +18,7 @@
#include <display_stream_id.h>
#include "captured_nv12_frame.h"
#include "captured_cursor_state.h"
#include "display_presence.h"
#include "dxgi_cursor_state.h"
#include "interactive_desktop.h"
#include "interactive_state.h"
@@ -30,6 +31,7 @@
#include "secure_desktop_status_poller.h"
#include "service_host.h"
#include "session_helper_shared.h"
#include "../virtual_display/usbmmidd_virtual_display.h"
#include "wgc_plugin_api.h"
namespace crossdesk {
@@ -180,6 +182,63 @@ const char* CaptureBackendName(const ScreenCapturer* capturer) {
return "unavailable";
}
// 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
// monitor is attached the real probe takes over again.
bool ForcedHeadlessForTesting() {
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;
}
// Headless compatibility mode: no monitor is attached, so the desktop lives on
// a phantom target at an OS-chosen resolution. DXGI cannot duplicate it and
// 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) {
if (method != ScreenCaptureMethod::Auto) return false;
if (ForcedHeadlessForTesting()) {
static std::atomic<bool> logged{false};
if (!logged.exchange(true)) {
LOG_WARN("Windows capturer: CROSSDESK_FORCE_HEADLESS=1 set; treating the "
"desktop as headless until a usbmmidd monitor is plugged");
}
return true;
}
const auto probe = ProbeDisplayPresence();
const auto presence = ClassifyDisplayPresence(probe);
// 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);
}
}
return presence == DisplayPresence::headless;
}
std::string BuildSecureCaptureCommand(int left, int top, int width, int height,
bool show_cursor,
const std::string& stage,
@@ -502,25 +561,22 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
};
impl_.reset();
const bool headless = DetectHeadlessDesktop(capture_method_);
headless_compat_.store(headless, std::memory_order_relaxed);
int ret = -1;
auto try_init = [&](std::unique_ptr<ScreenCapturer> candidate) {
for (const auto& create : PreferredBackends(headless)) {
std::unique_ptr<ScreenCapturer> candidate = create();
ret = candidate ? candidate->Init(fps_, cb_) : -1;
if (ret != 0) {
LOG_WARN("Windows capturer: {} init failed (ret={})",
CaptureBackendName(candidate.get()), ret);
return false;
if (ret == 0) {
impl_ = std::move(candidate);
break;
}
impl_ = std::move(candidate);
return true;
};
// Short-circuiting also keeps the unused WGC plugin unloaded.
if ((IsBackendEnabled(ScreenCaptureMethod::Dxgi) &&
try_init(std::make_unique<ScreenCapturerDxgi>())) ||
(IsBackendEnabled(ScreenCaptureMethod::Wgc) &&
try_init(WgcPluginCapturer::Create())) ||
(IsBackendEnabled(ScreenCaptureMethod::Gdi) &&
try_init(std::make_unique<ScreenCapturerGdi>()))) {
LOG_INFO("Windows capturer: using {}", CaptureBackendName(impl_.get()));
LOG_WARN("Windows capturer: {} init failed (ret={})",
CaptureBackendName(candidate.get()), ret);
}
if (impl_) {
LOG_INFO("Windows capturer: using {}{}", CaptureBackendName(impl_.get()),
headless ? " (headless compatibility mode)" : "");
BuildCanonicalFromImpl();
monitor_index_.store(0, std::memory_order_relaxed);
return 0;
@@ -533,6 +589,9 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
int ScreenCapturerWin::Destroy() {
Stop();
// Stop() releases the virtual display on a worker; finish it here so the
// provisioner is idle before its owner goes away.
JoinVirtualDisplayThread();
paused_.store(false, std::memory_order_relaxed);
if (impl_) {
impl_->Destroy();
@@ -651,6 +710,51 @@ bool ScreenCapturerWin::TryStartBackend(
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.
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>(); });
}
return factories;
}
bool ScreenCapturerWin::StartPreferredBackend(bool headless,
int monitor_index) {
for (const auto& create : PreferredBackends(headless)) {
if (TryStartBackend(create(), monitor_index)) return true;
}
return false;
}
void ScreenCapturerWin::ResetPostSecureDesktopState() {
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false,
std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
SharedSecureDesktopCursorState().SetActive(false);
}
int ScreenCapturerWin::MarkStarted() {
running_.store(true, std::memory_order_relaxed);
NotifyPrivacyCapture(true);
ResetPostSecureDesktopState();
if (!secure_capture_thread_.joinable()) {
secure_capture_thread_ =
std::thread([this]() { SecureDesktopCaptureLoop(); });
}
return 0;
}
int ScreenCapturerWin::Start(bool show_cursor) {
if (!impl_) return -1;
if (running_.load(std::memory_order_relaxed)) {
@@ -670,8 +774,40 @@ int ScreenCapturerWin::Start(bool show_cursor) {
// open; logical stream IDs must remain stable.
const int requested_monitor =
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_);
// 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);
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);
impl_->Destroy();
// Keep an instance around on failure so the next Start can retry.
std::unique_ptr<ScreenCapturer> previous;
{
std::lock_guard<std::mutex> lock(impl_mutex_);
previous = std::move(impl_);
}
if (!StartPreferredBackend(headless, requested_monitor)) {
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;
}
impl_->Destroy();
int ret = impl_->Init(fps_, cb_);
ret = impl_->Init(fps_, cb_);
if (ret == 0) {
RebuildAliasesFromImpl();
// Canonical wire/input slots may differ from this backend's refreshed
@@ -706,42 +842,100 @@ int ScreenCapturerWin::Start(bool show_cursor) {
}
}
running_.store(true, std::memory_order_relaxed);
NotifyPrivacyCapture(true);
SharedSecureDesktopCursorState().SetActive(false);
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false,
std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
if (!secure_capture_thread_.joinable()) {
secure_capture_thread_ =
std::thread([this]() { SecureDesktopCaptureLoop(); });
}
return 0;
ret = MarkStarted();
if (ret == 0 && headless) BeginVirtualDisplayProvisioning();
return ret;
}
int ScreenCapturerWin::Stop() {
NotifyPrivacyCapture(false);
running_.store(false, std::memory_order_relaxed);
last_capture_progress_tick_.store(0, std::memory_order_relaxed);
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false,
std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
int ret = 0;
// Drain management operations before stopping/replacing their backend.
virtual_display_.RequestCancel();
StopSecureCaptureThread();
JoinVirtualDisplayThread();
ResetPostSecureDesktopState();
if (impl_) {
ret = impl_->Stop();
}
SharedCapturedCursorState().Reset();
SharedSecureDesktopCursorState().SetActive(false);
StopSecureDesktopSharedCapture(secure_shared_session_id_);
// After the backend released its duplication of the virtual output. The
// unplug itself can take seconds; do not hold the UI thread for it.
BeginVirtualDisplayRelease();
return ret;
}
void ScreenCapturerWin::JoinVirtualDisplayThread() {
if (virtual_display_thread_.joinable()) virtual_display_thread_.join();
}
void ScreenCapturerWin::BeginVirtualDisplayProvisioning() {
JoinVirtualDisplayThread();
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.
std::string error;
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",
error);
}
});
}
void ScreenCapturerWin::BeginVirtualDisplayRelease() {
JoinVirtualDisplayThread();
virtual_display_ready_.store(false, std::memory_order_relaxed);
if (!virtual_display_.NeedsRelease()) return;
virtual_display_thread_ = std::thread([this] { virtual_display_.Release(); });
}
void ScreenCapturerWin::MaybeAdoptVirtualDisplay() {
if (!virtual_display_ready_.exchange(false, std::memory_order_acq_rel))
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 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");
NotifyPrivacyCapture(false);
// 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;
{
std::lock_guard<std::mutex> lock(impl_mutex_);
previous = std::move(impl_);
}
if (previous) previous->Destroy();
if (StartPreferredBackend(false, current_monitor)) {
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");
{
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));
}
}
NotifyPrivacyCapture(true);
}
int ScreenCapturerWin::Pause(int monitor_index) {
paused_.store(true, std::memory_order_relaxed);
std::lock_guard<std::mutex> lock(impl_mutex_);
@@ -949,21 +1143,15 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
return true;
}
if ((IsBackendEnabled(ScreenCaptureMethod::Dxgi) &&
TryStartBackend(std::make_unique<ScreenCapturerDxgi>(), current_monitor)) ||
(IsBackendEnabled(ScreenCaptureMethod::Wgc) &&
TryStartBackend(WgcPluginCapturer::Create(), current_monitor)) ||
(IsBackendEnabled(ScreenCaptureMethod::Gdi) &&
TryStartBackend(std::make_unique<ScreenCapturerGdi>(), current_monitor))) {
if (StartPreferredBackend(headless_compat_.load(std::memory_order_relaxed),
current_monitor)) {
return true;
}
if (impl_) {
LOG_WARN(
"Windows capturer: all backend restart attempts after secure desktop "
"failed (last_ret={})",
ret);
}
LOG_WARN(
"Windows capturer: all backend restart attempts after secure desktop "
"failed (last_ret={})",
ret);
return false;
}
@@ -1405,6 +1593,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
StopSecureDesktopSharedCapture(secure_shared_session_id_);
MaybeAdoptVirtualDisplay();
CheckCaptureProgress(now);
if (post_secure_restart_pending) {
if (now >= post_secure_restart_deadline_tick) {
@@ -12,6 +12,7 @@
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
@@ -22,6 +23,8 @@
#include "screen_capturer.h"
#include "screen_capture_method.h"
#include "privacy_controller.h"
// Relative: platform include directories stay private to the capture target.
#include "../virtual_display/virtual_display_provisioner.h"
namespace crossdesk {
@@ -50,6 +53,8 @@ class ScreenCapturerWin : public ScreenCapturer {
std::vector<DisplayInfo> GetDisplayInfoList() override;
int GetCurrentMonitorIndex() const override;
// True while this session captures a usbmmidd monitor it plugged itself.
bool IsVirtualDisplayActive() const { return virtual_display_.active(); }
void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; }
// Set before Init(), while capture is stopped.
void SetCaptureMethod(ScreenCaptureMethod method) { capture_method_ = method; }
@@ -58,6 +63,7 @@ class ScreenCapturerWin : public ScreenCapturer {
static bool CurrentFrameIsFromSecureDesktop();
private:
using BackendFactory = std::function<std::unique_ptr<ScreenCapturer>()>;
std::unique_ptr<ScreenCapturer> impl_;
// Serializes backend replacement (secure-desktop recovery runs on the
// management thread) against the public entry points that dereference it.
@@ -87,6 +93,16 @@ class ScreenCapturerWin : public ScreenCapturer {
std::atomic<bool> paused_{false};
std::atomic<bool> show_cursor_{true};
bool applied_show_cursor_ = true;
std::atomic<bool> headless_compat_{false};
// Session-scoped: plugged on a headless desktop after Start() returns and
// released after Stop(), so an attached physical monitor is never shadowed
// between sessions. Both run on virtual_display_thread_ because the helper
// may need up to two minutes to install the driver; Start() itself begins
// in GDI compatibility mode and the management thread adopts the virtual
// display (switching to a GPU backend) once virtual_display_ready_ is set.
VirtualDisplayProvisioner virtual_display_;
std::thread virtual_display_thread_;
std::atomic<bool> virtual_display_ready_{false};
std::atomic<int> monitor_index_{0};
std::atomic<bool> secure_desktop_capture_active_{false};
std::atomic<bool> post_secure_desktop_waiting_for_frame_{false};
@@ -112,8 +128,16 @@ class ScreenCapturerWin : public ScreenCapturer {
void BuildCanonicalFromImpl();
void RebuildAliasesFromImpl(bool preserve_backend_slots = false);
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;
bool TryStartBackend(std::unique_ptr<ScreenCapturer> candidate,
int monitor_index);
bool StartPreferredBackend(bool headless, int monitor_index);
void ResetPostSecureDesktopState();
// Common tail of every successful Start(): publish running state and
// launch the management thread.
int MarkStarted();
void EmitCapturedFrame(unsigned char* data, int size, int width, int height,
const char* stream_id,
const MiniRtcNativeVideoFrame* native_frame = nullptr,
@@ -121,6 +145,13 @@ class ScreenCapturerWin : public ScreenCapturer {
void StopSecureCaptureThread();
bool RestartCaptureBackendAfterSecureDesktop();
void CheckCaptureProgress(ULONGLONG now);
// Virtual display provisioning runs off the caller's thread.
void JoinVirtualDisplayThread();
void BeginVirtualDisplayProvisioning();
void BeginVirtualDisplayRelease();
// Management thread: once the worker plugged the virtual display, replace
// the GDI compatibility backend with a GPU backend on that display.
void MaybeAdoptVirtualDisplay();
void ApplyCursorCaptureSetting();
void SecureDesktopCaptureLoop();
bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height,
@@ -11,6 +11,7 @@
#include <chrono>
#include <cstring>
#include <filesystem>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
@@ -25,6 +26,8 @@
#include "path_manager.h"
#include "rd_log.h"
#include "session_helper_shared.h"
#include "usbmmidd_policy.h"
#include "usbmmidd_virtual_display.h"
#include "windows_cursor_state.h"
#include "cursor_draw.h"
#include "windows_input_marker.h"
@@ -453,8 +456,193 @@ std::string BuildHelperStatusResponse(HelperState* helper_state) {
return json.dump();
}
// Driver install and monitor plug/unplug take seconds. They run on a worker
// so the status pipe (polled by the service every second) stays responsive;
// the GUI polls the status command until |busy| clears.
struct VirtualDisplayJobState {
std::mutex mutex;
std::thread worker;
bool busy = false;
// Monitors this helper plugged and has not yet unplugged. Unplug releases
// only these, so a monitor the user plugged for other software survives.
int plugged_by_helper = 0;
// An unplug that arrived while a plug was running; the plug worker honours
// it on completion so a cancelled session never leaves a monitor behind.
bool unplug_queued = false;
std::string last_action;
std::string last_error;
~VirtualDisplayJobState() {
if (worker.joinable()) worker.join();
}
};
// The helper pipe grants read/write to every authenticated user so that the
// GUI can poll status without elevation. Plug/unplug install a driver and
// change the interactive desktop's topology as SYSTEM, so those commands are
// accepted only from a CrossDesk executable in this helper's own directory.
bool IsTrustedVirtualDisplayClient(HANDLE pipe) {
ULONG client_pid = 0;
if (!GetNamedPipeClientProcessId(pipe, &client_pid)) return false;
HANDLE process =
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, client_pid);
if (process == nullptr) return false;
wchar_t image[MAX_PATH] = {0};
DWORD length = MAX_PATH;
const bool queried =
QueryFullProcessImageNameW(process, 0, image, &length) && length > 0;
CloseHandle(process);
if (!queried) return false;
wchar_t own[MAX_PATH] = {0};
const DWORD own_length = GetModuleFileNameW(nullptr, own, MAX_PATH);
if (own_length == 0 || own_length >= MAX_PATH) return false;
std::error_code ignored;
const std::filesystem::path client(image);
if (_wcsicmp(client.filename().c_str(), L"CrossDesk.exe") != 0) return false;
const std::filesystem::path client_directory =
std::filesystem::weakly_canonical(client.parent_path(), ignored);
const std::filesystem::path own_directory = std::filesystem::weakly_canonical(
std::filesystem::path(own).parent_path(), ignored);
return _wcsicmp(client_directory.c_str(), own_directory.c_str()) == 0;
}
std::string BuildVirtualDisplayStatusResponse(VirtualDisplayJobState* job) {
const crossdesk::UsbmmiddStatus status = crossdesk::QueryUsbmmiddStatus();
Json json;
json["ok"] = true;
json["package_available"] = status.package_available;
json["package_directory"] = status.package_directory;
json["driver_installed"] = status.driver_installed;
json["plugged_monitors"] = status.plugged_monitors;
std::lock_guard<std::mutex> lock(job->mutex);
json["busy"] = job->busy;
json["last_action"] = job->last_action;
json["last_error"] = job->last_error;
return json.dump();
}
std::string StartVirtualDisplayJob(
VirtualDisplayJobState* job, const char* action,
std::function<bool(std::string*)> work) {
std::lock_guard<std::mutex> lock(job->mutex);
if (job->busy) return BuildErrorJson("virtual_display_busy");
// A finished worker no longer touches the mutex; joining under it is safe.
if (job->worker.joinable()) job->worker.join();
job->busy = true;
job->last_action = action;
job->last_error.clear();
LOG_INFO("Session helper virtual display job started: {}", action);
job->worker = std::thread([job, action, work = std::move(work)]() {
std::string error;
const bool ok = work(&error);
if (ok) {
LOG_INFO("Session helper virtual display job finished: {}", action);
} else {
LOG_WARN("Session helper virtual display job failed: {}, error={}",
action, error);
}
std::lock_guard<std::mutex> lock(job->mutex);
job->busy = false;
job->last_error = ok ? std::string() : (error.empty() ? "unknown" : error);
});
Json json;
json["ok"] = true;
json["pending"] = true;
return json.dump();
}
std::string HandleVirtualDisplayCommand(const std::string& command,
HANDLE pipe,
VirtualDisplayJobState* job,
bool* handled) {
*handled = true;
if (command == crossdesk::kCrossDeskVirtualDisplayStatusCommand) {
return BuildVirtualDisplayStatusResponse(job);
}
const bool mutating =
command == crossdesk::kCrossDeskVirtualDisplayUnplugCommand ||
command.rfind(crossdesk::kCrossDeskVirtualDisplayPlugCommandPrefix, 0) ==
0;
if (mutating && !IsTrustedVirtualDisplayClient(pipe)) {
LOG_WARN("Session helper rejected virtual display command '{}' from an "
"untrusted client",
command);
return BuildErrorJson("virtual_display_client_not_allowed",
ERROR_ACCESS_DENIED);
}
if (command == crossdesk::kCrossDeskVirtualDisplayUnplugCommand) {
{
std::lock_guard<std::mutex> lock(job->mutex);
if (job->busy && job->last_action == "plug") {
job->unplug_queued = true;
Json json;
json["ok"] = true;
json["pending"] = true;
json["queued"] = true;
return json.dump();
}
}
return StartVirtualDisplayJob(job, "unplug", [job](std::string* error) {
int count = 0;
{
std::lock_guard<std::mutex> lock(job->mutex);
count = job->plugged_by_helper;
}
if (!crossdesk::ReleaseUsbmmiddDisplays(count, error)) return false;
std::lock_guard<std::mutex> lock(job->mutex);
job->plugged_by_helper = 0;
return true;
});
}
crossdesk::VirtualDisplayMode mode;
if (crossdesk::ParseVirtualDisplayPlugCommand(command, &mode)) {
return StartVirtualDisplayJob(job, "plug", [job, mode](std::string* error) {
{
// This job only starts when no other job runs, so any unplug queued
// behind an earlier plug has already been honoured.
std::lock_guard<std::mutex> lock(job->mutex);
job->unplug_queued = false;
}
int plugged = 0;
const bool ok = crossdesk::ProvisionUsbmmiddDisplay(mode, &plugged, error);
int to_release = 0;
{
std::lock_guard<std::mutex> lock(job->mutex);
job->plugged_by_helper += plugged;
if (job->unplug_queued) {
job->unplug_queued = false;
to_release = job->plugged_by_helper;
}
}
if (to_release > 0) {
LOG_INFO("Session helper virtual display: releasing {} monitor(s) "
"for an unplug queued during the plug",
to_release);
std::string release_error;
if (crossdesk::ReleaseUsbmmiddDisplays(to_release, &release_error)) {
std::lock_guard<std::mutex> lock(job->mutex);
job->plugged_by_helper = 0;
} else {
LOG_WARN("Session helper virtual display: queued unplug failed, "
"error={}",
release_error);
}
}
return ok;
});
}
if (command.rfind(crossdesk::kCrossDeskVirtualDisplayPlugCommandPrefix, 0) ==
0) {
return BuildErrorJson("invalid_virtual_display_mode");
}
*handled = false;
return {};
}
void HelperIpcServerLoop(HANDLE stop_event, DWORD session_id,
HelperState* helper_state) {
HelperState* helper_state,
VirtualDisplayJobState* virtual_display_job) {
IpcSecurityAttributes security_attributes;
SECURITY_ATTRIBUTES* pipe_attributes = nullptr;
if (security_attributes.Initialize()) {
@@ -523,11 +711,14 @@ void HelperIpcServerLoop(HANDLE stop_event, DWORD session_id,
bytes_read > 0) {
std::string command(buffer, buffer + bytes_read);
std::string response;
bool handled = false;
if (command == crossdesk::kCrossDeskSessionHelperStatusCommand) {
response = BuildHelperStatusResponse(helper_state);
} else if (command == "ping") {
response = "{\"ok\":true,\"reply\":\"pong\"}";
} else {
} else if (response = HandleVirtualDisplayCommand(
command, pipe, virtual_display_job, &handled);
!handled) {
response = BuildErrorJson("unknown_command");
}
@@ -1865,8 +2056,9 @@ int main(int argc, char* argv[]) {
GetLastError());
}
VirtualDisplayJobState virtual_display_job;
std::thread ipc_thread(HelperIpcServerLoop, stop_event, current_session_id,
&helper_state);
&helper_state, &virtual_display_job);
std::string last_desktop_name;
bool last_lock_app = false;
@@ -0,0 +1,148 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_USBMMIDD_POLICY_H_
#define CROSSDESK_USBMMIDD_POLICY_H_
#include <algorithm>
#include <cstddef>
#include <cstdlib>
#include <cwctype>
#include <string>
#include <vector>
namespace crossdesk {
// Amyuni "USB Mobile Monitor" indirect display driver (usbmmidd_v2). The
// package ships deviceinstaller64.exe, usbmmIdd.inf/.cat and usbmmIdd.dll. It
// is bundled from apps/desktop/resources/windows/usbmmidd_v2 by the installer
// and the portable archive; a user may also drop it next to the executables.
inline constexpr wchar_t kUsbmmiddHardwareId[] = L"usbmmidd";
// Set when CrossDesk itself installed the driver. The NSIS uninstaller reads
// it (64-bit registry view) and removes the driver only in that case.
inline constexpr wchar_t kUsbmmiddInstallMarkerKey[] = L"SOFTWARE\\CrossDesk";
inline constexpr wchar_t kUsbmmiddInstallMarkerValue[] =
L"UsbmmiddDriverInstalled";
inline constexpr wchar_t kUsbmmiddAdapterDescription[] =
L"USB Mobile Monitor Virtual Display";
#if defined(_WIN64)
inline constexpr wchar_t kUsbmmiddInstallerName[] = L"deviceinstaller64.exe";
#else
inline constexpr wchar_t kUsbmmiddInstallerName[] = L"deviceinstaller.exe";
#endif
inline constexpr wchar_t kUsbmmiddInfName[] = L"usbmmidd.inf";
inline constexpr const wchar_t* kUsbmmiddPackageDirectories[] = {
L"usbmmidd_v2", L"usbmmidd", L"drivers\\usbmmidd_v2"};
// The driver accepts up to four monitors; remote capture needs one.
inline constexpr int kUsbmmiddMaxMonitors = 4;
// Session helper IPC sub-protocol (see session_helper_shared.h for the pipe).
// Plug/unplug run asynchronously in the helper; poll the status command.
inline constexpr char kCrossDeskVirtualDisplayStatusCommand[] =
"virtual-display-status";
inline constexpr char kCrossDeskVirtualDisplayPlugCommandPrefix[] =
"virtual-display-plug:";
inline constexpr char kCrossDeskVirtualDisplayUnplugCommand[] =
"virtual-display-unplug";
struct VirtualDisplayMode {
int width = 0;
int height = 0;
bool operator==(const VirtualDisplayMode& other) const {
return width == other.width && height == other.height;
}
bool operator!=(const VirtualDisplayMode& other) const {
return !(*this == other);
}
};
inline constexpr VirtualDisplayMode kDefaultVirtualDisplayMode{1920, 1080};
inline bool ContainsCaseInsensitive(const std::wstring& haystack,
const std::wstring& needle) {
if (needle.empty()) return true;
if (haystack.size() < needle.size()) return false;
const auto it = std::search(
haystack.begin(), haystack.end(), needle.begin(), needle.end(),
[](wchar_t a, wchar_t b) { return std::towlower(a) == std::towlower(b); });
return it != haystack.end();
}
// EnumDisplayDevices reports the adapter description plus a PnP id such as
// "PCI\VEN_..." or "ROOT\DISPLAY\0000". The IDD adapter carries the hardware
// id in its device id; the description is a second signal for older packages.
inline bool IsUsbmmiddAdapter(const std::wstring& device_string,
const std::wstring& device_id) {
return ContainsCaseInsensitive(device_id, kUsbmmiddHardwareId) ||
ContainsCaseInsensitive(device_string, kUsbmmiddAdapterDescription);
}
// Exact match first; otherwise the largest mode that fits inside the request,
// so a 4K request on a 1080p-only package still yields the best available
// picture; otherwise the smallest mode the driver offers.
inline bool ChooseVirtualDisplayMode(
const std::vector<VirtualDisplayMode>& supported,
VirtualDisplayMode requested, VirtualDisplayMode* chosen) {
if (chosen == nullptr || supported.empty()) return false;
const VirtualDisplayMode* best = nullptr;
const VirtualDisplayMode* smallest = nullptr;
auto area = [](const VirtualDisplayMode& mode) {
return static_cast<long long>(mode.width) * mode.height;
};
for (const auto& mode : supported) {
if (mode.width <= 0 || mode.height <= 0) continue;
if (mode == requested) {
*chosen = mode;
return true;
}
if (mode.width <= requested.width && mode.height <= requested.height &&
(best == nullptr || area(mode) > area(*best))) {
best = &mode;
}
if (smallest == nullptr || area(mode) < area(*smallest)) smallest = &mode;
}
if (best != nullptr) {
*chosen = *best;
return true;
}
if (smallest != nullptr) {
*chosen = *smallest;
return true;
}
return false;
}
// "virtual-display-plug:1920x1080"
inline std::string BuildVirtualDisplayPlugCommand(VirtualDisplayMode mode) {
return std::string(kCrossDeskVirtualDisplayPlugCommandPrefix) +
std::to_string(mode.width) + "x" + std::to_string(mode.height);
}
inline bool ParseVirtualDisplayPlugCommand(const std::string& command,
VirtualDisplayMode* mode) {
if (mode == nullptr) return false;
const std::string prefix(kCrossDeskVirtualDisplayPlugCommandPrefix);
if (command.rfind(prefix, 0) != 0) return false;
const std::string payload = command.substr(prefix.size());
const size_t separator = payload.find('x');
if (separator == std::string::npos || separator == 0 ||
separator + 1 >= payload.size()) {
return false;
}
char* end = nullptr;
const long width = std::strtol(payload.c_str(), &end, 10);
if (end != payload.c_str() + separator) return false;
const long height = std::strtol(payload.c_str() + separator + 1, &end, 10);
if (end == nullptr || *end != '\0') return false;
if (width <= 0 || height <= 0 || width > 16384 || height > 16384) return false;
mode->width = static_cast<int>(width);
mode->height = static_cast<int>(height);
return true;
}
} // namespace crossdesk
#endif // CROSSDESK_USBMMIDD_POLICY_H_
@@ -0,0 +1,384 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include "usbmmidd_virtual_display.h"
// clang-format off
#include <Windows.h>
#include <initguid.h> // defines GUID_DEVCLASS_DISPLAY in this unit
#include <SetupAPI.h>
#include <devguid.h>
// clang-format on
#include <algorithm>
#include <string>
#include <vector>
#include "rd_log.h"
namespace crossdesk {
namespace {
constexpr DWORD kInstallerPollIntervalMs = 250;
constexpr DWORD kDriverInstallTimeoutMs = 90 * 1000;
constexpr DWORD kInstallerCommandTimeoutMs = 20 * 1000;
std::string Narrow(const std::wstring& value) {
if (value.empty()) return {};
const int size = WideCharToMultiByte(CP_UTF8, 0, value.data(),
static_cast<int>(value.size()), nullptr,
0, nullptr, nullptr);
std::string result(static_cast<size_t>((std::max)(size, 0)), '\0');
if (size > 0) {
WideCharToMultiByte(CP_UTF8, 0, value.data(),
static_cast<int>(value.size()), result.data(), size,
nullptr, nullptr);
}
return result;
}
void SetError(std::string* error, const std::string& message) {
if (error != nullptr) *error = message;
}
std::filesystem::path CurrentExecutableDirectory() {
wchar_t module_path[MAX_PATH] = {0};
const DWORD length = GetModuleFileNameW(nullptr, module_path, MAX_PATH);
if (length == 0 || length >= MAX_PATH) return {};
return std::filesystem::path(module_path).parent_path();
}
struct UsbmmiddAdapter {
std::wstring device_name; // \\.\DISPLAYn
bool attached_to_desktop = false;
};
std::vector<UsbmmiddAdapter> EnumerateUsbmmiddAdapters() {
std::vector<UsbmmiddAdapter> adapters;
for (DWORD index = 0;; ++index) {
DISPLAY_DEVICEW device{sizeof(device)};
if (!EnumDisplayDevicesW(nullptr, index, &device, 0)) break;
if (!IsUsbmmiddAdapter(device.DeviceString, device.DeviceID)) continue;
UsbmmiddAdapter adapter;
adapter.device_name = device.DeviceName;
adapter.attached_to_desktop =
(device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) != 0;
adapters.push_back(std::move(adapter));
}
return adapters;
}
// Runs "<installer> <arguments>" hidden with the package folder as the
// working directory (the installer resolves usbmmIdd.dll relative to it).
bool RunUsbmmiddInstaller(const UsbmmiddPackage& package,
const std::wstring& arguments, DWORD timeout_ms,
std::string* error) {
if (!package.Available()) {
SetError(error, "usbmmidd_package_missing");
return false;
}
std::wstring command_line =
L"\"" + package.installer.wstring() + L"\" " + arguments;
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process{};
const std::wstring working_directory = package.directory.wstring();
if (!CreateProcessW(package.installer.c_str(), command_line.data(), nullptr,
nullptr, FALSE, CREATE_NO_WINDOW, nullptr,
working_directory.c_str(), &startup, &process)) {
const DWORD code = GetLastError();
// 740 (ERROR_ELEVATION_REQUIRED) is the non-elevated GUI fallback path.
SetError(error, "usbmmidd_installer_launch_failed:" + std::to_string(code));
return false;
}
CloseHandle(process.hThread);
const DWORD wait = WaitForSingleObject(process.hProcess, timeout_ms);
DWORD exit_code = 0;
if (wait != WAIT_OBJECT_0) {
TerminateProcess(process.hProcess, 1);
CloseHandle(process.hProcess);
SetError(error, wait == WAIT_TIMEOUT ? "usbmmidd_installer_timeout"
: "usbmmidd_installer_wait_failed");
return false;
}
GetExitCodeProcess(process.hProcess, &exit_code);
CloseHandle(process.hProcess);
if (exit_code != 0) {
SetError(error, "usbmmidd_installer_exit:" + std::to_string(exit_code));
return false;
}
return true;
}
// Waits until |predicate| holds or the deadline passes.
template <typename Predicate>
bool WaitUntil(DWORD timeout_ms, Predicate predicate) {
const ULONGLONG deadline = GetTickCount64() + timeout_ms;
while (true) {
if (predicate()) return true;
const ULONGLONG now = GetTickCount64();
if (now >= deadline) return false;
Sleep(static_cast<DWORD>(
(std::min)(static_cast<ULONGLONG>(kInstallerPollIntervalMs),
deadline - now)));
}
}
// The uninstaller removes the driver only when CrossDesk installed it, so a
// driver the user set up for other software is left alone. The helper runs as
// SYSTEM and the in-process path is elevated, so HKLM is writable here; a
// failure only costs a stale device node after uninstall.
void MarkUsbmmiddDriverInstalledByCrossDesk() {
HKEY key = nullptr;
const LONG open = RegCreateKeyExW(HKEY_LOCAL_MACHINE, kUsbmmiddInstallMarkerKey,
0, nullptr, 0, KEY_SET_VALUE, nullptr, &key,
nullptr);
if (open != ERROR_SUCCESS) {
LOG_WARN("usbmmidd: failed to record the driver install marker, error={}",
open);
return;
}
const DWORD value = 1;
const LONG set = RegSetValueExW(key, kUsbmmiddInstallMarkerValue, 0, REG_DWORD,
reinterpret_cast<const BYTE*>(&value),
sizeof(value));
RegCloseKey(key);
if (set != ERROR_SUCCESS) {
LOG_WARN("usbmmidd: failed to record the driver install marker, error={}",
set);
}
}
bool DeviceHasUsbmmiddHardwareId(HDEVINFO device_info,
SP_DEVINFO_DATA* device_data) {
DWORD required = 0;
SetupDiGetDeviceRegistryPropertyW(device_info, device_data, SPDRP_HARDWAREID,
nullptr, nullptr, 0, &required);
if (required == 0) return false;
std::vector<wchar_t> buffer(required / sizeof(wchar_t) + 2, L'\0');
if (!SetupDiGetDeviceRegistryPropertyW(
device_info, device_data, SPDRP_HARDWAREID, nullptr,
reinterpret_cast<BYTE*>(buffer.data()),
static_cast<DWORD>(buffer.size() * sizeof(wchar_t)), nullptr)) {
return false;
}
// REG_MULTI_SZ: hardware ids separated by NUL, terminated by a double NUL.
for (const wchar_t* id = buffer.data(); *id != L'\0'; id += wcslen(id) + 1) {
if (ContainsCaseInsensitive(id, kUsbmmiddHardwareId)) return true;
}
return false;
}
std::vector<VirtualDisplayMode> EnumerateModes(const std::wstring& device) {
std::vector<VirtualDisplayMode> modes;
for (DWORD index = 0;; ++index) {
DEVMODEW mode{};
mode.dmSize = sizeof(mode);
if (!EnumDisplaySettingsExW(device.c_str(), index, &mode, 0)) break;
if (mode.dmBitsPerPel != 32) continue;
const VirtualDisplayMode candidate{static_cast<int>(mode.dmPelsWidth),
static_cast<int>(mode.dmPelsHeight)};
if (std::find(modes.begin(), modes.end(), candidate) == modes.end()) {
modes.push_back(candidate);
}
}
return modes;
}
} // namespace
UsbmmiddPackage LocateUsbmmiddPackage() {
UsbmmiddPackage package;
const std::filesystem::path base = CurrentExecutableDirectory();
if (base.empty()) return package;
std::error_code ignored;
for (const wchar_t* relative : kUsbmmiddPackageDirectories) {
const std::filesystem::path directory = base / relative;
const std::filesystem::path installer = directory / kUsbmmiddInstallerName;
if (!std::filesystem::is_regular_file(installer, ignored)) continue;
package.directory = directory;
package.installer = installer;
const std::filesystem::path inf = directory / kUsbmmiddInfName;
if (std::filesystem::is_regular_file(inf, ignored)) package.inf = inf;
break;
}
return package;
}
int CountUsbmmiddMonitors() {
int count = 0;
for (const auto& adapter : EnumerateUsbmmiddAdapters()) {
if (adapter.attached_to_desktop) ++count;
}
return count;
}
bool IsUsbmmiddDriverInstalled() {
const HDEVINFO device_info = SetupDiGetClassDevsW(
&GUID_DEVCLASS_DISPLAY, nullptr, nullptr, DIGCF_PRESENT);
if (device_info == INVALID_HANDLE_VALUE) {
// Fall back to the adapter list; it misses an installed-but-idle driver.
return !EnumerateUsbmmiddAdapters().empty();
}
bool installed = false;
SP_DEVINFO_DATA device_data{};
device_data.cbSize = sizeof(device_data);
for (DWORD index = 0;
!installed && SetupDiEnumDeviceInfo(device_info, index, &device_data);
++index) {
installed = DeviceHasUsbmmiddHardwareId(device_info, &device_data);
}
SetupDiDestroyDeviceInfoList(device_info);
return installed || !EnumerateUsbmmiddAdapters().empty();
}
UsbmmiddStatus QueryUsbmmiddStatus() {
UsbmmiddStatus status;
const UsbmmiddPackage package = LocateUsbmmiddPackage();
status.package_available = package.Available();
status.package_directory = Narrow(package.directory.wstring());
status.driver_installed = IsUsbmmiddDriverInstalled();
status.plugged_monitors = CountUsbmmiddMonitors();
return status;
}
bool InstallUsbmmiddDriver(const UsbmmiddPackage& package, std::string* error) {
if (IsUsbmmiddDriverInstalled()) return true;
if (package.inf.empty()) {
SetError(error, "usbmmidd_inf_missing");
return false;
}
LOG_INFO("usbmmidd: installing driver from '{}'",
Narrow(package.directory.wstring()));
const std::wstring arguments = L"install \"" + package.inf.wstring() +
L"\" " + kUsbmmiddHardwareId;
if (!RunUsbmmiddInstaller(package, arguments, kDriverInstallTimeoutMs,
error)) {
return false;
}
if (!WaitUntil(5000, IsUsbmmiddDriverInstalled)) {
SetError(error, "usbmmidd_driver_not_present_after_install");
return false;
}
MarkUsbmmiddDriverInstalledByCrossDesk();
return true;
}
bool PlugUsbmmiddMonitor(const UsbmmiddPackage& package, DWORD timeout_ms,
std::string* error) {
const int before = CountUsbmmiddMonitors();
if (before >= kUsbmmiddMaxMonitors) {
SetError(error, "usbmmidd_monitor_limit");
return false;
}
if (!RunUsbmmiddInstaller(package, L"enableidd 1",
kInstallerCommandTimeoutMs, error)) {
return false;
}
if (!WaitUntil(timeout_ms,
[before] { return CountUsbmmiddMonitors() > before; })) {
SetError(error, "usbmmidd_monitor_not_attached");
return false;
}
return true;
}
bool UnplugUsbmmiddMonitor(const UsbmmiddPackage& package, DWORD timeout_ms,
std::string* error) {
const int before = CountUsbmmiddMonitors();
if (before == 0) return true;
if (!RunUsbmmiddInstaller(package, L"enableidd 0",
kInstallerCommandTimeoutMs, error)) {
return false;
}
if (!WaitUntil(timeout_ms,
[before] { return CountUsbmmiddMonitors() < before; })) {
SetError(error, "usbmmidd_monitor_still_attached");
return false;
}
return true;
}
bool ApplyUsbmmiddDisplayMode(VirtualDisplayMode requested,
std::string* error) {
std::wstring device;
for (const auto& adapter : EnumerateUsbmmiddAdapters()) {
if (adapter.attached_to_desktop) {
device = adapter.device_name;
break;
}
}
if (device.empty()) {
SetError(error, "usbmmidd_monitor_not_attached");
return false;
}
VirtualDisplayMode chosen;
if (!ChooseVirtualDisplayMode(EnumerateModes(device), requested, &chosen)) {
SetError(error, "usbmmidd_no_display_modes");
return false;
}
DEVMODEW current{};
current.dmSize = sizeof(current);
if (EnumDisplaySettingsExW(device.c_str(), ENUM_CURRENT_SETTINGS, &current,
0) &&
static_cast<int>(current.dmPelsWidth) == chosen.width &&
static_cast<int>(current.dmPelsHeight) == chosen.height) {
return true;
}
DEVMODEW mode{};
mode.dmSize = sizeof(mode);
mode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;
mode.dmPelsWidth = static_cast<DWORD>(chosen.width);
mode.dmPelsHeight = static_cast<DWORD>(chosen.height);
mode.dmBitsPerPel = 32;
const LONG result = ChangeDisplaySettingsExW(device.c_str(), &mode, nullptr,
CDS_UPDATEREGISTRY, nullptr);
if (result != DISP_CHANGE_SUCCESSFUL) {
SetError(error, "usbmmidd_mode_change_failed:" + std::to_string(result));
return false;
}
LOG_INFO("usbmmidd: display '{}' set to {}x{} (requested {}x{})",
Narrow(device), chosen.width, chosen.height, requested.width,
requested.height);
return true;
}
bool ProvisionUsbmmiddDisplay(VirtualDisplayMode mode, int* plugged,
std::string* error) {
if (plugged != nullptr) *plugged = 0;
const UsbmmiddPackage package = LocateUsbmmiddPackage();
if (!package.Available()) {
SetError(error, "usbmmidd_package_missing");
return false;
}
if (!InstallUsbmmiddDriver(package, error)) return false;
if (CountUsbmmiddMonitors() == 0) {
if (!PlugUsbmmiddMonitor(package, 10 * 1000, error)) return false;
if (plugged != nullptr) *plugged = 1;
}
std::string mode_error;
if (!ApplyUsbmmiddDisplayMode(mode, &mode_error)) {
LOG_WARN("usbmmidd: keeping the driver default mode, error={}", mode_error);
}
return true;
}
bool ReleaseUsbmmiddDisplays(int count, std::string* error) {
const int to_unplug = (std::min)(count, CountUsbmmiddMonitors());
if (to_unplug <= 0) return true;
const UsbmmiddPackage package = LocateUsbmmiddPackage();
if (!package.Available()) {
SetError(error, "usbmmidd_package_missing");
return false;
}
for (int index = 0; index < to_unplug; ++index) {
if (!UnplugUsbmmiddMonitor(package, 10 * 1000, error)) return false;
}
return true;
}
} // namespace crossdesk
@@ -0,0 +1,69 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_USBMMIDD_VIRTUAL_DISPLAY_H_
#define CROSSDESK_USBMMIDD_VIRTUAL_DISPLAY_H_
#include <Windows.h>
#include <filesystem>
#include <string>
#include "usbmmidd_policy.h"
namespace crossdesk {
// Plug/unplug talks to the driver through Amyuni's installer and needs
// administrator rights inside the interactive session. The session helper
// (SYSTEM, console session) is the intended caller; the GUI only calls these
// directly when it is elevated and no helper is reachable.
struct UsbmmiddPackage {
std::filesystem::path directory;
std::filesystem::path installer;
std::filesystem::path inf; // empty when only the installer is present
bool Available() const { return !installer.empty(); }
};
// Looks next to the running executable (usbmmidd_v2\, usbmmidd\,
// drivers\usbmmidd_v2\). Both the GUI and the helper live in the same folder.
UsbmmiddPackage LocateUsbmmiddPackage();
struct UsbmmiddStatus {
bool package_available = false;
bool driver_installed = false;
int plugged_monitors = 0;
std::string package_directory;
};
UsbmmiddStatus QueryUsbmmiddStatus();
// Adapters attached to the desktop, i.e. monitors currently plugged in.
int CountUsbmmiddMonitors();
// Root-enumerated device node present, plugged or not.
bool IsUsbmmiddDriverInstalled();
// Each step is idempotent so a retry after a partial failure converges.
bool InstallUsbmmiddDriver(const UsbmmiddPackage& package, std::string* error);
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.
bool ApplyUsbmmiddDisplayMode(VirtualDisplayMode requested, std::string* error);
// Installs the driver on first use, plugs one monitor when none is attached
// and applies |mode|. A mode failure is logged but keeps the display.
// |plugged| receives how many monitors this call plugged (0 when an existing
// usbmmidd monitor was reused) so the caller can release exactly those.
bool ProvisionUsbmmiddDisplay(VirtualDisplayMode mode, int* plugged,
std::string* error);
// Unplugs up to |count| usbmmidd monitors, never more than are attached. A
// monitor plugged by the user or another program is left alone.
bool ReleaseUsbmmiddDisplays(int count, std::string* error);
} // namespace crossdesk
#endif // CROSSDESK_USBMMIDD_VIRTUAL_DISPLAY_H_
@@ -0,0 +1,263 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include "virtual_display_provisioner.h"
#include <algorithm>
#include <functional>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include "display_presence.h"
#include "named_pipe_deadline.h"
#include "rd_log.h"
#include "session_helper_shared.h"
#include "usbmmidd_virtual_display.h"
namespace crossdesk {
namespace {
using Json = nlohmann::json;
constexpr DWORD kHelperStatusTimeoutMs = 500;
constexpr DWORD kHelperCommandTimeoutMs = 2000;
constexpr DWORD kHelperPollIntervalMs = 250;
constexpr DWORD kPlugTimeoutMs = 20 * 1000;
constexpr DWORD kInstallAndPlugTimeoutMs = 120 * 1000;
constexpr DWORD kUnplugTimeoutMs = 10 * 1000;
constexpr DWORD kPresenceSettleTimeoutMs = 5 * 1000;
struct HelperVirtualDisplayStatus {
bool ok = false;
bool package_available = false;
bool driver_installed = false;
int plugged_monitors = 0;
bool busy = false;
std::string last_error;
std::string error; // transport or helper-reported error
};
std::wstring OwnSessionHelperPipeName() {
DWORD session_id = 0xFFFFFFFF;
if (!ProcessIdToSessionId(GetCurrentProcessId(), &session_id)) return {};
return GetCrossDeskSessionHelperPipeName(session_id);
}
bool QueryHelper(const std::wstring& pipe, const std::string& command,
DWORD timeout_ms, Json* json, std::string* error) {
std::vector<uint8_t> response;
DWORD code = 0;
if (!QueryNamedPipeWithDeadline(pipe, command, timeout_ms, &response, error,
&code)) {
*error += ":" + std::to_string(code);
return false;
}
*json = Json::parse(response.begin(), response.end(), nullptr, false);
if (json->is_discarded() || !json->is_object()) {
*error = "invalid_helper_json";
return false;
}
if (!json->value("ok", false)) {
*error = json->value("error", std::string("helper_error"));
return false;
}
return true;
}
HelperVirtualDisplayStatus QueryHelperStatus(const std::wstring& pipe) {
HelperVirtualDisplayStatus status;
Json json;
if (!QueryHelper(pipe, kCrossDeskVirtualDisplayStatusCommand,
kHelperStatusTimeoutMs, &json, &status.error)) {
return status;
}
status.ok = true;
status.package_available = json.value("package_available", false);
status.driver_installed = json.value("driver_installed", false);
status.plugged_monitors = json.value("plugged_monitors", 0);
status.busy = json.value("busy", false);
status.last_error = json.value("last_error", std::string());
return status;
}
// Waits for the helper job to finish; returns false with the job's error.
// |cancelled| stops the wait early (the helper job itself keeps running).
bool WaitForHelperJob(const std::wstring& pipe, DWORD timeout_ms,
const std::function<bool()>& cancelled,
HelperVirtualDisplayStatus* final_status,
std::string* error) {
const ULONGLONG deadline = GetTickCount64() + timeout_ms;
while (true) {
*final_status = QueryHelperStatus(pipe);
if (!final_status->ok) {
*error = final_status->error;
return false;
}
if (!final_status->busy) {
if (!final_status->last_error.empty()) {
*error = final_status->last_error;
return false;
}
return true;
}
if (cancelled()) {
*error = "cancelled";
return false;
}
if (GetTickCount64() >= deadline) {
*error = "helper_job_timeout";
return false;
}
Sleep(kHelperPollIntervalMs);
}
}
bool WaitForDisplayPresence(DWORD timeout_ms,
const std::function<bool()>& cancelled) {
const ULONGLONG deadline = GetTickCount64() + timeout_ms;
while (true) {
if (ClassifyDisplayPresence(ProbeDisplayPresence()) ==
DisplayPresence::present) {
return true;
}
if (cancelled() || GetTickCount64() >= deadline) return false;
Sleep(kHelperPollIntervalMs);
}
}
} // namespace
bool VirtualDisplayProvisioner::AcquireViaHelper(VirtualDisplayMode mode,
std::string* error,
bool* helper_reachable) {
*helper_reachable = false;
const std::wstring pipe = OwnSessionHelperPipeName();
if (pipe.empty()) {
*error = "session_id_unavailable";
return false;
}
HelperVirtualDisplayStatus status = QueryHelperStatus(pipe);
if (!status.ok) {
*error = status.error;
return false;
}
*helper_reachable = true;
if (!status.package_available) {
*error = "usbmmidd_package_missing";
return false;
}
const auto cancelled = [this] { return Cancelled(); };
if (status.busy) {
// A previous unplug may still be running; let it finish first.
if (!WaitForHelperJob(pipe, kUnplugTimeoutMs, cancelled, &status, error))
return false;
}
Json json;
if (!QueryHelper(pipe, BuildVirtualDisplayPlugCommand(mode),
kHelperCommandTimeoutMs, &json, error)) {
return false;
}
const DWORD timeout =
status.driver_installed ? kPlugTimeoutMs : kInstallAndPlugTimeoutMs;
if (!WaitForHelperJob(pipe, timeout, cancelled, &status, error)) {
// The helper finishes the plug on its own; Release() must undo it.
if (*error == "cancelled") pending_helper_release_ = true;
return false;
}
if (status.plugged_monitors <= 0) {
*error = "usbmmidd_monitor_not_attached";
return false;
}
return true;
}
bool VirtualDisplayProvisioner::AcquireLocally(VirtualDisplayMode mode,
std::string* error) {
if (!LocateUsbmmiddPackage().Available()) {
*error = "usbmmidd_package_missing";
return false;
}
return ProvisionUsbmmiddDisplay(mode, &plugged_locally_, error);
}
bool VirtualDisplayProvisioner::Acquire(VirtualDisplayMode mode,
std::string* error) {
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);
bool helper_reachable = false;
const ULONGLONG started = GetTickCount64();
bool ok = AcquireViaHelper(mode, error, &helper_reachable);
if (ok) {
via_helper_ = true;
} else if (!helper_reachable && *error != "cancelled") {
LOG_INFO("Virtual display: session helper unreachable ({}), trying "
"in-process provisioning",
*error);
ok = AcquireLocally(mode, error);
via_helper_ = false;
}
if (!ok) {
LOG_WARN("Virtual display: provisioning failed after {} ms, error={}",
GetTickCount64() - started, *error);
return false;
}
// The desktop moves onto the new target asynchronously; DXGI needs it.
if (!WaitForDisplayPresence(kPresenceSettleTimeoutMs,
[this] { return Cancelled(); })) {
LOG_WARN("Virtual display: monitor plugged but no display target became "
"available within {} ms",
kPresenceSettleTimeoutMs);
}
active_.store(true, std::memory_order_relaxed);
LOG_INFO("Virtual display: usbmmidd monitor ready via {} in {} ms "
"(requested {}x{})",
via_helper_ ? "session helper" : "this process",
GetTickCount64() - started, mode.width, mode.height);
return true;
}
void VirtualDisplayProvisioner::Release() {
const bool was_active = active_.exchange(false, std::memory_order_relaxed);
// via_helper_ is only set by a completed helper plug; a cancelled one is
// tracked by pending_helper_release_ instead.
const bool release_helper =
(via_helper_ && was_active) || pending_helper_release_;
const bool release_local = !release_helper && plugged_locally_ > 0;
if (!release_helper && !release_local) return;
std::string error;
bool ok = false;
if (release_helper) {
const std::wstring pipe = OwnSessionHelperPipeName();
Json json;
HelperVirtualDisplayStatus status;
ok = !pipe.empty() &&
QueryHelper(pipe, kCrossDeskVirtualDisplayUnplugCommand,
kHelperCommandTimeoutMs, &json, &error);
// Wait for the unplug so a Start() right after this Stop() probes the
// desktop without the outgoing monitor. A plug still running in the helper
// queues the unplug behind itself; the next Acquire() waits for that job.
if (ok && !json.value("queued", false)) {
ok = WaitForHelperJob(pipe, kUnplugTimeoutMs, [] { return false; },
&status, &error);
}
if (ok) pending_helper_release_ = false;
} else {
ok = ReleaseUsbmmiddDisplays(plugged_locally_, &error);
if (ok) plugged_locally_ = 0;
}
if (ok) {
LOG_INFO("Virtual display: usbmmidd monitor released");
} else {
LOG_WARN("Virtual display: release failed, error={}", error);
}
}
} // namespace crossdesk
@@ -0,0 +1,63 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_VIRTUAL_DISPLAY_PROVISIONER_H_
#define CROSSDESK_VIRTUAL_DISPLAY_PROVISIONER_H_
#include <Windows.h>
#include <atomic>
#include <string>
#include "usbmmidd_policy.h"
namespace crossdesk {
// Session-scoped usbmmidd virtual display for a host without any monitor.
// Acquire() asks the session helper (SYSTEM in the console session) to
// install/plug the driver; without a reachable helper an elevated GUI does it
// itself. Release() unplugs only what this object plugged.
class VirtualDisplayProvisioner {
public:
VirtualDisplayProvisioner() = default;
~VirtualDisplayProvisioner() { Release(); }
VirtualDisplayProvisioner(const VirtualDisplayProvisioner&) = delete;
VirtualDisplayProvisioner& operator=(const VirtualDisplayProvisioner&) =
delete;
bool Acquire(VirtualDisplayMode mode, std::string* error);
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
// after the provisioning worker has been joined.
bool NeedsRelease() const {
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().
void RequestCancel() { cancel_.store(true, std::memory_order_relaxed); }
private:
// |helper_reachable| tells the caller whether a local fallback makes sense.
bool AcquireViaHelper(VirtualDisplayMode mode, std::string* error,
bool* helper_reachable);
bool AcquireLocally(VirtualDisplayMode mode, std::string* error);
bool Cancelled() const { return cancel_.load(std::memory_order_relaxed); }
// Acquire/Release run on the owner's provisioning worker; active() and
// RequestCancel() are used from other threads.
std::atomic<bool> active_{false};
std::atomic<bool> cancel_{false};
bool via_helper_ = false;
// A cancelled helper plug still completes in the helper; Release() undoes it.
bool pending_helper_release_ = false;
// Monitors plugged by this process (helper-plugged ones are tracked there).
int plugged_locally_ = 0;
};
} // namespace crossdesk
#endif // CROSSDESK_VIRTUAL_DISPLAY_PROVISIONER_H_
+9 -3
View File
@@ -230,9 +230,13 @@ function setup_targets()
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp",
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp",
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_factory.cpp")
add_files("apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp",
"apps/desktop/src/platform/windows/virtual_display/virtual_display_provisioner.cpp")
add_includedirs("apps/desktop/src/platform/windows",
"apps/desktop/src/platform/windows/screen_capturer",
"apps/desktop/src/platform/windows/service")
"apps/desktop/src/platform/windows/service",
"apps/desktop/src/platform/windows/virtual_display")
add_syslinks("Setupapi", {public = true})
elseif is_os("macosx") then
add_deps("privacy")
add_mxxflags("-fobjc-arc")
@@ -470,13 +474,15 @@ function setup_targets()
set_kind("binary")
add_packages("libyuv")
add_deps("rd_log", "path_manager")
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32", "Setupapi")
add_files("apps/desktop/src/platform/windows/service/session_helper_main.cpp")
add_files("apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp")
add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc",
{defines = windows_helper_resource_defines})
add_includedirs("apps/desktop/src/common",
"apps/desktop/src/platform/windows/service",
"apps/desktop/src/platform/windows/screen_capturer")
"apps/desktop/src/platform/windows/screen_capturer",
"apps/desktop/src/platform/windows/virtual_display")
add_includedirs("apps/desktop/src/platform/windows/input",
"apps/desktop/src/platform/common/input")
add_includedirs("libs/wire/include")