medias 0.6.1

TV media player on bare DRM/KMS: Vulkan UI plane over libmpv video, for local files, UPnP/DLNA servers and YouTube, controlled by HDMI-CEC remote, evdev or terminal
//! libdrm's drmModeAtomicReq, loaded with dlopen. mpv's drmprime-overlay hwdec
//! writes the decoded frame's plane properties into such a request
//! (MPV_RENDER_PARAM_DRM_DISPLAY_V2); medias adds its planes and commits it.

use std::ffi::{c_int, c_void};
use std::io;
use std::os::fd::{AsFd, AsRawFd};

type Req = *mut c_void;

struct Lib {
    alloc: unsafe extern "C" fn() -> Req,
    add: unsafe extern "C" fn(Req, u32, u32, u64) -> c_int,
    commit: unsafe extern "C" fn(c_int, Req, u32, *mut c_void) -> c_int,
    free: unsafe extern "C" fn(Req),
}

impl Lib {
    fn load() -> io::Result<Lib> {
        let handle = unsafe { libc::dlopen(c"libdrm.so.2".as_ptr(), libc::RTLD_NOW | libc::RTLD_GLOBAL) };
        if handle.is_null() {
            return Err(io::Error::other("libdrm.so.2 not found"));
        }
        let sym = |name: &std::ffi::CStr| {
            let p = unsafe { libc::dlsym(handle, name.as_ptr()) };
            if p.is_null() { Err(io::Error::other(format!("libdrm.so.2 lacks {name:?}"))) } else { Ok(p) }
        };
        // SAFETY: signatures as declared in xf86drmMode.h.
        unsafe {
            Ok(Lib {
                alloc: std::mem::transmute::<*mut c_void, unsafe extern "C" fn() -> Req>(sym(c"drmModeAtomicAlloc")?),
                add: std::mem::transmute::<*mut c_void, unsafe extern "C" fn(Req, u32, u32, u64) -> c_int>(
                    sym(c"drmModeAtomicAddProperty")?,
                ),
                commit: std::mem::transmute::<*mut c_void, unsafe extern "C" fn(c_int, Req, u32, *mut c_void) -> c_int>(
                    sym(c"drmModeAtomicCommit")?,
                ),
                free: std::mem::transmute::<*mut c_void, unsafe extern "C" fn(Req)>(sym(c"drmModeAtomicFree")?),
            })
        }
    }
}

/// Head of libdrm's private `struct _drmModeAtomicReq` (xf86drmMode.c).
#[repr(C)]
struct RawReq {
    cursor: u32,
    size_items: u32,
    items: *const RawItem,
}

/// Head of `struct _drmModeAtomicReqItem`; 24 bytes with the trailing
/// `cursor` field (libdrm 2.4.98 and later), 16 before.
#[repr(C)]
struct RawItem {
    object: u32,
    property: u32,
    value: u64,
}

/// Item size of this libdrm's request array, from a request with two known entries.
fn probe_item_size(lib: &Lib) -> Option<usize> {
    unsafe {
        let req = (lib.alloc)();
        if req.is_null() {
            return None;
        }
        let entries = [(0x5a5a_0001u32, 0x5a5a_0002u32, 0x5a5a_0003_5a5a_0004u64), (0x6b6b_0001, 0x6b6b_0002, 0x6b6b_0003_6b6b_0004)];
        let added = entries.iter().all(|&(o, p, v)| (lib.add)(req, o, p, v) >= 0);
        let raw = &*req.cast::<RawReq>();
        let size = (added && raw.cursor == 2 && raw.size_items >= 2 && !raw.items.is_null())
            .then(|| {
                [24usize, 16].into_iter().find(|&stride| {
                    entries.iter().enumerate().all(|(i, &(o, p, v))| {
                        let item = &*raw.items.cast::<u8>().add(i * stride).cast::<RawItem>();
                        (item.object, item.property, item.value) == (o, p, v)
                    })
                })
            })
            .flatten();
        (lib.free)(req);
        size
    }
}

