refactor(host/W6.2): extract the Linux zero-copy GPU plumbing into the pf-zerocopy leaf crate
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
+3
-1
@@ -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
|
||||
+2
-17
@@ -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<u32> {
|
||||
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
|
||||
@@ -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::*;
|
||||
@@ -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"
|
||||
|
||||
@@ -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<u32> {
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user