fix(host): survive Bazzite's SDDM session supervisor in the gamescope takeover

Diagnosed live on the .181 Bazzite F44 box (couldn't connect at all; field
reports of streams dying after 30 s-5 min):

Bazzite autologs into game mode via SDDM with Relogin=true, so the moment the
managed takeover stops gamescope-session-plus@<client>, SDDM logs back in and
restarts it within the same second. The resurrected autologin session then
fights our transient session-plus over the Steam single instance and the GPU
for the whole stream: its wrapper relaunches gamescope every ~7 s (each one
missing the wrapper's hard 5 s readiness window on a slow NVIDIA init), the
churn SIGSEGVs gamescopes, and eventually the streaming gamescope dies with it.
Meanwhile a client that gave up left the pipeline-rebuild retry loop SIGKILLing
and relaunching the box's Steam session for up to ~6 more minutes.

- stop_autologin_sessions: runtime-mask each autologin unit before the SIGKILL
  stop, so no supervisor can restart it underneath the stream; match every
  loaded instance (the unit flaps through activating/failed mid-churn). Every
  restore path unmasks unconditionally (including the desktop-active early
  return), and --runtime keeps the mask in tmpfs so a reboot clears it.
- launch_session: supervise the transient unit while polling for the node —
  the session-plus wrapper kill -9s a gamescope that missed its 5 s readiness
  handshake and exits 1, so relaunch it (after a short driver-settle cooldown)
  instead of waiting the rest of the 45 s on a corpse.
- build_pipeline_with_retry: abort between attempts once the session's QUIC
  connection is closed — no more minutes of Steam churn for a departed client.

Validated live on .181: cold-boot connect streams 2059 frames/45 s (p50
5.1 ms), zero SDDM resurrections while masked, TV session restored+unmasked on
disconnect, warm same-mode reconnect reuses the session (866 frames/15 s).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 06:15:12 +00:00
parent 71e1865519
commit 79bd847e85
2 changed files with 114 additions and 26 deletions
+15 -1
View File
@@ -3096,7 +3096,7 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
let _idd_setup_guard = (plan.capture == crate::session_plan::CaptureBackend::IddPush)
.then(|| crate::vdisplay::manager::vdm().begin_idd_setup(stop.clone()));
let (mut capturer, mut enc, mut frame, mut interval, mut cur_node_id) =
build_pipeline_with_retry(&mut vd, mode, bitrate_kbps, bit_depth, plan, &quit)?;
build_pipeline_with_retry(&mut vd, mode, bitrate_kbps, bit_depth, plan, &quit, &stop)?;
// Setup done — release the IDD-push setup lock so the next reconnect can begin (and preempt us).
#[cfg(target_os = "windows")]
drop(_idd_setup_guard);
@@ -3280,6 +3280,7 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
bit_depth,
plan,
&quit,
&stop,
)?;
Ok((new_vd, pipe))
})();
@@ -3486,6 +3487,7 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
bit_depth,
plan,
&quit,
&stop,
) {
Ok(p) => break p,
Err(e2) => {
@@ -3793,6 +3795,7 @@ fn build_pipeline_with_retry(
bit_depth: u8,
plan: crate::session_plan::SessionPlan,
quit: &Arc<AtomicBool>,
stop: &Arc<AtomicBool>,
) -> Result<Pipeline> {
// ~10s first-frame wait per attempt. 8 gives a ~90s budget for the SLOW case: a host-managed
// gamescope session cold-starting Steam Big Picture (the SteamOS/Bazzite takeover) can take
@@ -3819,6 +3822,17 @@ fn build_pipeline_with_retry(
const MAX_ATTEMPTS: u32 = 8;
let mut backoff = std::time::Duration::from_millis(500);
for attempt in 1..=MAX_ATTEMPTS {
// The client is gone (connection closed → `stop`): every further attempt only churns the
// box for a session no one is watching — on a Bazzite takeover that means SIGKILLing and
// relaunching the box's Steam session once per attempt for minutes (the .181 storm
// 2026-07-07). One in-flight attempt can still overhang; this bounds the damage to it.
if attempt > 1 && stop.load(Ordering::SeqCst) {
anyhow::bail!(
"session ended (client disconnected) during pipeline build — aborting retries \
after {} attempt(s)",
attempt - 1
);
}
match build_pipeline(vd, mode, bitrate_kbps, bit_depth, plan, quit) {
Ok(pipe) => {
if attempt > 1 {