From 37c3e2bed27b2b9c4957b451f3b3335a11fe17ff Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 29 Jun 2026 13:26:42 +0000 Subject: [PATCH] =?UTF-8?q?docs(steam):=20criterion-4=20RESOLVED=20?= =?UTF-8?q?=E2=80=94=20the=20interface-2=20ceiling;=20recommend=20dropping?= =?UTF-8?q?=20M7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- design/steam-controller-deck-support.md | 104 ++++++++++++++++++++---- 1 file changed, 88 insertions(+), 16 deletions(-) diff --git a/design/steam-controller-deck-support.md b/design/steam-controller-deck-support.md index e10622f..dd5b7b6 100644 --- a/design/steam-controller-deck-support.md +++ b/design/steam-controller-deck-support.md @@ -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 M0–M6 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).