From 4fb4d8712598c2a3ba45e6bd43de63492c728663 Mon Sep 17 00:00:00 2001 From: drendog Date: Tue, 18 Nov 2025 19:14:53 +0100 Subject: [PATCH] feat: shared egl context across outputs --- crates/adapters/src/rendering/egl/context.rs | 8 + .../src/rendering/egl/context_factory.rs | 237 ++++++++++++++++++ crates/adapters/src/rendering/egl/mod.rs | 1 + crates/adapters/src/wayland/shell_adapter.rs | 25 +- .../src/wayland/surfaces/popup_manager.rs | 16 +- 5 files changed, 273 insertions(+), 14 deletions(-) create mode 100644 crates/adapters/src/rendering/egl/context_factory.rs diff --git a/crates/adapters/src/rendering/egl/context.rs b/crates/adapters/src/rendering/egl/context.rs index a23129d..67868ff 100644 --- a/crates/adapters/src/rendering/egl/context.rs +++ b/crates/adapters/src/rendering/egl/context.rs @@ -107,6 +107,14 @@ impl EGLContext { EGLContextBuilder::new() } + #[must_use] + pub(super) fn from_raw( + surface: Surface, + context: PossiblyCurrentContext, + ) -> Self { + Self { surface, context } + } + fn ensure_current(&self) -> Result<()> { if !self.context.is_current() { self.context diff --git a/crates/adapters/src/rendering/egl/context_factory.rs b/crates/adapters/src/rendering/egl/context_factory.rs new file mode 100644 index 0000000..ba62713 --- /dev/null +++ b/crates/adapters/src/rendering/egl/context_factory.rs @@ -0,0 +1,237 @@ +use super::context::EGLContext; +use crate::errors::{EGLError, LayerShikaError, Result}; +use glutin::{ + api::egl::{ + config::Config, + context::{NotCurrentContext, PossiblyCurrentContext}, + display::Display, + surface::Surface, + }, + config::ConfigTemplateBuilder, + context::ContextAttributesBuilder, + prelude::*, + surface::{SurfaceAttributesBuilder, WindowSurface}, +}; +use log::{debug, info}; +use raw_window_handle::{ + RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, +}; +use slint::PhysicalSize; +use std::{cell::RefCell, ffi::c_void, num::NonZeroU32, ptr::NonNull, rc::Rc}; +use wayland_client::backend::ObjectId; + +pub struct RenderContextFactory { + shared_state: RefCell>, +} + +struct SharedRenderState { + display: Display, + config: Config, + primary_context: PossiblyCurrentContext, +} + +impl RenderContextFactory { + #[must_use] + pub fn new() -> Rc { + Rc::new(Self { + shared_state: RefCell::new(None), + }) + } + + pub fn create_context( + &self, + display_id: &ObjectId, + surface_id: &ObjectId, + size: PhysicalSize, + ) -> Result { + let mut state = self.shared_state.borrow_mut(); + + if state.is_none() { + info!("Creating primary EGL context (will be shared with subsequent contexts)"); + let new_state = self.create_primary_context(display_id, surface_id, size)?; + *state = Some(new_state); + } + + let Some(shared_state) = state.as_ref() else { + return Err(LayerShikaError::InvalidInput { + message: "Shared state initialization failed".into(), + }); + }; + + if shared_state.primary_context.is_current() { + debug!("Creating shared context while primary is current"); + self.create_shared_context_from_current( + &shared_state.display, + &shared_state.config, + &shared_state.primary_context, + surface_id, + size, + ) + } else { + debug!("Creating shared context (primary not current)"); + self.create_shared_context( + &shared_state.display, + &shared_state.config, + &shared_state.primary_context, + surface_id, + size, + ) + } + } + + #[allow(clippy::unused_self)] + fn create_primary_context( + &self, + display_id: &ObjectId, + surface_id: &ObjectId, + size: PhysicalSize, + ) -> Result { + let display_handle = create_wayland_display_handle(display_id)?; + let display = unsafe { Display::new(display_handle) } + .map_err(|e| EGLError::DisplayCreation { source: e.into() })?; + + let config_template = ConfigTemplateBuilder::default(); + let config = select_config(&display, config_template)?; + + let context_attributes = ContextAttributesBuilder::default(); + let not_current = create_context(&display, &config, context_attributes)?; + + let surface_handle = create_surface_handle(surface_id)?; + let surface = create_surface(&display, &config, surface_handle, size)?; + + let primary_context = not_current + .make_current(&surface) + .map_err(|e| EGLError::MakeCurrent { source: e.into() })?; + + info!("Primary EGL context created successfully"); + + Ok(SharedRenderState { + display, + config, + primary_context, + }) + } + + #[allow(clippy::unused_self)] + fn create_shared_context( + &self, + display: &Display, + config: &Config, + share_context: &PossiblyCurrentContext, + surface_id: &ObjectId, + size: PhysicalSize, + ) -> Result { + let context_attributes = ContextAttributesBuilder::default().with_sharing(share_context); + + let not_current = + unsafe { display.create_context(config, &context_attributes.build(None)) } + .map_err(|e| EGLError::ContextCreation { source: e.into() })?; + + let surface_handle = create_surface_handle(surface_id)?; + let surface = create_surface(display, config, surface_handle, size)?; + + let context = not_current + .make_current(&surface) + .map_err(|e| EGLError::MakeCurrent { source: e.into() })?; + + info!("Shared EGL context created successfully"); + + Ok(EGLContext::from_raw(surface, context)) + } + + #[allow(clippy::unused_self)] + fn create_shared_context_from_current( + &self, + display: &Display, + config: &Config, + share_context: &PossiblyCurrentContext, + surface_id: &ObjectId, + size: PhysicalSize, + ) -> Result { + let context_attributes = ContextAttributesBuilder::default().with_sharing(share_context); + + let not_current = + unsafe { display.create_context(config, &context_attributes.build(None)) } + .map_err(|e| EGLError::ContextCreation { source: e.into() })?; + + let surface_handle = create_surface_handle(surface_id)?; + let surface = create_surface(display, config, surface_handle, size)?; + + let context = not_current + .make_current(&surface) + .map_err(|e| EGLError::MakeCurrent { source: e.into() })?; + + info!("Shared EGL context created successfully (from current)"); + + Ok(EGLContext::from_raw(surface, context)) + } +} + +impl Default for RenderContextFactory { + fn default() -> Self { + Self { + shared_state: RefCell::new(None), + } + } +} + +fn create_wayland_display_handle(display_id: &ObjectId) -> Result { + let display = NonNull::new(display_id.as_ptr().cast::()).ok_or_else(|| { + LayerShikaError::InvalidInput { + message: "Failed to create NonNull pointer for display".into(), + } + })?; + let handle = WaylandDisplayHandle::new(display); + Ok(RawDisplayHandle::Wayland(handle)) +} + +fn select_config( + glutin_display: &Display, + config_template: ConfigTemplateBuilder, +) -> Result { + let mut configs = unsafe { glutin_display.find_configs(config_template.build()) } + .map_err(|e| EGLError::ConfigSelection { source: e.into() })?; + configs + .next() + .ok_or_else(|| EGLError::NoCompatibleConfig.into()) +} + +fn create_context( + glutin_display: &Display, + config: &Config, + context_attributes: ContextAttributesBuilder, +) -> Result { + unsafe { glutin_display.create_context(config, &context_attributes.build(None)) } + .map_err(|e| EGLError::ContextCreation { source: e.into() }.into()) +} + +fn create_surface_handle(surface_id: &ObjectId) -> Result { + let surface = NonNull::new(surface_id.as_ptr().cast::()).ok_or_else(|| { + LayerShikaError::InvalidInput { + message: "Failed to create NonNull pointer for surface".into(), + } + })?; + let handle = WaylandWindowHandle::new(surface); + Ok(RawWindowHandle::Wayland(handle)) +} + +fn create_surface( + glutin_display: &Display, + config: &Config, + surface_handle: RawWindowHandle, + size: PhysicalSize, +) -> Result> { + let width = NonZeroU32::new(size.width).ok_or_else(|| LayerShikaError::InvalidInput { + message: "Width cannot be zero".into(), + })?; + + let height = NonZeroU32::new(size.height).ok_or_else(|| LayerShikaError::InvalidInput { + message: "Height cannot be zero".into(), + })?; + + let attrs = + SurfaceAttributesBuilder::::new().build(surface_handle, width, height); + + unsafe { glutin_display.create_window_surface(config, &attrs) } + .map_err(|e| EGLError::SurfaceCreation { source: e.into() }.into()) +} diff --git a/crates/adapters/src/rendering/egl/mod.rs b/crates/adapters/src/rendering/egl/mod.rs index 9efb2ab..2f3ebb2 100644 --- a/crates/adapters/src/rendering/egl/mod.rs +++ b/crates/adapters/src/rendering/egl/mod.rs @@ -1 +1,2 @@ pub mod context; +pub mod context_factory; diff --git a/crates/adapters/src/wayland/shell_adapter.rs b/crates/adapters/src/wayland/shell_adapter.rs index 33f65cf..c584632 100644 --- a/crates/adapters/src/wayland/shell_adapter.rs +++ b/crates/adapters/src/wayland/shell_adapter.rs @@ -15,7 +15,7 @@ use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::z use crate::{ errors::{EventLoopError, LayerShikaError, RenderingError, Result}, rendering::{ - egl::context::EGLContext, + egl::context_factory::RenderContextFactory, femtovg::{main_window::FemtoVGWindow, renderable_window::RenderableWindow}, slint_integration::platform::CustomSlintPlatform, }, @@ -55,6 +55,8 @@ pub struct WaylandWindowingSystem { connection: Rc, event_queue: EventQueue, event_loop: EventLoop<'static, AppState>, + #[allow(dead_code)] + render_factory: Rc, } impl WaylandWindowingSystem { @@ -64,13 +66,15 @@ impl WaylandWindowingSystem { let event_loop = EventLoop::try_new().map_err(|e| EventLoopError::Creation { source: e })?; - let state = Self::init_state(config, &connection, &mut event_queue)?; + let render_factory = RenderContextFactory::new(); + let state = Self::init_state(config, &connection, &mut event_queue, &render_factory)?; Ok(Self { state, connection, event_queue, event_loop, + render_factory, }) } @@ -97,6 +101,7 @@ impl WaylandWindowingSystem { event_queue: &mut EventQueue, pointer: &Rc, layer_surface_config: &LayerSurfaceConfig, + render_factory: &Rc, ) -> Result> { let mut setups = Vec::new(); @@ -128,7 +133,7 @@ impl WaylandWindowingSystem { let main_surface_id = surface_ctx.surface.id(); let window = - Self::initialize_renderer(&surface_ctx.surface, &connection.display(), config)?; + Self::initialize_renderer(&surface_ctx.surface, &connection.display(), config, render_factory)?; let mut builder = WindowStateBuilder::new() .with_component_definition(config.component_definition.clone()) @@ -217,6 +222,7 @@ impl WaylandWindowingSystem { config: &WaylandWindowConfig, connection: &Connection, event_queue: &mut EventQueue, + render_factory: &Rc, ) -> Result { let global_ctx = GlobalContext::initialize(connection, &event_queue.handle())?; let layer_surface_config = Self::create_layer_surface_config(config); @@ -237,6 +243,7 @@ impl WaylandWindowingSystem { global_ctx.viewporter.clone(), connection.display(), Rc::new(connection.clone()), + Rc::clone(render_factory), ); let setups = Self::create_output_setups( @@ -246,6 +253,7 @@ impl WaylandWindowingSystem { event_queue, &pointer, &layer_surface_config, + render_factory, )?; let platform = Self::setup_platform(&setups)?; @@ -321,14 +329,15 @@ impl WaylandWindowingSystem { surface: &Rc, display: &WlDisplay, config: &WaylandWindowConfig, + render_factory: &Rc, ) -> Result> { let init_size = PhysicalSize::new(1, 1); - let context = EGLContext::builder() - .with_display_id(display.id()) - .with_surface_id(surface.id()) - .with_size(init_size) - .build()?; + let context = render_factory.create_context( + &display.id(), + &surface.id(), + init_size, + )?; let renderer = FemtoVGRenderer::new(context) .map_err(|e| LayerShikaError::FemtoVGRendererCreation { source: e })?; diff --git a/crates/adapters/src/wayland/surfaces/popup_manager.rs b/crates/adapters/src/wayland/surfaces/popup_manager.rs index 35b5544..c134082 100644 --- a/crates/adapters/src/wayland/surfaces/popup_manager.rs +++ b/crates/adapters/src/wayland/surfaces/popup_manager.rs @@ -1,5 +1,5 @@ use crate::errors::{LayerShikaError, Result}; -use crate::rendering::egl::context::EGLContext; +use crate::rendering::egl::context_factory::RenderContextFactory; use crate::rendering::femtovg::{popup_window::PopupWindow, renderable_window::RenderableWindow}; use crate::wayland::surfaces::display_metrics::{DisplayMetrics, SharedDisplayMetrics}; use layer_shika_domain::dimensions::LogicalSize as DomainLogicalSize; @@ -74,10 +74,12 @@ pub struct PopupContext { fractional_scale_manager: Option, viewporter: Option, display: WlDisplay, + render_factory: Rc, } impl PopupContext { #[must_use] + #[allow(clippy::too_many_arguments)] pub fn new( compositor: WlCompositor, xdg_wm_base: Option, @@ -86,6 +88,7 @@ impl PopupContext { viewporter: Option, display: WlDisplay, _connection: Rc, + render_factory: Rc, ) -> Self { Self { compositor, @@ -94,6 +97,7 @@ impl PopupContext { fractional_scale_manager, viewporter, display, + render_factory, } } } @@ -328,11 +332,11 @@ impl PopupManager { popup_surface.surface.commit(); } - let context = EGLContext::builder() - .with_display_id(self.context.display.id()) - .with_surface_id(popup_surface.surface.id()) - .with_size(popup_size) - .build()?; + let context = self.context.render_factory.create_context( + &self.context.display.id(), + &popup_surface.surface.id(), + popup_size, + )?; let renderer = FemtoVGRenderer::new(context) .map_err(|e| LayerShikaError::FemtoVGRendererCreation { source: e })?;