//! Runtime dlopen wrapper for `libgbm.so.1`. //! //! enBoxer reads dmabufs that Hyprland hands us through //! `zwlr_export_dmabuf_manager_v1`. To turn those GPU-allocated dma-bufs //! into CPU pixels we need `gbm_create_device` + `gbm_bo_import` + //! `gbm_bo_map` from libgbm. libgbm is a system library, not a Rust crate, //! and we don't want a build-time dep on `libgbm-dev`. So we dlopen it //! at runtime via `libc::dlopen`, look up just the functions we need //! with `libc::dlsym`, and store their raw addresses as `usize`. //! //! If libgbm.so.1 isn't installed on the operator's box the open call //! fails; callers fall back to the synthetic frame (the round-trip //! metadata is still useful) and document the libgbm dependency clearly //! in the docs and CHANGELOG. //! //! Why libc + raw usize instead of the `libloading` crate: //! libloading 0.8's `Symbol::into_raw` returns the `Symbol` wrapper //! rather than `*mut T`, and threading the lifetimes through `Syms` -> BO //! -> mapped slices is more trouble than it is worth for the seven //! symbols we need. A libc + transmute dance is the boring well-trodden //! path that compiles on every Rust version and every libloading //! revision without gymnastics. use std::os::fd::RawFd; use std::os::raw::{c_char, c_int, c_uint, c_void}; use std::path::{Path, PathBuf}; use thiserror::Error; #[derive(Debug, Error)] pub enum GbmError { #[error("libgbm.so.1: dlopen failed ({0}); install libgbm or accept the synthetic fallback")] LibLoad(String), #[error("libgbm.so.1: required symbol missing: {0}")] Symbol(&'static str), #[error("/dev/dri: no render node found (looked in /dev/dri/)")] NoRenderNode, #[error("gbm_create_device failed (is the render node accessible?)")] CreateDevice, #[error("gbm_bo_import failed for fd {0} ({1}x{2} fmt {3:#x})")] Import(RawFd, u32, u32, u32), #[error("gbm_bo_map failed")] Map, #[error("io: {0}")] Io(#[from] std::io::Error), } // GBM_BO_IMPORT_FD from . Stable ABI. const GBM_BO_IMPORT_FD: c_uint = 0x5501; // GBM_BO_USE_LINEAR (1 << 4). const GBM_BO_USE_LINEAR: c_uint = 1 << 4; // GBM_BO_TRANSFER_READ (1 << 0). const GBM_BO_TRANSFER_READ: c_uint = 1 << 0; type GbmDeviceT = c_void; type GbmBoT = c_void; // Mirror of `struct gbm_import_fd_data` from . #[repr(C)] struct GbmImportFdData { fd: c_int, width: c_uint, height: u32, stride: c_uint, format: c_uint, } // Raw function pointers resolved via dlsym, stored as usize. // At call time we transmute usize -> fn pointer. The pointers stay // valid for the lifetime of the loaded Library, which `GbmDevice` owns. #[derive(Debug, Clone, Copy)] struct Syms { create_device: usize, destroy_device: usize, bo_import: usize, #[allow(dead_code)] // resolved for future stride-overrun sanity bo_get_stride: usize, bo_destroy: usize, bo_map: usize, bo_unmap: usize, } /// Opaque handle to the dlopen'd libgbm library + the GBM device + the /// resolved symbol pointers we need. Owns the dlopen handle via libc; /// closes it on Drop via `dlclose`. pub struct GbmDevice { handle: *mut c_void, dev: *mut GbmDeviceT, sym: Syms, } impl GbmDevice { /// dlopen libgbm.so.1 and open the first accessible render node. If /// libgbm is missing or no render node exists, returns Err so the /// caller can fall back gracefully. pub fn open() -> Result { // libc::dlopen("libgbm.so.1", libc::RTLD_NOW) let path = b"libgbm.so.1\0"; let handle = unsafe { libc::dlopen(path.as_ptr() as *const c_char, libc::RTLD_NOW) }; if handle.is_null() { return Err(GbmError::LibLoad( std::io::Error::last_os_error().to_string(), )); } let sym = Syms { create_device: dlsym_required(handle, b"gbm_create_device\0")?, destroy_device: dlsym_required(handle, b"gbm_device_destroy\0")?, bo_import: dlsym_required(handle, b"gbm_bo_import\0")?, bo_get_stride: dlsym_required(handle, b"gbm_bo_get_stride\0")?, bo_destroy: dlsym_required(handle, b"gbm_bo_destroy\0")?, bo_map: dlsym_required(handle, b"gbm_bo_map\0")?, bo_unmap: dlsym_required(handle, b"gbm_bo_unmap\0")?, }; let fd = open_first_render_node()?; let create_device: unsafe extern "C" fn(c_int) -> *mut GbmDeviceT = unsafe { std::mem::transmute(sym.create_device) }; let dev = unsafe { create_device(fd) }; if dev.is_null() { unsafe { libc::dlclose(handle) }; return Err(GbmError::CreateDevice); } Ok(Self { handle, dev, sym }) } /// Import a Linux DMA-BUF fd as a linear (CPU-mappable) BO. Width, /// height, stride, format must match the producer's view. pub fn import_dmabuf( &self, fd: RawFd, width: u32, height: u32, stride: u32, format: u32, ) -> Result { let duped = unsafe { libc::dup(fd) }; if duped < 0 { return Err(GbmError::Io(std::io::Error::last_os_error())); } let data = GbmImportFdData { fd: duped, width, height, stride, format, }; let bo_import: unsafe extern "C" fn( *mut GbmDeviceT, c_uint, *const c_void, c_uint, ) -> *mut GbmBoT = unsafe { std::mem::transmute(self.sym.bo_import) }; let bo = unsafe { bo_import( self.dev, GBM_BO_IMPORT_FD, (&data as *const GbmImportFdData) as *const c_void, GBM_BO_USE_LINEAR, ) }; if bo.is_null() { unsafe { libc::close(duped) }; return Err(GbmError::Import(fd, width, height, format)); } Ok(GbmBo { handle: self.handle, inner: bo, sym: self.sym, }) } } impl Drop for GbmDevice { fn drop(&mut self) { let destroy_device: unsafe extern "C" fn(*mut GbmDeviceT) = unsafe { std::mem::transmute(self.sym.destroy_device) }; unsafe { destroy_device(self.dev) }; unsafe { libc::dlclose(self.handle) }; } } /// A BO that has been imported but not yet mapped. Call `map()` to read. pub struct GbmBo { #[allow(dead_code)] handle: *mut c_void, inner: *mut GbmBoT, sym: Syms, } /// A mapped (read-only) BO + its stride. Owns the mapping until drop, /// which calls gbm_bo_unmap. pub struct MappedBo { bo: *mut GbmBoT, sym: Syms, ptr: *mut c_void, map_data: *mut c_void, pub stride: u32, } impl GbmBo { /// Map the BO for reading (CPU side). stride may differ from the /// producer's stride; trust this one because GBM aligns as needed. pub fn map(&self) -> Result { let mut stride: c_uint = 0; let mut map_data: *mut c_void = std::ptr::null_mut(); let mut map_size: usize = 0; let bo_map: unsafe extern "C" fn( *mut GbmBoT, c_uint, c_uint, c_uint, c_uint, c_uint, *mut c_uint, *mut *mut c_void, *mut usize, ) -> *mut c_void = unsafe { std::mem::transmute(self.sym.bo_map) }; let ptr = unsafe { bo_map( self.inner, 0, 0, u32::MAX, u32::MAX, GBM_BO_TRANSFER_READ, &mut stride, &mut map_data, &mut map_size, ) }; if ptr.is_null() { return Err(GbmError::Map); } Ok(MappedBo { bo: self.inner, sym: self.sym, ptr, map_data, stride, }) } } impl Drop for GbmBo { fn drop(&mut self) { let bo_destroy: unsafe extern "C" fn(*mut GbmBoT) = unsafe { std::mem::transmute(self.sym.bo_destroy) }; unsafe { bo_destroy(self.inner) }; } } impl MappedBo { /// Borrow the mapped pixels as a byte slice of (stride * height) /// bytes. The caller knows the height from the dmabuf frame event. pub fn as_slice(&self, height: u32) -> &[u8] { unsafe { std::slice::from_raw_parts( self.ptr as *const u8, (self.stride as usize) * (height as usize), ) } } } impl Drop for MappedBo { fn drop(&mut self) { let bo_unmap: unsafe extern "C" fn(*mut GbmBoT, *mut c_void) = unsafe { std::mem::transmute(self.sym.bo_unmap) }; unsafe { bo_unmap(self.bo, self.map_data) }; } } // libc::dlsym helper that returns the raw address as usize. NUL-terminated // byte slices only. fn dlsym_required(handle: *mut c_void, name: &[u8]) -> Result { let name_str = std::str::from_utf8(name.trim_ascii_end()) .map_err(|_| GbmError::Symbol(""))?; let sym = unsafe { libc::dlsym(handle, name_str.as_ptr() as *const c_char) }; if sym.is_null() { // Leak a copy: the dlsym lookup runs once per GBM device // open and the leak lasts until process exit. Not worth a // thread-local String cache for the half-dozen call sites. let leaked: &'static str = Box::leak(name_str.to_string().into_boxed_str()); return Err(GbmError::Symbol(leaked)); } Ok(sym as usize) } fn open_first_render_node() -> Result { let entries = std::fs::read_dir("/dev/dri") .map_err(GbmError::Io)? .filter_map(Result::ok) .map(|e| e.path()) .filter(|p: &PathBuf| { let name = p.file_name().and_then(|n| n.to_str()).unwrap_or(""); name.starts_with("renderD") }) .collect::>(); for path in entries { match open_rdwr(&path) { Ok(fd) => return Ok(fd), Err(_) => continue, } } Err(GbmError::NoRenderNode) } fn open_rdwr(path: &Path) -> std::io::Result { use std::os::fd::IntoRawFd; let f = std::fs::OpenOptions::new() .read(true) .write(true) .open(path)?; Ok(f.into_raw_fd()) } #[cfg(test)] mod tests { use super::*; #[test] fn fourcc_constants_match_gbm_h() { // The numeric values are part of libgbm's ABI; if upstream // renumbers them we want to know. assert_eq!(GBM_BO_IMPORT_FD, 0x5501); assert_eq!(GBM_BO_USE_LINEAR, 1 << 4); assert_eq!(GBM_BO_TRANSFER_READ, 1 << 0); } #[test] fn no_render_node_is_a_clean_error() { let e = GbmError::NoRenderNode; assert!(e.to_string().contains("render node")); } }