Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,20 @@ exclude = ["flake.nix", "flake.lock", "justfile", "deny.toml", "rust-toolchain.t

# bindgen reads the vendored libva headers checked into `libva/` (refreshed by
# `just vendor`, pinned to a known VA-API version so a system libva bump can't
# drift the bindings); libva itself is linked on Linux via pkg-config (VA-API's
# ABI is stable, so a newer system libva links fine against the pinned bindings).
# The rlib build needs only libclang + the vendored headers, so it compiles on any
# OS (e.g. macOS) for development; the libva link only happens on Linux.
# drift the bindings). libva is not linked: bindgen's dynamic-loading mode emits a
# `Va` struct that dlopen's libva.so at runtime (see bindgen_gen.rs), so the build
# needs only libclang + the vendored headers, the binary carries no NEEDED libva
# (a libva-less host loads and lets the caller fall back), and the crate builds on
# any OS (macOS, docs.rs) without libva installed.

[dependencies]
anyhow = "1"
thiserror = "1"
bitflags = "2.5"
log = { version = "0", features = ["release_max_level_debug"] }
# Runtime dlopen of libva; the bindgen `Va` struct is built on libloading::Library.
libloading = "0.8"

[build-dependencies]
bindgen = "0.70.1"
pkg-config = "0.3.31"
regex = "1.11.1"
7 changes: 7 additions & 0 deletions bindgen_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@ pub fn vaapi_gen_builder(builder: bindgen::Builder) -> bindgen::Builder {
.allowlist_var("VA.*")
.allowlist_function("va.*")
.allowlist_type(ALLOW_LIST_TYPE)
// Emit a `Va` struct of libloading-resolved function pointers instead of
// link-time `extern "C"` decls, so libva is dlopen'd at runtime (no
// build-time or load-time libva dependency). `require_all(false)` lets a
// system libva older than the vendored headers still load: a missing
// symbol only errors if actually called, not at `Va::new`.
.dynamic_library_name("Va")
.dynamic_link_require_all(false)
}
20 changes: 5 additions & 15 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,21 +122,11 @@ fn main() {
println!("cargo::rustc-cfg=libva_1_10_or_higher");
}

// Bindings are generated from the vendored headers (pinned, drift-proof), but
// the va* symbols still have to resolve, so link the system libva on Linux (the
// only platform with VA-API). VA-API's ABI is stable, so a system libva newer
// than the vendored headers links fine. On other hosts (e.g. macOS) this crate
// only ever builds as an rlib, which leaves the symbols for the final binary, so
// skip linking and keep it compilable for development.
if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") {
pkg_config::Config::new()
.probe("libva")
.expect("libva not found via pkg-config (install libva / libva-dev)");
pkg_config::Config::new()
.probe("libva-drm")
.expect("libva-drm not found via pkg-config (install libva / libva-dev)");
}

// No link step: bindgen emits a `Va` struct that dlopen's libva at runtime
// (see bindgen_gen.rs and src/bindings.rs). So the build needs only libclang
// plus the vendored headers, and the binary carries no NEEDED libva, matching
// how the NVENC backend loads its driver. This is what lets docs.rs (and any
// libva-less Linux host) build the crate.
let bindings = vaapi_gen_builder(bindgen::builder())
.header(WRAPPER_PATH)
.clang_arg(format!("-I{}", va_h_path.display()))
Expand Down
27 changes: 27 additions & 0 deletions src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,30 @@
#![allow(rustdoc::all)]

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

use std::sync::OnceLock;

/// The dlopen'd libva symbol table, resolved once on first use. The result is
/// cached (success or failure) so a missing libva is probed at most once.
static VA: OnceLock<Result<Va, String>> = OnceLock::new();

