1//! Module for abstractions on drm device nodes.
2
3pub mod constants;
4
5use std::error::Error;
6use std::fmt::{self, Debug, Display, Formatter};
7use std::io;
8use std::os::unix::io::AsFd;
9use std::path::{Path, PathBuf};
10
11use rustix::fs::{fstat, major, minor, stat, Dev as dev_t, Stat};
12
13use crate::node::constants::*;
14
15/// A node which refers to a DRM device.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct DrmNode {
18 dev: dev_t,
19 ty: NodeType,
20}
21
22impl DrmNode {
23 /// Creates a DRM node from an open drm device.
24 pub fn from_file<A: AsFd>(file: A) -> Result<DrmNode, CreateDrmNodeError> {
25 let stat = fstat(file).map_err(Into::<io::Error>::into)?;
26 DrmNode::from_stat(stat)
27 }
28
29 /// Creates a DRM node from path.
30 pub fn from_path<A: AsRef<Path>>(path: A) -> Result<DrmNode, CreateDrmNodeError> {
31 let stat = stat(path.as_ref()).map_err(Into::<io::Error>::into)?;
32 DrmNode::from_stat(stat)
33 }
34
35 /// Creates a DRM node from a file stat.
36 pub fn from_stat(stat: Stat) -> Result<DrmNode, CreateDrmNodeError> {
37 let dev = stat.st_rdev;
38 DrmNode::from_dev_id(dev)
39 }
40
41 /// Creates a DRM node from a [`dev_t`].
42 pub fn from_dev_id(dev: dev_t) -> Result<Self, CreateDrmNodeError> {
43 if !is_device_drm(dev) {
44 return Err(CreateDrmNodeError::NotDrmNode);
45 }
46
47 // The type of the DRM node is determined by the minor number ranges:
48 // 0 - 63 -> Primary
49 // 64 - 127 -> Control
50 // 128 - 255 -> Render
51 let ty = match minor(dev) >> 6 {
52 0 => NodeType::Primary,
53 1 => NodeType::Control,
54 2 => NodeType::Render,
55 _ => return Err(CreateDrmNodeError::NotDrmNode),
56 };
57
58 Ok(DrmNode { dev, ty })
59 }
60
61 /// Returns the type of the DRM node.
62 pub fn ty(&self) -> NodeType {
63 self.ty
64 }
65
66 /// Returns the device_id of the underlying DRM node.
67 pub fn dev_id(&self) -> dev_t {
68 self.dev
69 }
70
71 /// Returns the path of the open device if possible.
72 pub fn dev_path(&self) -> Option<PathBuf> {
73 node_path(self, self.ty).ok()
74 }
75
76 /// Returns the path of the specified node type matching the device, if available.
77 pub fn dev_path_with_type(&self, ty: NodeType) -> Option<PathBuf> {
78 node_path(self, ty).ok()
79 }
80
81 /// Returns a new node of the specified node type matching the device, if available.
82 pub fn node_with_type(&self, ty: NodeType) -> Option<Result<DrmNode, CreateDrmNodeError>> {
83 self.dev_path_with_type(ty).map(DrmNode::from_path)
84 }
85
86 /// Returns the major device number of the DRM device.
87 pub fn major(&self) -> u32 {
88 major(self.dev_id())
89 }
90
91 /// Returns the minor device number of the DRM device.
92 pub fn minor(&self) -> u32 {
93 minor(self.dev_id())
94 }
95
96 /// Returns whether the DRM device has render nodes.
97 pub fn has_render(&self) -> bool {
98 #[cfg(target_os = "linux")]
99 {
100 node_path(self, NodeType::Render).is_ok()
101 }
102
103 // TODO: More robust checks on non-linux.
104
105 #[cfg(target_os = "freebsd")]
106 {
107 false
108 }
109
110 #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
111 {
112 false
113 }
114 }
115}
116
117impl Display for DrmNode {
118 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
119 write!(f, "{}{}", self.ty.minor_name_prefix(), minor(self.dev_id()))
120 }
121}
122
123/// A type of node
124#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
125pub enum NodeType {
126 /// A primary node may be used to allocate buffers.
127 ///
128 /// If no other node is present, this may be used to post a buffer to an output with mode-setting.
129 Primary,
130
131 /// A control node may be used for mode-setting.
132 ///
133 /// This is almost never used since no DRM API for control nodes is available yet.
134 Control,
135
136 /// A render node may be used by a client to allocate buffers.
137 ///
138 /// Mode-setting is not possible with a render node.
139 Render,
140}
141
142impl NodeType {
143 /// Returns a string representing the prefix of a minor device's name.
144 ///
145 /// For example, on Linux with a primary node, the returned string would be `card`.
146 pub fn minor_name_prefix(&self) -> &'static str {
147 match self {
148 NodeType::Primary => PRIMARY_NAME,
149 NodeType::Control => CONTROL_NAME,
150 NodeType::Render => RENDER_NAME,
151 }
152 }
153
154 #[cfg(not(target_os = "linux"))]
155 fn minor_base(&self) -> u32 {
156 match self {
157 NodeType::Primary => 0,
158 NodeType::Control => 64,
159 NodeType::Render => 128,
160 }
161 }
162}
163
164impl Display for NodeType {
165 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
166 Debug::fmt(self, f)
167 }
168}
169
170/// An error that may occur when creating a [`DrmNode`] from a file descriptor.
171#[derive(Debug)]
172pub enum CreateDrmNodeError {
173 /// Some underlying IO error occured while trying to create a DRM node.
174 Io(io::Error),
175
176 /// The provided file descriptor does not refer to a DRM node.
177 NotDrmNode,
178}
179
180impl Display for CreateDrmNodeError {
181 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
182 match self {
183 Self::Io(err: &Error) => Display::fmt(self:err, f),
184 Self::NotDrmNode => {
185 f.write_str(data:"the provided file descriptor does not refer to a DRM node")
186 }
187 }
188 }
189}
190
191impl Error for CreateDrmNodeError {
192 fn source(&self) -> Option<&(dyn Error + 'static)> {
193 match self {
194 Self::Io(err: &Error) => Some(err),
195 Self::NotDrmNode => None,
196 }
197 }
198}
199
200impl From<io::Error> for CreateDrmNodeError {
201 #[inline]
202 fn from(err: io::Error) -> Self {
203 CreateDrmNodeError::Io(err)
204 }
205}
206
207#[cfg(target_os = "freebsd")]
208fn devname(dev: dev_t) -> Option<String> {
209 use std::os::raw::{c_char, c_int};
210
211 // Matching value of SPECNAMELEN in FreeBSD 13+
212 let mut dev_name = vec![0u8; 255];
213
214 let buf: *mut c_char = unsafe {
215 libc::devname_r(
216 dev,
217 libc::S_IFCHR, // Must be S_IFCHR or S_IFBLK
218 dev_name.as_mut_ptr() as *mut c_char,
219 dev_name.len() as c_int,
220 )
221 };
222
223 // Buffer was too small (weird issue with the size of buffer) or the device could not be named.
224 if buf.is_null() {
225 return None;
226 }
227
228 // SAFETY: The buffer written to by devname_r is guaranteed to be NUL terminated.
229 unsafe { dev_name.set_len(libc::strlen(buf)) };
230
231 Some(String::from_utf8(dev_name).expect("Returned device name is not valid utf8"))
232}
233
234/// Returns if the given device by major:minor pair is a DRM device.
235#[cfg(target_os = "linux")]
236pub fn is_device_drm(dev: dev_t) -> bool {
237 // We `stat` the path rather than comparing the major to support dynamic device numbers:
238 // https://gitlab.freedesktop.org/mesa/drm/-/commit/f8392583418aef5e27bfed9989aeb601e20cc96d
239 let path: String = format!("/sys/dev/char/{}:{}/device/drm", major(dev), minor(dev));
240 stat(path.as_str()).is_ok()
241}
242
243/// Returns if the given device by major:minor pair is a DRM device.
244#[cfg(target_os = "freebsd")]
245pub fn is_device_drm(dev: dev_t) -> bool {
246 devname(dev).map_or(false, |dev_name| {
247 dev_name.starts_with("drm/")
248 || dev_name.starts_with("dri/card")
249 || dev_name.starts_with("dri/control")
250 || dev_name.starts_with("dri/renderD")
251 })
252}
253
254/// Returns if the given device by major:minor pair is a DRM device.
255#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
256pub fn is_device_drm(dev: dev_t) -> bool {
257 major(dev) == DRM_MAJOR
258}
259
260/// Returns the path of a specific type of node from the same DRM device as another path of the same node.
261pub fn path_to_type<P: AsRef<Path>>(path: P, ty: NodeType) -> io::Result<PathBuf> {
262 let stat: stat = stat(path.as_ref()).map_err(op:Into::<io::Error>::into)?;
263 dev_path(dev:stat.st_rdev, ty)
264}
265
266/// Returns the path of a specific type of node from the same DRM device as an existing [`DrmNode`].
267pub fn node_path(node: &DrmNode, ty: NodeType) -> io::Result<PathBuf> {
268 dev_path(node.dev, ty)
269}
270
271/// Returns the path of a specific type of node from the DRM device described by major and minor device numbers.
272#[cfg(target_os = "linux")]
273pub fn dev_path(dev: dev_t, ty: NodeType) -> io::Result<PathBuf> {
274 use std::fs;
275 use std::io::ErrorKind;
276
277 if !is_device_drm(dev) {
278 return Err(io::Error::new(
279 ErrorKind::NotFound,
280 format!("{}:{} is no DRM device", major(dev), minor(dev)),
281 ));
282 }
283
284 let read = fs::read_dir(format!(
285 "/sys/dev/char/{}:{}/device/drm",
286 major(dev),
287 minor(dev)
288 ))?;
289
290 for entry in read.flatten() {
291 let name = entry.file_name();
292 let name = name.to_string_lossy();
293
294 // Only 1 primary, control and render node may exist simultaneously, so the
295 // first occurrence is good enough.
296 if name.starts_with(ty.minor_name_prefix()) {
297 let path = Path::new("/dev/dri").join(&*name);
298 if path.exists() {
299 return Ok(path);
300 }
301 }
302 }
303
304 Err(io::Error::new(
305 ErrorKind::NotFound,
306 format!(
307 "Could not find node of type {} from DRM device {}:{}",
308 ty,
309 major(dev),
310 minor(dev)
311 ),
312 ))
313}
314
315/// Returns the path of a specific type of node from the DRM device described by major and minor device numbers.
316#[cfg(target_os = "freebsd")]
317pub fn dev_path(dev: dev_t, ty: NodeType) -> io::Result<PathBuf> {
318 // Based on libdrm `drmGetMinorNameForFD`. Should be updated if the code
319 // there is replaced with anything more sensible...
320
321 use std::io::ErrorKind;
322
323 if !is_device_drm(dev) {
324 return Err(io::Error::new(
325 ErrorKind::NotFound,
326 format!("{}:{} is no DRM device", major(dev), minor(dev)),
327 ));
328 }
329
330 if let Some(dev_name) = devname(dev) {
331 let suffix = dev_name.trim_start_matches(|c: char| !c.is_numeric());
332 if let Ok(old_id) = suffix.parse::<u32>() {
333 let id_mask = 0b11_1111;
334 let id = old_id & id_mask + ty.minor_base();
335 let path = PathBuf::from(format!("/dev/dri/{}{}", ty.minor_name_prefix(), id));
336 if path.exists() {
337 return Ok(path);
338 }
339 }
340 }
341
342 Err(io::Error::new(
343 ErrorKind::NotFound,
344 format!(
345 "Could not find node of type {} from DRM device {}:{}",
346 ty,
347 major(dev),
348 minor(dev)
349 ),
350 ))
351}
352
353/// Returns the path of a specific type of node from the DRM device described by major and minor device numbers.
354#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
355pub fn dev_path(dev: dev_t, ty: NodeType) -> io::Result<PathBuf> {
356 use std::io::ErrorKind;
357
358 if !is_device_drm(dev) {
359 return Err(io::Error::new(
360 ErrorKind::NotFound,
361 format!("{}:{} is no DRM device", major(dev), minor(dev)),
362 ));
363 }
364
365 let old_id = minor(dev);
366 let id_mask = 0b11_1111;
367 let id = old_id & id_mask + ty.minor_base();
368 let path = PathBuf::from(format!("/dev/dri/{}{}", ty.minor_name_prefix(), id));
369 if path.exists() {
370 return Ok(path);
371 }
372
373 Err(io::Error::new(
374 ErrorKind::NotFound,
375 format!(
376 "Could not find node of type {} for DRM device {}:{}",
377 ty,
378 major(dev),
379 minor(dev)
380 ),
381 ))
382}
383