diff --git a/Cargo.toml b/Cargo.toml index 9db4ea7..ef49fc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/bindgen_gen.rs b/bindgen_gen.rs index 25aa298..5a5003e 100644 --- a/bindgen_gen.rs +++ b/bindgen_gen.rs @@ -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) } diff --git a/build.rs b/build.rs index 5acf38e..e4154db 100644 --- a/build.rs +++ b/build.rs @@ -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())) diff --git a/src/bindings.rs b/src/bindings.rs index 71f0283..9703763 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -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> = 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") +} diff --git a/src/buffer.rs b/src/buffer.rs index 76ff2f6..c147fa1 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -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(), @@ -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()); @@ -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) }; @@ -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()); diff --git a/src/config.rs b/src/config.rs index c67fb56..86e49c6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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, @@ -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(), @@ -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(), @@ -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()); diff --git a/src/context.rs b/src/context.rs index 55d268d..f1a0b12 100644 --- a/src/context.rs +++ b/src/context.rs @@ -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, @@ -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()); diff --git a/src/display.rs b/src/display.rs index ef2eb55..64c4c6c 100644 --- a/src/display.rs +++ b/src/display.rs @@ -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")] @@ -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>(path: P) -> Result, 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) @@ -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); } @@ -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, @@ -146,13 +152,13 @@ impl Display { /// Queries supported profiles by this display by wrapping `vaQueryConfigProfiles`. pub fn query_config_profiles(&self) -> Result, 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 @@ -172,7 +178,7 @@ impl Display { /// 2.0.0.32L.0005`. pub fn query_vendor_string(&self) -> std::result::Result { // 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"); @@ -188,13 +194,18 @@ impl Display { profile: bindings::VAProfile::Type, ) -> Result, 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 @@ -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, @@ -317,14 +328,14 @@ impl Display { /// Returns available image formats for this display by wrapping around `vaQueryImageFormats`. pub fn query_image_formats(&self) -> Result, 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 @@ -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. } } diff --git a/src/image.rs b/src/image.rs index 6e94db7..3e1f1ae 100644 --- a/src/image.rs +++ b/src/image.rs @@ -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`. @@ -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) @@ -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) } @@ -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, @@ -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, @@ -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) @@ -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, @@ -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); } } } diff --git a/src/lib.rs b/src/lib.rs index c86d05a..4688c41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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) diff --git a/src/picture.rs b/src/picture.rs index 0dbb1d9..5b2e17e 100644 --- a/src/picture.rs +++ b/src/picture.rs @@ -156,7 +156,7 @@ impl Picture { // Safe because `self.inner.context` represents a valid VAContext and // `self.inner.surface` represents a valid VASurface. let res = va_check(unsafe { - bindings::vaBeginPicture( + bindings::va().vaBeginPicture( self.inner.context.display().handle(), self.inner.context.id(), self.surface().id(), @@ -178,7 +178,7 @@ impl Picture { // passed to the C function, so it is impossible to write past the end of the vector's // storage by mistake. va_check(unsafe { - bindings::vaRenderPicture( + bindings::va().vaRenderPicture( self.inner.context.display().handle(), self.inner.context.id(), Buffer::as_id_vec(&self.inner.buffers).as_mut_ptr(), @@ -196,12 +196,11 @@ impl Picture { /// Wrapper around `vaEndPicture`. pub fn end(self) -> Result, VaError> { // Safe because `self.inner.context` represents a valid `VAContext`. - va_check(unsafe { bindings::vaEndPicture(self.inner.context.display().handle(), self.inner.context.id()) }).map( - |()| Picture { + va_check(unsafe { bindings::va().vaEndPicture(self.inner.context.display().handle(), self.inner.context.id()) }) + .map(|()| Picture { inner: self.inner, phantom: PhantomData, - }, - ) + }) } } diff --git a/src/surface.rs b/src/surface.rs index 2e867ed..5dca700 100644 --- a/src/surface.rs +++ b/src/surface.rs @@ -223,7 +223,7 @@ impl Surface { // Also all the pointers in `attrs` are pointing to valid objects that haven't been // moved or destroyed. match va_check(unsafe { - bindings::vaCreateSurfaces( + bindings::va().vaCreateSurfaces( display.handle(), rt_format, width, @@ -259,7 +259,7 @@ impl Surface { /// is safe to use the render target for a different picture. pub fn sync(&self) -> Result<(), VaError> { // Safe because `self` represents a valid VASurface. - va_check(unsafe { bindings::vaSyncSurface(self.display.handle(), self.id) }) + va_check(unsafe { bindings::va().vaSyncSurface(self.display.handle(), self.id) }) } /// Convenience function to return a VASurfaceID vector. Useful to interface with the C API @@ -272,7 +272,7 @@ impl Surface { pub fn query_status(&self) -> Result { let mut status: bindings::VASurfaceStatus::Type = 0; // Safe because `self` represents a valid VASurface. - va_check(unsafe { bindings::vaQuerySurfaceStatus(self.display.handle(), self.id, &mut status) })?; + va_check(unsafe { bindings::va().vaQuerySurfaceStatus(self.display.handle(), self.id, &mut status) })?; Ok(status) } @@ -282,7 +282,7 @@ impl Surface { // Safe because `self` represents a valid VASurface. va_check(unsafe { - bindings::vaQuerySurfaceError( + bindings::va().vaQuerySurfaceError( self.display.handle(), self.id, bindings::VA_STATUS_ERROR_DECODING_ERROR as i32, @@ -342,7 +342,7 @@ impl Surface { let mut desc: bindings::VADRMPRIMESurfaceDescriptor = Default::default(); va_check(unsafe { - bindings::vaExportSurfaceHandle( + bindings::va().vaExportSurfaceHandle( self.display.handle(), self.id(), bindings::VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, @@ -413,7 +413,7 @@ impl AsMut for Surface { impl Drop for Surface { fn drop(&mut self) { // Safe because `self` represents a valid VASurface. - unsafe { bindings::vaDestroySurfaces(self.display.handle(), &mut self.id, 1) }; + unsafe { bindings::va().vaDestroySurfaces(self.display.handle(), &mut self.id, 1) }; } }