[refactor] reorganize platform and wire sources

This commit is contained in:
dijunkun
2026-09-01 01:29:30 +08:00
parent fb0ab4f14e
commit 90184f5251
354 changed files with 2837 additions and 2456 deletions
@@ -0,0 +1,69 @@
#include "platform/autostart_backend.h"
#include <windows.h>
#include <filesystem>
#include <string>
namespace crossdesk::platform {
namespace {
constexpr const char* kRunKey =
"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
} // namespace
std::string GetAutostartExecutablePath() {
char path[32768] = {};
const DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path));
return length > 0 && length < sizeof(path) ? std::string(path, length)
: std::string();
}
bool EnableAutostart(const std::string& app_name,
const std::string& executable_path) {
if (!std::filesystem::exists(executable_path)) return false;
HKEY key = nullptr;
if (RegOpenKeyExA(HKEY_CURRENT_USER, kRunKey, 0, KEY_WRITE, &key) !=
ERROR_SUCCESS) {
return false;
}
std::string value = executable_path;
if (value.find(' ') != std::string::npos &&
(value.front() != '"' || value.back() != '"')) {
value = "\"" + value + "\"";
}
const LONG result = RegSetValueExA(
key, app_name.c_str(), 0, REG_SZ,
reinterpret_cast<const BYTE*>(value.c_str()),
static_cast<DWORD>(value.size() + 1));
RegCloseKey(key);
return result == ERROR_SUCCESS;
}
bool DisableAutostart(const std::string& app_name) {
HKEY key = nullptr;
if (RegOpenKeyExA(HKEY_CURRENT_USER, kRunKey, 0, KEY_WRITE, &key) !=
ERROR_SUCCESS) {
return false;
}
const LONG result = RegDeleteValueA(key, app_name.c_str());
RegCloseKey(key);
return result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND;
}
bool IsAutostartEnabled(const std::string& app_name) {
HKEY key = nullptr;
if (RegOpenKeyExA(HKEY_CURRENT_USER, kRunKey, 0, KEY_READ, &key) !=
ERROR_SUCCESS) {
return false;
}
const LONG result =
RegQueryValueExA(key, app_name.c_str(), nullptr, nullptr, nullptr, nullptr);
RegCloseKey(key);
return result == ERROR_SUCCESS;
}
} // namespace crossdesk::platform
@@ -0,0 +1,139 @@
#include "clipboard.h"
#include <windows.h>
#include <string>
#include "platform/clipboard_backend.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
HWND g_clipboard_window = nullptr;
constexpr const char* kClipboardWindowClass = "CrossDeskClipboardMonitor";
LRESULT CALLBACK ClipboardWindowProc(HWND window, UINT message, WPARAM wparam,
LPARAM lparam) {
if (message == WM_CLIPBOARDUPDATE) {
platform::HandleClipboardChange();
return 0;
}
return DefWindowProc(window, message, wparam, lparam);
}
} // namespace
std::string Clipboard::GetText() {
if (!OpenClipboard(nullptr)) {
LOG_ERROR("Clipboard::GetText: failed to open clipboard");
return {};
}
std::string result;
HANDLE data = GetClipboardData(CF_UNICODETEXT);
if (data) {
const wchar_t* text = static_cast<const wchar_t*>(GlobalLock(data));
if (text) {
const int size =
WideCharToMultiByte(CP_UTF8, 0, text, -1, nullptr, 0, nullptr, nullptr);
if (size > 1) {
result.resize(static_cast<size_t>(size));
WideCharToMultiByte(CP_UTF8, 0, text, -1, result.data(), size,
nullptr, nullptr);
result.resize(static_cast<size_t>(size - 1));
}
GlobalUnlock(data);
}
}
CloseClipboard();
return result;
}
bool Clipboard::SetText(const std::string& text) {
if (!OpenClipboard(nullptr)) {
LOG_ERROR("Clipboard::SetText: failed to open clipboard");
return false;
}
if (!EmptyClipboard()) {
LOG_ERROR("Clipboard::SetText: failed to empty clipboard");
CloseClipboard();
return false;
}
const int size =
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, nullptr, 0);
HGLOBAL memory =
size > 0 ? GlobalAlloc(GMEM_MOVEABLE, size * sizeof(wchar_t)) : nullptr;
if (!memory) {
LOG_ERROR("Clipboard::SetText: failed to allocate memory");
CloseClipboard();
return false;
}
wchar_t* output = static_cast<wchar_t*>(GlobalLock(memory));
if (!output) {
GlobalFree(memory);
CloseClipboard();
return false;
}
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, output, size);
GlobalUnlock(memory);
if (!SetClipboardData(CF_UNICODETEXT, memory)) {
GlobalFree(memory);
CloseClipboard();
return false;
}
CloseClipboard();
return true;
}
bool Clipboard::HasText() {
if (!OpenClipboard(nullptr)) return false;
const bool result = IsClipboardFormatAvailable(CF_UNICODETEXT) ||
IsClipboardFormatAvailable(CF_TEXT);
CloseClipboard();
return result;
}
namespace platform {
void RunClipboardMonitor(int) {
WNDCLASSA window_class = {};
window_class.lpfnWndProc = ClipboardWindowProc;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.lpszClassName = kClipboardWindowClass;
RegisterClassA(&window_class);
g_clipboard_window = CreateWindowA(
kClipboardWindowClass, nullptr, 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
nullptr, nullptr);
if (!g_clipboard_window ||
!AddClipboardFormatListener(g_clipboard_window)) {
LOG_ERROR("Failed to initialize clipboard monitor window");
if (g_clipboard_window) DestroyWindow(g_clipboard_window);
g_clipboard_window = nullptr;
return;
}
LOG_INFO("Clipboard event monitoring started (Windows)");
MSG message = {};
while (ClipboardMonitoring()) {
const BOOL result = GetMessage(&message, nullptr, 0, 0);
if (result == 0 || result == -1) break;
TranslateMessage(&message);
DispatchMessage(&message);
}
RemoveClipboardFormatListener(g_clipboard_window);
DestroyWindow(g_clipboard_window);
g_clipboard_window = nullptr;
UnregisterClassA(kClipboardWindowClass, GetModuleHandle(nullptr));
}
void WakeClipboardMonitor() {
if (g_clipboard_window) PostMessage(g_clipboard_window, WM_QUIT, 0, 0);
}
} // namespace platform
} // namespace crossdesk
@@ -0,0 +1,64 @@
#include "platform/daemon_backend.h"
#include <windows.h>
#include <atomic>
#include <chrono>
#include <thread>
#include <vector>
namespace crossdesk::platform {
namespace {
std::atomic<bool> g_stop_requested{false};
} // namespace
void ResetDaemonStop() { g_stop_requested.store(false); }
void RequestDaemonStop() { g_stop_requested.store(true); }
bool DaemonStopRequested() { return g_stop_requested.load(); }
bool PrepareDaemon() { return true; }
std::string GetDaemonExecutablePath() {
char path[32768] = {};
const DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path));
return length > 0 && length < sizeof(path) ? std::string(path, length)
: std::string();
}
DaemonChildResult RunDaemonChild(const std::string& executable_path,
const std::function<bool()>& keep_running) {
DaemonChildResult result;
STARTUPINFOA startup = {sizeof(startup)};
PROCESS_INFORMATION process = {};
std::string command = "\"" + executable_path + "\" --child";
std::vector<char> command_buffer(command.begin(), command.end());
command_buffer.push_back('\0');
if (!CreateProcessA(nullptr, command_buffer.data(), nullptr, nullptr, FALSE,
0, nullptr, nullptr, &startup, &process)) {
return result;
}
result.started = true;
while (keep_running()) {
const DWORD wait_result = WaitForSingleObject(process.hProcess, 200);
if (wait_result == WAIT_OBJECT_0 || wait_result == WAIT_FAILED) break;
}
if (!keep_running()) {
TerminateProcess(process.hProcess, 1);
WaitForSingleObject(process.hProcess, 3000);
}
DWORD exit_code = 0;
GetExitCodeProcess(process.hProcess, &exit_code);
result.exit_code = static_cast<int>(exit_code);
result.normal_exit = exit_code == 0;
CloseHandle(process.hProcess);
CloseHandle(process.hThread);
return result;
}
} // namespace crossdesk::platform
@@ -0,0 +1,144 @@
#include "application/gui_application.h"
#if _WIN32 && CROSSDESK_PORTABLE
#include <Windows.h>
#include <shellapi.h>
#include <filesystem>
#include <vector>
#include "rd_log.h"
#include "service_host.h"
namespace crossdesk {
namespace {
std::filesystem::path GetCurrentExecutablePath() {
std::vector<wchar_t> buffer(MAX_PATH);
while (true) {
const DWORD length = GetModuleFileNameW(
nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (length == 0) {
return {};
}
if (length < buffer.size()) {
return std::filesystem::path(buffer.data(), buffer.data() + length);
}
if (buffer.size() >= 32768) {
return {};
}
buffer.resize(buffer.size() * 2);
}
}
bool InstallServiceWithElevation() {
const std::filesystem::path executable_path = GetCurrentExecutablePath();
if (executable_path.empty()) {
LOG_ERROR("Portable service install failed: current executable not found");
return false;
}
const std::filesystem::path service_path =
executable_path.parent_path() / L"crossdesk_service.exe";
const std::filesystem::path helper_path =
executable_path.parent_path() / L"crossdesk_session_helper.exe";
if (!std::filesystem::exists(service_path) ||
!std::filesystem::exists(helper_path)) {
LOG_ERROR("Portable service install failed: service binaries missing, "
"service={}, helper={}",
service_path.string(), helper_path.string());
return false;
}
const std::wstring executable = executable_path.wstring();
const std::wstring working_dir = executable_path.parent_path().wstring();
constexpr wchar_t parameters[] = L"--service-install";
SHELLEXECUTEINFOW execute_info{};
execute_info.cbSize = sizeof(execute_info);
execute_info.fMask = SEE_MASK_NOCLOSEPROCESS;
execute_info.lpVerb = L"runas";
execute_info.lpFile = executable.c_str();
execute_info.lpParameters = parameters;
execute_info.lpDirectory = working_dir.c_str();
execute_info.nShow = SW_HIDE;
if (!ShellExecuteExW(&execute_info)) {
LOG_ERROR("Portable service install failed: ShellExecuteExW error={}",
GetLastError());
return false;
}
const DWORD wait_result =
WaitForSingleObject(execute_info.hProcess, INFINITE);
DWORD exit_code = 1;
if (wait_result == WAIT_OBJECT_0) {
GetExitCodeProcess(execute_info.hProcess, &exit_code);
} else {
LOG_ERROR("Portable service install wait failed, result={}", wait_result);
}
CloseHandle(execute_info.hProcess);
if (exit_code != 0) {
LOG_ERROR("Portable service install command failed, exit_code={}",
exit_code);
return false;
}
const bool started = StartCrossDeskService();
if (!started) {
LOG_WARN("Portable service installed but start failed");
}
return IsCrossDeskServiceInstalled() && started;
}
} // namespace
void GuiApplication::CheckPortableWindowsService() {
if (portable_service_prompt_checked_) {
return;
}
portable_service_prompt_checked_ = true;
portable_service_installed_ = IsCrossDeskServiceInstalled();
if (portable_service_installed_ || portable_service_prompt_suppressed_) {
return;
}
portable_service_install_state_.store(PortableServiceInstallState::idle,
std::memory_order_relaxed);
show_portable_service_install_window_ = true;
}
void GuiApplication::StartPortableWindowsServiceInstall() {
portable_service_do_not_remind_ = false;
PortableServiceInstallState expected = PortableServiceInstallState::idle;
if (!portable_service_install_state_.compare_exchange_strong(
expected, PortableServiceInstallState::installing,
std::memory_order_acq_rel)) {
if (expected != PortableServiceInstallState::failed) {
return;
}
portable_service_install_state_.store(
PortableServiceInstallState::installing, std::memory_order_release);
}
JoinPortableWindowsServiceInstallThread();
portable_service_install_thread_ = std::thread([this]() {
const bool installed = InstallServiceWithElevation();
portable_service_install_state_.store(
installed ? PortableServiceInstallState::succeeded
: PortableServiceInstallState::failed,
std::memory_order_release);
});
}
void GuiApplication::JoinPortableWindowsServiceInstallThread() {
if (portable_service_install_thread_.joinable()) {
portable_service_install_thread_.join();
}
}
} // namespace crossdesk
#endif
@@ -0,0 +1,231 @@
#include "runtime/gui_runtime.h"
#include <remote_action.h>
#include <algorithm>
#include <cstring>
#include <shared_mutex>
#include <string>
#include "rd_log.h"
#if _WIN32
#include "interactive_state.h"
#include "service_host.h"
#endif
namespace crossdesk {
namespace {
#if _WIN32
struct WindowsServiceInteractiveStatus {
bool available = false;
bool sas_secure_desktop_grace_active = false;
unsigned int error_code = 0;
std::string interactive_stage;
std::string error;
};
constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000;
constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000;
constexpr DWORD kWindowsServiceQueryTimeoutMs = 500;
constexpr DWORD kWindowsServiceSasTimeoutMs = 500;
bool IsTransientWindowsServiceStatusError(const std::string &error) {
return error == "pipe_unavailable" || error == "pipe_connect_failed" ||
error == "pipe_read_failed";
}
RemoteAction
BuildWindowsServiceStatusAction(const WindowsServiceInteractiveStatus &status) {
RemoteAction action{};
action.type = ControlType::service_status;
action.ss.available = status.available;
std::strncpy(action.ss.interactive_stage, status.interactive_stage.c_str(),
sizeof(action.ss.interactive_stage) - 1);
action.ss.interactive_stage[sizeof(action.ss.interactive_stage) - 1] = '\0';
return action;
}
bool QueryWindowsServiceInteractiveStatus(
WindowsServiceInteractiveStatus *status) {
if (status == nullptr) {
return false;
}
*status = WindowsServiceInteractiveStatus{};
const std::string response =
QueryCrossDeskService("status", kWindowsServiceQueryTimeoutMs);
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.is_object()) {
status->error = "invalid_service_status_json";
return false;
}
status->available = json.value("ok", false);
if (!status->available) {
status->error = json.value("error", std::string("service_unavailable"));
status->error_code = json.value("code", 0u);
return true;
}
status->interactive_stage = json.value("interactive_stage", std::string());
status->sas_secure_desktop_grace_active =
json.value("sas_secure_desktop_grace_active", false);
if (ShouldNormalizeUnlockToUserDesktop(
json.value("interactive_lock_screen_visible", false),
status->interactive_stage, json.value("session_locked", false),
json.value("interactive_logon_ui_visible", false),
json.value("interactive_secure_desktop_active",
json.value("secure_desktop_active", false)),
json.value("credential_ui_visible", false),
json.value("password_box_visible", false),
json.value("unlock_ui_visible", false),
json.value("last_session_event", std::string()))) {
status->interactive_stage = "user-desktop";
}
return true;
}
#endif
} // namespace
void GuiRuntime::HandleWindowsServiceIntegration() {
#if _WIN32
static bool last_logged_service_available = true;
static unsigned int last_logged_service_error_code = 0;
static std::string last_logged_service_error;
if (!is_server_mode_ || peer_ == nullptr) {
ResetLocalWindowsServiceState(true);
return;
}
const bool has_connected_remote = [&] {
std::shared_lock lock(connection_status_mutex_);
return std::any_of(connection_status_.begin(), connection_status_.end(),
[](const auto &entry) {
return entry.second == ConnectionStatus::Connected;
});
}();
if (!has_connected_remote) {
ResetLocalWindowsServiceState(false);
return;
}
bool force_broadcast = false;
if (pending_windows_service_sas_.exchange(false, std::memory_order_relaxed)) {
const std::string response =
QueryCrossDeskService("sas", kWindowsServiceSasTimeoutMs);
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
LOG_WARN("Remote SAS request failed: {}", response);
} else {
LOG_INFO("Remote SAS request forwarded to local Windows service");
optimistic_windows_secure_desktop_until_tick_ =
static_cast<uint32_t>(SDL_GetTicks()) +
kWindowsServiceSasSecureDesktopGraceMs;
local_service_status_received_ = true;
local_service_available_ = true;
local_interactive_stage_ = "secure-desktop";
}
last_windows_service_status_tick_ = 0;
force_broadcast = true;
}
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (!force_broadcast && last_windows_service_status_tick_ != 0 &&
now - last_windows_service_status_tick_ <
kWindowsServiceStatusIntervalMs) {
return;
}
last_windows_service_status_tick_ = now;
WindowsServiceInteractiveStatus status;
const bool status_ok = QueryWindowsServiceInteractiveStatus(&status);
WindowsServiceInteractiveStatus broadcast_status = status;
const bool previous_secure_desktop_interaction =
IsSecureDesktopInteractionRequired(local_interactive_stage_);
const bool optimistic_secure_desktop_active =
optimistic_windows_secure_desktop_until_tick_ != 0 &&
static_cast<int32_t>(optimistic_windows_secure_desktop_until_tick_ -
now) > 0;
const bool keep_optimistic_secure_desktop =
status_ok && status.available && optimistic_secure_desktop_active &&
status.sas_secure_desktop_grace_active &&
status.interactive_stage == "user-desktop";
local_service_status_received_ =
status_ok || previous_secure_desktop_interaction;
local_service_available_ = status.available;
if (status.available) {
if (keep_optimistic_secure_desktop) {
local_interactive_stage_ = "secure-desktop";
broadcast_status.interactive_stage = local_interactive_stage_;
} else {
local_interactive_stage_ = status.interactive_stage;
optimistic_windows_secure_desktop_until_tick_ = 0;
}
} else if (!previous_secure_desktop_interaction) {
local_interactive_stage_.clear();
optimistic_windows_secure_desktop_until_tick_ = 0;
}
if (status_ok) {
const bool availability_changed =
status.available != last_logged_service_available;
const bool error_changed =
!status.available &&
(status.error != last_logged_service_error ||
status.error_code != last_logged_service_error_code);
if (availability_changed || error_changed) {
if (status.available) {
LOG_INFO(
"Local Windows service available for secure desktop integration");
} else if (IsTransientWindowsServiceStatusError(status.error)) {
LOG_INFO("Local Windows service temporarily unavailable, keeping last "
"secure desktop state: error={}, code={}",
status.error, status.error_code);
} else {
LOG_WARN(
"Local Windows service unavailable, secure desktop integration "
"disabled: error={}, code={}",
status.error, status.error_code);
}
last_logged_service_available = status.available;
last_logged_service_error = status.error;
last_logged_service_error_code = status.error_code;
}
} else if (last_logged_service_available ||
last_logged_service_error != "invalid_service_status_json") {
LOG_WARN(
"Local Windows service status query failed, secure desktop integration "
"disabled");
last_logged_service_available = false;
last_logged_service_error = "invalid_service_status_json";
last_logged_service_error_code = 0;
}
RemoteAction remote_action =
BuildWindowsServiceStatusAction(broadcast_status);
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(),
control_data_label_.c_str());
if (ret != 0) {
LOG_WARN("Broadcast Windows service status failed, ret={}", ret);
}
#endif
}
#if _WIN32
void GuiRuntime::ResetLocalWindowsServiceState(bool clear_pending_sas) {
last_windows_service_status_tick_ = 0;
if (clear_pending_sas) {
pending_windows_service_sas_.store(false, std::memory_order_relaxed);
}
local_service_status_received_ = false;
local_service_available_ = false;
local_interactive_stage_.clear();
optimistic_windows_secure_desktop_until_tick_ = 0;
}
#endif
} // namespace crossdesk
@@ -0,0 +1,169 @@
#include "win_tray.h"
#include <SDL3/SDL.h>
#include "localization.h"
#include <utility>
namespace crossdesk {
// callback for the message-only window that handles tray icon messages
static LRESULT CALLBACK MsgWndProc(HWND hwnd, UINT msg, WPARAM wParam,
LPARAM lParam) {
WinTray* tray =
reinterpret_cast<WinTray*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
if (!tray) {
return DefWindowProc(hwnd, msg, wParam, lParam);
}
if (msg == WM_TRAY_CALLBACK) {
MSG tmpMsg = {};
tmpMsg.message = msg;
tmpMsg.wParam = wParam;
tmpMsg.lParam = lParam;
tray->HandleTrayMessage(&tmpMsg);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
WinTray::WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
int language_index)
: app_hwnd_(app_hwnd),
icon_(icon),
tip_(tooltip),
hwnd_message_only_(nullptr),
language_index_(language_index) {
WNDCLASS wc = {};
wc.lpfnWndProc = MsgWndProc;
wc.hInstance = GetModuleHandle(nullptr);
wc.lpszClassName = L"TrayMessageWindow";
RegisterClass(&wc);
// create a message-only window to receive tray messages
hwnd_message_only_ =
CreateWindowEx(0, wc.lpszClassName, L"TrayMsg", 0, 0, 0, 0, 0,
HWND_MESSAGE, nullptr, wc.hInstance, nullptr);
// store pointer to this WinTray instance in window data
SetWindowLongPtr(hwnd_message_only_, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(this));
// initialize NOTIFYICONDATA structure
ZeroMemory(&nid_, sizeof(nid_));
nid_.cbSize = sizeof(nid_);
nid_.hWnd = hwnd_message_only_;
nid_.uID = 1;
nid_.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nid_.uCallbackMessage = WM_TRAY_CALLBACK;
nid_.hIcon = icon_;
wcsncpy_s(nid_.szTip, tip_.c_str(), _TRUNCATE);
}
WinTray::WinTray(std::function<void()> show_window,
std::function<void()> hide_window,
std::function<void()> open_settings,
std::function<void()> exit_app, HICON icon,
const std::wstring& tooltip, int language_index)
: WinTray(nullptr, icon, tooltip, language_index) {
show_window_ = std::move(show_window);
hide_window_ = std::move(hide_window);
open_settings_ = std::move(open_settings);
exit_app_ = std::move(exit_app);
}
WinTray::~WinTray() {
RemoveTrayIcon();
if (hwnd_message_only_) DestroyWindow(hwnd_message_only_);
}
void WinTray::MinimizeToTray() {
Shell_NotifyIcon(NIM_ADD, &nid_);
if (hide_window_) {
hide_window_();
} else if (app_hwnd_) {
ShowWindow(app_hwnd_, SW_HIDE);
}
}
void WinTray::RemoveTrayIcon() { Shell_NotifyIcon(NIM_DELETE, &nid_); }
void WinTray::ShowApplicationWindow() {
if (show_window_) {
show_window_();
} else if (app_hwnd_) {
ShowWindow(app_hwnd_, SW_SHOW);
SetForegroundWindow(app_hwnd_);
}
}
void WinTray::OpenSettings() {
if (open_settings_) {
open_settings_();
} else {
ShowApplicationWindow();
}
}
bool WinTray::HandleTrayMessage(MSG* msg) {
if (!msg || msg->message != WM_TRAY_CALLBACK) return false;
switch (LOWORD(msg->lParam)) {
case WM_LBUTTONDBLCLK:
case WM_LBUTTONUP: {
ShowApplicationWindow();
break;
}
case WM_RBUTTONUP: {
POINT pt;
GetCursorPos(&pt);
HMENU menu = CreatePopupMenu();
HBRUSH menu_background = CreateSolidBrush(RGB(255, 255, 255));
if (menu_background) {
MENUINFO menu_info{};
menu_info.cbSize = sizeof(menu_info);
menu_info.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS;
menu_info.hbrBack = menu_background;
SetMenuInfo(menu, &menu_info);
}
AppendMenuW(menu, MF_STRING, 1002,
localization::GetShowMainWindowLabel(language_index_));
AppendMenuW(menu, MF_STRING, 1003,
localization::GetSettingsLabel(language_index_));
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
AppendMenuW(menu, MF_STRING, 1001,
localization::GetExitProgramLabel(language_index_));
SetForegroundWindow(hwnd_message_only_);
int cmd =
TrackPopupMenu(menu, TPM_RETURNCMD | TPM_NONOTIFY | TPM_LEFTALIGN,
pt.x, pt.y, 0, hwnd_message_only_, nullptr);
DestroyMenu(menu);
if (menu_background) {
DeleteObject(menu_background);
}
// handle menu command
if (cmd == 1001) {
if (exit_app_) {
exit_app_();
} else {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
} else if (cmd == 1002) {
ShowApplicationWindow();
} else if (cmd == 1003) {
OpenSettings();
}
break;
}
}
return true;
}
} // namespace crossdesk
@@ -0,0 +1,51 @@
/*
* @Author: DI JUNKUN
* @Date: 2025-10-22
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WIN_TRAY_H_
#define _WIN_TRAY_H_
#include <Windows.h>
#include <shellapi.h>
#include <functional>
#include <string>
#define WM_TRAY_CALLBACK (WM_USER + 1)
namespace crossdesk {
class WinTray {
public:
WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
int language_index);
WinTray(std::function<void()> show_window,
std::function<void()> hide_window,
std::function<void()> open_settings,
std::function<void()> exit_app,
HICON icon, const std::wstring& tooltip, int language_index);
~WinTray();
void MinimizeToTray();
void RemoveTrayIcon();
bool HandleTrayMessage(MSG* msg);
private:
void ShowApplicationWindow();
void OpenSettings();
HWND app_hwnd_;
HWND hwnd_message_only_;
HICON icon_;
std::wstring tip_;
int language_index_;
NOTIFYICONDATA nid_;
std::function<void()> show_window_;
std::function<void()> hide_window_;
std::function<void()> open_settings_;
std::function<void()> exit_app_;
};
} // namespace crossdesk
#endif
@@ -0,0 +1,19 @@
#include "device_controller_factory.h"
#include "keyboard_capturer.h"
#include "mouse_controller.h"
namespace crossdesk {
DeviceController* DeviceControllerFactory::Create(Device device) {
switch (device) {
case Mouse:
return new PlatformMouseController();
case Keyboard:
return new PlatformKeyboardCapturer();
default:
return nullptr;
}
}
} // namespace crossdesk
@@ -0,0 +1,316 @@
#include "keyboard_capturer.h"
#include <hidusage.h>
#include "rd_log.h"
#include "windows_input_marker.h"
namespace crossdesk {
namespace {
constexpr wchar_t kRawInputWindowClassName[] =
L"CrossDeskKeyboardRawInputWindow";
bool PreferSideSpecificVkInjection(int key_code) {
switch (key_code) {
case VK_LSHIFT:
case VK_RSHIFT:
case VK_LCONTROL:
case VK_RCONTROL:
case VK_LMENU:
case VK_RMENU:
case VK_LWIN:
case VK_RWIN:
return true;
default:
return false;
}
}
} // namespace
PlatformKeyboardCapturer::PlatformKeyboardCapturer() {}
PlatformKeyboardCapturer::~PlatformKeyboardCapturer() { Unhook(); }
int PlatformKeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
if (capture_thread_.joinable()) {
return 0;
}
on_key_action_ = on_key_action;
user_ptr_ = user_ptr;
{
std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id_ = 0;
capture_start_complete_ = false;
capture_start_succeeded_ = false;
}
capture_thread_ = std::thread(&PlatformKeyboardCapturer::RawInputThreadMain, this);
std::unique_lock<std::mutex> lock(capture_state_mutex_);
capture_start_condition_.wait(
lock, [this] { return capture_start_complete_; });
const bool capture_started = capture_start_succeeded_;
lock.unlock();
if (!capture_started) {
if (capture_thread_.joinable()) {
capture_thread_.join();
}
on_key_action_ = nullptr;
user_ptr_ = nullptr;
return -1;
}
return 0;
}
int PlatformKeyboardCapturer::Unhook() {
DWORD capture_thread_id = 0;
{
std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id = capture_thread_id_;
}
if (capture_thread_id != 0 &&
!PostThreadMessageW(capture_thread_id, WM_QUIT, 0, 0)) {
LOG_WARN("Failed to stop keyboard raw input thread, thread_id={}, error={}",
capture_thread_id, GetLastError());
}
if (capture_thread_.joinable()) {
capture_thread_.join();
}
on_key_action_ = nullptr;
user_ptr_ = nullptr;
return 0;
}
void PlatformKeyboardCapturer::RawInputThreadMain() {
const DWORD thread_id = GetCurrentThreadId();
MSG message{};
PeekMessageW(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
const bool capture_started = CreateRawInputWindow();
{
std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id_ = thread_id;
capture_start_succeeded_ = capture_started;
capture_start_complete_ = true;
}
capture_start_condition_.notify_one();
if (!capture_started) {
std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id_ = 0;
return;
}
LOG_INFO("Keyboard raw input capture started, thread_id={}", thread_id);
while (true) {
const BOOL get_message_result = GetMessageW(&message, nullptr, 0, 0);
if (get_message_result <= 0) {
if (get_message_result < 0) {
LOG_WARN("Keyboard raw input message loop failed, thread_id={}, "
"error={}",
thread_id, GetLastError());
}
break;
}
TranslateMessage(&message);
DispatchMessageW(&message);
}
DestroyRawInputWindow();
{
std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id_ = 0;
}
LOG_INFO("Keyboard raw input capture stopped, thread_id={}", thread_id);
}
bool PlatformKeyboardCapturer::CreateRawInputWindow() {
const HINSTANCE instance = GetModuleHandleW(nullptr);
WNDCLASSEXW window_class{};
window_class.cbSize = sizeof(window_class);
window_class.lpfnWndProc = &PlatformKeyboardCapturer::RawInputWindowProc;
window_class.hInstance = instance;
window_class.lpszClassName = kRawInputWindowClassName;
if (RegisterClassExW(&window_class) == 0) {
const DWORD error = GetLastError();
if (error != ERROR_CLASS_ALREADY_EXISTS) {
LOG_WARN("Failed to register keyboard raw input window class, error={}",
error);
return false;
}
}
raw_input_window_ = CreateWindowExW(
0, kRawInputWindowClassName, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
instance, this);
if (!raw_input_window_) {
LOG_WARN("Failed to create keyboard raw input window, error={}",
GetLastError());
return false;
}
RAWINPUTDEVICE keyboard_device{};
keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC;
keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD;
keyboard_device.dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
keyboard_device.hwndTarget = raw_input_window_;
if (!RegisterRawInputDevices(&keyboard_device, 1,
sizeof(keyboard_device))) {
LOG_WARN("Failed to register keyboard raw input, error={}", GetLastError());
DestroyWindow(raw_input_window_);
raw_input_window_ = nullptr;
return false;
}
raw_input_registered_ = true;
return true;
}
void PlatformKeyboardCapturer::DestroyRawInputWindow() {
if (raw_input_registered_) {
RAWINPUTDEVICE keyboard_device{};
keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC;
keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD;
keyboard_device.dwFlags = RIDEV_REMOVE;
keyboard_device.hwndTarget = nullptr;
if (!RegisterRawInputDevices(&keyboard_device, 1,
sizeof(keyboard_device))) {
LOG_WARN("Failed to unregister keyboard raw input, error={}",
GetLastError());
}
raw_input_registered_ = false;
}
if (raw_input_window_) {
DestroyWindow(raw_input_window_);
raw_input_window_ = nullptr;
}
}
LRESULT CALLBACK PlatformKeyboardCapturer::RawInputWindowProc(
HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
PlatformKeyboardCapturer* capturer = reinterpret_cast<PlatformKeyboardCapturer*>(
GetWindowLongPtrW(window, GWLP_USERDATA));
if (message == WM_NCCREATE) {
auto* create = reinterpret_cast<CREATESTRUCTW*>(l_param);
capturer = static_cast<PlatformKeyboardCapturer*>(create->lpCreateParams);
SetWindowLongPtrW(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(capturer));
} else if (message == WM_INPUT && capturer) {
capturer->HandleRawInput(reinterpret_cast<HRAWINPUT>(l_param));
} else if (message == WM_NCDESTROY) {
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
}
return DefWindowProcW(window, message, w_param, l_param);
}
void PlatformKeyboardCapturer::HandleRawInput(HRAWINPUT raw_input_handle) {
RAWINPUT input{};
UINT input_size = sizeof(input);
const UINT bytes_read =
GetRawInputData(raw_input_handle, RID_INPUT, &input, &input_size,
sizeof(RAWINPUTHEADER));
if (bytes_read == static_cast<UINT>(-1) ||
bytes_read < sizeof(RAWINPUTHEADER) ||
input.header.dwType != RIM_TYPEKEYBOARD) {
return;
}
const RAWKEYBOARD& keyboard = input.data.keyboard;
if (keyboard.VKey == 0xFF ||
keyboard.ExtraInformation ==
static_cast<ULONG>(kInjectedKeyboardInputMarker)) {
return;
}
bool is_down = false;
if (keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN) {
is_down = true;
} else if (keyboard.Message != WM_KEYUP &&
keyboard.Message != WM_SYSKEYUP) {
return;
}
const bool extended = (keyboard.Flags & (RI_KEY_E0 | RI_KEY_E1)) != 0;
UINT mapped_scan_code = keyboard.MakeCode;
if ((keyboard.Flags & RI_KEY_E0) != 0) {
mapped_scan_code |= 0xE000;
} else if ((keyboard.Flags & RI_KEY_E1) != 0) {
mapped_scan_code |= 0xE100;
}
if (mapped_scan_code == 0xE11D || mapped_scan_code == 0xE02A) {
return;
}
int key_code = static_cast<int>(keyboard.VKey);
if (key_code == VK_SHIFT || key_code == VK_CONTROL || key_code == VK_MENU) {
const UINT normalized =
MapVirtualKeyW(mapped_scan_code, MAPVK_VSC_TO_VK_EX);
if (normalized != 0) {
key_code = static_cast<int>(normalized);
}
}
if (on_key_action_) {
on_key_action_(key_code, is_down, keyboard.MakeCode, extended, user_ptr_);
}
}
// Apply remote keyboard commands to the local machine.
int PlatformKeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
INPUT input = {0};
input.type = INPUT_KEYBOARD;
input.ki.dwExtraInfo =
static_cast<ULONG_PTR>(kInjectedKeyboardInputMarker);
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
const UINT resolved_scan_code =
scan_code != 0
? static_cast<UINT>(scan_code & 0xFF) | (extended ? 0xE000u : 0u)
: MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
if (scan_code != 0 && !prefer_vk) {
input.ki.wVk = 0;
input.ki.wScan = static_cast<WORD>(scan_code & 0xFF);
input.ki.dwFlags |= KEYEVENTF_SCANCODE;
if (extended) {
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
}
} else {
input.ki.wVk = static_cast<WORD>(key_code);
if (prefer_vk && resolved_scan_code != 0) {
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
if ((resolved_scan_code & 0xFF00) != 0) {
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
}
} else if (resolved_scan_code != 0) {
input.ki.wVk = 0;
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
input.ki.dwFlags |= KEYEVENTF_SCANCODE;
if ((resolved_scan_code & 0xFF00) != 0) {
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
}
}
}
if (!is_down) {
input.ki.dwFlags |= KEYEVENTF_KEYUP;
}
const UINT sent = SendInput(1, &input, sizeof(INPUT));
if (sent != 1) {
LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code,
is_down, GetLastError());
return -1;
}
return 0;
}
} // namespace crossdesk
@@ -0,0 +1,54 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-11-22
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _KEYBOARD_CAPTURER_H_
#define _KEYBOARD_CAPTURER_H_
#include <Windows.h>
#include <condition_variable>
#include <mutex>
#include <thread>
#include "device_controller.h"
namespace crossdesk {
class PlatformKeyboardCapturer final : public KeyboardCapturer {
public:
PlatformKeyboardCapturer();
virtual ~PlatformKeyboardCapturer();
public:
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
virtual int Unhook();
virtual int SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code = 0,
bool extended = false);
private:
static LRESULT CALLBACK RawInputWindowProc(HWND window, UINT message,
WPARAM w_param, LPARAM l_param);
void RawInputThreadMain();
bool CreateRawInputWindow();
void DestroyRawInputWindow();
void HandleRawInput(HRAWINPUT raw_input_handle);
OnKeyAction on_key_action_ = nullptr;
void* user_ptr_ = nullptr;
HWND raw_input_window_ = nullptr;
bool raw_input_registered_ = false;
std::thread capture_thread_;
DWORD capture_thread_id_ = 0;
bool capture_start_complete_ = false;
bool capture_start_succeeded_ = false;
std::mutex capture_state_mutex_;
std::condition_variable capture_start_condition_;
};
} // namespace crossdesk
#endif
@@ -0,0 +1,98 @@
#include "mouse_controller.h"
#include <remote_action.h>
#include <Windows.h>
#include "rd_log.h"
namespace crossdesk {
PlatformMouseController::PlatformMouseController() {}
PlatformMouseController::~PlatformMouseController() {}
int PlatformMouseController::Init(std::vector<DisplayInfo> display_info_list) {
display_info_list_ = display_info_list;
return 0;
}
int PlatformMouseController::Destroy() { return 0; }
int PlatformMouseController::SendMouseCommand(RemoteAction remote_action,
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={}",
display_index, display_info_list_.size());
return -1;
}
INPUT ip = {0};
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;
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;
}
}
}
return 0;
}
} // namespace crossdesk
@@ -0,0 +1,33 @@
/*
* @Author: DI JUNKUN
* @Date: 2023-12-14
* Copyright (c) 2023 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _MOUSE_CONTROLLER_H_
#define _MOUSE_CONTROLLER_H_
#include <vector>
#include <remote_action.h>
#include "device_controller.h"
#include "display_info.h"
namespace crossdesk {
class PlatformMouseController final : public MouseController {
public:
PlatformMouseController();
virtual ~PlatformMouseController();
public:
virtual int Init(std::vector<DisplayInfo> display_info_list);
virtual int Destroy();
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
private:
std::vector<DisplayInfo> display_info_list_;
};
} // namespace crossdesk
#endif
@@ -0,0 +1,15 @@
#ifndef CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
#define CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
#include <cstdint>
namespace crossdesk {
// SendInput copies dwExtraInfo into KBDLLHOOKSTRUCT. Tag CrossDesk-generated
// 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;
} // namespace crossdesk
#endif // CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
@@ -0,0 +1,206 @@
// virtual_key_codes.h
#ifndef VIRTUAL_KEY_CODES_H
#define VIRTUAL_KEY_CODES_H
#define VK_LBUTTON 0x01 // Left mouse button
#define VK_RBUTTON 0x02 // Right mouse button
#define VK_CANCEL 0x03 // Control-break processing
#define VK_MBUTTON 0x04 // Middle mouse button
#define VK_XBUTTON1 0x05 // X1 mouse button
#define VK_XBUTTON2 0x06 // X2 mouse button
// 0x07 Reserved
#define VK_BACK 0x08 // Backspace key
#define VK_TAB 0x09 // Tab key
// 0x0A-0B Reserved
#define VK_CLEAR 0x0C // Clear key
#define VK_RETURN 0x0D // Enter key
// 0x0E-0F Unassigned
#define VK_SHIFT 0x10 // Shift key
#define VK_CONTROL 0x11 // Ctrl key
#define VK_MENU 0x12 // Alt key
#define VK_PAUSE 0x13 // Pause key
#define VK_CAPITAL 0x14 // Caps lock key
#define VK_KANA 0x15 // IME Kana mode
#define VK_HANGUL 0x15 // IME Hangul mode
#define VK_IME_ON 0x16 // IME On
#define VK_JUNJA 0x17 // IME Junja mode
#define VK_FINAL 0x18 // IME final mode
#define VK_HANJA 0x19 // IME Hanja mode
#define VK_KANJI 0x19 // IME Kanji mode
#define VK_IME_OFF 0x1A // IME Off
#define VK_ESCAPE 0x1B // Esc key
#define VK_CONVERT 0x1C // IME convert
#define VK_NONCONVERT 0x1D // IME nonconvert
#define VK_ACCEPT 0x1E // IME accept
#define VK_MODECHANGE 0x1F // IME mode change request
#define VK_SPACE 0x20 // Spacebar key
#define VK_PRIOR 0x21 // Page up key
#define VK_NEXT 0x22 // Page down key
#define VK_END 0x23 // End key
#define VK_HOME 0x24 // Home key
#define VK_LEFT 0x25 // Left arrow key
#define VK_UP 0x26 // Up arrow key
#define VK_RIGHT 0x27 // Right arrow key
#define VK_DOWN 0x28 // Down arrow key
#define VK_SELECT 0x29 // Select key
#define VK_PRINT 0x2A // Print key
#define VK_EXECUTE 0x2B // Execute key
#define VK_SNAPSHOT 0x2C // Print screen key
#define VK_INSERT 0x2D // Insert key
#define VK_DELETE 0x2E // Delete key
#define VK_HELP 0x2F // Help key
#define VK_0 0x30 // 0 key
#define VK_1 0x31 // 1 key
#define VK_2 0x32 // 2 key
#define VK_3 0x33 // 3 key
#define VK_4 0x34 // 4 key
#define VK_5 0x35 // 5 key
#define VK_6 0x36 // 6 key
#define VK_7 0x37 // 7 key
#define VK_8 0x38 // 8 key
#define VK_9 0x39 // 9 key
// 0x3A-40 Undefined
#define VK_A 0x41 // A key
#define VK_B 0x42 // B key
#define VK_C 0x43 // C key
#define VK_D 0x44 // D key
#define VK_E 0x45 // E key
#define VK_F 0x46 // F key
#define VK_G 0x47 // G key
#define VK_H 0x48 // H key
#define VK_I 0x49 // I key
#define VK_J 0x4A // J key
#define VK_K 0x4B // K key
#define VK_L 0x4C // L key
#define VK_M 0x4D // M key
#define VK_N 0x4E // N key
#define VK_O 0x4F // O key
#define VK_P 0x50 // P key
#define VK_Q 0x51 // Q key
#define VK_R 0x52 // R key
#define VK_S 0x53 // S key
#define VK_T 0x54 // T key
#define VK_U 0x55 // U key
#define VK_V 0x56 // V key
#define VK_W 0x57 // W key
#define VK_X 0x58 // X key
#define VK_Y 0x59 // Y key
#define VK_Z 0x5A // Z key
#define VK_LWIN 0x5B // Left Windows logo key
#define VK_RWIN 0x5C // Right Windows logo key
#define VK_APPS 0x5D // Application key
// 0x5E Reserved
#define VK_SLEEP 0x5F // Computer Sleep key
#define VK_NUMPAD0 0x60 // Numeric keypad 0 key
#define VK_NUMPAD1 0x61 // Numeric keypad 1 key
#define VK_NUMPAD2 0x62 // Numeric keypad 2 key
#define VK_NUMPAD3 0x63 // Numeric keypad 3 key
#define VK_NUMPAD4 0x64 // Numeric keypad 4 key
#define VK_NUMPAD5 0x65 // Numeric keypad 5 key
#define VK_NUMPAD6 0x66 // Numeric keypad 6 key
#define VK_NUMPAD7 0x67 // Numeric keypad 7 key
#define VK_NUMPAD8 0x68 // Numeric keypad 8 key
#define VK_NUMPAD9 0x69 // Numeric keypad 9 key
#define VK_MULTIPLY 0x6A // Multiply key
#define VK_ADD 0x6B // Add key
#define VK_SEPARATOR 0x6C // Separator key
#define VK_SUBTRACT 0x6D // Subtract key
#define VK_DECIMAL 0x6E // Decimal key
#define VK_DIVIDE 0x6F // Divide key
#define VK_F1 0x70 // F1 key
#define VK_F2 0x71 // F2 key
#define VK_F3 0x72 // F3 key
#define VK_F4 0x73 // F4 key
#define VK_F5 0x74 // F5 key
#define VK_F6 0x75 // F6 key
#define VK_F7 0x76 // F7 key
#define VK_F8 0x77 // F8 key
#define VK_F9 0x78 // F9 key
#define VK_F10 0x79 // F10 key
#define VK_F11 0x7A // F11 key
#define VK_F12 0x7B // F12 key
#define VK_F13 0x7C // F13 key
#define VK_F14 0x7D // F14 key
#define VK_F15 0x7E // F15 key
#define VK_F16 0x7F // F16 key
#define VK_F17 0x80 // F17 key
#define VK_F18 0x81 // F18 key
#define VK_F19 0x82 // F19 key
#define VK_F20 0x83 // F20 key
#define VK_F21 0x84 // F21 key
#define VK_F22 0x85 // F22 key
#define VK_F23 0x86 // F23 key
#define VK_F24 0x87 // F24 key
// 0x880x8F Reserved
#define VK_NUMLOCK 0x90 // Num lock key
#define VK_SCROLL 0x91 // Scroll lock key
// 0x920x96 OEM specific
// 0x970x9F Unassigned
#define VK_LSHIFT 0xA0 // Left Shift key
#define VK_RSHIFT 0xA1 // Right Shift key
#define VK_LCONTROL 0xA2 // Left Ctrl key
#define VK_RCONTROL 0xA3 // Right Ctrl key
#define VK_LMENU 0xA4 // Left Alt key
#define VK_RMENU 0xA5 // Right Alt key
#define VK_BROWSER_BACK 0xA6 // Browser Back key
#define VK_BROWSER_FORWARD 0xA7 // Browser Forward key
#define VK_BROWSER_REFRESH 0xA8 // Browser Refresh key
#define VK_BROWSER_STOP 0xA9 // Browser Stop key
#define VK_BROWSER_SEARCH 0xAA // Browser Search key
#define VK_BROWSER_FAVORITES 0xAB // Browser Favorites key
#define VK_BROWSER_HOME 0xAC // Browser Start and Home key
#define VK_VOLUME_MUTE 0xAD // Volume Mute key
#define VK_VOLUME_DOWN 0xAE // Volume Down key
#define VK_VOLUME_UP 0xAF // Volume Up key
#define VK_MEDIA_NEXT_TRACK 0xB0 // Next Track key
#define VK_MEDIA_PREV_TRACK 0xB1 // Previous Track key
#define VK_MEDIA_STOP 0xB2 // Stop Media key
#define VK_MEDIA_PLAY_PAUSE 0xB3 // Play/Pause Media key
#define VK_LAUNCH_MAIL 0xB4 // Start Mail key
#define VK_LAUNCH_MEDIA_SELECT 0xB5 // Select Media key
#define VK_LAUNCH_APP1 0xB6 // Start Application 1 key
#define VK_LAUNCH_APP2 0xB7 // Start Application 2 key
// 0xB80xB9 Reserved
#define VK_OEM_1 0xBA // For US: Semicolon/Colon key
#define VK_OEM_PLUS 0xBB // Equals/Plus key
#define VK_OEM_COMMA 0xBC // Comma/Less Than key
#define VK_OEM_MINUS 0xBD // Dash/Underscore key
#define VK_OEM_PERIOD 0xBE // Period/Greater Than key
#define VK_OEM_2 0xBF // Slash/Question Mark key
#define VK_OEM_3 0xC0 // Grave Accent/Tilde key
// 0xC10xDA Reserved
#define VK_OEM_4 0xDB // Left Brace key
#define VK_OEM_5 0xDC // Backslash/Pipe key
#define VK_OEM_6 0xDD // Right Brace key
#define VK_OEM_7 0xDE // Apostrophe/Quote key
#define VK_OEM_8 0xDF // (Canadian CSA: Right Ctrl key)
// 0xE0 Reserved
#define VK_OEM_102 0xE2 // (European ISO: Backslash/Pipe key)
// 0xE3E4 OEM specific
#define VK_PROCESSKEY 0xE5 // IME PROCESS key
// 0xE6 OEM specific
#define VK_PACKET 0xE7 // Unicode characters as keystrokes
// 0xE8 Unassigned
// 0xE9F5 OEM specific
#define VK_ATTN 0xF6 // Attn key
#define VK_CRSEL 0xF7 // CrSel key
#define VK_EXSEL 0xF8 // ExSel key
#define VK_EREOF 0xF9 // Erase EOF key
#define VK_PLAY 0xFA // Play key
#define VK_ZOOM 0xFB // Zoom key
#define VK_NONAME 0xFC // Reserved
#define VK_PA1 0xFD // PA1 key
#define VK_OEM_CLEAR 0xFE // Clear key
#endif // VIRTUAL_KEY_CODES_H
@@ -0,0 +1,51 @@
#include "platform/path_backend.h"
#include <shlobj.h>
#include <windows.h>
#include <vector>
namespace crossdesk::platform {
namespace {
std::filesystem::path GetKnownFolder(REFKNOWNFOLDERID id) {
PWSTR value = nullptr;
if (FAILED(SHGetKnownFolderPath(id, 0, nullptr, &value))) return {};
const std::filesystem::path result(value);
CoTaskMemFree(value);
return result;
}
} // namespace
std::filesystem::path GetExecutableDirectory() {
std::vector<wchar_t> buffer(MAX_PATH);
while (true) {
const DWORD length = GetModuleFileNameW(
nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (length == 0 || buffer.size() >= 32768) return {};
if (length < buffer.size()) {
return std::filesystem::path(buffer.data(), buffer.data() + length)
.parent_path();
}
buffer.resize(buffer.size() * 2);
}
}
std::filesystem::path GetConfigPath(const std::string& app_name) {
return GetKnownFolder(FOLDERID_RoamingAppData) / app_name;
}
std::filesystem::path GetCachePath(const std::string& app_name) {
#ifdef CROSSDESK_DEBUG
return "cache";
#else
return GetKnownFolder(FOLDERID_LocalAppData) / app_name / "cache";
#endif
}
std::filesystem::path GetLogPath(const std::string& app_name) {
return GetKnownFolder(FOLDERID_LocalAppData) / app_name / "logs";
}
} // namespace crossdesk::platform
@@ -0,0 +1,399 @@
#include "screen_capturer_dxgi.h"
#include <algorithm>
#include <chrono>
#include <string>
#include <vector>
#include <display_stream_id.h>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
std::string WideToUtf8(const std::wstring& wstr) {
if (wstr.empty()) return {};
int size_needed = WideCharToMultiByte(
CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
std::string result(size_needed, 0);
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(),
size_needed, nullptr, nullptr);
return result;
}
std::string GetDisplayLabel(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name);
constexpr char kDevicePrefix[] = "\\\\.\\";
if (name.rfind(kDevicePrefix, 0) == 0) {
name.erase(0, sizeof(kDevicePrefix) - 1);
}
return name;
}
} // namespace
ScreenCapturerDxgi::ScreenCapturerDxgi() {}
ScreenCapturerDxgi::~ScreenCapturerDxgi() {
Stop();
Destroy();
}
int ScreenCapturerDxgi::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
callback_ = cb;
if (!callback_) {
LOG_ERROR("DXGI: callback is null");
return -1;
}
if (!InitializeDxgi()) {
LOG_ERROR("DXGI: initialize DXGI failed");
return -2;
}
EnumerateDisplays();
if (display_info_list_.empty()) {
LOG_ERROR("DXGI: no displays found");
return -3;
}
monitor_index_ = 0;
initial_monitor_index_ = monitor_index_;
return 0;
}
int ScreenCapturerDxgi::Destroy() {
Stop();
ReleaseDuplication();
outputs_.clear();
d3d_context_.Reset();
d3d_device_.Reset();
dxgi_factory_.Reset();
if (nv12_frame_) {
delete[] nv12_frame_;
nv12_frame_ = nullptr;
nv12_width_ = 0;
nv12_height_ = 0;
}
return 0;
}
int ScreenCapturerDxgi::Start(bool show_cursor) {
if (running_) return 0;
show_cursor_ = show_cursor;
if (!CreateDuplicationForMonitor(monitor_index_)) {
LOG_ERROR("DXGI: create duplication failed for monitor {}",
monitor_index_.load());
return -1;
}
paused_ = false;
running_ = true;
thread_ = std::thread([this]() { CaptureLoop(); });
return 0;
}
int ScreenCapturerDxgi::Stop() {
if (!running_) return 0;
running_ = false;
if (thread_.joinable()) thread_.join();
ReleaseDuplication();
return 0;
}
int ScreenCapturerDxgi::Pause(int monitor_index) {
paused_ = true;
return 0;
}
int ScreenCapturerDxgi::Resume(int monitor_index) {
paused_ = false;
return 0;
}
int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
std::lock_guard<std::mutex> lock(switch_mutex_);
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
return -1;
}
paused_ = true;
monitor_index_ = monitor_index;
ReleaseDuplication();
if (!CreateDuplicationForMonitor(monitor_index_)) {
LOG_ERROR("DXGI: create duplication failed for monitor {}",
monitor_index_.load());
paused_ = false; // Reset paused_ on failure
return -2;
}
paused_ = false;
LOG_INFO("DXGI: switched to monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
return 0;
}
int ScreenCapturerDxgi::ResetToInitialMonitor() {
std::lock_guard<std::mutex> lock(switch_mutex_);
if (display_info_list_.empty()) return -1;
int target = initial_monitor_index_;
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
if (monitor_index_ == target) return 0;
if (running_) {
paused_ = true;
monitor_index_ = target;
ReleaseDuplication();
if (!CreateDuplicationForMonitor(monitor_index_)) {
paused_ = false;
return -2;
}
paused_ = false;
LOG_INFO("DXGI: reset to initial monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
} else {
monitor_index_ = target;
}
return 0;
}
bool ScreenCapturerDxgi::InitializeDxgi() {
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#ifdef _DEBUG
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
D3D_FEATURE_LEVEL feature_levels[] = {
D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0};
D3D_FEATURE_LEVEL out_level{};
HRESULT hr = D3D11CreateDevice(
nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, feature_levels,
ARRAYSIZE(feature_levels), D3D11_SDK_VERSION, d3d_device_.GetAddressOf(),
&out_level, d3d_context_.GetAddressOf());
if (FAILED(hr)) {
hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, flags,
feature_levels, ARRAYSIZE(feature_levels),
D3D11_SDK_VERSION, d3d_device_.GetAddressOf(),
&out_level, d3d_context_.GetAddressOf());
if (FAILED(hr)) {
LOG_ERROR("DXGI: D3D11CreateDevice failed, hr={}", (int)hr);
return false;
}
}
hr = CreateDXGIFactory1(
__uuidof(IDXGIFactory1),
reinterpret_cast<void**>(dxgi_factory_.GetAddressOf()));
if (FAILED(hr)) {
LOG_ERROR("DXGI: CreateDXGIFactory1 failed, hr={}", (int)hr);
return false;
}
return true;
}
void ScreenCapturerDxgi::EnumerateDisplays() {
display_info_list_.clear();
outputs_.clear();
Microsoft::WRL::ComPtr<IDXGIAdapter> adapter;
for (UINT a = 0;
dxgi_factory_->EnumAdapters(a, adapter.ReleaseAndGetAddressOf()) !=
DXGI_ERROR_NOT_FOUND;
++a) {
Microsoft::WRL::ComPtr<IDXGIOutput> output;
for (UINT o = 0; adapter->EnumOutputs(o, output.ReleaseAndGetAddressOf()) !=
DXGI_ERROR_NOT_FOUND;
++o) {
DXGI_OUTPUT_DESC desc{};
if (FAILED(output->GetDesc(&desc))) {
continue;
}
std::string name = GetDisplayLabel(desc.DeviceName);
MONITORINFOEX mi{};
mi.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(desc.Monitor, &mi)) {
bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
DisplayInfo info((void*)desc.Monitor, name, is_primary,
mi.rcMonitor.left, mi.rcMonitor.top,
mi.rcMonitor.right, mi.rcMonitor.bottom);
// primary first
if (is_primary)
display_info_list_.insert(display_info_list_.begin(), info);
else
display_info_list_.push_back(info);
outputs_.push_back(output);
}
}
}
}
bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
if (monitor_index < 0 || monitor_index >= (int)outputs_.size()) return false;
Microsoft::WRL::ComPtr<IDXGIOutput1> output1;
HRESULT hr = outputs_[monitor_index]->QueryInterface(
IID_PPV_ARGS(output1.GetAddressOf()));
if (FAILED(hr)) {
LOG_ERROR("DXGI: Query IDXGIOutput1 failed, hr={}", (int)hr);
return false;
}
duplication_.Reset();
hr = output1->DuplicateOutput(d3d_device_.Get(), duplication_.GetAddressOf());
if (FAILED(hr)) {
LOG_ERROR("DXGI: DuplicateOutput failed, hr={}", (int)hr);
return false;
}
staging_.Reset();
return true;
}
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
std::lock_guard<std::mutex> lock(switch_mutex_);
ReleaseDuplication();
int current_monitor = monitor_index_.load();
if (CreateDuplicationForMonitor(current_monitor)) {
return true;
}
EnumerateDisplays();
if (display_info_list_.empty()) {
LOG_ERROR("DXGI: no displays found while recreating duplication");
return false;
}
if (current_monitor < 0 ||
current_monitor >= static_cast<int>(display_info_list_.size())) {
current_monitor = 0;
monitor_index_ = 0;
}
if (CreateDuplicationForMonitor(current_monitor)) {
LOG_INFO("DXGI: recreated duplication for monitor {}",
monitor_index_.load());
return true;
}
return false;
}
void ScreenCapturerDxgi::ReleaseDuplication() {
staging_.Reset();
if (duplication_) {
duplication_->ReleaseFrame();
}
duplication_.Reset();
}
void ScreenCapturerDxgi::CaptureLoop() {
const int timeout_ms = 33;
auto last_duplication_retry =
std::chrono::steady_clock::now() - std::chrono::milliseconds(1000);
while (running_) {
if (paused_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (!duplication_) {
const auto now = std::chrono::steady_clock::now();
if (now - last_duplication_retry >= std::chrono::milliseconds(500)) {
last_duplication_retry = now;
RecreateDuplicationForCurrentMonitor();
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
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) {
continue;
}
if (FAILED(hr)) {
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
RecreateDuplicationForCurrentMonitor();
continue;
}
Microsoft::WRL::ComPtr<ID3D11Texture2D> acquired_tex;
if (desktop_resource) {
hr = desktop_resource->QueryInterface(
IID_PPV_ARGS(acquired_tex.GetAddressOf()));
if (FAILED(hr)) {
duplication_->ReleaseFrame();
continue;
}
} else {
duplication_->ReleaseFrame();
continue;
}
D3D11_TEXTURE2D_DESC src_desc{};
acquired_tex->GetDesc(&src_desc);
if (!staging_) {
D3D11_TEXTURE2D_DESC staging_desc = src_desc;
staging_desc.Usage = D3D11_USAGE_STAGING;
staging_desc.BindFlags = 0;
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
staging_desc.MiscFlags = 0;
hr = d3d_device_->CreateTexture2D(&staging_desc, nullptr,
staging_.GetAddressOf());
if (FAILED(hr)) {
LOG_ERROR("DXGI: CreateTexture2D staging failed, hr={}", (int)hr);
duplication_->ReleaseFrame();
continue;
}
}
d3d_context_->CopyResource(staging_.Get(), acquired_tex.Get());
D3D11_MAPPED_SUBRESOURCE mapped{};
hr = d3d_context_->Map(staging_.Get(), 0, D3D11_MAP_READ, 0, &mapped);
if (FAILED(hr)) {
duplication_->ReleaseFrame();
continue;
}
int logical_width = static_cast<int>(src_desc.Width);
int even_width = logical_width & ~1;
int even_height = static_cast<int>(src_desc.Height) & ~1;
if (even_width <= 0 || even_height <= 0) {
d3d_context_->Unmap(staging_.Get(), 0);
duplication_->ReleaseFrame();
continue;
}
int nv12_size = even_width * even_height * 3 / 2;
if (!nv12_frame_ || nv12_width_ != even_width ||
nv12_height_ != even_height) {
delete[] nv12_frame_;
nv12_frame_ = new unsigned char[nv12_size];
nv12_width_ = even_width;
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);
if (callback_) {
int idx = monitor_index_.load();
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());
} else {
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size());
}
}
d3d_context_->Unmap(staging_.Get(), 0);
duplication_->ReleaseFrame();
}
}
} // namespace crossdesk
@@ -0,0 +1,83 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-27
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_DXGI_H_
#define _SCREEN_CAPTURER_DXGI_H_
#include <Windows.h>
#include <d3d11.h>
#include <dxgi1_2.h>
#include <wrl/client.h>
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "rd_log.h"
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerDxgi : public ScreenCapturer {
public:
ScreenCapturerDxgi();
~ScreenCapturerDxgi();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override {
return display_info_list_;
}
private:
bool InitializeDxgi();
void EnumerateDisplays();
bool CreateDuplicationForMonitor(int monitor_index);
bool RecreateDuplicationForCurrentMonitor();
void CaptureLoop();
void ReleaseDuplication();
private:
std::vector<DisplayInfo> display_info_list_;
std::vector<Microsoft::WRL::ComPtr<IDXGIOutput>> outputs_;
Microsoft::WRL::ComPtr<IDXGIFactory1> dxgi_factory_;
Microsoft::WRL::ComPtr<ID3D11Device> d3d_device_;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d_context_;
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging_;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
std::thread thread_;
int fps_ = 60;
cb_desktop_data callback_ = nullptr;
std::mutex switch_mutex_;
unsigned char* nv12_frame_ = nullptr;
int nv12_width_ = 0;
int nv12_height_ = 0;
};
} // namespace crossdesk
#endif
@@ -0,0 +1,11 @@
#include "screen_capturer_factory.h"
#include "screen_capturer_win.h"
namespace crossdesk {
ScreenCapturer* ScreenCapturerFactory::Create() {
return new ScreenCapturerWin();
}
} // namespace crossdesk
@@ -0,0 +1,227 @@
#include "screen_capturer_gdi.h"
#include <algorithm>
#include <chrono>
#include <string>
#include <vector>
#include <display_stream_id.h>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
std::string WideToUtf8(const std::wstring& wstr) {
if (wstr.empty()) return {};
int size_needed = WideCharToMultiByte(
CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
std::string result(size_needed, 0);
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(),
size_needed, nullptr, nullptr);
return result;
}
std::string GetDisplayLabel(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name);
constexpr char kDevicePrefix[] = "\\\\.\\";
if (name.rfind(kDevicePrefix, 0) == 0) {
name.erase(0, sizeof(kDevicePrefix) - 1);
}
return name;
}
} // namespace
ScreenCapturerGdi::ScreenCapturerGdi() {}
ScreenCapturerGdi::~ScreenCapturerGdi() {
Stop();
Destroy();
}
BOOL CALLBACK ScreenCapturerGdi::EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
LPARAM data) {
auto displays = reinterpret_cast<std::vector<DisplayInfo>*>(data);
MONITORINFOEX mi{};
mi.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(hMonitor, &mi)) {
std::string name = GetDisplayLabel(mi.szDevice);
bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
DisplayInfo info((void*)hMonitor, name, is_primary, mi.rcMonitor.left,
mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom);
if (is_primary)
displays->insert(displays->begin(), info);
else
displays->push_back(info);
}
return TRUE;
}
void ScreenCapturerGdi::EnumerateDisplays() {
display_info_list_.clear();
EnumDisplayMonitors(nullptr, nullptr, EnumMonitorProc,
(LPARAM)&display_info_list_);
}
int ScreenCapturerGdi::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
callback_ = cb;
if (!callback_) {
LOG_ERROR("GDI: callback is null");
return -1;
}
EnumerateDisplays();
if (display_info_list_.empty()) {
LOG_ERROR("GDI: no displays found");
return -2;
}
monitor_index_ = 0;
initial_monitor_index_ = monitor_index_;
return 0;
}
int ScreenCapturerGdi::Destroy() {
Stop();
if (nv12_frame_) {
delete[] nv12_frame_;
nv12_frame_ = nullptr;
nv12_width_ = 0;
nv12_height_ = 0;
}
return 0;
}
int ScreenCapturerGdi::Start(bool show_cursor) {
if (running_) return 0;
show_cursor_ = show_cursor;
paused_ = false;
running_ = true;
thread_ = std::thread([this]() { CaptureLoop(); });
return 0;
}
int ScreenCapturerGdi::Stop() {
if (!running_) return 0;
running_ = false;
if (thread_.joinable()) thread_.join();
return 0;
}
int ScreenCapturerGdi::Pause(int monitor_index) {
paused_ = true;
return 0;
}
int ScreenCapturerGdi::Resume(int monitor_index) {
paused_ = false;
return 0;
}
int ScreenCapturerGdi::SwitchTo(int monitor_index) {
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
LOG_ERROR("GDI: invalid monitor index {}", monitor_index);
return -1;
}
monitor_index_ = monitor_index;
LOG_INFO("GDI: switched to monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
return 0;
}
int ScreenCapturerGdi::ResetToInitialMonitor() {
if (display_info_list_.empty()) return -1;
int target = initial_monitor_index_;
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
monitor_index_ = target;
LOG_INFO("GDI: reset to initial monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
return 0;
}
void ScreenCapturerGdi::CaptureLoop() {
int interval_ms = fps_ > 0 ? (1000 / fps_) : 16;
HDC screen_dc = GetDC(nullptr);
while (running_) {
if (paused_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (!screen_dc) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
int idx = monitor_index_.load();
if (idx < 0 || idx >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("GDI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size());
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
continue;
}
const auto& di = display_info_list_[idx];
int left = di.left;
int top = di.top;
int width = di.width & ~1;
int height = di.height & ~1;
if (width <= 0 || height <= 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
continue;
}
BITMAPINFO bmi{};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = width;
bmi.bmiHeader.biHeight = -height;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* bits = nullptr;
HDC mem_dc = CreateCompatibleDC(screen_dc);
HBITMAP dib =
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
HGDIOBJ old = SelectObject(mem_dc, dib);
BitBlt(mem_dc, 0, 0, width, height, screen_dc, left, top,
SRCCOPY | CAPTUREBLT);
if (show_cursor_) {
CURSORINFO ci{};
ci.cbSize = sizeof(CURSORINFO);
if (GetCursorInfo(&ci) && ci.flags == CURSOR_SHOWING && ci.hCursor) {
POINT pt = ci.ptScreenPos;
int cx = pt.x - left;
int cy = pt.y - top;
if (cx >= -64 && cy >= -64 && cx < width + 64 && cy < height + 64) {
DrawIconEx(mem_dc, cx, cy, ci.hCursor, 0, 0, 0, nullptr, DI_NORMAL);
}
}
}
int stride_argb = width * 4;
int nv12_size = width * height * 3 / 2;
if (!nv12_frame_ || nv12_width_ != width || nv12_height_ != height) {
delete[] nv12_frame_;
nv12_frame_ = new unsigned char[nv12_size];
nv12_width_ = width;
nv12_height_ = height;
}
libyuv::ARGBToNV12(static_cast<const uint8_t*>(bits), stride_argb,
nv12_frame_, width, nv12_frame_ + width * height, width,
width, height);
if (callback_) {
const std::string stream_id = MakeDisplayStreamId(idx);
callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str());
}
SelectObject(mem_dc, old);
DeleteObject(dib);
DeleteDC(mem_dc);
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
}
ReleaseDC(nullptr, screen_dc);
}
} // namespace crossdesk
@@ -0,0 +1,68 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-27
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_GDI_H_
#define _SCREEN_CAPTURER_GDI_H_
#include <Windows.h>
#include <atomic>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "rd_log.h"
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerGdi : public ScreenCapturer {
public:
ScreenCapturerGdi();
~ScreenCapturerGdi();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override {
return display_info_list_;
}
private:
static BOOL CALLBACK EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
LPARAM data);
void EnumerateDisplays();
void CaptureLoop();
private:
std::vector<DisplayInfo> display_info_list_;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
std::thread thread_;
int fps_ = 60;
cb_desktop_data callback_ = nullptr;
unsigned char* nv12_frame_ = nullptr;
int nv12_width_ = 0;
int nv12_height_ = 0;
};
} // namespace crossdesk
#endif
@@ -0,0 +1,426 @@
#include "screen_capturer_wgc.h"
#include <Windows.h>
#include <d3d11_4.h>
#include <winrt/Windows.Foundation.Metadata.h>
#include <winrt/Windows.Graphics.Capture.h>
#include <iostream>
#include <display_stream_id.h>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
static std::vector<DisplayInfo> gs_display_list;
std::string WideToUtf8(const std::wstring& wstr) {
if (wstr.empty()) return {};
int size_needed = WideCharToMultiByte(
CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
std::string result(size_needed, 0);
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(),
size_needed, nullptr, nullptr);
return result;
}
std::string GetDisplayLabel(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name);
constexpr char kDevicePrefix[] = "\\\\.\\";
if (name.rfind(kDevicePrefix, 0) == 0) {
name.erase(0, sizeof(kDevicePrefix) - 1);
}
return name;
}
BOOL WINAPI EnumMonitorProc(HMONITOR hmonitor, [[maybe_unused]] HDC hdc,
[[maybe_unused]] LPRECT lprc, LPARAM data) {
MONITORINFOEX monitor_info_;
monitor_info_.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(hmonitor, &monitor_info_)) {
std::string display_name = GetDisplayLabel(monitor_info_.szDevice);
if (monitor_info_.dwFlags & MONITORINFOF_PRIMARY) {
gs_display_list.insert(
gs_display_list.begin(),
{(void*)hmonitor, display_name,
(monitor_info_.dwFlags & MONITORINFOF_PRIMARY) ? true : false,
monitor_info_.rcMonitor.left, monitor_info_.rcMonitor.top,
monitor_info_.rcMonitor.right, monitor_info_.rcMonitor.bottom});
*(HMONITOR*)data = hmonitor;
} else {
gs_display_list.push_back(DisplayInfo(
(void*)hmonitor, display_name,
(monitor_info_.dwFlags & MONITORINFOF_PRIMARY) ? true : false,
monitor_info_.rcMonitor.left, monitor_info_.rcMonitor.top,
monitor_info_.rcMonitor.right, monitor_info_.rcMonitor.bottom));
}
}
return true;
}
HMONITOR GetPrimaryMonitor() {
HMONITOR hmonitor = nullptr;
gs_display_list.clear();
::EnumDisplayMonitors(NULL, NULL, EnumMonitorProc, (LPARAM)&hmonitor);
return hmonitor;
}
ScreenCapturerWgc::ScreenCapturerWgc() : monitor_(nullptr) {}
ScreenCapturerWgc::~ScreenCapturerWgc() {
Stop();
CleanUp();
if (nv12_frame_) {
delete[] nv12_frame_;
nv12_frame_ = nullptr;
nv12_width_ = 0;
nv12_height_ = 0;
}
if (nv12_frame_scaled_) {
delete[] nv12_frame_scaled_;
nv12_frame_scaled_ = nullptr;
}
}
bool ScreenCapturerWgc::IsWgcSupported() {
try {
/* no contract for IGraphicsCaptureItemInterop, verify 10.0.18362.0 */
return winrt::Windows::Foundation::Metadata::ApiInformation::
IsApiContractPresent(L"Windows.Foundation.UniversalApiContract", 8);
} catch (const winrt::hresult_error&) {
return false;
} catch (...) {
return false;
}
}
int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
if (inited_ == true) return 0;
// nv12_frame_ = new unsigned char[rect.right * rect.bottom * 3 / 2];
// nv12_frame_scaled_ = new unsigned char[1280 * 720 * 3 / 2];
fps_ = fps;
on_data_ = cb;
if (!IsWgcSupported()) {
LOG_ERROR("WGC not supported");
return 2;
}
return RebuildSessions(monitor_index_);
}
int ScreenCapturerWgc::RebuildSessions(int preferred_monitor_index) {
CleanUp();
if (!IsWgcSupported()) {
LOG_ERROR("WGC not supported");
return 2;
}
monitor_ = GetPrimaryMonitor();
display_info_list_ = gs_display_list;
if (display_info_list_.empty()) {
LOG_ERROR("No display found");
return -1;
}
if (preferred_monitor_index < 0 ||
preferred_monitor_index >= static_cast<int>(display_info_list_.size())) {
preferred_monitor_index = 0;
}
monitor_index_ = preferred_monitor_index;
int error = 0;
for (int i = 0; i < display_info_list_.size(); i++) {
const auto& display = display_info_list_[i];
LOG_INFO(
"index: {}, display name: {}, is primary: {}, bounds: ({}, {}) - "
"({}, {})",
i, display.name, (display.is_primary ? "yes" : "no"), display.left,
display.top, display.right, display.bottom);
sessions_.push_back(
{std::make_unique<WgcSessionImpl>(i), false, false, false});
sessions_.back().session_->RegisterObserver(this);
error = sessions_.back().session_->Initialize((HMONITOR)display.handle);
if (error != 0) {
LOG_ERROR("WGC: initialize session {} failed, ret={}", i, error);
CleanUp();
return error;
}
sessions_[i].inited_ = true;
}
LOG_INFO("Default on monitor {}:{}", monitor_index_,
display_info_list_[monitor_index_].name);
if (initial_monitor_index_ < 0 ||
initial_monitor_index_ >= static_cast<int>(display_info_list_.size())) {
initial_monitor_index_ = monitor_index_;
}
inited_ = true;
return 0;
}
int ScreenCapturerWgc::Destroy() {
CleanUp();
return 0;
}
int ScreenCapturerWgc::Start(bool show_cursor) {
if (running_ == true) {
LOG_ERROR("Screen capturer already running");
return 0;
}
if (inited_ == false) {
const int ret = RebuildSessions(monitor_index_);
if (ret != 0) {
LOG_ERROR("Screen capturer not inited");
return ret;
}
}
int ret = StartSessions(show_cursor);
if (ret == 0) {
return 0;
}
LOG_WARN("WGC: start failed, rebuilding sessions");
ret = RebuildSessions(monitor_index_);
if (ret != 0) {
return ret;
}
return StartSessions(show_cursor);
}
int ScreenCapturerWgc::StartSessions(bool show_cursor) {
bool any_started = false;
bool active_started = false;
int last_error = 0;
int active_monitor = monitor_index_;
if (active_monitor < 0 ||
active_monitor >= static_cast<int>(sessions_.size())) {
active_monitor = 0;
monitor_index_ = 0;
}
for (int i = 0; i < static_cast<int>(sessions_.size()); i++) {
if (sessions_[i].inited_ == false) {
LOG_ERROR("Session {} not inited", i);
continue;
}
if (sessions_[i].running_) {
LOG_ERROR("Session {} is already running", i);
} else {
int ret = sessions_[i].session_->Start(show_cursor);
if (ret != 0) {
LOG_ERROR("Session {} start failed, ret={}", i, ret);
last_error = ret;
continue;
}
if (i != active_monitor) {
sessions_[i].session_->Pause();
sessions_[i].paused_ = true;
} else {
sessions_[i].session_->Resume();
sessions_[i].paused_ = false;
}
sessions_[i].running_ = true;
any_started = true;
if (i == active_monitor) {
active_started = true;
}
}
}
running_ = active_started;
if (!active_started) {
LOG_ERROR("WGC: active session did not start successfully");
Stop();
return last_error != 0 ? last_error : -1;
}
if (!any_started) {
LOG_ERROR("WGC: no session started successfully");
return last_error != 0 ? last_error : -1;
}
return 0;
}
int ScreenCapturerWgc::Pause(int monitor_index) {
if (monitor_index >= sessions_.size() || monitor_index < 0) {
LOG_ERROR("Invalid session index: {}", monitor_index);
return -1;
}
if (!sessions_[monitor_index].paused_) {
sessions_[monitor_index].session_->Pause();
sessions_[monitor_index].paused_ = true;
LOG_INFO("Pausing session {}", monitor_index);
}
return 0;
}
int ScreenCapturerWgc::Resume(int monitor_index) {
if (monitor_index >= sessions_.size() || monitor_index < 0) {
LOG_ERROR("Invalid session index: {}", monitor_index);
return -1;
}
if (sessions_[monitor_index].paused_) {
sessions_[monitor_index].session_->Resume();
sessions_[monitor_index].paused_ = false;
LOG_INFO("Resuming session {}", monitor_index);
}
return 0;
}
int ScreenCapturerWgc::Stop() {
running_ = false;
for (int i = 0; i < sessions_.size(); i++) {
if (sessions_[i].running_) {
sessions_[i].session_->Stop();
sessions_[i].running_ = false;
}
}
return 0;
}
int ScreenCapturerWgc::SwitchTo(int monitor_index) {
if (monitor_index_ == monitor_index) {
LOG_INFO("Already on monitor {}:{}", monitor_index_ + 1,
display_info_list_[monitor_index_].name);
return 0;
}
if (monitor_index < 0 || monitor_index >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("Invalid monitor index: {}", monitor_index);
return -1;
}
if (!sessions_[monitor_index].inited_) {
LOG_ERROR("Monitor {} not inited", monitor_index);
return -1;
}
Pause(monitor_index_);
monitor_index_ = monitor_index;
LOG_INFO("Switching to monitor {}:{}", monitor_index_,
display_info_list_[monitor_index_].name);
Resume(monitor_index);
return 0;
}
int ScreenCapturerWgc::ResetToInitialMonitor() {
if (display_info_list_.empty()) return -1;
if (initial_monitor_index_ < 0 ||
initial_monitor_index_ >= static_cast<int>(display_info_list_.size())) {
return -1;
}
if (monitor_index_ == initial_monitor_index_) {
return 0;
}
if (running_) {
Pause(monitor_index_);
}
monitor_index_ = initial_monitor_index_;
LOG_INFO("Reset to initial monitor {}:{}", monitor_index_,
display_info_list_[monitor_index_].name);
if (running_) {
Resume(monitor_index_);
}
return 0;
}
void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
int id) {
if (!running_ || !on_data_) {
return;
}
std::lock_guard<std::mutex> lock(frame_mutex_);
if (on_data_) {
if (id < 0 || id >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("WGC OnFrame invalid display index: {}", id);
return;
}
if (!frame.data || frame.row_pitch == 0) {
LOG_ERROR("WGC OnFrame received invalid frame: data={}, row_pitch={}",
(void*)frame.data, frame.row_pitch);
return;
}
// calculate the maximum width that can be contained in one row according to
// row_pitch (BGRA: 4 bytes per pixel), and take the minimum with logical
// width to avoid out-of-bounds access.
unsigned int max_width_by_pitch = frame.row_pitch / 4u;
int logical_width = static_cast<int>(
frame.width < max_width_by_pitch ? frame.width : max_width_by_pitch);
// libyuv::ARGBToNV12 requires even width/height
int even_width = logical_width & ~1;
int even_height = static_cast<int>(frame.height) & ~1;
if (even_width <= 0 || even_height <= 0) {
LOG_ERROR(
"WGC OnFrame invalid frame size after adjust: width={} "
"(frame.width={}, max_by_pitch={}), height={}",
logical_width, frame.width, max_width_by_pitch, frame.height);
return;
}
int nv12_size = even_width * even_height * 3 / 2;
if (!nv12_frame_ || nv12_width_ != even_width ||
nv12_height_ != even_height) {
delete[] nv12_frame_;
nv12_frame_ = new unsigned char[nv12_size];
nv12_width_ = even_width;
nv12_height_ = even_height;
}
libyuv::ARGBToNV12((const uint8_t*)frame.data,
static_cast<int>(frame.row_pitch), (uint8_t*)nv12_frame_,
even_width,
(uint8_t*)(nv12_frame_ + even_width * even_height),
even_width, even_width, even_height);
const std::string stream_id = MakeDisplayStreamId(id);
on_data_(nv12_frame_, nv12_size, even_width, even_height,
stream_id.c_str());
}
}
void ScreenCapturerWgc::CleanUp() {
running_ = false;
for (auto& session : sessions_) {
if (session.session_) {
session.session_->Stop();
}
}
sessions_.clear();
display_info_list_.clear();
gs_display_list.clear();
monitor_ = nullptr;
inited_ = false;
}
} // namespace crossdesk
@@ -0,0 +1,79 @@
#ifndef _SCREEN_CAPTURER_WGC_H_
#define _SCREEN_CAPTURER_WGC_H_
#include <atomic>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "screen_capturer.h"
#include "wgc_session.h"
#include "wgc_session_impl.h"
namespace crossdesk {
class ScreenCapturerWgc : public ScreenCapturer,
public WgcSession::wgc_session_observer {
public:
ScreenCapturerWgc();
~ScreenCapturerWgc();
public:
bool IsWgcSupported();
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
std::vector<DisplayInfo> GetDisplayInfoList() { return display_info_list_; }
int SwitchTo(int monitor_index);
int ResetToInitialMonitor() override;
void OnFrame(const WgcSession::wgc_session_frame& frame, int id);
protected:
void CleanUp();
int RebuildSessions(int preferred_monitor_index);
int StartSessions(bool show_cursor);
private:
HMONITOR monitor_;
MONITORINFOEX monitor_info_;
std::vector<DisplayInfo> display_info_list_;
int monitor_index_ = 0;
int initial_monitor_index_ = 0;
private:
class WgcSessionInfo {
public:
std::unique_ptr<WgcSession> session_;
bool inited_ = false;
bool running_ = false;
bool paused_ = false;
};
std::vector<WgcSessionInfo> sessions_;
std::atomic_bool running_{false};
std::atomic_bool inited_{false};
int fps_ = 60;
cb_desktop_data on_data_ = nullptr;
unsigned char* nv12_frame_ = nullptr;
unsigned char* nv12_frame_scaled_ = nullptr;
int nv12_width_ = 0;
int nv12_height_ = 0;
std::mutex frame_mutex_;
};
} // namespace crossdesk
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,104 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-27
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_WIN_H_
#define _SCREEN_CAPTURER_WIN_H_
#include <Windows.h>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <vector>
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerWin : public ScreenCapturer {
public:
ScreenCapturerWin();
~ScreenCapturerWin();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
private:
std::unique_ptr<ScreenCapturer> impl_;
bool impl_is_wgc_plugin_ = false;
int fps_ = 60;
cb_desktop_data cb_;
cb_desktop_data cb_orig_;
std::unordered_map<void*, size_t> handle_to_canonical_index_;
std::unordered_map<std::string, std::string> stream_id_alias_;
std::mutex alias_mutex_;
std::vector<DisplayInfo> canonical_displays_;
std::atomic<bool> invalid_stream_id_logged_{false};
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<bool> show_cursor_{true};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> secure_desktop_capture_active_{false};
std::atomic<bool> post_secure_desktop_waiting_for_frame_{false};
std::atomic<bool> post_secure_desktop_drop_logged_{false};
std::atomic<ULONGLONG> post_secure_desktop_started_tick_{0};
std::thread secure_capture_thread_;
HANDLE secure_frame_mapping_ = nullptr;
HANDLE secure_frame_ready_event_ = nullptr;
uint8_t* secure_frame_view_ = nullptr;
size_t secure_frame_view_size_ = 0;
DWORD secure_shared_session_id_ = 0xFFFFFFFF;
int secure_shared_left_ = 0;
int secure_shared_top_ = 0;
int secure_shared_width_ = 0;
int secure_shared_height_ = 0;
int secure_shared_fps_ = 0;
bool secure_shared_show_cursor_ = true;
std::string secure_shared_stage_;
std::string secure_shared_desktop_;
bool secure_shared_capture_started_ = false;
void BuildCanonicalFromImpl();
void RebuildAliasesFromImpl();
void StopSecureCaptureThread();
bool RestartCaptureBackendAfterSecureDesktop();
void SecureDesktopCaptureLoop();
bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height,
std::string* display_name);
bool StartSecureDesktopSharedCapture(DWORD session_id, int left, int top,
int width, int height,
const std::string& stage,
const std::string& desktop,
bool show_cursor, int fps,
std::string* error_out);
void StopSecureDesktopSharedCapture(DWORD session_id);
bool OpenSecureDesktopSharedFrame(DWORD session_id, size_t min_size,
std::string* error_out);
bool ReadSecureDesktopSharedFrame(DWORD wait_ms,
std::vector<uint8_t>* nv12_frame_out,
int* width_out, int* height_out,
std::string* error_out);
void CloseSecureDesktopSharedFrame();
};
} // namespace crossdesk
#endif
@@ -0,0 +1,29 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-03-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WGC_PLUGIN_API_H_
#define _WGC_PLUGIN_API_H_
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturer;
}
#if defined(_WIN32) && defined(CROSSDESK_WGC_PLUGIN_BUILD)
#define CROSSDESK_WGC_PLUGIN_API __declspec(dllexport)
#else
#define CROSSDESK_WGC_PLUGIN_API
#endif
extern "C" {
CROSSDESK_WGC_PLUGIN_API crossdesk::ScreenCapturer*
CrossDeskCreateWgcCapturer();
CROSSDESK_WGC_PLUGIN_API void CrossDeskDestroyWgcCapturer(
crossdesk::ScreenCapturer* capturer);
}
#endif
@@ -0,0 +1,30 @@
#include <mutex>
#include "path_manager.h"
#include "rd_log.h"
#include "screen_capturer_wgc.h"
#include "wgc_plugin_api.h"
namespace {
void InitializePluginLogger() {
static std::once_flag once;
std::call_once(once, []() {
crossdesk::PathManager path_manager("CrossDesk");
crossdesk::InitLogger(path_manager.GetLogPath().string());
});
}
} // namespace
extern "C" {
crossdesk::ScreenCapturer* CrossDeskCreateWgcCapturer() {
InitializePluginLogger();
return new crossdesk::ScreenCapturerWgc();
}
void CrossDeskDestroyWgcCapturer(crossdesk::ScreenCapturer* capturer) {
delete capturer;
}
}
@@ -0,0 +1,41 @@
#ifndef _WGC_SESSION_H_
#define _WGC_SESSION_H_
#include <Windows.h>
namespace crossdesk {
class WgcSession {
public:
struct wgc_session_frame {
unsigned int width;
unsigned int height;
unsigned int row_pitch;
const unsigned char* data;
};
class wgc_session_observer {
public:
virtual ~wgc_session_observer() {}
virtual void OnFrame(const wgc_session_frame& frame, int id) = 0;
};
public:
virtual void Release() = 0;
virtual int Initialize(HWND hwnd) = 0;
virtual int Initialize(HMONITOR hmonitor) = 0;
virtual void RegisterObserver(wgc_session_observer* observer) = 0;
virtual int Start(bool show_cursor) = 0;
virtual int Stop() = 0;
virtual int Pause() = 0;
virtual int Resume() = 0;
virtual ~WgcSession() {};
};
} // namespace crossdesk
#endif
@@ -0,0 +1,454 @@
#include "wgc_session_impl.h"
#include <Windows.Graphics.Capture.Interop.h>
#include <string>
#include "rd_log.h"
namespace crossdesk {
extern "C" {
HRESULT __stdcall CreateDirect3D11DeviceFromDXGIDevice(
::IDXGIDevice* dxgiDevice, ::IInspectable** graphicsDevice);
}
WgcSessionImpl::WgcSessionImpl(int id) : id_(id) {}
WgcSessionImpl::~WgcSessionImpl() {
Stop();
CleanUp();
}
void WgcSessionImpl::Release() { delete this; }
int WgcSessionImpl::Initialize(HWND hwnd) {
std::lock_guard locker(lock_);
target_.hwnd = hwnd;
target_.is_window = true;
return InitializeLocked();
}
int WgcSessionImpl::Initialize(HMONITOR hmonitor) {
std::lock_guard locker(lock_);
target_.hmonitor = hmonitor;
target_.is_window = false;
return InitializeLocked();
}
void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
std::lock_guard locker(lock_);
observer_ = observer;
}
int WgcSessionImpl::Start(bool show_cursor) {
std::lock_guard locker(lock_);
return StartLocked(show_cursor);
}
int WgcSessionImpl::Stop() {
std::lock_guard locker(lock_);
CleanUpLocked();
return 0;
}
int WgcSessionImpl::Pause() {
std::lock_guard locker(lock_);
is_paused_ = true;
if (!is_initialized_) {
LOG_ERROR("AE_NEED_INIT");
return 4;
}
return 0;
}
int WgcSessionImpl::Resume() {
std::lock_guard locker(lock_);
is_paused_ = false;
if (!is_initialized_) {
LOG_ERROR("AE_NEED_INIT");
return 4;
}
return 0;
}
auto WgcSessionImpl::CreateD3D11Device() {
winrt::com_ptr<ID3D11Device> d3d_device;
HRESULT hr;
WINRT_ASSERT(!d3d_device);
D3D_DRIVER_TYPE type = D3D_DRIVER_TYPE_HARDWARE;
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
hr = D3D11CreateDevice(nullptr, type, nullptr, flags, nullptr, 0,
D3D11_SDK_VERSION, d3d_device.put(), nullptr, nullptr);
if (DXGI_ERROR_UNSUPPORTED == hr) {
// change D3D_DRIVER_TYPE
type = D3D_DRIVER_TYPE_WARP;
hr = D3D11CreateDevice(nullptr, type, nullptr, flags, nullptr, 0,
D3D11_SDK_VERSION, d3d_device.put(), nullptr,
nullptr);
}
winrt::check_hresult(hr);
winrt::com_ptr<::IInspectable> d3d11_device;
winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(
d3d_device.as<IDXGIDevice>().get(), d3d11_device.put()));
return d3d11_device
.as<winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice>();
}
auto WgcSessionImpl::CreateCaptureItemForWindow(HWND hwnd) {
auto activation_factory = winrt::get_activation_factory<
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
winrt::check_hresult(interop_factory->CreateForWindow(
hwnd,
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
reinterpret_cast<void**>(winrt::put_abi(item))));
return item;
}
auto WgcSessionImpl::CreateCaptureItemForMonitor(HMONITOR hmonitor) {
auto activation_factory = winrt::get_activation_factory<
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
winrt::check_hresult(interop_factory->CreateForMonitor(
hmonitor,
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
reinterpret_cast<void**>(winrt::put_abi(item))));
return item;
}
HRESULT WgcSessionImpl::CreateMappedTexture(
winrt::com_ptr<ID3D11Texture2D> src_texture, unsigned int width,
unsigned int height) {
D3D11_TEXTURE2D_DESC src_desc;
src_texture->GetDesc(&src_desc);
D3D11_TEXTURE2D_DESC map_desc;
map_desc.Width = width == 0 ? src_desc.Width : width;
map_desc.Height = height == 0 ? src_desc.Height : height;
map_desc.MipLevels = src_desc.MipLevels;
map_desc.ArraySize = src_desc.ArraySize;
map_desc.Format = src_desc.Format;
map_desc.SampleDesc = src_desc.SampleDesc;
map_desc.Usage = D3D11_USAGE_STAGING;
map_desc.BindFlags = 0;
map_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
map_desc.MiscFlags = 0;
auto d3dDevice =
GetDXGIInterfaceFromObject<ID3D11Device>(d3d11_direct_device_);
return d3dDevice->CreateTexture2D(&map_desc, nullptr,
d3d11_texture_mapped_.put());
}
int WgcSessionImpl::StartCaptureLocked(bool show_cursor) {
if (!is_initialized_) {
LOG_ERROR("AE_NEED_INIT");
return 4;
}
if (!capture_item_) {
LOG_ERROR("WGC: capture item is null");
return 4;
}
try {
if (!capture_session_) {
auto current_size = capture_item_.Size();
capture_framepool_ =
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
CreateFreeThreaded(d3d11_direct_device_,
winrt::Windows::Graphics::DirectX::
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2, current_size);
capture_session_ = capture_framepool_.CreateCaptureSession(capture_item_);
capture_frame_size_ = current_size;
capture_framepool_trigger_ = capture_framepool_.FrameArrived(
winrt::auto_revoke, {this, &WgcSessionImpl::OnFrame});
capture_close_trigger_ = capture_item_.Closed(
winrt::auto_revoke, {this, &WgcSessionImpl::OnClosed});
}
if (!capture_framepool_ || !capture_session_) {
throw std::exception();
}
capture_session_.IsCursorCaptureEnabled(show_cursor);
capture_session_.StartCapture();
is_running_ = true;
return 0;
} catch (const winrt::hresult_error&) {
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
return 86;
} catch (...) {
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
return 86;
}
}
int WgcSessionImpl::StartLocked(bool show_cursor) {
if (is_running_) return 0;
last_show_cursor_ = show_cursor;
if (!is_initialized_) {
const int init_ret = InitializeLocked();
if (init_ret != 0) {
return init_ret;
}
}
int ret = StartCaptureLocked(show_cursor);
if (ret == 0) {
return 0;
}
LOG_WARN("WGC: start capture failed, rebuilding capture item");
CleanUpLocked();
ret = InitializeLocked();
if (ret != 0) {
return ret;
}
return StartCaptureLocked(show_cursor);
}
void WgcSessionImpl::StopLocked() {
is_running_ = false;
// if (loop_.joinable()) loop_.join();
if (capture_framepool_trigger_) capture_framepool_trigger_.revoke();
if (capture_close_trigger_) capture_close_trigger_.revoke();
if (capture_session_) {
try {
capture_session_.Close();
} catch (...) {
LOG_WARN("WGC: capture session close failed");
}
capture_session_ = nullptr;
}
if (capture_framepool_) {
try {
capture_framepool_.Close();
} catch (...) {
LOG_WARN("WGC: frame pool close failed");
}
capture_framepool_ = nullptr;
}
d3d11_texture_mapped_ = nullptr;
}
void WgcSessionImpl::OnFrame(
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender,
[[maybe_unused]] winrt::Windows::Foundation::IInspectable const& args) {
std::lock_guard locker(lock_);
auto is_new_size = false;
try {
auto frame = sender.TryGetNextFrame();
auto frame_size = frame.ContentSize();
if (frame_size.Width != capture_frame_size_.Width ||
frame_size.Height != capture_frame_size_.Height) {
// The thing we have been capturing has changed size.
// We need to resize our swap chain first, then blit the pixels.
// After we do that, retire the frame and then recreate our frame pool.
is_new_size = true;
capture_frame_size_ = frame_size;
}
// copy to mapped texture
if (is_paused_) {
return;
}
auto frame_captured =
GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
if (!d3d11_texture_mapped_ || is_new_size) {
HRESULT tex_hr = CreateMappedTexture(frame_captured);
if (FAILED(tex_hr)) {
OutputDebugStringW(
(L"CreateMappedTexture failed: " + std::to_wstring(tex_hr))
.c_str());
return;
}
}
d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(),
frame_captured.get());
D3D11_MAPPED_SUBRESOURCE map_result;
HRESULT hr = d3d11_device_context_->Map(
d3d11_texture_mapped_.get(), 0, D3D11_MAP_READ,
0 /*coz we use CreateFreeThreaded, so we cant use flags
D3D11_MAP_FLAG_DO_NOT_WAIT*/
,
&map_result);
if (FAILED(hr)) {
OutputDebugStringW(
(L"map resource failed: " + std::to_wstring(hr)).c_str());
return;
}
// copy data from map_result.pData
if (map_result.pData && observer_) {
observer_->OnFrame(
wgc_session_frame{static_cast<unsigned int>(frame_size.Width),
static_cast<unsigned int>(frame_size.Height),
map_result.RowPitch,
const_cast<const unsigned char*>(
(unsigned char*)map_result.pData)},
id_);
}
d3d11_device_context_->Unmap(d3d11_texture_mapped_.get(), 0);
if (is_new_size) {
capture_framepool_.Recreate(
d3d11_direct_device_,
winrt::Windows::Graphics::DirectX::
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2, capture_frame_size_);
}
} catch (const winrt::hresult_error&) {
LOG_WARN("WGC: frame processing failed");
} catch (...) {
LOG_WARN("WGC: frame processing failed");
}
}
void WgcSessionImpl::OnClosed(
winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
winrt::Windows::Foundation::IInspectable const&) {
std::lock_guard locker(lock_);
const bool was_running = is_running_;
const bool was_paused = is_paused_;
try {
CleanUpLocked();
is_paused_ = was_paused;
if (InitializeLocked() == 0) {
int ret = was_running ? StartCaptureLocked(last_show_cursor_) : 0;
if (ret == 0) {
OutputDebugStringW(L"WgcSessionImpl::OnClosed: auto recovered");
} else {
OutputDebugStringW(L"WgcSessionImpl::OnClosed: recover Start failed");
}
} else {
OutputDebugStringW(
L"WgcSessionImpl::OnClosed: recover Initialize failed");
}
} catch (...) {
OutputDebugStringW(L"WgcSessionImpl::OnClosed: exception during recover");
}
}
int WgcSessionImpl::InitializeLocked() {
if (is_initialized_) return 0;
d3d11_texture_mapped_ = nullptr;
d3d11_device_context_ = nullptr;
d3d11_direct_device_ = nullptr;
capture_frame_size_ = {};
if (!(d3d11_direct_device_ = CreateD3D11Device())) {
LOG_ERROR("AE_D3D_CREATE_DEVICE_FAILED");
return 1;
}
try {
if (target_.is_window)
capture_item_ = CreateCaptureItemForWindow(target_.hwnd);
else
capture_item_ = CreateCaptureItemForMonitor(target_.hmonitor);
if (!capture_item_) {
LOG_ERROR("WGC: create capture item returned null");
return 86;
}
// Set up
auto d3d11_device =
GetDXGIInterfaceFromObject<ID3D11Device>(d3d11_direct_device_);
d3d11_device->GetImmediateContext(d3d11_device_context_.put());
} catch (winrt::hresult_error) {
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
return 86;
} catch (...) {
return 86;
}
is_initialized_ = true;
return 0;
}
void WgcSessionImpl::CleanUp() {
std::lock_guard locker(lock_);
CleanUpLocked();
}
void WgcSessionImpl::CleanUpLocked() {
StopLocked();
capture_item_ = nullptr;
d3d11_device_context_ = nullptr;
d3d11_direct_device_ = nullptr;
capture_frame_size_ = {};
is_initialized_ = false;
is_paused_ = false;
}
LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w_param,
LPARAM l_param) {
return DefWindowProc(window, message, w_param, l_param);
}
// void WgcSessionImpl::message_func() {
// const std::wstring kClassName = L"am_fake_window";
// WNDCLASS wc = {};
// wc.style = CS_HREDRAW | CS_VREDRAW;
// wc.lpfnWndProc = DefWindowProc;
// wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
// wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW);
// wc.lpszClassName = kClassName.c_str();
// if (!::RegisterClassW(&wc)) return;
// hwnd_ = ::CreateWindowW(kClassName.c_str(), nullptr, WS_OVERLAPPEDWINDOW,
// 0,
// 0, 0, 0, nullptr, nullptr, nullptr, nullptr);
// MSG msg;
// while (is_running_) {
// while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
// if (!is_running_) break;
// TranslateMessage(&msg);
// DispatchMessage(&msg);
// }
// Sleep(10);
// }
// ::CloseWindow(hwnd_);
// ::DestroyWindow(hwnd_);
// }
} // namespace crossdesk
@@ -0,0 +1,123 @@
#ifndef _WGC_SESSION_IMPL_H_
#define _WGC_SESSION_IMPL_H_
#include <d3d11_4.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Graphics.Capture.h>
#include <mutex>
#include <thread>
#include "wgc_session.h"
namespace crossdesk {
class WgcSessionImpl : public WgcSession {
struct __declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1"))
IDirect3DDxgiInterfaceAccess : ::IUnknown {
virtual HRESULT __stdcall GetInterface(GUID const& id, void** object) = 0;
};
template <typename T>
inline auto GetDXGIInterfaceFromObject(
winrt::Windows::Foundation::IInspectable const& object) {
auto access = object.as<IDirect3DDxgiInterfaceAccess>();
winrt::com_ptr<T> result;
winrt::check_hresult(
access->GetInterface(winrt::guid_of<T>(), result.put_void()));
return result;
}
struct {
union {
HWND hwnd;
HMONITOR hmonitor;
};
bool is_window;
} target_{0};
public:
WgcSessionImpl(int id);
~WgcSessionImpl() override;
public:
void Release() override;
int Initialize(HWND hwnd) override;
int Initialize(HMONITOR hmonitor) override;
void RegisterObserver(wgc_session_observer* observer) override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause() override;
int Resume() override;
private:
auto CreateD3D11Device();
auto CreateCaptureItemForWindow(HWND hwnd);
auto CreateCaptureItemForMonitor(HMONITOR hmonitor);
HRESULT CreateMappedTexture(winrt::com_ptr<ID3D11Texture2D> src_texture,
unsigned int width = 0, unsigned int height = 0);
void OnFrame(
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const&
sender,
winrt::Windows::Foundation::IInspectable const& args);
void OnClosed(winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
winrt::Windows::Foundation::IInspectable const&);
int InitializeLocked();
int StartLocked(bool show_cursor);
int StartCaptureLocked(bool show_cursor);
void StopLocked();
void CleanUp();
void CleanUpLocked();
// void message_func();
private:
int id_ = -1;
std::mutex lock_;
bool is_initialized_ = false;
bool is_running_ = false;
bool is_paused_ = false;
bool last_show_cursor_ = false;
wgc_session_observer* observer_ = nullptr;
// wgc
winrt::Windows::Graphics::Capture::GraphicsCaptureItem capture_item_{nullptr};
winrt::Windows::Graphics::Capture::GraphicsCaptureSession capture_session_{
nullptr};
winrt::Windows::Graphics::SizeInt32 capture_frame_size_;
winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice
d3d11_direct_device_{nullptr};
winrt::com_ptr<ID3D11DeviceContext> d3d11_device_context_{nullptr};
winrt::com_ptr<ID3D11Texture2D> d3d11_texture_mapped_{nullptr};
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool
capture_framepool_{nullptr};
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
FrameArrived_revoker capture_framepool_trigger_;
winrt::Windows::Graphics::Capture::GraphicsCaptureItem::Closed_revoker
capture_close_trigger_;
// message loop
std::thread loop_;
HWND hwnd_ = nullptr;
};
// template <typename T>
// inline auto WgcSessionImpl::GetDXGIInterfaceFromObject(
// winrt::Windows::Foundation::IInspectable const &object) {
// auto access = object.as<IDirect3DDxgiInterfaceAccess>();
// winrt::com_ptr<T> result;
// winrt::check_hresult(
// access->GetInterface(winrt::guid_of<T>(), result.put_void()));
// return result;
// }
} // namespace crossdesk
#endif
@@ -0,0 +1,42 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-04-21
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _INTERACTIVE_STATE_H_
#define _INTERACTIVE_STATE_H_
#include <string>
namespace crossdesk {
inline bool IsSecureDesktopInteractionRequired(
const std::string& interactive_stage) {
return interactive_stage == "lock-screen" ||
interactive_stage == "credential-ui" ||
interactive_stage == "secure-desktop";
}
inline bool ShouldNormalizeUnlockToUserDesktop(
bool interactive_lock_screen_visible, const std::string& interactive_stage,
bool session_locked, bool interactive_logon_ui_visible,
bool interactive_secure_desktop_active, bool credential_ui_visible,
bool password_box_visible, bool unlock_ui_visible,
const std::string& last_session_event) {
if (!interactive_lock_screen_visible && interactive_stage != "lock-screen") {
return false;
}
if (session_locked || interactive_logon_ui_visible ||
interactive_secure_desktop_active || credential_ui_visible ||
password_box_visible || unlock_ui_visible) {
return false;
}
return last_session_event != "session-lock";
}
} // namespace crossdesk
#endif
@@ -0,0 +1,87 @@
#include <Windows.h>
#include <iostream>
#include <string>
#include "service_host.h"
namespace {
std::wstring GetExecutablePath() {
wchar_t path[MAX_PATH] = {0};
DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH);
if (length == 0 || length >= MAX_PATH) {
return L"";
}
return std::wstring(path, length);
}
void PrintUsage() {
std::cout
<< "CrossDesk Windows service skeleton\n"
<< " --service Run under the Windows Service Control Manager\n"
<< " --console Run the service loop in console mode\n"
<< " --install Install the service for the current executable\n"
<< " --uninstall Remove the installed service\n"
<< " --start Start the installed service\n"
<< " --stop Stop the installed service\n"
<< " --sas Ask the service to send Secure Attention Sequence\n"
<< " --ping Ping the running service over named pipe IPC\n"
<< " --status Query runtime status over named pipe IPC\n";
}
} // namespace
int main(int argc, char* argv[]) {
crossdesk::CrossDeskServiceHost host;
if (argc <= 1) {
PrintUsage();
return 0;
}
std::string command = argv[1];
if (command == "--service") {
return host.RunAsService();
}
if (command == "--console") {
return host.RunInConsole();
}
if (command == "--install") {
std::wstring executable_path = GetExecutablePath();
bool success = !executable_path.empty() &&
crossdesk::InstallCrossDeskService(executable_path);
std::cout << (success ? "install ok" : "install failed") << std::endl;
return success ? 0 : 1;
}
if (command == "--uninstall") {
bool success = crossdesk::UninstallCrossDeskService();
std::cout << (success ? "uninstall ok" : "uninstall failed") << std::endl;
return success ? 0 : 1;
}
if (command == "--start") {
bool success = crossdesk::StartCrossDeskService();
std::cout << (success ? "start ok" : "start failed") << std::endl;
return success ? 0 : 1;
}
if (command == "--stop") {
bool success = crossdesk::StopCrossDeskService();
std::cout << (success ? "stop ok" : "stop failed") << std::endl;
return success ? 0 : 1;
}
if (command == "--sas") {
std::cout << crossdesk::QueryCrossDeskService("sas") << std::endl;
return 0;
}
if (command == "--ping") {
std::cout << crossdesk::QueryCrossDeskService("ping") << std::endl;
return 0;
}
if (command == "--status") {
std::cout << crossdesk::QueryCrossDeskService("status") << std::endl;
return 0;
}
PrintUsage();
return 1;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,167 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-04-21
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SERVICE_HOST_H_
#define _SERVICE_HOST_H_
#include <Windows.h>
#include <cstdint>
#include <mutex>
#include <string>
#include <thread>
namespace crossdesk {
inline constexpr wchar_t kCrossDeskServiceName[] = L"CrossDeskService";
inline constexpr wchar_t kCrossDeskServiceDisplayName[] = L"CrossDesk Service";
inline constexpr wchar_t kCrossDeskServicePipeName[] =
L"\\\\.\\pipe\\CrossDeskService";
class CrossDeskServiceHost {
public:
CrossDeskServiceHost();
~CrossDeskServiceHost();
int RunAsService();
int RunInConsole();
private:
int RunServiceLoop(bool as_service);
int InitializeRuntime();
void ShutdownRuntime();
void RequestStop();
void ClientProcessMonitorLoop();
void ReportServiceStatus(DWORD current_state, DWORD win32_exit_code,
DWORD wait_hint);
void IpcServerLoop();
void RefreshSessionState();
void EnsureSessionHelper();
void ReapSessionHelper();
void StopSessionHelper();
bool LaunchSessionHelper(DWORD session_id);
void ReapSecureInputHelper();
void StopSecureInputHelper();
bool LaunchSecureInputHelper(DWORD session_id,
const std::string& interactive_stage,
const std::string& interactive_desktop);
std::wstring GetSessionHelperPath() const;
std::wstring GetSessionHelperStopEventName(DWORD session_id) const;
std::wstring GetSecureInputHelperPath() const;
std::wstring GetSecureInputHelperStopEventName(DWORD session_id) const;
void ResetSessionHelperReportedStateLocked(const char* error,
DWORD error_code);
bool GetEffectiveSessionLockedLocked() const;
bool IsHelperReportingLockScreenLocked() const;
bool HasSecureInputUiLocked() const;
void UpdateSasSecureDesktopGraceLocked(const std::string& observed_stage);
bool IsSasSecureDesktopGraceActiveLocked() const;
bool ShouldKeepSecureInputHelperLocked(DWORD target_session_id) const;
std::string ResolveInteractiveStageLocked() const;
std::string ResolveInteractiveDesktopLocked(
const std::string& interactive_stage) const;
void RefreshSessionHelperReportedState();
void RecordSessionEvent(DWORD event_type, DWORD session_id);
std::string HandleIpcCommand(const std::string& command);
std::string BuildStatusResponse();
std::string SendSecureAttentionSequence();
std::string SendSecureDesktopKeyboardInput(int key_code, bool is_down,
uint32_t scan_code = 0,
bool extended = false);
std::string SendSecureDesktopMouseInput(int x, int y, int wheel, int flag);
static void WINAPI ServiceMain(DWORD argc, LPWSTR* argv);
static BOOL WINAPI ConsoleControlHandler(DWORD control_type);
static DWORD WINAPI ServiceControlHandler(DWORD control, DWORD event_type,
LPVOID event_data, LPVOID context);
private:
SERVICE_STATUS_HANDLE status_handle_ = nullptr;
SERVICE_STATUS service_status_{};
HANDLE stop_event_ = nullptr;
std::thread ipc_thread_;
std::thread client_process_monitor_thread_;
std::mutex state_mutex_;
DWORD active_session_id_ = 0xFFFFFFFF;
DWORD process_session_id_ = 0xFFFFFFFF;
DWORD input_desktop_error_code_ = 0;
DWORD session_helper_process_id_ = 0;
DWORD session_helper_session_id_ = 0xFFFFFFFF;
DWORD session_helper_exit_code_ = 0;
DWORD session_helper_last_error_code_ = 0;
DWORD session_helper_status_error_code_ = 0;
DWORD session_helper_report_session_id_ = 0xFFFFFFFF;
DWORD session_helper_report_process_id_ = 0;
DWORD session_helper_report_input_desktop_error_code_ = 0;
DWORD secure_input_helper_process_id_ = 0;
DWORD secure_input_helper_session_id_ = 0xFFFFFFFF;
DWORD secure_input_helper_exit_code_ = 0;
DWORD secure_input_helper_last_error_code_ = 0;
DWORD last_session_event_type_ = 0;
DWORD last_session_event_session_id_ = 0xFFFFFFFF;
ULONGLONG started_at_tick_ = 0;
ULONGLONG last_sas_tick_ = 0;
ULONGLONG session_helper_started_at_tick_ = 0;
ULONGLONG session_helper_report_state_age_ms_ = 0;
ULONGLONG session_helper_report_uptime_ms_ = 0;
ULONGLONG secure_input_helper_started_at_tick_ = 0;
ULONGLONG sas_secure_desktop_until_tick_ = 0;
bool session_locked_ = false;
bool logon_ui_visible_ = false;
bool consent_ui_visible_ = false;
bool prelogin_ = false;
bool secure_desktop_active_ = false;
bool input_desktop_available_ = false;
bool session_helper_running_ = false;
bool session_helper_status_ok_ = false;
bool session_helper_report_session_locked_ = false;
bool session_helper_report_input_desktop_available_ = false;
bool session_helper_report_lock_app_visible_ = false;
bool session_helper_report_logon_ui_visible_ = false;
bool session_helper_report_consent_ui_visible_ = false;
bool session_helper_report_secure_desktop_active_ = false;
bool session_helper_report_credential_ui_visible_ = false;
bool session_helper_report_unlock_ui_visible_ = false;
bool secure_input_helper_running_ = false;
bool console_mode_ = false;
bool sas_secure_desktop_seen_ = false;
DWORD last_sas_error_code_ = 0;
bool last_sas_success_ = false;
HANDLE session_helper_process_handle_ = nullptr;
HANDLE session_helper_stop_event_ = nullptr;
HANDLE secure_input_helper_process_handle_ = nullptr;
HANDLE secure_input_helper_stop_event_ = nullptr;
std::string input_desktop_name_;
std::string last_sas_error_;
std::string session_helper_last_error_;
std::string session_helper_status_error_;
std::string session_helper_report_input_desktop_;
std::string session_helper_report_interactive_stage_;
std::string secure_input_helper_last_error_;
std::string secure_input_helper_interactive_stage_;
std::string secure_input_helper_interactive_desktop_;
static CrossDeskServiceHost* instance_;
};
bool IsCrossDeskServiceInstalled();
bool InstallCrossDeskService(const std::wstring& binary_path);
bool UninstallCrossDeskService();
bool StartCrossDeskService();
bool StopCrossDeskService(DWORD timeout_ms = 5000);
std::string QueryCrossDeskService(const std::string& command,
DWORD timeout_ms = 1000);
std::string SendCrossDeskSecureDesktopKeyInput(int key_code, bool is_down,
uint32_t scan_code = 0,
bool extended = false,
DWORD timeout_ms = 1000);
std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
int flag,
DWORD timeout_ms = 1000);
} // namespace crossdesk
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-04-21
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SESSION_HELPER_SHARED_H_
#define _SESSION_HELPER_SHARED_H_
#include <Windows.h>
#include <cstdint>
#include <string>
namespace crossdesk {
inline constexpr wchar_t kCrossDeskSessionHelperPipePrefix[] =
L"\\\\.\\pipe\\CrossDeskSessionHelper-";
inline constexpr wchar_t kCrossDeskSecureInputHelperPipePrefix[] =
L"\\\\.\\pipe\\CrossDeskSecureInputHelper-";
inline constexpr char kCrossDeskSessionHelperStatusCommand[] = "status";
inline constexpr char kCrossDeskSecureInputKeyboardCommandPrefix[] =
"keyboard:";
inline constexpr char kCrossDeskSecureInputMouseCommandPrefix[] = "mouse:";
inline constexpr char kCrossDeskSecureInputCaptureCommandPrefix[] = "capture:";
inline constexpr char kCrossDeskSecureInputCaptureStartCommandPrefix[] =
"capture-start:";
inline constexpr char kCrossDeskSecureInputCaptureStopCommand[] =
"capture-stop";
inline constexpr DWORD kCrossDeskSecureInputPipeBufferBytes = 16 * 1024 * 1024;
inline constexpr wchar_t kCrossDeskSecureDesktopFrameMappingPrefix[] =
L"Global\\CrossDeskSecureDesktopFrame-";
inline constexpr wchar_t kCrossDeskSecureDesktopFrameReadyEventPrefix[] =
L"Global\\CrossDeskSecureDesktopFrameReady-";
inline constexpr uint32_t kCrossDeskSecureDesktopFrameMagic = 0x50444358;
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 1;
#pragma pack(push, 1)
struct CrossDeskSecureDesktopFrameHeader {
uint32_t magic;
uint32_t version;
int32_t left;
int32_t top;
uint32_t width;
uint32_t height;
uint32_t payload_size;
};
struct CrossDeskSecureDesktopSharedFrameHeader {
uint32_t magic;
uint32_t version;
volatile uint32_t writing;
uint32_t sequence;
int32_t left;
int32_t top;
uint32_t width;
uint32_t height;
uint32_t payload_size;
uint32_t buffer_size;
};
#pragma pack(pop)
inline std::wstring GetCrossDeskSessionHelperPipeName(DWORD session_id) {
return std::wstring(kCrossDeskSessionHelperPipePrefix) +
std::to_wstring(session_id);
}
inline std::wstring GetCrossDeskSecureInputHelperPipeName(DWORD session_id) {
return std::wstring(kCrossDeskSecureInputHelperPipePrefix) +
std::to_wstring(session_id);
}
inline std::wstring GetCrossDeskSecureDesktopFrameMappingName(
DWORD session_id) {
return std::wstring(kCrossDeskSecureDesktopFrameMappingPrefix) +
std::to_wstring(session_id);
}
inline std::wstring GetCrossDeskSecureDesktopFrameReadyEventName(
DWORD session_id) {
return std::wstring(kCrossDeskSecureDesktopFrameReadyEventPrefix) +
std::to_wstring(session_id);
}
} // namespace crossdesk
#endif
@@ -0,0 +1,11 @@
#include "speaker_capturer_factory.h"
#include "speaker_capturer_wasapi.h"
namespace crossdesk {
SpeakerCapturer* SpeakerCapturerFactory::Create() {
return new SpeakerCapturerWasapi();
}
} // namespace crossdesk
@@ -0,0 +1,104 @@
#include "speaker_capturer_wasapi.h"
#include "rd_log.h"
#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio.h"
#define SAVE_AUDIO_FILE 0
namespace crossdesk {
static ma_device_config device_config_;
static ma_device device_;
static ma_format format_ = ma_format_s16;
static ma_uint32 sample_rate_ = ma_standard_sample_rate_48000;
static ma_uint32 channels_ = 1;
static FILE* fp_ = nullptr;
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput,
ma_uint32 frameCount) {
SpeakerCapturerWasapi* ptr = (SpeakerCapturerWasapi*)pDevice->pUserData;
if (ptr) {
if (SAVE_AUDIO_FILE) {
fwrite(pInput, frameCount * ma_get_bytes_per_frame(format_, channels_), 1,
fp_);
}
ptr->GetCallback()((unsigned char*)pInput,
frameCount * ma_get_bytes_per_frame(format_, channels_),
"audio");
}
(void)pOutput;
}
SpeakerCapturerWasapi::speaker_data_cb SpeakerCapturerWasapi::GetCallback() {
return cb_;
}
SpeakerCapturerWasapi::SpeakerCapturerWasapi() {}
SpeakerCapturerWasapi::~SpeakerCapturerWasapi() {
if (SAVE_AUDIO_FILE) {
fclose(fp_);
}
}
int SpeakerCapturerWasapi::Init(speaker_data_cb cb) {
if (inited_) {
return 0;
}
cb_ = cb;
if (SAVE_AUDIO_FILE) {
fopen_s(&fp_, "system_audio.pcm", "wb");
}
ma_result result;
ma_backend backends[] = {ma_backend_wasapi};
device_config_ = ma_device_config_init(ma_device_type_loopback);
device_config_.capture.pDeviceID = NULL;
device_config_.capture.format = format_;
device_config_.capture.channels = channels_;
device_config_.sampleRate = sample_rate_;
device_config_.dataCallback = data_callback;
device_config_.pUserData = this;
result = ma_device_init_ex(backends, sizeof(backends) / sizeof(backends[0]),
NULL, &device_config_, &device_);
if (result != MA_SUCCESS) {
LOG_ERROR("Failed to initialize loopback device");
return -1;
}
inited_ = true;
return 0;
}
int SpeakerCapturerWasapi::Start() {
ma_result result = ma_device_start(&device_);
if (result != MA_SUCCESS) {
ma_device_uninit(&device_);
LOG_ERROR("Failed to start device");
return -1;
}
return 0;
}
int SpeakerCapturerWasapi::Stop() {
ma_device_stop(&device_);
return 0;
}
int SpeakerCapturerWasapi::Destroy() {
ma_device_uninit(&device_);
return 0;
}
int SpeakerCapturerWasapi::Pause() { return 0; }
} // namespace crossdesk
@@ -0,0 +1,37 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-08-15
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SPEAKER_CAPTURER_WASAPI_H_
#define _SPEAKER_CAPTURER_WASAPI_H_
#include "speaker_capturer.h"
namespace crossdesk {
class SpeakerCapturerWasapi : public SpeakerCapturer {
public:
SpeakerCapturerWasapi();
~SpeakerCapturerWasapi();
public:
virtual int Init(speaker_data_cb cb);
virtual int Destroy();
virtual int Start();
virtual int Stop();
int Pause();
int Resume();
speaker_data_cb GetCallback();
private:
speaker_data_cb cb_ = nullptr;
private:
bool inited_ = false;
};
} // namespace crossdesk
#endif
@@ -0,0 +1,45 @@
#include "platform.h"
#include <Winsock2.h>
#include <iphlpapi.h>
#include <cstdio>
#include "rd_log.h"
namespace crossdesk {
std::string GetMac() {
IP_ADAPTER_INFO adapters[16] = {};
DWORD size = sizeof(adapters);
if (GetAdaptersInfo(adapters, &size) != ERROR_SUCCESS) return {};
char address[16] = {};
int length = 0;
const PIP_ADAPTER_INFO adapter = adapters;
for (UINT index = 0; index < adapter->AddressLength; ++index) {
length += sprintf_s(address + length, sizeof(address) - length, "%.2X",
adapter->Address[index]);
}
return address;
}
std::string GetHostName() {
WSADATA data = {};
if (WSAStartup(MAKEWORD(2, 2), &data) != 0) {
LOG_ERROR("WSAStartup failed");
return {};
}
char hostname[256] = {};
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) {
LOG_ERROR("gethostname failed: {}", WSAGetLastError());
WSACleanup();
return {};
}
WSACleanup();
return hostname;
}
bool IsWaylandSession() { return false; }
} // namespace crossdesk