[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);
}
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) {
std::lock_guard<std::mutex> 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<MouseController *>(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<int>(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<DisplayInfo> SessionDeviceManager::display_info_list() const {
@@ -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<DisplayInfo> displays);
void RecordCaptureCadence(std::chrono::steady_clock::time_point now, int fps,
bool from_secure_desktop);
@@ -18,11 +18,28 @@ PlatformMouseController::PlatformMouseController() {}
PlatformMouseController::~PlatformMouseController() { Destroy(); }
int PlatformMouseController::Init(std::vector<DisplayInfo> display_info_list) {
std::lock_guard<std::mutex> lock(display_info_mutex_);
display_info_list_ = display_info_list;
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::ReleasePressedButtons() {
@@ -44,13 +61,19 @@ int PlatformMouseController::ReleasePressedButtons() {
int PlatformMouseController::SendMouseCommand(RemoteAction remote_action,
int display_index) {
std::unique_lock<std::mutex> geometry_lock(display_info_mutex_);
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={}",
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<LONG>(
(static_cast<int64_t>(ip.mi.dx - virtual_left) * 65536 + 32768) /
virtual_width);
ip.mi.dy = static_cast<LONG>(
(static_cast<int64_t>(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<LONG>(std::clamp<int64_t>(
(static_cast<int64_t>(ip.mi.dx - virtual_left) * 65535 +
virtual_width_range / 2) /
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 |=
MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK;
ip.mi.dwExtraInfo = kInjectedMouseInputMarker;
@@ -9,6 +9,7 @@
#include <vector>
#include <atomic>
#include <mutex>
#include <remote_action.h>
@@ -26,10 +27,13 @@ class PlatformMouseController final : public MouseController {
virtual int Init(std::vector<DisplayInfo> display_info_list);
virtual int Destroy();
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
void UpdateDisplayInfoList(
const std::vector<DisplayInfo>& display_info_list) override;
int ReleasePressedButtons();
private:
std::vector<DisplayInfo> display_info_list_;
std::mutex display_info_mutex_;
std::atomic<unsigned> pressed_buttons_{0};
};
} // 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 "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<std::mutex> 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<void**>(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<IDXGIFactory1> 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<IDXGIOutput> output;
std::string identity;
};
std::vector<Output> current;
Microsoft::WRL::ComPtr<IDXGIAdapter> 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<IDXGIOutput> 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<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) {
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;
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<int>(rotation_));
DXGI_ADAPTER_DESC adapter_desc{};
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;
}
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() {
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<int>(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<int>(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<int>(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;
}
}
}
@@ -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<DisplayInfo> GetDisplayInfoList() override {
std::lock_guard<std::mutex> 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<DisplayInfo> display_info_list_;
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<ID3D11DeviceContext> d3d_context_;
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
@@ -76,7 +83,6 @@ class ScreenCapturerDxgi : public ScreenCapturer {
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
std::thread thread_;
int fps_ = 60;
@@ -8,32 +8,12 @@
#include <display_stream_id.h>
#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();
@@ -44,6 +44,9 @@ class ScreenCapturerGdi : public ScreenCapturer {
std::vector<DisplayInfo> 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,
@@ -9,6 +9,7 @@
#include <display_stream_id.h>
#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<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,
[[maybe_unused]] LPRECT lprc, LPARAM data) {
MONITORINFOEX monitor_info_;
@@ -2,6 +2,7 @@
#include <Windows.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstring>
@@ -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<std::mutex> 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<std::mutex> 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<std::mutex> lock(impl_mutex_);
if (!impl_) return -1;
return impl_->Resume(monitor_index);
}
int ScreenCapturerWin::SwitchTo(int monitor_index) {
std::lock_guard<std::mutex> impl_lock(impl_mutex_);
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) {
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<std::mutex> 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<DisplayInfo> ScreenCapturerWin::GetDisplayInfoList() {
return canonical_displays_;
}
int ScreenCapturerWin::GetCurrentMonitorIndex() const {
return monitor_index_.load(std::memory_order_relaxed);
}
void ScreenCapturerWin::BuildCanonicalFromImpl() {
std::lock_guard<std::mutex> lock(alias_mutex_);
handle_to_canonical_index_.clear();
stream_id_alias_.clear();
canonical_displays_ = impl_->GetDisplayInfoList();
backend_to_canonical_.clear();
std::unordered_map<std::string, size_t> 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<int>(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<std::mutex> 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<ScreenCapturerDxgi*>(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<std::string, size_t> 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<int>(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<size_t>(canonical_index);
if (di.handle)
handle_to_canonical_index_[di.handle] =
static_cast<size_t>(canonical_index);
canonical = di;
if (canonical.name.empty()) {
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() {
@@ -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<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() {
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(
@@ -49,6 +49,7 @@ class ScreenCapturerWin : public ScreenCapturer {
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> 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<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;
bool IsBackendEnabled(ScreenCaptureMethod method) const {
return capture_method_ == ScreenCaptureMethod::Auto ||
@@ -73,15 +78,16 @@ class ScreenCapturerWin : public ScreenCapturer {
std::unordered_map<std::string, std::string> stream_id_alias_;
std::mutex alias_mutex_;
std::vector<DisplayInfo> canonical_displays_;
std::vector<int> backend_to_canonical_;
std::atomic<bool> invalid_stream_id_logged_{false};
std::atomic<bool> native_output_logged_{false};
std::atomic<bool> native_output_error_logged_{false};
std::atomic<bool> running_{false};
std::atomic<ULONGLONG> last_capture_progress_tick_{0};
std::atomic<bool> paused_{false};
std::atomic<bool> show_cursor_{true};
bool applied_show_cursor_ = true;
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> secure_desktop_capture_active_{false};
std::atomic<bool> post_secure_desktop_waiting_for_frame_{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_;
void BuildCanonicalFromImpl();
void RebuildAliasesFromImpl();
void RebuildAliasesFromImpl(bool preserve_backend_slots = false);
void RestoreMonitor(int monitor_index);
bool TryStartBackend(std::unique_ptr<ScreenCapturer> 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,
@@ -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.
// 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<DisplayInfo> 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