diff --git a/crates/pf-vdisplay/src/lib.rs b/crates/pf-vdisplay/src/lib.rs index 2bfc79b4..fca354aa 100644 --- a/crates/pf-vdisplay/src/lib.rs +++ b/crates/pf-vdisplay/src/lib.rs @@ -254,6 +254,34 @@ pub fn detect() -> Result { } } +/// Attach-only probes: while any scope is held, backend `create` paths must not stop, relaunch, +/// or take over box sessions — they may only attach to an already-live output, and fail fast +/// otherwise. The capture-loss rebuild holds one for its first seconds: right after a capture +/// loss the active-session detection can be STALE (a Game→Desktop switch observed live: the +/// probe's gamescope re-acquire restarted `gamescope-session.target` and yanked the user out of +/// the KDE session they had just switched to). A counter, so overlapping scopes compose. +static REBUILD_PROBES: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); + +/// RAII scope marking pipeline builds as attach-only probes (see [`rebuild_probe_active`]). +pub struct RebuildProbeScope(()); + +pub fn rebuild_probe_scope() -> RebuildProbeScope { + REBUILD_PROBES.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + RebuildProbeScope(()) +} + +impl Drop for RebuildProbeScope { + fn drop(&mut self) { + REBUILD_PROBES.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); + } +} + +/// Is any [`rebuild_probe_scope`] active? Destructive session operations (stop/relaunch/ +/// takeover-restart) must be skipped while true. +pub fn rebuild_probe_active() -> bool { + REBUILD_PROBES.load(std::sync::atomic::Ordering::SeqCst) > 0 +} + /// Open the virtual-display driver for `compositor`. pub fn open(compositor: Compositor) -> Result> { #[cfg(target_os = "linux")] diff --git a/crates/pf-vdisplay/src/vdisplay/linux/gamescope.rs b/crates/pf-vdisplay/src/vdisplay/linux/gamescope.rs index e858c5ff..38279404 100644 --- a/crates/pf-vdisplay/src/vdisplay/linux/gamescope.rs +++ b/crates/pf-vdisplay/src/vdisplay/linux/gamescope.rs @@ -325,6 +325,29 @@ fn create_managed_session(client: &str, mode: Mode) -> Result { if steamos_session_present() { return create_managed_session_steamos(mode); } + // Attach-only rebuild probe: reuse a live same-mode session, but NEVER stop/relaunch box + // sessions — right after a capture loss the caller's session detection can be stale, and a + // destructive rebuild here would fight the session the user just switched to. + if crate::rebuild_probe_active() { + let guard = MANAGED_SESSION.lock().unwrap_or_else(|e| e.into_inner()); + let same_mode = guard.as_ref().is_some_and(|s| { + s.width == mode.width && s.height == mode.height && s.refresh_hz == mode.refresh_hz + }); + if same_mode { + if let Some(node_id) = find_gamescope_node() { + point_injector_at_eis(); + tracing::info!( + node_id, + "gamescope session: attach-only probe reusing live node" + ); + return Ok(managed_output(node_id, mode)); + } + } + return Err(anyhow!( + "gamescope session has no attachable live node — attach-only rebuild probe refuses \ + to stop/relaunch box sessions (re-detection follows the live session)" + )); + } // Steam is single-instance: if the box autologged into gaming mode on a physical display (the // Bazzite default — `gamescope-session-plus@ogui-steam` on the TV), that session holds Steam and // renders to the TV's native mode, which we'd capture instead of the client's. Free Steam by @@ -650,6 +673,16 @@ fn create_managed_session_steamos(mode: Mode) -> Result { } *guard = None; // tracked session lost its node — fall through to a clean restart } + // Attach-only rebuild probe: the reuse path above may attach, but a restart of the session + // target is out of bounds — observed live on a Deck: a stale post-capture-loss detection made + // this restart steal the seat back from the KDE session the user had just switched to. + if crate::rebuild_probe_active() { + return Err(anyhow!( + "gamescope has no live node and this is an attach-only rebuild probe — refusing to \ + restart {STEAMOS_SESSION_TARGET} (the box may be mid-switch to another session; \ + re-detection follows it)" + )); + } let shim_dir = write_headless_shim()?; write_steamos_dropin(&shim_dir, mode)?; systemctl_user(&["daemon-reload"]); diff --git a/crates/punktfunk-host/src/native/stream.rs b/crates/punktfunk-host/src/native/stream.rs index 97fd9c3c..753063e7 100644 --- a/crates/punktfunk-host/src/native/stream.rs +++ b/crates/punktfunk-host/src/native/stream.rs @@ -1884,7 +1884,15 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option