diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 53269c51..0fcfb3ea 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -186,13 +186,15 @@ impl<'app> WgpuWinitApp<'app> { let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new( egui_ctx.clone(), self.native_options.wgpu_options.clone(), - self.native_options.multisampling.max(1) as _, - egui_wgpu::depth_format_from_bits( - self.native_options.depth_buffer, - self.native_options.stencil_buffer, - ), self.native_options.viewport.transparent.unwrap_or(false), - self.native_options.dithering, + egui_wgpu::RendererOptions { + msaa_samples: self.native_options.multisampling as _, + depth_stencil_format: egui_wgpu::depth_format_from_bits( + self.native_options.depth_buffer, + self.native_options.stencil_buffer, + ), + dithering: self.native_options.dithering, + }, )); let window = Arc::new(window); diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 645f6250..9faba9dd 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -14,7 +14,7 @@ pub(crate) struct WebPainterWgpu { surface_configuration: wgpu::SurfaceConfiguration, render_state: Option, on_surface_error: Arc SurfaceErrorAction>, - depth_format: Option, + depth_stencil_format: Option, depth_texture_view: Option, screen_capture_state: Option, capture_tx: CaptureSender, @@ -35,7 +35,7 @@ impl WebPainterWgpu { height_in_pixels: u32, ) -> Option { let device = &render_state.device; - self.depth_format.map(|depth_format| { + self.depth_stencil_format.map(|depth_stencil_format| { device .create_texture(&wgpu::TextureDescriptor { label: Some("egui_depth_texture"), @@ -47,9 +47,9 @@ impl WebPainterWgpu { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: depth_format, + format: depth_stencil_format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[depth_format], + view_formats: &[depth_stencil_format], }) .create_view(&wgpu::TextureViewDescriptor::default()) }) @@ -68,15 +68,17 @@ impl WebPainterWgpu { .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) .map_err(|err| format!("failed to create wgpu surface: {err}"))?; - let depth_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0); + let depth_stencil_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0); let render_state = RenderState::create( &options.wgpu_options, &instance, Some(&surface), - depth_format, - 1, - options.dithering, + egui_wgpu::RendererOptions { + dithering: options.dithering, + depth_stencil_format, + ..Default::default() + }, ) .await .map_err(|err| err.to_string())?; @@ -101,7 +103,7 @@ impl WebPainterWgpu { render_state: Some(render_state), surface, surface_configuration, - depth_format, + depth_stencil_format, depth_texture_view: None, on_surface_error: options.wgpu_options.on_surface_error.clone(), screen_capture_state: None, diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 2cd2235d..59e27e7a 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -173,9 +173,7 @@ impl RenderState { config: &WgpuConfiguration, instance: &wgpu::Instance, compatible_surface: Option<&wgpu::Surface<'static>>, - depth_format: Option, - msaa_samples: u32, - dithering: bool, + options: RendererOptions, ) -> Result { profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function` @@ -244,13 +242,7 @@ impl RenderState { }; let target_format = crate::preferred_framebuffer_format(&surface_formats)?; - let renderer = Renderer::new( - &device, - target_format, - depth_format, - msaa_samples, - dithering, - ); + let renderer = Renderer::new(&device, target_format, options); // On wasm, depending on feature flags, wgpu objects may or may not implement sync. // It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint. diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 2cb5bda5..815ee3e7 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -3,6 +3,7 @@ use std::{borrow::Cow, num::NonZeroU64, ops::Range}; use ahash::HashMap; +use bytemuck::Zeroable as _; use epaint::{PaintCallbackInfo, Primitive, Vertex, emath::NumExt as _}; use wgpu::util::DeviceExt as _; @@ -175,6 +176,57 @@ pub struct Texture { pub options: Option, } +/// Ways to configure [`Renderer`] during creation. +#[derive(Clone, Copy, Debug)] +pub struct RendererOptions { + /// Set the level of the multisampling anti-aliasing (MSAA). + /// + /// Must be a power-of-two. Higher = more smooth 3D. + /// + /// A value of `0` or `1` turns it off (default). + /// + /// `egui` already performs anti-aliasing via "feathering" + /// (controlled by [`egui::epaint::TessellationOptions`]), + /// but if you are embedding 3D in egui you may want to turn on multisampling. + pub msaa_samples: u32, + + /// What format to use for the depth and stencil buffers, + /// e.g. [`wgpu::TextureFormat::Depth32FloatStencil8`]. + /// + /// egui doesn't need depth/stencil, so the default value is `None` (no depth or stancil buffers). + pub depth_stencil_format: Option, + + /// Controls whether to apply dithering to minimize banding artifacts. + /// + /// Dithering assumes an sRGB output and thus will apply noise to any input value that lies between + /// two 8bit values after applying the sRGB OETF function, i.e. if it's not a whole 8bit value in "gamma space". + /// This means that only inputs from texture interpolation and vertex colors should be affected in practice. + /// + /// Defaults to true. + pub dithering: bool, +} + +impl RendererOptions { + /// Set options that produce the most predicatable output. + /// + /// Useful for image snapshot tests. + pub const PREDICTABLE: Self = Self { + msaa_samples: 1, + depth_stencil_format: None, + dithering: false, + }; +} + +impl Default for RendererOptions { + fn default() -> Self { + Self { + msaa_samples: 0, + depth_stencil_format: None, + dithering: true, + } + } +} + /// Renderer for a egui based GUI. pub struct Renderer { pipeline: wgpu::RenderPipeline, @@ -194,7 +246,7 @@ pub struct Renderer { next_user_texture_id: u64, samplers: HashMap, - dithering: bool, + options: RendererOptions, /// Storage for resources shared with all invocations of [`CallbackTrait`]'s methods. /// @@ -210,9 +262,7 @@ impl Renderer { pub fn new( device: &wgpu::Device, output_color_format: wgpu::TextureFormat, - output_depth_format: Option, - msaa_samples: u32, - dithering: bool, + options: RendererOptions, ) -> Self { profiling::function_scope!(); @@ -229,7 +279,7 @@ impl Renderer { label: Some("egui_uniform_buffer"), contents: bytemuck::cast_slice(&[UniformBuffer { screen_size_in_points: [0.0, 0.0], - dithering: u32::from(dithering), + dithering: u32::from(options.dithering), _padding: Default::default(), }]), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, @@ -299,13 +349,15 @@ impl Renderer { push_constant_ranges: &[], }); - let depth_stencil = output_depth_format.map(|format| wgpu::DepthStencilState { - format, - depth_write_enabled: false, - depth_compare: wgpu::CompareFunction::Always, - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }); + let depth_stencil = options + .depth_stencil_format + .map(|format| wgpu::DepthStencilState { + format, + depth_write_enabled: false, + depth_compare: wgpu::CompareFunction::Always, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }); let pipeline = { profiling::scope!("create_render_pipeline"); @@ -337,7 +389,7 @@ impl Renderer { depth_stencil, multisample: wgpu::MultisampleState { alpha_to_coverage_enabled: false, - count: msaa_samples, + count: options.msaa_samples.max(1), mask: !0, }, @@ -392,17 +444,13 @@ impl Renderer { }, uniform_buffer, // Buffers on wgpu are zero initialized, so this is indeed its current state! - previous_uniform_buffer_content: UniformBuffer { - screen_size_in_points: [0.0, 0.0], - dithering: 0, - _padding: 0, - }, + previous_uniform_buffer_content: UniformBuffer::zeroed(), uniform_bind_group, texture_bind_group_layout, textures: HashMap::default(), next_user_texture_id: 0, samplers: HashMap::default(), - dithering, + options, callback_resources: CallbackResources::default(), } } @@ -846,7 +894,7 @@ impl Renderer { let uniform_buffer_content = UniformBuffer { screen_size_in_points, - dithering: u32::from(self.dithering), + dithering: u32::from(self.options.dithering), _padding: Default::default(), }; if uniform_buffer_content != self.previous_uniform_buffer_content { diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 917e704f..bbd19edb 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -1,8 +1,11 @@ #![allow(clippy::missing_errors_doc)] #![allow(clippy::undocumented_unsafe_blocks)] -use crate::capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel}; use crate::{RenderState, SurfaceErrorAction, WgpuConfiguration, renderer}; +use crate::{ + RendererOptions, + capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel}, +}; use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet}; use std::{num::NonZeroU32, sync::Arc}; @@ -21,10 +24,8 @@ struct SurfaceState { pub struct Painter { context: Context, configuration: WgpuConfiguration, - msaa_samples: u32, + options: RendererOptions, support_transparent_backbuffer: bool, - dithering: bool, - depth_format: Option, screen_capture_state: Option, instance: wgpu::Instance, @@ -54,10 +55,8 @@ impl Painter { pub async fn new( context: Context, configuration: WgpuConfiguration, - msaa_samples: u32, - depth_format: Option, support_transparent_backbuffer: bool, - dithering: bool, + options: RendererOptions, ) -> Self { let (capture_tx, capture_rx) = capture_channel(); let instance = configuration.wgpu_setup.new_instance().await; @@ -65,10 +64,8 @@ impl Painter { Self { context, configuration, - msaa_samples, + options, support_transparent_backbuffer, - dithering, - depth_format, screen_capture_state: None, instance, @@ -204,9 +201,7 @@ impl Painter { &self.configuration, &self.instance, Some(&surface), - self.depth_format, - self.msaa_samples, - self.dithering, + self.options, ) .await?; self.render_state.get_or_insert(render_state) @@ -279,7 +274,7 @@ impl Painter { Self::configure_surface(surface_state, render_state, &self.configuration); - if let Some(depth_format) = self.depth_format { + if let Some(depth_format) = self.options.depth_stencil_format { self.depth_texture_view.insert( viewport_id, render_state @@ -292,7 +287,7 @@ impl Painter { depth_or_array_layers: 1, }, mip_level_count: 1, - sample_count: self.msaa_samples, + sample_count: self.options.msaa_samples.max(1), dimension: wgpu::TextureDimension::D2, format: depth_format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT @@ -303,7 +298,7 @@ impl Painter { ); } - if let Some(render_state) = (self.msaa_samples > 1) + if let Some(render_state) = (self.options.msaa_samples > 1) .then_some(self.render_state.as_ref()) .flatten() { @@ -320,7 +315,7 @@ impl Painter { depth_or_array_layers: 1, }, mip_level_count: 1, - sample_count: self.msaa_samples, + sample_count: self.options.msaa_samples.max(1), dimension: wgpu::TextureDimension::D2, format: texture_format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, @@ -450,7 +445,7 @@ impl Painter { }; let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default()); - let (view, resolve_target) = (self.msaa_samples > 1) + let (view, resolve_target) = (self.options.msaa_samples > 1) .then_some(self.msaa_texture_view.get(&viewport_id)) .flatten() .map_or((&target_view, None), |texture_view| { diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index d0a6e848..ae773095 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -67,9 +67,7 @@ pub fn create_render_state(setup: WgpuSetup) -> egui_wgpu::RenderState { }, &instance, None, - None, - 1, - false, + egui_wgpu::RendererOptions::PREDICTABLE, )) .expect("Failed to create render state") }