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

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:
2026-07-15 11:22:16 +02:00
parent 705a8baddf
commit 2621b6e6b1
29 changed files with 1977 additions and 40 deletions
+46 -2
View File
@@ -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]