medias 0.6.0

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
//! GBM allocation, KMS framebuffer registration, and dma-buf memory plane export.
//! GEM handles belong to the DRM fd shared by the allocator and KMS card.

use std::fs::File;
use std::io;
use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
use std::os::unix::fs::MetadataExt;

use drm::buffer::{DrmFourcc, DrmModifier, Handle as BufferHandle, PlanarBuffer};
use drm::control::{Device as ControlDevice, FbCmd2Flags, framebuffer};
pub use gbm::BufferObjectFlags;
use gbm::{AsRaw, BufferObject, Device as GbmDevice};

use crate::kms::Card;

/// A dup'd DRM fd so the gbm device outlives independently of the Card.
struct DupFd(OwnedFd);
impl AsFd for DupFd {
    fn as_fd(&self) -> BorrowedFd<'_> {
        self.0.as_fd()
    }
}

pub struct Allocator {
    dev: GbmDevice<DupFd>,
}

impl Allocator {
    pub fn new(card: &Card) -> io::Result<Allocator> {
        let fd = DupFd(card.as_fd().try_clone_to_owned()?);
        Ok(Allocator {
            dev: GbmDevice::new(fd)?,
        })
    }

    #[allow(dead_code)] // diagnostic (gbm backend name), used ad hoc
    pub fn backend(&self) -> &str {
        self.dev.backend_name()
    }

    /// Raw `gbm_device*`, for creating an EGL display on the GBM platform.
    pub fn device_ptr(&self) -> *mut std::ffi::c_void {
        self.dev.as_raw() as *mut std::ffi::c_void
    }

    /// Allocates a BO and registers its memory planes with KMS.
    pub fn alloc(
        &self,
        card: &Card,
        width: u32,
        height: u32,
        format: DrmFourcc,
        flags: BufferObjectFlags,
    ) -> io::Result<Scanout> {
        let bo = self
            .dev
            .create_buffer_object::<()>(width, height, format, flags)?;
        self.register(card, bo, width, height)
    }

    pub fn alloc_with_modifiers(
        &self,
        card: &Card,
        width: u32,
        height: u32,
        format: DrmFourcc,
        modifiers: &[u64],
    ) -> io::Result<Scanout> {
        if modifiers.is_empty() || modifiers.contains(&u64::from(DrmModifier::Invalid)) {
            return Err(io::Error::other("explicit GBM allocation requires valid modifiers"));
        }
        let bo = self.dev.create_buffer_object_with_modifiers2::<()>(
            width, height, format,
            modifiers.iter().copied().map(DrmModifier::from),
            BufferObjectFlags::SCANOUT | BufferObjectFlags::RENDERING,
        )?;
        let modifier = explicit_modifier(bo.modifier())
            .ok_or_else(|| io::Error::other("GBM returned an implicit layout for an explicit allocation"))?;
        if !modifiers.contains(&modifier) {
            return Err(io::Error::other(format!("GBM returned unrequested modifier {modifier:#x}")));
        }
        self.register(card, bo, width, height)
    }

    fn register(
        &self, card: &Card, bo: BufferObject<()>, width: u32, height: u32,
    ) -> io::Result<Scanout> {
        validate_plane_count(bo.plane_count() as usize)?;
        let fb = card
            .add_planar_framebuffer(&BoFb(&bo), fb_flags(&bo))
            .map_err(io::Error::other)?;
        Ok(Scanout {
            bo,
            fb,
            width,
            height,
        })
    }
}

