From ce7a3c178eb09672274d9ab269578dab99a6fa33 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Wed, 16 Sep 2026 23:03:36 +0800 Subject: [PATCH] [fix] correct WGC and GDI cursor capture and positioning --- .../src/gui/application/application_state.h | 1 - .../devices/session_device_manager.cpp | 25 ++++- .../features/devices/session_device_manager.h | 8 ++ .../src/gui/runtime/connection_runtime.cpp | 5 +- .../src/gui/runtime/cursor_state_provider.cpp | 6 +- .../src/gui/runtime/peer_event_handler.cpp | 11 +-- .../windows/input/windows_cursor_state.h | 10 +- .../screen_capturer/captured_cursor_state.h | 55 +++++++++++ .../windows/screen_capturer/cursor_draw.h | 49 ++++++++++ .../screen_capturer/screen_capturer_gdi.cpp | 14 +-- .../screen_capturer/screen_capturer_gdi.h | 5 +- .../screen_capturer/screen_capturer_wgc.cpp | 12 +++ .../screen_capturer/screen_capturer_wgc.h | 1 + .../screen_capturer/screen_capturer_win.cpp | 95 ++++++++++++++++++- .../screen_capturer/screen_capturer_win.h | 6 ++ .../windows/screen_capturer/wgc_plugin_api.h | 6 +- .../screen_capturer/wgc_plugin_entry.cpp | 9 ++ .../windows/screen_capturer/wgc_session.h | 3 +- .../screen_capturer/wgc_session_impl.cpp | 22 ++++- .../windows/service/session_helper_main.cpp | 22 ++--- 20 files changed, 309 insertions(+), 56 deletions(-) create mode 100644 apps/desktop/src/platform/windows/screen_capturer/captured_cursor_state.h create mode 100644 apps/desktop/src/platform/windows/screen_capturer/cursor_draw.h diff --git a/apps/desktop/src/gui/application/application_state.h b/apps/desktop/src/gui/application/application_state.h index dab41043..88c961ff 100644 --- a/apps/desktop/src/gui/application/application_state.h +++ b/apps/desktop/src/gui/application/application_state.h @@ -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; diff --git a/apps/desktop/src/gui/features/devices/session_device_manager.cpp b/apps/desktop/src/gui/features/devices/session_device_manager.cpp index 395a8c91..3ffe2667 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.cpp +++ b/apps/desktop/src/gui/features/devices/session_device_manager.cpp @@ -271,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()) { @@ -286,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); } @@ -503,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(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 = diff --git a/apps/desktop/src/gui/features/devices/session_device_manager.h b/apps/desktop/src/gui/features/devices/session_device_manager.h index 532dcf5e..28365c19 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.h +++ b/apps/desktop/src/gui/features/devices/session_device_manager.h @@ -10,6 +10,7 @@ #include +#include #include #include #include @@ -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,6 +90,7 @@ 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); @@ -91,6 +98,7 @@ private: SDL_AudioStream *output_stream_ = nullptr; ScreenCapturerFactory *screen_capturer_factory_ = nullptr; ScreenCapturer *screen_capturer_ = nullptr; + std::atomic cursor_capture_dirty_{true}; SpeakerCaptureController speaker_capture_; DeviceControllerFactory *device_controller_factory_ = nullptr; MouseController *mouse_controller_ = nullptr; diff --git a/apps/desktop/src/gui/runtime/connection_runtime.cpp b/apps/desktop/src/gui/runtime/connection_runtime.cpp index 15ed0f47..391fd243 100644 --- a/apps/desktop/src/gui/runtime/connection_runtime.cpp +++ b/apps/desktop/src/gui/runtime/connection_runtime.cpp @@ -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; diff --git a/apps/desktop/src/gui/runtime/cursor_state_provider.cpp b/apps/desktop/src/gui/runtime/cursor_state_provider.cpp index 1199b8f7..ec48529f 100644 --- a/apps/desktop/src/gui/runtime/cursor_state_provider.cpp +++ b/apps/desktop/src/gui/runtime/cursor_state_provider.cpp @@ -7,6 +7,7 @@ #include #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" @@ -53,8 +54,9 @@ bool CursorStateProvider::Sample(const std::vector& displays, void* cursor_monitor = state->position_valid ? displays[state->display_id].handle : nullptr; - const bool embedded = (info.flags & CURSOR_SHOWING) != 0 && - !SharedDxgiCursorState().ShouldDrawCursor(true, cursor_monitor); + 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; } diff --git a/apps/desktop/src/gui/runtime/peer_event_handler.cpp b/apps/desktop/src/gui/runtime/peer_event_handler.cpp index 6432af51..aec2efdd 100644 --- a/apps/desktop/src/gui/runtime/peer_event_handler.cpp +++ b/apps/desktop/src/gui/runtime/peer_event_handler.cpp @@ -419,6 +419,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, } } runtime->connection_status_[remote_id] = status; + runtime->devices_.OnControllerConnectionsChanged(); } switch (status) { @@ -485,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; } diff --git a/apps/desktop/src/platform/windows/input/windows_cursor_state.h b/apps/desktop/src/platform/windows/input/windows_cursor_state.h index 7ace2638..0c3528c4 100644 --- a/apps/desktop/src/platform/windows/input/windows_cursor_state.h +++ b/apps/desktop/src/platform/windows/input/windows_cursor_state.h @@ -24,14 +24,14 @@ class WindowsCursorState { public: void Sample(const CURSORINFO& info, bool embedded, CursorState* state) { const bool showing = (info.flags & CURSOR_SHOWING) != 0; - state->render_mode = showing ? (embedded ? CursorRenderMode::embedded - : CursorRenderMode::separate) - : CursorRenderMode::hidden; + 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) { + if (!showing && !embedded) { state->hidden_reason = (info.flags & CURSOR_SUPPRESSED) ? CursorHiddenReason::system_suppressed : info.hCursor && NoPointingDevice() @@ -83,4 +83,4 @@ class WindowsCursorState { } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/apps/desktop/src/platform/windows/screen_capturer/captured_cursor_state.h b/apps/desktop/src/platform/windows/screen_capturer/captured_cursor_state.h new file mode 100644 index 00000000..6bc2df85 --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/captured_cursor_state.h @@ -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 + +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 embedded_monitor_{nullptr}; +}; + +inline CapturedCursorState& SharedCapturedCursorState() { + static CapturedCursorState state; + return state; +} + +} // namespace crossdesk + +#endif diff --git a/apps/desktop/src/platform/windows/screen_capturer/cursor_draw.h b/apps/desktop/src/platform/windows/screen_capturer/cursor_draw.h new file mode 100644 index 00000000..67a5acd2 --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/cursor_draw.h @@ -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 + +#include + +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(cursor.ptScreenPos.x) - left - icon.xHotspot; + const int64_t y = + static_cast(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(x), static_cast(y), + cursor.hCursor, 0, 0, 0, nullptr, DI_NORMAL) != FALSE; +} + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp index 17c0185f..4f90fe61 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp @@ -6,6 +6,8 @@ #include #include +#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); diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.h b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.h index 47684907..ea159296 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.h +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.h @@ -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 \ No newline at end of file +#endif diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.cpp index 89426830..dc28c973 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.cpp @@ -8,6 +8,7 @@ #include #include +#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); } diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.h b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.h index 35bb98de..d694ab54 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.h +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.h @@ -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; diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp index 04f05286..e347fb0a 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp @@ -16,6 +16,7 @@ #include #include "captured_nv12_frame.h" +#include "captured_cursor_state.h" #include "dxgi_cursor_state.h" #include "interactive_state.h" #include "named_pipe_deadline.h" @@ -52,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 Create() { std::filesystem::path plugin_path; @@ -99,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; @@ -124,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( + GetProcAddress(module, "CrossDeskSetWgcCursorCapture")); + frame_cursor_fn_ = reinterpret_cast( + 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 legacy_capture_cursor_{false}; }; const char* CaptureBackendName(const ScreenCapturer* capturer) { @@ -363,16 +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 lock(alias_mutex_); auto it = stream_id_alias_.find(raw_stream_id); @@ -386,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"); @@ -400,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 = @@ -476,7 +527,10 @@ void ScreenCapturerWin::EmitCapturedFrame( 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(); + 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()) { @@ -543,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); @@ -555,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; @@ -568,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); @@ -636,10 +694,12 @@ 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; @@ -798,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; } @@ -812,6 +873,7 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() { ret = impl_->Start(show_cursor); } if (ret == 0) { + applied_show_cursor_ = show_cursor; RestoreMonitor(current_monitor); return true; } @@ -834,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(impl_.get())) { + gdi->SetCursorCapture(requested); + } else if (auto* wgc = dynamic_cast(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) { @@ -1113,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, @@ -1250,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; diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h index 7480ed88..bc5d15bd 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h @@ -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; @@ -75,6 +79,7 @@ class ScreenCapturerWin : public ScreenCapturer { std::atomic running_{false}; std::atomic paused_{false}; std::atomic show_cursor_{true}; + bool applied_show_cursor_ = true; std::atomic monitor_index_{0}; int initial_monitor_index_ = 0; std::atomic secure_desktop_capture_active_{false}; @@ -109,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); diff --git a/apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_api.h b/apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_api.h index 4b6714cd..2afb3faa 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_api.h +++ b/apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_api.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 \ No newline at end of file +#endif diff --git a/apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_entry.cpp b/apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_entry.cpp index 15145203..5df35759 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_entry.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_entry.cpp @@ -1,5 +1,6 @@ #include +#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(capturer)->SetCursorCapture(enabled); +} + +bool CrossDeskWgcFrameCapturesCursor() { + return crossdesk::current_frame_has_cursor; +} } diff --git a/apps/desktop/src/platform/windows/screen_capturer/wgc_session.h b/apps/desktop/src/platform/windows/screen_capturer/wgc_session.h index ff8471eb..5ee56305 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/wgc_session.h +++ b/apps/desktop/src/platform/windows/screen_capturer/wgc_session.h @@ -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 \ No newline at end of file +#endif diff --git a/apps/desktop/src/platform/windows/screen_capturer/wgc_session_impl.cpp b/apps/desktop/src/platform/windows/screen_capturer/wgc_session_impl.cpp index 77a561d8..3dc21c5f 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/wgc_session_impl.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/wgc_session_impl.cpp @@ -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(frame_size.Width), static_cast(frame_size.Height), map_result.RowPitch, const_cast( - (unsigned char*)map_result.pData)}, + (unsigned char*)map_result.pData), + cursor_captured}, id_); } diff --git a/apps/desktop/src/platform/windows/service/session_helper_main.cpp b/apps/desktop/src/platform/windows/service/session_helper_main.cpp index b651bd72..3ce9fbdc 100644 --- a/apps/desktop/src/platform/windows/service/session_helper_main.cpp +++ b/apps/desktop/src/platform/windows/service/session_helper_main.cpp @@ -26,6 +26,7 @@ #include "rd_log.h" #include "session_helper_shared.h" #include "windows_cursor_state.h" +#include "cursor_draw.h" #include "windows_input_marker.h" namespace { @@ -1201,21 +1202,12 @@ struct SecureDesktopGdiResources { cursor_snapshot.hidden_reason = static_cast(state.hidden_reason); cursor_snapshot.x = cursor.ptScreenPos.x; cursor_snapshot.y = cursor.ptScreenPos.y; - if (request.show_cursor && cursor_snapshot.visible && - 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) { - if (DrawIconEx(mem_dc, x, y, cursor.hCursor, 0, 0, 0, nullptr, - DI_NORMAL)) { - // A frame with an embedded cursor must not get a second cursor - // overlay from a native controller sharing the web capture path. - cursor_snapshot.visible = 0; - cursor_snapshot.render_mode = - static_cast(crossdesk::CursorRenderMode::embedded); - } - } + 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(crossdesk::CursorRenderMode::embedded); } } // Complete GDI writes before libyuv reads the DIB's pixel memory.