Skip to main content

drmkit_core/
device.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! The DRM device handle.
5
6use std::os::fd::{AsFd, AsRawFd as _, BorrowedFd, OwnedFd, RawFd};
7use std::path::Path;
8
9use drm::ClientCapability;
10use drm::control::Device as ControlDevice;
11
12use rustix::io::Errno;
13
14use crate::error::{CoreError, Result};
15
16/// How a [`Device`] holds its descriptor.
17#[derive(Debug)]
18enum Fd {
19    /// Opened by this crate; closed on drop.
20    Owned(OwnedFd),
21    /// Owned elsewhere — typically a revocable descriptor from a libseat
22    /// session. Never closed here.
23    Borrowed(RawFd),
24}
25
26/// An open DRM device.
27///
28/// Port of `drm::Device` from `src/core/device.{hpp,cpp}`.
29///
30/// Implements the `drm` crate's [`Device`](drm::Device) and
31/// [`control::Device`](drm::control::Device) traits, so every KMS operation
32/// those traits provide is available directly. drmkit wraps rather than
33/// re-binds (DR-2); the methods here are the ones the C++ adds on top or gives
34/// a different contract.
35#[derive(Debug)]
36pub struct Device {
37    fd: Fd,
38}
39
40impl Device {
41    /// Open a DRM device node and verify the kernel agrees it is one.
42    ///
43    /// Sets DRM master if possible. Failing to become master is **not** an
44    /// error: a client running under a compositor never will be, and the C++
45    /// ignores the result for the same reason.
46    ///
47    /// # Errors
48    ///
49    /// - [`CoreError::Io`] if the node cannot be opened.
50    /// - [`CoreError::NotADrmDevice`] if `drmGetVersion` fails on it, which is
51    ///   how the C++ distinguishes a real card from any other character device.
52    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
53        let path = path.as_ref();
54        let file = rustix::fs::open(
55            path,
56            rustix::fs::OFlags::RDWR | rustix::fs::OFlags::CLOEXEC,
57            rustix::fs::Mode::empty(),
58        )
59        // Not `from_errno`: `EACCES` here is the node's permissions, not DRM
60        // master, and the reference returns the errno as it is.
61        .map_err(CoreError::Io)?;
62
63        let device = Self {
64            fd: Fd::Owned(file),
65        };
66
67        // Verify this is actually a DRM device.
68        if drm::Device::get_driver(&device).is_err() {
69            return Err(CoreError::NotADrmDevice {
70                path: path.display().to_string(),
71            });
72        }
73
74        // Non-fatal: a client under a compositor is never master.
75        if let Err(e) = drm::Device::acquire_master_lock(&device) {
76            drmkit_log::log_debug!(
77                "could not become DRM master on {}: {e} (expected under a compositor)",
78                path.display()
79            );
80        }
81
82        Ok(device)
83    }
84
85    /// Take ownership of an already-open DRM descriptor.
86    ///
87    /// The descriptor is closed when the device is dropped. This has no C++
88    /// counterpart: `Device::from_fd` there is always non-owning, because C++
89    /// cannot express the transfer in a type.
90    #[must_use]
91    pub const fn from_owned_fd(fd: OwnedFd) -> Self {
92        Self { fd: Fd::Owned(fd) }
93    }
94
95    /// Wrap a descriptor owned by someone else.
96    ///
97    /// The returned device does **not** close `fd` on drop; the caller — for
98    /// instance a seat session holding a revocable libseat descriptor — keeps
99    /// that responsibility. Resume flows replace the device wholesale with a
100    /// freshly built one, exactly as the C++ move-assigns a new `from_fd`.
101    ///
102    /// # Safety
103    ///
104    /// `fd` must be a valid descriptor that stays open for the whole life of
105    /// the returned `Device` and of everything derived from it. This cannot be
106    /// checked: the borrow has no lifetime to tie it to, which is precisely why
107    /// the safe [`Device::from_owned_fd`] exists and should be preferred.
108    #[must_use]
109    pub const unsafe fn from_borrowed_fd(fd: RawFd) -> Self {
110        Self {
111            fd: Fd::Borrowed(fd),
112        }
113    }
114
115    /// The raw descriptor, for interop with code that needs one.
116    ///
117    /// Borrowed, never transferred.
118    #[must_use]
119    pub fn raw_fd(&self) -> RawFd {
120        match &self.fd {
121            Fd::Owned(fd) => fd.as_raw_fd(),
122            Fd::Borrowed(fd) => *fd,
123        }
124    }
125
126    /// Whether this device will close its descriptor on drop.
127    #[must_use]
128    pub const fn owns_fd(&self) -> bool {
129        matches!(self.fd, Fd::Owned(_))
130    }
131
132    /// Enable the universal-planes client capability.
133    ///
134    /// Without it the kernel hides primary and cursor planes from
135    /// `plane_handles`, and the allocator sees only overlays.
136    ///
137    /// # Errors
138    ///
139    /// [`CoreError::Io`] if the driver rejects the capability.
140    pub fn enable_universal_planes(&self) -> Result<()> {
141        self.set_client_cap(ClientCapability::UniversalPlanes, true)
142    }
143
144    /// Enable the atomic client capability.
145    ///
146    /// # Errors
147    ///
148    /// [`CoreError::Io`] if the driver does not support atomic modesetting.
149    pub fn enable_atomic(&self) -> Result<()> {
150        self.set_client_cap(ClientCapability::Atomic, true)
151    }
152
153    /// Set an arbitrary client capability.
154    ///
155    /// # Errors
156    ///
157    /// [`CoreError::Io`] if the driver rejects it.
158    pub fn set_client_cap(&self, cap: ClientCapability, enable: bool) -> Result<()> {
159        drm::Device::set_client_capability(self, cap, enable).map_err(|e| CoreError::from_io(&e))
160    }
161
162    /// Become DRM master.
163    ///
164    /// # Errors
165    ///
166    /// [`CoreError::Io`] with `EBUSY` if another client already holds it, or
167    /// [`CoreError::NotMaster`] if the caller lacks the privilege to take it.
168    pub fn set_master(&self) -> Result<()> {
169        drm::Device::acquire_master_lock(self).map_err(|e| CoreError::from_io(&e))
170    }
171
172    /// Release DRM master.
173    ///
174    /// # Errors
175    ///
176    /// [`CoreError::Io`] if the ioctl fails.
177    pub fn drop_master(&self) -> Result<()> {
178        drm::Device::release_master_lock(self).map_err(|e| CoreError::from_io(&e))
179    }
180
181    /// Read an immutable blob property's payload.
182    ///
183    /// This is the seam `drmkit-fmt` is built around: the `IN_FORMATS` blob is
184    /// fetched here and parsed by
185    /// [`FormatTable::from_blob`](https://docs.rs/drmkit-fmt), which keeps the
186    /// format crate device-free and `no_std`.
187    ///
188    /// # Errors
189    ///
190    /// [`CoreError::Io`] if the blob id is unknown to the kernel.
191    pub fn property_blob(&self, blob_id: u64) -> Result<Vec<u8>> {
192        self.get_property_blob(blob_id)
193            .map_err(|e| CoreError::from_io(&e))
194    }
195}
196
197/// A kernel property blob, freed when it is dropped.
198///
199/// `MODE_ID` takes a blob id rather than a value, so a scene that sets a mode
200/// has to create one and keep it alive for as long as the mode is set. Letting
201/// it leak would be invisible until a long-running compositor that re-modesets
202/// often ran the kernel out of blob ids.
203///
204/// The borrow of the device is what makes freeing it safe. Holding a raw
205/// descriptor instead would compile, and would destroy a blob on whatever the
206/// kernel had since recycled that number into if the blob ever outlived the
207/// device it came from.
208pub struct PropertyBlob<'a> {
209    id: u64,
210    device: &'a Device,
211}
212
213impl PropertyBlob<'_> {
214    /// The blob id, to be written into a blob-typed property.
215    #[must_use]
216    pub const fn id(&self) -> u64 {
217        self.id
218    }
219
220    /// Give up the blob without asking the kernel to destroy it.
221    ///
222    /// For when the descriptor the blob was created on is gone -- a libseat
223    /// session pause revokes it, and the kernel reclaimed every blob on it at
224    /// that moment. Destroying through the replacement descriptor would at
225    /// best fail and at worst free a *different* blob that happens to have
226    /// been given the same id there.
227    ///
228    /// This leaks nothing: the blob is already gone. Do not reach for it to
229    /// silence an ordinary drop -- on a live descriptor that really is a leak,
230    /// and a long-running compositor that re-modesets will run the kernel out
231    /// of blob ids.
232    pub fn forget(self) {
233        core::mem::forget(self);
234    }
235}
236
237impl core::fmt::Debug for PropertyBlob<'_> {
238    /// The id, without the device -- a blob is identified by its id, and
239    /// printing the device behind it would be noise at best.
240    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
241        f.debug_struct("PropertyBlob")
242            .field("id", &self.id)
243            .finish()
244    }
245}
246
247impl Drop for PropertyBlob<'_> {
248    fn drop(&mut self) {
249        // Nothing useful to do if this fails: the blob dies with the device
250        // either way, and panicking in `drop` would be worse than the leak.
251        let _ = drm::control::Device::destroy_property_blob(self.device, self.id);
252    }
253}
254
255impl Device {
256    /// Stage `data` as a kernel property blob.
257    ///
258    /// The blob borrows the device, so it cannot outlive the descriptor that
259    /// has to free it.
260    ///
261    /// # Errors
262    ///
263    /// [`CoreError`] if the kernel refuses to create the blob.
264    pub fn create_property_blob<T: ?Sized>(&self, data: &T) -> Result<PropertyBlob<'_>> {
265        let value = drm::control::Device::create_property_blob(self, data)
266            .map_err(|e| CoreError::Io(Errno::from_io_error(&e).unwrap_or(Errno::INVAL)))?;
267        // The call is documented to return a blob value. A different variant
268        // would mean the crate changed under us, and writing whatever integer
269        // it carried into a blob property would be worse than failing here.
270        let drm::control::property::Value::Blob(id) = value else {
271            return Err(CoreError::Io(Errno::INVAL));
272        };
273        Ok(PropertyBlob { id, device: self })
274    }
275}
276
277impl AsFd for Device {
278    fn as_fd(&self) -> BorrowedFd<'_> {
279        match &self.fd {
280            Fd::Owned(fd) => fd.as_fd(),
281            // SAFETY: the constructor is `unsafe` precisely because the caller
282            // promised this descriptor outlives the `Device`. The borrow here
283            // is bounded by `&self`, which is shorter.
284            Fd::Borrowed(raw) => unsafe { BorrowedFd::borrow_raw(*raw) },
285        }
286    }
287}
288
289impl drm::Device for Device {}
290impl drm::control::Device for Device {}
291
292/// The modifier to declare when registering a framebuffer, or `None` to
293/// take the legacy path, which leaves the layout to the driver.
294///
295/// Port of upstream's `addfb2_needs_modifiers` (`be147be`), so every
296/// import agrees on it:
297/// * `INVALID` never declares: it means "no explicit modifier", and passing
298///   it with `DRM_MODE_FB_MODIFIERS` is ill-formed.
299/// * `LINEAR` declares only when the driver took `DRM_CAP_ADDFB2_MODIFIERS`.
300///   Drivers without it (i.MX LCDIF, tilcdc) reject the flag outright, even
301///   for linear -- which is exactly their implicit layout, so the legacy
302///   path imports the same buffer. Where the cap exists it is declared,
303///   because "no modifier" there means the driver's default, and that is
304///   not promised to be linear.
305/// * Anything else always declares. A driver without the cap cannot scan
306///   it out either way, and the kernel says so.
307#[must_use]
308pub fn framebuffer_modifier(device: &impl drm::Device, modifier: u64) -> Option<u64> {
309    declared_modifier(modifier, || supports_framebuffer_modifiers(device))
310}
311
312/// [`framebuffer_modifier`]'s rule, with the capability supplied. Asked only
313/// for `LINEAR`, the one case it decides.
314pub(crate) fn declared_modifier(
315    modifier: u64,
316    takes_modifiers: impl FnOnce() -> bool,
317) -> Option<u64> {
318    const LINEAR: u64 = 0;
319    const INVALID: u64 = (1 << 56) - 1;
320    match modifier {
321        INVALID => None,
322        LINEAR => takes_modifiers().then_some(LINEAR),
323        _ => Some(modifier),
324    }
325}
326
327/// Whether the driver accepts `DRM_MODE_FB_MODIFIERS`
328/// (`DRM_CAP_ADDFB2_MODIFIERS`).
329#[must_use]
330pub fn supports_framebuffer_modifiers(device: &impl drm::Device) -> bool {
331    device
332        .get_driver_capability(drm::DriverCapability::AddFB2Modifiers)
333        .is_ok_and(|value| value != 0)
334}