feat(host/windows): ViGEm virtual gamepad backend
apple / swift (push) Successful in 53s
android / android (push) Failing after 2m28s
ci / web (push) Successful in 27s
ci / docs-site (push) Failing after 13s
ci / bench (push) Failing after 0s
deb / build-publish (push) Failing after 1s
ci / rust (push) Failing after 44s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
decky / build-publish (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
flatpak / build-publish (push) Failing after 2s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m40s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m40s
docker / deploy-docs (push) Successful in 6s

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 01:46:51 +00:00
parent 1a9a733f02
commit 8cba886c17
4 changed files with 110 additions and 2 deletions
@@ -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<Arc<Client>>,
pads: HashMap<u8, Xbox360Wired<Arc<Client>>>,
}
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).
}
}