//! This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [wgpu](https://crates.io/crates/wgpu). //! //! If you're targeting WebGL you also need to turn on the //! `webgl` feature of the `wgpu` crate: //! //! ```toml //! # Enable both WebGL and WebGPU backends on web. //! wgpu = { version = "*", features = ["webgpu", "webgl"] } //! ``` //! //! You can control whether WebGL or WebGPU will be picked at runtime by configuring //! [`WgpuConfiguration::wgpu_setup`]. //! The default is to prefer WebGPU and fall back on WebGL. //! //! ## Feature flags #![doc = document_features::document_features!()] //! #![allow(unsafe_code)] pub use wgpu; /// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`]. mod renderer; mod setup; pub use renderer::*; pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting}; /// Helpers for capturing screenshots of the UI. pub mod capture; /// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`]. #[cfg(feature = "winit")] pub mod winit; use std::sync::Arc; 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: {0}")] NoSuitableAdapterFound(String), #[error("There was no valid format for the surface at all.")] NoSurfaceFormatsAvailable, #[error(transparent)] RequestDeviceError(#[from] wgpu::RequestDeviceError), #[error(transparent)] CreateSurfaceError(#[from] wgpu::CreateSurfaceError), #[cfg(feature = "winit")] #[error(transparent)] HandleError(#[from] ::winit::raw_window_handle::HandleError), } /// Access to the render state for egui. #[derive(Clone)] pub struct RenderState { /// Wgpu adapter used for rendering. pub adapter: wgpu::Adapter, /// All the available adapters. /// /// This is not available on web. /// On web, we always select WebGPU is available, then fall back to WebGL if not. #[cfg(not(target_arch = "wasm32"))] pub available_adapters: Vec, /// Wgpu device used for rendering, created from the adapter. pub device: wgpu::Device, /// Wgpu queue used for rendering, created from the adapter. pub queue: wgpu::Queue, /// The target texture format used for presenting to the window. pub target_format: wgpu::TextureFormat, /// Egui renderer responsible for drawing the UI. pub renderer: Arc>, } async fn request_adapter( instance: &wgpu::Instance, power_preference: wgpu::PowerPreference, compatible_surface: Option<&wgpu::Surface<'_>>, _available_adapters: &[wgpu::Adapter], ) -> Result { 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()) ); } Ok(adapter) } impl RenderState { /// Creates a new [`RenderState`], containing everything needed for drawing egui with wgpu. /// /// # Errors /// Wgpu initialization may fail due to incompatible hardware or driver for a given config. pub async fn create( config: &WgpuConfiguration, instance: &wgpu::Instance, compatible_surface: Option<&wgpu::Surface<'static>>, depth_format: Option, msaa_samples: u32, dithering: bool, ) -> Result { profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function` // This is always an empty list on web. #[cfg(not(target_arch = "wasm32"))] 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) }; let (adapter, device, queue) = match config.wgpu_setup.clone() { WgpuSetup::CreateNew(WgpuSetupCreateNew { instance_descriptor: _, power_preference, native_adapter_selector: _native_adapter_selector, device_descriptor, trace_path, }) => { let adapter = { #[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, &available_adapters, ) .await } }?; let (device, queue) = { profiling::scope!("request_device"); adapter .request_device(&(*device_descriptor)(&adapter), trace_path.as_deref()) .await? }; (adapter, device, queue) } WgpuSetup::Existing(WgpuSetupExisting { instance: _, adapter, device, queue, }) => (adapter, device, queue), }; let surface_formats = { profiling::scope!("get_capabilities"); compatible_surface.map_or_else( || vec![wgpu::TextureFormat::Rgba8Unorm], |s| s.get_capabilities(&adapter).formats, ) }; let target_format = crate::preferred_framebuffer_format(&surface_formats)?; let renderer = Renderer::new( &device, target_format, depth_format, msaa_samples, dithering, ); // 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(Self { adapter, #[cfg(not(target_arch = "wasm32"))] available_adapters, device, queue, target_format, renderer: Arc::new(RwLock::new(renderer)), }) } } #[cfg(not(target_arch = "wasm32"))] fn describe_adapters(adapters: &[wgpu::Adapter]) -> String { if adapters.is_empty() { "(none)".to_owned() } else if adapters.len() == 1 { adapter_info_summary(&adapters[0].get_info()) } else { adapters .iter() .map(|a| format!("{{{}}}", adapter_info_summary(&a.get_info()))) .collect::>() .join(", ") } } /// 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 { /// Present mode used for the primary surface. pub present_mode: wgpu::PresentMode, /// Desired maximum number of frames that the presentation engine should queue in advance. /// /// Use `1` for low-latency, and `2` for high-throughput. /// /// See [`wgpu::SurfaceConfiguration::desired_maximum_frame_latency`] for details. /// /// `None` = `wgpu` default. pub desired_maximum_frame_latency: Option, /// How to create the wgpu adapter & device pub wgpu_setup: WgpuSetup, /// Callback for surface errors. pub on_surface_error: Arc SurfaceErrorAction + Send + Sync>, } #[test] fn wgpu_config_impl_send_sync() { fn assert_send_sync() {} assert_send_sync::(); } impl std::fmt::Debug for WgpuConfiguration { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Self { present_mode, desired_maximum_frame_latency, wgpu_setup, on_surface_error: _, } = self; f.debug_struct("WgpuConfiguration") .field("present_mode", &present_mode) .field( "desired_maximum_frame_latency", &desired_maximum_frame_latency, ) .field("wgpu_setup", &wgpu_setup) .finish_non_exhaustive() } } impl Default for WgpuConfiguration { fn default() -> Self { Self { present_mode: wgpu::PresentMode::AutoVsync, desired_maximum_frame_latency: None, 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. // Silently return here to prevent spamming the console with: // "The underlying surface has changed, and therefore the swap chain must be updated" } else { log::warn!("Dropped frame with error: {err}"); } SurfaceErrorAction::SkipFrame }), } } } /// Find the framebuffer format that egui prefers /// /// # Errors /// Returns [`WgpuError::NoSurfaceFormatsAvailable`] if the given list of formats is empty. pub fn preferred_framebuffer_format( formats: &[wgpu::TextureFormat], ) -> Result { for &format in formats { if matches!( format, wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm ) { return Ok(format); } } formats .first() .copied() .ok_or(WgpuError::NoSurfaceFormatsAvailable) } /// Take's epi's depth/stencil bits and returns the corresponding wgpu format. pub fn depth_format_from_bits(depth_buffer: u8, stencil_buffer: u8) -> Option { match (depth_buffer, stencil_buffer) { (0, 8) => Some(wgpu::TextureFormat::Stencil8), (16, 0) => Some(wgpu::TextureFormat::Depth16Unorm), (24, 0) => Some(wgpu::TextureFormat::Depth24Plus), (24, 8) => Some(wgpu::TextureFormat::Depth24PlusStencil8), (32, 0) => Some(wgpu::TextureFormat::Depth32Float), (32, 8) => Some(wgpu::TextureFormat::Depth32FloatStencil8), _ => None, } } // --------------------------------------------------------------------------- /// A human-readable summary about an adapter pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { let wgpu::AdapterInfo { name, vendor, device, device_type, driver, driver_info, backend, } = &info; // Example values: // > name: "llvmpipe (LLVM 16.0.6, 256 bits)", device_type: Cpu, backend: Vulkan, driver: "llvmpipe", driver_info: "Mesa 23.1.6-arch1.4 (LLVM 16.0.6)" // > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: "" // > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: "" let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}"); if !name.is_empty() { summary += &format!(", name: {name:?}"); } if !driver.is_empty() { summary += &format!(", driver: {driver:?}"); } if !driver_info.is_empty() { summary += &format!(", driver_info: {driver_info:?}"); } if *vendor != 0 { #[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}"); } 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", } }