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)