fix(zerocopy): the mechanical sweep batch — truthful fence waits, forgiving env flags, no leaked planes
From the pf-zerocopy review sweep (design/pf-zerocopy-sweep-handoff.md), the compile-verifiable batch: - dmabuf_fence: the blocking poll's result was discarded — EINTR silently skipped the wait (reopening the stale-frame race a SIGCHLD away) and a timeout reported as waited. Now EINTR retries with the remaining budget and the caller gets Signaled/TimedOut/NoFence, so the one diagnostic operators have about implicit fencing stops lying. (C2) - env flags: PUNKTFUNK_ZEROCOPY=TRUE meant *off* — values are case-folded now, and an unrecognised spelling falls back to the flag's default with a one-shot warning instead of silently inverting the operator's intent. (C3) - cuda: alloc_pitched_nv12 leaked the Y plane when the UV allocation failed (per-frame under VRAM pressure — the worst possible time to leak); a failed async-copy enqueue now drains the stream before returning, so a recycled pool buffer can't race an orphaned in-flight copy. (C4, C5) - worker: --fd is validated (>= 3, fstat + S_ISSOCK) before OwnedFd adoption — 'zerocopy-worker --fd -1' was constructing OwnedFd's niche value. (V1) - docs: all 15 rustdoc warnings fixed, including the link to the renamed note_raw_dmabuf_negotiation_failed. (D1) - CI: a SPIR-V drift gate — the committed .spv blobs are include_bytes!'d and rebuilt by hand; the gate diffs disassembly (filtering only the shaderc/glslang generator difference) so a forgotten rebuild fails CI instead of shipping the old kernel. Both blobs verified in sync. (S1) - tests: bt709_limited pinned to external BT.709 anchors (it is the sole oracle for the GPU colour self-test); blend_geometry gets its first tests — empty rect, CURSOR_MAX clamp, per-format group counts, and negative-ox floor alignment. (T2, T3) Verified on .25: clippy -D warnings clean, 27/27 tests, cargo doc 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -45,7 +45,7 @@ use ash::vk;
|
||||
pub const CURSOR_MAX: u32 = cuda::CURSOR_MAX;
|
||||
|
||||
/// The vendored SPIR-V for `cursor_blend.comp` (beside this file; rebuild with
|
||||
/// `glslc cursor_blend.comp -o cursor_blend.spv`).
|
||||
/// `glslangValidator -V cursor_blend.comp -o cursor_blend.spv`; CI gates drift).
|
||||
const CURSOR_SPV: &[u8] = include_bytes!("cursor_blend.spv");
|
||||
|
||||
/// NVENC input-surface layout — selects the spec-constant `MODE` pipeline and the allocation
|
||||
@@ -84,9 +84,10 @@ impl SlotFormat {
|
||||
}
|
||||
|
||||
/// What the encoder holds per ring slot: the CUDA view it registers with NVENC plus the id it
|
||||
/// hands back to [`VkSlotBlend::blend`]. The backing Vulkan objects + CUDA mapping live in the
|
||||
/// [`VkSlotBlend`] (freed by [`free_slots`](VkSlotBlend::free_slots) / drop), so this is Copy —
|
||||
/// the encoder's ring keeps its existing shape.
|
||||
/// hands back to [`VkSlotBlend::blend_ref`] / [`VkSlotBlend::blend_ref_ordered`]. The backing
|
||||
/// Vulkan objects + CUDA mapping live in the [`VkSlotBlend`] (freed by
|
||||
/// [`free_slots`](VkSlotBlend::free_slots) / drop), so this is Copy — the encoder's ring keeps
|
||||
/// its existing shape.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct VkSlotRef {
|
||||
/// Device pointer NVENC registers (CUDA's mapping of the Vulkan memory).
|
||||
@@ -654,7 +655,7 @@ impl VkSlotBlend {
|
||||
}
|
||||
|
||||
/// Free every allocated slot (encoder teardown, alongside its ring clear). CUDA mappings drop
|
||||
/// first (field order in [`SlotAlloc`] frees `cuda` via its own `Drop` before we free the VK
|
||||
/// first (field order in `SlotAlloc` frees `cuda` via its own `Drop` before we free the VK
|
||||
/// objects explicitly here).
|
||||
pub fn free_slots(&mut self) {
|
||||
// Ordered blends return with their work still on the queue — quiesce before freeing the
|
||||
@@ -1062,3 +1063,62 @@ impl Drop for VkSlotBlend {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn slot() -> VkSlotRef {
|
||||
VkSlotRef {
|
||||
ptr: 0,
|
||||
pitch: 2048,
|
||||
height: 1080,
|
||||
id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn geo(fmt: SlotFormat, cw: u32, ch: u32, ox: i32, oy: i32) -> Option<(Push, u32, u32)> {
|
||||
VkSlotBlend::blend_geometry(&slot(), fmt, 1920, cw, ch, ox, oy)
|
||||
}
|
||||
|
||||
/// An empty (clamped-away) cursor rect dispatches nothing.
|
||||
#[test]
|
||||
fn empty_rect_is_none() {
|
||||
assert!(geo(SlotFormat::Argb, 0, 32, 10, 10).is_none());
|
||||
assert!(geo(SlotFormat::Nv12, 32, 0, 10, 10).is_none());
|
||||
}
|
||||
|
||||
/// Oversized bitmaps clamp to `CURSOR_MAX` — the push constants must agree with the staging
|
||||
/// buffer's capacity, or the shader reads past the uploaded bitmap.
|
||||
#[test]
|
||||
fn cursor_dims_clamp_to_max() {
|
||||
let (push, _, _) = geo(SlotFormat::Argb, CURSOR_MAX + 100, CURSOR_MAX + 1, 0, 0).unwrap();
|
||||
assert_eq!(push.cur_w, CURSOR_MAX);
|
||||
assert_eq!(push.cur_h, CURSOR_MAX);
|
||||
}
|
||||
|
||||
/// ARGB dispatches per cursor pixel; NV12/YUV444 per word-aligned 4-px span, NV12 walking
|
||||
/// 2-row blocks and YUV444 single rows. A 32×32 cursor at ox=13: spans cover the aligned
|
||||
/// x∈[12,48) → 9 spans → 2 groups of 8.
|
||||
#[test]
|
||||
fn group_counts_per_format() {
|
||||
let (_, gx, gy) = geo(SlotFormat::Argb, 32, 32, 13, 0).unwrap();
|
||||
assert_eq!((gx, gy), (4, 4)); // 32/8 in both axes
|
||||
|
||||
let (_, gx, gy) = geo(SlotFormat::Nv12, 32, 32, 13, 0).unwrap();
|
||||
assert_eq!((gx, gy), (2, 2)); // 9 spans → 2 groups; 16 2-row blocks → 2 groups
|
||||
|
||||
let (_, gx, gy) = geo(SlotFormat::Yuv444, 32, 32, 13, 0).unwrap();
|
||||
assert_eq!((gx, gy), (2, 4)); // 9 spans → 2 groups; 32 rows → 4 groups
|
||||
}
|
||||
|
||||
/// Negative `ox` must anchor spans with FLOOR alignment (`>>` on the signed value), not
|
||||
/// truncating division: at ox=-5 the aligned start is -8, giving 9 spans over a 32-px cursor
|
||||
/// (truncation would start at -4 and dispatch only 8 — silently dropping the right edge).
|
||||
#[test]
|
||||
fn negative_ox_floor_aligns_the_span_origin() {
|
||||
let (push, gx, _) = geo(SlotFormat::Nv12, 32, 32, -5, 0).unwrap();
|
||||
assert_eq!(push.ox, -5, "push constants carry the true origin");
|
||||
assert_eq!(gx, 2, "9 spans from the floor-aligned start → 2 groups");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user