From 000b2e44e34ec676791637ea57fc9d168a5f35a9 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Fri, 18 Sep 2026 23:26:06 +0800 Subject: [PATCH] [fix] restore Windows cursor capture and web input control --- .../screen_capturer/cursor_frame_compositor.h | 93 +++++++++ .../screen_capturer/screen_capturer_dxgi.cpp | 181 +++++++++++------- .../screen_capturer/screen_capturer_dxgi.h | 8 + .../screen_capturer/screen_capturer_win.cpp | 2 + apps/desktop/xmake/targets.lua | 3 +- deps/submodules/minirtc | 2 +- 6 files changed, 220 insertions(+), 69 deletions(-) create mode 100644 apps/desktop/src/platform/windows/screen_capturer/cursor_frame_compositor.h diff --git a/apps/desktop/src/platform/windows/screen_capturer/cursor_frame_compositor.h b/apps/desktop/src/platform/windows/screen_capturer/cursor_frame_compositor.h new file mode 100644 index 00000000..e5d8d7ae --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/cursor_frame_compositor.h @@ -0,0 +1,93 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-18 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _CURSOR_FRAME_COMPOSITOR_H_ +#define _CURSOR_FRAME_COMPOSITOR_H_ + +#include + +#include +#include + +#include "cursor_draw.h" + +namespace crossdesk { + +// Keep the source texture free of cursors. Each draw starts from the original +// pixels, including when a static desktop is replayed after a cursor toggle. +class CursorFrameCompositor { + public: + CursorFrameCompositor() = default; + ~CursorFrameCompositor() { Reset(); } + CursorFrameCompositor(const CursorFrameCompositor&) = delete; + CursorFrameCompositor& operator=(const CursorFrameCompositor&) = delete; + + const uint8_t* Draw(const uint8_t* source, int stride, int width, int height, + const CURSORINFO& cursor, int left, int top) { + if (!source || width <= 0 || height <= 0 || !EnsureSurface(width, height)) + return nullptr; + const size_t row_bytes = static_cast(width) * 4; + for (int y = 0; y < height; ++y) { + std::memcpy(static_cast(bits_) + y * row_bytes, + source + static_cast(y) * stride, row_bytes); + } + const bool drawn = + DrawCursorInCapture(dc_, cursor, left, top, width, height); + GdiFlush(); + return drawn ? static_cast(bits_) : nullptr; + } + + private: + bool EnsureSurface(int width, int height) { + if (bits_ && width_ == width && height_ == height) return true; + Reset(); + dc_ = CreateCompatibleDC(nullptr); + if (!dc_) return false; + BITMAPINFO info{}; + info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + info.bmiHeader.biWidth = width; + info.bmiHeader.biHeight = -height; + info.bmiHeader.biPlanes = 1; + info.bmiHeader.biBitCount = 32; + info.bmiHeader.biCompression = BI_RGB; + bitmap_ = CreateDIBSection(dc_, &info, DIB_RGB_COLORS, &bits_, nullptr, 0); + if (!bitmap_ || !bits_) { + Reset(); + return false; + } + previous_ = SelectObject(dc_, bitmap_); + if (!previous_ || previous_ == HGDI_ERROR) { + previous_ = nullptr; + Reset(); + return false; + } + width_ = width; + height_ = height; + return true; + } + + void Reset() { + if (dc_ && previous_) SelectObject(dc_, previous_); + if (bitmap_) DeleteObject(bitmap_); + if (dc_) DeleteDC(dc_); + dc_ = nullptr; + bitmap_ = nullptr; + previous_ = nullptr; + bits_ = nullptr; + width_ = height_ = 0; + } + + HDC dc_ = nullptr; + HBITMAP bitmap_ = nullptr; + HGDIOBJ previous_ = nullptr; + void* bits_ = nullptr; + int width_ = 0; + int height_ = 0; +}; + +} // namespace crossdesk + +#endif diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp index 1c4cdb24..98542b51 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp @@ -7,9 +7,12 @@ #include #include +#include "captured_cursor_state.h" +#include "cursor_frame_compositor.h" #include "dxgi_cursor_state.h" #include "libyuv.h" #include "rd_log.h" +#include "windows_thread_dpi.h" namespace crossdesk { @@ -295,12 +298,103 @@ void ScreenCapturerDxgi::ReleaseDuplication() { duplication_.Reset(); } +bool ScreenCapturerDxgi::ConvertFrame(int frame_monitor, bool* cursor_embedded, + CursorFrameCompositor& compositor) { + *cursor_embedded = false; + if (!staging_) return false; + D3D11_TEXTURE2D_DESC src_desc{}; + staging_->GetDesc(&src_desc); + D3D11_MAPPED_SUBRESOURCE mapped{}; + const HRESULT hr = + d3d_context_->Map(staging_.Get(), 0, D3D11_MAP_READ, 0, &mapped); + if (FAILED(hr)) { + return false; + } + + auto pixels = static_cast(mapped.pData); + int stride = static_cast(mapped.RowPitch); + int logical_width = static_cast(src_desc.Width); + int logical_height = static_cast(src_desc.Height); + libyuv::RotationMode rotation = libyuv::kRotate0; + switch (rotation_) { + case DXGI_MODE_ROTATION_ROTATE90: + rotation = libyuv::kRotate90; + break; + case DXGI_MODE_ROTATION_ROTATE180: + rotation = libyuv::kRotate180; + break; + case DXGI_MODE_ROTATION_ROTATE270: + rotation = libyuv::kRotate270; + break; + default: + break; + } + if (rotation != libyuv::kRotate0) { + const bool swap_axes = rotation != libyuv::kRotate180; + const int rotated_width = swap_axes ? logical_height : logical_width; + rotated_frame_.resize(static_cast(logical_width) * logical_height * + 4); + if (libyuv::ARGBRotate(pixels, stride, rotated_frame_.data(), + rotated_width * 4, logical_width, logical_height, + rotation) != 0) { + d3d_context_->Unmap(staging_.Get(), 0); + return false; + } + pixels = rotated_frame_.data(); + stride = rotated_width * 4; + if (swap_axes) std::swap(logical_width, logical_height); + } + int even_width = logical_width & ~1; + int even_height = logical_height & ~1; + if (even_width <= 0 || even_height <= 0) { + d3d_context_->Unmap(staging_.Get(), 0); + return false; + } + + if (show_cursor_.load(std::memory_order_relaxed) && frame_monitor >= 0 && + frame_monitor < static_cast(display_info_list_.size())) { + const auto& display = display_info_list_[frame_monitor]; + CURSORINFO cursor{}; + cursor.cbSize = sizeof(cursor); + if (GetCursorInfo(&cursor) && + SharedDxgiCursorState().ShouldDrawCursor( + (cursor.flags & CURSOR_SHOWING) != 0, display.handle)) { + if (const auto* composited = + compositor.Draw(pixels, stride, even_width, even_height, cursor, + display.left, display.top)) { + pixels = composited; + stride = even_width * 4; + *cursor_embedded = true; + } + } + } + + int nv12_size = even_width * even_height * 3 / 2; + if (!nv12_frame_ || nv12_width_ != even_width || + nv12_height_ != even_height) { + delete[] nv12_frame_; + nv12_frame_ = new unsigned char[nv12_size]; + nv12_width_ = even_width; + nv12_height_ = even_height; + } + + const int converted = + libyuv::ARGBToNV12(pixels, stride, nv12_frame_, even_width, + nv12_frame_ + even_width * even_height, even_width, + even_width, even_height); + + d3d_context_->Unmap(staging_.Get(), 0); + return converted == 0; +} + void ScreenCapturerDxgi::CaptureLoop() { + ScopedWindowsPhysicalCoordinates physical_coordinates; + CursorFrameCompositor cursor_compositor; const int timeout_ms = (std::max)(1, 1000 / (std::max)(1, fps_)); bool cached_frame_valid = false; + bool cached_cursor_embedded = false; int cached_monitor = -1; uint64_t cached_generation = 0; - std::vector rotated_frame; auto last_duplication_retry = std::chrono::steady_clock::now() - std::chrono::milliseconds(1000); while (running_) { @@ -336,8 +430,19 @@ void ScreenCapturerDxgi::CaptureLoop() { // successfully acquired for this monitor to keep delivering static frames. if (cached_frame_valid && cached_generation == duplication_generation_ && cached_monitor == monitor_index_.load() && callback_ && nv12_frame_) { + // Recompose from the clean staging texture while showing a cursor, or + // once after disabling it. Never replay a baked-in stale cursor. + if (show_cursor_.load(std::memory_order_relaxed) || + cached_cursor_embedded) { + if (!ConvertFrame(cached_monitor, &cached_cursor_embedded, + cursor_compositor)) { + cached_frame_valid = false; + continue; + } + } const auto stream_id = MakeDisplayStreamId(cached_monitor); capture_lock.unlock(); + CapturedCursorFrameScope cursor_scope(cached_cursor_embedded); callback_(nv12_frame_, nv12_width_ * nv12_height_ * 3 / 2, nv12_width_, nv12_height_, stream_id.c_str(), nullptr); } @@ -397,80 +502,22 @@ void ScreenCapturerDxgi::CaptureLoop() { d3d_context_->CopyResource(staging_.Get(), acquired_tex.Get()); - D3D11_MAPPED_SUBRESOURCE mapped{}; - hr = d3d_context_->Map(staging_.Get(), 0, D3D11_MAP_READ, 0, &mapped); - if (FAILED(hr)) { - duplication_->ReleaseFrame(); - continue; - } - - auto pixels = static_cast(mapped.pData); - int stride = static_cast(mapped.RowPitch); - int logical_width = static_cast(src_desc.Width); - int logical_height = static_cast(src_desc.Height); - libyuv::RotationMode rotation = libyuv::kRotate0; - switch (rotation_) { - case DXGI_MODE_ROTATION_ROTATE90: - rotation = libyuv::kRotate90; - break; - case DXGI_MODE_ROTATION_ROTATE180: - rotation = libyuv::kRotate180; - break; - case DXGI_MODE_ROTATION_ROTATE270: - rotation = libyuv::kRotate270; - break; - default: - break; - } - if (rotation != libyuv::kRotate0) { - const bool swap_axes = rotation != libyuv::kRotate180; - const int rotated_width = swap_axes ? logical_height : logical_width; - rotated_frame.resize(static_cast(logical_width) * logical_height * - 4); - if (libyuv::ARGBRotate(pixels, stride, rotated_frame.data(), - rotated_width * 4, logical_width, logical_height, - rotation) != 0) { - d3d_context_->Unmap(staging_.Get(), 0); - duplication_->ReleaseFrame(); - continue; - } - pixels = rotated_frame.data(); - stride = rotated_width * 4; - if (swap_axes) std::swap(logical_width, logical_height); - } - int even_width = logical_width & ~1; - int even_height = logical_height & ~1; - if (even_width <= 0 || even_height <= 0) { - d3d_context_->Unmap(staging_.Get(), 0); - duplication_->ReleaseFrame(); - continue; - } - - int nv12_size = even_width * even_height * 3 / 2; - if (!nv12_frame_ || nv12_width_ != even_width || - nv12_height_ != even_height) { - delete[] nv12_frame_; - nv12_frame_ = new unsigned char[nv12_size]; - nv12_width_ = even_width; - nv12_height_ = even_height; - } - - const int converted = - libyuv::ARGBToNV12(pixels, stride, nv12_frame_, even_width, - nv12_frame_ + even_width * even_height, even_width, - even_width, even_height); + bool cursor_embedded = false; + const bool converted = + ConvertFrame(frame_monitor, &cursor_embedded, cursor_compositor); cached_generation = duplication_generation_; - d3d_context_->Unmap(staging_.Get(), 0); duplication_->ReleaseFrame(); capture_lock.unlock(); - if (converted == 0 && frame_monitor == monitor_index_.load() && callback_) { + if (converted && frame_monitor == monitor_index_.load() && callback_) { int idx = frame_monitor; if (idx >= 0 && idx < static_cast(display_info_list_.size())) { const std::string stream_id = MakeDisplayStreamId(idx); - callback_(nv12_frame_, nv12_size, even_width, even_height, - stream_id.c_str(), nullptr); + CapturedCursorFrameScope cursor_scope(cursor_embedded); + callback_(nv12_frame_, nv12_width_ * nv12_height_ * 3 / 2, nv12_width_, + nv12_height_, stream_id.c_str(), nullptr); + cached_cursor_embedded = cursor_embedded; cached_monitor = idx; cached_frame_valid = true; } else { diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.h b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.h index 1bce1103..dadacf27 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.h +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.h @@ -25,6 +25,8 @@ namespace crossdesk { +class CursorFrameCompositor; + class ScreenCapturerDxgi : public ScreenCapturer { public: ScreenCapturerDxgi(); @@ -35,6 +37,9 @@ class ScreenCapturerDxgi : 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; @@ -52,6 +57,8 @@ class ScreenCapturerDxgi : public ScreenCapturer { bool CreateDuplicationForMonitor(int monitor_index); bool RecreateDuplicationForCurrentMonitor(); void CaptureLoop(); + bool ConvertFrame(int frame_monitor, bool* cursor_embedded, + CursorFrameCompositor& compositor); void ReleaseDuplication(); private: @@ -79,6 +86,7 @@ class ScreenCapturerDxgi : public ScreenCapturer { unsigned char* nv12_frame_ = nullptr; int nv12_width_ = 0; int nv12_height_ = 0; + std::vector rotated_frame_; }; } // namespace crossdesk 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 e347fb0a..881915ee 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 @@ -902,6 +902,8 @@ void ScreenCapturerWin::ApplyCursorCaptureSetting() { int ret = 0; if (auto* gdi = dynamic_cast(impl_.get())) { gdi->SetCursorCapture(requested); + } else if (auto* dxgi = dynamic_cast(impl_.get())) { + dxgi->SetCursorCapture(requested); } else if (auto* wgc = dynamic_cast(impl_.get())) { ret = wgc->SetCursorCapture(requested); } diff --git a/apps/desktop/xmake/targets.lua b/apps/desktop/xmake/targets.lua index 8f9a44e8..67769df9 100644 --- a/apps/desktop/xmake/targets.lua +++ b/apps/desktop/xmake/targets.lua @@ -230,7 +230,8 @@ function setup_targets() "apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp", "apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp", "apps/desktop/src/platform/windows/screen_capturer/screen_capturer_factory.cpp") - add_includedirs("apps/desktop/src/platform/windows/screen_capturer", + add_includedirs("apps/desktop/src/platform/windows", + "apps/desktop/src/platform/windows/screen_capturer", "apps/desktop/src/platform/windows/service") elseif is_os("macosx") then add_deps("privacy") diff --git a/deps/submodules/minirtc b/deps/submodules/minirtc index 96b765f8..a25a3b4c 160000 --- a/deps/submodules/minirtc +++ b/deps/submodules/minirtc @@ -1 +1 @@ -Subproject commit 96b765f81844210679d52e5a8b7d8af40fd4bb11 +Subproject commit a25a3b4c4abb041ad129d60abc8b2508aa7c33fa