docs(steam): criterion-4 RESOLVED — the interface-2 ceiling; recommend dropping M7

Hardware finding (a SteamOS Deck @ .253 + a Bazzite host @ .41, both running
Steam, via a minimal C UHID probe on Bazzite): a UHID virtual Steam Deck binds
the kernel hid-steam and creates the evdevs (so kernel-evdev + SDL-hidapi
consumers see the full grips/trackpads/IMU surface), but Steam Input will NOT
manage it. Steam's controller.txt enumerates it ("Local Device Found, 28de 1205,
Product Punktfunk Steam Deck") but logs Interface: -1 and never promotes it (no
28de:11ff XInput pad). The physical Deck on the same logs is Interface: 2 — a
real Deck is a 3-interface USB device (kbd 0 / mouse 1 / controller 2) and Steam
binds the controller on interface 2; a single UHID device has no USB interface
number, so Steam reads -1 and filters it out. (The feared 0x83/0xA1 attribute
probes never fired — it's an interface filter, not a probe-reject.)

Consequences (design §11):
- The virtual Deck's value is non-Steam / SDL games on Linux (grips + trackpads
  + gyro via evdev / SDL HIDAPI), NOT Steam Input.
- The virtual DualSense stays the Steam-Input path everywhere (Steam recognizes
  a single-interface DualSense); M5's paddle-fold carries the back grips.
- M7 (a Windows UMDF virtual Deck) is NOT recommended: same interface filter,
  and Windows has no kernel-hid-steam evdev fallback, so nothing would consume
  it; the existing Windows virtual DualSense already covers that case.
- M0-M6 is not wasted: the protocol/wire + client capture feed the DualSense
  path too, and the virtual Deck is the best option for non-Steam Linux games.

