feat(core,host,android): Steam Controller 2 as-is passthrough to Linux hosts
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m2s
ci / rust (push) Failing after 5m34s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 52s
ci / bench (push) Successful in 6m3s
docker / deploy-docs (push) Successful in 25s
android / android (push) Successful in 14m2s
arch / build-publish (push) Successful in 11m57s
deb / build-publish (push) Successful in 11m49s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m5s
flatpak / build-publish (push) Failing after 8m3s
windows-host / package (push) Failing after 8m32s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m6s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m18s
apple / swift (push) Successful in 5m4s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 5m14s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 5m43s
release / apple (push) Successful in 26m11s
apple / screenshots (push) Has been cancelled
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m2s
ci / rust (push) Failing after 5m34s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 52s
ci / bench (push) Successful in 6m3s
docker / deploy-docs (push) Successful in 25s
android / android (push) Successful in 14m2s
arch / build-publish (push) Successful in 11m57s
deb / build-publish (push) Successful in 11m49s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m5s
flatpak / build-publish (push) Failing after 8m3s
windows-host / package (push) Failing after 8m32s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m6s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m18s
apple / swift (push) Successful in 5m4s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 5m14s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 5m43s
release / apple (push) Successful in 26m11s
apple / screenshots (push) Has been cancelled
The 2026 Steam Controller (Valve "Ibex" / SDL "Triton") captured on an Android client is passed through AS-IS: the host presents a virtual pad with the real wired identity (28DE:1302) and mirrors the physical pad's raw HID reports, so Steam on the host drives it over hidraw exactly like the real thing — trackpads, gyro, paddles, and its rumble/settings writes flow back onto the physical controller. Protocol ground truth: SDL's Valve-maintained SDL_hidapi_steam_triton.c + steam/controller_structs.h. Core: - GamepadPref::SteamController2 (wire byte 9; names steamcontroller2/ sc2/ibex) + PUNKTFUNK_GAMEPAD_STEAMCONTROLLER2 in the C ABI. - Raw HID planes: RichInput::HidReport (0xCC/0x04, client→host input reports verbatim, Copy fixed-64 body) and HidOutput::HidRaw (0xCD/0x05, host→client feature/output writes for replay). Best-effort is sound by the device protocol's own design (rumble re-sent every ~40 ms, settings every ~3 s — losses self-heal); HidRaw bypasses hidout dedup for exactly that reason. Host (Linux): - triton_proto.rs + steam_controller2.rs: Triton2Manager UHID backend — no kernel driver binds the PID (hidraw only; Steam Input is the consumer), raw mirroring with a typed-fallback 0x42 synthesizer until the first raw report, SET_REPORT ack + raw forward, canned GET_REPORT serial reply, rumble also parsed onto the universal 0xCA plane (phone mirror). Rides the uhid + 28DE-conflict degrades; UHID promotion by Steam is flagged in the creation log (usbip transport is the known follow-up if Steam ignores Interface:-1 devices for Triton too). Android: - Sc2UsbLink (wired/Puck: vendor-interface claim detaches the OS driver, interrupt read loop, lizard-off on the watchdog cadence, raw replay via interrupt-OUT / SET_REPORT with hidapi report-id framing) and Sc2BleLink (Valve vendor GATT service, notify subscribe machine, 0x45 re-framing, HIGH connection priority). - Sc2Capture orchestrator: raw plane + typed mirror (exit chord + host degrade paths keep working) on a GamepadRouter external slot; raw return path via GamepadFeedback.onHidRaw. - nativeSendPadHidReport JNI (direct ByteBuffer, no per-report copy), hidout raw decode, usb-host/BLUETOOTH_CONNECT manifest bits, opt-out settings toggle, StreamScreen engagement incl. the USB permission flow. Verified: core 149 + host 312 tests green on Linux (.21), on-box uhid smoke creates/mirrors/tears down the virtual 28DE:1302, C ABI harness round-trips, Android compileDebugKotlin green. On-glass with the real controller owed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1869,6 +1869,8 @@ struct Pads {
|
||||
switchpro: Option<crate::inject::switch_pro::SwitchProManager>,
|
||||
#[cfg(target_os = "linux")]
|
||||
steamctrl: Option<crate::inject::steam_controller::SteamCtrlManager>,
|
||||
#[cfg(target_os = "linux")]
|
||||
steamctrl2: Option<crate::inject::steam_controller2::Triton2Manager>,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualsense_win: Option<crate::inject::dualsense_windows::DualSenseWindowsManager>,
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -1906,6 +1908,8 @@ impl Pads {
|
||||
switchpro: None,
|
||||
#[cfg(target_os = "linux")]
|
||||
steamctrl: None,
|
||||
#[cfg(target_os = "linux")]
|
||||
steamctrl2: None,
|
||||
#[cfg(target_os = "windows")]
|
||||
dualsense_win: None,
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -1989,6 +1993,11 @@ impl Pads {
|
||||
.get_or_insert_with(crate::inject::steam_controller::SteamCtrlManager::new)
|
||||
.handle(ev),
|
||||
#[cfg(target_os = "linux")]
|
||||
GamepadPref::SteamController2 => self
|
||||
.steamctrl2
|
||||
.get_or_insert_with(crate::inject::steam_controller2::Triton2Manager::new)
|
||||
.handle(ev),
|
||||
#[cfg(target_os = "linux")]
|
||||
GamepadPref::XboxOne => self
|
||||
.xboxone
|
||||
.get_or_insert_with(|| {
|
||||
@@ -2036,7 +2045,8 @@ impl Pads {
|
||||
let idx = match rich {
|
||||
RichInput::Touchpad { pad, .. }
|
||||
| RichInput::Motion { pad, .. }
|
||||
| RichInput::TouchpadEx { pad, .. } => pad as usize,
|
||||
| RichInput::TouchpadEx { pad, .. }
|
||||
| RichInput::HidReport { pad, .. } => pad as usize,
|
||||
};
|
||||
// Route to the manager that actually owns the device (falling back to the declared kind
|
||||
// before the first frame builds it), so a pad's touchpad/motion never lands on the wrong
|
||||
@@ -2085,6 +2095,12 @@ impl Pads {
|
||||
m.apply_rich(rich)
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
GamepadPref::SteamController2 => {
|
||||
if let Some(m) = &mut self.steamctrl2 {
|
||||
m.apply_rich(rich)
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
GamepadPref::DualSense => {
|
||||
if let Some(m) = &mut self.dualsense_win {
|
||||
@@ -2148,6 +2164,9 @@ impl Pads {
|
||||
if let Some(m) = &mut self.steamctrl {
|
||||
m.pump(&mut rumble, &mut hidout);
|
||||
}
|
||||
if let Some(m) = &mut self.steamctrl2 {
|
||||
m.pump(&mut rumble, &mut hidout);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -2192,6 +2211,9 @@ impl Pads {
|
||||
if let Some(m) = &mut self.steamctrl {
|
||||
m.heartbeat(gap);
|
||||
}
|
||||
if let Some(m) = &mut self.steamctrl2 {
|
||||
m.heartbeat(gap);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -2903,6 +2925,11 @@ fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool
|
||||
// Switch Pro: Linux UHID hid-nintendo (≥ 5.16) — correct Nintendo glyphs + positional
|
||||
// layout + gyro + HD rumble. No Windows backend; folds to Xbox360 there.
|
||||
GamepadPref::SwitchPro if linux => GamepadPref::SwitchPro,
|
||||
// New Steam Controller (2026, `28DE:1302`): passed through as-is on Linux — the Triton
|
||||
// UHID backend mirrors the client's raw reports under the real identity and Steam on
|
||||
// the host drives it over hidraw (no kernel driver binds the PID; Steam Input is the
|
||||
// consumer). No Windows backend; folds to Xbox360 there.
|
||||
GamepadPref::SteamController2 if linux => GamepadPref::SteamController2,
|
||||
_ => GamepadPref::Xbox360,
|
||||
}
|
||||
}
|
||||
@@ -2920,6 +2947,7 @@ fn degrade_if_no_uhid(chosen: GamepadPref) -> GamepadPref {
|
||||
| GamepadPref::DualShock4
|
||||
| GamepadPref::SteamDeck
|
||||
| GamepadPref::SteamController
|
||||
| GamepadPref::SteamController2
|
||||
| GamepadPref::SwitchPro
|
||||
);
|
||||
if needs_uhid
|
||||
@@ -2985,7 +3013,7 @@ fn physical_steam_controller_present() -> bool {
|
||||
fn degrade_steam_on_conflict(chosen: GamepadPref) -> GamepadPref {
|
||||
if !matches!(
|
||||
chosen,
|
||||
GamepadPref::SteamDeck | GamepadPref::SteamController
|
||||
GamepadPref::SteamDeck | GamepadPref::SteamController | GamepadPref::SteamController2
|
||||
) {
|
||||
return chosen;
|
||||
}
|
||||
@@ -5622,6 +5650,22 @@ mod tests {
|
||||
assert_eq!(pick_gamepad(Auto, Some("switch"), true, false), SwitchPro);
|
||||
assert_eq!(pick_gamepad(SwitchPro, None, false, true), Xbox360);
|
||||
assert_eq!(pick_gamepad(SwitchPro, None, false, false), Xbox360);
|
||||
// New Steam Controller (as-is Triton passthrough): native on Linux (UHID, Steam-driven);
|
||||
// Xbox360 on Windows and elsewhere.
|
||||
assert_eq!(
|
||||
pick_gamepad(SteamController2, None, true, false),
|
||||
SteamController2
|
||||
);
|
||||
assert_eq!(
|
||||
pick_gamepad(Auto, Some("sc2"), true, false),
|
||||
SteamController2
|
||||
);
|
||||
assert_eq!(
|
||||
pick_gamepad(Auto, Some("ibex"), true, false),
|
||||
SteamController2
|
||||
);
|
||||
assert_eq!(pick_gamepad(SteamController2, None, false, true), Xbox360);
|
||||
assert_eq!(pick_gamepad(SteamController2, None, false, false), Xbox360);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user