feat(gamepad): pure-user-mode Windows DualShock 4 + Xbox 360 (drop ViGEm) + installer + multi-pad
audit / cargo-audit (push) Successful in 17s
apple / swift (push) Successful in 57s
android / android (push) Successful in 4m36s
ci / web (push) Successful in 34s
ci / docs-site (push) Successful in 52s
release / apple (push) Successful in 7m31s
ci / rust (push) Successful in 8m37s
ci / bench (push) Successful in 4m39s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
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
deb / build-publish (push) Successful in 2m35s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m18s
flatpak / build-publish (push) Successful in 4m0s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m31s
docker / deploy-docs (push) Successful in 19s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m22s
windows-host / package (push) Successful in 2m56s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m13s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m15s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 59s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m3s

Windows virtual gamepads now have zero external dependencies - ViGEmBus is removed.

- DualShock 4: Windows UMDF backend (inject/dualshock4_windows.rs + dualshock4_proto.rs),
  reusing the DualSense SwDeviceCreate game-detection identity fix. The one UMDF driver serves
  the DS5 or DS4 identity/descriptor/features/strings per a device_type byte the host stamps into
  shared memory. Driver also gains IOCTL_HID_GET_STRING and a 41-byte calibration feature.
- Xbox 360: a new UMDF2 XUSB companion driver (packaging/windows/xusb-driver/) that registers
  GUID_DEVINTERFACE_XUSB and answers the buffered XInput IOCTLs from a shared section, so classic
  XInputGetState/SetState work with no kernel bus driver. inject/gamepad_windows.rs is rewritten
  to drive it and the vigem-client dependency is removed. Xbox One folds to the 360 XInput path.
- Installer: vendor + pnputil-install the three UMDF drivers (packaging/windows/gamepad-drivers/
  + install-gamepad-drivers.ps1, wired into pack-host-installer.ps1 + punktfunk-host.iss).
- Multi-pad: the host stamps each pad index into the device Location (pszDeviceLocation); the
  driver reads it via WdfDeviceAllocAndQueryProperty to map its own *-shm-<index>, with
  UmdfHostProcessSharing=ProcessSharingDisabled giving each pad its own host (per-pad statics).