Doc-only (design/steam-controller-deck-support.md): added §11, updated the status
+ pending-validation. Not pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 13:26:42 +00:00
parent 4f40fa3cb7
commit 37c3e2bed2
+88 -16
View File
@@ -6,21 +6,32 @@
> the **conflict gate** keeps a virtual Steam pad off a host that already has a physical one. Clients:
> the Linux + Windows SDL clients capture + send them; the Decky plugin has the Steam Deck mode +
> Disable-Steam-Input UX; the C-ABI has the `TouchpadEx` send path; Apple/Android round-trip the type.
> Remaining: M7 (Windows UMDF Steam driver) + the last hardware validation.
>
> **⚠ Hardware finding that reframes the ceiling (2026-06-29, §11):** a UHID virtual Deck binds the
> kernel `hid-steam` (so the **kernel evdev + SDL-hidapi** consumers see the full surface — grips,
> trackpads, IMU) but **Steam Input will NOT manage it** — Steam filters the Deck's controller to USB
> **interface 2**, and a single UHID device reports interface `-1`. So the virtual Deck's value is for
> **non-Steam / SDL games on Linux**, not Steam Input; the **virtual DualSense** stays the right path
> for Steam-Input hosts (Steam recognizes a single-interface DualSense). **Recommendation: do NOT
> build M7** (a Windows virtual Deck would hit the same filter with no kernel-evdev fallback — nothing
> on Windows would consume it). Remaining is validation only (Moonlight paddle regression; a live
> SDL-game consume test).
>
> **M6 (conflict gate) result — validated on real hardware (a SteamOS Deck + a Bazzite host running
> Steam, 2026-06-29):** (a) **Empirical conflict confirmed.** A Deck-as-host already has its physical
> `28DE:1205` *and* Steam's `28DE:11FF` XInput output pad live — so a second virtual `28DE` makes Steam
> juggle two Decks. (b) **Bind robustness:** the virtual Deck binds `hid-steam` on a *second*
> independent kernel (Bazzite 6.17.7, vs the dev box 7.0) and the kernel accepted our serial (the M1
> report-id-0 fix). (c) **Criterion-4 (running-Steam recognition) — partial:** with Steam running on
> Bazzite, a userspace consumer (Steam/SDL) *did* engage the virtual Deck — it opened the hidraw and
> ran the lizard-disable + settings sequence (`0xAE`/`0x85`/`0x8E`/`0x81`/`0x87`; the kernel's Deck path
> skips lizard, so that's Steam/SDL) — **but emitted no `28DE:11FF` XInput pad** on the desktop, even
> though the Deck proves desktop-Steam *does* emit one for a recognized Deck. So Steam recognizes the
> device enough to manage its lizard mode but did **not** promote it to a managed XInput controller —
> likely needs a Big-Picture/game foreground context or a richer device (the `0x83`/`0xA1` attribute
> probes the research feared never fired, so it wasn't a probe-reject either). **Policy (code):**
> report-id-0 fix). (c) **Criterion-4 (running-Steam recognition) — RESOLVED, negative for Steam
> Input (this is the third wall, §2).** Steam's `controller.txt` *enumerates* the virtual Deck
> (`Local Device Found, type: 28de 1205, Product "Punktfunk Steam Deck"`) but logs **`Interface:
> -1`** and never promotes it (no `28DE:11FF` pad, no "Controller connected"). On the same Steam logs
> the **physical** Deck is **`Interface: 2`** — a real Deck is a 3-interface USB device (keyboard 0,
> mouse 1, **controller 2**), and Steam binds the controller on interface 2. A single UHID device has
> no USB interface number → `-1` → Steam skips it. `hid-steam` binds by VID/PID regardless (so the
> kernel evdevs + SDL-hidapi path work), **but Steam Input itself will not manage a UHID virtual
> Deck.** (The feared `0x83`/`0xA1` attribute probes never fired — it's an interface filter, not a
> probe-reject.) See §11 for what this means + the M7 recommendation. **Policy (code):**
> `physical_steam_controller_present()` (scans `/sys/bus/hid/devices` for a non-virtual `28DE`) +
> `degrade_steam_on_conflict()` in `resolve_gamepad` — a resolved `SteamDeck`/`SteamController` on a
> host with a physical Steam controller degrades to DualSense (then the uhid ladder), overridable with
@@ -53,13 +64,13 @@
> device; Android has no rich-input plane yet). Rust workspace clippy/fmt/test green; Decky `src/`
> typechecks clean; Swift/Kotlin compile on their CI.
>
> **Pending VALIDATION:** (1) **running-Steam recognition — PARTIAL** (M6: Steam engages the virtual
> Deck's hidraw but didn't emit an XInput pad on the desktop; re-test with a foreground Big-Picture
> game on the Deck/Bazzite to see if it promotes then — and if not, capture the real `0x83`/`0xA1`
> attribute blobs from a physical Deck for M7-style fidelity); (2) a **live Deck/Steam Controller
> client** actually sending paddles/trackpads/gyro (the desktop capture is built but unexercised by
> real hardware — note the Deck's `/dev/uhid` is root-only `0600`, so the Deck-as-host needs a udev
> rule for the input group); (3) the **Moonlight paddle regression** from the M3 xpad-map change.
> **Pending VALIDATION (construction is done; M7 is NOT recommended — §11):** (1) running-Steam
> recognition is **RESOLVED** — Steam won't promote a UHID virtual Deck (interface filter, §11); the
> virtual Deck serves non-Steam/SDL games, the virtual DualSense serves Steam Input. (2) A **live
> SDL/non-Steam game** consuming the virtual Deck's grips/trackpads (the path that works) — needs a
> real Deck/SC client + a Steam-Input-disabled consumer; note the Deck's `/dev/uhid` is root-only
> `0600`, so a Deck-as-host needs a udev rule for the input group. (3) The **Moonlight paddle
> regression** from the M3 xpad-map change.
>
> **M4 (desktop client capture) result:** `clients/{linux,windows}/src/gamepad.rs` (the SDL services)
> now: set the SDL HIDAPI Steam hints (`SDL_JOYSTICK_HIDAPI_STEAMDECK`/`_STEAM`) so SDL opens Valve
@@ -646,3 +657,64 @@ phase, itself re-gated on its own recognition spike.
(Full risks + open questions in the structured fields.)
## 11. The interface-2 ceiling — Steam Input won't manage a UHID virtual Deck (hardware-validated 2026-06-29)
Validated on a SteamOS Steam Deck (`192.168.1.253`) + a Bazzite host (`192.168.1.41`), both running
Steam, with a minimal C UHID probe (`28DE:1205` + the proven descriptor/handshake) run on Bazzite
(no physical Steam controller, so a clean test bed).
**What works.** The kernel `hid-steam` binds the virtual Deck by VID/PID on a second independent
kernel (Bazzite 6.17.7) exactly as on the dev box (7.0): it accepts our serial (the M1 report-id-0
fix), and creates both the `"Steam Deck"` gamepad evdev and the `"Steam Deck Motion Sensors"` IMU
evdev. So **any consumer that reads the kernel evdev or opens the hidraw via SDL's HIDAPI Steam Deck
driver sees the full surface** — the four grips (`BTN_GRIPL/R/L2/R2`), both trackpads (`ABS_HAT0/1`),
and the IMU.
**What does NOT work: Steam Input promotion.** Steam's own controller driver *enumerates* the device
— `controller.txt` logs `Local Device Found, type: 28de 1205, Product "Punktfunk Steam Deck", path
/dev/hidraw1, Interface: -1` — but never promotes it: no `28DE:11FF` virtual XInput pad, no
"Controller N connected". On the same Steam logs the **physical** Deck appears as **`Interface: 2`**.
A real Steam Deck is a **3-interface USB device** (keyboard = interface 0, mouse = 1, **controller =
2**), and Steam binds the controller specifically on interface 2. A single `/dev/uhid` device is not
a USB device and has no `bInterfaceNumber`, so Steam reads **`-1`** and filters it out. (Notably the
`0x83 GET_ATTRIBUTES` / `0xA1 GET_DEVICE_INFO` probes the prior research feared — SDL #12166 — never
fired: this is an interface filter, not an attribute-probe rejection. That blocker, if it exists, is
Windows-driver-specific.)
**Why UHID can't fix it.** UHID creates one HID interface with no USB interface number; you cannot set
one, and creating three UHID devices wouldn't help (each is still interface-less / `-1`). Presenting a
real multi-interface USB Deck with the controller on interface 2 needs a **USB gadget** (`dummy_hcd` +
configfs) or a kernel USB bus driver — a much larger, less portable lift, and contrary to punktfunk's
"no kernel bus driver" stance (ViGEm was deliberately removed).
### Strategic consequences
- **The virtual Deck's real value is non-Steam / SDL games on Linux** (emulators, native SDL titles,
anything reading the kernel evdev or SDL's HIDAPI Steam driver) — there it delivers grips +
trackpads + gyro. It does **not** deliver Steam Input glyphs/bindings.
- **For Steam-Input hosts, the virtual DualSense is the right path** on every platform: Steam *does*
recognize a single-interface DualSense (it needs no interface filtering), giving the client gyro +
a touchpad through Steam Input; the M5 paddle-fold carries the back grips onto standard buttons.
This is why the M6 conflict gate degrades to DualSense, and why `Auto` already prefers it.
- **M7 (a Windows UMDF virtual Steam Deck) is NOT recommended.** Windows Steam applies the same
interface filter, and Windows has **no kernel-`hid-steam` evdev fallback** — Windows games consume
XInput / RawInput / Windows.Gaming.Input, none of which a non-promoted virtual Deck feeds. So a
Windows virtual Deck would be consumed by *nothing*. The existing Windows **virtual DualSense**
already covers the Steam-Input + gyro/touchpad case there.
### What the M0M6 work still delivers (not wasted)
- The **protocol/wire** (back buttons, second trackpad, gyro/accel, trackpad haptics) and the
**client capture** (paddles, both trackpads, gyro from a real Deck/SC) are general — they feed the
virtual DualSense path (Deck client → Steam-Input host) just as well, with the grips folded in (M5).
- The **virtual Deck backend** is the best option for non-Steam Linux games, and the **M5 motion
rescale + fallback remap** + the **M6 conflict gate** make the cross-backend behavior correct.
- The whole effort proved the greenfield `hid-steam` UHID device is real and kernel-validated on two
kernels — the open question was always Steam-userspace promotion, and now it's answered.
### Remaining validation (no further construction recommended)
1. A **live SDL/non-Steam game** on a Linux host actually consuming the virtual Deck's grips/trackpads
(the path that *does* work) — needs a real Deck/SC client + a Steam-Input-disabled consumer.
2. The **Moonlight paddle regression** from the M3 xpad-map change (stock paddle client → host).