diff --git a/crates/freya-performance-plugin/src/lib.rs b/crates/freya-performance-plugin/src/lib.rs index 9a7fd74d1..fda2e40ba 100644 --- a/crates/freya-performance-plugin/src/lib.rs +++ b/crates/freya-performance-plugin/src/lib.rs @@ -134,6 +134,10 @@ impl FreyaPlugin for PerformanceOverlayPlugin { window, graphics_driver, .. + } + | PluginEvent::GraphicsDriverChanged { + window, + graphics_driver, } => { self.get_metrics(window.id()).graphics_driver = graphics_driver; } diff --git a/crates/freya-winit/src/drivers/gl.rs b/crates/freya-winit/src/drivers/gl.rs index 3c0d7419c..a64cd030a 100644 --- a/crates/freya-winit/src/drivers/gl.rs +++ b/crates/freya-winit/src/drivers/gl.rs @@ -21,6 +21,7 @@ use gl::{ }; use glutin::{ config::{ + Config, ConfigTemplateBuilder, GlConfig, }, @@ -87,23 +88,40 @@ impl OpenGLDriver { let display_builder = DisplayBuilder::new().with_window_attributes(Some(window_attributes)); let (window, gl_config) = display_builder.build(event_loop, template, |configs| { - configs - .reduce(|accum, config| { - let transparency_check = transparent - && config.supports_transparency().unwrap_or(false) - && !accum.supports_transparency().unwrap_or(false); - - if transparency_check || config.num_samples() < accum.num_samples() { - config - } else { - accum - } - }) - .expect("at least one OpenGL config") + Self::pick_config(configs, transparent) })?; let window = window.ok_or("OpenGL display builder returned no window")?; + let driver = Self::build(&gl_config, &window, gpu_resource_cache_limit)?; + + Ok((driver, window)) + } + + /// Build the driver on an existing window instead of creating a new one. + pub fn from_window( + event_loop: &ActiveEventLoop, + window: &Window, + gpu_resource_cache_limit: usize, + transparent: bool, + ) -> Result> { + let template = ConfigTemplateBuilder::new() + .with_alpha_size(8) + .with_transparency(transparent) + .compatible_with_native_window(window.window_handle()?.as_raw()); + + let (_, gl_config) = DisplayBuilder::new().build(event_loop, template, |configs| { + Self::pick_config(configs, transparent) + })?; + + Self::build(&gl_config, window, gpu_resource_cache_limit) + } + + fn build( + gl_config: &Config, + window: &Window, + gpu_resource_cache_limit: usize, + ) -> Result> { let window_handle = window.window_handle()?; let context_attributes = ContextAttributesBuilder::new() @@ -118,12 +136,12 @@ impl OpenGLDriver { let not_current_gl_context = unsafe { match gl_config .display() - .create_context(&gl_config, &context_attributes) + .create_context(gl_config, &context_attributes) { Ok(ctx) => ctx, Err(_) => gl_config .display() - .create_context(&gl_config, &fallback_context_attributes)?, + .create_context(gl_config, &fallback_context_attributes)?, } }; @@ -138,7 +156,7 @@ impl OpenGLDriver { let gl_surface = unsafe { gl_config .display() - .create_window_surface(&gl_config, &attrs)? + .create_window_surface(gl_config, &attrs)? }; let gl_context = not_current_gl_context.make_current(&gl_surface)?; @@ -208,7 +226,7 @@ impl OpenGLDriver { surface, }; - Ok((driver, window)) + Ok(driver) } pub fn present(&mut self, window: &Window, render: impl FnOnce(&mut SkiaSurface)) { @@ -250,4 +268,21 @@ impl OpenGLDriver { self.surface = surface; } + + /// Pick the OpenGL config, preferring transparency then fewer samples. + fn pick_config(configs: Box + '_>, transparent: bool) -> Config { + configs + .reduce(|accum, config| { + let transparency_check = transparent + && config.supports_transparency().unwrap_or(false) + && !accum.supports_transparency().unwrap_or(false); + + if transparency_check || config.num_samples() < accum.num_samples() { + config + } else { + accum + } + }) + .expect("at least one OpenGL config") + } } diff --git a/crates/freya-winit/src/drivers/mod.rs b/crates/freya-winit/src/drivers/mod.rs index db2b9b0fd..617886dd0 100644 --- a/crates/freya-winit/src/drivers/mod.rs +++ b/crates/freya-winit/src/drivers/mod.rs @@ -16,6 +16,14 @@ use winit::{ }, }; +/// Unrecoverable graphics error requiring a driver rebuild. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] +pub enum DriverError { + DeviceLost, + OutOfMemory, +} + #[allow(clippy::large_enum_variant)] pub enum GraphicsDriver { #[cfg(any(target_os = "linux", target_os = "windows", target_os = "android"))] @@ -122,20 +130,57 @@ impl GraphicsDriver { } } + /// Rebuild the driver on the existing window, skipping Vulkan. + pub fn recover_reusing_window( + event_loop: &ActiveEventLoop, + window: &Window, + gpu_resource_cache_limit: usize, + transparent: bool, + ) -> Self { + #[cfg(target_os = "macos")] + let _ = (event_loop, gpu_resource_cache_limit, transparent); + + #[cfg(any(target_os = "linux", target_os = "windows", target_os = "android"))] + match gl::OpenGLDriver::from_window( + event_loop, + window, + gpu_resource_cache_limit, + transparent, + ) { + Ok(driver) => return Self::OpenGl(driver), + Err(error) => { + tracing::warn!("OpenGL recovery failed, falling back to software: {error}"); + } + } + + let driver = software::SoftwareDriver::from_window(window) + .expect("Failed to initialize software renderer fallback"); + Self::Software(driver) + } + pub fn present( &mut self, _size: PhysicalSize, window: &Window, render: impl FnOnce(&mut SkiaSurface), - ) { + ) -> Result<(), DriverError> { match self { #[cfg(any(target_os = "linux", target_os = "windows", target_os = "android"))] - Self::OpenGl(gl) => gl.present(window, render), + Self::OpenGl(gl) => { + gl.present(window, render); + Ok(()) + } #[cfg(target_os = "macos")] - Self::Metal(mtl) => mtl.present(_size, window, render), + Self::Metal(mtl) => { + mtl.present(_size, window, render); + Ok(()) + } #[cfg(any(target_os = "linux", target_os = "windows"))] Self::Vulkan(vk) => vk.present(_size, window, render), - Self::Software(sw) => sw.present(_size, window, render), + Self::Software(sw) => { + sw.present(_size, window, render); + Ok(()) + } } } @@ -152,15 +197,24 @@ impl GraphicsDriver { } } - pub fn resize(&mut self, size: PhysicalSize) { + pub fn resize(&mut self, size: PhysicalSize) -> Result<(), DriverError> { match self { #[cfg(any(target_os = "linux", target_os = "windows", target_os = "android"))] - Self::OpenGl(gl) => gl.resize(size), + Self::OpenGl(gl) => { + gl.resize(size); + Ok(()) + } #[cfg(target_os = "macos")] - Self::Metal(mtl) => mtl.resize(size), + Self::Metal(mtl) => { + mtl.resize(size); + Ok(()) + } #[cfg(any(target_os = "linux", target_os = "windows"))] Self::Vulkan(vk) => vk.resize(size), - Self::Software(sw) => sw.resize(size), + Self::Software(sw) => { + sw.resize(size); + Ok(()) + } } } } diff --git a/crates/freya-winit/src/drivers/software.rs b/crates/freya-winit/src/drivers/software.rs index a00334d01..b1c0594c1 100644 --- a/crates/freya-winit/src/drivers/software.rs +++ b/crates/freya-winit/src/drivers/software.rs @@ -55,7 +55,12 @@ impl SoftwareDriver { window_attributes: WindowAttributes, ) -> Result<(Self, Window), Box> { let window = event_loop.create_window(window_attributes)?; + let driver = Self::from_window(&window)?; + Ok((driver, window)) + } + /// Build the driver on an existing window instead of creating a new one. + pub fn from_window(window: &Window) -> Result> { let display_handle = window.display_handle()?.as_raw(); let window_handle = window.window_handle()?.as_raw(); @@ -73,13 +78,10 @@ impl SoftwareDriver { .map_err(|err| format!("Could not size softbuffer surface: {err}"))?; } - Ok(( - Self { - _context: context, - surface, - }, - window, - )) + Ok(Self { + _context: context, + surface, + }) } pub fn present( diff --git a/crates/freya-winit/src/drivers/vulkan.rs b/crates/freya-winit/src/drivers/vulkan.rs index 2f93638f3..9ddf4af15 100644 --- a/crates/freya-winit/src/drivers/vulkan.rs +++ b/crates/freya-winit/src/drivers/vulkan.rs @@ -3,6 +3,7 @@ use std::{ CStr, CString, }, + mem::ManuallyDrop, ptr, sync::Arc, }; @@ -24,6 +25,7 @@ use ash::{ CommandBufferAllocateInfo, CommandBufferBeginInfo, CommandBufferLevel, + CommandPool, CommandPoolCreateFlags, CommandPoolCreateInfo, CompositeAlphaFlagsKHR, @@ -75,6 +77,7 @@ use freya_engine::prelude::{ }; use raw_window_handle::{ DisplayHandle, + HandleError, HasDisplayHandle, HasWindowHandle, }; @@ -87,6 +90,8 @@ use winit::{ }, }; +use crate::drivers::DriverError; + /// Graphics driver using Vulkan. pub struct VulkanDriver { _entry: Entry, // Dont drop until backend is dropped @@ -104,13 +109,34 @@ pub struct VulkanDriver { swapchain_extent: Extent2D, swapchain_image_index: u32, swapchain_suboptimal: bool, - swapchain_size: PhysicalSize, transparent: bool, - gr_context: DirectContext, + gr_context: ManuallyDrop, image_available_semaphore: Semaphore, render_finished_semaphore: Semaphore, in_flight_fence: Fence, cmd_buf: CommandBuffer, + cmd_pool: CommandPool, + gpu_cache_purged: bool, +} + +impl Drop for VulkanDriver { + fn drop(&mut self) { + unsafe { + let _ = self.device.device_wait_idle(); + // Skia must release its GPU resources while the device is still alive. + ManuallyDrop::drop(&mut self.gr_context); + self.device + .destroy_semaphore(self.image_available_semaphore, None); + self.device + .destroy_semaphore(self.render_finished_semaphore, None); + self.device.destroy_fence(self.in_flight_fence, None); + self.device.destroy_command_pool(self.cmd_pool, None); + self.swapchain_fns.destroy_swapchain(self.swapchain, None); + self.surface_fns.destroy_surface(self.surface, None); + self.device.destroy_device(None); + self.instance.destroy_instance(None); + } + } } impl VulkanDriver { @@ -206,21 +232,24 @@ impl VulkanDriver { swapchain_extent, swapchain_image_index: 0, swapchain_suboptimal: false, - swapchain_size, transparent, - gr_context, + gr_context: ManuallyDrop::new(gr_context), image_available_semaphore, render_finished_semaphore, in_flight_fence, cmd_buf, + cmd_pool, + gpu_cache_purged: false, }; Ok((driver, window)) } - fn recreate_swapchain(&mut self) { + fn recreate_swapchain(&mut self, size: PhysicalSize) -> Result<(), DriverError> { unsafe { - self.device.device_wait_idle().unwrap(); + self.device + .device_wait_idle() + .map_err(Self::classify_error)?; } let old_swapchain = self.swapchain; let (swapchain, swapchain_fns, swapchain_images, swapchain_format, swapchain_extent) = @@ -231,11 +260,14 @@ impl VulkanDriver { &self.surface_fns, self.surface, self.queue_family_index, - self.swapchain_size, + size, Some(old_swapchain), self.transparent, ) - .expect("Failed to recreate Vulkan swapchain"); + .map_err(|error| { + tracing::error!("Failed to recreate Vulkan swapchain: {error}"); + DriverError::DeviceLost + })?; self.swapchain = swapchain; self.swapchain_fns = swapchain_fns; self.swapchain_images = swapchain_images; @@ -245,6 +277,7 @@ impl VulkanDriver { unsafe { self.swapchain_fns.destroy_swapchain(old_swapchain, None); } + Ok(()) } pub fn present( @@ -252,35 +285,61 @@ impl VulkanDriver { size: PhysicalSize, window: &Window, render: impl FnOnce(&mut SkiaSurface), - ) { + ) -> Result<(), DriverError> { if size.width == 0 || size.height == 0 { - return; + return Ok(()); } - let mut surface = unsafe { + unsafe { self.device .wait_for_fences(&[self.in_flight_fence], true, u64::MAX) - .unwrap(); - - self.device.reset_fences(&[self.in_flight_fence]).unwrap(); + .map_err(Self::classify_error)?; + } - let (image_index, suboptimal) = self - .swapchain_fns - .acquire_next_image( - self.swapchain, - u64::MAX, - self.image_available_semaphore, - Fence::null(), - ) - .unwrap(); + let (image_index, suboptimal) = match unsafe { + self.swapchain_fns.acquire_next_image( + self.swapchain, + u64::MAX, + self.image_available_semaphore, + Fence::null(), + ) + } { + Ok(acquired) => acquired, + Err(ash::vk::Result::ERROR_OUT_OF_DATE_KHR) => { + self.recreate_swapchain(size)?; + window.request_redraw(); + return Ok(()); + } + Err(ash::vk::Result::ERROR_SURFACE_LOST_KHR) => { + self.recreate_surface(window, size)?; + window.request_redraw(); + return Ok(()); + } + Err( + ash::vk::Result::ERROR_OUT_OF_DEVICE_MEMORY + | ash::vk::Result::ERROR_OUT_OF_HOST_MEMORY, + ) => { + if self.gpu_cache_purged { + return Err(DriverError::OutOfMemory); + } + tracing::warn!("Vulkan out of memory acquiring image, purging GPU cache"); + self.gr_context.free_gpu_resources(); + self.gpu_cache_purged = true; + window.request_redraw(); + return Ok(()); + } + Err(error) => return Err(Self::classify_error(error)), + }; - self.swapchain_image_index = image_index; - self.swapchain_suboptimal = suboptimal; + self.swapchain_image_index = image_index; + self.swapchain_suboptimal = suboptimal; + self.gpu_cache_purged = false; - let image = self.swapchain_images[image_index as usize]; + let image = self.swapchain_images[image_index as usize]; - let alloc = vk::Alloc::default(); - let sk_image_info = vk::ImageInfo::new( + let alloc = vk::Alloc::default(); + let sk_image_info = unsafe { + vk::ImageInfo::new( image.as_raw() as _, alloc, vk::ImageTiling::OPTIMAL, @@ -291,25 +350,25 @@ impl VulkanDriver { None, None, vk::SharingMode::EXCLUSIVE, - ); - let render_target = backend_render_targets::make_vk( - ( - self.swapchain_extent.width as i32, - self.swapchain_extent.height as i32, - ), - &sk_image_info, - ); - - wrap_backend_render_target( - &mut self.gr_context, - &render_target, - SurfaceOrigin::TopLeft, - ColorType::BGRA8888, - None, - None, ) - .unwrap() }; + let render_target = backend_render_targets::make_vk( + ( + self.swapchain_extent.width as i32, + self.swapchain_extent.height as i32, + ), + &sk_image_info, + ); + + let mut surface = wrap_backend_render_target( + &mut self.gr_context, + &render_target, + SurfaceOrigin::TopLeft, + ColorType::BGRA8888, + None, + None, + ) + .ok_or(DriverError::DeviceLost)?; render(&mut surface); @@ -322,7 +381,7 @@ impl VulkanDriver { unsafe { self.device .begin_command_buffer(self.cmd_buf, &CommandBufferBeginInfo::default()) - .unwrap(); + .map_err(Self::classify_error)?; let image_barrier = ImageMemoryBarrier::default() .src_access_mask(AccessFlags::COLOR_ATTACHMENT_WRITE) @@ -348,7 +407,9 @@ impl VulkanDriver { &[image_barrier], ); - self.device.end_command_buffer(self.cmd_buf).unwrap(); + self.device + .end_command_buffer(self.cmd_buf) + .map_err(Self::classify_error)?; }; let wait_semaphores = [self.image_available_semaphore]; @@ -363,9 +424,14 @@ impl VulkanDriver { .signal_semaphores(&signal_semaphores)]; unsafe { + // Reset only right before submit so earlier returns leave the fence signaled. + self.device + .reset_fences(&[self.in_flight_fence]) + .map_err(Self::classify_error)?; + self.device .queue_submit(self.queue, &submit_infos, self.in_flight_fence) - .unwrap(); + .map_err(Self::classify_error)?; }; let swapchains = [self.swapchain]; @@ -379,20 +445,77 @@ impl VulkanDriver { drop(surface); - if self.swapchain_suboptimal - || matches!(result, Err(ash::vk::Result::ERROR_OUT_OF_DATE_KHR)) - { - self.swapchain_size = size; - self.recreate_swapchain(); + match result { + Ok(_) => {} + Err(ash::vk::Result::ERROR_OUT_OF_DATE_KHR) => self.swapchain_suboptimal = true, + Err(ash::vk::Result::ERROR_SURFACE_LOST_KHR) => { + self.recreate_surface(window, size)?; + window.request_redraw(); + return Ok(()); + } + Err(error) => return Err(Self::classify_error(error)), + } + + if self.swapchain_suboptimal { + self.recreate_swapchain(size)?; + } + + Ok(()) + } + + /// Recreate the surface and its swapchain after the surface was lost. + fn recreate_surface( + &mut self, + window: &Window, + size: PhysicalSize, + ) -> Result<(), DriverError> { + unsafe { + self.device + .device_wait_idle() + .map_err(Self::classify_error)?; + self.swapchain_fns.destroy_swapchain(self.swapchain, None); + self.surface_fns.destroy_surface(self.surface, None); } + // Null handles keep Drop safe if any of the steps below fail. + self.swapchain = SwapchainKHR::null(); + self.surface = SurfaceKHR::null(); + + let handle_error = |error: HandleError| { + tracing::error!("Failed to get a window handle: {error}"); + DriverError::DeviceLost + }; + self.surface = unsafe { + ash_window::create_surface( + &self._entry, + &self.instance, + window.display_handle().map_err(handle_error)?.as_raw(), + window.window_handle().map_err(handle_error)?.as_raw(), + None, + ) + .map_err(Self::classify_error)? + }; + + self.recreate_swapchain(size) } - pub fn resize(&mut self, size: PhysicalSize) { + pub fn resize(&mut self, size: PhysicalSize) -> Result<(), DriverError> { if size.width == 0 || size.height == 0 { - return; + return Ok(()); + } + self.recreate_swapchain(size) + } + + /// Map a Vulkan error to a recovery action. + fn classify_error(error: ash::vk::Result) -> DriverError { + match error { + ash::vk::Result::ERROR_OUT_OF_DEVICE_MEMORY + | ash::vk::Result::ERROR_OUT_OF_HOST_MEMORY => DriverError::OutOfMemory, + ash::vk::Result::ERROR_DEVICE_LOST => DriverError::DeviceLost, + other => { + tracing::error!("Unexpected Vulkan error, treating as device loss: {other:?}"); + DriverError::DeviceLost + } } - self.swapchain_size = size; - self.recreate_swapchain(); } } diff --git a/crates/freya-winit/src/plugins.rs b/crates/freya-winit/src/plugins.rs index 391a9dee0..7786ae8a9 100644 --- a/crates/freya-winit/src/plugins.rs +++ b/crates/freya-winit/src/plugins.rs @@ -102,6 +102,12 @@ pub enum PluginEvent<'a> { graphics_driver: &'static str, }, + /// The graphics driver was rebuilt at runtime. + GraphicsDriverChanged { + window: &'a Window, + graphics_driver: &'static str, + }, + /// A Window just got closed. WindowClosed { window: &'a Window, diff --git a/crates/freya-winit/src/renderer.rs b/crates/freya-winit/src/renderer.rs index 8dcb383b1..9f82d3abd 100644 --- a/crates/freya-winit/src/renderer.rs +++ b/crates/freya-winit/src/renderer.rs @@ -651,6 +651,7 @@ impl ApplicationHandler for WinitRenderer { window_id: winit::window::WindowId, event: winit::event::WindowEvent, ) { + let mut needs_recovery = false; if let Some(app) = &mut self.windows.get_mut(&window_id) { app.accessibility_adapter.process_event(&app.window, &event); match event { @@ -759,7 +760,7 @@ impl ApplicationHandler for WinitRenderer { ); } - app.driver.present( + let present_result = app.driver.present( app.window.inner_size().cast(), &app.window, |surface| { @@ -804,6 +805,13 @@ impl ApplicationHandler for WinitRenderer { ); }, ); + if let Err(error) = present_result { + tracing::warn!( + "Graphics driver lost ({error:?}), rebuilding on the same window" + ); + needs_recovery = true; + } + self.plugins.send( PluginEvent::AfterPresenting { window: &app.window, @@ -899,7 +907,12 @@ impl ApplicationHandler for WinitRenderer { }); } WindowEvent::Resized(size) => { - app.driver.resize(size); + if let Err(error) = app.driver.resize(size) { + tracing::warn!( + "Graphics driver lost while resizing ({error:?}), rebuilding on the same window" + ); + needs_recovery = true; + } app.window.request_redraw(); @@ -1204,6 +1217,28 @@ impl ApplicationHandler for WinitRenderer { _ => {} } } + + // Rebuild on the same window to keep input and accessibility working. + if needs_recovery && let Some(mut app) = self.windows.remove(&window_id) { + // Drop the lost driver first to release its GPU surface. + drop(app.driver); + app.driver = GraphicsDriver::recover_reusing_window( + event_loop, + &app.window, + self.gpu_resource_cache_limit, + app.window_attributes.transparent, + ); + tracing::info!("Recovered onto the {} driver", app.driver.name()); + self.plugins.send( + PluginEvent::GraphicsDriverChanged { + window: &app.window, + graphics_driver: app.driver.name(), + }, + PluginHandle::new(&self.proxy), + ); + app.window.request_redraw(); + self.windows.insert(window_id, app); + } } }