From 3efbe4164e798983f676d24355ab9fdf2acba247 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 25 Jul 2026 14:57:55 +0200 Subject: [PATCH] fix(capture): capture CPU frames once the encoder proves it can't import dmabufs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dmabuf import the GPU driver refuses is refused identically on every retry, but the only recovery above it was the encode-stall ladder: five in-place encoder rebuilds, then the video session ends. So a host whose driver will not take what its compositor allocates lost every session on its first frame, and every reconnect repeated it — while the very same host streamed fine with `PUNKTFUNK_ZEROCOPY=0`. The software knew how to run that machine and never chose to. Latch it, exactly as the sibling CUDA-import path already does after repeated worker deaths: three consecutive import failures with no frame in between disable the raw-dmabuf passthrough for the host process, and capture negotiates CPU frames from the next session on. Three sits below the encoder's rebuild budget, so the latch is set before the session it doomed ends — one bad session, then a working (if slower) host, with a log line saying which and why instead of an operator having to find an environment variable. Only the two stages that ARE the import are counted — the buffersrc push of the DRM-PRIME descriptor and the buffersink pull where `hwmap` maps it into a VA surface. `avcodec_send_frame` is deliberately left out: that one is the encoder stalling, which the in-place rebuild exists to recover, and taking zero-copy away permanently over a transient fault would be a bad trade. The latch lives in pf-zerocopy because it is the leaf both sides can see — the capture→encode edge is one-way by design, so pf-capture cannot ask pf-encode anything. --- crates/pf-capture/src/linux/mod.rs | 16 +++++++++- crates/pf-encode/src/enc/linux/vaapi.rs | 17 ++++++++-- crates/pf-zerocopy/src/imp/mod.rs | 42 +++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/crates/pf-capture/src/linux/mod.rs b/crates/pf-capture/src/linux/mod.rs index facb9a8d..8244c710 100644 --- a/crates/pf-capture/src/linux/mod.rs +++ b/crates/pf-capture/src/linux/mod.rs @@ -2191,7 +2191,15 @@ mod pipewire { // consumer imports raw dmabufs itself — the VAAPI backend (libva import + GPU CSC) or a // PyroWave session (the wavelet encoder's own Vulkan device, any vendor) → hand the raw // dmabuf straight to the encoder. - let vaapi_passthrough = zerocopy && !force_shm && importer.is_none() && raw_passthrough; + // + // ...unless the encoder already proved it cannot import them here. A driver that refuses + // the compositor's buffers refuses them identically on every retry, so without this the + // session died on its first frame and every reconnect repeated it. The latch (set by the + // encode side after consecutive import failures) is what turns that into one bad session + // followed by a working, if slower, host. + let raw_dmabuf_off = raw_passthrough && pf_zerocopy::raw_dmabuf_import_disabled(); + let vaapi_passthrough = + zerocopy && !force_shm && importer.is_none() && raw_passthrough && !raw_dmabuf_off; // Producer-side NV12 (default-on; PUNKTFUNK_PIPEWIRE_NV12=0 escapes): gamescope offers a // one-fd LINEAR NV12 image when the consumer asks — its compositor pass does the RGB→YUV, // and the Vulkan Video encoder imports the buffer as its encode source directly (no host @@ -2249,6 +2257,12 @@ mod pipewire { tracing::info!( "capture: PUNKTFUNK_FORCE_SHM — race-free SHM download path (no dmabuf, no zero-copy)" ); + } else if raw_dmabuf_off { + tracing::warn!( + "zero-copy raw-dmabuf passthrough disabled after repeated encoder import failures \ + — capturing CPU frames instead (this host's GPU driver would not import the \ + compositor's buffers)" + ); } else if zerocopy && !want_dmabuf { tracing::warn!("zero-copy: no importable dmabuf modifiers — using CPU path"); } else if vaapi_passthrough && policy.pyrowave_modifiers.is_empty() { diff --git a/crates/pf-encode/src/enc/linux/vaapi.rs b/crates/pf-encode/src/enc/linux/vaapi.rs index 1fb475e0..c2836f84 100644 --- a/crates/pf-encode/src/enc/linux/vaapi.rs +++ b/crates/pf-encode/src/enc/linux/vaapi.rs @@ -1025,8 +1025,18 @@ impl DmabufInner { ffi::AV_BUFFERSRC_FLAG_KEEP_REF as c_int, ); ffi::av_frame_free(&mut drm); + // These two stages ARE the import: the push hands libav our DRM-PRIME descriptor, and + // the pull is where `hwmap` actually maps it into a VA surface (and `scale_vaapi` runs + // the CSC). A failure here means this driver would not take this compositor's dmabuf — + // which no encoder rebuild can fix — so tell the process-wide latch, and capture + // negotiates CPU frames from the next session on. `avcodec_send_frame` below is + // deliberately NOT counted: that one is the encoder stalling, which the in-place + // rebuild above us exists to recover, and disabling zero-copy over it would be a + // permanent penalty for a transient fault. if r < 0 { - bail!("av_buffersrc_add_frame failed ({r})"); + let e = format!("av_buffersrc_add_frame failed ({r})"); + pf_zerocopy::note_raw_dmabuf_import_failure(&e); + bail!("{e}"); } t_push = t0.elapsed(); let mut nv12 = ffi::av_frame_alloc(); @@ -1036,8 +1046,11 @@ impl DmabufInner { let r = ffi::av_buffersink_get_frame(self.sink, nv12); if r < 0 { ffi::av_frame_free(&mut nv12); - bail!("av_buffersink_get_frame failed ({r})"); + let e = format!("av_buffersink_get_frame failed ({r})"); + pf_zerocopy::note_raw_dmabuf_import_failure(&e); + bail!("{e}"); } + pf_zerocopy::note_raw_dmabuf_import_ok(); t_pull = t0.elapsed() - t_push; (*nv12).pts = pts; (*nv12).pict_type = if idr { diff --git a/crates/pf-zerocopy/src/imp/mod.rs b/crates/pf-zerocopy/src/imp/mod.rs index 19c24036..d6199483 100644 --- a/crates/pf-zerocopy/src/imp/mod.rs +++ b/crates/pf-zerocopy/src/imp/mod.rs @@ -232,6 +232,48 @@ pub fn gpu_import_disabled() -> bool { GPU_IMPORT_DISABLED.load(Ordering::Relaxed) } +/// The same idea for the OTHER zero-copy half: consecutive failures to import a raw dmabuf in the +/// encoder (VAAPI's libva import, PyroWave's Vulkan one) with no successful frame in between. +/// +/// That import can fail for reasons no retry can fix — a driver that will not take what the +/// compositor allocates. The encode-stall recovery above it cannot tell the difference, so it +/// rebuilt the identical failing encoder five times and then ended the video session, on every +/// connection, forever: a hybrid Intel/NVIDIA laptop reporting `vaCreateSurfaces` → +/// `VA_STATUS_ERROR_ALLOCATION_FAILED` on its first frame could not stream at all until its +/// operator found `PUNKTFUNK_ZEROCOPY=0` by hand. The host already knows how to encode that +/// machine — capture just has to stop handing it dmabufs. Latching here is what makes the next +/// session negotiate CPU frames on its own. +static RAW_DMABUF_FAILURE_STREAK: AtomicU32 = AtomicU32::new(0); +static RAW_DMABUF_DISABLED: AtomicBool = AtomicBool::new(false); +/// Below the encoder's own rebuild budget, so the latch is set before the session it doomed ends. +const RAW_DMABUF_FAILURE_LATCH: u32 = 3; + +/// Record an encoder-side raw-dmabuf import failure. Latches the process-wide disable after +/// [`RAW_DMABUF_FAILURE_LATCH`] consecutive failures. +pub fn note_raw_dmabuf_import_failure(reason: &str) { + let streak = RAW_DMABUF_FAILURE_STREAK.fetch_add(1, Ordering::Relaxed) + 1; + if streak >= RAW_DMABUF_FAILURE_LATCH && !RAW_DMABUF_DISABLED.swap(true, Ordering::Relaxed) { + tracing::error!( + streak, + reason, + "zero-copy raw-dmabuf passthrough disabled for this host process: the encoder failed \ + to import the compositor's dmabuf {streak} times in a row — captures fall back to the \ + CPU path (slower, but this host could not stream at all otherwise)" + ); + } +} + +/// Record a raw dmabuf that imported and encoded — resets the failure streak. +pub fn note_raw_dmabuf_import_ok() { + RAW_DMABUF_FAILURE_STREAK.store(0, Ordering::Relaxed); +} + +/// True once repeated encoder import failures latched the raw-dmabuf passthrough off (see +/// [`note_raw_dmabuf_import_failure`]). +pub fn raw_dmabuf_import_disabled() -> bool { + RAW_DMABUF_DISABLED.load(Ordering::Relaxed) +} + /// DRM FourCC for a packed 32-bit format name (little-endian, e.g. `b"XR24"`). const fn fourcc(c: &[u8; 4]) -> u32 { (c[0] as u32) | ((c[1] as u32) << 8) | ((c[2] as u32) << 16) | ((c[3] as u32) << 24)