cursor: name a shape by what it looks like, not by its handle

A cursor id was the platform's handle for the shape: HCURSOR on
Windows, the XFixes serial on X11. Apps mint a new handle for a shape
they have shown before (Chrome, Electron, Qt custom cursors), and an
X11 animated cursor gets one per frame, so one arrow arrived under
thousands of ids over a session. The controlled side cached and sent
it again under each, and the controller decoded, rendered and kept it
again under each, and grew by gigabytes over weeks of connection.

The controlled side now names a shape by an xxh3 hash of its size,
hotspot and pixels, taken once when a handle is first seen, before
the pixels are compressed. The per-connection send already sends a
shape once and afterwards only its id, so a shape now crosses once
however many handles it has. The id is opaque in the protocol, so
controllers of any version accept it. The DRM backend already named
shapes by content with FNV-1a; it uses the same hash now.

A peer before 1.5.0 still names shapes by handle, and the fix has to
work against every deployed peer. For those peers the controller
hashes each CursorData's compressed colors with its geometry: one
peer compresses the same pixels to the same bytes, so a shape seen
before is recognised without being decompressed. The UI gets that
shape once, under one id, and every later handle for it is passed on
as a cursor_id of that id. The map from the peer's ids is kept for the
connection, unbounded, since the peer sends a shape once and may
select any id it named again; an entry is two integers. From 1.5.0
the controller passes the peer's ids through unchanged, on the old
path.

The web core in flutter/web does the same in its own tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
This commit is contained in:
rustdeskandClaude Opus 5.5 committed 2026-09-23 19:52:42 +08:00
1 parent 58ff78a823
commit d64ff4961b
8 files changed
+177 -11

No files matched your search

+2 -1
View File
@@ -20,7 +20,7 @@ wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "
# Depends on `wayland`: the three drm modules live inside the `#[cfg(feature = "wayland")]` arm of
# common/mod.rs, so `scrap/drm` on its own would compile nothing. The root crate happens to always
# enable `scrap/wayland`, which is what hid this.
drm = ["wayland", "base/wayland_probe"]
drm = ["wayland", "base/wayland_probe", "dep:xxhash-rust"]
mediacodec = ["ndk"]
linux-pkg-config = ["dep:pkg-config"]
hwcodec = ["dep:hwcodec"]
@@ -69,6 +69,7 @@ gstreamer = { version = "0.16", optional = true }
gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
gstreamer-video = { version = "0.16", optional = true }
zbus = { version = "3.15", optional = true }
xxhash-rust = { version = "0.8", features = ["xxh3"], optional = true }
[dependencies.hwcodec]
git = "https://github.com/rustdesk-org/hwcodec"
+5 -7
View File
@@ -50,18 +50,18 @@ pub struct CursorSnapshot {
/// unchanged but whose provenance flipped is a different cursor as far as the client is concerned,
/// and without this it would be deduped away and never sent.
fn cursor_id(hash: u64, width: u32, height: u32, hotx: i32, hoty: i32, hot_measured: bool) -> u64 {
let mut id = hash;
let mut hasher = xxhash_rust::xxh3::Xxh3::new();
for v in [
hash,
width as u64,
height as u64,
hotx as u32 as u64,
hoty as u32 as u64,
hot_measured as u64,
] {
id ^= v;
id = id.wrapping_mul(1099511628211);
hasher.update(&v.to_le_bytes());
}
id
hasher.digest()
}
/// Encodes a provenance answer for the one-shot log below. -1 is "nothing reported yet".
@@ -649,16 +649,14 @@ impl DrmReader {
let ch = c.height as i32;
let n = (cw * ch) as usize;
let src = std::slice::from_raw_parts(c.pixels, n);
let mut hash: u64 = 1469598103934665603;
let mut colors = Vec::with_capacity(n * 4);
for &p in src.iter() {
colors.push(((p >> 16) & 0xff) as u8);
colors.push(((p >> 8) & 0xff) as u8);
colors.push((p & 0xff) as u8);
colors.push(((p >> 24) & 0xff) as u8);
hash ^= p as u64;
hash = hash.wrapping_mul(1099511628211);
}
let hash = xxhash_rust::xxh3::xxh3_64(&colors);
// Ask the library where the hotspot came from instead of guessing from the
// coordinates. `hot_x/hot_y == (0, 0)` means two opposite things - the plane
// exposes no HOTSPOT_X/Y (every bare-metal driver), or it exposes them and the