From cc6d17169e0ae355f570d705e91f5f5201964b6d Mon Sep 17 00:00:00 2001 From: Charles Saracco Date: Sun, 31 May 2020 14:29:32 -0400 Subject: [PATCH] OpenGL surface for X11/XCB --- .github/workflows/rust.yml | 6 +- Cargo.toml | 3 +- README.md | 12 +- src/lib.rs | 2 +- src/x11/mod.rs | 5 + src/x11/opengl_util.rs | 66 ++++++++++ src/x11/window.rs | 238 ++++++++++++++++++++++++++++++------- src/x11/xcb_connection.rs | 15 +++ 8 files changed, 296 insertions(+), 51 deletions(-) create mode 100644 src/x11/opengl_util.rs create mode 100644 src/x11/xcb_connection.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 746c672..e2266eb 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -11,12 +11,12 @@ jobs: steps: - uses: actions/checkout@v2 - - name: install XCB dependencies + - name: Install XCB and GL dependencies run: | sudo apt update - sudo apt install libx11-xcb-dev libxcb-dri2-0-dev + sudo apt install libx11-xcb-dev libxcb-dri2-0-dev libgl1-mesa-dev if: contains(matrix.os, 'ubuntu') - - name: install rust stable + - name: Install rust stable uses: actions-rs/toolchain@v1 with: toolchain: stable diff --git a/Cargo.toml b/Cargo.toml index 6450c5d..c1cd457 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,14 @@ authors = [ edition = "2018" [dependencies] +gl = "0.14.0" [target.'cfg(target_os="linux")'.dependencies] xcb = { version = "0.9", features = ["thread", "xlib_xcb", "dri2"] } +x11 = { version = "2.3", features = ["xlib", "glx"]} [target.'cfg(target_os="windows")'.dependencies] winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", "minwindef", "guiddef", "combaseapi", "wingdi"] } -gl = "0.14.0" [target.'cfg(target_os="macos")'.dependencies] cocoa = "0.20.1" diff --git a/README.md b/README.md index 4d0c76d..430c603 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ A low-level windowing system geared towards making audio plugin UIs. Below is a proposed list of milestones (roughly in-order) and their status. Subject to change at any time. -| Feature | Windows | Mac OS | Linux | -| -------------------------------------- | ------------------ | ------------------ | ----------------- | -| Spawns a window, no parent | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark:| -| Cross-platform API for window spawning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark:| -| Window uses an OpenGL surface | :heavy_check_mark: | | | -| Basic DPI scaling support | | | :question: | +| Feature | Windows | Mac OS | Linux | +| -------------------------------------- | ------------------ | ------------------ | ------------------ | +| Spawns a window, no parent | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Cross-platform API for window spawning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Window uses an OpenGL surface | :heavy_check_mark: | | :heavy_check_mark: | +| Basic DPI scaling support | | | :question: | diff --git a/src/lib.rs b/src/lib.rs index 079dff6..d1adb24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ pub use win::*; #[cfg(target_os = "linux")] mod x11; #[cfg(target_os = "linux")] -pub use x11::*; +pub use crate::x11::*; #[cfg(target_os = "macos")] mod macos; diff --git a/src/x11/mod.rs b/src/x11/mod.rs index 01e0b8e..e1dd8e0 100644 --- a/src/x11/mod.rs +++ b/src/x11/mod.rs @@ -1,2 +1,7 @@ mod window; pub use window::*; + +mod xcb_connection; +use xcb_connection::XcbConnection; + +mod opengl_util; diff --git a/src/x11/opengl_util.rs b/src/x11/opengl_util.rs new file mode 100644 index 0000000..16763a8 --- /dev/null +++ b/src/x11/opengl_util.rs @@ -0,0 +1,66 @@ +use std::ffi::CString; +use std::os::raw::{c_int, c_void}; + +use ::x11::{glx, xlib}; + +use super::XcbConnection; + +pub type GlXCreateContextAttribsARBProc = unsafe extern "C" fn( + dpy: *mut xlib::Display, + fbc: glx::GLXFBConfig, + share_context: glx::GLXContext, + direct: xlib::Bool, + attribs: *const c_int, +) -> glx::GLXContext; + +// Check to make sure this system supports the correct version of GLX (>= 1.3 for now) +// For now it just panics if not, but TODO: do correct error handling +pub fn check_glx_version(xcb_connection: &XcbConnection) { + let raw_display = xcb_connection.conn.get_raw_dpy(); + let mut maj: c_int = 0; + let mut min: c_int = 0; + + unsafe { + if glx::glXQueryVersion(raw_display, &mut maj as *mut c_int, &mut min as *mut c_int) == 0 { + panic!("Cannot get GLX version"); + } + if (maj < 1) || (maj == 1 && min < 3) { + panic!("GLX version >= 1.3 required! (have {}.{})", maj, min); + } + } +} + +// Get GLX framebuffer config +// History: https://stackoverflow.com/questions/51558473/whats-the-difference-between-a-glx-visual-and-a-fbconfig +pub fn get_glxfbconfig(xcb_connection: &XcbConnection, visual_attribs: &[i32]) -> glx::GLXFBConfig { + let raw_display = xcb_connection.conn.get_raw_dpy(); + let xlib_display = xcb_connection.xlib_display; + + unsafe { + let mut fbcount: c_int = 0; + let fbcs = glx::glXChooseFBConfig( + raw_display, + xlib_display, + visual_attribs.as_ptr(), + &mut fbcount as *mut c_int, + ); + + if fbcount == 0 { + panic!("Could not find compatible GLX FB config."); + } + + // If we get more than one, any of the different configs work. Just choose the first one. + let fbc = *fbcs; + xlib::XFree(fbcs as *mut c_void); + fbc + } +} + +pub unsafe fn load_gl_func(name: &str) -> *mut c_void { + let cname = CString::new(name).unwrap(); + let ptr: *mut c_void = std::mem::transmute(glx::glXGetProcAddress(cname.as_ptr() as *const u8)); + if ptr.is_null() { + panic!("could not load {}", name); + } + ptr +} diff --git a/src/x11/window.rs b/src/x11/window.rs index e64a55c..999f2bf 100644 --- a/src/x11/window.rs +++ b/src/x11/window.rs @@ -1,15 +1,24 @@ // TODO: messy for now, will refactor when I have more of an idea of the API/architecture -// TODO: actually handle events -// TODO: set window title // TODO: close window -// TODO: proper error handling (no bare `unwrap`s) -// TODO: refactor X connections (+setup, +screen) to a new struct +// TODO: proper error handling (no bare `unwrap`s, no panics) +// TODO: move more OpenGL-related stuff into opengl_util.rs +// TODO: consider researching all unsafe calls here and figuring out what invariants need to be upheld. +// (write safe wrappers?) +use std::ffi::CStr; +use std::os::raw::{c_int, c_void}; +use std::ptr::null_mut; + +use ::x11::{glx, xlib}; +// use xcb::dri2; // needed later + +use super::opengl_util; +use super::XcbConnection; use crate::Parent; use crate::WindowOpenOptions; pub struct Window { - xcb_connection: xcb::Connection, + xcb_connection: XcbConnection, } impl Window { @@ -17,21 +26,81 @@ impl Window { // Convert the parent to a X11 window ID if we're given one let parent = match options.parent { Parent::None => None, - Parent::AsIfParented => None, // ??? + Parent::AsIfParented => None, // TODO: ??? Parent::WithParent(p) => Some(p as u32), }; // Connect to the X server - let (conn, screen_num) = xcb::Connection::connect_with_xlib_display().unwrap(); + let xcb_connection = XcbConnection::new(); - // Figure out screen information - let setup = conn.get_setup(); - let screen = setup.roots().nth(screen_num as usize).unwrap(); + // Check GLX version (>= 1.3 needed) + opengl_util::check_glx_version(&xcb_connection); + + // Get GLX framebuffer config (requires GLX >= 1.3) + #[rustfmt::skip] + let fb_config = opengl_util::get_glxfbconfig( + &xcb_connection, + &[ + glx::GLX_X_RENDERABLE, 1, + glx::GLX_DRAWABLE_TYPE, glx::GLX_WINDOW_BIT, + glx::GLX_RENDER_TYPE, glx::GLX_RGBA_BIT, + glx::GLX_X_VISUAL_TYPE, glx::GLX_TRUE_COLOR, + glx::GLX_RED_SIZE, 8, + glx::GLX_GREEN_SIZE, 8, + glx::GLX_BLUE_SIZE, 8, + glx::GLX_ALPHA_SIZE, 8, + glx::GLX_DEPTH_SIZE, 24, + glx::GLX_STENCIL_SIZE, 8, + glx::GLX_DOUBLEBUFFER, 1, + 0 + ], + ); + + // The GLX framebuffer config holds an XVisualInfo, which we'll need for other X operations. + let x_visual_info: *const xlib::XVisualInfo = + unsafe { glx::glXGetVisualFromFBConfig(xcb_connection.conn.get_raw_dpy(), fb_config) }; + + // Load up DRI2 extensions. + // See also: https://www.x.org/releases/X11R7.7/doc/dri2proto/dri2proto.txt + /* + // needed later when we handle events + let dri2_ev = { + xcb_connection.conn.prefetch_extension_data(dri2::id()); + match xcb_connection.conn.get_extension_data(dri2::id()) { + None => panic!("could not load dri2 extension"), + Some(r) => r.first_event(), + } + }; + */ + + // Get screen information (?) + let setup = xcb_connection.conn.get_setup(); + let screen = unsafe { setup.roots().nth((*x_visual_info).screen as usize).unwrap() }; + + // Convert parent into something that X understands + let parent_id = if let Some(p) = parent { + p + } else { + screen.root() + }; + + // Create a colormap + let colormap = xcb_connection.conn.generate_id(); + unsafe { + xcb::create_colormap( + &xcb_connection.conn, + xcb::COLORMAP_ALLOC_NONE as u8, + colormap, + parent_id, + (*x_visual_info).visualid as u32, + ); + } // Create window, connecting to the parent if we have one - let win = conn.generate_id(); - let event_mask = &[ - (xcb::CW_BACK_PIXEL, screen.black_pixel()), + let window_id = xcb_connection.conn.generate_id(); + let cw_values = [ + (xcb::CW_BACK_PIXEL, screen.white_pixel()), + (xcb::CW_BORDER_PIXEL, screen.black_pixel()), ( xcb::CW_EVENT_MASK, xcb::EVENT_MASK_EXPOSURE @@ -39,69 +108,154 @@ impl Window { | xcb::EVENT_MASK_BUTTON_RELEASE | xcb::EVENT_MASK_BUTTON_1_MOTION, ), + (xcb::CW_COLORMAP, colormap), ]; xcb::create_window( // Connection - &conn, + &xcb_connection.conn, // Depth - xcb::COPY_FROM_PARENT as u8, + unsafe { *x_visual_info }.depth as u8, // Window ID - win, + window_id, // Parent ID - if let Some(p) = parent { - p - } else { - screen.root() - }, + parent_id, // x 0, // y 0, // width - 1024, + options.width as u16, // height - 1024, + options.height as u16, // border width 0, // class xcb::WINDOW_CLASS_INPUT_OUTPUT as u16, // visual - screen.root_visual(), - // masks - event_mask, + unsafe { *x_visual_info }.visualid as u32, + // value list + &cw_values, ); + // Don't need the visual info anymore + unsafe { + xlib::XFree(x_visual_info as *mut c_void); + } + // Change window title let title = options.title; xcb::change_property( - &conn, + &xcb_connection.conn, xcb::PROP_MODE_REPLACE as u8, - win, + window_id, xcb::ATOM_WM_NAME, xcb::ATOM_STRING, 8, title.as_bytes(), ); - // Display the window - xcb::map_window(&conn, win); - conn.flush(); + // Load GLX extensions + // We need at least `GLX_ARB_create_context` + let glx_extensions = unsafe { + CStr::from_ptr(glx::glXQueryExtensionsString( + xcb_connection.conn.get_raw_dpy(), + xcb_connection.xlib_display, + )) + .to_str() + .unwrap() + }; + glx_extensions + .find("GLX_ARB_create_context") + .expect("could not find GLX extension GLX_ARB_create_context"); - let x11_window = Self { - xcb_connection: conn, + // With GLX, we don't need a context pre-created in order to load symbols. + // Otherwise, we would need to create a temporary legacy (dummy) GL context to load them. + // (something that has at least GlXCreateContextAttribsARB) + let glx_create_context_attribs: opengl_util::GlXCreateContextAttribsARBProc = + unsafe { std::mem::transmute(opengl_util::load_gl_func("glXCreateContextAttribsARB")) }; + + // Load all other symbols + unsafe { + gl::load_with(|n| opengl_util::load_gl_func(&n)); + } + + // Check GL3 support + if !gl::GenVertexArrays::is_loaded() { + panic!("no GL3 support available!"); + } + + // TODO: This requires a global, which is a no. Figure out if there's a better way to do it. + /* + // installing an event handler to check if error is generated + unsafe { + ctx_error_occurred = false; + } + let old_handler = unsafe { + xlib::XSetErrorHandler(Some(ctx_error_handler)) + }; + */ + + // Create GLX context attributes. (?) + let context_attribs: [c_int; 5] = [ + glx::arb::GLX_CONTEXT_MAJOR_VERSION_ARB as c_int, + 3, + glx::arb::GLX_CONTEXT_MINOR_VERSION_ARB as c_int, + 0, + 0, + ]; + let ctx = unsafe { + glx_create_context_attribs( + xcb_connection.conn.get_raw_dpy(), + fb_config, + null_mut(), + xlib::True, + &context_attribs[0] as *const c_int, + ) }; - x11_window.handle_events(); + if ctx.is_null() + /* || ctx_error_occurred */ + { + panic!("Error when creating a GL 3.0 context"); + } + if unsafe { glx::glXIsDirect(xcb_connection.conn.get_raw_dpy(), ctx) } == 0 { + panic!("Obtained indirect rendering context"); + } + + // Display the window + xcb::map_window(&xcb_connection.conn, window_id); + xcb_connection.conn.flush(); + unsafe { + xlib::XSync(xcb_connection.conn.get_raw_dpy(), xlib::False); + } + + let x11_window = Self { xcb_connection }; + + x11_window.handle_events(window_id, ctx); return x11_window; } // Event loop - fn handle_events(&self) { + fn handle_events(&self, window_id: u32, ctx: *mut x11::glx::__GLXcontextRec) { + let raw_display = self.xcb_connection.conn.get_raw_dpy(); loop { - let ev = self.xcb_connection.wait_for_event(); + let ev = self.xcb_connection.conn.wait_for_event(); if let Some(event) = ev { - println!("{:?}", event.response_type()); + let event_type = event.response_type() & !0x80; + //println!("{:?}", event_type); + + match event_type { + xcb::EXPOSE => unsafe { + glx::glXMakeCurrent(raw_display, window_id as xlib::XID, ctx); + gl::ClearColor(0.3, 0.8, 0.3, 1.0); + gl::Clear(gl::COLOR_BUFFER_BIT); + gl::Flush(); + glx::glXSwapBuffers(raw_display, window_id as xlib::XID); + glx::glXMakeCurrent(raw_display, 0, null_mut()); + }, + _ => {} + } } } } @@ -111,11 +265,15 @@ impl Window { // TODO: currently returning (96, 96) on my system, even though I have 4k screens. Problem with my setup perhaps? #[allow(dead_code)] pub fn get_scaling() -> (u32, u32) { - let (conn, screen_num) = xcb::Connection::connect_with_xlib_display().unwrap(); + // Connect to the X server + let xcb_connection = XcbConnection::new(); // Figure out screen information - let setup = conn.get_setup(); - let screen = setup.roots().nth(screen_num as usize).unwrap(); + let setup = xcb_connection.conn.get_setup(); + let screen = setup + .roots() + .nth(xcb_connection.xlib_display as usize) + .unwrap(); // Get the DPI from the screen struct // diff --git a/src/x11/xcb_connection.rs b/src/x11/xcb_connection.rs new file mode 100644 index 0000000..113b9f2 --- /dev/null +++ b/src/x11/xcb_connection.rs @@ -0,0 +1,15 @@ +/// A very light abstraction around the XCB connection. +/// +/// Keeps track of the xcb connection itself and the xlib display ID that was used to connect. + +pub struct XcbConnection { + pub conn: xcb::Connection, + pub xlib_display: i32, +} + +impl XcbConnection { + pub fn new() -> Self { + let (conn, xlib_display) = xcb::Connection::connect_with_xlib_display().unwrap(); + Self { conn, xlib_display } + } +}