diff --git a/src/server/drm_capturer.rs b/src/server/drm_capturer.rs index e72199de2..bf405a7f4 100644 --- a/src/server/drm_capturer.rs +++ b/src/server/drm_capturer.rs @@ -635,7 +635,27 @@ pub(super) fn get_display_infos() -> Option> { ProbeState::Available(list) => list.clone(), _ => return None, }; - Some(augment_with_wayland_geometry(&list)) + let multi = list.len() > 1; + let mut infos = augment_with_wayland_geometry(&list); + // review 4.5: on a multi-monitor host a display demoted to PipeWire has no geometry-consistent + // per-connector stream to fall through to -- the portal exposes a single whole-desktop stream, so + // serving it for one connector would stretch the frame and offset all input. Advertise such a + // display OFFLINE while keeping its list position, so the index space stays aligned with + // get_capturer_info() (dropping it would shift every later index) and the client re-enumerates + // against a consistent list instead of driving a display the server then refuses. A single-display + // host is left online: there the whole-desktop stream IS that display, so the PipeWire fallback is + // geometry-consistent and get_capturer_for_display serves it. + if multi { + let failures = DRM_DISPLAY_FAILURES.lock().unwrap(); + for (idx, info) in infos.iter_mut().enumerate() { + if let Some((count, since)) = failures.get(&(idx as i32)).copied() { + if count >= DRM_GRAB_MAX_FAILURES && since.elapsed() < DEMOTE_COOLDOWN { + info.online = false; + } + } + } + } + Some(infos) } /// Index (into the cached DRM display list) of the compositor's PRIMARY output. DRM connector order diff --git a/src/server/wayland.rs b/src/server/wayland.rs index ab1a54b49..9f7ac25f8 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -383,6 +383,40 @@ pub(super) fn get_capturer_for_display( unsafe { let cap_display_info = &*cap_display_info; let rect = cap_display_info.rects[cap_display_info.current]; + // review 4.5: reaching here with DRM active means get_capturer_info bailed (a demoted + // display) and we fell through to PipeWire. Serve this stream ONLY if its rect matches the + // geometry we advertised for this index. The portal typically exposes one whole-desktop + // stream, so on a multi-monitor host that rect is the FULL desktop while the advertised DRM + // geometry is a single connector -> serving it would stretch the frame and offset all + // input. Bail instead; get_display_infos advertised the display offline, so the client + // re-enumerates against a consistent list. A single-display host matches (whole-desktop == + // that display) and is served normally. On a pure-PipeWire host is_available() is false and + // this guard is skipped, preserving upstream behavior exactly. + #[cfg(feature = "drm")] + if super::drm_capturer::is_available() { + if let Some(advertised) = super::drm_capturer::get_display_infos() + .and_then(|l| l.get(display_idx).cloned()) + { + let consistent = advertised.x == rect.0 .0 + && advertised.y == rect.0 .1 + && advertised.width as usize == rect.1 + && advertised.height as usize == rect.2; + if !consistent { + bail!( + "drm display {} demoted with no geometry-consistent PipeWire stream (advertised {}x{}+{}+{} vs stream {}x{}+{}+{}); advertised offline", + display_idx, + advertised.width, + advertised.height, + advertised.x, + advertised.y, + rect.1, + rect.2, + rect.0 .0, + rect.0 .1 + ); + } + } + } Ok(super::video_service::CapturerInfo { origin: rect.0, width: rect.1,