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