diff --git a/Cargo.lock b/Cargo.lock index a739f3e2d..8cff8bf68 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 6fc63e0bf..986e6c39a 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/epi_integration.rs b/eframe/src/native/epi_integration.rs index cde1d187b..093ce162c 100644 --- a/eframe/src/native/epi_integration.rs +++ b/eframe/src/native/epi_integration.rs @@ -1,5 +1,6 @@ use crate::{epi, WindowInfo}; use egui_winit::{native_pixels_per_point, WindowSettings}; +use winit::event_loop::EventLoopWindowTarget; pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize { winit::dpi::LogicalSize { @@ -181,7 +182,8 @@ pub struct EpiIntegration { } impl EpiIntegration { - pub fn new( + pub fn new( + event_loop: &EventLoopWindowTarget, max_texture_side: usize, window: &winit::window::Window, storage: Option>, @@ -213,11 +215,16 @@ impl EpiIntegration { egui_ctx.set_visuals(egui::Visuals::light()); } + let mut egui_winit = egui_winit::State::new(event_loop); + egui_winit.set_max_texture_side(max_texture_side); + let pixels_per_point = window.scale_factor() as f32; + egui_winit.set_pixels_per_point(pixels_per_point); + Self { frame, last_auto_save: std::time::Instant::now(), egui_ctx, - egui_winit: egui_winit::State::new(max_texture_side, window), + egui_winit, pending_full_output: Default::default(), quit: false, can_drag_window: false, diff --git a/eframe/src/native/run.rs b/eframe/src/native/run.rs index 7b0fde591..332e80750 100644 --- a/eframe/src/native/run.rs +++ b/eframe/src/native/run.rs @@ -57,6 +57,7 @@ pub fn run_glow( .unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error)); let mut integration = epi_integration::EpiIntegration::new( + &event_loop, painter.max_texture_side(), gl_window.window(), storage, @@ -213,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")] @@ -303,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 5b9ea9d2d..b87d52663 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 b29f22453..96ddec735 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(); diff --git a/egui-winit/CHANGELOG.md b/egui-winit/CHANGELOG.md index a1e264571..383863261 100644 --- a/egui-winit/CHANGELOG.md +++ b/egui-winit/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to the `egui-winit` integration will be noted in this file. ## Unreleased * Fix clipboard on Wayland ([#1613](https://github.com/emilk/egui/pull/1613)). +* Allow deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)) ## 0.18.0 - 2022-04-30 diff --git a/egui-winit/src/lib.rs b/egui-winit/src/lib.rs index 497db28ad..12670cc09 100644 --- a/egui-winit/src/lib.rs +++ b/egui-winit/src/lib.rs @@ -16,6 +16,7 @@ mod window_settings; pub use window_settings::WindowSettings; +use winit::event_loop::EventLoopWindowTarget; #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -23,7 +24,7 @@ pub use window_settings::WindowSettings; target_os = "netbsd", target_os = "openbsd" ))] -use winit::platform::unix::WindowExtUnix; +use winit::platform::unix::EventLoopWindowTargetExtUnix; pub fn native_pixels_per_point(window: &winit::window::Window) -> f32 { window.scale_factor() as f32 @@ -60,36 +61,18 @@ pub struct State { } impl State { - /// Initialize with: - /// * `max_texture_side`: e.g. `GL_MAX_TEXTURE_SIZE` - /// * the native `pixels_per_point` (dpi scaling). - pub fn new(max_texture_side: usize, window: &winit::window::Window) -> Self { - Self::from_pixels_per_point( - max_texture_side, - native_pixels_per_point(window), - get_wayland_display(window), - ) + pub fn new(event_loop: &EventLoopWindowTarget) -> Self { + Self::new_with_wayland_display(get_wayland_display(event_loop)) } - /// Initialize with: - /// * `max_texture_side`: e.g. `GL_MAX_TEXTURE_SIZE` - /// * the given `pixels_per_point` (dpi scaling). - pub fn from_pixels_per_point( - max_texture_side: usize, - pixels_per_point: f32, - wayland_display: Option<*mut c_void>, - ) -> Self { + pub fn new_with_wayland_display(wayland_display: Option<*mut c_void>) -> Self { Self { start_time: instant::Instant::now(), - egui_input: egui::RawInput { - pixels_per_point: Some(pixels_per_point), - max_texture_side: Some(max_texture_side), - ..Default::default() - }, + egui_input: Default::default(), pointer_pos_in_points: None, any_pointer_button_down: false, current_cursor_icon: egui::CursorIcon::Default, - current_pixels_per_point: pixels_per_point, + current_pixels_per_point: 1.0, clipboard: clipboard::Clipboard::new(wayland_display), screen_reader: screen_reader::ScreenReader::default(), @@ -99,6 +82,25 @@ impl State { } } + /// Call this once a graphics context has been created to update the maximum texture dimensions + /// that egui will use. + pub fn set_max_texture_side(&mut self, max_texture_side: usize) { + self.egui_input.max_texture_side = Some(max_texture_side); + } + + /// Call this when a new native Window is created for rendering to initialize the `pixels_per_point` + /// for that window. + /// + /// In particular, on Android it is necessary to call this after each `Resumed` lifecycle + /// event, each time a new native window is created. + /// + /// Once this has been initialized for a new window then this state will be maintained by handling + /// [`winit::event::WindowEvent::ScaleFactorChanged`] events. + pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) { + self.egui_input.pixels_per_point = Some(pixels_per_point); + self.current_pixels_per_point = pixels_per_point; + } + /// The number of physical pixels per logical point, /// as configured on the current egui context (see [`egui::Context::pixels_per_point`]). #[inline] @@ -679,7 +681,7 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option Option<*mut c_void> { +fn get_wayland_display(_event_loop: &EventLoopWindowTarget) -> Option<*mut c_void> { #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -688,11 +690,14 @@ fn get_wayland_display(_window: &winit::window::Window) -> Option<*mut c_void> { target_os = "openbsd" ))] { - return _window.wayland_display(); + return _event_loop.wayland_display(); } #[allow(unreachable_code)] - None + { + let _ = _event_loop; + None + } } // --------------------------------------------------------------------------- diff --git a/egui_glium/examples/native_texture.rs b/egui_glium/examples/native_texture.rs index 35e9a9c46..de7ec818c 100644 --- a/egui_glium/examples/native_texture.rs +++ b/egui_glium/examples/native_texture.rs @@ -6,7 +6,7 @@ fn main() { let event_loop = glutin::event_loop::EventLoop::with_user_event(); let display = create_display(&event_loop); - let mut egui_glium = egui_glium::EguiGlium::new(&display); + let mut egui_glium = egui_glium::EguiGlium::new(&display, &event_loop); let png_data = include_bytes!("../../examples/retained_image/src/rust-logo-256x256.png"); let image = load_glium_image(png_data); diff --git a/egui_glium/examples/pure_glium.rs b/egui_glium/examples/pure_glium.rs index 1eebbe09f..d1d81a918 100644 --- a/egui_glium/examples/pure_glium.rs +++ b/egui_glium/examples/pure_glium.rs @@ -8,7 +8,7 @@ fn main() { let event_loop = glutin::event_loop::EventLoop::with_user_event(); let display = create_display(&event_loop); - let mut egui_glium = egui_glium::EguiGlium::new(&display); + let mut egui_glium = egui_glium::EguiGlium::new(&display, &event_loop); event_loop.run(move |event, _, control_flow| { let mut redraw = || { diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index f5c85e686..84a2459fb 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -11,6 +11,7 @@ mod painter; pub use painter::Painter; pub use egui_winit; +use egui_winit::winit::event_loop::EventLoopWindowTarget; // ---------------------------------------------------------------------------- @@ -25,14 +26,17 @@ pub struct EguiGlium { } impl EguiGlium { - pub fn new(display: &glium::Display) -> Self { + pub fn new(display: &glium::Display, event_loop: &EventLoopWindowTarget) -> Self { let painter = crate::Painter::new(display); + + let mut egui_winit = egui_winit::State::new(event_loop); + egui_winit.set_max_texture_side(painter.max_texture_side()); + let pixels_per_point = display.gl_window().window().scale_factor() as f32; + egui_winit.set_pixels_per_point(pixels_per_point); + Self { egui_ctx: Default::default(), - egui_winit: egui_winit::State::new( - painter.max_texture_side(), - display.gl_window().window(), - ), + egui_winit, painter, shapes: Default::default(), textures_delta: Default::default(), diff --git a/egui_glow/CHANGELOG.md b/egui_glow/CHANGELOG.md index 0e4854803..0fccbb550 100644 --- a/egui_glow/CHANGELOG.md +++ b/egui_glow/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to the `egui_glow` integration will be noted in this file. ## Unreleased - +* `EguiGlow::new` now takes an `EventLoopWindowTarget` instead of a `winit::Window` ([#1634](https://github.com/emilk/egui/pull/1634)) ## 0.18.1 - 2022-05-05 * Remove calls to `gl.get_error` in release builds to speed up rendering ([#1583](https://github.com/emilk/egui/pull/1583)). diff --git a/egui_glow/examples/pure_glow.rs b/egui_glow/examples/pure_glow.rs index 65a1bdaaf..3af2f8bc5 100644 --- a/egui_glow/examples/pure_glow.rs +++ b/egui_glow/examples/pure_glow.rs @@ -10,7 +10,7 @@ fn main() { let (gl_window, gl) = create_display(&event_loop); let gl = std::sync::Arc::new(gl); - let mut egui_glow = egui_glow::EguiGlow::new(gl_window.window(), gl.clone()); + let mut egui_glow = egui_glow::EguiGlow::new(&event_loop, gl.clone()); event_loop.run(move |event, _, control_flow| { let mut redraw = || { diff --git a/egui_glow/src/winit.rs b/egui_glow/src/winit.rs index cf475cf95..52a0301f3 100644 --- a/egui_glow/src/winit.rs +++ b/egui_glow/src/winit.rs @@ -12,7 +12,10 @@ pub struct EguiGlow { } impl EguiGlow { - pub fn new(window: &winit::window::Window, gl: std::sync::Arc) -> Self { + pub fn new( + event_loop: &winit::event_loop::EventLoopWindowTarget, + gl: std::sync::Arc, + ) -> Self { let painter = crate::Painter::new(gl, None, "") .map_err(|error| { tracing::error!("error occurred in initializing painter:\n{}", error); @@ -21,7 +24,7 @@ impl EguiGlow { Self { egui_ctx: Default::default(), - egui_winit: egui_winit::State::new(painter.max_texture_side(), window), + egui_winit: egui_winit::State::new(event_loop), painter, shapes: Default::default(), textures_delta: Default::default(),