diff --git a/crates/compositor/src/d3d_linux.rs b/crates/compositor/src/d3d_linux.rs index 1b11fadb6..2900892c3 100644 --- a/crates/compositor/src/d3d_linux.rs +++ b/crates/compositor/src/d3d_linux.rs @@ -271,6 +271,38 @@ impl Gpu { probe() } + /// Le noeud DRM render du GPU sur lequel ce device rend (`/dev/dri/renderDN`), + /// lu par `VK_EXT_physical_device_drm`. C'est celui ou l'encodeur VAAPI doit + /// importer les dmabufs du compositeur. `None` si le pilote ne l'expose pas + /// (lavapipe, instance Vulkan 1.0) : l'appelant balaie alors les noeuds. + pub fn render_node(&self) -> Option { + use ash::vk; + unsafe { + self.device.as_hal::(|dev| { + let dev = dev?; + let shared = dev.shared_instance(); + // `vkGetPhysicalDeviceProperties2` est du coeur 1.1. + if shared.instance_api_version() < vk::API_VERSION_1_1 { + return None; + } + let instance = shared.raw_instance(); + let phys = dev.raw_physical_device(); + let exts = instance.enumerate_device_extension_properties(phys).ok()?; + if !exts + .iter() + .any(|e| e.extension_name_as_c_str() == Ok(ash::ext::physical_device_drm::NAME)) + { + return None; + } + let mut drm = vk::PhysicalDeviceDrmPropertiesEXT::default(); + let mut props = vk::PhysicalDeviceProperties2::default().push_next(&mut drm); + instance.get_physical_device_properties2(phys, &mut props); + (drm.has_render == vk::TRUE) + .then(|| format!("/dev/dri/renderD{}", drm.render_minor)) + }) + } + } + /// Le device de TOUS les tests unitaires du crate : cree une fois par `create_auto`, /// jamais detruit. `None` (et le test se saute) sans adaptateur Vulkan. /// diff --git a/crates/compositor/src/pipeline_linux.rs b/crates/compositor/src/pipeline_linux.rs index edc9ea623..0c90da36e 100644 --- a/crates/compositor/src/pipeline_linux.rs +++ b/crates/compositor/src/pipeline_linux.rs @@ -931,7 +931,9 @@ fn run_multi_inner( Some(name) => name.contains("vaapi"), }; let hw = if hw_allowed && matches!(params.codec, ExportCodec::H264) { - unsafe { VaapiEncoder::open(out_w as i32, out_h as i32, out_fps, bit_rate) } + unsafe { + VaapiEncoder::open(out_w as i32, out_h as i32, out_fps, bit_rate, gpu.render_node()) + } .and_then(|v| { // PLUS DE TAMPONS QUE L'ENCODEUR N'A DE LATENCE. Deux suffisaient // pour recouvrir composition et encodage, mais pas pour la @@ -1325,44 +1327,54 @@ unsafe extern "C" fn drm_desc_free(_opaque: *mut std::ffi::c_void, data: *mut u8 crate::ffi::av_free(data as *mut std::ffi::c_void); } +/// Les noeuds DRM a essayer, dans l'ordre. `OPENSCREEN_LINUX_RENDER_NODE` d'abord +/// (echappatoire), puis le noeud du GPU de wgpu. Le balayage de `renderD128..135` +/// ne sert que si ce noeud est illisible : sur un portable hybride, un autre GPU +/// ouvrirait VAAPI sans encombre puis refuserait chaque dmabuf du compositeur. +fn render_node_candidates(explicit: Option, adapter: Option) -> Vec { + let mut nodes: Vec = explicit.into_iter().filter(|n| !n.is_empty()).collect(); + if let Some(node) = adapter { + if !nodes.contains(&node) { + nodes.push(node); + } + return nodes; + } + for idx in 128..=135 { + let p = format!("/dev/dri/renderD{idx}"); + if Path::new(&p).exists() && !nodes.contains(&p) { + nodes.push(p); + } + } + if nodes.is_empty() { + nodes.push("/dev/dri/renderD128".to_owned()); + } + nodes +} + impl VaapiEncoder { /// Ouvre la chaine DRM -> VAAPI -> `h264_vaapi`. `None` si quoi que ce soit /// manque : l'appelant retombe alors sur l'encodeur software. - pub unsafe fn open(w: i32, h: i32, fps: i32, bit_rate: i64) -> Option { + /// + /// `adapter_node` est le noeud DRM du GPU sur lequel wgpu rend + /// (`Gpu::render_node`) : les dmabufs viennent de lui. + pub unsafe fn open( + w: i32, + h: i32, + fps: i32, + bit_rate: i64, + adapter_node: Option, + ) -> Option { use crate::ffi::*; - let mut me = VaapiEncoder { - ctx: ptr::null_mut(), - drm_device: ptr::null_mut(), - va_device: ptr::null_mut(), - drm_frames: ptr::null_mut(), - va_frames: ptr::null_mut(), - graph: ptr::null_mut(), - vpp_in: ptr::null_mut(), - vpp_out: ptr::null_mut(), - w, - h, - }; // LE DEVICE DRM D'ABORD, PUIS VAAPI DERIVE DE LUI. L'ordre inverse // (VAAPI ouvert seul) rend ENOSYS sur radeonsi : le mapping veut les deux // cotes d'un meme device. - let node = std::ffi::CString::new("/dev/dri/renderD128").ok()?; - if av_hwdevice_ctx_create( - &mut me.drm_device, - AVHWDeviceType::AV_HWDEVICE_TYPE_DRM, - node.as_ptr(), - ptr::null_mut(), - 0, - ) < 0 - { - return None; - } - if av_hwdevice_ctx_create_derived( - &mut me.va_device, - AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI, - me.drm_device, - 0, - ) < 0 - { + let candidates = render_node_candidates( + std::env::var("OPENSCREEN_LINUX_RENDER_NODE").ok(), + adapter_node, + ); + let name = std::ffi::CString::new("h264_vaapi").ok()?; + let enc = avcodec_find_encoder_by_name(name.as_ptr()); + if enc.is_null() { return None; } // `initial_pool_size = 0` sur LES DEUX contextes : ils ne font @@ -1382,70 +1394,107 @@ impl VaapiEncoder { (*c).height = h; (*c).initial_pool_size = 0; if av_hwframe_ctx_init(frames) < 0 { + let mut f = frames; + av_buffer_unref(&mut f); return ptr::null_mut(); } frames }; - me.drm_frames = mk_frames(me.drm_device, AVPixelFormat::AV_PIX_FMT_DRM_PRIME); - me.va_frames = mk_frames(me.va_device, AVPixelFormat::AV_PIX_FMT_VAAPI); - if me.drm_frames.is_null() || me.va_frames.is_null() { - return None; - } - // Le pilote decide. Une chaine illisible compte comme « pas iHD » : on - // garde alors le chemin direct, celui d'avant la copie. - if va_vendor(me.va_device).is_some_and(|v| needs_vpp_copy(&v)) { - me.build_vpp()?; - } - let name = std::ffi::CString::new("h264_vaapi").ok()?; - let enc = avcodec_find_encoder_by_name(name.as_ptr()); - if enc.is_null() { - return None; - } - me.ctx = avcodec_alloc_context3(enc); - if me.ctx.is_null() { - return None; - } - (*me.ctx).width = w; - (*me.ctx).height = h; - (*me.ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_VAAPI as i32; - (*me.ctx).time_base = AVRational { num: 1, den: fps }; - (*me.ctx).framerate = AVRational { num: fps, den: 1 }; - (*me.ctx).bit_rate = bit_rate; - // MEME INTERVALLE D'IMAGES CLES QUE LE CHEMIN SOFTWARE, et pour la meme - // raison : deux secondes est le compromis choisi pour un fichier de - // sortie, il n'a pas a dependre de l'encodeur qui se trouve disponible. - // - // Sans cette ligne le resultat est correct A 60 FPS ET NULLE PART - // AILLEURS : `h264_vaapi` a un defaut de 120 frames (mesure), qui vaut - // deux secondes a 60 fps par coincidence. A 30 fps ca donnerait quatre - // secondes, a 120 fps une seule. Le defaut de `libopenh264` est -1, ce - // qui est un autre probleme encore (une seule image cle pour tout le - // fichier) traite dans `try_open`. - (*me.ctx).gop_size = (fps * 2).max(1); - // MP4 veut SPS/PPS dans l'extradata, pas repetes devant chaque image - // cle. Le chemin software le pose depuis toujours (`try_open`) ; l'avoir - // oublie ici produisait un fichier qui se lit quand meme, parce que le - // muxer recupere ce qu'il trouve — mais un lecteur qui se fie a - // `codecpar` seul aurait de quoi echouer. - (*me.ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as i32; - // La matrice des frames que `send_dmabuf` pose, celle de `yuv.wgsl`. - tag_export_color(me.ctx, AVColorSpace::AVCOL_SPC_SMPTE170M); - // Avec la copie, les surfaces de SORTIE du VPP, pas celles du mapping : - // c'est elles que l'encodeur recoit. - let out_frames = if me.graph.is_null() { - me.va_frames - } else { - av_buffersink_get_hw_frames_ctx(me.vpp_out) - }; - if out_frames.is_null() { - return None; - } - (*me.ctx).hw_frames_ctx = av_buffer_ref(out_frames); - if avcodec_open2(me.ctx, enc, ptr::null_mut()) < 0 { - return None; + for cand in candidates { + let Ok(node) = std::ffi::CString::new(cand) else { + continue; + }; + let mut me = VaapiEncoder { + ctx: ptr::null_mut(), + drm_device: ptr::null_mut(), + va_device: ptr::null_mut(), + drm_frames: ptr::null_mut(), + va_frames: ptr::null_mut(), + graph: ptr::null_mut(), + vpp_in: ptr::null_mut(), + vpp_out: ptr::null_mut(), + w, + h, + }; + if av_hwdevice_ctx_create( + &mut me.drm_device, + AVHWDeviceType::AV_HWDEVICE_TYPE_DRM, + node.as_ptr(), + ptr::null_mut(), + 0, + ) < 0 + { + continue; + } + if av_hwdevice_ctx_create_derived( + &mut me.va_device, + AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI, + me.drm_device, + 0, + ) < 0 + { + continue; + } + me.drm_frames = mk_frames(me.drm_device, AVPixelFormat::AV_PIX_FMT_DRM_PRIME); + me.va_frames = mk_frames(me.va_device, AVPixelFormat::AV_PIX_FMT_VAAPI); + if me.drm_frames.is_null() || me.va_frames.is_null() { + continue; + } + // Le pilote decide. Une chaine illisible compte comme « pas iHD » : on + // garde alors le chemin direct, celui d'avant la copie. + if va_vendor(me.va_device).is_some_and(|v| needs_vpp_copy(&v)) + && me.build_vpp().is_none() + { + continue; + } + + me.ctx = avcodec_alloc_context3(enc); + if me.ctx.is_null() { + continue; + } + (*me.ctx).width = w; + (*me.ctx).height = h; + (*me.ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_VAAPI as i32; + (*me.ctx).time_base = AVRational { num: 1, den: fps }; + (*me.ctx).framerate = AVRational { num: fps, den: 1 }; + (*me.ctx).bit_rate = bit_rate; + // MEME INTERVALLE D'IMAGES CLES QUE LE CHEMIN SOFTWARE, et pour la meme + // raison : deux secondes est le compromis choisi pour un fichier de + // sortie, il n'a pas a dependre de l'encodeur qui se trouve disponible. + // + // Sans cette ligne le resultat est correct A 60 FPS ET NULLE PART + // AILLEURS : `h264_vaapi` a un defaut de 120 frames (mesure), qui vaut + // deux secondes a 60 fps par coincidence. A 30 fps ca donnerait quatre + // secondes, a 120 fps une seule. Le defaut de `libopenh264` est -1, ce + // qui est un autre probleme encore (une seule image cle pour tout le + // fichier) traite dans `try_open`. + (*me.ctx).gop_size = (fps * 2).max(1); + // MP4 veut SPS/PPS dans l'extradata, pas repetes devant chaque image + // cle. Le chemin software le pose depuis toujours (`try_open`) ; l'avoir + // oublie ici produisait un fichier qui se lit quand meme, parce que le + // muxer recupere ce qu'il trouve — mais un lecteur qui se fie a + // `codecpar` seul aurait de quoi echouer. + (*me.ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as i32; + // La matrice des frames que `send_dmabuf` pose, celle de `yuv.wgsl`. + tag_export_color(me.ctx, AVColorSpace::AVCOL_SPC_SMPTE170M); + // Avec la copie, les surfaces de SORTIE du VPP, pas celles du mapping : + // c'est elles que l'encodeur recoit. + let out_frames = if me.graph.is_null() { + me.va_frames + } else { + av_buffersink_get_hw_frames_ctx(me.vpp_out) + }; + if out_frames.is_null() { + continue; + } + (*me.ctx).hw_frames_ctx = av_buffer_ref(out_frames); + if avcodec_open2(me.ctx, enc, ptr::null_mut()) < 0 { + continue; + } + return Some(me); } - Some(me) + None } /// Construit `buffer (VAAPI, surface mappee) -> scale_vaapi -> buffersink`. @@ -1809,7 +1858,7 @@ mod vaapi_tests { gpu.device.poll(wgpu::Maintain::Wait); unsafe { - let Some(mut enc) = VaapiEncoder::open(w, h, 60, 4_000_000) else { + let Some(mut enc) = VaapiEncoder::open(w, h, 60, 4_000_000, gpu.render_node()) else { eprintln!("h264_vaapi indisponible — test saute"); return; }; @@ -1866,7 +1915,7 @@ mod vaapi_tests { gpu.device.poll(wgpu::Maintain::Wait); unsafe { - let Some(mut enc) = VaapiEncoder::open(w as i32, h as i32, 30, 8_000_000) else { + let Some(mut enc) = VaapiEncoder::open(w as i32, h as i32, 30, 8_000_000, gpu.render_node()) else { eprintln!("h264_vaapi indisponible — test saute"); return; }; @@ -1951,12 +2000,25 @@ mod vaapi_tests { } } + /// Le noeud du GPU de wgpu remplace le balayage ; l'echappatoire passe devant. + #[test] + fn le_noeud_du_gpu_remplace_le_balayage() { + let gpu = Some("/dev/dri/renderD129".to_owned()); + assert_eq!(render_node_candidates(None, gpu.clone()), ["/dev/dri/renderD129"]); + assert_eq!( + render_node_candidates(Some("/dev/dri/renderD130".to_owned()), gpu.clone()), + ["/dev/dri/renderD130", "/dev/dri/renderD129"] + ); + assert_eq!(render_node_candidates(Some(String::new()), gpu), ["/dev/dri/renderD129"]); + assert!(!render_node_candidates(None, None).is_empty()); + } + /// Sur une vraie machine : la chaine du pilote se lit, et elle choisit le /// chemin que l'encodeur a pris. Se saute sans VAAPI, comme les autres. #[test] fn le_pilote_choisit_le_chemin() { unsafe { - let Some(enc) = VaapiEncoder::open(640, 480, 30, 4_000_000) else { + let Some(enc) = VaapiEncoder::open(640, 480, 30, 4_000_000, None) else { eprintln!("h264_vaapi indisponible — test saute"); return; }; diff --git a/electron/native/pipewire-capture/src/dmabuf_import.rs b/electron/native/pipewire-capture/src/dmabuf_import.rs index 59e900a84..8f618b5fa 100644 --- a/electron/native/pipewire-capture/src/dmabuf_import.rs +++ b/electron/native/pipewire-capture/src/dmabuf_import.rs @@ -107,6 +107,14 @@ impl DmabufImporter { /// incoming dmabuf size (the whole stream); `out` is the recorded size — equal /// to `src` for a monitor, or the window's crop rectangle for a window (the /// graph then crops the source region down to it, on the GPU). + /// + /// Probes `/dev/dri/renderD128` first (unchanged default for working single-GPU + /// systems) and falls back across `/dev/dri/renderD129..renderD135` if DRM or + /// VAAPI setup fails. Because dmabuf mapping itself (`av_hwframe_map`) can only + /// be validated when frames arrive in [`Self::import`], hybrid-GPU hosts where + /// the default node opens VAAPI but drops imported compositor frames can set + /// `OPENSCREEN_LINUX_RENDER_NODE` as an optional workaround to select the + /// compositor's render node directly. pub fn new( src_width: i32, src_height: i32, @@ -119,21 +127,6 @@ impl DmabufImporter { // SAFETY: every pointer is checked before use and freed in Drop. unsafe { - let mut me = DmabufImporter { - src_width, - src_height, - out_width, - out_height, - sw_format, - va_device: ptr::null_mut(), - drm_device: ptr::null_mut(), - drm_frames: ptr::null_mut(), - va_map_frames: ptr::null_mut(), - graph: ptr::null_mut(), - buffersrc_ctx: ptr::null_mut(), - buffersink_ctx: ptr::null_mut(), - }; - // Order matters: create the DRM device on the render node FIRST, then // derive VAAPI from it. The reverse (DRM derived from VAAPI) returns // ENOSYS on radeonsi — VAAPI knows how to open on a DRM fd, but not the @@ -142,40 +135,80 @@ impl DmabufImporter { // /dev/dri/renderD128 is the default, but on hybrid graphics the // compositor may render on a different node — mapping a dmabuf from the // wrong GPU then fails on every frame, with `available()` still passing. - // Honour an override so such a machine can point at the right node - // without a rebuild; the single-GPU default is unchanged. - let node_path = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") - .unwrap_or_else(|_| "/dev/dri/renderD128".to_owned()); - let node = std::ffi::CString::new(node_path) - .map_err(|_| "OPENSCREEN_LINUX_RENDER_NODE has an interior NUL byte".to_owned())?; - let created = ff::av_hwdevice_ctx_create( - &mut me.drm_device, - ff::AV_HWDEVICE_TYPE_DRM, - node.as_ptr(), - ptr::null_mut(), - 0, - ); - if created < 0 { - return Err(format!("av_hwdevice_ctx_create(DRM): {}", ff::err_to_string(created))); + // Honour `OPENSCREEN_LINUX_RENDER_NODE` as an optional workaround when + // frames are dropped on the default node; single-GPU systems with a + // working `renderD128` do not need to set it. + let mut candidates = crate::encoder::render_node_candidates(); + if candidates.is_empty() { + candidates.push("/dev/dri/renderD128".to_owned()); } - let derived = ff::av_hwdevice_ctx_create_derived( - &mut me.va_device, - ff::AV_HWDEVICE_TYPE_VAAPI, - me.drm_device, - 0, - ); - if derived < 0 { - return Err(format!( - "av_hwdevice_ctx_create_derived(VAAPI): {}", - ff::err_to_string(derived) - )); - } + let mut last_err = String::from("no DRM/VAAPI render node available"); + for node_path in candidates { + let node = std::ffi::CString::new(node_path) + .map_err(|_| "OPENSCREEN_LINUX_RENDER_NODE has an interior NUL byte".to_owned())?; + let mut me = DmabufImporter { + src_width, + src_height, + out_width, + out_height, + sw_format, + va_device: ptr::null_mut(), + drm_device: ptr::null_mut(), + drm_frames: ptr::null_mut(), + va_map_frames: ptr::null_mut(), + graph: ptr::null_mut(), + buffersrc_ctx: ptr::null_mut(), + buffersink_ctx: ptr::null_mut(), + }; + + let created = ff::av_hwdevice_ctx_create( + &mut me.drm_device, + ff::AV_HWDEVICE_TYPE_DRM, + node.as_ptr(), + ptr::null_mut(), + 0, + ); + if created < 0 { + last_err = format!("av_hwdevice_ctx_create(DRM): {}", ff::err_to_string(created)); + continue; + } + + let derived = ff::av_hwdevice_ctx_create_derived( + &mut me.va_device, + ff::AV_HWDEVICE_TYPE_VAAPI, + me.drm_device, + 0, + ); + if derived < 0 { + last_err = format!( + "av_hwdevice_ctx_create_derived(VAAPI): {}", + ff::err_to_string(derived) + ); + continue; + } - me.drm_frames = me.alloc_frames(me.drm_device, ff::AV_PIX_FMT_DRM_PRIME)?; - me.va_map_frames = me.alloc_frames(me.va_device, ff::AV_PIX_FMT_VAAPI)?; - me.build_graph()?; - Ok(me) + me.drm_frames = match me.alloc_frames(me.drm_device, ff::AV_PIX_FMT_DRM_PRIME) { + Ok(frames) => frames, + Err(err) => { + last_err = err; + continue; + } + }; + me.va_map_frames = match me.alloc_frames(me.va_device, ff::AV_PIX_FMT_VAAPI) { + Ok(frames) => frames, + Err(err) => { + last_err = err; + continue; + } + }; + if let Err(err) = me.build_graph() { + last_err = err; + continue; + } + return Ok(me); + } + Err(last_err) } } diff --git a/electron/native/pipewire-capture/src/encoder.rs b/electron/native/pipewire-capture/src/encoder.rs index 9ffbcc9c5..2ec93f32f 100644 --- a/electron/native/pipewire-capture/src/encoder.rs +++ b/electron/native/pipewire-capture/src/encoder.rs @@ -158,6 +158,25 @@ pub fn prepare_environment() { add_flag("RADV_EXPERIMENTAL", "video_encode"); } +/// DRM render nodes to try for VA-API, in order: `OPENSCREEN_LINUX_RENDER_NODE` +/// when set, then every `/dev/dri/renderD128..=135` that exists. The dmabuf +/// importer and the encoder both walk this list, so they pick the same node. +pub(crate) fn render_node_candidates() -> Vec { + let mut nodes = Vec::new(); + if let Ok(explicit) = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") { + if !explicit.is_empty() { + nodes.push(explicit); + } + } + for idx in 128..=135 { + let path = format!("/dev/dri/renderD{idx}"); + if Path::new(&path).exists() && !nodes.contains(&path) { + nodes.push(path); + } + } + nodes +} + /// True when libva is new enough that libavutil's VA-API code will not abort. /// /// See the module header. This deliberately checks the SYMBOL rather than a @@ -576,43 +595,71 @@ impl VideoEncoder { return Ok(()); } - let created = ff::av_hwdevice_ctx_create( - &mut self.hw_device, - device_type, - ptr::null(), - ptr::null_mut(), - 0, - ); - if created < 0 { - return Err(format!( - "av_hwdevice_ctx_create: {}", - ff::err_to_string(created) - )); + // VA-API tries the nodes in the same order as the dmabuf importer, so + // both settle on the same GPU. Other backends use their own default. + let mut candidate_nodes: Vec> = Vec::new(); + if device_type == ff::AV_HWDEVICE_TYPE_VAAPI { + candidate_nodes.extend( + render_node_candidates() + .into_iter() + .filter_map(|node| CString::new(node).ok().map(Some)), + ); } - if device_type == ff::AV_HWDEVICE_TYPE_VULKAN && !vulkan_can_encode_h264(self.hw_device) { - return Err("the Vulkan device has no H.264 encode queue".to_owned()); + if candidate_nodes.is_empty() { + candidate_nodes.push(None); } - self.hw_frames = ff::av_hwframe_ctx_alloc(self.hw_device); - if self.hw_frames.is_null() { - return Err("av_hwframe_ctx_alloc returned null".to_owned()); + let mut last_err = String::from("no hardware device available"); + let mut ready = false; + for node in &candidate_nodes { + let device_ptr = node.as_ref().map_or(ptr::null(), |c| c.as_ptr()); + let created = ff::av_hwdevice_ctx_create( + &mut self.hw_device, + device_type, + device_ptr, + ptr::null_mut(), + 0, + ); + if created < 0 { + last_err = format!("av_hwdevice_ctx_create: {}", ff::err_to_string(created)); + continue; + } + if device_type == ff::AV_HWDEVICE_TYPE_VULKAN + && !vulkan_can_encode_h264(self.hw_device) + { + ff::av_buffer_unref(&mut self.hw_device); + return Err("the Vulkan device has no H.264 encode queue".to_owned()); + } + + self.hw_frames = ff::av_hwframe_ctx_alloc(self.hw_device); + if self.hw_frames.is_null() { + last_err = "av_hwframe_ctx_alloc returned null".to_owned(); + ff::av_buffer_unref(&mut self.hw_device); + continue; + } + let frames_ctx = (*self.hw_frames).data as *mut ff::AVHWFramesContext; + (*frames_ctx).format = self.backend.codec_pixel_format(); + (*frames_ctx).sw_format = self.backend.upload_format(); + (*frames_ctx).width = params.width; + (*frames_ctx).height = params.height; + // Enough surfaces that the encoder can hold a few references while we + // are filling the next one. Too small a pool stalls; too large just + // wastes VRAM. + (*frames_ctx).initial_pool_size = 8; + + let initialised = ff::av_hwframe_ctx_init(self.hw_frames); + if initialised < 0 { + last_err = format!("av_hwframe_ctx_init: {}", ff::err_to_string(initialised)); + ff::av_buffer_unref(&mut self.hw_frames); + ff::av_buffer_unref(&mut self.hw_device); + continue; + } + + ready = true; + break; } - let frames_ctx = (*self.hw_frames).data as *mut ff::AVHWFramesContext; - (*frames_ctx).format = self.backend.codec_pixel_format(); - (*frames_ctx).sw_format = self.backend.upload_format(); - (*frames_ctx).width = params.width; - (*frames_ctx).height = params.height; - // Enough surfaces that the encoder can hold a few references while we - // are filling the next one. Too small a pool stalls; too large just - // wastes VRAM. - (*frames_ctx).initial_pool_size = 8; - - let initialised = ff::av_hwframe_ctx_init(self.hw_frames); - if initialised < 0 { - return Err(format!( - "av_hwframe_ctx_init: {}", - ff::err_to_string(initialised) - )); + if !ready { + return Err(last_err); } (*self.codec_ctx).hw_frames_ctx = ff::av_buffer_ref(self.hw_frames);