mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-11 10:59:06 +08:00
[fix] preserve Windows display resolution across headless sessions
This commit is contained in:
@@ -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
@@ -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
|
||||
# 停止无人值守被控端,并恢复服务按需启动
|
||||
|
||||
Reference in New Issue
Block a user