feat(host/steam): M6 — Steam-pad conflict gate (hardware-validated)
Don't present a virtual Steam (28DE) pad on a host that already has a physical Steam controller — the host's own Steam Input would then manage two Decks and confuse player assignment. - physical_steam_controller_present(): scans /sys/bus/hid/devices for a 28DE HID device on a real (non-/virtual/) path. - degrade_steam_on_conflict() in resolve_gamepad: a resolved SteamDeck / SteamController with a physical Steam controller attached degrades to DualSense (then the M5 uhid ladder); PUNKTFUNK_STEAM_FORCE=1 overrides (e.g. a remote-only box with no competing Steam Input). Validated on real hardware (a SteamOS Steam Deck @ .253 + a Bazzite host @ .41, both running Steam): - Conflict confirmed: the Deck-as-host already has its physical 28DE:1205 AND Steam's 28DE:11FF XInput output pad live; a 2nd virtual 28DE = two Decks. - Bind robustness: the virtual Deck binds hid-steam on a SECOND kernel (Bazzite 6.17.7, vs the dev box 7.0) and the kernel accepted our serial (the M1 fix). - Criterion-4 (running-Steam recognition) PARTIAL: a userspace consumer (Steam/ SDL) engaged the virtual Deck (opened the hidraw, ran the lizard-disable + settings sequence the kernel's Deck path skips) but emitted NO 28DE:11FF XInput pad on the desktop — so Steam recognizes it enough to manage lizard mode but did not promote it to a managed XInput controller (likely needs a Big-Picture/game context, or a richer device; the 0x83/0xA1 attribute probes never fired, so it wasn't a probe-reject either). - The heuristic itself checks TRUE on the Deck, FALSE on Bazzite. Workspace clippy/fmt/test green. Not pushed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1947,6 +1947,57 @@ fn degrade_if_no_uhid(chosen: GamepadPref) -> GamepadPref {
|
||||
chosen
|
||||
}
|
||||
|
||||
/// True if a **physical** Valve Steam controller (`28DE`) is already attached. The host's own Steam
|
||||
/// Input is then managing a `28DE` device, and presenting a second (virtual) one makes Steam juggle
|
||||
/// two Decks — confirmed conflict-prone on a Deck-as-host (the physical `28DE:1205` + Steam's
|
||||
/// `28DE:11FF` XInput output pad are both live). HID device dirs are named `BUS:VID:PID.INST`
|
||||
/// (uppercase); a UHID virtual device resolves through `/devices/virtual/…`, a real one does not.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn physical_steam_controller_present() -> bool {
|
||||
let Ok(entries) = std::fs::read_dir("/sys/bus/hid/devices") else {
|
||||
return false;
|
||||
};
|
||||
entries.flatten().any(|e| {
|
||||
if !e.file_name().to_string_lossy().contains(":28DE:") {
|
||||
return false;
|
||||
}
|
||||
match std::fs::read_link(e.path()) {
|
||||
Ok(target) => !target.to_string_lossy().contains("/virtual/"),
|
||||
Err(_) => true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Gate a virtual Steam pad off when a physical Steam controller is attached (§ conflict). Degrade to
|
||||
/// DualSense (then the uhid ladder), which Steam treats as an ordinary, distinct pad. Override with
|
||||
/// `PUNKTFUNK_STEAM_FORCE=1` when the host has no competing Steam Input (e.g. a remote-only box).
|
||||
#[cfg(target_os = "linux")]
|
||||
fn degrade_steam_on_conflict(chosen: GamepadPref) -> GamepadPref {
|
||||
if !matches!(
|
||||
chosen,
|
||||
GamepadPref::SteamDeck | GamepadPref::SteamController
|
||||
) {
|
||||
return chosen;
|
||||
}
|
||||
let forced = std::env::var("PUNKTFUNK_STEAM_FORCE")
|
||||
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false);
|
||||
if !forced && physical_steam_controller_present() {
|
||||
tracing::warn!(
|
||||
wanted = chosen.as_str(),
|
||||
"a physical Steam controller is attached — the host's Steam Input would manage two 28DE \
|
||||
devices; falling back to DualSense (set PUNKTFUNK_STEAM_FORCE=1 to override)"
|
||||
);
|
||||
return degrade_if_no_uhid(GamepadPref::DualSense);
|
||||
}
|
||||
chosen
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn degrade_steam_on_conflict(chosen: GamepadPref) -> GamepadPref {
|
||||
chosen
|
||||
}
|
||||
|
||||
/// Resolve the client's gamepad-backend preference (the env/logging shell around
|
||||
/// [`pick_gamepad`]). Always concrete — the `Welcome` reports what the session will drive.
|
||||
fn resolve_gamepad(pref: GamepadPref) -> GamepadPref {
|
||||
@@ -1961,6 +2012,10 @@ fn resolve_gamepad(pref: GamepadPref) -> GamepadPref {
|
||||
// backends need `/dev/uhid` usable *now*, else creating the device just fails and the controller
|
||||
// goes dead — fall back to the always-available uinput X-Box 360 pad instead.
|
||||
let chosen = degrade_if_no_uhid(chosen);
|
||||
// Conflict gate: don't present a virtual Steam (28DE) pad when the host already has a physical
|
||||
// Steam controller — its own Steam Input would then manage two Decks (confirmed conflict-prone on
|
||||
// a Deck-as-host). `PUNKTFUNK_STEAM_FORCE=1` overrides.
|
||||
let chosen = degrade_steam_on_conflict(chosen);
|
||||
match pref {
|
||||
GamepadPref::Auto => {
|
||||
// The operator's env knob deserves a diagnostic when it didn't drive the
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
# Rich Steam Controller & Steam Deck support
|
||||
|
||||
> **Status:** **M0–M5 GREEN — full pipeline + fallback polish built (2026-06-29).** Host: the
|
||||
> virtual `hid-steam` Deck binds, is byte-exact, is a wired backend (`PUNKTFUNK_GAMEPAD=steamdeck`),
|
||||
> the protocol carries the rich inputs, and the **fallback remap** keeps them from silently dropping
|
||||
> on a non-Steam backend. 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 is **validation, not construction** (see below) + the
|
||||
> deferred extras (M6 SteamOS-host conflict check, M7 Windows UMDF Steam driver).
|
||||
> **Status:** **M0–M6 GREEN — full pipeline + fallback + conflict gate built (2026-06-29).** Host:
|
||||
> the virtual `hid-steam` Deck binds, is byte-exact, is a wired backend (`PUNKTFUNK_GAMEPAD=steamdeck`),
|
||||
> the protocol carries the rich inputs, the **fallback remap** keeps them from silently dropping, and
|
||||
> 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.
|
||||
>
|
||||
> **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):**
|
||||
> `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
|
||||
> `PUNKTFUNK_STEAM_FORCE=1`. Heuristic hardware-checked: **TRUE on the Deck, FALSE on Bazzite.**
|
||||
> Workspace clippy/fmt/test green.
|
||||
>
|
||||
> **M5 (fallback remap + degrade ladder) result:** new pure, unit-tested `inject/proto/steam_remap.rs`:
|
||||
> (1) **motion rescale** `motion_wire_to_deck` — the wire carries DualSense-convention units (what
|
||||
@@ -34,9 +53,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 (whole feature, needs hardware we don't have):** (1) recognition of the
|
||||
> virtual Deck by a **running Steam** on the host; (2) a **live Deck/Steam Controller client** actually
|
||||
> sending paddles/trackpads/gyro; (3) the **Moonlight paddle regression** from the M3 xpad-map change.
|
||||
> **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.
|
||||
>
|
||||
> **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
|
||||
|
||||
Reference in New Issue
Block a user