From 276385b81628793a9e3cc4957166e1b272437ca7 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Mon, 21 Sep 2026 01:04:59 +0800 Subject: [PATCH] [fix] keep Windows display slots and input mapping stable across hotplug --- .../devices/session_device_manager.cpp | 56 ++- .../features/devices/session_device_manager.h | 1 + .../windows/input/mouse/mouse_controller.cpp | 49 ++- .../windows/input/mouse/mouse_controller.h | 4 + .../windows/screen_capturer/display_label.h | 41 ++ .../screen_capturer/screen_capturer_dxgi.cpp | 375 +++++++++--------- .../screen_capturer/screen_capturer_dxgi.h | 16 +- .../screen_capturer/screen_capturer_gdi.cpp | 22 +- .../screen_capturer/screen_capturer_gdi.h | 3 + .../screen_capturer/screen_capturer_wgc.cpp | 20 +- .../screen_capturer/screen_capturer_win.cpp | 109 ++++- .../screen_capturer/screen_capturer_win.h | 11 +- .../screen_capturer/stable_display_slots.h | 41 ++ .../src/screen_capturer/screen_capturer.h | 5 + 14 files changed, 482 insertions(+), 271 deletions(-) create mode 100644 apps/desktop/src/platform/windows/screen_capturer/display_label.h create mode 100644 apps/desktop/src/platform/windows/screen_capturer/stable_display_slots.h diff --git a/apps/desktop/src/gui/features/devices/session_device_manager.cpp b/apps/desktop/src/gui/features/devices/session_device_manager.cpp index 3b2f403d..e3d4f68c 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.cpp +++ b/apps/desktop/src/gui/features/devices/session_device_manager.cpp @@ -330,6 +330,28 @@ void SessionDeviceManager::StopSpeakerCapturer() { speaker_capture_.SetEnabled(false); } +bool SessionDeviceManager::SyncDisplayInfo() { + if (!screen_capturer_) return false; + auto displays = screen_capturer_->GetDisplayInfoList(); + if (displays.empty()) return false; + // Called every UI tick; replace the shared list only when geometry actually + // changed so readers on other threads rarely contend for the lock. + bool changed = false; + { + std::lock_guard lock(display_info_mutex_); + changed = !std::equal( + displays.begin(), displays.end(), display_info_list_.begin(), + display_info_list_.end(), [](const auto &a, const auto &b) { + return a.handle == b.handle && a.name == b.name && + a.is_primary == b.is_primary && a.left == b.left && + a.top == b.top && a.width == b.width && a.height == b.height; + }); + if (changed) display_info_list_ = displays; + } + if (changed && mouse_controller_) mouse_controller_->UpdateDisplayInfoList(displays); + return true; +} + void SessionDeviceManager::SetDisplayInfoList(std::vector displays) { std::lock_guard lock(display_info_mutex_); display_info_list_ = std::move(displays); @@ -359,14 +381,9 @@ int SessionDeviceManager::StartMouseController() { } } - if (screen_capturer_) { - const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); - if (!latest_display_info.empty()) { - display_info_list_ = latest_display_info; - } - } #endif + SyncDisplayInfo(); mouse_controller_ = static_cast(device_controller_factory_->Create( DeviceControllerFactory::Device::Mouse)); @@ -375,7 +392,7 @@ int SessionDeviceManager::StartMouseController() { return -1; } - const int init_ret = mouse_controller_->Init(display_info_list_); + const int init_ret = mouse_controller_->Init(display_info_list()); if (init_ret != 0) { LOG_INFO("Destroy mouse controller"); mouse_controller_->Destroy(); @@ -564,16 +581,19 @@ void SessionDeviceManager::UpdateInteractions() { owner_.mouse_controller_is_started_ = false; } -#if defined(__linux__) || defined(__APPLE__) - if (owner_.screen_capturer_is_started_ && screen_capturer_ && - mouse_controller_) { - const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); - if (!latest_display_info.empty()) { - display_info_list_ = latest_display_info; - mouse_controller_->UpdateDisplayInfoList(display_info_list_); + if (owner_.screen_capturer_is_started_ && mouse_controller_ && SyncDisplayInfo()) { +#ifdef _WIN32 + const int active_display = screen_capturer_->GetCurrentMonitorIndex(); + const auto displays = display_info_list(); + if (active_display >= 0 && + active_display < static_cast(displays.size())) { + owner_.selected_display_ = active_display; + if (displays[active_display].width <= 0 || + displays[active_display].height <= 0) + ReleaseRemoteMouseButtons(); } - } #endif + } if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_) { if (!owner_.keyboard_capturer_is_started_ && StartKeyboardCapturer() == 0) { @@ -646,7 +666,10 @@ void SessionDeviceManager::SendMouseCommand(const RemoteAction &action, } int SessionDeviceManager::SwitchDisplay(int display_id) { - return screen_capturer_ ? screen_capturer_->SwitchTo(display_id) : -1; + if (!screen_capturer_) return -1; + const int ret = screen_capturer_->SwitchTo(display_id); + if (ret == 0) SyncDisplayInfo(); + return ret; } void SessionDeviceManager::ResetToInitialDisplay() { @@ -661,6 +684,7 @@ void SessionDeviceManager::ResetToInitialDisplay() { // Capture backends start at display 0. Reset the input mapping as well so // the next connection cannot show that display while controlling another. owner_.selected_display_ = 0; + SyncDisplayInfo(); } std::vector SessionDeviceManager::display_info_list() const { diff --git a/apps/desktop/src/gui/features/devices/session_device_manager.h b/apps/desktop/src/gui/features/devices/session_device_manager.h index 2b1a3bbb..0e40e426 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.h +++ b/apps/desktop/src/gui/features/devices/session_device_manager.h @@ -96,6 +96,7 @@ private: bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now, int fps); bool ShouldCaptureCursor() const; + bool SyncDisplayInfo(); void SetDisplayInfoList(std::vector displays); void RecordCaptureCadence(std::chrono::steady_clock::time_point now, int fps, bool from_secure_desktop); diff --git a/apps/desktop/src/platform/windows/input/mouse/mouse_controller.cpp b/apps/desktop/src/platform/windows/input/mouse/mouse_controller.cpp index 6da8539f..3caa1b49 100644 --- a/apps/desktop/src/platform/windows/input/mouse/mouse_controller.cpp +++ b/apps/desktop/src/platform/windows/input/mouse/mouse_controller.cpp @@ -18,11 +18,28 @@ PlatformMouseController::PlatformMouseController() {} PlatformMouseController::~PlatformMouseController() { Destroy(); } int PlatformMouseController::Init(std::vector display_info_list) { + std::lock_guard lock(display_info_mutex_); display_info_list_ = display_info_list; return 0; } +void PlatformMouseController::UpdateDisplayInfoList( + const std::vector& display_info_list) { + if (display_info_list.empty()) return; + std::lock_guard lock(display_info_mutex_); + if (std::equal(display_info_list.begin(), display_info_list.end(), + display_info_list_.begin(), display_info_list_.end(), + [](const auto& a, const auto& b) { + return a.handle == b.handle && a.left == b.left && + a.top == b.top && a.width == b.width && + a.height == b.height; + })) return; + display_info_list_ = display_info_list; + LOG_INFO("Windows mouse display geometry updated: displays={}", + display_info_list_.size()); +} + int PlatformMouseController::Destroy() { return ReleasePressedButtons(); } int PlatformMouseController::ReleasePressedButtons() { @@ -44,13 +61,19 @@ int PlatformMouseController::ReleasePressedButtons() { int PlatformMouseController::SendMouseCommand(RemoteAction remote_action, int display_index) { + std::unique_lock geometry_lock(display_info_mutex_); if (display_index < 0 || - display_index >= static_cast(display_info_list_.size())) { + display_index >= static_cast(display_info_list_.size()) || + display_info_list_[display_index].width <= 0 || + display_info_list_[display_index].height <= 0) { LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}", display_index, display_info_list_.size()); return -1; } + const auto display = display_info_list_[display_index]; + geometry_lock.unlock(); + INPUT ip = {0}; ScopedWindowsPhysicalCoordinates physical_coordinates; @@ -59,11 +82,9 @@ int PlatformMouseController::SendMouseCommand(RemoteAction remote_action, return -1; ip.type = INPUT_MOUSE; ip.mi.dx = (LONG)(std::clamp(remote_action.m.x, 0.0f, 1.0f) * - (display_info_list_[display_index].width - 1)) + - display_info_list_[display_index].left; + (display.width - 1)) + display.left; ip.mi.dy = (LONG)(std::clamp(remote_action.m.y, 0.0f, 1.0f) * - (display_info_list_[display_index].height - 1)) + - display_info_list_[display_index].top; + (display.height - 1)) + display.top; switch (remote_action.m.flag) { case MouseFlag::left_down: @@ -106,12 +127,18 @@ int PlatformMouseController::SendMouseCommand(RemoteAction remote_action, } // Pixel centres in the virtual desktop, including negative monitor origins. // SetCursorPos cannot carry dwExtraInfo through the privacy mouse hook. - ip.mi.dx = static_cast( - (static_cast(ip.mi.dx - virtual_left) * 65536 + 32768) / - virtual_width); - ip.mi.dy = static_cast( - (static_cast(ip.mi.dy - virtual_top) * 65536 + 32768) / - virtual_height); + const int virtual_width_range = (std::max)(1, virtual_width - 1); + const int virtual_height_range = (std::max)(1, virtual_height - 1); + ip.mi.dx = static_cast(std::clamp( + (static_cast(ip.mi.dx - virtual_left) * 65535 + + virtual_width_range / 2) / + virtual_width_range, + 0, 65535)); + ip.mi.dy = static_cast(std::clamp( + (static_cast(ip.mi.dy - virtual_top) * 65535 + + virtual_height_range / 2) / + virtual_height_range, + 0, 65535)); ip.mi.dwFlags |= MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK; ip.mi.dwExtraInfo = kInjectedMouseInputMarker; diff --git a/apps/desktop/src/platform/windows/input/mouse/mouse_controller.h b/apps/desktop/src/platform/windows/input/mouse/mouse_controller.h index 98722fe6..08a5ccca 100644 --- a/apps/desktop/src/platform/windows/input/mouse/mouse_controller.h +++ b/apps/desktop/src/platform/windows/input/mouse/mouse_controller.h @@ -9,6 +9,7 @@ #include #include +#include #include @@ -26,10 +27,13 @@ class PlatformMouseController final : public MouseController { virtual int Init(std::vector display_info_list); virtual int Destroy(); virtual int SendMouseCommand(RemoteAction remote_action, int display_index); + void UpdateDisplayInfoList( + const std::vector& display_info_list) override; int ReleasePressedButtons(); private: std::vector display_info_list_; + std::mutex display_info_mutex_; std::atomic pressed_buttons_{0}; }; } // namespace crossdesk diff --git a/apps/desktop/src/platform/windows/screen_capturer/display_label.h b/apps/desktop/src/platform/windows/screen_capturer/display_label.h new file mode 100644 index 00000000..559793ed --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/display_label.h @@ -0,0 +1,41 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-20 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef CROSSDESK_DISPLAY_LABEL_H_ +#define CROSSDESK_DISPLAY_LABEL_H_ + +#include + +#include + +namespace crossdesk { + +inline std::string WideToUtf8(const std::wstring& wstr) { + if (wstr.empty()) return {}; + const int size_needed = + WideCharToMultiByte(CP_UTF8, 0, wstr.data(), static_cast(wstr.size()), + nullptr, 0, nullptr, nullptr); + if (size_needed <= 0) return {}; + std::string result(static_cast(size_needed), '\0'); + WideCharToMultiByte(CP_UTF8, 0, wstr.data(), static_cast(wstr.size()), + result.data(), size_needed, nullptr, nullptr); + return result; +} + +// "\\.\DISPLAY1" -> "DISPLAY1": the label every Windows backend reports so +// the wrapper can alias older plugins that identify streams by name. +inline std::string GetDisplayLabel(const std::wstring& wide_name) { + std::string name = WideToUtf8(wide_name); + constexpr char kDevicePrefix[] = "\\\\.\\"; + if (name.rfind(kDevicePrefix, 0) == 0) { + name.erase(0, sizeof(kDevicePrefix) - 1); + } + return name; +} + +} // namespace crossdesk + +#endif // CROSSDESK_DISPLAY_LABEL_H_ diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp index 98542b51..968cc6d3 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp @@ -9,6 +9,7 @@ #include "captured_cursor_state.h" #include "cursor_frame_compositor.h" +#include "display_label.h" #include "dxgi_cursor_state.h" #include "libyuv.h" #include "rd_log.h" @@ -17,29 +18,18 @@ namespace crossdesk { namespace { -std::string WideToUtf8(const std::wstring& wstr) { - if (wstr.empty()) return {}; - int size_needed = WideCharToMultiByte( - CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr); - std::string result(size_needed, 0); - WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(), - size_needed, nullptr, nullptr); - return result; -} - -std::string GetDisplayLabel(const std::wstring& wide_name) { - std::string name = WideToUtf8(wide_name); - constexpr char kDevicePrefix[] = "\\\\.\\"; - if (name.rfind(kDevicePrefix, 0) == 0) { - name.erase(0, sizeof(kDevicePrefix) - 1); +std::wstring GetOutputDeviceId(const std::wstring& device_name) { + DISPLAY_DEVICEW device{sizeof(device)}; + if (!EnumDisplayDevicesW(device_name.c_str(), 0, &device, + EDD_GET_DEVICE_INTERFACE_NAME)) { + return {}; } - return name; + return device.DeviceID; } } // namespace ScreenCapturerDxgi::ScreenCapturerDxgi() {} ScreenCapturerDxgi::~ScreenCapturerDxgi() { - Stop(); Destroy(); } @@ -51,11 +41,9 @@ int ScreenCapturerDxgi::Init(const int fps, cb_desktop_data cb) { return -1; } - if (!InitializeDxgi()) { - LOG_ERROR("DXGI: initialize DXGI failed"); - return -2; - } - + display_slots_.Reset(); + display_info_list_.clear(); + topology_refresh_requested_.store(false); EnumerateDisplays(); if (display_info_list_.empty()) { LOG_ERROR("DXGI: no displays found"); @@ -63,7 +51,6 @@ int ScreenCapturerDxgi::Init(const int fps, cb_desktop_data cb) { } monitor_index_ = 0; - initial_monitor_index_ = monitor_index_; return 0; } @@ -71,9 +58,10 @@ int ScreenCapturerDxgi::Destroy() { Stop(); ReleaseDuplication(); outputs_.clear(); + display_info_list_.clear(); + display_slots_.Reset(); d3d_context_.Reset(); d3d_device_.Reset(); - dxgi_factory_.Reset(); if (nv12_frame_) { delete[] nv12_frame_; nv12_frame_ = nullptr; @@ -123,13 +111,24 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) { LOG_ERROR("DXGI: invalid monitor index {}", monitor_index); return -1; } + if (monitor_index == monitor_index_ && (!running_ || duplication_)) return 0; + // Select before Start without allocating a second D3D device/duplication. + if (!running_) { + monitor_index_ = monitor_index; + return 0; + } paused_ = true; - monitor_index_ = monitor_index; + const int previous = monitor_index_.exchange(monitor_index); ReleaseDuplication(); - if (!CreateDuplicationForMonitor(monitor_index_)) { - LOG_ERROR("DXGI: create duplication failed for monitor {}", - monitor_index_.load()); - paused_ = false; // Reset paused_ on failure + if (!CreateDuplicationForMonitor(monitor_index)) { + LOG_ERROR("DXGI: create duplication failed for monitor {}", monitor_index); + // Keep the backend on the output it was capturing so the wrapper's index + // and ours do not diverge after a rejected switch. + monitor_index_ = previous; + if (!CreateDuplicationForMonitor(previous)) { + LOG_WARN("DXGI: could not restore duplication for monitor {}", previous); + } + paused_ = false; return -2; } paused_ = false; @@ -139,107 +138,134 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) { } int ScreenCapturerDxgi::ResetToInitialMonitor() { - std::lock_guard lock(switch_mutex_); - if (display_info_list_.empty()) return -1; - int target = initial_monitor_index_; - if (target < 0 || target >= (int)display_info_list_.size()) return -1; - if (monitor_index_ == target) return 0; - if (running_) { - paused_ = true; - monitor_index_ = target; - ReleaseDuplication(); - if (!CreateDuplicationForMonitor(monitor_index_)) { - paused_ = false; - return -2; - } - paused_ = false; - LOG_INFO("DXGI: reset to initial monitor {}:{}", monitor_index_.load(), - display_info_list_[monitor_index_].name); - } else { - monitor_index_ = target; - } - return 0; + return SwitchTo(0); } -bool ScreenCapturerDxgi::InitializeDxgi() { - UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; -#ifdef _DEBUG - flags |= D3D11_CREATE_DEVICE_DEBUG; -#endif - - D3D_FEATURE_LEVEL feature_levels[] = { - D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, - D3D_FEATURE_LEVEL_10_0}; - - D3D_FEATURE_LEVEL out_level{}; - HRESULT hr = D3D11CreateDevice( - nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, feature_levels, - ARRAYSIZE(feature_levels), D3D11_SDK_VERSION, d3d_device_.GetAddressOf(), - &out_level, d3d_context_.GetAddressOf()); - if (FAILED(hr)) { - hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, flags, - feature_levels, ARRAYSIZE(feature_levels), - D3D11_SDK_VERSION, d3d_device_.GetAddressOf(), - &out_level, d3d_context_.GetAddressOf()); - if (FAILED(hr)) { - LOG_ERROR("DXGI: D3D11CreateDevice failed, hr={}", (int)hr); - return false; - } - } - - hr = CreateDXGIFactory1( - __uuidof(IDXGIFactory1), - reinterpret_cast(dxgi_factory_.GetAddressOf())); - if (FAILED(hr)) { - LOG_ERROR("DXGI: CreateDXGIFactory1 failed, hr={}", (int)hr); +bool ScreenCapturerDxgi::EnumerateDisplays() { + ScopedWindowsPhysicalCoordinates physical_coordinates; + // Factories cache adapter topology. Refresh the factory as well as outputs. + Microsoft::WRL::ComPtr factory; + const HRESULT factory_result = + CreateDXGIFactory1(IID_PPV_ARGS(factory.GetAddressOf())); + if (FAILED(factory_result)) { + LOG_ERROR("DXGI: CreateDXGIFactory1 failed, hr={}", (int)factory_result); return false; } - return true; -} - -void ScreenCapturerDxgi::EnumerateDisplays() { - display_info_list_.clear(); - outputs_.clear(); - + struct Output { + DisplayInfo display; + Microsoft::WRL::ComPtr output; + std::string identity; + }; + std::vector current; Microsoft::WRL::ComPtr adapter; for (UINT a = 0; - dxgi_factory_->EnumAdapters(a, adapter.ReleaseAndGetAddressOf()) != - DXGI_ERROR_NOT_FOUND; - ++a) { + factory->EnumAdapters(a, adapter.ReleaseAndGetAddressOf()) != + DXGI_ERROR_NOT_FOUND; ++a) { + if (!adapter) break; Microsoft::WRL::ComPtr output; for (UINT o = 0; adapter->EnumOutputs(o, output.ReleaseAndGetAddressOf()) != - DXGI_ERROR_NOT_FOUND; - ++o) { + DXGI_ERROR_NOT_FOUND; ++o) { + if (!output) break; DXGI_OUTPUT_DESC desc{}; - if (FAILED(output->GetDesc(&desc))) { - continue; - } - std::string name = GetDisplayLabel(desc.DeviceName); + if (FAILED(output->GetDesc(&desc)) || !desc.AttachedToDesktop) continue; MONITORINFOEX mi{}; - mi.cbSize = sizeof(MONITORINFOEX); - if (GetMonitorInfo(desc.Monitor, &mi)) { - bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false; - DisplayInfo info((void*)desc.Monitor, name, is_primary, - mi.rcMonitor.left, mi.rcMonitor.top, - mi.rcMonitor.right, mi.rcMonitor.bottom); - // primary first - if (is_primary) { - display_info_list_.insert(display_info_list_.begin(), info); - outputs_.insert(outputs_.begin(), output); - } else { - display_info_list_.push_back(info); - outputs_.push_back(output); - } - } + mi.cbSize = sizeof(mi); + if (!GetMonitorInfo(desc.Monitor, &mi)) continue; + const auto device_id = GetOutputDeviceId(desc.DeviceName); + current.push_back({ + DisplayInfo(desc.Monitor, GetDisplayLabel(desc.DeviceName), + (mi.dwFlags & MONITORINFOF_PRIMARY) != 0, + mi.rcMonitor.left, mi.rcMonitor.top, + mi.rcMonitor.right, mi.rcMonitor.bottom), + output, WideToUtf8(device_id.empty() ? desc.DeviceName : device_id)}); } } + std::stable_partition(current.begin(), current.end(), + [](const auto& item) { return item.display.is_primary; }); + std::vector identities; + for (const auto& item : current) identities.push_back(item.identity); + const auto slots = display_slots_.Update(identities); + std::vector displays; + std::vector> outputs; + bool changed = slots.size() != display_info_list_.size(); + for (size_t slot = 0; slot < slots.size(); ++slot) { + if (slots[slot] < 0) { + auto missing = display_info_list_[slot]; + missing.handle = nullptr; + missing.left = missing.top = missing.right = missing.bottom = 0; + missing.width = missing.height = 0; + missing.is_primary = false; + displays.push_back(std::move(missing)); + outputs.emplace_back(); + } else { + const auto& item = current[slots[slot]]; + displays.push_back(item.display); + outputs.push_back(item.output); + } + if (slot >= display_info_list_.size()) continue; + const auto& before = display_info_list_[slot]; + const auto& after = displays.back(); + changed = changed || before.handle != after.handle || + before.left != after.left || before.top != after.top || + before.right != after.right || before.bottom != after.bottom || + before.is_primary != after.is_primary || before.name != after.name; + } + display_info_list_ = std::move(displays); + outputs_ = std::move(outputs); + if (changed) { + for (size_t slot = 0; slot < display_info_list_.size(); ++slot) { + const auto& display = display_info_list_[slot]; + LOG_INFO("DXGI: session display slot={} name='{}' available={} " + "bounds=({},{};{},{}), selected={}", + slot, display.name, outputs_[slot] != nullptr, + display.left, display.top, display.right, display.bottom, + static_cast(slot) == monitor_index_.load()); + } + } + return changed; } bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) { SharedDxgiCursorState().Reset(); - if (monitor_index < 0 || monitor_index >= (int)outputs_.size()) return false; + if (monitor_index < 0 || monitor_index >= (int)outputs_.size() || + !outputs_[monitor_index]) return false; + + // Outputs may move between GPU adapters when the display topology changes. + // Desktop Duplication requires the D3D device to be created from the same + // adapter as the selected IDXGIOutput. + Microsoft::WRL::ComPtr output_adapter; + HRESULT hr = outputs_[monitor_index]->GetParent( + IID_PPV_ARGS(output_adapter.GetAddressOf())); + if (FAILED(hr) || !output_adapter) { + LOG_ERROR("DXGI: get adapter for output {} failed, hr={}", monitor_index, + (int)hr); + return false; + } + + UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; +#ifdef _DEBUG + flags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + D3D_FEATURE_LEVEL feature_levels[] = { + D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0}; + D3D_FEATURE_LEVEL out_level{}; + Microsoft::WRL::ComPtr output_device; + Microsoft::WRL::ComPtr output_context; + hr = D3D11CreateDevice( + output_adapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, flags, + feature_levels, ARRAYSIZE(feature_levels), D3D11_SDK_VERSION, + output_device.GetAddressOf(), &out_level, output_context.GetAddressOf()); + if (FAILED(hr)) { + LOG_ERROR("DXGI: create D3D device for output adapter {} failed, hr={}", + monitor_index, (int)hr); + return false; + } + d3d_context_ = std::move(output_context); + d3d_device_ = std::move(output_device); + Microsoft::WRL::ComPtr output1; - HRESULT hr = outputs_[monitor_index]->QueryInterface( + hr = outputs_[monitor_index]->QueryInterface( IID_PPV_ARGS(output1.GetAddressOf())); if (FAILED(hr)) { LOG_ERROR("DXGI: Query IDXGIOutput1 failed, hr={}", (int)hr); @@ -257,37 +283,14 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) { DXGI_OUTDUPL_DESC desc{}; duplication_->GetDesc(&desc); rotation_ = desc.Rotation; - LOG_INFO("DXGI: duplication ready, monitor={}, rotation={}", monitor_index, - static_cast(rotation_)); + DXGI_ADAPTER_DESC adapter_desc{}; + output_adapter->GetDesc(&adapter_desc); + LOG_INFO("DXGI: duplication ready, monitor={}, rotation={}, adapter='{}'", + monitor_index, static_cast(rotation_), + WideToUtf8(adapter_desc.Description)); return true; } -bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() { - std::lock_guard lock(switch_mutex_); - ReleaseDuplication(); - int current_monitor = monitor_index_.load(); - if (CreateDuplicationForMonitor(current_monitor)) { - return true; - } - - EnumerateDisplays(); - if (display_info_list_.empty()) { - LOG_ERROR("DXGI: no displays found while recreating duplication"); - return false; - } - if (current_monitor < 0 || - current_monitor >= static_cast(display_info_list_.size())) { - current_monitor = 0; - monitor_index_ = 0; - } - if (CreateDuplicationForMonitor(current_monitor)) { - LOG_INFO("DXGI: recreated duplication for monitor {}", - monitor_index_.load()); - return true; - } - return false; -} - void ScreenCapturerDxgi::ReleaseDuplication() { SharedDxgiCursorState().Reset(); ++duplication_generation_; @@ -351,20 +354,29 @@ bool ScreenCapturerDxgi::ConvertFrame(int frame_monitor, bool* cursor_embedded, return false; } - if (show_cursor_.load(std::memory_order_relaxed) && frame_monitor >= 0 && + if (frame_monitor >= 0 && frame_monitor < static_cast(display_info_list_.size())) { const auto& display = display_info_list_[frame_monitor]; CURSORINFO cursor{}; cursor.cbSize = sizeof(cursor); - if (GetCursorInfo(&cursor) && - SharedDxgiCursorState().ShouldDrawCursor( - (cursor.flags & CURSOR_SHOWING) != 0, display.handle)) { - if (const auto* composited = - compositor.Draw(pixels, stride, even_width, even_height, cursor, - display.left, display.top)) { - pixels = composited; - stride = even_width * 4; - *cursor_embedded = true; + if (GetCursorInfo(&cursor)) { + const bool visible = (cursor.flags & CURSOR_SHOWING) != 0; + const bool should_draw = SharedDxgiCursorState().ShouldDrawCursor( + visible, display.handle); + // show_cursor controls our extra composition, not cursors the OS has + // already put in the texture. Publish that distinction for native + // controllers as well, so they do not draw a second cursor. + *cursor_embedded = visible && !should_draw && + MonitorFromPoint(cursor.ptScreenPos, MONITOR_DEFAULTTONULL) == + display.handle; + if (show_cursor_.load(std::memory_order_relaxed) && should_draw) { + if (const auto* composited = + compositor.Draw(pixels, stride, even_width, even_height, cursor, + display.left, display.top)) { + pixels = composited; + stride = even_width * 4; + *cursor_embedded = true; + } } } } @@ -395,8 +407,8 @@ void ScreenCapturerDxgi::CaptureLoop() { bool cached_cursor_embedded = false; int cached_monitor = -1; uint64_t cached_generation = 0; - auto last_duplication_retry = - std::chrono::steady_clock::now() - std::chrono::milliseconds(1000); + auto next_topology_check = std::chrono::steady_clock::now(); + unsigned acquire_failures = 0; while (running_) { if (paused_) { cached_frame_valid = false; @@ -408,14 +420,25 @@ void ScreenCapturerDxgi::CaptureLoop() { // replace them in SwitchTo while AcquireNextFrame/Map is using them. std::unique_lock capture_lock(switch_mutex_); if (paused_ || !running_) continue; + const auto now = std::chrono::steady_clock::now(); + const bool refresh = topology_refresh_requested_.exchange(false); + if (refresh || now >= next_topology_check) { + next_topology_check = now + std::chrono::milliseconds(500); + const bool changed = EnumerateDisplays(); + if (changed || refresh || !duplication_) { + cached_frame_valid = false; + ReleaseDuplication(); + CreateDuplicationForMonitor(monitor_index_.load()); + capture_lock.unlock(); + if (callback_) + callback_(nullptr, ScreenCapturer::kDisplayTopologyChanged, + 0, 0, "", nullptr); + continue; + } + } if (!duplication_) { cached_frame_valid = false; capture_lock.unlock(); - const auto now = std::chrono::steady_clock::now(); - if (now - last_duplication_retry >= std::chrono::milliseconds(500)) { - last_duplication_retry = now; - RecreateDuplicationForCurrentMonitor(); - } std::this_thread::sleep_for(std::chrono::milliseconds(10)); continue; } @@ -449,17 +472,21 @@ void ScreenCapturerDxgi::CaptureLoop() { continue; } // Never replay a cached image after an acquisition/conversion failure or - // a backend rebuild. Normal error/reset handling releases optional privacy. + // a backend rebuild. Display recovery retains the session privacy state. cached_frame_valid = false; if (FAILED(hr)) { - SharedDxgiCursorState().Reset(); LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr); - capture_lock.unlock(); - if (callback_) - callback_(nullptr, ScreenCapturer::kBackendReset, 0, 0, "", nullptr); - RecreateDuplicationForCurrentMonitor(); + ReleaseDuplication(); + // A single ACCESS_LOST (mode switch, fullscreen toggle, UAC return) is + // rebuilt on the next iteration. Only repeated failures fall back to + // the 500 ms topology cadence so a broken output does not spin while + // recreating D3D devices. + if (++acquire_failures == 1) { + next_topology_check = std::chrono::steady_clock::now(); + } continue; } + acquire_failures = 0; if (frame_monitor >= 0 && frame_monitor < static_cast(display_info_list_.size())) { @@ -506,24 +533,18 @@ void ScreenCapturerDxgi::CaptureLoop() { const bool converted = ConvertFrame(frame_monitor, &cursor_embedded, cursor_compositor); + const std::string stream_id = MakeDisplayStreamId(frame_monitor); cached_generation = duplication_generation_; duplication_->ReleaseFrame(); capture_lock.unlock(); if (converted && frame_monitor == monitor_index_.load() && callback_) { - int idx = frame_monitor; - if (idx >= 0 && idx < static_cast(display_info_list_.size())) { - const std::string stream_id = MakeDisplayStreamId(idx); - CapturedCursorFrameScope cursor_scope(cursor_embedded); - callback_(nv12_frame_, nv12_width_ * nv12_height_ * 3 / 2, nv12_width_, - nv12_height_, stream_id.c_str(), nullptr); - cached_cursor_embedded = cursor_embedded; - cached_monitor = idx; - cached_frame_valid = true; - } else { - LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})", - idx, display_info_list_.size()); - } + CapturedCursorFrameScope cursor_scope(cursor_embedded); + callback_(nv12_frame_, nv12_width_ * nv12_height_ * 3 / 2, nv12_width_, + nv12_height_, stream_id.c_str(), nullptr); + cached_cursor_embedded = cursor_embedded; + cached_monitor = frame_monitor; + cached_frame_valid = true; } } } diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.h b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.h index dadacf27..692e8ffd 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.h +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.h @@ -22,6 +22,7 @@ #include "rd_log.h" #include "screen_capturer.h" +#include "stable_display_slots.h" namespace crossdesk { @@ -47,15 +48,20 @@ class ScreenCapturerDxgi : public ScreenCapturer { int SwitchTo(int monitor_index) override; int ResetToInitialMonitor() override; + // Rebuild the selected output on the capture thread, never on the caller. + void RequestTopologyRefresh() { topology_refresh_requested_.store(true); } + std::vector GetDisplayInfoList() override { + std::lock_guard lock(switch_mutex_); return display_info_list_; } + int GetCurrentMonitorIndex() const override { + return monitor_index_.load(std::memory_order_relaxed); + } private: - bool InitializeDxgi(); - void EnumerateDisplays(); + bool EnumerateDisplays(); bool CreateDuplicationForMonitor(int monitor_index); - bool RecreateDuplicationForCurrentMonitor(); void CaptureLoop(); bool ConvertFrame(int frame_monitor, bool* cursor_embedded, CursorFrameCompositor& compositor); @@ -64,8 +70,9 @@ class ScreenCapturerDxgi : public ScreenCapturer { private: std::vector display_info_list_; std::vector> outputs_; + StableDisplaySlots display_slots_; + std::atomic topology_refresh_requested_{false}; - Microsoft::WRL::ComPtr dxgi_factory_; Microsoft::WRL::ComPtr d3d_device_; Microsoft::WRL::ComPtr d3d_context_; Microsoft::WRL::ComPtr duplication_; @@ -76,7 +83,6 @@ class ScreenCapturerDxgi : public ScreenCapturer { std::atomic running_{false}; std::atomic paused_{false}; std::atomic monitor_index_{0}; - int initial_monitor_index_ = 0; std::atomic show_cursor_{true}; std::thread thread_; int fps_ = 60; diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp index 4f90fe61..4f3525bb 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp @@ -8,32 +8,12 @@ #include #include "captured_cursor_state.h" #include "cursor_draw.h" +#include "display_label.h" #include "libyuv.h" #include "rd_log.h" namespace crossdesk { -namespace { -std::string WideToUtf8(const std::wstring& wstr) { - if (wstr.empty()) return {}; - int size_needed = WideCharToMultiByte( - CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr); - std::string result(size_needed, 0); - WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(), - size_needed, nullptr, nullptr); - return result; -} - -std::string GetDisplayLabel(const std::wstring& wide_name) { - std::string name = WideToUtf8(wide_name); - constexpr char kDevicePrefix[] = "\\\\.\\"; - if (name.rfind(kDevicePrefix, 0) == 0) { - name.erase(0, sizeof(kDevicePrefix) - 1); - } - return name; -} -} // namespace - ScreenCapturerGdi::ScreenCapturerGdi() {} ScreenCapturerGdi::~ScreenCapturerGdi() { Stop(); diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.h b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.h index ea159296..698c95b9 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.h +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.h @@ -44,6 +44,9 @@ class ScreenCapturerGdi : public ScreenCapturer { std::vector GetDisplayInfoList() override { return display_info_list_; } + int GetCurrentMonitorIndex() const override { + return monitor_index_.load(std::memory_order_relaxed); + } private: static BOOL CALLBACK EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT, diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.cpp index dc28c973..ef49cbde 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.cpp @@ -9,6 +9,7 @@ #include #include "captured_cursor_state.h" +#include "display_label.h" #include "libyuv.h" #include "rd_log.h" @@ -16,25 +17,6 @@ namespace crossdesk { static std::vector gs_display_list; -std::string WideToUtf8(const std::wstring& wstr) { - if (wstr.empty()) return {}; - int size_needed = WideCharToMultiByte( - CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr); - std::string result(size_needed, 0); - WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(), - size_needed, nullptr, nullptr); - return result; -} - -std::string GetDisplayLabel(const std::wstring& wide_name) { - std::string name = WideToUtf8(wide_name); - constexpr char kDevicePrefix[] = "\\\\.\\"; - if (name.rfind(kDevicePrefix, 0) == 0) { - name.erase(0, sizeof(kDevicePrefix) - 1); - } - return name; -} - BOOL WINAPI EnumMonitorProc(HMONITOR hmonitor, [[maybe_unused]] HDC hdc, [[maybe_unused]] LPRECT lprc, LPARAM data) { MONITORINFOEX monitor_info_; diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp index aa558155..84362bb9 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -46,6 +47,7 @@ constexpr DWORD kSecureDesktopTransientErrorGraceMs = 1500; constexpr DWORD kSecureDesktopTransientErrorLogIntervalMs = 5000; constexpr DWORD kPostSecureDesktopRestartRetryMs = 500; constexpr DWORD kPostSecureDesktopRestartTimeoutMs = 10000; +constexpr ULONGLONG kCaptureStallTimeoutMs = 3000; constexpr int kSecureDesktopCaptureMinFps = 30; constexpr int kSecureDesktopCaptureMaxIntervalMs = 1000 / kSecureDesktopCaptureMinFps; @@ -411,6 +413,7 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { cb_orig_ = cb; native_output_logged_.store(false, std::memory_order_relaxed); native_output_error_logged_.store(false, std::memory_order_relaxed); + last_capture_progress_tick_.store(0, std::memory_order_relaxed); try { native_frame_pool_ = CapturedNv12FramePool::Create(); } catch (...) { @@ -418,13 +421,22 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { return -1; } cb_ = [this](unsigned char* data, int size, int w, int h, - const char* reported_stream_id, - const MiniRtcNativeVideoFrame* native_frame) { + const char* reported_stream_id, + const MiniRtcNativeVideoFrame* native_frame) { + if (size == ScreenCapturer::kDisplayTopologyChanged) { + SharedCapturedCursorState().Reset(); + RebuildAliasesFromImpl(true); + return; + } if (size == ScreenCapturer::kBackendReset) { SharedCapturedCursorState().Reset(); if (privacy_) privacy_->Fail("Capture restarted; privacy screen will turn off"); return; } + if (data && size > 0 && w > 0 && h > 0) { + last_capture_progress_tick_.store(GetTickCount64(), + std::memory_order_relaxed); + } if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) { SharedDxgiCursorState().Reset(); SharedCapturedCursorState().Reset(); @@ -511,7 +523,6 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { LOG_INFO("Windows capturer: using {}", CaptureBackendName(impl_.get())); BuildCanonicalFromImpl(); monitor_index_.store(0, std::memory_order_relaxed); - initial_monitor_index_ = 0; return 0; } @@ -604,9 +615,7 @@ void ScreenCapturerWin::EmitCapturedFrame( void ScreenCapturerWin::RestoreMonitor(int monitor_index) { RebuildAliasesFromImpl(); - if (monitor_index > 0 && impl_->SwitchTo(monitor_index) != 0) { - monitor_index_.store(0, std::memory_order_relaxed); - } + if (monitor_index >= 0) SwitchTo(monitor_index); } bool ScreenCapturerWin::TryStartBackend( @@ -618,8 +627,10 @@ bool ScreenCapturerWin::TryStartBackend( const char* name = CaptureBackendName(candidate.get()); const bool show_cursor = show_cursor_.load(std::memory_order_relaxed); int ret = candidate->Init(fps_, cb_); - if (ret == 0) + if (ret == 0) { + last_capture_progress_tick_.store(GetTickCount64(), std::memory_order_relaxed); ret = candidate->Start(show_cursor); + } if (ret != 0) { LOG_WARN("Windows capturer: {} initialization/start failed (ret={})", name, ret); @@ -628,7 +639,10 @@ bool ScreenCapturerWin::TryStartBackend( } // Commit only after the replacement is running. Failed candidates release // their resources without replacing the current backend. - impl_ = std::move(candidate); + { + std::lock_guard lock(impl_mutex_); + impl_ = std::move(candidate); + } applied_show_cursor_ = show_cursor; RestoreMonitor(monitor_index); LOG_INFO("Windows capturer: started {}", name); @@ -647,6 +661,7 @@ int ScreenCapturerWin::Start(bool show_cursor) { SharedCapturedCursorState().Reset(); paused_.store(false, std::memory_order_relaxed); invalid_stream_id_logged_.store(false, std::memory_order_relaxed); + last_capture_progress_tick_.store(0, std::memory_order_relaxed); // Refresh physical monitor identities before every session. HMONITOR and // DXGI output handles may change after an HDMI hotplug while CrossDesk stays @@ -657,11 +672,11 @@ int ScreenCapturerWin::Start(bool show_cursor) { int ret = impl_->Init(fps_, cb_); if (ret == 0) { RebuildAliasesFromImpl(); + // Canonical wire/input slots may differ from this backend's refreshed + // enumeration. Select through the mapping before its first callback. + SwitchTo(requested_monitor); + last_capture_progress_tick_.store(GetTickCount64(), std::memory_order_relaxed); ret = impl_->Start(show_cursor); - if (ret == 0 && requested_monitor > 0 && - impl_->SwitchTo(requested_monitor) != 0) { - monitor_index_.store(0, std::memory_order_relaxed); - } } if (ret != 0) { if (capture_method_ != ScreenCaptureMethod::Auto) { @@ -707,6 +722,7 @@ int ScreenCapturerWin::Start(bool show_cursor) { 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); @@ -726,19 +742,35 @@ int ScreenCapturerWin::Stop() { int ScreenCapturerWin::Pause(int monitor_index) { paused_.store(true, std::memory_order_relaxed); + std::lock_guard lock(impl_mutex_); if (!impl_) return -1; return impl_->Pause(monitor_index); } int ScreenCapturerWin::Resume(int monitor_index) { paused_.store(false, std::memory_order_relaxed); + std::lock_guard lock(impl_mutex_); if (!impl_) return -1; return impl_->Resume(monitor_index); } int ScreenCapturerWin::SwitchTo(int monitor_index) { + std::lock_guard impl_lock(impl_mutex_); if (!impl_) return -1; - const int ret = impl_->SwitchTo(monitor_index); + int backend_index = -1; + { + std::lock_guard lock(alias_mutex_); + if (monitor_index < 0 || + monitor_index >= static_cast(canonical_displays_.size()) || + canonical_displays_[monitor_index].width <= 0 || + canonical_displays_[monitor_index].height <= 0) return -1; + const auto found = std::find(backend_to_canonical_.begin(), + backend_to_canonical_.end(), monitor_index); + if (found != backend_to_canonical_.end()) + backend_index = static_cast(found - backend_to_canonical_.begin()); + } + if (backend_index < 0) return -1; + const int ret = impl_->SwitchTo(backend_index); if (ret == 0) { monitor_index_.store(monitor_index, std::memory_order_relaxed); } else if (privacy_) { @@ -748,11 +780,10 @@ int ScreenCapturerWin::SwitchTo(int monitor_index) { } int ScreenCapturerWin::ResetToInitialMonitor() { + std::lock_guard lock(impl_mutex_); if (!impl_) return -1; const int ret = impl_->ResetToInitialMonitor(); - if (ret == 0) { - monitor_index_.store(initial_monitor_index_, std::memory_order_relaxed); - } + RebuildAliasesFromImpl(true); return ret; } @@ -762,11 +793,16 @@ std::vector ScreenCapturerWin::GetDisplayInfoList() { return canonical_displays_; } +int ScreenCapturerWin::GetCurrentMonitorIndex() const { + return monitor_index_.load(std::memory_order_relaxed); +} + void ScreenCapturerWin::BuildCanonicalFromImpl() { std::lock_guard lock(alias_mutex_); handle_to_canonical_index_.clear(); stream_id_alias_.clear(); canonical_displays_ = impl_->GetDisplayInfoList(); + backend_to_canonical_.clear(); std::unordered_map name_counts; for (const auto& display : canonical_displays_) { if (!display.name.empty()) { @@ -774,6 +810,7 @@ void ScreenCapturerWin::BuildCanonicalFromImpl() { } } for (size_t i = 0; i < canonical_displays_.size(); ++i) { + backend_to_canonical_.push_back(static_cast(i)); auto& di = canonical_displays_[i]; const std::string stream_id = MakeDisplayStreamId(i); if (di.name.empty()) { @@ -788,11 +825,14 @@ void ScreenCapturerWin::BuildCanonicalFromImpl() { } } -void ScreenCapturerWin::RebuildAliasesFromImpl() { +void ScreenCapturerWin::RebuildAliasesFromImpl(bool preserve_backend_slots) { std::lock_guard lock(alias_mutex_); stream_id_alias_.clear(); auto current = impl_->GetDisplayInfoList(); if (current.empty() || canonical_displays_.empty()) return; + const bool stable_slots = preserve_backend_slots && + dynamic_cast(impl_.get()); + if (!stable_slots) backend_to_canonical_.assign(current.size(), -1); const auto previous_handles = handle_to_canonical_index_; handle_to_canonical_index_.clear(); std::unordered_map name_counts; @@ -813,8 +853,14 @@ void ScreenCapturerWin::RebuildAliasesFromImpl() { ++current_index) { const auto& di = current[current_index]; int canonical_index = -1; + if (stable_slots) { + // New outputs have no registered wire stream in this session. + if (current_index >= backend_to_canonical_.size() || + backend_to_canonical_[current_index] < 0) continue; + canonical_index = backend_to_canonical_[current_index]; + } auto old_handle = previous_handles.find(di.handle); - if (old_handle != previous_handles.end() && + if (canonical_index < 0 && old_handle != previous_handles.end() && old_handle->second < canonical_displays_.size() && !used[old_handle->second]) { canonical_index = static_cast(old_handle->second); @@ -840,6 +886,7 @@ void ScreenCapturerWin::RebuildAliasesFromImpl() { } used[canonical_index] = true; + backend_to_canonical_[current_index] = canonical_index; auto& canonical = canonical_displays_[canonical_index]; const std::string backend_stream_id = MakeDisplayStreamId(current_index); @@ -849,13 +896,18 @@ void ScreenCapturerWin::RebuildAliasesFromImpl() { if (!di.name.empty() && name_counts[di.name] == 1) { stream_id_alias_[di.name] = stable_stream_id; } - handle_to_canonical_index_[di.handle] = - static_cast(canonical_index); + if (di.handle) + handle_to_canonical_index_[di.handle] = + static_cast(canonical_index); canonical = di; if (canonical.name.empty()) { canonical.name = stable_stream_id; } } + const int active = impl_->GetCurrentMonitorIndex(); + if (active >= 0 && active < static_cast(backend_to_canonical_.size()) && + backend_to_canonical_[active] >= 0) + monitor_index_.store(backend_to_canonical_[active]); } void ScreenCapturerWin::StopSecureCaptureThread() { @@ -915,6 +967,22 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() { return false; } +void ScreenCapturerWin::CheckCaptureProgress(ULONGLONG now) { + if (capture_method_ != ScreenCaptureMethod::Auto || !running_.load()) return; + auto* dxgi = dynamic_cast(impl_.get()); + if (!dxgi) return; + + const auto last_progress = + last_capture_progress_tick_.load(std::memory_order_relaxed); + // A capture callback may have advanced the tick after this loop sampled now. + if (last_progress == 0 || now < last_progress || + now - last_progress < kCaptureStallTimeoutMs) return; + // Retry only the pinned output. A missing target must never trigger a + // backend switch or move the session onto another display. + dxgi->RequestTopologyRefresh(); + last_capture_progress_tick_.store(now, std::memory_order_relaxed); +} + void ScreenCapturerWin::ApplyCursorCaptureSetting() { const bool requested = show_cursor_.load(std::memory_order_relaxed); if (!impl_ || requested == applied_show_cursor_) return; @@ -1337,6 +1405,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) { StopSecureDesktopSharedCapture(secure_shared_session_id_); + CheckCaptureProgress(now); if (post_secure_restart_pending) { if (now >= post_secure_restart_deadline_tick) { LOG_WARN( diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h index bc5d15bd..1c432a48 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h @@ -49,6 +49,7 @@ class ScreenCapturerWin : public ScreenCapturer { int ResetToInitialMonitor() override; std::vector GetDisplayInfoList() override; + int GetCurrentMonitorIndex() const override; void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; } // Set before Init(), while capture is stopped. void SetCaptureMethod(ScreenCaptureMethod method) { capture_method_ = method; } @@ -58,6 +59,10 @@ class ScreenCapturerWin : public ScreenCapturer { private: std::unique_ptr impl_; + // Serializes backend replacement (secure-desktop recovery runs on the + // management thread) against the public entry points that dereference it. + // Lock order: impl_mutex_ before alias_mutex_; never the reverse. + std::mutex impl_mutex_; ScreenCaptureMethod capture_method_ = ScreenCaptureMethod::Auto; bool IsBackendEnabled(ScreenCaptureMethod method) const { return capture_method_ == ScreenCaptureMethod::Auto || @@ -73,15 +78,16 @@ class ScreenCapturerWin : public ScreenCapturer { std::unordered_map stream_id_alias_; std::mutex alias_mutex_; std::vector canonical_displays_; + std::vector backend_to_canonical_; std::atomic invalid_stream_id_logged_{false}; std::atomic native_output_logged_{false}; std::atomic native_output_error_logged_{false}; std::atomic running_{false}; + std::atomic last_capture_progress_tick_{0}; std::atomic paused_{false}; std::atomic show_cursor_{true}; bool applied_show_cursor_ = true; std::atomic monitor_index_{0}; - int initial_monitor_index_ = 0; std::atomic secure_desktop_capture_active_{false}; std::atomic post_secure_desktop_waiting_for_frame_{false}; std::atomic post_secure_desktop_drop_logged_{false}; @@ -104,7 +110,7 @@ class ScreenCapturerWin : public ScreenCapturer { std::shared_ptr native_frame_pool_; void BuildCanonicalFromImpl(); - void RebuildAliasesFromImpl(); + void RebuildAliasesFromImpl(bool preserve_backend_slots = false); void RestoreMonitor(int monitor_index); bool TryStartBackend(std::unique_ptr candidate, int monitor_index); @@ -114,6 +120,7 @@ class ScreenCapturerWin : public ScreenCapturer { bool from_secure_desktop = false); void StopSecureCaptureThread(); bool RestartCaptureBackendAfterSecureDesktop(); + void CheckCaptureProgress(ULONGLONG now); void ApplyCursorCaptureSetting(); void SecureDesktopCaptureLoop(); bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height, diff --git a/apps/desktop/src/platform/windows/screen_capturer/stable_display_slots.h b/apps/desktop/src/platform/windows/screen_capturer/stable_display_slots.h new file mode 100644 index 00000000..1ec0f735 --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/stable_display_slots.h @@ -0,0 +1,41 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-21 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _STABLE_DISPLAY_SLOTS_H_ +#define _STABLE_DISPLAY_SLOTS_H_ + +#include +#include +#include + +namespace crossdesk { + +// Logical DisplayN slots live for one capture session. A missing monitor keeps +// its slot; newly attached outputs cannot replace it just by becoming primary. +class StableDisplaySlots { + public: + void Reset() { identities_.clear(); } + + std::vector Update(const std::vector& current) { + for (const auto& identity : current) { + if (std::find(identities_.begin(), identities_.end(), identity) == + identities_.end()) identities_.push_back(identity); + } + std::vector indices(identities_.size(), -1); + for (size_t slot = 0; slot < identities_.size(); ++slot) { + const auto found = std::find(current.begin(), current.end(), identities_[slot]); + if (found != current.end()) indices[slot] = static_cast(found - current.begin()); + } + return indices; + } + + private: + std::vector identities_; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/screen_capturer/screen_capturer.h b/apps/desktop/src/screen_capturer/screen_capturer.h index 8b2da871..dbbc06f1 100644 --- a/apps/desktop/src/screen_capturer/screen_capturer.h +++ b/apps/desktop/src/screen_capturer/screen_capturer.h @@ -21,6 +21,8 @@ class ScreenCapturer { // Windows backends emit a control-only callback BEFORE automatic recovery. // No video buffer accompanies this notification; wrappers must consume it. static constexpr int kBackendReset = -1; + // Display geometry/availability changed; keep the session and privacy intent. + static constexpr int kDisplayTopologyChanged = -2; // |stream_id| is a logical MiniRTC stream ID (DisplayN), not a platform // display name or physical handle. |native_frame| is borrowed for the // duration of the callback; retain its owner before using it asynchronously. @@ -44,6 +46,9 @@ class ScreenCapturer { virtual std::vector GetDisplayInfoList() = 0; virtual int SwitchTo(int monitor_index) = 0; virtual int ResetToInitialMonitor() = 0; + // Append virtual methods to preserve existing WGC plugin vtable slots. + // Backends without index reporting retain the default primary index. + virtual int GetCurrentMonitorIndex() const { return 0; } }; } // namespace crossdesk #endif