drmkit-core 0.1.2

Core DRM/KMS device access for drmkit: device handles, property caching, and atomic requests.
Documentation
// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
// SPDX-License-Identifier: MIT

//! The DRM device handle.

use std::os::fd::{AsFd, AsRawFd as _, BorrowedFd, OwnedFd, RawFd};
use std::path::Path;

use drm::ClientCapability;
use drm::control::Device as ControlDevice;

use rustix::io::Errno;

use crate::error::{CoreError, Result};

/// How a [`Device`] holds its descriptor.
#[derive(Debug)]
enum Fd {
    /// Opened by this crate; closed on drop.
    Owned(OwnedFd),
    /// Owned elsewhere — typically a revocable descriptor from a libseat
    /// session. Never closed here.
    Borrowed(RawFd),
}

/// An open DRM device.
///
/// Port of `drm::Device` from `src/core/device.{hpp,cpp}`.
///
/// Implements the `drm` crate's [`Device`](drm::Device) and
/// [`control::Device`](drm::control::Device) traits, so every KMS operation
/// those traits provide is available directly. drmkit wraps rather than
/// re-binds (DR-2); the methods here are the ones the C++ adds on top or gives
/// a different contract.
#[derive(Debug)]
pub struct Device {
    fd: Fd,
}

impl Device {
    /// Open a DRM device node and verify the kernel agrees it is one.
    ///
    /// Sets DRM master if possible. Failing to become master is **not** an
    /// error: a client running under a compositor never will be, and the C++
    /// ignores the result for the same reason.
    ///
    /// # Errors
    ///
    /// - [`CoreError::Io`] if the node cannot be opened.
    /// - [`CoreError::NotADrmDevice`] if `drmGetVersion` fails on it, which is
    ///   how the C++ distinguishes a real card from any other character device.
    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
        let path = path.as_ref();
        let file = rustix::fs::open(
            path,
            rustix::fs::OFlags::RDWR | rustix::fs::OFlags::CLOEXEC,
            rustix::fs::Mode::empty(),
        )
        // Not `from_errno`: `EACCES` here is the node's permissions, not DRM
        // master, and the reference returns the errno as it is.
        .map_err(CoreError::Io)?;

        let device = Self {
            fd: Fd::Owned(file),
        };

        // Verify this is actually a DRM device.
        if drm::Device::get_driver(&device).is_err() {
            return Err(CoreError::NotADrmDevice {
                path: path.display().to_string(),
            });
        }

        // Non-fatal: a client under a compositor is never master.
        if let Err(e) = drm::Device::acquire_master_lock(&device) {
            drmkit_log::log_debug!(
                "could not become DRM master on {}: {e} (expected under a compositor)",
                path.display()
            );
        }

