Skip to main content

optic_render/
context.rs

1use khronos_egl as egl;
2use optic_core::{OpticError, OpticErrorKind, OpticResult, Size2D};
3use raw_window_handle::RawWindowHandle;
4use std::ffi::c_void;
5use std::ptr;
6
7/// An EGL window surface and its dimensions.
8///
9/// Created by [`RenderContext::new_windowed`] or [`RenderContext::attach_window`].
10/// Each surface corresponds to one native window.
11pub struct WindowSurface {
12    pub surface: egl::Surface,
13    pub size: Size2D,
14}
15
16/// EGL + OpenGL 4.6 context with support for multiple window surfaces.
17///
18/// # Initialisation
19///
20/// Create a headless context for off-screen rendering:
21///
22/// ```ignore
23/// let ctx = RenderContext::new_headless()?;
24/// ```
25///
26/// Or create a windowed context from a raw window handle:
27///
28/// ```ignore
29/// let ctx = RenderContext::new_windowed(raw_handle, display_handle, size)?;
30/// ```
31///
32/// # Multi-window
33///
34/// Additional windows can be attached with [`attach_window`](RenderContext::attach_window)
35/// and made current with [`make_current`](RenderContext::make_current).
36pub struct RenderContext {
37    pub display: egl::Display,
38    pub context: egl::Context,
39    config: egl::Config,
40    pub surfaces: Vec<WindowSurface>,
41    pub active_index: Option<usize>,
42    pub gl_ver: String,
43    pub glsl_ver: String,
44    pub device: String,
45}
46
47const GL_ATTRIBS: [i32; 7] = [
48    egl::CONTEXT_MAJOR_VERSION as i32, 4,
49    egl::CONTEXT_MINOR_VERSION as i32, 6,
50    egl::CONTEXT_OPENGL_PROFILE_MASK as i32,
51    egl::CONTEXT_OPENGL_CORE_PROFILE_BIT as i32,
52    egl::NONE as i32,
53];
54
55const PBUFFER_ATTRIBS: [i32; 5] = [
56    egl::WIDTH as i32, 1,
57    egl::HEIGHT as i32, 1,
58    egl::NONE as i32,
59];
60
61const CONFIG_ATTRIBS: [i32; 15] = [
62    egl::SURFACE_TYPE as i32, egl::PBUFFER_BIT as i32 | egl::WINDOW_BIT as i32,
63    egl::RENDERABLE_TYPE as i32, egl::OPENGL_BIT as i32,
64    egl::RED_SIZE as i32, 8,
65    egl::GREEN_SIZE as i32, 8,
66    egl::BLUE_SIZE as i32, 8,
67    egl::ALPHA_SIZE as i32, 8,
68    egl::DEPTH_SIZE as i32, 24,
69    egl::NONE as i32,
70];
71
72fn create_display_and_context() -> OpticResult<(egl::Instance<egl::Static>, egl::Display, egl::Context, egl::Config)> {
73    let egl_instance = egl::Instance::new(egl::Static);
74
75    let display = unsafe {
76        egl_instance.get_display(egl::DEFAULT_DISPLAY)
77            .ok_or_else(|| OpticError::new(OpticErrorKind::OpenGL, "no EGL display found"))?
78    };
79
80    egl_instance.initialize(display)
81        .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("EGL init failed: {e}")))?;
82
83    let mut configs = Vec::with_capacity(1);
84    egl_instance.choose_config(display, &CONFIG_ATTRIBS, &mut configs)
85        .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("EGL config failed: {e}")))?;
86
87    let config = *configs.first()
88        .ok_or_else(|| OpticError::new(OpticErrorKind::OpenGL, "no suitable EGL config"))?;
89
90    egl_instance.bind_api(egl::OPENGL_API)
91        .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("EGL bind API failed: {e}")))?;
92
93    let context = egl_instance.create_context(display, config, None, &GL_ATTRIBS)
94        .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("EGL context creation failed: {e}")))?;
95
96    Ok((egl_instance, display, context, config))
97}
98
99fn load_gl_info() -> (String, String, String) {
100    let gl_ver = unsafe {
101        let ptr = gl::GetString(gl::VERSION);
102        if ptr.is_null() {
103            return ("unknown".into(), "unknown".into(), "unknown".into());
104        }
105        std::ffi::CStr::from_ptr(ptr as *const i8)
106            .to_string_lossy()
107            .to_string()
108    };
109
110    let glsl_ver = unsafe {
111        let ptr = gl::GetString(gl::SHADING_LANGUAGE_VERSION);
112        if ptr.is_null() {
113            return (gl_ver, "unknown".into(), "unknown".into());
114        }
115        std::ffi::CStr::from_ptr(ptr as *const i8)
116            .to_string_lossy()
117            .to_string()
118    };
119
120    let device = unsafe {
121        let ptr = gl::GetString(gl::RENDERER);
122        if ptr.is_null() {
123            return (gl_ver, glsl_ver, "unknown".into());
124        }
125        format!(
126            "OPENGL {}",
127            std::ffi::CStr::from_ptr(ptr as *const i8)
128                .to_string_lossy()
129        )
130    };
131
132    (gl_ver, glsl_ver, device)
133}
134
135fn raw_handle_to_native(handle: RawWindowHandle) -> OpticResult<*mut c_void> {
136    match handle {
137        RawWindowHandle::Xlib(h) => Ok(h.window as usize as *mut c_void),
138        RawWindowHandle::Xcb(h) => Ok(h.window.get() as usize as *mut c_void),
139        RawWindowHandle::Wayland(h) => Ok(h.surface.as_ptr() as *mut c_void),
140        RawWindowHandle::Win32(h) => Ok(h.hwnd.get() as *mut c_void),
141        _ => Err(OpticError::new(
142            OpticErrorKind::OpenGL,
143            "unsupported platform for EGL window surface",
144        )),
145    }
146}
147
148impl RenderContext {
149    /// Creates a headless EGL context with a 1×1 pbuffer surface.
150    ///
151    /// This is useful for off-screen rendering or when no window is available.
152    /// Loads OpenGL function pointers via EGL and queries driver info.
153    pub fn new_headless() -> OpticResult<Self> {
154        let (egl_instance, display, context, config) = create_display_and_context()?;
155
156        let pbuffer = egl_instance.create_pbuffer_surface(display, config, &PBUFFER_ATTRIBS)
157            .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("pbuffer creation failed: {e}")))?;
158
159        egl_instance.make_current(display, Some(pbuffer), Some(pbuffer), Some(context))
160            .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("make current failed: {e}")))?;
161
162        gl::load_with(|s| {
163            egl_instance.get_proc_address(s)
164                .map(|p| p as *const _)
165                .unwrap_or(ptr::null())
166        });
167
168        let (gl_ver, glsl_ver, device) = load_gl_info();
169        let pbuffer_surface = WindowSurface { surface: pbuffer, size: Size2D::from(1, 1) };
170
171        Ok(Self {
172            display,
173            context,
174            config,
175            surfaces: vec![pbuffer_surface],
176            active_index: Some(0),
177            gl_ver,
178            glsl_ver,
179            device,
180        })
181    }
182
183    /// Creates a windowed EGL context from raw window and display handles.
184    ///
185    /// Supports X11 (with optional visual ID matching), Wayland, and Win32
186    /// platforms. Falls back to the default EGL display if platform-specific
187    /// display creation fails.
188    pub fn new_windowed(
189        raw_handle: RawWindowHandle,
190        display_handle: raw_window_handle::RawDisplayHandle,
191        size: Size2D,
192    ) -> OpticResult<Self> {
193        let egl_instance = egl::Instance::new(egl::Static);
194
195        // Use platform-specific display for better compatibility
196        let display: egl::Display = match display_handle {
197            raw_window_handle::RawDisplayHandle::Xlib(h) => {
198                let platform = 0x31D5; // EGL_PLATFORM_X11_EXT
199                let native_display = h.display.map_or(std::ptr::null_mut(), |d| d.as_ptr());
200                let r = unsafe {
201                    egl_instance.get_platform_display(platform, native_display, &[])
202                };
203                match r {
204                    Ok(d) => d,
205                    Err(_) => unsafe {
206                        egl_instance.get_display(egl::DEFAULT_DISPLAY)
207                            .ok_or_else(|| OpticError::new(OpticErrorKind::OpenGL, "no EGL display found"))?
208                    },
209                }
210            }
211            raw_window_handle::RawDisplayHandle::Wayland(h) => {
212                let platform = 0x31D6; // EGL_PLATFORM_WAYLAND_EXT
213                let native_display = h.display.as_ptr() as *mut c_void;
214                let r = unsafe {
215                    egl_instance.get_platform_display(platform, native_display, &[])
216                };
217                match r {
218                    Ok(d) => d,
219                    Err(_) => unsafe {
220                        egl_instance.get_display(egl::DEFAULT_DISPLAY)
221                            .ok_or_else(|| OpticError::new(OpticErrorKind::OpenGL, "no EGL display found"))?
222                    },
223                }
224            }
225            _ => unsafe {
226                egl_instance.get_display(egl::DEFAULT_DISPLAY)
227                    .ok_or_else(|| OpticError::new(OpticErrorKind::OpenGL, "no EGL display found"))?
228            },
229        };
230
231        egl_instance.initialize(display)
232            .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("EGL init failed: {e}")))?;
233
234        let native = raw_handle_to_native(raw_handle)?;
235
236        let visual_id: Option<u32> = match raw_handle {
237            RawWindowHandle::Xlib(h) => Some(h.visual_id as u32),
238            RawWindowHandle::Xcb(h) => h.visual_id.map(|v| v.get()),
239            _ => None,
240        };
241
242        let result = if let Some(vid) = visual_id {
243            Self::try_create_windowed(&egl_instance, display, native, size, Some(vid))
244        } else {
245            Self::try_create_windowed(&egl_instance, display, native, size, None)
246        };
247
248        match result {
249            Ok(ctx) => Ok(ctx),
250            Err(_) if visual_id.is_some() => {
251                Self::try_create_windowed(&egl_instance, display, native, size, None)
252            }
253            Err(e) => Err(e),
254        }
255    }
256
257    fn try_create_windowed(
258        egl_instance: &egl::Instance<egl::Static>,
259        display: egl::Display,
260        native: *mut c_void,
261        size: Size2D,
262        visual_id: Option<u32>,
263    ) -> OpticResult<RenderContext> {
264        let mut cfg_attribs = Vec::new();
265        cfg_attribs.push(egl::SURFACE_TYPE as i32);
266        cfg_attribs.push(egl::WINDOW_BIT as i32);
267        cfg_attribs.push(egl::RENDERABLE_TYPE as i32);
268        cfg_attribs.push(egl::OPENGL_BIT as i32);
269        cfg_attribs.push(egl::RED_SIZE as i32); cfg_attribs.push(8);
270        cfg_attribs.push(egl::GREEN_SIZE as i32); cfg_attribs.push(8);
271        cfg_attribs.push(egl::BLUE_SIZE as i32); cfg_attribs.push(8);
272        cfg_attribs.push(egl::ALPHA_SIZE as i32); cfg_attribs.push(8);
273        cfg_attribs.push(egl::DEPTH_SIZE as i32); cfg_attribs.push(24);
274        if let Some(vid) = visual_id {
275            cfg_attribs.push(egl::NATIVE_VISUAL_ID as i32);
276            cfg_attribs.push(vid as i32);
277        }
278        cfg_attribs.push(egl::NONE as i32);
279
280        let mut configs = Vec::with_capacity(1);
281        egl_instance.choose_config(display, &cfg_attribs, &mut configs)
282            .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("EGL config failed: {e}")))?;
283
284        let config = *configs.first()
285            .ok_or_else(|| OpticError::new(OpticErrorKind::OpenGL, "no suitable EGL config"))?;
286
287        egl_instance.bind_api(egl::OPENGL_API)
288            .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("EGL bind API failed: {e}")))?;
289
290        let context = egl_instance.create_context(display, config, None, &GL_ATTRIBS)
291            .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("EGL context creation failed: {e}")))?;
292
293        let surface = unsafe { egl_instance.create_window_surface(display, config, native, None)
294            .map_err(|e| OpticError::new(
295                OpticErrorKind::OpenGL,
296                &format!("EGL window surface creation failed: {e}"),
297            ))? };
298
299        egl_instance.make_current(display, Some(surface), Some(surface), Some(context))
300            .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("make current failed: {e}")))?;
301
302        gl::load_with(|s| {
303            egl_instance.get_proc_address(s)
304                .map(|p| p as *const _)
305                .unwrap_or(ptr::null())
306        });
307
308        let (gl_ver, glsl_ver, device) = load_gl_info();
309        let window_surface = WindowSurface { surface, size };
310
311        Ok(Self {
312            display,
313            context,
314            config,
315            surfaces: vec![window_surface],
316            active_index: Some(0),
317            gl_ver,
318            glsl_ver,
319            device,
320        })
321    }
322
323    /// Attaches a new window surface to this context.
324    ///
325    /// Returns the index of the new surface, which can be used with
326    /// [`make_current`](RenderContext::make_current).
327    pub fn attach_window(
328        &mut self,
329        raw_handle: RawWindowHandle,
330        size: Size2D,
331    ) -> OpticResult<usize> {
332        let native = raw_handle_to_native(raw_handle)?;
333        let egl = egl::Instance::new(egl::Static);
334
335        let surface = unsafe { egl.create_window_surface(
336            self.display,
337            self.config,
338            native,
339            None,
340        ).map_err(|e| OpticError::new(
341            OpticErrorKind::OpenGL,
342            &format!("EGL window surface creation failed: {e}"),
343        ))? };
344
345        let index = self.surfaces.len();
346        self.surfaces.push(WindowSurface { surface, size });
347        Ok(index)
348    }
349
350    /// Resizes the tracked size for a window surface.
351    ///
352    /// Does **not** call EGL surface resize — just updates the stored
353    /// dimensions used by [`make_current`](RenderContext::make_current)
354    /// for the viewport call.
355    pub fn resize_window(&mut self, index: usize, size: Size2D) {
356        if let Some(ws) = self.surfaces.get_mut(index) {
357            ws.size = size;
358        }
359    }
360
361    /// Makes the given surface current and sets the viewport.
362    pub fn make_current(&self, index: usize) -> OpticResult<()> {
363        let egl_instance = egl::Instance::new(egl::Static);
364        let ws = self.surfaces.get(index).ok_or_else(|| {
365            OpticError::new(OpticErrorKind::OpenGL, "invalid surface index")
366        })?;
367
368        egl_instance.make_current(self.display, Some(ws.surface), Some(ws.surface), Some(self.context))
369            .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("make current failed: {e}")))?;
370
371        unsafe { gl::Viewport(0, 0, ws.size.w as i32, ws.size.h as i32); }
372        Ok(())
373    }
374
375    /// Swaps front and back buffers for the given surface (double-buffering).
376    pub fn swap_buffers(&self, index: usize) -> OpticResult<()> {
377        let egl_instance = egl::Instance::new(egl::Static);
378        let ws = self.surfaces.get(index).ok_or_else(|| {
379            OpticError::new(OpticErrorKind::OpenGL, "invalid surface index")
380        })?;
381
382        egl_instance.swap_buffers(self.display, ws.surface)
383            .map_err(|e| OpticError::new(OpticErrorKind::OpenGL, &format!("swap buffers failed: {e}")))
384    }
385
386    /// Clears the colour and depth buffers of the currently bound framebuffer.
387    pub fn clear(&self) {
388        unsafe { gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT); }
389    }
390
391    /// Enables or disables vertical sync (swap interval).
392    pub fn set_vsync(&self, enable: bool) {
393        let egl_instance = egl::Instance::new(egl::Static);
394        let interval = if enable { 1 } else { 0 };
395        let _ = egl_instance.swap_interval(self.display, interval);
396    }
397
398    /// Sets the clear colour used by [`clear`](RenderContext::clear).
399    pub fn set_clear_color(&self, color: optic_core::RGBA) {
400        unsafe { gl::ClearColor(color.0, color.1, color.2, color.3); }
401    }
402}
403
404impl Drop for RenderContext {
405    fn drop(&mut self) {
406        let egl_instance = egl::Instance::new(egl::Static);
407        let _ = egl_instance.make_current(self.display, None, None, None);
408        for ws in self.surfaces.drain(..) {
409            let _ = egl_instance.destroy_surface(self.display, ws.surface);
410        }
411        let _ = egl_instance.destroy_context(self.display, self.context);
412        let _ = egl_instance.terminate(self.display);
413    }
414}