diff --git a/src/gl/x11.rs b/src/gl/x11.rs index 706f0280..c9b0a22b 100644 --- a/src/gl/x11.rs +++ b/src/gl/x11.rs @@ -1,18 +1,19 @@ -use super::{GlConfig, GlError, Profile}; +use super::{GlConfig, GlError}; use crate::x11::XcbConnection; use std::ffi::{c_void, CString}; -use std::os::raw::{c_int, c_ulong}; +use std::os::raw::c_ulong; use std::rc::Rc; use x11_dl::error::OpenError; -use x11_dl::glx; -use x11_dl::glx::Glx; -use x11_dl::xlib; +use x11_dl::glx::GLXContext; mod errors; +mod glx; +use crate::gl::x11::glx::GlxFbConfig; +use glx::Glx; #[derive(Debug)] pub enum CreationFailedError { - InvalidFBConfig, + NoValidFBConfig, NoVisual, GetProcAddressFailed, MakeCurrentFailed, @@ -33,42 +34,18 @@ impl From for GlError { } } -// See https://www.khronos.org/registry/OpenGL/extensions/ARB/GLX_ARB_create_context.txt - -type GlXCreateContextAttribsARB = unsafe extern "C" fn( - dpy: *mut xlib::Display, - fbc: glx::GLXFBConfig, - share_context: glx::GLXContext, - direct: xlib::Bool, - attribs: *const c_int, -) -> glx::GLXContext; - -// See https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_swap_control.txt - -type GlXSwapIntervalEXT = - unsafe extern "C" fn(dpy: *mut xlib::Display, drawable: glx::GLXDrawable, interval: i32); - -// See https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_framebuffer_sRGB.txt - -const GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB: i32 = 0x20B2; - -fn get_proc_address(glx: &Glx, symbol: &str) -> *const c_void { - let symbol = CString::new(symbol).unwrap(); - unsafe { (glx.glXGetProcAddress)(symbol.as_ptr() as *const u8).unwrap() as *const c_void } -} - pub struct GlContext { glx: Glx, window: c_ulong, connection: Rc, - context: glx::GLXContext, + context: GLXContext, } /// The frame buffer configuration along with the general OpenGL configuration to somewhat minimize /// misuse. pub struct FbConfig { gl_config: GlConfig, - fb_config: *mut glx::__GLXFBConfigRec, + fb_config: GlxFbConfig, } /// The configuration a window should be created with after calling @@ -92,76 +69,38 @@ impl GlContext { ) -> Result { let glx = Glx::open()?; - let context = errors::XErrorHandler::handle(&connection, |error_handler| { - #[allow(non_snake_case)] - let glXCreateContextAttribsARB: GlXCreateContextAttribsARB = { - let addr = get_proc_address(&glx, "glXCreateContextAttribsARB"); - if addr.is_null() { - return Err(GlError::CreationFailed(CreationFailedError::GetProcAddressFailed)); - } else { - #[allow(clippy::missing_transmute_annotations)] - std::mem::transmute(addr) - } + errors::XErrorHandler::handle(&connection, |error_handler| { + let Some(create_context) = glx.get_glx_create_context_attribs_arb() else { + return Err(GlError::CreationFailed(CreationFailedError::GetProcAddressFailed)); }; - #[allow(non_snake_case)] - let glXSwapIntervalEXT: GlXSwapIntervalEXT = { - let addr = get_proc_address(&glx, "glXSwapIntervalEXT"); - if addr.is_null() { - return Err(GlError::CreationFailed(CreationFailedError::GetProcAddressFailed)); - } else { - #[allow(clippy::missing_transmute_annotations)] - std::mem::transmute(addr) - } + let Some(swap_interval) = glx.get_glx_swap_interval_ext() else { + return Err(GlError::CreationFailed(CreationFailedError::GetProcAddressFailed)); }; - error_handler.check()?; - - let profile_mask = match config.gl_config.profile { - Profile::Core => glx::arb::GLX_CONTEXT_CORE_PROFILE_BIT_ARB, - Profile::Compatibility => glx::arb::GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB, - }; - - #[rustfmt::skip] - let ctx_attribs = [ - glx::arb::GLX_CONTEXT_MAJOR_VERSION_ARB, config.gl_config.version.0 as i32, - glx::arb::GLX_CONTEXT_MINOR_VERSION_ARB, config.gl_config.version.1 as i32, - glx::arb::GLX_CONTEXT_PROFILE_MASK_ARB, profile_mask, - 0, - ]; - - let context = glXCreateContextAttribsARB( - connection.dpy, + let context = create_context.call( + &connection, + &config.gl_config, config.fb_config, - std::ptr::null_mut(), - 1, - ctx_attribs.as_ptr(), - ); - - error_handler.check()?; - - if context.is_null() { - return Err(GlError::CreationFailed(CreationFailedError::ContextCreationFailed)); - } - - let res = (glx.glXMakeCurrent)(connection.dpy, window, context); - error_handler.check()?; - if res == 0 { - return Err(GlError::CreationFailed(CreationFailedError::MakeCurrentFailed)); - } - - glXSwapIntervalEXT(connection.dpy, window, config.gl_config.vsync as i32); - error_handler.check()?; - - if (glx.glXMakeCurrent)(connection.dpy, 0, std::ptr::null_mut()) == 0 { - error_handler.check()?; - return Err(GlError::CreationFailed(CreationFailedError::MakeCurrentFailed)); - } + error_handler, + )?; + + // Create context object here so that error or panic will properly free the context + let context = GlContext { glx, window, connection: Rc::clone(&connection), context }; + + context.glx.with_current_context( + &connection, + window, + context.context, + error_handler, + || { + swap_interval(connection.dpy, window, config.gl_config.vsync as i32); + error_handler.check() + }, + )??; Ok(context) - })?; - - Ok(GlContext { glx, window, connection, context }) + }) } /// Find a matching framebuffer config and window visual for the given OpenGL configuration. @@ -173,45 +112,11 @@ impl GlContext { let glx = Glx::open()?; errors::XErrorHandler::handle(conn, |error_handler| { - let screen = conn.screen; - - #[rustfmt::skip] - let fb_attribs = [ - glx::GLX_X_RENDERABLE, 1, - glx::GLX_X_VISUAL_TYPE, glx::GLX_TRUE_COLOR, - glx::GLX_DRAWABLE_TYPE, glx::GLX_WINDOW_BIT, - glx::GLX_RENDER_TYPE, glx::GLX_RGBA_BIT, - glx::GLX_RED_SIZE, config.red_bits as i32, - glx::GLX_GREEN_SIZE, config.green_bits as i32, - glx::GLX_BLUE_SIZE, config.blue_bits as i32, - glx::GLX_ALPHA_SIZE, config.alpha_bits as i32, - glx::GLX_DEPTH_SIZE, config.depth_bits as i32, - glx::GLX_STENCIL_SIZE, config.stencil_bits as i32, - glx::GLX_DOUBLEBUFFER, config.double_buffer as i32, - glx::GLX_SAMPLE_BUFFERS, config.samples.is_some() as i32, - glx::GLX_SAMPLES, config.samples.unwrap_or(0) as i32, - GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB, config.srgb as i32, - 0, - ]; - - let mut n_configs = 0; - let fb_config = unsafe { - (glx.glXChooseFBConfig)(conn.dpy, screen, fb_attribs.as_ptr(), &mut n_configs) - }; - - error_handler.check()?; - if n_configs <= 0 || fb_config.is_null() { - return Err(GlError::CreationFailed(CreationFailedError::InvalidFBConfig)); - } + let fb_config = glx.choose_best_fb_config(conn, &config, error_handler)?; // Now that we have a matching framebuffer config, we need to know which visual matches - // thsi config so the window is compatible with the OpenGL context we're about to create - let fb_config = unsafe { fb_config.read() }; - let visual = unsafe { (glx.glXGetVisualFromFBConfig)(conn.dpy, fb_config) }; - if visual.is_null() { - return Err(GlError::CreationFailed(CreationFailedError::NoVisual)); - } - let visual = unsafe { visual.read() }; + // this config so the window is compatible with the OpenGL context we're about to create + let visual = glx.get_visual_from_fb_config(conn, fb_config, error_handler)?; Ok(( FbConfig { fb_config, gl_config: config }, @@ -222,42 +127,36 @@ impl GlContext { pub unsafe fn make_current(&self) { errors::XErrorHandler::handle(&self.connection, |error_handler| { - let res = (self.glx.glXMakeCurrent)(self.connection.dpy, self.window, self.context); - error_handler.check().unwrap(); - if res == 0 { - panic!("make_current failed") - } + self.glx + .make_current(&self.connection, self.window, self.context, error_handler) + .unwrap(); }) } pub unsafe fn make_not_current(&self) { errors::XErrorHandler::handle(&self.connection, |error_handler| { - let res = (self.glx.glXMakeCurrent)(self.connection.dpy, 0, std::ptr::null_mut()); - error_handler.check().unwrap(); - if res == 0 { - panic!("make_not_current failed") - } + self.glx.clear_current(&self.connection, error_handler).unwrap(); }) } pub fn get_proc_address(&self, symbol: &str) -> *const c_void { - get_proc_address(&self.glx, symbol) + let symbol = CString::new(symbol).unwrap(); + + match self.glx.get_proc_address(&symbol) { + Some(ptr) => ptr.as_ptr(), + None => std::ptr::null(), + } } pub fn swap_buffers(&self) { - unsafe { - errors::XErrorHandler::handle(&self.connection, |error_handler| { - (self.glx.glXSwapBuffers)(self.connection.dpy, self.window); - error_handler.check().unwrap(); - }) - } + errors::XErrorHandler::handle(&self.connection, |error_handler| { + self.glx.swap_buffers(&self.connection, self.window, error_handler).unwrap() + }) } } impl Drop for GlContext { fn drop(&mut self) { - unsafe { - (self.glx.glXDestroyContext)(self.connection.dpy, self.context); - } + unsafe { self.glx.destroy_context(&self.connection, self.context) } } } diff --git a/src/gl/x11/errors.rs b/src/gl/x11/errors.rs index 7c63d715..f1808861 100644 --- a/src/gl/x11/errors.rs +++ b/src/gl/x11/errors.rs @@ -3,31 +3,31 @@ use std::fmt::{Debug, Display, Formatter}; use x11_dl::xlib; use crate::x11::XcbConnection; -use std::cell::RefCell; +use std::cell::Cell; use std::error::Error; use std::os::raw::{c_int, c_uchar, c_ulong}; use std::panic::AssertUnwindSafe; thread_local! { /// Used as part of [`XErrorHandler::handle()`]. When an X11 error occurs during this function, - /// the error gets copied to this RefCell after which the program is allowed to resume. The + /// the error gets copied to this Cell after which the program is allowed to resume. The /// error can then be converted to a regular Rust Result value afterward. - static CURRENT_X11_ERROR: RefCell> = const { RefCell::new(None) }; + static CURRENT_X11_ERROR: Cell> = const { Cell::new(None) }; } /// A helper struct for safe X11 error handling pub struct XErrorHandler<'a> { conn: &'a XcbConnection, - error: &'a RefCell>, + error: &'a Cell>, } impl<'a> XErrorHandler<'a> { /// Syncs and checks if any previous X11 calls from the given display returned an error - pub fn check(&mut self) -> Result<(), XLibError> { + pub fn check(&self) -> Result<(), XLibError> { // Flush all possible previous errors unsafe { (self.conn.xlib.XSync)(self.conn.dpy, 0) }; - let error = self.error.borrow_mut().take(); + let error = self.error.take(); match error { None => Ok(()), @@ -43,17 +43,16 @@ impl<'a> XErrorHandler<'a> { unsafe extern "C" fn error_handler( _dpy: *mut xlib::Display, err: *mut xlib::XErrorEvent, ) -> i32 { - // SAFETY: the error pointer should be safe to access - let err = &*err; + // SAFETY: the error pointer should always be valid for reads + let err = unsafe { err.read() }; CURRENT_X11_ERROR.with(|error| { - let mut error = error.borrow_mut(); - match error.as_mut() { + match error.get() { // If multiple errors occur, keep the first one since that's likely going to be the // cause of the other errors Some(_) => 1, None => { - *error = Some(CaughtXLibError::from_event(err)); + error.set(Some(CaughtXLibError::from_event(err))); 0 } } @@ -65,9 +64,7 @@ impl<'a> XErrorHandler<'a> { CURRENT_X11_ERROR.with(|error| { // Make sure to clear any errors from the last call to this function - { - *error.borrow_mut() = None; - } + error.set(None); let old_handler = unsafe { (conn.xlib.XSetErrorHandler)(Some(error_handler)) }; let panic_result = std::panic::catch_unwind(AssertUnwindSafe(|| { @@ -85,6 +82,7 @@ impl<'a> XErrorHandler<'a> { } } +#[derive(Copy, Clone)] struct CaughtXLibError { type_: c_int, resourceid: xlib::XID, @@ -95,7 +93,7 @@ struct CaughtXLibError { } impl CaughtXLibError { - fn from_event(error: &xlib::XErrorEvent) -> CaughtXLibError { + fn from_event(error: xlib::XErrorEvent) -> CaughtXLibError { Self { type_: error.type_, resourceid: error.resourceid, diff --git a/src/gl/x11/glx.rs b/src/gl/x11/glx.rs new file mode 100644 index 00000000..d794469f --- /dev/null +++ b/src/gl/x11/glx.rs @@ -0,0 +1,244 @@ +use crate::gl::platform::CreationFailedError; +use crate::gl::x11::errors::XErrorHandler; +use crate::gl::{GlConfig, GlError, Profile}; +use crate::x11::XcbConnection; +use std::ffi::{c_ulong, c_void, CStr}; +use std::os::raw::c_int; +use std::ptr::NonNull; +use x11_dl::glx::{arb::*, *}; +use x11_dl::xlib; +use x11_dl::xlib::XVisualInfo; + +/// See https://www.khronos.org/registry/OpenGL/extensions/ARB/GLX_ARB_create_context.txt +type GlXCreateContextAttribsARB = unsafe extern "C" fn( + dpy: *mut xlib::Display, + fbc: GLXFBConfig, + share_context: GLXContext, + direct: xlib::Bool, + attribs: *const c_int, +) -> GLXContext; + +/// See https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_swap_control.txt +type GlXSwapIntervalEXT = + unsafe extern "C" fn(dpy: *mut xlib::Display, drawable: GLXDrawable, interval: i32); + +/// See https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_framebuffer_sRGB.txt +const GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB: i32 = 0x20B2; + +pub struct Glx { + inner: x11_dl::glx::Glx, +} + +impl Glx { + pub fn open() -> Result { + Ok(Self { inner: x11_dl::glx::Glx::open()? }) + } + + fn get_fb_attribs(config: &GlConfig) -> [c_int; 29] { + #[rustfmt::skip] + let fb_attribs = [ + GLX_X_RENDERABLE, 1, + GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR, + GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_RED_SIZE, config.red_bits as i32, + GLX_GREEN_SIZE, config.green_bits as i32, + GLX_BLUE_SIZE, config.blue_bits as i32, + GLX_ALPHA_SIZE, config.alpha_bits as i32, + GLX_DEPTH_SIZE, config.depth_bits as i32, + GLX_STENCIL_SIZE, config.stencil_bits as i32, + GLX_DOUBLEBUFFER, config.double_buffer as i32, + GLX_SAMPLE_BUFFERS, config.samples.is_some() as i32, + GLX_SAMPLES, config.samples.unwrap_or(0) as i32, + GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB, config.srgb as i32, + 0, + ]; + + fb_attribs + } + + pub fn choose_best_fb_config( + &self, connection: &XcbConnection, config: &GlConfig, error_handler: &XErrorHandler, + ) -> Result { + let fb_attribs = Self::get_fb_attribs(config); + + let mut nelements = 0; + // SAFETY: XcbConnection guarantees the inner dpy is valid. + // The fb_attribs and nelements pointers come from references and are therefore valid. + let result = unsafe { + (self.inner.glXChooseFBConfig)( + connection.dpy, + connection.screen, + fb_attribs.as_ptr(), + &mut nelements, + ) + }; + + error_handler.check()?; + if nelements == 0 || result.is_null() { + return Err(GlError::CreationFailed(CreationFailedError::NoValidFBConfig)); + } + + // SAFETY: If nelements != 0, the result pointer is non-null, and no Xlib error occured, then + // there must be at least one element in that array, which is valid for reads. + let first_result = unsafe { result.read() }; + + // SAFETY: for the same reasons above, glXChooseFBConfig returned a valid array + // that we must free ourselves. + unsafe { (connection.xlib.XFree)(result.cast()) }; + + Ok(GlxFbConfig(first_result)) + } + + pub fn get_visual_from_fb_config( + &self, connection: &XcbConnection, fb_config: GlxFbConfig, error_handler: &XErrorHandler, + ) -> Result { + // SAFETY: XcbConnection guarantees the inner dpy is valid. + let result = unsafe { (self.inner.glXGetVisualFromFBConfig)(connection.dpy, fb_config.0) }; + + error_handler.check()?; + if result.is_null() { + return Err(GlError::CreationFailed(CreationFailedError::NoVisual)); + } + + // SAFETY: If the result pointer is non-null, and no Xlib error occured, then + // glXGetVisualFromFBConfig must have returned a valid result. + let visual = unsafe { result.read() }; + // SAFETY: for the same reasons above, glXGetVisualFromFBConfig returned a valid array + // that we must free ourselves. + unsafe { (connection.xlib.XFree)(result.cast()) }; + + Ok(visual) + } + + pub fn swap_buffers( + &self, connection: &XcbConnection, window_id: c_ulong, error_handler: &XErrorHandler, + ) -> Result<(), GlError> { + // SAFETY: XcbConnection guarantees the inner dpy is valid. + unsafe { (self.inner.glXSwapBuffers)(connection.dpy, window_id) }; + + Ok(error_handler.check()?) + } + + pub fn get_proc_address(&self, proc_name: &CStr) -> Option> { + let result = unsafe { (self.inner.glXGetProcAddress)(proc_name.as_ptr().cast())? }; + + NonNull::new(result as *mut c_void) + } + + pub fn get_glx_swap_interval_ext(&self) -> Option { + let ptr = self.get_proc_address(c"glXSwapIntervalEXT")?; + + // SAFETY: NonNull is repr(transparent), GlXSwapIntervalEXT is the correct type for this function pointer + Some(unsafe { core::mem::transmute::, GlXSwapIntervalEXT>(ptr) }) + } + + pub fn get_glx_create_context_attribs_arb(&self) -> Option { + let ptr = self.get_proc_address(c"glXCreateContextAttribsARB")?; + + // SAFETY: NonNull is repr(transparent), GlxCreateContextAttribsARB is the correct type for this function pointer + Some(GlxCreateContextAttribsARB(unsafe { + core::mem::transmute::, GlXCreateContextAttribsARB>(ptr) + })) + } + + pub unsafe fn destroy_context(&self, connection: &XcbConnection, context: GLXContext) { + // SAFETY: + unsafe { (self.inner.glXDestroyContext)(connection.dpy, context) }; + } + + pub unsafe fn make_current( + &self, connection: &XcbConnection, window_id: c_ulong, context: GLXContext, + error_handler: &XErrorHandler, + ) -> Result<(), GlError> { + let res = unsafe { (self.inner.glXMakeCurrent)(connection.dpy, window_id, context) }; + + error_handler.check()?; + if res == 0 { + return Err(GlError::CreationFailed(CreationFailedError::MakeCurrentFailed)); + } + + Ok(()) + } + + pub unsafe fn clear_current( + &self, connection: &XcbConnection, error_handler: &XErrorHandler, + ) -> Result<(), GlError> { + self.make_current(connection, 0, core::ptr::null_mut(), error_handler) + } + + pub unsafe fn with_current_context( + &self, connection: &XcbConnection, window_id: c_ulong, context: GLXContext, + error_handler: &XErrorHandler, closure: impl FnOnce() -> T, + ) -> Result { + self.make_current(connection, window_id, context, error_handler)?; + + // Using a "drop" allows us to clear the GL context even if the given closure panics + let clearer = ContextClearOnDrop { glx: self, connection, error_handler }; + + let result = closure(); + + drop(clearer); + + Ok(result) + } +} + +pub struct ContextClearOnDrop<'a> { + glx: &'a Glx, + connection: &'a XcbConnection, + error_handler: &'a XErrorHandler<'a>, +} + +impl Drop for ContextClearOnDrop<'_> { + fn drop(&mut self) { + let _ = unsafe { self.glx.clear_current(self.connection, self.error_handler) }; + } +} + +pub struct GlxCreateContextAttribsARB(GlXCreateContextAttribsARB); + +impl GlxCreateContextAttribsARB { + fn get_ctx_attribs(config: &GlConfig) -> [c_int; 7] { + let profile_mask = match config.profile { + Profile::Core => GLX_CONTEXT_CORE_PROFILE_BIT_ARB, + Profile::Compatibility => GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB, + }; + + #[rustfmt::skip] + let ctx_attribs = [ + GLX_CONTEXT_MAJOR_VERSION_ARB, config.version.0 as i32, + GLX_CONTEXT_MINOR_VERSION_ARB, config.version.1 as i32, + GLX_CONTEXT_PROFILE_MASK_ARB, profile_mask, + 0, + ]; + + ctx_attribs + } + + pub fn call( + &self, connection: &XcbConnection, gl_config: &GlConfig, glx_fb_config: GlxFbConfig, + error_handler: &XErrorHandler, + ) -> Result { + let ctx_attribs = Self::get_ctx_attribs(gl_config); + + let context = unsafe { + self.0(connection.dpy, glx_fb_config.0, std::ptr::null_mut(), 1, ctx_attribs.as_ptr()) + }; + + error_handler.check()?; + + if context.is_null() { + return Err(GlError::CreationFailed(CreationFailedError::ContextCreationFailed)); + } + + Ok(context) + } +} + +/// Handle to a GLX Framebuffer configuration object. +/// +/// These point to objects in a global table managed by glXChooseFBConfig, and are never destroyed. +/// Therefore, these have the 'static lifetime, and are always safe to use. +#[derive(Copy, Clone)] +pub struct GlxFbConfig(GLXFBConfig);