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:
Generated
+9
@@ -2804,6 +2804,15 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pf-ffvk"
|
||||||
|
version = "0.8.2"
|
||||||
|
dependencies = [
|
||||||
|
"ash",
|
||||||
|
"bindgen",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pf-presenter"
|
name = "pf-presenter"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ members = [
|
|||||||
"crates/pf-client-core",
|
"crates/pf-client-core",
|
||||||
"crates/pf-presenter",
|
"crates/pf-presenter",
|
||||||
"crates/pf-console-ui",
|
"crates/pf-console-ui",
|
||||||
|
"crates/pf-ffvk",
|
||||||
"crates/pf-driver-proto",
|
"crates/pf-driver-proto",
|
||||||
"clients/probe",
|
"clients/probe",
|
||||||
"clients/linux",
|
"clients/linux",
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libgl-dev libegl-dev libgbm-dev \
|
libgl-dev libegl-dev libgbm-dev \
|
||||||
# punktfunk-client-linux (GTK4/libadwaita shell, SDL3 gamepads)
|
# punktfunk-client-linux (GTK4/libadwaita shell, SDL3 gamepads)
|
||||||
libgtk-4-dev libadwaita-1-dev libsdl3-dev \
|
libgtk-4-dev libadwaita-1-dev libsdl3-dev \
|
||||||
|
# pf-ffvk (bindgen over libavutil/hwcontext_vulkan.h needs <vulkan/vulkan.h>)
|
||||||
|
libvulkan-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# bun — builds the punktfunk-web console in deb.yml (which runs the web build in THIS image).
|
# bun — builds the punktfunk-web console in deb.yml (which runs the web build in THIS image).
|
||||||
|
|||||||
@@ -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