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}