[fix] preserve Windows display resolution across headless sessions

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