drm/node/
mod.rs

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) => Display::fmt(err, f),
184            Self::NotDrmNode => {
185                f.write_str("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) => 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 = 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(path.as_ref()).map_err(Into::<io::Error>::into)?;
263    dev_path(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}