medias 0.5.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")?),
            })
        }
    }
}

/// 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>,
}

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"));
        }
        Ok(RequestSlot { lib, slot: Box::new(req) })
    }

    /// `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(())
    }

    /// 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;
    }
}