From 664c26cb2bbdfe2e332b71ecd6a53a2df892944f Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 29 Jul 2026 01:26:21 +0200 Subject: [PATCH] fix(vdisplay): a pin with no heads to mirror must not refuse the session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The streamed-screen pin is host-wide and persisted, but the session it applies to is not. A box that boots between a desktop (heads to mirror) and a nested or headless Game Mode (none) carried the pin into a session where `monitors::resolve` could only fail — and since `vdisplay::open` is the one place every session opens a display, that failure was a host refusing to stream at all rather than one streaming the normal way. Pinning a monitor in Desktop mode bricked Game Mode. A compositor reporting NO heads whatsoever now degrades to the virtual-display path with the reason logged. Narrow on purpose: a pin that misses among heads that DO exist stays the hard error §5.2 makes it — showing someone a different screen than they asked for is the failure worth refusing over, and "there are no screens here" is not that. An enumeration ERROR also stays on the mirror path, so the session fails with the real reason instead of quietly ignoring the operator. Co-Authored-By: Claude Opus 5 (1M context) --- crates/pf-vdisplay/src/lib.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/pf-vdisplay/src/lib.rs b/crates/pf-vdisplay/src/lib.rs index bfad93f9..7c1b670d 100644 --- a/crates/pf-vdisplay/src/lib.rs +++ b/crates/pf-vdisplay/src/lib.rs @@ -369,7 +369,28 @@ pub fn capture_monitor() -> Option { pub fn open(compositor: Compositor) -> Result> { #[cfg(target_os = "linux")] if let Some(connector) = capture_monitor() { - return Ok(Box::new(mirror::MirrorDisplay::new(compositor, connector)?)); + // A pin is host-wide and PERSISTED, but the session it applies to is not: a box that boots + // between a desktop (heads to mirror) and a nested/headless Game Mode (none) would carry the + // pin into a session where `resolve` can only fail — and since this is the one place every + // session opens a display, that failure is a host that refuses to stream at all rather than + // one that streams the normal way. So a compositor reporting NO heads whatsoever degrades to + // the virtual-display path with the reason logged. + // + // Narrow on purpose. A pin that misses among heads that DO exist stays the hard error + // `monitors::resolve` makes it (design/per-monitor-portal-capture.md §5.2): showing someone + // a different screen than they asked for is the failure worth refusing over, and "there are + // no screens here" is not that. An enumeration ERROR also stays on the mirror path, so the + // session fails with the real reason instead of quietly ignoring the operator's choice. + match monitors::list(compositor) { + Ok(heads) if heads.is_empty() => tracing::warn!( + pinned = %connector, + compositor = compositor.id(), + "the streamed-screen pin names a monitor but this session has no physical heads to \ + mirror (a nested or headless compositor) — creating a virtual display instead; the \ + pin applies again as soon as a session with real heads runs" + ), + _ => return Ok(Box::new(mirror::MirrorDisplay::new(compositor, connector)?)), + } } #[cfg(target_os = "linux")] {