hbm 0.1.6

A hardware buffer allocator
Documentation
// Copyright 2024 Google LLC
// SPDX-License-Identifier: MIT

//! A backend for DRM KMS.
//!
//! This module provides a backend for DRM KMS.

use super::{Class, Constraint, Description, Extent, Handle, Layout, MemoryType};
use crate::dma_buf;
use crate::formats;
use crate::types::{Error, Format, Modifier, Result, Size};
use crate::utils;
use drm::buffer::{Buffer as DrmBuffer, DrmFourcc};
use drm::control::{plane, Device as DrmControlDevice};
use drm::Device as DrmDevice;
use std::collections::HashMap;
use std::ops::{Bound, RangeBounds};
use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};

bitflags::bitflags! {
    /// A DRM KMS backend usage.
    #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
    pub struct Usage: u32 {
        /// The BO can be used with a primary plane.
        const PRIMARY = 1 << 0;
        /// The BO can be used with a cursor plane.
        const CURSOR = 1 << 1;
    }
}

fn open_drm_primary_device(node_path: Option<PathBuf>, device_id: Option<u64>) -> Result<OwnedFd> {
    for path in utils::drm_scan_primary()? {
        if let Some(node_path) = &node_path {
            if *node_path != path {
                continue;
            }
        }
        if let Some(device_id) = device_id {
            if !path.metadata().is_ok_and(|s| device_id == s.rdev()) {
                continue;
            }
        }

        return utils::open(&path);
    }

    Error::unsupported()
}

fn get_drm_usage(usage: super::Usage) -> Result<Usage> {
    let usage = match usage {
        super::Usage::DrmKms(usage) => usage,
        _ => return Error::user(),
    };

    if !usage.bits().is_power_of_two() {
        return Error::user();
    }

    Ok(usage)
}

struct Device(OwnedFd);

impl AsFd for Device {
    fn as_fd(&self) -> BorrowedFd {
        self.0.as_fd()
    }
}
impl DrmDevice for Device {}
impl DrmControlDevice for Device {}

type FormatTable = HashMap<Format, Vec<Modifier>>;

/// A DRM KMS backend.
pub struct Backend {
    device: Device,
    alloc_only: bool,

    max_width: u32,
    max_height: u32,
    primary_formats: FormatTable,
    cursor_formats: FormatTable,
}

impl Backend {
    fn new(fd: OwnedFd, alloc_only: bool) -> Result<Self> {
        let mut backend = Backend {
            device: Device(fd),
            alloc_only,
            max_width: 0,
            max_height: 0,
            primary_formats: HashMap::new(),
            cursor_formats: HashMap::new(),
        };

        if !backend.alloc_only {
            backend.init()?;
        }

        Ok(backend)
    }

    fn init(&mut self) -> Result<()> {
        self.device
            .set_client_capability(drm::ClientCapability::UniversalPlanes, true)?;

        self.init_max_size()?;

        let planes = self.device.plane_handles()?;
        for plane in planes {
            self.init_plane(plane)?;
        }

        Ok(())
    }

    fn init_max_size(&mut self) -> Result<()> {
        let get_val = |b: Bound<&u32>| match b {
            Bound::Included(&v) => v,
            Bound::Excluded(&v) => {
                if v > 0 {
                    v - 1
                } else {
                    0
                }
            }
            Bound::Unbounded => 65536,
        };

        let res = self.device.resource_handles()?;
        self.max_width = get_val(res.supported_fb_width().end_bound());
        self.max_height = get_val(res.supported_fb_height().end_bound());

        Ok(())
    }

    fn init_plane(&mut self, plane: plane::Handle) -> Result<()> {
        let info = self.device.get_plane(plane)?;

        let mut ty = None;
        let mut in_fmts = None;

        let props = self.device.get_properties(info.handle())?;
        for (id, val) in props {
            let Ok(prop) = self.device.get_property(id) else {
                continue;
            };

            let name = prop.name().to_str().unwrap();
            match prop.value_type() {
                drm::control::property::ValueType::Enum(_) => {
                    if name == "type" {
                        ty = Some(val);
                    }
                }
                drm::control::property::ValueType::Blob => {
                    if name == "IN_FORMATS" {
                        let blob = self.device.get_property_blob(val)?;
                        in_fmts = Some(blob);
                    }
                }
                _ => (),
            }
            if ty.is_some() && in_fmts.is_some() {
                break;
            }
        }

        if let Some(ty) = ty {
            let ty = if ty == drm::control::PlaneType::Primary as u64 {
                drm::control::PlaneType::Primary
            } else if ty == drm::control::PlaneType::Cursor as u64 {
                drm::control::PlaneType::Cursor
            } else {
                drm::control::PlaneType::Overlay
            };

            self.init_plane_formats(info, ty, in_fmts);
        }

        Ok(())
    }

