diff --git a/Cargo.lock b/Cargo.lock index a739f3e2d06..8cff8bf6899 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1144,6 +1144,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "wgpu", "winit", ] diff --git a/eframe/Cargo.toml b/eframe/Cargo.toml index 6fc63e0bf9a..986e6c39a0c 100644 --- a/eframe/Cargo.toml +++ b/eframe/Cargo.toml @@ -52,7 +52,7 @@ screen_reader = [ ] # Use WGPU as the backend instead of glow -wgpu = ["egui-wgpu"] +wgpu = ["dep:wgpu", "egui-wgpu"] [dependencies] @@ -68,6 +68,7 @@ egui-wgpu = { version = "0.18.0", path = "../egui-wgpu", optional = true, featur glow = { version = "0.11", optional = true } ron = { version = "0.7", optional = true } serde = { version = "1", optional = true, features = ["derive"] } +wgpu = { version = "0.12", optional = true } # ------------------------------------------- # native: diff --git a/eframe/src/native/run.rs b/eframe/src/native/run.rs index f8cb47fa6b7..332e80750cc 100644 --- a/eframe/src/native/run.rs +++ b/eframe/src/native/run.rs @@ -214,11 +214,25 @@ pub fn run_wgpu( // SAFETY: `window` must outlive `painter`. #[allow(unsafe_code)] let mut painter = unsafe { - egui_wgpu::winit::Painter::new(&window, native_options.multisampling.max(1) as _) + let mut painter = egui_wgpu::winit::Painter::new( + wgpu::Backends::PRIMARY | wgpu::Backends::GL, + wgpu::PowerPreference::HighPerformance, + wgpu::DeviceDescriptor { + label: None, + features: wgpu::Features::default(), + limits: wgpu::Limits::default(), + }, + wgpu::PresentMode::Fifo, + native_options.multisampling.max(1) as _, + ); + #[cfg(not(target_os = "android"))] + painter.set_window(Some(&window)); + painter }; let mut integration = epi_integration::EpiIntegration::new( - painter.max_texture_side(), + &event_loop, + painter.max_texture_side().unwrap_or(2048), &window, storage, #[cfg(feature = "glow")] @@ -304,6 +318,15 @@ pub fn run_wgpu( winit::event::Event::RedrawEventsCleared if cfg!(windows) => redraw(), winit::event::Event::RedrawRequested(_) if !cfg!(windows) => redraw(), + #[cfg(target_os = "android")] + winit::event::Event::Resumed => unsafe { + painter.set_window(Some(&window)); + }, + #[cfg(target_os = "android")] + winit::event::Event::Paused => unsafe { + painter.set_window(None); + }, + winit::event::Event::WindowEvent { event, .. } => { match &event { winit::event::WindowEvent::Focused(new_focused) => { diff --git a/egui-wgpu/CHANGELOG.md b/egui-wgpu/CHANGELOG.md index 5b9ea9d2d4a..b87d5266315 100644 --- a/egui-wgpu/CHANGELOG.md +++ b/egui-wgpu/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to the `egui-wgpu` integration will be noted in this file. ## Unreleased - +Enables deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)) ## 0.18.0 - 2022-05-15 First published version since moving the code into the `egui` repository from . diff --git a/egui-wgpu/src/winit.rs b/egui-wgpu/src/winit.rs index b29f2245329..96ddec73568 100644 --- a/egui-wgpu/src/winit.rs +++ b/egui-wgpu/src/winit.rs @@ -1,72 +1,203 @@ +use tracing::error; +use wgpu::{Adapter, Instance, Surface, TextureFormat}; + use crate::renderer; -/// Everything you need to paint egui with [`wgpu`] on [`winit`]. -/// -/// Alternatively you can use [`crate::renderer`] directly. -pub struct Painter { +struct RenderState { device: wgpu::Device, queue: wgpu::Queue, - surface_config: wgpu::SurfaceConfiguration, - surface: wgpu::Surface, + target_format: TextureFormat, egui_rpass: renderer::RenderPass, } -impl Painter { - /// Creates a [`wgpu`] surface for the given window, and things required to render egui onto it. - /// - /// # Safety - /// The given `window` must outlive the returned [`Painter`]. - pub unsafe fn new(window: &winit::window::Window, msaa_samples: u32) -> Self { - let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY | wgpu::Backends::GL); - let surface = instance.create_surface(&window); - - let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - compatible_surface: Some(&surface), - force_fallback_adapter: false, - })) - .unwrap(); - - let (device, queue) = pollster::block_on(adapter.request_device( - &wgpu::DeviceDescriptor { - features: wgpu::Features::default(), - limits: wgpu::Limits::default(), - label: None, - }, - None, - )) - .unwrap(); - - let size = window.inner_size(); - let surface_format = surface.get_preferred_format(&adapter).unwrap(); - let surface_config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: surface_format, - width: size.width as u32, - height: size.height as u32, - present_mode: wgpu::PresentMode::Fifo, // TODO(emilk): make vsync configurable - }; - surface.configure(&device, &surface_config); +struct SurfaceState { + surface: Surface, + width: u32, + height: u32, +} - let egui_rpass = renderer::RenderPass::new(&device, surface_format, msaa_samples); +/// Everything you need to paint egui with [`wgpu`] on [`winit`]. +/// +/// Alternatively you can use [`crate::renderer`] directly. +pub struct Painter<'a> { + power_preference: wgpu::PowerPreference, + device_descriptor: wgpu::DeviceDescriptor<'a>, + present_mode: wgpu::PresentMode, + msaa_samples: u32, + + instance: Instance, + adapter: Option, + render_state: Option, + surface_state: Option, +} + +impl<'a> Painter<'a> { + /// Manages [`wgpu`] state, including surface state, required to render egui. + /// + /// Only the [`wgpu::Instance`] is initialized here. Device selection and the initialization + /// of render + surface state is deferred until the painter is given its first window target + /// via [`set_window()`](Self::set_window). (Ensuring that a device that's compatible with the + /// native window is chosen) + /// + /// Before calling [`paint_and_update_textures()`](Self::paint_and_update_textures) a + /// [`wgpu::Surface`] must be initialized (and corresponding render state) by calling + /// [`set_window()`](Self::set_window) once you have + /// a [`winit::window::Window`] with a valid `.raw_window_handle()` + /// associated. + pub fn new( + backends: wgpu::Backends, + power_preference: wgpu::PowerPreference, + device_descriptor: wgpu::DeviceDescriptor<'a>, + present_mode: wgpu::PresentMode, + msaa_samples: u32, + ) -> Self { + let instance = wgpu::Instance::new(backends); Self { + power_preference, + device_descriptor, + present_mode, + msaa_samples, + + instance, + adapter: None, + render_state: None, + surface_state: None, + } + } + + async fn init_render_state( + &self, + adapter: &Adapter, + target_format: TextureFormat, + ) -> RenderState { + let (device, queue) = + pollster::block_on(adapter.request_device(&self.device_descriptor, None)).unwrap(); + + let egui_rpass = renderer::RenderPass::new(&device, target_format, self.msaa_samples); + + RenderState { device, queue, - surface_config, - surface, + target_format, egui_rpass, } } - pub fn max_texture_side(&self) -> usize { - self.device.limits().max_texture_dimension_2d as usize + // We want to defer the initialization of our render state until we have a surface + // so we can take its format into account. + // + // After we've initialized our render state once though we expect all future surfaces + // will have the same format and so this render state will remain valid. + fn ensure_render_state_for_surface(&mut self, surface: &Surface) { + self.adapter.get_or_insert_with(|| { + pollster::block_on(self.instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: self.power_preference, + compatible_surface: Some(surface), + force_fallback_adapter: false, + })) + .unwrap() + }); + + if self.render_state.is_none() { + let adapter = self.adapter.as_ref().unwrap(); + let swapchain_format = surface.get_preferred_format(adapter).unwrap(); + + let rs = pollster::block_on(self.init_render_state(adapter, swapchain_format)); + self.render_state = Some(rs); + } + } + + fn configure_surface(&mut self, width_in_pixels: u32, height_in_pixels: u32) { + let render_state = self + .render_state + .as_ref() + .expect("Render state should exist before surface configuration"); + let format = render_state.target_format; + + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format, + width: width_in_pixels, + height: height_in_pixels, + present_mode: self.present_mode, + }; + + let surface_state = self + .surface_state + .as_mut() + .expect("Surface state should exist before surface configuration"); + surface_state + .surface + .configure(&render_state.device, &config); + surface_state.width = width_in_pixels; + surface_state.height = height_in_pixels; + } + + /// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`] + /// + /// This creates a [`wgpu::Surface`] for the given Window (as well as initializing render + /// state if needed) that is used for egui rendering. + /// + /// This must be called before trying to render via + /// [`paint_and_update_textures`](Self::paint_and_update_textures) + /// + /// # Portability + /// + /// _In particular it's important to note that on Android a it's only possible to create + /// a window surface between `Resumed` and `Paused` lifecycle events, and Winit will panic on + /// attempts to query the raw window handle while paused._ + /// + /// On Android [`set_window`](Self::set_window) should be called with `Some(window)` for each + /// `Resumed` event and `None` for each `Paused` event. Currently, on all other platforms + /// [`set_window`](Self::set_window) may be called with `Some(window)` as soon as you have a + /// valid [`winit::window::Window`]. + /// + /// # Safety + /// + /// The raw Window handle associated with the given `window` must be a valid object to create a + /// surface upon and must remain valid for the lifetime of the created surface. (The surface may + /// be cleared by passing `None`). + pub unsafe fn set_window(&mut self, window: Option<&winit::window::Window>) { + match window { + Some(window) => { + let surface = self.instance.create_surface(&window); + + self.ensure_render_state_for_surface(&surface); + + let size = window.inner_size(); + let width = size.width as u32; + let height = size.height as u32; + self.surface_state = Some(SurfaceState { + surface, + width, + height, + }); + self.configure_surface(width, height); + } + None => { + self.surface_state = None; + } + } + } + + /// Returns the maximum texture dimension supported if known + /// + /// This API will only return a known dimension after `set_window()` has been called + /// at least once, since the underlying device and render state are initialized lazily + /// once we have a window (that may determine the choice of adapter/device). + pub fn max_texture_side(&self) -> Option { + self.render_state + .as_ref() + .map(|rs| rs.device.limits().max_texture_dimension_2d as usize) } pub fn on_window_resized(&mut self, width_in_pixels: u32, height_in_pixels: u32) { - self.surface_config.width = width_in_pixels; - self.surface_config.height = height_in_pixels; - self.surface.configure(&self.device, &self.surface_config); + if self.surface_state.is_some() { + self.configure_surface(width_in_pixels, height_in_pixels); + } else { + error!("Ignoring window resize notification with no surface created via Painter::set_window()"); + } } pub fn paint_and_update_textures( @@ -76,7 +207,16 @@ impl Painter { clipped_primitives: &[egui::ClippedPrimitive], textures_delta: &egui::TexturesDelta, ) { - let output_frame = match self.surface.get_current_texture() { + let render_state = match self.render_state.as_mut() { + Some(rs) => rs, + None => return, + }; + let surface_state = match self.surface_state.as_ref() { + Some(rs) => rs, + None => return, + }; + + let output_frame = match surface_state.surface.get_current_texture() { Ok(frame) => frame, Err(wgpu::SurfaceError::Outdated) => { // This error occurs when the app is minimized on Windows. @@ -93,35 +233,40 @@ impl Painter { .texture .create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("encoder"), - }); + let mut encoder = + render_state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("encoder"), + }); // Upload all resources for the GPU. let screen_descriptor = renderer::ScreenDescriptor { - size_in_pixels: [self.surface_config.width, self.surface_config.height], + size_in_pixels: [surface_state.width, surface_state.height], pixels_per_point, }; for (id, image_delta) in &textures_delta.set { - self.egui_rpass - .update_texture(&self.device, &self.queue, *id, image_delta); + render_state.egui_rpass.update_texture( + &render_state.device, + &render_state.queue, + *id, + image_delta, + ); } for id in &textures_delta.free { - self.egui_rpass.free_texture(id); + render_state.egui_rpass.free_texture(id); } - self.egui_rpass.update_buffers( - &self.device, - &self.queue, + render_state.egui_rpass.update_buffers( + &render_state.device, + &render_state.queue, clipped_primitives, &screen_descriptor, ); // Record all render passes. - self.egui_rpass.execute( + render_state.egui_rpass.execute( &mut encoder, &output_view, clipped_primitives, @@ -135,7 +280,7 @@ impl Painter { ); // Submit the commands. - self.queue.submit(std::iter::once(encoder.finish())); + render_state.queue.submit(std::iter::once(encoder.finish())); // Redraw egui output_frame.present();