diff --git a/Cargo.toml b/Cargo.toml index 5e70305..e959cd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ rand = "0.8.4" slog = "2" slog-stdlog = { version = "4", optional = true } tempfile = { version = "3.0", optional = true } -thiserror = "1.0.2" +thiserror = "1.0.7" udev = { version = "0.6", optional = true } wayland-commons = { version = "0.29.0", optional = true } wayland-egl = { version = "0.29.0", optional = true } @@ -61,7 +61,7 @@ pkg-config = { version = "0.3.17", optional = true } [features] default = ["backend_drm", "backend_gbm", "backend_libinput", "backend_udev", "backend_session_logind", "backend_winit", "renderer_gl", "xwayland", "wayland_frontend", "slog-stdlog", "backend_x11"] backend_winit = ["winit", "wayland-server/dlopen", "backend_egl", "wayland-egl", "renderer_gl"] -backend_x11 = ["x11rb", "x11rb/dri3", "x11rb/xfixes", "x11rb/present", "x11rb_event_source", "backend_gbm", "backend_drm"] +backend_x11 = ["x11rb", "x11rb/dri3", "x11rb/xfixes", "x11rb/present", "x11rb_event_source", "backend_gbm", "backend_drm", "backend_egl"] backend_drm = ["drm", "drm-ffi"] backend_gbm = ["gbm"] backend_egl = ["gl_generator", "libloading"] diff --git a/anvil/src/x11.rs b/anvil/src/x11.rs index d06ae83..2da4ffb 100644 --- a/anvil/src/x11.rs +++ b/anvil/src/x11.rs @@ -12,6 +12,7 @@ use smithay::{ }, reexports::{ calloop::EventLoop, + gbm, wayland_server::{protocol::wl_output, Display}, }, wayland::{ @@ -53,13 +54,27 @@ pub fn run_x11(log: Logger) { let mut event_loop = EventLoop::try_new().unwrap(); let display = Rc::new(RefCell::new(Display::new())); - let (backend, surface) = - X11Backend::with_title("Anvil", log.clone()).expect("Failed to initialize X11 backend"); - let window = backend.window(); + let mut backend = X11Backend::with_title("Anvil", log.clone()).expect("Failed to initialize X11 backend"); + // Obtain the DRM node the X server uses for direct rendering. + let drm_node = backend + .drm_node() + .expect("Could not get DRM node used by X server"); + // Create the gbm device for buffer allocation and the X11 surface which presents to the window. + let device = gbm::Device::new(drm_node).expect("Failed to create gbm device"); // Initialize EGL using the GBM device setup earlier. - let egl = EGLDisplay::new(&surface, log.clone()).expect("Failed to create EGLDisplay"); + let egl = EGLDisplay::new(&device, log.clone()).expect("Failed to create EGLDisplay"); let context = EGLContext::new(&egl, log.clone()).expect("Failed to create EGLContext"); + let surface = X11Surface::new( + &mut backend, + device, + context + .dmabuf_render_formats() + .iter() + .map(|format| format.modifier), + ) + .expect("Failed to create X11 surface"); + let renderer = unsafe { Gles2Renderer::new(context, log.clone()) }.expect("Failed to initialize renderer"); let renderer = Rc::new(RefCell::new(renderer)); @@ -83,8 +98,10 @@ pub fn run_x11(log: Logger) { } } + let window = backend.window(); + let size = { - let s = backend.window().size(); + let s = window.size(); (s.w as i32, s.h as i32).into() }; @@ -202,7 +219,7 @@ pub fn run_x11(log: Logger) { #[cfg(feature = "debug")] let fps_texture = &backend_data.fps_texture; - if let Err(err) = renderer.bind(present.buffer()) { + if let Err(err) = renderer.bind(present.buffer().expect("gbm device was destroyed")) { error!(log, "Error while binding buffer: {}", err); } diff --git a/build.rs b/build.rs index 58e9097..5ebcb19 100644 --- a/build.rs +++ b/build.rs @@ -17,6 +17,12 @@ fn gl_generate() { "EGL_EXT_create_context_robustness", "EGL_KHR_create_context_no_error", "EGL_KHR_no_config_context", + "EGL_EXT_device_base", + "EGL_EXT_device_enumeration", + "EGL_EXT_device_query", + "EGL_EXT_device_drm", + "EGL_KHR_stream", + "EGL_KHR_stream_producer_eglsurface", "EGL_EXT_platform_base", "EGL_KHR_platform_x11", "EGL_EXT_platform_x11", @@ -24,6 +30,7 @@ fn gl_generate() { "EGL_EXT_platform_wayland", "EGL_KHR_platform_gbm", "EGL_MESA_platform_gbm", + "EGL_EXT_platform_device", "EGL_WL_bind_wayland_display", "EGL_KHR_image_base", "EGL_EXT_image_dma_buf_import", diff --git a/src/backend/drm/mod.rs b/src/backend/drm/mod.rs index e677d57..7a61840 100644 --- a/src/backend/drm/mod.rs +++ b/src/backend/drm/mod.rs @@ -71,14 +71,14 @@ pub(crate) mod device; pub(self) mod error; -pub(self) mod node; +pub 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}; +pub use node::{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 index a85ee6d..ed7a5cc 100644 --- a/src/backend/drm/node/constants.rs +++ b/src/backend/drm/node/constants.rs @@ -37,9 +37,3 @@ 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 index b2374f8..e53e420 100644 --- a/src/backend/drm/node/mod.rs +++ b/src/backend/drm/node/mod.rs @@ -1,3 +1,5 @@ +//! Module for abstractions on drm device nodes + pub(crate) mod constants; use constants::*; @@ -7,12 +9,11 @@ use std::{ fmt::{self, Display, Formatter}, fs, io, os::unix::prelude::{AsRawFd, IntoRawFd, RawFd}, - path::PathBuf, + path::{Path, PathBuf}, }; use nix::{ - fcntl::{self, OFlag}, - sys::stat::{fstat, major, minor, Mode}, + sys::stat::{fstat, major, minor, stat}, unistd::close, }; @@ -62,15 +63,6 @@ impl DrmNode { }) } - /// 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 @@ -83,7 +75,12 @@ impl DrmNode { /// 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() + node_path(self, self.ty).ok() + } + + /// Returns the path of the specified node type matching the open device if possible. + pub fn dev_path_with_type(&self, ty: NodeType) -> Option { + node_path(self, ty).ok() } /// Returns the major device number of the DRM device. @@ -206,54 +203,14 @@ impl From for CreateDrmNodeError { } } -/// 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) - } -} - +/// Returns if the given device by major:minor pair is a drm device #[cfg(target_os = "linux")] -fn is_device_drm(major: u64, minor: u64) -> bool { - use nix::sys::stat::stat; - +pub fn is_device_drm(major: u64, minor: u64) -> bool { let path = format!("/sys/dev/char/{}:{}/device/drm", major, minor); stat(path.as_str()).is_ok() } +/// Returns if the given device by major:minor pair is a drm device #[cfg(target_os = "freebsd")] pub fn is_device_drm(major: u64, _minor: u64) -> bool { use nix::sys::stat::makedev; @@ -287,71 +244,44 @@ pub fn is_device_drm(major: u64, _minor: u64) -> bool { || dev_name.starts_with("dri/renderD") } +/// Returns if the given device by major:minor pair is a drm device #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] pub fn is_device_drm(major: u64, _minor: u64) -> bool { major == DRM_MAJOR } +/// Returns the path of a specific type of node from the same DRM device as another path of the same node. #[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 }), - }; +pub fn path_to_type>(path: P, ty: NodeType) -> io::Result { + let stat = stat(path.as_ref()).map_err(Into::::into)?; + let dev = stat.st_rdev; + let major = major(dev); + let minor = minor(dev); - 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), - }) + dev_path(major, minor, 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; - +pub fn node_path(node: &DrmNode, ty: NodeType) -> io::Result { let major = node.major(); let minor = node.minor(); + dev_path(major, minor, ty) +} + +/// Returns the path of a specific type of node from the DRM device described by major and minor device numbers. +#[cfg(target_os = "linux")] +pub fn dev_path(major: u64, minor: u64, ty: NodeType) -> io::Result { + use std::io::ErrorKind; + + if !is_device_drm(major, minor) { + return Err(io::Error::new( + ErrorKind::NotFound, + format!("{}:{} is no DRM device", major, minor), + )); + } + let read = fs::read_dir(format!("/sys/dev/char/{}:{}/device/drm", major, minor))?; for entry in read.flatten() { @@ -361,9 +291,7 @@ fn node_path(node: &DrmNode, ty: NodeType) -> io::Result { // 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); - + let path = [r"/", "dev", "dri", &name].iter().collect::(); if path.exists() { return Ok(path); } diff --git a/src/backend/egl/device.rs b/src/backend/egl/device.rs new file mode 100644 index 0000000..5f8b4e0 --- /dev/null +++ b/src/backend/egl/device.rs @@ -0,0 +1,209 @@ +use std::{ffi::CStr, mem::MaybeUninit, os::raw::c_void, path::PathBuf, ptr}; + +use super::{ + ffi::{self, egl::types::EGLDeviceEXT}, + wrap_egl_call, EGLDisplay, EGLError, Error, +}; + +/// safe EGLDevice wrapper +#[derive(Debug)] +pub struct EGLDevice { + pub(super) inner: EGLDeviceEXT, + device_extensions: Vec, +} + +unsafe impl Send for EGLDevice {} + +impl EGLDevice { + /// Returns an iterator which enumerates over the available [`EGLDevices`](EGLDevice) on the system. + /// + /// This function will return an error if the following extensions are not available: + /// - [`EGL_EXT_device_base`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_base.txt) + /// - [`EGL_EXT_device_enumeration`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_enumeration.txt) + /// - [`EGL_EXT_device_query`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_query.txt) + pub fn enumerate() -> Result, Error> { + // Check the required extensions are present: + let extensions = ffi::make_sure_egl_is_loaded()?; + + if !extensions.iter().any(|s| s == "EGL_EXT_device_base") { + return Err(Error::EglExtensionNotSupported(&["EGL_EXT_device_base"])); + } + + if !extensions.iter().any(|s| s == "EGL_EXT_device_enumeration") { + return Err(Error::EglExtensionNotSupported(&["EGL_EXT_device_enumeration"])); + } + + if !extensions.iter().any(|s| s == "EGL_EXT_device_query") { + return Err(Error::EglExtensionNotSupported(&["EGL_EXT_device_query"])); + } + + // Yes, this is marked as `mut` even though the value is never mutated. EGL expects a mutable pointer + // for num_devices and will not modify the value if we are asking for pointers to some EGLDeviceEXT. + let mut device_amount = match wrap_egl_call(|| { + let mut amount: MaybeUninit = MaybeUninit::uninit(); + + // Passing 0 for max devices and a null-pointer for devices is safe because we indicate we only + // want the number of devices. + if unsafe { ffi::egl::QueryDevicesEXT(0, ptr::null_mut(), amount.as_mut_ptr()) } + == ffi::egl::FALSE + { + 0 + } else { + // SAFETY: Success + unsafe { amount.assume_init() } + } + }) { + Ok(number) => number, + Err(err) => return Err(Error::QueryDevices(err)), + }; + + let mut devices = Vec::with_capacity(device_amount as usize); + + wrap_egl_call(|| unsafe { + // SAFETY: + // - Vector used as pointer is correct size. + // - Device amount will accommodate all available devices because we have checked the size earlier. + ffi::egl::QueryDevicesEXT(device_amount, devices.as_mut_ptr(), &mut device_amount) + }) + .map_err(Error::QueryDevices)?; + + // Set the length of the vec so that rust does not think it is still empty. + + // SAFETY: + // 1) the vector is pre-allocated to the same size as the amount of returned devices. + // 2) EGL has initialized every value in the vector. + unsafe { devices.set_len(device_amount as usize) }; + + Ok(devices + .into_iter() + .map(|device| { + // SAFETY: We have queried that the extensions are valid and the device pointer is valid. + let device_extensions = unsafe { device_extensions(device) }?; + Ok(EGLDevice { + inner: device, + device_extensions, + }) + }) + .collect::, EGLError>>() + .map_err(Error::QueryDevices)? + .into_iter()) + } + + /// Returns the [`EGLDevices`](EGLDevice) related to the given `EGLDisplay`. + /// + /// This function will return an error if the following extensions are not available: + /// - [`EGL_EXT_device_base`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_base.txt) + /// - [`EGL_EXT_device_query`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_query.txt) + pub fn device_for_display(display: &EGLDisplay) -> Result { + // Check the required extensions are present: + let extensions = ffi::make_sure_egl_is_loaded()?; + + if !extensions.iter().any(|s| s == "EGL_EXT_device_base") { + return Err(Error::EglExtensionNotSupported(&["EGL_EXT_device_base"])); + } + + if !extensions.iter().any(|s| s == "EGL_EXT_device_query") { + return Err(Error::EglExtensionNotSupported(&["EGL_EXT_device_query"])); + } + + let mut device: ffi::egl::types::EGLAttrib = 0; + if unsafe { + ffi::egl::QueryDisplayAttribEXT( + display.get_display_handle().handle, + ffi::egl::DEVICE_EXT as i32, + &mut device as *mut _, + ) + } != ffi::egl::TRUE + { + return Err(Error::DisplayNotSupported); + } + + let device = device as EGLDeviceEXT; + + // Per the EGL specification: + // + // > Functions with a return type of EGLDeviceEXT will return this value on failure: EGL_NO_DEVICE_EXT + if device == ffi::egl::NO_DEVICE_EXT { + return Err(Error::DisplayNotSupported); + } + + // SAFETY: We have queried that the extensions are valid and the device pointer is valid. + let device_extensions = unsafe { device_extensions(device) }.map_err(Error::QueryDevices)?; + Ok(EGLDevice { + inner: device, + device_extensions, + }) + } + + /// Returns a list of extensions the device supports. + pub fn extensions(&self) -> Vec { + self.device_extensions.clone() + } + + /// Returns the path to the drm node of this EGLDevice. + /// + /// This function will return an error if the following extensions are not available: + /// - [`EGL_EXT_device_drm`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_drm.txt) + pub fn drm_device_path(&self) -> Result { + if !self.extensions().contains(&"EGL_EXT_device_drm".to_owned()) { + Err(Error::EglExtensionNotSupported(&["EGL_EXT_device_drm"])) + } else { + let raw_path = wrap_egl_call(|| unsafe { + ffi::egl::QueryDeviceStringEXT( + self.inner, + ffi::egl::DRM_DEVICE_FILE_EXT as ffi::egl::types::EGLint, + ) + }) + .expect("TODO: Add error variant"); + + // FIXME: Ensure EGL_FALSE is not returned. + + // This is safe because of the following: + // 1) The string returned by `eglQueryDeviceStringEXT` is string which will exist as long + // as the EGLDisplay is valid. Since the pointer is only used in this function, the + // lifetime of the pointer will fulfil Rust's CStr requirements on lifetime. + // 2) The string returned by EGL is null terminated. + let device_path = unsafe { CStr::from_ptr(raw_path) } + .to_str() + // EGL ensures the string is valid UTF-8 + .expect("Non-UTF8 device path name"); + + Ok(PathBuf::from(device_path)) + } + } + + /// Returns the pointer to the raw [`EGLDevice`]. + pub fn inner(&self) -> *const c_void { + self.inner + } +} + +/// Returns all device extensions a device supports. +/// +/// # Safety +/// +/// - The `device` must be a valid pointer to an `EGLDeviceEXT`. +/// - The following extensions must be supported by the display which provides the device: +/// - [`EGL_EXT_device_base`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_base.txt) +/// - [`EGL_EXT_device_query`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_query.txt) +unsafe fn device_extensions(device: EGLDeviceEXT) -> Result, EGLError> { + let raw_extensions = wrap_egl_call(|| { + ffi::egl::QueryDeviceStringEXT(device, ffi::egl::EXTENSIONS as ffi::egl::types::EGLint) + })?; + + // SAFETY: + // 1) The string returned by `eglQueryDeviceStringEXT` is string which will exist as long + // as the EGLDisplay is valid. Safety requirements for the function ensure this. + // 2) The string returned by EGL is null terminated. + let c_extensions = CStr::from_ptr(raw_extensions); + + Ok(c_extensions + .to_str() + // EGL ensures the string is valid UTF-8 + .expect("Non-UTF8 device extension name") + // Each extension is space separated (0x20) in the pointer, so strlen cannot return an improper length. + .split_whitespace() + // Take an owned copy so we do not point to garbage if EGL somehow vanishes. + .map(ToOwned::to_owned) + .collect::>()) +} diff --git a/src/backend/egl/error.rs b/src/backend/egl/error.rs index d389182..9f5a348 100644 --- a/src/backend/egl/error.rs +++ b/src/backend/egl/error.rs @@ -45,6 +45,9 @@ pub enum Error { /// Failed to create `EGLBuffer` from the buffer #[error("Failed to create `EGLBuffer` from the buffer")] EGLImageCreationFailed, + /// Failed to query the available `EGLDevice`s + #[error("Failed to query the available `EGLDevice`s")] + QueryDevices(#[source] EGLError), } /// Raw EGL error @@ -73,6 +76,9 @@ pub enum EGLError { /// The current surface of the calling thread is a window, pixel buffer or pixmap that is no longer valid. #[error("The current surface of the calling thread is a window, pixel buffer or pixmap that is no longer valid.")] BadCurrentSurface, + /// An EGLDevice argument is not valid for this display. + #[error("An EGLDevice argument is not valid for this display.")] + BadDevice, /// An EGLDisplay argument does not name a valid EGL display connection. #[error("An EGLDisplay argument does not name a valid EGL display connection.")] BadDisplay, @@ -91,7 +97,6 @@ pub enum EGLError { /// A NativeWindowType argument does not refer to a valid native window. #[error("A NativeWindowType argument does not refer to a valid native window.")] BadNativeWindow, - #[cfg(feature = "backend_drm_eglstream")] /// The EGL operation failed due to temporary unavailability of a requested resource, but the arguments were otherwise valid, and a subsequent attempt may succeed. #[error("The EGL operation failed due to temporary unavailability of a requested resource, but the arguments were otherwise valid, and a subsequent attempt may succeed.")] ResourceBusy, @@ -112,13 +117,13 @@ impl From for EGLError { ffi::egl::BAD_ATTRIBUTE => EGLError::BadAttribute, ffi::egl::BAD_CONTEXT => EGLError::BadContext, ffi::egl::BAD_CURRENT_SURFACE => EGLError::BadCurrentSurface, + ffi::egl::BAD_DEVICE_EXT => EGLError::BadDevice, ffi::egl::BAD_DISPLAY => EGLError::BadDisplay, ffi::egl::BAD_SURFACE => EGLError::BadSurface, ffi::egl::BAD_MATCH => EGLError::BadMatch, ffi::egl::BAD_PARAMETER => EGLError::BadParameter, ffi::egl::BAD_NATIVE_PIXMAP => EGLError::BadNativePixmap, ffi::egl::BAD_NATIVE_WINDOW => EGLError::BadNativeWindow, - #[cfg(feature = "backend_drm_eglstream")] ffi::egl::RESOURCE_BUSY_EXT => EGLError::ResourceBusy, ffi::egl::CONTEXT_LOST => EGLError::ContextLost, x => EGLError::Unknown(x), diff --git a/src/backend/egl/ffi.rs b/src/backend/egl/ffi.rs index 7272438..6fc7c69 100644 --- a/src/backend/egl/ffi.rs +++ b/src/backend/egl/ffi.rs @@ -129,6 +129,8 @@ pub mod egl { include!(concat!(env!("OUT_DIR"), "/egl_bindings.rs")); + pub const RESOURCE_BUSY_EXT: u32 = 0x3353; + type EGLDEBUGPROCKHR = Option< extern "system" fn( _error: egl::types::EGLenum, diff --git a/src/backend/egl/mod.rs b/src/backend/egl/mod.rs index f4e7069..a6a4b0f 100644 --- a/src/backend/egl/mod.rs +++ b/src/backend/egl/mod.rs @@ -31,6 +31,7 @@ use std::fmt; pub mod context; pub use self::context::EGLContext; +mod device; mod error; pub use self::error::*; use crate::backend::SwapBuffersError as GraphicsSwapBuffersError; @@ -47,6 +48,7 @@ use self::{display::EGLDisplayHandle, ffi::egl::types::EGLImage}; pub mod display; pub mod native; pub mod surface; +pub use self::device::EGLDevice; pub use self::display::EGLDisplay; pub use self::surface::EGLSurface; diff --git a/src/backend/egl/native.rs b/src/backend/egl/native.rs index 6c6dad9..6feef7c 100644 --- a/src/backend/egl/native.rs +++ b/src/backend/egl/native.rs @@ -1,6 +1,6 @@ //! Type safe native types for safe context/surface creation -use super::{display::EGLDisplayHandle, ffi, wrap_egl_call, SwapBuffersError}; +use super::{display::EGLDisplayHandle, ffi, wrap_egl_call, EGLDevice, SwapBuffersError}; #[cfg(feature = "backend_winit")] use std::os::raw::c_int; use std::os::raw::c_void; @@ -16,9 +16,6 @@ use winit::{platform::unix::WindowExtUnix, window::Window as WinitWindow}; #[cfg(feature = "backend_gbm")] use gbm::{AsRaw, Device as GbmDevice}; -#[cfg(feature = "backend_x11")] -use crate::backend::x11::X11Surface; - /// Create a `EGLPlatform<'a>` for the provided platform. /// /// # Arguments @@ -168,24 +165,35 @@ impl EGLNativeDisplay for WinitWindow { } } -#[cfg(feature = "backend_x11")] -impl EGLNativeDisplay for X11Surface { +/// Shallow type for EGL_PLATFORM_X11_EXT with the default X11 display +#[derive(Debug)] +pub struct X11DefaultDisplay; + +impl EGLNativeDisplay for X11DefaultDisplay { fn supported_platforms(&self) -> Vec> { - vec![ - // todo: https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_platform_device.txt - // see: https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_platform_gbm.txt - egl_platform!( - PLATFORM_GBM_KHR, - self.device().as_raw(), - &["EGL_KHR_platform_gbm"] - ), - // see: https://www.khronos.org/registry/EGL/extensions/MESA/EGL_MESA_platform_gbm.txt - egl_platform!( - PLATFORM_GBM_MESA, - self.device().as_raw(), - &["EGL_MESA_platform_gbm"] - ), - ] + vec![egl_platform!( + PLATFORM_X11_EXT, + // We pass DEFAULT_DISPLAY (null pointer) because the driver should open a connection to the X server. + ffi::egl::DEFAULT_DISPLAY, + &["EGL_EXT_platform_x11"] + )] + } +} + +impl EGLNativeDisplay for EGLDevice { + fn supported_platforms(&self) -> Vec> { + // see: https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_platform_device.txt + vec![egl_platform!( + PLATFORM_DEVICE_EXT, + self.inner, + &["EGL_EXT_platform_device"] + )] + } + + fn surface_type(&self) -> ffi::EGLint { + // EGLDisplays based on EGLDevices do not support normal windowed surfaces. + // But they may support streams, so lets allow users to create them themselves. + ffi::egl::STREAM_BIT_KHR as ffi::EGLint } } diff --git a/src/backend/x11/error.rs b/src/backend/x11/error.rs index b1c2e42..5a2c279 100644 --- a/src/backend/x11/error.rs +++ b/src/backend/x11/error.rs @@ -1,5 +1,6 @@ use std::io; +use gbm::DeviceDestroyedError; use nix::errno::Errno; use x11rb::rust_connection::{ConnectError, ConnectionError, ReplyError, ReplyOrIdError}; @@ -24,6 +25,10 @@ pub enum X11Error { #[error("Creating the window failed")] CreateWindow(CreateWindowError), + /// An X11 surface already exists for this backend. + #[error("An X11 surface already exists for this backend")] + SurfaceExists, + /// The X server is not capable of direct rendering. #[error("The X server is not capable of direct rendering")] CannotDirectRender, @@ -116,7 +121,11 @@ impl From for X11Error { pub enum AllocateBuffersError { /// Failed to open the DRM device to allocate buffers. #[error("Failed to open the DRM device to allocate buffers.")] - OpenDevice(io::Error), + OpenDevice(#[from] io::Error), + + /// The gbm device was destroyed + #[error("The gbm device was destroyed.")] + DeviceDestroyed(#[from] DeviceDestroyedError), /// The device used to allocate buffers is not the correct drm node type. #[error("The device used to allocate buffers is not the correct drm node type.")] @@ -124,7 +133,7 @@ pub enum AllocateBuffersError { /// Exporting a dmabuf failed. #[error("Exporting a dmabuf failed.")] - ExportDmabuf(GbmConvertError), + ExportDmabuf(#[from] GbmConvertError), } impl From for AllocateBuffersError { @@ -133,18 +142,6 @@ impl From for AllocateBuffersError { } } -impl From for AllocateBuffersError { - fn from(err: io::Error) -> Self { - Self::OpenDevice(err) - } -} - -impl From for AllocateBuffersError { - fn from(err: GbmConvertError) -> Self { - Self::ExportDmabuf(err) - } -} - impl From for AllocateBuffersError { fn from(err: CreateDrmNodeError) -> Self { match err { diff --git a/src/backend/x11/mod.rs b/src/backend/x11/mod.rs index 31dcb9a..6e5671c 100644 --- a/src/backend/x11/mod.rs +++ b/src/backend/x11/mod.rs @@ -13,17 +13,36 @@ //! //! ```rust,no_run //! # use std::error::Error; -//! # use smithay::backend::x11::X11Backend; +//! # use smithay::backend::x11::{X11Backend, X11Surface}; +//! use smithay::backend::egl::{EGLDisplay, EGLContext}; +//! use smithay::reexports::gbm; +//! use std::collections::HashSet; +//! //! # struct CompositorState; //! fn init_x11_backend( //! handle: calloop::LoopHandle, //! logger: slog::Logger //! ) -> Result<(), Box> { //! // Create the backend, also yielding a surface that may be used to render to the window. -//! let (backend, surface) = X11Backend::new(logger)?; +//! let mut backend = X11Backend::new(logger.clone())?; //! // You can get a handle to the window the backend has created for later use. //! let window = backend.window(); //! +//! // To render to the window the X11 backend creates, we need to create an X11 surface. +//! +//! // Get the DRM node used by the X server for direct rendering. +//! let drm_node = backend.drm_node()?; +//! // Create the gbm device for allocating buffers +//! let device = gbm::Device::new(drm_node)?; +//! // Initialize EGL to retrieve the support modifier list +//! let egl = EGLDisplay::new(&device, logger.clone()).expect("Failed to create EGLDisplay"); +//! let context = EGLContext::new(&egl, logger).expect("Failed to create EGLContext"); +//! let modifiers = context.dmabuf_render_formats().iter().map(|format| format.modifier).collect::>(); +//! +//! // Finally create the X11 surface, you will use this to obtain buffers that will be presented to the +//! // window. +//! let surface = X11Surface::new(&mut backend, device, modifiers.into_iter()); +//! //! // Insert the backend into the event loop to receive events. //! handle.insert_source(backend, |event, _window, state| { //! // Process events from the X server that apply to the window. @@ -58,15 +77,19 @@ use self::{buffer::PixmapWrapperExt, window_inner::WindowInner}; use crate::{ backend::{ allocator::dmabuf::{AsDmabuf, Dmabuf}, - drm::{DrmNode, NodeType}, + drm::{node::path_to_type, CreateDrmNodeError, DrmNode, NodeType}, + egl::{native::X11DefaultDisplay, EGLDevice, EGLDisplay, Error as EGLError}, input::{Axis, ButtonState, InputEvent, KeyState}, }, utils::{x11rb::X11Source, Logical, Size}, }; use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory}; -use drm_fourcc::DrmFourcc; -use gbm::BufferObjectFlags; -use nix::fcntl; +use drm_fourcc::{DrmFourcc, DrmModifier}; +use gbm::{BufferObject, BufferObjectFlags}; +use nix::{ + fcntl::{self, OFlag}, + sys::stat::Mode, +}; use slog::{error, info, o, Logger}; use std::{ io, mem, @@ -122,8 +145,12 @@ pub struct X11Backend { source: X11Source, screen_number: usize, window: Arc, - resize: Sender>, + /// Channel used to send resize notifications to the surface. + /// + /// This value will be [`None`] if no surface is bound to the window managed by the backend. + resize: Option>>, key_counter: Arc, + window_format: DrmFourcc, } atom_manager! { @@ -140,7 +167,7 @@ impl X11Backend { /// Initializes the X11 backend. /// /// This connects to the X server and configures the window using the default options. - pub fn new(logger: L) -> Result<(X11Backend, X11Surface), X11Error> + pub fn new(logger: L) -> Result where L: Into>, { @@ -151,7 +178,7 @@ impl X11Backend { /// /// This connects to the X server and configures the window using the default size and the /// specified window title. - pub fn with_title(title: &str, logger: L) -> Result<(X11Backend, X11Surface), X11Error> + pub fn with_title(title: &str, logger: L) -> Result where L: Into>, { @@ -162,7 +189,7 @@ impl X11Backend { /// /// This connects to the X server and configures the window using the default window title /// and the specified window size. - pub fn with_size(size: Size, logger: L) -> Result<(X11Backend, X11Surface), X11Error> + pub fn with_size(size: Size, logger: L) -> Result where L: Into>, { @@ -176,7 +203,7 @@ impl X11Backend { size: Size, title: &str, logger: L, - ) -> Result<(X11Backend, X11Surface), X11Error> + ) -> Result where L: Into>, { @@ -244,21 +271,16 @@ impl X11Backend { info!(logger, "Window created"); - let (resize_send, resize_recv) = mpsc::channel(); - - let backend = X11Backend { + Ok(X11Backend { log: logger, source, connection, window, key_counter: Arc::new(AtomicU32::new(0)), screen_number, - resize: resize_send, - }; - - let surface = X11Surface::new(&backend, format, resize_recv)?; - - Ok((backend, surface)) + resize: None, + window_format: format, + }) } /// Returns the default screen number of the X server. @@ -275,66 +297,16 @@ impl X11Backend { pub fn window(&self) -> Window { self.window.clone().into() } -} -/// An X11 surface which uses GBM to allocate and present buffers. -#[derive(Debug)] -pub struct X11Surface { - connection: Weak, - window: Window, - resize: Receiver>, - device: gbm::Device, - format: DrmFourcc, - width: u16, - height: u16, - current: Dmabuf, - next: Dmabuf, -} - -impl X11Surface { - fn new( - backend: &X11Backend, - format: DrmFourcc, - resize: Receiver>, - ) -> Result { - let connection = &backend.connection; - let window = backend.window(); - - // Determine which drm-device the Display is using. - let screen = &connection.setup().roots[backend.screen()]; - // provider being NONE tells the X server to use the RandR provider. - let dri3 = match connection.dri3_open(screen.root, x11rb::NONE)?.reply() { - Ok(reply) => reply, - Err(err) => { - return Err(if let ReplyError::X11Error(ref protocol_error) = err { - match protocol_error.error_kind { - // Implementation is risen when the renderer is not capable of X server is not capable - // of rendering at all. - ErrorKind::Implementation => X11Error::CannotDirectRender, - // Match may occur when the node cannot be authenticated for the client. - ErrorKind::Match => X11Error::CannotDirectRender, - _ => err.into(), - } - } else { - err.into() - }); - } - }; - - // Take ownership of the container's inner value so we do not need to duplicate the fd. - // This is fine because the X server will always open a new file descriptor. - let drm_device_fd = dri3.device_fd.into_raw_fd(); - - let fd_flags = - fcntl::fcntl(drm_device_fd.as_raw_fd(), fcntl::F_GETFD).map_err(AllocateBuffersError::from)?; - - // Enable the close-on-exec flag. - fcntl::fcntl( - drm_device_fd, - fcntl::F_SETFD(fcntl::FdFlag::from_bits_truncate(fd_flags) | fcntl::FdFlag::FD_CLOEXEC), - ) - .map_err(AllocateBuffersError::from)?; + /// Returns the format of the window. + pub fn format(&self) -> DrmFourcc { + self.window_format + } + /// Returns the DRM node the X server uses for direct rendering. + /// + /// The DRM node may be used to create a [`gbm::Device`] to allocate buffers. + pub fn drm_node(&self) -> Result { // Kernel documentation explains why we should prefer the node to be a render node: // https://kernel.readthedocs.io/en/latest/gpu/drm-uapi.html // @@ -349,46 +321,165 @@ impl X11Surface { // // Of course if the DRM device does not support render nodes, no DRIVER_RENDER capability, then // fall back to the primary node. - let drm_node = DrmNode::from_fd(drm_device_fd).map_err(Into::::into)?; - let drm_node = if drm_node.ty() != NodeType::Render { - if drm_node.has_render() { - // Try to get the render node. - match DrmNode::from_node_with_type(drm_node, NodeType::Render) { - Ok(node) => node, - Err(err) => { - slog::warn!(&backend.log, "Could not create render node from existing DRM node, falling back to primary node"); - err.node() - } + + // We cannot fallback on the egl_init method, because there is no way for us to authenticate a primary node. + // dri3 does not work for closed-source drivers, but *may* give us a authenticated fd as a fallback. + // As a result we try to use egl for a cleaner, better supported approach at first and only if that fails use dri3. + egl_init(self).or_else(|err| { + slog::warn!( + &self.log, + "Failed to init X11 surface via egl, falling back to dri3: {}", + err + ); + dri3_init(self) + }) + } +} + +/// An X11 surface which uses GBM to allocate and present buffers. +#[derive(Debug)] +pub struct X11Surface { + connection: Weak, + window: Window, + resize: Receiver>, + device: gbm::Device, + format: DrmFourcc, + width: u16, + height: u16, + current: BufferObject, + next: BufferObject, +} + +#[derive(Debug, thiserror::Error)] +enum EGLInitError { + #[error(transparent)] + EGL(#[from] EGLError), + #[error(transparent)] + IO(#[from] io::Error), +} + +fn egl_init(_backend: &X11Backend) -> Result { + let display = EGLDisplay::new(&X11DefaultDisplay, None)?; + let device = EGLDevice::device_for_display(&display)?; + let path = path_to_type(device.drm_device_path()?, NodeType::Render)?; + fcntl::open(&path, OFlag::O_RDWR | OFlag::O_CLOEXEC, Mode::empty()) + .map_err(Into::::into) + .and_then(|fd| { + DrmNode::from_fd(fd).map_err(|err| match err { + CreateDrmNodeError::Io(err) => err, + _ => unreachable!(), + }) + }) + .map_err(EGLInitError::IO) +} + +fn dri3_init(backend: &X11Backend) -> Result { + let connection = &backend.connection; + + // Determine which drm-device the Display is using. + let screen = &connection.setup().roots[backend.screen()]; + // provider being NONE tells the X server to use the RandR provider. + let dri3 = match connection.dri3_open(screen.root, x11rb::NONE)?.reply() { + Ok(reply) => reply, + Err(err) => { + return Err(if let ReplyError::X11Error(ref protocol_error) = err { + match protocol_error.error_kind { + // Implementation is risen when the renderer is not capable of X server is not capable + // of rendering at all. + ErrorKind::Implementation => X11Error::CannotDirectRender, + // Match may occur when the node cannot be authenticated for the client. + ErrorKind::Match => X11Error::CannotDirectRender, + _ => err.into(), } } else { - slog::warn!( - &backend.log, - "DRM Device does not have a render node, falling back to primary node" - ); - drm_node - } - } else { - drm_node - }; + err.into() + }); + } + }; - // Finally create a GBMDevice to manage the buffers. - let device = gbm::Device::new(drm_node).map_err(Into::::into)?; + // Take ownership of the container's inner value so we do not need to duplicate the fd. + // This is fine because the X server will always open a new file descriptor. + let drm_device_fd = dri3.device_fd.into_raw_fd(); - let size = backend.window().size(); - let current = device - .create_buffer_object::<()>(size.w as u32, size.h as u32, format, BufferObjectFlags::empty()) - .map_err(Into::::into)? - .export() + let fd_flags = + fcntl::fcntl(drm_device_fd.as_raw_fd(), fcntl::F_GETFD).map_err(AllocateBuffersError::from)?; + + // Enable the close-on-exec flag. + fcntl::fcntl( + drm_device_fd, + fcntl::F_SETFD(fcntl::FdFlag::from_bits_truncate(fd_flags) | fcntl::FdFlag::FD_CLOEXEC), + ) + .map_err(AllocateBuffersError::from)?; + let drm_node = DrmNode::from_fd(drm_device_fd).map_err(Into::::into)?; + + if drm_node.ty() != NodeType::Render && drm_node.has_render() { + // Try to get the render node. + if let Some(path) = drm_node.dev_path_with_type(NodeType::Render) { + return Ok(fcntl::open(&path, OFlag::O_RDWR | OFlag::O_CLOEXEC, Mode::empty()) + .map_err(Into::::into) + .map_err(CreateDrmNodeError::Io) + .and_then(DrmNode::from_fd) + .unwrap_or_else(|err| { + slog::warn!(&backend.log, "Could not create render node from existing DRM node ({}), falling back to primary node", err); + drm_node + })); + } + } + + slog::warn!( + &backend.log, + "DRM Device does not have a render node, falling back to primary node" + ); + Ok(drm_node) +} + +impl X11Surface { + /// Creates a surface that allocates and presents buffers to the window managed by the backend. + /// + /// This will fail if the backend has already been used to create a surface. + pub fn new( + backend: &mut X11Backend, + device: gbm::Device, + modifiers: impl Iterator, + ) -> Result { + if backend.resize.is_some() { + return Err(X11Error::SurfaceExists); + } + + let modifiers = modifiers.collect::>(); + + let window = backend.window(); + let format = window.format().unwrap(); + let size = window.size(); + let mut current = device + .create_buffer_object_with_modifiers( + size.w as u32, + size.h as u32, + format, + modifiers.iter().cloned(), + ) + .map_err(Into::::into)?; + current + .set_userdata(current.export().map_err(Into::::into)?) .map_err(Into::::into)?; - let next = device - .create_buffer_object::<()>(size.w as u32, size.h as u32, format, BufferObjectFlags::empty()) - .map_err(Into::::into)? - .export() + let mut next = device + .create_buffer_object_with_modifiers( + size.w as u32, + size.h as u32, + format, + modifiers.iter().cloned(), + ) .map_err(Into::::into)?; + next.set_userdata(next.export().map_err(Into::::into)?) + .map_err(Into::::into)?; + + let (sender, recv) = mpsc::channel(); + + backend.resize = Some(sender); Ok(X11Surface { - connection: Arc::downgrade(connection), + connection: Arc::downgrade(&backend.connection), window, device, format, @@ -396,7 +487,7 @@ impl X11Surface { height: size.h, current, next, - resize, + resize: recv, }) } @@ -422,25 +513,30 @@ impl X11Surface { } fn resize(&mut self, size: Size) -> Result<(), AllocateBuffersError> { - let current = self + let mut current = self .device - .create_buffer_object::<()>( + .create_buffer_object( size.w as u32, size.h as u32, self.format, BufferObjectFlags::empty(), - )? - .export()?; + ) + .map_err(Into::::into)?; + current + .set_userdata(current.export().map_err(Into::::into)?) + .map_err(Into::::into)?; - let next = self + let mut next = self .device - .create_buffer_object::<()>( + .create_buffer_object( size.w as u32, size.h as u32, self.format, BufferObjectFlags::empty(), - )? - .export()?; + ) + .map_err(Into::::into)?; + next.set_userdata(next.export().map_err(Into::::into)?) + .map_err(Into::::into)?; self.width = size.w; self.height = size.h; @@ -480,8 +576,13 @@ impl Present<'_> { /// Returns the next buffer that will be presented to the Window. /// /// You may bind this buffer to a renderer to render. - pub fn buffer(&self) -> Dmabuf { - self.surface.next.clone() + pub fn buffer(&self) -> Result { + Ok(self + .surface + .next + .userdata() + .map(|dmabuf| dmabuf.cloned()) + .map(Option::unwrap)?) } } @@ -493,9 +594,16 @@ impl Drop for Present<'_> { // Swap the buffers mem::swap(&mut surface.next, &mut surface.current); - if let Ok(pixmap) = PixmapWrapper::with_dmabuf(&*connection, &surface.window, &surface.current) { - // Now present the current buffer - let _ = pixmap.present(&*connection, &surface.window); + match surface.current.userdata().map(Option::unwrap) { + Ok(dmabuf) => { + if let Ok(pixmap) = PixmapWrapper::with_dmabuf(&*connection, &surface.window, dmabuf) { + // Now present the current buffer + let _ = pixmap.present(&*connection, &surface.window); + } + } + Err(_err) => { + todo!("Log error") + } } // Flush the connection after presenting to the window to ensure we don't run out of buffer space in the X11 connection. @@ -771,7 +879,10 @@ impl EventSource for X11Backend { } (callback)(X11Event::Resized(configure_notify_size), &mut event_window); - let _ = resize.send(configure_notify_size); + + if let Some(resize_sender) = resize { + let _ = resize_sender.send(configure_notify_size); + } } } }