From 5819cf054b23ad42fff8d1b6d3a448beb53ab6e5 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 8 Aug 2026 18:15:04 +0200 Subject: [PATCH] =?UTF-8?q?feat(windows/cursor):=20clear=20a=20sticky=20ha?= =?UTF-8?q?rdware-cursor=20declare=20at=20start-up=20=E2=80=94=20capture?= =?UTF-8?q?=20sessions=20get=20the=20OS's=20own=20pointer=20back?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The goal this serves: a session that never engages desktop mode should get the LOSSLESS cursor — composited by Windows itself, for free, with true XOR — and the host should only pay for compositing when the user actually asks for the desktop mouse model. That is already what happens on a clean adapter. The problem is that adapters do not stay clean. A hardware-cursor declare is irrevocable and ADAPTER-WIDE (pf-driver-proto v6): once any desktop-mode session declares, DWM stops compositing the pointer into every later frame on that adapter, so every capture-latched session afterwards has to self-composite — a full-frame copy per visible-pointer frame, and our straight-alpha approximation of an XOR cursor instead of the real thing — for the rest of the adapter's life. "Until the next reboot" turned out to be far longer than it sounds. With Fast Startup on (the Windows default) a shutdown plus power-on is a HIBERBOOT: it restores session 0 and its drivers, so the declare survives what the operator calls a reboot. Measured on .173 — Kernel-Boot event id 27 reporting `0x1` where a cold boot reports `0x0`, with lsass/services/wininit keeping their pre-"reboot" start times, while `LastBootUpTime` reports the older cold boot and makes uptime checks lie. On such a box the lossless path can be gone for weeks. So clear it explicitly at host start, where no session holds a display yet. `pnputil /restart-device` recycles the WUDFHost process the driver's `DECLARED_TARGETS` lives in, which is all it takes. Measured at **0.07 s** against ~6 s of sleeps for the existing Disable+Enable cycle, and unlike that cycle it is designed for a device in use, so it does not hit the refusal `reload_vdisplay_adapter` documents as "the expected case here". In the same call it also repaired an adapter found in CM_PROB_FAILED_POST_START (Code 43). Best-effort throughout: a failure just leaves the adapter as it was and sessions self-composite exactly as before. `PUNKTFUNK_CURSOR_CLEAN_START=0` opts out. Verified: `scripts/xcheck.sh windows` green; `cargo fmt --all` clean; full `cargo build -p punktfunk-host --release --features nvenc` on .173 (xcheck cannot reach punktfunk-host — it needs ffmpeg). --- .../src/vdisplay/windows/pf_vdisplay.rs | 80 +++++++++++++++++++ crates/punktfunk-host/src/main.rs | 17 ++++ 2 files changed, 97 insertions(+) diff --git a/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs b/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs index 9c5cf45a..3a0cb0aa 100644 --- a/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs +++ b/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs @@ -158,6 +158,86 @@ enum AdapterCycle { Refused(String), } +/// Restart the pf-vdisplay device to CLEAR a sticky IddCx hardware-cursor declare, so sessions that +/// do not want the host to own the pointer get the OS's own cursor compositing back (full fidelity, +/// zero host cost — no GDI poller, no per-frame blend, true XOR instead of our outline +/// approximation). +/// +/// **Why this exists.** A hardware-cursor declare is irrevocable and ADAPTER-WIDE +/// (`pf-driver-proto` v6 note): once any desktop-mode session declares, DWM stops compositing the +/// pointer into EVERY later frame on that adapter, and every subsequent session — including +/// capture-latched ones that never asked for a cursor channel — has to self-composite. The state +/// lives in the driver's `DECLARED_TARGETS`, whose scope is the WUDFHost process, so recycling that +/// process clears it. +/// +/// **Why `/restart-device` and not the [`reload_vdisplay_adapter`] cycle.** Measured on-glass +/// 2026-08-08 (`.173`): `pnputil /restart-device` returned in **0.07 s** with a NEW WUDFHost pid, +/// against ~6 s of sleeps for `Disable`+`Enable` — and, being designed for a device that is in use, +/// it does not hit the refusal that doc calls "the expected case here". It also repaired an adapter +/// found in `CM_PROB_FAILED_POST_START` (Code 43) in the same call. +/// +/// ⚠ This tears the adapter down, so it must run only when NO session holds a display — the host +/// start-up path. `PUNKTFUNK_CURSOR_CLEAN_START=0` disables it. +/// +/// Returns `true` only when pnputil reported success. Best-effort: a failure just leaves the +/// adapter as it was (sessions then self-composite exactly as before). +pub fn restart_device_for_clean_cursor() -> bool { + if std::env::var("PUNKTFUNK_CURSOR_CLEAN_START").is_ok_and(|v| v == "0") { + tracing::info!( + "pf-vdisplay: cursor clean-start disabled (PUNKTFUNK_CURSOR_CLEAN_START=0) — a sticky \ + hardware-cursor declare from an earlier boot will keep sessions self-compositing" + ); + return false; + } + // `$LASTEXITCODE` is pre-seeded to 1 for the same reason `reload_vdisplay_adapter` does it: if + // pnputil never launches, a stale value must not read as success. + const PS: &str = "$ErrorActionPreference='SilentlyContinue'; \ + $ad = Get-PnpDevice -Class Display | Where-Object { $_.FriendlyName -match 'punktfunk Virtual Display' } | Select-Object -First 1; \ + if (-not $ad) { Write-Output 'ABSENT'; exit }; \ + $pnp = ($env:SystemRoot + '\\System32\\pnputil.exe'); $LASTEXITCODE = 1; \ + if (Test-Path $pnp) { & $pnp /restart-device $ad.InstanceId *> $null }; \ + if ($LASTEXITCODE -eq 0) { Write-Output 'RESTARTED' } else { Write-Output 'FAILED' }"; + let ps = std::env::var("SystemRoot") + .map(|r| format!(r"{r}\System32\WindowsPowerShell\v1.0\powershell.exe")) + .unwrap_or_else(|_| "powershell.exe".to_string()); + let out = match std::process::Command::new(&ps) + .args([ + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-Command", + PS, + ]) + .output() + { + Ok(o) => String::from_utf8_lossy(&o.stdout).trim().to_string(), + Err(e) => { + tracing::warn!(error = %e, "pf-vdisplay: cursor clean-start could not spawn powershell"); + return false; + } + }; + match out.as_str() { + "RESTARTED" => { + tracing::info!( + "pf-vdisplay: restarted the adapter at start-up — any sticky hardware-cursor \ + declare is cleared, so sessions without a cursor channel get the OS's own \ + (full-fidelity, zero-cost) pointer compositing until one declares again" + ); + true + } + "ABSENT" => false, // driver not installed — nothing to clean, and `open` reports that later + other => { + tracing::warn!( + outcome = other, + "pf-vdisplay: cursor clean-start did not restart the adapter — sessions without a \ + cursor channel will self-composite the pointer if an earlier declare is sticky" + ); + false + } + } +} + /// Reload the pf-vdisplay ADAPTER device — the in-process equivalent of `reset-pf-vdisplay.ps1` /// step 3. A crashed/killed WUDFHost can leave the devnode "started" yet HOSTLESS (PnP Status OK, no /// WUDFHost process, zero device-interface instances) — a zombie no session can open until the stack diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs index ed28763d..bd48ddc4 100644 --- a/crates/punktfunk-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -382,6 +382,23 @@ fn real_main() -> Result<()> { // driver to a stray second host started while the service sat idle. #[cfg(target_os = "windows")] vdisplay::manager::claim_instance_eagerly(); + // Clean-cursor start (design/windows-cursor-model-determinism.md §4.3): clear any + // sticky IddCx hardware-cursor declare left on the adapter by an EARLIER boot's + // desktop-mode session. That declare is irrevocable and adapter-wide, so without this + // every capture-latched session on the box self-composites the pointer for the rest of + // the adapter's life — paying a full-frame copy per visible-pointer frame and drawing + // our straight-alpha approximation of an XOR cursor — when the OS would otherwise + // composite it natively, for free, at full fidelity. + // + // It is NOT enough to wait for a reboot: with Fast Startup on (the Windows default) a + // shutdown+power-on is a hiberboot that RESTORES session 0 and its drivers, so the + // declare survives what the operator calls a reboot (measured: Kernel-Boot event id 27 + // `0x1`, and `lsass`/`services` keeping their pre-"reboot" start times). Only a cold + // boot or a device restart actually clears it — and the device restart costs 0.07 s. + // + // Runs HERE, before any session holds a display: the restart tears the adapter down. + #[cfg(target_os = "windows")] + vdisplay::driver::restart_device_for_clean_cursor(); // Crash recovery for the experimental `pnp_disable_monitors` axis: re-enable any // monitor devnodes a previous host disabled for an Exclusive session and never // restored (crash/kill/power loss) — before any new session touches the topology.