feat(host/steam): M1 — byte-exact Deck input serializer, on-box validated

Flesh out inject/proto/steam_proto.rs into the full Steam Deck HID contract,
transcribed verbatim from the kernel steam_do_deck_input_event /
steam_do_deck_sensors_event and validated field-for-field against kernel 7.0:

- SteamState: the u64 button map (bytes 8..16), sticks/triggers/trackpads/IMU
  stored as raw little-endian report values; serialize_deck_state is a pure,
  byte-exact memcpy into the 64-byte unnumbered frame.
- from_gamepad (XInput frame -> Deck buttons/sticks/triggers) + apply_rich
  (RichInput touchpad -> right pad, motion -> IMU).
- parse_steam_output: the 0xEB ID_TRIGGER_RUMBLE_CMD feedback -> (low, high)
  for the universal rumble plane.
- serial_reply fixed: prepend the report-id-0 byte the kernel strips
  (steam_recv_report does memcpy(data, buf+1, ...)); M0's reply lacked it, so
  the kernel fell back to the "XXXXXXXXXX" serial.
- SteamModel (Deck now; classic Controller later), command/feature IDs.

The spike is repurposed as the M1 validator: it pulses the b9.6 mode-switch to
enter gamepad_mode (steam_do_deck_input_event early-returns under the default
lizard_mode otherwise), then holds a known test pattern. Reading both evdevs via
EVIOCGABS/EVIOCGKEY, every field matched: ABS_X/Y/RX/RY (incl. the kernel
Y-negation), both triggers, the touched right-pad HAT1X/Y, the IMU accel/gyro
(with ABS_Z/RZ negations), and the 6 expected buttons incl. the L4/R5 grips.

5 unit tests + workspace clippy/fmt/test green. Next: M2 (SteamControllerManager
UHID backend + PadBackend wiring). Not pushed — pipeline not yet shippable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 11:07:20 +00:00
parent 2b47d8cc28
commit 9ff7d41bfe
3 changed files with 450 additions and 164 deletions
+30 -5
View File
@@ -1,10 +1,35 @@
# Rich Steam Controller & Steam Deck support
> **Status:** **M0 GREEN — Linux feasibility PROVEN 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 parses our input reports. The full client-capture → protocol
> → inject pipeline (M1+) is unblocked. This remains the design + milestone plan; the Steam analogue
> of the shipped virtual DualSense (`design/windows-dualsense-scoping.md`).
> **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`).
>
> **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` /
> `steam_do_deck_sensors_event`: the `u64` button map (bytes 8..16), sticks/triggers/trackpads/IMU
> at their exact offsets, `from_gamepad` + `apply_rich` mappers, the rumble-feedback parser
> (`0xEB`), and the serial reply (now with the leading report-id byte the kernel strips — fixes the
> M0 `XXXXXXXXXX` fallback). The validator pulses the `b9.6` mode-switch to enter `gamepad_mode`
> (the parser early-returns under default `lizard_mode` otherwise), holds a known test pattern, and
> reads both evdevs via `EVIOCGABS`/`EVIOCGKEY`: **every field matched** — `ABS_X/Y/RX/RY` (incl. the
> kernel Y-negation), both triggers, the touched right-pad `HAT1X/Y`, the IMU accel/gyro (with the
> `ABS_Z/RZ` negations), and the 6 expected buttons incl. the L4/R5 grips. `byte 8 bit 7 = BTN_A` IS
> correct (the M0 "didn't hold" was a flaky single-bit read before `gamepad_mode` was entered). 5
> unit tests + workspace clippy/fmt/test green.
>
> **M0 result (box: headless Ubuntu 26.04, kernel 7.0, no Steam):** the spike
> (`crates/punktfunk-host/src/bin/steam_uhid_spike.rs` + the keeper `inject/proto/steam_proto.rs`)
> created a UHID `28DE:1205` device with a minimal vendor descriptor (one feature report — the sole
> thing `steam_is_valve_interface` checks) and serviced the handshake (the `UHID_SET_REPORT`
> answers the DualSense backend omits). `journalctl -k` showed **`hid-steam` binding it** (rebound
> off `hid-generic`), `"Steam Controller … connected"`, and the kernel creating **both** evdevs:
> `"Steam Deck"` (gamepad) **and** `"Steam Deck Motion Sensors"` (`INPUT_PROP_ACCELEROMETER`).
> Outstanding for later: recognition by a **running Steam** client (needs a box with Steam —
> untestable here); the `gamepad_mode` entry strategy on a real host (pulse `b9.6` at session start,
> or load `hid_steam lizard_mode=0`) is an M2 backend decision.
>
> **M0 result (box: headless Ubuntu 26.04, kernel 7.0, no Steam):** the spike
> (`crates/punktfunk-host/src/bin/steam_uhid_spike.rs` + the keeper `inject/proto/steam_proto.rs`)