fix(client-linux): Deck Gaming Mode — auto pad type, real chrome-less fullscreen, leave-to-Gaming-Mode, colour bisect

- "Automatic" gamepad type resolves to the virtual Steam Deck pad on Deck
  hardware (env SteamDeck / DMI Jupiter|Galileo): the built-in 28DE:1205
  identity is invisible at Hello time — the Valve HIDAPI drivers run
  in-session only and Steam Input shadows the pad with its virtual X360 —
  so auto always fell through to Xbox 360. "steamdeck" is now also
  selectable in Settings.
- Chrome-less launches flatten the window CSS (border-radius/box-shadow)
  and fullscreen at startup: gamescope never ACKs the xdg fullscreen
  state, so adwaita kept the floating-CSD rounded corners + shadow
  visible over the stream.
- Gaming-Mode --connect launches quit on session end, so Steam ends the
  "game" and the Deck returns to Gaming Mode — previously the app popped
  to its own hosts page, stranding the user fullscreen and making the
  escape chord read as broken.
- The capture hint is controller-aware; the chromeless hint teaches the
  hold-chord ("hold L1+R1+Start+Select to leave") and a quick chord press
  re-flashes it.
- Colour bisect for the reported off-colours on the VAAPI dmabuf path:
  graphics offload defaults OFF under gamescope (a subsurface hands the
  NV12 CSC to the compositor), PUNKTFUNK_OFFLOAD=1|0 overrides, and each
  colour-signaling change logs whether GDK accepted the BT.709-narrow
  color state (fallback = GDK's BT.601 dmabuf default).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 17:16:26 +00:00
parent 20f0d2802f
commit e9c1f4083a
5 changed files with 128 additions and 14 deletions
+26
View File
@@ -114,6 +114,21 @@ fn pref_for_type(t: sdl3::gamepad::GamepadType) -> GamepadPref {
}
}
/// Best-effort "this machine is a Steam Deck". The Gaming-Mode env short-circuits; desktop
/// mode falls back to DMI (Valve board, Jupiter = LCD / Galileo = OLED — readable inside the
/// flatpak sandbox). Cached: the answer can't change while we run.
pub fn is_steam_deck() -> bool {
static DECK: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*DECK.get_or_init(|| {
if std::env::var_os("SteamDeck").is_some() {
return true;
}
let dmi = |f: &str| std::fs::read_to_string(format!("/sys/class/dmi/id/{f}"));
dmi("board_vendor").is_ok_and(|v| v.trim() == "Valve")
&& dmi("product_name").is_ok_and(|p| matches!(p.trim(), "Jupiter" | "Galileo"))
})
}
enum Ctl {
Attach(Arc<NativeClient>),
Detach,
@@ -197,8 +212,19 @@ impl GamepadService {
/// What "Automatic" resolves to right now — the virtual pad matching the physical one
/// (Swift parity); no pad connected leaves the host's own default.
///
/// **Steam Deck special case:** this is read at session start, *before* attach — but the
/// Deck's built-in controller is only enumerable with its real 28DE:1205 identity while
/// the Valve HIDAPI drivers run, and those are enabled on attach only (see
/// [`set_valve_hidapi`]); with Steam Input on, SDL sees nothing but Steam's virtual
/// X360 pad anyway. Both cases used to fall through to Xbox 360. On a Deck, a virtual
/// pad (or no pad at all) means the physical controller behind it IS the built-in one —
/// resolve to the Steam Deck virtual pad so the paddles/trackpads/gyro have somewhere
/// to land. A real external controller still wins (it's the one that gets forwarded).
pub fn auto_pref(&self) -> GamepadPref {
match self.active() {
Some(p) if !p.steam_virtual => p.pref,
_ if is_steam_deck() => GamepadPref::SteamDeck,
Some(p) => p.pref,
None => GamepadPref::Auto,
}