feat(gamepad): classic Steam Controller backend — Linux UHID via hid-steam (N3)

The reserved GamepadPref::SteamController = 5 slot goes live: the same
hid-steam driver under the wired-SC identity (28DE:1102,
ID_CONTROLLER_STATE), UHID-only in v1 (no captured SC USB interface
layout, so no Steam-Input promotion — the pre-usbip Deck state;
acceptable for discontinued hardware).

Layout pinned against the kernel's ID_CONTROLLER_STATE table: 24-bit
buttons at 8..11 (low bits shared with the Deck; grips at 9.7/10.0 =
the Deck's L5/R5 positions; right-pad click 10.2; joystick click 10.6),
u8 triggers at 11/12, the joystick/left-pad MULTIPLEX at 16..20 (a
left-pad contact shadows the stick, like real hardware's lpad_touched
flag), right pad at 20..24. Mapping: wire left stick -> SC stick; wire
right stick -> right-pad coords + touched bit (the SC's camera surface —
the second-stick loss is inherent); PADDLE1/2 -> the two grips (natively,
masked out of the fold input); PADDLE3/4 + MISC1 -> the remap policy.
The SC parser has NO gamepad_mode gate, so no mode-entry pulse.

SteamDeckPad grew a SteamModel (open_model); ScProto/SteamCtrlManager;
pick_gamepad flips SteamController -> itself on Linux (replacing the
Xbox360 fold); SDL picker splits Valve PIDs (Deck 1205 stays SteamDeck,
SC 1102/1142 now declare SteamController).

Verified: .21 clippy -D warnings + 304/0 tests + on-box UHID smoke
(hid-steam binds 1102, BTN_A + right-pad ABS_RX land on evdev, no mode
pulse); .133 clippy -D warnings green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 11:15:54 +02:00
parent 70a74b0d7c
commit 8c854e0a19
6 changed files with 417 additions and 25 deletions
+35 -2
View File
@@ -1779,6 +1779,8 @@ struct Pads {
steamdeck: Option<crate::inject::steam_controller::SteamControllerManager>,
#[cfg(target_os = "linux")]
switchpro: Option<crate::inject::switch_pro::SwitchProManager>,
#[cfg(target_os = "linux")]
steamctrl: Option<crate::inject::steam_controller::SteamCtrlManager>,
#[cfg(target_os = "windows")]
dualsense_win: Option<crate::inject::dualsense_windows::DualSenseWindowsManager>,
#[cfg(target_os = "windows")]
@@ -1812,6 +1814,8 @@ impl Pads {
steamdeck: None,
#[cfg(target_os = "linux")]
switchpro: None,
#[cfg(target_os = "linux")]
steamctrl: None,
#[cfg(target_os = "windows")]
dualsense_win: None,
#[cfg(target_os = "windows")]
@@ -1888,6 +1892,11 @@ impl Pads {
.get_or_insert_with(crate::inject::switch_pro::SwitchProManager::new)
.handle(ev),
#[cfg(target_os = "linux")]
GamepadPref::SteamController => self
.steamctrl
.get_or_insert_with(crate::inject::steam_controller::SteamCtrlManager::new)
.handle(ev),
#[cfg(target_os = "linux")]
GamepadPref::XboxOne => self
.xboxone
.get_or_insert_with(|| {
@@ -1973,6 +1982,12 @@ impl Pads {
m.apply_rich(rich)
}
}
#[cfg(target_os = "linux")]
GamepadPref::SteamController => {
if let Some(m) = &mut self.steamctrl {
m.apply_rich(rich)
}
}
#[cfg(target_os = "windows")]
GamepadPref::DualSense => {
if let Some(m) = &mut self.dualsense_win {
@@ -2027,6 +2042,9 @@ impl Pads {
if let Some(m) = &mut self.switchpro {
m.pump(&mut rumble, &mut hidout);
}
if let Some(m) = &mut self.steamctrl {
m.pump(&mut rumble, &mut hidout);
}
}
#[cfg(target_os = "windows")]
{
@@ -2065,6 +2083,9 @@ impl Pads {
if let Some(m) = &mut self.switchpro {
m.heartbeat(gap);
}
if let Some(m) = &mut self.steamctrl {
m.heartbeat(gap);
}
}
#[cfg(target_os = "windows")]
{
@@ -2758,9 +2779,10 @@ fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool
// 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,
// Steam Deck: Linux UHID hid-steam. The classic Steam Controller's backend isn't built yet,
// so it folds to Xbox360 for now (Windows Steam devices are M7).
// Steam Deck / classic Steam Controller: Linux UHID hid-steam (Windows Steam devices
// are the N4 spike).
GamepadPref::SteamDeck if linux => GamepadPref::SteamDeck,
GamepadPref::SteamController if linux => GamepadPref::SteamController,
// No virtual Deck on Windows (M7) — fold to DualSense, the closest rich pad: its
// backend keeps gyro + trackpads + pad-click alive (the Deck's dual pads split the
// DualSense touchpad left/right per DsState::apply_rich). Folding to Xbox360 dropped
@@ -2789,6 +2811,7 @@ fn degrade_if_no_uhid(chosen: GamepadPref) -> GamepadPref {
| GamepadPref::DualSenseEdge
| GamepadPref::DualShock4
| GamepadPref::SteamDeck
| GamepadPref::SteamController
| GamepadPref::SwitchPro
);
if needs_uhid
@@ -5356,6 +5379,16 @@ mod tests {
assert_eq!(pick_gamepad(SteamDeck, None, false, true), DualSense);
assert_eq!(pick_gamepad(Auto, Some("deck"), false, true), DualSense);
assert_eq!(pick_gamepad(SteamDeck, None, false, false), Xbox360);
// Classic Steam Controller: native on Linux (UHID hid-steam); Xbox360 elsewhere.
assert_eq!(
pick_gamepad(SteamController, None, true, false),
SteamController
);
assert_eq!(
pick_gamepad(Auto, Some("steamcontroller"), true, false),
SteamController
);
assert_eq!(pick_gamepad(SteamController, None, false, true), Xbox360);
// DualSense Edge: native on Linux (UHID) AND Windows (UMDF device-type 2); Xbox360
// elsewhere.