From 963c406f3379e35d959d28c4991ef1010e84c5a8 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 29 Jun 2026 16:32:42 +0000 Subject: [PATCH] feat(host/steam): default the gadget Deck on for SteamOS (glass-confirmed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The virtual Steam Deck is validated glass-to-glass on a Deck: it appears as a distinct second Steam controller, a held A drives Steam's overlay ("Resume Game"), and a button press registers in a real game (confirmed in-game). gadget_preferred() now defaults ON for SteamOS hosts (/etc/os-release ID=steamos or ID_LIKE), OFF elsewhere where the universal UHID path stays the default; PUNKTFUNK_STEAM_GADGET=1/0 forces it. A Deck-as-host with a physical Deck never reaches this path — resolve_gamepad's conflict gate degrades SteamDeck → DualSense first, so the two-Deck case never happens in production (it was only a test-rig confound on the dev Deck). The feature is complete: a virtual Steam Deck that Steam Input recognizes + promotes, churn-free, with input flowing to games. Workspace clippy/fmt/test green. Not pushed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/inject/linux/steam_controller.rs | 4 ++-- .../src/inject/linux/steam_gadget.rs | 23 +++++++++++++++---- design/steam-controller-deck-support.md | 12 ++++++++++ packaging/linux/steam-deck-gadget/README.md | 11 +++++---- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/crates/punktfunk-host/src/inject/linux/steam_controller.rs b/crates/punktfunk-host/src/inject/linux/steam_controller.rs index 3d9d4d2..9c19f7d 100644 --- a/crates/punktfunk-host/src/inject/linux/steam_controller.rs +++ b/crates/punktfunk-host/src/inject/linux/steam_controller.rs @@ -384,8 +384,8 @@ impl SteamControllerManager { if idx >= MAX_PADS || self.pads[idx].is_some() || self.broken { return; } - // Prefer the USB gadget on SteamOS (the only transport Steam Input promotes); fall back to the - // universal UHID pad if the gadget is unavailable or not opted in. + // Prefer the USB gadget on SteamOS (default there — the only transport Steam Input promotes); + // fall back to the universal UHID pad if the gadget is unavailable or disabled. let opened = if crate::inject::steam_gadget::gadget_preferred() { crate::inject::steam_gadget::ensure_modules(); match crate::inject::steam_gadget::SteamDeckGadget::open(idx as u8) { diff --git a/crates/punktfunk-host/src/inject/linux/steam_gadget.rs b/crates/punktfunk-host/src/inject/linux/steam_gadget.rs index 981231a..b0e6764 100644 --- a/crates/punktfunk-host/src/inject/linux/steam_gadget.rs +++ b/crates/punktfunk-host/src/inject/linux/steam_gadget.rs @@ -606,11 +606,24 @@ pub fn ensure_modules() { } } -/// Whether to prefer the USB-gadget Deck over the UHID `SteamDeckPad`. SteamOS-host only (needs the -/// gadget modules + root) and opt-in for now (`PUNKTFUNK_STEAM_GADGET=1`) while the full Steam-Input -/// feature contract is hardened; defaults off, so the universal UHID path stays the default. +/// Whether to prefer the USB-gadget Deck over the UHID `SteamDeckPad` — the only transport Steam Input +/// promotes (validated glass-to-glass on a Deck). Defaults **on for SteamOS** hosts (which ship the +/// gadget modules + run Steam Input); off elsewhere, where the universal UHID path stays the default. +/// `PUNKTFUNK_STEAM_GADGET=1`/`0` forces it on/off. A Deck-as-host with a *physical* Deck never reaches +/// here: `resolve_gamepad`'s conflict gate degrades `SteamDeck` → DualSense before the manager is built. pub fn gadget_preferred() -> bool { - std::env::var("PUNKTFUNK_STEAM_GADGET") - .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + if let Ok(v) = std::env::var("PUNKTFUNK_STEAM_GADGET") { + return v == "1" || v.eq_ignore_ascii_case("true"); + } + is_steamos() +} + +/// True on SteamOS-class hosts (`/etc/os-release` `ID=steamos`, or `ID_LIKE` naming it). +fn is_steamos() -> bool { + std::fs::read_to_string("/etc/os-release") + .map(|s| { + s.lines() + .any(|l| l == "ID=steamos" || (l.starts_with("ID_LIKE=") && l.contains("steamos"))) + }) .unwrap_or(false) } diff --git a/design/steam-controller-deck-support.md b/design/steam-controller-deck-support.md index 061f5a2..36ab957 100644 --- a/design/steam-controller-deck-support.md +++ b/design/steam-controller-deck-support.md @@ -822,3 +822,15 @@ confirms our state reports (pressed button at byte 8) are delivered on the inter by hid-steam — so with M1/M2's byte-8→BTN_SOUTH decode, the input chain is proven end-to-end. The only piece left is a foreground-game confirmation that Steam Input maps it onto the X-Box pad (Steam only maps contextually), after which the gadget can default on for SteamOS hosts. + +### Glass confirmed + default-on for SteamOS (2026-06-29) + +Validated glass-to-glass on the Deck: the gadget shows up as a distinct second Steam controller, a +held A snaps the Steam overlay shut as "Resume Game" (so Steam Input receives + acts on the gadget's +input), and **a button press registers in a real game** — confirmed in-game. The two-Deck test +confound (the Deck has its physical Deck + the virtual one) is a test-rig artifact, not a feature +limit: a real non-Deck SteamOS host has only the virtual Deck, and a Deck-as-host degrades `SteamDeck` +→ DualSense via the M6 conflict gate before the gadget is ever built. So `gadget_preferred()` now +defaults **on for SteamOS** (`/etc/os-release` `ID=steamos`), off elsewhere (UHID stays default), +with `PUNKTFUNK_STEAM_GADGET=1`/`0` to force. The virtual Steam Deck — recognized + promoted by Steam +Input, churn-free, input flowing to games — is complete. diff --git a/packaging/linux/steam-deck-gadget/README.md b/packaging/linux/steam-deck-gadget/README.md index 0fa2e61..28b819a 100644 --- a/packaging/linux/steam-deck-gadget/README.md +++ b/packaging/linux/steam-deck-gadget/README.md @@ -56,14 +56,17 @@ Steam Input, which exposes its own X-Box 360 pad — exactly a real Deck's behav `EP_WRITE` starves the control path. - `dummy_hcd` + `raw_gadget` must both be loaded and `/dev/raw-gadget` present before launch. -## Host backend (shipped, opt-in) +## Host backend (shipped — default on for SteamOS) The C PoC's transport is ported to a Rust host gamepad backend: `crates/punktfunk-host/src/inject/linux/steam_gadget.rs` (`SteamDeckGadget`), driven by the same `steam_proto` serializer as the UHID `SteamDeckPad`. The Steam-Deck manager -(`inject/linux/steam_controller.rs`) now selects per-pad between **UHID** (default, universal) and the -**USB gadget** (`PUNKTFUNK_STEAM_GADGET=1`, SteamOS-only — best-effort `modprobe dummy_hcd raw_gadget`, -graceful fallback to UHID if `/dev/raw-gadget` is unusable). +(`inject/linux/steam_controller.rs`) selects per-pad between **UHID** (universal) and the **USB +gadget**: the gadget is the **default on SteamOS hosts** (`gadget_preferred()` → `ID=steamos`; +best-effort `modprobe dummy_hcd raw_gadget`, graceful fallback to UHID if `/dev/raw-gadget` is +unusable), and off elsewhere where UHID stays the default. `PUNKTFUNK_STEAM_GADGET=1`/`0` forces it. +A Deck-as-host with a *physical* Deck never uses it — `resolve_gamepad`'s conflict gate degrades +`SteamDeck` → DualSense first. The Rust transport is **validated on the Deck** (a static musl test binary that `#[path]`-includes the real module): it enumerates the 3-interface Deck, hid-steam binds it + reads our serial + creates the