From 85bc5b9a3ffaef921de68b66327a84919b326b27 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 17 Jul 2026 09:41:15 +0200 Subject: [PATCH] refactor(host/W6.2): extract the Linux zero-copy GPU plumbing into the pf-zerocopy leaf crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit linux/zerocopy/* (CUDA context/buffers + EGL/Vulkan dmabuf import + the isolated import worker) and linux/dmabuf_fence.rs move wholesale into crates/pf-zerocopy, so the coming pf-frame vocabulary crate (FramePayload::Cuda owns a DeviceBuffer) and the pf-encode/pf-capture subsystem crates can reach the GPU plumbing without the host orchestrator in between (plan §W6). Content stays Linux-only; the crate compiles to an empty lib elsewhere, so dependents carry a plain dependency. drm_fourcc deliberately does NOT move: it consumes the frame vocabulary (PixelFormat), which sits ABOVE pf-zerocopy — it lives with capture for now and moves into pf-frame next. cuda's ffi re-export bumps pub(crate)->pub (the raw CUdeviceptr vocabulary is consumed across the crate boundary by the encode backends). A crate::zerocopy shim module keeps every existing path valid until capture/encode themselves move out. Verified: Linux clippy -D warnings (pf-zerocopy --all-targets + host nvenc,vulkan-encode,pyrowave --all-targets) + 17/17 pf-zerocopy tests + 321/321 host tests; Windows clippy nvenc,amf-qsv --all-targets Finished exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 15 +++++++++ Cargo.toml | 1 + crates/pf-zerocopy/Cargo.toml | 32 +++++++++++++++++++ .../linux => pf-zerocopy/src}/dmabuf_fence.rs | 0 .../src/imp}/client.rs | 0 .../zerocopy => pf-zerocopy/src/imp}/cuda.rs | 4 ++- .../src/imp}/cuda/ffi.rs | 0 .../zerocopy => pf-zerocopy/src/imp}/egl.rs | 0 .../src/imp}/egl/gl.rs | 0 .../zerocopy => pf-zerocopy/src/imp}/mod.rs | 19 ++--------- .../zerocopy => pf-zerocopy/src/imp}/proto.rs | 0 .../src/imp}/vulkan.rs | 0 .../src/imp}/worker.rs | 0 crates/pf-zerocopy/src/lib.rs | 22 +++++++++++++ crates/punktfunk-host/Cargo.toml | 3 ++ crates/punktfunk-host/src/capture.rs | 24 ++++++++++++++ .../punktfunk-host/src/capture/linux/mod.rs | 2 +- crates/punktfunk-host/src/linux/drm_sync.rs | 2 +- crates/punktfunk-host/src/main.rs | 12 ++++--- 19 files changed, 111 insertions(+), 25 deletions(-) create mode 100644 crates/pf-zerocopy/Cargo.toml rename crates/{punktfunk-host/src/linux => pf-zerocopy/src}/dmabuf_fence.rs (100%) rename crates/{punktfunk-host/src/linux/zerocopy => pf-zerocopy/src/imp}/client.rs (100%) rename crates/{punktfunk-host/src/linux/zerocopy => pf-zerocopy/src/imp}/cuda.rs (99%) rename crates/{punktfunk-host/src/linux/zerocopy => pf-zerocopy/src/imp}/cuda/ffi.rs (100%) rename crates/{punktfunk-host/src/linux/zerocopy => pf-zerocopy/src/imp}/egl.rs (100%) rename crates/{punktfunk-host/src/linux/zerocopy => pf-zerocopy/src/imp}/egl/gl.rs (100%) rename crates/{punktfunk-host/src/linux/zerocopy => pf-zerocopy/src/imp}/mod.rs (94%) rename crates/{punktfunk-host/src/linux/zerocopy => pf-zerocopy/src/imp}/proto.rs (100%) rename crates/{punktfunk-host/src/linux/zerocopy => pf-zerocopy/src/imp}/vulkan.rs (100%) rename crates/{punktfunk-host/src/linux/zerocopy => pf-zerocopy/src/imp}/worker.rs (100%) create mode 100644 crates/pf-zerocopy/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f38b8963..7636fa50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2848,6 +2848,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pf-zerocopy" +version = "0.12.0" +dependencies = [ + "anyhow", + "ash", + "khronos-egl", + "libc", + "libloading", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -3150,6 +3164,7 @@ dependencies = [ "pf-gpu", "pf-host-config", "pf-paths", + "pf-zerocopy", "pipewire", "punktfunk-core", "pyrowave-sys", diff --git a/Cargo.toml b/Cargo.toml index 6b6e87ea..2cd5170b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/pf-paths", "crates/pf-host-config", "crates/pf-gpu", + "crates/pf-zerocopy", "crates/pyrowave-sys", "clients/probe", "clients/linux", diff --git a/crates/pf-zerocopy/Cargo.toml b/crates/pf-zerocopy/Cargo.toml new file mode 100644 index 00000000..e5aa81c8 --- /dev/null +++ b/crates/pf-zerocopy/Cargo.toml @@ -0,0 +1,32 @@ +# The Linux GPU zero-copy plumbing (plan §9 / §W6), extracted into a leaf crate so the shared +# frame vocabulary (pf-frame `FramePayload::Cuda`), the encode backends (CUDA copies/context), +# and the capture import machinery can all reach it WITHOUT the host orchestrator in between. +# Content is Linux-only; the crate compiles to an empty lib elsewhere so dependents can carry a +# plain (non-target-gated) dependency. +[package] +name = "pf-zerocopy" +version = "0.12.0" +edition = "2021" +# Inherit the workspace MSRV: clippy keys MSRV-gated lints off it (without this, e.g. +# `manual_is_multiple_of` fires on code the host crate compiles clean). +rust-version.workspace = true +license = "MIT OR Apache-2.0" +description = "punktfunk host Linux zero-copy GPU plumbing: CUDA context/buffers, EGL/Vulkan dmabuf import, the isolated import worker, and dmabuf implicit-fence sync." +publish = false + +[target.'cfg(target_os = "linux")'.dependencies] +anyhow = "1" +tracing = "0.1" +libc = "0.2" +# `libcuda.so.1` is dlopen'd at runtime (NOT link-time) so one Linux binary runs on NVIDIA +# (zero-copy via CUDA) AND on AMD/Intel (VAAPI, no NVIDIA driver present) — see `cuda::ffi`. +libloading = "0.8" +# EGL imports the PipeWire dmabuf, CUDA maps it (`dynamic` = load the NVIDIA libEGL at runtime). +khronos-egl = { version = "6", features = ["dynamic"] } +# Vulkan bridge for LINEAR dmabufs (gamescope): VK_EXT_external_memory_dma_buf import, +# GPU-copy into an exportable allocation, export OPAQUE_FD → cuImportExternalMemory. +ash = "0.38" +# The isolated import worker's control protocol: small serde_json blobs over a Unix socket +# (SCM_RIGHTS carries the fds; pixels never cross the socket). +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/crates/punktfunk-host/src/linux/dmabuf_fence.rs b/crates/pf-zerocopy/src/dmabuf_fence.rs similarity index 100% rename from crates/punktfunk-host/src/linux/dmabuf_fence.rs rename to crates/pf-zerocopy/src/dmabuf_fence.rs diff --git a/crates/punktfunk-host/src/linux/zerocopy/client.rs b/crates/pf-zerocopy/src/imp/client.rs similarity index 100% rename from crates/punktfunk-host/src/linux/zerocopy/client.rs rename to crates/pf-zerocopy/src/imp/client.rs diff --git a/crates/punktfunk-host/src/linux/zerocopy/cuda.rs b/crates/pf-zerocopy/src/imp/cuda.rs similarity index 99% rename from crates/punktfunk-host/src/linux/zerocopy/cuda.rs rename to crates/pf-zerocopy/src/imp/cuda.rs index 0f8efa0b..ce1ab1bf 100644 --- a/crates/punktfunk-host/src/linux/zerocopy/cuda.rs +++ b/crates/pf-zerocopy/src/imp/cuda.rs @@ -24,7 +24,9 @@ use std::sync::{Arc, Mutex, OnceLock}; #[path = "cuda/ffi.rs"] mod ffi; -pub(crate) use ffi::*; +// `pub` (not `pub(crate)`): the raw driver-API vocabulary (`CUdeviceptr`, …) is consumed across +// the crate boundary by the encode backends' CUDA-frame paths. +pub use ffi::*; /// Copy a pitched device plane `(src_ptr, src_pitch)` down to a tightly-packed host buffer of /// `width_bytes`×`height` (no row padding). Synchronous on the priority stream. Used by the NV12 diff --git a/crates/punktfunk-host/src/linux/zerocopy/cuda/ffi.rs b/crates/pf-zerocopy/src/imp/cuda/ffi.rs similarity index 100% rename from crates/punktfunk-host/src/linux/zerocopy/cuda/ffi.rs rename to crates/pf-zerocopy/src/imp/cuda/ffi.rs diff --git a/crates/punktfunk-host/src/linux/zerocopy/egl.rs b/crates/pf-zerocopy/src/imp/egl.rs similarity index 100% rename from crates/punktfunk-host/src/linux/zerocopy/egl.rs rename to crates/pf-zerocopy/src/imp/egl.rs diff --git a/crates/punktfunk-host/src/linux/zerocopy/egl/gl.rs b/crates/pf-zerocopy/src/imp/egl/gl.rs similarity index 100% rename from crates/punktfunk-host/src/linux/zerocopy/egl/gl.rs rename to crates/pf-zerocopy/src/imp/egl/gl.rs diff --git a/crates/punktfunk-host/src/linux/zerocopy/mod.rs b/crates/pf-zerocopy/src/imp/mod.rs similarity index 94% rename from crates/punktfunk-host/src/linux/zerocopy/mod.rs rename to crates/pf-zerocopy/src/imp/mod.rs index 366817a8..9d028e06 100644 --- a/crates/punktfunk-host/src/linux/zerocopy/mod.rs +++ b/crates/pf-zerocopy/src/imp/mod.rs @@ -8,7 +8,8 @@ //! //! Pieces: [`cuda`] (driver-API FFI + the shared `CUcontext` + device buffers), [`egl`] (the //! headless EGLDisplay + dmabuf→`EGLImage`→CUDA import). The encoder's CUDA-frame path lives in -//! `encode/linux.rs`; the dmabuf negotiation lives in `capture/linux.rs`. +//! the encode backends (`pf-encode`); the dmabuf negotiation lives in the Linux capturer +//! (`pf-capture`). pub mod client; pub mod cuda; @@ -221,22 +222,6 @@ 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) } -/// Map a SPA/our [`crate::capture::PixelFormat`] to the DRM FourCC EGL expects for import. -/// SPA byte order `BGRx` ⇒ DRM `XRGB8888` (memory B,G,R,X), etc. -pub fn drm_fourcc(format: crate::capture::PixelFormat) -> Option { - use crate::capture::PixelFormat::*; - Some(match format { - Bgrx => fourcc(b"XR24"), // DRM_FORMAT_XRGB8888 - Bgra => fourcc(b"AR24"), // DRM_FORMAT_ARGB8888 - Rgbx => fourcc(b"XB24"), // DRM_FORMAT_XBGR8888 - Rgba => fourcc(b"AB24"), // DRM_FORMAT_ABGR8888 - // 24-bit packed RGB/BGR have no straightforward dmabuf import here; use the CPU path. - // Rgb10a2/Nv12/P010 are the Windows HDR / video-processor formats — never produced on - // Linux; Yuv444 is OUR convert's OUTPUT, never a capture source format. - Rgb | Bgr | Rgb10a2 | Nv12 | P010 | Yuv444 => return None, - }) -} - /// Standalone probe (the `zerocopy-probe` subcommand): initialize the EGL importer + CUDA /// context and report, then exercise the production path — spawn the isolated worker (exec'd /// from this binary's pinned exe fd), handshake, and query modifiers. De-risks the diff --git a/crates/punktfunk-host/src/linux/zerocopy/proto.rs b/crates/pf-zerocopy/src/imp/proto.rs similarity index 100% rename from crates/punktfunk-host/src/linux/zerocopy/proto.rs rename to crates/pf-zerocopy/src/imp/proto.rs diff --git a/crates/punktfunk-host/src/linux/zerocopy/vulkan.rs b/crates/pf-zerocopy/src/imp/vulkan.rs similarity index 100% rename from crates/punktfunk-host/src/linux/zerocopy/vulkan.rs rename to crates/pf-zerocopy/src/imp/vulkan.rs diff --git a/crates/punktfunk-host/src/linux/zerocopy/worker.rs b/crates/pf-zerocopy/src/imp/worker.rs similarity index 100% rename from crates/punktfunk-host/src/linux/zerocopy/worker.rs rename to crates/pf-zerocopy/src/imp/worker.rs diff --git a/crates/pf-zerocopy/src/lib.rs b/crates/pf-zerocopy/src/lib.rs new file mode 100644 index 00000000..150804e4 --- /dev/null +++ b/crates/pf-zerocopy/src/lib.rs @@ -0,0 +1,22 @@ +//! Linux GPU zero-copy plumbing (plan §9), extracted from the host's `linux/zerocopy/` module +//! tree (plan §W6): the shared CUDA context + device buffers, the EGL/Vulkan dmabuf importers, +//! the isolated import-worker subprocess (client/proto/worker), the zero-copy policy latches, and +//! the dmabuf implicit-fence wait. Everything is Linux-only; on other targets this crate compiles +//! to an empty lib so dependents can carry a plain (non-target-gated) dependency. +//! +//! The `PixelFormat → DRM FourCC` mapping (`drm_fourcc`) deliberately does NOT live here — it +//! consumes the shared frame vocabulary, which sits ABOVE this crate (this crate provides the +//! `DeviceBuffer` that vocabulary's `FramePayload::Cuda` owns). + +// Unsafe-proof program: every `unsafe {}` / `unsafe impl` must carry a `// SAFETY:` proof. Each +// file keeps its own `#![deny(...)]` too; this crate-root deny is the catch-all gate. +#![deny(clippy::undocumented_unsafe_blocks)] + +/// Wait for a dmabuf's implicit read-ready fence (`DMA_BUF_IOCTL_EXPORT_SYNC_FILE` + poll). +#[cfg(target_os = "linux")] +pub mod dmabuf_fence; + +#[cfg(target_os = "linux")] +mod imp; +#[cfg(target_os = "linux")] +pub use imp::*; diff --git a/crates/punktfunk-host/Cargo.toml b/crates/punktfunk-host/Cargo.toml index 7f05b703..3e85f8fb 100644 --- a/crates/punktfunk-host/Cargo.toml +++ b/crates/punktfunk-host/Cargo.toml @@ -16,6 +16,9 @@ pf-paths = { path = "../pf-paths" } pf-host-config = { path = "../pf-host-config" } # GPU vendor/adapter detection + selection, extracted to a leaf crate (plan §W6). pf-gpu = { path = "../pf-gpu" } +# Linux GPU zero-copy plumbing (CUDA/EGL/Vulkan dmabuf import + the isolated worker), extracted +# to a leaf crate (plan §W6). Compiles empty on non-Linux, so it lives in the main deps. +pf-zerocopy = { path = "../pf-zerocopy" } # M3 native control plane (the `punktfunk/1` QUIC handshake; data plane stays native-thread UDP). quinn = "0.11" anyhow = "1" diff --git a/crates/punktfunk-host/src/capture.rs b/crates/punktfunk-host/src/capture.rs index fe427af0..1f226618 100644 --- a/crates/punktfunk-host/src/capture.rs +++ b/crates/punktfunk-host/src/capture.rs @@ -55,6 +55,30 @@ impl PixelFormat { } } +/// DRM FourCC for a packed 32-bit format name (little-endian, e.g. `b"XR24"`). +#[cfg(target_os = "linux")] +const fn drm_fourcc_code(c: &[u8; 4]) -> u32 { + (c[0] as u32) | ((c[1] as u32) << 8) | ((c[2] as u32) << 16) | ((c[3] as u32) << 24) +} + +/// Map a SPA/our [`PixelFormat`] to the DRM FourCC EGL expects for import. SPA byte order `BGRx` +/// ⇒ DRM `XRGB8888` (memory B,G,R,X), etc. Lives with the frame vocabulary (not in +/// `pf-zerocopy`) because it consumes [`PixelFormat`], which sits above that crate. +#[cfg(target_os = "linux")] +pub fn drm_fourcc(format: PixelFormat) -> Option { + use PixelFormat::*; + Some(match format { + Bgrx => drm_fourcc_code(b"XR24"), // DRM_FORMAT_XRGB8888 + Bgra => drm_fourcc_code(b"AR24"), // DRM_FORMAT_ARGB8888 + Rgbx => drm_fourcc_code(b"XB24"), // DRM_FORMAT_XBGR8888 + Rgba => drm_fourcc_code(b"AB24"), // DRM_FORMAT_ABGR8888 + // 24-bit packed RGB/BGR have no straightforward dmabuf import here; use the CPU path. + // Rgb10a2/Nv12/P010 are the Windows HDR / video-processor formats — never produced on + // Linux; Yuv444 is OUR convert's OUTPUT, never a capture source format. + Rgb | Bgr | Rgb10a2 | Nv12 | P010 | Yuv444 => return None, + }) +} + /// What a Windows capturer should produce, resolved **once** per session and passed **into** /// [`capture_virtual_output`] (Goal-1 stage 5, plan §2.3/§5). Passing the format in is what lets a /// capturer stop re-deriving the encode backend itself — it kills the diff --git a/crates/punktfunk-host/src/capture/linux/mod.rs b/crates/punktfunk-host/src/capture/linux/mod.rs index 0c172f42..58877cb7 100644 --- a/crates/punktfunk-host/src/capture/linux/mod.rs +++ b/crates/punktfunk-host/src/capture/linux/mod.rs @@ -1205,7 +1205,7 @@ mod pipewire { // closing the stale/old-frame race on NVIDIA. No-op for shm buffers or drivers that // attach no fence. Covers both the GPU import and the CPU mmap read below. if datas[0].type_() == pw::spa::buffer::DataType::DmaBuf { - match crate::dmabuf_fence::wait_read_ready(datas[0].fd(), 100) { + match pf_zerocopy::dmabuf_fence::wait_read_ready(datas[0].fd(), 100) { Ok(waited) => { static F1: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true); diff --git a/crates/punktfunk-host/src/linux/drm_sync.rs b/crates/punktfunk-host/src/linux/drm_sync.rs index 7998a9a9..a5989d82 100644 --- a/crates/punktfunk-host/src/linux/drm_sync.rs +++ b/crates/punktfunk-host/src/linux/drm_sync.rs @@ -4,7 +4,7 @@ //! RETAINED BUT CURRENTLY UNUSED: producer-driven explicit sync is the "right" fix, but no //! compositor we target produces a usable sync_fd today — Mutter+NVIDIA fails buffer allocation //! (`error alloc buffers`, no cogl sync_fd), KWin/gamescope blit so they don't race at all. We sync -//! zero-copy from the consumer side instead (see [`crate::dmabuf_fence`]). This module is kept, +//! zero-copy from the consumer side instead (see `pf_zerocopy::dmabuf_fence`). This module is kept, //! verified (ioctl numbers + a live signal→wait round trip), ready to wire in the moment a producer //! gains working `SPA_META_SyncTimeline`. #![allow(dead_code)] diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs index 5cf4bc3a..68865a81 100644 --- a/crates/punktfunk-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -36,9 +36,6 @@ mod ddc; #[path = "windows/display_events.rs"] mod display_events; #[cfg(target_os = "linux")] -#[path = "linux/dmabuf_fence.rs"] -mod dmabuf_fence; -#[cfg(target_os = "linux")] #[path = "linux/drm_sync.rs"] mod drm_sync; mod encode; @@ -85,9 +82,14 @@ mod win_adapter; #[cfg(target_os = "windows")] #[path = "windows/win_display.rs"] mod win_display; +// The zero-copy GPU plumbing lives in the `pf-zerocopy` leaf crate (plan §W6); this shim keeps +// every existing `crate::zerocopy::*` path valid. `drm_fourcc` consumes the frame vocabulary, so +// it sits with `capture` and is re-exported here for its old callers. #[cfg(target_os = "linux")] -#[path = "linux/zerocopy/mod.rs"] -mod zerocopy; +mod zerocopy { + pub(crate) use crate::capture::drm_fourcc; + pub(crate) use pf_zerocopy::*; +} use anyhow::{bail, Context, Result}; use encode::Codec;