From 62624c1daf22aa449b49d46a6a7853e4ac13d212 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 8 Aug 2026 17:03:33 +0200 Subject: [PATCH] =?UTF-8?q?feat(probe):=20--cursor-hold=20=E2=80=94=20stop?= =?UTF-8?q?=20the=20wiggle=20so=20the=20pointer=20can=20be=20parked=20on?= =?UTF-8?q?=20a=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The capture-model repro flags (`--cursor-capture` / `--cursor-nochannel`) drive relative pointer motion in circles for the whole dump, which is right for keeping a damage-driven desktop publishing frames but makes the pointer impossible to aim: at radius 10 every 25 ms it walks several hundred pixels a second, so a `SetCursorPos` on the host is undone before the next frame. That mattered because the shape UNDER the pointer is the whole question. The arrow is a colour cursor and proves nothing about the monochrome path — the I-beam is the only common Windows system cursor with `hbmColor == null`, so it is the only one that exercises `mono_planes_to_rgba`. Without being able to park the pointer on a text field there is no way to photograph the case that matters. `--cursor-hold` primes the wiggle for ~3 s (enough to clear CURSOR_SUPPRESSED and get metadata flowing) and then stops, leaving the pointer wherever the host puts it for the rest of the dump. Used it to settle the question on .173: a `--cursor-nochannel` session — the byte-identical wire behaviour of an iPad/Android/tvOS client, which never advertises CLIENT_CAP_CURSOR — receives a correctly rendered monochrome I-beam in the video. Decoded from the dump with ffmpeg; the host log shows the session took the forced-composite path with the GDI poller live. Verified: `scripts/xcheck.sh windows` green; `cargo fmt --all` clean. --- clients/probe/src/main.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/clients/probe/src/main.rs b/clients/probe/src/main.rs index 399704f0..869dd6ff 100644 --- a/clients/probe/src/main.rs +++ b/clients/probe/src/main.rs @@ -129,6 +129,16 @@ struct Args { /// host must composite the metadata cursor on its own; decode the dump and look for the /// pointer. cursor_nochannel: bool, + /// `--cursor-hold` — with `--cursor-capture`/`--cursor-nochannel`, stop the relative wiggle + /// after a short priming burst instead of circling forever. The wiggle exists to keep a + /// damage-driven desktop publishing frames, but it also DRAGS the host pointer several hundred + /// pixels a second, which makes it impossible to hold the pointer over a chosen target — and + /// the shape under the pointer is the whole point when the question is "does the MONOCHROME + /// I-beam survive compositing?" (the arrow is a colour cursor and proves nothing about the + /// mono path). With this flag: prime for ~3 s so the pointer is un-suppressed and metadata is + /// flowing, then hold still so a `SetCursorPos` on the host can park it on a text field for + /// the rest of the dump. + cursor_hold: bool, /// `--discover [SECS]` — browse the LAN for native (`_punktfunk._udp`) hosts for `SECS` /// seconds (default 4), print what's found, and exit. No connection is made. discover: Option, @@ -309,6 +319,7 @@ fn parse_args() -> Args { clock_resync: argv.iter().any(|a| a == "--clock-resync"), cursor_capture: argv.iter().any(|a| a == "--cursor-capture"), cursor_nochannel: argv.iter().any(|a| a == "--cursor-nochannel"), + cursor_hold: argv.iter().any(|a| a == "--cursor-hold"), } } @@ -900,13 +911,23 @@ async fn session(args: Args) -> Result<()> { } }); let wiggle_conn = conn.clone(); + let hold = args.cursor_hold; tokio::spawn(async move { - // Relative circles, forever: keeps the host pointer moving (and, on metadata-cursor - // compositors, keeps cursor updates flowing) for the whole dump. + // Relative circles: keeps the host pointer moving (and, on metadata-cursor + // compositors, keeps cursor updates flowing) for the whole dump — unless + // `--cursor-hold`, which primes and then stops so the pointer can be parked. tokio::time::sleep(std::time::Duration::from_secs(2)).await; - tracing::info!("cursor-capture: relative pointer wiggle running"); + tracing::info!(hold, "cursor-capture: relative pointer wiggle running"); + let prime_until = std::time::Instant::now() + std::time::Duration::from_secs(3); let mut t = 0.0f64; loop { + if hold && std::time::Instant::now() >= prime_until { + tracing::info!( + "cursor-capture: wiggle primed and STOPPED (--cursor-hold) — the pointer \ + now stays where the host puts it" + ); + return; + } let e = InputEvent { kind: InputKind::MouseMove, _pad: [0; 3],