From 58ccd530fce5f293ce65d3a97d3ebeebed43088d Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Tue, 7 Jul 2026 21:21:37 +0200 Subject: [PATCH] feat(ffvk): bindgen shim for FFmpeg's Vulkan hwcontext (pf-ffvk) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ffmpeg-sys-next binds a curated header list that omits every hwcontext_*.h, so AVVulkanDeviceContext/AVVulkanFramesContext/AVVkFrame — the surface that lets FFmpeg's Vulkan Video decoder run on the presenter's own VkDevice and hand the decoded VkImages back — had no Rust bindings at all. pf-ffvk generates them at build time from the SYSTEM headers, which is the only ABI-safe option: the struct layout depends on FF_API_* deprecation gates resolved in libavutil/version.h (confirmed: FFmpeg 8.1 still carries the FIXED_QUEUES fields). Ships ash<->vulkan.h handle conversions; opaque AVHW*/AVFrame types keep ffmpeg-sys-next the owner of the core surface (pointer casts across). Needs vulkan-headers (Arch) / libvulkan-dev (CI, added); PF_FFVK_VULKAN_INCLUDE overrides for cross builds. Verified: bindgen layout tests pass, av_vk_frame_alloc links and zero-initializes. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 9 ++++ Cargo.toml | 1 + ci/rust-ci.Dockerfile | 2 + crates/pf-ffvk/Cargo.toml | 22 ++++++++++ crates/pf-ffvk/build.rs | 75 ++++++++++++++++++++++++++++++++ crates/pf-ffvk/src/lib.rs | 91 +++++++++++++++++++++++++++++++++++++++ crates/pf-ffvk/wrapper.h | 3 ++ 7 files changed, 203 insertions(+) create mode 100644 crates/pf-ffvk/Cargo.toml create mode 100644 crates/pf-ffvk/build.rs create mode 100644 crates/pf-ffvk/src/lib.rs create mode 100644 crates/pf-ffvk/wrapper.h diff --git a/Cargo.lock b/Cargo.lock index c70f4575..1344a6bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2804,6 +2804,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "pf-ffvk" +version = "0.8.2" +dependencies = [ + "ash", + "bindgen", + "pkg-config", +] + [[package]] name = "pf-presenter" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index f90c7b7b..3e237d39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/pf-client-core", "crates/pf-presenter", "crates/pf-console-ui", + "crates/pf-ffvk", "crates/pf-driver-proto", "clients/probe", "clients/linux", diff --git a/ci/rust-ci.Dockerfile b/ci/rust-ci.Dockerfile index 711a3b9b..acde7672 100644 --- a/ci/rust-ci.Dockerfile +++ b/ci/rust-ci.Dockerfile @@ -22,6 +22,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libgl-dev libegl-dev libgbm-dev \ # punktfunk-client-linux (GTK4/libadwaita shell, SDL3 gamepads) libgtk-4-dev libadwaita-1-dev libsdl3-dev \ + # pf-ffvk (bindgen over libavutil/hwcontext_vulkan.h needs ) + libvulkan-dev \ && rm -rf /var/lib/apt/lists/* # bun — builds the punktfunk-web console in deb.yml (which runs the web build in THIS image). diff --git a/crates/pf-ffvk/Cargo.toml b/crates/pf-ffvk/Cargo.toml new file mode 100644 index 00000000..30daf141 --- /dev/null +++ b/crates/pf-ffvk/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "pf-ffvk" +description = "Bindgen shim for FFmpeg's Vulkan hwcontext (libavutil/hwcontext_vulkan.h) — the AVVulkanDeviceContext/AVVkFrame surface ffmpeg-sys-next doesn't bind; enables Vulkan Video decode straight onto the presenter's device" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +# The bindings are generated at build time from the SYSTEM headers (libavutil + +# vulkan-headers), so they are ABI-exact for the installed FFmpeg — including the +# FF_API_VULKAN_* deprecation gates that change AVVulkanDeviceContext's layout between +# FFmpeg builds. This is deliberately not hand-transcribed. + +[target.'cfg(target_os = "linux")'.dependencies] +ash = { version = "0.38", features = ["loaded"] } + +[build-dependencies] +# Same bindgen configuration as ffmpeg-sys-next (runtime = dlopen libclang). +bindgen = { version = "0.72", features = ["runtime"], default-features = false } +pkg-config = "0.3" diff --git a/crates/pf-ffvk/build.rs b/crates/pf-ffvk/build.rs new file mode 100644 index 00000000..73d2ec61 --- /dev/null +++ b/crates/pf-ffvk/build.rs @@ -0,0 +1,75 @@ +//! Generate bindings for `libavutil/hwcontext_vulkan.h` against the SYSTEM headers. +//! +//! ffmpeg-sys-next binds a curated header list that omits every hwcontext_*.h; the +//! Vulkan hwcontext structs (`AVVulkanDeviceContext`, `AVVkFrame`) are what let us run +//! FFmpeg's Vulkan Video decoder on the presenter's own VkDevice and read the decoded +//! VkImages back. Their layout depends on compile-time FF_API_* deprecation gates in +//! libavutil/version.h, so bindgen over the installed header is the only ABI-safe +//! source of truth — hand transcription would silently skew on the next FFmpeg bump. +//! +//! Non-Linux targets get an empty file: the workspace builds on macOS (clients/apple is +//! the client there), and this shim is Linux-client plumbing only. + +use std::env; +use std::path::PathBuf; + +fn main() { + println!("cargo:rerun-if-changed=wrapper.h"); + let out = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bindings.rs"); + + if env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("linux") { + std::fs::write(&out, "// pf-ffvk: Linux-only, empty on this target\n").unwrap(); + return; + } + + // Include paths from pkg-config (libavutil for the hwcontext header; the Vulkan + // headers usually live in /usr/include, but honor a registered vulkan.pc too). + // PF_FFVK_VULKAN_INCLUDE prepends an explicit Vulkan-Headers include dir — for + // cross builds and boxes without the system package. + println!("cargo:rerun-if-env-changed=PF_FFVK_VULKAN_INCLUDE"); + let mut includes: Vec = Vec::new(); + if let Ok(dir) = env::var("PF_FFVK_VULKAN_INCLUDE") { + includes.push(PathBuf::from(dir)); + } + let avutil = pkg_config::Config::new() + .cargo_metadata(false) + .probe("libavutil") + .expect("pkg-config: libavutil not found — install the FFmpeg dev package"); + includes.extend(avutil.include_paths); + if let Ok(vk) = pkg_config::Config::new() + .cargo_metadata(false) + .probe("vulkan") + { + includes.extend(vk.include_paths); + } + + let mut builder = bindgen::Builder::default() + .header("wrapper.h") + // The whole point of this crate: the Vulkan hwcontext surface… + .allowlist_type("AVVulkan.*") + .allowlist_type("AVVkFrame.*") + .allowlist_function("av_vk_frame_alloc") + .allowlist_function("av_vkfmt_from_pixfmt") + // …plus nothing else of FFmpeg: the core types these structs reference only + // ever appear behind pointers here, so keep them opaque instead of duplicating + // ffmpeg-sys-next's definitions (callers cast pointers between the crates). + .opaque_type("AVHWDeviceContext") + .opaque_type("AVHWFramesContext") + .opaque_type("AVBufferRef") + .opaque_type("AVFrame") + .derive_debug(false) + .layout_tests(true); + for dir in &includes { + builder = builder.clang_arg(format!("-I{}", dir.display())); + } + let bindings = builder.generate().expect( + "bindgen over libavutil/hwcontext_vulkan.h failed — is `vulkan-headers` installed? \ + (the header includes )", + ); + bindings.write_to_file(&out).unwrap(); + + // The av_vk_* symbols live in libavutil, which ffmpeg-sys-next already links into + // every consumer of this crate; no extra link flags needed. Emitting the lib anyway + // keeps `cargo test -p pf-ffvk` linking standalone. + println!("cargo:rustc-link-lib=avutil"); +} diff --git a/crates/pf-ffvk/src/lib.rs b/crates/pf-ffvk/src/lib.rs new file mode 100644 index 00000000..16014513 --- /dev/null +++ b/crates/pf-ffvk/src/lib.rs @@ -0,0 +1,91 @@ +//! FFmpeg's Vulkan hwcontext surface (`AVVulkanDeviceContext`, `AVVulkanFramesContext`, +//! `AVVkFrame`), bindgen-generated from the system headers at build time — see build.rs +//! for why this must not be hand-transcribed. +//! +//! The raw bindings use vulkan.h's own handle types (pointers on 64-bit). The [`ash`] +//! conversion helpers below cross between them and ash's u64-newtype handles; both sides +//! are the same underlying Vulkan object handles, so the casts are value-preserving. + +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(clippy::missing_safety_doc)] +// bindgen's layout tests deref-null-pointer by design; silence the lints they trip. +#![allow(deref_nullptr)] +#![allow(unnecessary_transmutes)] + +#[cfg(target_os = "linux")] +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +/// Conversions between the generated vulkan.h handle types and ash's. +#[cfg(target_os = "linux")] +pub mod ashx { + use super::*; + use ash::vk::Handle as _; + + /// vulkan.h non-dispatchable handles are `*mut T` on 64-bit; ash's are `u64` + /// newtypes. Same bits either way. + pub fn image(h: VkImage) -> ash::vk::Image { + ash::vk::Image::from_raw(h as u64) + } + + pub fn semaphore(h: VkSemaphore) -> ash::vk::Semaphore { + ash::vk::Semaphore::from_raw(h as u64) + } + + pub fn image_layout(l: VkImageLayout) -> ash::vk::ImageLayout { + ash::vk::ImageLayout::from_raw(l as i32) + } + + // --- ash → vulkan.h (filling AVVulkanDeviceContext) --------------------------------- + + pub fn to_instance(h: ash::vk::Instance) -> VkInstance { + h.as_raw() as VkInstance + } + + pub fn to_physical_device(h: ash::vk::PhysicalDevice) -> VkPhysicalDevice { + h.as_raw() as VkPhysicalDevice + } + + pub fn to_device(h: ash::vk::Device) -> VkDevice { + h.as_raw() as VkDevice + } + + /// ash's loader-level `vkGetInstanceProcAddr` as the header's PFN type. Both are the + /// same C ABI function pointer (`extern "system"` == `extern "C"` on the platforms + /// this crate builds for). + pub fn to_get_proc_addr( + f: unsafe extern "system" fn( + ash::vk::Instance, + *const std::ffi::c_char, + ) -> ash::vk::PFN_vkVoidFunction, + ) -> PFN_vkGetInstanceProcAddr { + unsafe { std::mem::transmute(f) } + } +} + +#[cfg(all(test, target_os = "linux"))] +mod tests { + use super::*; + + /// The allocator runs (links against the system libavutil) and the struct is + /// readable at the offsets bindgen computed — sem_value zero-initialized. + #[test] + fn vk_frame_alloc_links_and_zeroes() { + unsafe { + let f = av_vk_frame_alloc(); + assert!(!f.is_null(), "av_vk_frame_alloc returned NULL"); + assert_eq!((*f).sem_value[0], 0); + assert_eq!((*f).queue_family[0], 0); + // Leak the one test frame rather than binding av_free here. + } + } + + /// AV_NUM_DATA_POINTERS-sized arrays came through with the right length. + #[test] + fn frame_arrays_are_av_num_data_pointers() { + let f: AVVkFrame = unsafe { std::mem::zeroed() }; + assert_eq!(f.img.len(), 8); + assert_eq!(f.sem_value.len(), 8); + } +} diff --git a/crates/pf-ffvk/wrapper.h b/crates/pf-ffvk/wrapper.h new file mode 100644 index 00000000..9a8a286f --- /dev/null +++ b/crates/pf-ffvk/wrapper.h @@ -0,0 +1,3 @@ +/* The one header ffmpeg-sys-next's bindgen list omits: FFmpeg's Vulkan hwcontext. + * Pulls (the vulkan-headers package) transitively. */ +#include