From 2bce5e93a615b746c41c7b97cea9180d20e0247f Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 25 Jun 2026 18:14:51 -0400 Subject: [PATCH] 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. --- .../gpu-video-encoder/src/encoder.rs | 17 +++----- .../gpu-video-encoder/src/vaapi.rs | 42 +++++++++++++++++++ .../gpu-video-encoder/src/vk_device.rs | 5 ++- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/lightningbeam-ui/gpu-video-encoder/src/encoder.rs b/lightningbeam-ui/gpu-video-encoder/src/encoder.rs index c209e9b..1e42a82 100644 --- a/lightningbeam-ui/gpu-video-encoder/src/encoder.rs +++ b/lightningbeam-ui/gpu-video-encoder/src/encoder.rs @@ -35,6 +35,10 @@ pub struct ZeroCopyEncoder { cache: HashMap, } +// 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 { /// 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. @@ -48,18 +52,7 @@ impl ZeroCopyEncoder { let drm = vk_device::create()?; let renderer = Rgba2Nv12::new(&drm.device); unsafe { - let mut hw_device: *mut ff::AVBufferRef = ptr::null_mut(); - 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 mut hw_device = crate::vaapi::create_device()?; let name = CString::new("h264_vaapi").unwrap(); let codec = ff::avcodec_find_encoder_by_name(name.as_ptr()); if codec.is_null() { diff --git a/lightningbeam-ui/gpu-video-encoder/src/vaapi.rs b/lightningbeam-ui/gpu-video-encoder/src/vaapi.rs index d2eee0c..f7502e0 100644 --- a/lightningbeam-ui/gpu-video-encoder/src/vaapi.rs +++ b/lightningbeam-ui/gpu-video-encoder/src/vaapi.rs @@ -16,6 +16,48 @@ fn averror(e: i32) -> i32 { -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 /// each plane's linesize (which FFmpeg may pad). unsafe fn fill_nv12(frame: *mut ff::AVFrame, nv12: &[u8], width: u32, height: u32) { diff --git a/lightningbeam-ui/gpu-video-encoder/src/vk_device.rs b/lightningbeam-ui/gpu-video-encoder/src/vk_device.rs index 946d364..889b57d 100644 --- a/lightningbeam-ui/gpu-video-encoder/src/vk_device.rs +++ b/lightningbeam-ui/gpu-video-encoder/src/vk_device.rs @@ -131,7 +131,10 @@ unsafe fn create_inner() -> Result { &wgpu::DeviceDescriptor { label: Some("drm-import-device"), 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() }, )