1 | //! Module for abstractions on drm device nodes. |
2 | |
3 | pub mod constants; |
4 | |
5 | use std::error::Error; |
6 | use std::fmt::{self, Debug, Display, Formatter}; |
7 | use std::io; |
8 | use std::os::unix::io::AsFd; |
9 | use std::path::{Path, PathBuf}; |
10 | |
11 | use rustix::fs::{fstat, major, minor, stat, Dev as dev_t, Stat}; |
12 | |
13 | use crate::node::constants::*; |
14 | |
15 | /// A node which refers to a DRM device. |
16 | #[derive (Debug, Clone, Copy, PartialEq, Eq, Hash)] |
17 | pub struct DrmNode { |
18 | dev: dev_t, |
19 | ty: NodeType, |
20 | } |
21 | |
22 | impl 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 | |
117 | impl 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)] |
125 | pub 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 | |
142 | impl 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 | |
164 | impl 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)] |
172 | pub 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 | |
180 | impl 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 | |
191 | impl 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 | |
200 | impl 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" )] |
208 | fn 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" )] |
236 | pub 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" )] |
245 | pub 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" )))] |
256 | pub 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. |
261 | pub 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`]. |
267 | pub 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" )] |
273 | pub 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" )] |
317 | pub 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" )))] |
355 | pub 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 | |