diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 695f0df8..6c493298 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -15,6 +15,7 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C * Web: you can access your application from JS using `AppRunner::app_mut`. See `crates/egui_demo_app/src/lib.rs`. * Web: You can now use WebGL on top of `wgpu` by enabling the `wgpu` feature (and disabling `glow` via disabling default features) ([#2107](https://github.com/emilk/egui/pull/2107)). * Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)). +* * Wgpu device/adapter/surface creation has now various configuration options exposed via `NativeOptions/WebOptions::wgpu_options` ([#2207](https://github.com/emilk/egui/pull/2207)). ## 0.19.0 - 2022-08-20 diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 5239364b..7b74f1c8 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -11,6 +11,7 @@ use std::any::Any; #[cfg(not(target_arch = "wasm32"))] pub use crate::native::run::RequestRepaintEvent; + #[cfg(not(target_arch = "wasm32"))] pub use winit::event_loop::EventLoopBuilder; @@ -364,6 +365,10 @@ pub struct NativeOptions { /// /// Wayland desktop currently not supported. pub centered: bool, + + /// Configures wgpu instance/device/adapter/surface creation and renderloop. + #[cfg(feature = "wgpu")] + pub wgpu_options: egui_wgpu::WgpuConfiguration, } #[cfg(not(target_arch = "wasm32"))] @@ -372,6 +377,8 @@ impl Clone for NativeOptions { Self { icon_data: self.icon_data.clone(), event_loop_builder: None, // Skip any builder callbacks if cloning + #[cfg(feature = "wgpu")] + wgpu_options: self.wgpu_options.clone(), ..*self } } @@ -409,6 +416,8 @@ impl Default for NativeOptions { #[cfg(feature = "glow")] shader_version: None, centered: false, + #[cfg(feature = "wgpu")] + wgpu_options: egui_wgpu::WgpuConfiguration::default(), } } } @@ -459,6 +468,10 @@ pub struct WebOptions { /// Default: [`WebGlContextOption::BestFirst`]. #[cfg(feature = "glow")] pub webgl_context_option: WebGlContextOption, + + /// Configures wgpu instance/device/adapter/surface creation and renderloop. + #[cfg(feature = "wgpu")] + pub wgpu_options: egui_wgpu::WgpuConfiguration, } #[cfg(target_arch = "wasm32")] @@ -467,8 +480,26 @@ impl Default for WebOptions { Self { follow_system_theme: true, default_theme: Theme::Dark, + #[cfg(feature = "glow")] webgl_context_option: WebGlContextOption::BestFirst, + + #[cfg(feature = "wgpu")] + wgpu_options: egui_wgpu::WgpuConfiguration { + // WebGPU is not stable enough yet, use WebGL emulation + backends: wgpu::Backends::GL, + device_descriptor: wgpu::DeviceDescriptor { + label: Some("egui wgpu device"), + features: wgpu::Features::default(), + limits: wgpu::Limits { + // When using a depth buffer, we have to be able to create a texture + // large enough for the entire surface, and we want to support 4k+ displays. + max_texture_dimension_2d: 8192, + ..wgpu::Limits::downlevel_webgl2_defaults() + }, + }, + ..Default::default() + }, } } } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 802b44a0..5af24347 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -647,7 +647,7 @@ mod wgpu_integration { /// a Resumed event. On Android this ensures that any graphics state is only /// initialized once the application has an associated `SurfaceView`. struct WgpuWinitRunning { - painter: egui_wgpu::winit::Painter<'static>, + painter: egui_wgpu::winit::Painter, integration: epi_integration::EpiIntegration, app: Box, } @@ -723,23 +723,10 @@ mod wgpu_integration { storage: Option>, window: winit::window::Window, ) { - let mut limits = wgpu::Limits::downlevel_webgl2_defaults(); - if self.native_options.depth_buffer > 0 { - // When using a depth buffer, we have to be able to create a texture large enough for the entire surface. - limits.max_texture_dimension_2d = 8192; - } - #[allow(unsafe_code, unused_mut, unused_unsafe)] let painter = unsafe { 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::PresentMode::Fifo, + self.native_options.wgpu_options.clone(), self.native_options.multisampling.max(1) as _, self.native_options.depth_buffer, ); diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 75f0b53e..0cf111ea 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -4,7 +4,7 @@ use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; use egui::{mutex::RwLock, Rgba}; -use egui_wgpu::{renderer::ScreenDescriptor, RenderState}; +use egui_wgpu::{renderer::ScreenDescriptor, RenderState, SurfaceErrorAction}; use crate::WebOptions; @@ -14,9 +14,10 @@ pub(crate) struct WebPainterWgpu { canvas: HtmlCanvasElement, canvas_id: String, surface: wgpu::Surface, - surface_size: [u32; 2], + surface_configuration: wgpu::SurfaceConfiguration, limits: wgpu::Limits, render_state: Option, + on_surface_error: Arc SurfaceErrorAction>, } impl WebPainterWgpu { @@ -26,30 +27,27 @@ impl WebPainterWgpu { } #[allow(unused)] // only used if `wgpu` is the only active feature. - pub async fn new(canvas_id: &str, _options: &WebOptions) -> Result { - tracing::debug!("Creating wgpu painter with WebGL backend…"); + pub async fn new(canvas_id: &str, options: &WebOptions) -> Result { + tracing::debug!("Creating wgpu painter"); let canvas = super::canvas_element_or_die(canvas_id); - let limits = wgpu::Limits::downlevel_webgl2_defaults(); // TODO(Wumpf): Expose to eframe user - // TODO(Wumpf): Should be able to switch between WebGL & WebGPU (only) - let backends = wgpu::Backends::GL; //wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::Backends::all); - let instance = wgpu::Instance::new(backends); + let instance = wgpu::Instance::new(options.wgpu_options.backends); let surface = instance.create_surface_from_canvas(&canvas); - let adapter = - wgpu::util::initialize_adapter_from_env_or_default(&instance, backends, Some(&surface)) - .await - .ok_or_else(|| "No suitable GPU adapters found on the system".to_owned())?; + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: options.wgpu_options.power_preference, + force_fallback_adapter: false, + compatible_surface: None, + }) + .await + .ok_or_else(|| "No suitable GPU adapters found on the system".to_owned())?; let (device, queue) = adapter .request_device( - &wgpu::DeviceDescriptor { - label: Some("egui_webpainter"), - features: wgpu::Features::empty(), - limits: limits.clone(), - }, - None, // No capture exposed so far - unclear how we can expose this in a browser environment (?) + &options.wgpu_options.device_descriptor, + None, // Capture doesn't work in the browser environment. ) .await .map_err(|err| format!("Failed to find wgpu device: {}", err))?; @@ -65,6 +63,15 @@ impl WebPainterWgpu { renderer: Arc::new(RwLock::new(renderer)), }; + let surface_configuration = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: target_format, + width: 0, + height: 0, + present_mode: options.wgpu_options.present_mode, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + }; + tracing::debug!("wgpu painter initialized."); Ok(Self { @@ -72,8 +79,9 @@ impl WebPainterWgpu { canvas_id: canvas_id.to_owned(), render_state: Some(render_state), surface, - surface_size: [0, 0], - limits, + surface_configuration, + limits: options.wgpu_options.device_descriptor.limits.clone(), + on_surface_error: options.wgpu_options.on_surface_error.clone(), }) } } @@ -103,28 +111,30 @@ impl WebPainter for WebPainterWgpu { }; // Resize surface if needed - let canvas_size = [self.canvas.width(), self.canvas.height()]; - if canvas_size != self.surface_size { - self.surface.configure( - &render_state.device, - &wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: render_state.target_format, - width: canvas_size[0], - height: canvas_size[1], - present_mode: wgpu::PresentMode::Fifo, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - }, - ); - self.surface_size = canvas_size; + let size_in_pixels = [self.canvas.width(), self.canvas.height()]; + if size_in_pixels[0] != self.surface_configuration.width + || size_in_pixels[1] != self.surface_configuration.height + { + self.surface_configuration.width = size_in_pixels[0]; + self.surface_configuration.height = size_in_pixels[1]; + self.surface + .configure(&render_state.device, &self.surface_configuration); } - let frame = self.surface.get_current_texture().map_err(|err| { - JsValue::from_str(&format!( - "Failed to acquire next swap chain texture: {}", - err - )) - })?; + let frame = match self.surface.get_current_texture() { + Ok(frame) => frame, + #[allow(clippy::single_match_else)] + Err(e) => match (*self.on_surface_error)(e) { + SurfaceErrorAction::RecreateSurface => { + self.surface + .configure(&render_state.device, &self.surface_configuration); + return Ok(()); + } + SurfaceErrorAction::SkipFrame => { + return Ok(()); + } + }, + }; let mut encoder = render_state @@ -135,10 +145,9 @@ impl WebPainter for WebPainterWgpu { // Upload all resources for the GPU. let screen_descriptor = ScreenDescriptor { - size_in_pixels: canvas_size, + size_in_pixels, pixels_per_point, }; - { let mut renderer = render_state.renderer.write(); for (id, image_delta) in &textures_delta.set { diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index f880a942..6a591f58 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to the `egui-wgpu` integration will be noted in this file. * `PrepareCallback` now passes `wgpu::CommandEncoder` ([#2136](https://github.com/emilk/egui/pull/2136)) * Only a single vertex & index buffer is now created and resized when necessary (previously, vertex/index buffers were allocated for every mesh) ([#2148](https://github.com/emilk/egui/pull/2148)). * `Renderer::update_texture` no longer creates a new `wgpu::Sampler` with every new texture ([#2198](https://github.com/emilk/egui/pull/2198)) +* `Painter`'s instance/device/adapter/surface creation is now configurable via `WgpuConfiguration` ([#2207](https://github.com/emilk/egui/pull/2207)) ## 0.19.0 - 2022-08-20 * Enables deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)). diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 7455943b..d88b78e7 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -20,8 +20,7 @@ pub mod winit; use egui::mutex::RwLock; use std::sync::Arc; -/// Access to the render state for egui, which can be useful in combination with -/// [`egui::PaintCallback`]s for custom rendering using WGPU. +/// Access to the render state for egui. #[derive(Clone)] pub struct RenderState { pub device: Arc, @@ -30,6 +29,60 @@ pub struct RenderState { pub renderer: Arc>, } +/// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`] +pub enum SurfaceErrorAction { + /// Do nothing and skip the current frame. + SkipFrame, + + /// Instructs egui to recreate the surface, then skip the current frame. + RecreateSurface, +} + +/// Configuration for using wgpu with eframe or the egui-wgpu winit feature. +#[derive(Clone)] +pub struct WgpuConfiguration { + /// Configuration passed on device request. + pub device_descriptor: wgpu::DeviceDescriptor<'static>, + + /// Backends that should be supported (wgpu will pick one of these) + pub backends: wgpu::Backends, + + /// Present mode used for the primary surface. + pub present_mode: wgpu::PresentMode, + + /// Power preference for the adapter. + pub power_preference: wgpu::PowerPreference, + + /// Callback for surface errors. + pub on_surface_error: Arc SurfaceErrorAction>, +} + +impl Default for WgpuConfiguration { + fn default() -> Self { + Self { + device_descriptor: wgpu::DeviceDescriptor { + label: Some("egui wgpu device"), + features: wgpu::Features::default(), + limits: wgpu::Limits::default(), + }, + backends: wgpu::Backends::PRIMARY | wgpu::Backends::GL, + present_mode: wgpu::PresentMode::AutoVsync, + power_preference: wgpu::PowerPreference::HighPerformance, + + on_surface_error: Arc::new(|err| { + if err == wgpu::SurfaceError::Outdated { + // This error occurs when the app is minimized on Windows. + // Silently return here to prevent spamming the console with: + // "The underlying surface has changed, and therefore the swap chain must be updated" + } else { + tracing::warn!("Dropped frame with error: {err}"); + } + SurfaceErrorAction::SkipFrame + }), + } + } +} + /// Find the framebuffer format that egui prefers pub fn preferred_framebuffer_format(formats: &[wgpu::TextureFormat]) -> wgpu::TextureFormat { for &format in formats { diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 3c9bd748..0919c9e0 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -4,7 +4,7 @@ use egui::mutex::RwLock; use tracing::error; use wgpu::{Adapter, Instance, Surface}; -use crate::{renderer, RenderState, Renderer}; +use crate::{renderer, RenderState, Renderer, SurfaceErrorAction, WgpuConfiguration}; struct SurfaceState { surface: Surface, @@ -15,10 +15,8 @@ struct SurfaceState { /// 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, +pub struct Painter { + configuration: WgpuConfiguration, msaa_samples: u32, depth_format: Option, depth_texture_view: Option, @@ -29,7 +27,7 @@ pub struct Painter<'a> { surface_state: Option, } -impl<'a> Painter<'a> { +impl Painter { /// Manages [`wgpu`] state, including surface state, required to render egui. /// /// Only the [`wgpu::Instance`] is initialized here. Device selection and the initialization @@ -42,20 +40,11 @@ impl<'a> Painter<'a> { /// [`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, - depth_bits: u8, - ) -> Self { - let instance = wgpu::Instance::new(backends); + pub fn new(configuration: WgpuConfiguration, msaa_samples: u32, depth_bits: u8) -> Self { + let instance = wgpu::Instance::new(configuration.backends); Self { - power_preference, - device_descriptor, - present_mode, + configuration, msaa_samples, depth_format: (depth_bits > 0).then(|| wgpu::TextureFormat::Depth32Float), depth_texture_view: None, @@ -80,7 +69,8 @@ impl<'a> Painter<'a> { target_format: wgpu::TextureFormat, ) -> RenderState { let (device, queue) = - pollster::block_on(adapter.request_device(&self.device_descriptor, None)).unwrap(); + pollster::block_on(adapter.request_device(&self.configuration.device_descriptor, None)) + .unwrap(); let renderer = Renderer::new(&device, target_format, self.depth_format, self.msaa_samples); @@ -100,7 +90,7 @@ impl<'a> Painter<'a> { 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, + power_preference: self.configuration.power_preference, compatible_surface: Some(surface), force_fallback_adapter: false, })) @@ -130,7 +120,7 @@ impl<'a> Painter<'a> { format, width: width_in_pixels, height: height_in_pixels, - present_mode: self.present_mode, + present_mode: self.configuration.present_mode, alpha_mode: wgpu::CompositeAlphaMode::Auto, }; @@ -245,19 +235,20 @@ impl<'a> Painter<'a> { Some(rs) => rs, None => return, }; + let (width, height) = (surface_state.width, surface_state.height); 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. - // Silently return here to prevent spamming the console with: - // "The underlying surface has changed, and therefore the swap chain must be updated" - return; - } - Err(e) => { - tracing::warn!("Dropped frame with error: {e}"); - return; - } + #[allow(clippy::single_match_else)] + Err(e) => match (*self.configuration.on_surface_error)(e) { + SurfaceErrorAction::RecreateSurface => { + self.configure_surface(width, height); + return; + } + SurfaceErrorAction::SkipFrame => { + return; + } + }, }; let mut encoder = @@ -269,7 +260,7 @@ impl<'a> Painter<'a> { // Upload all resources for the GPU. let screen_descriptor = renderer::ScreenDescriptor { - size_in_pixels: [surface_state.width, surface_state.height], + size_in_pixels: [width, height], pixels_per_point, };