From 36c3b11a7f5c6a06dd23165673df7dfe1eace6f4 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Tue, 16 Jun 2026 08:30:49 +0000 Subject: [PATCH] perf(host/windows): SendInput retry-on-failure model (two-process step 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The injector reattached the input desktop (OpenInputDesktop + SetThreadDesktop, two syscalls) before EVERY event. Now it stays bound to its desktop and only reattaches on a SendInput short write (the input desktop switched into UAC/lock) + retries once — Sunshine's model. No steady-state per-event overhead; still follows the desktop across the secure boundary, serving both desktops. Validated on the RTX 4090 (host as SYSTEM): client-rs --input-test injected for ~6s with no "blocked desktop" errors. Completes all 6 steps of the two-process secure-desktop build; only a real-UAC user smoke test remains. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/punktfunk-host/src/inject/sendinput.rs | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/crates/punktfunk-host/src/inject/sendinput.rs b/crates/punktfunk-host/src/inject/sendinput.rs index 19b6d931..208702b5 100644 --- a/crates/punktfunk-host/src/inject/sendinput.rs +++ b/crates/punktfunk-host/src/inject/sendinput.rs @@ -1,8 +1,9 @@ //! Windows input injection via `SendInput` (Win32 KeyboardAndMouse) — the Windows analogue of //! [`super::wlr`]: absolute mouse normalized to the virtual desktop, relative mouse for games, //! scancode keyboard, scroll, buttons. The client already sends Windows VK codes, so there is no -//! keycode table. Survives UAC/lock desktop switches by reattaching the thread to the current -//! input desktop before each event (`OpenInputDesktop`/`SetThreadDesktop`). +//! keycode table. Survives UAC/lock desktop switches with Sunshine's retry-on-failure model: the +//! thread stays bound to its desktop and only reattaches (`OpenInputDesktop`/`SetThreadDesktop`) when +//! `SendInput` reports a short write (the input desktop switched) — no per-event reattach overhead. use anyhow::Result; use punktfunk_core::input::{InputEvent, InputKind}; @@ -68,11 +69,20 @@ impl SendInputInjector { } } - fn send(inputs: &[INPUT]) -> Result<()> { + /// Inject with Sunshine's retry-on-failure model: the thread stays bound to whatever desktop it + /// last attached to (no per-event `OpenInputDesktop`/`SetThreadDesktop` — two syscalls saved on + /// every mouse move), and only when `SendInput` reports a short write (0 = the input desktop + /// switched out from under us, e.g. into UAC/lock) do we reattach to the now-current input desktop + /// and retry once. This serves both the normal and secure desktops with no steady-state overhead. + fn send(&mut self, inputs: &[INPUT]) -> Result<()> { + let n = unsafe { SendInput(inputs, size_of::() as i32) }; + if n as usize == inputs.len() { + return Ok(()); + } + // Short write → the input desktop likely changed. Reattach + retry once. + self.reattach_input_desktop(); let n = unsafe { SendInput(inputs, size_of::() as i32) }; if n as usize != inputs.len() { - // 0 = blocked (different/secure desktop). Surface as Err so the host service drops + - // reopens the injector (which reattaches the input desktop). anyhow::bail!( "SendInput injected {n}/{} events (blocked desktop?)", inputs.len() @@ -94,7 +104,8 @@ impl Drop for SendInputInjector { impl InputInjector for SendInputInjector { fn inject(&mut self, event: &InputEvent) -> Result<()> { - self.reattach_input_desktop(); + // No per-event desktop reattach — `send` reattaches lazily only on a short write (desktop + // switch). The injector is bound to the input desktop at open() and follows switches on demand. match event.kind { InputKind::MouseMove => { let mi = MOUSEINPUT { @@ -105,7 +116,7 @@ impl InputInjector for SendInputInjector { time: 0, dwExtraInfo: 0, }; - Self::send(&[mouse(mi)]) + self.send(&[mouse(mi)]) } InputKind::MouseMoveAbs => { let w = (event.flags >> 16) & 0xffff; @@ -128,7 +139,7 @@ impl InputInjector for SendInputInjector { time: 0, dwExtraInfo: 0, }; - Self::send(&[mouse(mi)]) + self.send(&[mouse(mi)]) } InputKind::MouseButtonDown | InputKind::MouseButtonUp => { let down = event.kind == InputKind::MouseButtonDown; @@ -183,7 +194,7 @@ impl InputInjector for SendInputInjector { time: 0, dwExtraInfo: 0, }; - Self::send(&[mouse(mi)]) + self.send(&[mouse(mi)]) } InputKind::MouseScroll => { // GameStream WHEEL_DELTA(120) units. Windows WHEEL positive=up (matches GameStream — @@ -201,7 +212,7 @@ impl InputInjector for SendInputInjector { time: 0, dwExtraInfo: 0, }; - Self::send(&[mouse(mi)]) + self.send(&[mouse(mi)]) } InputKind::KeyDown | InputKind::KeyUp => { let down = event.kind == InputKind::KeyDown; @@ -226,7 +237,7 @@ impl InputInjector for SendInputInjector { time: 0, dwExtraInfo: 0, }; - Self::send(&[key(ki)]) + self.send(&[key(ki)]) } // Gamepad goes through ViGEm (separate backend). Touch: no SendInput equivalent -> no-op. InputKind::GamepadButton