mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-16 08:43:17 +08:00
[fix] keep Windows display slots and input mapping stable across hotplug
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user