1pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct DrmNode {
18 dev: dev_t,
19 ty: NodeType,
20}
21
22impl DrmNode {
23 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 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 pub fn from_stat(stat: Stat) -> Result<DrmNode, CreateDrmNodeError> {
37 let dev = stat.st_rdev;
38 DrmNode::from_dev_id(dev)
39 }
40
41 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 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 pub fn ty(&self) -> NodeType {
63 self.ty
64 }
65
66 pub fn dev_id(&self) -> dev_t {
68 self.dev
69 }
70
71 pub fn dev_path(&self) -> Option<PathBuf> {
73 node_path(self, self.ty).ok()
74 }
75
76 pub fn dev_path_with_type(&self, ty: NodeType) -> Option<PathBuf> {
78 node_path(self, ty).ok()
79 }
80
81 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 pub fn major(&self) -> u32 {
88 major(self.dev_id())
89 }
90
91 pub fn minor(&self) -> u32 {
93 minor(self.dev_id())
94 }
95
96 pub fn has_render(&self) -> bool {
98 #[cfg(target_os = "linux")]
99 {
100 node_path(self, NodeType::Render).is_ok()
101 }
102
103 #[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#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
125pub enum NodeType {
126 Primary,
130
131 Control,
135
136 Render,
140}
141
142impl NodeType {
143 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#[derive(Debug)]
172pub enum CreateDrmNodeError {
173 Io(io::Error),
175
176 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 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, dev_name.as_mut_ptr() as *mut c_char,
219 dev_name.len() as c_int,
220 )
221 };
222
223 if buf.is_null() {
225 return None;
226 }
227
228 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#[cfg(target_os = "linux")]
236pub fn is_device_drm(dev: dev_t) -> bool {
237 let path = format!("/sys/dev/char/{}:{}/device/drm", major(dev), minor(dev));
240 stat(path.as_str()).is_ok()
241}
242
243#[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#[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
260pub 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
266pub fn node_path(node: &DrmNode, ty: NodeType) -> io::Result<PathBuf> {
268 dev_path(node.dev, ty)
269}
270
271#[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 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#[cfg(target_os = "freebsd")]
317pub fn dev_path(dev: dev_t, ty: NodeType) -> io::Result<PathBuf> {
318 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#[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}