diff --git a/crates/pf-capture/src/lib.rs b/crates/pf-capture/src/lib.rs index d18f256c..dc8f1957 100644 --- a/crates/pf-capture/src/lib.rs +++ b/crates/pf-capture/src/lib.rs @@ -259,6 +259,12 @@ pub struct ZeroCopyPolicy { /// passthrough (like the VAAPI backend) instead of the EGL→CUDA import whose payloads only /// NVENC can consume. Per-session (the codec is negotiated), unlike `backend_is_vaapi`. pub pyrowave_session: bool, + /// THIS session's encoder can ingest a producer-native NV12 capture (the Linux raw Vulkan + /// Video backend on an H265/AV1 session — resolved by the host facade via + /// `pf_encode::linux_native_nv12_ok`). Gates whether the negotiation PREFERS gamescope's + /// producer-side NV12 pod: libav VAAPI (H264's backend) would misread the two-plane buffer, + /// so H264/GameStream/PyroWave sessions must never see NV12 frames. + pub native_nv12_session: bool, /// The PyroWave encoder's Vulkan-importable dmabuf modifiers for the capture's packed-RGB fourcc, /// resolved when the session encodes PyroWave (the passthrough advertises them so Mutter+NVIDIA, /// which allocates tiled-only, still negotiates zero-copy). Empty otherwise. diff --git a/crates/pf-capture/src/linux/mod.rs b/crates/pf-capture/src/linux/mod.rs index 79c67182..fe161fba 100644 --- a/crates/pf-capture/src/linux/mod.rs +++ b/crates/pf-capture/src/linux/mod.rs @@ -2086,22 +2086,26 @@ mod pipewire { // 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; - // Bring-up gate for producer-side NV12. Gamescope offers a one-fd LINEAR NV12 image when - // the consumer asks for it; keep this opt-in until the live Vulkan import path is proven. - // Raw passthrough is required because CUDA/PyroWave expect packed RGB, and 4:4:4/HDR must - // not be silently subsampled/downconverted. - let prefer_native_nv12 = std::env::var("PUNKTFUNK_PIPEWIRE_NV12").as_deref() == Ok("1") + // 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 + // CSC at all). `native_nv12_session` restricts this to sessions whose encoder can ingest + // it (Linux vulkan-encode H265/AV1 — never H264/libav-VAAPI, GameStream-resolve, or + // PyroWave, whose Vulkan compute CSC ingests packed RGB only). Raw passthrough is + // required because the CUDA importer expects packed RGB, and 4:4:4/HDR must not be + // silently subsampled/downconverted. Non-NV12 compositors (KWin/GNOME) simply match the + // packed-RGB fallback pod. + let prefer_native_nv12 = std::env::var("PUNKTFUNK_PIPEWIRE_NV12").as_deref() != Ok("0") + && policy.native_nv12_session && backend_is_vaapi && vaapi_passthrough - // PyroWave rides the same raw passthrough but its Vulkan compute CSC ingests packed - // RGB only — native NV12 would feed it an unreadable two-plane image. && !policy.pyrowave_session && !want_444 && !want_hdr; if prefer_native_nv12 { tracing::info!( - "PUNKTFUNK_PIPEWIRE_NV12=1: preferring gamescope producer-side NV12 LINEAR \ - DMA-BUF (no host RGB CSC)" + "zero-copy: preferring gamescope producer-side NV12 LINEAR DMA-BUF (no host \ + RGB CSC; PUNKTFUNK_PIPEWIRE_NV12=0 restores the packed-RGB negotiation)" ); } // Modifiers our import stack handles for BGRx: the EGL-importable (tiled) set, plus LINEAR diff --git a/crates/pf-encode/src/enc/linux/vulkan_video.rs b/crates/pf-encode/src/enc/linux/vulkan_video.rs index 9095a01f..530586d9 100644 --- a/crates/pf-encode/src/enc/linux/vulkan_video.rs +++ b/crates/pf-encode/src/enc/linux/vulkan_video.rs @@ -84,13 +84,14 @@ fn rgb_request() -> Option { } } -/// `PUNKTFUNK_VULKAN_RGB_TRUE_EXTENT=1` (bring-up): at unaligned modes, replace RGB-direct's -/// padded-copy staging with a direct import carrying the TRUE-SIZE source `codedExtent` — RADV -/// derives nonzero VCN firmware padding from it, so the EFC is told the source lacks the -/// alignment rows (see [`RgbDirect::true_extent`]). Off by default until the EFC front-end is -/// proven to honor the padding like the YUV fetch path does. +/// True-extent RGB-direct at unaligned modes (default ON; `PUNKTFUNK_VULKAN_RGB_TRUE_EXTENT=0` +/// restores the padded-copy staging): direct-import the visible-size capture with the TRUE-SIZE +/// source `codedExtent` — RADV derives nonzero VCN firmware padding from it, so the EFC is told +/// the source lacks the alignment rows (see [`RgbDirect::true_extent`]). Guarded-tested on Van +/// Gogh 2026-07-21 (kernel clean, and the fastest 1080p encode path measured); the EFC only +/// exists on Mesa ≥ 26, where the `codedExtent`-driven `session_init` is guaranteed. fn rgb_true_extent_request() -> bool { - std::env::var("PUNKTFUNK_VULKAN_RGB_TRUE_EXTENT").as_deref() == Ok("1") + std::env::var("PUNKTFUNK_VULKAN_RGB_TRUE_EXTENT").as_deref() != Ok("0") } /// Live RGB-direct session config: the chroma-siting bits the session was created with @@ -105,13 +106,13 @@ struct RgbDirect { /// with the edge rows/columns duplicated into the padding (transfer-only, no shader) and /// encoded from there. Aligned modes keep the true zero-copy import. padded: bool, - /// `PUNKTFUNK_VULKAN_RGB_TRUE_EXTENT=1` bring-up alternative to `padded` at unaligned - /// modes: direct-import the visible-size buffer and pass the TRUE-SIZE source + /// The default unaligned-mode source strategy (`PUNKTFUNK_VULKAN_RGB_TRUE_EXTENT=0` falls + /// back to `padded`): direct-import the visible-size buffer and pass the TRUE-SIZE source /// `codedExtent` — RADV then programs nonzero firmware padding from it (Mesa ≥ 24.2 /// derives `session_init` padding from `srcPictureResource.codedExtent`; see /// [`VulkanVideoEncoder::native_nv12`]), telling the VCN the source lacks the alignment - /// rows. The session/SPS/DPB stay app-aligned. Field-proven for YUV fetches; the EFC RGB - /// front-end honoring the padding is what this gate exists to prove. + /// rows, which the hardware edge-extends internally. The session/SPS/DPB stay app-aligned. + /// Guarded-tested on Van Gogh (kernel clean; fastest 1080p path measured). true_extent: bool, } @@ -696,7 +697,7 @@ impl VulkanVideoEncoder { ) => "active(true-extent: unaligned mode, direct import with the true-size \ source codedExtent — RADV firmware padding covers the alignment rows; \ - PUNKTFUNK_VULKAN_RGB_TRUE_EXTENT bring-up)", + PUNKTFUNK_VULKAN_RGB_TRUE_EXTENT=0 restores the padded copy)", (_, _, Some(RgbDirect { padded: false, .. })) => "active", (_, _, Some(RgbDirect { padded: true, .. })) => "active(padded-copy: mode is not 64x16-aligned — staging blit + edge \ diff --git a/crates/pf-encode/src/lib.rs b/crates/pf-encode/src/lib.rs index f1cc6118..4fbc6d8d 100644 --- a/crates/pf-encode/src/lib.rs +++ b/crates/pf-encode/src/lib.rs @@ -351,8 +351,8 @@ fn open_video_backend( Err(e) if format == PixelFormat::Nv12 => { return Err(e.context( "Vulkan Video open failed on a native-NV12 capture \ - (PUNKTFUNK_PIPEWIRE_NV12=1) — no VAAPI fallback exists; unset the \ - flag to restore the packed-RGB pipeline", + — no VAAPI fallback exists; set PUNKTFUNK_PIPEWIRE_NV12=0 to \ + restore the packed-RGB negotiation", )); } Err(e) => tracing::warn!( @@ -365,10 +365,10 @@ fn open_video_backend( // PUNKTFUNK_VULKAN_ENCODE=0, or a build without the feature). if format == PixelFormat::Nv12 { anyhow::bail!( - "native NV12 capture (PUNKTFUNK_PIPEWIRE_NV12=1) requires the Vulkan Video \ - encoder (HEVC/AV1 session, --features vulkan-encode, \ - PUNKTFUNK_VULKAN_ENCODE not 0) — this session resolved to libav VAAPI; \ - unset the flag" + "native NV12 capture requires the Vulkan Video encoder (HEVC/AV1 \ + session, --features vulkan-encode, PUNKTFUNK_VULKAN_ENCODE not 0) — this \ + session resolved to libav VAAPI; set PUNKTFUNK_PIPEWIRE_NV12=0 to restore \ + the packed-RGB negotiation" ); } vaapi::VaapiEncoder::open( @@ -841,6 +841,33 @@ fn vulkan_encode_enabled() -> bool { .unwrap_or(true) } +/// Whether THIS session's encoder can ingest a producer-native NV12 capture: only the raw +/// Vulkan Video backend does (libav VAAPI would misread the two-plane buffer as packed RGB — +/// [`open_video`] refuses the combination), so the session's codec must be one it encodes and +/// the backend must be eligible to open. The host facade threads the verdict into the capture +/// negotiation (`OutputFormat::nv12_native` → `ZeroCopyPolicy::native_nv12_session`), which +/// then PREFERS gamescope's producer-side NV12 pod (default-on; `PUNKTFUNK_PIPEWIRE_NV12=0` +/// escapes at the capture gate). +#[cfg(target_os = "linux")] +pub fn linux_native_nv12_ok(codec: Codec) -> bool { + #[cfg(feature = "vulkan-encode")] + { + matches!(codec, Codec::H265 | Codec::Av1) + && vulkan_encode_enabled() + // NVENC/PyroWave prefs never open the Vulkan Video backend; every other pref + // (auto/vaapi/amd/intel/vulkan) tries it first on AMD/Intel — see [`open_video`]. + && !matches!( + pf_host_config::config().encoder_pref.as_str(), + "nvenc" | "nvidia" | "cuda" | "pyrowave" + ) + } + #[cfg(not(feature = "vulkan-encode"))] + { + let _ = codec; + false + } +} + /// Cheap, side-effect-free NVIDIA-presence probe for the `auto` backend selector: the NVIDIA /// kernel driver exposes these device nodes, AMD/Intel boxes have neither. Deliberately does NOT /// create a CUDA context (that would allocate GPU state on every host that merely *might* be diff --git a/crates/pf-frame/src/lib.rs b/crates/pf-frame/src/lib.rs index c8a2b082..682caa27 100644 --- a/crates/pf-frame/src/lib.rs +++ b/crates/pf-frame/src/lib.rs @@ -146,6 +146,12 @@ pub struct OutputFormat { /// (never BGRA-passthrough / P010). `false` on every non-PyroWave session and on Linux (the /// wavelet encoder ingests dmabufs / CPU RGB there, not a D3D11 texture). pub pyrowave: bool, + /// THIS session's encoder can ingest a producer-native NV12 capture (Linux raw Vulkan Video + /// backend on an H265/AV1 session — see `pf_encode::linux_native_nv12_ok`). The Linux capture + /// negotiation only offers gamescope the NV12 pod when this is set: libav VAAPI (the H264 + /// codec's backend, and the fallback family) would misread the two-plane buffer as packed + /// RGB. Always `false` on Windows (the IDD-push capturer owns its own formats). + pub nv12_native: bool, } impl OutputFormat { @@ -163,6 +169,11 @@ impl OutputFormat { chroma_444: false, // GameStream never negotiates PyroWave (native punktfunk/1 only). pyrowave: false, + // Conservative: the GameStream + spike paths don't resolve the codec here, and a + // Moonlight client may negotiate H264 (whose VAAPI backend can't ingest NV12) — so + // they never prefer the producer-native NV12 pod. The punktfunk/1 plane opts in via + // `SessionPlan::output_format()`, which knows the codec. + nv12_native: false, } } } diff --git a/crates/punktfunk-host/src/capture.rs b/crates/punktfunk-host/src/capture.rs index f8aa5374..efa2a83a 100644 --- a/crates/punktfunk-host/src/capture.rs +++ b/crates/punktfunk-host/src/capture.rs @@ -26,7 +26,10 @@ pub use pf_capture::{dxgi, synthetic_nv12}; /// capture→encode cycle). Resolved here (the host facade) and threaded in, so the edge stays one-way /// (plan §2.4 / §W6). #[cfg(target_os = "linux")] -fn zero_copy_policy(pyrowave_session: bool) -> pf_capture::ZeroCopyPolicy { +fn zero_copy_policy( + pyrowave_session: bool, + native_nv12_session: bool, +) -> pf_capture::ZeroCopyPolicy { let backend_is_vaapi = crate::encode::linux_zero_copy_is_vaapi(); // The raw-dmabuf passthrough serves a PyroWave session on ANY vendor (the wavelet encoder's // own Vulkan device imports the dmabuf) — per-session from the negotiated codec, plus the @@ -56,6 +59,7 @@ fn zero_copy_policy(pyrowave_session: bool) -> pf_capture::ZeroCopyPolicy { backend_is_gpu: crate::encode::resolved_backend_is_gpu(), pyrowave_session, pyrowave_modifiers, + native_nv12_session, } } @@ -70,7 +74,9 @@ pub fn open_portal_monitor(want_hdr: bool) -> Result> { let anchored = crate::inject::default_backend() == crate::inject::Backend::Libei; // Monitor mirrors never carry the native PyroWave plane (GameStream protocol) — per-session // passthrough is virtual-output-only; the global encoder-pref lever still applies inside. - pf_capture::open_portal_monitor(anchored, want_hdr, zero_copy_policy(false)) + // Native NV12 stays off too: the mirror path doesn't resolve the codec here, and the desktop + // compositors it mirrors (GNOME/KWin) don't produce NV12 anyway. + pf_capture::open_portal_monitor(anchored, want_hdr, zero_copy_policy(false, false)) } #[cfg(not(target_os = "linux"))] @@ -101,7 +107,7 @@ pub fn capture_virtual_output( vout.keepalive, want.gpu, want.chroma_444, - zero_copy_policy(want.pyrowave), + zero_copy_policy(want.pyrowave, want.nv12_native), ) } diff --git a/crates/punktfunk-host/src/session_plan.rs b/crates/punktfunk-host/src/session_plan.rs index 8873953f..0031825e 100644 --- a/crates/punktfunk-host/src/session_plan.rs +++ b/crates/punktfunk-host/src/session_plan.rs @@ -179,6 +179,13 @@ impl SessionPlan { // Vulkan device; on Linux the capture facade flips the zero-copy policy to the // raw-dmabuf passthrough (see above). pyrowave: self.codec == crate::encode::Codec::PyroWave, + // Producer-native NV12 (gamescope) is consumable only by the Linux Vulkan Video + // backend — resolved HERE from the plan's codec so the capturer never reaches back + // into encode (the same one-way edge as `gpu` above). + #[cfg(target_os = "linux")] + nv12_native: crate::encode::linux_native_nv12_ok(self.codec), + #[cfg(not(target_os = "linux"))] + nv12_native: false, } } }