feat(host): game-mode integration + dedicated game sessions

Implements design/gamemode-and-dedicated-sessions.md (Parts A1-A5 + B0-B2):
reconciles the merged display-management registry with session-mobile
Bazzite/SteamOS hosts and adds a per-launch dedicated gamescope mode.

- A1 DisplayOwnership {Owned,External,SessionManaged} + poolable_now(): the
  registry pools only what it owns, so gamescope managed/attach outputs are no
  longer double-owned by the registry AND the gamescope restore worker (fixes
  the game-mode-reconnect stale-node wedge).
- A2 validated reuse: (backend,mode,launch,epoch) reuse key + kept_display_alive
  liveness probe + reused_gen/mark_failed on a reused-display first-frame failure.
- A3 policy-driven managed restore (keep_alive replaces the hardcoded 5s debounce;
  forever = held = gaming-rig truthful) + crash-restore persist + SIGKILL teardown
  (kill_unit, applied to our transient unit AND the autologin stop -- validated
  live on .181 to avoid the F44 GPU-context leak).
- A4 session epoch: observe_session_instance bumps the epoch + invalidate_backend
  on a desktop-compositor instance change; gamescope spawns are exempt.
- A5 per-spawn log + PID-scoped gamescope node discovery.
- B0 game_session {auto,dedicated} policy (top-level, preset-orthogonal) +
  pick_gamescope_mode dedicated_launch + steam -silent command shaping.
- B1 free the autologin Steam before a dedicated Steam spawn (single-instance).
- B2 game-exit -> APP_EXITED_CLOSE_CODE (0x52) clean session end.

Adversarially reviewed (11 findings fixed). Validated on glass (.181 Bazzite F44,
RTX 4090): dedicated spawn streams a real game smoothly; keep-alive reuse; the
SIGKILL fix avoids the F44 vkCreateDevice leak. Workspace green
(build / test --workspace / clippy -D warnings / fmt), OpenAPI + C header
regenerated, web console tsc + vite build green. clients/probe: bump the
no-video timeout 8s->45s for gamescope cold starts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 06:26:11 +00:00
parent d76a42e0e9
commit 04309d0ad9
22 changed files with 1711 additions and 163 deletions
@@ -106,6 +106,26 @@ Per-backend support:
each client), up to **max displays**. Arrange them under Host → *Virtual displays* once two or more
are streaming.
### Dedicated game sessions
**Dedicated game sessions** control how a session that *launches a game from your library* is served
(Linux hosts):
- **Auto** (default) — the launch rides whatever session the box is in: the managed Steam session on a
Steam Deck / Bazzite couch box, a bare gamescope on a plain distro, or spawned into your live KDE /
GNOME / Sway desktop.
- **Dedicated** — every library launch gets its **own headless gamescope at your exact resolution and
refresh**, with just the game inside. The game boots straight in — no Steam Big Picture to navigate,
no game-mode desktop. Steam titles launch with the client hidden (`steam -silent`); non-Steam titles
start almost instantly (gamescope up in ~1 s, then the game's own boot). Combined with **keep alive**,
the game keeps running when you disconnect and you re-attach straight back into it; when you quit the
game, the session ends cleanly and your client returns to its library.
Dedicated needs `gamescope` installed on the host; if it isn't, a launch falls back to **Auto**
routing. This axis is independent of the preset — pick it under Host → *Virtual displays*. On a box
that's already in Steam game mode, a dedicated Steam launch frees game mode's Steam first and restores
it when the session ends. (GameStream / Moonlight launches follow the same routing.)
## Persistent scaling
Set your display **scaling** once and have it stick across reconnects. This works by giving each
@@ -149,3 +169,13 @@ an empty extension. Use **Primary** or **Exclusive** so your desktop actually la
**KWin virtual outputs need KWin ≥ 6.5.6.** Older KWin can't create the virtual output at all —
see [requirements](/docs/requirements).
**Reconnecting into game mode reconnects cleanly now.** On a Steam Deck / Bazzite box, disconnecting
and reconnecting within game mode reuses the still-warm session (or cleanly recreates it) instead of
landing on a dead stream — and switching between game mode and the KDE / GNOME desktop mid-stream
follows the switch. If a launched game **exits**, a dedicated session ends and returns you to your
library; a game mode / desktop session keeps streaming.
**My couch box's TV stayed on the streamed session after I disconnected.** With the **gaming-rig**
preset (keep alive = *forever*), a managed Steam session is held indefinitely so a reconnect resumes
instantly — return to game mode on the box (or restart the host) to hand the TV back.