Compare commits

...
Author SHA1 Message Date
dijunkun e263aab10d [fix] keep Windows privacy screen active when running as administrator 2026-09-17 09:55:34 +08:00
dijunkun 9abdfbf684 [release] prepare v1.5.1 release notes 2026-09-17 02:56:49 +08:00
dijunkun 31a0b86cee [fix] refine network stats layout and alignment 2026-09-17 02:45:35 +08:00
dijunkun fb34f42744 [fix] show encryption status and simplify network stats labels 2026-09-17 02:01:55 +08:00
dijunkun 4302ab291c [fix] overlay control bar tooltips on expanded network stats 2026-09-17 01:39:09 +08:00
dijunkun 4bf92c9e2c [fix] use reliable transport for mouse input and cursor state 2026-09-17 01:31:54 +08:00
dijunkun 28de4cf938 [fix] improve Windows taskbar icon clarity at small sizes 2026-09-16 23:19:12 +08:00
dijunkun ce7a3c178e [fix] correct WGC and GDI cursor capture and positioning 2026-09-16 23:03:36 +08:00
dijunkun d9116200fe [fix] restore remote cursors on Windows without a physical mouse 2026-09-16 22:20:36 +08:00
dijunkun 1d9242515a [fix] restore remote cursor visibility during Windows privacy screen 2026-09-16 20:27:59 +08:00
dijunkun ad62e60b9a [fix] restore shared capture on Windows secure desktop 2026-09-16 15:31:27 +08:00
dijunkun c4aca303c8 [fix] restore cursor visibility on Windows lock screen 2026-09-16 14:48:39 +08:00
dijunkun 57bb91dba3 [fix] prevent duplicate cursors during Windows window dragging 2026-09-16 13:51:35 +08:00
dijunkun 57cab3979e [fix] initialize logging before startup operations and update MiniRTC 2026-09-16 12:54:07 +08:00
dijunkun d8ccdbaa38 [fix] add capture delivery diagnostics 2026-09-16 12:54:06 +08:00
46 changed files with 1356 additions and 243 deletions
+9 -1
View File
@@ -22,6 +22,7 @@
#include "config_center.h"
#include "daemon.h"
#include "path_manager.h"
#include "rd_log.h"
#include "render.h"
#ifdef __linux__
@@ -192,6 +193,14 @@ int main(int argc, char* argv[]) {
std::strcmp(argv[1], crossdesk::kSlintRendererProbeArgument) == 0) {
return crossdesk::RunSlintRendererProbe();
}
#endif
// Service operations and config migration can log before the GUI starts.
// Configure the directory first, including CLI and daemon child paths.
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
crossdesk::InitLogger(path_manager->GetLogPath().string());
#ifdef _WIN32
if (argc > 1 && IsServiceCliCommand(argv[1])) {
return HandleServiceCliCommand(argv[1]);
}
@@ -218,7 +227,6 @@ int main(int argc, char* argv[]) {
#endif
bool enable_daemon = false;
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
if (path_manager) {
std::string cache_path = path_manager->GetCachePath().string();
crossdesk::ConfigCenter config_center(cache_path + "/config.ini");
@@ -67,7 +67,6 @@ struct InteractionState {
bool start_screen_capturer_ = false;
bool screen_capturer_is_started_ = false;
bool start_keyboard_capturer_ = false;
bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false;
bool keyboard_capturer_uses_window_events_ = false;
bool foucs_on_main_window_ = false;
@@ -40,6 +40,7 @@
#include "platform/windows/gui/slint_backend.h"
#include "platform/windows/gui/tray/win_tray.h"
#include "platform/windows/gui/window_icons.h"
#elif defined(__APPLE__)
#include "platform/macos/gui/tray/mac_tray.h"
#include "platform/window_drag.h"
@@ -950,10 +951,10 @@ int GuiApplication::Run() {
}
exec_log_path_ = path_manager_->GetLogPath().string();
dll_log_path_ = exec_log_path_;
InitializeLogger();
cache_path_ = path_manager_->GetCachePath().string();
config_center_ = std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
InitializeLogger();
LOG_INFO("CrossDesk version: {} (Slint UI)", CROSSDESK_VERSION);
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
@@ -1013,6 +1014,9 @@ int GuiApplication::Run() {
// when deciding whether the last hidden window should stop the event loop.
// Keep the loop alive until the tray's explicit Exit action requests quit.
ui_->main->show();
#if _WIN32
ConfigureWindowsWindowIcons(ui_->main->window().win32_hwnd());
#endif
slint::run_event_loop(slint::EventLoopMode::RunUntilQuit);
Cleanup();
return 0;
@@ -1155,6 +1159,9 @@ void GuiApplication::InitializeSystemTray() {
return;
}
ui_->main->show();
#if _WIN32
ConfigureWindowsWindowIcons(ui_->main->window().win32_hwnd());
#endif
ui_->main->window().set_minimized(false);
#if defined(__APPLE__)
MacActivateWindow(ui_->main->window().appkit_view());
@@ -1584,6 +1591,10 @@ void GuiApplication::BindStreamCallbacks() {
return;
}
props->selected_display_ = index;
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->cursor_presentation_ = {};
}
RemoteAction action{};
action.type = ControlType::display_id;
action.d = index;
@@ -1883,9 +1894,20 @@ void GuiApplication::ShareLocalCursorState() {
}
CursorState sampled{};
if (!cursor_state_provider_.Sample(devices_.display_info_list(),
selected_display_, &sampled)) {
return;
const bool sample_ok = cursor_state_provider_.Sample(
devices_.display_info_list(), selected_display_, &sampled);
if (!sample_ok) {
sampled.render_mode = CursorRenderMode::unknown;
if (sampled.hidden_reason == CursorHiddenReason::unspecified) {
sampled.hidden_reason = CursorHiddenReason::sampling_failed;
}
} else if (sampled.render_mode == CursorRenderMode::legacy) {
// Providers without platform-specific presentation metadata only need to
// sample visible/shape; normalize that common case once for every peer.
sampled.render_mode = sampled.visible ? CursorRenderMode::separate
: CursorRenderMode::hidden;
sampled.hidden_reason = sampled.visible ? CursorHiddenReason::unspecified
: CursorHiddenReason::system_hidden;
}
const auto now = std::chrono::steady_clock::now();
@@ -1924,21 +1946,23 @@ void GuiApplication::ShareLocalCursorState() {
const CursorState& previous = delivery.last_sent;
const bool shape_changed =
!delivery.has_sent || sampled.visible != previous.visible ||
sampled.shape != previous.shape ||
std::abs(sampled.visual_offset_x - previous.visual_offset_x) >
kCursorPositionEpsilon ||
std::abs(sampled.visual_offset_y - previous.visual_offset_y) >
kCursorPositionEpsilon;
!delivery.has_sent || sampled.render_mode != previous.render_mode ||
sampled.hidden_reason != previous.hidden_reason ||
(sample_ok &&
(sampled.visible != previous.visible || sampled.shape != previous.shape ||
std::abs(sampled.visual_offset_x - previous.visual_offset_x) >
kCursorPositionEpsilon ||
std::abs(sampled.visual_offset_y - previous.visual_offset_y) >
kCursorPositionEpsilon));
// Preserve sub-point cursor motion. At 10x client zoom, the old roughly
// one-logical-pixel threshold became several visible phone points.
const bool position_changed =
!delivery.has_sent ||
sampled.position_valid != previous.position_valid ||
sampled.display_id != previous.display_id ||
(sampled.position_valid && previous.position_valid &&
(std::abs(sampled.x - previous.x) > kCursorPositionEpsilon ||
std::abs(sampled.y - previous.y) > kCursorPositionEpsilon));
sample_ok &&
(!delivery.has_sent || sampled.position_valid != previous.position_valid ||
sampled.display_id != previous.display_id ||
(sampled.position_valid && previous.position_valid &&
(std::abs(sampled.x - previous.x) > kCursorPositionEpsilon ||
std::abs(sampled.y - previous.y) > kCursorPositionEpsilon)));
const bool position_update_due =
position_changed &&
(delivery.last_sent_time.time_since_epoch().count() == 0 ||
@@ -1946,16 +1970,12 @@ void GuiApplication::ShareLocalCursorState() {
const bool heartbeat_due =
delivery.last_sent_time.time_since_epoch().count() == 0 ||
now - delivery.last_sent_time >= kCursorStateHeartbeatInterval;
if (suppress_position) {
// Cursor appearance is independent of cursor position. Continue sending
// shape changes to every controller while withholding only the sampled
// position from the connection that originated recent input.
if (shape_changed || heartbeat_due) {
recipients.push_back({remote_id, false});
}
} else if (delivery.feedback_pending || shape_changed ||
position_update_due || heartbeat_due) {
recipients.push_back({remote_id, true});
// Position feedback waits for a good sample and the echo-suppression
// interval. Appearance changes and heartbeats are independent of both.
const bool include_position = sample_ok && !suppress_position;
if (shape_changed || heartbeat_due ||
(include_position && (delivery.feedback_pending || position_update_due))) {
recipients.push_back({remote_id, include_position});
}
}
if (recipients.empty()) return;
@@ -1964,6 +1984,18 @@ void GuiApplication::ShareLocalCursorState() {
for (const auto& recipient : recipients) {
auto& delivery = cursor_delivery_states_[recipient.remote_id];
CursorState outgoing = sampled;
if (!sample_ok) {
// Explicitly report unknown to new receivers, but retain the last
// presentation for peers that only understand visible/shape.
outgoing = delivery.has_sent ? delivery.last_sent : CursorState{};
if (!delivery.has_sent) {
outgoing.visible = true;
outgoing.shape = RemoteCursorShape::default_cursor;
}
outgoing.seq = sampled.seq;
outgoing.render_mode = sampled.render_mode;
outgoing.hidden_reason = sampled.hidden_reason;
}
outgoing.position_update = recipient.include_position;
if (!recipient.include_position) {
// Keep legacy receivers at their last acknowledged position as well.
@@ -1982,7 +2014,7 @@ void GuiApplication::ShareLocalCursorState() {
action.type = ControlType::cursor_state;
action.cs = outgoing;
const std::string message = action.to_json();
const int result = SendDataFrameToPeer(
const int result = SendReliableDataFrameToPeer(
peer_, message.c_str(), message.size(), mouse_label_.c_str(),
recipient.remote_id.c_str(), recipient.remote_id.size());
if (result != 0) {
@@ -1991,6 +2023,13 @@ void GuiApplication::ShareLocalCursorState() {
continue;
}
if (!delivery.has_sent ||
delivery.last_sent.render_mode != outgoing.render_mode ||
delivery.last_sent.hidden_reason != outgoing.hidden_reason) {
LOG_INFO("Send cursor presentation to [{}]: mode={}, reason={}",
recipient.remote_id, static_cast<int>(outgoing.render_mode),
static_cast<int>(outgoing.hidden_reason));
}
delivery.last_sent = outgoing;
delivery.has_sent = true;
if (recipient.include_position) {
@@ -2130,6 +2169,9 @@ void GuiApplication::SyncMainWindow() {
if (!ui_) {
return;
}
#if _WIN32
ConfigureWindowsWindowIcons(ui_->main->window().win32_hwnd());
#endif
#if defined(__APPLE__)
if (ui_->main_native_titlebar_attempts > 0) {
if (HideDisabledMainWindowZoomButton()) {
@@ -2419,6 +2461,9 @@ void GuiApplication::SyncStreamWindow() {
if (!ui_->stream) {
return;
}
#if _WIN32
ConfigureWindowsWindowIcons((*ui_->stream)->window().win32_hwnd());
#endif
(*ui_->stream)->set_window_maximized((*ui_->stream)->window().is_maximized());
#if defined(__APPLE__)
if (ui_->stream_live_resize_configuration_attempts > 0) {
@@ -2582,9 +2627,7 @@ void GuiApplication::SyncStreamWindow() {
props->remote_cursor_state_received_;
if (remote_cursor_active) {
remote_cursor_shape = static_cast<int>(
props->remote_cursor_state_.visible
? props->remote_cursor_state_.shape
: RemoteCursorShape::none);
props->cursor_presentation_.Resolve(SDL_GetTicks()));
}
}
(*ui_->stream)->set_remote_cursor_active(remote_cursor_active);
@@ -2609,6 +2652,8 @@ void GuiApplication::SyncStreamWindow() {
(*ui_->stream)->set_selected_display(props->selected_display_);
const auto& net = props->net_traffic_stats_;
(*ui_->stream)->set_stats_encryption_enabled(
status == ConnectionStatus::Connected && net.srtp_active);
std::vector<ui::NetworkStatsRow> stats_rows;
stats_rows.reserve(4);
const auto append_stats_row = [&](const std::string& label,
@@ -2865,6 +2910,10 @@ void GuiApplication::SyncServerWindow() {
return;
}
#if _WIN32
ConfigureWindowsWindowIcons((*ui_->server)->window().win32_hwnd());
#endif
std::vector<ui::ControllerEntry> controllers;
std::vector<slint::SharedString> names;
ui_->controller_ids.clear();
@@ -3267,8 +3316,12 @@ void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
controlled_remote_id_ = props->remote_id_;
const std::string message = action.to_json();
SendDataFrame(props->peer_, message.c_str(), message.size(),
props->mouse_label_.c_str());
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->cursor_presentation_.NoteInput(SDL_GetTicks());
}
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
props->mouse_label_.c_str());
}
void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
@@ -3305,10 +3358,14 @@ void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
if (action.m.s == 0) {
return;
}
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->cursor_presentation_.NoteInput(SDL_GetTicks());
}
controlled_remote_id_ = props->remote_id_;
const std::string message = action.to_json();
SendDataFrame(props->peer_, message.c_str(), message.size(),
props->mouse_label_.c_str());
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
props->mouse_label_.c_str());
}
void GuiApplication::SendKeyInput(const std::string& text, bool pressed,
@@ -366,8 +366,8 @@ int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
}
if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
}
} else if (SDL_EVENT_MOUSE_WHEEL == event.type &&
last_mouse_event.button.x >= render_rect.x &&
@@ -412,8 +412,8 @@ int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
}
if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
}
}
}
@@ -233,6 +233,9 @@ struct TranslationRow {
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
X(transport_encryption, u8"加密传输", "Encryption", u8"Шифрование") \
X(transport_encryption_enabled, u8"已开启", "Enabled", u8"Включено") \
X(transport_encryption_disabled, u8"未开启", "Disabled", u8"Выключено") \
X(online, u8"在线", "Online", u8"Онлайн") \
X(offline, u8"离线", "Offline", u8"Офлайн") \
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
@@ -1,11 +1,12 @@
#include "features/devices/session_device_manager.h"
#include <display_stream_id.h>
#include <remote_action.h>
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <display_stream_id.h>
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
@@ -59,6 +60,53 @@ bool SessionDeviceManager::ShouldSendCapturedFrame(
return true;
}
void SessionDeviceManager::RecordCaptureCadence(
std::chrono::steady_clock::time_point now, int fps,
bool from_secure_desktop) {
std::lock_guard<std::mutex> lock(capture_metrics_mutex_);
if (capture_metrics_started_ == std::chrono::steady_clock::time_point{})
capture_metrics_started_ = now;
const auto elapsed_us = std::chrono::duration_cast<std::chrono::microseconds>(
now - capture_metrics_started_)
.count();
if (elapsed_us >= 2000000) {
if (capture_forwarded_) {
const char* source = capture_secure_callbacks_ == 0 ? "normal_desktop"
: capture_secure_callbacks_ == capture_callbacks_
? "secure_desktop"
: "mixed";
LOG_INFO(
"Capture delivery: source={} normal_callbacks={} secure_callbacks={} "
"target_fps={} input_fps={:.2f} "
"forwarded_fps={:.2f} callbacks={} rate_dropped={} "
"forwarded={} send_errors={} max_callback_gap_us={} "
"send_avg_us={} send_max_us={} window_ms={}",
source, capture_callbacks_ - capture_secure_callbacks_,
capture_secure_callbacks_, fps,
capture_callbacks_ * 1000000.0 / elapsed_us,
capture_forwarded_ * 1000000.0 / elapsed_us, capture_callbacks_,
capture_rate_drops_, capture_forwarded_, capture_send_errors_,
capture_max_gap_us_, capture_send_us_ / capture_forwarded_,
capture_max_send_us_, elapsed_us / 1000);
}
capture_metrics_started_ = now;
capture_callbacks_ = capture_rate_drops_ = capture_forwarded_ = 0;
capture_secure_callbacks_ = 0;
capture_send_errors_ = 0;
capture_max_gap_us_ = capture_send_us_ = capture_max_send_us_ = 0;
}
if (last_capture_callback_ != std::chrono::steady_clock::time_point{}) {
capture_max_gap_us_ =
std::max<int64_t>(capture_max_gap_us_,
std::chrono::duration_cast<std::chrono::microseconds>(
now - last_capture_callback_)
.count());
}
last_capture_callback_ = std::max(last_capture_callback_, now);
++capture_callbacks_;
if (from_secure_desktop) ++capture_secure_callbacks_;
}
void SessionDeviceManager::Initialize() {
InitializeAudioOutput();
screen_capturer_factory_ = new ScreenCapturerFactory();
@@ -88,6 +136,11 @@ int SessionDeviceManager::InitializeScreenCapturer() {
last_frame_time_ = {};
next_frame_deadline_ = {};
capture_metrics_started_ = last_capture_callback_ = {};
capture_callbacks_ = capture_rate_drops_ = capture_forwarded_ = 0;
capture_secure_callbacks_ = 0;
capture_send_errors_ = 0;
capture_max_gap_us_ = capture_send_us_ = capture_max_send_us_ = 0;
const int fps = owner_.config_center_->GetVideoFrameRate() ==
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
? 30
@@ -103,7 +156,15 @@ int SessionDeviceManager::InitializeScreenCapturer() {
const char *display_name,
const MiniRtcNativeVideoFrame *native_frame) {
const auto now_time = std::chrono::steady_clock::now();
bool from_secure_desktop = false;
#ifdef _WIN32
from_secure_desktop =
ScreenCapturerWin::CurrentFrameIsFromSecureDesktop();
#endif
RecordCaptureCadence(now_time, fps, from_secure_desktop);
if (!ShouldSendCapturedFrame(now_time, fps)) {
std::lock_guard<std::mutex> lock(capture_metrics_mutex_);
++capture_rate_drops_;
return;
}
@@ -170,9 +231,24 @@ int SessionDeviceManager::InitializeScreenCapturer() {
frame.height = height;
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
frame.native_frame = native_frame;
const auto send_started = std::chrono::steady_clock::now();
uint64_t send_errors = 0;
for (const std::string &remote_id : connected_remote_ids) {
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
remote_id.data(), remote_id.size());
if (SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
remote_id.data(), remote_id.size()) != 0)
++send_errors;
}
const auto send_us =
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - send_started)
.count();
{
std::lock_guard<std::mutex> lock(capture_metrics_mutex_);
++capture_forwarded_;
capture_send_errors_ += send_errors;
capture_send_us_ += send_us;
capture_max_send_us_ =
std::max<int64_t>(capture_max_send_us_, send_us);
}
last_video_frame_stream_id_ = stream_id;
last_frame_time_ = now_time;
@@ -195,6 +271,15 @@ int SessionDeviceManager::InitializeScreenCapturer() {
return -1;
}
bool SessionDeviceManager::ShouldCaptureCursor() const {
std::shared_lock lock(owner_.connection_status_mutex_);
return std::any_of(owner_.connection_status_.begin(),
owner_.connection_status_.end(), [](const auto& entry) {
return entry.second == ConnectionStatus::Connected &&
entry.first.find("web") != std::string::npos;
});
}
int SessionDeviceManager::StartScreenCapturer() {
#ifdef __APPLE__
if (!owner_.EnsureMacScreenRecordingPermission()) {
@@ -210,8 +295,11 @@ int SessionDeviceManager::StartScreenCapturer() {
}
}
LOG_INFO("Start screen capturer, show cursor: {}", owner_.show_cursor_);
const int ret = screen_capturer_->Start(owner_.show_cursor_);
// Clear before sampling, so a change arriving after the sample remains pending.
cursor_capture_dirty_.store(false, std::memory_order_relaxed);
const bool show_cursor = ShouldCaptureCursor();
LOG_INFO("Start screen capturer, show cursor: {}", show_cursor);
const int ret = screen_capturer_->Start(show_cursor);
if (ret != 0) {
LOG_ERROR("Start screen capturer failed: {}", ret);
}
@@ -427,6 +515,15 @@ void SessionDeviceManager::PushAudio(const char *data, size_t size) {
}
void SessionDeviceManager::UpdateInteractions() {
#ifdef _WIN32
if (owner_.screen_capturer_is_started_ &&
cursor_capture_dirty_.load(std::memory_order_relaxed)) {
if (auto* windows = dynamic_cast<ScreenCapturerWin*>(screen_capturer_)) {
cursor_capture_dirty_.store(false, std::memory_order_relaxed);
windows->SetCursorCapture(ShouldCaptureCursor());
}
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
const bool is_wayland_session = IsWaylandSession();
const bool stop_wayland_mouse_before_screen =
@@ -10,6 +10,7 @@
#include <SDL3/SDL.h>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
@@ -40,6 +41,11 @@ public:
void Initialize();
void UpdateInteractions();
// Call after changing the controller map. UpdateInteractions consumes this
// notification on the UI thread before reading the map under its mutex.
void OnControllerConnectionsChanged() {
cursor_capture_dirty_.store(true, std::memory_order_relaxed);
}
void DestroyDevices();
void DestroyFactories();
@@ -84,11 +90,15 @@ private:
void ClearCapturedKeyboardInput();
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
int fps);
bool ShouldCaptureCursor() const;
void RecordCaptureCadence(std::chrono::steady_clock::time_point now, int fps,
bool from_secure_desktop);
GuiRuntime &owner_;
SDL_AudioStream *output_stream_ = nullptr;
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
ScreenCapturer *screen_capturer_ = nullptr;
std::atomic<bool> cursor_capture_dirty_{true};
SpeakerCaptureController speaker_capture_;
DeviceControllerFactory *device_controller_factory_ = nullptr;
MouseController *mouse_controller_ = nullptr;
@@ -99,6 +109,14 @@ private:
std::mutex captured_keyboard_inputs_mutex_;
std::chrono::steady_clock::time_point last_frame_time_{};
std::chrono::steady_clock::time_point next_frame_deadline_{};
std::mutex capture_metrics_mutex_;
std::chrono::steady_clock::time_point capture_metrics_started_{};
std::chrono::steady_clock::time_point last_capture_callback_{};
uint64_t capture_callbacks_ = 0, capture_rate_drops_ = 0;
uint64_t capture_secure_callbacks_ = 0;
uint64_t capture_forwarded_ = 0, capture_send_errors_ = 0;
int64_t capture_max_gap_us_ = 0;
int64_t capture_send_us_ = 0, capture_max_send_us_ = 0;
std::string last_video_frame_stream_id_;
bool invalid_video_stream_id_logged_ = false;
};
@@ -140,11 +140,11 @@ void GuiRuntime::HandleServerControllerDisconnected(
}
bool has_connected_controller = false;
bool has_web_controller = false;
std::string remaining_controller_id;
{
std::unique_lock lock(connection_status_mutex_);
connection_status_.erase(remote_id);
devices_.OnControllerConnectionsChanged();
connection_host_names_.erase(remote_id);
privacy_sessions_.Disconnected(remote_id);
for (const auto& [id, status] : connection_status_) {
@@ -152,14 +152,11 @@ void GuiRuntime::HandleServerControllerDisconnected(
continue;
}
has_connected_controller = true;
has_web_controller =
has_web_controller || id.find("web") != std::string::npos;
if (remaining_controller_id.empty()) {
remaining_controller_id = id;
}
}
}
show_cursor_ = has_web_controller;
if (has_connected_controller) {
remote_client_id_ = remaining_controller_id;
return;
@@ -438,6 +435,7 @@ void GuiRuntime::ResetRemoteSessionResources(
std::lock_guard<std::mutex> lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
props->cursor_presentation_ = {};
}
}
@@ -520,7 +518,7 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
}
AddAudioStream(props->peer_, props->audio_label_.c_str());
AddDataStream(props->peer_, props->data_label_.c_str(), false);
AddDataStream(props->peer_, props->mouse_label_.c_str(), false);
AddDataStream(props->peer_, props->mouse_label_.c_str(), true);
AddDataStream(props->peer_, props->keyboard_label_.c_str(), true);
AddDataStream(props->peer_, props->control_data_label_.c_str(), true);
AddDataStream(props->peer_, props->file_label_.c_str(), true);
@@ -0,0 +1,77 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-16
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CURSOR_PRESENTATION_H_
#define _CURSOR_PRESENTATION_H_
#include <remote_action.h>
#include <cstdint>
#include <optional>
namespace crossdesk {
// All times use one monotonic millisecond clock. The owning session's cursor
// mutex protects this policy from the transport and UI threads.
class CursorPresentation {
public:
static constexpr uint64_t kStateTimeoutMs = 1500; // Three 500ms heartbeats.
static constexpr uint64_t kInputActivityMs = 1500;
void Observe(const CursorState& state, uint64_t now) {
if (state.render_mode == CursorRenderMode::unknown) {
// Repeated unavailable samples must not extend the recovery deadline.
if (!sample_unavailable_) stale_at_ = now + kStateTimeoutMs;
sample_unavailable_ = true;
return;
}
sample_unavailable_ = false;
stale_at_ = now + kStateTimeoutMs;
const bool separate =
state.render_mode == CursorRenderMode::separate ||
(state.render_mode == CursorRenderMode::legacy && state.visible);
// Old senders cannot distinguish embedded from hidden. Only explicit
// metadata from a new sender can establish the no-device compatibility
// case.
no_device_ = state.render_mode == CursorRenderMode::hidden &&
state.hidden_reason == CursorHiddenReason::no_pointing_device;
held_shape_ = separate ? state.shape : RemoteCursorShape::none;
if (held_shape_ != RemoteCursorShape::none) {
last_visible_shape_ = held_shape_;
}
if (!no_device_) fallback_engaged_ = false;
}
void NoteInput(uint64_t now) { last_input_ = now; }
RemoteCursorShape Resolve(uint64_t now) {
if (!stale_at_) return RemoteCursorShape::default_cursor;
const bool recent_input =
last_input_ && now - *last_input_ < kInputActivityMs;
const bool can_recover =
now >= *stale_at_ || (!sample_unavailable_ && no_device_);
if (can_recover && recent_input) {
fallback_engaged_ = true;
}
// Once needed, retain the locator while idle until a good non-compatibility
// sample arrives. Otherwise it would blink off whenever input pauses.
return fallback_engaged_ ? last_visible_shape_ : held_shape_;
}
private:
bool sample_unavailable_ = false;
bool no_device_ = false;
bool fallback_engaged_ = false;
std::optional<uint64_t> stale_at_;
std::optional<uint64_t> last_input_;
RemoteCursorShape held_shape_ = RemoteCursorShape::default_cursor;
RemoteCursorShape last_visible_shape_ = RemoteCursorShape::default_cursor;
};
} // namespace crossdesk
#endif
@@ -6,39 +6,17 @@
#include <windows.h>
#include "platform/windows/input/windows_cursor_state.h"
#include "platform/windows/screen_capturer/captured_cursor_state.h"
#include "platform/windows/screen_capturer/dxgi_cursor_state.h"
#include "platform/windows/screen_capturer/secure_desktop_cursor_state.h"
#include "runtime/cursor_position.h"
namespace crossdesk {
namespace {
bool IsSystemCursor(HCURSOR cursor, LPCWSTR resource) {
return cursor != nullptr && cursor == LoadCursorW(nullptr, resource);
}
RemoteCursorShape ShapeFromWindowsCursor(HCURSOR cursor) {
if (IsSystemCursor(cursor, IDC_HELP)) return RemoteCursorShape::help;
if (IsSystemCursor(cursor, IDC_HAND)) return RemoteCursorShape::pointer;
if (IsSystemCursor(cursor, IDC_APPSTARTING))
return RemoteCursorShape::progress;
if (IsSystemCursor(cursor, IDC_WAIT)) return RemoteCursorShape::wait;
if (IsSystemCursor(cursor, IDC_CROSS)) return RemoteCursorShape::crosshair;
if (IsSystemCursor(cursor, IDC_IBEAM)) return RemoteCursorShape::text;
if (IsSystemCursor(cursor, IDC_NO))
return RemoteCursorShape::not_allowed;
if (IsSystemCursor(cursor, IDC_SIZEALL)) return RemoteCursorShape::move;
if (IsSystemCursor(cursor, IDC_SIZEWE)) return RemoteCursorShape::ew_resize;
if (IsSystemCursor(cursor, IDC_SIZENS)) return RemoteCursorShape::ns_resize;
if (IsSystemCursor(cursor, IDC_SIZENESW))
return RemoteCursorShape::nesw_resize;
if (IsSystemCursor(cursor, IDC_SIZENWSE))
return RemoteCursorShape::nwse_resize;
if (IsSystemCursor(cursor, IDC_UPARROW)) return RemoteCursorShape::n_resize;
return RemoteCursorShape::default_cursor;
}
} // namespace
struct CursorStateProvider::Impl {};
struct CursorStateProvider::Impl {
WindowsCursorState cursor;
};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
@@ -47,16 +25,39 @@ bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state) {
if (!state) return false;
SecureDesktopCursorSnapshot secure_cursor{};
if (SharedSecureDesktopCursorState().Get(&secure_cursor)) {
if (!secure_cursor.valid) {
state->hidden_reason = CursorHiddenReason::secure_desktop_pending;
return false;
}
state->seq = 0;
NormalizeCursorPosition(secure_cursor.x, secure_cursor.y, displays,
preferred_display, state);
state->visible = secure_cursor.visible != 0;
state->render_mode = static_cast<CursorRenderMode>(secure_cursor.render_mode);
state->hidden_reason =
static_cast<CursorHiddenReason>(secure_cursor.hidden_reason);
state->shape = state->visible
? static_cast<RemoteCursorShape>(secure_cursor.shape)
: RemoteCursorShape::none;
return true;
}
CURSORINFO info{};
info.cbSize = sizeof(info);
if (!GetCursorInfo(&info)) return false;
state->seq = 0;
state->visible = (info.flags & CURSOR_SHOWING) != 0;
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
: RemoteCursorShape::none;
NormalizeCursorPosition(info.ptScreenPos.x, info.ptScreenPos.y, displays,
preferred_display, state);
void* cursor_monitor = state->position_valid
? displays[state->display_id].handle
: nullptr;
const bool embedded = SharedCapturedCursorState().IsEmbedded(cursor_monitor) ||
((info.flags & CURSOR_SHOWING) != 0 &&
!SharedDxgiCursorState().ShouldDrawCursor(true, cursor_monitor));
impl_->cursor.Sample(info, embedded, state);
return true;
}
+1 -1
View File
@@ -199,7 +199,7 @@ int GuiRuntime::CreateConnectionPeer() {
AddAudioStream(peer_, audio_label_.c_str());
AddDataStream(peer_, data_label_.c_str(), false);
AddDataStream(peer_, mouse_label_.c_str(), false);
AddDataStream(peer_, mouse_label_.c_str(), true);
AddDataStream(peer_, keyboard_label_.c_str(), true);
AddDataStream(peer_, control_data_label_.c_str(), true);
AddDataStream(peer_, file_label_.c_str(), true);
@@ -234,7 +234,11 @@ void PeerEventHandler::OnReceiveDataBuffer(
const bool changed =
!props->remote_cursor_state_received_ ||
props->remote_cursor_state_.visible != remote_action.cs.visible ||
props->remote_cursor_state_.shape != remote_action.cs.shape;
props->remote_cursor_state_.shape != remote_action.cs.shape ||
props->remote_cursor_state_.render_mode !=
remote_action.cs.render_mode ||
props->remote_cursor_state_.hidden_reason !=
remote_action.cs.hidden_reason;
CursorState merged = remote_action.cs;
if (!remote_action.cs.position_update &&
props->remote_cursor_state_received_) {
@@ -245,10 +249,14 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
props->remote_cursor_state_ = merged;
props->remote_cursor_state_received_ = true;
props->cursor_presentation_.Observe(merged, SDL_GetTicks());
if (changed) {
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}",
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}, "
"mode={}, reason={}",
remote_action.cs.seq, remote_action.cs.visible,
static_cast<int>(remote_action.cs.shape));
static_cast<int>(remote_action.cs.shape),
static_cast<int>(remote_action.cs.render_mode),
static_cast<int>(remote_action.cs.hidden_reason));
}
}
return;
@@ -262,6 +262,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
props->cursor_presentation_ = {};
}
{
RemoteAction remote_action{};
@@ -331,6 +332,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
props->cursor_presentation_ = {};
}
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
@@ -417,6 +419,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
}
}
runtime->connection_status_[remote_id] = status;
runtime->devices_.OnControllerConnectionsChanged();
}
switch (status) {
@@ -483,16 +486,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->devices_.StartSpeakerCapturer();
runtime->remote_client_id_ = remote_id;
runtime->start_mouse_controller_ = true;
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(),
[](const auto& kv) {
return kv.first.find("web") != std::string::npos;
})) {
runtime->show_cursor_ = true;
}
}
break;
}
@@ -22,6 +22,7 @@
#include <remote_action.h>
#include <stream_names.h>
#include "display_info.h"
#include "cursor_presentation.h"
#include "minirtc.h"
namespace crossdesk { class PeerEventHandler; }
@@ -173,6 +174,7 @@ struct RemoteSession {
std::mutex remote_cursor_state_mutex_;
CursorState remote_cursor_state_{};
bool remote_cursor_state_received_ = false;
CursorPresentation cursor_presentation_;
// Shared by minirtc callbacks, Slint rendering and SDL audio callbacks.
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
+143 -66
View File
@@ -45,6 +45,9 @@ export global StreamStrings {
in-out property <string> stats-loss-rate: "Loss Rate";
in-out property <string> stats-resolution: "Res";
in-out property <string> stats-connection-mode: "Mode";
in-out property <string> stats-encryption: "Encryption";
in-out property <string> stats-encryption-enabled: "Enabled";
in-out property <string> stats-encryption-disabled: "Disabled";
}
struct ControlBarTooltip {
@@ -166,6 +169,7 @@ export component StreamWindow inherits Window {
in property <string> stats-fps: "0";
in property <string> stats-resolution: "";
in property <string> stats-connection-mode: "";
in property <bool> stats-encryption-enabled: false;
in property <bool> file-transfer-visible: false;
in property <[FileTransferEntry]> file-transfers;
@@ -194,6 +198,12 @@ export component StreamWindow inherits Window {
callback keyboard-focus-changed(bool);
private property <bool> control-expanded: true;
private property <length> control-toolbar-height: 38px;
private property <length> control-button-step: 32px;
private property <length> stats-row-height: 18px;
private property <length> stats-header-height: 18px;
private property <length> stats-content-height:
root.stats-header-height + root.stats-rows.length * root.stats-row-height + 44px;
private property <bool> display-menu-open: false;
private property <bool> display-menu-animation-enabled: false;
private property <float> display-menu-progress: 0.0;
@@ -283,7 +293,7 @@ export component StreamWindow inherits Window {
&& pointer-x <= display-menu-x + 180px
&& pointer-y >= display-menu-y
&& pointer-y <= display-menu-y + display-menu-height;
let shortcut-menu-x = control.x + (root.control-docked-left ? 41px : 74px);
let shortcut-menu-x = control.x + shortcut-button.x;
let shortcut-menu-y = control.y + 40px;
let over-shortcut-menu = root.shortcut-menu-open
&& pointer-x >= shortcut-menu-x
@@ -682,8 +692,10 @@ export component StreamWindow inherits Window {
control := Rectangle {
x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width;
y: min(max(0px, root.control-pos-y), parent.height - self.height);
width: root.control-expanded ? min(332px, parent.width) : 20px;
height: root.stats-visible && root.control-expanded ? min(180px, parent.height) : 38px;
width: root.control-expanded ? min(76px + 8 * root.control-button-step, parent.width) : 20px;
height: root.stats-visible && root.control-expanded
? min(root.control-toolbar-height + root.stats-content-height + 8px, parent.height)
: root.control-toolbar-height;
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
@@ -719,7 +731,7 @@ export component StreamWindow inherits Window {
}
shortcut-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 41px : 74px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + root.control-button-step; y: 7px;
icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut;
clicked => {
display-menu-open-timer.running = false;
@@ -731,51 +743,51 @@ export component StreamWindow inherits Window {
}
mouse-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 73px : 106px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 2 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.computer-mouse; slashed: !root.mouse-control-enabled;
tooltip: root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse;
clicked => { root.toggle-mouse-control(); }
}
audio-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 137px : 170px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 4 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled;
tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio;
clicked => { root.toggle-audio(); }
}
file-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 169px : 202px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 5 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file;
clicked => { root.select-file(); }
}
stats-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 201px : 234px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 6 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.signal; selected: root.stats-visible;
tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats;
clicked => { root.toggle-stats(); }
}
fullscreen-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 233px : 266px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 7 * root.control-button-step; y: 7px;
icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen;
clicked => { root.toggle-fullscreen(); }
}
close-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 265px : 298px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 8 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect;
clicked => { root.disconnect(); }
}
if root.control-expanded && root.control-docked-left: Rectangle {
x: 299px; y: 12px; width: 2px; height: 15px;
x: parent.width - 33px; y: 12px; width: 2px; height: 15px;
background: ImGuiLineStyle.control-separator;
}
collapse-button := ControlBarButton {
x: root.control-expanded ? (root.control-docked-left ? 307px : 10px) : 2px;
x: root.control-expanded ? (root.control-docked-left ? parent.width - 25px : 10px) : 2px;
y: 7px;
width: 15px;
icon: root.control-expanded
@@ -787,7 +799,7 @@ export component StreamWindow inherits Window {
privacy-button := ControlBarButton {
visible: root.control-expanded;
x: root.control-docked-left ? 105px : 138px; y: 7px;
x: (root.control-docked-left ? 9px : 42px) + 3 * root.control-button-step; y: 7px;
icon: FontAwesomeIcons.user-secret;
slashed: root.privacy-off && !root.privacy-unsupported;
muted: root.privacy-unsupported;
@@ -799,75 +811,110 @@ export component StreamWindow inherits Window {
}
if root.stats-visible && root.control-expanded: stats-table := Rectangle {
x: 14px;
y: 38px;
width: parent.width - 28px;
height: 128px;
x: 8px;
y: root.control-toolbar-height + 2px;
width: parent.width - 16px;
height: root.stats-content-height;
background: transparent;
private property <length> row-height: 16px;
private property <length> label-column-width: 60px;
private property <length> loss-column-width: 66px;
private property <length> row-height: root.stats-row-height;
private property <length> label-column-width: 50px;
private property <length> loss-column-width:
max((self.width - self.label-column-width) / 3,
stats-loss-label.preferred-width + 8px);
private property <length> value-column-width:
(self.width - self.label-column-width - self.loss-column-width) / 2;
private property <length> details-right-width:
right-label-width-probe.preferred-width + 6px
+ max(resolution-width-probe.preferred-width,
encryption-width-probe.preferred-width
+ (root.stats-encryption-enabled ? 14px : 0px));
private property <length> details-right-x: self.width / 2;
Rectangle {
y: 0px;
height: 1px;
background: ImGuiLineStyle.border;
left-label-width-probe := Text {
visible: false;
text: "FPS\n" + StreamStrings.stats-connection-mode;
font-size: ImGuiFontStyle.small;
}
right-label-width-probe := Text {
visible: false;
text: StreamStrings.stats-resolution + "\n" + StreamStrings.stats-encryption;
font-size: ImGuiFontStyle.small;
}
resolution-width-probe := Text {
visible: false;
text: root.stats-resolution;
font-size: ImGuiFontStyle.body;
font-weight: 600;
}
encryption-width-probe := Text {
visible: false;
text: root.stats-encryption-enabled
? StreamStrings.stats-encryption-enabled : StreamStrings.stats-encryption-disabled;
font-size: ImGuiFontStyle.body;
font-weight: 600;
}
Rectangle {
x: 0px;
y: 0px;
height: stats-table.row-height;
width: parent.width;
height: root.stats-header-height;
background: #f3f5f8;
border-radius: 4px;
Text {
x: stats-table.label-column-width + 4px;
width: stats-table.value-column-width - 8px;
text: StreamStrings.stats-in;
color: #30343b;
color: #667085;
font-size: ImGuiFontStyle.small;
horizontal-alignment: right;
vertical-alignment: center;
}
Text {
x: stats-table.label-column-width + stats-table.value-column-width + 4px;
width: stats-table.value-column-width - 8px;
text: StreamStrings.stats-out;
color: #30343b;
color: #667085;
font-size: ImGuiFontStyle.small;
horizontal-alignment: right;
vertical-alignment: center;
}
Text {
stats-loss-label := Text {
x: stats-table.label-column-width + stats-table.value-column-width * 2 + 4px;
width: stats-table.loss-column-width - 8px;
text: StreamStrings.stats-loss-rate;
color: #30343b;
color: #667085;
font-size: ImGuiFontStyle.small;
horizontal-alignment: right;
vertical-alignment: center;
overflow: elide;
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: ImGuiLineStyle.border;
}
}
for row[index] in root.stats-rows: Rectangle {
y: (index + 1) * stats-table.row-height;
x: 0px;
y: root.stats-header-height + index * stats-table.row-height;
width: parent.width;
height: stats-table.row-height;
background: index == root.stats-rows.length - 1 ? #edf3fc : transparent;
border-radius: 4px;
Text {
x: 4px;
width: stats-table.label-column-width - 8px;
text: row.label;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: #475467;
font-size: ImGuiFontStyle.body;
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
vertical-alignment: center;
}
Text {
x: stats-table.label-column-width + 4px;
width: stats-table.value-column-width - 8px;
text: row.inbound;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: #253247;
font-size: ImGuiFontStyle.body;
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
horizontal-alignment: right;
vertical-alignment: center;
overflow: elide;
}
@@ -875,8 +922,10 @@ export component StreamWindow inherits Window {
x: stats-table.label-column-width + stats-table.value-column-width + 4px;
width: stats-table.value-column-width - 8px;
text: row.outbound;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: #253247;
font-size: ImGuiFontStyle.body;
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
horizontal-alignment: right;
vertical-alignment: center;
overflow: elide;
}
@@ -884,46 +933,73 @@ export component StreamWindow inherits Window {
x: stats-table.label-column-width + stats-table.value-column-width * 2 + 4px;
width: stats-table.loss-column-width - 8px;
text: row.loss-rate;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: #475467;
font-size: ImGuiFontStyle.body;
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
horizontal-alignment: right;
vertical-alignment: center;
overflow: elide;
}
Rectangle {
if index < root.stats-rows.length - 2: Rectangle {
x: 4px;
y: parent.height - 1px;
width: parent.width - 8px;
height: 1px;
background: ImGuiLineStyle.border;
background: #edf0f4;
}
}
for metric[index] in [
{ label: "FPS:", value: root.stats-fps },
{ label: StreamStrings.stats-resolution + ":", value: root.stats-resolution },
{ label: StreamStrings.stats-connection-mode + ":", value: root.stats-connection-mode },
{ label: "FPS", value: root.stats-fps },
{ label: StreamStrings.stats-resolution, value: root.stats-resolution },
{ label: StreamStrings.stats-connection-mode, value: root.stats-connection-mode },
{ label: StreamStrings.stats-encryption, value: root.stats-encryption-enabled
? StreamStrings.stats-encryption-enabled : StreamStrings.stats-encryption-disabled },
]: Rectangle {
y: (index + 5) * stats-table.row-height;
height: stats-table.row-height;
Text {
x: 4px;
width: stats-table.label-column-width - 8px;
private property <length> encryption-icon-width:
index == 3 && root.stats-encryption-enabled ? 14px : 0px;
x: mod(index, 2) == 1 ? stats-table.details-right-x : 6px;
y: root.stats-header-height + root.stats-rows.length * stats-table.row-height
+ 2px + floor(index / 2) * 21px;
width: mod(index, 2) == 1 ? stats-table.details-right-width
: stats-table.details-right-x - 14px;
height: 21px;
metric-label := Text {
x: 0px;
y: 0px;
width: mod(index, 2) == 1 ? right-label-width-probe.preferred-width
: left-label-width-probe.preferred-width;
height: parent.height;
text: metric.label;
color: #30343b;
color: #667085;
font-size: ImGuiFontStyle.small;
vertical-alignment: center;
}
Text {
x: stats-table.label-column-width + 4px;
width: stats-table.value-column-width - 8px;
metric-value := Text {
x: metric-label.width + 6px;
y: 0px;
height: parent.height;
width: parent.width - self.x - parent.encryption-icon-width;
text: metric.value;
color: #30343b;
font-size: ImGuiFontStyle.small;
color: index == 3 && root.stats-encryption-enabled ? #18794e : #253247;
font-size: ImGuiFontStyle.body;
font-weight: 600;
horizontal-alignment: left;
vertical-alignment: center;
overflow: elide;
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: ImGuiLineStyle.border;
if index == 3 && root.stats-encryption-enabled: Text {
x: metric-value.x + min(metric-value.preferred-width, metric-value.width) + 4px;
y: metric-value.y + (metric-value.height - self.height) / 2;
width: 10px;
height: 12px;
text: FontAwesomeIcons.shield-halved;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 10px;
color: #238653;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
@@ -1007,7 +1083,7 @@ export component StreamWindow inherits Window {
}
if root.shortcut-menu-open: Rectangle {
x: control.x + (root.control-docked-left ? 41px : 74px); y: control.y + 40px; width: 180px; height: 68px;
x: control.x + shortcut-button.x; y: control.y + 40px; width: 180px; height: 68px;
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11;
VerticalLayout {
padding: 4px; spacing: 1px;
@@ -1039,8 +1115,9 @@ export component StreamWindow inherits Window {
x: min(max(4px, control.x + root.hovered-control.anchor-x - self.width / 2),
video.width - self.width - 4px);
height: control-tip.preferred-height + 12px;
y: control.y + control.height + self.height + 8px <= video.height
? control.y + control.height + 4px : max(4px, control.y - self.height - 4px);
// Anchor to the button row so expanded stats stay beneath the tooltip.
y: control.y + root.control-toolbar-height + self.height + 8px <= video.height
? control.y + root.control-toolbar-height + 4px : max(4px, control.y - self.height - 4px);
background: #30343bf5; border-radius: 5px; z: 12;
control-tip := Text {
x: 8px; y: 6px; width: parent.width - 16px;
@@ -166,6 +166,12 @@ inline int ApplyStreamWindowStrings(
strings.set_stats_resolution(Text(localization::resolution[language]));
strings.set_stats_connection_mode(
Text(localization::connection_mode[language]));
strings.set_stats_encryption(
Text(localization::transport_encryption[language]));
strings.set_stats_encryption_enabled(
Text(localization::transport_encryption_enabled[language]));
strings.set_stats_encryption_disabled(
Text(localization::transport_encryption_disabled[language]));
return language;
}
+26 -13
View File
@@ -1,6 +1,5 @@
#include "rd_log.h"
#include <atomic>
#include <filesystem>
namespace crossdesk {
@@ -9,26 +8,38 @@ namespace {
std::string g_log_dir = "logs";
std::once_flag g_logger_once_flag;
std::mutex g_logger_mutex;
std::shared_ptr<spdlog::logger> g_logger;
std::atomic<bool> g_logger_created{false};
std::string NormalizeLogDirectory(const std::string& log_dir) {
const std::filesystem::path path = log_dir.empty() ? "logs" : log_dir;
std::error_code ec;
const auto absolute = std::filesystem::absolute(path, ec);
return (ec ? path : absolute).lexically_normal().string();
}
} // namespace
void InitLogger(const std::string& log_dir) {
if (g_logger_created.load()) {
LOG_WARN(
"InitLogger called after logger initialized. Ignoring log_dir: {}, "
"using previous log_dir: {}",
log_dir, g_log_dir);
const auto directory = NormalizeLogDirectory(log_dir);
std::lock_guard<std::mutex> lock(g_logger_mutex);
if (g_logger) {
if (directory != g_log_dir) {
g_logger->warn(
"InitLogger called after logger initialized. Ignoring log_dir: {}, "
"using previous log_dir: {}",
directory, g_log_dir);
}
return;
}
g_log_dir = log_dir;
g_log_dir = directory;
}
std::shared_ptr<spdlog::logger> get_logger() {
std::call_once(g_logger_once_flag, []() {
g_logger_created.store(true);
std::lock_guard<std::mutex> lock(g_logger_mutex);
g_log_dir = NormalizeLogDirectory(g_log_dir);
std::error_code ec;
std::filesystem::create_directories(g_log_dir, ec);
@@ -54,10 +65,12 @@ std::shared_ptr<spdlog::logger> get_logger() {
sinks.push_back(std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
filename, 5 * 1024 * 1024, 3));
g_logger = std::make_shared<spdlog::logger>(LOGGER_NAME, sinks.begin(),
sinks.end());
g_logger->flush_on(spdlog::level::info);
spdlog::register_logger(g_logger);
auto logger = std::make_shared<spdlog::logger>(LOGGER_NAME, sinks.begin(),
sinks.end());
logger->flush_on(spdlog::level::info);
spdlog::register_logger(logger);
logger->info("Logger initialized: path={}", filename);
g_logger = std::move(logger);
});
return g_logger;
@@ -0,0 +1,100 @@
#include "window_icons.h"
#include <commctrl.h>
#include <memory>
#include <utility>
namespace crossdesk {
namespace {
constexpr UINT_PTR kIconSubclassId = 1;
struct WindowIcons {
HICON small_icon = nullptr;
HICON large_icon = nullptr;
~WindowIcons() {
if (small_icon) DestroyIcon(small_icon);
if (large_icon) DestroyIcon(large_icon);
}
bool Load(UINT dpi) {
const HMODULE module = GetModuleHandleW(nullptr);
small_icon = static_cast<HICON>(LoadImageW(
module, L"IDI_ICON1", IMAGE_ICON,
GetSystemMetricsForDpi(SM_CXSMICON, dpi),
GetSystemMetricsForDpi(SM_CYSMICON, dpi), 0));
large_icon = static_cast<HICON>(LoadImageW(
module, L"IDI_ICON1", IMAGE_ICON,
GetSystemMetricsForDpi(SM_CXICON, dpi),
GetSystemMetricsForDpi(SM_CYICON, dpi), 0));
return small_icon && large_icon;
}
void Apply(HWND window) const {
// Do not destroy the replaced handles: Slint/winit owns those images.
SendMessageW(window, WM_SETICON, ICON_SMALL,
reinterpret_cast<LPARAM>(small_icon));
SendMessageW(window, WM_SETICON, ICON_BIG,
reinterpret_cast<LPARAM>(large_icon));
}
};
LRESULT CALLBACK IconSubclassProc(HWND window, UINT message, WPARAM wparam,
LPARAM lparam, UINT_PTR subclass_id,
DWORD_PTR reference_data) {
auto* icons = reinterpret_cast<WindowIcons*>(reference_data);
if (message == WM_SETICON) {
// Slint supplies the same PNG for both sizes, including during deferred
// window initialization. Keep using the matching frames from our ICO.
if (wparam == ICON_SMALL) {
lparam = reinterpret_cast<LPARAM>(icons->small_icon);
} else if (wparam == ICON_BIG) {
lparam = reinterpret_cast<LPARAM>(icons->large_icon);
}
return DefSubclassProc(window, message, wparam, lparam);
}
if (message == WM_DPICHANGED) {
WindowIcons next;
if (next.Load(HIWORD(wparam))) {
std::swap(icons->small_icon, next.small_icon);
std::swap(icons->large_icon, next.large_icon);
icons->Apply(window);
// 'next' releases the previous pair only after both were replaced.
}
}
if (message == WM_NCDESTROY) {
const LRESULT result = DefSubclassProc(window, message, wparam, lparam);
RemoveWindowSubclass(window, IconSubclassProc, subclass_id);
delete icons;
return result;
}
return DefSubclassProc(window, message, wparam, lparam);
}
} // namespace
bool ConfigureWindowsWindowIcons(HWND window) {
if (!window) {
return false;
}
DWORD_PTR existing = 0;
if (GetWindowSubclass(window, IconSubclassProc, kIconSubclassId, &existing)) {
return true;
}
auto icons = std::make_unique<WindowIcons>();
const UINT dpi = GetDpiForWindow(window);
if (!dpi || !icons->Load(dpi) ||
!SetWindowSubclass(window, IconSubclassProc, kIconSubclassId,
reinterpret_cast<DWORD_PTR>(icons.get()))) {
return false;
}
// The subclass owns the pair until WM_NCDESTROY, even if Slint hides and
// recreates its native window while the component itself stays alive.
auto* installed = icons.release();
installed->Apply(window);
return true;
}
} // namespace crossdesk
@@ -0,0 +1,14 @@
#ifndef CROSSDESK_WINDOWS_WINDOW_ICONS_H_
#define CROSSDESK_WINDOWS_WINDOW_ICONS_H_
#include <windows.h>
namespace crossdesk {
// Call on the window's UI thread once its native handle exists. Repeated calls
// are harmless. Icons follow DPI changes and are released with the window.
bool ConfigureWindowsWindowIcons(HWND window);
} // namespace crossdesk
#endif // CROSSDESK_WINDOWS_WINDOW_ICONS_H_
@@ -0,0 +1,32 @@
#ifndef CROSSDESK_WINDOWS_CURSOR_SHAPE_H_
#define CROSSDESK_WINDOWS_CURSOR_SHAPE_H_
#include <windows.h>
#include <remote_cursor_shape.h>
namespace crossdesk {
inline RemoteCursorShape ShapeFromWindowsCursor(HCURSOR cursor) {
const auto is_system_cursor = [cursor](LPCWSTR resource) {
return cursor != nullptr && cursor == LoadCursorW(nullptr, resource);
};
if (is_system_cursor(IDC_HELP)) return RemoteCursorShape::help;
if (is_system_cursor(IDC_HAND)) return RemoteCursorShape::pointer;
if (is_system_cursor(IDC_APPSTARTING)) return RemoteCursorShape::progress;
if (is_system_cursor(IDC_WAIT)) return RemoteCursorShape::wait;
if (is_system_cursor(IDC_CROSS)) return RemoteCursorShape::crosshair;
if (is_system_cursor(IDC_IBEAM)) return RemoteCursorShape::text;
if (is_system_cursor(IDC_NO)) return RemoteCursorShape::not_allowed;
if (is_system_cursor(IDC_SIZEALL)) return RemoteCursorShape::move;
if (is_system_cursor(IDC_SIZEWE)) return RemoteCursorShape::ew_resize;
if (is_system_cursor(IDC_SIZENS)) return RemoteCursorShape::ns_resize;
if (is_system_cursor(IDC_SIZENESW)) return RemoteCursorShape::nesw_resize;
if (is_system_cursor(IDC_SIZENWSE)) return RemoteCursorShape::nwse_resize;
if (is_system_cursor(IDC_UPARROW)) return RemoteCursorShape::n_resize;
return RemoteCursorShape::default_cursor;
}
} // namespace crossdesk
#endif // CROSSDESK_WINDOWS_CURSOR_SHAPE_H_
@@ -0,0 +1,86 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-16
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOWS_CURSOR_STATE_H_
#define _WINDOWS_CURSOR_STATE_H_
#include <remote_action.h>
#include <windows.h>
#include <chrono>
#include <vector>
#include "windows_cursor_shape.h"
namespace crossdesk {
// Used by both ordinary and secure-desktop capture. Device absence is only a
// compatibility hint: a hidden cursor with a null handle, or touch/pen
// suppression, must retain the application's normal hiding behavior.
class WindowsCursorState {
public:
void Sample(const CURSORINFO& info, bool embedded, CursorState* state) {
const bool showing = (info.flags & CURSOR_SHOWING) != 0;
state->render_mode = embedded ? CursorRenderMode::embedded
: showing ? CursorRenderMode::separate
: CursorRenderMode::hidden;
state->visible = state->render_mode == CursorRenderMode::separate;
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
: RemoteCursorShape::none;
state->hidden_reason = CursorHiddenReason::unspecified;
if (!showing && !embedded) {
state->hidden_reason = (info.flags & CURSOR_SUPPRESSED)
? CursorHiddenReason::system_suppressed
: info.hCursor && NoPointingDevice()
? CursorHiddenReason::no_pointing_device
: CursorHiddenReason::system_hidden;
}
}
private:
// Query only for potentially recoverable hidden cursors, at most once per
// second. Require two successful absent observations. Query failures, RDP
// (whose devices may be absent from Raw Input), and present mice all disallow
// fallback and break the sequence of absent observations.
bool NoPointingDevice() {
const auto now = std::chrono::steady_clock::now();
if (now < next_device_check_) return absence_confirmed_;
next_device_check_ = now + std::chrono::seconds(1);
const bool absent = DetectDeviceAbsence();
absence_confirmed_ = absent && absent_before_;
absent_before_ = absent;
return absence_confirmed_;
}
static bool DetectDeviceAbsence() {
if (GetSystemMetrics(SM_MOUSEPRESENT) ||
GetSystemMetrics(SM_REMOTESESSION)) {
return false;
}
UINT count = 0;
if (GetRawInputDeviceList(nullptr, &count, sizeof(RAWINPUTDEVICELIST)) ==
static_cast<UINT>(-1) ||
count > 4096)
return false;
if (count == 0) return true;
std::vector<RAWINPUTDEVICELIST> devices(count);
const UINT read = GetRawInputDeviceList(devices.data(), &count,
sizeof(RAWINPUTDEVICELIST));
if (read == static_cast<UINT>(-1) || read > devices.size()) return false;
for (UINT i = 0; i < read; ++i) {
if (devices[i].dwType == RIM_TYPEMOUSE) return false;
}
return true;
}
bool absent_before_ = false;
bool absence_confirmed_ = false;
std::chrono::steady_clock::time_point next_device_check_{};
};
} // namespace crossdesk
#endif
@@ -192,7 +192,7 @@ class Windows {
GetPrivacyWindowBand get_band_ = nullptr;
};
DWORD WINAPI Run(void*) {
DWORD Run() {
wchar_t bootstrap[256]{};
if (!GetEnvironmentVariableW(kPrivacyBandBootstrap, bootstrap, 256))
return ERROR_INVALID_PARAMETER;
@@ -239,8 +239,8 @@ DWORD WINAPI Run(void*) {
auto last_check = GetTickCount64();
HANDLE watched[] = {parent, stop};
// Once enabled, retain surviving covers on a validation/layout fault.
// The parent pauses the session after reading the latched error. Only
// an explicit stop or parent death tears the remaining windows down.
// The parent releases privacy after reading the latched error. Only an
// explicit stop or parent death tears the remaining windows down.
while (initialized) {
const DWORD wait = MsgWaitForMultipleObjectsEx(
2, watched, 100, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
@@ -273,21 +273,26 @@ DWORD WINAPI Run(void*) {
}
UnmapViewOfFile(shared);
for (HANDLE handle : handles) CloseHandle(handle);
// This module runs only in the dedicated process created for this session.
// Exit outside DllMain/loader lock, after HWND destruction on their UI
// thread.
ExitProcess(exit_code);
return exit_code;
}
void CALLBACK RunBeforeBrokerEntryPoint(ULONG_PTR) {
// The LoadLibraryW startup APC has returned, so the loader lock is no longer
// held. Own the primary thread until privacy stops, including startup errors.
// Never return into RuntimeBroker's COM server startup: an elevated launch
// can fail there with CO_E_WRONG_SERVER_IDENTITY and kill healthy covers.
ExitProcess(Run());
}
} // namespace
BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID) {
if (reason == DLL_PROCESS_ATTACH) {
module = instance;
// Keep thread notifications enabled: this module uses the static CRT.
HANDLE thread = CreateThread(nullptr, 0, Run, nullptr, 0, nullptr);
if (!thread) return FALSE;
CloseHandle(thread);
// Loaded by the startup APC on our dedicated, suspended broker's primary
// thread. Queue the window loop after that APC instead of starting a worker
// alongside RuntimeBroker's own entry point. Do not run it under loader lock.
if (!QueueUserAPC(RunBeforeBrokerEntryPoint, GetCurrentThread(), 0))
return FALSE;
}
return TRUE;
}
@@ -9,6 +9,7 @@
#include <filesystem>
#include <vector>
#include "../screen_capturer/dxgi_cursor_state.h"
#include "privacy_cursor_api.h"
#include "rd_log.h"
@@ -105,6 +106,9 @@ bool PrivacyCursorGuard::Start(std::string& error) {
process_ = child.hProcess;
stop_event_ = stop.value;
stop.value = nullptr;
// The helper may hide the pointer before signaling readiness. Track its
// entire lifetime, including startup failure and delayed restoration.
SharedDxgiCursorState().SetPrivacyCursorHidden(true);
HANDLE startup_events[] = {ready.value, process_};
const DWORD result = WaitForMultipleObjects(2, startup_events, FALSE, 2000);
const DWORD ready_error =
@@ -127,6 +131,7 @@ bool PrivacyCursorGuard::RestoreAfterFailure(std::string& error) {
return Failed("Restore system cursor after helper failure", error);
}
restore_pending_ = false;
SharedDxgiCursorState().SetPrivacyCursorHidden(false);
return true;
}
@@ -152,6 +157,7 @@ bool PrivacyCursorGuard::Stop(std::string& error) {
CloseHandle(stop_event_);
stop_event_ = nullptr;
if (code != ERROR_SUCCESS) return RestoreAfterFailure(error);
SharedDxgiCursorState().SetPrivacyCursorHidden(false);
return true;
}
@@ -0,0 +1,55 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-16
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CAPTURED_CURSOR_STATE_H_
#define _CAPTURED_CURSOR_STATE_H_
#include <atomic>
namespace crossdesk {
// Metadata belongs to the frame callback, not to the requested capture mode.
// WGC's DLL adapter copies its frame metadata into this executable-local scope.
inline thread_local bool current_frame_has_cursor = false;
class CapturedCursorFrameScope {
public:
explicit CapturedCursorFrameScope(bool embedded)
: previous_(current_frame_has_cursor) {
current_frame_has_cursor = embedded;
}
~CapturedCursorFrameScope() { current_frame_has_cursor = previous_; }
CapturedCursorFrameScope(const CapturedCursorFrameScope&) = delete;
CapturedCursorFrameScope& operator=(const CapturedCursorFrameScope&) = delete;
private:
bool previous_;
};
class CapturedCursorState {
public:
void Update(bool embedded, void* monitor) {
embedded_monitor_.store(embedded ? monitor : nullptr,
std::memory_order_relaxed);
}
void Reset() { Update(false, nullptr); }
bool IsEmbedded(void* monitor) const {
return monitor && embedded_monitor_.load(std::memory_order_relaxed) == monitor;
}
private:
// This opaque handle is the entire state; no other data needs publishing.
std::atomic<void*> embedded_monitor_{nullptr};
};
inline CapturedCursorState& SharedCapturedCursorState() {
static CapturedCursorState state;
return state;
}
} // namespace crossdesk
#endif
@@ -0,0 +1,49 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-16
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CURSOR_DRAW_H_
#define _CURSOR_DRAW_H_
#include <windows.h>
#include <cstdint>
namespace crossdesk {
// ptScreenPos is the input hotspot; DrawIconEx expects the bitmap's top-left.
// Return true only if a cursor intersects the captured rectangle and was drawn.
inline bool DrawCursorInCapture(HDC dc, const CURSORINFO& cursor, int left,
int top, int width, int height) {
if (!(cursor.flags & CURSOR_SHOWING) || !cursor.hCursor) return false;
struct CursorIconInfo : ICONINFO {
CursorIconInfo() : ICONINFO{} {}
~CursorIconInfo() {
if (hbmColor) DeleteObject(hbmColor);
if (hbmMask) DeleteObject(hbmMask);
}
} icon;
if (!GetIconInfo(cursor.hCursor, &icon)) return false;
BITMAP bitmap{};
if (!GetObject(icon.hbmColor ? icon.hbmColor : icon.hbmMask, sizeof(bitmap),
&bitmap))
return false;
const int cursor_width = bitmap.bmWidth;
const int cursor_height =
icon.hbmColor ? bitmap.bmHeight : bitmap.bmHeight / 2;
const int64_t x =
static_cast<int64_t>(cursor.ptScreenPos.x) - left - icon.xHotspot;
const int64_t y =
static_cast<int64_t>(cursor.ptScreenPos.y) - top - icon.yHotspot;
if (x >= width || y >= height || x + cursor_width <= 0 ||
y + cursor_height <= 0)
return false;
return DrawIconEx(dc, static_cast<int>(x), static_cast<int>(y),
cursor.hCursor, 0, 0, 0, nullptr, DI_NORMAL) != FALSE;
}
} // namespace crossdesk
#endif
@@ -0,0 +1,66 @@
#ifndef CROSSDESK_DXGI_CURSOR_STATE_H_
#define CROSSDESK_DXGI_CURSOR_STATE_H_
#include <cstdint>
#include <mutex>
namespace crossdesk {
// DXGI's Visible flag describes the separate hardware pointer, not the
// system cursor. When it is false, a visible system cursor can already be
// included in the desktop texture (for example, during window dragging).
// Share that distinction with the GUI thread without changing cursor input
// coordinates or the wire protocol.
class DxgiCursorState {
public:
void Update(int64_t last_mouse_update_time, bool separate_pointer_visible,
void* monitor) {
// PointerPosition is undefined on desktop-only updates. Retain the last
// pointer state until DXGI actually reports another pointer update.
if (last_mouse_update_time == 0) return;
std::lock_guard<std::mutex> lock(mutex_);
if (privacy_cursor_hidden_) return;
embedded_monitor_ = separate_pointer_visible ? nullptr : monitor;
}
void Reset() {
std::lock_guard<std::mutex> lock(mutex_);
embedded_monitor_ = nullptr;
}
void SetPrivacyCursorHidden(bool hidden) {
std::lock_guard<std::mutex> lock(mutex_);
if (privacy_cursor_hidden_ == hidden) return;
privacy_cursor_hidden_ = hidden;
// Pointer updates from before/during suppression cannot describe whether
// the restored cursor is embedded. Wait for a fresh DXGI pointer update.
embedded_monitor_ = nullptr;
}
bool ShouldDrawCursor(bool system_cursor_visible, void* cursor_monitor) const {
std::lock_guard<std::mutex> lock(mutex_);
// MagShowSystemCursor(FALSE) hides the local pointer plane without changing
// CURSOR_SHOWING. DXGI Visible=false then means hidden, not embedded in the
// captured frame. Keep sending the application's cursor to the controller.
// CURSOR_SHOWING still takes precedence when the application hides it.
if (privacy_cursor_hidden_) return system_cursor_visible;
// A hidden cursor or a pointer on another monitor also makes DXGI report
// Visible=false. Only suppress a visible cursor on the captured monitor.
return system_cursor_visible &&
(!cursor_monitor || embedded_monitor_ != cursor_monitor);
}
private:
mutable std::mutex mutex_;
void* embedded_monitor_ = nullptr;
bool privacy_cursor_hidden_ = false;
};
inline DxgiCursorState& SharedDxgiCursorState() {
static DxgiCursorState state;
return state;
}
} // namespace crossdesk
#endif // CROSSDESK_DXGI_CURSOR_STATE_H_
@@ -7,6 +7,7 @@
#include <string>
#include <vector>
#include "dxgi_cursor_state.h"
#include "libyuv.h"
#include "rd_log.h"
@@ -232,6 +233,7 @@ void ScreenCapturerDxgi::EnumerateDisplays() {
}
bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
SharedDxgiCursorState().Reset();
if (monitor_index < 0 || monitor_index >= (int)outputs_.size()) return false;
Microsoft::WRL::ComPtr<IDXGIOutput1> output1;
HRESULT hr = outputs_[monitor_index]->QueryInterface(
@@ -284,6 +286,7 @@ bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
}
void ScreenCapturerDxgi::ReleaseDuplication() {
SharedDxgiCursorState().Reset();
++duplication_generation_;
staging_.Reset();
if (duplication_) {
@@ -344,6 +347,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
// a backend rebuild. Normal error/reset handling releases optional privacy.
cached_frame_valid = false;
if (FAILED(hr)) {
SharedDxgiCursorState().Reset();
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
capture_lock.unlock();
if (callback_)
@@ -352,6 +356,14 @@ void ScreenCapturerDxgi::CaptureLoop() {
continue;
}
if (frame_monitor >= 0 &&
frame_monitor < static_cast<int>(display_info_list_.size())) {
SharedDxgiCursorState().Update(
frame_info.LastMouseUpdateTime.QuadPart,
frame_info.PointerPosition.Visible != FALSE,
display_info_list_[frame_monitor].handle);
}
Microsoft::WRL::ComPtr<ID3D11Texture2D> acquired_tex;
if (desktop_resource) {
hr = desktop_resource->QueryInterface(
@@ -6,6 +6,8 @@
#include <vector>
#include <display_stream_id.h>
#include "captured_cursor_state.h"
#include "cursor_draw.h"
#include "libyuv.h"
#include "rd_log.h"
@@ -184,18 +186,15 @@ void ScreenCapturerGdi::CaptureLoop() {
BitBlt(mem_dc, 0, 0, width, height, screen_dc, left, top,
SRCCOPY | CAPTUREBLT);
bool cursor_embedded = false;
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);
}
if (GetCursorInfo(&ci)) {
cursor_embedded = DrawCursorInCapture(mem_dc, ci, left, top, width, height);
}
}
GdiFlush();
int stride_argb = width * 4;
int nv12_size = width * height * 3 / 2;
@@ -211,6 +210,7 @@ void ScreenCapturerGdi::CaptureLoop() {
width, height);
if (callback_) {
CapturedCursorFrameScope cursor_scope(cursor_embedded);
const std::string stream_id = MakeDisplayStreamId(idx);
callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str(),
nullptr);
@@ -31,6 +31,9 @@ class ScreenCapturerGdi : public ScreenCapturer {
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
void SetCursorCapture(bool enabled) {
show_cursor_.store(enabled, std::memory_order_relaxed);
}
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
@@ -65,4 +68,4 @@ class ScreenCapturerGdi : public ScreenCapturer {
};
} // namespace crossdesk
#endif
#endif
@@ -8,6 +8,7 @@
#include <iostream>
#include <display_stream_id.h>
#include "captured_cursor_state.h"
#include "libyuv.h"
#include "rd_log.h"
@@ -72,6 +73,16 @@ HMONITOR GetPrimaryMonitor() {
ScreenCapturerWgc::ScreenCapturerWgc() : monitor_(nullptr) {}
int ScreenCapturerWgc::SetCursorCapture(bool enabled) {
int result = 0;
for (auto& session : sessions_) {
if (!session.inited_ || !session.running_) continue;
const int ret = session.session_->Start(enabled);
if (ret != 0) result = ret;
}
return result;
}
ScreenCapturerWgc::~ScreenCapturerWgc() {
Stop();
CleanUp();
@@ -409,6 +420,7 @@ void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
even_width, even_width, even_height);
const std::string stream_id = MakeDisplayStreamId(id);
CapturedCursorFrameScope cursor_scope(frame.cursor_captured);
on_data_(nv12_frame_, nv12_size, even_width, even_height,
stream_id.c_str(), nullptr);
}
@@ -27,6 +27,7 @@ class ScreenCapturerWgc : public ScreenCapturer,
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int SetCursorCapture(bool enabled);
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
@@ -16,11 +16,14 @@
#include <display_stream_id.h>
#include "captured_nv12_frame.h"
#include "captured_cursor_state.h"
#include "dxgi_cursor_state.h"
#include "interactive_state.h"
#include "named_pipe_deadline.h"
#include "rd_log.h"
#include "screen_capturer_dxgi.h"
#include "screen_capturer_gdi.h"
#include "secure_desktop_cursor_state.h"
#include "secure_desktop_frame_schedule.h"
#include "secure_desktop_status_poller.h"
#include "service_host.h"
@@ -33,6 +36,8 @@ namespace {
using Json = nlohmann::json;
thread_local bool current_frame_from_secure_desktop = false;
constexpr DWORD kSecureDesktopStatusIntervalMs = 250;
constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 500;
constexpr DWORD kSecureDesktopHelperPipeTimeoutMs = 120;
@@ -48,6 +53,8 @@ class WgcPluginCapturer final : public ScreenCapturer {
public:
using CreateFn = ScreenCapturer* (*)();
using DestroyFn = void (*)(ScreenCapturer*);
using SetCursorFn = decltype(&CrossDeskSetWgcCursorCapture);
using FrameCursorFn = decltype(&CrossDeskWgcFrameCapturesCursor);
static std::unique_ptr<ScreenCapturer> Create() {
std::filesystem::path plugin_path;
@@ -95,12 +102,38 @@ class WgcPluginCapturer final : public ScreenCapturer {
}
int Init(const int fps, cb_desktop_data cb) override {
return impl_ ? impl_->Init(fps, std::move(cb)) : -1;
if (!impl_) return -1;
return impl_->Init(fps, [this, cb = std::move(cb)](
unsigned char* data, int size, int width, int height,
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame) {
bool embedded = false;
if (frame_cursor_fn_) {
embedded = frame_cursor_fn_();
} else if (legacy_capture_cursor_.load(std::memory_order_relaxed)) {
CURSORINFO info{};
info.cbSize = sizeof(info);
embedded = GetCursorInfo(&info) && (info.flags & CURSOR_SHOWING) != 0;
}
// The plugin has its own globals. Bridge only this callback's metadata
// into the executable before the wrapper accepts/publishes the frame.
CapturedCursorFrameScope cursor_scope(embedded);
cb(data, size, width, height, stream_id, native_frame);
});
}
int Destroy() override { return impl_ ? impl_->Destroy() : 0; }
int Start(bool show_cursor) override {
if (!frame_cursor_fn_) {
legacy_capture_cursor_.store(show_cursor, std::memory_order_relaxed);
}
return impl_ ? impl_->Start(show_cursor) : -1;
}
int SetCursorCapture(bool enabled) {
if (!impl_) return -1;
if (set_cursor_fn_) return set_cursor_fn_(impl_, enabled);
// Older plugins only apply the cursor setting at startup.
const int ret = impl_->Stop();
return ret == 0 ? Start(enabled) : ret;
}
int Stop() override { return impl_ ? impl_->Stop() : 0; }
int Pause(int monitor_index) override {
return impl_ ? impl_->Pause(monitor_index) : -1;
@@ -120,11 +153,21 @@ class WgcPluginCapturer final : public ScreenCapturer {
private:
WgcPluginCapturer(HMODULE module, ScreenCapturer* impl, DestroyFn destroy_fn)
: module_(module), impl_(impl), destroy_fn_(destroy_fn) {}
: module_(module), impl_(impl), destroy_fn_(destroy_fn) {
set_cursor_fn_ = reinterpret_cast<SetCursorFn>(
GetProcAddress(module, "CrossDeskSetWgcCursorCapture"));
frame_cursor_fn_ = reinterpret_cast<FrameCursorFn>(
GetProcAddress(module, "CrossDeskWgcFrameCapturesCursor"));
// Use the legacy restart path unless both parts of the extension exist.
if (!frame_cursor_fn_) set_cursor_fn_ = nullptr;
}
HMODULE module_ = nullptr;
ScreenCapturer* impl_ = nullptr;
DestroyFn destroy_fn_ = nullptr;
SetCursorFn set_cursor_fn_ = nullptr;
FrameCursorFn frame_cursor_fn_ = nullptr;
std::atomic<bool> legacy_capture_cursor_{false};
};
const char* CaptureBackendName(const ScreenCapturer* capturer) {
@@ -222,6 +265,7 @@ bool ParseSecureDesktopFrameResponse(const std::vector<uint8_t>& response,
*width_out = static_cast<int>(header.width);
*height_out = static_cast<int>(header.height);
nv12_frame_out->assign(response.begin() + sizeof(header), response.end());
SharedSecureDesktopCursorState().Update(header.cursor);
return true;
}
@@ -358,15 +402,19 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
const char* reported_stream_id,
const MiniRtcNativeVideoFrame* native_frame) {
if (size == ScreenCapturer::kBackendReset) {
SharedCapturedCursorState().Reset();
if (privacy_) privacy_->Fail("Capture restarted; privacy screen will turn off");
return;
}
if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
SharedDxgiCursorState().Reset();
SharedCapturedCursorState().Reset();
return;
}
const char* raw_stream_id = reported_stream_id ? reported_stream_id : "";
std::string mapped_stream_id;
void* frame_monitor = nullptr;
{
std::lock_guard<std::mutex> lock(alias_mutex_);
auto it = stream_id_alias_.find(raw_stream_id);
@@ -380,6 +428,14 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
mapped_stream_id = ResolveDisplayStreamId(
mapped_stream_id.c_str(), canonical_displays_.size(),
monitor_index_.load(std::memory_order_relaxed));
if (current_frame_has_cursor) {
for (size_t index = 0; index < canonical_displays_.size(); ++index) {
if (mapped_stream_id == MakeDisplayStreamId(index)) {
frame_monitor = canonical_displays_[index].handle;
break;
}
}
}
}
if (privacy_ && privacy_->Engaged() && !IsWindowsPrivacyDesktopAvailable()) {
privacy_->Fail("Secure or unavailable desktop; privacy screen will turn off");
@@ -394,6 +450,7 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
return;
}
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
SharedCapturedCursorState().Update(current_frame_has_cursor, frame_monitor);
if (post_secure_desktop_waiting_for_frame_.exchange(
false, std::memory_order_relaxed)) {
const ULONGLONG start_tick =
@@ -460,10 +517,20 @@ int ScreenCapturerWin::Destroy() {
return 0;
}
bool ScreenCapturerWin::CurrentFrameIsFromSecureDesktop() {
return current_frame_from_secure_desktop;
}
void ScreenCapturerWin::EmitCapturedFrame(
unsigned char* data, int size, int width, int height,
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame,
bool from_secure_desktop) {
// Helper frames do not use DXGI's pointer plane. Do not carry normal-desktop
// cursor suppression into the lock screen or UAC desktop.
if (from_secure_desktop) {
SharedDxgiCursorState().Reset();
SharedCapturedCursorState().Reset();
}
// Secure-desktop capture takes priority; remove privacy without dropping
// the helper frame or delaying the remote session.
if (from_secure_desktop && privacy_ && privacy_->Engaged()) {
@@ -473,6 +540,14 @@ void ScreenCapturerWin::EmitCapturedFrame(
return;
}
// Normal and helper callbacks can originate on different threads during
// desktop transitions. Keep diagnostics tied to this callback's source.
struct RestoreFrameSource {
bool previous;
~RestoreFrameSource() { current_frame_from_secure_desktop = previous; }
} restore_source{
std::exchange(current_frame_from_secure_desktop, from_secure_desktop)};
if (native_frame) {
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
LOG_INFO("Windows capturer native frame output enabled (type={})",
@@ -522,9 +597,10 @@ bool ScreenCapturerWin::TryStartBackend(
return false;
}
const char* name = CaptureBackendName(candidate.get());
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
int ret = candidate->Init(fps_, cb_);
if (ret == 0)
ret = candidate->Start(show_cursor_.load(std::memory_order_relaxed));
ret = candidate->Start(show_cursor);
if (ret != 0) {
LOG_WARN("Windows capturer: {} initialization/start failed (ret={})", name,
ret);
@@ -534,6 +610,7 @@ bool ScreenCapturerWin::TryStartBackend(
// Commit only after the replacement is running. Failed candidates release
// their resources without replacing the current backend.
impl_ = std::move(candidate);
applied_show_cursor_ = show_cursor;
RestoreMonitor(monitor_index);
LOG_INFO("Windows capturer: started {}", name);
return true;
@@ -547,6 +624,8 @@ int ScreenCapturerWin::Start(bool show_cursor) {
NotifyPrivacyCapture(false);
show_cursor_.store(show_cursor, std::memory_order_relaxed);
applied_show_cursor_ = show_cursor;
SharedCapturedCursorState().Reset();
paused_.store(false, std::memory_order_relaxed);
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
@@ -593,6 +672,7 @@ int ScreenCapturerWin::Start(bool show_cursor) {
running_.store(true, std::memory_order_relaxed);
NotifyPrivacyCapture(true);
SharedSecureDesktopCursorState().SetActive(false);
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false,
std::memory_order_relaxed);
@@ -614,10 +694,13 @@ int ScreenCapturerWin::Stop() {
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
int ret = 0;
// Drain management operations before stopping/replacing their backend.
StopSecureCaptureThread();
if (impl_) {
ret = impl_->Stop();
}
StopSecureCaptureThread();
SharedCapturedCursorState().Reset();
SharedSecureDesktopCursorState().SetActive(false);
StopSecureDesktopSharedCapture(secure_shared_session_id_);
return ret;
}
@@ -775,6 +858,7 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
impl_->Stop();
int ret = impl_->Start(show_cursor);
if (ret == 0) {
applied_show_cursor_ = show_cursor;
RestoreMonitor(current_monitor);
return true;
}
@@ -789,6 +873,7 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
ret = impl_->Start(show_cursor);
}
if (ret == 0) {
applied_show_cursor_ = show_cursor;
RestoreMonitor(current_monitor);
return true;
}
@@ -811,6 +896,24 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
return false;
}
void ScreenCapturerWin::ApplyCursorCaptureSetting() {
const bool requested = show_cursor_.load(std::memory_order_relaxed);
if (!impl_ || requested == applied_show_cursor_) return;
int ret = 0;
if (auto* gdi = dynamic_cast<ScreenCapturerGdi*>(impl_.get())) {
gdi->SetCursorCapture(requested);
} else if (auto* wgc = dynamic_cast<WgcPluginCapturer*>(impl_.get())) {
ret = wgc->SetCursorCapture(requested);
}
if (ret == 0) {
applied_show_cursor_ = requested;
LOG_INFO("Windows capturer cursor capture updated: backend={}, enabled={}",
CaptureBackendName(impl_.get()), requested);
} else {
LOG_WARN("Windows capturer cursor update failed, ret={}; will retry", ret);
}
}
bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, int* width,
int* height,
std::string* display_name) {
@@ -999,6 +1102,9 @@ bool ScreenCapturerWin::ReadSecureDesktopSharedFrame(
}
nv12_frame_out->resize(payload_size);
const SecureDesktopCursorSnapshot cursor = header->cursor;
const int captured_width = static_cast<int>(header->width);
const int captured_height = static_cast<int>(header->height);
std::memcpy(nv12_frame_out->data(), secure_frame_view_ + sizeof(*header),
payload_size);
MemoryBarrier();
@@ -1009,8 +1115,9 @@ bool ScreenCapturerWin::ReadSecureDesktopSharedFrame(
return false;
}
*width_out = static_cast<int>(header->width);
*height_out = static_cast<int>(header->height);
*width_out = captured_width;
*height_out = captured_height;
SharedSecureDesktopCursorState().Update(cursor);
return true;
}
@@ -1086,6 +1193,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
bool post_secure_restart_pending = false;
ULONGLONG post_secure_restart_deadline_tick = 0;
ULONGLONG last_post_secure_restart_tick = 0;
ULONGLONG next_cursor_update_tick = 0;
SecureDesktopServiceStatus status;
SecureDesktopStatusPoller status_poller(
QuerySecureDesktopServiceStatus,
@@ -1111,6 +1219,18 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
max_status_ms = 0;
};
std::vector<uint8_t> secure_frame;
ULONGLONG last_shared_error_tick = 0;
auto report_shared_error = [&](const char* operation,
const std::string& error) {
const ULONGLONG now = GetTickCount64();
if (last_shared_error_tick == 0 || now - last_shared_error_tick >= 5000) {
LOG_WARN(
"Windows capturer secure shared capture {} failed; using pipe "
"fallback, stage='{}', session_id={}, error={}",
operation, status.interactive_stage, status.active_session_id, error);
last_shared_error_tick = now;
}
};
while (running_.load(std::memory_order_relaxed)) {
// Privacy covers belong to the ordinary desktop. Drop privacy on a
@@ -1153,6 +1273,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
secure_desktop_capture_active_.store(status.capture_active,
std::memory_order_relaxed);
SharedSecureDesktopCursorState().SetActive(status.capture_active);
if (status.capture_active != previous.capture_active ||
status.interactive_stage != previous.interactive_stage ||
status.active_session_id != previous.active_session_id ||
@@ -1210,6 +1331,10 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
!RestartCaptureBackendAfterSecureDesktop();
}
}
if (!post_secure_restart_pending && now >= next_cursor_update_tick) {
next_cursor_update_tick = now + 500;
ApplyCursorCaptureSetting();
}
std::this_thread::sleep_for(
std::chrono::milliseconds(status.service_available ? 50 : 200));
continue;
@@ -1257,6 +1382,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
status.interactive_stage, status.interactive_desktop, show_cursor,
shared_fps, &error_message);
if (!shared_ready) {
report_shared_error("start", error_message);
frame_schedule.OnStartFailure(GetTickCount64());
} else if (!was_started) {
frame_schedule.Reset(GetTickCount64());
@@ -1276,6 +1402,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
IsPendingSecureDesktopFrame(error_message);
if (frame_pending) ++shared_waits;
if (shared_ready && !frame_delivered && !frame_pending) {
report_shared_error("read", error_message);
StopSecureDesktopSharedCapture(secure_shared_session_id_);
frame_schedule.OnStartFailure(GetTickCount64());
}
@@ -1339,6 +1466,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
report_stats(true);
StopSecureDesktopSharedCapture(secure_shared_session_id_);
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
SharedSecureDesktopCursorState().SetActive(false);
}
} // namespace crossdesk
@@ -37,6 +37,10 @@ class ScreenCapturerWin : public ScreenCapturer {
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
// Applied on the capture-management thread, including secure-desktop capture.
void SetCursorCapture(bool enabled) {
show_cursor_.store(enabled, std::memory_order_relaxed);
}
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
@@ -48,6 +52,9 @@ class ScreenCapturerWin : public ScreenCapturer {
void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; }
// Set before Init(), while capture is stopped.
void SetCaptureMethod(ScreenCaptureMethod method) { capture_method_ = method; }
// Query synchronously inside the frame callback, on the delivering thread.
// This describes the actual frame, independently of service status polling.
static bool CurrentFrameIsFromSecureDesktop();
private:
std::unique_ptr<ScreenCapturer> impl_;
@@ -72,6 +79,7 @@ class ScreenCapturerWin : public ScreenCapturer {
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<bool> show_cursor_{true};
bool applied_show_cursor_ = true;
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> secure_desktop_capture_active_{false};
@@ -106,6 +114,7 @@ class ScreenCapturerWin : public ScreenCapturer {
bool from_secure_desktop = false);
void StopSecureCaptureThread();
bool RestartCaptureBackendAfterSecureDesktop();
void ApplyCursorCaptureSetting();
void SecureDesktopCaptureLoop();
bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height,
std::string* display_name);
@@ -0,0 +1,67 @@
#ifndef CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
#define CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
#include <chrono>
#include <cstdint>
#include <mutex>
namespace crossdesk {
// Fixed-width IPC metadata. Cursor handles belong to the helper's desktop and
// must be converted to a semantic shape before crossing the process boundary.
struct SecureDesktopCursorSnapshot {
uint32_t valid = 0;
uint32_t visible = 0;
uint32_t shape = 0;
int32_t x = 0;
int32_t y = 0;
uint32_t render_mode = 0;
uint32_t hidden_reason = 0;
};
static_assert(sizeof(SecureDesktopCursorSnapshot) == 28);
// The capture thread publishes helper samples for the GUI thread. While secure
// capture is active, even an unavailable sample must not fall back to sampling
// the ordinary desktop, which can report a hidden or stale cursor.
class SecureDesktopCursorState {
public:
void SetActive(bool active) {
std::lock_guard<std::mutex> lock(mutex_);
if (active_ != active) snapshot_ = {};
active_ = active;
}
void Update(const SecureDesktopCursorSnapshot& snapshot) {
std::lock_guard<std::mutex> lock(mutex_);
if (active_) {
snapshot_ = snapshot;
updated_at_ = std::chrono::steady_clock::now();
}
}
bool Get(SecureDesktopCursorSnapshot* snapshot) const {
std::lock_guard<std::mutex> lock(mutex_);
if (!active_ || !snapshot) return false;
*snapshot = snapshot_;
if (std::chrono::steady_clock::now() - updated_at_ >=
std::chrono::milliseconds(1500)) {
snapshot->valid = 0;
}
return true;
}
private:
mutable std::mutex mutex_;
bool active_ = false;
SecureDesktopCursorSnapshot snapshot_{};
std::chrono::steady_clock::time_point updated_at_{};
};
inline SecureDesktopCursorState& SharedSecureDesktopCursorState() {
static SecureDesktopCursorState state;
return state;
}
} // namespace crossdesk
#endif // CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
@@ -24,6 +24,10 @@ CROSSDESK_WGC_PLUGIN_API crossdesk::ScreenCapturer*
CrossDeskCreateWgcCapturer();
CROSSDESK_WGC_PLUGIN_API void CrossDeskDestroyWgcCapturer(
crossdesk::ScreenCapturer* capturer);
// Optional extensions; they do not change ScreenCapturer's virtual ABI.
CROSSDESK_WGC_PLUGIN_API int CrossDeskSetWgcCursorCapture(
crossdesk::ScreenCapturer* capturer, bool enabled);
CROSSDESK_WGC_PLUGIN_API bool CrossDeskWgcFrameCapturesCursor();
}
#endif
#endif
@@ -1,5 +1,6 @@
#include <mutex>
#include "captured_cursor_state.h"
#include "path_manager.h"
#include "rd_log.h"
#include "screen_capturer_wgc.h"
@@ -27,4 +28,12 @@ crossdesk::ScreenCapturer* CrossDeskCreateWgcCapturer() {
void CrossDeskDestroyWgcCapturer(crossdesk::ScreenCapturer* capturer) {
delete capturer;
}
int CrossDeskSetWgcCursorCapture(crossdesk::ScreenCapturer* capturer, bool enabled) {
return static_cast<crossdesk::ScreenCapturerWgc*>(capturer)->SetCursorCapture(enabled);
}
bool CrossDeskWgcFrameCapturesCursor() {
return crossdesk::current_frame_has_cursor;
}
}
@@ -13,6 +13,7 @@ class WgcSession {
unsigned int row_pitch;
const unsigned char* data;
bool cursor_captured;
};
class wgc_session_observer {
@@ -38,4 +39,4 @@ class WgcSession {
virtual ~WgcSession() {};
};
} // namespace crossdesk
#endif
#endif
@@ -201,7 +201,13 @@ int WgcSessionImpl::StartCaptureLocked(bool show_cursor) {
}
int WgcSessionImpl::StartLocked(bool show_cursor) {
if (is_running_) return 0;
const bool was_paused = is_paused_;
if (is_running_) {
if (last_show_cursor_ == show_cursor) return 0;
// Replace the pool along with the setting, so buffered frames from the
// previous cursor mode cannot be labeled with the new mode.
StopLocked();
}
last_show_cursor_ = show_cursor;
if (!is_initialized_) {
@@ -218,6 +224,7 @@ int WgcSessionImpl::StartLocked(bool show_cursor) {
LOG_WARN("WGC: start capture failed, rebuilding capture item");
CleanUpLocked();
is_paused_ = was_paused;
ret = InitializeLocked();
if (ret != 0) {
return ret;
@@ -259,10 +266,12 @@ void WgcSessionImpl::OnFrame(
[[maybe_unused]] winrt::Windows::Foundation::IInspectable const& args) {
std::lock_guard locker(lock_);
if (!is_running_ || sender != capture_framepool_) return;
auto is_new_size = false;
try {
auto frame = sender.TryGetNextFrame();
if (!frame) return;
auto frame_size = frame.ContentSize();
if (frame_size.Width != capture_frame_size_.Width ||
@@ -310,12 +319,21 @@ void WgcSessionImpl::OnFrame(
// copy data from map_result.pData
if (map_result.pData && observer_) {
CURSORINFO cursor{};
cursor.cbSize = sizeof(cursor);
const bool cursor_captured =
last_show_cursor_ && GetCursorInfo(&cursor) && cursor.hCursor &&
(cursor.flags & CURSOR_SHOWING) != 0 &&
(target_.is_window ||
MonitorFromPoint(cursor.ptScreenPos, MONITOR_DEFAULTTONULL) ==
target_.hmonitor);
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)},
(unsigned char*)map_result.pData),
cursor_captured},
id_);
}
@@ -25,6 +25,8 @@
#include "path_manager.h"
#include "rd_log.h"
#include "session_helper_shared.h"
#include "windows_cursor_state.h"
#include "cursor_draw.h"
#include "windows_input_marker.h"
namespace {
@@ -97,18 +99,18 @@ struct SecureSharedCaptureState {
uint32_t sequence = 0;
};
struct PipeSecurityAttributes {
PipeSecurityAttributes() = default;
~PipeSecurityAttributes() {
struct IpcSecurityAttributes {
IpcSecurityAttributes() = default;
~IpcSecurityAttributes() {
if (security_descriptor_ != nullptr) {
LocalFree(security_descriptor_);
}
}
bool Initialize() {
constexpr wchar_t kPipeSddl[] = L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;AU)";
bool Initialize(const wchar_t* sddl =
L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;AU)") {
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
kPipeSddl, SDDL_REVISION_1, &security_descriptor_, nullptr)) {
sddl, SDDL_REVISION_1, &security_descriptor_, nullptr)) {
return false;
}
@@ -452,7 +454,7 @@ std::string BuildHelperStatusResponse(HelperState* helper_state) {
void HelperIpcServerLoop(HANDLE stop_event, DWORD session_id,
HelperState* helper_state) {
PipeSecurityAttributes security_attributes;
IpcSecurityAttributes security_attributes;
SECURITY_ATTRIBUTES* pipe_attributes = nullptr;
if (security_attributes.Initialize()) {
pipe_attributes = security_attributes.get();
@@ -1135,6 +1137,7 @@ struct SecureDesktopGdiResources {
void* bits = nullptr;
const char* error = nullptr;
DWORD error_code = ERROR_SUCCESS;
crossdesk::SecureDesktopCursorSnapshot cursor_snapshot{};
~SecureDesktopGdiResources() {
if (old_bitmap != nullptr && old_bitmap != HGDI_ERROR) {
@@ -1183,17 +1186,28 @@ struct SecureDesktopGdiResources {
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
return Fail("bitblt_failed");
}
if (request.show_cursor) {
CURSORINFO cursor{};
cursor.cbSize = sizeof(cursor);
if (GetCursorInfo(&cursor) && cursor.flags == CURSOR_SHOWING &&
cursor.hCursor != nullptr) {
const int x = cursor.ptScreenPos.x - request.left;
const int y = cursor.ptScreenPos.y - request.top;
if (x >= -64 && y >= -64 && x < request.width + 64 &&
y < request.height + 64) {
DrawIconEx(mem_dc, x, y, cursor.hCursor, 0, 0, 0, nullptr, DI_NORMAL);
}
// Sample on the thread bound to the captured input desktop, including when
// native controllers render the cursor themselves (show_cursor == false).
cursor_snapshot = {};
CURSORINFO cursor{};
cursor.cbSize = sizeof(cursor);
if (GetCursorInfo(&cursor)) {
static thread_local crossdesk::WindowsCursorState cursor_state;
crossdesk::CursorState state{};
cursor_state.Sample(cursor, false, &state);
cursor_snapshot.valid = 1;
cursor_snapshot.visible = state.visible;
cursor_snapshot.shape = static_cast<uint32_t>(state.shape);
cursor_snapshot.render_mode = static_cast<uint32_t>(state.render_mode);
cursor_snapshot.hidden_reason = static_cast<uint32_t>(state.hidden_reason);
cursor_snapshot.x = cursor.ptScreenPos.x;
cursor_snapshot.y = cursor.ptScreenPos.y;
if (request.show_cursor && crossdesk::DrawCursorInCapture(
mem_dc, cursor, request.left, request.top,
request.width, request.height)) {
cursor_snapshot.visible = 0;
cursor_snapshot.render_mode =
static_cast<uint32_t>(crossdesk::CursorRenderMode::embedded);
}
}
// Complete GDI writes before libyuv reads the DIB's pixel memory.
@@ -1237,6 +1251,7 @@ std::vector<uint8_t> CaptureSecureDesktopFrame(
header.height = static_cast<uint32_t>(request.height);
header.payload_size =
static_cast<uint32_t>(capture_buffers->nv12_frame.size());
header.cursor = capture.cursor_snapshot;
std::vector<uint8_t> response(sizeof(header) +
capture_buffers->nv12_frame.size());
@@ -1362,6 +1377,7 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
header->width = static_cast<uint32_t>(request.width);
header->height = static_cast<uint32_t>(request.height);
header->payload_size = static_cast<uint32_t>(nv12_frame.size());
header->cursor = capture->cursor_snapshot;
std::memcpy(payload, nv12_frame.data(), nv12_frame.size());
header->sequence = ++capture_state->sequence;
MemoryBarrier();
@@ -1453,12 +1469,21 @@ std::vector<uint8_t> StartSecureDesktopSharedCapture(
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_size"));
}
PipeSecurityAttributes security_attributes;
IpcSecurityAttributes security_attributes;
SECURITY_ATTRIBUTES* attributes = nullptr;
if (security_attributes.Initialize()) {
attributes = security_attributes.get();
}
// Event GENERIC_READ/GENERIC_WRITE omit SYNCHRONIZE. The GUI only needs
// to wait for frames; the SYSTEM helper retains permission to signal them.
IpcSecurityAttributes event_security_attributes;
if (!event_security_attributes.Initialize(
L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;0x00100000;;;AU)")) {
return BuildTextResponseBytes(
BuildErrorJson("create_frame_event_security_failed", GetLastError()));
}
const std::wstring mapping_name =
crossdesk::GetCrossDeskSecureDesktopFrameMappingName(
capture_state->session_id);
@@ -1485,7 +1510,8 @@ std::vector<uint8_t> StartSecureDesktopSharedCapture(
}
HANDLE frame_ready_event =
CreateEventW(attributes, FALSE, FALSE, event_name.c_str());
CreateEventW(event_security_attributes.get(), FALSE, FALSE,
event_name.c_str());
if (frame_ready_event == nullptr) {
const DWORD error = GetLastError();
UnmapViewOfFile(frame_view);
@@ -1647,7 +1673,7 @@ void SecureInputHelperIpcServerLoop(HANDLE stop_event, DWORD session_id) {
auto capture_state = std::make_shared<SecureSharedCaptureState>();
capture_state->session_id = session_id;
PipeSecurityAttributes security_attributes;
IpcSecurityAttributes security_attributes;
SECURITY_ATTRIBUTES* pipe_attributes = nullptr;
if (security_attributes.Initialize()) {
pipe_attributes = security_attributes.get();
@@ -12,6 +12,8 @@
#include <cstdint>
#include <string>
#include "secure_desktop_cursor_state.h"
namespace crossdesk {
inline constexpr wchar_t kCrossDeskSessionHelperPipePrefix[] =
@@ -33,7 +35,9 @@ inline constexpr wchar_t kCrossDeskSecureDesktopFrameMappingPrefix[] =
inline constexpr wchar_t kCrossDeskSecureDesktopFrameReadyEventPrefix[] =
L"Global\\CrossDeskSecureDesktopFrameReady-";
inline constexpr uint32_t kCrossDeskSecureDesktopFrameMagic = 0x50444358;
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 1;
// Version 3 distinguishes embedded, hidden and unavailable cursor states.
// The GUI and local session helper must be deployed together.
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 3;
#pragma pack(push, 1)
struct CrossDeskSecureDesktopFrameHeader {
@@ -44,6 +48,7 @@ struct CrossDeskSecureDesktopFrameHeader {
uint32_t width;
uint32_t height;
uint32_t payload_size;
SecureDesktopCursorSnapshot cursor;
};
struct CrossDeskSecureDesktopSharedFrameHeader {
@@ -57,6 +62,7 @@ struct CrossDeskSecureDesktopSharedFrameHeader {
uint32_t height;
uint32_t payload_size;
uint32_t buffer_size;
SecureDesktopCursorSnapshot cursor;
};
#pragma pack(pop)
+9 -3
View File
@@ -399,8 +399,10 @@ function setup_targets()
if is_os("windows") then
add_cxxflags("/bigobj")
add_links("opengl32")
add_syslinks("Comctl32", {public = true})
add_files("apps/desktop/src/platform/windows/gui/tray/win_tray.cpp",
"apps/desktop/src/platform/windows/gui/slint_backend.cpp",
"apps/desktop/src/platform/windows/gui/window_icons.cpp",
"apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp",
"apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp",
"apps/desktop/src/platform/common/gui/cuda_driver.cpp",
@@ -459,7 +461,8 @@ function setup_targets()
"apps/desktop/src/platform/windows/service/service_host.cpp")
add_files("apps/desktop/resources/windows/crossdesk_service.rc",
{defines = windows_resource_defines})
add_includedirs("apps/desktop/src/platform/windows/service")
add_includedirs("apps/desktop/src/platform/windows/service",
"apps/desktop/src/platform/windows/screen_capturer")
target("crossdesk_session_helper")
set_kind("binary")
@@ -470,8 +473,10 @@ function setup_targets()
add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc",
{defines = windows_helper_resource_defines})
add_includedirs("apps/desktop/src/common",
"apps/desktop/src/platform/windows/service")
"apps/desktop/src/platform/windows/service",
"apps/desktop/src/platform/windows/screen_capturer")
add_includedirs("apps/desktop/src/platform/windows/input")
add_includedirs("libs/wire/include")
end
target("crossdesk")
@@ -482,7 +487,8 @@ function setup_targets()
if is_os("windows") then
add_files("apps/desktop/src/platform/windows/daemon_backend.cpp")
add_files("apps/desktop/src/platform/windows/service/service_host.cpp")
add_includedirs("apps/desktop/src/platform/windows/service")
add_includedirs("apps/desktop/src/platform/windows/service",
"apps/desktop/src/platform/windows/screen_capturer")
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
add_files(crossdesk_windows_resource, {defines = windows_resource_defines})
@@ -621,21 +621,15 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
y:(float)y
action:(CrossDeskPointerAction)action {
const std::string message = MouseJSON(x, y, 0, static_cast<int>(action));
// Movement is latency-sensitive and can be superseded by newer positions.
// Button transitions must not be lost or reordered, otherwise a tap can be
// applied at a stale pointer position or leave a button logically pressed.
// MiniRTC reliability is configured per stream, so transitions use the
// existing reliable control stream while movement stays on the mouse stream.
const BOOL is_movement = action == CrossDeskPointerActionMove;
// Keep movement, button transitions, and scrolling ordered on the reliable
// mouse stream.
#if DEBUG
if (!is_movement) {
if (action != CrossDeskPointerActionMove) {
NSLog(@"CrossDesk pointer action=%ld normalized=(%.5f, %.5f)",
static_cast<long>(action), x, y);
}
#endif
[self sendMessage:message
reliable:!is_movement
stream:is_movement ? kMouseStream : kControlStream];
[self sendMessage:message reliable:YES stream:kMouseStream];
}
- (void)sendScrollX:(float)x
@@ -648,7 +642,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
const int flag = vertical ? 7 : 8;
const int wheel = raw_delta > 0 ? 1 : -1;
const std::string message = MouseJSON(x, y, wheel, flag);
[self sendMessage:message reliable:NO stream:kMouseStream];
[self sendMessage:message reliable:YES stream:kMouseStream];
}
- (void)sendWindowsKeyCode:(NSUInteger)keyCode isDown:(BOOL)isDown {
@@ -893,7 +887,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
// display. Remote display receivers are created from the host's SDP.
AddAudioStream(_state->controller_peer, kAudioStream);
AddDataStream(_state->controller_peer, kDataStream, false);
AddDataStream(_state->controller_peer, kMouseStream, false);
AddDataStream(_state->controller_peer, kMouseStream, true);
AddDataStream(_state->controller_peer, kKeyboardStream, true);
AddDataStream(_state->controller_peer, kControlStream, true);
AddDataStream(_state->controller_peer, kFileStream, true);
+10
View File
@@ -0,0 +1,10 @@
# **更新内容**
1. 新增Windows、macOS与Linux X11隐私屏,支持遮蔽被控端本地画面、屏蔽本地键鼠输入,并在断开连接或异常时自动恢复;远程会话保持正常操作。
2. 增强桌面端与iOS端P2P穿透能力,优化UDP打洞、ICE连接建立与带宽恢复,支持使用服务器下发的连接配置,并改善加密传输与旧版客户端的兼容性。
3. 新增Windows屏幕采集方式设置,支持DXGI、WGC与GDI;修复锁屏、隐私屏及未连接物理鼠标时远程光标不可见、位置异常和拖动窗口时出现重复光标的问题。
4. 优化画面缩小时的显示清晰度、帧率与分辨率恢复,修复Windows、macOS与Linux系统音频采集可能卡住的问题,提升远程画面与音频的稳定性。
5. 修复Linux X11远程键盘输入与macOS窗口缩放光标识别,改进鼠标操作和光标状态传输的可靠性;优化控制栏提示、网络状态布局与加密状态显示,并修复部分平台的Dock及任务栏图标显示问题。
6. 完善iOS/iPadOS连接失败提示,保留密码错误、设备不可用和网络中断等具体原因,简化服务器配置并优化原生客户端构建流程。
PC客户端版本为v1.5.1,iOS/iPadOS客户端暂未上架App Store,目前仅支持作为控制端连接远端电脑。隐私屏需控制端与被控端均支持,暂不支持Web及旧版客户端;macOS要求14.0及以上版本,Linux仅支持X11会话。
+22
View File
@@ -73,6 +73,24 @@ struct KeyboardState {
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
};
// Optional presentation metadata. Zero preserves pre-metadata peers' behavior.
enum class CursorRenderMode : uint8_t {
legacy = 0,
separate,
embedded,
hidden,
unknown,
};
enum class CursorHiddenReason : uint8_t {
unspecified = 0,
system_hidden,
system_suppressed,
no_pointing_device,
sampling_failed,
secure_desktop_pending,
};
struct CursorState {
uint32_t seq;
bool visible;
@@ -89,6 +107,10 @@ struct CursorState {
// Shape-only updates set this to false so cursor appearance can remain
// responsive while position feedback to the input source is suppressed.
bool position_update;
// Keep visible/shape for old receivers; new receivers can distinguish an
// embedded pointer from a hidden pointer and an unavailable sample.
CursorRenderMode render_mode;
CursorHiddenReason hidden_reason;
};
struct HostInfo {
+16 -1
View File
@@ -91,7 +91,9 @@ std::string RemoteAction::ToJson(const RemoteAction& action) {
{"visual_offset_x", action.cs.visual_offset_x},
{"visual_offset_y", action.cs.visual_offset_y},
{"display_id", action.cs.display_id},
{"position_update", action.cs.position_update}};
{"position_update", action.cs.position_update},
{"render_mode", static_cast<int>(action.cs.render_mode)},
{"hidden_reason", static_cast<int>(action.cs.hidden_reason)}};
break;
case ControlType::audio_capture:
object["audio_capture"] = action.a;
@@ -197,6 +199,19 @@ bool RemoteAction::FromJson(const std::string& json_string,
output.cs.seq = cursor_state_object.at("seq").get<uint32_t>();
output.cs.visible = cursor_state_object.at("visible").get<bool>();
output.cs.shape = static_cast<RemoteCursorShape>(shape);
const int mode = cursor_state_object.value("render_mode", 0);
const int reason = cursor_state_object.value("hidden_reason", 0);
// Unknown extensions from a newer peer fall back to visible/shape.
output.cs.render_mode =
mode >= 0 && mode <= static_cast<int>(CursorRenderMode::unknown)
? static_cast<CursorRenderMode>(mode)
: CursorRenderMode::legacy;
output.cs.hidden_reason =
reason >= 0 &&
reason <= static_cast<int>(
CursorHiddenReason::secure_desktop_pending)
? static_cast<CursorHiddenReason>(reason)
: CursorHiddenReason::unspecified;
output.cs.position_valid =
cursor_state_object.value("position_valid", false);
output.cs.x = cursor_state_object.value("x", 0.5f);