From 50e17b350827320de0a8910943751e20cfcd624b Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 28 Jun 2026 09:31:47 +0000 Subject: [PATCH] fix(host/capture): hold the session through a slow compositor switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Bazzite/SteamOS Gaming↔Desktop switch tears the old compositor down and can take 15s+ to bring the new one up — longer than the capture-loss rebuild's ~10s window, so the session failed mid-switch ("disconnect — session failed") and forced the client to cold-reconnect. Retry the rebuild within a 40s budget instead of giving up after one round, and re-detect the live compositor on each attempt so the stream follows the box to whatever session comes up (a new instance of the same compositor, or a different one — the kind-change case). The QUIC keepalive runs on its own thread, so the client stays connected (frozen on the last frame) and the stream resumes when the new output appears, with no reconnect. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/punktfunk-host/src/punktfunk1.rs | 67 +++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/crates/punktfunk-host/src/punktfunk1.rs b/crates/punktfunk-host/src/punktfunk1.rs index 4870a91..652dc8b 100644 --- a/crates/punktfunk-host/src/punktfunk1.rs +++ b/crates/punktfunk-host/src/punktfunk1.rs @@ -2714,15 +2714,76 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> { } tracing::warn!(error = %format!("{e:#}"), rebuild = capture_rebuilds, "capture lost — rebuilding pipeline in place"); - let (new_cap, new_enc, new_frame, new_interval) = - build_pipeline_with_retry(&mut vd, cur_mode, bitrate_kbps, bit_depth, plan) - .context("rebuild after capture loss")?; + // A Bazzite/SteamOS Gaming↔Desktop switch tears the old compositor down and can take + // 15s+ to bring the new one up. Don't fail the session over that (the client would + // have to cold-reconnect, surfacing a "session failed") — keep retrying within a + // generous budget while the QUIC keepalive (its own thread) holds the connection, + // RE-DETECTING the live compositor each attempt so we follow the box to whatever + // session comes up: a fresh instance of the same compositor, OR a different one + // (the kind-change case the session watcher also handles). The client stays + // connected, frozen on the last frame, and the stream resumes when the new output + // appears — no reconnect. + const REBUILD_BUDGET: std::time::Duration = std::time::Duration::from_secs(40); + let rebuild_deadline = std::time::Instant::now() + REBUILD_BUDGET; + let (new_cap, new_enc, new_frame, new_interval) = loop { + // Follow the active session unless an explicit PUNKTFUNK_COMPOSITOR pin forbids + // retargeting (then we stick to the pinned backend and just rebuild it). + if crate::config::config().compositor.is_none() { + let active = crate::vdisplay::detect_active_session(); + if let Some(c) = crate::vdisplay::compositor_for_kind(active.kind) { + crate::vdisplay::apply_session_env(&active); + crate::vdisplay::apply_input_env(c); + if c != compositor { + if matches!( + c, + crate::vdisplay::Compositor::Kwin + | crate::vdisplay::Compositor::Mutter + ) { + crate::vdisplay::settle_desktop_portal(c); + } + match crate::vdisplay::open(c) { + Ok(v) => { + tracing::info!(from = compositor.id(), to = c.id(), + "capture loss: active session switched compositor — retargeting"); + vd = v; + compositor = c; + } + Err(e2) => tracing::warn!(error = %format!("{e2:#}"), + "capture loss: opening the newly-detected compositor failed — retrying"), + } + } + } + } + match build_pipeline_with_retry( + &mut vd, + cur_mode, + bitrate_kbps, + bit_depth, + plan, + ) { + Ok(p) => break p, + Err(e2) => { + if stop.load(Ordering::SeqCst) + || std::time::Instant::now() >= rebuild_deadline + { + return Err(e2) + .context("capture lost — no compositor came up within the rebuild budget"); + } + tracing::warn!(error = %format!("{e2:#}"), + "capture lost — new session not up yet, retrying"); + } + } + }; capturer = new_cap; enc = new_enc; frame = new_frame; interval = new_interval; enc.request_keyframe(); // belt-and-suspenders; a fresh encoder opens on an IDR anyway next = std::time::Instant::now(); + tracing::info!( + compositor = compositor.id(), + "capture loss: pipeline rebuilt — stream resumes" + ); } } if perf && diag_at.elapsed() >= std::time::Duration::from_secs(2) {