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