feat(host/pads): route the Xbox pad to the HID backend behind PUNKTFUNK_XBOX_BACKEND=hid
Wires `xbox_windows` into the per-pad router so an Xbox-family pad can be built as a real HID device instead of the XUSB companion, and adds the knob that selects between them. Opt-in rather than the new default, deliberately. XUSB is what classic-XInput games read today; the HID pad buys the Steam / WGI / GameInput / DirectInput visibility XUSB can never have, but whether Windows promotes it into an Xbox-profile device that XInput and WGI Gamepad accept is still the open question. Flipping the default before that is settled would trade a known-working path for an unproven one. The two backends are mutually exclusive per pad by construction — one match arm or the other — because presenting both hands a game two controllers for one pair of hands. Verified on .173: cargo check -p punktfunk-host exit 0, clippy -D warnings clean, `cargo test -p punktfunk-host gamepad` 8/8 green, fmt clean.
This commit is contained in:
@@ -221,6 +221,30 @@ fn degrade_steam_on_conflict(chosen: GamepadPref) -> GamepadPref {
|
||||
chosen
|
||||
}
|
||||
|
||||
/// Whether an Xbox-family pad should be built as a real **HID** device
|
||||
/// ([`crate::inject::xbox_windows`]) instead of the **XUSB** companion
|
||||
/// ([`crate::inject::gamepad`]). Windows only; `PUNKTFUNK_XBOX_BACKEND=hid` opts in.
|
||||
///
|
||||
/// **Why this is a knob and not simply the new default.** The XUSB companion registers only
|
||||
/// `GUID_DEVINTERFACE_XUSB` and exposes no HID collection, so Steam's hidapi enumeration,
|
||||
/// DirectInput, `joy.cpl` and WGI/GameInput cannot see it at all — only classic `XInputGetState`
|
||||
/// via xinput1_4's interface walk does. That is what left a reporter with a dead controller for two
|
||||
/// weeks (2026-08-09) until they switched the client to DualSense, a real HID pad.
|
||||
///
|
||||
/// But the converse is not yet proven: classic-XInput games DO read the XUSB pad today, and whether
|
||||
/// Windows promotes our HID pad into an Xbox-profile device that XInput and WGI `Gamepad` accept is
|
||||
/// exactly the open question. Until that is settled on glass, flipping the default would trade a
|
||||
/// known-working path for an unproven one. Opt in, measure, then decide.
|
||||
///
|
||||
/// The two backends are mutually exclusive per pad by construction (one match arm or the other) —
|
||||
/// presenting both would hand a game two controllers for one pair of hands.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(super) fn windows_xbox_hid() -> bool {
|
||||
std::env::var("PUNKTFUNK_XBOX_BACKEND")
|
||||
.map(|v| v.trim().eq_ignore_ascii_case("hid"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Resolve the client's gamepad-backend preference (the env/logging shell around
|
||||
/// [`pick_gamepad`]). Always concrete — the `Welcome` reports what the session will drive.
|
||||
pub(super) fn resolve_gamepad(pref: GamepadPref) -> GamepadPref {
|
||||
|
||||
@@ -123,6 +123,11 @@ struct Pads {
|
||||
steamctrl2_puck: Option<crate::inject::steam_controller2::Triton2Manager>,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualsense_win: Option<crate::inject::dualsense_windows::DualSenseWindowsManager>,
|
||||
/// The HID-visible Xbox pad ([`crate::inject::xbox_windows`]) — used INSTEAD of `xbox360`'s
|
||||
/// XUSB companion when [`super::gamepad::windows_xbox_hid`] says so. Never both at once: two
|
||||
/// devices for one wire pad is the "the game sees two controllers" bug.
|
||||
#[cfg(target_os = "windows")]
|
||||
xbox_hid: Option<crate::inject::xbox_windows::XboxWindowsManager>,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualsense_edge_win: Option<crate::inject::dualsense_edge_windows::DualSenseEdgeWindowsManager>,
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -165,6 +170,8 @@ impl Pads {
|
||||
#[cfg(target_os = "windows")]
|
||||
dualsense_win: None,
|
||||
#[cfg(target_os = "windows")]
|
||||
xbox_hid: None,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualsense_edge_win: None,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualshock4_win: None,
|
||||
@@ -291,6 +298,16 @@ impl Pads {
|
||||
.steamdeck_win
|
||||
.get_or_insert_with(crate::inject::steam_deck_windows::SteamDeckWindowsManager::new)
|
||||
.handle(ev),
|
||||
// The Xbox pad, as a real HID device rather than the XUSB companion. Opt-in for now
|
||||
// (see `windows_xbox_hid`): XUSB is what classic-XInput games read today, and this
|
||||
// trades that for the Steam / WGI / GameInput / DirectInput visibility XUSB can never
|
||||
// have — a swap that has to be proven on glass before it becomes the default.
|
||||
#[cfg(target_os = "windows")]
|
||||
GamepadPref::Xbox360 | GamepadPref::XboxOne if super::gamepad::windows_xbox_hid() => {
|
||||
self.xbox_hid
|
||||
.get_or_insert_with(crate::inject::xbox_windows::XboxWindowsManager::new)
|
||||
.handle(ev)
|
||||
}
|
||||
_ => self
|
||||
.xbox360
|
||||
.get_or_insert_with(crate::inject::gamepad::GamepadManager::new)
|
||||
@@ -451,6 +468,11 @@ impl Pads {
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Some(m) = &mut self.xbox_hid {
|
||||
// Rumble only — an Xbox pad has no rich-feedback plane (no lightbar / adaptive
|
||||
// triggers), same as its XUSB sibling above.
|
||||
m.pump(&mut rumble, &mut hidout);
|
||||
}
|
||||
if let Some(m) = &mut self.dualsense_win {
|
||||
m.pump(&mut rumble, &mut hidout);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user