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