From 3eeafad834748c5915837a1431a68e310cf5e24a Mon Sep 17 00:00:00 2001 From: John Nunley Date: Tue, 27 Dec 2022 09:14:54 -0800 Subject: [PATCH] x11: Add XCB support to the X11 backend (#52) --- Cargo.toml | 5 +- examples/libxcb.rs | 136 +++++++++++++++++++++++++++ src/lib.rs | 9 +- src/x11.rs | 227 +++++++++++++++++++++++++++++---------------- 4 files changed, 296 insertions(+), 81 deletions(-) create mode 100644 examples/libxcb.rs diff --git a/Cargo.toml b/Cargo.toml index 48b02e9..c98fbad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,18 +15,21 @@ exclude = ["examples"] default = ["x11", "wayland", "wayland-dlopen"] wayland = ["wayland-backend", "wayland-client", "nix"] wayland-dlopen = ["wayland-sys/dlopen"] -x11 = ["x11-dl"] +x11 = ["bytemuck", "x11rb", "x11-dl"] [dependencies] thiserror = "1.0.30" raw-window-handle = "0.5.0" +log = "0.4.17" [target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] nix = { version = "0.26.1", optional = true } wayland-backend = { version = "0.1.0-beta.14", features = ["client_system"], optional = true } wayland-client = { version = "0.30.0-beta.14", optional = true } wayland-sys = "0.30.0" +bytemuck = { version = "1.12.3", optional = true } x11-dl = { version = "2.19.1", optional = true } +x11rb = { version = "0.11.0", features = ["allow-unsafe-code", "dl-libxcb"], optional = true } [target.'cfg(target_os = "windows")'.dependencies.windows-sys] version = "0.42.0" diff --git a/examples/libxcb.rs b/examples/libxcb.rs new file mode 100644 index 0000000..8c2b691 --- /dev/null +++ b/examples/libxcb.rs @@ -0,0 +1,136 @@ +//! Example of using `softbuffer` with `libxcb`. + +#[cfg(all(feature = "x11", any(target_os = "linux", target_os = "freebsd")))] +mod example { + use raw_window_handle::{RawDisplayHandle, RawWindowHandle, XcbDisplayHandle, XcbWindowHandle}; + use softbuffer::GraphicsContext; + use x11rb::{ + connection::Connection, + protocol::{ + xproto::{self, ConnectionExt as _}, + Event, + }, + xcb_ffi::XCBConnection, + }; + + pub(crate) fn run() { + // Create a new XCB connection + let (conn, screen) = XCBConnection::connect(None).expect("Failed to connect to X server"); + + // x11rb doesn't use raw-window-handle yet, so just create our own. + let mut display_handle = XcbDisplayHandle::empty(); + display_handle.connection = conn.get_raw_xcb_connection() as *mut _; + display_handle.screen = screen as _; + + // Create a new window. + let mut width = 640u16; + let mut height = 480u16; + + let window = conn.generate_id().unwrap(); + let screen = &conn.setup().roots[screen]; + let (root_visual, root_parent) = (screen.root_visual, screen.root); + conn.create_window( + x11rb::COPY_FROM_PARENT as _, + window, + root_parent, + 0, + 0, + width, + height, + 0, + xproto::WindowClass::COPY_FROM_PARENT, + root_visual, + &xproto::CreateWindowAux::new() + .background_pixel(screen.white_pixel) + .event_mask(xproto::EventMask::EXPOSURE | xproto::EventMask::STRUCTURE_NOTIFY), + ) + .unwrap() + .check() + .unwrap(); + + let mut window_handle = XcbWindowHandle::empty(); + window_handle.window = window as _; + window_handle.visual_id = root_visual as _; + + // Create a new softbuffer context. + // SAFETY: The display and window handles outlive the context. + let mut context = unsafe { + GraphicsContext::from_raw( + RawWindowHandle::Xcb(window_handle), + RawDisplayHandle::Xcb(display_handle), + ) + } + .unwrap(); + + // Register an atom for closing the window. + let wm_protocols_atom = conn + .intern_atom(false, "WM_PROTOCOLS".as_bytes()) + .unwrap() + .reply() + .unwrap() + .atom; + let delete_window_atom = conn + .intern_atom(false, "WM_DELETE_WINDOW".as_bytes()) + .unwrap() + .reply() + .unwrap() + .atom; + conn.change_property( + xproto::PropMode::REPLACE as _, + window, + wm_protocols_atom, + xproto::AtomEnum::ATOM, + 32, + 1, + &delete_window_atom.to_ne_bytes(), + ) + .unwrap() + .check() + .unwrap(); + + // Map the window to the screen. + conn.map_window(window).unwrap().check().unwrap(); + + // Pump events. + loop { + let event = conn.wait_for_event().unwrap(); + + match event { + Event::Expose(_) => { + // Draw a width x height red rectangle. + let red = 255 << 16; + let source = std::iter::repeat(red) + .take((width as usize * height as usize) as _) + .collect::>(); + + // Draw the buffer. + context.set_buffer(&source, width, height); + } + Event::ConfigureNotify(configure_notify) => { + width = configure_notify.width; + height = configure_notify.height; + } + Event::ClientMessage(cm) => { + if cm.data.as_data32()[0] == delete_window_atom { + break; + } + } + _ => {} + } + } + + // Delete the context and drop the window. + drop(context); + conn.destroy_window(window).unwrap().check().unwrap(); + } +} + +#[cfg(all(feature = "x11", any(target_os = "linux", target_os = "freebsd")))] +fn main() { + example::run(); +} + +#[cfg(not(all(feature = "x11", any(target_os = "linux", target_os = "freebsd"))))] +fn main() { + eprintln!("This example requires the `x11` feature to be enabled on a supported platform."); +} diff --git a/src/lib.rs b/src/lib.rs index ed5d230..f4cf21d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,7 +110,14 @@ impl GraphicsContext { RawWindowHandle::Xlib(xlib_window_handle), RawDisplayHandle::Xlib(xlib_display_handle), ) => Dispatch::X11(unsafe { - x11::X11Impl::new(xlib_window_handle, xlib_display_handle)? + x11::X11Impl::from_xlib(xlib_window_handle, xlib_display_handle)? + }), + #[cfg(all(feature = "x11", any(target_os = "linux", target_os = "freebsd")))] + ( + RawWindowHandle::Xcb(xcb_window_handle), + RawDisplayHandle::Xcb(xcb_display_handle), + ) => Dispatch::X11(unsafe { + x11::X11Impl::from_xcb(xcb_window_handle, xcb_display_handle)? }), #[cfg(all(feature = "wayland", any(target_os = "linux", target_os = "freebsd")))] ( diff --git a/src/x11.rs b/src/x11.rs index 47c63da..a8ac8cc 100644 --- a/src/x11.rs +++ b/src/x11.rs @@ -5,29 +5,29 @@ //! addition, we may also want to blit to a pixmap instead of a window. use crate::SwBufError; -use raw_window_handle::{XlibDisplayHandle, XlibWindowHandle}; -use std::os::raw::{c_char, c_uint}; -use x11_dl::xlib::{Display, Visual, Xlib, ZPixmap, GC}; +use raw_window_handle::{XcbDisplayHandle, XcbWindowHandle, XlibDisplayHandle, XlibWindowHandle}; +use std::fmt; + +use x11_dl::xlib::Display; +use x11_dl::xlib_xcb::Xlib_xcb; + +use x11rb::connection::Connection; +use x11rb::protocol::xproto::{self, ConnectionExt as _, Gcontext, Window}; +use x11rb::xcb_ffi::XCBConnection; /// The handle to an X11 drawing context. pub struct X11Impl { - /// The window handle. - window_handle: XlibWindowHandle, + /// The handle to the XCB connection. + connection: XCBConnection, - /// The display handle. - display_handle: XlibDisplayHandle, + /// The window to draw to. + window: Window, - /// Reference to the X11 shared library. - lib: Xlib, - - /// The graphics context for drawing. - gc: GC, - - /// Information about the screen to use for drawing. - visual: *mut Visual, + /// The graphics context to use when drawing. + gc: Gcontext, /// The depth (bits per pixel) of the drawing context. - depth: i32, + depth: u8, } impl X11Impl { @@ -36,90 +36,159 @@ impl X11Impl { /// # Safety /// /// The `XlibWindowHandle` and `XlibDisplayHandle` must be valid. - pub unsafe fn new( + pub unsafe fn from_xlib( window_handle: XlibWindowHandle, display_handle: XlibDisplayHandle, ) -> Result { - // Try to open the X11 shared library. - let lib = match Xlib::open() { - Ok(lib) => lib, - Err(e) => { - return Err(SwBufError::PlatformError( - Some("Failed to open Xlib".into()), - Some(Box::new(e)), - )) - } - }; + // TODO: We should cache the shared libraries. - // Validate the handles to ensure that they aren't incomplete. + // Try to open the XlibXCB shared library. + let lib_xcb = Xlib_xcb::open().swbuf_err("Failed to open XlibXCB shared library")?; + + // Validate the display handle to ensure we can use it. if display_handle.display.is_null() { return Err(SwBufError::IncompleteDisplayHandle); } + // Get the underlying XCB connection. + // SAFETY: The user has asserted that the display handle is valid. + let connection = + unsafe { (lib_xcb.XGetXCBConnection)(display_handle.display as *mut Display) }; + + // Construct the equivalent XCB display and window handles. + let mut xcb_display_handle = XcbDisplayHandle::empty(); + xcb_display_handle.connection = connection; + xcb_display_handle.screen = display_handle.screen; + + let mut xcb_window_handle = XcbWindowHandle::empty(); + xcb_window_handle.window = window_handle.window as _; + xcb_window_handle.visual_id = window_handle.visual_id as _; + + // SAFETY: If the user passed in valid Xlib handles, then these are valid XCB handles. + unsafe { Self::from_xcb(xcb_window_handle, xcb_display_handle) } + } + + /// Create a new `X11Impl` from a `XcbWindowHandle` and `XcbDisplayHandle`. + /// + /// # Safety + /// + /// The `XcbWindowHandle` and `XcbDisplayHandle` must be valid. + pub(crate) unsafe fn from_xcb( + window_handle: XcbWindowHandle, + display_handle: XcbDisplayHandle, + ) -> Result { + // Check that the handles are valid. + if display_handle.connection.is_null() { + return Err(SwBufError::IncompleteDisplayHandle); + } + if window_handle.window == 0 { return Err(SwBufError::IncompleteWindowHandle); } - // Get the screen number from the handle. - // NOTE: By default, XlibDisplayHandle sets the screen number to 0. If we see a zero, - // it could mean either screen index zero, or that the screen number was not set. We - // can't tell which, so we'll just assume that the screen number was not set. - let screen = match display_handle.screen { - 0 => unsafe { (lib.XDefaultScreen)(display_handle.display as *mut Display) }, - screen => screen, + // Wrap the display handle in an x11rb connection. + // SAFETY: We don't own the connection, so don't drop it. We also assert that the connection is valid. + let connection = { + let result = + unsafe { XCBConnection::from_raw_xcb_connection(display_handle.connection, false) }; + + result.swbuf_err("Failed to wrap XCB connection")? }; - // Use the default graphics context, visual and depth for this screen. - let gc = unsafe { (lib.XDefaultGC)(display_handle.display as *mut Display, screen) }; - let visual = - unsafe { (lib.XDefaultVisual)(display_handle.display as *mut Display, screen) }; - let depth = unsafe { (lib.XDefaultDepth)(display_handle.display as *mut Display, screen) }; + let window = window_handle.window; + + // Start getting the depth of the window. + let geometry_token = connection + .get_geometry(window) + .swbuf_err("Failed to send geometry request")?; + + // Create a new graphics context to draw to. + let gc = connection + .generate_id() + .swbuf_err("Failed to generate GC ID")?; + connection + .create_gc( + gc, + window, + &xproto::CreateGCAux::new().graphics_exposures(0), + ) + .swbuf_err("Failed to send GC creation request")? + .check() + .swbuf_err("Failed to create GC")?; + + // Finish getting the depth of the window. + let geometry_reply = geometry_token + .reply() + .swbuf_err("Failed to get geometry reply")?; Ok(Self { - window_handle, - display_handle, - lib, + connection, + window, gc, - visual, - depth, + depth: geometry_reply.depth, }) } pub(crate) unsafe fn set_buffer(&mut self, buffer: &[u32], width: u16, height: u16) { - // Create the image from the buffer. - let image = unsafe { - (self.lib.XCreateImage)( - self.display_handle.display as *mut Display, - self.visual, - self.depth as u32, - ZPixmap, - 0, - (buffer.as_ptr()) as *mut c_char, - width as u32, - height as u32, - 32, - (width * 4) as i32, - ) - }; + // Draw the image to the buffer. + let result = self.connection.put_image( + xproto::ImageFormat::Z_PIXMAP, + self.window, + self.gc, + width, + height, + 0, + 0, + 0, + self.depth, + bytemuck::cast_slice(buffer), + ); - // Draw the image to the window. - unsafe { - (self.lib.XPutImage)( - self.display_handle.display as *mut Display, - self.window_handle.window, - self.gc, - image, - 0, - 0, - 0, - 0, - width as c_uint, - height as c_uint, - ) - }; - - // Delete the image data. - unsafe { (*image).data = std::ptr::null_mut() }; - unsafe { (self.lib.XDestroyImage)(image) }; + match result { + Err(e) => log::error!("Failed to draw image to window: {}", e), + Ok(token) => token.ignore_error(), + } } } + +impl Drop for X11Impl { + fn drop(&mut self) { + // Close the graphics context that we created. + if let Ok(token) = self.connection.free_gc(self.gc) { + token.ignore_error(); + } + } +} + +/// Convenient wrapper to cast errors into SwBufError. +trait ResultExt { + fn swbuf_err(self, msg: impl Into) -> Result; +} + +impl ResultExt for Result { + fn swbuf_err(self, msg: impl Into) -> Result { + self.map_err(|e| { + SwBufError::PlatformError(Some(msg.into()), Some(Box::new(LibraryError(e)))) + }) + } +} + +/// A wrapper around a library error. +/// +/// This prevents `x11-dl` and `x11rb` from becoming public dependencies, since users cannot downcast +/// to this type. +struct LibraryError(E); + +impl fmt::Debug for LibraryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl fmt::Display for LibraryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl std::error::Error for LibraryError {}