feat(ffvk): bindgen shim for FFmpeg's Vulkan hwcontext (pf-ffvk)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
@@ -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<PathBuf> = 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 <vulkan/vulkan.h>)",
|
||||
);
|
||||
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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
/* The one header ffmpeg-sys-next's bindgen list omits: FFmpeg's Vulkan hwcontext.
|
||||
* Pulls <vulkan/vulkan.h> (the vulkan-headers package) transitively. */
|
||||
#include <libavutil/hwcontext_vulkan.h>
|
||||
Reference in New Issue
Block a user