feat(host/vdisplay): per-connect active-session backend selection
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 29s
apple / swift (push) Successful in 1m16s
ci / bench (push) Successful in 1m34s
deb / build-publish (push) Successful in 4m32s
ci / rust (push) Successful in 7m2s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m23s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m26s
docker / deploy-docs (push) Successful in 18s

Bazzite/SteamOS boxes flip between Steam Gaming Mode (gamescope) and a
KDE/GNOME desktop. The host statically read PUNKTFUNK_COMPOSITOR /
XDG_CURRENT_DESKTOP once, so switching to Desktop Mode failed the stream, and
the gamescope managed-session path stopped+relaunched the autologin per connect
— leaking GPU context on F44 (reconnect → black screen).

Replace the static read with a runtime probe of the live session and route each
connect to the right backend, churn-free:

- vdisplay::detect_active_session() probes /proc for the running compositor of
  our uid (gamescope|kwin_wayland|gnome-shell|sway, desktop outranks a leftover
  gamescope) + scans the runtime dir for the live wayland-* socket. Returns an
  ActiveKind + the SessionEnv (WAYLAND_DISPLAY/XDG_RUNTIME_DIR/DBUS/
  XDG_CURRENT_DESKTOP) that targets it.
- apply_session_env() writes that into the process env per connect (host serves
  one session at a time), so every backend (capture + input) opens against the
  live session; apply_input_env() points input at the matching backend and
  selects gamescope ATTACH (no managed restart) unless PUNKTFUNK_GAMESCOPE_MANAGED.
- resolve_compositor() (native path) auto-detects + applies; explicit
  PUNKTFUNK_COMPOSITOR still wins (legacy/CI/forcing). detect() is now
  active-aware for the GameStream/mgmt callers too.
- Bazzite host.env drops the static gamescope force; documents auto-detection
  + the optional overrides.

Result: Desktop Mode → KWin/Mutter virtual output at the client's mode
(churn-free, the reliable path); Gaming Mode → attach to the running gamescope
(no SIGSEGV/GPU leak on reconnect). Compiles + clippy-clean; 78 host tests pass.
Live validation on the Bazzite box pending (box offline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 21:41:51 +00:00
parent 0bc60ebc44
commit 6f77574876
4 changed files with 345 additions and 17 deletions
+23 -2
View File
@@ -1499,11 +1499,32 @@ fn pick_compositor(
/// async reactor (`spawn_blocking`).
fn resolve_compositor(pref: CompositorPref) -> Result<crate::vdisplay::Compositor> {
use crate::vdisplay::Compositor;
// Explicit operator override (legacy / CI / forcing a backend for a test) wins and is assumed
// to come with a hand-set env — don't retarget the process env in that case.
let overridden = std::env::var_os("PUNKTFUNK_COMPOSITOR").is_some();
let detected = if overridden {
crate::vdisplay::detect().ok()
} else {
// Auto: detect the LIVE session (Gaming vs Desktop) and retarget the process env at it so
// every backend (video capture + input) this connect opens against the active session —
// this is the state machine that lets one host follow a Bazzite box across Gaming↔Desktop.
let active = crate::vdisplay::detect_active_session();
crate::vdisplay::apply_session_env(&active);
tracing::info!(
active = ?active.kind,
wayland = active.env.wayland_display.as_deref().unwrap_or("-"),
"detected active graphical session"
);
crate::vdisplay::compositor_for_kind(active.kind)
};
let available = crate::vdisplay::available();
let detected = crate::vdisplay::detect().ok();
let chosen = pick_compositor(pref, &available, detected).ok_or_else(|| {
anyhow!("no usable compositor (set PUNKTFUNK_COMPOSITOR or run inside a supported desktop)")
anyhow!("no usable compositor (no live graphical session for this uid; set PUNKTFUNK_COMPOSITOR or start a desktop/gaming session)")
})?;
if !overridden {
// Point input at the same backend and select gamescope ATTACH (no churny managed restart).
crate::vdisplay::apply_input_env(chosen);
}
let avail_ids: Vec<&str> = available.iter().map(|c| c.id()).collect();
match Compositor::from_pref(pref) {
Some(want) if want == chosen => {