/// A heap slot that always holds a live request. mpv reads the slot through
/// `mpv_ptr` during each render; `commit` submits and replaces the request.
pub struct RequestSlot {
    lib: Lib,
    slot: Box<Req>,
    item_size: Option<usize>,
}

impl RequestSlot {
    pub fn new() -> io::Result<RequestSlot> {
        let lib = Lib::load()?;
        let req = unsafe { (lib.alloc)() };
        if req.is_null() {
            return Err(io::Error::other("drmModeAtomicAlloc failed"));
        }
        let item_size = probe_item_size(&lib);
        Ok(RequestSlot { lib, slot: Box::new(req), item_size })
    }

    /// `struct _drmModeAtomicReq **` for mpv_opengl_drm_params_v2; valid while `self` lives.
    pub fn mpv_ptr(&mut self) -> *mut *mut c_void {
        &mut *self.slot
    }

    pub fn add(&mut self, object: u32, property: u32, value: u64) -> io::Result<()> {
        if unsafe { (self.lib.add)(*self.slot, object, property, value) } < 0 {
            return Err(io::Error::other("drmModeAtomicAddProperty failed"));
        }
        Ok(())
    }

    /// Last value written for (`object`, `property`) in the pending request,
    /// by mpv or `add`. None also when libdrm's request layout is unknown.
    pub fn find(&self, object: u32, property: u32) -> Option<u64> {
        let stride = self.item_size?;
        // SAFETY: layout verified by `probe_item_size` against this libdrm.
        unsafe {
            let req = &*(*self.slot).cast::<RawReq>();
            (0..req.cursor as usize).rev().find_map(|i| {
                let item = &*req.items.cast::<u8>().add(i * stride).cast::<RawItem>();
                (item.object == object && item.property == property).then_some(item.value)
            })
        }
    }

    /// Blocking commit of the accumulated properties; the slot gets a new request.
    pub fn commit(&mut self, card: &impl AsFd) -> io::Result<()> {
        let fresh = unsafe { (self.lib.alloc)() };
        if fresh.is_null() {
            return Err(io::Error::other("drmModeAtomicAlloc failed"));
        }
        let req = std::mem::replace(&mut *self.slot, fresh);
        let ret = unsafe { (self.lib.commit)(card.as_fd().as_raw_fd(), req, 0, std::ptr::null_mut()) };
        let error = io::Error::last_os_error();
        unsafe { (self.lib.free)(req) };
        if ret != 0 { Err(error) } else { Ok(()) }
    }
}

impl Drop for RequestSlot {
    fn drop(&mut self) {
        unsafe { (self.lib.free)(*self.slot) };
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn slot_replaces_request_on_commit() {
        let Ok(mut slot) = super::RequestSlot::new() else { return }; // no libdrm on this host
        let first = unsafe { *slot.mpv_ptr() };
        slot.add(1, 2, 3).unwrap();
        // /dev/null is not a DRM device: the commit fails, the slot is still renewed.
        let null = std::fs::File::open("/dev/null").unwrap();
        assert!(slot.commit(&null).is_err());
        let second = unsafe { *slot.mpv_ptr() };
        assert!(!second.is_null());
        slot.add(1, 2, 4).unwrap();
        let _ = first;
    }

    #[test]
    fn find_returns_the_last_value_added() {
        let Ok(mut slot) = super::RequestSlot::new() else { return };
        assert!(slot.item_size.is_some(), "libdrm request layout not recognised");
        assert_eq!(slot.find(7, 8), None);
        slot.add(7, 8, 100).unwrap();
        slot.add(7, 9, 200).unwrap();
        slot.add(7, 8, 300).unwrap();
        assert_eq!(slot.find(7, 8), Some(300));
        assert_eq!(slot.find(7, 9), Some(200));
        assert_eq!(slot.find(9, 9), None);
    }
}