diff --git a/Cargo.lock b/Cargo.lock index 2f3085cb..930cf332 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1349,7 +1349,6 @@ dependencies = [ "egui_kittest", "serde", "unicode_names2", - "wgpu", ] [[package]] @@ -1927,6 +1926,18 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror", + "windows", +] + [[package]] name = "gpu-descriptor" version = "0.3.0" @@ -3110,6 +3121,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -3263,6 +3280,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "range-alloc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" + [[package]] name = "raw-window-handle" version = "0.5.2" @@ -4513,6 +4536,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", + "bit-set 0.8.0", "bitflags 2.6.0", "block", "bytemuck", @@ -4521,6 +4545,7 @@ dependencies = [ "glow 0.14.2", "glutin_wgl_sys", "gpu-alloc", + "gpu-allocator", "gpu-descriptor", "js-sys", "khronos-egl", @@ -4534,6 +4559,7 @@ dependencies = [ "once_cell", "parking_lot", "profiling", + "range-alloc", "raw-window-handle 0.6.2", "renderdoc-sys", "rustc-hash", @@ -4543,6 +4569,7 @@ dependencies = [ "web-sys", "wgpu-types", "windows", + "windows-core 0.58.0", ] [[package]] diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 93643c22..dc12c52a 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -183,7 +183,7 @@ impl<'app> WgpuWinitApp<'app> { ) -> crate::Result<&mut WgpuWinitRunning<'app>> { profiling::function_scope!(); #[allow(unsafe_code, unused_mut, unused_unsafe)] - let mut painter = egui_wgpu::winit::Painter::new( + 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 _, @@ -193,7 +193,7 @@ impl<'app> WgpuWinitApp<'app> { ), self.native_options.viewport.transparent.unwrap_or(false), 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 5e217180..f018c731 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -4,7 +4,7 @@ use super::web_painter::WebPainter; use crate::WebOptions; use egui::{Event, UserData, ViewportId}; use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState}; -use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup}; +use egui_wgpu::{RenderState, SurfaceErrorAction}; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; @@ -63,49 +63,7 @@ impl WebPainterWgpu { ) -> Result { log::debug!("Creating wgpu painter"); - let instance = match &options.wgpu_options.wgpu_setup { - WgpuSetup::CreateNew { - supported_backends: backends, - power_preference, - .. - } => { - let mut backends = *backends; - - // Don't try WebGPU if we're not in a secure context. - if backends.contains(wgpu::Backends::BROWSER_WEBGPU) { - let is_secure_context = - web_sys::window().map_or(false, |w| w.is_secure_context()); - if !is_secure_context { - log::info!( - "WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost." - ); - - // Don't try WebGPU since we established now that it will fail. - backends.remove(wgpu::Backends::BROWSER_WEBGPU); - - if backends.is_empty() { - return Err("No available supported graphics backends.".to_owned()); - } - } - } - - log::debug!("Creating wgpu instance with backends {:?}", backends); - - let instance = - wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor { - backends, - ..Default::default() - }) - .await; - - // 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. - #[allow(clippy::arc_with_non_send_sync)] - Arc::new(instance) - } - WgpuSetup::Existing { instance, .. } => instance.clone(), - }; - + let instance = options.wgpu_options.wgpu_setup.new_instance().await; let surface = instance .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) .map_err(|err| format!("failed to create wgpu surface: {err}"))?; diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index b90e5200..0d795783 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -23,8 +23,10 @@ pub use wgpu; /// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`]. mod renderer; +mod setup; + pub use renderer::*; -use wgpu::{Adapter, Device, Instance, Queue, TextureFormat}; +pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting}; /// Helpers for capturing screenshots of the UI. pub mod capture; @@ -40,8 +42,8 @@ use epaint::mutex::RwLock; /// An error produced by egui-wgpu. #[derive(thiserror::Error, Debug)] pub enum WgpuError { - #[error("Failed to create wgpu adapter, no suitable adapter found.")] - NoSuitableAdapterFound, + #[error("Failed to create wgpu adapter, no suitable adapter found: {0}")] + NoSuitableAdapterFound(String), #[error("There was no valid format for the surface at all.")] NoSurfaceFormatsAvailable, @@ -67,8 +69,9 @@ pub struct RenderState { /// /// This is not available on web. /// On web, we always select WebGPU is available, then fall back to WebGL if not. + // TODO(gfx-rs/wgpu#6665): Remove layer of `Arc` here once we update to wgpu 24 #[cfg(not(target_arch = "wasm32"))] - pub available_adapters: Arc<[wgpu::Adapter]>, + pub available_adapters: Arc<[Arc]>, /// Wgpu device used for rendering, created from the adapter. pub device: Arc, @@ -83,6 +86,75 @@ pub struct RenderState { pub renderer: Arc>, } +async fn request_adapter( + instance: &wgpu::Instance, + power_preference: wgpu::PowerPreference, + compatible_surface: Option<&wgpu::Surface<'_>>, + _available_adapters: &[Arc], +) -> Result, WgpuError> { + profiling::function_scope!(); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference, + compatible_surface, + // We don't expose this as an option right now since it's fairly rarely useful: + // * only has an effect on native + // * fails if there's no software rasterizer available + // * can achieve the same with `native_adapter_selector` + force_fallback_adapter: false, + }) + .await + .ok_or_else(|| { + #[cfg(not(target_arch = "wasm32"))] + if _available_adapters.is_empty() { + log::info!("No wgpu adapters found"); + } else if _available_adapters.len() == 1 { + log::info!( + "The only available wgpu adapter was not suitable: {}", + adapter_info_summary(&_available_adapters[0].get_info()) + ); + } else { + log::info!( + "No suitable wgpu adapter found out of the {} available ones: {}", + _available_adapters.len(), + describe_adapters(_available_adapters) + ); + } + + WgpuError::NoSuitableAdapterFound("`request_adapters` returned `None`".to_owned()) + })?; + + #[cfg(target_arch = "wasm32")] + log::debug!( + "Picked wgpu adapter: {}", + adapter_info_summary(&adapter.get_info()) + ); + + #[cfg(not(target_arch = "wasm32"))] + if _available_adapters.len() == 1 { + log::debug!( + "Picked the only available wgpu adapter: {}", + adapter_info_summary(&adapter.get_info()) + ); + } else { + log::info!( + "There were {} available wgpu adapters: {}", + _available_adapters.len(), + describe_adapters(_available_adapters) + ); + log::debug!( + "Picked wgpu adapter: {}", + adapter_info_summary(&adapter.get_info()) + ); + } + + // 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. + #[allow(clippy::arc_with_non_send_sync)] + Ok(Arc::new(adapter)) +} + impl RenderState { /// Creates a new [`RenderState`], containing everything needed for drawing egui with wgpu. /// @@ -100,96 +172,73 @@ impl RenderState { // This is always an empty list on web. #[cfg(not(target_arch = "wasm32"))] - let available_adapters = instance.enumerate_adapters(wgpu::Backends::all()); + let available_adapters = { + let backends = if let WgpuSetup::CreateNew(create_new) = &config.wgpu_setup { + create_new.instance_descriptor.backends + } else { + wgpu::Backends::all() + }; + + instance + .enumerate_adapters(backends) + // TODO(gfx-rs/wgpu#6665): Remove layer of `Arc` here once we update to wgpu 24. + .into_iter() + .map(Arc::new) + .collect::>() + }; let (adapter, device, queue) = match config.wgpu_setup.clone() { - WgpuSetup::CreateNew { - supported_backends: _, + WgpuSetup::CreateNew(WgpuSetupCreateNew { + instance_descriptor: _, power_preference, + native_adapter_selector: _native_adapter_selector, device_descriptor, - } => { + trace_path, + }) => { let adapter = { - profiling::scope!("request_adapter"); - instance - .request_adapter(&wgpu::RequestAdapterOptions { + #[cfg(target_arch = "wasm32")] + { + request_adapter(instance, power_preference, compatible_surface, &[]).await + } + #[cfg(not(target_arch = "wasm32"))] + if let Some(native_adapter_selector) = _native_adapter_selector { + native_adapter_selector(&available_adapters, compatible_surface) + .map_err(WgpuError::NoSuitableAdapterFound) + } else { + request_adapter( + instance, power_preference, compatible_surface, - force_fallback_adapter: false, - }) + &available_adapters, + ) .await - .ok_or_else(|| { - #[cfg(not(target_arch = "wasm32"))] - if available_adapters.is_empty() { - log::info!("No wgpu adapters found"); - } else if available_adapters.len() == 1 { - log::info!( - "The only available wgpu adapter was not suitable: {}", - adapter_info_summary(&available_adapters[0].get_info()) - ); - } else { - log::info!( - "No suitable wgpu adapter found out of the {} available ones: {}", - available_adapters.len(), - describe_adapters(&available_adapters) - ); - } + } + }?; - WgpuError::NoSuitableAdapterFound - })? - }; - - #[cfg(target_arch = "wasm32")] - log::debug!( - "Picked wgpu adapter: {}", - adapter_info_summary(&adapter.get_info()) - ); - - #[cfg(not(target_arch = "wasm32"))] - if available_adapters.len() == 1 { - log::debug!( - "Picked the only available wgpu adapter: {}", - adapter_info_summary(&adapter.get_info()) - ); - } else { - log::info!( - "There were {} available wgpu adapters: {}", - available_adapters.len(), - describe_adapters(&available_adapters) - ); - log::debug!( - "Picked wgpu adapter: {}", - adapter_info_summary(&adapter.get_info()) - ); - } - - let trace_path = std::env::var("WGPU_TRACE"); let (device, queue) = { profiling::scope!("request_device"); adapter - .request_device( - &(*device_descriptor)(&adapter), - trace_path.ok().as_ref().map(std::path::Path::new), - ) + .request_device(&(*device_descriptor)(&adapter), trace_path.as_deref()) .await? }; // 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. #[allow(clippy::arc_with_non_send_sync)] - (Arc::new(adapter), Arc::new(device), Arc::new(queue)) + (adapter, Arc::new(device), Arc::new(queue)) } - WgpuSetup::Existing { + WgpuSetup::Existing(WgpuSetupExisting { instance: _, adapter, device, queue, - } => (adapter, device, queue), + }) => (adapter, device, queue), }; let surface_formats = { profiling::scope!("get_capabilities"); compatible_surface.map_or_else( - || vec![TextureFormat::Rgba8Unorm], + || vec![wgpu::TextureFormat::Rgba8Unorm], |s| s.get_capabilities(&adapter).formats, ) }; @@ -219,20 +268,17 @@ impl RenderState { } #[cfg(not(target_arch = "wasm32"))] -fn describe_adapters(adapters: &[wgpu::Adapter]) -> String { +fn describe_adapters(adapters: &[Arc]) -> String { if adapters.is_empty() { "(none)".to_owned() } else if adapters.len() == 1 { adapter_info_summary(&adapters[0].get_info()) } else { - let mut list_string = String::new(); - for adapter in adapters { - if !list_string.is_empty() { - list_string += ", "; - } - list_string += &format!("{{{}}}", adapter_info_summary(&adapter.get_info())); - } - list_string + adapters + .iter() + .map(|a| format!("{{{}}}", adapter_info_summary(&a.get_info()))) + .collect::>() + .join(", ") } } @@ -245,62 +291,6 @@ pub enum SurfaceErrorAction { RecreateSurface, } -#[derive(Clone)] -pub enum WgpuSetup { - /// Construct a wgpu setup using some predefined settings & heuristics. - /// This is the default option. You can customize most behaviours overriding the - /// supported backends, power preferences, and device description. - /// - /// This can also be configured with the environment variables: - /// * `WGPU_BACKEND`: `vulkan`, `dx11`, `dx12`, `metal`, `opengl`, `webgpu` - /// * `WGPU_POWER_PREF`: `low`, `high` or `none` - CreateNew { - /// Backends that should be supported (wgpu will pick one of these). - /// - /// For instance, if you only want to support WebGL (and not WebGPU), - /// you can set this to [`wgpu::Backends::GL`]. - /// - /// By default on web, WebGPU will be used if available. - /// WebGL will only be used as a fallback, - /// and only if you have enabled the `webgl` feature of crate `wgpu`. - supported_backends: wgpu::Backends, - - /// Power preference for the adapter. - power_preference: wgpu::PowerPreference, - - /// Configuration passed on device request, given an adapter - device_descriptor: - Arc wgpu::DeviceDescriptor<'static> + Send + Sync>, - }, - - /// Run on an existing wgpu setup. - Existing { - instance: Arc, - adapter: Arc, - device: Arc, - queue: Arc, - }, -} - -impl std::fmt::Debug for WgpuSetup { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::CreateNew { - supported_backends, - power_preference, - device_descriptor: _, - } => f - .debug_struct("AdapterSelection::Standard") - .field("supported_backends", &supported_backends) - .field("power_preference", &power_preference) - .finish(), - Self::Existing { .. } => f - .debug_struct("AdapterSelection::Existing") - .finish_non_exhaustive(), - } - } -} - /// Configuration for using wgpu with eframe or the egui-wgpu winit feature. #[derive(Clone)] pub struct WgpuConfiguration { @@ -352,42 +342,8 @@ impl Default for WgpuConfiguration { fn default() -> Self { Self { present_mode: wgpu::PresentMode::AutoVsync, - desired_maximum_frame_latency: None, - - // By default, create a new wgpu setup. This will create a new instance, adapter, device and queue. - // This will create an instance for the supported backends (which can be configured by - // `WGPU_BACKEND`), and will pick an adapter by iterating adapters based on their power preference. The power - // preference can also be configured by `WGPU_POWER_PREF`. - wgpu_setup: WgpuSetup::CreateNew { - // Add GL backend, primarily because WebGPU is not stable enough yet. - // (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl") - supported_backends: wgpu::util::backend_bits_from_env() - .unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL), - - power_preference: wgpu::util::power_preference_from_env() - .unwrap_or(wgpu::PowerPreference::HighPerformance), - device_descriptor: Arc::new(|adapter| { - let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl { - wgpu::Limits::downlevel_webgl2_defaults() - } else { - wgpu::Limits::default() - }; - - wgpu::DeviceDescriptor { - label: Some("egui wgpu device"), - required_features: wgpu::Features::default(), - required_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, - ..base_limits - }, - memory_hints: wgpu::MemoryHints::default(), - } - }), - }, - + wgpu_setup: Default::default(), on_surface_error: Arc::new(|err| { if err == wgpu::SurfaceError::Outdated { // This error occurs when the app is minimized on Windows. @@ -468,8 +424,14 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { summary += &format!(", driver_info: {driver_info:?}"); } if *vendor != 0 { - // TODO(emilk): decode using https://github.com/gfx-rs/wgpu/blob/767ac03245ee937d3dc552edc13fe7ab0a860eec/wgpu-hal/src/auxil/mod.rs#L7 - summary += &format!(", vendor: 0x{vendor:04X}"); + #[cfg(not(target_arch = "wasm32"))] + { + summary += &format!(", vendor: {} (0x{vendor:04X})", parse_vendor_id(*vendor)); + } + #[cfg(target_arch = "wasm32")] + { + summary += &format!(", vendor: 0x{vendor:04X}"); + } } if *device != 0 { summary += &format!(", device: 0x{device:02X}"); @@ -477,3 +439,20 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { summary } + +/// Tries to parse the adapter's vendor ID to a human-readable string. +#[cfg(not(target_arch = "wasm32"))] +pub fn parse_vendor_id(vendor_id: u32) -> &'static str { + match vendor_id { + wgpu::hal::auxil::db::amd::VENDOR => "AMD", + wgpu::hal::auxil::db::apple::VENDOR => "Apple", + wgpu::hal::auxil::db::arm::VENDOR => "ARM", + wgpu::hal::auxil::db::broadcom::VENDOR => "Broadcom", + wgpu::hal::auxil::db::imgtec::VENDOR => "Imagination Technologies", + wgpu::hal::auxil::db::intel::VENDOR => "Intel", + wgpu::hal::auxil::db::mesa::VENDOR => "Mesa", + wgpu::hal::auxil::db::nvidia::VENDOR => "NVIDIA", + wgpu::hal::auxil::db::qualcomm::VENDOR => "Qualcomm", + _ => "Unknown", + } +} diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs new file mode 100644 index 00000000..567c0d75 --- /dev/null +++ b/crates/egui-wgpu/src/setup.rs @@ -0,0 +1,237 @@ +use std::sync::Arc; + +#[derive(Clone)] +pub enum WgpuSetup { + /// Construct a wgpu setup using some predefined settings & heuristics. + /// This is the default option. You can customize most behaviours overriding the + /// supported backends, power preferences, and device description. + /// + /// By default can also be configured with various environment variables: + /// * `WGPU_BACKEND`: `vulkan`, `dx12`, `metal`, `opengl`, `webgpu` + /// * `WGPU_POWER_PREF`: `low`, `high` or `none` + /// * `WGPU_TRACE`: Path to a file to output a wgpu trace file. + /// + /// Each instance flag also comes with an environment variable (for details see [`wgpu::InstanceFlags`]): + /// * `WGPU_VALIDATION`: Enables validation (enabled by default in debug builds). + /// * `WGPU_DEBUG`: Generate debug information in shaders and objects (enabled by default in debug builds). + /// * `WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER`: Whether wgpu should expose adapters that run on top of non-compliant adapters. + /// * `WGPU_GPU_BASED_VALIDATION`: Enable GPU-based validation. + CreateNew(WgpuSetupCreateNew), + + /// Run on an existing wgpu setup. + Existing(WgpuSetupExisting), +} + +impl Default for WgpuSetup { + fn default() -> Self { + Self::CreateNew(WgpuSetupCreateNew::default()) + } +} + +impl std::fmt::Debug for WgpuSetup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::CreateNew(create_new) => f + .debug_tuple("WgpuSetup::CreateNew") + .field(create_new) + .finish(), + Self::Existing { .. } => f.debug_tuple("WgpuSetup::Existing").finish(), + } + } +} + +impl WgpuSetup { + /// Creates a new [`wgpu::Instance`] or clones the existing one. + /// + /// Does *not* store the wgpu instance, so calling this repeatedly may + /// create a new instance every time! + pub async fn new_instance(&self) -> Arc { + match self { + Self::CreateNew(create_new) => { + #[allow(unused_mut)] + let mut backends = create_new.instance_descriptor.backends; + + // Don't try WebGPU if we're not in a secure context. + #[cfg(target_arch = "wasm32")] + if backends.contains(wgpu::Backends::BROWSER_WEBGPU) { + let is_secure_context = + wgpu::web_sys::window().map_or(false, |w| w.is_secure_context()); + if !is_secure_context { + log::info!( + "WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost." + ); + backends.remove(wgpu::Backends::BROWSER_WEBGPU); + } + } + + log::debug!("Creating wgpu instance with backends {:?}", backends); + + #[allow(clippy::arc_with_non_send_sync)] + Arc::new( + wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor { + backends: create_new.instance_descriptor.backends, + flags: create_new.instance_descriptor.flags, + dx12_shader_compiler: create_new + .instance_descriptor + .dx12_shader_compiler + .clone(), + gles_minor_version: create_new.instance_descriptor.gles_minor_version, + }) + .await, + ) + } + Self::Existing(existing) => existing.instance.clone(), + } + } +} + +impl From for WgpuSetup { + fn from(create_new: WgpuSetupCreateNew) -> Self { + Self::CreateNew(create_new) + } +} + +impl From for WgpuSetup { + fn from(existing: WgpuSetupExisting) -> Self { + Self::Existing(existing) + } +} + +/// Method for selecting an adapter on native. +/// +/// This can be used for fully custom adapter selection. +/// If available, `wgpu::Surface` is passed to allow checking for surface compatibility. +// TODO(gfx-rs/wgpu#6665): Remove layer of `Arc` here. +pub type NativeAdapterSelectorMethod = Arc< + dyn Fn(&[Arc], Option<&wgpu::Surface<'_>>) -> Result, String> + + Send + + Sync, +>; + +/// Configuration for creating a new wgpu setup. +/// +/// Used for [`WgpuSetup::CreateNew`]. +pub struct WgpuSetupCreateNew { + /// Instance descriptor for creating a wgpu instance. + /// + /// The most important field is [`wgpu::InstanceDescriptor::backends`], which + /// controls which backends are supported (wgpu will pick one of these). + /// If you only want to support WebGL (and not WebGPU), + /// you can set this to [`wgpu::Backends::GL`]. + /// By default on web, WebGPU will be used if available. + /// WebGL will only be used as a fallback, + /// and only if you have enabled the `webgl` feature of crate `wgpu`. + pub instance_descriptor: wgpu::InstanceDescriptor, + + /// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web. + pub power_preference: wgpu::PowerPreference, + + /// Optional selector for native adapters. + /// + /// This field has no effect when targeting web! + /// Otherwise, if set [`Self::power_preference`] is ignored and the adapter is instead selected by this method. + /// Note that [`Self::instance_descriptor`]'s [`wgpu::InstanceDescriptor::backends`] + /// are still used to filter the adapter enumeration in the first place. + /// + /// Defaults to `None`. + pub native_adapter_selector: Option, + + /// Configuration passed on device request, given an adapter + pub device_descriptor: + Arc wgpu::DeviceDescriptor<'static> + Send + Sync>, + + /// Option path to output a wgpu trace file. + /// + /// This only works if this feature is enabled in `wgpu-core`. + /// Does not work when running with WebGPU. + /// Defaults to the path set in the `WGPU_TRACE` environment variable. + pub trace_path: Option, +} + +impl Clone for WgpuSetupCreateNew { + fn clone(&self) -> Self { + Self { + // TODO(gfx-rs/wgpu/#6849): use .clone() + instance_descriptor: wgpu::InstanceDescriptor { + backends: self.instance_descriptor.backends, + flags: self.instance_descriptor.flags, + dx12_shader_compiler: self.instance_descriptor.dx12_shader_compiler.clone(), + gles_minor_version: self.instance_descriptor.gles_minor_version, + }, + power_preference: self.power_preference, + native_adapter_selector: self.native_adapter_selector.clone(), + device_descriptor: self.device_descriptor.clone(), + trace_path: self.trace_path.clone(), + } + } +} + +impl std::fmt::Debug for WgpuSetupCreateNew { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WgpuSetupCreateNew") + .field("instance_descriptor", &self.instance_descriptor) + .field("power_preference", &self.power_preference) + .field( + "native_adapter_selector", + &self.native_adapter_selector.is_some(), + ) + .field("trace_path", &self.trace_path) + .finish() + } +} + +impl Default for WgpuSetupCreateNew { + fn default() -> Self { + Self { + instance_descriptor: wgpu::InstanceDescriptor { + // Add GL backend, primarily because WebGPU is not stable enough yet. + // (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl") + backends: wgpu::util::backend_bits_from_env() + .unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL), + flags: wgpu::InstanceFlags::from_build_config().with_env(), + dx12_shader_compiler: wgpu::Dx12Compiler::default(), + gles_minor_version: wgpu::Gles3MinorVersion::Automatic, + }, + + power_preference: wgpu::util::power_preference_from_env() + .unwrap_or(wgpu::PowerPreference::HighPerformance), + + native_adapter_selector: None, + + device_descriptor: Arc::new(|adapter| { + let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl { + wgpu::Limits::downlevel_webgl2_defaults() + } else { + wgpu::Limits::default() + }; + + wgpu::DeviceDescriptor { + label: Some("egui wgpu device"), + required_features: wgpu::Features::default(), + required_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, + ..base_limits + }, + memory_hints: wgpu::MemoryHints::default(), + } + }), + + trace_path: std::env::var("WGPU_TRACE") + .ok() + .map(std::path::PathBuf::from), + } + } +} + +/// Configuration for using an existing wgpu setup. +/// +/// Used for [`WgpuSetup::Existing`]. +#[derive(Clone)] +pub struct WgpuSetupExisting { + pub instance: Arc, + pub adapter: Arc, + pub device: Arc, + pub queue: Arc, +} diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 19be186f..86f7bd84 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -51,7 +51,7 @@ impl Painter { /// [`set_window()`](Self::set_window) once you have /// a [`winit::window::Window`] with a valid `.raw_window_handle()` /// associated. - pub fn new( + pub async fn new( context: Context, configuration: WgpuConfiguration, msaa_samples: u32, @@ -59,17 +59,8 @@ impl Painter { support_transparent_backbuffer: bool, dithering: bool, ) -> Self { - let instance = match &configuration.wgpu_setup { - crate::WgpuSetup::CreateNew { - supported_backends, .. - } => Arc::new(wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: *supported_backends, - ..Default::default() - })), - crate::WgpuSetup::Existing { instance, .. } => instance.clone(), - }; - let (capture_tx, capture_rx) = capture_channel(); + let instance = configuration.wgpu_setup.new_instance().await; Self { context, diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index b494e18a..047ed305 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -57,7 +57,6 @@ serde = { workspace = true, optional = true } [dev-dependencies] criterion.workspace = true egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } -wgpu = { workspace = true, features = ["metal"] } egui = { workspace = true, features = ["default_fonts"] } [[bench]] diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index 4f52d4ae..1e4af19e 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -20,7 +20,13 @@ include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [features] # Adds a wgpu-based test renderer. -wgpu = ["dep:egui-wgpu", "dep:pollster", "dep:image", "eframe?/wgpu"] +wgpu = [ + "dep:egui-wgpu", + "dep:pollster", + "dep:image", + "dep:wgpu", + "eframe?/wgpu", +] # Adds a dify-based image snapshot utility. snapshot = ["dep:dify", "dep:image", "image/png"] @@ -38,6 +44,8 @@ eframe = { workspace = true, optional = true } egui-wgpu = { workspace = true, optional = true } pollster = { workspace = true, optional = true } image = { workspace = true, optional = true } +# Enable DX12 because it always comes with a software rasterizer. +wgpu = { workspace = true, features = ["metal", "dx12"], optional = true } # snapshot dependencies dify = { workspace = true, optional = true } @@ -48,7 +56,6 @@ document-features = { workspace = true, optional = true } [dev-dependencies] egui = { workspace = true, features = ["default_fonts"] } image = { workspace = true, features = ["png"] } -wgpu = { workspace = true, features = ["metal"] } [lints] workspace = true diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index 3f229763..e7734c95 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -1,27 +1,56 @@ -use crate::texture_to_image::texture_to_image; -use eframe::epaint::TextureId; -use egui::TexturesDelta; -use egui_wgpu::wgpu::{Backends, StoreOp, TextureFormat}; -use egui_wgpu::{wgpu, RenderState, ScreenDescriptor, WgpuSetup}; -use image::RgbaImage; use std::iter::once; use std::sync::Arc; -use wgpu::Maintain; -// TODO(#5506): Replace this with the setup from https://github.com/emilk/egui/pull/5506 +use egui::TexturesDelta; +use egui_wgpu::{wgpu, RenderState, ScreenDescriptor, WgpuSetup}; +use image::RgbaImage; + +use crate::texture_to_image::texture_to_image; + +/// Default wgpu setup used for the wgpu renderer. pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup { - egui_wgpu::WgpuSetup::CreateNew { - supported_backends: Backends::all(), - device_descriptor: Arc::new(|_| wgpu::DeviceDescriptor::default()), - power_preference: wgpu::PowerPreference::default(), - } + let mut setup = egui_wgpu::WgpuSetupCreateNew::default(); + + // WebGPU not supported yet since we rely on blocking screenshots. + setup + .instance_descriptor + .backends + .remove(wgpu::Backends::BROWSER_WEBGPU); + + // Prefer software rasterizers. + setup.native_adapter_selector = Some(Arc::new(|adapters, _surface| { + let mut adapters = adapters.iter().collect::>(); + + // Adapters are already sorted by preferred backend by wgpu, but let's be explicit. + adapters.sort_by_key(|a| match a.get_info().backend { + wgpu::Backend::Metal => 0, + wgpu::Backend::Vulkan => 1, + wgpu::Backend::Dx12 => 2, + wgpu::Backend::Gl => 4, + wgpu::Backend::BrowserWebGpu => 6, + wgpu::Backend::Empty => 7, + }); + + // Prefer CPU adapters, otherwise if we can't, prefer discrete GPU over integrated GPU. + adapters.sort_by_key(|a| match a.get_info().device_type { + wgpu::DeviceType::Cpu => 0, // CPU is the best for our purposes! + wgpu::DeviceType::DiscreteGpu => 1, + wgpu::DeviceType::Other + | wgpu::DeviceType::IntegratedGpu + | wgpu::DeviceType::VirtualGpu => 2, + }); + + adapters + .first() + .map(|a| (*a).clone()) + .ok_or("No adapter found".to_owned()) + })); + + egui_wgpu::WgpuSetup::CreateNew(setup) } pub fn create_render_state(setup: WgpuSetup) -> egui_wgpu::RenderState { - let instance = match &setup { - WgpuSetup::Existing { instance, .. } => instance.clone(), - WgpuSetup::CreateNew { .. } => Default::default(), - }; + let instance = pollster::block_on(setup.new_instance()); pollster::block_on(egui_wgpu::RenderState::create( &egui_wgpu::WgpuConfiguration { @@ -72,7 +101,7 @@ impl WgpuTestRenderer { render_state .renderer .read() - .texture(&TextureId::Managed(0)) + .texture(&egui::epaint::TextureId::Managed(0)) .is_none(), "The RenderState passed in has been used before, pass in a fresh RenderState instead." ); @@ -143,7 +172,7 @@ impl crate::TestRenderer for WgpuTestRenderer { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: TextureFormat::Rgba8Unorm, + format: self.render_state.target_format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, view_formats: &[], }); @@ -159,12 +188,10 @@ impl crate::TestRenderer for WgpuTestRenderer { resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - store: StoreOp::Store, + store: wgpu::StoreOp::Store, }, })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, + ..Default::default() }) .forget_lifetime(); @@ -175,7 +202,7 @@ impl crate::TestRenderer for WgpuTestRenderer { .queue .submit(user_buffers.into_iter().chain(once(encoder.finish()))); - self.render_state.device.poll(Maintain::Wait); + self.render_state.device.poll(wgpu::Maintain::Wait); Ok(texture_to_image( &self.render_state.device,