From 8cba886c17938beea5c5743f4e23e81ae7fcf1d8 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 15 Jun 2026 01:46:51 +0000 Subject: [PATCH] feat(host/windows): ViGEm virtual gamepad backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows GamepadManager via vigem-client (ViGEmBus) — the uinput-xpad analogue: one virtual Xbox 360 controller per client pad index, created lazily on first State. GameStream/Moonlight already uses the XInput conventions (low-16 button bits, sticks -32768..32767 +Y up, triggers 0..255), so the GamepadFrame->XGamepad mapping is 1:1. Replaces the non-Linux GamepadManager stub (same new/handle/pump_rumble API the m3 PadBackend drives, so no m3 change). Graceful when ViGEmBus is absent (gamepad disabled, session continues). Compiles clean on Windows + Linux; live-test needs the ViGEmBus driver + a physical pad. Rumble back-channel is a TODO (ViGEm notification API). Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 32 +++++++++ crates/punktfunk-host/Cargo.toml | 2 + crates/punktfunk-host/src/inject.rs | 8 ++- .../src/inject/gamepad_windows.rs | 70 +++++++++++++++++++ 4 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 crates/punktfunk-host/src/inject/gamepad_windows.rs diff --git a/Cargo.lock b/Cargo.lock index f6f6055..114e2f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2658,6 +2658,7 @@ dependencies = [ "utoipa", "utoipa-axum", "utoipa-scalar", + "vigem-client", "wasapi", "wayland-backend", "wayland-client", @@ -3967,6 +3968,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vigem-client" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b857e6f99efe1e1eb1e4dfb035de8ae7ec8ec56bd1928edcbd7c6e4427634d52" +dependencies = [ + "winapi", +] + [[package]] name = "wait-timeout" version = "0.2.1" @@ -4214,6 +4224,22 @@ dependencies = [ "safe_arch", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -4223,6 +4249,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows" version = "0.62.2" diff --git a/crates/punktfunk-host/Cargo.toml b/crates/punktfunk-host/Cargo.toml index 02fd85a..74f7dd4 100644 --- a/crates/punktfunk-host/Cargo.toml +++ b/crates/punktfunk-host/Cargo.toml @@ -127,6 +127,8 @@ windows = { version = "0.62", features = [ openh264 = "0.9" # WASAPI loopback audio capture (default render endpoint -> 48 kHz stereo f32 for the Opus path). wasapi = "0.23" +# Virtual Xbox 360 gamepad via ViGEmBus (the uinput-xpad analogue) — driver installed separately. +vigem-client = "0.1" # NVENC hardware encoder (NVENC SDK, D3D11 input). The SDK pins `cudarc` with # `cuda-version-from-build-system` (a build-time CUDA-toolkit probe); its `ci-check` feature switches # cudarc to `dynamic-loading` (loads nvcuda.dll at runtime — nothing needed at build), which is how diff --git a/crates/punktfunk-host/src/inject.rs b/crates/punktfunk-host/src/inject.rs index f832f7d..6792d05 100644 --- a/crates/punktfunk-host/src/inject.rs +++ b/crates/punktfunk-host/src/inject.rs @@ -298,8 +298,12 @@ fn gs_button_to_evdev(b: u32) -> Option { pub mod dualsense; #[cfg(target_os = "linux")] pub mod gamepad; -/// Stub — virtual gamepads need Linux uinput; events are dropped elsewhere. -#[cfg(not(target_os = "linux"))] +/// Windows: virtual Xbox 360 pads via ViGEmBus. +#[cfg(target_os = "windows")] +#[path = "inject/gamepad_windows.rs"] +pub mod gamepad; +/// Stub — virtual gamepads need Linux uinput or Windows ViGEmBus; events are dropped elsewhere. +#[cfg(not(any(target_os = "linux", target_os = "windows")))] pub mod gamepad { #[derive(Default)] pub struct GamepadManager; diff --git a/crates/punktfunk-host/src/inject/gamepad_windows.rs b/crates/punktfunk-host/src/inject/gamepad_windows.rs new file mode 100644 index 0000000..9ffd484 --- /dev/null +++ b/crates/punktfunk-host/src/inject/gamepad_windows.rs @@ -0,0 +1,70 @@ +//! Windows virtual gamepad via ViGEmBus — the analogue of the Linux uinput Xbox-360 pad. +//! One virtual Xbox 360 controller per client pad index. GameStream/Moonlight already uses the +//! XInput button/stick/trigger conventions (low 16 button bits, sticks −32768..32767 +Y up, +//! triggers 0..255), so the mapping is ~1:1. +//! +//! Needs the ViGEmBus driver installed (like SudoVDA for the display); absent → gamepad is disabled +//! and the session continues without it. Rumble back-channel: TODO (ViGEm notification API). + +use crate::gamestream::gamepad::GamepadEvent; +use std::collections::HashMap; +use std::sync::Arc; +use vigem_client::{Client, TargetId, XButtons, XGamepad, Xbox360Wired}; + +pub struct GamepadManager { + client: Option>, + pads: HashMap>>, +} + +impl GamepadManager { + pub fn new() -> GamepadManager { + let client = match Client::connect() { + Ok(c) => { + tracing::info!("ViGEmBus connected (virtual Xbox 360 gamepads)"); + Some(Arc::new(c)) + } + Err(e) => { + tracing::warn!( + error = format!("{e:?}"), + "ViGEmBus unavailable — gamepad disabled (install ViGEmBus)" + ); + None + } + }; + GamepadManager { + client, + pads: HashMap::new(), + } + } + + pub fn handle(&mut self, ev: &GamepadEvent) { + let Some(client) = self.client.clone() else { + return; + }; + let GamepadEvent::State(f) = ev else { + return; // Arrival metadata — the pad is created lazily on the first State + }; + let target = self.pads.entry(f.index.max(0) as u8).or_insert_with(|| { + let mut t = Xbox360Wired::new(client, TargetId::XBOX360_WIRED); + let _ = t.plugin(); + let _ = t.wait_ready(); + t + }); + let gp = XGamepad { + buttons: XButtons { + raw: (f.buttons & 0xffff) as u16, + }, + left_trigger: f.left_trigger, + right_trigger: f.right_trigger, + thumb_lx: f.ls_x, + thumb_ly: f.ls_y, + thumb_rx: f.rs_x, + thumb_ry: f.rs_y, + }; + let _ = target.update(&gp); + } + + pub fn pump_rumble(&mut self, _send: impl FnMut(u16, u16, u16)) { + // TODO: wire the ViGEm rumble notification back-channel (Xbox360Wired::request_notification). + } +}