| 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 | |