Validated live on the Windows host: Cyberpunk native DualSense detection, DS4 identity + descriptor,
XInputGetState + rumble round-trip, two pads -> two distinct XInput slots, and a full installer build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 16:34:22 +02:00
parent f208f3d92e
commit b0c82333d2
29 changed files with 2389 additions and 288 deletions
+36 -14
View File
@@ -1167,7 +1167,8 @@ fn mic_service_thread(rx: std::sync::mpsc::Receiver<Vec<u8>>) {
/// The session's virtual-gamepad backend, resolved once per session (sessions run serially).
///
/// - `Xbox360` — uinput X-Box-360 pads on Linux ([`GamepadManager`](crate::inject::gamepad::GamepadManager)),
/// ViGEm on Windows. Also the X-Box One/Series identity (`PUNKTFUNK_GAMEPAD=xboxone`): the same
/// the in-tree XUSB companion driver (classic XInput) on Windows. Also the X-Box One/Series identity
/// (`PUNKTFUNK_GAMEPAD=xboxone`): the same
/// backend with the One/Series USB VID/PID so games show One/Series glyphs (XInput-identical
/// otherwise). The Linux pad carries it as a [`PadIdentity`](crate::inject::gamepad::PadIdentity).
/// - `DualSense` (`PUNKTFUNK_GAMEPAD=dualsense`) — virtual DualSense via UHID + `hid-playstation`,
@@ -1187,6 +1188,8 @@ enum PadBackend {
DualShock4(crate::inject::dualshock4::DualShock4Manager),
#[cfg(target_os = "windows")]
DualSenseWindows(crate::inject::dualsense_windows::DualSenseWindowsManager),
#[cfg(target_os = "windows")]
DualShock4Windows(crate::inject::dualshock4_windows::DualShock4WindowsManager),
}
impl PadBackend {
@@ -1213,11 +1216,20 @@ impl PadBackend {
_ => {}
}
#[cfg(target_os = "windows")]
if kind == GamepadPref::DualSense {
tracing::info!("gamepad backend: virtual DualSense (Windows UMDF shm channel)");
return PadBackend::DualSenseWindows(
crate::inject::dualsense_windows::DualSenseWindowsManager::new(),
);
match kind {
GamepadPref::DualSense => {
tracing::info!("gamepad backend: virtual DualSense (Windows UMDF shm channel)");
return PadBackend::DualSenseWindows(
crate::inject::dualsense_windows::DualSenseWindowsManager::new(),
);
}
GamepadPref::DualShock4 => {
tracing::info!("gamepad backend: virtual DualShock 4 (Windows UMDF shm channel)");
return PadBackend::DualShock4Windows(
crate::inject::dualshock4_windows::DualShock4WindowsManager::new(),
);
}
_ => {}
}
let _ = kind;
PadBackend::Xbox360(crate::inject::gamepad::GamepadManager::new())
@@ -1232,6 +1244,8 @@ impl PadBackend {
PadBackend::DualShock4(m) => m.handle(ev),
#[cfg(target_os = "windows")]
PadBackend::DualSenseWindows(m) => m.handle(ev),
#[cfg(target_os = "windows")]
PadBackend::DualShock4Windows(m) => m.handle(ev),
}
}
@@ -1246,6 +1260,8 @@ impl PadBackend {
PadBackend::DualShock4(m) => m.apply_rich(_rich),
#[cfg(target_os = "windows")]
PadBackend::DualSenseWindows(m) => m.apply_rich(_rich),
#[cfg(target_os = "windows")]
PadBackend::DualShock4Windows(m) => m.apply_rich(_rich),
}
}
@@ -1269,6 +1285,8 @@ impl PadBackend {
PadBackend::DualShock4(m) => m.pump(rumble, hidout),
#[cfg(target_os = "windows")]
PadBackend::DualSenseWindows(m) => m.pump(rumble, hidout),
#[cfg(target_os = "windows")]
PadBackend::DualShock4Windows(m) => m.pump(rumble, hidout),
}
}
@@ -1286,6 +1304,8 @@ impl PadBackend {
PadBackend::DualShock4(m) => m.heartbeat(std::time::Duration::from_millis(8)),
#[cfg(target_os = "windows")]
PadBackend::DualSenseWindows(m) => m.heartbeat(std::time::Duration::from_millis(8)),
#[cfg(target_os = "windows")]
PadBackend::DualShock4Windows(m) => m.heartbeat(std::time::Duration::from_millis(8)),
}
}
}
@@ -1570,10 +1590,11 @@ fn synthetic_stream(
/// Pure selection of the session's virtual-gamepad backend: the client's explicit `pref` wins,
/// then the host's `PUNKTFUNK_GAMEPAD` env var (under a client `Auto`), then X-Box 360.
///
/// `linux` is whether this is a Linux host (uinput + UHID). The rich UHID pads (DualSense, DualShock
/// 4) need it — off Linux any such wish degrades to X-Box 360 (never an error: a session without rich
/// pads still streams). X-Box One/Series is a distinct uinput *identity* on Linux, but XInput-identical
/// to the 360 pad on Windows (ViGEm has no One target), so it degrades to `Xbox360` there.
/// `linux`/`windows` flag the host platform. DualSense and DualShock 4 each have both a Linux (UHID
/// hid-playstation) and a Windows (UMDF minidriver) backend; on any other platform such a wish degrades
/// to X-Box 360 (never an error: a session without rich pads still streams). X-Box One/Series is a
/// distinct uinput *identity* on Linux, but XInput-identical to the 360 pad on Windows (the XUSB
/// companion presents a 360 identity), so it degrades to `Xbox360` there.
fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool) -> GamepadPref {
let want = match pref {
GamepadPref::Auto => env
@@ -1582,9 +1603,9 @@ fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool
explicit => explicit,
};
match want {
// DualSense: Linux UHID hid-playstation, or the Windows UMDF minidriver backend.
// DualSense / DualShock 4: Linux UHID hid-playstation, or the Windows UMDF minidriver backend.
GamepadPref::DualSense if linux || windows => GamepadPref::DualSense,
GamepadPref::DualShock4 if linux => GamepadPref::DualShock4,
GamepadPref::DualShock4 if linux || windows => GamepadPref::DualShock4,
// One/Series: a real, distinct uinput identity on Linux; folded into the 360 backend on
// Windows (XInput can't tell them apart anyway).
GamepadPref::XboxOne if linux => GamepadPref::XboxOne,
@@ -3092,10 +3113,11 @@ mod tests {
);
assert_eq!(pick_gamepad(DualSense, None, false, false), Xbox360);
assert_eq!(pick_gamepad(Auto, Some("dualsense"), false, false), Xbox360);
// DualShock 4: Linux-only (UHID); degrades to X-Box 360 off it (including Windows).
// DualShock 4: honored on Linux (UHID) AND Windows (UMDF minidriver); degrades elsewhere.
assert_eq!(pick_gamepad(DualShock4, None, true, false), DualShock4);
assert_eq!(pick_gamepad(Auto, Some("ps4"), true, false), DualShock4);
assert_eq!(pick_gamepad(DualShock4, None, false, true), Xbox360);
assert_eq!(pick_gamepad(DualShock4, None, false, true), DualShock4);
assert_eq!(pick_gamepad(DualShock4, None, false, false), Xbox360);
// X-Box One: a distinct uinput identity on Linux, folded into the 360 pad on Windows.
assert_eq!(pick_gamepad(XboxOne, None, true, false), XboxOne);
assert_eq!(pick_gamepad(Auto, Some("series"), true, false), XboxOne);