From 8da629c57de7baf97127448089100cd75e8cb939 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 12 Sep 2026 00:32:58 +0800 Subject: [PATCH] revert(cursor): return to f5b98b32 before splitting plugin changes --- Cargo.toml | 2 +- flutter/lib/desktop/pages/remote_page.dart | 105 ++----- flutter/lib/models/model.dart | 70 +---- flutter/lib/native/custom_cursor.dart | 39 +-- flutter/lib/utils/cursor_size.dart | 55 ---- flutter/lib/utils/image.dart | 8 +- flutter/lib/web/custom_cursor.dart | 8 +- flutter/linux/CMakeLists.txt | 2 - flutter/linux/cursor_size.h | 109 ------- flutter/linux/my_application.cc | 3 - flutter/macos/Runner/MainFlutterWindow.swift | 39 --- flutter/test/cursor_dpi_scale_test.dart | 196 ------------ flutter/test/cursor_local_size_test.dart | 292 ------------------ .../test/cursor_measurement_failure_test.dart | 181 ----------- flutter/test/cursor_paint_scale_test.dart | 187 ----------- flutter/test/cursor_resize_limit_test.dart | 43 --- flutter/test/cursor_web_test.dart | 64 ---- flutter/windows/runner/cursor_size.h | 146 --------- flutter/windows/runner/flutter_window.cpp | 5 - libs/base/protos/message.proto | 4 +- libs/scrap/Cargo.toml | 1 - libs/scrap/src/dxgi/cursor.rs | 248 --------------- libs/scrap/src/dxgi/mod.rs | 28 -- src/platform/macos.rs | 24 +- src/platform/macos/cursor.rs | 174 ----------- src/platform/windows.cc | 2 +- src/platform/windows.rs | 53 +--- src/platform/windows/cursor.rs | 155 ---------- src/server/drm_capturer.rs | 43 +-- src/server/drm_capturer/cursor.rs | 200 ------------ src/server/drm_capturer/cursor/ffi.rs | 232 -------------- src/server/drm_capturer/cursor/metadata.rs | 255 --------------- src/server/drm_capturer/cursor/pipewire.rs | 218 ------------- src/server/drm_capturer/cursor/tests.rs | 54 ---- 34 files changed, 68 insertions(+), 3177 deletions(-) delete mode 100644 flutter/lib/utils/cursor_size.dart delete mode 100644 flutter/linux/cursor_size.h delete mode 100644 flutter/test/cursor_dpi_scale_test.dart delete mode 100644 flutter/test/cursor_local_size_test.dart delete mode 100644 flutter/test/cursor_measurement_failure_test.dart delete mode 100644 flutter/test/cursor_paint_scale_test.dart delete mode 100644 flutter/test/cursor_resize_limit_test.dart delete mode 100644 flutter/test/cursor_web_test.dart delete mode 100644 flutter/windows/runner/cursor_size.h delete mode 100644 libs/scrap/src/dxgi/cursor.rs delete mode 100644 src/platform/macos/cursor.rs delete mode 100644 src/platform/windows/cursor.rs delete mode 100644 src/server/drm_capturer/cursor.rs delete mode 100644 src/server/drm_capturer/cursor/ffi.rs delete mode 100644 src/server/drm_capturer/cursor/metadata.rs delete mode 100644 src/server/drm_capturer/cursor/pipewire.rs delete mode 100644 src/server/drm_capturer/cursor/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 8f254438e..00bb88fae 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"] +flutter = ["flutter_rust_bridge"] default = ["use_dasp"] hwcodec = ["scrap/hwcodec"] vram = ["scrap/vram"] diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index b6c334982..78612f48e 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -19,7 +19,6 @@ import '../../models/input_model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import '../../utils/image.dart'; -import '../../utils/cursor_size.dart'; import '../widgets/remote_toolbar.dart'; import '../widgets/kb_layout_type_chooser.dart'; import '../widgets/tabbar_widget.dart'; @@ -1089,21 +1088,6 @@ class ImagePaint extends StatefulWidget { class _ImagePaintState extends State { bool _lastRemoteCursorMoved = false; - final _localCursorSize = LocalCursorSize(); - - @override - void initState() { - super.initState(); - _localCursorSize.addListener(_cursorSizeChanged); - } - - void _cursorSizeChanged() => setState(() {}); - - @override - void dispose() { - _localCursorSize.dispose(); - super.dispose(); - } String get id => widget.id; RxBool get zoomCursor => widget.zoomCursor; @@ -1121,29 +1105,22 @@ class _ImagePaintState extends State { // changes, so read it live to follow the window across monitors. final dpr = MediaQuery.devicePixelRatioOf(context); - bool isViewScaled() => - c.viewStyle.style == kRemoteViewStyleAdaptive || - c.viewStyle.style == kRemoteViewStyleCustom; + bool isViewAdaptive() => c.viewStyle.style == kRemoteViewStyleAdaptive; bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal; mouseRegion({child}) => Obx(() { - final useLocalSize = !isWeb && - (isLinux || isMacOS || isWindows) && - !zoomCursor.value && - isViewScaled(); - if (useLocalSize) _localCursorSize.ensureLoaded(dpr); double getCursorScale() { var c = Provider.of(context); var cursorScale = 1.0; if (isWindows) { // debug win10 - if (zoomCursor.value && isViewScaled()) { + if (zoomCursor.value && isViewAdaptive()) { cursorScale = s * c.devicePixelRatio; } } else { if (zoomCursor.value || isViewOriginal()) { cursorScale = s; - } else if (isLinux || isMacOS) { + } else { // NSCursor and GdkCursor treat the bitmap size as logical // pixels, so an unzoomed cursor must be shrunk by the DPR to // keep 1 remote px == 1 physical px, the size Original view @@ -1172,16 +1149,11 @@ class _ImagePaintState extends State { _firstEnterImage.value = true; } return _buildCustomCursor( - context, getCursorScale(), - useLocalSize: useLocalSize); + context, getCursorScale()); } }()) - : _buildDisabledCursor(context, getCursorScale(), - useLocalSize: useLocalSize) + : _buildDisabledCursor(context, getCursorScale()) : MouseCursor.defer, - onEnter: (_) { - if (useLocalSize) _localCursorSize.refresh(); - }, onHover: (evt) {}, child: child); }); @@ -1294,19 +1266,15 @@ class _ImagePaintState extends State { ); } - MouseCursor _buildCustomCursor(BuildContext context, double scale, - {bool useLocalSize = false}) { + MouseCursor _buildCustomCursor(BuildContext context, double scale) { final cursor = Provider.of(context); final cache = cursor.cache ?? preDefaultCursor.cache; - cache?.localSize = useLocalSize ? _localCursorSize.value : null; return buildCursorOfCache(cursor, scale, cache); } - MouseCursor _buildDisabledCursor(BuildContext context, double scale, - {bool useLocalSize = false}) { + MouseCursor _buildDisabledCursor(BuildContext context, double scale) { final cursor = Provider.of(context); final cache = preForbiddenCursor.cache; - cache?.localSize = useLocalSize ? _localCursorSize.value : null; return buildCursorOfCache(cursor, scale, cache); } @@ -1425,9 +1393,8 @@ class CursorPaint extends StatelessWidget { } } - final imageOffset = _softwareImageOffset(c); - double cx = imageOffset?.dx ?? c.x; - double cy = imageOffset?.dy ?? c.y; + double cx = c.x; + double cy = c.y; if (c.viewStyle.style == kRemoteViewStyleOriginal && c.scrollStyle == ScrollStyle.scrollbar) { final rect = c.parent.target!.ffiModel.rect; @@ -1446,44 +1413,38 @@ class CursorPaint extends StatelessWidget { } } - final image = m.image ?? preDefaultCursor.image; - final nativePixels = isWindows ? MediaQuery.devicePixelRatioOf(context) : 1.0; - double scale = c.scale; - if (image != null && scale * nativePixels != 1.0) { - final sx = kMinCursorSize / (image.width * nativePixels); - final sy = kMinCursorSize / (image.height * nativePixels); - final minimumScale = sx > sy ? sx : sy; - if (scale < minimumScale) scale = minimumScale; + double x = m.x * c.scale + cx - hotx; + double y = m.y * c.scale + cy - hoty; + double scale = 1.0; + final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; + if (zoomCursor.value || isViewOriginal) { + x = m.x - hotx + cx / c.scale; + y = m.y - hoty + cy / c.scale; + scale = c.scale; + } else if (!isWindows) { + // Keep the painted cursor the same physical size as the native one + // built by getCursorScale() above, including its min-size clamp. + scale = 1.0 / MediaQuery.devicePixelRatioOf(context); + final image = m.image ?? preDefaultCursor.image; + if (scale != 1.0 && + image != null && + ((image.width * scale).toInt() < kMinCursorSize || + (image.height * scale).toInt() < kMinCursorSize)) { + final sw = kMinCursorSize / image.width; + final sh = kMinCursorSize / image.height; + scale = sw < sh ? sh : sw; + } + x = (m.x * c.scale + cx) / scale - hotx; + y = (m.y * c.scale + cy) / scale - hoty; } - final x = (m.x * c.scale + cx) / scale - hotx; - final y = (m.y * c.scale + cy) / scale - hoty; return CustomPaint( painter: ImagePainter( - image: image, + image: m.image ?? preDefaultCursor.image, x: x, y: y, scale: scale, - useIntegerPosition: false, ), ); } - - Offset? _softwareImageOffset(CanvasModel canvas) { - if (canvas.imageOverflow.isTrue && - canvas.scrollStyle != ScrollStyle.scrollauto) { - return null; - } - final ffi = canvas.parent.target!; - final peer = ffi.ffiModel; - if (ffi.imageModel.useTextureRender || peer.pi.forceTextureRender) { - return null; - } - var scale = canvas.scale; - final displays = peer.pi.getCurDisplays(); - if (peer.isPeerLinux && displays.isNotEmpty) scale /= displays[0].scale; - // Match the origin used by _buildScrollAutoNonTextureRender's ImagePainter. - return Offset( - (canvas.x / scale).toInt() * scale, (canvas.y / scale).toInt() * scale); - } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7ac4cf95c..76d04faf1 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -39,7 +39,6 @@ import 'package:vector_math/vector_math.dart' show Vector2; import '../common.dart'; import '../utils/image.dart' as img; -import '../utils/cursor_size.dart'; import '../common/widgets/dialog.dart'; import 'input_model.dart'; import 'platform_model.dart'; @@ -2864,8 +2863,6 @@ class CursorData { double hoty; final int width; final int height; - double? localSize; - late final int _visibleSize = cursorVisibleSize(image); CursorData({ required this.peerId, @@ -2882,34 +2879,9 @@ class CursorData { int _doubleToInt(double v) => (v * 10e6).round().toInt(); - bool get _usesLogicalCursorPixels => isLinux || isMacOS || isWeb; - int get scaledWidth => _scaledDimension(width, scale); - int get scaledHeight => _scaledDimension(height, scale); - - int _scaledDimension(int dimension, double scale) { - const minBitmapSize = 1; - final pixels = dimension * scale; - return _usesLogicalCursorPixels || localSize != null - ? max(minBitmapSize, pixels.round()) - : pixels.toInt(); - } - double _checkUpdateScale(double scale) { double oldScale = this.scale; - if (localSize != null) { - scale = _visibleSize == 0 - ? 1.0 - : max(localSize!, kMinCursorSize) / _visibleSize; - // Sparse peer artwork must not amplify the native bitmap allocation. - const maxNativeCursorBitmapSize = 512; - final maxScale = maxNativeCursorBitmapSize / max(width, height); - if (scale > maxScale) { - if (oldScale != maxScale) { - debugPrint('Cursor $id exceeds the native bitmap limit; reducing scale'); - } - scale = maxScale; - } - } else if (scale != 1.0) { + if (scale != 1.0) { // Update data if scale changed. final tgtWidth = (width * scale).toInt(); final tgtHeight = (height * scale).toInt(); @@ -2920,17 +2892,13 @@ class CursorData { } } - const bytesPerPixel = 4; - final byteLength = _scaledDimension(width, scale) * - _scaledDimension(height, scale) * bytesPerPixel; - if (_doubleToInt(oldScale) != _doubleToInt(scale) || - (isWindows && data != null && data!.length != byteLength)) { + if (_doubleToInt(oldScale) != _doubleToInt(scale)) { if (isWindows) { data = img2 .copyResize( image, - width: _scaledDimension(width, scale), - height: _scaledDimension(height, scale), + width: (width * scale).toInt(), + height: (height * scale).toInt(), interpolation: img2.Interpolation.average, ) .getBytes(order: img2.ChannelOrder.bgra); @@ -2939,8 +2907,8 @@ class CursorData { img2.encodePng( img2.copyResize( image, - width: _scaledDimension(width, scale), - height: _scaledDimension(height, scale), + width: (width * scale).toInt(), + height: (height * scale).toInt(), interpolation: img2.Interpolation.average, ), ), @@ -2949,14 +2917,14 @@ class CursorData { } this.scale = scale; - hotx = hotxOrigin * scaledWidth / width; - hoty = hotyOrigin * scaledHeight / height; + hotx = hotxOrigin * scale; + hoty = hotyOrigin * scale; return scale; } String updateGetKey(double scale) { scale = _checkUpdateScale(scale); - return '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}${localSize == null ? '' : '_local'}'; + return '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}'; } } @@ -3470,8 +3438,6 @@ class CursorModel with ChangeNotifier { if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) { _images[id]?.item1.dispose(); _images[id] = Tuple3(image, hotx, hoty); - } else { - image.dispose(); } // Update last cursor data. @@ -3492,32 +3458,14 @@ class CursorModel with ChangeNotifier { img2.Image imgOrigin = img2.Image.fromBytes( width: w, height: h, bytes: rgba.buffer, order: img2.ChannelOrder.rgba); if (isWindows) { - final pixels = - await image.toByteData(format: ui.ImageByteFormat.rawStraightRgba); - if (pixels == null) { - debugPrint('Could not read straight-alpha cursor pixels: $id'); - return false; - } - imgOrigin = img2.Image.fromBytes( - width: w, height: h, bytes: pixels.buffer, order: img2.ChannelOrder.rgba); data = imgOrigin.getBytes(order: img2.ChannelOrder.bgra); } else { ByteData? imgBytes = await image.toByteData(format: ui.ImageByteFormat.png); if (imgBytes == null) { - debugPrint('Could not encode cursor PNG: $id'); return false; } data = imgBytes.buffer.asUint8List(); - if (isLinux || isMacOS) { - // Preserve the PNG's straight-alpha colors when resizing native cursors. - final decoded = img2.decodePng(data); - if (decoded == null) { - debugPrint('Invalid native cursor PNG: $id'); - return false; - } - imgOrigin = decoded; - } } final cache = CursorData( peerId: peerId, diff --git a/flutter/lib/native/custom_cursor.dart b/flutter/lib/native/custom_cursor.dart index af73f2694..e85d42a55 100644 --- a/flutter/lib/native/custom_cursor.dart +++ b/flutter/lib/native/custom_cursor.dart @@ -1,39 +1,16 @@ -import 'dart:math'; - import 'package:flutter_custom_cursor/cursor_manager.dart' as custom_cursor_manager; import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:image/image.dart' as img; -import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/model.dart'; deleteCustomCursor(String key) => custom_cursor_manager.CursorManager.instance.deleteCursor(key); resetSystemCursor() {} -double _nativeHotspot(double hotspot, int bitmapSize) { - if (!isLinux && !isWindows) return hotspot; - // GDK and Win32 take integer hotspots inside the bitmap; use the nearest pixel. - return min(hotspot.round(), bitmapSize - 1).toDouble(); -} - -Uint8List _padCursorWidth(Uint8List data, int width) { - final bitmap = img.decodePng(data); - if (bitmap == null) { - throw const FormatException('Invalid native cursor PNG'); - } - final padded = img.copyExpandCanvas(bitmap, - newWidth: width, - newHeight: bitmap.height, - position: img.ExpandCanvasPosition.topLeft, - toImage: img.Image(width: width, height: bitmap.height, numChannels: 4)); - return Uint8List.fromList(img.encodePng(padded)); -} - MouseCursor buildCursorOfCache( CursorModel cursor, double scale, CursorData? cache) { if (cache == null) { @@ -46,12 +23,6 @@ MouseCursor buildCursorOfCache( if (data == null) { return MouseCursor.defer; } - // Pad tall Linux buffers to prevent clipping in the hardware cursor plane. - final width = isLinux - ? max(cache.scaledWidth, cache.scaledHeight) - : cache.scaledWidth; - final buffer = - width == cache.scaledWidth ? data : _padCursorWidth(data, width); debugPrint( "Register custom cursor with key $key (${cache.hotx},${cache.hoty})"); // [Safety] @@ -61,11 +32,11 @@ MouseCursor buildCursorOfCache( custom_cursor_manager.CursorManager.instance .registerCursor(custom_cursor_manager.CursorData() ..name = key - ..buffer = buffer - ..width = width - ..height = cache.scaledHeight - ..hotX = _nativeHotspot(cache.hotx, cache.scaledWidth) - ..hotY = _nativeHotspot(cache.hoty, cache.scaledHeight)); + ..buffer = data + ..width = (cache.width * cache.scale).toInt() + ..height = (cache.height * cache.scale).toInt() + ..hotX = cache.hotx + ..hotY = cache.hoty); cursor.addKey(key); } return FlutterCustomMemoryImageCursor(key: key); diff --git a/flutter/lib/utils/cursor_size.dart b/flutter/lib/utils/cursor_size.dart deleted file mode 100644 index 9c04d1bd4..000000000 --- a/flutter/lib/utils/cursor_size.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:image/image.dart' as img; - -int cursorVisibleSize(img.Image image) { - var left = image.width; - var top = image.height; - var right = -1; - var bottom = -1; - for (final pixel in image) { - if (pixel.a == 0) continue; - if (pixel.x < left) left = pixel.x; - if (pixel.x > right) right = pixel.x; - if (pixel.y < top) top = pixel.y; - if (pixel.y > bottom) bottom = pixel.y; - } - if (right < left) return 0; - final width = right - left + 1; - final height = bottom - top + 1; - return width > height ? width : height; -} - -class LocalCursorSize extends ValueNotifier { - LocalCursorSize() : super(null); - - static const _channel = MethodChannel('org.rustdesk.rustdesk/cursor'); - double? _devicePixelRatio; - int _generation = 0; - - void ensureLoaded(double devicePixelRatio) { - if (_devicePixelRatio == devicePixelRatio) return; - _devicePixelRatio = devicePixelRatio; - refresh(); - } - - Future refresh() async { - final generation = ++_generation; - try { - final size = await _channel.invokeMethod('getSystemCursorSize'); - if (size == null || !size.isFinite || size <= 0) { - throw const FormatException('Invalid local system cursor size'); - } - if (generation == _generation) value = size; - } catch (error, stack) { - FlutterError.reportError(FlutterErrorDetails( - exception: error, stack: stack, library: 'local cursor size')); - } - } - - @override - void dispose() { - ++_generation; - super.dispose(); - } -} diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart index 4e2fe1e74..d5042e61a 100644 --- a/flutter/lib/utils/image.dart +++ b/flutter/lib/utils/image.dart @@ -96,14 +96,12 @@ class ImagePainter extends CustomPainter { required this.x, required this.y, required this.scale, - this.useIntegerPosition = true, }); ui.Image? image; double x; double y; double scale; - final bool useIntegerPosition; @override void paint(Canvas canvas, Size size) { @@ -124,10 +122,8 @@ class ImagePainter extends CustomPainter { if (isWeb) { paint.filterQuality = FilterQuality.high; } - final position = useIntegerPosition - ? Offset(x.toInt().toDouble(), y.toInt().toDouble()) - : Offset(x, y); - canvas.drawImage(image!, position, paint); + canvas.drawImage( + image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint); } @override diff --git a/flutter/lib/web/custom_cursor.dart b/flutter/lib/web/custom_cursor.dart index cc1ed081a..54df77e98 100644 --- a/flutter/lib/web/custom_cursor.dart +++ b/flutter/lib/web/custom_cursor.dart @@ -52,8 +52,8 @@ class CursorManager { 'cursor', jsonEncode({ 'url': cursorData.url, - 'hotx': cursorData.hotX.round(), - 'hoty': cursorData.hotY.round(), + 'hotx': cursorData.hotX.toInt(), + 'hoty': cursorData.hotY.toInt(), }) ]); } @@ -116,8 +116,8 @@ MouseCursor buildCursorOfCache( CursorManager.instance.registerCursor(CursorData( key: key, url: 'data:image/rgba;base64,${base64Encode(data)}', - width: cache.scaledWidth, - height: cache.scaledHeight, + width: (cache.width * cache.scale).toInt(), + height: (cache.height * cache.scale).toInt(), hotX: cache.hotx, hotY: cache.hoty)); cursor.addKey(key); diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index ed12b41b2..56a8dbb70 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -53,7 +53,6 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(XCURSOR REQUIRED IMPORTED_TARGET xcursor) # Wayland protocol for keyboard shortcuts inhibit pkg_check_modules(WAYLAND_CLIENT IMPORTED_TARGET wayland-client) @@ -127,7 +126,6 @@ apply_standard_settings(${BINARY_NAME}) # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::XCURSOR) target_link_libraries(${BINARY_NAME} PRIVATE ${CMAKE_DL_LIBS}) # target_link_libraries(${BINARY_NAME} PRIVATE librustdesk) diff --git a/flutter/linux/cursor_size.h b/flutter/linux/cursor_size.h deleted file mode 100644 index 9100b4257..000000000 --- a/flutter/linux/cursor_size.h +++ /dev/null @@ -1,109 +0,0 @@ -#ifndef RUSTDESK_CURSOR_SIZE_H_ -#define RUSTDESK_CURSOR_SIZE_H_ - -#include -#include -#ifdef GDK_WINDOWING_WAYLAND -#include -#endif - -#include -#include -#include - -namespace cursor_size { - -template -double VisibleSize(int width, int height, IsVisible visible) { - int left = width, top = height, right = -1, bottom = -1; - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { - if (!visible(x, y)) continue; - left = std::min(left, x); - right = std::max(right, x); - top = std::min(top, y); - bottom = std::max(bottom, y); - } - } - if (right < left) throw std::runtime_error("System cursor has no visible pixels"); - return std::max(right - left + 1, bottom - top + 1); -} - -inline double WaylandSize(GtkWidget* view) { - g_autofree gchar* theme = nullptr; - gint size = 0; - g_object_get(gtk_widget_get_settings(view), "gtk-cursor-theme-name", &theme, - "gtk-cursor-theme-size", &size, nullptr); - // GTK uses 24 when the theme size is unset, and loads at the window scale. - constexpr int kDefaultThemeSize = 24; - if (size == 0) size = kDefaultThemeSize; - const int scale = gtk_widget_get_scale_factor(view); - if (size < 0 || scale <= 0 || size > G_MAXINT / scale) { - throw std::runtime_error("Invalid GTK cursor theme size or window scale"); - } - auto* loaded = XcursorLibraryLoadImage("default", theme, size * scale); - // Match GTK's CSS default -> traditional left_ptr name mapping. - if (!loaded) loaded = XcursorLibraryLoadImage("left_ptr", theme, size * scale); - std::unique_ptr image( - loaded, XcursorImageDestroy); - if (!image) throw std::runtime_error("Could not load the GTK system cursor"); - // GTK reduces the buffer scale until both theme dimensions are divisible. - // For example, a 64px theme at 3x is displayed using a buffer scale of 2. - int cursor_scale = scale; - while (image->width % cursor_scale != 0 || image->height % cursor_scale != 0) { - --cursor_scale; - } - constexpr XcursorPixel kAlphaMask = 0xff000000; - return VisibleSize(image->width, image->height, [&](int x, int y) { - return (image->pixels[y * image->width + x] & kAlphaMask) != 0; - }) / cursor_scale; -} - -inline double SystemSize(GtkWidget* view) { - GdkDisplay* display = gtk_widget_get_display(view); -#ifdef GDK_WINDOWING_WAYLAND - if (GDK_IS_WAYLAND_DISPLAY(display)) return WaylandSize(view); -#endif - g_autoptr(GdkCursor) cursor = gdk_cursor_new_from_name(display, "default"); - if (!cursor) throw std::runtime_error("Could not load the GDK system cursor"); - g_autoptr(GdkPixbuf) image = gdk_cursor_get_image(cursor); - if (!image) throw std::runtime_error("Could not read the GDK system cursor"); - const auto* pixels = gdk_pixbuf_read_pixels(image); - const int stride = gdk_pixbuf_get_rowstride(image); - const int channels = gdk_pixbuf_get_n_channels(image); - return VisibleSize(gdk_pixbuf_get_width(image), gdk_pixbuf_get_height(image), - [&](int x, int y) { - return !gdk_pixbuf_get_has_alpha(image) || - pixels[y * stride + x * channels + channels - 1] != 0; - }); -} - -inline void Register(FlView* view) { - g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); - FlMethodChannel* channel = fl_method_channel_new( - fl_engine_get_binary_messenger(fl_view_get_engine(view)), - "org.rustdesk.rustdesk/cursor", FL_METHOD_CODEC(codec)); - fl_method_channel_set_method_call_handler(channel, - [](FlMethodChannel*, FlMethodCall* call, gpointer data) { - g_autoptr(GError) error = nullptr; - if (g_strcmp0(fl_method_call_get_name(call), "getSystemCursorSize") != 0) { - fl_method_call_respond_not_implemented(call, &error); - } else { - try { - g_autoptr(FlValue) value = fl_value_new_float(SystemSize(GTK_WIDGET(data))); - fl_method_call_respond_success(call, value, &error); - } catch (const std::exception& failure) { - fl_method_call_respond_error(call, "cursor_size", failure.what(), nullptr, &error); - } - } - if (error) g_warning("Cursor size response failed: %s", error->message); - }, view, nullptr); - g_object_set_data_full(G_OBJECT(view), "cursor-size-channel", channel, [](gpointer data) { - fl_method_channel_set_method_call_handler(FL_METHOD_CHANNEL(data), nullptr, nullptr, nullptr); - g_object_unref(data); - }); -} - -} // namespace cursor_size - -#endif // RUSTDESK_CURSOR_SIZE_H_ diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 292f94c9a..210adba96 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -1,7 +1,6 @@ #include "my_application.h" #include "bump_mouse.h" -#include "cursor_size.h" #include #ifdef GDK_WINDOWING_X11 @@ -95,7 +94,6 @@ static void on_subwindow_created(FlPluginRegistry* registry) { // Set up side button forwarding for sub-windows. if (registry == NULL || !FL_IS_VIEW(registry)) return; FlView* view = FL_VIEW(registry); - cursor_size::Register(view); GtkWidget* toplevel = gtk_widget_get_toplevel(GTK_WIDGET(view)); if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) { FlMethodChannel* channel = side_buttons_create_channel(fl_view_get_engine(view)); @@ -180,7 +178,6 @@ static void my_application_activate(GApplication* application) { (WindowCreatedCallback)on_subwindow_created); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - cursor_size::Register(view); g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); self->host_channel = fl_method_channel_new( diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 8e379e8a6..336d94f1d 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -198,46 +198,7 @@ class MainFlutterWindow: NSWindow { } } - private func registerCursorSizeChannel(registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/cursor", binaryMessenger: registrar.messenger) - channel.setMethodCallHandler { call, result in - guard call.method == "getSystemCursorSize" else { - result(FlutterMethodNotImplemented) - return - } - guard let size = self.systemCursorSize() else { - result(FlutterError(code: "cursor_size", message: "Could not measure the local system cursor", details: nil)) - return - } - result(size) - } - } - - private func systemCursorSize() -> Double? { - let image = NSCursor.arrow.image - guard let data = image.tiffRepresentation, - let bitmap = NSBitmapImageRep(data: data), - bitmap.pixelsWide > 0, bitmap.pixelsHigh > 0 else { return nil } - var left = bitmap.pixelsWide, top = bitmap.pixelsHigh - var right = -1, bottom = -1 - for y in 0..= left else { return nil } - let width = Double(right - left + 1) * image.size.width / Double(bitmap.pixelsWide) - let height = Double(bottom - top + 1) * image.size.height / Double(bitmap.pixelsHigh) - return max(width, height) - } - public func setMethodHandler(registrar: FlutterPluginRegistrar) { - registerCursorSizeChannel(registrar: registrar) let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/host", binaryMessenger: registrar.messenger) channel.setMethodCallHandler({ (call, result) -> Void in diff --git a/flutter/test/cursor_dpi_scale_test.dart b/flutter/test/cursor_dpi_scale_test.dart deleted file mode 100644 index 0bbb475ff..000000000 --- a/flutter/test/cursor_dpi_scale_test.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/services.dart'; -import 'package:flutter_hbb/models/model.dart'; -import 'package:flutter_hbb/native/custom_cursor.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:image/image.dart' as img; - -// Source size, source hotspot, requested/effective scale, artwork size, integer hotspot. -final _cases = [ - ((17, 23), (4.0, 4.0), 1 / 3, 12 / 17, (12, 16), (3.0, 3.0)), - ((32, 16), (16.0, 8.0), 0.5, 0.75, (24, 12), (12.0, 6.0)), - ((34, 46), (8.0, 8.0), 0.5, 0.5, (17, 23), (4.0, 4.0)), - ((24, 24), (11.0, 11.0), 1 / 3, 0.5, (12, 12), (6.0, 6.0)), - ((1, 48), (0.0, 24.0), 1.0, 1.0, (1, 48), (0.0, 24.0)), - ((9, 18), (4.0, 9.0), 1.0, 1.0, (9, 18), (4.0, 9.0)), - ((24, 24), (4.0, 4.0), 0.1, 0.5, (12, 12), (2.0, 2.0)), - ( - (19, 27), - (6.0, 13.0), - 1.37, - 1.37, - Platform.isWindows ? (26, 36) : (26, 37), - Platform.isWindows ? (8.0, 17.0) : (8.0, 18.0) - ), - ( - (18, 36), - (8.0, 18.0), - 0.75, - 0.75, - Platform.isWindows ? (13, 27) : (14, 27), - (6.0, 14.0) - ), -]; -const _hotspotTolerance = 1e-9; - -class _CursorModel implements CursorModel { - @override - final Set cachedKeys = {}; - - @override - void addKey(String key) => cachedKeys.add(key); - - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - -class _CursorFFI implements FFI { - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - -CursorData _cursorData((int, int) size, (double, double) hotspot) { - final image = img.Image(width: size.$1, height: size.$2, numChannels: 4); - for (var y = 0; y < image.height; y++) { - image.setPixelRgba(image.width ~/ 2, y, 255, 255, 255, 255); - } - for (var x = 0; x < image.width; x++) { - image.setPixelRgba(x, 0, 255, 0, 0, 255); - image.setPixelRgba(x, image.height - 1, 0, 0, 255, 255); - } - return CursorData( - peerId: 'cursor-test', - id: 'native', - image: image, - scale: 1, - data: Platform.isWindows - ? image.getBytes(order: img.ChannelOrder.bgra) - : Uint8List.fromList(img.encodePng(image)), - hotxOrigin: hotspot.$1, - hotyOrigin: hotspot.$2, - width: size.$1, - height: size.$2, - ); -} - -Future> _register( - WidgetTester tester, CursorData data, double scale) async { - final channel = Platform.isWindows - ? SystemChannels.mouseCursor - : const MethodChannel('flutter_custom_cursor'); - Map? registered; - tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, - (call) async { - expect( - call.method, - Platform.isWindows - ? 'createCustomCursor/windows' - : 'createCustomCursor'); - registered = call.arguments as Map; - return registered!['name']; - }); - addTearDown(() => tester.binding.defaultBinaryMessenger - .setMockMethodCallHandler(channel, null)); - buildCursorOfCache(_CursorModel(), scale, data); - await tester.pump(); - expect(registered, isNotNull); - return registered!; -} - -void _expectArtwork(img.Image native, img.Image artwork) { - final content = img.copyCrop(native, - x: 0, y: 0, width: artwork.width, height: artwork.height); - expect(content.getBytes(), artwork.getBytes()); - for (final pixel in native) { - if (pixel.x >= artwork.width) { - expect(pixel.a, 0, reason: 'Cursor padding must be transparent'); - } - } -} - -void main() { - testWidgets('received cursor keeps edge colors across scale changes', - (tester) async { - const side = 4; - for (final (rgba, expected) in [ - ([128, 64, 32, 128], [255, 128, 64, 128]), - ([0, 0, 0, 0], [0, 0, 0, 0]), - ([32, 64, 128, 255], [32, 64, 128, 255]), - ]) { - final cursor = CursorModel(WeakReference(_CursorFFI())) - ..id = 'edge-colors'; - addTearDown(cursor.disposeImages); - addTearDown(cursor.dispose); - await tester.runAsync(() => cursor.updateCursorData({ - 'id': 'edge-colors', - 'hotx': '1', - 'hoty': '1', - 'width': '$side', - 'height': '$side', - 'colors': jsonEncode(List.generate(side * side, (_) => rgba) - .expand((pixel) => pixel) - .toList()), - })); - final data = cursor.cache!; - for (final scale in [1.0, 0.5, 1.0]) { - data.updateGetKey(scale); - final image = Platform.isWindows - ? img.Image.fromBytes( - width: data.scaledWidth, - height: data.scaledHeight, - bytes: data.data!.buffer, - order: img.ChannelOrder.bgra) - : img.decodePng(data.data!)!; - for (final pixel in image) { - expect([pixel.r, pixel.g, pixel.b, pixel.a], expected, - reason: 'Edge color must survive scale $scale'); - } - } - } - }); - - for (final (source, hotspot, scale, effectiveScale, size, integerHotspot) - in _cases) { - testWidgets('${source.$1}x${source.$2} cursor at scale $scale', - (tester) async { - final data = _cursorData(source, hotspot); - final cursor = await _register(tester, data, scale); - final artwork = Platform.isWindows - ? img.Image.fromBytes( - width: data.scaledWidth, - height: data.scaledHeight, - bytes: data.data!.buffer, - order: img.ChannelOrder.bgra) - : img.decodePng(data.data!)!; - final native = Platform.isWindows - ? img.Image.fromBytes( - width: cursor['width'], - height: cursor['height'], - bytes: (cursor['buffer'] as Uint8List).buffer, - bytesOffset: (cursor['buffer'] as Uint8List).offsetInBytes, - order: img.ChannelOrder.bgra) - : img.decodePng(cursor['buffer'] as Uint8List)!; - final width = Platform.isLinux && size.$2 > size.$1 ? size.$2 : size.$1; - - expect(data.scale, effectiveScale); - expect((artwork.width, artwork.height), size); - expect(data.hotx / size.$1, - closeTo(hotspot.$1 / source.$1, _hotspotTolerance)); - expect(data.hoty / size.$2, - closeTo(hotspot.$2 / source.$2, _hotspotTolerance)); - expect((native.width, native.height), (width, size.$2)); - expect((cursor['width'], cursor['height']), (width, size.$2)); - expect( - (cursor['hotX'], cursor['hotY']), - Platform.isLinux || Platform.isWindows - ? integerHotspot - : (data.hotx, data.hoty)); - _expectArtwork(native, artwork); - if (width == artwork.width) { - expect(cursor['buffer'], data.data); - } - }); - } -} diff --git a/flutter/test/cursor_local_size_test.dart b/flutter/test/cursor_local_size_test.dart deleted file mode 100644 index 354f1e5cd..000000000 --- a/flutter/test/cursor_local_size_test.dart +++ /dev/null @@ -1,292 +0,0 @@ -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_hbb/desktop/pages/remote_page.dart'; -import 'package:flutter_hbb/models/input_model.dart'; -import 'package:flutter_hbb/models/model.dart'; -import 'package:flutter_hbb/utils/cursor_size.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:get/get.dart'; -import 'package:image/image.dart' as img; -import 'package:provider/provider.dart'; - -const _viewport = Size(200, 160); - -class _Image extends ChangeNotifier implements ImageModel { - @override - bool get useTextureRender => false; - @override - ui.Image? get image => null; - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - -class _Canvas extends ChangeNotifier implements CanvasModel { - _Canvas(this.devicePixelRatio, {required this.style, required this.scale}); - - final String style; - - @override - final double devicePixelRatio; - @override - final imageOverflow = false.obs; - @override - late final viewStyle = ViewStyle( - style: style, - width: _viewport.width, - height: _viewport.height, - displayWidth: 400, - displayHeight: 320, - ); - @override - bool get cursorEmbedded => false; - @override - Size get size => _viewport; - @override - final double scale; - @override - double get x => 0; - @override - double get y => 0; - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - -class _Cursor extends ChangeNotifier implements CursorModel { - _Cursor(this.cache); - - @override - CursorData cache; - @override - ui.Image? get image => null; - @override - double get hotx => cache.hotxOrigin; - @override - double get hoty => cache.hotyOrigin; - @override - final Set cachedKeys = {}; - @override - void addKey(String key) => cachedKeys.add(key); - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - -class _Input extends Fake implements InputModel { - @override - final relativeMouseMode = false.obs; -} - -class _Peer extends Fake implements FfiModel { - @override - final pi = PeerInfo(); - @override - bool get isPeerLinux => false; -} - -class _FFI extends Fake implements FFI { - @override - final inputModel = _Input(); - @override - final ffiModel = _Peer(); -} - -CursorData _cursor(img.Image image, - {String id = 'sprite', double hotspot = 0}) { - return CursorData( - peerId: 'local-size-test', - id: id, - image: image, - scale: 1, - data: Platform.isWindows - ? image.getBytes(order: img.ChannelOrder.bgra) - : Uint8List.fromList(img.encodePng(image)), - hotxOrigin: hotspot, - hotyOrigin: hotspot, - width: image.width, - height: image.height, - ); -} - -CursorData _arrow(int density) { - final image = - img.Image(width: 17 * density, height: 23 * density, numChannels: 4); - img.fill(image, color: img.ColorRgba8(255, 255, 255, 255)); - return _cursor(image, id: 'arrow-$density', hotspot: 4.0 * density); -} - -void main() { - test('local size preserves remote pixels, padding and asymmetric hotspot', - () { - final artwork = img.Image(width: 40, height: 48, numChannels: 4); - img.fillRect(artwork, - x1: 8, - y1: 12, - x2: 22, - y2: 35, - color: img.ColorRgba8(180, 90, 45, 128), - alphaBlend: false); - for (final density in [1, 2, 3]) { - final source = img.copyResize(artwork, - width: artwork.width * density, - height: artwork.height * density, - interpolation: img.Interpolation.nearest); - expect(cursorVisibleSize(source), 24 * density); - final cursor = CursorData( - peerId: 'local-size-test', - id: 'padded-$density', - image: source, - scale: 1, - data: Platform.isWindows - ? source.getBytes(order: img.ChannelOrder.bgra) - : Uint8List.fromList(img.encodePng(source)), - hotxOrigin: 10.0 * density, - hotyOrigin: 17.0 * density, - width: source.width, - height: source.height, - )..localSize = 24; - cursor.updateGetKey(0.01); - final output = Platform.isWindows - ? img.Image.fromBytes( - width: cursor.scaledWidth, - height: cursor.scaledHeight, - bytes: cursor.data!.buffer, - order: img.ChannelOrder.bgra) - : img.decodePng(cursor.data!)!; - expect(cursorVisibleSize(output), 24); - expect(output.getBytes(), artwork.getBytes()); - expect((cursor.hotx, cursor.hoty), (10, 17)); - } - }); - - test('local cursor minimum keeps a thin remote cursor visible', () { - final thin = img.Image(width: 1, height: 48, numChannels: 4); - img.fill(thin, color: img.ColorRgba8(255, 255, 255, 255)); - final cursor = _cursor(thin)..localSize = 2; - cursor.updateGetKey(1); - expect(cursor.scaledWidth, 1); - expect(cursor.scaledHeight, kMinCursorSize); - final blank = _cursor(img.Image(width: 17, height: 23, numChannels: 4)) - ..localSize = 24; - blank.updateGetKey(1); - final output = Platform.isWindows - ? img.Image.fromBytes( - width: 17, - height: 23, - bytes: blank.data!.buffer, - order: img.ChannelOrder.bgra) - : img.decodePng(blank.data!)!; - expect(cursorVisibleSize(output), 0); - expect(blank.scale, 1); - }); - - test('switching local sizing keeps bitmap data and cache geometry consistent', - () { - final pending = _arrow(1)..data = null; - pending.updateGetKey(1); - expect(pending.data, isNull); - final cursor = _arrow(1); - const scale = 1.5; - final originalKey = cursor.updateGetKey(scale); - cursor.localSize = 23 * scale; - final localKey = cursor.updateGetKey(scale); - expect(cursor.scaledWidth, 26); - if (Platform.isWindows) { - expect(localKey, isNot(originalKey)); - expect(cursor.data!.length, 26 * 35 * 4); - } - cursor.localSize = null; - expect(cursor.updateGetKey(scale), originalKey); - if (Platform.isWindows) expect(cursor.data!.length, 25 * 34 * 4); - }); - - for (final (dpr, style, scale) in [ - for (final dpr in [1.0, 2.0, 3.0]) - for (final (style, scale) in [ - (kRemoteViewStyleAdaptive, 0.5), - (kRemoteViewStyleCustom, 0.25), - (kRemoteViewStyleCustom, 2.0), - ]) (dpr, style, scale), - ]) { - testWidgets('$style scale=$scale DPR=$dpr preserves local size and hotspot', - (tester) async { - final channel = Platform.isWindows - ? SystemChannels.mouseCursor - : const MethodChannel('flutter_custom_cursor'); - final registered = >[]; - tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, - (call) async { - if (call.method.startsWith('createCustomCursor')) { - final arguments = call.arguments as Map; - registered.add(arguments); - return arguments['name']; - } - return null; - }); - addTearDown(() => tester.binding.defaultBinaryMessenger - .setMockMethodCallHandler(channel, null)); - const sizeChannel = MethodChannel('org.rustdesk.rustdesk/cursor'); - tester.binding.defaultBinaryMessenger - .setMockMethodCallHandler(sizeChannel, (call) async { - expect(call.method, 'getSystemCursorSize'); - return Platform.isWindows ? 23.0 * dpr : 23.0; - }); - addTearDown(() => tester.binding.defaultBinaryMessenger - .setMockMethodCallHandler(sizeChannel, null)); - final cursor = _Cursor(_arrow(1)); - final zoom = false.obs; - await tester.pumpWidget(MediaQuery( - data: MediaQueryData(devicePixelRatio: dpr), - child: MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => _Image()), - ChangeNotifierProvider( - create: (_) => _Canvas(dpr, style: style, scale: scale)), - ChangeNotifierProvider.value(value: cursor), - ], - child: ImagePaint( - ffi: _FFI(), - id: 'local-size-test', - zoomCursor: zoom, - cursorOverImage: true.obs, - keyboardEnabled: true.obs, - remoteCursorMoved: false.obs, - ), - ), - )); - await tester.pump(); - expect(registered, isNotEmpty); - expect(cursor.cache.localSize, Platform.isWindows ? 23.0 * dpr : 23.0); - final original = registered.last; - final beforeDensityChange = registered.length; - cursor.cache = _arrow(2); - cursor.notifyListeners(); - await tester.pump(); - expect(registered, hasLength(beforeDensityChange + 1)); - final retina = registered.last; - expect( - [retina['width'], retina['height'], retina['hotX'], retina['hotY']], - [ - original['width'], - original['height'], - original['hotX'], - original['hotY'] - ], - reason: 'Only the remote raster density changed; the local cursor must ' - 'retain its size and hotspot.', - ); - zoom.value = true; - await tester.pump(); - expect(cursor.cache.localSize, isNull); - final zoomScale = scale * (Platform.isWindows ? dpr : 1); - expect(cursor.cache.scale, - zoomScale < kMinCursorSize / 34 ? kMinCursorSize / 34 : zoomScale); - expect(cursor.cache.hotx / cursor.cache.scaledWidth, closeTo(4 / 17, 1e-9)); - expect(cursor.cache.hoty / cursor.cache.scaledHeight, closeTo(4 / 23, 1e-9)); - await tester.pumpWidget(const SizedBox.shrink()); - cursor.dispose(); - }); - } -} diff --git a/flutter/test/cursor_measurement_failure_test.dart b/flutter/test/cursor_measurement_failure_test.dart deleted file mode 100644 index 242b2d370..000000000 --- a/flutter/test/cursor_measurement_failure_test.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_hbb/desktop/pages/remote_page.dart'; -import 'package:flutter_hbb/models/input_model.dart'; -import 'package:flutter_hbb/models/model.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:get/get.dart'; -import 'package:image/image.dart' as img; -import 'package:provider/provider.dart'; - -const _viewport = Size(200, 160); - -class _Image extends ChangeNotifier implements ImageModel { - @override - bool get useTextureRender => false; - @override - ui.Image? get image => null; - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - -class _Canvas extends ChangeNotifier implements CanvasModel { - _Canvas(this.devicePixelRatio, {required this.style, required this.scale}); - - final String style; - - @override - final double devicePixelRatio; - @override - final imageOverflow = false.obs; - @override - late final viewStyle = ViewStyle( - style: style, - width: _viewport.width, - height: _viewport.height, - displayWidth: 400, - displayHeight: 320, - ); - @override - bool get cursorEmbedded => false; - @override - Size get size => _viewport; - @override - final double scale; - @override - double get x => 0; - @override - double get y => 0; - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - -class _Cursor extends ChangeNotifier implements CursorModel { - _Cursor(this.cache); - - @override - CursorData cache; - @override - ui.Image? get image => null; - @override - double get hotx => cache.hotxOrigin; - @override - double get hoty => cache.hotyOrigin; - @override - final Set cachedKeys = {}; - @override - void addKey(String key) => cachedKeys.add(key); - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - -class _Input extends Fake implements InputModel { - @override - final relativeMouseMode = false.obs; -} - -class _Peer extends Fake implements FfiModel { - @override - final pi = PeerInfo(); - @override - bool get isPeerLinux => false; -} - -class _FFI extends Fake implements FFI { - @override - final inputModel = _Input(); - @override - final ffiModel = _Peer(); -} - -CursorData _cursor(img.Image image, - {String id = 'sprite', double hotspot = 0}) { - return CursorData( - peerId: 'local-size-test', - id: id, - image: image, - scale: 1, - data: Platform.isWindows - ? image.getBytes(order: img.ChannelOrder.bgra) - : Uint8List.fromList(img.encodePng(image)), - hotxOrigin: hotspot, - hotyOrigin: hotspot, - width: image.width, - height: image.height, - ); -} - -CursorData _arrow(int density) { - final image = - img.Image(width: 17 * density, height: 23 * density, numChannels: 4); - img.fill(image, color: img.ColorRgba8(255, 255, 255, 255)); - return _cursor(image, id: 'arrow-$density', hotspot: 4.0 * density); -} - -void main() { - testWidgets('pending or failed size measurement preserves the remote cursor', - (tester) async { - final channel = Platform.isWindows - ? SystemChannels.mouseCursor - : const MethodChannel('flutter_custom_cursor'); - final registered = >[]; - tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, - (call) async { - if (call.method.startsWith('createCustomCursor')) { - final arguments = call.arguments as Map; - registered.add(arguments); - return arguments['name']; - } - return null; - }); - addTearDown(() => tester.binding.defaultBinaryMessenger - .setMockMethodCallHandler(channel, null)); - const sizeChannel = MethodChannel('org.rustdesk.rustdesk/cursor'); - final measurement = Completer(); - tester.binding.defaultBinaryMessenger - .setMockMethodCallHandler(sizeChannel, (_) => measurement.future); - addTearDown(() => tester.binding.defaultBinaryMessenger - .setMockMethodCallHandler(sizeChannel, null)); - final cursor = _Cursor(_arrow(2)); - await tester.pumpWidget(_view(cursor)); - expect(registered, isNotEmpty); - measurement.completeError(PlatformException(code: 'cursor_size')); - await tester.pump(); - expect(tester.takeException(), isA()); - expect(cursor.cache.localSize, isNull); - final scale = Platform.isWindows ? 1.0 : 0.5; - expect(cursor.cache.scaledWidth, 34 * scale); - expect(cursor.cache.scaledHeight, 46 * scale); - expect(cursor.cache.hotx, 8 * scale); - expect(cursor.cache.hoty, 8 * scale); - expect(registered.last['hotX'], 8 * scale); - expect(registered.last['hotY'], 8 * scale); - await tester.pumpWidget(const SizedBox.shrink()); - cursor.dispose(); - }); -} - -Widget _view(_Cursor cursor) => MediaQuery( - data: const MediaQueryData(devicePixelRatio: 2), - child: MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => _Image()), - ChangeNotifierProvider(create: (_) => _Canvas(2, - style: kRemoteViewStyleAdaptive, scale: 0.25)), - ChangeNotifierProvider.value(value: cursor), - ], - child: ImagePaint( - ffi: _FFI(), - id: 'measurement-failure-test', - zoomCursor: false.obs, - cursorOverImage: true.obs, - keyboardEnabled: true.obs, - remoteCursorMoved: false.obs, - ), - ), - ); diff --git a/flutter/test/cursor_paint_scale_test.dart b/flutter/test/cursor_paint_scale_test.dart deleted file mode 100644 index 61a00af4b..000000000 --- a/flutter/test/cursor_paint_scale_test.dart +++ /dev/null @@ -1,187 +0,0 @@ -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:flutter/widgets.dart'; -import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_hbb/desktop/pages/remote_page.dart'; -import 'package:flutter_hbb/models/model.dart'; -import 'package:flutter_hbb/utils/image.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:get/get.dart'; -import 'package:provider/provider.dart'; - -const _hotspot = Offset(4, 9); -const _remotePosition = Offset(100.25, 80.75); -const _canvasOffset = Offset(15.125, 10.25); -const _viewport = Size(200, 160); - -class _CursorModel extends ChangeNotifier implements CursorModel { - _CursorModel(this.image); - - @override - final ui.Image image; - @override - double get hotx => _hotspot.dx; - @override - double get hoty => _hotspot.dy; - @override - double get x => _remotePosition.dx; - @override - double get y => _remotePosition.dy; - - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - -class _ImageModel extends Fake implements ImageModel { - _ImageModel(this.useTextureRender); - - @override - final bool useTextureRender; -} - -class _Peer extends Fake implements FfiModel { - @override - final pi = PeerInfo(); - @override - bool get isPeerLinux => false; -} - -class _FFI extends Fake implements FFI { - _FFI(bool useTexture) : imageModel = _ImageModel(useTexture); - - @override - final ImageModel imageModel; - @override - final ffiModel = _Peer(); -} - -class _CanvasModel extends ChangeNotifier implements CanvasModel { - _CanvasModel(String style, this.scale, bool useTexture) - : _ffi = _FFI(useTexture), - viewStyle = ViewStyle( - style: style, - width: _viewport.width, - height: _viewport.height, - displayWidth: _viewport.width.toInt(), - displayHeight: _viewport.height.toInt(), - ); - - final FFI _ffi; - @override - WeakReference get parent => WeakReference(_ffi); - @override - final imageOverflow = false.obs; - @override - final ViewStyle viewStyle; - @override - double get x => _canvasOffset.dx; - @override - double get y => _canvasOffset.dy; - @override - final double scale; - @override - ScrollStyle get scrollStyle => ScrollStyle.scrollauto; - - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - -class _Canvas extends Fake implements Canvas { - double factor = 1; - Offset? position; - - @override - void scale(double sx, [double? sy]) => factor *= sx; - - @override - void drawImage(ui.Image image, Offset offset, Paint paint) { - position = offset * factor; - } -} - -void main() { - for (final (style, zoom, dpr, source, canvasScale, scale, texture) in [ - (kRemoteViewStyleAdaptive, false, 2.0, (48, 64), 0.375, 0.375, true), - (kRemoteViewStyleAdaptive, false, 3.0, (48, 64), 0.25, 0.25, true), - (kRemoteViewStyleAdaptive, true, 3.0, (48, 64), 0.375, 0.375, true), - (kRemoteViewStyleOriginal, false, 2.0, (48, 64), 0.5, 0.5, true), - (kRemoteViewStyleOriginal, true, 2.0, (48, 64), 0.5, 0.5, true), - for (final zoom in [false, true]) - for (final scale in [0.25, 2.0]) - for (final texture in [false, true]) - (kRemoteViewStyleCustom, zoom, 2.0, (48, 64), scale, scale, texture), - ( - kRemoteViewStyleAdaptive, - false, - 2.0, - (9, 18), - 0.1, - Platform.isWindows ? 2 / 3 : 4 / 3, - true - ), - ( - kRemoteViewStyleAdaptive, - true, - 2.0, - (9, 18), - 0.1, - Platform.isWindows ? 2 / 3 : 4 / 3, - true - ), - (kRemoteViewStyleAdaptive, false, 2.25, (48, 48), 0.375, 0.375, false), - ( - kRemoteViewStyleAdaptive, - true, - 2.0, - (9, 18), - 0.1, - Platform.isWindows ? 2 / 3 : 4 / 3, - false - ), - ]) { - testWidgets( - '$style zoom=$zoom dpr=$dpr source=$source texture=$texture keeps remote geometry', - (tester) async { - final image = (await tester.runAsync( - () => createTestImage(width: source.$1, height: source.$2)))!; - addTearDown(image.dispose); - await tester.pumpWidget(MediaQuery( - data: MediaQueryData(devicePixelRatio: dpr), - child: MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (_) => _CursorModel(image)), - ChangeNotifierProvider( - create: (_) => _CanvasModel(style, canvasScale, texture)), - ], - child: CursorPaint(id: 'cursor-test', zoomCursor: zoom.obs), - ), - )); - final painter = tester - .widget(find.byType(CustomPaint)) - .painter! as ImagePainter; - - expect(painter.image, same(image)); - expect(painter.scale, scale); - var imageOrigin = _canvasOffset; - if (!texture) { - final background = _Canvas(); - ImagePainter( - image: image, - x: _canvasOffset.dx / canvasScale, - y: _canvasOffset.dy / canvasScale, - scale: canvasScale, - ).paint(background, _viewport); - imageOrigin = background.position!; - } - final target = _remotePosition * canvasScale + imageOrigin; - expect((Offset(painter.x, painter.y) + _hotspot) * scale, target); - final canvas = _Canvas(); - painter.paint(canvas, _viewport); - final position = canvas.position! + _hotspot * canvas.factor; - expect(position.dx, closeTo(target.dx, 1e-9)); - expect(position.dy, closeTo(target.dy, 1e-9)); - }); - } -} diff --git a/flutter/test/cursor_resize_limit_test.dart b/flutter/test/cursor_resize_limit_test.dart deleted file mode 100644 index 50e7c14f5..000000000 --- a/flutter/test/cursor_resize_limit_test.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:math'; -import 'dart:typed_data'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_hbb/models/model.dart'; -import 'package:image/image.dart' as img; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - test('sparse remote artwork cannot amplify the native bitmap without limit', - () { - const sourceSize = 64; - const bitmapLimit = 512; - final image = img.Image(width: sourceSize, height: sourceSize, numChannels: 4) - ..setPixelRgba(0, 0, 255, 255, 255, 255); - final cache = CursorData( - peerId: 'sparse-cursor', - id: '1', - image: image, - scale: 1, - data: Uint8List.fromList(img.encodePng(image)), - hotxOrigin: 32, - hotyOrigin: 48, - width: sourceSize, - height: sourceSize, - )..localSize = sourceSize.toDouble(); - final messages = []; - final previous = debugPrint; - debugPrint = (String? message, {int? wrapWidth}) { - messages.add(message ?? ''); - }; - addTearDown(() => debugPrint = previous); - - cache.updateGetKey(1); - - expect(max(cache.scaledWidth, cache.scaledHeight), bitmapLimit); - expect(cache.hotx, 32 * cache.scaledWidth / sourceSize); - expect(cache.hoty, 48 * cache.scaledHeight / sourceSize); - expect(messages.single, contains('bitmap limit')); - }); -} diff --git a/flutter/test/cursor_web_test.dart b/flutter/test/cursor_web_test.dart deleted file mode 100644 index 0e32d35e1..000000000 --- a/flutter/test/cursor_web_test.dart +++ /dev/null @@ -1,64 +0,0 @@ -@TestOn('browser') -library; - -import 'dart:convert'; -import 'dart:js' as js; -import 'dart:typed_data'; - -import 'package:flutter_hbb/models/model.dart' as model; -import 'package:flutter_hbb/web/custom_cursor.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:image/image.dart' as img; - -class _CursorModel extends Fake implements model.CursorModel { - @override - final Set cachedKeys = {}; - - @override - void addKey(String key) => cachedKeys.add(key); -} - -void main() { - test('Web cursor rounds its bitmap and hotspot together', () async { - Map? registered; - final original = js.context['setByName']; - js.context['setByName'] = js.allowInterop((String name, String value) { - expect(name, 'cursor'); - registered = jsonDecode(value) as Map; - }); - addTearDown(() => js.context['setByName'] = original); - - const sourceSide = 48; - final image = img.Image(width: sourceSide, height: sourceSide, numChannels: 4); - img.fill(image, color: img.ColorRgba8(255, 255, 255, 255)); - for (final (sourceHotspot, scale, outputSide, expectedHotspot) in [ - ((7.0, 7.0), 633 / 1600, 19, (3, 3)), - ((21.0, 23.0), 633 / 1600, 19, (8, 9)), - ((22.0, 22.0), 633 / 1600, 19, (9, 9)), - ((21.0, 23.0), 0.05, 12, (5, 6)), - ((21.0, 23.0), 0.5, 24, (11, 12)), - ((7.0, 7.0), 1.0, 48, (7, 7)), - ]) { - final cache = model.CursorData( - peerId: 'web-cursor-test', - id: '$sourceHotspot', - image: image, - scale: 1, - data: Uint8List.fromList(img.encodePng(image)), - hotxOrigin: sourceHotspot.$1, - hotyOrigin: sourceHotspot.$2, - width: sourceSide, - height: sourceSide, - ); - final cursor = buildCursorOfCache(_CursorModel(), scale, cache); - final session = cursor.createSession(1); - await session.activate(); - final uri = Uri.parse(registered!['url'] as String); - final bitmap = img.decodePng(uri.data!.contentAsBytes())!; - expect((bitmap.width, bitmap.height), (outputSide, outputSide)); - expect((registered!['hotx'], registered!['hoty']), expectedHotspot); - session.dispose(); - await deleteCustomCursor(cache.updateGetKey(scale)); - } - }); -} diff --git a/flutter/windows/runner/cursor_size.h b/flutter/windows/runner/cursor_size.h deleted file mode 100644 index be0a642cd..000000000 --- a/flutter/windows/runner/cursor_size.h +++ /dev/null @@ -1,146 +0,0 @@ -#ifndef RUSTDESK_CURSOR_SIZE_H_ -#define RUSTDESK_CURSOR_SIZE_H_ - -#include -#include -#include - -#include -#include -#include - -namespace cursor_size { - -struct IconBitmaps { - ICONINFO info{}; - ~IconBitmaps() { - if (info.hbmColor) DeleteObject(info.hbmColor); - if (info.hbmMask) DeleteObject(info.hbmMask); - } -}; - -struct Surface { - HDC dc = CreateCompatibleDC(nullptr); - HBITMAP bitmap = nullptr; - HGDIOBJ previous = nullptr; - uint32_t* pixels = nullptr; - int width = 0, height = 0; - - ~Surface() { - if (previous) SelectObject(dc, previous); - if (bitmap) DeleteObject(bitmap); - if (dc) DeleteDC(dc); - } - - void Init(const BITMAP& source, bool monochrome) { - width = source.bmWidth; - height = monochrome ? source.bmHeight / 2 : source.bmHeight; - if (!dc || width <= 0 || height <= 0) { - throw std::runtime_error("Could not create the cursor measurement surface"); - } - constexpr WORD kColorBits = 32; - BITMAPINFO format{}; - format.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - format.bmiHeader.biWidth = width; - format.bmiHeader.biHeight = -height; - format.bmiHeader.biPlanes = 1; - format.bmiHeader.biBitCount = kColorBits; - format.bmiHeader.biCompression = BI_RGB; - bitmap = CreateDIBSection(dc, &format, DIB_RGB_COLORS, - reinterpret_cast(&pixels), nullptr, 0); - if (!bitmap) throw std::runtime_error("Could not allocate the cursor bitmap"); - previous = SelectObject(dc, bitmap); - if (!previous || previous == HGDI_ERROR) { - previous = nullptr; - throw std::runtime_error("Could not select the cursor bitmap"); - } - } -}; - -inline void IncludeVisiblePixels(const Surface& surface, uint32_t background, - RECT& bounds) { - constexpr uint32_t kRgbMask = 0x00ffffff; - for (int y = 0; y < surface.height; ++y) { - for (int x = 0; x < surface.width; ++x) { - if ((surface.pixels[y * surface.width + x] & kRgbMask) == background) continue; - bounds.left = (std::min)(bounds.left, static_cast(x)); - bounds.right = (std::max)(bounds.right, static_cast(x + 1)); - bounds.top = (std::min)(bounds.top, static_cast(y)); - bounds.bottom = (std::max)(bounds.bottom, static_cast(y + 1)); - } - } -} - -inline double WindowScale(HWND window) { - const auto user32 = GetModuleHandleW(L"user32.dll"); - const auto window_dpi = reinterpret_cast( - GetProcAddress(user32, "GetDpiForWindow")); - const auto system_dpi = reinterpret_cast( - GetProcAddress(user32, "GetDpiForSystem")); - const auto metrics = reinterpret_cast( - GetProcAddress(user32, "GetSystemMetricsForDpi")); - // Preserve the system-DPI path on Windows versions without per-window metrics. - if (!window_dpi || !system_dpi || !metrics) return 1.0; - const UINT target_dpi = window_dpi(window); - const UINT source_dpi = system_dpi(); - if (!target_dpi || !source_dpi) { - throw std::runtime_error("Could not read the cursor window DPI"); - } - const int target_size = metrics(SM_CXCURSOR, target_dpi); - const int source_size = metrics(SM_CXCURSOR, source_dpi); - if (target_size <= 0 || source_size <= 0) { - throw std::runtime_error("Could not read the DPI-specific cursor metrics"); - } - return static_cast(target_size) / source_size; -} - -inline double SystemSize(HWND window) { - HCURSOR cursor = LoadCursorW(nullptr, IDC_ARROW); - IconBitmaps bitmaps; - if (!cursor || !GetIconInfo(cursor, &bitmaps.info)) { - throw std::runtime_error("Could not read the Windows system cursor"); - } - BITMAP source{}; - const auto bitmap = bitmaps.info.hbmColor ? bitmaps.info.hbmColor : bitmaps.info.hbmMask; - if (!GetObject(bitmap, sizeof(source), &source)) { - throw std::runtime_error("Could not read the system cursor bitmap size"); - } - Surface surface; - surface.Init(source, !bitmaps.info.hbmColor); - RECT bounds{surface.width, surface.height, 0, 0}; - // Drawing on both backgrounds measures alpha and legacy AND/XOR cursors. - constexpr uint32_t kBlack = 0x00000000, kWhite = 0x00ffffff; - for (const auto background : {kBlack, kWhite}) { - std::fill_n(surface.pixels, surface.width * surface.height, background); - if (!DrawIconEx(surface.dc, 0, 0, cursor, 0, 0, 0, nullptr, DI_NORMAL) || !GdiFlush()) { - throw std::runtime_error("Could not draw the Windows system cursor"); - } - IncludeVisiblePixels(surface, background, bounds); - } - if (bounds.right <= bounds.left) { - throw std::runtime_error("System cursor has no visible pixels"); - } - return (std::max)(bounds.right - bounds.left, bounds.bottom - bounds.top) * - WindowScale(window); -} - -inline void Register(flutter::BinaryMessenger* messenger, HWND window) { - flutter::MethodChannel<> channel(messenger, "org.rustdesk.rustdesk/cursor", - &flutter::StandardMethodCodec::GetInstance()); - channel.SetMethodCallHandler([window](const flutter::MethodCall<>& call, - std::unique_ptr> result) { - if (call.method_name() != "getSystemCursorSize") { - result->NotImplemented(); - return; - } - try { - result->Success(flutter::EncodableValue(SystemSize(window))); - } catch (const std::exception& error) { - result->Error("cursor_size", error.what()); - } - }); -} - -} // namespace cursor_size - -#endif // RUSTDESK_CURSOR_SIZE_H_ diff --git a/flutter/windows/runner/flutter_window.cpp b/flutter/windows/runner/flutter_window.cpp index a21149cef..501436d71 100644 --- a/flutter/windows/runner/flutter_window.cpp +++ b/flutter/windows/runner/flutter_window.cpp @@ -18,7 +18,6 @@ #include #include "win32_desktop.h" -#include "cursor_size.h" namespace { @@ -101,8 +100,6 @@ bool FlutterWindow::OnCreate() { return false; } RegisterPlugins(flutter_controller_->engine()); - cursor_size::Register(flutter_controller_->engine()->messenger(), - flutter_controller_->view()->GetNativeWindow()); flutter::MethodChannel<> channel( flutter_controller_->engine()->messenger(), @@ -149,8 +146,6 @@ bool FlutterWindow::OnCreate() { auto *flutter_view_controller = reinterpret_cast(controller); auto *registry = flutter_view_controller->engine(); - cursor_size::Register(registry->messenger(), - flutter_view_controller->view()->GetNativeWindow()); TextureRgbaRendererPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("TextureRgbaRendererPlugin")); FlutterGpuTextureRendererPluginCApiRegisterWithRegistrar( diff --git a/libs/base/protos/message.proto b/libs/base/protos/message.proto index a45d05e41..1276deaa8 100644 --- a/libs/base/protos/message.proto +++ b/libs/base/protos/message.proto @@ -310,7 +310,7 @@ message KeyEvent { } message CursorData { - uint64 id = 1 [jstype = JS_STRING]; + uint64 id = 1; sint32 hotx = 2; sint32 hoty = 3; int32 width = 4; @@ -999,7 +999,7 @@ message Message { AudioFrame audio_frame = 11; CursorData cursor_data = 12; CursorPosition cursor_position = 13; - uint64 cursor_id = 14 [jstype = JS_STRING]; + uint64 cursor_id = 14; KeyEvent key_event = 15; Clipboard clipboard = 16; FileAction file_action = 17; diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index df269633d..1cc1ff619 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -10,7 +10,6 @@ authors = ["Ram "] edition = "2018" [features] -cursor = [] 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 2fa58ed34..000000000 --- a/libs/scrap/src/dxgi/cursor.rs +++ /dev/null @@ -1,248 +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, -}; - -pub const CURSOR_ID_FLAG: u64 = 1 << 63; -const CHANNELS: u32 = 4; -const BITS_PER_BYTE: u32 = 8; - -pub struct Shape { - pub id: u64, - 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); - let mut hash = DefaultHasher::new(); - (info.Type, info.Width, height, info.Pitch, hotspot, &pixels).hash(&mut hash); - Ok(Self { - id: hash.finish() | CURSOR_ID_FLAG, - kind: info.Type, - width: info.Width, - height, - pitch: info.Pitch, - hotspot, - pixels, - }) - } -} - -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, - ) { - if frame.PointerShapeBufferSize == 0 { - return; - } - let snapshot = match read(duplication, frame.PointerShapeBufferSize) { - Ok(shape) => Snapshot::Ready(Arc::new(shape)), - 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(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 { - 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(); - assert_eq!( - (shape.width, shape.height, shape.hotspot), - (64, 64, (31, 29)) - ); - assert_eq!(shape.pixels, pixels); - 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().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/macos.rs b/src/platform/macos.rs index 2c8d52987..82bcf9d6f 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -35,14 +35,12 @@ use std::{ sync::Mutex, }; -mod cursor; - // macOS boolean_t is defined as `int` in type BooleanT = hbb_common::libc::c_int; static PRIVILEGES_SCRIPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); -static mut LATEST_SEED: (i32, f64) = (0, 0.0); +static mut LATEST_SEED: i32 = 0; #[inline] fn get_update_temp_dir() -> PathBuf { @@ -563,7 +561,7 @@ pub fn get_cursor() -> ResultType> { fn unsafe_get_cursor() -> ResultType> { unsafe { - let seed = (CGSCurrentCursorSeed(), cursor::scale()?); + let seed = CGSCurrentCursorSeed(); if seed == LATEST_SEED { return Ok(None); } @@ -575,11 +573,11 @@ fn unsafe_get_cursor() -> ResultType> { pub fn reset_input_cache() { unsafe { - LATEST_SEED = (0, 0.0); + LATEST_SEED = 0; } } -fn get_cursor_id() -> ResultType<(id, u64, f64)> { +fn get_cursor_id() -> ResultType<(id, u64)> { unsafe { let c: id = msg_send![class!(NSCursor), currentSystemCursor]; if c == nil { @@ -622,8 +620,7 @@ fn get_cursor_id() -> ResultType<(id, u64, f64)> { hcursor += (r + g + b + a) * (255 << i) as f64; } } - let scale = cursor::scale()?; - Ok((c, cursor::cache_id(hcursor as _, scale), scale)) + Ok((c, hcursor as _)) } } @@ -634,13 +631,10 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType { // https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c fn unsafe_get_cursor_data(hcursor: u64) -> ResultType { unsafe { - let (c, hcursor2, scale) = get_cursor_id()?; + let (c, hcursor2) = get_cursor_id()?; if hcursor != hcursor2 { bail!("cursor changed"); } - if scale > 1.0 { - return cursor::data(c, hcursor, scale); - } let hotspot: NSPoint = msg_send![c, hotSpot]; let img: id = msg_send![c, image]; let size: NSSize = msg_send![img, size]; @@ -674,9 +668,9 @@ fn unsafe_get_cursor_data(hcursor: u64) -> ResultType { let g: f64 = msg_send![color, greenComponent]; let b: f64 = msg_send![color, blueComponent]; let a: f64 = msg_send![color, alphaComponent]; - colors.push((r * a * 255.).round() as _); - colors.push((g * a * 255.).round() as _); - colors.push((b * a * 255.).round() as _); + colors.push((r * 255.) as _); + colors.push((g * 255.) as _); + colors.push((b * 255.) as _); colors.push((a * 255.) as _); } } diff --git a/src/platform/macos/cursor.rs b/src/platform/macos/cursor.rs deleted file mode 100644 index 5d2056f37..000000000 --- a/src/platform/macos/cursor.rs +++ /dev/null @@ -1,174 +0,0 @@ -use super::{CursorData, ResultType}; -use cocoa::{ - appkit::NSCompositingOperation, - base::{id, nil, NO, YES}, - foundation::{NSInteger, NSPoint, NSRect, NSSize, NSString}, -}; -use hbb_common::{anyhow::Context, bail}; -use objc::{class, msg_send, rc::StrongPtr, sel, sel_impl}; -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, - ptr, slice, -}; - -const CHANNELS: usize = 4; -const BITS_PER_SAMPLE: NSInteger = 8; - -pub(super) fn scale() -> ResultType { - if !*scrap::quartz::ENABLE_RETINA.lock().unwrap() { - return Ok(1.0); - } - unsafe { - let point: NSPoint = msg_send![class!(NSEvent), mouseLocation]; - let screens: id = msg_send![class!(NSScreen), screens]; - let count: usize = msg_send![screens, count]; - for index in 0..count { - let screen: id = msg_send![screens, objectAtIndex: index]; - let frame: NSRect = msg_send![screen, frame]; - // AppKit's bottom-left coordinates include the upper screen edge. - if point.x >= frame.origin.x - && point.y > frame.origin.y - && point.x < frame.origin.x + frame.size.width - && point.y <= frame.origin.y + frame.size.height - { - return Ok(msg_send![screen, backingScaleFactor]); - } - } - } - bail!("No macOS display contains the cursor") -} - -pub(super) fn cache_id(cursor: u64, scale: f64) -> u64 { - let mut hash = DefaultHasher::new(); - (cursor, scale.to_bits()).hash(&mut hash); - hash.finish() -} - -unsafe fn bitmap(size: NSSize) -> ResultType { - let color_space = StrongPtr::new(NSString::alloc(nil).init_str("NSDeviceRGBColorSpace")); - let bitmap: id = msg_send![class!(NSBitmapImageRep), alloc]; - let bitmap: id = msg_send![bitmap, - initWithBitmapDataPlanes: ptr::null_mut::<*mut u8>() - pixelsWide: size.width as NSInteger pixelsHigh: size.height as NSInteger - bitsPerSample: BITS_PER_SAMPLE samplesPerPixel: CHANNELS as NSInteger - hasAlpha: YES isPlanar: NO colorSpaceName: *color_space - bitmapFormat: 0usize bytesPerRow: (size.width as usize * CHANNELS) as NSInteger - bitsPerPixel: BITS_PER_SAMPLE * CHANNELS as NSInteger]; - if bitmap == nil { - bail!("Could not allocate the macOS cursor bitmap"); - } - Ok(StrongPtr::new(bitmap)) -} - -unsafe fn render(image: id, bitmap: id, size: NSSize) -> ResultType<()> { - let context: id = - msg_send![class!(NSGraphicsContext), graphicsContextWithBitmapImageRep: bitmap]; - if context == nil { - bail!("Could not create the macOS cursor graphics context"); - } - let (): () = msg_send![class!(NSGraphicsContext), saveGraphicsState]; - let (): () = msg_send![class!(NSGraphicsContext), setCurrentContext: context]; - // Drawing at the pixel size lets AppKit select the matching image representation. - let (): () = msg_send![image, - drawInRect: NSRect::new(NSPoint::new(0.0, 0.0), size) - fromRect: NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0)) - operation: NSCompositingOperation::NSCompositeCopy fraction: 1.0f64]; - let (): () = msg_send![class!(NSGraphicsContext), restoreGraphicsState]; - Ok(()) -} - -pub(super) unsafe fn data(cursor: id, id: u64, scale: f64) -> ResultType { - let image: id = msg_send![cursor, image]; - let logical: NSSize = msg_send![image, size]; - let size = NSSize::new( - (logical.width * scale).round(), - (logical.height * scale).round(), - ); - if !size.width.is_finite() - || !size.height.is_finite() - || size.width <= 0.0 - || size.height <= 0.0 - || size.width > i32::MAX as f64 - || size.height > i32::MAX as f64 - { - bail!("Invalid macOS cursor dimensions"); - } - let length = (size.width as usize) - .checked_mul(size.height as usize) - .and_then(|pixels| pixels.checked_mul(CHANNELS)) - .context("Cursor bitmap size overflow")?; - let bitmap = bitmap(size)?; - render(image, *bitmap, size)?; - let pixels: *const u8 = msg_send![*bitmap, bitmapData]; - if pixels.is_null() { - bail!("Could not read the macOS cursor bitmap"); - } - let hotspot: NSPoint = msg_send![cursor, hotSpot]; - Ok(CursorData { - id, - colors: slice::from_raw_parts(pixels, length).to_vec().into(), - hotx: (hotspot.x * size.width / logical.width).round() as _, - hoty: (hotspot.y * size.height / logical.height).round() as _, - width: size.width as _, - height: size.height as _, - ..Default::default() - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use objc::rc::autoreleasepool; - - #[test] - fn retina_cursor_uses_complete_high_resolution_artwork() { - autoreleasepool(|| unsafe { - let logical = NSSize::new(9.0, 18.0); - let image: id = msg_send![class!(NSImage), alloc]; - let image = StrongPtr::new(msg_send![image, initWithSize: logical]); - let mut expected = Vec::new(); - for scale in [1, 2] { - let width = logical.width as usize * scale; - let height = logical.height as usize * scale; - let rep = bitmap(NSSize::new(width as f64, height as f64)).unwrap(); - let mut pixels = vec![0; width * height * CHANNELS]; - for y in 0..height { - for x in 0..width { - if y == 0 || y == height - 1 || x == width / 2 { - let color = if y == 0 { - [255, 0, 0, 255] - } else { - [0, 255, 0, 255] - }; - pixels[(y * width + x) * CHANNELS..(y * width + x + 1) * CHANNELS] - .copy_from_slice(&color); - } - } - } - let buffer: *mut u8 = msg_send![*rep, bitmapData]; - ptr::copy_nonoverlapping(pixels.as_ptr(), buffer, pixels.len()); - let (): () = msg_send![*rep, setSize: logical]; - let (): () = msg_send![*image, addRepresentation: *rep]; - if scale == 2 { - expected = pixels; - } - } - let cursor: id = msg_send![class!(NSCursor), alloc]; - let cursor = StrongPtr::new( - msg_send![cursor, initWithImage: *image hotSpot: NSPoint::new(4.0, 9.0)], - ); - let result = data(*cursor, 1, 2.0).unwrap(); - assert_eq!( - (result.width, result.height, result.hotx, result.hoty), - (18, 36, 8, 18) - ); - assert_eq!(result.colors.as_ref(), expected.as_slice()); - }); - } - - #[test] - fn cursor_cache_changes_with_display_scale() { - assert_ne!(cache_id(123, 1.0), cache_id(123, 2.0)); - } -} diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 64625556c..9027d9d89 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -413,7 +413,7 @@ extern "C" { auto in = in0; auto out0_end = out0 + out0_size; - auto offset = (width + 2) * 4 + 4; + auto offset = width * 4 + 4; auto out = out0 + offset; for (int y = 0; y < height; y++) { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index fb17fcb38..a3c3d68f0 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)?; @@ -4763,44 +4750,6 @@ pub(super) fn get_pids_with_first_arg_by_wmic, S2: AsRef>( mod tests { use super::*; - #[test] - fn cursor_outline_preserves_pixels_at_hotspot_offset() { - const CHANNELS: usize = 4; - const BORDER: usize = 1; - const WIDTH: usize = 3; - const HEIGHT: usize = 9; - const INK: [u8; CHANNELS] = [32, 64, 96, 255]; - let mut source = vec![0; WIDTH * HEIGHT * CHANNELS]; - for y in 0..HEIGHT { - for x in 0..WIDTH { - if x == WIDTH / 2 || y == 0 || y == HEIGHT - 1 { - let offset = (y * WIDTH + x) * CHANNELS; - source[offset..offset + CHANNELS].copy_from_slice(&INK); - } - } - } - let stride = WIDTH + BORDER * 2; - let mut outlined = vec![0; stride * (HEIGHT + BORDER * 2) * CHANNELS]; - unsafe { - drawOutline( - outlined.as_mut_ptr(), - source.as_ptr(), - WIDTH as _, - HEIGHT as _, - outlined.len() as _, - ); - } - for y in 0..HEIGHT { - for x in 0..WIDTH { - let input = (y * WIDTH + x) * CHANNELS; - if source[input + CHANNELS - 1] != 0 { - let output = ((y + BORDER) * stride + x + BORDER) * CHANNELS; - assert_eq!(&outlined[output..output + CHANNELS], &INK, "({x}, {y})"); - } - } - } - } - // Test-only reusable Win32 HANDLE RAII helper. // If a future non-test path needs the same pattern, move it out of this test module. // diff --git a/src/platform/windows/cursor.rs b/src/platform/windows/cursor.rs deleted file mode 100644 index f6d50549c..000000000 --- a/src/platform/windows/cursor.rs +++ /dev/null @@ -1,155 +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, - ..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, - 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) - ); - } -} diff --git a/src/server/drm_capturer.rs b/src/server/drm_capturer.rs index 8f7b54ff3..23d7a6720 100644 --- a/src/server/drm_capturer.rs +++ b/src/server/drm_capturer.rs @@ -14,8 +14,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant}; -mod cursor; - const HANDSHAKE_TIMEOUT_MS: u64 = 3000; const DRM_CONNECT_TIMEOUT_MS: u64 = 1000; /// The service may hold the list back while it wakes sleeping displays: ~3.6s (DRM_WAKE_*). @@ -632,19 +630,6 @@ async fn recv_thread( let _ = tx.send(Err(err)); return; } - let mut mutter_cursor = match cursor::Capture::start( - display, - cursor_epoch, - displays[wire_idx].name.clone(), - ) - .await - { - Ok(cursor) => cursor, - Err(error) => { - log::error!("drm: could not start Mutter cursor capture; using wire cursor: {error:#}"); - None - } - }; let _ = tx.send(Ok((displays, wire_idx))); // A cursor that arrived before new() stored the session transform, held for replay. Only the @@ -654,21 +639,9 @@ async fn recv_thread( if stop.load(Ordering::SeqCst) { break "stopped".to_owned(); } - if let Some(error) = mutter_cursor.as_ref().and_then(cursor::Capture::error) { - log::error!("drm: {error}; resuming wire cursor capture"); - if let Some(mut cursor) = mutter_cursor.take() { - if let Some(wire) = cursor.wire_cursor.take() { - pending_cursor = Some(wire); - } - if let Err(error) = cursor.stop().await { - log::error!("drm: could not join the Mutter cursor worker: {error:#}"); - } - } - } if pending_cursor.is_some() { - let ready = mutter_cursor.as_ref().map(cursor::Capture::ready); let t = shared.transform.load(std::sync::atomic::Ordering::Acquire); - if t != TRANSFORM_PENDING && ready.as_deref() != Some(&true) { + if t != TRANSFORM_PENDING { if let Some((id, width, height, hotx, hoty, raw)) = pending_cursor.take() { deliver_drm_cursor(display, cursor_epoch, id, width, height, hotx, hoty, raw, t); } @@ -790,15 +763,6 @@ async fn recv_thread( raw.len() ); } - // Retain the latest wire shape for a failed Mutter worker, even when - // that shape arrived before the first Mutter sprite. - if let Some(cursor) = mutter_cursor.as_mut() { - cursor.wire_cursor = Some((id, width, height, hotx, hoty, raw.clone())); - } - let ready = mutter_cursor.as_ref().map(cursor::Capture::ready); - if ready.as_deref() == Some(&true) { - continue; - } let t = shared.transform.load(std::sync::atomic::Ordering::Acquire); if t == TRANSFORM_PENDING { pending_cursor = Some((id, width, height, hotx, hoty, raw)); @@ -894,11 +858,6 @@ async fn recv_thread( // Drop the render context on THIS thread: its EGL state + cached imports are thread-local and // a cross-thread close strands them. Never in `Drop`, which runs on the encoder thread. drop(converter); - if let Some(cursor) = mutter_cursor { - if let Err(error) = cursor.stop().await { - log::error!("drm: could not join the Mutter cursor worker: {error:#}"); - } - } remove_drm_cursor(display, cursor_epoch); let mut slot = shared.slot.lock().unwrap(); slot.ended = Some(format!("drm stream ended ({end_reason})")); diff --git a/src/server/drm_capturer/cursor.rs b/src/server/drm_capturer/cursor.rs deleted file mode 100644 index 3e339b89a..000000000 --- a/src/server/drm_capturer/cursor.rs +++ /dev/null @@ -1,200 +0,0 @@ -use super::DrmCursorData; -use dbus::{ - arg::{PropMap, Variant}, - blocking::Connection, - message::MatchRule, - Path, -}; -use hbb_common::{anyhow::anyhow, bail, log, tokio, ResultType}; -use std::{ - sync::{ - mpsc::{self, Receiver, Sender, TryRecvError}, - Arc, Mutex, MutexGuard, - }, - thread::{self, JoinHandle}, - time::{Duration, Instant}, -}; - -mod ffi; -mod metadata; -mod pipewire; -#[cfg(test)] -mod tests; - -const BUS: &str = "org.gnome.Mutter.ScreenCast"; -const SESSION_INTERFACE: &str = "org.gnome.Mutter.ScreenCast.Session"; -const STREAM_INTERFACE: &str = "org.gnome.Mutter.ScreenCast.Stream"; -const CURSOR_METADATA_MODE: u32 = 2; -const DBUS_TIMEOUT: Duration = Duration::from_secs(2); -const START_TIMEOUT: Duration = Duration::from_secs(5); -const POLL_INTERVAL: Duration = Duration::from_millis(20); - -pub(super) type WireCursor = (u64, u32, u32, i32, i32, Vec); - -pub struct Capture { - stop: Option>, - thread: Option>, - error: Arc>>, - ready: Arc>, - pub(super) wire_cursor: Option, -} - -impl Capture { - pub async fn start(display: i32, epoch: u64, connector: String) -> ResultType> { - if !tokio::task::spawn_blocking(mutter_available).await?? { - return Ok(None); - } - let (stop, receiver) = mpsc::channel(); - let error = Arc::new(Mutex::new(None)); - let worker_error = error.clone(); - let ready = Arc::new(Mutex::new(false)); - let worker_ready = ready.clone(); - let thread = thread::Builder::new() - .name("drm-cursor".into()) - .spawn(move || { - let result = run(connector, receiver, move |cursor| { - publish((display, epoch), &worker_ready, cursor); - }); - if let Err(error) = result { - log::error!("drm: Mutter cursor capture failed: {error:#}"); - *worker_error.lock().unwrap() = - Some(format!("Mutter cursor capture: {error:#}")); - } - })?; - Ok(Some(Self { - stop: Some(stop), - thread: Some(thread), - error, - ready, - wire_cursor: None, - })) - } - - pub fn error(&self) -> Option { - self.error.lock().unwrap().clone() - } - - pub fn ready(&self) -> MutexGuard<'_, bool> { - self.ready.lock().unwrap() - } - - pub async fn stop(mut self) -> ResultType<()> { - drop(self.stop.take()); - if let Some(thread) = self.thread.take() { - tokio::task::spawn_blocking(move || { - thread - .join() - .map_err(|_| anyhow!("Mutter cursor worker panicked")) - }) - .await??; - } - Ok(()) - } -} - -fn mutter_available() -> ResultType { - // Service-spawned servers have a session bus, but no XDG_CURRENT_DESKTOP. - let conn = Connection::new_session()?; - let (available,): (bool,) = conn - .with_proxy( - "org.freedesktop.DBus", - "/org/freedesktop/DBus", - DBUS_TIMEOUT, - ) - .method_call("org.freedesktop.DBus", "NameHasOwner", (BUS,))?; - Ok(available) -} - -fn stopped(receiver: &Receiver<()>) -> bool { - !matches!(receiver.try_recv(), Err(TryRecvError::Empty)) -} - -fn publish(target: (i32, u64), ready: &Mutex, cursor: DrmCursorData) { - // Serialize source switching with wire publication, including pending replay. - let mut ready = ready.lock().unwrap(); - if !*ready { - log::info!("drm: using Mutter cursor metadata for display {}", target.0); - } - *ready = true; - // These sprites already have the monitor's upright orientation and physical scale. - super::set_drm_cursor(target.0, target.1, cursor); -} - -fn run( - connector: String, - stop: Receiver<()>, - publish: impl FnMut(DrmCursorData) + Send + 'static, -) -> ResultType<()> { - let session = Session::new()?; - let Some(node) = session.start(&super::normalize_connector(&connector), &stop)? else { - return Ok(()); - }; - let stream = pipewire::Stream::new(node, publish)?; - let started = Instant::now(); - while !stopped(&stop) { - if !stream.received()? && started.elapsed() >= START_TIMEOUT { - bail!("Timed out waiting for PipeWire cursor metadata"); - } - session.conn.process(POLL_INTERVAL)?; - } - // Drop order closes PipeWire before stopping its Mutter session. - Ok(()) -} - -struct Session { - conn: Connection, - path: Path<'static>, -} - -impl Session { - fn new() -> ResultType { - let conn = Connection::new_session()?; - let (path,): (Path<'static>,) = conn - .with_proxy(BUS, "/org/gnome/Mutter/ScreenCast", DBUS_TIMEOUT) - .method_call(BUS, "CreateSession", (PropMap::new(),))?; - Ok(Self { conn, path }) - } - - fn start(&self, connector: &str, stop: &Receiver<()>) -> ResultType> { - let mut options = PropMap::new(); - options.insert( - "cursor-mode".into(), - Variant(Box::new(CURSOR_METADATA_MODE)), - ); - let proxy = self.conn.with_proxy(BUS, self.path.clone(), DBUS_TIMEOUT); - let (stream,): (Path<'static>,) = - proxy.method_call(SESSION_INTERFACE, "RecordMonitor", (connector, options))?; - let (sender, receiver) = mpsc::channel(); - let rule = MatchRule::new_signal(STREAM_INTERFACE, "PipeWireStreamAdded") - .with_sender(BUS) - .with_path(stream); - self.conn - .add_match(rule, move |(node,): (u32,), _, _| sender.send(node).is_ok())?; - proxy.method_call::<(), _, _, _>(SESSION_INTERFACE, "Start", ())?; - let started = Instant::now(); - while !stopped(stop) { - match receiver.try_recv() { - Ok(node) => return Ok(Some(node)), - Err(TryRecvError::Disconnected) => bail!("Mutter cursor node subscription closed"), - Err(TryRecvError::Empty) => {} - } - if started.elapsed() >= START_TIMEOUT { - bail!("Timed out waiting for the Mutter cursor node"); - } - self.conn.process(POLL_INTERVAL)?; - } - Ok(None) - } -} - -impl Drop for Session { - fn drop(&mut self) { - let result: Result<(), _> = self - .conn - .with_proxy(BUS, self.path.clone(), DBUS_TIMEOUT) - .method_call(SESSION_INTERFACE, "Stop", ()); - if let Err(error) = result { - log::error!("drm: could not stop the Mutter cursor session: {error}"); - } - } -} diff --git a/src/server/drm_capturer/cursor/ffi.rs b/src/server/drm_capturer/cursor/ffi.rs deleted file mode 100644 index 7019069c7..000000000 --- a/src/server/drm_capturer/cursor/ffi.rs +++ /dev/null @@ -1,232 +0,0 @@ -// The stable PipeWire 0.3/SPA C ABI; load it at runtime like the DRM capture library. -use hbb_common::{anyhow::anyhow, libloading::Library, ResultType}; -use std::{ - ffi::{c_char, c_int, c_void}, - mem::size_of, - sync::OnceLock, -}; - -pub type Handle = *mut c_void; -pub const META_CURSOR: u32 = 5; -pub const PARAM_FORMAT: u32 = 4; -pub const STREAM_ERROR: c_int = -1; -pub const STREAM_UNCONNECTED: c_int = 0; -pub const DIRECTION_INPUT: c_int = 0; -pub const AUTOCONNECT: u32 = 1; -pub const DONT_RECONNECT: u32 = 1 << 7; -const PARAM_ENUM_FORMAT: u32 = 3; -const PARAM_META: u32 = 6; -const TYPE_ID: u32 = 3; -const TYPE_INT: u32 = 4; -const TYPE_OBJECT: u32 = 15; -const TYPE_CHOICE: u32 = 19; -const CHOICE_RANGE: u32 = 1; -const OBJECT_FORMAT: u32 = 0x40003; -const OBJECT_META: u32 = 0x40005; -const FORMAT_MEDIA_TYPE: u32 = 1; -const FORMAT_MEDIA_SUBTYPE: u32 = 2; -const FORMAT_VIDEO: u32 = 0x20001; -const MEDIA_VIDEO: u32 = 2; -const MEDIA_RAW: u32 = 1; -const VIDEO_BGRA: u32 = 12; -const META_TYPE: u32 = 1; -const META_SIZE: u32 = 2; - -macro_rules! api { - ($($name:ident: $signature:ty),* $(,)?) => { - pub struct Api { - _library: Library, - $(pub $name: $signature,)* - } - - impl Api { - pub fn get() -> ResultType<&'static Self> { - static API: OnceLock> = OnceLock::new(); - API.get_or_init(|| unsafe { Self::load() }.map_err(|e| e.to_string())) - .as_ref().map_err(|e| anyhow!("PipeWire cursor library: {e}")) - } - - unsafe fn load() -> ResultType { - let library = Library::new("libpipewire-0.3.so.0")?; - let api = Self { - $($name: *library.get(concat!(stringify!($name), "\0").as_bytes())?,)* - _library: library, - }; - (api.pw_init)(std::ptr::null_mut(), std::ptr::null_mut()); - Ok(api) - } - } - }; -} - -api! { - pw_init: unsafe extern "C" fn(*mut c_int, *mut *mut *mut c_char), - pw_thread_loop_new: unsafe extern "C" fn(*const c_char, Handle) -> Handle, - pw_thread_loop_get_loop: unsafe extern "C" fn(Handle) -> Handle, - pw_thread_loop_start: unsafe extern "C" fn(Handle) -> c_int, - pw_thread_loop_stop: unsafe extern "C" fn(Handle), - pw_thread_loop_destroy: unsafe extern "C" fn(Handle), - pw_thread_loop_lock: unsafe extern "C" fn(Handle), - pw_thread_loop_unlock: unsafe extern "C" fn(Handle), - pw_properties_new_string: unsafe extern "C" fn(*const c_char) -> Handle, - pw_stream_new_simple: unsafe extern "C" fn(Handle, *const c_char, Handle, *const Events, Handle) -> Handle, - pw_stream_connect: unsafe extern "C" fn(Handle, c_int, u32, u32, *const *const Pod, u32) -> c_int, - pw_stream_update_params: unsafe extern "C" fn(Handle, *const *const Pod, u32) -> c_int, - pw_stream_dequeue_buffer: unsafe extern "C" fn(Handle) -> *mut PwBuffer, - pw_stream_queue_buffer: unsafe extern "C" fn(Handle, *mut PwBuffer) -> c_int, - pw_stream_destroy: unsafe extern "C" fn(Handle), -} - -#[repr(C)] -pub struct Events { - pub version: u32, - pub destroy: Option, - pub state_changed: Option, - pub control_info: Option, - pub io_changed: Option, - pub param_changed: Option, - pub add_buffer: Option, - pub remove_buffer: Option, - pub process: Option, - pub drained: Option, - pub command: Option, - pub trigger_done: Option, -} - -#[repr(C)] -pub struct PwBuffer { - pub buffer: *const Buffer, -} - -#[repr(C)] -pub struct Buffer { - pub n_metas: u32, - pub n_datas: u32, - pub metas: *const Meta, - pub datas: Handle, -} - -#[repr(C)] -pub struct Meta { - pub kind: u32, - pub size: u32, - pub data: Handle, -} - -#[repr(C)] -pub struct Pod { - size: u32, - kind: u32, -} - -#[repr(C)] -struct Property { - key: u32, - flags: u32, - pod: Pod, - value: u32, - padding: u32, -} - -impl Property { - fn new(key: u32, kind: u32, value: u32) -> Self { - Self { - key, - flags: 0, - pod: Pod { - size: size_of::() as u32, - kind, - }, - value, - padding: 0, - } - } -} - -#[repr(C, align(8))] -pub struct Object { - pub pod: Pod, - kind: u32, - id: u32, - properties: [Property; N], -} - -impl Object { - fn new(kind: u32, id: u32, properties: [Property; N]) -> Self { - Self { - pod: Pod { - size: (size_of::() - size_of::()) as u32, - kind: TYPE_OBJECT, - }, - kind, - id, - properties, - } - } -} - -pub fn video_format() -> Object<3> { - Object::new( - OBJECT_FORMAT, - PARAM_ENUM_FORMAT, - [ - Property::new(FORMAT_MEDIA_TYPE, TYPE_ID, MEDIA_VIDEO), - Property::new(FORMAT_MEDIA_SUBTYPE, TYPE_ID, MEDIA_RAW), - Property::new(FORMAT_VIDEO, TYPE_ID, VIDEO_BGRA), - ], - ) -} - -#[repr(C)] -struct SizeRange { - key: u32, - flags: u32, - pod: Pod, - choice: u32, - choice_flags: u32, - child: Pod, - values: [u32; 3], - padding: u32, -} - -#[repr(C, align(8))] -pub struct CursorMeta { - pub pod: Pod, - kind: u32, - id: u32, - meta_type: Property, - size: SizeRange, -} - -pub fn cursor_meta() -> CursorMeta { - CursorMeta { - pod: Pod { - size: (size_of::() - size_of::()) as u32, - kind: TYPE_OBJECT, - }, - kind: OBJECT_META, - id: PARAM_META, - meta_type: Property::new(META_TYPE, TYPE_ID, META_CURSOR), - size: SizeRange { - key: META_SIZE, - flags: 0, - pod: Pod { - size: (size_of::<[u32; 2]>() + size_of::() + size_of::<[u32; 3]>()) as u32, - kind: TYPE_CHOICE, - }, - choice: CHOICE_RANGE, - choice_flags: 0, - child: Pod { - size: size_of::() as u32, - kind: TYPE_INT, - }, - // Older Mutter allocates 64x64, newer versions 384x384. Let the producer choose. - values: [ - super::metadata::META_BYTES, - super::metadata::META_HEADER_BYTES, - i32::MAX as u32, - ], - padding: 0, - }, - } -} diff --git a/src/server/drm_capturer/cursor/metadata.rs b/src/server/drm_capturer/cursor/metadata.rs deleted file mode 100644 index 237de8efe..000000000 --- a/src/server/drm_capturer/cursor/metadata.rs +++ /dev/null @@ -1,255 +0,0 @@ -use super::DrmCursorData; -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, - io, - mem::size_of, -}; - -const RGBA: u32 = 11; -const PIXEL_BYTES: usize = 4; -const CURSOR_WORDS: usize = 7; -const BITMAP_WORDS: usize = 5; -const CURSOR_BYTES: usize = CURSOR_WORDS * size_of::(); -const BITMAP_BYTES: usize = BITMAP_WORDS * size_of::(); -pub const META_HEADER_BYTES: u32 = (CURSOR_BYTES + BITMAP_BYTES) as u32; -// Preferred allocation; negotiation accepts the compositor's metadata size. -const CURSOR_META_SIDE: usize = 384; -pub const META_BYTES: u32 = - META_HEADER_BYTES + (CURSOR_META_SIDE * CURSOR_META_SIDE * PIXEL_BYTES) as u32; - -#[derive(Default)] -pub struct CursorState { - image: Option, - published: Option, -} - -impl CursorState { - pub fn update(&mut self, data: &[u8]) -> io::Result> { - // Mutter uses id 0 outside this monitor or while the pointer is hidden. Reentry may - // contain only a position, so retain the sprite while publishing the hidden sentinel. - if words::(data)?[0] == 0 { - if self.published == Some(scrap::drm_reader::HIDDEN_CURSOR_ID) { - return Ok(None); - } - self.published = Some(scrap::drm_reader::HIDDEN_CURSOR_ID); - return Ok(Some(hidden_cursor())); - } - if let Some(cursor) = decode(data)? { - self.image = Some(cursor); - } - if let Some(cursor) = self.image.as_ref() { - if self.published != Some(cursor.id) { - self.published = Some(cursor.id); - return Ok(Some(cursor.clone())); - } - } - Ok(None) - } -} - -fn hidden_cursor() -> DrmCursorData { - DrmCursorData { - id: scrap::drm_reader::HIDDEN_CURSOR_ID, - width: 1, - height: 1, - hotx: 0, - hoty: 0, - colors: vec![0; PIXEL_BYTES], - } -} - -fn invalid(message: &str) -> io::Error { - io::Error::new(io::ErrorKind::InvalidData, message) -} - -fn words(data: &[u8]) -> io::Result<[u32; N]> { - let bytes = data - .get(..N * size_of::()) - .ok_or_else(|| invalid("Truncated PipeWire cursor metadata"))?; - let mut values = [0; N]; - for (value, bytes) in values.iter_mut().zip(bytes.chunks_exact(size_of::())) { - *value = u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); - } - Ok(values) -} - -fn decode(data: &[u8]) -> io::Result> { - let [id, _flags, _x, _y, hotx, hoty, offset] = words::(data)?; - // Position-only updates have no valid hotspot or bitmap. Keep the previous shape. - if id == 0 || offset == 0 { - return Ok(None); - } - if (offset as usize) < CURSOR_BYTES { - return Err(invalid("PipeWire cursor bitmap overlaps its header")); - } - let bitmap = data - .get(offset as usize..) - .ok_or_else(|| invalid("PipeWire cursor bitmap offset exceeds metadata"))?; - let [format, width, height, stride, offset] = words::(bitmap)?; - if offset == 0 { - return Ok(Some(hidden_cursor())); - } - if format == 0 { - return Ok(None); - } - let mut cursor = decode_bitmap(bitmap, [format, width, height, stride, offset])?; - cursor.hotx = hotx as i32; - cursor.hoty = hoty as i32; - let mut hash = DefaultHasher::new(); - (cursor.width, cursor.height, cursor.hotx, cursor.hoty).hash(&mut hash); - cursor.colors.hash(&mut hash); - // Mutter reuses id 1 for every sprite, including different hotspots. - cursor.id = hash.finish(); - Ok(Some(cursor)) -} - -fn decode_bitmap(data: &[u8], header: [u32; BITMAP_WORDS]) -> io::Result { - let [format, width, height, stride, offset] = header; - if format != RGBA { - return Err(invalid("Unsupported PipeWire cursor pixel format")); - } - if width == 0 || height == 0 || width > i32::MAX as u32 || height > i32::MAX as u32 { - return Err(invalid("Invalid PipeWire cursor dimensions")); - } - let row = (width as usize) - .checked_mul(PIXEL_BYTES) - .ok_or_else(|| invalid("PipeWire cursor row overflows"))?; - if (stride as i32) <= 0 || (stride as usize) < row || (offset as usize) < BITMAP_BYTES { - return Err(invalid("Invalid PipeWire cursor stride or pixel offset")); - } - let length = (stride as usize) - .checked_mul(height as usize - 1) - .and_then(|n| n.checked_add(row)) - .ok_or_else(|| invalid("PipeWire cursor bitmap size overflows"))?; - let pixels = data - .get(offset as usize..) - .and_then(|bytes| bytes.get(..length)) - .ok_or_else(|| invalid("Truncated PipeWire cursor pixels"))?; - let mut colors = Vec::with_capacity(row * height as usize); - for bytes in pixels.chunks(stride as usize) { - colors.extend_from_slice(&bytes[..row]); - } - Ok(DrmCursorData { - id: 0, - width: width as i32, - height: height as i32, - hotx: 0, - hoty: 0, - colors, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn packet(hotspot: [u32; 2]) -> Vec { - let mut bytes: Vec<_> = [ - 1, - 0, - 100, - 200, - hotspot[0], - hotspot[1], - CURSOR_BYTES as u32, - RGBA, - 96, - 96, - 96 * PIXEL_BYTES as u32, - BITMAP_BYTES as u32, - ] - .into_iter() - .flat_map(u32::to_ne_bytes) - .collect(); - bytes.resize(CURSOR_BYTES + BITMAP_BYTES + 96 * 96 * PIXEL_BYTES, 0); - let artwork = CURSOR_BYTES + BITMAP_BYTES + (4 * 96 + 4) * PIXEL_BYTES; - bytes[artwork..artwork + PIXEL_BYTES].copy_from_slice(&[128, 64, 32, 128]); - bytes - } - - #[test] - fn compositor_hotspot_is_independent_of_visible_bounds_and_id() { - let bytes = packet([42, 42]); - let cross = decode(&bytes).unwrap().unwrap(); - assert_eq!( - (cross.width, cross.height, cross.hotx, cross.hoty), - (96, 96, 42, 42) - ); - assert_eq!(&cross.colors, &bytes[CURSOR_BYTES + BITMAP_BYTES..]); - let origin = decode(&packet([0, 0])).unwrap().unwrap(); - assert_eq!((origin.hotx, origin.hoty), (0, 0)); - assert_ne!(cross.id, origin.id); - let mut changed = bytes.clone(); - *changed.last_mut().unwrap() = 255; - assert_ne!(cross.id, decode(&changed).unwrap().unwrap().id); - } - - #[test] - fn movement_keeps_the_shape_and_empty_bitmap_hides_it() { - let mut bytes = packet([42, 45]); - bytes[CURSOR_BYTES - size_of::()..CURSOR_BYTES].fill(0); - assert!(decode(&bytes).unwrap().is_none()); - bytes[..size_of::()].fill(0); - assert!(decode(&bytes).unwrap().is_none()); - let mut hidden = packet([42, 45]); - hidden[CURSOR_BYTES..CURSOR_BYTES + BITMAP_BYTES].fill(0); - let hidden = decode(&hidden).unwrap().unwrap(); - assert_eq!(hidden.id, scrap::drm_reader::HIDDEN_CURSOR_ID); - assert_eq!(hidden.colors, [0; PIXEL_BYTES]); - } - - #[test] - fn leaving_and_returning_to_a_monitor_restores_the_cached_sprite() { - let mut state = CursorState::default(); - let mut bytes = packet([42, 42]); - let initial = state.update(&bytes).unwrap().unwrap(); - bytes[..size_of::()].fill(0); - let hidden = state - .update(&bytes) - .unwrap() - .expect("Mutter id 0 hides the cursor"); - assert_eq!(hidden.id, scrap::drm_reader::HIDDEN_CURSOR_ID); - assert!(state.update(&bytes).unwrap().is_none()); - bytes[..size_of::()].copy_from_slice(&1u32.to_ne_bytes()); - bytes[CURSOR_BYTES - size_of::()..CURSOR_BYTES].fill(0); - let restored = state - .update(&bytes) - .unwrap() - .expect("Position-only reentry restores the sprite"); - assert_eq!( - (restored.id, restored.hotx, restored.hoty), - (initial.id, 42, 42) - ); - assert_eq!(restored.colors, initial.colors); - assert!(state.update(&bytes).unwrap().is_none()); - } - - #[test] - fn malformed_metadata_is_an_error() { - let bytes = packet([12, 3]); - for length in [ - 0, - CURSOR_BYTES - 1, - CURSOR_BYTES + BITMAP_BYTES - 1, - bytes.len() - 1, - ] { - assert!(decode(&bytes[..length]).is_err()); - } - for (word, value) in [ - (6, 1), - (6, u32::MAX), - (7, 99), - (8, 0), - (9, u32::MAX), - (10, 1), - (10, u32::MAX), - (11, 1), - ] { - let mut invalid = bytes.clone(); - invalid[word * size_of::()..(word + 1) * size_of::()] - .copy_from_slice(&value.to_ne_bytes()); - assert!(decode(&invalid).is_err(), "word {word}, value {value}"); - } - } -} diff --git a/src/server/drm_capturer/cursor/pipewire.rs b/src/server/drm_capturer/cursor/pipewire.rs deleted file mode 100644 index 75e8d4760..000000000 --- a/src/server/drm_capturer/cursor/pipewire.rs +++ /dev/null @@ -1,218 +0,0 @@ -use super::{ffi, metadata, DrmCursorData}; -use hbb_common::{anyhow::anyhow, bail, ResultType}; -use std::{ - cell::UnsafeCell, - ffi::{c_char, c_int, CStr}, - ptr, slice, -}; - -struct State { - api: &'static ffi::Api, - stream: ffi::Handle, - publish: Box, - error: Option, - received: bool, - cursor: metadata::CursorState, -} - -pub struct Stream { - api: &'static ffi::Api, - thread: ffi::Handle, - // PipeWire callbacks own this state while holding the thread-loop lock. - state: Box>, - started: bool, -} - -impl Stream { - pub fn new(node: u32, publish: impl FnMut(DrmCursorData) + Send + 'static) -> ResultType { - let api = ffi::Api::get()?; - let thread = unsafe { - (api.pw_thread_loop_new)(b"rustdesk-cursor\0".as_ptr().cast(), ptr::null_mut()) - }; - if thread.is_null() { - bail!("Could not create the PipeWire cursor loop"); - } - let mut stream = Self { - api, - thread, - started: false, - state: Box::new(UnsafeCell::new(State { - api, - stream: ptr::null_mut(), - publish: Box::new(publish), - error: None, - received: false, - cursor: metadata::CursorState::default(), - })), - }; - stream.connect(node)?; - Ok(stream) - } - - fn connect(&mut self, node: u32) -> ResultType<()> { - // The loop has not started; callbacks during setup run synchronously on this thread. - unsafe { - let properties = (self.api.pw_properties_new_string)( - b"media.type=Video media.category=Capture media.role=Screen\0" - .as_ptr() - .cast(), - ); - if properties.is_null() { - bail!("Could not create PipeWire cursor properties"); - } - let stream = (self.api.pw_stream_new_simple)( - (self.api.pw_thread_loop_get_loop)(self.thread), - b"RustDesk cursor\0".as_ptr().cast(), - properties, - &EVENTS, - self.state.get().cast(), - ); - (*self.state.get()).stream = stream; - if stream.is_null() { - bail!("Could not create the PipeWire cursor stream"); - } - let format = ffi::video_format(); - let params = [&format.pod as *const _]; - check((self.api.pw_stream_connect)( - stream, - ffi::DIRECTION_INPUT, - node, - ffi::AUTOCONNECT | ffi::DONT_RECONNECT, - params.as_ptr(), - params.len() as u32, - ))?; - check((self.api.pw_thread_loop_start)(self.thread))?; - self.started = true; - } - Ok(()) - } - - pub fn received(&self) -> ResultType { - unsafe { - (self.api.pw_thread_loop_lock)(self.thread); - let state = &*self.state.get(); - let result = match &state.error { - Some(error) => Err(anyhow!("PipeWire cursor: {error}")), - None => Ok(state.received), - }; - (self.api.pw_thread_loop_unlock)(self.thread); - result - } - } -} - -impl Drop for Stream { - fn drop(&mut self) { - unsafe { - if self.started { - (self.api.pw_thread_loop_stop)(self.thread); - } - let stream = (*self.state.get()).stream; - if !stream.is_null() { - (self.api.pw_stream_destroy)(stream); - } - (self.api.pw_thread_loop_destroy)(self.thread); - } - } -} - -fn check(result: c_int) -> ResultType<()> { - if result < 0 { - bail!("{}", std::io::Error::from_raw_os_error(-result)); - } - Ok(()) -} - -unsafe extern "C" fn state_changed( - data: ffi::Handle, - old: c_int, - state: c_int, - error: *const c_char, -) { - let context = &mut *data.cast::(); - if state == ffi::STREAM_ERROR || (state == ffi::STREAM_UNCONNECTED && old != state) { - context.error = Some(if error.is_null() { - "Cursor stream disconnected".to_owned() - } else { - CStr::from_ptr(error).to_string_lossy().into_owned() - }); - } -} - -unsafe extern "C" fn param_changed(data: ffi::Handle, id: u32, param: *const ffi::Pod) { - if id != ffi::PARAM_FORMAT || param.is_null() { - return; - } - let context = data.cast::(); - let meta = ffi::cursor_meta(); - let params = [&meta.pod as *const _]; - if let Err(error) = check(((*context).api.pw_stream_update_params)( - (*context).stream, - params.as_ptr(), - params.len() as u32, - )) { - (*context).error = Some(error.to_string()); - } -} - -unsafe fn cursor( - decoder: &mut metadata::CursorState, - buffer: *const ffi::Buffer, -) -> ResultType> { - let buffer = buffer - .as_ref() - .ok_or_else(|| anyhow!("Missing PipeWire buffer"))?; - if buffer.metas.is_null() { - bail!("PipeWire did not negotiate cursor metadata"); - } - let metas = slice::from_raw_parts(buffer.metas, buffer.n_metas as usize); - let meta = metas - .iter() - .find(|meta| meta.kind == ffi::META_CURSOR) - .ok_or_else(|| anyhow!("PipeWire did not negotiate cursor metadata"))?; - if meta.data.is_null() { - bail!("Missing PipeWire cursor metadata payload"); - } - Ok(decoder.update(slice::from_raw_parts(meta.data.cast(), meta.size as usize))?) -} - -unsafe extern "C" fn process(data: ffi::Handle) { - let state = data.cast::(); - let buffer = ((*state).api.pw_stream_dequeue_buffer)((*state).stream); - if buffer.is_null() { - return; - } - let result = cursor(&mut (*state).cursor, (*buffer).buffer); - let queued = check(((*state).api.pw_stream_queue_buffer)( - (*state).stream, - buffer, - )); - let context = &mut *state; - if context.error.is_some() { - return; - } - match result.and_then(|cursor| queued.map(|_| cursor)) { - Ok(cursor) => { - context.received = true; - if let Some(cursor) = cursor { - (context.publish)(cursor); - } - } - Err(error) => context.error = Some(error.to_string()), - } -} - -static EVENTS: ffi::Events = ffi::Events { - version: 2, - destroy: None, - state_changed: Some(state_changed), - control_info: None, - io_changed: None, - param_changed: Some(param_changed), - add_buffer: None, - remove_buffer: None, - process: Some(process), - drained: None, - command: None, - trigger_done: None, -}; diff --git a/src/server/drm_capturer/cursor/tests.rs b/src/server/drm_capturer/cursor/tests.rs deleted file mode 100644 index 0118b7681..000000000 --- a/src/server/drm_capturer/cursor/tests.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::*; - -fn capture() -> Capture { - Capture { - stop: None, - thread: None, - error: Arc::new(Mutex::new(None)), - ready: Arc::new(Mutex::new(false)), - wire_cursor: None, - } -} - -fn sprite(id: u64) -> DrmCursorData { - DrmCursorData { - id, - width: 1, - height: 1, - hotx: 0, - hoty: 0, - colors: vec![255; 4], - } -} - -#[test] -fn wire_cursor_remains_active_until_a_sprite_is_published() { - let capture = capture(); - assert!(!*capture.ready()); - let target = (-1614401, 1); - publish(target, &capture.ready, sprite(7)); - assert!(*capture.ready()); - let mut cursors = super::super::DRM_CURSOR.lock().unwrap(); - assert_eq!(cursors.remove(&target.0).unwrap().1.id, 7); -} - -#[test] -fn wire_publication_cannot_overwrite_the_first_mutter_sprite() { - let capture = capture(); - let target = (-1614402, 1); - let ready = capture.ready(); - assert!(!*ready); - let worker_ready = capture.ready.clone(); - let (started, receiver) = mpsc::channel(); - let worker = thread::spawn(move || { - started.send(()).unwrap(); - publish(target, &worker_ready, sprite(9)); - }); - receiver.recv_timeout(Duration::from_secs(1)).unwrap(); - super::super::set_drm_cursor(target.0, target.1, sprite(8)); - drop(ready); - worker.join().unwrap(); - assert!(*capture.ready()); - let mut cursors = super::super::DRM_CURSOR.lock().unwrap(); - assert_eq!(cursors.remove(&target.0).unwrap().1.id, 9); -}