gpu-video-encoder: VAAPI driver retry, Vello-capable device, Send

Three fixes found while running the zero-copy export on real Intel hardware:

- vaapi::create_device() retries LIBVA_DRIVER_NAME in order iHD -> auto ->
  i965 -> radeonsi. libva was auto-selecting the legacy i965 driver, which
  fails on newer Intel GPUs; the modern iHD (intel-media-driver) is needed.
  encoder.rs now builds its hwdevice through this helper.
- vk_device: request the adapter's full limits instead of downlevel_defaults.
  Vello's compute pipelines need max_storage_buffers_per_shader_stage >= 5
  (downlevel caps at 4), which panicked Vello's shader init on the export
  device. This device only ever runs on a real VAAPI GPU.
- ZeroCopyEncoder: unsafe impl Send. It owns its FFmpeg/Vulkan handles
  exclusively and is only moved (onto the export thread), never shared.
This commit is contained in:
Skyler Lehmkuhl 2026-06-25 18:14:51 -04:00
parent 3e4f29c297
commit 2bce5e93a6
3 changed files with 51 additions and 13 deletions

View File

@ -35,6 +35,10 @@ pub struct ZeroCopyEncoder {
cache: HashMap<usize, ImportedNv12>, cache: HashMap<usize, ImportedNv12>,
} }
// The encoder owns its FFmpeg contexts (raw `*mut`) and Vulkan/wgpu handles exclusively; it is
// never shared, only moved. Sending it to a dedicated export thread is sound.
unsafe impl Send for ZeroCopyEncoder {}
impl ZeroCopyEncoder { impl ZeroCopyEncoder {
/// Build a zero-copy `h264_vaapi` encoder writing to `output_path` (container inferred /// Build a zero-copy `h264_vaapi` encoder writing to `output_path` (container inferred
/// from the extension, e.g. `.mp4`). `Err` if VAAPI/the device is unavailable. /// from the extension, e.g. `.mp4`). `Err` if VAAPI/the device is unavailable.
@ -48,18 +52,7 @@ impl ZeroCopyEncoder {
let drm = vk_device::create()?; let drm = vk_device::create()?;
let renderer = Rgba2Nv12::new(&drm.device); let renderer = Rgba2Nv12::new(&drm.device);
unsafe { unsafe {
let mut hw_device: *mut ff::AVBufferRef = ptr::null_mut(); let mut hw_device = crate::vaapi::create_device()?;
let node = CString::new("/dev/dri/renderD128").unwrap();
if ff::av_hwdevice_ctx_create(
&mut hw_device,
ff::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI,
node.as_ptr(),
ptr::null_mut(),
0,
) < 0
{
return Err("av_hwdevice_ctx_create failed".into());
}
let name = CString::new("h264_vaapi").unwrap(); let name = CString::new("h264_vaapi").unwrap();
let codec = ff::avcodec_find_encoder_by_name(name.as_ptr()); let codec = ff::avcodec_find_encoder_by_name(name.as_ptr());
if codec.is_null() { if codec.is_null() {

View File

@ -16,6 +16,48 @@ fn averror(e: i32) -> i32 {
-e -e
} }
/// Create a VAAPI hwdevice on `/dev/dri/renderD128`, trying driver names in turn.
///
/// libva's auto-selection can pick a driver that doesn't support the GPU — notably it
/// chooses the legacy `i965` driver on newer Intel parts (Gen 11+) where the modern `iHD`
/// driver is required. Each `av_hwdevice_ctx_create` opens a fresh VADisplay, so
/// `LIBVA_DRIVER_NAME` is re-read per attempt. We try `iHD` first (modern Intel), then the
/// caller's original setting, then `i965` (older Intel) and `radeonsi` (AMD). On success the
/// working driver name is left in the env; on total failure the original value is restored.
pub fn create_device() -> Result<*mut ff::AVBufferRef, String> {
unsafe {
let node = CString::new("/dev/dri/renderD128").unwrap();
let original = std::env::var_os("LIBVA_DRIVER_NAME");
let attempts: [Option<&str>; 4] = [Some("iHD"), None, Some("i965"), Some("radeonsi")];
for drv in attempts {
match drv {
Some(d) => std::env::set_var("LIBVA_DRIVER_NAME", d),
// `None` = the caller's original setting (or libva auto if unset).
None => match &original {
Some(v) => std::env::set_var("LIBVA_DRIVER_NAME", v),
None => std::env::remove_var("LIBVA_DRIVER_NAME"),
},
}
let mut hw: *mut ff::AVBufferRef = ptr::null_mut();
if ff::av_hwdevice_ctx_create(
&mut hw,
ff::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI,
node.as_ptr(),
ptr::null_mut(),
0,
) >= 0
{
return Ok(hw);
}
}
match &original {
Some(v) => std::env::set_var("LIBVA_DRIVER_NAME", v),
None => std::env::remove_var("LIBVA_DRIVER_NAME"),
}
Err("av_hwdevice_ctx_create(VAAPI) failed for all drivers (iHD/i965/radeonsi)".into())
}
}
/// Copy tight NV12 (`Y` then interleaved `UV`) into an AVFrame's planes, respecting /// Copy tight NV12 (`Y` then interleaved `UV`) into an AVFrame's planes, respecting
/// each plane's linesize (which FFmpeg may pad). /// each plane's linesize (which FFmpeg may pad).
unsafe fn fill_nv12(frame: *mut ff::AVFrame, nv12: &[u8], width: u32, height: u32) { unsafe fn fill_nv12(frame: *mut ff::AVFrame, nv12: &[u8], width: u32, height: u32) {

View File

@ -131,7 +131,10 @@ unsafe fn create_inner() -> Result<DrmDevice, String> {
&wgpu::DeviceDescriptor { &wgpu::DeviceDescriptor {
label: Some("drm-import-device"), label: Some("drm-import-device"),
required_features: wgpu::Features::empty(), required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_defaults(), // Vello's compute pipelines need more than downlevel limits (e.g.
// max_storage_buffers_per_shader_stage >= 5). This device only ever runs on a
// real VAAPI-capable GPU, so request the adapter's full limits.
required_limits: wgpu_adapter.limits(),
..Default::default() ..Default::default()
}, },
) )