ADR-0185: Hide volk / Vulkan-loader symbols from libvmaf's public ABI¶
- Status: Accepted
- Date: 2026-04-26
- Deciders: Lusoris, Claude (Anthropic)
- Tags: vulkan, build, fork-local, abi
Context¶
When libvmaf is built with -Denable_vulkan=enabled, the bundled volk Vulkan-loader (a static-archive function-pointer dispatcher, volk on GitHub) gets folded into libvmaf.so along with its vk* and volk* entry points. Those symbols have default visibility, so they end up in the shared library's public ABI:
$ nm -D libvmaf.so.3.0.0 | grep volk
T volkFinalize
T volkGetInstanceVersion
T volkGetLoadedDevice
T volkInitialize
[…etc, ~30 entry points]
Plus volk's vk* thunks. This is invisible to most consumers (libvmaf's pkg-config --libs libvmaf adds libvmaf.so and a small downstream binary doesn't care about a few extra exported symbols). It bites in one specific environment that matters to the fork's user base:
Static FFmpeg builds (BtbN-style cross-toolchain releases, Docker static images, and similar all-static link environments) that link both libvmaf-with-Vulkan and libvulkan.a/.so into the same final binary. Both contributors define every Vulkan entry point (vkGetInstanceProcAddr, vkCopyMemoryToImage, …); the GNU linker rejects with:
/opt/ffbuild/lib/libvulkan.a(loader.c.o):
multiple definition of `vkGetInstanceProcAddr';
volk.c.o (symbol from plugin): first defined here
— exactly the failure lawrence reported in chat on 2026-04-26 on his glibc-2.28 / BtbN-style static FFmpeg build.
The collision is fundamental to the volk-vs-libvulkan-loader choice: both are full Vulkan loaders. They can coexist only when one is internal (private to a single .so) and the other is the publicly-resolved one. libvmaf's volk usage is purely internal — the kernels in core/src/feature/vulkan/ call vkXxx only inside libvmaf — so volk's symbols never need to be exported.
Decision¶
Pass -Wl,--exclude-libs,ALL to libvmaf.so's link step. GNU-ld-specific flag that hides every symbol coming from a static archive on the link line; symbols compiled directly from libvmaf source TUs (vmaf_* and the kernel-internal vk* thunks via volk) keep their original visibility.
Net effect on libvmaf's public ABI:
- Before: 80+ exported
volk*andvk*symbols leaking from the bundled volk static archive. - After: 0 exported
volk*/vk*symbols. Only libvmaf's ownvmaf_*API surface remains public.
Linker compatibility:
- GNU ld (Linux, mingw-w64):
-Wl,--exclude-libs,ALLhonoured. - Apple ld (macOS): no equivalent flag with the same semantics. The Vulkan backend isn't shipped on macOS today (no
-Denable_vulkan=enabledCI lane), so we gate the flag off on Darwin to avoid a "unknown linker option" warning. - MSVC link.exe (Windows): symbols from static archives aren't auto-exported by default; the equivalent issue doesn't arise. Gate off the flag on Windows for the same reason.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
-Wl,--exclude-libs,ALL (chosen) | Two lines in meson.build; covers volk, VMA, and any future static-archive Vulkan dep without listing each one; widely-supported GNU-ld feature | Linux/mingw only | Best signal-to-cost ratio. macOS and Windows don't have the conflict in the first place. |
Per-archive -Wl,--exclude-libs,libvolk.a:libvk-mem-alloc.a | More explicit | List grows when we add static-archive deps; archive names depend on meson's wrap layout | Brittle — easier to use ALL and inherit broad protection |
GNU ld version script (--version-script vmaf.map) | Most precise; lets us declare the exact public API once | Requires maintaining an explicit list of every vmaf_* export — drift between header and .map; touches every PR that adds a public function | Too much maintenance for a single-bug fix |
Drop volk; link libvulkan.so dynamically with dlopen | Matches FFmpeg's model; eliminates the conflict at the source | Significant refactor of core/src/vulkan/common.c's init path; volk's whole-vulkan-API loader is replaced with hand-written dlsym for every entry we use | Too much code churn for a build-system fix |
Mark volk symbols __attribute__((visibility("hidden"))) at the C level | Per-symbol granularity | Requires editing volk's source (a vendored subproject); rebases when we bump volk; misses vk* symbols volk emits with its own attributes | Brittle — meson link_args is the right layer |
Consequences¶
- Positive: BtbN-style static FFmpeg builds that link both libvmaf and libvulkan in one binary now succeed. Closes the symbol-collision gap lawrence hit on glibc-2.28 / gcc. Reduces libvmaf.so's public ABI surface — anyone relying on the leaked
volk*/vk*symbols from libvmaf would need to switch to a real Vulkan loader, but no fork user ever meant for those to be public. - Negative: Linux/mingw-only fix; macOS users theoretically exposed to the same collision pattern would need an Apple-ld-specific equivalent (none ships today). Windows isn't affected by this class of issue.
- Neutral / follow-ups:
- Validation in CI: add an
nm -Dpost-build check on the Linux Vulkan lane that asserts novk*/volk*symbols leak. Cheap regression gate. - macOS coverage: if a future
enable_vulkanmacOS CI lane lands, port the equivalent (-Wl,-hidden-l<name>per archive, orMachO export list) to keep the symbol-hide invariant on Darwin.
References¶
- Source: lawrence's chat report 2026-04-26 (BtbN-style static FFmpeg build with
--enable-libvulkanfailing at link time). - Pre-fix verification:
nm -D libvmaf.so | grep volkon build-vulkan-test before this commit shows ~30 leakedvolk*symbols + the full Vulkan API. - Post-fix verification: same command shows zero leaked symbols;
vmaf_*API surface unchanged; smoke test + end-to-end psnr_vulkan on Arc A380 still match CPU scalar byte-for-byte (psnr_y = 34.760779on Netflix normal pair frame 0, identical to PR #125's reference value). - Relevant: GNU ld
--exclude-libsdocs.