diff --git a/crates/compositor/src/pipeline_linux.rs b/crates/compositor/src/pipeline_linux.rs index edc9ea623d..2a7d843d7f 100644 --- a/crates/compositor/src/pipeline_linux.rs +++ b/crates/compositor/src/pipeline_linux.rs @@ -1330,39 +1330,29 @@ impl VaapiEncoder { /// manque : l'appelant retombe alors sur l'encodeur software. pub unsafe fn open(w: i32, h: i32, fps: i32, bit_rate: i64) -> 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; + // cotes d'un meme device. Sur les portables hybrides, la carte avec VAAPI + // peut etre sur renderD128 ou renderD129 : on honore OPENSCREEN_LINUX_RENDER_NODE + // puis on sonde les noeuds DRM disponibles. + let mut candidates: Vec = Vec::new(); + if let Ok(explicit) = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") { + if !explicit.is_empty() { + candidates.push(explicit); + } } - if av_hwdevice_ctx_create_derived( - &mut me.va_device, - AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI, - me.drm_device, - 0, - ) < 0 - { + for idx in 128..=135 { + let p = format!("/dev/dri/renderD{idx}"); + if Path::new(&p).exists() && !candidates.contains(&p) { + candidates.push(p); + } + } + if candidates.is_empty() { + candidates.push("/dev/dri/renderD128".to_owned()); + } + 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 +1372,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`. diff --git a/electron/native/pipewire-capture/build.rs b/electron/native/pipewire-capture/build.rs index 72b3121d17..7ae76bb10d 100644 --- a/electron/native/pipewire-capture/build.rs +++ b/electron/native/pipewire-capture/build.rs @@ -134,6 +134,7 @@ fn ffmpeg_dir(root: &Path) -> PathBuf { root.join("../../../crates/thirdparty/ffmpeg-linux64-lgpl-shared") } +/// Configures Cargo link directives and bindgen for the vendored FFmpeg libraries. fn link_ffmpeg(dir: &Path) { let include = dir.join("include"); let lib = dir.join("lib"); @@ -171,6 +172,7 @@ fn link_ffmpeg(dir: &Path) { // of the repo. `--disable-new-dtags` is what makes these RUNPATH entries // apply to the transitive ffmpeg libs too; with the default DT_RUNPATH they // would not. + println!("cargo:rustc-link-arg=-Wl,--export-dynamic"); println!("cargo:rustc-link-arg=-Wl,--disable-new-dtags"); println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN/helper-ffmpeg"); println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib.display()); diff --git a/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c b/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c index 0001bcde58..206add8d3d 100644 --- a/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c +++ b/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c @@ -1,5 +1,99 @@ +#define _GNU_SOURCE +#include +#include #include +/* Compatibility shim for libva < 2.22 (e.g. Ubuntu 24.04 LTS with libva 2.20). + * The vendored FFmpeg 8.1 build links libva via implib-gen and resolves + * vaMapBuffer2 at runtime via dlsym(). On libva < 2.22, vaMapBuffer2 is absent + * and the implib trampoline aborts the process. When dlsym(handle, "vaMapBuffer2") + * returns NULL on a handle that exports vaMapBuffer, forward vaMapBuffer2 to + * vaMapBuffer(dpy, buf_id, pbuf). */ +typedef int VAStatus; +typedef void *VADisplay; +typedef unsigned int VABufferID; + +#ifndef VA_STATUS_ERROR_UNKNOWN +#define VA_STATUS_ERROR_UNKNOWN 0xFFFFFFFF +#endif + +static void *(*real_dlsym_fn)(void *, const char *) = NULL; +static VAStatus (*real_vaMapBuffer_fn)(VADisplay, VABufferID, void **) = NULL; + +#if defined(__aarch64__) +#define OSC_DLSYM_GLIBC_VER "GLIBC_2.17" +#else +#define OSC_DLSYM_GLIBC_VER "GLIBC_2.2.5" +#endif + +/** + * Resolves the underlying glibc `dlsym` implementation using the architecture's + * base symbol version (`GLIBC_2.17` on aarch64, `GLIBC_2.2.5` on x86_64) with a + * `GLIBC_2.34` fallback so `real_dlsym_fn` is never left NULL when available. + */ +static void resolve_real_dlsym(void) +{ + if (!real_dlsym_fn) { + real_dlsym_fn = (void *(*)(void *, const char *))dlvsym(RTLD_NEXT, "dlsym", OSC_DLSYM_GLIBC_VER); + if (!real_dlsym_fn) { + real_dlsym_fn = (void *(*)(void *, const char *))dlvsym(RTLD_DEFAULT, "dlsym", OSC_DLSYM_GLIBC_VER); + } + if (!real_dlsym_fn) { + real_dlsym_fn = (void *(*)(void *, const char *))dlvsym(RTLD_NEXT, "dlsym", "GLIBC_2.34"); + } + if (!real_dlsym_fn) { + real_dlsym_fn = (void *(*)(void *, const char *))dlvsym(RTLD_DEFAULT, "dlsym", "GLIBC_2.34"); + } + } +} + +/** + * Compatibility entry point for `vaMapBuffer2` (added in libva 2.22) that + * forwards buffer mapping calls to `vaMapBuffer` on older libva runtimes. + */ +VAStatus vaMapBuffer2(VADisplay dpy, VABufferID buf_id, void **pbuf, unsigned int flags) +{ + (void)flags; + if (!real_vaMapBuffer_fn) { + resolve_real_dlsym(); + void *h = dlopen("libva.so.2", RTLD_LAZY | RTLD_NOLOAD); + if (!h) { + h = dlopen("libva.so.2", RTLD_LAZY); + } + if (h && real_dlsym_fn) { + real_vaMapBuffer_fn = + (VAStatus (*)(VADisplay, VABufferID, void **))real_dlsym_fn(h, "vaMapBuffer"); + } + } + if (!real_vaMapBuffer_fn) { + return VA_STATUS_ERROR_UNKNOWN; + } + return real_vaMapBuffer_fn(dpy, buf_id, pbuf); +} + +/** + * Interposes `dlsym` so lookups for `vaMapBuffer2` fall back to our + * `vaMapBuffer`-backed shim when the loaded `libva.so.2` predates 2.22. + * + * NOTE: Overriding dlsym process-wide causes any internal RTLD_NEXT lookups + * in loaded shared objects to resolve relative to this helper binary rather + * than the caller library. In openscreen-pipewire-helper, this interposition + * specifically handles libavutil's runtime resolution of vaMapBuffer2. + */ +void *dlsym(void *handle, const char *name) +{ + resolve_real_dlsym(); + void *sym = real_dlsym_fn ? real_dlsym_fn(handle, name) : NULL; + if (!sym && strcmp(name, "vaMapBuffer2") == 0) { + void *map1 = real_dlsym_fn ? real_dlsym_fn(handle, "vaMapBuffer") : NULL; + if (map1) { + real_vaMapBuffer_fn = (VAStatus (*)(VADisplay, VABufferID, void **))map1; + return (void *)&vaMapBuffer2; + } + } + return sym; +} + /* FFmpeg 8 exposes AVFormatContext as opaque to bindgen on some clang builds. * Keep these field reads in C, compiled against the exact headers being linked. */ AVIOContext **osc_avformat_pb(AVFormatContext *context) diff --git a/electron/native/pipewire-capture/src/dmabuf_import.rs b/electron/native/pipewire-capture/src/dmabuf_import.rs index 59e900a84a..e1cdf23476 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,91 @@ 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: Vec = Vec::new(); + if let Ok(explicit) = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") { + if !explicit.is_empty() { + candidates.push(explicit); + } } - - 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) - )); + for idx in 128..=135 { + let p = format!("/dev/dri/renderD{idx}"); + if std::path::Path::new(&p).exists() && !candidates.contains(&p) { + candidates.push(p); + } + } + if candidates.is_empty() { + candidates.push("/dev/dri/renderD128".to_owned()); } - 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) + 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 = 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 5f0a2fe49e..9afa29483a 100644 --- a/electron/native/pipewire-capture/src/encoder.rs +++ b/electron/native/pipewire-capture/src/encoder.rs @@ -165,15 +165,21 @@ pub fn prepare_environment() { /// against, and what matters is what is loadable here and now. fn vaapi_is_safe_to_probe() -> bool { // SAFETY: dlopen/dlsym with a literal name; every pointer is checked before - // use and the handle is closed on both paths. + // use and the handle is closed on both paths. csrc/ffmpeg_accessors.c + // provides a vaMapBuffer2 -> vaMapBuffer compatibility bridge for libva < 2.22. unsafe { let handle = dlopen(c"libva.so.2".as_ptr(), RTLD_LAZY | RTLD_LOCAL); if handle.is_null() { return false; } let symbol = dlsym(handle, c"vaMapBuffer2".as_ptr()); + let fallback = if symbol.is_null() { + dlsym(handle, c"vaMapBuffer".as_ptr()) + } else { + symbol + }; dlclose(handle); - !symbol.is_null() + !fallback.is_null() } } @@ -484,40 +490,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) - )); + let mut candidate_nodes: Vec> = Vec::new(); + if device_type == ff::AV_HWDEVICE_TYPE_VAAPI { + if let Ok(explicit) = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") { + if let Ok(cstr) = CString::new(explicit) { + candidate_nodes.push(Some(cstr)); + } + } + candidate_nodes.push(None); + for idx in 128..=135 { + let path = format!("/dev/dri/renderD{idx}"); + if Path::new(&path).exists() { + if let Ok(cstr) = CString::new(path) { + candidate_nodes.push(Some(cstr)); + } + } + } + } else { + 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; + } + + 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);