From 54881092b782bc39a3ce3885677e1f91f483fd13 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 12 Sep 2026 10:40:23 +0800 Subject: [PATCH] refactor(cursor): separate native capture from rendering fixes --- Cargo.toml | 2 +- flutter/test/cursor_dpi_policy_test.dart | 14 +- libs/scrap/Cargo.toml | 1 - libs/scrap/src/dxgi/cursor.rs | 259 ----------------------- libs/scrap/src/dxgi/cursor/tests.rs | 47 ---- libs/scrap/src/dxgi/mod.rs | 28 --- src/platform/windows.rs | 15 +- src/platform/windows/cursor.rs | 157 -------------- 8 files changed, 11 insertions(+), 512 deletions(-) delete mode 100644 libs/scrap/src/dxgi/cursor.rs delete mode 100644 libs/scrap/src/dxgi/cursor/tests.rs delete mode 100644 src/platform/windows/cursor.rs diff --git a/Cargo.toml b/Cargo.toml index e829d13ef..870f97988 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ inline = [] use_samplerate = ["samplerate"] use_rubato = ["rubato"] use_dasp = ["dasp"] -flutter = ["flutter_rust_bridge", "scrap/cursor", "dep:x11rb"] +flutter = ["flutter_rust_bridge", "dep:x11rb"] default = ["use_dasp"] hwcodec = ["scrap/hwcodec"] vram = ["scrap/vram"] diff --git a/flutter/test/cursor_dpi_policy_test.dart b/flutter/test/cursor_dpi_policy_test.dart index ce1869840..3a3cfe029 100644 --- a/flutter/test/cursor_dpi_policy_test.dart +++ b/flutter/test/cursor_dpi_policy_test.dart @@ -102,7 +102,9 @@ class _FFI extends Fake implements FFI { } Future _data(int density, String id) async { - final image = await createTestImage(width: 9 * density, height: 18 * density); + final bitmapDensity = density == 0 ? 1 : density; + final image = await createTestImage( + width: 9 * bitmapDensity, height: 18 * bitmapDensity); return CursorData( peerId: 'dpi-policy', id: id, @@ -110,8 +112,8 @@ Future _data(int density, String id) async { nativeImage: image, scale: 1, data: null, - hotxOrigin: 4.0 * density, - hotyOrigin: 9.0 * density, + hotxOrigin: 4.0 * bitmapDensity, + hotyOrigin: 9.0 * bitmapDensity, width: image.width, height: image.height, pixelRatio: density.toDouble(), @@ -120,6 +122,8 @@ Future _data(int density, String id) async { void main() { for (final (style, zoom, density, viewScale, expectedScale) in [ + (kRemoteViewStyleAdaptive, false, 0, 0.25, Platform.isWindows ? 1.0 : 4 / 3), + (kRemoteViewStyleCustom, false, 0, 0.25, Platform.isWindows ? 1.0 : 4 / 3), (kRemoteViewStyleAdaptive, false, 1, 0.25, Platform.isWindows ? 2.0 : 1.0), (kRemoteViewStyleAdaptive, false, 2, 0.25, Platform.isWindows ? 1.0 : 0.5), (kRemoteViewStyleCustom, false, 2, 0.25, Platform.isWindows ? 1.0 : 0.5), @@ -173,8 +177,8 @@ void main() { tester.binding.defaultBinaryMessenger .setMockMethodCallHandler(channel, null); expect(data.scale, closeTo(expectedScale, 1e-9)); - expect(data.hotx, closeTo(4 * density * expectedScale, 1e-9)); - expect(data.hoty, closeTo(9 * density * expectedScale, 1e-9)); + expect(data.hotx, closeTo(data.hotxOrigin * expectedScale, 1e-9)); + expect(data.hoty, closeTo(data.hotyOrigin * expectedScale, 1e-9)); })); } } diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index 5c963a5ac..1cc1ff619 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -10,7 +10,6 @@ authors = ["Ram "] edition = "2018" [features] -cursor = ["winapi/shellscalingapi"] wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "zbus"] # `drm` is a pure runtime-dlopen backend: rustdesk loads `libdrmtap.so.0` at runtime (`drmtap_dl.rs`) # and NEVER link-time links it, so the graceful PipeWire fallback when the .so or EGL is absent is diff --git a/libs/scrap/src/dxgi/cursor.rs b/libs/scrap/src/dxgi/cursor.rs deleted file mode 100644 index dcd599b47..000000000 --- a/libs/scrap/src/dxgi/cursor.rs +++ /dev/null @@ -1,259 +0,0 @@ -use super::wrap_hresult; -use std::{ - collections::{hash_map::DefaultHasher, HashMap}, - hash::{Hash, Hasher}, - io, - sync::{Arc, Mutex, Weak}, - time::Instant, -}; -use winapi::shared::dxgi1_2::{ - IDXGIOutputDuplication, DXGI_OUTDUPL_FRAME_INFO, DXGI_OUTDUPL_POINTER_SHAPE_INFO, - DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR, DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR, - DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME, -}; -// USER handles use 32 bits; reserve a separate namespace for physical DXGI shapes. -pub const CURSOR_ID_FLAG: u64 = 1 << 63; -const CHANNELS: u32 = 4; -const BITS_PER_BYTE: u32 = 8; -const DEFAULT_DPI: f64 = 96.0; -#[derive(Clone)] -pub struct Shape { - pub id: u64, - pub scale: f64, - pub kind: u32, - pub width: u32, - pub height: u32, - pub pitch: u32, - pub hotspot: (i32, i32), - pub pixels: Vec, -} - -impl Shape { - fn new(info: DXGI_OUTDUPL_POINTER_SHAPE_INFO, pixels: Vec) -> io::Result { - let (height, minimum_pitch) = match info.Type { - DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME if info.Height % 2 == 0 => { - (info.Height / 2, info.Width.div_ceil(BITS_PER_BYTE)) - } - DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR - | DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR => ( - info.Height, - info.Width.checked_mul(CHANNELS).ok_or_else(invalid_shape)?, - ), - _ => return Err(invalid_shape()), - }; - let length = (info.Pitch as usize).checked_mul(info.Height as usize); - if info.Width == 0 - || height == 0 - || info.Width > i32::MAX as u32 - || info.Height > i32::MAX as u32 - || info.Pitch > i32::MAX as u32 - || info.Pitch < minimum_pitch - || length != Some(pixels.len()) - || info.HotSpot.x < 0 - || info.HotSpot.x as u32 >= info.Width - || info.HotSpot.y < 0 - || info.HotSpot.y as u32 >= height - { - return Err(invalid_shape()); - } - let hotspot = (info.HotSpot.x, info.HotSpot.y); - Ok(Self { - id: 0, - scale: 0.0, - kind: info.Type, - width: info.Width, - height, - pitch: info.Pitch, - hotspot, - pixels, - }) - } - - fn with_scale(self, scale: f64) -> Self { - let mut hash = DefaultHasher::new(); - ( - self.kind, - self.width, - self.height, - self.pitch, - self.hotspot, - &self.pixels, - scale.to_bits(), - ) - .hash(&mut hash); - Self { - id: hash.finish() | CURSOR_ID_FLAG, - scale, - ..self - } - } -} - -fn invalid_shape() -> io::Error { - io::Error::new(io::ErrorKind::InvalidData, "Invalid DXGI cursor shape") -} - -#[derive(Clone)] -pub enum Snapshot { - Unavailable, - Pending, - Ready(Arc), - Failed(String), -} - -struct State { - updated: Instant, - snapshot: Snapshot, -} - -type SharedState = Arc>; - -lazy_static::lazy_static! { - static ref CAPTURES: Mutex>>>> = - Mutex::new(HashMap::new()); -} - -pub fn snapshot(monitor: usize) -> Snapshot { - let captures = CAPTURES.lock().unwrap(); - captures - .get(&monitor) - .into_iter() - .flatten() - .filter_map(Weak::upgrade) - .map(|state| { - let state = state.lock().unwrap(); - (state.updated, state.snapshot.clone()) - }) - .max_by_key(|(updated, _)| *updated) - .map(|(_, snapshot)| snapshot) - .unwrap_or(Snapshot::Unavailable) -} - -pub fn shape(id: u64) -> Option> { - CAPTURES - .lock() - .unwrap() - .values() - .flatten() - .filter_map(Weak::upgrade) - .find_map(|state| match &state.lock().unwrap().snapshot { - Snapshot::Ready(shape) if shape.id == id => Some(shape.clone()), - _ => None, - }) -} - -pub(super) struct Capture { - monitor: usize, - state: SharedState, -} - -impl Capture { - pub fn new(monitor: usize) -> Self { - let state = Arc::new(Mutex::new(State { - updated: Instant::now(), - snapshot: Snapshot::Pending, - })); - let capture = Self { monitor, state }; - capture.activate(); - capture - } - - pub fn activate(&self) { - let mut captures = CAPTURES.lock().unwrap(); - let states = captures.entry(self.monitor).or_default(); - let own = Arc::downgrade(&self.state); - if !states.iter().any(|state| state.ptr_eq(&own)) { - states.push(own); - } - } - - pub fn deactivate(&self) { - let mut captures = CAPTURES.lock().unwrap(); - if let Some(states) = captures.get_mut(&self.monitor) { - let own = Arc::downgrade(&self.state); - states.retain(|state| !state.ptr_eq(&own)); - if states.is_empty() { - captures.remove(&self.monitor); - } - } - } - - pub unsafe fn update( - &self, - duplication: *mut IDXGIOutputDuplication, - frame: &DXGI_OUTDUPL_FRAME_INFO, - ) { - let snapshot = match self.read(duplication, frame) { - Ok(Some(shape)) => Snapshot::Ready(Arc::new(shape)), - Ok(None) => return, - Err(error) => { - hbb_common::log::error!("DXGI cursor capture failed: {error}"); - Snapshot::Failed(error.to_string()) - } - }; - *self.state.lock().unwrap() = State { - updated: Instant::now(), - snapshot, - }; - } - - unsafe fn read( - &self, - duplication: *mut IDXGIOutputDuplication, - frame: &DXGI_OUTDUPL_FRAME_INFO, - ) -> io::Result> { - use winapi::um::shellscalingapi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI}; - // The Flutter runner is per-monitor DPI aware, so this is the output DPI. - let (mut x, mut y) = (0, 0); - wrap_hresult(GetDpiForMonitor( - self.monitor as _, - MDT_EFFECTIVE_DPI, - &mut x, - &mut y, - ))?; - if x == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Invalid monitor scale", - )); - } - let scale = x as f64 / DEFAULT_DPI; - if frame.PointerShapeBufferSize > 0 { - return read(duplication, frame.PointerShapeBufferSize) - .map(|shape| Some(shape.with_scale(scale))); - } - // A DPI change can leave a custom cursor's physical bitmap unchanged. - Ok(match &self.state.lock().unwrap().snapshot { - Snapshot::Ready(shape) if shape.scale != scale => { - Some(shape.as_ref().clone().with_scale(scale)) - } - _ => None, - }) - } -} - -unsafe fn read(duplication: *mut IDXGIOutputDuplication, size: u32) -> io::Result { - let mut pixels = vec![0; size as usize]; - let mut required = 0; - let mut info = std::mem::zeroed(); - wrap_hresult((*duplication).GetFramePointerShape( - size, - pixels.as_mut_ptr().cast(), - &mut required, - &mut info, - ))?; - if required > size { - return Err(invalid_shape()); - } - pixels.truncate(required as usize); - Shape::new(info, pixels) -} - -impl Drop for Capture { - fn drop(&mut self) { - self.deactivate(); - } -} - -#[cfg(test)] -mod tests; diff --git a/libs/scrap/src/dxgi/cursor/tests.rs b/libs/scrap/src/dxgi/cursor/tests.rs deleted file mode 100644 index 4e6474b4e..000000000 --- a/libs/scrap/src/dxgi/cursor/tests.rs +++ /dev/null @@ -1,47 +0,0 @@ -use super::*; -use winapi::shared::windef::POINT; - -#[test] -fn cursor_state_follows_capture_lifetime_and_gdi_switches() { - const MONITOR: usize = usize::MAX; - let first = Capture::new(MONITOR); - let second = Capture::new(MONITOR); - first.deactivate(); - assert!(matches!(snapshot(MONITOR), Snapshot::Pending)); - drop(second); - assert!(matches!(snapshot(MONITOR), Snapshot::Unavailable)); - first.activate(); - first.activate(); - assert!(matches!(snapshot(MONITOR), Snapshot::Pending)); - drop(first); - assert!(matches!(snapshot(MONITOR), Snapshot::Unavailable)); -} - -#[test] -fn cursor_keeps_physical_hotspot_and_both_monochrome_planes() { - let info = DXGI_OUTDUPL_POINTER_SHAPE_INFO { - Type: DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME, - Width: 64, - Height: 128, - Pitch: 8, - HotSpot: POINT { x: 31, y: 29 }, - }; - let pixels = vec![0xa5; 1024]; - let shape = Shape::new(info, pixels.clone()).unwrap().with_scale(1.0); - assert_eq!( - (shape.width, shape.height, shape.hotspot), - (64, 64, (31, 29)) - ); - assert_eq!(shape.pixels, pixels); - let scaled = shape.clone().with_scale(2.0); - assert_eq!(scaled.scale, 2.0); - assert_ne!(shape.id, scaled.id); - assert_eq!(shape.id, scaled.with_scale(1.0).id); - assert!(Shape::new(info, vec![0; 512]).is_err()); - let mut changed = info; - changed.HotSpot.y += 1; - assert_ne!( - shape.id, - Shape::new(changed, pixels).unwrap().with_scale(1.0).id - ); -} diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 90feb0abb..1f5296954 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -2,8 +2,6 @@ use std::{io, mem, ptr, slice}; pub mod gdi; pub use gdi::CapturerGDI; pub mod mag; -#[cfg(feature = "cursor")] -pub mod cursor; use winapi::{ shared::{ @@ -44,8 +42,6 @@ impl Drop for ComPtr { } pub struct Capturer { - #[cfg(feature = "cursor")] - cursor: Option, device: ComPtr, display: Display, context: ComPtr, @@ -162,9 +158,6 @@ impl Capturer { let rotate = Self::create_rotations(device.0, context.0, &display); Ok(Capturer { - #[cfg(feature = "cursor")] - cursor: (!duplication.is_null()) - .then(|| cursor::Capture::new(display.hmonitor() as usize)), device, context, duplication: ComPtr(duplication), @@ -323,25 +316,12 @@ impl Capturer { pub fn set_gdi(&mut self) -> bool { self.gdi_capturer = self.display.create_gdi(); - #[cfg(feature = "cursor")] - if self.is_gdi() { - if let Some(cursor) = &self.cursor { - cursor.deactivate(); - } - } self.is_gdi() } pub fn cancel_gdi(&mut self) { self.gdi_buffer = Vec::new(); self.gdi_capturer.take(); - #[cfg(feature = "cursor")] - if !self.duplication.is_null() { - let monitor = self.display.hmonitor() as usize; - self.cursor - .get_or_insert_with(|| cursor::Capture::new(monitor)) - .activate(); - } } #[cfg(feature = "vram")] @@ -356,10 +336,6 @@ impl Capturer { wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?; let frame = ComPtr(frame); - #[cfg(feature = "cursor")] - if let Some(cursor) = &self.cursor { - cursor.update(self.duplication.0, &info); - } if *info.LastPresentTime.QuadPart() == 0 { return Err(std::io::ErrorKind::WouldBlock.into()); @@ -503,10 +479,6 @@ impl Capturer { wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?; let frame = ComPtr(frame); - #[cfg(feature = "cursor")] - if let Some(cursor) = &self.cursor { - cursor.update(self.duplication.0, &info); - } if info.AccumulatedFrames == 0 || *info.LastPresentTime.QuadPart() == 0 { return Err(std::io::ErrorKind::WouldBlock.into()); diff --git a/src/platform/windows.rs b/src/platform/windows.rs index fb17fcb38..8b4143fdf 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -97,8 +97,6 @@ use windows_service::{ use winreg::{enums::*, RegKey}; mod acl; -#[cfg(feature = "flutter")] -mod cursor; mod installer_handoff; mod installer_shell; mod msi_registry; @@ -217,14 +215,7 @@ pub fn get_cursor() -> ResultType> { if ci.flags & CURSOR_SHOWING == 0 { Ok(None) } else { - #[cfg(feature = "flutter")] - { - cursor::current(&ci) - } - #[cfg(not(feature = "flutter"))] - { - Ok(Some(ci.hCursor as _)) - } + Ok(Some(ci.hCursor as _)) } } } @@ -269,10 +260,6 @@ impl Drop for IconInfo { // https://github.com/TurboVNC/tightvnc/blob/a235bae328c12fd1c3aed6f3f034a37a6ffbbd22/vnc_winsrc/winvnc/vncEncoder.cpp // https://github.com/TigerVNC/tigervnc/blob/master/win/rfb_win32/DeviceFrameBuffer.cxx pub fn get_cursor_data(hcursor: u64) -> ResultType { - #[cfg(feature = "flutter")] - if let Some(data) = cursor::data(hcursor)? { - return Ok(data); - } unsafe { let mut ii = IconInfo::new(hcursor as _)?; let bm_mask = get_bitmap(ii.0.hbmMask)?; diff --git a/src/platform/windows/cursor.rs b/src/platform/windows/cursor.rs deleted file mode 100644 index becf08b51..000000000 --- a/src/platform/windows/cursor.rs +++ /dev/null @@ -1,157 +0,0 @@ -use super::{drawOutline, handleMask, CursorData}; -use hbb_common::{anyhow::Context, bail, ResultType}; -use scrap::dxgi::cursor::{self, Shape, Snapshot, CURSOR_ID_FLAG}; -use winapi::{ - shared::dxgi1_2::{ - DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR, DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME, - }, - um::winuser::{MonitorFromPoint, CURSORINFO, MONITOR_DEFAULTTONULL}, -}; - -const CHANNELS: usize = 4; -const BORDER: i32 = 1; - -pub(super) fn current(info: &CURSORINFO) -> ResultType> { - let monitor = unsafe { MonitorFromPoint(info.ptScreenPos, MONITOR_DEFAULTTONULL) }; - match cursor::snapshot(monitor as usize) { - Snapshot::Unavailable => Ok(Some(info.hCursor as usize as u32 as u64)), - Snapshot::Pending => Ok(None), - Snapshot::Ready(shape) => Ok(Some(shape.id)), - Snapshot::Failed(error) => bail!("DXGI cursor capture: {error}"), - } -} - -pub(super) fn data(id: u64) -> ResultType> { - if id & CURSOR_ID_FLAG == 0 { - return Ok(None); - } - let shape = cursor::shape(id).context("DXGI cursor changed before export")?; - let (colors, outline) = colors(&shape)?; - let data = CursorData { - id, - colors: colors.into(), - width: shape.width as _, - height: shape.height as _, - hotx: shape.hotspot.0, - hoty: shape.hotspot.1, - scale: shape.scale, - ..Default::default() - }; - Ok(Some(if outline { outlined(data)? } else { data })) -} - -fn colors(shape: &Shape) -> ResultType<(Vec, bool)> { - let length = (shape.width as usize) - .checked_mul(shape.height as usize) - .and_then(|pixels| pixels.checked_mul(CHANNELS)) - .context("Cursor size overflow")?; - if shape.kind == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME { - let mut colors = vec![0; length]; - let outline = unsafe { - handleMask( - colors.as_mut_ptr(), - shape.pixels.as_ptr(), - shape.width as _, - shape.height as _, - shape.pitch as _, - (shape.height * 2) as _, - ) - } > 0; - return Ok((colors, outline)); - } - let mut colors = Vec::with_capacity(length); - let mut outline = false; - for row in shape.pixels.chunks_exact(shape.pitch as usize) { - for pixel in row[..shape.width as usize * CHANNELS].chunks_exact(CHANNELS) { - let (rgba, xor) = rgba(pixel, shape.kind); - outline |= xor; - colors.extend_from_slice(&rgba); - } - } - Ok((colors, outline)) -} - -fn rgba(pixel: &[u8], kind: u32) -> ([u8; CHANNELS], bool) { - if kind != DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR { - return ([pixel[2], pixel[1], pixel[0], pixel[3]], false); - } - if pixel[3] == 0 { - return ([pixel[2], pixel[1], pixel[0], 255], false); - } - // Match the Win32 exporter's outlined replacement for background-dependent XOR. - if pixel[..3].iter().any(|value| *value != 0) { - ([0, 0, 0, 255], true) - } else { - ([0; CHANNELS], false) - } -} - -fn outlined(data: CursorData) -> ResultType { - let width = data - .width - .checked_add(BORDER * 2) - .context("Cursor width overflow")?; - let height = data - .height - .checked_add(BORDER * 2) - .context("Cursor height overflow")?; - let length = (width as usize) - .checked_mul(height as usize) - .and_then(|pixels| pixels.checked_mul(CHANNELS)) - .context("Cursor size overflow")?; - let length_i32 = - i32::try_from(length).context("Cursor outline exceeds the native buffer size")?; - let mut colors = vec![0; length]; - unsafe { - drawOutline( - colors.as_mut_ptr(), - data.colors.as_ptr(), - data.width, - data.height, - length_i32, - ); - } - Ok(CursorData { - colors: colors.into(), - width, - height, - hotx: data.hotx + BORDER, - hoty: data.hoty + BORDER, - ..data - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use winapi::shared::dxgi1_2::DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR; - - #[test] - fn physical_cursor_preserves_alpha_and_ignores_row_padding() { - let shape = Shape { - id: CURSOR_ID_FLAG, - scale: 0.0, - kind: DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR, - width: 1, - height: 2, - pitch: 8, - hotspot: (0, 1), - pixels: vec![ - 32, 64, 128, 128, 255, 255, 255, 255, 1, 2, 3, 255, 255, 255, 255, 255, - ], - }; - assert_eq!( - colors(&shape).unwrap(), - (vec![128, 64, 32, 128, 3, 2, 1, 255], false) - ); - let masked = Shape { - kind: DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR, - pixels: vec![0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0], - ..shape - }; - assert_eq!( - colors(&masked).unwrap(), - (vec![0, 0, 0, 255, 0, 0, 0, 255], true) - ); - } -}