mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-10 17:49:20 +08:00
[fix] improve Windows secure desktop capture and input transitions
This commit is contained in:
@@ -408,6 +408,7 @@ SecureDesktopServiceStatus ReadSecureDesktopServiceStatus() {
|
||||
}
|
||||
|
||||
status.active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
|
||||
status.consent_ui_visible = json.value("consent_ui_visible", false);
|
||||
status.helper_running = json.value("secure_input_helper_running", false);
|
||||
status.helper_process_id = json.value("secure_input_helper_pid", 0u);
|
||||
status.interactive_stage = json.value("interactive_stage", std::string());
|
||||
@@ -425,11 +426,7 @@ SecureDesktopServiceStatus ReadSecureDesktopServiceStatus() {
|
||||
|
||||
SecureDesktopServiceStatus QuerySecureDesktopServiceStatus() {
|
||||
auto status = ReadSecureDesktopServiceStatus();
|
||||
if (!status.service_available && IsCurrentSessionUserDesktopActive()) {
|
||||
status.desktop_state_known = true;
|
||||
status.capture_active = false;
|
||||
status.interactive_stage = "user-desktop";
|
||||
}
|
||||
if (IsCurrentSessionUserDesktopActive()) status.OnUserDesktopObserved();
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -445,10 +442,11 @@ bool QuerySecureDesktopHelperCommand(DWORD session_id,
|
||||
GetCrossDeskSecureInputHelperPipeName(session_id);
|
||||
std::string error;
|
||||
DWORD code = 0;
|
||||
// The legacy single-frame path includes a full GDI capture and a large
|
||||
// response. Give it a separate deadline from the small control messages.
|
||||
// Starting may have to join/release the old GPU producer first. Its reply
|
||||
// cannot use the short deadline intended for lightweight control messages.
|
||||
const DWORD timeout_ms =
|
||||
command.rfind(kCrossDeskSecureInputCaptureCommandPrefix, 0) == 0
|
||||
(command.rfind(kCrossDeskSecureInputCaptureCommandPrefix, 0) == 0 ||
|
||||
command.rfind(kCrossDeskSecureInputCaptureStartCommandPrefix, 0) == 0)
|
||||
? 1000
|
||||
: kSecureDesktopHelperPipeTimeoutMs;
|
||||
const bool ok = QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms,
|
||||
@@ -579,8 +577,6 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
0, std::memory_order_relaxed);
|
||||
const ULONGLONG elapsed_ms =
|
||||
start_tick == 0 ? 0 : GetTickCount64() - start_tick;
|
||||
post_secure_desktop_drop_logged_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
LOG_INFO(
|
||||
"Windows capturer first normal frame after secure desktop: "
|
||||
"reported_stream='{}', mapped_stream='{}', size={}x{}, bytes={}, "
|
||||
@@ -804,7 +800,6 @@ void ScreenCapturerWin::ResetPostSecureDesktopState() {
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_waiting_for_frame_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||
SharedSecureDesktopCursorState().SetActive(false);
|
||||
}
|
||||
@@ -1237,6 +1232,7 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
|
||||
|
||||
LOG_INFO("Windows capturer: restarting capture backend after secure desktop");
|
||||
impl_->Stop();
|
||||
last_capture_progress_tick_.store(GetTickCount64(), std::memory_order_relaxed);
|
||||
int ret = impl_->Start(show_cursor);
|
||||
if (ret == 0) {
|
||||
applied_show_cursor_ = show_cursor;
|
||||
@@ -1403,7 +1399,7 @@ void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) {
|
||||
target_session_id = secure_shared_session_id_;
|
||||
}
|
||||
|
||||
if (secure_shared_capture_started_ &&
|
||||
if ((secure_shared_capture_started_ || secure_shared_start_pending_) &&
|
||||
target_session_id != 0xFFFFFFFF) {
|
||||
std::vector<uint8_t> response;
|
||||
std::string error_message;
|
||||
@@ -1414,15 +1410,9 @@ void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) {
|
||||
|
||||
CloseSecureDesktopSharedFrame();
|
||||
secure_shared_capture_started_ = false;
|
||||
secure_shared_start_pending_ = false;
|
||||
secure_shared_session_id_ = 0xFFFFFFFF;
|
||||
secure_shared_left_ = 0;
|
||||
secure_shared_top_ = 0;
|
||||
secure_shared_width_ = 0;
|
||||
secure_shared_height_ = 0;
|
||||
secure_shared_fps_ = 0;
|
||||
secure_shared_show_cursor_ = true;
|
||||
secure_shared_stage_.clear();
|
||||
secure_shared_desktop_.clear();
|
||||
secure_shared_configuration_ = {};
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::OpenSecureDesktopSharedFrame(DWORD session_id,
|
||||
@@ -1569,27 +1559,37 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
||||
return false;
|
||||
}
|
||||
|
||||
const SharedCaptureConfiguration configuration{left, top, width, height,
|
||||
show_cursor, fps};
|
||||
if (secure_shared_capture_started_ &&
|
||||
secure_shared_session_id_ == session_id &&
|
||||
secure_shared_left_ == left && secure_shared_top_ == top &&
|
||||
secure_shared_width_ == width && secure_shared_height_ == height &&
|
||||
secure_shared_stage_ == stage && secure_shared_desktop_ == desktop &&
|
||||
secure_shared_show_cursor_ == show_cursor && secure_shared_fps_ == fps &&
|
||||
secure_shared_configuration_ == configuration &&
|
||||
OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
if (secure_shared_session_id_ != session_id) {
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
} else {
|
||||
// capture-start performs the replacement atomically in the helper. Avoid
|
||||
// a separate stop round trip and keep retries idempotent after a timeout.
|
||||
CloseSecureDesktopSharedFrame();
|
||||
}
|
||||
secure_shared_capture_started_ = false;
|
||||
secure_shared_start_pending_ = true;
|
||||
secure_shared_session_id_ = session_id;
|
||||
|
||||
const std::string command =
|
||||
BuildSecureCaptureStartCommand(left, top, width, height, show_cursor, fps,
|
||||
stage, desktop);
|
||||
const std::string command = BuildSecureCaptureStartCommand(
|
||||
left, top, width, height, show_cursor, fps, stage, desktop);
|
||||
std::vector<uint8_t> response;
|
||||
if (!QuerySecureDesktopHelperCommand(session_id, command, &response,
|
||||
error_out)) {
|
||||
secure_shared_start_pending_ =
|
||||
error_out && IsPendingSecureCaptureStart(*error_out);
|
||||
return false;
|
||||
}
|
||||
|
||||
secure_shared_start_pending_ = false;
|
||||
Json json = Json::parse(response.begin(), response.end(), nullptr, false);
|
||||
if (json.is_discarded() || !json.value("ok", false)) {
|
||||
if (error_out != nullptr) {
|
||||
@@ -1600,14 +1600,7 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
||||
|
||||
secure_shared_capture_started_ = true;
|
||||
secure_shared_session_id_ = session_id;
|
||||
secure_shared_left_ = left;
|
||||
secure_shared_top_ = top;
|
||||
secure_shared_width_ = width;
|
||||
secure_shared_height_ = height;
|
||||
secure_shared_show_cursor_ = show_cursor;
|
||||
secure_shared_fps_ = fps;
|
||||
secure_shared_stage_ = stage;
|
||||
secure_shared_desktop_ = desktop;
|
||||
secure_shared_configuration_ = configuration;
|
||||
|
||||
if (!OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
||||
StopSecureDesktopSharedCapture(session_id);
|
||||
@@ -1628,6 +1621,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
ULONGLONG capture_stage_started_tick = 0;
|
||||
bool post_secure_restart_pending = false;
|
||||
bool desktop_was_unavailable = false;
|
||||
bool normal_backend_suspended = false;
|
||||
ULONGLONG post_secure_restart_deadline_tick = 0;
|
||||
ULONGLONG last_post_secure_restart_tick = 0;
|
||||
ULONGLONG next_cursor_update_tick = 0;
|
||||
@@ -1685,9 +1679,14 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
const auto frame_started = std::chrono::steady_clock::now();
|
||||
report_stats(false);
|
||||
if (auto sample = status_poller.Take()) {
|
||||
const auto previous = std::exchange(status, std::move(sample->status));
|
||||
max_status_ms = (std::max)(max_status_ms, sample->query_ms);
|
||||
auto sample = status_poller.Take();
|
||||
if (sample || (desktop_available && status.capture_active)) {
|
||||
auto latest = sample ? std::move(sample->status) : status;
|
||||
// Recheck when applying a queued reply, not only when querying. A local
|
||||
// unlock must never be undone by a delayed secure-desktop snapshot.
|
||||
if (desktop_available) latest.OnUserDesktopObserved();
|
||||
const auto previous = std::exchange(status, std::move(latest));
|
||||
if (sample) max_status_ms = (std::max)(max_status_ms, sample->query_ms);
|
||||
if (status.service_available != previous.service_available ||
|
||||
status.error != previous.error) {
|
||||
if (status.service_available) {
|
||||
@@ -1718,8 +1717,12 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
report_stats(true);
|
||||
// A restarted helper can leave our mapping/event handles alive, but
|
||||
// their old producer is gone. Explicitly establish a fresh stream.
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
frame_schedule.Reset(now);
|
||||
if (status.capture_active != previous.capture_active ||
|
||||
status.active_session_id != previous.active_session_id ||
|
||||
status.helper_process_id != previous.helper_process_id) {
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
frame_schedule.Reset(now);
|
||||
}
|
||||
const bool secure_capture_started =
|
||||
!previous.capture_active && status.capture_active;
|
||||
const bool secure_capture_ended =
|
||||
@@ -1734,8 +1737,6 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
post_secure_restart_pending = false;
|
||||
post_secure_desktop_waiting_for_frame_.store(
|
||||
false, std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||
} else if (secure_capture_ended) {
|
||||
post_secure_restart_pending = true;
|
||||
@@ -1744,8 +1745,6 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
last_post_secure_restart_tick = 0;
|
||||
post_secure_desktop_waiting_for_frame_.store(
|
||||
true, std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(now,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
@@ -1763,28 +1762,52 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
last_post_secure_restart_tick = 0;
|
||||
}
|
||||
|
||||
if ((!desktop_available || status.capture_active) &&
|
||||
!normal_backend_suspended) {
|
||||
// LockApp may return to Default while the session remains locked. Merely
|
||||
// dropping callbacks leaves a second DXGI/GDI/WGC pipeline capturing and
|
||||
// converting the same desktop alongside the helper. Stop it once, also
|
||||
// when local lock detection precedes the service/helper startup.
|
||||
std::lock_guard<std::mutex> lock(impl_mutex_);
|
||||
if (impl_) {
|
||||
impl_->Stop();
|
||||
normal_backend_suspended = true;
|
||||
LOG_INFO("Windows capturer: normal backend suspended for secure desktop");
|
||||
}
|
||||
}
|
||||
|
||||
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
CheckDisplayPresence(now);
|
||||
MaybeAdoptVirtualDisplay();
|
||||
CheckCaptureProgress(now);
|
||||
// Normal capture was intentionally unavailable on the secure desktop.
|
||||
// Do not schedule a second topology/stall rebuild during its recovery.
|
||||
if (!post_secure_restart_pending && desktop_available &&
|
||||
!normal_backend_suspended) {
|
||||
CheckDisplayPresence(now);
|
||||
MaybeAdoptVirtualDisplay();
|
||||
CheckCaptureProgress(now);
|
||||
}
|
||||
if (post_secure_restart_pending && desktop_available) {
|
||||
if (now >= post_secure_restart_deadline_tick) {
|
||||
LOG_WARN(
|
||||
"Windows capturer: capture backend restart after secure desktop "
|
||||
"timed out");
|
||||
post_secure_restart_pending = false;
|
||||
"still unavailable; retrying");
|
||||
// The normal backend is stopped. Retain recovery ownership instead
|
||||
// of falling back to a stall probe on a thread that is not running.
|
||||
post_secure_restart_deadline_tick =
|
||||
now + kPostSecureDesktopRestartTimeoutMs;
|
||||
} else if (last_post_secure_restart_tick == 0 ||
|
||||
now - last_post_secure_restart_tick >=
|
||||
kPostSecureDesktopRestartRetryMs) {
|
||||
last_post_secure_restart_tick = now;
|
||||
post_secure_restart_pending =
|
||||
!RestartCaptureBackendAfterSecureDesktop();
|
||||
if (!post_secure_restart_pending) normal_backend_suspended = false;
|
||||
if (!post_secure_restart_pending && privacy_)
|
||||
privacy_->ResumeAfterDesktopRecovery();
|
||||
}
|
||||
}
|
||||
if (!post_secure_restart_pending && now >= next_cursor_update_tick) {
|
||||
if (!post_secure_restart_pending && !normal_backend_suspended &&
|
||||
now >= next_cursor_update_tick) {
|
||||
next_cursor_update_tick = now + 500;
|
||||
ApplyCursorCaptureSetting();
|
||||
}
|
||||
@@ -1835,7 +1858,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
status.interactive_stage, status.interactive_desktop, show_cursor,
|
||||
shared_fps, &error_message);
|
||||
if (!shared_ready) {
|
||||
report_shared_error("start", error_message);
|
||||
if (!secure_shared_start_pending_) report_shared_error("start", error_message);
|
||||
frame_schedule.OnStartFailure(GetTickCount64());
|
||||
} else if (!was_started) {
|
||||
frame_schedule.Reset(GetTickCount64());
|
||||
@@ -1851,8 +1874,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
frame_schedule.OnSharedFrame(GetTickCount64());
|
||||
}
|
||||
|
||||
const bool frame_pending = shared_ready && !frame_delivered &&
|
||||
IsPendingSecureDesktopFrame(error_message);
|
||||
const bool frame_pending = secure_shared_start_pending_ ||
|
||||
(shared_ready && !frame_delivered && IsPendingSecureDesktopFrame(error_message));
|
||||
if (frame_pending) ++shared_waits;
|
||||
if (shared_ready && !frame_delivered && !frame_pending) {
|
||||
report_shared_error("read", error_message);
|
||||
@@ -1905,6 +1928,10 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
// The shared producer owns frame pacing. Waiting again here can miss the
|
||||
// next publication (and overwrite a pending frame) due to timer rounding.
|
||||
if (shared_ready && (frame_delivered || frame_pending)) continue;
|
||||
|
||||
const auto elapsed_ms =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - frame_started)
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "privacy_controller.h"
|
||||
// Relative: platform include directories stay private to the capture target.
|
||||
#include "../virtual_display/virtual_display_provisioner.h"
|
||||
#include "../service/shared_capture_configuration.h"
|
||||
#include "headless_recovery.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -117,7 +118,6 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
std::atomic<int> 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};
|
||||
std::atomic<ULONGLONG> post_secure_desktop_started_tick_{0};
|
||||
std::thread secure_capture_thread_;
|
||||
HANDLE secure_frame_mapping_ = nullptr;
|
||||
@@ -125,15 +125,9 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
uint8_t* secure_frame_view_ = nullptr;
|
||||
size_t secure_frame_view_size_ = 0;
|
||||
DWORD secure_shared_session_id_ = 0xFFFFFFFF;
|
||||
int secure_shared_left_ = 0;
|
||||
int secure_shared_top_ = 0;
|
||||
int secure_shared_width_ = 0;
|
||||
int secure_shared_height_ = 0;
|
||||
int secure_shared_fps_ = 0;
|
||||
bool secure_shared_show_cursor_ = true;
|
||||
std::string secure_shared_stage_;
|
||||
std::string secure_shared_desktop_;
|
||||
SharedCaptureConfiguration secure_shared_configuration_;
|
||||
bool secure_shared_capture_started_ = false;
|
||||
bool secure_shared_start_pending_ = false;
|
||||
std::shared_ptr<CapturedNv12FramePool> native_frame_pool_;
|
||||
|
||||
void BuildCanonicalFromImpl();
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-08
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SECURE_DESKTOP_DXGI_CAPTURE_H_
|
||||
#define _SECURE_DESKTOP_DXGI_CAPTURE_H_
|
||||
|
||||
#include <Windows.h>
|
||||
#include <d3d11.h>
|
||||
#include <dxgi1_2.h>
|
||||
#include <libyuv.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Owned by the SYSTEM helper's capture thread, after binding its input desktop.
|
||||
// Destroy before switching desktops. The uncomposited pixels stay valid on a
|
||||
// timeout, so a static lock screen and a moving separate cursor keep their
|
||||
// requested cadence without another GPU readback or a GDI screenshot.
|
||||
class SecureDesktopDxgiCapture {
|
||||
public:
|
||||
HRESULT Initialize(int left, int top, int width, int height) {
|
||||
if (width <= 0 || height <= 0) return E_INVALIDARG;
|
||||
Microsoft::WRL::ComPtr<IDXGIFactory1> factory;
|
||||
HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(factory.GetAddressOf()));
|
||||
if (FAILED(hr)) return hr;
|
||||
Microsoft::WRL::ComPtr<IDXGIAdapter1> adapter;
|
||||
for (UINT a = 0; SUCCEEDED(factory->EnumAdapters1(
|
||||
a, adapter.ReleaseAndGetAddressOf())); ++a) {
|
||||
Microsoft::WRL::ComPtr<IDXGIOutput> output;
|
||||
for (UINT o = 0; SUCCEEDED(adapter->EnumOutputs(
|
||||
o, output.ReleaseAndGetAddressOf())); ++o) {
|
||||
DXGI_OUTPUT_DESC desc{};
|
||||
if (FAILED(output->GetDesc(&desc)) || !desc.AttachedToDesktop) continue;
|
||||
const RECT& bounds = desc.DesktopCoordinates;
|
||||
if (left < bounds.left || top < bounds.top ||
|
||||
static_cast<int64_t>(left) + width > bounds.right ||
|
||||
static_cast<int64_t>(top) + height > bounds.bottom) continue;
|
||||
|
||||
hr = D3D11CreateDevice(
|
||||
adapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr,
|
||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT, nullptr, 0, D3D11_SDK_VERSION,
|
||||
device_.GetAddressOf(), nullptr, context_.GetAddressOf());
|
||||
if (FAILED(hr)) return hr;
|
||||
Microsoft::WRL::ComPtr<IDXGIOutput1> output1;
|
||||
hr = output.As(&output1);
|
||||
if (FAILED(hr)) return hr;
|
||||
hr = output1->DuplicateOutput(device_.Get(), duplication_.GetAddressOf());
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
output_desc_ = desc;
|
||||
offset_x_ = left - bounds.left;
|
||||
offset_y_ = top - bounds.top;
|
||||
DXGI_OUTDUPL_DESC duplication_desc{};
|
||||
duplication_->GetDesc(&duplication_desc);
|
||||
switch (duplication_desc.Rotation) {
|
||||
case DXGI_MODE_ROTATION_ROTATE90: rotation_ = libyuv::kRotate90; break;
|
||||
case DXGI_MODE_ROTATION_ROTATE180: rotation_ = libyuv::kRotate180; break;
|
||||
case DXGI_MODE_ROTATION_ROTATE270: rotation_ = libyuv::kRotate270; break;
|
||||
default: rotation_ = libyuv::kRotate0; break;
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
// Includes headless desktops and regions spanning more than one output.
|
||||
return DXGI_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
// S_FALSE means no first frame yet; the caller can use GDI until one arrives.
|
||||
// On any failure the caller must discard this instance, including its cache.
|
||||
HRESULT Capture() {
|
||||
updated_ = false;
|
||||
if (!duplication_) return E_UNEXPECTED;
|
||||
Microsoft::WRL::ComPtr<IDXGIResource> resource;
|
||||
DXGI_OUTDUPL_FRAME_INFO info{};
|
||||
const HRESULT hr = duplication_->AcquireNextFrame(0, &info, &resource);
|
||||
if (hr == DXGI_ERROR_WAIT_TIMEOUT) return pixels_.empty() ? S_FALSE : S_OK;
|
||||
if (FAILED(hr)) return hr;
|
||||
struct ReleaseFrame {
|
||||
IDXGIOutputDuplication* duplication;
|
||||
~ReleaseFrame() { duplication->ReleaseFrame(); }
|
||||
} release{duplication_.Get()};
|
||||
if (info.LastMouseUpdateTime.QuadPart != 0) {
|
||||
cursor_embedded_ = !info.PointerPosition.Visible;
|
||||
}
|
||||
// Pointer-only updates leave the desktop image unchanged.
|
||||
if (info.LastPresentTime.QuadPart == 0 && !pixels_.empty()) return S_OK;
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> texture;
|
||||
HRESULT result = resource.As(&texture);
|
||||
if (FAILED(result)) return result;
|
||||
D3D11_TEXTURE2D_DESC desc{};
|
||||
texture->GetDesc(&desc);
|
||||
const bool swap_axes = rotation_ == libyuv::kRotate90 ||
|
||||
rotation_ == libyuv::kRotate270;
|
||||
const UINT logical_width = swap_axes ? desc.Height : desc.Width;
|
||||
const UINT logical_height = swap_axes ? desc.Width : desc.Height;
|
||||
const RECT& bounds = output_desc_.DesktopCoordinates;
|
||||
if (desc.Format != DXGI_FORMAT_B8G8R8A8_UNORM ||
|
||||
logical_width != static_cast<UINT>(bounds.right - bounds.left) ||
|
||||
logical_height != static_cast<UINT>(bounds.bottom - bounds.top)) {
|
||||
return DXGI_ERROR_ACCESS_LOST;
|
||||
}
|
||||
if (!staging_) {
|
||||
desc.Usage = D3D11_USAGE_STAGING;
|
||||
desc.BindFlags = 0;
|
||||
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||
desc.MiscFlags = 0;
|
||||
result = device_->CreateTexture2D(&desc, nullptr, staging_.GetAddressOf());
|
||||
if (FAILED(result)) return result;
|
||||
}
|
||||
context_->CopyResource(staging_.Get(), texture.Get());
|
||||
D3D11_MAPPED_SUBRESOURCE mapped{};
|
||||
result = context_->Map(staging_.Get(), 0, D3D11_MAP_READ, 0, &mapped);
|
||||
if (FAILED(result)) return result;
|
||||
stride_ = static_cast<int>(logical_width) * 4;
|
||||
pixels_.resize(static_cast<size_t>(stride_) * logical_height);
|
||||
const int converted = libyuv::ARGBRotate(
|
||||
static_cast<const uint8_t*>(mapped.pData), static_cast<int>(mapped.RowPitch),
|
||||
pixels_.data(), stride_, static_cast<int>(desc.Width),
|
||||
static_cast<int>(desc.Height), rotation_);
|
||||
context_->Unmap(staging_.Get(), 0);
|
||||
updated_ = converted == 0;
|
||||
return converted == 0 ? S_OK : E_FAIL;
|
||||
}
|
||||
|
||||
const uint8_t* pixels() const {
|
||||
return pixels_.data() + static_cast<size_t>(offset_y_) * stride_ + offset_x_ * 4;
|
||||
}
|
||||
int stride() const { return stride_; }
|
||||
bool updated() const { return updated_; }
|
||||
const DXGI_OUTPUT_DESC& output() const { return output_desc_; }
|
||||
void SetSoftwareCursor(bool software) { cursor_embedded_ = software; }
|
||||
bool CursorEmbedded(const CURSORINFO& cursor) const {
|
||||
return cursor_embedded_ && (cursor.flags & CURSOR_SHOWING) != 0 &&
|
||||
MonitorFromPoint(cursor.ptScreenPos, MONITOR_DEFAULTTONULL) ==
|
||||
output_desc_.Monitor;
|
||||
}
|
||||
|
||||
private:
|
||||
Microsoft::WRL::ComPtr<ID3D11Device> device_;
|
||||
Microsoft::WRL::ComPtr<ID3D11DeviceContext> context_;
|
||||
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
|
||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging_;
|
||||
DXGI_OUTPUT_DESC output_desc_{};
|
||||
libyuv::RotationMode rotation_ = libyuv::kRotate0;
|
||||
int offset_x_ = 0, offset_y_ = 0, stride_ = 0;
|
||||
bool cursor_embedded_ = false;
|
||||
bool updated_ = false;
|
||||
std::vector<uint8_t> pixels_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _SECURE_DESKTOP_DXGI_CAPTURE_H_
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-08
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SECURE_DESKTOP_FRAME_PACER_H_
|
||||
#define _SECURE_DESKTOP_FRAME_PACER_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Sleep's system-tick rounding can turn a 33ms frame into a 47ms frame.
|
||||
// Request precise waits only on this capture thread, without changing the
|
||||
// machine's timer resolution. Older Windows versions use a regular timer.
|
||||
class SecureDesktopFramePacer {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
explicit SecureDesktopFramePacer(int fps)
|
||||
: interval_(std::chrono::nanoseconds(
|
||||
1000000000LL / (std::clamp)(fps > 0 ? fps : 30, 1, 1000))) {
|
||||
constexpr DWORD kHighResolution = 0x00000002;
|
||||
timer_ = CreateWaitableTimerExW(nullptr, nullptr, kHighResolution,
|
||||
TIMER_MODIFY_STATE | SYNCHRONIZE);
|
||||
if (!timer_) timer_ = CreateWaitableTimerW(nullptr, FALSE, nullptr);
|
||||
}
|
||||
~SecureDesktopFramePacer() {
|
||||
if (timer_) CloseHandle(timer_);
|
||||
}
|
||||
SecureDesktopFramePacer(const SecureDesktopFramePacer&) = delete;
|
||||
SecureDesktopFramePacer& operator=(const SecureDesktopFramePacer&) = delete;
|
||||
|
||||
void Wait(Clock::time_point frame_started) {
|
||||
const auto now = Clock::now();
|
||||
// Keep fractional frame periods and compensate for small scheduler delays.
|
||||
// After a slow frame, resume now instead of issuing a burst of old frames.
|
||||
if (deadline_ == Clock::time_point{}) deadline_ = frame_started;
|
||||
deadline_ += interval_;
|
||||
if (deadline_ <= now) {
|
||||
deadline_ = now;
|
||||
return;
|
||||
}
|
||||
LARGE_INTEGER due{};
|
||||
const auto ticks = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
deadline_ - now).count() / 100;
|
||||
due.QuadPart = -(ticks > 0 ? ticks : 1);
|
||||
if (timer_ && SetWaitableTimer(timer_, &due, 0, nullptr, nullptr, FALSE) &&
|
||||
WaitForSingleObject(timer_, INFINITE) == WAIT_OBJECT_0) return;
|
||||
std::this_thread::sleep_until(deadline_);
|
||||
}
|
||||
|
||||
private:
|
||||
HANDLE timer_ = nullptr;
|
||||
Clock::duration interval_;
|
||||
Clock::time_point deadline_{};
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _SECURE_DESKTOP_FRAME_PACER_H_
|
||||
@@ -12,6 +12,12 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline bool IsPendingSecureCaptureStart(const std::string& error) {
|
||||
// Once a command was written, a lost response does not mean startup failed.
|
||||
return error.rfind("pipe_read_failed:", 0) == 0 ||
|
||||
error.rfind("pipe_write_failed:", 0) == 0;
|
||||
}
|
||||
|
||||
inline bool IsPendingSecureDesktopFrame(const std::string& error) {
|
||||
return error == "frame_wait_timeout" ||
|
||||
error == "shared_frame_write_in_progress" ||
|
||||
@@ -47,4 +53,4 @@ class SecureDesktopFrameSchedule {
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-08
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SECURE_DESKTOP_NV12_CACHE_H_
|
||||
#define _SECURE_DESKTOP_NV12_CACHE_H_
|
||||
|
||||
#include <libyuv.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// One cache per capture desktop/resource lifetime. DXGI can report a pointer
|
||||
// update or timeout without changing any pixels. Keep publishing those frames
|
||||
// (and fresh cursor metadata) without converting the entire desktop again.
|
||||
class SecureDesktopNv12Cache {
|
||||
public:
|
||||
bool Convert(const uint8_t* pixels, int stride, int width, int height,
|
||||
bool pixels_changed) {
|
||||
updated_ = false;
|
||||
if (!pixels || width <= 0 || height <= 0 || (width & 1) || (height & 1) ||
|
||||
stride < static_cast<int64_t>(width) * 4) {
|
||||
valid_ = false;
|
||||
return false;
|
||||
}
|
||||
if (valid_ && !pixels_changed && width == width_ && height == height_)
|
||||
return true;
|
||||
|
||||
valid_ = false;
|
||||
const size_t luma_size = static_cast<size_t>(width) * height;
|
||||
pixels_.resize(luma_size + luma_size / 2);
|
||||
if (libyuv::ARGBToNV12(pixels, stride, pixels_.data(), width,
|
||||
pixels_.data() + luma_size, width, width,
|
||||
height) != 0)
|
||||
return false;
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
valid_ = updated_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool updated() const { return updated_; }
|
||||
const std::vector<uint8_t>& pixels() const { return pixels_; }
|
||||
|
||||
private:
|
||||
bool valid_ = false;
|
||||
bool updated_ = false;
|
||||
int width_ = 0, height_ = 0;
|
||||
std::vector<uint8_t> pixels_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _SECURE_DESKTOP_NV12_CACHE_H_
|
||||
@@ -23,6 +23,7 @@ struct SecureDesktopServiceStatus {
|
||||
bool service_available = false;
|
||||
bool desktop_state_known = false;
|
||||
bool capture_active = false;
|
||||
bool consent_ui_visible = false;
|
||||
bool helper_running = false;
|
||||
uint32_t helper_process_id = 0;
|
||||
uint32_t active_session_id = 0xFFFFFFFF;
|
||||
@@ -31,6 +32,16 @@ struct SecureDesktopServiceStatus {
|
||||
std::string interactive_desktop;
|
||||
std::string error;
|
||||
|
||||
void OnUserDesktopObserved() {
|
||||
if (consent_ui_visible) return;
|
||||
desktop_state_known = true;
|
||||
capture_active = false;
|
||||
helper_running = false;
|
||||
helper_process_id = 0;
|
||||
interactive_stage = "user-desktop";
|
||||
interactive_desktop.clear();
|
||||
}
|
||||
|
||||
void UpdateFrom(SecureDesktopServiceStatus latest) {
|
||||
if (latest.service_available || latest.desktop_state_known) {
|
||||
*this = std::move(latest);
|
||||
@@ -108,4 +119,4 @@ class SecureDesktopStatusPoller {
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user