    fn init_plane_formats(
        &mut self,
        info: plane::Info,
        ty: drm::control::PlaneType,
        in_fmts: Option<Vec<u8>>,
    ) {
        let fmts = match ty {
            drm::control::PlaneType::Primary => &mut self.primary_formats,
            drm::control::PlaneType::Cursor => &mut self.cursor_formats,
            _ => return,
        };

        if let Some(in_fmts) = in_fmts {
            let Ok(iter) = utils::drm_parse_in_formats_blob(&in_fmts) else {
                return;
            };

            for (modifier, fmt) in iter {
                let mods = fmts.entry(Format(fmt)).or_default();

                if !mods.iter().any(|m| m.0 == modifier) {
                    mods.push(Modifier(modifier));
                }
            }
        } else {
            for fmt in info.formats() {
                fmts.insert(
                    Format(*fmt),
                    vec![formats::MOD_LINEAR, formats::MOD_INVALID],
                );
            }
        }
    }

    fn get_supported_modifiers(
        &self,
        usage: Usage,
        fmt: Format,
        modifier: Modifier,
    ) -> Result<Vec<Modifier>> {
        let fmts = if usage.contains(Usage::CURSOR) {
            &self.cursor_formats
        } else {
            &self.primary_formats
        };

        let mods = fmts.get(&fmt).ok_or(Error::Unsupported)?;

        let mods = if modifier.is_invalid() {
            mods.clone()
        } else {
            if !mods.iter().any(|m| *m == modifier) {
                return Error::unsupported();
            }

            vec![modifier]
        };

        Ok(mods)
    }
}

impl super::Backend for Backend {
    fn classify(&self, desc: Description, usage: super::Usage) -> Result<Class> {
        if desc.is_buffer() {
            return Error::unsupported();
        }

        let drm_usage = get_drm_usage(usage)?;
        let mods = self.get_supported_modifiers(drm_usage, desc.format, desc.modifier)?;
        let class = Class::new(desc)
            .usage(usage)
            .max_extent(Extent::Image(self.max_width, self.max_height))
            .modifiers(mods);

        Ok(class)
    }

    fn with_constraint(
        &self,
        class: &Class,
        extent: Extent,
        con: Option<Constraint>,
    ) -> Result<Handle> {
        assert!(!class.is_buffer());

        let fmt_class = formats::format_class(class.format)?;
        let size = (extent.width(), extent.height());
        let fmt = DrmFourcc::try_from(class.format.0).or(Error::unsupported())?;
        let bpp = (fmt_class.block_size[0] as u32) * 8;

        let buf = self.device.create_dumb_buffer(size, fmt, bpp)?;
        let pitch = buf.pitch();

        let dmabuf = self
            .device
            .buffer_to_prime_fd(buf.handle(), drm::RDWR | drm::CLOEXEC);
        let _ = self.device.destroy_dumb_buffer(buf);
        let dmabuf = dmabuf?;

        let layout = Layout::new()
            .size((extent.height() * pitch) as Size)
            .modifier(formats::MOD_LINEAR)
            .plane_count(1)
            .stride(0, pitch as Size);
        if !layout.fit(con) {
            return Error::unsupported();
        }

        let mut res = dma_buf::Resource::new(layout);
        res.bind_memory(dmabuf);
        let handle = Handle::from(res);

        Ok(handle)
    }

    fn bind_memory(
        &self,
        handle: &mut Handle,
        mt: MemoryType,
        dmabuf: Option<OwnedFd>,
    ) -> Result<()> {
        let alloc = |_| Error::user();
        dma_buf::bind_memory(handle, mt, dmabuf, alloc)
    }
}

/// A DRM KMS backend builder.
#[derive(Default)]
pub struct Builder {
    node_path: Option<PathBuf>,
    node_fd: Option<OwnedFd>,
    device_id: Option<u64>,
    alloc_only: bool,
}

impl Builder {
    /// Creates a DRM KMS backend builder.
    pub fn new() -> Self {
        Default::default()
    }

    /// Sets the primary node path to use.
    pub fn node_path(mut self, node_path: impl AsRef<Path>) -> Self {
        self.node_path = Some(PathBuf::from(node_path.as_ref()));
        self
    }

    /// Sets the primary node fd to use.
    pub fn node_fd(mut self, node_fd: OwnedFd) -> Self {
        self.node_fd = Some(node_fd);
        self
    }

    /// Sets the primary node device id (`st_rdev`) to use.
    pub fn device_id(mut self, device_id: u64) -> Self {
        self.device_id = Some(device_id);
        self
    }

    /// Skips querying DRM KMS properties.
    pub fn alloc_only(mut self, alloc_only: bool) -> Self {
        self.alloc_only = alloc_only;
        self
    }

    /// Builds a DRM KMS backend.
    ///
    /// One and only one of node path, node fd, or device id must be set.
    pub fn build(self) -> Result<Backend> {
        if self.node_path.is_some() as i32
            + self.node_fd.is_some() as i32
            + self.device_id.is_some() as i32
            > 1
        {
            return Error::user();
        }

        if !utils::drm_exists() {
            return Error::unsupported();
        }

        let node_fd = if let Some(fd) = self.node_fd {
            fd
        } else {
            open_drm_primary_device(self.node_path, self.device_id)?
        };

        Backend::new(node_fd, self.alloc_only)
    }
}