[fix] keep Windows display slots and input mapping stable across hotplug

This commit is contained in:
dijunkun
2026-09-21 01:09:12 +08:00
parent 1a59a6ac2e
commit 276385b816
14 changed files with 482 additions and 271 deletions
@@ -330,6 +330,28 @@ void SessionDeviceManager::StopSpeakerCapturer() {
speaker_capture_.SetEnabled(false); 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<std::mutex> 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<DisplayInfo> displays) { void SessionDeviceManager::SetDisplayInfoList(std::vector<DisplayInfo> displays) {
std::lock_guard<std::mutex> lock(display_info_mutex_); std::lock_guard<std::mutex> lock(display_info_mutex_);
display_info_list_ = std::move(displays); 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 #endif
SyncDisplayInfo();
mouse_controller_ = mouse_controller_ =
static_cast<MouseController *>(device_controller_factory_->Create( static_cast<MouseController *>(device_controller_factory_->Create(
DeviceControllerFactory::Device::Mouse)); DeviceControllerFactory::Device::Mouse));
@@ -375,7 +392,7 @@ int SessionDeviceManager::StartMouseController() {
return -1; 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) { if (init_ret != 0) {
LOG_INFO("Destroy mouse controller"); LOG_INFO("Destroy mouse controller");
mouse_controller_->Destroy(); mouse_controller_->Destroy();
@@ -564,16 +581,19 @@ void SessionDeviceManager::UpdateInteractions() {
owner_.mouse_controller_is_started_ = false; owner_.mouse_controller_is_started_ = false;
} }
#if defined(__linux__) || defined(__APPLE__) if (owner_.screen_capturer_is_started_ && mouse_controller_ && SyncDisplayInfo()) {
if (owner_.screen_capturer_is_started_ && screen_capturer_ && #ifdef _WIN32
mouse_controller_) { const int active_display = screen_capturer_->GetCurrentMonitorIndex();
const auto latest_display_info = screen_capturer_->GetDisplayInfoList(); const auto displays = display_info_list();
if (!latest_display_info.empty()) { if (active_display >= 0 &&
display_info_list_ = latest_display_info; active_display < static_cast<int>(displays.size())) {
mouse_controller_->UpdateDisplayInfoList(display_info_list_); owner_.selected_display_ = active_display;
if (displays[active_display].width <= 0 ||
displays[active_display].height <= 0)
ReleaseRemoteMouseButtons();
} }
}
#endif #endif
}
if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_) { if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_) {
if (!owner_.keyboard_capturer_is_started_ && StartKeyboardCapturer() == 0) { if (!owner_.keyboard_capturer_is_started_ && StartKeyboardCapturer() == 0) {
@@ -646,7 +666,10 @@ void SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
} }
int SessionDeviceManager::SwitchDisplay(int display_id) { 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() { void SessionDeviceManager::ResetToInitialDisplay() {
@@ -661,6 +684,7 @@ void SessionDeviceManager::ResetToInitialDisplay() {
// Capture backends start at display 0. Reset the input mapping as well so // Capture backends start at display 0. Reset the input mapping as well so
// the next connection cannot show that display while controlling another. // the next connection cannot show that display while controlling another.
owner_.selected_display_ = 0; owner_.selected_display_ = 0;
SyncDisplayInfo();
} }
std::vector<DisplayInfo> SessionDeviceManager::display_info_list() const { std::vector<DisplayInfo> SessionDeviceManager::display_info_list() const {
@@ -96,6 +96,7 @@ private:
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now, bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
int fps); int fps);
bool ShouldCaptureCursor() const; bool ShouldCaptureCursor() const;
bool SyncDisplayInfo();
void SetDisplayInfoList(std::vector<DisplayInfo> displays); void SetDisplayInfoList(std::vector<DisplayInfo> displays);
void RecordCaptureCadence(std::chrono::steady_clock::time_point now, int fps, void RecordCaptureCadence(std::chrono::steady_clock::time_point now, int fps,
bool from_secure_desktop); bool from_secure_desktop);
@@ -18,11 +18,28 @@ PlatformMouseController::PlatformMouseController() {}
PlatformMouseController::~PlatformMouseController() { Destroy(); } PlatformMouseController::~PlatformMouseController() { Destroy(); }
int PlatformMouseController::Init(std::vector<DisplayInfo> display_info_list) { int PlatformMouseController::Init(std::vector<DisplayInfo> display_info_list) {
std::lock_guard<std::mutex> lock(display_info_mutex_);
display_info_list_ = display_info_list; display_info_list_ = display_info_list;
return 0; return 0;
} }
void PlatformMouseController::UpdateDisplayInfoList(
const std::vector<DisplayInfo>& display_info_list) {
if (display_info_list.empty()) return;
std::lock_guard<std::mutex> 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::Destroy() { return ReleasePressedButtons(); }
int PlatformMouseController::ReleasePressedButtons() { int PlatformMouseController::ReleasePressedButtons() {
@@ -44,13 +61,19 @@ int PlatformMouseController::ReleasePressedButtons() {
int PlatformMouseController::SendMouseCommand(RemoteAction remote_action, int PlatformMouseController::SendMouseCommand(RemoteAction remote_action,
int display_index) { int display_index) {
std::unique_lock<std::mutex> geometry_lock(display_info_mutex_);
if (display_index < 0 || if (display_index < 0 ||
display_index >= static_cast<int>(display_info_list_.size())) { display_index >= static_cast<int>(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={}", LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
display_index, display_info_list_.size()); display_index, display_info_list_.size());
return -1; return -1;
} }
const auto display = display_info_list_[display_index];
geometry_lock.unlock();
INPUT ip = {0}; INPUT ip = {0};
ScopedWindowsPhysicalCoordinates physical_coordinates; ScopedWindowsPhysicalCoordinates physical_coordinates;
@@ -59,11 +82,9 @@ int PlatformMouseController::SendMouseCommand(RemoteAction remote_action,
return -1; return -1;
ip.type = INPUT_MOUSE; ip.type = INPUT_MOUSE;
ip.mi.dx = (LONG)(std::clamp(remote_action.m.x, 0.0f, 1.0f) * ip.mi.dx = (LONG)(std::clamp(remote_action.m.x, 0.0f, 1.0f) *
(display_info_list_[display_index].width - 1)) + (display.width - 1)) + display.left;
display_info_list_[display_index].left;
ip.mi.dy = (LONG)(std::clamp(remote_action.m.y, 0.0f, 1.0f) * ip.mi.dy = (LONG)(std::clamp(remote_action.m.y, 0.0f, 1.0f) *
(display_info_list_[display_index].height - 1)) + (display.height - 1)) + display.top;
display_info_list_[display_index].top;
switch (remote_action.m.flag) { switch (remote_action.m.flag) {
case MouseFlag::left_down: case MouseFlag::left_down:
@@ -106,12 +127,18 @@ int PlatformMouseController::SendMouseCommand(RemoteAction remote_action,
} }
// Pixel centres in the virtual desktop, including negative monitor origins. // Pixel centres in the virtual desktop, including negative monitor origins.
// SetCursorPos cannot carry dwExtraInfo through the privacy mouse hook. // SetCursorPos cannot carry dwExtraInfo through the privacy mouse hook.
ip.mi.dx = static_cast<LONG>( const int virtual_width_range = (std::max)(1, virtual_width - 1);
(static_cast<int64_t>(ip.mi.dx - virtual_left) * 65536 + 32768) / const int virtual_height_range = (std::max)(1, virtual_height - 1);
virtual_width); ip.mi.dx = static_cast<LONG>(std::clamp<int64_t>(
ip.mi.dy = static_cast<LONG>( (static_cast<int64_t>(ip.mi.dx - virtual_left) * 65535 +
(static_cast<int64_t>(ip.mi.dy - virtual_top) * 65536 + 32768) / virtual_width_range / 2) /
virtual_height); virtual_width_range,
0, 65535));
ip.mi.dy = static_cast<LONG>(std::clamp<int64_t>(
(static_cast<int64_t>(ip.mi.dy - virtual_top) * 65535 +
virtual_height_range / 2) /
virtual_height_range,
0, 65535));
ip.mi.dwFlags |= ip.mi.dwFlags |=
MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK; MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK;
ip.mi.dwExtraInfo = kInjectedMouseInputMarker; ip.mi.dwExtraInfo = kInjectedMouseInputMarker;
@@ -9,6 +9,7 @@
#include <vector> #include <vector>
#include <atomic> #include <atomic>
#include <mutex>
#include <remote_action.h> #include <remote_action.h>
@@ -26,10 +27,13 @@ class PlatformMouseController final : public MouseController {
virtual int Init(std::vector<DisplayInfo> display_info_list); virtual int Init(std::vector<DisplayInfo> display_info_list);
virtual int Destroy(); virtual int Destroy();
virtual int SendMouseCommand(RemoteAction remote_action, int display_index); virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
void UpdateDisplayInfoList(
const std::vector<DisplayInfo>& display_info_list) override;
int ReleasePressedButtons(); int ReleasePressedButtons();
private: private:
std::vector<DisplayInfo> display_info_list_; std::vector<DisplayInfo> display_info_list_;
std::mutex display_info_mutex_;
std::atomic<unsigned> pressed_buttons_{0}; std::atomic<unsigned> pressed_buttons_{0};
}; };
} // namespace crossdesk } // namespace crossdesk
@@ -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 <Windows.h>
#include <string>
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<int>(wstr.size()),
nullptr, 0, nullptr, nullptr);
if (size_needed <= 0) return {};
std::string result(static_cast<size_t>(size_needed), '\0');
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), static_cast<int>(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_
@@ -9,6 +9,7 @@
#include "captured_cursor_state.h" #include "captured_cursor_state.h"
#include "cursor_frame_compositor.h" #include "cursor_frame_compositor.h"
#include "display_label.h"
#include "dxgi_cursor_state.h" #include "dxgi_cursor_state.h"
#include "libyuv.h" #include "libyuv.h"
#include "rd_log.h" #include "rd_log.h"
@@ -17,29 +18,18 @@
namespace crossdesk { namespace crossdesk {
namespace { namespace {
std::string WideToUtf8(const std::wstring& wstr) { std::wstring GetOutputDeviceId(const std::wstring& device_name) {
if (wstr.empty()) return {}; DISPLAY_DEVICEW device{sizeof(device)};
int size_needed = WideCharToMultiByte( if (!EnumDisplayDevicesW(device_name.c_str(), 0, &device,
CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr); EDD_GET_DEVICE_INTERFACE_NAME)) {
std::string result(size_needed, 0); return {};
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; return device.DeviceID;
} }
} // namespace } // namespace
ScreenCapturerDxgi::ScreenCapturerDxgi() {} ScreenCapturerDxgi::ScreenCapturerDxgi() {}
ScreenCapturerDxgi::~ScreenCapturerDxgi() { ScreenCapturerDxgi::~ScreenCapturerDxgi() {
Stop();
Destroy(); Destroy();
} }
@@ -51,11 +41,9 @@ int ScreenCapturerDxgi::Init(const int fps, cb_desktop_data cb) {
return -1; return -1;
} }
if (!InitializeDxgi()) { display_slots_.Reset();
LOG_ERROR("DXGI: initialize DXGI failed"); display_info_list_.clear();
return -2; topology_refresh_requested_.store(false);
}
EnumerateDisplays(); EnumerateDisplays();
if (display_info_list_.empty()) { if (display_info_list_.empty()) {
LOG_ERROR("DXGI: no displays found"); LOG_ERROR("DXGI: no displays found");
@@ -63,7 +51,6 @@ int ScreenCapturerDxgi::Init(const int fps, cb_desktop_data cb) {
} }
monitor_index_ = 0; monitor_index_ = 0;
initial_monitor_index_ = monitor_index_;
return 0; return 0;
} }
@@ -71,9 +58,10 @@ int ScreenCapturerDxgi::Destroy() {
Stop(); Stop();
ReleaseDuplication(); ReleaseDuplication();
outputs_.clear(); outputs_.clear();
display_info_list_.clear();
display_slots_.Reset();
d3d_context_.Reset(); d3d_context_.Reset();
d3d_device_.Reset(); d3d_device_.Reset();
dxgi_factory_.Reset();
if (nv12_frame_) { if (nv12_frame_) {
delete[] nv12_frame_; delete[] nv12_frame_;
nv12_frame_ = nullptr; nv12_frame_ = nullptr;
@@ -123,13 +111,24 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index); LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
return -1; 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; paused_ = true;
monitor_index_ = monitor_index; const int previous = monitor_index_.exchange(monitor_index);
ReleaseDuplication(); ReleaseDuplication();
if (!CreateDuplicationForMonitor(monitor_index_)) { if (!CreateDuplicationForMonitor(monitor_index)) {
LOG_ERROR("DXGI: create duplication failed for monitor {}", LOG_ERROR("DXGI: create duplication failed for monitor {}", monitor_index);
monitor_index_.load()); // Keep the backend on the output it was capturing so the wrapper's index
paused_ = false; // Reset paused_ on failure // 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; return -2;
} }
paused_ = false; paused_ = false;
@@ -139,107 +138,134 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
} }
int ScreenCapturerDxgi::ResetToInitialMonitor() { int ScreenCapturerDxgi::ResetToInitialMonitor() {
std::lock_guard<std::mutex> lock(switch_mutex_); return SwitchTo(0);
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;
} }
bool ScreenCapturerDxgi::InitializeDxgi() { bool ScreenCapturerDxgi::EnumerateDisplays() {
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; ScopedWindowsPhysicalCoordinates physical_coordinates;
#ifdef _DEBUG // Factories cache adapter topology. Refresh the factory as well as outputs.
flags |= D3D11_CREATE_DEVICE_DEBUG; Microsoft::WRL::ComPtr<IDXGIFactory1> factory;
#endif const HRESULT factory_result =
CreateDXGIFactory1(IID_PPV_ARGS(factory.GetAddressOf()));
D3D_FEATURE_LEVEL feature_levels[] = { if (FAILED(factory_result)) {
D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, LOG_ERROR("DXGI: CreateDXGIFactory1 failed, hr={}", (int)factory_result);
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<void**>(dxgi_factory_.GetAddressOf()));
if (FAILED(hr)) {
LOG_ERROR("DXGI: CreateDXGIFactory1 failed, hr={}", (int)hr);
return false; return false;
} }
return true; struct Output {
} DisplayInfo display;
Microsoft::WRL::ComPtr<IDXGIOutput> output;
void ScreenCapturerDxgi::EnumerateDisplays() { std::string identity;
display_info_list_.clear(); };
outputs_.clear(); std::vector<Output> current;
Microsoft::WRL::ComPtr<IDXGIAdapter> adapter; Microsoft::WRL::ComPtr<IDXGIAdapter> adapter;
for (UINT a = 0; for (UINT a = 0;
dxgi_factory_->EnumAdapters(a, adapter.ReleaseAndGetAddressOf()) != factory->EnumAdapters(a, adapter.ReleaseAndGetAddressOf()) !=
DXGI_ERROR_NOT_FOUND; DXGI_ERROR_NOT_FOUND; ++a) {
++a) { if (!adapter) break;
Microsoft::WRL::ComPtr<IDXGIOutput> output; Microsoft::WRL::ComPtr<IDXGIOutput> output;
for (UINT o = 0; adapter->EnumOutputs(o, output.ReleaseAndGetAddressOf()) != for (UINT o = 0; adapter->EnumOutputs(o, output.ReleaseAndGetAddressOf()) !=
DXGI_ERROR_NOT_FOUND; DXGI_ERROR_NOT_FOUND; ++o) {
++o) { if (!output) break;
DXGI_OUTPUT_DESC desc{}; DXGI_OUTPUT_DESC desc{};
if (FAILED(output->GetDesc(&desc))) { if (FAILED(output->GetDesc(&desc)) || !desc.AttachedToDesktop) continue;
continue;
}
std::string name = GetDisplayLabel(desc.DeviceName);
MONITORINFOEX mi{}; MONITORINFOEX mi{};
mi.cbSize = sizeof(MONITORINFOEX); mi.cbSize = sizeof(mi);
if (GetMonitorInfo(desc.Monitor, &mi)) { if (!GetMonitorInfo(desc.Monitor, &mi)) continue;
bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false; const auto device_id = GetOutputDeviceId(desc.DeviceName);
DisplayInfo info((void*)desc.Monitor, name, is_primary, current.push_back({
mi.rcMonitor.left, mi.rcMonitor.top, DisplayInfo(desc.Monitor, GetDisplayLabel(desc.DeviceName),
mi.rcMonitor.right, mi.rcMonitor.bottom); (mi.dwFlags & MONITORINFOF_PRIMARY) != 0,
// primary first mi.rcMonitor.left, mi.rcMonitor.top,
if (is_primary) { mi.rcMonitor.right, mi.rcMonitor.bottom),
display_info_list_.insert(display_info_list_.begin(), info); output, WideToUtf8(device_id.empty() ? desc.DeviceName : device_id)});
outputs_.insert(outputs_.begin(), output);
} else {
display_info_list_.push_back(info);
outputs_.push_back(output);
}
}
} }
} }
std::stable_partition(current.begin(), current.end(),
[](const auto& item) { return item.display.is_primary; });
std::vector<std::string> identities;
for (const auto& item : current) identities.push_back(item.identity);
const auto slots = display_slots_.Update(identities);
std::vector<DisplayInfo> displays;
std::vector<Microsoft::WRL::ComPtr<IDXGIOutput>> 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<int>(slot) == monitor_index_.load());
}
}
return changed;
} }
bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) { bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
SharedDxgiCursorState().Reset(); 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<IDXGIAdapter> 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<ID3D11Device> output_device;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> 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<IDXGIOutput1> output1; Microsoft::WRL::ComPtr<IDXGIOutput1> output1;
HRESULT hr = outputs_[monitor_index]->QueryInterface( hr = outputs_[monitor_index]->QueryInterface(
IID_PPV_ARGS(output1.GetAddressOf())); IID_PPV_ARGS(output1.GetAddressOf()));
if (FAILED(hr)) { if (FAILED(hr)) {
LOG_ERROR("DXGI: Query IDXGIOutput1 failed, hr={}", (int)hr); LOG_ERROR("DXGI: Query IDXGIOutput1 failed, hr={}", (int)hr);
@@ -257,37 +283,14 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
DXGI_OUTDUPL_DESC desc{}; DXGI_OUTDUPL_DESC desc{};
duplication_->GetDesc(&desc); duplication_->GetDesc(&desc);
rotation_ = desc.Rotation; rotation_ = desc.Rotation;
LOG_INFO("DXGI: duplication ready, monitor={}, rotation={}", monitor_index, DXGI_ADAPTER_DESC adapter_desc{};
static_cast<int>(rotation_)); output_adapter->GetDesc(&adapter_desc);
LOG_INFO("DXGI: duplication ready, monitor={}, rotation={}, adapter='{}'",
monitor_index, static_cast<int>(rotation_),
WideToUtf8(adapter_desc.Description));
return true; return true;
} }
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
std::lock_guard<std::mutex> 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<int>(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() { void ScreenCapturerDxgi::ReleaseDuplication() {
SharedDxgiCursorState().Reset(); SharedDxgiCursorState().Reset();
++duplication_generation_; ++duplication_generation_;
@@ -351,20 +354,29 @@ bool ScreenCapturerDxgi::ConvertFrame(int frame_monitor, bool* cursor_embedded,
return false; return false;
} }
if (show_cursor_.load(std::memory_order_relaxed) && frame_monitor >= 0 && if (frame_monitor >= 0 &&
frame_monitor < static_cast<int>(display_info_list_.size())) { frame_monitor < static_cast<int>(display_info_list_.size())) {
const auto& display = display_info_list_[frame_monitor]; const auto& display = display_info_list_[frame_monitor];
CURSORINFO cursor{}; CURSORINFO cursor{};
cursor.cbSize = sizeof(cursor); cursor.cbSize = sizeof(cursor);
if (GetCursorInfo(&cursor) && if (GetCursorInfo(&cursor)) {
SharedDxgiCursorState().ShouldDrawCursor( const bool visible = (cursor.flags & CURSOR_SHOWING) != 0;
(cursor.flags & CURSOR_SHOWING) != 0, display.handle)) { const bool should_draw = SharedDxgiCursorState().ShouldDrawCursor(
if (const auto* composited = visible, display.handle);
compositor.Draw(pixels, stride, even_width, even_height, cursor, // show_cursor controls our extra composition, not cursors the OS has
display.left, display.top)) { // already put in the texture. Publish that distinction for native
pixels = composited; // controllers as well, so they do not draw a second cursor.
stride = even_width * 4; *cursor_embedded = visible && !should_draw &&
*cursor_embedded = true; 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; bool cached_cursor_embedded = false;
int cached_monitor = -1; int cached_monitor = -1;
uint64_t cached_generation = 0; uint64_t cached_generation = 0;
auto last_duplication_retry = auto next_topology_check = std::chrono::steady_clock::now();
std::chrono::steady_clock::now() - std::chrono::milliseconds(1000); unsigned acquire_failures = 0;
while (running_) { while (running_) {
if (paused_) { if (paused_) {
cached_frame_valid = false; cached_frame_valid = false;
@@ -408,14 +420,25 @@ void ScreenCapturerDxgi::CaptureLoop() {
// replace them in SwitchTo while AcquireNextFrame/Map is using them. // replace them in SwitchTo while AcquireNextFrame/Map is using them.
std::unique_lock capture_lock(switch_mutex_); std::unique_lock capture_lock(switch_mutex_);
if (paused_ || !running_) continue; 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_) { if (!duplication_) {
cached_frame_valid = false; cached_frame_valid = false;
capture_lock.unlock(); 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)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue; continue;
} }
@@ -449,17 +472,21 @@ void ScreenCapturerDxgi::CaptureLoop() {
continue; continue;
} }
// Never replay a cached image after an acquisition/conversion failure or // 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; cached_frame_valid = false;
if (FAILED(hr)) { if (FAILED(hr)) {
SharedDxgiCursorState().Reset();
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr); LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
capture_lock.unlock(); ReleaseDuplication();
if (callback_) // A single ACCESS_LOST (mode switch, fullscreen toggle, UAC return) is
callback_(nullptr, ScreenCapturer::kBackendReset, 0, 0, "", nullptr); // rebuilt on the next iteration. Only repeated failures fall back to
RecreateDuplicationForCurrentMonitor(); // 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; continue;
} }
acquire_failures = 0;
if (frame_monitor >= 0 && if (frame_monitor >= 0 &&
frame_monitor < static_cast<int>(display_info_list_.size())) { frame_monitor < static_cast<int>(display_info_list_.size())) {
@@ -506,24 +533,18 @@ void ScreenCapturerDxgi::CaptureLoop() {
const bool converted = const bool converted =
ConvertFrame(frame_monitor, &cursor_embedded, cursor_compositor); ConvertFrame(frame_monitor, &cursor_embedded, cursor_compositor);
const std::string stream_id = MakeDisplayStreamId(frame_monitor);
cached_generation = duplication_generation_; cached_generation = duplication_generation_;
duplication_->ReleaseFrame(); duplication_->ReleaseFrame();
capture_lock.unlock(); capture_lock.unlock();
if (converted && frame_monitor == monitor_index_.load() && callback_) { if (converted && frame_monitor == monitor_index_.load() && callback_) {
int idx = frame_monitor; CapturedCursorFrameScope cursor_scope(cursor_embedded);
if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) { callback_(nv12_frame_, nv12_width_ * nv12_height_ * 3 / 2, nv12_width_,
const std::string stream_id = MakeDisplayStreamId(idx); nv12_height_, stream_id.c_str(), nullptr);
CapturedCursorFrameScope cursor_scope(cursor_embedded); cached_cursor_embedded = cursor_embedded;
callback_(nv12_frame_, nv12_width_ * nv12_height_ * 3 / 2, nv12_width_, cached_monitor = frame_monitor;
nv12_height_, stream_id.c_str(), nullptr); cached_frame_valid = true;
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());
}
} }
} }
} }
@@ -22,6 +22,7 @@
#include "rd_log.h" #include "rd_log.h"
#include "screen_capturer.h" #include "screen_capturer.h"
#include "stable_display_slots.h"
namespace crossdesk { namespace crossdesk {
@@ -47,15 +48,20 @@ class ScreenCapturerDxgi : public ScreenCapturer {
int SwitchTo(int monitor_index) override; int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() 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<DisplayInfo> GetDisplayInfoList() override { std::vector<DisplayInfo> GetDisplayInfoList() override {
std::lock_guard<std::mutex> lock(switch_mutex_);
return display_info_list_; return display_info_list_;
} }
int GetCurrentMonitorIndex() const override {
return monitor_index_.load(std::memory_order_relaxed);
}
private: private:
bool InitializeDxgi(); bool EnumerateDisplays();
void EnumerateDisplays();
bool CreateDuplicationForMonitor(int monitor_index); bool CreateDuplicationForMonitor(int monitor_index);
bool RecreateDuplicationForCurrentMonitor();
void CaptureLoop(); void CaptureLoop();
bool ConvertFrame(int frame_monitor, bool* cursor_embedded, bool ConvertFrame(int frame_monitor, bool* cursor_embedded,
CursorFrameCompositor& compositor); CursorFrameCompositor& compositor);
@@ -64,8 +70,9 @@ class ScreenCapturerDxgi : public ScreenCapturer {
private: private:
std::vector<DisplayInfo> display_info_list_; std::vector<DisplayInfo> display_info_list_;
std::vector<Microsoft::WRL::ComPtr<IDXGIOutput>> outputs_; std::vector<Microsoft::WRL::ComPtr<IDXGIOutput>> outputs_;
StableDisplaySlots display_slots_;
std::atomic<bool> topology_refresh_requested_{false};
Microsoft::WRL::ComPtr<IDXGIFactory1> dxgi_factory_;
Microsoft::WRL::ComPtr<ID3D11Device> d3d_device_; Microsoft::WRL::ComPtr<ID3D11Device> d3d_device_;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d_context_; Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d_context_;
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_; Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
@@ -76,7 +83,6 @@ class ScreenCapturerDxgi : public ScreenCapturer {
std::atomic<bool> running_{false}; std::atomic<bool> running_{false};
std::atomic<bool> paused_{false}; std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0}; std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true}; std::atomic<bool> show_cursor_{true};
std::thread thread_; std::thread thread_;
int fps_ = 60; int fps_ = 60;
@@ -8,32 +8,12 @@
#include <display_stream_id.h> #include <display_stream_id.h>
#include "captured_cursor_state.h" #include "captured_cursor_state.h"
#include "cursor_draw.h" #include "cursor_draw.h"
#include "display_label.h"
#include "libyuv.h" #include "libyuv.h"
#include "rd_log.h" #include "rd_log.h"
namespace crossdesk { 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() {}
ScreenCapturerGdi::~ScreenCapturerGdi() { ScreenCapturerGdi::~ScreenCapturerGdi() {
Stop(); Stop();
@@ -44,6 +44,9 @@ class ScreenCapturerGdi : public ScreenCapturer {
std::vector<DisplayInfo> GetDisplayInfoList() override { std::vector<DisplayInfo> GetDisplayInfoList() override {
return display_info_list_; return display_info_list_;
} }
int GetCurrentMonitorIndex() const override {
return monitor_index_.load(std::memory_order_relaxed);
}
private: private:
static BOOL CALLBACK EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT, static BOOL CALLBACK EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
@@ -9,6 +9,7 @@
#include <display_stream_id.h> #include <display_stream_id.h>
#include "captured_cursor_state.h" #include "captured_cursor_state.h"
#include "display_label.h"
#include "libyuv.h" #include "libyuv.h"
#include "rd_log.h" #include "rd_log.h"
@@ -16,25 +17,6 @@ namespace crossdesk {
static std::vector<DisplayInfo> gs_display_list; static std::vector<DisplayInfo> 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, BOOL WINAPI EnumMonitorProc(HMONITOR hmonitor, [[maybe_unused]] HDC hdc,
[[maybe_unused]] LPRECT lprc, LPARAM data) { [[maybe_unused]] LPRECT lprc, LPARAM data) {
MONITORINFOEX monitor_info_; MONITORINFOEX monitor_info_;
@@ -2,6 +2,7 @@
#include <Windows.h> #include <Windows.h>
#include <algorithm>
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
@@ -46,6 +47,7 @@ constexpr DWORD kSecureDesktopTransientErrorGraceMs = 1500;
constexpr DWORD kSecureDesktopTransientErrorLogIntervalMs = 5000; constexpr DWORD kSecureDesktopTransientErrorLogIntervalMs = 5000;
constexpr DWORD kPostSecureDesktopRestartRetryMs = 500; constexpr DWORD kPostSecureDesktopRestartRetryMs = 500;
constexpr DWORD kPostSecureDesktopRestartTimeoutMs = 10000; constexpr DWORD kPostSecureDesktopRestartTimeoutMs = 10000;
constexpr ULONGLONG kCaptureStallTimeoutMs = 3000;
constexpr int kSecureDesktopCaptureMinFps = 30; constexpr int kSecureDesktopCaptureMinFps = 30;
constexpr int kSecureDesktopCaptureMaxIntervalMs = constexpr int kSecureDesktopCaptureMaxIntervalMs =
1000 / kSecureDesktopCaptureMinFps; 1000 / kSecureDesktopCaptureMinFps;
@@ -411,6 +413,7 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
cb_orig_ = cb; cb_orig_ = cb;
native_output_logged_.store(false, std::memory_order_relaxed); native_output_logged_.store(false, std::memory_order_relaxed);
native_output_error_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 { try {
native_frame_pool_ = CapturedNv12FramePool::Create(); native_frame_pool_ = CapturedNv12FramePool::Create();
} catch (...) { } catch (...) {
@@ -418,13 +421,22 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
return -1; return -1;
} }
cb_ = [this](unsigned char* data, int size, int w, int h, cb_ = [this](unsigned char* data, int size, int w, int h,
const char* reported_stream_id, const char* reported_stream_id,
const MiniRtcNativeVideoFrame* native_frame) { const MiniRtcNativeVideoFrame* native_frame) {
if (size == ScreenCapturer::kDisplayTopologyChanged) {
SharedCapturedCursorState().Reset();
RebuildAliasesFromImpl(true);
return;
}
if (size == ScreenCapturer::kBackendReset) { if (size == ScreenCapturer::kBackendReset) {
SharedCapturedCursorState().Reset(); SharedCapturedCursorState().Reset();
if (privacy_) privacy_->Fail("Capture restarted; privacy screen will turn off"); if (privacy_) privacy_->Fail("Capture restarted; privacy screen will turn off");
return; 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)) { if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
SharedDxgiCursorState().Reset(); SharedDxgiCursorState().Reset();
SharedCapturedCursorState().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())); LOG_INFO("Windows capturer: using {}", CaptureBackendName(impl_.get()));
BuildCanonicalFromImpl(); BuildCanonicalFromImpl();
monitor_index_.store(0, std::memory_order_relaxed); monitor_index_.store(0, std::memory_order_relaxed);
initial_monitor_index_ = 0;
return 0; return 0;
} }
@@ -604,9 +615,7 @@ void ScreenCapturerWin::EmitCapturedFrame(
void ScreenCapturerWin::RestoreMonitor(int monitor_index) { void ScreenCapturerWin::RestoreMonitor(int monitor_index) {
RebuildAliasesFromImpl(); RebuildAliasesFromImpl();
if (monitor_index > 0 && impl_->SwitchTo(monitor_index) != 0) { if (monitor_index >= 0) SwitchTo(monitor_index);
monitor_index_.store(0, std::memory_order_relaxed);
}
} }
bool ScreenCapturerWin::TryStartBackend( bool ScreenCapturerWin::TryStartBackend(
@@ -618,8 +627,10 @@ bool ScreenCapturerWin::TryStartBackend(
const char* name = CaptureBackendName(candidate.get()); const char* name = CaptureBackendName(candidate.get());
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed); const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
int ret = candidate->Init(fps_, cb_); 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); ret = candidate->Start(show_cursor);
}
if (ret != 0) { if (ret != 0) {
LOG_WARN("Windows capturer: {} initialization/start failed (ret={})", name, LOG_WARN("Windows capturer: {} initialization/start failed (ret={})", name,
ret); ret);
@@ -628,7 +639,10 @@ bool ScreenCapturerWin::TryStartBackend(
} }
// Commit only after the replacement is running. Failed candidates release // Commit only after the replacement is running. Failed candidates release
// their resources without replacing the current backend. // their resources without replacing the current backend.
impl_ = std::move(candidate); {
std::lock_guard<std::mutex> lock(impl_mutex_);
impl_ = std::move(candidate);
}
applied_show_cursor_ = show_cursor; applied_show_cursor_ = show_cursor;
RestoreMonitor(monitor_index); RestoreMonitor(monitor_index);
LOG_INFO("Windows capturer: started {}", name); LOG_INFO("Windows capturer: started {}", name);
@@ -647,6 +661,7 @@ int ScreenCapturerWin::Start(bool show_cursor) {
SharedCapturedCursorState().Reset(); SharedCapturedCursorState().Reset();
paused_.store(false, std::memory_order_relaxed); paused_.store(false, std::memory_order_relaxed);
invalid_stream_id_logged_.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 // Refresh physical monitor identities before every session. HMONITOR and
// DXGI output handles may change after an HDMI hotplug while CrossDesk stays // 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_); int ret = impl_->Init(fps_, cb_);
if (ret == 0) { if (ret == 0) {
RebuildAliasesFromImpl(); 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); 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 (ret != 0) {
if (capture_method_ != ScreenCaptureMethod::Auto) { if (capture_method_ != ScreenCaptureMethod::Auto) {
@@ -707,6 +722,7 @@ int ScreenCapturerWin::Start(bool show_cursor) {
int ScreenCapturerWin::Stop() { int ScreenCapturerWin::Stop() {
NotifyPrivacyCapture(false); NotifyPrivacyCapture(false);
running_.store(false, std::memory_order_relaxed); 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); secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false, post_secure_desktop_waiting_for_frame_.store(false,
std::memory_order_relaxed); std::memory_order_relaxed);
@@ -726,19 +742,35 @@ int ScreenCapturerWin::Stop() {
int ScreenCapturerWin::Pause(int monitor_index) { int ScreenCapturerWin::Pause(int monitor_index) {
paused_.store(true, std::memory_order_relaxed); paused_.store(true, std::memory_order_relaxed);
std::lock_guard<std::mutex> lock(impl_mutex_);
if (!impl_) return -1; if (!impl_) return -1;
return impl_->Pause(monitor_index); return impl_->Pause(monitor_index);
} }
int ScreenCapturerWin::Resume(int monitor_index) { int ScreenCapturerWin::Resume(int monitor_index) {
paused_.store(false, std::memory_order_relaxed); paused_.store(false, std::memory_order_relaxed);
std::lock_guard<std::mutex> lock(impl_mutex_);
if (!impl_) return -1; if (!impl_) return -1;
return impl_->Resume(monitor_index); return impl_->Resume(monitor_index);
} }
int ScreenCapturerWin::SwitchTo(int monitor_index) { int ScreenCapturerWin::SwitchTo(int monitor_index) {
std::lock_guard<std::mutex> impl_lock(impl_mutex_);
if (!impl_) return -1; if (!impl_) return -1;
const int ret = impl_->SwitchTo(monitor_index); int backend_index = -1;
{
std::lock_guard<std::mutex> lock(alias_mutex_);
if (monitor_index < 0 ||
monitor_index >= static_cast<int>(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<int>(found - backend_to_canonical_.begin());
}
if (backend_index < 0) return -1;
const int ret = impl_->SwitchTo(backend_index);
if (ret == 0) { if (ret == 0) {
monitor_index_.store(monitor_index, std::memory_order_relaxed); monitor_index_.store(monitor_index, std::memory_order_relaxed);
} else if (privacy_) { } else if (privacy_) {
@@ -748,11 +780,10 @@ int ScreenCapturerWin::SwitchTo(int monitor_index) {
} }
int ScreenCapturerWin::ResetToInitialMonitor() { int ScreenCapturerWin::ResetToInitialMonitor() {
std::lock_guard<std::mutex> lock(impl_mutex_);
if (!impl_) return -1; if (!impl_) return -1;
const int ret = impl_->ResetToInitialMonitor(); const int ret = impl_->ResetToInitialMonitor();
if (ret == 0) { RebuildAliasesFromImpl(true);
monitor_index_.store(initial_monitor_index_, std::memory_order_relaxed);
}
return ret; return ret;
} }
@@ -762,11 +793,16 @@ std::vector<DisplayInfo> ScreenCapturerWin::GetDisplayInfoList() {
return canonical_displays_; return canonical_displays_;
} }
int ScreenCapturerWin::GetCurrentMonitorIndex() const {
return monitor_index_.load(std::memory_order_relaxed);
}
void ScreenCapturerWin::BuildCanonicalFromImpl() { void ScreenCapturerWin::BuildCanonicalFromImpl() {
std::lock_guard<std::mutex> lock(alias_mutex_); std::lock_guard<std::mutex> lock(alias_mutex_);
handle_to_canonical_index_.clear(); handle_to_canonical_index_.clear();
stream_id_alias_.clear(); stream_id_alias_.clear();
canonical_displays_ = impl_->GetDisplayInfoList(); canonical_displays_ = impl_->GetDisplayInfoList();
backend_to_canonical_.clear();
std::unordered_map<std::string, size_t> name_counts; std::unordered_map<std::string, size_t> name_counts;
for (const auto& display : canonical_displays_) { for (const auto& display : canonical_displays_) {
if (!display.name.empty()) { if (!display.name.empty()) {
@@ -774,6 +810,7 @@ void ScreenCapturerWin::BuildCanonicalFromImpl() {
} }
} }
for (size_t i = 0; i < canonical_displays_.size(); ++i) { for (size_t i = 0; i < canonical_displays_.size(); ++i) {
backend_to_canonical_.push_back(static_cast<int>(i));
auto& di = canonical_displays_[i]; auto& di = canonical_displays_[i];
const std::string stream_id = MakeDisplayStreamId(i); const std::string stream_id = MakeDisplayStreamId(i);
if (di.name.empty()) { if (di.name.empty()) {
@@ -788,11 +825,14 @@ void ScreenCapturerWin::BuildCanonicalFromImpl() {
} }
} }
void ScreenCapturerWin::RebuildAliasesFromImpl() { void ScreenCapturerWin::RebuildAliasesFromImpl(bool preserve_backend_slots) {
std::lock_guard<std::mutex> lock(alias_mutex_); std::lock_guard<std::mutex> lock(alias_mutex_);
stream_id_alias_.clear(); stream_id_alias_.clear();
auto current = impl_->GetDisplayInfoList(); auto current = impl_->GetDisplayInfoList();
if (current.empty() || canonical_displays_.empty()) return; if (current.empty() || canonical_displays_.empty()) return;
const bool stable_slots = preserve_backend_slots &&
dynamic_cast<ScreenCapturerDxgi*>(impl_.get());
if (!stable_slots) backend_to_canonical_.assign(current.size(), -1);
const auto previous_handles = handle_to_canonical_index_; const auto previous_handles = handle_to_canonical_index_;
handle_to_canonical_index_.clear(); handle_to_canonical_index_.clear();
std::unordered_map<std::string, size_t> name_counts; std::unordered_map<std::string, size_t> name_counts;
@@ -813,8 +853,14 @@ void ScreenCapturerWin::RebuildAliasesFromImpl() {
++current_index) { ++current_index) {
const auto& di = current[current_index]; const auto& di = current[current_index];
int canonical_index = -1; 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); 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() && old_handle->second < canonical_displays_.size() &&
!used[old_handle->second]) { !used[old_handle->second]) {
canonical_index = static_cast<int>(old_handle->second); canonical_index = static_cast<int>(old_handle->second);
@@ -840,6 +886,7 @@ void ScreenCapturerWin::RebuildAliasesFromImpl() {
} }
used[canonical_index] = true; used[canonical_index] = true;
backend_to_canonical_[current_index] = canonical_index;
auto& canonical = canonical_displays_[canonical_index]; auto& canonical = canonical_displays_[canonical_index];
const std::string backend_stream_id = const std::string backend_stream_id =
MakeDisplayStreamId(current_index); MakeDisplayStreamId(current_index);
@@ -849,13 +896,18 @@ void ScreenCapturerWin::RebuildAliasesFromImpl() {
if (!di.name.empty() && name_counts[di.name] == 1) { if (!di.name.empty() && name_counts[di.name] == 1) {
stream_id_alias_[di.name] = stable_stream_id; stream_id_alias_[di.name] = stable_stream_id;
} }
handle_to_canonical_index_[di.handle] = if (di.handle)
static_cast<size_t>(canonical_index); handle_to_canonical_index_[di.handle] =
static_cast<size_t>(canonical_index);
canonical = di; canonical = di;
if (canonical.name.empty()) { if (canonical.name.empty()) {
canonical.name = stable_stream_id; canonical.name = stable_stream_id;
} }
} }
const int active = impl_->GetCurrentMonitorIndex();
if (active >= 0 && active < static_cast<int>(backend_to_canonical_.size()) &&
backend_to_canonical_[active] >= 0)
monitor_index_.store(backend_to_canonical_[active]);
} }
void ScreenCapturerWin::StopSecureCaptureThread() { void ScreenCapturerWin::StopSecureCaptureThread() {
@@ -915,6 +967,22 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
return false; return false;
} }
void ScreenCapturerWin::CheckCaptureProgress(ULONGLONG now) {
if (capture_method_ != ScreenCaptureMethod::Auto || !running_.load()) return;
auto* dxgi = dynamic_cast<ScreenCapturerDxgi*>(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() { void ScreenCapturerWin::ApplyCursorCaptureSetting() {
const bool requested = show_cursor_.load(std::memory_order_relaxed); const bool requested = show_cursor_.load(std::memory_order_relaxed);
if (!impl_ || requested == applied_show_cursor_) return; if (!impl_ || requested == applied_show_cursor_) return;
@@ -1337,6 +1405,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) { if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
StopSecureDesktopSharedCapture(secure_shared_session_id_); StopSecureDesktopSharedCapture(secure_shared_session_id_);
CheckCaptureProgress(now);
if (post_secure_restart_pending) { if (post_secure_restart_pending) {
if (now >= post_secure_restart_deadline_tick) { if (now >= post_secure_restart_deadline_tick) {
LOG_WARN( LOG_WARN(
@@ -49,6 +49,7 @@ class ScreenCapturerWin : public ScreenCapturer {
int ResetToInitialMonitor() override; int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override; std::vector<DisplayInfo> GetDisplayInfoList() override;
int GetCurrentMonitorIndex() const override;
void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; } void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; }
// Set before Init(), while capture is stopped. // Set before Init(), while capture is stopped.
void SetCaptureMethod(ScreenCaptureMethod method) { capture_method_ = method; } void SetCaptureMethod(ScreenCaptureMethod method) { capture_method_ = method; }
@@ -58,6 +59,10 @@ class ScreenCapturerWin : public ScreenCapturer {
private: private:
std::unique_ptr<ScreenCapturer> impl_; std::unique_ptr<ScreenCapturer> 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; ScreenCaptureMethod capture_method_ = ScreenCaptureMethod::Auto;
bool IsBackendEnabled(ScreenCaptureMethod method) const { bool IsBackendEnabled(ScreenCaptureMethod method) const {
return capture_method_ == ScreenCaptureMethod::Auto || return capture_method_ == ScreenCaptureMethod::Auto ||
@@ -73,15 +78,16 @@ class ScreenCapturerWin : public ScreenCapturer {
std::unordered_map<std::string, std::string> stream_id_alias_; std::unordered_map<std::string, std::string> stream_id_alias_;
std::mutex alias_mutex_; std::mutex alias_mutex_;
std::vector<DisplayInfo> canonical_displays_; std::vector<DisplayInfo> canonical_displays_;
std::vector<int> backend_to_canonical_;
std::atomic<bool> invalid_stream_id_logged_{false}; std::atomic<bool> invalid_stream_id_logged_{false};
std::atomic<bool> native_output_logged_{false}; std::atomic<bool> native_output_logged_{false};
std::atomic<bool> native_output_error_logged_{false}; std::atomic<bool> native_output_error_logged_{false};
std::atomic<bool> running_{false}; std::atomic<bool> running_{false};
std::atomic<ULONGLONG> last_capture_progress_tick_{0};
std::atomic<bool> paused_{false}; std::atomic<bool> paused_{false};
std::atomic<bool> show_cursor_{true}; std::atomic<bool> show_cursor_{true};
bool applied_show_cursor_ = true; bool applied_show_cursor_ = true;
std::atomic<int> monitor_index_{0}; std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> secure_desktop_capture_active_{false}; std::atomic<bool> secure_desktop_capture_active_{false};
std::atomic<bool> post_secure_desktop_waiting_for_frame_{false}; std::atomic<bool> post_secure_desktop_waiting_for_frame_{false};
std::atomic<bool> post_secure_desktop_drop_logged_{false}; std::atomic<bool> post_secure_desktop_drop_logged_{false};
@@ -104,7 +110,7 @@ class ScreenCapturerWin : public ScreenCapturer {
std::shared_ptr<CapturedNv12FramePool> native_frame_pool_; std::shared_ptr<CapturedNv12FramePool> native_frame_pool_;
void BuildCanonicalFromImpl(); void BuildCanonicalFromImpl();
void RebuildAliasesFromImpl(); void RebuildAliasesFromImpl(bool preserve_backend_slots = false);
void RestoreMonitor(int monitor_index); void RestoreMonitor(int monitor_index);
bool TryStartBackend(std::unique_ptr<ScreenCapturer> candidate, bool TryStartBackend(std::unique_ptr<ScreenCapturer> candidate,
int monitor_index); int monitor_index);
@@ -114,6 +120,7 @@ class ScreenCapturerWin : public ScreenCapturer {
bool from_secure_desktop = false); bool from_secure_desktop = false);
void StopSecureCaptureThread(); void StopSecureCaptureThread();
bool RestartCaptureBackendAfterSecureDesktop(); bool RestartCaptureBackendAfterSecureDesktop();
void CheckCaptureProgress(ULONGLONG now);
void ApplyCursorCaptureSetting(); void ApplyCursorCaptureSetting();
void SecureDesktopCaptureLoop(); void SecureDesktopCaptureLoop();
bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height, bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height,
@@ -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 <algorithm>
#include <string>
#include <vector>
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<int> Update(const std::vector<std::string>& current) {
for (const auto& identity : current) {
if (std::find(identities_.begin(), identities_.end(), identity) ==
identities_.end()) identities_.push_back(identity);
}
std::vector<int> 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<int>(found - current.begin());
}
return indices;
}
private:
std::vector<std::string> identities_;
};
} // namespace crossdesk
#endif
@@ -21,6 +21,8 @@ class ScreenCapturer {
// Windows backends emit a control-only callback BEFORE automatic recovery. // Windows backends emit a control-only callback BEFORE automatic recovery.
// No video buffer accompanies this notification; wrappers must consume it. // No video buffer accompanies this notification; wrappers must consume it.
static constexpr int kBackendReset = -1; 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 // |stream_id| is a logical MiniRTC stream ID (DisplayN), not a platform
// display name or physical handle. |native_frame| is borrowed for the // display name or physical handle. |native_frame| is borrowed for the
// duration of the callback; retain its owner before using it asynchronously. // duration of the callback; retain its owner before using it asynchronously.
@@ -44,6 +46,9 @@ class ScreenCapturer {
virtual std::vector<DisplayInfo> GetDisplayInfoList() = 0; virtual std::vector<DisplayInfo> GetDisplayInfoList() = 0;
virtual int SwitchTo(int monitor_index) = 0; virtual int SwitchTo(int monitor_index) = 0;
virtual int ResetToInitialMonitor() = 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 } // namespace crossdesk
#endif #endif