diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aedca5..c10762e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ - Support for `xdg_wm_base` protocol version 3 - Added the option to initialize the dmabuf global with a client filter +#### Backends + +- New `DrmNode` type in drm backend. This is primarily for use a backend which needs to run as client inside another session. + ### Bugfixes - EGLBufferReader now checks if buffers are alive before using them. diff --git a/Cargo.toml b/Cargo.toml index 6399415..42726c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ drm-ffi = { version = "0.2.0", optional = true } gbm = { version = "0.7.0", optional = true, default-features = false, features = ["drm-support"] } input = { version = "0.6", default-features = false, features=["libinput_1_14"], optional = true } lazy_static = "1" -libc = "0.2.70" +libc = "0.2.103" libseat= { version = "0.1.1", optional = true } libloading = { version="0.7.0", optional = true } nix = "0.22" diff --git a/src/backend/drm/mod.rs b/src/backend/drm/mod.rs index 89dc281..e677d57 100644 --- a/src/backend/drm/mod.rs +++ b/src/backend/drm/mod.rs @@ -1,6 +1,6 @@ //! This module represents abstraction on top the linux direct rendering manager api (drm). //! -//! ## DrmDevice +//! ## [`DrmDevice`] //! //! A device exposes certain properties, which are directly derived //! from the *device* as perceived by the direct rendering manager api (drm). These resources consists @@ -39,7 +39,7 @@ //! smithay does not make sure that `connectors` are not already in use by another `Surface`. Overlapping `connector`-Sets may //! be an error or result in undefined rendering behavior depending on the `Surface` implementation. //! -//! ## DrmSurface +//! ## [`DrmSurface`] //! //! A surface is a part of a `Device` that may output a picture to a number of connectors. It pumps pictures of buffers to outputs. //! @@ -61,15 +61,24 @@ //! //! Buffer management and details about the various types can be found in the [`allocator`-Module](crate::backend::allocator) and //! rendering abstractions, which can target these buffers can be found in the [`renderer`-Module](crate::backend::renderer). +//! +//! ## [`DrmNode`] +//! +//! A drm node refers to a drm device and the capabilities that may be performed using the node. +//! Generally [`DrmNode`] is primarily used by clients (such as the output backends) which need +//! to allocate buffers for use in X11 or Wayland. If you need to do mode setting, you should use +//! [`DrmDevice`] instead. pub(crate) mod device; pub(self) mod error; +pub(self) mod node; #[cfg(feature = "backend_session")] pub(self) mod session; pub(self) mod surface; pub use device::{DevPath, DrmDevice, DrmEvent}; pub use error::Error as DrmError; +pub use node::{ConvertErrorKind, ConvertNodeError, CreateDrmNodeError, DrmNode, NodeType}; #[cfg(feature = "backend_gbm")] pub use surface::gbm::{Error as GbmBufferedSurfaceError, GbmBufferedSurface}; pub use surface::DrmSurface; diff --git a/src/backend/drm/node/constants.rs b/src/backend/drm/node/constants.rs new file mode 100644 index 0000000..a85ee6d --- /dev/null +++ b/src/backend/drm/node/constants.rs @@ -0,0 +1,45 @@ +//! OS-Specific DRM constants + +// DRM major value. + +#[cfg(target_os = "dragonfly")] +pub const DRM_MAJOR: u64 = 145; + +#[cfg(target_os = "netbsd")] +pub const DRM_MAJOR: u64 = 34; + +#[cfg(all(target_os = "openbsd", target_arch = "i386"))] +pub const DRM_MAJOR: u64 = 88; + +#[cfg(all(target_os = "openbsd", not(target_arch = "i386")))] +pub const DRM_MAJOR: u64 = 87; + +#[cfg(not(any(target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd")))] +#[allow(dead_code)] // Not used on Linux +pub const DRM_MAJOR: u64 = 226; + +// DRM node prefixes + +#[cfg(not(target_os = "openbsd"))] +pub const PRIMARY_NAME: &str = "card"; + +#[cfg(target_os = "freebsd")] +pub const PRIMARY_NAME: &str = "drm"; + +#[cfg(not(target_os = "openbsd"))] +pub const CONTROL_NAME: &str = "controlD"; + +#[cfg(target_os = "freebsd")] +pub const CONTROL_NAME: &str = "drmC"; + +#[cfg(not(target_os = "openbsd"))] +pub const RENDER_NAME: &str = "renderD"; + +#[cfg(target_os = "freebsd")] +pub const RENDER_NAME: &str = "drmR"; + +#[cfg(not(target_os = "openbsd"))] +pub const DIR_NAME: &str = "dev"; + +#[cfg(target_os = "openbsd")] +pub const DIR_NAME: &str = "dev/dri"; diff --git a/src/backend/drm/node/mod.rs b/src/backend/drm/node/mod.rs new file mode 100644 index 0000000..b2374f8 --- /dev/null +++ b/src/backend/drm/node/mod.rs @@ -0,0 +1,380 @@ +pub(crate) mod constants; + +use constants::*; +use libc::dev_t; + +use std::{ + fmt::{self, Display, Formatter}, + fs, io, + os::unix::prelude::{AsRawFd, IntoRawFd, RawFd}, + path::PathBuf, +}; + +use nix::{ + fcntl::{self, OFlag}, + sys::stat::{fstat, major, minor, Mode}, + unistd::close, +}; + +/// A node which refers to a DRM device. +#[derive(Debug)] +pub struct DrmNode { + // Always `Some`, None variant is used when taking ownership + fd: Option, + dev: dev_t, + ty: NodeType, +} + +impl DrmNode { + /// Creates a DRM node from a file descriptor. + /// + /// This function takes ownership of the passed in file descriptor, which will be closed when + /// dropped. + pub fn from_fd(fd: A) -> Result { + let fd = fd.as_raw_fd(); + let stat = fstat(fd).map_err(Into::::into)?; + let dev = stat.st_rdev; + let major = major(dev); + let minor = minor(dev); + + if !is_device_drm(major, minor) { + return Err(CreateDrmNodeError::NotDrmNode); + } + + /* + The type of the DRM node is determined by the minor number ranges. + + 0-63 -> Primary + 64-127 -> Control + 128-255 -> Render + */ + let ty = match minor >> 6 { + 0 => NodeType::Primary, + 1 => NodeType::Control, + 2 => NodeType::Render, + _ => return Err(CreateDrmNodeError::NotDrmNode), + }; + + Ok(DrmNode { + fd: Some(fd), + dev, + ty, + }) + } + + /// Creates a DRM node of the specified type using the same DRM device as the provided node. + /// The provided node will be consumed if the new node is successfully created. + /// + /// This function is useful for obtaining the render node of a DRM device when the provided + /// node is a primary node. + pub fn from_node_with_type(node: DrmNode, ty: NodeType) -> Result { + from_node_with_type(node, ty) + } + + /// Returns the type of the DRM node. + pub fn ty(&self) -> NodeType { + self.ty + } + + /// Returns the device_id of the underlying DRM node. + pub fn dev_id(&self) -> dev_t { + self.dev + } + + /// Returns the path of the open device if possible. + pub fn dev_path(&self) -> Option { + fs::read_link(format!("/proc/self/fd/{:?}", self.as_raw_fd())).ok() + } + + /// Returns the major device number of the DRM device. + pub fn major(&self) -> u64 { + major(self.dev_id()) + } + + /// Returns the minor device number of the DRM device. + pub fn minor(&self) -> u64 { + minor(self.dev_id()) + } + + /// Returns whether the DRM device has render nodes. + pub fn has_render(&self) -> bool { + #[cfg(target_os = "linux")] + { + node_path(self, NodeType::Render).is_ok() + } + + // TODO: More robust checks on non-linux. + #[cfg(target_os = "freebsd")] + { + false + } + + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + { + false + } + } +} + +impl Display for DrmNode { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", self.ty.minor_name_prefix(), minor(self.dev_id())) + } +} + +impl IntoRawFd for DrmNode { + fn into_raw_fd(mut self) -> RawFd { + self.fd.take().unwrap() + } +} + +impl AsRawFd for DrmNode { + fn as_raw_fd(&self) -> RawFd { + self.fd.unwrap() + } +} + +impl Drop for DrmNode { + fn drop(&mut self) { + if let Some(fd) = self.fd { + let _ = close(fd); + } + } +} + +/// A type of node +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum NodeType { + /// A primary node may be used to allocate buffers. + /// + /// If no other node is present, this may be used to post a buffer to an output with mode-setting. + Primary, + + /// A control node may be used for mode-setting. + /// + /// This is almost never used since no DRM API for control nodes is available yet. + Control, + + /// A render node may be used by a client to allocate buffers. + /// + /// Mode-setting is not possible with a render node. + Render, +} + +impl NodeType { + /// Returns a string representing the prefix of a minor device's name. + /// + /// For example, on Linux with a primary node, the returned string would be `card`. + pub fn minor_name_prefix(&self) -> &str { + match self { + NodeType::Primary => PRIMARY_NAME, + NodeType::Control => CONTROL_NAME, + NodeType::Render => RENDER_NAME, + } + } +} + +impl Display for NodeType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + NodeType::Primary => "Primary", + NodeType::Control => "Control", + NodeType::Render => "Render", + } + ) + } +} + +/// An error that may occur when creating a DrmNode from a file descriptor. +#[derive(Debug, thiserror::Error)] +pub enum CreateDrmNodeError { + /// Some underlying IO error occured while trying to create a DRM node. + #[error("{0}")] + Io(io::Error), + + /// The provided file descriptor does not refer to a DRM node. + #[error("the provided file descriptor does not refer to a DRM node.")] + NotDrmNode, +} + +impl From for CreateDrmNodeError { + fn from(err: io::Error) -> Self { + CreateDrmNodeError::Io(err) + } +} + +/// A DRM node could not be used to get an alternative type of node. +/// +/// If this error is returned, the original DRM node may be retrieved using [`ConvertNodeError::node`]. +#[derive(Debug, thiserror::Error)] +#[error("{kind}")] +pub struct ConvertNodeError { + node: DrmNode, + kind: ConvertErrorKind, +} + +impl ConvertNodeError { + /// Returns the original DrmNode. + pub fn node(self) -> DrmNode { + self.node + } + + /// Returns the kind of error that occurred when obtaining a different type of DRM node. + pub fn kind(&self) -> &ConvertErrorKind { + &self.kind + } +} + +/// An error that may occur when obtaining a different type of DRM node. +#[derive(Debug, thiserror::Error)] +pub enum ConvertErrorKind { + /// The requested node type is not available. + #[error("the requested node type, {0}, is not available.")] + NodeTypeNotAvailable(NodeType), + + /// An IO error occurred when when obtaining a different type of DRM node. + #[error("{0}")] + Io(io::Error), +} + +impl From for ConvertErrorKind { + fn from(err: io::Error) -> Self { + ConvertErrorKind::Io(err) + } +} + +#[cfg(target_os = "linux")] +fn is_device_drm(major: u64, minor: u64) -> bool { + use nix::sys::stat::stat; + + let path = format!("/sys/dev/char/{}:{}/device/drm", major, minor); + stat(path.as_str()).is_ok() +} + +#[cfg(target_os = "freebsd")] +pub fn is_device_drm(major: u64, _minor: u64) -> bool { + use nix::sys::stat::makedev; + use nix::sys::stat::SFlag; + use std::ffi::CStr; + use std::os::raw::{c_char, c_int}; + + let dev_name = Vec::::with_capacity(255); // Matching value of SPECNAMELEN in FreeBSD 13+ + + let buf: *mut c_char = unsafe { + libc::devname_r( + makedev(major, minor), + SFlag::S_IFCHR.bits(), // Must be S_IFCHR or S_IFBLK + dev_name.as_mut_ptr(), + dev_name.len() as c_int, + ) + }; + + // Buffer was too small (weird issue with the size of buffer) or the device could not be named. + if buf.is_null() { + return Err(CreateDrmNodeError::NotDrmNode); + } + + // SAFETY: The buffer written to by devname_r is guaranteed to be NUL terminated. + let dev_name = unsafe { CStr::from_ptr(buf) }; + let dev_name = dev_name.to_str().expect("Returned device name is not valid utf8"); + + dev_name.starts_with("drm/") + || dev_name.starts_with("dri/card") + || dev_name.starts_with("dri/control") + || dev_name.starts_with("dri/renderD") +} + +#[cfg(not(any(target_os = "linux", target_os = "freebsd")))] +pub fn is_device_drm(major: u64, _minor: u64) -> bool { + major == DRM_MAJOR +} + +#[cfg(target_os = "linux")] +fn from_node_with_type(node: DrmNode, ty: NodeType) -> Result { + match node_path(&node, ty) { + Ok(path) => { + let fd = match fcntl::open(&path, OFlag::O_RDWR | OFlag::O_CLOEXEC, Mode::empty()) + .map_err(Into::::into) + .map_err(Into::::into) + { + Ok(fd) => fd, + Err(kind) => return Err(ConvertNodeError { node, kind }), + }; + + let result = DrmNode::from_fd(fd); + + match result { + Ok(node) => Ok(node), + + // Some error occurred while opening the new node + Err(CreateDrmNodeError::Io(err)) => Err(ConvertNodeError { + node, + kind: err.into(), + }), + + _ => unreachable!(), + } + } + + Err(err) => Err(ConvertNodeError { + node, + kind: err.into(), + }), + } +} + +#[cfg(target_os = "freebsd")] +fn from_node_with_type(node: DrmNode, ty: NodeType) -> Result { + // TODO: Not implemented yet, so fail conversion + Err(ConvertNodeError { + node, + kind: ConvertErrorKind::NodeTypeNotAvailable(ty), + }) +} + +#[cfg(not(any(target_os = "linux", target_os = "freebsd")))] +fn from_node_with_type(node: DrmNode, ty: NodeType) -> Result { + // TODO: Not implemented yet, so fail conversion + Err(ConvertNodeError { + node, + kind: ConvertErrorKind::NodeTypeNotAvailable(ty), + }) +} + +/// Returns the path of a specific type of node from the same DRM device as an existing DrmNode. +#[cfg(target_os = "linux")] +fn node_path(node: &DrmNode, ty: NodeType) -> io::Result { + use std::io::ErrorKind; + + let major = node.major(); + let minor = node.minor(); + + let read = fs::read_dir(format!("/sys/dev/char/{}:{}/device/drm", major, minor))?; + + for entry in read.flatten() { + let name = entry.file_name(); + let name = name.to_string_lossy(); + + // Only 1 primary, control and render node may exist simultaneously, so the + // first occurrence is good enough. + if name.starts_with(ty.minor_name_prefix()) { + let mut path = entry.path(); + path.push(DIR_NAME); + + if path.exists() { + return Ok(path); + } + } + } + + Err(io::Error::new( + ErrorKind::NotFound, + format!( + "Could not find node of type {} from DRM device {}:{}", + ty, major, minor + ), + )) +}