/// A GBM buffer imported as a KMS framebuffer.
pub struct Scanout {
    pub bo: BufferObject<()>,
    pub fb: framebuffer::Handle,
    pub width: u32,
    pub height: u32,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct PlaneLayout {
    pub offset: u32,
    pub pitch: u32,
}

#[derive(Clone, Debug)]
pub(crate) struct ScanoutLayout {
    pub width: u32,
    pub height: u32,
    pub fourcc: u32,
    pub modifier: Option<u64>,
    pub planes: Vec<PlaneLayout>,
}

pub(crate) struct ExportedScanout {
    pub layout: ScanoutLayout,
    pub planes: Vec<File>,
}

impl ExportedScanout {
    /// A non-disjoint Vulkan image binds all modifier planes to one allocation.
    pub fn shares_backing(&self) -> io::Result<bool> {
        let identities = self
            .planes
            .iter()
            .map(|fd| {
                let m = fd.metadata()?;
                Ok((m.dev(), m.ino()))
            })
            .collect::<io::Result<Vec<_>>>()?;
        Ok(same_backing(&identities))
    }
}

fn same_backing(identities: &[(u64, u64)]) -> bool {
    !identities.is_empty() && identities.iter().all(|id| id == &identities[0])
}

pub(crate) fn validate_plane_count(count: usize) -> io::Result<()> {
    if !(1..=4).contains(&count) {
        return Err(io::Error::other(format!(
            "gbm: unsupported memory plane count {count} (expected 1..=4)"
        )));
    }
    Ok(())
}

fn explicit_modifier(modifier: DrmModifier) -> Option<u64> {
    match modifier {
        DrmModifier::Invalid => None,
        modifier => Some(modifier.into()),
    }
}

impl Scanout {
    pub(crate) fn export(&self) -> io::Result<ExportedScanout> {
        let count = self.bo.plane_count() as usize;
        validate_plane_count(count)?;
        let mut planes = Vec::with_capacity(count);
        let mut layouts = Vec::with_capacity(count);
        for i in 0..count as i32 {
            let pitch = self.bo.stride_for_plane(i);
            if pitch == 0 {
                return Err(io::Error::other(format!("gbm: zero pitch for memory plane {i}")));
            }
            layouts.push(PlaneLayout {
                offset: self.bo.offset(i),
                pitch,
            });
            planes.push(File::from(
                self.bo.fd_for_plane(i).map_err(|_| {
                    io::Error::other(format!("gbm: export dmabuf memory plane {i}"))
                })?,
            ));
        }
        Ok(ExportedScanout {
            layout: ScanoutLayout {
                width: self.width,
                height: self.height,
                fourcc: self.fourcc(),
                modifier: explicit_modifier(self.bo.modifier()),
                planes: layouts,
            },
            planes,
        })
    }
    pub fn stride(&self) -> u32 {
        self.bo.stride()
    }
    pub fn offset(&self) -> u32 {
        self.bo.offset(0)
    }
    pub fn modifier(&self) -> u64 {
        u64::from(self.bo.modifier())
    }
    pub fn fourcc(&self) -> u32 {
        self.bo.format() as u32
    }
    /// CPU-fill the buffer via `f(bytes, stride)`. Requires a LINEAR, WRITE bo.
    pub fn fill(&mut self, mut f: impl FnMut(&mut [u8], usize)) -> io::Result<()> {
        let (w, h) = (self.width, self.height);
        self.bo
            .map_mut(0, 0, w, h, |m| {
                let stride = m.stride() as usize;
                f(m.buffer_mut(), stride);
            })
            .map_err(io::Error::other)?;
        Ok(())
    }
}

/// `drm::PlanarBuffer` view over a gbm BO, for `add_planar_framebuffer`.
struct BoFb<'a>(&'a BufferObject<()>);

impl PlanarBuffer for BoFb<'_> {
    fn size(&self) -> (u32, u32) {
        (self.0.width(), self.0.height())
    }
    fn format(&self) -> DrmFourcc {
        self.0.format()
    }
    fn modifier(&self) -> Option<DrmModifier> {
        match self.0.modifier() {
            DrmModifier::Invalid => None,
            m => Some(m),
        }
    }
    fn pitches(&self) -> [u32; 4] {
        let mut p = [0; 4];
        for i in 0..self.0.plane_count() as i32 {
            p[i as usize] = self.0.stride_for_plane(i);
        }
        p
    }
    fn offsets(&self) -> [u32; 4] {
        let mut o = [0; 4];
        for i in 0..self.0.plane_count() as i32 {
            o[i as usize] = self.0.offset(i);
        }
        o
    }
    fn handles(&self) -> [Option<BufferHandle>; 4] {
        let mut h = [None; 4];
        for i in 0..self.0.plane_count() as i32 {
            // gbm_bo_handle is a union; the GEM handle is its u32 field.
            let raw = unsafe { self.0.handle_for_plane(i).u32_ };
            h[i as usize] = drm::control::from_u32(raw);
        }
        h
    }
}

/// AddFB2 needs the MODIFIERS flag iff the bo carries an explicit modifier.
fn fb_flags(bo: &BufferObject<()>) -> FbCmd2Flags {
    match bo.modifier() {
        DrmModifier::Invalid => FbCmd2Flags::empty(),
        _ => FbCmd2Flags::MODIFIERS,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn implicit_modifier_is_distinct_from_linear() {
        assert_eq!(explicit_modifier(DrmModifier::Invalid), None);
        assert_eq!(explicit_modifier(DrmModifier::Linear), Some(0));
    }

    #[test]
    fn memory_plane_count_is_bounded_by_drm_arrays() {
        for count in 1..=4 {
            assert!(validate_plane_count(count).is_ok());
        }
        for count in [0, 5, usize::MAX] {
            assert!(validate_plane_count(count).is_err());
        }
    }

    #[test]
    fn single_allocation_requires_matching_device_and_inode() {
        assert!(same_backing(&[(1, 10), (1, 10), (1, 10)]));
        assert!(!same_backing(&[(1, 10), (1, 11)]));
        assert!(!same_backing(&[(1, 10), (2, 10)]));
        assert!(!same_backing(&[]));
    }
}