        Ok(device)
    }

    /// Take ownership of an already-open DRM descriptor.
    ///
    /// The descriptor is closed when the device is dropped. This has no C++
    /// counterpart: `Device::from_fd` there is always non-owning, because C++
    /// cannot express the transfer in a type.
    #[must_use]
    pub const fn from_owned_fd(fd: OwnedFd) -> Self {
        Self { fd: Fd::Owned(fd) }
    }

    /// Wrap a descriptor owned by someone else.
    ///
    /// The returned device does **not** close `fd` on drop; the caller — for
    /// instance a seat session holding a revocable libseat descriptor — keeps
    /// that responsibility. Resume flows replace the device wholesale with a
    /// freshly built one, exactly as the C++ move-assigns a new `from_fd`.
    ///
    /// # Safety
    ///
    /// `fd` must be a valid descriptor that stays open for the whole life of
    /// the returned `Device` and of everything derived from it. This cannot be
    /// checked: the borrow has no lifetime to tie it to, which is precisely why
    /// the safe [`Device::from_owned_fd`] exists and should be preferred.
    #[must_use]
    pub const unsafe fn from_borrowed_fd(fd: RawFd) -> Self {
        Self {
            fd: Fd::Borrowed(fd),
        }
    }

    /// The raw descriptor, for interop with code that needs one.
    ///
    /// Borrowed, never transferred.
    #[must_use]
    pub fn raw_fd(&self) -> RawFd {
        match &self.fd {
            Fd::Owned(fd) => fd.as_raw_fd(),
            Fd::Borrowed(fd) => *fd,
        }
    }

    /// Whether this device will close its descriptor on drop.
    #[must_use]
    pub const fn owns_fd(&self) -> bool {
        matches!(self.fd, Fd::Owned(_))
    }

    /// Enable the universal-planes client capability.
    ///
    /// Without it the kernel hides primary and cursor planes from
    /// `plane_handles`, and the allocator sees only overlays.
    ///
    /// # Errors
    ///
    /// [`CoreError::Io`] if the driver rejects the capability.
    pub fn enable_universal_planes(&self) -> Result<()> {
        self.set_client_cap(ClientCapability::UniversalPlanes, true)
    }

    /// Enable the atomic client capability.
    ///
    /// # Errors
    ///
    /// [`CoreError::Io`] if the driver does not support atomic modesetting.
    pub fn enable_atomic(&self) -> Result<()> {
        self.set_client_cap(ClientCapability::Atomic, true)
    }

    /// Set an arbitrary client capability.
    ///
    /// # Errors
    ///
    /// [`CoreError::Io`] if the driver rejects it.
    pub fn set_client_cap(&self, cap: ClientCapability, enable: bool) -> Result<()> {
        drm::Device::set_client_capability(self, cap, enable).map_err(|e| CoreError::from_io(&e))
    }

    /// Become DRM master.
    ///
    /// # Errors
    ///
    /// [`CoreError::Io`] with `EBUSY` if another client already holds it, or
    /// [`CoreError::NotMaster`] if the caller lacks the privilege to take it.
    pub fn set_master(&self) -> Result<()> {
        drm::Device::acquire_master_lock(self).map_err(|e| CoreError::from_io(&e))
    }

    /// Release DRM master.
    ///
    /// # Errors
    ///
    /// [`CoreError::Io`] if the ioctl fails.
    pub fn drop_master(&self) -> Result<()> {
        drm::Device::release_master_lock(self).map_err(|e| CoreError::from_io(&e))
    }

    /// Read an immutable blob property's payload.
    ///
    /// This is the seam `drmkit-fmt` is built around: the `IN_FORMATS` blob is
    /// fetched here and parsed by
    /// [`FormatTable::from_blob`](https://docs.rs/drmkit-fmt), which keeps the
    /// format crate device-free and `no_std`.
    ///
    /// # Errors
    ///
    /// [`CoreError::Io`] if the blob id is unknown to the kernel.
    pub fn property_blob(&self, blob_id: u64) -> Result<Vec<u8>> {
        self.get_property_blob(blob_id)
            .map_err(|e| CoreError::from_io(&e))
    }
}

/// A kernel property blob, freed when it is dropped.
///
/// `MODE_ID` takes a blob id rather than a value, so a scene that sets a mode
/// has to create one and keep it alive for as long as the mode is set. Letting
/// it leak would be invisible until a long-running compositor that re-modesets
/// often ran the kernel out of blob ids.
///
/// The borrow of the device is what makes freeing it safe. Holding a raw
/// descriptor instead would compile, and would destroy a blob on whatever the
/// kernel had since recycled that number into if the blob ever outlived the
/// device it came from.
pub struct PropertyBlob<'a> {
    id: u64,
    device: &'a Device,
}

impl PropertyBlob<'_> {
    /// The blob id, to be written into a blob-typed property.
    #[must_use]
    pub const fn id(&self) -> u64 {
        self.id
    }

    /// Give up the blob without asking the kernel to destroy it.
    ///
    /// For when the descriptor the blob was created on is gone -- a libseat
    /// session pause revokes it, and the kernel reclaimed every blob on it at
    /// that moment. Destroying through the replacement descriptor would at
    /// best fail and at worst free a *different* blob that happens to have
    /// been given the same id there.
    ///
    /// This leaks nothing: the blob is already gone. Do not reach for it to
    /// silence an ordinary drop -- on a live descriptor that really is a leak,
    /// and a long-running compositor that re-modesets will run the kernel out
    /// of blob ids.
    pub fn forget(self) {
        core::mem::forget(self);
    }
}

