Skip to main content

egui_wgpu/
lib.rs

1//! This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [wgpu](https://crates.io/crates/wgpu).
2//!
3//! If you're targeting WebGL you also need to turn on the
4//! `webgl` feature of the `wgpu` crate:
5//!
6//! ```toml
7//! # Enable both WebGL and WebGPU backends on web.
8//! wgpu = { version = "*", features = ["webgpu", "webgl"] }
9//! ```
10//!
11//! You can control whether WebGL or WebGPU will be picked at runtime by configuring
12//! [`WgpuConfiguration::wgpu_setup`].
13//! The default is to prefer WebGPU and fall back on WebGL.
14//!
15//! ## Feature flags
16#![doc = document_features::document_features!()]
17//!
18
19pub use wgpu;
20
21/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
22mod renderer;
23
24mod setup;
25
26pub use renderer::*;
27pub use setup::{
28    EguiDisplayHandle, NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew,
29    WgpuSetupExisting,
30};
31
32/// Helpers for capturing screenshots of the UI.
33#[cfg(feature = "capture")]
34pub mod capture;
35
36/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
37#[cfg(feature = "winit")]
38pub mod winit;
39
40use std::sync::Arc;
41
42use epaint::mutex::RwLock;
43
44/// An error produced by egui-wgpu.
45#[derive(thiserror::Error, Debug)]
46pub enum WgpuError {
47    #[error(transparent)]
48    RequestAdapterError(#[from] wgpu::RequestAdapterError),
49
50    #[error("Adapter selection failed: {0}")]
51    CustomNativeAdapterSelectionError(String),
52
53    #[error("There was no valid format for the surface at all.")]
54    NoSurfaceFormatsAvailable,
55
56    #[error(transparent)]
57    RequestDeviceError(#[from] wgpu::RequestDeviceError),
58
59    #[error(transparent)]
60    CreateSurfaceError(#[from] wgpu::CreateSurfaceError),
61
62    #[cfg(feature = "winit")]
63    #[error(transparent)]
64    HandleError(#[from] ::winit::raw_window_handle::HandleError),
65}
66
67/// Runtime-mutable subset of [`WgpuConfiguration`].
68///
69/// Edit any field to have the surface reconfigured on the next paint.
70#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
71pub struct SurfaceConfig {
72    /// Present mode used for the primary surface.
73    pub present_mode: wgpu::PresentMode,
74
75    /// Desired maximum number of frames that the presentation engine should queue in advance.
76    ///
77    /// Use `1` for low-latency, and `2` for high-throughput.
78    ///
79    /// See [`wgpu::SurfaceConfiguration::desired_maximum_frame_latency`] for details.
80    ///
81    /// `None` => Let `wgpu` pick a default (currently `2`).
82    pub desired_maximum_frame_latency: Option<u32>,
83}
84
85impl SurfaceConfig {
86    /// Good default for GUIs with very little (or no) extra GPU work.
87    pub const LOW_LATENCY: Self = Self {
88        present_mode: wgpu::PresentMode::AutoVsync,
89
90        desired_maximum_frame_latency: if cfg!(target_os = "ios") {
91            None // The default is good on iOS, while `Some(1)` cuts FPS in half
92        } else {
93            Some(1)
94        },
95    };
96
97    /// Good default for GUIs with a lot of extra GPU work,
98    /// or that want to prioritize smoothness over latency.
99    pub const HIGH_THROUGHPUT: Self = Self {
100        present_mode: wgpu::PresentMode::AutoVsync,
101        desired_maximum_frame_latency: Some(2), // High-throughput.
102    };
103}
104
105/// Access to the render state for egui.
106#[derive(Clone)]
107pub struct RenderState {
108    /// Wgpu adapter used for rendering.
109    pub adapter: wgpu::Adapter,
110
111    /// All the available adapters.
112    ///
113    /// This is not available on web.
114    /// On web, we always select WebGPU is available, then fall back to WebGL if not.
115    #[cfg(not(target_arch = "wasm32"))]
116    pub available_adapters: Vec<wgpu::Adapter>,
117
118    /// Wgpu instance used for creating surfaces and adapters.
119    pub instance: wgpu::Instance,
120
121    /// Wgpu device used for rendering, created from the adapter.
122    pub device: wgpu::Device,
123
124    /// Wgpu queue used for rendering, created from the adapter.
125    pub queue: wgpu::Queue,
126
127    /// The target texture format used for presenting to the window.
128    pub target_format: wgpu::TextureFormat,
129
130    /// Egui renderer responsible for drawing the UI.
131    pub renderer: Arc<RwLock<Renderer>>,
132
133    /// Runtime-mutable subset of the wgpu configuration.
134    ///
135    /// Update this to have the surface reconfigured on the next paint.
136    pub surface_config: SurfaceConfig,
137}
138
139async fn request_adapter(
140    instance: &wgpu::Instance,
141    power_preference: wgpu::PowerPreference,
142    compatible_surface: Option<&wgpu::Surface<'_>>,
143    available_adapters: &[wgpu::Adapter],
144) -> Result<wgpu::Adapter, WgpuError> {
145    profiling::function_scope!();
146
147    let adapter = instance
148        .request_adapter(&wgpu::RequestAdapterOptions {
149            power_preference,
150            compatible_surface,
151            // We don't expose this as an option right now since it's fairly rarely useful:
152            // * only has an effect on native
153            // * fails if there's no software rasterizer available
154            // * can achieve the same with `native_adapter_selector`
155            force_fallback_adapter: false,
156            apply_limit_buckets: false,
157        })
158        .await
159        .inspect_err(|_err| {
160            if cfg!(target_arch = "wasm32") {
161                // Nothing to add here
162            } else if available_adapters.is_empty() {
163                if std::env::var("DYLD_LIBRARY_PATH").is_ok() {
164                    // DYLD_LIBRARY_PATH can sometimes lead to loading dylibs that cause
165                    // us to find zero adapters. Very strange.
166                    // I don't want to debug this again.
167                    // See https://github.com/rerun-io/rerun/issues/11351 for more
168                    log::warn!(
169                        "No wgpu adapter found. This could be because DYLD_LIBRARY_PATH causes dylibs to be loaded that interfere with Metal device creation. Try restarting with DYLD_LIBRARY_PATH=''"
170                    );
171                } else {
172                    log::info!("No wgpu adapter found");
173                }
174            } else if available_adapters.len() == 1 {
175                log::info!(
176                    "The only available wgpu adapter was not suitable: {}",
177                    adapter_info_summary(&available_adapters[0].get_info())
178                );
179            } else {
180                log::info!(
181                    "No suitable wgpu adapter found out of the {} available ones: {}",
182                    available_adapters.len(),
183                    describe_adapters(available_adapters)
184                );
185            }
186        })?;
187
188    if 1 < available_adapters.len() {
189        log::info!(
190            "There are {} available wgpu adapters: {}",
191            available_adapters.len(),
192            describe_adapters(available_adapters)
193        );
194    }
195
196    Ok(adapter)
197}
198
199impl RenderState {
200    /// Creates a new [`RenderState`], containing everything needed for drawing egui with wgpu.
201    ///
202    /// # Errors
203    /// Wgpu initialization may fail due to incompatible hardware or driver for a given config.
204    pub async fn create(
205        config: &WgpuConfiguration,
206        instance: &wgpu::Instance,
207        compatible_surface: Option<&wgpu::Surface<'static>>,
208        options: RendererOptions,
209    ) -> Result<Self, WgpuError> {
210        profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function`
211
212        // This is always an empty list on web.
213        #[cfg(not(target_arch = "wasm32"))]
214        let available_adapters = {
215            let backends = if let WgpuSetup::CreateNew(create_new) = &config.wgpu_setup {
216                create_new.instance_descriptor.backends
217            } else {
218                wgpu::Backends::all()
219            };
220
221            instance.enumerate_adapters(backends).await
222        };
223
224        let (instance, adapter, device, queue) = match config.wgpu_setup.clone() {
225            WgpuSetup::CreateNew(WgpuSetupCreateNew {
226                instance_descriptor: _,
227                display_handle: _,
228                power_preference,
229                native_adapter_selector: _native_adapter_selector,
230                device_descriptor,
231            }) => {
232                let adapter = {
233                    #[cfg(target_arch = "wasm32")]
234                    {
235                        request_adapter(instance, power_preference, compatible_surface, &[]).await
236                    }
237                    #[cfg(not(target_arch = "wasm32"))]
238                    if let Some(native_adapter_selector) = _native_adapter_selector {
239                        native_adapter_selector(&available_adapters, compatible_surface)
240                            .map_err(WgpuError::CustomNativeAdapterSelectionError)
241                    } else {
242                        request_adapter(
243                            instance,
244                            power_preference,
245                            compatible_surface,
246                            &available_adapters,
247                        )
248                        .await
249                    }
250                }?;
251
252                let (device, queue) = {
253                    profiling::scope!("request_device");
254                    adapter
255                        .request_device(&(*device_descriptor)(&adapter))
256                        .await?
257                };
258
259                (instance.clone(), adapter, device, queue)
260            }
261            WgpuSetup::Existing(WgpuSetupExisting {
262                instance,
263                adapter,
264                device,
265                queue,
266            }) => (instance, adapter, device, queue),
267        };
268
269        log_adapter_info(&adapter.get_info());
270
271        let surface_formats = {
272            profiling::scope!("get_capabilities");
273            compatible_surface.map_or_else(
274                || vec![wgpu::TextureFormat::Rgba8Unorm],
275                |s| s.get_capabilities(&adapter).formats,
276            )
277        };
278        let target_format = crate::preferred_framebuffer_format(&surface_formats)?;
279
280        let renderer = Renderer::new(&device, target_format, options);
281
282        // On wasm, depending on feature flags, wgpu objects may or may not implement sync.
283        // It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
284        #[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
285        Ok(Self {
286            instance,
287            adapter,
288            #[cfg(not(target_arch = "wasm32"))]
289            available_adapters,
290            device,
291            queue,
292            target_format,
293            renderer: Arc::new(RwLock::new(renderer)),
294            surface_config: config.surface,
295        })
296    }
297}
298
299fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
300    if adapters.is_empty() {
301        "(none)".to_owned()
302    } else if adapters.len() == 1 {
303        adapter_info_summary(&adapters[0].get_info())
304    } else {
305        adapters
306            .iter()
307            .map(|a| format!("{{{}}}", adapter_info_summary(&a.get_info())))
308            .collect::<Vec<_>>()
309            .join(", ")
310    }
311}
312
313/// Specifies which action should be taken as consequence of a surface error.
314pub enum SurfaceErrorAction {
315    /// Do nothing and skip the current frame.
316    SkipFrame,
317
318    /// Reconfigure the existing surface, then skip the current frame.
319    ///
320    /// Calls [`wgpu::Surface::configure`] on the current surface object.
321    /// Use for [`wgpu::CurrentSurfaceTexture::Outdated`].
322    Reconfigure,
323
324    /// Drop the surface, create a new one via [`wgpu::Instance::create_surface`], configure it,
325    /// then skip the current frame.
326    ///
327    /// Use for [`wgpu::CurrentSurfaceTexture::Lost`], where reconfiguring the same surface
328    /// object cannot recover.
329    RecreateSurface,
330}
331
332/// Configuration for using wgpu with eframe or the egui-wgpu winit feature.
333#[derive(Clone)]
334pub struct WgpuConfiguration {
335    /// Runtime-mutable configuration for the surface (present mode, frame latency).
336    ///
337    /// These are the fields exposed via [`RenderState::surface_config`] for live
338    /// reconfiguration at runtime.
339    pub surface: SurfaceConfig,
340
341    /// How to create the wgpu adapter & device
342    pub wgpu_setup: WgpuSetup,
343
344    /// Callback for surface status changes.
345    ///
346    /// Called with the [`wgpu::CurrentSurfaceTexture`] result whenever acquiring a frame
347    /// does not return [`wgpu::CurrentSurfaceTexture::Success`]. For
348    /// [`wgpu::CurrentSurfaceTexture::Suboptimal`], egui uses the frame as-is and
349    /// defers surface reconfiguration to the next frame — the callback is not invoked
350    /// in that case either.
351    pub on_surface_status:
352        Arc<dyn Fn(&wgpu::CurrentSurfaceTexture) -> SurfaceErrorAction + Send + Sync>,
353}
354
355#[test]
356fn wgpu_config_impl_send_sync() {
357    fn assert_send_sync<T: Send + Sync>() {}
358    assert_send_sync::<WgpuConfiguration>();
359}
360
361impl std::fmt::Debug for WgpuConfiguration {
362    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363        let Self {
364            surface,
365            wgpu_setup,
366            on_surface_status: _,
367        } = self;
368        f.debug_struct("WgpuConfiguration")
369            .field("surface", &surface)
370            .field("wgpu_setup", &wgpu_setup)
371            .finish_non_exhaustive()
372    }
373}
374
375impl WgpuConfiguration {
376    #[inline]
377    pub fn with_surface_config(mut self, surface_config: SurfaceConfig) -> Self {
378        self.surface = surface_config;
379        self
380    }
381}
382
383impl Default for WgpuConfiguration {
384    fn default() -> Self {
385        Self {
386            surface: SurfaceConfig::HIGH_THROUGHPUT,
387
388            // No display handle available at this point — callers should replace this with
389            // `WgpuSetup::from_display_handle(...)` before creating the instance if one is available.
390            wgpu_setup: WgpuSetup::without_display_handle(),
391            on_surface_status: Arc::new(|status| match status {
392                wgpu::CurrentSurfaceTexture::Outdated => {
393                    // The compositor changed the surface (resize, scale, output, …). wgpu
394                    // requires us to reconfigure before the next acquire. Skipping would mean
395                    // we are stuck in `Outdated` forever.
396                    log::trace!("Dropped frame with error: {status:?}");
397                    SurfaceErrorAction::Reconfigure
398                }
399                wgpu::CurrentSurfaceTexture::Lost => {
400                    // The underlying surface is gone and we need a fresh one from the `wgpu::Instance`.
401                    log::debug!("Dropped frame with error: {status:?}");
402                    SurfaceErrorAction::RecreateSurface
403                }
404                wgpu::CurrentSurfaceTexture::Occluded => {
405                    // App is hidden (minimized / behind another window). Skip silently.
406                    log::trace!("Skipping frame due to occlusion.");
407                    SurfaceErrorAction::SkipFrame
408                }
409                _ => {
410                    log::warn!("Dropped frame with error: {status:?}");
411                    SurfaceErrorAction::SkipFrame
412                }
413            }),
414        }
415    }
416}
417
418/// Find the framebuffer format that egui prefers
419///
420/// # Errors
421/// Returns [`WgpuError::NoSurfaceFormatsAvailable`] if the given list of formats is empty.
422pub fn preferred_framebuffer_format(
423    formats: &[wgpu::TextureFormat],
424) -> Result<wgpu::TextureFormat, WgpuError> {
425    for &format in formats {
426        if matches!(
427            format,
428            wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
429        ) {
430            return Ok(format);
431        }
432    }
433
434    formats
435        .first()
436        .copied()
437        .ok_or(WgpuError::NoSurfaceFormatsAvailable)
438}
439
440/// Take's epi's depth/stencil bits and returns the corresponding wgpu format.
441pub fn depth_format_from_bits(depth_buffer: u8, stencil_buffer: u8) -> Option<wgpu::TextureFormat> {
442    match (depth_buffer, stencil_buffer) {
443        (0, 8) => Some(wgpu::TextureFormat::Stencil8),
444        (16, 0) => Some(wgpu::TextureFormat::Depth16Unorm),
445        (24, 0) => Some(wgpu::TextureFormat::Depth24Plus),
446        (24, 8) => Some(wgpu::TextureFormat::Depth24PlusStencil8),
447        (32, 0) => Some(wgpu::TextureFormat::Depth32Float),
448        (32, 8) => Some(wgpu::TextureFormat::Depth32FloatStencil8),
449        _ => None,
450    }
451}
452
453// ---------------------------------------------------------------------------
454
455fn log_adapter_info(info: &wgpu::AdapterInfo) {
456    let summary = adapter_info_summary(info);
457
458    let is_test = cfg!(test); // Software rasterizers are expected (and preferred) during testing!
459
460    if info.device_type == wgpu::DeviceType::Cpu && !is_test {
461        log::warn!("Software rasterizer detected - loss of performance expected. {summary}");
462    } else {
463        log::debug!("wgpu adapter: {summary}");
464    }
465}
466
467/// A human-readable summary about an adapter
468pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
469    let wgpu::AdapterInfo {
470        name,
471        vendor,
472        device,
473        device_type,
474        driver,
475        driver_info,
476        backend,
477        device_pci_bus_id,
478        subgroup_min_size,
479        subgroup_max_size,
480        transient_saves_memory,
481        limit_bucket,
482    } = &info;
483
484    // Example values:
485    // > 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)"
486    // > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: ""
487    // > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: ""
488
489    use std::fmt::Write as _;
490
491    let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}");
492
493    if !name.is_empty() {
494        write!(summary, ", name: {name:?}").ok();
495    }
496    if !driver.is_empty() {
497        write!(summary, ", driver: {driver:?}").ok();
498    }
499    if !driver_info.is_empty() {
500        write!(summary, ", driver_info: {driver_info:?}").ok();
501    }
502    if *vendor != 0 {
503        #[cfg(not(target_arch = "wasm32"))]
504        {
505            write!(
506                summary,
507                ", vendor: {} (0x{vendor:04X})",
508                parse_vendor_id(*vendor)
509            )
510            .ok();
511        }
512        #[cfg(target_arch = "wasm32")]
513        {
514            write!(summary, ", vendor: 0x{vendor:04X}").ok();
515        }
516    }
517    if *device != 0 {
518        write!(summary, ", device: 0x{device:02X}").ok();
519    }
520    if !device_pci_bus_id.is_empty() {
521        write!(summary, ", pci_bus_id: {device_pci_bus_id:?}").ok();
522    }
523    if *subgroup_min_size != 0 || *subgroup_max_size != 0 {
524        write!(
525            summary,
526            ", subgroup_size: {subgroup_min_size}..={subgroup_max_size}"
527        )
528        .ok();
529    }
530    write!(
531        summary,
532        ", transient_saves_memory: {transient_saves_memory:?}"
533    )
534    .ok();
535    write!(summary, ", limit_bucket: {limit_bucket:?}").ok();
536
537    summary
538}
539
540/// Tries to parse the adapter's vendor ID to a human-readable string.
541#[cfg(not(target_arch = "wasm32"))]
542pub fn parse_vendor_id(vendor_id: u32) -> &'static str {
543    match vendor_id {
544        wgpu::hal::auxil::db::amd::VENDOR => "AMD",
545        wgpu::hal::auxil::db::apple::VENDOR => "Apple",
546        wgpu::hal::auxil::db::arm::VENDOR => "ARM",
547        wgpu::hal::auxil::db::broadcom::VENDOR => "Broadcom",
548        wgpu::hal::auxil::db::imgtec::VENDOR => "Imagination Technologies",
549        wgpu::hal::auxil::db::intel::VENDOR => "Intel",
550        wgpu::hal::auxil::db::mesa::VENDOR => "Mesa",
551        wgpu::hal::auxil::db::nvidia::VENDOR => "NVIDIA",
552        wgpu::hal::auxil::db::qualcomm::VENDOR => "Qualcomm",
553        _ => "Unknown",
554    }
555}