[feat] add Windows privacy screen and prefer DXGI capture

This commit is contained in:
dijunkun
2026-09-11 16:11:50 +08:00
parent 4f200095f4
commit ceb0a1654a
57 changed files with 4208 additions and 217 deletions
@@ -3,6 +3,13 @@
namespace crossdesk {
void GuiRuntime::ResetRemoteServiceStatus(RemoteSession& props) {
{
std::lock_guard lock(props.privacy_status_mutex_);
props.privacy_status_ = {};
props.privacy_status_received_ = false;
props.privacy_command_pending_ = false;
props.privacy_status_tick_ = 0;
}
props.remote_service_status_received_ = false;
props.remote_service_available_ = false;
props.remote_interactive_stage_.clear();
@@ -114,6 +114,10 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
bool force_broadcast = false;
if (pending_windows_service_sas_.exchange(false, std::memory_order_relaxed)) {
if (privacy_.Engaged()) {
privacy_.Fail("Queued security shortcut cancelled during privacy mode; remote operation paused");
return;
}
const std::string response =
QueryCrossDeskService("sas", kWindowsServiceSasTimeoutMs);
auto json = nlohmann::json::parse(response, nullptr, false);
@@ -1,16 +1,20 @@
#include "mouse_controller.h"
#include <Windows.h>
#include <remote_action.h>
#include <Windows.h>
#include <algorithm>
#include <cmath>
#include "../../windows_thread_dpi.h"
#include "rd_log.h"
#include "windows_input_marker.h"
namespace crossdesk {
PlatformMouseController::PlatformMouseController() {}
PlatformMouseController::~PlatformMouseController() {}
PlatformMouseController::~PlatformMouseController() { Destroy(); }
int PlatformMouseController::Init(std::vector<DisplayInfo> display_info_list) {
display_info_list_ = display_info_list;
@@ -18,10 +22,27 @@ int PlatformMouseController::Init(std::vector<DisplayInfo> display_info_list) {
return 0;
}
int PlatformMouseController::Destroy() { return 0; }
int PlatformMouseController::Destroy() { return ReleasePressedButtons(); }
int PlatformMouseController::ReleasePressedButtons() {
const unsigned pressed = pressed_buttons_.load();
if (!pressed) return 0;
INPUT input{};
input.type = INPUT_MOUSE;
input.mi.dwExtraInfo = kInjectedMouseInputMarker;
if (pressed & 1) input.mi.dwFlags |= MOUSEEVENTF_LEFTUP;
if (pressed & 2) input.mi.dwFlags |= MOUSEEVENTF_RIGHTUP;
if (pressed & 4) input.mi.dwFlags |= MOUSEEVENTF_MIDDLEUP;
if (SendInput(1, &input, sizeof(INPUT)) != 1) {
LOG_WARN("Release remote mouse buttons failed, error={}", GetLastError());
return -1;
}
pressed_buttons_.fetch_and(~pressed);
return 0;
}
int PlatformMouseController::SendMouseCommand(RemoteAction remote_action,
int display_index) {
int display_index) {
if (display_index < 0 ||
display_index >= static_cast<int>(display_info_list_.size())) {
LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
@@ -30,69 +51,83 @@ int PlatformMouseController::SendMouseCommand(RemoteAction remote_action,
}
INPUT ip = {0};
ScopedWindowsPhysicalCoordinates physical_coordinates;
if (remote_action.type == ControlType::mouse) {
ip.type = INPUT_MOUSE;
ip.mi.dx =
(LONG)(remote_action.m.x * display_info_list_[display_index].width) +
display_info_list_[display_index].left;
ip.mi.dy =
(LONG)(remote_action.m.y * display_info_list_[display_index].height) +
display_info_list_[display_index].top;
if (remote_action.type != ControlType::mouse) return 0;
if (!std::isfinite(remote_action.m.x) || !std::isfinite(remote_action.m.y))
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;
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;
switch (remote_action.m.flag) {
case MouseFlag::left_down:
ip.mi.dwFlags = MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_ABSOLUTE;
break;
case MouseFlag::left_up:
ip.mi.dwFlags = MOUSEEVENTF_LEFTUP | MOUSEEVENTF_ABSOLUTE;
break;
case MouseFlag::right_down:
ip.mi.dwFlags = MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_ABSOLUTE;
break;
case MouseFlag::right_up:
ip.mi.dwFlags = MOUSEEVENTF_RIGHTUP | MOUSEEVENTF_ABSOLUTE;
break;
case MouseFlag::middle_down:
ip.mi.dwFlags = MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_ABSOLUTE;
break;
case MouseFlag::middle_up:
ip.mi.dwFlags = MOUSEEVENTF_MIDDLEUP | MOUSEEVENTF_ABSOLUTE;
break;
case MouseFlag::wheel_vertical:
ip.mi.dwFlags = MOUSEEVENTF_WHEEL;
ip.mi.mouseData = remote_action.m.s * 120;
break;
case MouseFlag::wheel_horizontal:
ip.mi.dwFlags = MOUSEEVENTF_HWHEEL;
ip.mi.mouseData = remote_action.m.s * 120;
break;
default:
ip.mi.dwFlags = MOUSEEVENTF_MOVE;
break;
}
ip.mi.time = 0;
if (!SetCursorPos(ip.mi.dx, ip.mi.dy)) {
LOG_WARN("SetCursorPos failed for mouse x={}, y={}, flag={}, err={}",
ip.mi.dx, ip.mi.dy, static_cast<int>(remote_action.m.flag),
GetLastError());
return -1;
}
if (ip.mi.dwFlags != MOUSEEVENTF_MOVE) {
UINT sent = SendInput(1, &ip, sizeof(INPUT));
if (sent != 1) {
LOG_WARN(
"SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}",
ip.mi.dx, ip.mi.dy, remote_action.m.s,
static_cast<int>(remote_action.m.flag), GetLastError());
return -1;
}
}
switch (remote_action.m.flag) {
case MouseFlag::left_down:
ip.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
break;
case MouseFlag::left_up:
ip.mi.dwFlags = MOUSEEVENTF_LEFTUP;
break;
case MouseFlag::right_down:
ip.mi.dwFlags = MOUSEEVENTF_RIGHTDOWN;
break;
case MouseFlag::right_up:
ip.mi.dwFlags = MOUSEEVENTF_RIGHTUP;
break;
case MouseFlag::middle_down:
ip.mi.dwFlags = MOUSEEVENTF_MIDDLEDOWN;
break;
case MouseFlag::middle_up:
ip.mi.dwFlags = MOUSEEVENTF_MIDDLEUP;
break;
case MouseFlag::wheel_vertical:
ip.mi.dwFlags = MOUSEEVENTF_WHEEL;
ip.mi.mouseData = remote_action.m.s * 120;
break;
case MouseFlag::wheel_horizontal:
ip.mi.dwFlags = MOUSEEVENTF_HWHEEL;
ip.mi.mouseData = remote_action.m.s * 120;
break;
default:
break;
}
const int virtual_left = GetSystemMetrics(SM_XVIRTUALSCREEN);
const int virtual_top = GetSystemMetrics(SM_YVIRTUALSCREEN);
const int virtual_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
const int virtual_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
if (virtual_width <= 0 || virtual_height <= 0) {
LOG_WARN("Remote mouse skipped: invalid virtual desktop geometry");
return -1;
}
// 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);
ip.mi.dwFlags |=
MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK;
ip.mi.dwExtraInfo = kInjectedMouseInputMarker;
const UINT sent = SendInput(1, &ip, sizeof(INPUT));
if (sent != 1) {
LOG_WARN("SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}",
ip.mi.dx, ip.mi.dy, remote_action.m.s,
static_cast<int>(remote_action.m.flag), GetLastError());
return -1;
}
if (ip.mi.dwFlags & MOUSEEVENTF_LEFTDOWN) pressed_buttons_.fetch_or(1);
if (ip.mi.dwFlags & MOUSEEVENTF_RIGHTDOWN) pressed_buttons_.fetch_or(2);
if (ip.mi.dwFlags & MOUSEEVENTF_MIDDLEDOWN) pressed_buttons_.fetch_or(4);
if (ip.mi.dwFlags & MOUSEEVENTF_LEFTUP) pressed_buttons_.fetch_and(~1u);
if (ip.mi.dwFlags & MOUSEEVENTF_RIGHTUP) pressed_buttons_.fetch_and(~2u);
if (ip.mi.dwFlags & MOUSEEVENTF_MIDDLEUP) pressed_buttons_.fetch_and(~4u);
return 0;
}
} // namespace crossdesk
@@ -8,6 +8,7 @@
#define _MOUSE_CONTROLLER_H_
#include <vector>
#include <atomic>
#include <remote_action.h>
@@ -25,9 +26,11 @@ 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);
int ReleasePressedButtons();
private:
std::vector<DisplayInfo> display_info_list_;
std::atomic<unsigned> pressed_buttons_{0};
};
} // namespace crossdesk
#endif
@@ -9,6 +9,7 @@ namespace crossdesk {
// keyboard input so the controller hook can ignore only its own injections
// while still accepting input from accessibility tools or remote sessions.
inline constexpr std::uintptr_t kInjectedKeyboardInputMarker = 0x4353444B;
inline constexpr std::uintptr_t kInjectedMouseInputMarker = 0x4353444D;
} // namespace crossdesk
@@ -0,0 +1,293 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-10
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include <Windows.h>
#include <dwmapi.h>
#include <algorithm>
#include <array>
#include <cwchar>
#include <vector>
#include "../../windows_thread_dpi.h"
#include "../privacy_band_ipc.h"
#include "privacy_cover_renderer.h"
namespace {
using namespace crossdesk;
constexpr wchar_t kWindowClass[] = L"CrossDesk.HighBandPrivacy";
constexpr DWORD kCoverStyles = WS_EX_TOPMOST | WS_EX_NOACTIVATE |
WS_EX_TOOLWINDOW | WS_EX_LAYERED |
WS_EX_TRANSPARENT;
HMODULE module = nullptr;
bool SameRect(const RECT& a, const RECT& b) {
return a.left == b.left && a.top == b.top && a.right == b.right &&
a.bottom == b.bottom;
}
class Windows {
public:
explicit Windows(PrivacyBandShared* shared) : shared_(shared) {}
~Windows() {
for (const auto& window : windows_)
DestroyWindow(reinterpret_cast<HWND>(window.hwnd));
if (atom_) UnregisterClassW(kWindowClass, module);
}
bool Fail(const wchar_t* operation, DWORD code = GetLastError()) {
wcsncpy_s(shared_->operation, operation, _TRUNCATE);
InterlockedExchange(&shared_->error, code ? code : ERROR_GEN_FAILURE);
return false;
}
bool Initialize() {
const HMODULE user = GetModuleHandleW(L"user32.dll");
create_ = reinterpret_cast<CreatePrivacyWindowInBand>(
GetProcAddress(user, "CreateWindowInBand"));
get_band_ = reinterpret_cast<GetPrivacyWindowBand>(
GetProcAddress(user, "GetWindowBand"));
if (!create_ || !get_band_)
return Fail(L"Resolve high-band APIs", ERROR_PROC_NOT_FOUND);
WNDCLASSEXW cls{sizeof(cls)};
cls.hInstance = module;
cls.lpfnWndProc = WindowProc;
cls.lpszClassName = kWindowClass;
cls.hbrBackground = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
atom_ = RegisterClassExW(&cls);
return atom_ ? Reconcile() : Fail(L"Register high-band window class");
}
bool Reconcile() {
std::vector<RECT> monitors;
if (!EnumDisplayMonitors(
nullptr, nullptr,
[](HMONITOR, HDC, LPRECT rect, LPARAM data) -> BOOL {
auto& result = *reinterpret_cast<std::vector<RECT>*>(data);
if (result.size() >= kPrivacyMaxMonitors) return FALSE;
result.push_back(*rect);
return TRUE;
},
reinterpret_cast<LPARAM>(&monitors)) ||
monitors.empty())
return Fail(L"Enumerate high-band displays");
bool changed = monitors.size() != windows_.size();
for (const auto& rect : monitors) {
const auto found =
std::find_if(windows_.begin(), windows_.end(),
[&](const auto& w) { return SameRect(w.rect, rect); });
if (found != windows_.end()) continue;
changed = true;
HWND hwnd = create_(kCoverStyles, atom_, L"CrossDesk Privacy", WS_POPUP,
rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, nullptr, nullptr, module,
this, kPrivacyWindowBand);
if (!hwnd) return Fail(L"CreateWindowInBand ZBID_ABOVELOCK_UX");
windows_.push_back({reinterpret_cast<uint64_t>(hwnd), rect});
if (!SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA))
return Fail(L"Make high-band cover opaque");
if (!SetWindowDisplayAffinity(hwnd, 0x11))
return Fail(L"Exclude high-band cover from capture");
const BOOL yes = TRUE;
const HRESULT peek = DwmSetWindowAttribute(hwnd, DWMWA_EXCLUDED_FROM_PEEK,
&yes, sizeof(yes));
if (FAILED(peek)) return Fail(L"Exclude high-band cover from Peek", peek);
if (!SetWindowPos(hwnd, nullptr, rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW) ||
!RedrawWindow(hwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW))
return Fail(L"Present high-band cover");
}
if (changed) {
const HRESULT result = DwmFlush();
if (FAILED(result))
return Fail(L"Flush high-band cover presentation", result);
// New areas are covered before old windows are removed.
for (auto it = windows_.begin(); it != windows_.end();) {
if (std::none_of(
monitors.begin(), monitors.end(),
[&](const auto& rect) { return SameRect(rect, it->rect); })) {
if (!DestroyWindow(reinterpret_cast<HWND>(it->hwnd)))
return Fail(L"Destroy obsolete high-band cover");
it = windows_.erase(it);
} else
++it;
}
}
if (!Validate()) return false;
if (changed) {
InterlockedIncrement(&shared_->sequence);
shared_->count = static_cast<uint32_t>(windows_.size());
std::copy(windows_.begin(), windows_.end(), shared_->windows);
InterlockedIncrement(&shared_->sequence);
}
return true;
}
bool Validate() {
if (paint_failed_) return false;
for (const auto& w : windows_) {
HWND hwnd = reinterpret_cast<HWND>(w.hwnd);
DWORD band = 0, affinity = 0, flags = 0, cloaked = 0;
BYTE alpha = 0;
RECT rect{};
if ((GetWindowLongPtrW(hwnd, GWL_EXSTYLE) & kCoverStyles) !=
kCoverStyles ||
!get_band_(hwnd, &band) || band != kPrivacyWindowBand ||
!GetWindowDisplayAffinity(hwnd, &affinity) || affinity != 0x11 ||
!GetLayeredWindowAttributes(hwnd, nullptr, &alpha, &flags) ||
alpha != 255 || flags != LWA_ALPHA || !IsWindowVisible(hwnd) ||
!GetWindowRect(hwnd, &rect) || !SameRect(rect, w.rect) ||
FAILED(DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked,
sizeof(cloaked))) ||
cloaked)
return Fail(L"High-band cover lost style/band/affinity/visibility",
ERROR_INVALID_STATE);
}
return true;
}
bool changed = false;
private:
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wp,
LPARAM lp) {
if (message == WM_NCCREATE) {
auto create = reinterpret_cast<CREATESTRUCTW*>(lp);
SetWindowLongPtrW(hwnd, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(create->lpCreateParams));
}
auto self =
reinterpret_cast<Windows*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
if (message == WM_MOUSEACTIVATE) return MA_NOACTIVATE;
if (message == WM_NCHITTEST) return HTTRANSPARENT;
if (message == WM_SETCURSOR) {
SetCursor(nullptr);
return TRUE;
}
if (message == WM_CLOSE) return 0;
if ((message == WM_DISPLAYCHANGE || message == WM_DPICHANGED) && self) {
self->changed = true;
InvalidateRect(hwnd, nullptr, TRUE);
return 0;
}
if (message == WM_PAINT) {
PAINTSTRUCT paint{};
HDC dc = BeginPaint(hwnd, &paint);
RECT rect{};
GetClientRect(hwnd, &rect);
if (self && !DrawPrivacyCover(dc, rect, PrivacyWindowDpi(hwnd), module,
self->shared_->unlock_hint)) {
self->paint_failed_ = true;
self->Fail(L"Paint privacy logo and unlock instructions");
}
EndPaint(hwnd, &paint);
return 0;
}
return DefWindowProcW(hwnd, message, wp, lp);
}
PrivacyBandShared* shared_;
bool paint_failed_ = false;
std::vector<PrivacyBandWindow> windows_;
ATOM atom_ = 0;
CreatePrivacyWindowInBand create_ = nullptr;
GetPrivacyWindowBand get_band_ = nullptr;
};
DWORD WINAPI Run(void*) {
wchar_t bootstrap[256]{};
if (!GetEnvironmentVariableW(kPrivacyBandBootstrap, bootstrap, 256))
return ERROR_INVALID_PARAMETER;
SetEnvironmentVariableW(kPrivacyBandBootstrap, nullptr);
std::array<HANDLE, 4> handles{};
wchar_t* item = bootstrap;
for (size_t i = 0; i < handles.size(); ++i) {
wchar_t* end = nullptr;
const auto value = wcstoull(item, &end, 10);
if (!value || end == item ||
(i + 1 == handles.size() ? *end != 0 : *end != L','))
return ERROR_INVALID_PARAMETER;
handles[i] = reinterpret_cast<HANDLE>(value);
item = end + 1;
}
HANDLE parent = handles[0], stop = handles[1], ready = handles[2],
mapping = handles[3];
auto shared = static_cast<PrivacyBandShared*>(MapViewOfFile(
mapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(PrivacyBandShared)));
if (!shared) return GetLastError();
if (shared->magic != kPrivacyBandMagic ||
shared->version != kPrivacyBandVersion ||
shared->size != sizeof(PrivacyBandShared) ||
shared->broker_pid != GetCurrentProcessId() ||
shared->parent_pid != GetProcessId(parent) ||
WaitForSingleObject(parent, 0) != WAIT_TIMEOUT) {
UnmapViewOfFile(shared);
return ERROR_INVALID_DATA;
}
DWORD exit_code = ERROR_SUCCESS;
{
ScopedWindowsPhysicalCoordinates dpi;
Windows windows(shared);
try {
bool healthy = dpi.active() && windows.Initialize();
const bool initialized = healthy;
if (!dpi.active()) windows.Fail(L"Set high-band thread DPI context");
InterlockedExchange64(&shared->heartbeat, GetTickCount64());
if (healthy) {
InterlockedExchange(&shared->error, ERROR_SUCCESS);
InterlockedExchange(&shared->active, 1);
}
SetEvent(ready);
auto last_check = GetTickCount64();
HANDLE watched[] = {parent, stop};
// Once enabled, retain surviving covers on a validation/layout fault.
// The parent pauses the session after reading the latched error. Only
// an explicit stop or parent death tears the remaining windows down.
while (initialized) {
const DWORD wait = MsgWaitForMultipleObjectsEx(
2, watched, 100, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
if (wait == WAIT_OBJECT_0 || wait == WAIT_OBJECT_0 + 1) break;
if (wait == WAIT_FAILED) {
windows.Fail(L"Wait for high-band lifecycle");
break;
}
MSG message{};
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
if (message.message == WM_QUIT) healthy = false;
TranslateMessage(&message);
DispatchMessageW(&message);
}
const auto now = GetTickCount64();
if (healthy && (windows.changed || now - last_check >= 250)) {
windows.changed = false;
last_check = now;
healthy = windows.Reconcile();
}
InterlockedExchange64(&shared->heartbeat, now);
}
} catch (...) {
windows.Fail(L"High-band window thread exception",
ERROR_UNHANDLED_EXCEPTION);
SetEvent(ready);
}
exit_code = InterlockedCompareExchange(&shared->error, 0, 0);
InterlockedExchange(&shared->active, 0);
}
UnmapViewOfFile(shared);
for (HANDLE handle : handles) CloseHandle(handle);
// This module runs only in the dedicated process created for this session.
// Exit outside DllMain/loader lock, after HWND destruction on their UI
// thread.
ExitProcess(exit_code);
return exit_code;
}
} // namespace
BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID) {
if (reason == DLL_PROCESS_ATTACH) {
module = instance;
// Keep thread notifications enabled: this module uses the static CRT.
HANDLE thread = CreateThread(nullptr, 0, Run, nullptr, 0, nullptr);
if (!thread) return FALSE;
CloseHandle(thread);
}
return TRUE;
}
@@ -0,0 +1,123 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include "privacy_cover_renderer.h"
#include <algorithm>
#include <array>
namespace crossdesk {
namespace {
constexpr size_t kInstructionLines = 2;
constexpr size_t kBrandLine = kInstructionLines;
struct PaintResources {
HDC dc;
int saved;
HICON logo = nullptr;
std::array<HFONT, kInstructionLines + 1> fonts{};
~PaintResources() {
// Deselect every owned GDI object before releasing it.
if (saved) RestoreDC(dc, saved);
for (auto font : fonts)
if (font) DeleteObject(font);
if (logo) DestroyIcon(logo);
}
};
bool Select(HDC dc, HGDIOBJ object) {
const auto previous = SelectObject(dc, object);
return previous && previous != HGDI_ERROR;
}
} // namespace
bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module,
const wchar_t* unlock_hint) {
if (!FillRect(dc, &rect, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH))))
return false;
const int width = rect.right - rect.left;
const int height = rect.bottom - rect.top;
if (width <= 0 || height <= 0) return false;
// Scale with the monitor DPI, shrinking the whole group on small displays.
// All coordinates remain in the owning window's physical-pixel context.
const double scale = (std::min)({dpi / 96.0, width / 640.0, height / 200.0});
const auto px = [scale](int value) {
return (std::max)(1, static_cast<int>(value * scale + 0.5));
};
const int margin = px(32);
const int text_width = px(398);
PaintResources resources{dc, SaveDC(dc)};
if (!resources.saved || !SetBkMode(dc, TRANSPARENT)) return false;
struct Line {
const wchar_t* text;
int size;
int weight;
COLORREF color;
RECT bounds{};
};
std::array<Line, kInstructionLines + 1> lines{{
{unlock_hint, 16, FW_NORMAL, RGB(181, 190, 204)},
{L"Ctrl + Alt + Shift + F12", 24, FW_SEMIBOLD, RGB(245, 247, 250)},
{L"CrossDesk", 14, FW_SEMIBOLD, RGB(245, 247, 250)},
}};
constexpr UINT flags = DT_LEFT | DT_WORDBREAK | DT_NOPREFIX;
for (size_t i = 0; i < lines.size(); ++i) {
auto& line = lines[i];
resources.fonts[i] =
CreateFontW(-px(line.size), 0, 0, 0, line.weight, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
ANTIALIASED_QUALITY, DEFAULT_PITCH, L"Microsoft YaHei UI");
if (!resources.fonts[i] || !Select(dc, resources.fonts[i])) return false;
line.bounds = {0, 0, text_width, 0};
if (!DrawTextW(dc, line.text, -1, &line.bounds, flags | DT_CALCRECT) ||
line.bounds.right > text_width)
return false;
}
const int content_height =
static_cast<int>(lines[0].bounds.bottom + lines[1].bounds.bottom) +
px(12);
// Match the icon width to the name; each side keeps its own height.
const int brand_gap = px(6);
const int brand_width = static_cast<int>(lines[kBrandLine].bounds.right);
const int logo_size = brand_width;
const int brand_height =
logo_size + brand_gap + static_cast<int>(lines[kBrandLine].bounds.bottom);
if (logo_size <= 0 ||
(std::max)(brand_height, content_height) + 2 * margin > height ||
brand_width + px(24) + text_width + 2 * margin > width)
return false;
resources.logo = static_cast<HICON>(LoadImageW(
module, L"IDI_CROSSDESK_LOGO", IMAGE_ICON, logo_size, logo_size, 0));
if (!resources.logo) return false;
const int brand_left = rect.left + margin;
const int left = brand_left + brand_width + px(24);
const int brand_top = rect.bottom - margin - brand_height;
int top = rect.bottom - margin - content_height;
if (!DrawIconEx(dc, brand_left, brand_top, resources.logo, logo_size,
logo_size, 0, nullptr, DI_NORMAL))
return false;
if (!Select(dc, resources.fonts[kBrandLine])) return false;
RECT brand_bounds{brand_left, brand_top + logo_size + brand_gap,
brand_left + brand_width, rect.bottom - margin};
SetTextColor(dc, lines[kBrandLine].color);
if (!DrawTextW(dc, lines[kBrandLine].text, -1, &brand_bounds,
DT_CENTER | DT_SINGLELINE | DT_NOPREFIX))
return false;
for (size_t i = 0; i < kInstructionLines; ++i) {
const auto& line = lines[i];
if (!Select(dc, resources.fonts[i])) return false;
RECT bounds{left, top, left + text_width, top + line.bounds.bottom};
SetTextColor(dc, line.color);
if (!DrawTextW(dc, line.text, -1, &bounds, flags)) return false;
top += line.bounds.bottom + px(12);
}
return true;
}
} // namespace crossdesk
@@ -0,0 +1,20 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_COVER_RENDERER_H_
#define _PRIVACY_COVER_RENDERER_H_
#include <Windows.h>
namespace crossdesk {
// Paints the local, opaque cover. The caller owns the HWND and its DPI context.
bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module,
const wchar_t* unlock_hint);
} // namespace crossdesk
#endif // _PRIVACY_COVER_RENDERER_H_
@@ -0,0 +1,83 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include <Windows.h>
#include <cerrno>
#include <cstdint>
#include <cwchar>
#include <limits>
#include "../privacy_cursor_api.h"
namespace {
HANDLE ParseHandle(const wchar_t* text) {
wchar_t* end = nullptr;
errno = 0;
const auto value = std::wcstoull(text, &end, 10);
if (errno || !text[0] || *end || text[0] == L'-' || value == 0 ||
value > (std::numeric_limits<uintptr_t>::max)())
return nullptr;
return reinterpret_cast<HANDLE>(static_cast<uintptr_t>(value));
}
DWORD FailureCode() {
const DWORD code = GetLastError();
return code ? code : ERROR_GEN_FAILURE;
}
} // namespace
int wmain(int argc, wchar_t** argv) {
if (argc != 5) return ERROR_INVALID_PARAMETER;
HANDLE parent = ParseHandle(argv[1]);
HANDLE stop = ParseHandle(argv[2]);
HANDLE ready = ParseHandle(argv[3]);
HANDLE mapping = ParseHandle(argv[4]);
// Observe the inherited process OBJECT, never a reused PID or a process name.
if (!parent || !stop || !ready || !mapping ||
WaitForSingleObject(parent, 0) != WAIT_TIMEOUT ||
WaitForSingleObject(stop, 0) != WAIT_TIMEOUT)
return ERROR_INVALID_HANDLE;
auto shared = static_cast<crossdesk::PrivacyCursorReady*>(MapViewOfFile(
mapping, FILE_MAP_WRITE, 0, 0, sizeof(crossdesk::PrivacyCursorReady)));
if (!shared) return static_cast<int>(FailureCode());
crossdesk::PrivacyCursorApi cursor;
DWORD error = ERROR_SUCCESS;
if (!cursor.Initialize() || !cursor.Hide()) error = FailureCode();
InterlockedExchange(&shared->error, static_cast<LONG>(error));
if (!SetEvent(ready)) error = FailureCode();
if (!error) {
HANDLE watched[] = {parent, stop};
for (;;) {
const DWORD result =
MsgWaitForMultipleObjects(2, watched, FALSE, 250, QS_ALLINPUT);
if (result == WAIT_OBJECT_0 || result == WAIT_OBJECT_0 + 1) break;
if (result == WAIT_OBJECT_0 + 2) {
MSG message{};
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
} else if (result != WAIT_TIMEOUT) {
error = FailureCode();
break;
}
if (!cursor.Hide()) {
error = FailureCode();
break;
}
}
}
// Also runs when the parent is force-terminated or crashes. The helper does
// not own any input hooks or privacy HWNDs and never changes magnification.
if (!cursor.RestoreAndClose()) error = FailureCode();
UnmapViewOfFile(shared);
CloseHandle(mapping);
CloseHandle(ready);
CloseHandle(stop);
CloseHandle(parent);
return static_cast<int>(error);
}
@@ -0,0 +1,708 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include <ShlObj.h>
#include <Windows.h>
#include <dwmapi.h>
#include <winternl.h>
#include <wtsapi32.h>
#include <algorithm>
#include <cstring>
#include <vector>
#include "../windows_thread_dpi.h"
#include "privacy_backend.h"
#include "privacy_band_guard.h"
#include "privacy_cursor_guard.h"
#include "privacy_input_guard.h"
#include "privacy_probe_pattern.h"
#include "privacy_probe_renderer.h"
#include "rd_log.h"
#ifndef WDA_EXCLUDEFROMCAPTURE
#define WDA_EXCLUDEFROMCAPTURE 0x00000011
#endif
namespace crossdesk {
namespace {
constexpr wchar_t kClassName[] = L"CrossDesk.PrivacyWindow";
constexpr UINT kEmergency = WM_APP + 71;
constexpr int kHotkeyId = 0x4353;
std::string Error(const char* operation, DWORD code = GetLastError()) {
const std::string result = std::string(operation) +
" failed (Windows error " + std::to_string(code) +
")";
LOG_ERROR("Privacy: {}", result);
return result;
}
bool DesktopName(HDESK desktop, wchar_t (&name)[256]) {
DWORD needed = 0;
return desktop && GetUserObjectInformationW(desktop, UOI_NAME, name,
sizeof(name), &needed);
}
bool SameRect(const RECT& a, const RECT& b) {
return a.left == b.left && a.top == b.top && a.right == b.right &&
a.bottom == b.bottom;
}
struct MonitorEnumeration {
std::vector<RECT> rects;
bool ok = true;
};
BOOL CALLBACK EnumMonitor(HMONITOR monitor, HDC, LPRECT, LPARAM data) {
auto& result = *reinterpret_cast<MonitorEnumeration*>(data);
MONITORINFO info{sizeof(info)};
if (!GetMonitorInfoW(monitor, &info)) {
Error("GetMonitorInfoW");
result.ok = false;
return FALSE;
}
result.rects.push_back(info.rcMonitor);
return TRUE;
}
class WindowsPrivacyBackend final : public PrivacyBackend {
public:
WindowsPrivacyBackend() {
wake_event_ = CreateEventW(nullptr, FALSE, FALSE, nullptr);
if (!wake_event_) wake_error_ = Error("CreateEventW privacy commands");
}
~WindowsPrivacyBackend() override {
Recover();
if (sink_) DestroyWindow(sink_);
if (registered_) UnregisterClassW(kClassName, GetModuleHandleW(nullptr));
if (wake_event_) CloseHandle(wake_event_);
}
PrivacyCapabilities Query() override {
if (!wake_event_) return {false, false, wake_error_};
using RtlGetVersionFn = LONG(WINAPI*)(PRTL_OSVERSIONINFOW);
const auto rtl = reinterpret_cast<RtlGetVersionFn>(
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetVersion"));
RTL_OSVERSIONINFOW version{};
version.dwOSVersionInfoSize = sizeof(version);
if (!rtl || rtl(&version) != 0 || version.dwMajorVersion < 10 ||
version.dwBuildNumber < 19041) {
return {false, false, "Requires Windows 10 2004 (build 19041) or later"};
}
BOOL composed = FALSE;
if (FAILED(DwmIsCompositionEnabled(&composed)) || !composed) {
return {false, false, "Desktop composition is unavailable"};
}
if (GetSystemMetrics(SM_REMOTESESSION)) {
return {false, false, "Privacy screen requires a local console session"};
}
if (!IsWindowsPrivacyDesktopAvailable()) {
return {false, false,
"Privacy screen requires the unlocked Default desktop"};
}
QUERY_USER_NOTIFICATION_STATE shell_state{};
if (FAILED(SHQueryUserNotificationState(&shell_state)) ||
shell_state == QUNS_RUNNING_D3D_FULL_SCREEN) {
return {false, false,
"Exclusive fullscreen or unknown display presentation cannot "
"guarantee privacy coverage"};
}
std::string high_band_error;
if (!PrivacyBandGuard::Available(high_band_error))
return {false, false, high_band_error};
return {true, true,
"High-band privacy; live capture exclusion will be verified"};
}
bool Enable(bool block_input, const PrivacyScreenText& text,
std::string& error) override {
const auto caps = Query();
if (!caps.overlay) {
error = caps.reason;
return false;
}
if (!Initialize(error)) return false;
emergency_ = false;
health_error_.clear();
if (!RegisterHotKey(sink_, kHotkeyId,
MOD_CONTROL | MOD_ALT | MOD_SHIFT | MOD_NOREPEAT,
VK_F12)) {
error = Error("RegisterHotKey Ctrl+Alt+Shift+F12");
return false;
}
hotkey_ = true;
if (!cursor_guard_.Start(error)) return false;
block_input_ = block_input;
if (block_input && !input_guard_.Start(sink_, kEmergency, error))
return false;
if (!band_guard_.Start(text, error)) return false;
if (!ReconcileMonitors(error)) return false;
event_owner_ = this;
for (DWORD event : {EVENT_OBJECT_SHOW, EVENT_OBJECT_REORDER,
EVENT_OBJECT_LOCATIONCHANGE}) {
auto hook = SetWinEventHook(event, event, nullptr, WindowEvent, 0, 0,
WINEVENT_OUTOFCONTEXT);
if (!hook) {
error = Error("Watch privacy window coverage changes");
return false;
}
window_events_.push_back(hook);
}
return true;
}
void Recover() override {
for (auto& hook : window_events_) {
if (UnhookWinEvent(hook))
hook = nullptr;
else
Error("Unhook privacy window events");
}
window_events_.erase(
std::remove(window_events_.begin(), window_events_.end(), nullptr),
window_events_.end());
if (window_events_.empty()) event_owner_ = nullptr;
// Remove verification panels before exposing the local desktop.
for (auto& window : windows_) {
Destroy(window.probe);
}
std::string band_error;
band_guard_.Stop(band_error);
for (auto& window : windows_) {
if (band_guard_.stopped()) window.cover = nullptr;
}
windows_.erase(
std::remove_if(windows_.begin(), windows_.end(),
[](const auto& w) { return !w.cover && !w.probe; }),
windows_.end());
input_guard_.Stop();
block_input_ = false;
if (hotkey_) {
if (UnregisterHotKey(sink_, kHotkeyId) ||
GetLastError() == ERROR_HOTKEY_NOT_REGISTERED)
hotkey_ = false;
else
Error("UnregisterHotKey");
}
// The sink lives across sessions. Do not let an already-posted emergency
// command from a closed session cancel a subsequent connection.
if (sink_) {
MSG discarded{};
while (PeekMessageW(&discarded, sink_, WM_HOTKEY, WM_HOTKEY, PM_REMOVE)) {
}
while (
PeekMessageW(&discarded, sink_, kEmergency, kEmergency, PM_REMOVE)) {
}
}
last_check_ = 0;
last_health_ = {};
last_obstruction_ = nullptr;
last_obstruction_log_ = 0;
layout_changed_ = false;
coverage_dirty_ = false;
emergency_ = false;
health_error_.clear();
std::string cursor_error;
cursor_guard_.Stop(cursor_error);
}
bool IsRecovered() const override {
return band_guard_.stopped() && windows_.empty() &&
window_events_.empty() && input_guard_.stopped() && !hotkey_ &&
cursor_guard_.stopped();
}
void Wake() override {
if (wake_event_) SetEvent(wake_event_);
}
void WaitForEvents(unsigned timeout_ms) override {
const DWORD result = MsgWaitForMultipleObjectsEx(
wake_event_ ? 1 : 0, wake_event_ ? &wake_event_ : nullptr, timeout_ms,
QS_ALLINPUT, MWMO_INPUTAVAILABLE);
if (result == WAIT_FAILED)
health_error_ = Error("MsgWaitForMultipleObjectsEx privacy thread");
}
PrivacyHealth Poll(bool force_check = false) override {
MSG message{};
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
if (message.message == WM_QUIT)
health_error_ =
"Privacy message thread is exiting; remote operation paused";
TranslateMessage(&message);
DispatchMessageW(&message);
}
if (emergency_) {
emergency_ = false;
return {{}, true};
}
if (block_input_) {
std::string error;
if (!input_guard_.Healthy(error)) return {error, false};
}
if (windows_.empty()) return {health_error_, false};
// Input messages wake the thread immediately; expensive health queries
// must retain their own cadence instead of running once per input event.
if (!force_check && !layout_changed_ &&
GetTickCount64() - last_check_ < 100) {
if (coverage_dirty_ && last_health_.failure.empty()) {
coverage_dirty_ = false;
last_health_ = CheckCoverage();
}
return last_health_;
}
coverage_dirty_ = false;
last_check_ = GetTickCount64();
last_health_ = CheckHealth();
return last_health_;
}
private:
PrivacyHealth CheckHealth() {
if (!health_error_.empty()) return {health_error_, false};
if (!band_guard_.Healthy(health_error_)) return {health_error_, false};
if (!IsWindowsPrivacyDesktopAvailable()) {
std::string cursor_error;
cursor_guard_.Stop(cursor_error);
return {
"Secure desktop, lock screen or unavailable input desktop; remote "
"operation paused. Privacy windows cannot cover system security UI.",
false};
}
std::string cursor_error;
if (!cursor_guard_.Healthy(cursor_error)) return {cursor_error, false};
QUERY_USER_NOTIFICATION_STATE shell_state{};
if (FAILED(SHQueryUserNotificationState(&shell_state)) ||
shell_state == QUNS_RUNNING_D3D_FULL_SCREEN) {
return {
"Exclusive fullscreen or unknown display presentation; remote "
"operation paused",
false};
}
MonitorEnumeration monitors;
if (!EnumDisplayMonitors(nullptr, nullptr, EnumMonitor,
reinterpret_cast<LPARAM>(&monitors)) ||
!monitors.ok) {
return {Error("EnumDisplayMonitors"), false};
}
const bool changed =
monitors.rects.size() != windows_.size() ||
std::any_of(monitors.rects.begin(), monitors.rects.end(),
[this](const RECT& rect) {
return std::none_of(windows_.begin(), windows_.end(),
[&](const auto& w) {
return SameRect(w.rect, rect);
});
});
if (changed || layout_changed_) {
layout_changed_ = false;
// Retain old covers until new monitor regions are covered. Never
// automatically resume remote work after a topology/DPI transition.
std::string error;
if (!ReconcileMonitors(error))
health_error_ = error;
else
health_error_ =
"Display layout/DPI changed; coverage rebuilt, remote operation "
"paused. Turn privacy off and reconnect to refresh display "
"mapping.";
}
if (!health_error_.empty()) return {health_error_, false};
BOOL composed = FALSE;
if (FAILED(DwmIsCompositionEnabled(&composed)) || !composed) {
return {
"Desktop composition stopped; privacy coverage cannot be guaranteed",
false};
}
return CheckCoverage();
}
PrivacyHealth CheckCoverage() {
const auto get_band = reinterpret_cast<GetPrivacyWindowBand>(
GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetWindowBand"));
if (!get_band) return {"High-band window API became unavailable", false};
for (const auto& w : windows_) {
DWORD band = 0;
DWORD affinity = 0;
BYTE alpha = 0;
DWORD layered_flags = 0;
RECT rect{};
DWORD cloaked = 0;
if (!get_band(w.cover, &band) || band != kPrivacyWindowBand ||
!IsWindowVisible(w.cover) || !GetWindowRect(w.cover, &rect) ||
!SameRect(rect, w.rect) ||
!(GetWindowLongPtrW(w.cover, GWL_EXSTYLE) & WS_EX_TOPMOST) ||
!GetLayeredWindowAttributes(w.cover, nullptr, &alpha,
&layered_flags) ||
alpha != 255 || layered_flags != LWA_ALPHA ||
!GetWindowDisplayAffinity(w.cover, &affinity) ||
affinity != WDA_EXCLUDEFROMCAPTURE ||
FAILED(DwmGetWindowAttribute(w.cover, DWMWA_CLOAKED, &cloaked,
sizeof(cloaked))) ||
cloaked) {
return {
"Privacy window visibility, geometry or capture affinity was lost; "
"remote operation paused",
false};
}
}
for (const auto& w : windows_) {
// Detect windows in special bands above the cover. A high-band window
// still cannot cover another desktop or guarantee exclusive fullscreen.
for (HWND above = GetWindow(w.cover, GW_HWNDPREV); above;
above = GetWindow(above, GW_HWNDPREV)) {
if (!IsWindowVisible(above) || IsIconic(above)) continue;
DWORD above_band = 0;
if (!get_band(above, &above_band))
return {"Could not inspect a window above the high-band cover",
false};
// HWND list order alone does not express compositor band order.
// Only skip the two known ordinary desktop bands, never unknown bands.
if (above_band == 0 || above_band == 1) continue;
if (std::any_of(windows_.begin(), windows_.end(),
[above](const auto& own) {
return above == own.cover || above == own.probe;
}))
continue;
RECT bounds{}, intersection{};
DWORD hidden = 0;
if (SUCCEEDED(DwmGetWindowAttribute(above, DWMWA_CLOAKED, &hidden,
sizeof(hidden))) &&
hidden)
continue;
if (GetWindowRect(above, &bounds) &&
IntersectRect(&intersection, &bounds, &w.rect)) {
char window_class[128]{};
DWORD process_id = 0;
GetClassNameA(above, window_class, sizeof(window_class));
GetWindowThreadProcessId(above, &process_id);
const auto now = GetTickCount64();
if (above != last_obstruction_ ||
now - last_obstruction_log_ >= 1000) {
LOG_WARN(
"Privacy cover obstructed: class={}, process={}; "
"remote operation paused",
window_class, process_id);
last_obstruction_ = above;
last_obstruction_log_ = now;
}
// A higher-band/system window cannot be repaired by raising an
// ordinary cover. Retain protection and require explicit recovery.
return {
"A system or higher-band window overrides privacy coverage; "
"remote operation paused. Turn privacy off to recover.",
false};
}
}
}
return {health_error_, false};
}
public:
bool PaintChallenge(uint32_t value, const std::string& title,
std::string& error) override {
using Pattern = PrivacyProbePattern;
for (auto& w : windows_) {
if (!w.probe && !CreateProbe(w, error)) return false;
PrivacyProbeImage image;
if (!RenderPrivacyProbe(title, PrivacyWindowDpi(w.probe),
w.rect.right - w.rect.left - 2 * Pattern::kOffset,
w.rect.bottom - w.rect.top - 2 * Pattern::kOffset,
value, image, error)) {
LOG_ERROR("Privacy verification panel: {}", error);
return false;
}
HDC screen = GetDC(nullptr);
HDC memory = CreateCompatibleDC(screen);
BITMAPINFO info{};
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = image.width;
info.bmiHeader.biHeight = -image.height;
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB;
void* bits = nullptr;
HBITMAP bitmap =
CreateDIBSection(screen, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
bool ok = screen && memory && bitmap && bits;
if (ok) {
std::memcpy(bits, image.pixels.data(),
image.pixels.size() * sizeof(uint32_t));
const auto old = SelectObject(memory, bitmap);
POINT position{w.rect.left + Pattern::kOffset,
w.rect.top + Pattern::kOffset},
source{};
SIZE size{image.width, image.height};
ok = old && old != HGDI_ERROR;
if (ok) {
ok = UpdateLayeredWindow(w.probe, screen, &position, &size, memory,
&source, 0, nullptr, ULW_OPAQUE);
SelectObject(memory, old);
}
}
if (!ok) error = Error("Present opaque privacy verification panel");
if (bitmap) DeleteObject(bitmap);
if (memory) DeleteDC(memory);
if (screen) ReleaseDC(nullptr, screen);
if (!ok) return false;
// Present only after the complete text and square bitmap is ready.
if (!IsWindowVisible(w.probe) &&
(!SetWindowPos(
w.probe, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW) ||
!IsWindowVisible(w.probe))) {
error = Error("Show privacy verification panel beneath cover");
return false;
}
}
return true;
}
bool ClearChallenges(std::string& error) override {
for (auto& w : windows_) {
Destroy(w.probe);
if (w.probe) {
error = "Could not remove the temporary capture verification window";
return false;
}
}
return true;
}
bool HasMonitor(int left, int top, int width, int height) const override {
// Metadata only; also serialized against Poll by PrivacyController.
return std::any_of(windows_.begin(), windows_.end(), [&](const auto& w) {
return w.rect.left == left && w.rect.top == top &&
w.rect.right - left == width && w.rect.bottom - top == height;
});
}
private:
struct Window {
RECT rect{};
HWND cover = nullptr;
HWND probe = nullptr;
};
static void Destroy(HWND& hwnd) {
if (hwnd && IsWindow(hwnd) && !DestroyWindow(hwnd)) {
Error("DestroyWindow");
return;
}
hwnd = nullptr;
}
bool Initialize(std::string& error) {
if (sink_) return true;
// This dedicated thread owns no existing application windows. Never alter
// process-wide or Slint/SDL DPI awareness.
if (!dpi_scope_ || !dpi_scope_->active()) {
dpi_scope_.reset();
dpi_scope_ = std::make_unique<ScopedWindowsPhysicalCoordinates>();
}
if (!dpi_scope_->active()) {
error = Error("SetThreadDpiAwarenessContext");
return false;
}
WNDCLASSEXW cls{sizeof(cls)};
cls.lpfnWndProc = WindowProc;
cls.hInstance = GetModuleHandleW(nullptr);
cls.lpszClassName = kClassName;
cls.hCursor = nullptr;
cls.hbrBackground = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
if (!registered_ && !RegisterClassExW(&cls)) {
error = Error("RegisterClassExW");
return false;
}
registered_ = true;
sink_ = CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, kClassName,
L"", WS_POPUP, 0, 0, 0, 0, nullptr, nullptr,
cls.hInstance, this);
if (!sink_) {
error = Error("CreateWindowExW message sink");
return false;
}
return true;
}
bool CreateProbe(Window& w, std::string& error) {
constexpr DWORD styles = WS_EX_TOPMOST | WS_EX_TOOLWINDOW |
WS_EX_NOACTIVATE | WS_EX_LAYERED |
WS_EX_TRANSPARENT;
w.probe =
CreateWindowExW(styles, kClassName, L"CrossDesk capture verification",
WS_POPUP, w.rect.left + PrivacyProbePattern::kOffset,
w.rect.top + PrivacyProbePattern::kOffset, 1, 1,
nullptr, nullptr, GetModuleHandleW(nullptr), this);
if (!w.probe) {
error = Error("CreateWindowExW capture challenge");
return false;
}
return true;
}
bool ReconcileMonitors(std::string& error) {
MonitorEnumeration monitors;
if (!EnumDisplayMonitors(nullptr, nullptr, EnumMonitor,
reinterpret_cast<LPARAM>(&monitors)) ||
!monitors.ok || monitors.rects.empty()) {
error = Error("EnumDisplayMonitors");
return false;
}
std::vector<PrivacyBandWindow> covers;
if (!band_guard_.Windows(covers, error)) return false;
for (const RECT& rect : monitors.rects) {
auto existing =
std::find_if(windows_.begin(), windows_.end(),
[&](const auto& w) { return SameRect(w.rect, rect); });
if (existing != windows_.end()) continue;
const auto cover = std::find_if(covers.begin(), covers.end(),
[&](const auto& candidate) {
return SameRect(candidate.rect, rect);
});
if (cover == covers.end()) {
error = "High-band broker has not covered every physical monitor";
return false;
}
windows_.push_back({rect, reinterpret_cast<HWND>(cover->hwnd), nullptr});
LOG_INFO(
"Privacy high-band cover ready: bounds=({}, {})-({}, {}), "
"affinity=0x11, alpha=255",
rect.left, rect.top, rect.right, rect.bottom);
}
for (auto it = windows_.begin(); it != windows_.end();) {
if (std::none_of(monitors.rects.begin(), monitors.rects.end(),
[&](const RECT& r) { return SameRect(it->rect, r); })) {
Destroy(it->probe);
it->cover = nullptr; // The broker owns cover destruction.
if (!it->probe)
it = windows_.erase(it);
else {
error = "Old privacy window could not be released";
return false;
}
} else
++it;
}
return true;
}
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wp,
LPARAM lp) {
if (msg == WM_NCCREATE) {
const auto create = reinterpret_cast<CREATESTRUCTW*>(lp);
SetWindowLongPtrW(hwnd, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(create->lpCreateParams));
}
auto self = reinterpret_cast<WindowsPrivacyBackend*>(
GetWindowLongPtrW(hwnd, GWLP_USERDATA));
if (msg == WM_HOTKEY || msg == kEmergency) {
if (self && hwnd == self->sink_) self->emergency_ = true;
return 0;
}
if (msg == WM_MOUSEACTIVATE) return MA_NOACTIVATE;
if (msg == WM_SETCURSOR) {
SetCursor(nullptr);
return TRUE;
}
if (msg == WM_NCHITTEST) return HTTRANSPARENT;
if (msg == WM_CLOSE) return 0;
if (msg == WM_PAINT) {
// UpdateLayeredWindow owns the complete probe image; WM_PAINT only
// validates the update region and never draws a second copy.
PAINTSTRUCT paint{};
BeginPaint(hwnd, &paint);
EndPaint(hwnd, &paint);
return 0;
}
if ((msg == WM_DISPLAYCHANGE || msg == WM_DPICHANGED) && self) {
self->layout_changed_ = true;
return 0;
}
return DefWindowProcW(hwnd, msg, wp, lp);
}
static void CALLBACK WindowEvent(HWINEVENTHOOK, DWORD, HWND hwnd, LONG object,
LONG child, DWORD, DWORD) {
auto self = event_owner_;
if (!self || !hwnd || object != OBJID_WINDOW || child != CHILDID_SELF ||
hwnd == self->sink_ || GetAncestor(hwnd, GA_ROOT) != hwnd)
return;
for (const auto& w : self->windows_)
if (hwnd == w.cover || hwnd == w.probe) return;
// OUTOFCONTEXT delivers on this window thread. Only mark work here;
// monitor/session queries and Z-order changes remain in the control loop.
const auto get_band = reinterpret_cast<GetPrivacyWindowBand>(
GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetWindowBand"));
DWORD band = 0;
if (get_band && get_band(hwnd, &band) && (band == 0 || band == 1)) return;
self->coverage_dirty_ = true;
}
inline static thread_local WindowsPrivacyBackend* event_owner_ = nullptr;
std::vector<HWINEVENTHOOK> window_events_;
std::vector<Window> windows_;
HWND sink_ = nullptr;
HANDLE wake_event_ = nullptr;
std::string wake_error_;
PrivacyInputGuard input_guard_;
bool block_input_ = false;
PrivacyCursorGuard cursor_guard_;
PrivacyBandGuard band_guard_;
std::unique_ptr<ScopedWindowsPhysicalCoordinates> dpi_scope_;
bool registered_ = false;
bool hotkey_ = false;
bool emergency_ = false;
bool layout_changed_ = false;
bool coverage_dirty_ = false;
uint64_t last_check_ = 0;
PrivacyHealth last_health_;
HWND last_obstruction_ = nullptr;
uint64_t last_obstruction_log_ = 0;
std::string health_error_;
};
} // namespace
bool IsWindowsPrivacyDesktopAvailable() {
// LockApp can display the lock screen while OpenInputDesktop still reports
// Default. Desktop-name checks alone are NOT a session-unlocked check.
DWORD session = 0;
if (!ProcessIdToSessionId(GetCurrentProcessId(), &session) ||
session != WTSGetActiveConsoleSessionId())
return false;
LPWSTR buffer = nullptr;
DWORD bytes = 0;
if (!WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, session,
WTSSessionInfoEx, &buffer, &bytes))
return false;
const auto info = reinterpret_cast<WTSINFOEXW*>(buffer);
const bool unlocked =
bytes >= sizeof(WTSINFOEXW) && info->Level == 1 &&
info->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_UNLOCK;
WTSFreeMemory(buffer);
if (!unlocked) return false;
HDESK input = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS);
if (!input) return false;
wchar_t input_name[256]{}, thread_name[256]{};
const bool ok =
DesktopName(input, input_name) &&
DesktopName(GetThreadDesktop(GetCurrentThreadId()), thread_name) &&
_wcsicmp(input_name, L"Default") == 0 &&
_wcsicmp(input_name, thread_name) == 0;
CloseDesktop(input);
return ok;
}
std::unique_ptr<PrivacyBackend> CreateWindowsPrivacyBackend() {
return std::make_unique<WindowsPrivacyBackend>();
}
} // namespace crossdesk
@@ -0,0 +1,323 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-10
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include "privacy_band_guard.h"
#include <objbase.h>
#include <algorithm>
#include <iterator>
#include <new>
#include "privacy_backend.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
struct ScopedHandle {
HANDLE value = nullptr;
~ScopedHandle() {
if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value);
}
};
bool Failed(const char* operation, std::string& error,
DWORD code = GetLastError()) {
error =
std::string(operation) + " (Windows error " + std::to_string(code) + ")";
LOG_ERROR("Privacy high-band: {}", error);
return false;
}
std::filesystem::path ModuleDirectory() {
std::vector<wchar_t> path(32768);
const DWORD length = GetModuleFileNameW(nullptr, path.data(), path.size());
if (!length || length >= path.size()) return {};
return std::filesystem::path(path.data()).parent_path();
}
std::vector<wchar_t> ChildEnvironment(const std::wstring& bootstrap) {
std::vector<std::wstring> entries;
LPWCH environment = GetEnvironmentStringsW();
if (!environment) return {};
const std::wstring prefix = std::wstring(kPrivacyBandBootstrap) + L"=";
for (const wchar_t* item = environment; *item; item += wcslen(item) + 1) {
if (_wcsnicmp(item, prefix.c_str(), prefix.size()) != 0)
entries.emplace_back(item);
}
FreeEnvironmentStringsW(environment);
entries.push_back(prefix + bootstrap);
std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
return _wcsicmp(a.c_str(), b.c_str()) < 0;
});
std::vector<wchar_t> result;
for (const auto& entry : entries) {
result.insert(result.end(), entry.begin(), entry.end());
result.push_back(0);
}
result.push_back(0);
return result;
}
} // namespace
PrivacyBandGuard::~PrivacyBandGuard() {
std::string error;
if (!Stop(error)) {
// Last-resort teardown still targets only this object's dedicated job.
if (job_) CloseHandle(job_);
job_ = nullptr;
Stop(error);
}
}
bool PrivacyBandGuard::Available(std::string& error) {
#if !defined(_M_X64) && !defined(__x86_64__)
error = "High-band privacy currently requires an x64 Windows process";
return false;
#else
const HMODULE user = GetModuleHandleW(L"user32.dll");
if (!GetProcAddress(user, "CreateWindowInBand") ||
!GetProcAddress(user, "GetWindowBand")) {
error = "Windows high-band window APIs are unavailable";
return false;
}
const auto directory = ModuleDirectory();
if (directory.empty())
return Failed("Resolve privacy module directory", error);
const auto dll = directory / kPrivacyBandDll;
if (GetFileAttributesW(dll.c_str()) == INVALID_FILE_ATTRIBUTES)
return Failed("Missing crossdesk_privacy_window.dll", error);
return true;
#endif
}
bool PrivacyBandGuard::Start(const PrivacyScreenText& text,
std::string& error) {
if (process_)
return Failed("High-band broker is already running", error, ERROR_BUSY);
if (!Available(error)) return false;
const auto directory = ModuleDirectory();
if (directory.empty())
return Failed("Resolve privacy module directory", error);
const auto dll = directory / kPrivacyBandDll;
wchar_t system[MAX_PATH]{}, temporary[MAX_PATH]{}, guid_text[40]{};
GUID guid{};
if (!GetSystemDirectoryW(system, MAX_PATH) ||
!GetTempPathW(MAX_PATH, temporary) || FAILED(CoCreateGuid(&guid)) ||
!StringFromGUID2(guid, guid_text, 40))
return Failed("Resolve high-band broker paths", error);
broker_directory_ = std::filesystem::path(temporary) /
(std::wstring(L"CrossDesk-privacy-") + guid_text);
if (!CreateDirectoryW(broker_directory_.c_str(), nullptr))
return Failed("Create private broker directory", error);
broker_path_ = broker_directory_ / L"RuntimeBroker_crossdesk.exe";
if (!CopyFileW((std::filesystem::path(system) / L"RuntimeBroker.exe").c_str(),
broker_path_.c_str(), TRUE))
return Failed("Copy system RuntimeBroker for this session", error);
// Keep both images immutable throughout creation/injection and the session.
image_lock_ = CreateFileW(broker_path_.c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, 0, nullptr);
dll_lock_ = CreateFileW(dll.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, 0, nullptr);
if (image_lock_ == INVALID_HANDLE_VALUE || dll_lock_ == INVALID_HANDLE_VALUE)
return Failed("Lock privacy broker images", error);
SECURITY_ATTRIBUTES inherit{sizeof(inherit), nullptr, TRUE};
ScopedHandle parent;
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(),
GetCurrentProcess(), &parent.value,
SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, TRUE,
0))
return Failed("Duplicate privacy parent process handle", error);
stop_ = CreateEventW(&inherit, TRUE, FALSE, nullptr);
ready_ = CreateEventW(&inherit, TRUE, FALSE, nullptr);
mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, &inherit, PAGE_READWRITE,
0, sizeof(PrivacyBandShared), nullptr);
if (!stop_ || !ready_ || !mapping_) {
const auto code = GetLastError();
return Failed("Create high-band broker IPC", error, code);
}
shared_ = static_cast<PrivacyBandShared*>(MapViewOfFile(
mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(PrivacyBandShared)));
if (!shared_) {
return Failed("Map high-band broker IPC", error);
}
new (shared_) PrivacyBandShared{};
// Immutable after process startup; no cross-process string objects or
// concurrent writes while the window thread paints.
if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
text.unlock_hint.c_str(), -1, shared_->unlock_hint,
static_cast<int>(std::size(shared_->unlock_hint))))
return Failed("Encode privacy screen text", error);
shared_->parent_pid = GetCurrentProcessId();
HANDLE handles[] = {parent.value, stop_, ready_, mapping_};
std::wstring bootstrap;
for (HANDLE handle : handles) {
if (!bootstrap.empty()) bootstrap += L",";
bootstrap += std::to_wstring(reinterpret_cast<uintptr_t>(handle));
}
auto environment = ChildEnvironment(bootstrap);
if (environment.empty()) {
return Failed("Build broker environment", error);
}
job_ = CreateJobObjectW(nullptr, nullptr);
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!job_ || !SetInformationJobObject(job_, JobObjectExtendedLimitInformation,
&limits, sizeof(limits))) {
return Failed("Bind high-band broker lifetime", error);
}
SIZE_T bytes = 0;
InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes);
std::vector<unsigned char> attributes(bytes);
STARTUPINFOEXW startup{};
startup.StartupInfo.cb = sizeof(startup);
startup.lpAttributeList =
reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attributes.data());
if (!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0,
&bytes)) {
return Failed("Initialize broker handle list", error);
}
bool ok = UpdateProcThreadAttribute(
startup.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
handles, sizeof(handles), nullptr, nullptr) != FALSE;
PROCESS_INFORMATION child{};
std::wstring command = L"\"" + broker_path_.wstring() + L"\"";
if (ok) {
ok = CreateProcessW(
broker_path_.c_str(), command.data(), nullptr, nullptr, TRUE,
CREATE_SUSPENDED | CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT |
EXTENDED_STARTUPINFO_PRESENT,
environment.data(), broker_directory_.c_str(),
&startup.StartupInfo, &child) != FALSE;
}
const DWORD create_error = GetLastError();
DeleteProcThreadAttributeList(startup.lpAttributeList);
if (!ok)
return Failed("Create owned RuntimeBroker process", error, create_error);
process_ = child.hProcess;
shared_->broker_pid = child.dwProcessId;
if (!AssignProcessToJobObject(job_, process_)) {
const DWORD code = GetLastError();
TerminateProcess(process_, code);
CloseHandle(child.hThread);
return Failed("Assign owned RuntimeBroker to privacy job", error, code);
}
const auto path = dll.wstring();
const auto size = (path.size() + 1) * sizeof(wchar_t);
void* remote_path = VirtualAllocEx(process_, nullptr, size,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
SIZE_T written = 0;
const auto loader =
GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW");
ok =
remote_path && loader &&
WriteProcessMemory(process_, remote_path, path.c_str(), size, &written) &&
written == size &&
QueueUserAPC(reinterpret_cast<PAPCFUNC>(loader), child.hThread,
reinterpret_cast<ULONG_PTR>(remote_path));
DWORD injection_error = GetLastError();
if (ok && ResumeThread(child.hThread) == DWORD(-1)) {
ok = false;
injection_error = GetLastError();
}
CloseHandle(child.hThread);
if (!ok)
return Failed("Load high-band window module into owned broker", error,
injection_error);
HANDLE startup_events[] = {ready_, process_};
const DWORD result = WaitForMultipleObjects(2, startup_events, FALSE, 5000);
if (result != WAIT_OBJECT_0) {
DWORD code = ERROR_TIMEOUT;
if (result == WAIT_OBJECT_0 + 1) GetExitCodeProcess(process_, &code);
return Failed("High-band broker did not become ready", error, code);
}
std::vector<PrivacyBandWindow> windows;
if (!Windows(windows, error)) return false;
LOG_INFO("Privacy high-band broker ready: process={}, band={}, monitors={}",
process_id(), kPrivacyWindowBand, windows.size());
return true;
}
bool PrivacyBandGuard::Healthy(std::string& error) {
if (!process_ || !shared_)
return Failed("High-band broker is not initialized", error,
ERROR_INVALID_HANDLE);
const LONG code = InterlockedCompareExchange(&shared_->error, 0, 0);
if (code != ERROR_SUCCESS) {
char operation[256]{};
WideCharToMultiByte(CP_UTF8, 0, shared_->operation, -1, operation,
sizeof(operation), nullptr, nullptr);
return Failed(operation[0] ? operation : "High-band window startup failed",
error, code);
}
if (WaitForSingleObject(process_, 0) != WAIT_TIMEOUT) {
DWORD exit_code = ERROR_PROCESS_ABORTED;
GetExitCodeProcess(process_, &exit_code);
return Failed("High-band window process exited", error, exit_code);
}
if (!InterlockedCompareExchange(&shared_->active, 0, 0) ||
GetTickCount64() - static_cast<ULONGLONG>(InterlockedCompareExchange64(
&shared_->heartbeat, 0, 0)) >
1500)
return Failed("High-band window thread stopped responding", error,
ERROR_TIMEOUT);
return true;
}
bool PrivacyBandGuard::Windows(std::vector<PrivacyBandWindow>& windows,
std::string& error) {
if (!Healthy(error)) return false;
for (int attempt = 0; attempt < 4; ++attempt) {
const LONG before = InterlockedCompareExchange(&shared_->sequence, 0, 0);
if (before & 1) continue;
const auto count = shared_->count;
if (count == 0 || count > kPrivacyMaxMonitors) break;
windows.assign(shared_->windows, shared_->windows + count);
if (before == InterlockedCompareExchange(&shared_->sequence, 0, 0)) {
for (const auto& window : windows) {
DWORD pid = 0;
GetWindowThreadProcessId(reinterpret_cast<HWND>(window.hwnd), &pid);
if (pid != process_id())
return Failed("Invalid high-band window owner", error,
ERROR_INVALID_HANDLE);
}
return true;
}
}
return Failed("High-band monitor snapshot is changing or invalid", error,
ERROR_RETRY);
}
bool PrivacyBandGuard::Stop(std::string& error) {
bool ok = true;
if (process_) {
if (stop_) SetEvent(stop_);
if (WaitForSingleObject(process_, 1500) != WAIT_OBJECT_0) {
// This handle came only from our CreateProcessW call, never discovery.
if (!TerminateProcess(process_, ERROR_CANCELLED) ||
WaitForSingleObject(process_, 1500) != WAIT_OBJECT_0)
ok = Failed("Stop owned high-band broker", error);
}
if (!ok) return false;
CloseHandle(process_);
process_ = nullptr;
}
if (job_) CloseHandle(job_);
job_ = nullptr;
if (shared_) UnmapViewOfFile(shared_);
shared_ = nullptr;
for (HANDLE handle : {stop_, ready_, mapping_, image_lock_, dll_lock_})
if (handle && handle != INVALID_HANDLE_VALUE) CloseHandle(handle);
stop_ = ready_ = mapping_ = image_lock_ = dll_lock_ = nullptr;
if (!broker_path_.empty()) DeleteFileW(broker_path_.c_str());
if (!broker_directory_.empty()) RemoveDirectoryW(broker_directory_.c_str());
broker_path_.clear();
broker_directory_.clear();
return true;
}
} // namespace crossdesk
@@ -0,0 +1,51 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-10
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_BAND_GUARD_H_
#define _PRIVACY_BAND_GUARD_H_
#include <Windows.h>
#include <filesystem>
#include <string>
#include <vector>
#include "privacy_band_ipc.h"
namespace crossdesk {
struct PrivacyScreenText;
// Lifecycle and snapshots are owned by the privacy control thread. The broker
// owns all high-band HWND operations. This class never injects an existing
// process and never terminates a process found by name or PID.
class PrivacyBandGuard {
public:
~PrivacyBandGuard();
static bool Available(std::string& error);
bool Start(const PrivacyScreenText& text, std::string& error);
bool Stop(std::string& error);
bool Healthy(std::string& error);
bool Windows(std::vector<PrivacyBandWindow>& windows, std::string& error);
bool stopped() const { return !process_; }
DWORD process_id() const { return process_ ? GetProcessId(process_) : 0; }
private:
HANDLE process_ = nullptr;
HANDLE job_ = nullptr;
HANDLE stop_ = nullptr;
HANDLE ready_ = nullptr;
HANDLE mapping_ = nullptr;
HANDLE image_lock_ = nullptr;
HANDLE dll_lock_ = nullptr;
PrivacyBandShared* shared_ = nullptr;
std::filesystem::path broker_path_;
std::filesystem::path broker_directory_;
};
} // namespace crossdesk
#endif // _PRIVACY_BAND_GUARD_H_
@@ -0,0 +1,53 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-10
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_BAND_IPC_H_
#define _PRIVACY_BAND_IPC_H_
#include <Windows.h>
#include <cstdint>
namespace crossdesk {
constexpr uint32_t kPrivacyBandMagic = 0x43534231;
constexpr uint32_t kPrivacyBandVersion = 2;
constexpr DWORD kPrivacyWindowBand = 18; // ZBID_ABOVELOCK_UX, current desktop.
constexpr size_t kPrivacyMaxMonitors = 64;
constexpr wchar_t kPrivacyBandBootstrap[] = L"CROSSDESK_PRIVACY_BAND_BOOTSTRAP";
constexpr wchar_t kPrivacyBandDll[] = L"crossdesk_privacy_window.dll";
struct PrivacyBandWindow {
uint64_t hwnd = 0;
RECT rect{};
};
// Unnamed mapping and synchronization/process handles are inherited only by
// the newly created broker. No global window-name/PID discovery or named IPC.
struct PrivacyBandShared {
uint32_t magic = kPrivacyBandMagic;
uint32_t version = kPrivacyBandVersion;
uint32_t size = sizeof(PrivacyBandShared);
DWORD parent_pid = 0;
DWORD broker_pid = 0;
volatile LONG sequence = 0;
volatile LONG error = ERROR_IO_PENDING;
volatile LONG active = 0;
alignas(8) volatile LONG64 heartbeat = 0;
wchar_t operation[128]{};
wchar_t unlock_hint[128]{};
uint32_t count = 0;
PrivacyBandWindow windows[kPrivacyMaxMonitors]{};
};
using CreatePrivacyWindowInBand = HWND(WINAPI*)(DWORD, ATOM, LPCWSTR, DWORD,
int, int, int, int, HWND, HMENU,
HINSTANCE, LPVOID, DWORD);
using GetPrivacyWindowBand = BOOL(WINAPI*)(HWND, DWORD*);
} // namespace crossdesk
#endif // _PRIVACY_BAND_IPC_H_
@@ -0,0 +1,70 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-10
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_CURSOR_API_H_
#define _PRIVACY_CURSOR_API_H_
#include <Windows.h>
namespace crossdesk {
// A separate cursor plane can remain visible above a capture-excluded HWND.
// Magnification's documented cursor API hides that plane across input queues;
// it does not change cursor schemes, shapes, positions, focus or DPI awareness.
class PrivacyCursorApi {
public:
PrivacyCursorApi() = default;
PrivacyCursorApi(const PrivacyCursorApi&) = delete;
PrivacyCursorApi& operator=(const PrivacyCursorApi&) = delete;
~PrivacyCursorApi() { RestoreAndClose(); }
bool Initialize() {
module_ = LoadLibraryExW(L"Magnification.dll", nullptr,
LOAD_LIBRARY_SEARCH_SYSTEM32);
if (!module_) return false;
const auto initialize = reinterpret_cast<InitializeFn>(
GetProcAddress(module_, "MagInitialize"));
uninitialize_ = reinterpret_cast<InitializeFn>(
GetProcAddress(module_, "MagUninitialize"));
show_ = reinterpret_cast<ShowFn>(
GetProcAddress(module_, "MagShowSystemCursor"));
if (!initialize || !uninitialize_ || !show_) {
SetLastError(ERROR_PROC_NOT_FOUND);
return false;
}
initialized_ = initialize() != FALSE;
return initialized_;
}
bool Hide() { return initialized_ && show_(FALSE); }
bool RestoreAndClose() {
bool ok = true;
if (initialized_) {
ok = show_(TRUE) != FALSE;
ok = (uninitialize_() != FALSE) && ok;
initialized_ = false;
}
if (module_) {
FreeLibrary(module_);
module_ = nullptr;
}
return ok;
}
private:
using InitializeFn = BOOL(WINAPI*)();
using ShowFn = BOOL(WINAPI*)(BOOL);
HMODULE module_ = nullptr;
InitializeFn uninitialize_ = nullptr;
ShowFn show_ = nullptr;
bool initialized_ = false;
};
struct PrivacyCursorReady {
volatile LONG error;
};
} // namespace crossdesk
#endif // _PRIVACY_CURSOR_API_H_
@@ -0,0 +1,158 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include "privacy_cursor_guard.h"
#include <filesystem>
#include <vector>
#include "privacy_cursor_api.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
struct Handle {
HANDLE value = nullptr;
~Handle() {
if (value) CloseHandle(value);
}
};
bool Failed(const char* operation, std::string& error,
DWORD code = GetLastError()) {
error = std::string(operation) + " failed (Windows error " +
std::to_string(code) + ")";
LOG_ERROR("Privacy cursor: {}", error);
return false;
}
} // namespace
PrivacyCursorGuard::~PrivacyCursorGuard() {
std::string error;
Stop(error);
// The inherited parent handle still lets a slow helper restore on exit.
if (process_) CloseHandle(process_);
if (stop_event_) CloseHandle(stop_event_);
}
bool PrivacyCursorGuard::Start(std::string& error) {
if (process_ || restore_pending_)
return Failed("Cursor guard already active", error, ERROR_BUSY);
helper_failure_reported_ = false;
std::vector<wchar_t> module(32768);
const DWORD count = GetModuleFileNameW(nullptr, module.data(),
static_cast<DWORD>(module.size()));
if (!count || count >= module.size())
return Failed("GetModuleFileNameW", error);
const auto helper = std::filesystem::path(module.data()).parent_path() /
L"crossdesk_privacy_cursor_helper.exe";
SECURITY_ATTRIBUTES security{sizeof(security), nullptr, TRUE};
Handle parent;
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(),
GetCurrentProcess(), &parent.value, SYNCHRONIZE, TRUE,
0))
return Failed("Duplicate parent process handle", error);
Handle stop{CreateEventW(&security, TRUE, FALSE, nullptr)};
Handle ready{CreateEventW(&security, TRUE, FALSE, nullptr)};
Handle mapping{CreateFileMappingW(INVALID_HANDLE_VALUE, &security,
PAGE_READWRITE, 0,
sizeof(PrivacyCursorReady), nullptr)};
if (!stop.value || !ready.value || !mapping.value)
return Failed("Create cursor guard IPC", error);
auto shared = static_cast<PrivacyCursorReady*>(MapViewOfFile(
mapping.value, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(PrivacyCursorReady)));
if (!shared) return Failed("Map cursor guard readiness", error);
InterlockedExchange(&shared->error, ERROR_IO_PENDING);
SIZE_T bytes = 0;
InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes);
std::vector<unsigned char> attributes(bytes);
STARTUPINFOEXW startup{};
startup.StartupInfo.cb = sizeof(startup);
startup.lpAttributeList =
reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attributes.data());
if (!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0,
&bytes)) {
const DWORD code = GetLastError();
UnmapViewOfFile(shared);
return Failed("Initialize cursor guard handle list", error, code);
}
HANDLE inherited[] = {parent.value, stop.value, ready.value, mapping.value};
bool ok = UpdateProcThreadAttribute(
startup.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
inherited, sizeof(inherited), nullptr, nullptr) != FALSE;
PROCESS_INFORMATION child{};
if (ok) {
std::wstring command = L"\"" + helper.wstring() + L"\"";
for (HANDLE handle : inherited)
command += L" " + std::to_wstring(reinterpret_cast<uintptr_t>(handle));
ok =
CreateProcessW(helper.c_str(), command.data(), nullptr, nullptr, TRUE,
CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT |
CREATE_BREAKAWAY_FROM_JOB,
nullptr, nullptr, &startup.StartupInfo, &child) != FALSE;
}
const DWORD start_error = GetLastError();
DeleteProcThreadAttributeList(startup.lpAttributeList);
if (!ok) {
UnmapViewOfFile(shared);
return Failed("Start crossdesk_privacy_cursor_helper.exe", error,
start_error);
}
CloseHandle(child.hThread);
process_ = child.hProcess;
stop_event_ = stop.value;
stop.value = nullptr;
HANDLE startup_events[] = {ready.value, process_};
const DWORD result = WaitForMultipleObjects(2, startup_events, FALSE, 2000);
const DWORD ready_error =
static_cast<DWORD>(InterlockedCompareExchange(&shared->error, 0, 0));
UnmapViewOfFile(shared);
if (result != WAIT_OBJECT_0 || ready_error != ERROR_SUCCESS) {
std::string cleanup;
Stop(cleanup);
return Failed(
"Hide system cursor", error,
ready_error == ERROR_IO_PENDING ? ERROR_TIMEOUT : ready_error);
}
return true;
}
bool PrivacyCursorGuard::RestoreAfterFailure(std::string& error) {
PrivacyCursorApi cursor;
if (!cursor.Initialize() || !cursor.RestoreAndClose()) {
restore_pending_ = true;
return Failed("Restore system cursor after helper failure", error);
}
restore_pending_ = false;
return true;
}
bool PrivacyCursorGuard::Healthy(std::string& error) {
if (process_ && WaitForSingleObject(process_, 0) == WAIT_TIMEOUT) return true;
if (restore_pending_ || (process_ && !helper_failure_reported_))
RestoreAfterFailure(error);
helper_failure_reported_ = true;
error = "System cursor helper exited; remote operation paused";
return false;
}
bool PrivacyCursorGuard::Stop(std::string& error) {
if (!process_) return !restore_pending_ || RestoreAfterFailure(error);
if (!SetEvent(stop_event_)) return Failed("Stop cursor helper", error);
if (WaitForSingleObject(process_, 2000) != WAIT_OBJECT_0) {
return Failed("Wait for cursor helper restoration", error, ERROR_TIMEOUT);
}
DWORD code = ERROR_GEN_FAILURE;
GetExitCodeProcess(process_, &code);
CloseHandle(process_);
process_ = nullptr;
CloseHandle(stop_event_);
stop_event_ = nullptr;
if (code != ERROR_SUCCESS) return RestoreAfterFailure(error);
return true;
}
} // namespace crossdesk
@@ -0,0 +1,39 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-10
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_CURSOR_GUARD_H_
#define _PRIVACY_CURSOR_GUARD_H_
#include <Windows.h>
#include <string>
namespace crossdesk {
// Called only on the privacy message thread. The child owns cursor suppression
// and restores it when either this guard stops it or its parent process exits.
class PrivacyCursorGuard {
public:
PrivacyCursorGuard() = default;
PrivacyCursorGuard(const PrivacyCursorGuard&) = delete;
PrivacyCursorGuard& operator=(const PrivacyCursorGuard&) = delete;
~PrivacyCursorGuard();
bool Start(std::string& error);
bool Healthy(std::string& error);
bool Stop(std::string& error);
bool stopped() const { return !process_ && !restore_pending_; }
private:
bool RestoreAfterFailure(std::string& error);
HANDLE process_ = nullptr;
HANDLE stop_event_ = nullptr;
bool restore_pending_ = false;
bool helper_failure_reported_ = false;
};
} // namespace crossdesk
#endif // _PRIVACY_CURSOR_GUARD_H_
@@ -0,0 +1,304 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-10
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include "privacy_input_guard.h"
#include "rd_log.h"
#include "windows_input_marker.h"
namespace crossdesk {
PrivacyInputGuard::~PrivacyInputGuard() { Stop(); }
void PrivacyInputGuard::Fault(const std::string& operation, DWORD code) {
{
std::lock_guard lock(error_mutex_);
error_ = operation + " (Windows error " + std::to_string(code) + ")";
LOG_ERROR("Privacy input: {}", error_);
}
healthy_.store(false);
// Wake the owner's message loop; recovery is never performed in a hook.
PostMessageW(owner_, WM_NULL, 0, 0);
}
bool PrivacyInputGuard::Start(HWND owner, UINT exit_message,
std::string& error) {
if (thread_.joinable()) {
error = "Privacy input thread is already running";
return false;
}
owner_ = owner;
exit_message_ = exit_message;
healthy_.store(false);
{
std::lock_guard lock(error_mutex_);
error_.clear();
}
stop_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
ready_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!stop_ || !ready_) {
Fault("Create privacy input events failed", GetLastError());
Healthy(error);
Stop();
return false;
}
try {
thread_ = std::thread([this] { Run(); });
} catch (const std::exception& e) {
error = std::string("Start privacy input thread failed: ") + e.what();
Stop();
return false;
}
if (WaitForSingleObject(ready_, 2000) != WAIT_OBJECT_0) {
error = "Privacy input thread startup timed out";
Stop();
return false;
}
if (!Healthy(error)) {
Stop();
return false;
}
return true;
}
void PrivacyInputGuard::Stop() {
if (stop_) SetEvent(stop_);
if (thread_.joinable()) thread_.join();
if (stop_) CloseHandle(stop_);
if (ready_) CloseHandle(ready_);
stop_ = ready_ = nullptr;
owner_ = nullptr;
}
bool PrivacyInputGuard::Healthy(std::string& error) const {
if (healthy_.load() && running_.load() &&
GetTickCount64() - last_pump_.load() < 1500)
return true;
std::lock_guard lock(error_mutex_);
error = error_.empty() ? "Privacy input message thread stopped responding"
: error_;
return false;
}
bool PrivacyInputGuard::Renew(HHOOK& hook, int kind, HOOKPROC procedure) {
HHOOK replacement =
SetWindowsHookExW(kind, procedure, GetModuleHandleW(nullptr), 0);
if (!replacement) {
Fault("Renew privacy input hook failed", GetLastError());
return false;
}
HHOOK previous = hook;
hook = replacement;
if (UnhookWindowsHookEx(previous)) return true;
const DWORD code = GetLastError();
if (code != ERROR_INVALID_HOOK_HANDLE) retired_.push_back(previous);
Fault("Privacy input hook was lost or could not be retired", code);
return false;
}
bool PrivacyInputGuard::PumpMessages() {
MSG message{};
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
if (message.message == WM_QUIT) return false;
TranslateMessage(&message);
DispatchMessageW(&message);
}
last_pump_.store(GetTickCount64());
return WaitForSingleObject(stop_, 0) == WAIT_TIMEOUT;
}
bool PrivacyInputGuard::ReleasePressedInputs() {
// Both hooks must already be installed: otherwise a new local press can
// slip between this snapshot and the transition to blocked input.
std::array<bool, 256> pressed{};
for (int key = 1; key < 256; ++key)
pressed[key] = (GetAsyncKeyState(key) & 0x8000) != 0;
// Keep held modifiers available to the physical emergency-exit chord.
// Our injected releases intentionally do not clear this hook-side state.
physical_keys_ = pressed;
std::vector<INPUT> releases;
for (int key = 1; key < 256; ++key) {
if (!pressed[key]) continue;
// Generic modifier VKs alias their left/right variants. Release each
// side once; use the generic VK only if Windows reports no specific side.
if ((key == VK_SHIFT && (pressed[VK_LSHIFT] || pressed[VK_RSHIFT])) ||
(key == VK_CONTROL && (pressed[VK_LCONTROL] || pressed[VK_RCONTROL])) ||
(key == VK_MENU && (pressed[VK_LMENU] || pressed[VK_RMENU])))
continue;
INPUT input{};
input.type = INPUT_MOUSE;
input.mi.dwExtraInfo = kInjectedMouseInputMarker;
switch (key) {
// GetAsyncKeyState reports physical mouse buttons, as do these flags,
// including when Windows swaps the primary and secondary buttons.
case VK_LBUTTON:
input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
break;
case VK_RBUTTON:
input.mi.dwFlags = MOUSEEVENTF_RIGHTUP;
break;
case VK_MBUTTON:
input.mi.dwFlags = MOUSEEVENTF_MIDDLEUP;
break;
case VK_XBUTTON1:
case VK_XBUTTON2:
input.mi.dwFlags = MOUSEEVENTF_XUP;
input.mi.mouseData = key == VK_XBUTTON1 ? XBUTTON1 : XBUTTON2;
break;
default: {
input = {};
input.type = INPUT_KEYBOARD;
input.ki.wVk = static_cast<WORD>(key);
const UINT scan = MapVirtualKeyW(key, MAPVK_VK_TO_VSC_EX);
input.ki.wScan = static_cast<WORD>(scan & 0xff);
input.ki.dwFlags = KEYEVENTF_KEYUP;
if ((scan & 0xff00) == 0xe000)
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
input.ki.dwExtraInfo = kInjectedKeyboardInputMarker;
break;
}
}
LOG_INFO("Privacy input handoff: releasing held input vk=0x{:02X}", key);
releases.push_back(input);
}
if (!releases.empty()) {
const UINT count = static_cast<UINT>(releases.size());
SetLastError(ERROR_SUCCESS);
const UINT sent = SendInput(count, releases.data(), sizeof(INPUT));
if (sent != count) {
Fault("Release held privacy inputs failed (sent " + std::to_string(sent) +
"/" + std::to_string(count) + ")",
GetLastError());
return false;
}
}
// SendInput acknowledges insertion, not delivery. Pump this hook thread
// until the tagged releases have passed through and Windows reports no
// pressed inputs. The caller cannot publish input_blocked/ready earlier.
const auto deadline = GetTickCount64() + 1000;
for (;;) {
if (!PumpMessages()) {
Fault("Privacy input handoff cancelled", ERROR_CANCELLED);
return false;
}
int held = 0;
for (int key = 1; key < 256; ++key) {
if (GetAsyncKeyState(key) & 0x8000) {
held = key;
break;
}
}
if (!held) {
LOG_INFO("Privacy input handoff complete: released {} held inputs",
releases.size());
return true;
}
if (GetTickCount64() >= deadline) {
Fault("Privacy input release timed out, still pressed vk=" +
std::to_string(held),
ERROR_TIMEOUT);
return false;
}
if (MsgWaitForMultipleObjectsEx(1, &stop_, 10, QS_ALLINPUT,
MWMO_INPUTAVAILABLE) == WAIT_FAILED) {
Fault("Wait for privacy input releases failed", GetLastError());
return false;
}
}
}
void PrivacyInputGuard::Run() {
current_ = this;
running_.store(true);
try {
keyboard_ = SetWindowsHookExW(WH_KEYBOARD_LL, KeyboardHook,
GetModuleHandleW(nullptr), 0);
if (!keyboard_)
Fault("Install privacy keyboard hook failed", GetLastError());
if (keyboard_) {
mouse_ =
SetWindowsHookExW(WH_MOUSE_LL, MouseHook, GetModuleHandleW(nullptr), 0);
if (!mouse_) Fault("Install privacy mouse hook failed", GetLastError());
}
healthy_.store(keyboard_ && mouse_ && ReleasePressedInputs());
last_pump_.store(GetTickCount64());
SetEvent(ready_);
auto last_refresh = GetTickCount64();
LOG_INFO("Privacy input message thread started: thread={}",
GetCurrentThreadId());
while (keyboard_ && mouse_ &&
WaitForSingleObject(stop_, 0) == WAIT_TIMEOUT) {
if (!PumpMessages()) break;
const auto now = GetTickCount64();
if (healthy_.load() && now - last_refresh >= 1000) {
last_refresh = now;
// Install replacements before retiring old hooks, with no input gap.
if (Renew(keyboard_, WH_KEYBOARD_LL, KeyboardHook))
Renew(mouse_, WH_MOUSE_LL, MouseHook);
}
if (MsgWaitForMultipleObjectsEx(1, &stop_, 100, QS_ALLINPUT,
MWMO_INPUTAVAILABLE) == WAIT_FAILED) {
Fault("Wait for privacy input messages failed", GetLastError());
break;
}
}
} catch (const std::exception& e) {
Fault(std::string("Privacy input thread failed: ") + e.what(),
ERROR_GEN_FAILURE);
SetEvent(ready_);
}
healthy_.store(false);
auto unhook = [this](HHOOK hook) {
if (hook && !UnhookWindowsHookEx(hook) &&
GetLastError() != ERROR_INVALID_HOOK_HANDLE)
Fault("Release privacy input hook failed", GetLastError());
};
unhook(keyboard_);
unhook(mouse_);
for (auto hook : retired_) unhook(hook);
keyboard_ = mouse_ = nullptr;
retired_.clear();
physical_keys_.fill(false);
current_ = nullptr;
running_.store(false);
PostMessageW(owner_, WM_NULL, 0, 0);
// Windows also removes hooks installed by a thread when that thread exits.
LOG_INFO("Privacy input message thread stopped");
}
LRESULT CALLBACK PrivacyInputGuard::KeyboardHook(int code, WPARAM wp,
LPARAM lp) {
auto self = current_;
if (code < 0 || !self) return CallNextHookEx(nullptr, code, wp, lp);
const auto& key = *reinterpret_cast<KBDLLHOOKSTRUCT*>(lp);
if ((key.flags & LLKHF_INJECTED) &&
key.dwExtraInfo == kInjectedKeyboardInputMarker)
return CallNextHookEx(nullptr, code, wp, lp);
if (!(key.flags & LLKHF_INJECTED) && key.vkCode < 256) {
self->physical_keys_[key.vkCode] = !(key.flags & LLKHF_UP);
const auto& keys = self->physical_keys_;
if (key.vkCode == VK_F12 && !(key.flags & LLKHF_UP) &&
(keys[VK_LCONTROL] || keys[VK_RCONTROL]) &&
(keys[VK_LMENU] || keys[VK_RMENU]) &&
(keys[VK_LSHIFT] || keys[VK_RSHIFT]))
PostMessageW(self->owner_, self->exit_message_, 0, 0);
}
return 1;
}
LRESULT CALLBACK PrivacyInputGuard::MouseHook(int code, WPARAM wp, LPARAM lp) {
auto self = current_;
if (code < 0 || !self) return CallNextHookEx(nullptr, code, wp, lp);
const auto& mouse = *reinterpret_cast<MSLLHOOKSTRUCT*>(lp);
if ((mouse.flags & LLMHF_INJECTED) &&
mouse.dwExtraInfo == kInjectedMouseInputMarker)
return CallNextHookEx(nullptr, code, wp, lp);
return 1;
}
} // namespace crossdesk
@@ -0,0 +1,59 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-10
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_INPUT_GUARD_H_
#define _PRIVACY_INPUT_GUARD_H_
#include <Windows.h>
#include <array>
#include <atomic>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
namespace crossdesk {
// The owner controls lifecycle; the dedicated message thread owns LL hooks.
// Rendering, capture verification and session queries never run on this thread.
class PrivacyInputGuard {
public:
~PrivacyInputGuard();
bool Start(HWND owner, UINT exit_message, std::string& error);
void Stop();
bool Healthy(std::string& error) const;
bool stopped() const { return !thread_.joinable(); }
private:
void Run();
bool ReleasePressedInputs();
bool PumpMessages();
void Fault(const std::string& operation, DWORD code);
bool Renew(HHOOK& hook, int kind, HOOKPROC procedure);
static LRESULT CALLBACK KeyboardHook(int code, WPARAM wp, LPARAM lp);
static LRESULT CALLBACK MouseHook(int code, WPARAM wp, LPARAM lp);
std::thread thread_;
HANDLE stop_ = nullptr;
HANDLE ready_ = nullptr;
HWND owner_ = nullptr;
UINT exit_message_ = 0;
std::atomic<bool> healthy_{false};
std::atomic<bool> running_{false};
std::atomic<ULONGLONG> last_pump_{0};
mutable std::mutex error_mutex_;
std::string error_;
HHOOK keyboard_ = nullptr;
HHOOK mouse_ = nullptr;
std::vector<HHOOK> retired_;
std::array<bool, 256> physical_keys_{};
inline static thread_local PrivacyInputGuard* current_ = nullptr;
};
} // namespace crossdesk
#endif // _PRIVACY_INPUT_GUARD_H_
@@ -0,0 +1,142 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include "privacy_probe_renderer.h"
#include <algorithm>
#include "privacy_probe_pattern.h"
namespace crossdesk {
namespace {
struct Canvas {
HDC dc = CreateCompatibleDC(nullptr);
HFONT font = nullptr;
HGDIOBJ old_font = nullptr;
HBITMAP bitmap = nullptr;
HGDIOBJ old_bitmap = nullptr;
~Canvas() {
if (old_bitmap) SelectObject(dc, old_bitmap);
if (old_font) SelectObject(dc, old_font);
if (bitmap) DeleteObject(bitmap);
if (font) DeleteObject(font);
if (dc) DeleteDC(dc);
}
};
bool Failed(const char* operation, std::string& error) {
error = std::string(operation) + " (Windows error " +
std::to_string(GetLastError()) + ")";
return false;
}
} // namespace
bool RenderPrivacyProbe(const std::string& title, UINT dpi, int maximum_width,
int maximum_height, uint32_t value,
PrivacyProbeImage& image, std::string& error) {
using Pattern = PrivacyProbePattern;
const int length =
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, title.data(),
static_cast<int>(title.size()), nullptr, 0);
if (length <= 0) return Failed("Decode privacy verification text", error);
std::wstring text(length, L'\0');
if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, title.data(),
static_cast<int>(title.size()), text.data(), length))
return Failed("Decode privacy verification text", error);
const int available_text_width =
maximum_width - Pattern::kTextLeft - Pattern::kPadding;
if (available_text_width < 80 ||
maximum_height < Pattern::kSide + 2 * Pattern::kPadding) {
error = "Display is too small for the privacy verification panel";
return false;
}
Canvas canvas;
if (!canvas.dc)
return Failed("Create privacy verification drawing context", error);
canvas.font = CreateFontW(
-MulDiv(28, static_cast<int>(dpi), 96), 0, 0, 0, FW_SEMIBOLD, FALSE,
FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
ANTIALIASED_QUALITY, DEFAULT_PITCH, L"Microsoft YaHei UI");
if (!canvas.font) return Failed("Create privacy verification font", error);
canvas.old_font = SelectObject(canvas.dc, canvas.font);
if (!canvas.old_font || canvas.old_font == HGDI_ERROR) {
canvas.old_font = nullptr;
return Failed("Select privacy verification font", error);
}
SIZE extent{};
if (!GetTextExtentPoint32W(canvas.dc, text.data(), length, &extent))
return Failed("Measure privacy verification text", error);
const int text_width =
(std::min)(available_text_width,
(std::min)(static_cast<int>(extent.cx) + 4,
MulDiv(520, static_cast<int>(dpi), 96)));
RECT text_bounds{0, 0, text_width, 0};
constexpr UINT text_flags = DT_LEFT | DT_WORDBREAK | DT_NOPREFIX;
if (!DrawTextW(canvas.dc, text.data(), length, &text_bounds,
text_flags | DT_CALCRECT))
return Failed("Lay out privacy verification text", error);
const int width = Pattern::kTextLeft + text_width + Pattern::kPadding;
const int height =
(std::max)(Pattern::kSide, static_cast<int>(text_bounds.bottom)) +
2 * Pattern::kPadding;
if (height > maximum_height || text_bounds.right > text_width) {
error =
"Display is too small for localized privacy verification text at this "
"DPI";
return false;
}
BITMAPINFO info{};
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = width;
info.bmiHeader.biHeight = -height;
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB;
void* bits = nullptr;
canvas.bitmap =
CreateDIBSection(canvas.dc, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
if (!canvas.bitmap || !bits)
return Failed("Create privacy verification image", error);
canvas.old_bitmap = SelectObject(canvas.dc, canvas.bitmap);
if (!canvas.old_bitmap || canvas.old_bitmap == HGDI_ERROR) {
canvas.old_bitmap = nullptr;
return Failed("Select privacy verification image", error);
}
auto pixels = static_cast<uint32_t*>(bits);
std::fill(pixels, pixels + static_cast<size_t>(width) * height, 0xFFF4F6F9u);
for (int y = 0; y < height; ++y)
for (int x = 0; x < width; ++x)
if (x == 0 || y == 0 || x == width - 1 || y == height - 1)
pixels[static_cast<size_t>(y) * width + x] = 0xFFD0D6E0u;
SetBkMode(canvas.dc, TRANSPARENT);
SetTextColor(canvas.dc, RGB(34, 46, 64));
RECT text_rect{Pattern::kTextLeft, (height - text_bounds.bottom) / 2,
width - Pattern::kPadding, (height + text_bounds.bottom) / 2};
// Explicit clipping makes font fallback/overhang unable to reach the code.
IntersectClipRect(canvas.dc, text_rect.left, text_rect.top, text_rect.right,
text_rect.bottom);
if (!DrawTextW(canvas.dc, text.data(), length, &text_rect, text_flags))
return Failed("Draw privacy verification text", error);
if (!GdiFlush()) return Failed("Flush privacy verification text", error);
for (size_t index = 0; index < static_cast<size_t>(width) * height; ++index)
pixels[index] |= 0xFF000000u;
for (int row = 0; row < Pattern::kRows; ++row)
for (int column = 0; column < Pattern::kColumns; ++column) {
const uint32_t color =
Pattern::White(value, column, row) ? 0xFFFFFFFFu : 0xFF000000u;
for (int y = 0; y < Pattern::kCell; ++y)
for (int x = 0; x < Pattern::kCell; ++x)
pixels[static_cast<size_t>(Pattern::kPadding + row * Pattern::kCell +
y) *
width +
Pattern::kPadding + column * Pattern::kCell + x] = color;
}
image.width = width;
image.height = height;
image.pixels.assign(pixels, pixels + static_cast<size_t>(width) * height);
return true;
}
} // namespace crossdesk
@@ -0,0 +1,32 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_PROBE_RENDERER_H_
#define _PRIVACY_PROBE_RENDERER_H_
#include <Windows.h>
#include <cstdint>
#include <string>
#include <vector>
namespace crossdesk {
struct PrivacyProbeImage {
int width = 0;
int height = 0;
std::vector<uint32_t> pixels;
};
// Produces an opaque BGRA panel. The localized text and the machine-readable
// square occupy disjoint rectangles. Caller runs on the owning window thread.
bool RenderPrivacyProbe(const std::string& title, UINT dpi, int maximum_width,
int maximum_height, uint32_t value,
PrivacyProbeImage& image, std::string& error);
} // namespace crossdesk
#endif // _PRIVACY_PROBE_RENDERER_H_
@@ -1,11 +1,12 @@
#include "screen_capturer_dxgi.h"
#include <display_stream_id.h>
#include <algorithm>
#include <chrono>
#include <string>
#include <vector>
#include <display_stream_id.h>
#include "libyuv.h"
#include "rd_log.h"
@@ -218,11 +219,13 @@ void ScreenCapturerDxgi::EnumerateDisplays() {
mi.rcMonitor.left, mi.rcMonitor.top,
mi.rcMonitor.right, mi.rcMonitor.bottom);
// primary first
if (is_primary)
if (is_primary) {
display_info_list_.insert(display_info_list_.begin(), info);
else
outputs_.insert(outputs_.begin(), output);
} else {
display_info_list_.push_back(info);
outputs_.push_back(output);
outputs_.push_back(output);
}
}
}
}
@@ -246,6 +249,11 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
}
staging_.Reset();
DXGI_OUTDUPL_DESC desc{};
duplication_->GetDesc(&desc);
rotation_ = desc.Rotation;
LOG_INFO("DXGI: duplication ready, monitor={}, rotation={}", monitor_index,
static_cast<int>(rotation_));
return true;
}
@@ -276,6 +284,7 @@ bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
}
void ScreenCapturerDxgi::ReleaseDuplication() {
++duplication_generation_;
staging_.Reset();
if (duplication_) {
duplication_->ReleaseFrame();
@@ -284,16 +293,27 @@ void ScreenCapturerDxgi::ReleaseDuplication() {
}
void ScreenCapturerDxgi::CaptureLoop() {
const int timeout_ms = 33;
const int timeout_ms = (std::max)(1, 1000 / (std::max)(1, fps_));
bool cached_frame_valid = false;
int cached_monitor = -1;
uint64_t cached_generation = 0;
std::vector<uint8_t> rotated_frame;
auto last_duplication_retry =
std::chrono::steady_clock::now() - std::chrono::milliseconds(1000);
while (running_) {
if (paused_) {
cached_frame_valid = false;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
// Duplication/staging textures belong to one monitor. Do not release or
// replace them in SwitchTo while AcquireNextFrame/Map is using them.
std::unique_lock capture_lock(switch_mutex_);
if (paused_ || !running_) 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;
@@ -303,15 +323,32 @@ void ScreenCapturerDxgi::CaptureLoop() {
continue;
}
const int frame_monitor = monitor_index_.load();
DXGI_OUTDUPL_FRAME_INFO frame_info{};
Microsoft::WRL::ComPtr<IDXGIResource> desktop_resource;
HRESULT hr = duplication_->AcquireNextFrame(
timeout_ms, &frame_info, desktop_resource.GetAddressOf());
if (hr == DXGI_ERROR_WAIT_TIMEOUT) {
// DXGI explicitly reports an unchanged desktop. Re-emit only a frame
// successfully acquired for this monitor, so a static screen still has
// a delivery heartbeat after temporary privacy probes are removed.
if (cached_frame_valid && cached_generation == duplication_generation_ &&
cached_monitor == monitor_index_.load() && callback_ && nv12_frame_) {
const auto stream_id = MakeDisplayStreamId(cached_monitor);
capture_lock.unlock();
callback_(nv12_frame_, nv12_width_ * nv12_height_ * 3 / 2, nv12_width_,
nv12_height_, stream_id.c_str(), nullptr);
}
continue;
}
// Never replay a cached image after an acquisition/conversion failure or
// a backend rebuild. Normal error/reset handling must pause privacy.
cached_frame_valid = false;
if (FAILED(hr)) {
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
capture_lock.unlock();
if (callback_)
callback_(nullptr, ScreenCapturer::kBackendReset, 0, 0, "", nullptr);
RecreateDuplicationForCurrentMonitor();
continue;
}
@@ -356,9 +393,42 @@ void ScreenCapturerDxgi::CaptureLoop() {
continue;
}
auto pixels = static_cast<const uint8_t*>(mapped.pData);
int stride = static_cast<int>(mapped.RowPitch);
int logical_width = static_cast<int>(src_desc.Width);
int logical_height = static_cast<int>(src_desc.Height);
libyuv::RotationMode rotation = libyuv::kRotate0;
switch (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:
break;
}
if (rotation != libyuv::kRotate0) {
const bool swap_axes = rotation != libyuv::kRotate180;
const int rotated_width = swap_axes ? logical_height : logical_width;
rotated_frame.resize(static_cast<size_t>(logical_width) * logical_height *
4);
if (libyuv::ARGBRotate(pixels, stride, rotated_frame.data(),
rotated_width * 4, logical_width, logical_height,
rotation) != 0) {
d3d_context_->Unmap(staging_.Get(), 0);
duplication_->ReleaseFrame();
continue;
}
pixels = rotated_frame.data();
stride = rotated_width * 4;
if (swap_axes) std::swap(logical_width, logical_height);
}
int even_width = logical_width & ~1;
int even_height = static_cast<int>(src_desc.Height) & ~1;
int even_height = logical_height & ~1;
if (even_width <= 0 || even_height <= 0) {
d3d_context_->Unmap(staging_.Get(), 0);
duplication_->ReleaseFrame();
@@ -374,25 +444,29 @@ void ScreenCapturerDxgi::CaptureLoop() {
nv12_height_ = even_height;
}
libyuv::ARGBToNV12(static_cast<const uint8_t*>(mapped.pData),
static_cast<int>(mapped.RowPitch), nv12_frame_,
even_width, nv12_frame_ + even_width * even_height,
even_width, even_width, even_height);
const int converted =
libyuv::ARGBToNV12(pixels, stride, nv12_frame_, even_width,
nv12_frame_ + even_width * even_height, even_width,
even_width, even_height);
if (callback_) {
int idx = monitor_index_.load();
cached_generation = duplication_generation_;
d3d_context_->Unmap(staging_.Get(), 0);
duplication_->ReleaseFrame();
capture_lock.unlock();
if (converted == 0 && 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);
callback_(nv12_frame_, nv12_size, even_width, even_height,
stream_id.c_str(), nullptr);
cached_monitor = idx;
cached_frame_valid = true;
} else {
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size());
}
}
d3d_context_->Unmap(staging_.Get(), 0);
duplication_->ReleaseFrame();
}
}
@@ -63,6 +63,8 @@ class ScreenCapturerDxgi : public ScreenCapturer {
Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d_context_;
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging_;
DXGI_MODE_ROTATION rotation_ = DXGI_MODE_ROTATION_IDENTITY;
uint64_t duplication_generation_ = 0;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
@@ -351,6 +351,10 @@ int ScreenCapturerWgc::ResetToInitialMonitor() {
}
void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
int id) {
if (!frame.data && frame.width == 0 && frame.height == 0) {
if (on_data_) on_data_(nullptr, ScreenCapturer::kBackendReset, 0, 0, "", nullptr);
return;
}
if (!running_ || !on_data_) {
return;
}
@@ -127,6 +127,13 @@ class WgcPluginCapturer final : public ScreenCapturer {
DestroyFn destroy_fn_ = nullptr;
};
const char* CaptureBackendName(const ScreenCapturer* capturer) {
if (dynamic_cast<const ScreenCapturerDxgi*>(capturer)) return "DXGI";
if (dynamic_cast<const WgcPluginCapturer*>(capturer)) return "WGC";
if (dynamic_cast<const ScreenCapturerGdi*>(capturer)) return "GDI";
return "unavailable";
}
std::string BuildSecureCaptureCommand(int left, int top, int width, int height,
bool show_cursor,
const std::string& stage,
@@ -330,7 +337,13 @@ bool QuerySecureDesktopHelperFrame(
ScreenCapturerWin::ScreenCapturerWin() {}
ScreenCapturerWin::~ScreenCapturerWin() { Destroy(); }
void ScreenCapturerWin::NotifyPrivacyCapture(bool running) {
if (privacy_)
privacy_->CaptureChanged(CaptureBackendName(impl_.get()), running);
}
int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
NotifyPrivacyCapture(false);
fps_ = fps;
cb_orig_ = cb;
native_output_logged_.store(false, std::memory_order_relaxed);
@@ -344,12 +357,17 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
cb_ = [this](unsigned char* data, int size, int w, int h,
const char* reported_stream_id,
const MiniRtcNativeVideoFrame* native_frame) {
if (size == ScreenCapturer::kBackendReset) {
if (privacy_) privacy_->CaptureInterrupted();
return;
}
if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
return;
}
const char* raw_stream_id = reported_stream_id ? reported_stream_id : "";
std::string mapped_stream_id;
DisplayInfo privacy_display{"", 0, 0, 0, 0};
{
std::lock_guard<std::mutex> lock(alias_mutex_);
auto it = stream_id_alias_.find(raw_stream_id);
@@ -363,6 +381,21 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
mapped_stream_id = ResolveDisplayStreamId(
mapped_stream_id.c_str(), canonical_displays_.size(),
monitor_index_.load(std::memory_order_relaxed));
for (size_t index = 0; index < canonical_displays_.size(); ++index) {
if (mapped_stream_id == MakeDisplayStreamId(index)) {
privacy_display = canonical_displays_[index];
break;
}
}
}
if (privacy_) {
if (privacy_->Engaged() && !IsWindowsPrivacyDesktopAvailable()) {
privacy_->Fail("Secure or unavailable desktop; privacy cannot cover system security UI. Remote operation paused.");
return;
}
if (!privacy_->ObserveFrame(data, size > 0 ? static_cast<size_t>(size) : 0,
w, h, privacy_display.left, privacy_display.top,
privacy_display.width, privacy_display.height)) return;
}
if (mapped_stream_id.empty()) {
if (!invalid_stream_id_logged_.exchange(true,
@@ -393,42 +426,23 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
native_frame);
};
impl_.reset();
int ret = -1;
impl_ = WgcPluginCapturer::Create();
impl_is_wgc_plugin_ = (impl_ != nullptr);
ret = impl_ ? impl_->Init(fps_, cb_) : -1;
if (ret == 0) {
LOG_INFO("Windows capturer: using WGC plugin");
BuildCanonicalFromImpl();
monitor_index_.store(0, std::memory_order_relaxed);
initial_monitor_index_ = 0;
return 0;
}
LOG_WARN("Windows capturer: WGC plugin init failed (ret={}), try DXGI", ret);
impl_.reset();
impl_is_wgc_plugin_ = false;
impl_ = std::make_unique<ScreenCapturerDxgi>();
impl_is_wgc_plugin_ = false;
ret = impl_->Init(fps_, cb_);
if (ret == 0) {
LOG_INFO("Windows capturer: using DXGI Desktop Duplication");
BuildCanonicalFromImpl();
monitor_index_.store(0, std::memory_order_relaxed);
initial_monitor_index_ = 0;
return 0;
}
LOG_WARN("Windows capturer: DXGI init failed (ret={}), fallback to GDI", ret);
impl_.reset();
impl_ = std::make_unique<ScreenCapturerGdi>();
impl_is_wgc_plugin_ = false;
ret = impl_->Init(fps_, cb_);
if (ret == 0) {
LOG_INFO("Windows capturer: using GDI BitBlt");
auto try_init = [&](std::unique_ptr<ScreenCapturer> candidate) {
ret = candidate ? candidate->Init(fps_, cb_) : -1;
if (ret != 0) {
LOG_WARN("Windows capturer: {} init failed (ret={})",
CaptureBackendName(candidate.get()), ret);
return false;
}
impl_ = std::move(candidate);
return true;
};
// Short-circuiting also keeps the unused WGC plugin unloaded.
if (try_init(std::make_unique<ScreenCapturerDxgi>()) ||
try_init(WgcPluginCapturer::Create()) ||
try_init(std::make_unique<ScreenCapturerGdi>())) {
LOG_INFO("Windows capturer: using {}", CaptureBackendName(impl_.get()));
BuildCanonicalFromImpl();
monitor_index_.store(0, std::memory_order_relaxed);
initial_monitor_index_ = 0;
@@ -436,7 +450,6 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
}
LOG_ERROR("Windows capturer: all implementations failed, ret={}", ret);
impl_.reset();
return -1;
}
@@ -446,7 +459,6 @@ int ScreenCapturerWin::Destroy() {
if (impl_) {
impl_->Destroy();
impl_.reset();
impl_is_wgc_plugin_ = false;
}
{
std::lock_guard<std::mutex> lock(alias_mutex_);
@@ -459,8 +471,15 @@ int ScreenCapturerWin::Destroy() {
void ScreenCapturerWin::EmitCapturedFrame(
unsigned char* data, int size, int width, int height,
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame) {
if (!cb_orig_) {
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame,
bool from_secure_desktop) {
// A helper IPC already in flight when enable began must not bypass the
// verified ordinary-desktop callback when it completes later.
if (from_secure_desktop && privacy_ && privacy_->Engaged()) {
privacy_->Fail("Secure desktop helper frame arrived during privacy; remote operation paused");
return;
}
if (!cb_orig_ || (privacy_ && !privacy_->RemoteAllowed())) {
return;
}
@@ -499,11 +518,43 @@ void ScreenCapturerWin::EmitCapturedFrame(
owned_frame->Release();
}
void ScreenCapturerWin::RestoreMonitor(int monitor_index) {
RebuildAliasesFromImpl();
if (monitor_index > 0 && impl_->SwitchTo(monitor_index) != 0) {
monitor_index_.store(0, std::memory_order_relaxed);
}
}
bool ScreenCapturerWin::TryStartBackend(
std::unique_ptr<ScreenCapturer> candidate, int monitor_index) {
if (!candidate) {
LOG_WARN("Windows capturer: WGC plugin unavailable");
return false;
}
const char* name = CaptureBackendName(candidate.get());
int ret = candidate->Init(fps_, cb_);
if (ret == 0)
ret = candidate->Start(show_cursor_.load(std::memory_order_relaxed));
if (ret != 0) {
LOG_WARN("Windows capturer: {} initialization/start failed (ret={})", name,
ret);
candidate->Destroy();
return false;
}
// Commit only after the replacement is running. Failed candidates release
// their resources without replacing the current backend.
impl_ = std::move(candidate);
RestoreMonitor(monitor_index);
LOG_INFO("Windows capturer: started {}", name);
return true;
}
int ScreenCapturerWin::Start(bool show_cursor) {
if (!impl_) return -1;
if (running_.load(std::memory_order_relaxed)) {
return 0;
}
NotifyPrivacyCapture(false);
show_cursor_.store(show_cursor, std::memory_order_relaxed);
paused_.store(false, std::memory_order_relaxed);
@@ -528,37 +579,15 @@ int ScreenCapturerWin::Start(bool show_cursor) {
LOG_WARN("Windows capturer: refresh/start failed (ret={}), trying fallback",
ret);
auto try_init_start = [&](std::unique_ptr<ScreenCapturer> cand) -> bool {
int r = cand->Init(fps_, cb_);
if (r != 0) return false;
int s = cand->Start(show_cursor);
if (s == 0) {
impl_ = std::move(cand);
impl_is_wgc_plugin_ = false;
RebuildAliasesFromImpl();
if (requested_monitor > 0 &&
impl_->SwitchTo(requested_monitor) != 0) {
monitor_index_.store(0, std::memory_order_relaxed);
}
return true;
}
return false;
};
bool fallback_started = false;
if (impl_is_wgc_plugin_) {
if (try_init_start(std::make_unique<ScreenCapturerDxgi>())) {
LOG_INFO("Windows capturer: fallback to DXGI");
fallback_started = true;
} else if (try_init_start(std::make_unique<ScreenCapturerGdi>())) {
LOG_INFO("Windows capturer: fallback to GDI");
fallback_started = true;
}
} else if (dynamic_cast<ScreenCapturerDxgi*>(impl_.get())) {
if (try_init_start(std::make_unique<ScreenCapturerGdi>())) {
LOG_INFO("Windows capturer: fallback to GDI");
fallback_started = true;
}
if (dynamic_cast<ScreenCapturerDxgi*>(impl_.get()) &&
TryStartBackend(WgcPluginCapturer::Create(), requested_monitor)) {
fallback_started = true;
}
if (!fallback_started && (dynamic_cast<WgcPluginCapturer*>(impl_.get()) ||
dynamic_cast<ScreenCapturerDxgi*>(impl_.get()))) {
fallback_started = TryStartBackend(std::make_unique<ScreenCapturerGdi>(),
requested_monitor);
}
if (!fallback_started) {
@@ -568,6 +597,7 @@ int ScreenCapturerWin::Start(bool show_cursor) {
}
running_.store(true, std::memory_order_relaxed);
NotifyPrivacyCapture(true);
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false,
std::memory_order_relaxed);
@@ -581,6 +611,7 @@ int ScreenCapturerWin::Start(bool show_cursor) {
}
int ScreenCapturerWin::Stop() {
NotifyPrivacyCapture(false);
running_.store(false, std::memory_order_relaxed);
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false,
@@ -610,9 +641,12 @@ int ScreenCapturerWin::Resume(int monitor_index) {
int ScreenCapturerWin::SwitchTo(int monitor_index) {
if (!impl_) return -1;
if (privacy_) privacy_->DisplayChanging();
const int ret = impl_->SwitchTo(monitor_index);
if (ret == 0) {
monitor_index_.store(monitor_index, std::memory_order_relaxed);
} else if (privacy_) {
privacy_->Fail("Display switch failed; remote operation paused");
}
return ret;
}
@@ -735,53 +769,19 @@ void ScreenCapturerWin::StopSecureCaptureThread() {
}
bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
NotifyPrivacyCapture(false);
if (!impl_ || !running_.load(std::memory_order_relaxed)) {
return false;
}
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
const int current_monitor = monitor_index_.load(std::memory_order_relaxed);
auto restore_monitor = [&]() {
RebuildAliasesFromImpl();
if (current_monitor > 0 && impl_->SwitchTo(current_monitor) != 0) {
monitor_index_.store(0, std::memory_order_relaxed);
}
};
auto try_started_backend = [&](std::unique_ptr<ScreenCapturer> cand,
const char* name,
bool is_wgc_plugin) -> bool {
if (!cand) {
return false;
}
const int init_ret = cand->Init(fps_, cb_);
if (init_ret != 0) {
LOG_WARN("Windows capturer: {} init after secure desktop failed (ret={})",
name, init_ret);
return false;
}
const int start_ret = cand->Start(show_cursor);
if (start_ret != 0) {
LOG_WARN(
"Windows capturer: {} start after secure desktop failed (ret={})",
name, start_ret);
cand->Destroy();
return false;
}
if (impl_) {
impl_->Destroy();
}
impl_ = std::move(cand);
impl_is_wgc_plugin_ = is_wgc_plugin;
restore_monitor();
LOG_INFO("Windows capturer: restarted {} after secure desktop", name);
return true;
};
LOG_INFO("Windows capturer: restarting capture backend after secure desktop");
impl_->Stop();
int ret = impl_->Start(show_cursor);
if (ret == 0) {
restore_monitor();
RestoreMonitor(current_monitor);
return true;
}
@@ -795,20 +795,14 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
ret = impl_->Start(show_cursor);
}
if (ret == 0) {
restore_monitor();
RestoreMonitor(current_monitor);
return true;
}
if (impl_is_wgc_plugin_ &&
try_started_backend(WgcPluginCapturer::Create(), "WGC plugin", true)) {
return true;
}
if (try_started_backend(std::make_unique<ScreenCapturerDxgi>(), "DXGI",
false)) {
return true;
}
if (try_started_backend(std::make_unique<ScreenCapturerGdi>(), "GDI",
false)) {
if (TryStartBackend(std::make_unique<ScreenCapturerDxgi>(),
current_monitor) ||
TryStartBackend(WgcPluginCapturer::Create(), current_monitor) ||
TryStartBackend(std::make_unique<ScreenCapturerGdi>(), current_monitor)) {
return true;
}
@@ -1123,6 +1117,14 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
std::vector<uint8_t> secure_frame;
while (running_.load(std::memory_order_relaxed)) {
// A privacy session must NEVER continue using the helper's secure-desktop
// GDI frames. Its HWNDs belong to the ordinary interactive desktop only.
if (privacy_ && privacy_->Engaged()) {
if (!IsWindowsPrivacyDesktopAvailable())
privacy_->Fail("Secure desktop or lock screen; remote operation paused. Privacy cannot cover Windows security UI.");
std::this_thread::sleep_for(std::chrono::milliseconds(25));
continue;
}
if (paused_.load(std::memory_order_relaxed)) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
@@ -1295,7 +1297,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
if (frame_delivered && !secure_frame.empty()) {
EmitCapturedFrame(secure_frame.data(),
static_cast<int>(secure_frame.size()), captured_width,
captured_height, display_name.c_str());
captured_height, display_name.c_str(), nullptr, true);
}
if (!frame_delivered && !frame_pending) {
@@ -20,6 +20,7 @@
#include <vector>
#include "screen_capturer.h"
#include "privacy_controller.h"
namespace crossdesk {
@@ -43,10 +44,12 @@ class ScreenCapturerWin : public ScreenCapturer {
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
void SetPrivacyController(PrivacyController* privacy) { privacy_ = privacy; }
private:
std::unique_ptr<ScreenCapturer> impl_;
bool impl_is_wgc_plugin_ = false;
PrivacyController* privacy_ = nullptr;
void NotifyPrivacyCapture(bool running);
int fps_ = 60;
cb_desktop_data cb_;
cb_desktop_data cb_orig_;
@@ -86,9 +89,13 @@ class ScreenCapturerWin : public ScreenCapturer {
void BuildCanonicalFromImpl();
void RebuildAliasesFromImpl();
void RestoreMonitor(int monitor_index);
bool TryStartBackend(std::unique_ptr<ScreenCapturer> candidate,
int monitor_index);
void EmitCapturedFrame(unsigned char* data, int size, int width, int height,
const char* stream_id,
const MiniRtcNativeVideoFrame* native_frame = nullptr);
const MiniRtcNativeVideoFrame* native_frame = nullptr,
bool from_secure_desktop = false);
void StopSecureCaptureThread();
bool RestartCaptureBackendAfterSecureDesktop();
void SecureDesktopCaptureLoop();
@@ -339,6 +339,7 @@ void WgcSessionImpl::OnClosed(
winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
winrt::Windows::Foundation::IInspectable const&) {
std::lock_guard locker(lock_);
if (observer_) observer_->OnFrame({}, id_);
const bool was_running = is_running_;
const bool was_paused = is_paused_;
try {
@@ -0,0 +1,53 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-10
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOWS_THREAD_DPI_H_
#define _WINDOWS_THREAD_DPI_H_
#include <Windows.h>
namespace crossdesk {
// Keep newer DPI entry points out of the import table: privacy capability
// checks must still be able to report unsupported on pre-2004 Windows.
class ScopedWindowsPhysicalCoordinates {
public:
using SetContext = DPI_AWARENESS_CONTEXT(WINAPI*)(DPI_AWARENESS_CONTEXT);
ScopedWindowsPhysicalCoordinates() {
setter_ = reinterpret_cast<SetContext>(GetProcAddress(
GetModuleHandleW(L"user32.dll"), "SetThreadDpiAwarenessContext"));
if (setter_) {
previous_ = setter_(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
// Ordinary remote input retains compatibility with Windows 10 1607.
// The privacy backend itself requires build 19041 before using this.
if (!previous_)
previous_ = setter_(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE);
}
}
~ScopedWindowsPhysicalCoordinates() {
if (setter_ && previous_) setter_(previous_);
}
ScopedWindowsPhysicalCoordinates(const ScopedWindowsPhysicalCoordinates&) =
delete;
ScopedWindowsPhysicalCoordinates& operator=(
const ScopedWindowsPhysicalCoordinates&) = delete;
bool active() const { return previous_ != nullptr; }
private:
SetContext setter_ = nullptr;
DPI_AWARENESS_CONTEXT previous_ = nullptr;
};
inline UINT PrivacyWindowDpi(HWND hwnd) {
using GetDpi = UINT(WINAPI*)(HWND);
static const auto get_dpi = reinterpret_cast<GetDpi>(
GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"));
return get_dpi ? get_dpi(hwnd) : 96;
}
} // namespace crossdesk
#endif // _WINDOWS_THREAD_DPI_H_