The native VAAPI decoder now runs end to end: pf-vaadec's plans go into libva's buffers, the surface comes back as DRM-PRIME dmabufs, and the presenter imports them exactly as it does the FFmpeg rung's. Pin-only — `PUNKTFUNK_DECODER=native-vaapi` — for the reason M5's D3D11VA rung was: `auto` admission is earned with hardware parity and a soak, and this rung has decoded nothing yet. libva is dlopen'd rather than linked, so the pf-lxcheck2 container compiles and clippies the whole thing without libva-dev, and a machine without a VAAPI runtime gets a clean refusal instead of a packaging dependency. The surface pool is not the slot map. `SlotMap::assign` hands out the lowest free slot, and a slot freed by an access unit's own removals is free by the time that unit's picture takes it — measured at 225 of the vendored vector's 250 access units. A surface bound by slot index would therefore decode, on nine frames in ten, into the surface still holding the picture on screen. So `plan_to_va` now takes the decode target as a parameter, bound by the caller at activation time the way pf-vkdecode binds a pool image, and a surface is free only when no live picture is bound to it, no output is owed for it, and no consumer holds it. Measured rather than transcribed, as everywhere else here: layout-probe.c grew the export descriptor (312 bytes, objects[4]/layers[4]), the buffer-type enumerators — VASliceParameterBufferType is 4 and VASliceDataBufferType is 5, not the 3 and 4 that counting off the header suggests — and the config, attribute and generic-value layouts. All pinned as compile-time assertions, which is how the 12-byte VAGenericValue in the first draft was caught: the C union holds a pointer, so it is 8-aligned and 16 bytes. The plane walk lives in pf-vaadec, pure and unit-tested on macOS, because it is the one structure the DRIVER writes and we read: SEPARATE_LAYERS returns NV12 as two layers, and taking layers[0] is the green screen this project has already paid for. It also refuses what it cannot express rather than guessing — a bogus object count, a plane naming an object that is not there, objects disagreeing on tiling. Own DecodedImage variant, same payload type. The physical hand-off is identical to the FFmpeg rung's, so the presenter keeps ONE arm and one demotion streak; the variant exists so the compiler asks which rung decoded wherever that matters. Both D3D11VA rungs share a variant and `1573a987` had to fix the consequence afterwards — a "native" soak that could silently have been an FFmpeg soak. Here the four uncovered matches were compile errors. Buffers are destroyed by us, not by vaEndPicture: va.h is explicit that the user must call vaDestroyBuffer, and the libva 0.x behaviour is long gone. Leaking two per picture at 60 fps exhausts the driver's store in minutes. pf-vaadec's presenter headroom was 4, written against no consumer. The Vulkan rung had already measured the client pipeline at four to seven held frames; it is 8 now, pinned to that crate's constant so a re-measurement moves both. Gates: macOS fmt/clippy/341 tests/cargo doc, and in the container clippy -D warnings over six crates, 795 tests, workspace check. Hardware legs are still owed — no AMD/Mesa or Intel box was reachable.
288 lines
13 KiB
Rust
288 lines
13 KiB
Rust
//! VAAPI (libavcodec hwaccel) decode backend → DRM-PRIME dmabuf for the presenter. Linux-only.
|
|
|
|
use crate::video::{
|
|
averr, drm_fourcc_for, frame_is_keyframe, DmabufFrame, DmabufPlane, DrmFrameGuard,
|
|
AVERROR_EAGAIN,
|
|
};
|
|
use crate::video_color::ColorDesc;
|
|
use crate::video_libav::AvBuffer;
|
|
use anyhow::{anyhow, bail, Context, Result};
|
|
use ffmpeg_next as ffmpeg;
|
|
use std::ptr;
|
|
|
|
/// libavcodec offers the formats it can decode into; pick the VAAPI hw surface. Falling
|
|
/// back to the first (software) entry would silently decode on the CPU *and* break our
|
|
/// dmabuf mapping — return NONE instead so the error surfaces and the session demotes
|
|
/// to the software backend explicitly.
|
|
#[cfg(target_os = "linux")]
|
|
unsafe extern "C" fn pick_vaapi(
|
|
_ctx: *mut ffmpeg::ffi::AVCodecContext,
|
|
mut list: *const ffmpeg::ffi::AVPixelFormat,
|
|
) -> ffmpeg::ffi::AVPixelFormat {
|
|
// SAFETY: libav calls this `get_format` callback with a list it owns, terminated by
|
|
// `AV_PIX_FMT_NONE` — the walk stops at that terminator, so it stays inside the array, and it
|
|
// only reads.
|
|
unsafe {
|
|
while *list != ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_NONE {
|
|
if *list == ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_VAAPI {
|
|
return ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_VAAPI;
|
|
}
|
|
list = list.add(1);
|
|
}
|
|
}
|
|
ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_NONE
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
pub(crate) struct VaapiDecoder {
|
|
ctx: *mut ffmpeg::ffi::AVCodecContext,
|
|
/// The VAAPI hwdevice, owned. Nothing reads this field after construction — the codec context
|
|
/// took its own ref via `av_buffer_ref` — it exists so the device outlives the decoder and is
|
|
/// unref'd exactly once when it drops. Declared after `ctx` so it still releases AFTER the
|
|
/// `Drop` below frees packet/frame/context, which is the order the hand-written unref had.
|
|
/// `dead_code` is answered here rather than by removing the field (that would free the device
|
|
/// early) or by an underscore name (that would hide what it is).
|
|
#[allow(dead_code)]
|
|
hw_device: AvBuffer,
|
|
packet: *mut ffmpeg::ffi::AVPacket,
|
|
frame: *mut ffmpeg::ffi::AVFrame,
|
|
/// The selected decoder's registry name (`(*codec).name`) — `"av1"` vs `"libdav1d"`
|
|
/// is the difference between hardware decode and a silent CPU fallback, so every
|
|
/// log a field report leans on carries it.
|
|
name: String,
|
|
}
|
|
|
|
// SAFETY: the three raw pointers (`ctx`, `packet`, `frame`) are allocations this decoder makes in
|
|
// its constructor and frees exactly once in `Drop`; nothing else holds them, and `hw_device` is an
|
|
// owning `AvBuffer` whose refcount is atomic. `Send` only permits MOVING that ownership to another
|
|
// thread, which libav supports — a codec context may be used from a thread other than the one that
|
|
// created it, provided use is serialised, and `&mut self` on every method is that serialisation.
|
|
// Deliberately NOT `Sync`: two threads holding `&VaapiDecoder` could call into libav concurrently
|
|
// on one context, which libav does not allow.
|
|
#[cfg(target_os = "linux")]
|
|
unsafe impl Send for VaapiDecoder {}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
impl VaapiDecoder {
|
|
pub(crate) fn new(codec_id: ffmpeg::codec::Id) -> Result<VaapiDecoder> {
|
|
use ffmpeg::ffi;
|
|
// SAFETY: a self-contained builder — every allocation below is made here, each result is
|
|
// checked before the next call uses it, and the ones that survive are moved into the
|
|
// returned `VaapiDecoder`, which frees each exactly once in `Drop`.
|
|
unsafe {
|
|
let mut hw_device: *mut ffi::AVBufferRef = ptr::null_mut();
|
|
let r = ffi::av_hwdevice_ctx_create(
|
|
&mut hw_device,
|
|
ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI,
|
|
ptr::null(),
|
|
ptr::null_mut(),
|
|
0,
|
|
);
|
|
if r < 0 {
|
|
bail!("no VAAPI device ({})", ffmpeg::Error::from(r));
|
|
}
|
|
// Owned from here: every `bail!` below drops it, so none of them unref by hand.
|
|
let hw_device = AvBuffer::from_raw(hw_device)
|
|
.context("av_hwdevice_ctx_create(VAAPI) gave no device")?;
|
|
// NOT `avcodec_find_decoder`: the ID lookup returns the registry's FIRST
|
|
// decoder, and for AV1 that is libdav1d (upstream orders the hwaccel-only
|
|
// native decoder last) — a software decoder that silently ignores
|
|
// `hw_device_ctx` and fails every frame's VAAPI-format guard mid-stream.
|
|
// Select by capability instead: the first decoder that can drive
|
|
// AV_PIX_FMT_VAAPI via hw_device_ctx, or fail here at open.
|
|
let codec =
|
|
crate::video::find_hw_decoder(codec_id, ffi::AVPixelFormat::AV_PIX_FMT_VAAPI)?;
|
|
let name = crate::video::codec_name(codec);
|
|
let ctx = ffi::avcodec_alloc_context3(codec);
|
|
(*ctx).hw_device_ctx = ffi::av_buffer_ref(hw_device.as_ptr());
|
|
(*ctx).get_format = Some(pick_vaapi);
|
|
(*ctx).flags |= ffi::AV_CODEC_FLAG_LOW_DELAY as i32;
|
|
(*ctx).thread_count = 1; // hwaccel: threads only add latency
|
|
|
|
// The presenter holds mapped surfaces PAST receive_frame (the paintable's
|
|
// current texture + the newest frame in flight each pin one until GDK's
|
|
// release func) — surfaces libavcodec doesn't know are missing from its
|
|
// fixed-size VAAPI pool. Without headroom the decoder can recycle a surface
|
|
// the renderer is still sampling (intermittent block corruption) or fail
|
|
// allocation under scheduling jitter.
|
|
(*ctx).extra_hw_frames = 4;
|
|
let r = ffi::avcodec_open2(ctx, codec, ptr::null_mut());
|
|
if r < 0 {
|
|
let mut ctx = ctx;
|
|
ffi::avcodec_free_context(&mut ctx);
|
|
bail!("avcodec_open2: {}", ffmpeg::Error::from(r));
|
|
}
|
|
Ok(VaapiDecoder {
|
|
ctx,
|
|
hw_device,
|
|
packet: ffi::av_packet_alloc(),
|
|
frame: ffi::av_frame_alloc(),
|
|
name,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The selected decoder's registry name (e.g. `"av1"`) — see the field doc.
|
|
pub(crate) fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
|
|
pub(crate) fn decode(&mut self, au: &[u8]) -> Result<Option<DmabufFrame>> {
|
|
use ffmpeg::ffi;
|
|
// SAFETY: `packet`/`frame`/`ctx` are this decoder's own allocations, live for its whole
|
|
// lifetime; `au` outlives the synchronous `send_packet` that copies out of it, and every
|
|
// libav return is checked before the result is used.
|
|
unsafe {
|
|
let r = ffi::av_new_packet(self.packet, au.len() as i32);
|
|
if r < 0 {
|
|
return Err(averr("av_new_packet", r));
|
|
}
|
|
ptr::copy_nonoverlapping(au.as_ptr(), (*self.packet).data, au.len());
|
|
let r = ffi::avcodec_send_packet(self.ctx, self.packet);
|
|
ffi::av_packet_unref(self.packet);
|
|
if r < 0 {
|
|
return Err(averr("send_packet", r));
|
|
}
|
|
let mut out = None;
|
|
loop {
|
|
let r = ffi::avcodec_receive_frame(self.ctx, self.frame);
|
|
if r == AVERROR_EAGAIN {
|
|
break;
|
|
}
|
|
if r < 0 {
|
|
return Err(averr("receive_frame", r));
|
|
}
|
|
out = Some(self.map_dmabuf()?); // newest wins; older guards drop here
|
|
ffi::av_frame_unref(self.frame);
|
|
}
|
|
Ok(out)
|
|
}
|
|
}
|
|
|
|
/// Map the VAAPI surface to DRM PRIME (zero copy) and lift the descriptor into a
|
|
/// `DmabufFrame`. The mapped frame keeps the surface alive via its buffer refs.
|
|
///
|
|
/// FFmpeg's VAAPI export uses `VA_EXPORT_SURFACE_SEPARATE_LAYERS`, so an NV12 surface
|
|
/// comes back as TWO layers (`R8` luma + `GR88` chroma), each one plane — NOT a single
|
|
/// `NV12` layer. The previous code took `layers[0]` only: GTK then saw an `R8`
|
|
/// single-plane texture with the chroma dropped, painting the screen green. The fix:
|
|
/// derive the COMBINED fourcc from the decoder's software pixel format (NV12 →
|
|
/// `DRM_FORMAT_NV12`) and flatten every plane across every layer in order (Y then UV).
|
|
fn map_dmabuf(&mut self) -> Result<DmabufFrame> {
|
|
use ffmpeg::ffi;
|
|
// SAFETY: `self.frame` is this decoder's own `AVFrame`, holding a decoded VAAPI surface —
|
|
// the format check below is what proves that before anything reads the hardware layout.
|
|
unsafe {
|
|
if (*self.frame).format != ffi::AVPixelFormat::AV_PIX_FMT_VAAPI as i32 {
|
|
bail!("decoder returned a software frame (no VAAPI surface)");
|
|
}
|
|
// The real pixel layout lives on the hardware frames context, not the
|
|
// DRM-PRIME layer formats (those are the per-plane R8/GR88 component formats).
|
|
let sw_format = {
|
|
let hwfc = (*self.frame).hw_frames_ctx;
|
|
if hwfc.is_null() {
|
|
bail!("VAAPI frame without a hardware frames context");
|
|
}
|
|
(*((*hwfc).data as *const ffi::AVHWFramesContext)).sw_format
|
|
};
|
|
let fourcc = drm_fourcc_for(sw_format)
|
|
.ok_or_else(|| anyhow!("unsupported VAAPI output format {sw_format:?}"))?;
|
|
|
|
let drm = ffi::av_frame_alloc();
|
|
(*drm).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32;
|
|
let r = ffi::av_hwframe_map(drm, self.frame, ffi::AV_HWFRAME_MAP_READ as i32);
|
|
if r < 0 {
|
|
let mut drm = drm;
|
|
ffi::av_frame_free(&mut drm);
|
|
return Err(averr("av_hwframe_map", r));
|
|
}
|
|
let desc = (*drm).data[0] as *const ffi::AVDRMFrameDescriptor;
|
|
let guard = DrmFrameGuard::Av(drm);
|
|
let d = &*desc;
|
|
if d.nb_layers < 1 || d.nb_objects < 1 {
|
|
bail!("DRM descriptor without layers/objects");
|
|
}
|
|
|
|
// Flatten planes across ALL layers, in declared order — the combined fourcc's
|
|
// plane order (Y, then UV for NV12) matches the layer order FFmpeg emits.
|
|
let mut planes = Vec::new();
|
|
for layer in &d.layers[..d.nb_layers as usize] {
|
|
for p in &layer.planes[..layer.nb_planes as usize] {
|
|
let obj = &d.objects[p.object_index as usize];
|
|
planes.push(DmabufPlane {
|
|
fd: obj.fd,
|
|
offset: p.offset as u32,
|
|
stride: p.pitch as u32,
|
|
});
|
|
}
|
|
}
|
|
|
|
// The whole surface shares one tiling modifier (one BO on radeonsi); GTK takes
|
|
// a single modifier for the texture.
|
|
let modifier = d.objects[0].format_modifier;
|
|
|
|
log_descriptor_once(d, sw_format, fourcc, modifier, &self.name);
|
|
|
|
Ok(DmabufFrame {
|
|
width: (*self.frame).width as u32,
|
|
height: (*self.frame).height as u32,
|
|
fourcc,
|
|
modifier,
|
|
planes,
|
|
// SAFETY: `self.frame` is the live decoded AVFrame (unref'd only after
|
|
// this returns); plain CICP field reads.
|
|
color: ColorDesc::from_raw(self.frame),
|
|
keyframe: frame_is_keyframe(self.frame),
|
|
guard,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One-time dump of the DRM descriptor layout (objects, layers, planes, modifier) — so a
|
|
/// new client/driver combination's real layout is visible in the logs without a debugger.
|
|
#[cfg(target_os = "linux")]
|
|
fn log_descriptor_once(
|
|
d: &ffmpeg_next::ffi::AVDRMFrameDescriptor,
|
|
sw: ffmpeg_next::ffi::AVPixelFormat,
|
|
fourcc: u32,
|
|
modifier: u64,
|
|
decoder: &str,
|
|
) {
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
static ONCE: AtomicBool = AtomicBool::new(true);
|
|
if !ONCE.swap(false, Ordering::Relaxed) {
|
|
return;
|
|
}
|
|
let layers: Vec<(u32, i32)> = d.layers[..d.nb_layers.max(0) as usize]
|
|
.iter()
|
|
.map(|l| (l.format, l.nb_planes))
|
|
.collect();
|
|
tracing::info!(
|
|
sw_format = ?sw,
|
|
chosen_fourcc = format_args!("{:#010x}", fourcc),
|
|
nb_objects = d.nb_objects,
|
|
nb_layers = d.nb_layers,
|
|
?layers,
|
|
modifier = format_args!("{:#018x}", modifier),
|
|
decoder,
|
|
"VAAPI dmabuf descriptor layout (first frame)"
|
|
);
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
impl Drop for VaapiDecoder {
|
|
fn drop(&mut self) {
|
|
use ffmpeg::ffi;
|
|
// SAFETY: each pointer is this decoder's own allocation and nothing else holds it; `Drop`
|
|
// runs exactly once, and each free nulls the pointer through its `&mut`, so none can be
|
|
// released twice. Freed packet-then-frame-then-context, the order libav documents.
|
|
unsafe {
|
|
ffi::av_packet_free(&mut self.packet);
|
|
ffi::av_frame_free(&mut self.frame);
|
|
ffi::avcodec_free_context(&mut self.ctx);
|
|
// `hw_device` is an `AvBuffer` and unrefs itself when the field drops, right after this.
|
|
}
|
|
}
|
|
}
|