diff --git a/electron/native/pipewire-capture/build.rs b/electron/native/pipewire-capture/build.rs index 72b3121d1..7ae76bb10 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 0001bcde5..d277072c2 100644 --- a/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c +++ b/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c @@ -1,5 +1,170 @@ +#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 + +typedef void *(*dlsym_fn)(void *, const char *); +typedef VAStatus (*vaMapBuffer_fn)(VADisplay, VABufferID, void **); + +/* Both pointers are read and written from any thread that calls dlsym or + * vaMapBuffer2, so every access is atomic. Two threads racing on the first + * call resolve the same value; the atomics only make that race defined. */ +static dlsym_fn real_dlsym_fn = NULL; +static vaMapBuffer_fn real_vaMapBuffer_fn = 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 dlsym_fn resolve_real_dlsym(void) +{ + dlsym_fn fn = __atomic_load_n(&real_dlsym_fn, __ATOMIC_ACQUIRE); + if (!fn) { + fn = (dlsym_fn)dlvsym(RTLD_NEXT, "dlsym", OSC_DLSYM_GLIBC_VER); + if (!fn) { + fn = (dlsym_fn)dlvsym(RTLD_DEFAULT, "dlsym", OSC_DLSYM_GLIBC_VER); + } + if (!fn) { + fn = (dlsym_fn)dlvsym(RTLD_NEXT, "dlsym", "GLIBC_2.34"); + } + if (!fn) { + fn = (dlsym_fn)dlvsym(RTLD_DEFAULT, "dlsym", "GLIBC_2.34"); + } + if (fn) { + __atomic_store_n(&real_dlsym_fn, fn, __ATOMIC_RELEASE); + } + } + return fn; +} + +/** + * 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; + vaMapBuffer_fn map = __atomic_load_n(&real_vaMapBuffer_fn, __ATOMIC_ACQUIRE); + if (!map) { + dlsym_fn real = 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) { + map = (vaMapBuffer_fn)real(h, "vaMapBuffer"); + if (map) { + __atomic_store_n(&real_vaMapBuffer_fn, map, __ATOMIC_RELEASE); + } + } + } + if (!map) { + return VA_STATUS_ERROR_UNKNOWN; + } + return map(dpy, buf_id, pbuf); +} + +/* The vaMapBuffer2 fallback itself: a real handle asked for vaMapBuffer2. */ +static void *osc_dlsym_va(void *handle, const char *name) +{ + dlsym_fn real = resolve_real_dlsym(); + void *sym = real ? real(handle, name) : NULL; + if (!sym) { + void *map1 = real ? real(handle, "vaMapBuffer") : NULL; + if (map1) { + __atomic_store_n(&real_vaMapBuffer_fn, (vaMapBuffer_fn)map1, __ATOMIC_RELEASE); + return (void *)&vaMapBuffer2; + } + } + return sym; +} + +static void *osc_dlsym_null(void *handle, const char *name) +{ + (void)handle; + (void)name; + return NULL; +} + +/** + * Picks where the exported `dlsym` below jumps: the fallback for a vaMapBuffer2 + * lookup on a real handle (libavutil's implib trampoline), glibc's dlsym for + * everything else. + */ +__attribute__((visibility("hidden"), used)) void *osc_dlsym_target(void *handle, const char *name) +{ + if (handle != RTLD_NEXT && handle != RTLD_DEFAULT && name != NULL && + strcmp(name, "vaMapBuffer2") == 0) { + return (void *)&osc_dlsym_va; + } + dlsym_fn real = resolve_real_dlsym(); + return real ? (void *)real : (void *)&osc_dlsym_null; +} + +/* Interposes `dlsym` process-wide, so libavutil's vaMapBuffer2 lookup reaches + * the fallback when the loaded libva.so.2 predates 2.22. + * + * WHY ASSEMBLY. glibc resolves RTLD_NEXT (and the caller's scope for + * RTLD_DEFAULT) from dlsym's return address. A C wrapper that calls the real + * dlsym makes every lookup look like it comes from this helper, so an + * LD_PRELOAD wrapper calling dlsym(RTLD_NEXT, ...) could get its own address + * back and recurse. This entry asks osc_dlsym_target where to go, restores the + * arguments, and JUMPS there: the real dlsym sees the original caller's return + * address, so every other lookup behaves exactly as without the shim. */ +#if defined(__x86_64__) +__asm__(".text\n" + ".globl dlsym\n" + ".type dlsym, @function\n" + "dlsym:\n" +#if defined(__CET__) + " endbr64\n" +#endif + " push %rdi\n" + " push %rsi\n" + " sub $8, %rsp\n" + " call osc_dlsym_target\n" + " add $8, %rsp\n" + " pop %rsi\n" + " pop %rdi\n" + " jmp *%rax\n" + ".size dlsym, .-dlsym\n"); +#elif defined(__aarch64__) +__asm__(".text\n" + ".globl dlsym\n" + ".type dlsym, %function\n" + "dlsym:\n" + " stp x29, x30, [sp, #-32]!\n" + " mov x29, sp\n" + " stp x0, x1, [sp, #16]\n" + " bl osc_dlsym_target\n" + " mov x16, x0\n" + " ldp x0, x1, [sp, #16]\n" + " ldp x29, x30, [sp], #32\n" + " br x16\n" + ".size dlsym, .-dlsym\n"); +#endif + /* 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/encoder.rs b/electron/native/pipewire-capture/src/encoder.rs index 9ffbcc9c5..15f19945f 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() } }