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