1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use std::cell::RefCell;
5use std::num::NonZeroU32;
6use std::rc::Rc;
7
8use glutin::{
9 config::GetGlConfig,
10 context::{ContextApi, ContextAttributesBuilder},
11 display::GetGlDisplay,
12 prelude::*,
13 surface::{SurfaceAttributesBuilder, WindowSurface},
14};
15use i_slint_core::graphics::RequestedGraphicsAPI;
16use i_slint_core::item_rendering::DirtyRegion;
17use i_slint_core::{api::GraphicsAPI, platform::PlatformError};
18use i_slint_core::{
19 api::{PhysicalSize as PhysicalWindowSize, Window},
20 graphics::RequestedOpenGLVersion,
21};
22
23/// This surface type renders into the given window with OpenGL, using glutin and glow libraries.
24pub struct OpenGLSurface {
25 fb_info: skia_safe::gpu::gl::FramebufferInfo,
26 surface: RefCell<skia_safe::Surface>,
27 gr_context: RefCell<skia_safe::gpu::DirectContext>,
28 glutin_context: glutin::context::PossiblyCurrentContext,
29 glutin_surface: glutin::surface::Surface<glutin::surface::WindowSurface>,
30}
31
32impl super::Surface for OpenGLSurface {
33 fn new(
34 window_handle: Rc<dyn raw_window_handle::HasWindowHandle>,
35 display_handle: Rc<dyn raw_window_handle::HasDisplayHandle>,
36 size: PhysicalWindowSize,
37 requested_graphics_api: Option<RequestedGraphicsAPI>,
38 ) -> Result<Self, PlatformError> {
39 Self::new_with_config(
40 window_handle,
41 display_handle,
42 size,
43 requested_graphics_api.map(TryInto::try_into).transpose()?,
44 glutin::config::ConfigTemplateBuilder::new(),
45 None,
46 )
47 }
48
49 fn name(&self) -> &'static str {
50 "opengl"
51 }
52
53 fn supports_graphics_api() -> bool {
54 true
55 }
56
57 fn supports_graphics_api_with_self(&self) -> bool {
58 true
59 }
60
61 fn with_graphics_api(&self, callback: &mut dyn FnMut(GraphicsAPI<'_>)) {
62 let api = GraphicsAPI::NativeOpenGL {
63 get_proc_address: &|name| {
64 self.glutin_context.display().get_proc_address(name) as *const _
65 },
66 };
67 callback(api)
68 }
69
70 fn with_active_surface(&self, callback: &mut dyn FnMut()) -> Result<(), PlatformError> {
71 self.ensure_context_current()?;
72 callback();
73 Ok(())
74 }
75
76 fn render(
77 &self,
78 _window: &Window,
79 size: PhysicalWindowSize,
80 callback: &dyn Fn(
81 &skia_safe::Canvas,
82 Option<&mut skia_safe::gpu::DirectContext>,
83 u8,
84 ) -> Option<DirtyRegion>,
85 pre_present_callback: &RefCell<Option<Box<dyn FnMut()>>>,
86 ) -> Result<(), PlatformError> {
87 self.ensure_context_current()?;
88
89 let current_context = &self.glutin_context;
90
91 let gr_context = &mut self.gr_context.borrow_mut();
92
93 let mut surface = self.surface.borrow_mut();
94
95 let width = size.width.try_into().ok();
96 let height = size.height.try_into().ok();
97
98 if let Some((width, height)) = width.zip(height) {
99 if width != surface.width() || height != surface.height() {
100 *surface = Self::create_internal_surface(
101 self.fb_info,
102 current_context,
103 gr_context,
104 width,
105 height,
106 )?;
107 }
108 }
109
110 let skia_canvas = surface.canvas();
111
112 skia_canvas.save();
113 callback(
114 skia_canvas,
115 Some(gr_context),
116 u8::try_from(self.glutin_surface.buffer_age()).unwrap_or_default(),
117 );
118 skia_canvas.restore();
119
120 if let Some(pre_present_callback) = pre_present_callback.borrow_mut().as_mut() {
121 pre_present_callback();
122 }
123
124 self.glutin_surface.swap_buffers(current_context).map_err(|glutin_error| {
125 format!("Skia OpenGL Renderer: Error swapping buffers: {glutin_error}").into()
126 })
127 }
128
129 fn resize_event(&self, size: PhysicalWindowSize) -> Result<(), PlatformError> {
130 self.ensure_context_current()?;
131
132 if let Some((width, height)) = size.width.try_into().ok().zip(size.height.try_into().ok()) {
133 self.glutin_surface.resize(&self.glutin_context, width, height);
134 }
135
136 Ok(())
137 }
138
139 fn bits_per_pixel(&self) -> Result<u8, PlatformError> {
140 let config = self.glutin_context.config();
141 let rgb_bits = match config.color_buffer_type() {
142 Some(glutin::config::ColorBufferType::Rgb { r_size, g_size, b_size }) => {
143 r_size + g_size + b_size
144 }
145 other @ _ => {
146 return Err(format!(
147 "Skia OpenGL Renderer: unsupported color buffer {other:?} encountered"
148 )
149 .into())
150 }
151 };
152 Ok(rgb_bits + config.alpha_size())
153 }
154}
155
156impl OpenGLSurface {
157 pub fn new_with_config(
158 window_handle: Rc<dyn raw_window_handle::HasWindowHandle>,
159 display_handle: Rc<dyn raw_window_handle::HasDisplayHandle>,
160 size: PhysicalWindowSize,
161 requested_opengl_version: Option<RequestedOpenGLVersion>,
162 config_builder: glutin::config::ConfigTemplateBuilder,
163 config_filter: Option<&dyn Fn(&glutin::config::Config) -> bool>,
164 ) -> Result<Self, PlatformError> {
165 let width: std::num::NonZeroU32 = size.width.try_into().map_err(|_| {
166 format!("Attempting to create window surface with an invalid width: {}", size.width)
167 })?;
168 let height: std::num::NonZeroU32 = size.height.try_into().map_err(|_| {
169 format!("Attempting to create window surface with an invalid height: {}", size.height)
170 })?;
171
172 let window_handle = window_handle
173 .window_handle()
174 .map_err(|e| format!("error obtaining window handle for skia opengl renderer: {e}"))?;
175 let display_handle = display_handle
176 .display_handle()
177 .map_err(|e| format!("error obtaining display handle for skia opengl renderer: {e}"))?;
178
179 let (current_glutin_context, glutin_surface) = Self::init_glutin(
180 window_handle,
181 display_handle,
182 width,
183 height,
184 requested_opengl_version,
185 config_builder,
186 config_filter,
187 )?;
188
189 glutin_surface.resize(&current_glutin_context, width, height);
190
191 let fb_info = {
192 use glow::HasContext;
193
194 let gl = unsafe {
195 glow::Context::from_loader_function_cstr(|name| {
196 current_glutin_context.display().get_proc_address(name) as *const _
197 })
198 };
199 let fboid = unsafe { gl.get_parameter_i32(glow::FRAMEBUFFER_BINDING) };
200
201 skia_safe::gpu::gl::FramebufferInfo {
202 fboid: fboid.try_into().map_err(|_| {
203 "Skia Renderer: Internal error, framebuffer binding returned signed id"
204 .to_string()
205 })?,
206 format: skia_safe::gpu::gl::Format::RGBA8.into(),
207 ..Default::default()
208 }
209 };
210
211 let gl_interface = skia_safe::gpu::gl::Interface::new_load_with_cstr(|name| {
212 current_glutin_context.display().get_proc_address(name) as *const _
213 })
214 .ok_or_else(|| {
215 "Skia Renderer: Internal Error: Could not create OpenGL Interface".to_string()
216 })?;
217
218 let mut gr_context =
219 skia_safe::gpu::direct_contexts::make_gl(gl_interface, None).ok_or_else(|| {
220 "Skia Renderer: Internal Error: Could not create Skia Direct Context from GL interface".to_string()
221 })?;
222
223 let width: i32 = size.width.try_into().map_err(|e| {
224 format!("Attempting to create window surface with width that doesn't fit into non-zero i32: {e}")
225 })?;
226 let height: i32 = size.height.try_into().map_err(|e| {
227 format!(
228 "Attempting to create window surface with height that doesn't fit into non-zero i32: {e}"
229 )
230 })?;
231
232 let surface = Self::create_internal_surface(
233 fb_info,
234 &current_glutin_context,
235 &mut gr_context,
236 width,
237 height,
238 )?
239 .into();
240
241 Ok(Self {
242 fb_info,
243 surface,
244 gr_context: RefCell::new(gr_context),
245 glutin_context: current_glutin_context,
246 glutin_surface,
247 })
248 }
249
250 fn init_glutin(
251 _window_handle: raw_window_handle::WindowHandle<'_>,
252 _display_handle: raw_window_handle::DisplayHandle<'_>,
253 width: NonZeroU32,
254 height: NonZeroU32,
255 requested_opengl_version: Option<RequestedOpenGLVersion>,
256 config_template_builder: glutin::config::ConfigTemplateBuilder,
257 config_filter: Option<&dyn Fn(&glutin::config::Config) -> bool>,
258 ) -> Result<
259 (
260 glutin::context::PossiblyCurrentContext,
261 glutin::surface::Surface<glutin::surface::WindowSurface>,
262 ),
263 PlatformError,
264 > {
265 cfg_if::cfg_if! {
266 if #[cfg(target_os = "macos")] {
267 let display_api_preference = glutin::display::DisplayApiPreference::Cgl;
268 } else if #[cfg(not(target_family = "windows"))] {
269 let display_api_preference = glutin::display::DisplayApiPreference::Egl;
270 } else {
271 let display_api_preference = glutin::display::DisplayApiPreference::EglThenWgl(Some(_window_handle.as_raw()));
272 }
273 }
274
275 let gl_display = unsafe {
276 glutin::display::Display::new(_display_handle.as_raw(), display_api_preference)
277 .map_err(|glutin_error| {
278 format!(
279 "Error creating glutin display for native display {:#?}: {}",
280 _display_handle.as_raw(),
281 glutin_error
282 )
283 })?
284 };
285
286 // On macOS, there's only one GL config and that's initialized based on the values in the config template
287 // builder. So if that one has transparency enabled, it'll show up in the config, and will be set on the
288 // context later. So we must enable it here, there's no way of enabling it later.
289 // On EGL/GLX/WGL there are system provided configs that may or may not support transparency. Here in case
290 // the system doesn't support transparency, we want to fall back to a config that doesn't - better than not
291 // rendering anything at all. So we don't want to limit the configurations we get to see early on.
292 // Commented out due to https://github.com/rust-windowing/glutin/issues/1640
293 #[cfg(target_os = "macos")]
294 let config_template_builder = config_template_builder.with_transparency(true);
295
296 // Upstream advises to use this only on Windows.
297 #[cfg(target_family = "windows")]
298 let config_template_builder =
299 config_template_builder.compatible_with_native_window(_window_handle.as_raw());
300
301 let config_template = config_template_builder.build();
302
303 let config = unsafe {
304 gl_display
305 .find_configs(config_template)
306 .map_err(|e| format!("Could not find valid OpenGL display configurations: {e}"))?
307 .filter(|config| config_filter.as_ref().map_or(true, |filter_fn| filter_fn(config)))
308 .reduce(|accum, config| {
309 let transparency_check = config.supports_transparency().unwrap_or(false)
310 & !accum.supports_transparency().unwrap_or(false);
311
312 if transparency_check || config.num_samples() < accum.num_samples() {
313 config
314 } else {
315 accum
316 }
317 })
318 .ok_or("Unable to find suitable GL config")?
319 };
320
321 let requested_opengl_version =
322 requested_opengl_version.unwrap_or(RequestedOpenGLVersion::OpenGLES(Some((3, 0))));
323 let preferred_context_attributes = match requested_opengl_version {
324 RequestedOpenGLVersion::OpenGL(version) => {
325 let version =
326 version.map(|(major, minor)| glutin::context::Version { major, minor });
327 ContextAttributesBuilder::new()
328 .with_context_api(ContextApi::OpenGl(version))
329 .build(Some(_window_handle.as_raw()))
330 }
331 RequestedOpenGLVersion::OpenGLES(version) => {
332 let version =
333 version.map(|(major, minor)| glutin::context::Version { major, minor });
334
335 ContextAttributesBuilder::new()
336 .with_context_api(ContextApi::Gles(version))
337 .build(Some(_window_handle.as_raw()))
338 }
339 };
340
341 let gles2_fallback_context_attributes = ContextAttributesBuilder::new()
342 .with_context_api(ContextApi::Gles(Some(glutin::context::Version {
343 major: 2,
344 minor: 0,
345 })))
346 .build(Some(_window_handle.as_raw()));
347
348 let fallback_context_attributes =
349 ContextAttributesBuilder::new().build(Some(_window_handle.as_raw()));
350
351 let not_current_gl_context = unsafe {
352 gl_display
353 .create_context(&config, &preferred_context_attributes)
354 .or_else(|_| gl_display.create_context(&config, &gles2_fallback_context_attributes))
355 .or_else(|_| gl_display.create_context(&config, &fallback_context_attributes))
356 .map_err(|e| format!("Error creating OpenGL context: {e}"))
357 }?;
358
359 let attrs = SurfaceAttributesBuilder::<WindowSurface>::new().build(
360 _window_handle.as_raw(),
361 width,
362 height,
363 );
364
365 let surface = unsafe {
366 config
367 .display()
368 .create_window_surface(&config, &attrs)
369 .map_err(|e| format!("Error creating OpenGL window surface: {e}"))?
370 };
371
372 let context = not_current_gl_context.make_current(&surface)
373 .map_err(|glutin_error: glutin::error::Error| -> PlatformError {
374 format!("FemtoVG Renderer: Failed to make newly created OpenGL context current: {glutin_error}")
375 .into()
376 })?;
377
378 // Align the GL layer to the top-left, so that resizing only invalidates the bottom/right
379 // part of the window.
380 #[cfg(target_os = "macos")]
381 if let raw_window_handle::RawWindowHandle::AppKit(raw_window_handle::AppKitWindowHandle {
382 ns_view,
383 ..
384 }) = _window_handle.as_raw()
385 {
386 let ns_view: &objc2_app_kit::NSView = unsafe { ns_view.cast().as_ref() };
387 unsafe {
388 ns_view.setLayerContentsPlacement(
389 objc2_app_kit::NSViewLayerContentsPlacement::TopLeft,
390 );
391 }
392 }
393
394 // Sanity check, as all this might succeed on Windows without working GL drivers, but this will fail:
395 if context
396 .display()
397 .get_proc_address(&std::ffi::CString::new("glCreateShader").unwrap())
398 .is_null()
399 {
400 return Err(
401 "Failed to initialize OpenGL driver: Could not locate glCreateShader symbol"
402 .to_string()
403 .into(),
404 );
405 }
406
407 // Try to default to vsync and ignore if the driver doesn't support it.
408 surface
409 .set_swap_interval(
410 &context,
411 glutin::surface::SwapInterval::Wait(NonZeroU32::new(1).unwrap()),
412 )
413 .ok();
414
415 Ok((context, surface))
416 }
417
418 fn create_internal_surface(
419 fb_info: skia_safe::gpu::gl::FramebufferInfo,
420 gl_context: &glutin::context::PossiblyCurrentContext,
421 gr_context: &mut skia_safe::gpu::DirectContext,
422 width: i32,
423 height: i32,
424 ) -> Result<skia_safe::Surface, PlatformError> {
425 let config = gl_context.config();
426
427 let backend_render_target = skia_safe::gpu::backend_render_targets::make_gl(
428 (width, height),
429 Some(config.num_samples() as _),
430 config.stencil_size() as _,
431 fb_info,
432 );
433 match skia_safe::gpu::surfaces::wrap_backend_render_target(
434 gr_context,
435 &backend_render_target,
436 skia_safe::gpu::SurfaceOrigin::BottomLeft,
437 skia_safe::ColorType::RGBA8888,
438 None,
439 None,
440 ) {
441 Some(surface) => Ok(surface),
442 None => {
443 Err("Skia OpenGL Renderer: Failed to allocate internal backend rendering target"
444 .into())
445 }
446 }
447 }
448
449 fn ensure_context_current(&self) -> Result<(), PlatformError> {
450 if !self.glutin_context.is_current() {
451 self.glutin_context.make_current(&self.glutin_surface).map_err(
452 |glutin_error| -> PlatformError {
453 format!("Skia Renderer: Error making context current: {glutin_error}").into()
454 },
455 )?;
456 }
457 Ok(())
458 }
459}
460
461impl Drop for OpenGLSurface {
462 fn drop(&mut self) {
463 // Make sure that the context is current before Skia calls glDelete***
464 // In the event that this fails for some reason (lost GL context), convey that to Skia so that it doesn't try to call
465 // glDelete***
466 if self.ensure_context_current().is_err() {
467 i_slint_core::debug_log!("Skia OpenGL Renderer warning: Failed to make context current for destruction - considering context abandoned.");
468 self.gr_context.borrow_mut().abandon();
469 }
470 }
471}
472