impl core::fmt::Debug for PropertyBlob<'_> {
    /// The id, without the device -- a blob is identified by its id, and
    /// printing the device behind it would be noise at best.
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("PropertyBlob")
            .field("id", &self.id)
            .finish()
    }
}

impl Drop for PropertyBlob<'_> {
    fn drop(&mut self) {
        // Nothing useful to do if this fails: the blob dies with the device
        // either way, and panicking in `drop` would be worse than the leak.
        let _ = drm::control::Device::destroy_property_blob(self.device, self.id);
    }
}

impl Device {
    /// Stage `data` as a kernel property blob.
    ///
    /// The blob borrows the device, so it cannot outlive the descriptor that
    /// has to free it.
    ///
    /// # Errors
    ///
    /// [`CoreError`] if the kernel refuses to create the blob.
    pub fn create_property_blob<T: ?Sized>(&self, data: &T) -> Result<PropertyBlob<'_>> {
        let value = drm::control::Device::create_property_blob(self, data)
            .map_err(|e| CoreError::Io(Errno::from_io_error(&e).unwrap_or(Errno::INVAL)))?;
        // The call is documented to return a blob value. A different variant
        // would mean the crate changed under us, and writing whatever integer
        // it carried into a blob property would be worse than failing here.
        let drm::control::property::Value::Blob(id) = value else {
            return Err(CoreError::Io(Errno::INVAL));
        };
        Ok(PropertyBlob { id, device: self })
    }
}

impl AsFd for Device {
    fn as_fd(&self) -> BorrowedFd<'_> {
        match &self.fd {
            Fd::Owned(fd) => fd.as_fd(),
            // SAFETY: the constructor is `unsafe` precisely because the caller
            // promised this descriptor outlives the `Device`. The borrow here
            // is bounded by `&self`, which is shorter.
            Fd::Borrowed(raw) => unsafe { BorrowedFd::borrow_raw(*raw) },
        }
    }
}

impl drm::Device for Device {}
impl drm::control::Device for Device {}

/// The modifier to declare when registering a framebuffer, or `None` to
/// take the legacy path, which leaves the layout to the driver.
///
/// Port of upstream's `addfb2_needs_modifiers` (`be147be`), so every
/// import agrees on it:
/// * `INVALID` never declares: it means "no explicit modifier", and passing
///   it with `DRM_MODE_FB_MODIFIERS` is ill-formed.
/// * `LINEAR` declares only when the driver took `DRM_CAP_ADDFB2_MODIFIERS`.
///   Drivers without it (i.MX LCDIF, tilcdc) reject the flag outright, even
///   for linear -- which is exactly their implicit layout, so the legacy
///   path imports the same buffer. Where the cap exists it is declared,
///   because "no modifier" there means the driver's default, and that is
///   not promised to be linear.
/// * Anything else always declares. A driver without the cap cannot scan
///   it out either way, and the kernel says so.
#[must_use]
pub fn framebuffer_modifier(device: &impl drm::Device, modifier: u64) -> Option<u64> {
    declared_modifier(modifier, || supports_framebuffer_modifiers(device))
}

/// [`framebuffer_modifier`]'s rule, with the capability supplied. Asked only
/// for `LINEAR`, the one case it decides.
pub(crate) fn declared_modifier(
    modifier: u64,
    takes_modifiers: impl FnOnce() -> bool,
) -> Option<u64> {
    const LINEAR: u64 = 0;
    const INVALID: u64 = (1 << 56) - 1;
    match modifier {
        INVALID => None,
        LINEAR => takes_modifiers().then_some(LINEAR),
        _ => Some(modifier),
    }
}

/// Whether the driver accepts `DRM_MODE_FB_MODIFIERS`
/// (`DRM_CAP_ADDFB2_MODIFIERS`).
#[must_use]
pub fn supports_framebuffer_modifiers(device: &impl drm::Device) -> bool {
    device
        .get_driver_capability(drm::DriverCapability::AddFB2Modifiers)
        .is_ok_and(|value| value != 0)
}