Detect the DMA-BUF crash bug by Hyprland version, add a capture-stall watchdog
The 1080p DMA-BUF pipeline crashed the user's entire Hyprland session tonight. Root cause identified: hyprwm/Hyprland PR #15167 (fixed 2026-06-18, first shipped v0.56.0) -- CGLRenderbuffer's destructor unconditionally dereferences m_framebuffer, which is left null when createEGLImage() fails to import the DMA-BUF. This machine runs v0.55.4, a week before the fix. The crash happens inside Hyprland's own Screenshare::CScreenshareFrame::copyDmabuf(), confirmed against the coredump; it is not a breadcast, GStreamer, or PipeWire bug, and it is not resolution-dependent (upstream's own reports span unrelated triggers -- touchpad gestures, tab switching, a Discord stream -- not capture geometry), so 1080p vs 720p was never the actual variable. choose_capture_backend() now reads the running compositor's version over its own IPC socket and only uses the DMA-BUF path on Hyprland >= 0.56.0 (or non-Hyprland sessions, unaffected by this bug). Below that, or if the version can't be determined, it falls back to the plain system-memory/wl_shm pipeline -- slower and still subject to xdg-desktop-portal-hyprland's separate "Out of buffers" stall bug (also confirmed via journalctl, and also not fixed in the installed xdpw 1.3.12), but that failure mode is a stall, not a compositor-wide abort. That stall used to be silent forever: xdpw stops requesting frames after 10 failed retries and never signals PipeWire, so GStreamer's own bus reports nothing -- no error, no EOS -- and the frame pump just blocks. pull_encoded_frame now bails after 10s of a Playing pipeline producing nothing, converting an indefinite silent freeze into a real, reported session failure (still correctly distinguishing a genuine stall from ordinary EOS/teardown, so a normal stop() doesn't trip it). VideoParams is no longer a hand-synced constant: build_video_pipeline_for_streaming now returns the geometry/frame-rate it actually chose alongside the pipeline, and both breadcastd::cast_mirror and cast_stream_test thread that straight into the OFFER instead of a separately-maintained default. Keeping two copies in sync by hand is exactly how the resolution mismatch bug happened earlier tonight; returning the real value makes that class of bug unrepresentable rather than just fixed once. Verified without touching the real compositor: cargo build --workspace --examples, clippy, and both new unit tests (version parsing, the 0.55.4/0.56.0 backend-selection boundary) are clean. The watchdog's firing path and the DMA-BUF path post-Hyprland-update are not yet validated against real hardware -- deliberately, given what the last live test cost. Recommended order: update Hyprland (pacman -Syu hyprland xdg-desktop-portal-hyprland gets 0.56.1 + xdpw 1.4.1, which also picks up upstream fixes for the exact copy-fence and SHM-handling bugs hit tonight) and confirm `hyprctl version` reports >= 0.56.0 before testing DMA-BUF again. Without updating, this commit still helps: the wl_shm path is selected automatically and the stall is now bounded instead of indefinite.
This commit is contained in:
parent
bd511fea33
commit
2cc1310752
4 changed files with 368 additions and 46 deletions
|
|
@ -13,7 +13,7 @@ use std::sync::Arc;
|
|||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use breadcast_core::caststream::{CastStreamEvent, VideoParams, WEBRTC_NAMESPACE};
|
||||
use breadcast_core::caststream::{CastStreamEvent, WEBRTC_NAMESPACE};
|
||||
use breadcast_core::pipeline::{
|
||||
build_video_pipeline_for_streaming, pull_encoded_frame, request_key_frame, set_video_bitrate_kbps,
|
||||
};
|
||||
|
|
@ -87,7 +87,13 @@ impl CastMirrorSession {
|
|||
let capture = CaptureSession::start().await.context("failed to start portal screen capture")?;
|
||||
let video_node_id = capture.video_node_id();
|
||||
|
||||
let (pipeline, appsink, encoder) =
|
||||
// `video_params` describes what this pipeline will *actually* encode
|
||||
// -- it isn't a constant, because the pipeline picks its capture path
|
||||
// at runtime (see `build_video_pipeline_for_streaming`) and the two
|
||||
// paths differ in resolution and frame rate. It's threaded into the
|
||||
// OFFER below rather than re-derived there, so the advertised stream
|
||||
// and the encoded stream cannot drift apart.
|
||||
let (pipeline, appsink, encoder, video_params) =
|
||||
build_video_pipeline_for_streaming(video_node_id).context("failed to build the encode pipeline")?;
|
||||
|
||||
{
|
||||
|
|
@ -116,7 +122,7 @@ impl CastMirrorSession {
|
|||
.context("failed to connect and launch the Mirroring receiver")?;
|
||||
|
||||
let (sender, stream_events) =
|
||||
CastStreamSender::start(&device.host, "sender-0", session.transport_id(), VideoParams::default())
|
||||
CastStreamSender::start(&device.host, "sender-0", session.transport_id(), video_params)
|
||||
.context("failed to start the Cast Streaming session")?;
|
||||
let sender = Arc::new(sender);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue