feat(encode): raw Vulkan Video HEVC backend on Linux (AMD/Intel) with real RFI

Add `VulkanVideoEncoder` (`VK_KHR_video_encode_h265` via ash) — the open-stack
twin of the direct-NVENC RFI path, giving AMD/Intel Linux hosts real
reference-frame invalidation loss recovery: a clean P-frame recovery anchor
that re-references a known-good older frame instead of a full IDR. The app owns
the DPB, so recovery = pointing the P-frame's single L0 reference at a resident
slot strictly older than the loss (never a concealed frame).

The backend owns its own ash instance/device with encode + compute queues,
authors VPS/SPS/PPS (Main, conformance-window crop for non-16-aligned heights
like 1080->1088), runs a DPB-ring reference-slot state machine with monotonic
POC and CBR rate control, and does an on-GPU RGB->NV12 BT.709 compute CSC
(embedded rgb2yuv.spv) since capture delivers packed-RGB dmabufs — importing
each frame's dmabuf (explicit DRM modifier) or uploading a CPU-RGB fallback,
CSC on the compute queue, then encode on the encode queue, ordered by a
semaphore.

Wired into `open_video_backend`: an AMD/Intel HEVC session opens this instead
of libav VAAPI when `PUNKTFUNK_VULKAN_ENCODE=1` (VAAPI fallback on any open
error, so it can only improve recovery, never break a stream); `PUNKTFUNK_
ENCODER=vulkan` forces it. Gated behind the new `vulkan-encode` Cargo feature,
which pulls no new dependency (reuses the `ash` bindings already carried for
the dmabuf zero-copy bridge). Opt-in until on-glass validated, mirroring how
the direct-NVENC path shipped.

Headless-validated on real RADV (RDNA3 780M, Mesa 26): open + multi-frame
encode + `invalidate_ref_frames` all run through the real struct and ffmpeg
decodes the output `I P P P P P` with 0 errors; the recovery frame is a clean
P-frame (not an IDR); and dropping the "lost" AU still decodes cleanly because
the recovery re-anchored to an older frame — the RFI heal, proven on real
hardware. `cargo check`/`clippy -D warnings` green with the feature on and off.

Design: design/linux-vulkan-video-encode.md. Harness: design/vkenc-probe-harness/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 16:59:27 +02:00
parent 76594f27c1
commit 5ab6daa694
5 changed files with 1851 additions and 4 deletions
@@ -0,0 +1,27 @@
#version 450
// RGB(A) -> NV12 (BT.709 limited range). One invocation per chroma sample = 2x2 luma block.
layout(local_size_x = 8, local_size_y = 8) in;
layout(binding = 0) uniform sampler2D rgb; // packed RGB input (sampled; BGRA import ok)
layout(binding = 1, r8) uniform writeonly image2D yImg; // full-res Y
layout(binding = 2, rg8) uniform writeonly image2D uvImg; // half-res UV (interleaved)
float lumaY(vec3 c) { return 16.0/255.0 + 0.1826*c.r + 0.6142*c.g + 0.0620*c.b; }
void main() {
ivec2 sz = imageSize(yImg);
ivec2 uvc = ivec2(gl_GlobalInvocationID.xy);
ivec2 p = uvc * 2;
if (p.x >= sz.x || p.y >= sz.y) return;
vec3 c00 = texelFetch(rgb, p, 0).rgb;
vec3 c10 = texelFetch(rgb, p + ivec2(1, 0), 0).rgb;
vec3 c01 = texelFetch(rgb, p + ivec2(0, 1), 0).rgb;
vec3 c11 = texelFetch(rgb, p + ivec2(1, 1), 0).rgb;
imageStore(yImg, p, vec4(lumaY(c00), 0, 0, 1));
imageStore(yImg, p + ivec2(1, 0), vec4(lumaY(c10), 0, 0, 1));
imageStore(yImg, p + ivec2(0, 1), vec4(lumaY(c01), 0, 0, 1));
imageStore(yImg, p + ivec2(1, 1), vec4(lumaY(c11), 0, 0, 1));
vec3 a = (c00 + c10 + c01 + c11) * 0.25;
float U = 128.0/255.0 - 0.1006*a.r - 0.3386*a.g + 0.4392*a.b;
float V = 128.0/255.0 + 0.4392*a.r - 0.3989*a.g - 0.0403*a.b;
imageStore(uvImg, uvc, vec4(U, V, 0, 1));
}