feat(host/steam): M2 — virtual Steam Deck as a wired PadBackend (Linux)

Make the virtual hid-steam device a selectable per-session host gamepad,
end-to-end on Linux: PUNKTFUNK_GAMEPAD=steamdeck now builds a
SteamControllerManager that creates a /dev/uhid 28DE:1205 Deck, enters
gamepad_mode, and feeds the byte-exact Deck report (M1).

- inject/linux/steam_controller.rs: SteamControllerManager / SteamDeckPad,
  mirroring dualsense.rs (open/create2, GET/SET_REPORT pump, heartbeat, RAII
  destroy). Two Steam-specific quirks beyond the DualSense path:
    * gamepad_mode entry — best-effort `lizard_mode=0` via sysfs, plus a b9.6
      creation pulse (MODE_ENTER) so steam_do_deck_input_event stops
      early-returning, plus an anti-toggle guard (MENU_HOLD_CAP) so a long
      in-game Start-hold can't flip gamepad_mode back off.
    * UHID_SET_REPORT answered err=0 (DualSense omits it; the kernel stalls
      ~5s/cmd otherwise); the 0xEB rumble report parsed onto the 0xCA plane.
- core config.rs: GamepadPref::SteamDeck (wire byte 6) + SteamController
  (byte 5, reserved — folds to Xbox360 until its backend lands); from_u8 /
  from_name / as_str. Forward-compatible (unknown byte -> Auto); the C-ABI
  PUNKTFUNK_GAMEPAD_* constants stay M3, so no generated-header drift.
- punktfunk1.rs: PadBackend::SteamDeck variant + select / handle / apply_rich
  / pump / heartbeat arms; pick_gamepad Linux arm.

On-box: an #[ignore]d backend test (backend_binds_and_input_flows) drives the
real SteamDeckPad — it binds hid-steam (gamepad + IMU evdevs), enters gamepad
mode, BTN_A reaches the evdev, and the device tears down on drop. Workspace
clippy/fmt/test green. Not pushed. Next: M3 (protocol/ABI wire) + M4 (client
capture).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 11:32:57 +00:00
parent 9ff7d41bfe
commit 95308d352b
5 changed files with 535 additions and 10 deletions
+21 -6
View File
@@ -1,11 +1,26 @@
# Rich Steam Controller & Steam Deck support
> **Status:** **M0 + M1 GREEN — Linux virtual Deck binds AND is byte-exact, on-box (2026-06-29).**
> The greenfield virtual `hid-steam` device works: a `/dev/uhid` `28DE:1205` device binds the kernel
> `hid-steam` driver, registers as a real Steam Deck, and our full input report is parsed
> field-for-field. Next: M2 (the `SteamControllerManager` UHID backend + `PadBackend` wiring). This
> remains the design + milestone plan; the Steam analogue of the shipped virtual DualSense
> (`design/windows-dualsense-scoping.md`).
> **Status:** **M0M2 GREEN — Linux virtual Deck binds, is byte-exact, AND is a wired host backend,
> on-box (2026-06-29).** The greenfield virtual `hid-steam` device works end-to-end as a selectable
> host gamepad backend: `PUNKTFUNK_GAMEPAD=steamdeck` builds a per-session `SteamControllerManager`
> that creates a `/dev/uhid` `28DE:1205` device, enters `gamepad_mode`, and feeds the byte-exact Deck
> report. Next: M3 (the protocol/ABI wire surface — back-button bits, `TouchpadEx`, the C-ABI
> `GamepadPref` constants) + M4 (client capture). This remains the design + milestone plan; the Steam
> analogue of the shipped virtual DualSense (`design/windows-dualsense-scoping.md`).
>
> **M2 result (backend + wiring, on-box):** `inject/linux/steam_controller.rs`
> (`SteamControllerManager`/`SteamDeckPad`, mirroring `dualsense.rs`) is wired into `PadBackend`
> (new `SteamDeck` variant + `select`/`handle`/`apply_rich`/`pump`/`heartbeat` arms) and selectable
> via `GamepadPref::SteamDeck` (core enum byte 6 + `pick_gamepad` Linux arm; `SteamController` = byte
> 5 is reserved, folds to Xbox360 until its backend lands). Two Steam-specific quirks beyond the
> DualSense path: (1) **`gamepad_mode` entry** — best-effort `lizard_mode=0` via sysfs + a `b9.6`
> creation pulse (`MODE_ENTER` 650 ms) + an **anti-toggle guard** (`MENU_HOLD_CAP` 350 ms) so a long
> in-game Start-hold can't flip `gamepad_mode` off; (2) **`UHID_SET_REPORT`** answered `err=0`
> (DualSense omits it) + the `0xEB` rumble parsed onto the universal 0xCA plane. An `#[ignore]`d
> on-box test (`backend_binds_and_input_flows`) drives the real backend: it binds `hid-steam`
> (gamepad + IMU evdevs), enters gamepad mode, `BTN_A` reaches the evdev, and the device tears down
> on drop. Workspace clippy/fmt/test green; no generated-header drift (the C-ABI `GamepadPref`
> constants are M3).
>
> **M1 result (byte-exact serializer, on-box):** `inject/proto/steam_proto.rs` now carries the full
> Deck contract transcribed verbatim from the kernel `steam_do_deck_input_event` /