/// Loads libva (via `libva-drm.so.2`) on first call and returns the symbol table.
///
/// `libva-drm.so.2` is linked against `libva.so.2`, so a single dlopen exposes
/// both `vaGetDisplayDRM` and the core `va*` symbols (dlsym on the handle searches
/// its dependencies). Returns `Err` if the library is absent, so a caller on a
/// libva-less host can fall back to another encoder instead of aborting.
pub(crate) fn load() -> Result<&'static Va, &'static str> {
VA.get_or_init(|| unsafe { Va::new("libva-drm.so.2") }.map_err(|e| e.to_string()))
.as_ref()
.map_err(String::as_str)
}

/// Accessor for the loaded libva symbols.
///
/// Panics if libva has not loaded. Every entry point that reaches libva goes
/// through a [`crate::Display`], whose constructor calls [`load`] and bails out on
/// failure, so by the time any other `va*` call runs the library is present.
pub(crate) fn va() -> &'static Va {
load().expect("libva not loaded; open a Display first")
}
11 changes: 6 additions & 5 deletions src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ impl Buffer {
// be correct, as `ptr` is just a cast to `*c_void` from a Rust struct, and `size` is
// computed from `std::mem::size_of_val`.
va_check(unsafe {
bindings::vaCreateBuffer(
bindings::va().vaCreateBuffer(
context.display().handle(),
context.id(),
type_.inner(),
Expand All @@ -333,7 +333,7 @@ impl Drop for Buffer {
fn drop(&mut self) {
// Safe because `self` represents a valid buffer, created with
// vaCreateBuffers.
let status = va_check(unsafe { bindings::vaDestroyBuffer(self.context.display().handle(), self.id) });
let status = va_check(unsafe { bindings::va().vaDestroyBuffer(self.context.display().handle(), self.id) });

if status.is_err() {
error!("vaDestroyBuffer failed: {}", status.unwrap_err());
Expand Down Expand Up @@ -565,7 +565,7 @@ impl<'p> MappedCodedBuffer<'p> {
let mut addr = std::ptr::null_mut();
let mut segments = Vec::new();

va_check(unsafe { bindings::vaMapBuffer(buffer.0.context.display().handle(), buffer.id(), &mut addr) })?;
va_check(unsafe { bindings::va().vaMapBuffer(buffer.0.context.display().handle(), buffer.id(), &mut addr) })?;

while !addr.is_null() {
let segment: &bindings::VACodedBufferSegment = unsafe { &*(addr as *const bindings::VACodedBufferSegment) };
Expand Down Expand Up @@ -600,8 +600,9 @@ impl<'p> MappedCodedBuffer<'p> {

impl<'p> Drop for MappedCodedBuffer<'p> {
fn drop(&mut self) {
let status =
va_check(unsafe { bindings::vaUnmapBuffer(self.buffer.0.context.display().handle(), self.buffer.id()) });
let status = va_check(unsafe {
bindings::va().vaUnmapBuffer(self.buffer.0.context.display().handle(), self.buffer.id())
});

if status.is_err() {
error!("vaUnmapBuffer failed: {}", status.unwrap_err());
Expand Down
8 changes: 4 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ impl Config {
// The `attrs` vector is also properly initialized and its actual size is passed to
// `vaCreateConfig`, so it is impossible to write past the end of its storage by mistake.
va_check(unsafe {
bindings::vaCreateConfig(
bindings::va().vaCreateConfig(
display.handle(),
profile,
entrypoint,
Expand Down Expand Up @@ -73,7 +73,7 @@ impl Config {
// call to `vaQuerySurfaceAttributes`.
let attrs_len: std::os::raw::c_uint = 0;
va_check(unsafe {
bindings::vaQuerySurfaceAttributes(
bindings::va().vaQuerySurfaceAttributes(
self.display.handle(),
self.id,
std::ptr::null_mut(),
Expand All @@ -86,7 +86,7 @@ impl Config {
// returned by the initial call to vaQuerySurfaceAttributes. We then
// pass a valid pointer to it.
va_check(unsafe {
bindings::vaQuerySurfaceAttributes(
bindings::va().vaQuerySurfaceAttributes(
self.display.handle(),
self.id,
attrs.as_mut_ptr(),
Expand Down Expand Up @@ -122,7 +122,7 @@ impl Config {
impl Drop for Config {
fn drop(&mut self) {
// Safe because `self` represents a valid Config.
let status = va_check(unsafe { bindings::vaDestroyConfig(self.display.handle(), self.id) });
let status = va_check(unsafe { bindings::va().vaDestroyConfig(self.display.handle(), self.id) });

if status.is_err() {
error!("vaDestroyConfig failed: {}", status.unwrap_err());
Expand Down
4 changes: 2 additions & 2 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ impl Context {
// and ntargets are properly initialized. Note that render_targets==NULL
// is valid so long as ntargets==0.
va_check(unsafe {
bindings::vaCreateContext(
bindings::va().vaCreateContext(
display.handle(),
config.id(),
coded_width as i32,
Expand Down Expand Up @@ -93,7 +93,7 @@ impl Context {
impl Drop for Context {
fn drop(&mut self) {
// Safe because `self` represents a valid VAContext.
let status = va_check(unsafe { bindings::vaDestroyContext(self.display.handle(), self.id) });
let status = va_check(unsafe { bindings::va().vaDestroyContext(self.display.handle(), self.id) });

if status.is_err() {
error!("vaDestroyContext failed: {}", status.unwrap_err());
Expand Down
33 changes: 22 additions & 11 deletions src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ pub struct Display {
/// Error type for `Display::open_drm_display`.
#[derive(Debug, Error)]
pub enum OpenDrmDisplayError {
#[error("libva could not be loaded: {0}")]
LibvaUnavailable(&'static str),
#[error("cannot open DRM device: {0}")]
DeviceOpen(io::Error),
#[error("vaGetDisplayDRM returned NULL")]
Expand All @@ -94,6 +96,10 @@ impl Display {
///
/// `path` is the path to a DRM device that supports VAAPI, e.g. `/dev/dri/renderD128`.
pub fn open_drm_display<P: AsRef<Path>>(path: P) -> Result<Arc<Self>, OpenDrmDisplayError> {
// dlopen libva up front so a libva-less host fails here (and the caller
// falls back) instead of panicking on the first va* call below.
bindings::load().map_err(OpenDrmDisplayError::LibvaUnavailable)?;

let file = std::fs::File::options()
.read(true)
.write(true)
Expand All @@ -102,7 +108,7 @@ impl Display {

// Safe because fd represents a valid file descriptor and the pointer is checked for
// NULL afterwards.
let display = unsafe { bindings::vaGetDisplayDRM(file.as_raw_fd()) };
let display = unsafe { bindings::va().vaGetDisplayDRM(file.as_raw_fd()) };
if display.is_null() {
return Err(OpenDrmDisplayError::VaGetDisplayDrm);
}
Expand All @@ -111,7 +117,7 @@ impl Display {
let mut minor = 0i32;
// Safe because we ensure that the display is valid (i.e not NULL) before calling
// vaInitialize. The File will close the DRM fd on drop.
va_check(unsafe { bindings::vaInitialize(display, &mut major, &mut minor) })
va_check(unsafe { bindings::va().vaInitialize(display, &mut major, &mut minor) })
.map(|()| {
Arc::new(Self {
handle: display,
Expand Down Expand Up @@ -146,13 +152,13 @@ impl Display {
/// Queries supported profiles by this display by wrapping `vaQueryConfigProfiles`.
pub fn query_config_profiles(&self) -> Result<Vec<bindings::VAProfile::Type>, VaError> {
// Safe because `self` represents a valid VADisplay.
let mut max_num_profiles = unsafe { bindings::vaMaxNumProfiles(self.handle) };
let mut max_num_profiles = unsafe { bindings::va().vaMaxNumProfiles(self.handle) };
let mut profiles = Vec::with_capacity(max_num_profiles as usize);

// Safe because `self` represents a valid `VADisplay` and the vector has `max_num_profiles`
// as capacity.
va_check(unsafe {
bindings::vaQueryConfigProfiles(self.handle, profiles.as_mut_ptr(), &mut max_num_profiles)
bindings::va().vaQueryConfigProfiles(self.handle, profiles.as_mut_ptr(), &mut max_num_profiles)
})?;

// Safe because `profiles` is allocated with a `max_num_profiles` capacity and
Expand All @@ -172,7 +178,7 @@ impl Display {
/// 2.0.0.32L.0005`.
pub fn query_vendor_string(&self) -> std::result::Result<String, &'static str> {
// Safe because `self` represents a valid VADisplay.
let vendor_string = unsafe { bindings::vaQueryVendorString(self.handle) };
let vendor_string = unsafe { bindings::va().vaQueryVendorString(self.handle) };

if vendor_string.is_null() {
return Err("vaQueryVendorString() returned NULL");
Expand All @@ -188,13 +194,18 @@ impl Display {
profile: bindings::VAProfile::Type,
) -> Result<Vec<bindings::VAEntrypoint::Type>, VaError> {
// Safe because `self` represents a valid VADisplay.
let mut max_num_entrypoints = unsafe { bindings::vaMaxNumEntrypoints(self.handle) };
let mut max_num_entrypoints = unsafe { bindings::va().vaMaxNumEntrypoints(self.handle) };
let mut entrypoints = Vec::with_capacity(max_num_entrypoints as usize);

// Safe because `self` represents a valid VADisplay and the vector has `max_num_entrypoints`
// as capacity.
va_check(unsafe {
bindings::vaQueryConfigEntrypoints(self.handle, profile, entrypoints.as_mut_ptr(), &mut max_num_entrypoints)
bindings::va().vaQueryConfigEntrypoints(
self.handle,
profile,
entrypoints.as_mut_ptr(),
&mut max_num_entrypoints,
)
})?;

// Safe because `entrypoints` is allocated with a `max_num_entrypoints` capacity, and
Expand All @@ -221,7 +232,7 @@ impl Display {
// Safe because `self` represents a valid VADisplay. The slice length is passed to the C
// function, so it is impossible to write past the end of the slice's storage by mistake.
va_check(unsafe {
bindings::vaGetConfigAttributes(
bindings::va().vaGetConfigAttributes(
self.handle,
profile,
entrypoint,
Expand Down Expand Up @@ -317,14 +328,14 @@ impl Display {
/// Returns available image formats for this display by wrapping around `vaQueryImageFormats`.
pub fn query_image_formats(&self) -> Result<Vec<bindings::VAImageFormat>, VaError> {
// Safe because `self` represents a valid VADisplay.
let mut num_image_formats = unsafe { bindings::vaMaxNumImageFormats(self.handle) };
let mut num_image_formats = unsafe { bindings::va().vaMaxNumImageFormats(self.handle) };
let mut image_formats = Vec::with_capacity(num_image_formats as usize);

// Safe because `self` represents a valid VADisplay. The `image_formats` vector is properly
// initialized and a valid size is passed to the C function, so it is impossible to write
// past the end of their storage by mistake.
va_check(unsafe {
bindings::vaQueryImageFormats(self.handle, image_formats.as_mut_ptr(), &mut num_image_formats)
bindings::va().vaQueryImageFormats(self.handle, image_formats.as_mut_ptr(), &mut num_image_formats)
})?;

// Safe because the C function will have written exactly `num_image_format` entries, which
Expand All @@ -341,7 +352,7 @@ impl Drop for Display {
fn drop(&mut self) {
// Safe because `self` represents a valid VADisplay.
unsafe {
bindings::vaTerminate(self.handle);
bindings::va().vaTerminate(self.handle);
// The File will close the DRM fd on drop.
}
}
Expand Down
18 changes: 9 additions & 9 deletions src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl<'a> Image<'a> {

// Safe since `picture.inner.context` represents a valid `VAContext` and `image` has been
// successfully created at this point.
match va_check(unsafe { bindings::vaMapBuffer(surface.display().handle(), image.buf, &mut addr) }) {
match va_check(unsafe { bindings::va().vaMapBuffer(surface.display().handle(), image.buf, &mut addr) }) {
Ok(_) => {
// Assert that libva provided us with a coded resolution that is
// at least as large as `display_resolution`.
Expand All @@ -78,7 +78,7 @@ impl<'a> Image<'a> {
// Safe because `picture.inner.context` represents a valid `VAContext` and `image`
// represents a valid `VAImage`.
unsafe {
bindings::vaDestroyImage(surface.display().handle(), image.image_id);
bindings::va().vaDestroyImage(surface.display().handle(), image.image_id);
}

Err(e)
Expand All @@ -100,7 +100,7 @@ impl<'a> Image<'a> {
let mut image: bindings::VAImage = Default::default();

// Safe because `self` has a valid display handle and ID.
va_check(unsafe { bindings::vaDeriveImage(surface.display().handle(), surface.id(), &mut image) })?;
va_check(unsafe { bindings::va().vaDeriveImage(surface.display().handle(), surface.id(), &mut image) })?;

Self::new(surface, image, true, visible_rect)
}
Expand All @@ -124,7 +124,7 @@ impl<'a> Image<'a> {

// Safe because `dpy` is a valid display handle.
va_check(unsafe {
bindings::vaCreateImage(
bindings::va().vaCreateImage(
dpy,
&mut format,
coded_resolution.0 as i32,
Expand All @@ -136,7 +136,7 @@ impl<'a> Image<'a> {
// Safe because `dpy` is a valid display handle, `picture.surface` is a valid VASurface and
// `image` is a valid `VAImage`.
match va_check(unsafe {
bindings::vaGetImage(
bindings::va().vaGetImage(
dpy,
surface.id(),
0,
Expand All @@ -151,7 +151,7 @@ impl<'a> Image<'a> {
Err(e) => {
// Safe because `image` is a valid `VAImage`.
unsafe {
bindings::vaDestroyImage(dpy, image.image_id);
bindings::va().vaDestroyImage(dpy, image.image_id);
}

Err(e)
Expand Down Expand Up @@ -203,7 +203,7 @@ impl<'a> Drop for Image<'a> {
// `picture.surface` represents a valid `VASurface` and `image` represents a valid
// `VAImage`.
unsafe {
bindings::vaPutImage(
bindings::va().vaPutImage(
self.display.handle(),
self.surface_id,
self.image.image_id,
Expand All @@ -222,9 +222,9 @@ impl<'a> Drop for Image<'a> {
unsafe {
// Safe since the buffer is mapped in `Image::new`, so `self.image.buf` points to a
// valid `VABufferID`.
bindings::vaUnmapBuffer(self.display.handle(), self.image.buf);
bindings::va().vaUnmapBuffer(self.display.handle(), self.image.buf);
// Safe since `self.image` represents a valid `VAImage`.
bindings::vaDestroyImage(self.display.handle(), self.image.image_id);
bindings::va().vaDestroyImage(self.display.handle(), self.image.image_id);
}
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ impl std::fmt::Display for VaError {

// Safe because `vaErrorStr` will return a pointer to a statically allocated, null
// terminated C string. The pointer is guaranteed to never be null.
let err_str = unsafe { CStr::from_ptr(bindings::vaErrorStr(self.0.get())) }
let err_str = unsafe { CStr::from_ptr(bindings::va().vaErrorStr(self.0.get())) }
.to_str()
.unwrap();
f.write_str(err_str)
Expand Down
Loading
Loading