refactor(windows-host): confine platform code under windows/ + linux/ folders (Goal-1 stage 6)

Move 36 platform-specific files into per-module `windows/` and `linux/` subfolders (and the
shared HID codecs into `inject/proto/`):
  capture/{windows,linux}/  encode/{windows,linux}/  inject/{windows,linux,proto}/
  audio/{windows,linux}/  vdisplay/{windows,linux}/
  src/windows/ (service, wgc_helper, win_adapter, win_display)
  src/linux/  (dmabuf_fence, drm_sync, zerocopy/)

Done with `#[path]`, NOT a module rename: every file moves into its folder while the
`crate::*::*` module names stay FLAT, so all caller paths and every internal `super::`/`crate::`
reference are unchanged — only the parent `mod` decls gained `#[path = "..."]`. This is the
codebase's existing pattern (inject's gamepad_windows) and makes the move byte-identical in
behaviour with ZERO reference churn, far lower risk than collapsing to a single
`crate::capture::windows::` namespace (that deeper rename is an optional follow-on; this delivers
the cfg-sprawl folder confinement the stage is about). Done LAST, after the semantic stages, so
the path churn didn't fight them.

Verified: Linux cargo check + clippy (-D warnings) clean; my mod-decl changes fmt-clean (the 3
remaining fmt diffs are pre-existing local-rustfmt-version skew that moved with their files); all
36 `#[path]` targets exist; no internal `#[path]`/`include!`/file-child-mod in any moved file
(the inline `mod X {` blocks are self-contained). Box build to follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 18:53:45 +00:00
parent a0427cd2a3
commit 38c68c33e5
49 changed files with 62 additions and 6 deletions
@@ -0,0 +1,286 @@
//! 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 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};
use std::mem::size_of;
use windows::Win32::System::StationsAndDesktops::{
CloseDesktop, OpenInputDesktop, SetThreadDesktop, DESKTOP_ACCESS_FLAGS, DESKTOP_CONTROL_FLAGS,
HDESK,
};
use windows::Win32::UI::Input::KeyboardAndMouse::{
MapVirtualKeyExW, SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT,
KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, MAPVK_VK_TO_VSC_EX,
MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP,
MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN,
MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_VIRTUALDESK, MOUSEEVENTF_WHEEL, MOUSEEVENTF_XDOWN,
MOUSEEVENTF_XUP, MOUSEINPUT, VIRTUAL_KEY,
};
use windows::Win32::UI::WindowsAndMessaging::{
GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
};
use super::InputInjector;
const ABS_MAX: f64 = 65535.0; // SendInput absolute coords are 0..65535 over the chosen surface.
const GENERIC_ALL: u32 = 0x1000_0000;
const XBUTTON1: u32 = 0x0001;
const XBUTTON2: u32 = 0x0002;
pub struct SendInputInjector {
desktop: Option<HDESK>,
}
// Only ever used from the host's single injector thread (like SudoVdaDisplay).
unsafe impl Send for SendInputInjector {}
impl SendInputInjector {
pub fn open() -> Result<Self> {
let mut me = Self { desktop: None };
me.reattach_input_desktop(); // best-effort
tracing::info!("SendInput injector ready (Win32 KeyboardAndMouse)");
Ok(me)
}
/// Bind this thread to the desktop currently receiving input. UAC / lock screen / Ctrl-Alt-Del
/// swap the input desktop; `SendInput` silently no-ops unless our thread is on it.
fn reattach_input_desktop(&mut self) {
unsafe {
match OpenInputDesktop(
DESKTOP_CONTROL_FLAGS(0),
false,
DESKTOP_ACCESS_FLAGS(GENERIC_ALL),
) {
Ok(h) => {
if SetThreadDesktop(h).is_ok() {
if let Some(old) = self.desktop.replace(h) {
let _ = CloseDesktop(old);
}
} else {
let _ = CloseDesktop(h);
}
}
Err(_) => { /* not privileged enough for the secure desktop; stay put */ }
}
}
}
/// 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::<INPUT>() 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::<INPUT>() as i32) };
if n as usize != inputs.len() {
anyhow::bail!(
"SendInput injected {n}/{} events (blocked desktop?)",
inputs.len()
);
}
Ok(())
}
}
impl Drop for SendInputInjector {
fn drop(&mut self) {
if let Some(h) = self.desktop.take() {
unsafe {
let _ = CloseDesktop(h);
}
}
}
}
impl InputInjector for SendInputInjector {
fn inject(&mut self, event: &InputEvent) -> Result<()> {
// 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 {
dx: event.x,
dy: event.y,
mouseData: 0,
dwFlags: MOUSEEVENTF_MOVE,
time: 0,
dwExtraInfo: 0,
};
self.send(&[mouse(mi)])
}
InputKind::MouseMoveAbs => {
let w = (event.flags >> 16) & 0xffff;
let h = event.flags & 0xffff;
if w == 0 || h == 0 {
return Ok(()); // contract: drop zero extent
}
let (_vx, _vy, vw, vh) = virtual_desktop_rect();
// One virtual output spanning the virtual desktop: map client (0..w,0..h) -> 0..65535.
let cx = (event.x.clamp(0, w as i32)) as f64 / w as f64;
let cy = (event.y.clamp(0, h as i32)) as f64 / h as f64;
let ax = (cx * ABS_MAX).round() as i32;
let ay = (cy * ABS_MAX).round() as i32;
let _ = (vw, vh); // virtual-desktop rect reserved for multi-output mapping
let mi = MOUSEINPUT {
dx: ax,
dy: ay,
mouseData: 0,
dwFlags: MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK,
time: 0,
dwExtraInfo: 0,
};
self.send(&[mouse(mi)])
}
InputKind::MouseButtonDown | InputKind::MouseButtonUp => {
let down = event.kind == InputKind::MouseButtonDown;
let (flag, data) = match event.code {
1 => (
if down {
MOUSEEVENTF_LEFTDOWN
} else {
MOUSEEVENTF_LEFTUP
},
0u32,
),
2 => (
if down {
MOUSEEVENTF_MIDDLEDOWN
} else {
MOUSEEVENTF_MIDDLEUP
},
0,
),
3 => (
if down {
MOUSEEVENTF_RIGHTDOWN
} else {
MOUSEEVENTF_RIGHTUP
},
0,
),
4 => (
if down {
MOUSEEVENTF_XDOWN
} else {
MOUSEEVENTF_XUP
},
XBUTTON1,
),
5 => (
if down {
MOUSEEVENTF_XDOWN
} else {
MOUSEEVENTF_XUP
},
XBUTTON2,
),
_ => return Ok(()),
};
let mi = MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: data,
dwFlags: flag,
time: 0,
dwExtraInfo: 0,
};
self.send(&[mouse(mi)])
}
InputKind::MouseScroll => {
// GameStream WHEEL_DELTA(120) units. Windows WHEEL positive=up (matches GameStream —
// no flip, unlike Wayland); HWHEEL positive=right (matches). x is 120-scaled already.
let horizontal = event.code == 1;
let mi = MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: event.x as u32, // signed wheel delta reinterpreted as DWORD
dwFlags: if horizontal {
MOUSEEVENTF_HWHEEL
} else {
MOUSEEVENTF_WHEEL
},
time: 0,
dwExtraInfo: 0,
};
self.send(&[mouse(mi)])
}
InputKind::KeyDown | InputKind::KeyUp => {
let down = event.kind == InputKind::KeyDown;
let vk = (event.code & 0xff) as u16; // client sends Windows VK
let sc_ex = unsafe { MapVirtualKeyExW(vk as u32, MAPVK_VK_TO_VSC_EX, None) };
if sc_ex == 0 {
return Ok(()); // unmappable -> drop
}
let extended = (sc_ex & 0xe000) == 0xe000 || forced_extended(vk);
let scan = (sc_ex & 0xff) as u16;
let mut flags = KEYEVENTF_SCANCODE;
if extended {
flags |= KEYEVENTF_EXTENDEDKEY;
}
if !down {
flags |= KEYEVENTF_KEYUP;
}
let ki = KEYBDINPUT {
wVk: VIRTUAL_KEY(0),
wScan: scan,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
};
self.send(&[key(ki)])
}
// Gamepad goes through ViGEm (separate backend). Touch: no SendInput equivalent -> no-op.
InputKind::GamepadButton
| InputKind::GamepadAxis
| InputKind::TouchDown
| InputKind::TouchMove
| InputKind::TouchUp => Ok(()),
}
}
}
fn mouse(mi: MOUSEINPUT) -> INPUT {
INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 { mi },
}
}
fn key(ki: KEYBDINPUT) -> INPUT {
INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 { ki },
}
}
fn virtual_desktop_rect() -> (i32, i32, i32, i32) {
unsafe {
(
GetSystemMetrics(SM_XVIRTUALSCREEN),
GetSystemMetrics(SM_YVIRTUALSCREEN),
GetSystemMetrics(SM_CXVIRTUALSCREEN),
GetSystemMetrics(SM_CYVIRTUALSCREEN),
)
}
}
// VKs Windows wants flagged extended even when the scancode high bits aren't set: the editing
// cluster (Ins/Del/Home/End/PgUp/PgDn = 0x21..0x28, 0x2D, 0x2E), the Win keys (0x5B/0x5C/0x5D),
// RCtrl (0xA3), RAlt (0xA5), Pause (0x90). MAPVK_VK_TO_VSC_EX already encodes E0 for most; this is a
// thin safety net.
fn forced_extended(vk: u16) -> bool {
matches!(
vk,
0x21..=0x28 | 0x2D | 0x2E | 0x5B | 0x5C | 0x5D | 0xA3 | 0xA5 | 0x90
)
}