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