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:
@@ -246,14 +246,33 @@ fn open_gs_virtual_source(
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// A client is (re)connecting → cancel any pending TV-session restore (review #3).
|
||||
crate::vdisplay::cancel_pending_tv_restore();
|
||||
let active = crate::vdisplay::detect_active_session();
|
||||
// A4: fold any compositor-instance change (idle-time Game↔Desktop switch) into the epoch
|
||||
// before acquiring, so a GameStream reconnect never reuses a dead-instance node.
|
||||
crate::vdisplay::observe_session_instance(&active);
|
||||
crate::vdisplay::apply_session_env(&active);
|
||||
let c = crate::vdisplay::compositor_for_kind(active.kind)
|
||||
.map(Ok)
|
||||
.unwrap_or_else(crate::vdisplay::detect)
|
||||
.context("detect compositor")?;
|
||||
crate::vdisplay::apply_input_env(c);
|
||||
c
|
||||
// Dedicated game session (B0): a GameStream app whose launch RESOLVES to a command (library
|
||||
// id / apps.json command), under `game_session=dedicated` with gamescope available, gets its
|
||||
// own headless gamescope spawn at the client mode — same routing as the native plane. Gate on
|
||||
// the resolved command so an unresolvable entry falls back to auto routing (review #9).
|
||||
let has_launch = crate::library::resolve_session_launch(
|
||||
app.and_then(|a| a.library_id.as_deref()),
|
||||
app.and_then(|a| a.cmd.as_deref()),
|
||||
)
|
||||
.is_some();
|
||||
if crate::vdisplay::wants_dedicated_game_session(has_launch) {
|
||||
crate::vdisplay::apply_input_env(crate::vdisplay::Compositor::Gamescope, true);
|
||||
crate::vdisplay::Compositor::Gamescope
|
||||
} else {
|
||||
let c = crate::vdisplay::compositor_for_kind(active.kind)
|
||||
.map(Ok)
|
||||
.unwrap_or_else(crate::vdisplay::detect)
|
||||
.context("detect compositor")?;
|
||||
crate::vdisplay::apply_input_env(c, false);
|
||||
c
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut vd = crate::vdisplay::open(compositor).context("open virtual display")?;
|
||||
|
||||
Reference in New Issue
Block a user