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}