luminol_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 setting
12//! [`WgpuConfiguration::supported_backends`].
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// Luminol's wgpu resources are not Send or Sync on web.
21// We are doing this here to reduce merge conflicts, since it's likely wgpu will fix this.
22#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
23
24pub use wgpu;
25
26/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
27mod renderer;
28
29pub use renderer::*;
30
31/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
32#[cfg(feature = "winit")]
33pub mod winit;
34
35use std::sync::Arc;
36
37use epaint::mutex::RwLock;
38
39/// An error produced by egui-wgpu.
40#[derive(thiserror::Error, Debug)]
41pub enum WgpuError {
42    #[error("Failed to create wgpu adapter, no suitable adapter found.")]
43    NoSuitableAdapterFound,
44
45    #[error("There was no valid format for the surface at all.")]
46    NoSurfaceFormatsAvailable,
47
48    #[error(transparent)]
49    RequestDeviceError(#[from] wgpu::RequestDeviceError),
50
51    #[error(transparent)]
52    CreateSurfaceError(#[from] wgpu::CreateSurfaceError),
53
54    #[cfg(feature = "winit")]
55    #[error(transparent)]
56    HandleError(#[from] ::winit::raw_window_handle::HandleError),
57}
58
59/// Access to the render state for egui.
60#[derive(Clone)]
61pub struct RenderState {
62    /// Wgpu adapter used for rendering.
63    pub adapter: Arc<wgpu::Adapter>,
64
65    /// All the available adapters.
66    ///
67    /// This is not available on web.
68    /// On web, we always select WebGPU is available, then fall back to WebGL if not.
69    #[cfg(not(target_arch = "wasm32"))]
70    pub available_adapters: Arc<[wgpu::Adapter]>,
71
72    /// Wgpu device used for rendering, created from the adapter.
73    pub device: Arc<wgpu::Device>,
74
75    /// Wgpu queue used for rendering, created from the adapter.
76    pub queue: Arc<wgpu::Queue>,
77
78    /// The target texture format used for presenting to the window.
79    pub target_format: wgpu::TextureFormat,
80
81    /// Egui renderer responsible for drawing the UI.
82    pub renderer: Arc<RwLock<Renderer>>,
83}
84
85impl RenderState {
86    /// Creates a new [`RenderState`], containing everything needed for drawing egui with wgpu.
87    ///
88    /// # Errors
89    /// Wgpu initialization may fail due to incompatible hardware or driver for a given config.
90    pub async fn create(
91        config: &WgpuConfiguration,
92        instance: &wgpu::Instance,
93        surface: &wgpu::Surface<'static>,
94        depth_format: Option<wgpu::TextureFormat>,
95        msaa_samples: u32,
96    ) -> Result<Self, WgpuError> {
97        crate::profile_scope!("RenderState::create"); // async yield give bad names using `profile_function`
98
99        // This is always an empty list on web.
100        #[cfg(not(target_arch = "wasm32"))]
101        let available_adapters = instance.enumerate_adapters(wgpu::Backends::all());
102
103        let adapter = {
104            crate::profile_scope!("request_adapter");
105            instance
106                .request_adapter(&wgpu::RequestAdapterOptions {
107                    power_preference: config.power_preference,
108                    compatible_surface: Some(surface),
109                    force_fallback_adapter: false,
110                })
111                .await
112                .ok_or_else(|| {
113                    #[cfg(not(target_arch = "wasm32"))]
114                    if available_adapters.is_empty() {
115                        log::info!("No wgpu adapters found");
116                    } else if available_adapters.len() == 1 {
117                        log::info!(
118                            "The only available wgpu adapter was not suitable: {}",
119                            adapter_info_summary(&available_adapters[0].get_info())
120                        );
121                    } else {
122                        log::info!(
123                            "No suitable wgpu adapter found out of the {} available ones: {}",
124                            available_adapters.len(),
125                            describe_adapters(&available_adapters)
126                        );
127                    }
128
129                    WgpuError::NoSuitableAdapterFound
130                })?
131        };
132
133        #[cfg(target_arch = "wasm32")]
134        log::debug!(
135            "Picked wgpu adapter: {}",
136            adapter_info_summary(&adapter.get_info())
137        );
138
139        #[cfg(not(target_arch = "wasm32"))]
140        if available_adapters.len() == 1 {
141            log::debug!(
142                "Picked the only available wgpu adapter: {}",
143                adapter_info_summary(&adapter.get_info())
144            );
145        } else {
146            log::info!(
147                "There were {} available wgpu adapters: {}",
148                available_adapters.len(),
149                describe_adapters(&available_adapters)
150            );
151            log::debug!(
152                "Picked wgpu adapter: {}",
153                adapter_info_summary(&adapter.get_info())
154            );
155        }
156
157        let capabilities = {
158            crate::profile_scope!("get_capabilities");
159            surface.get_capabilities(&adapter).formats
160        };
161        let target_format = crate::preferred_framebuffer_format(&capabilities)?;
162
163        let (device, queue) = {
164            crate::profile_scope!("request_device");
165            adapter
166                .request_device(&(*config.device_descriptor)(&adapter), None)
167                .await?
168        };
169
170        let renderer = Renderer::new(&device, target_format, depth_format, msaa_samples);
171
172        Ok(Self {
173            adapter: Arc::new(adapter),
174            #[cfg(not(target_arch = "wasm32"))]
175            available_adapters: available_adapters.into(),
176            device: Arc::new(device),
177            queue: Arc::new(queue),
178            target_format,
179            renderer: Arc::new(RwLock::new(renderer)),
180        })
181    }
182}
183
184#[cfg(not(target_arch = "wasm32"))]
185fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
186    if adapters.is_empty() {
187        "(none)".to_owned()
188    } else if adapters.len() == 1 {
189        adapter_info_summary(&adapters[0].get_info())
190    } else {
191        let mut list_string = String::new();
192        for adapter in adapters {
193            if !list_string.is_empty() {
194                list_string += ", ";
195            }
196            list_string += &format!("{{{}}}", adapter_info_summary(&adapter.get_info()));
197        }
198        list_string
199    }
200}
201
202/// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`]
203pub enum SurfaceErrorAction {
204    /// Do nothing and skip the current frame.
205    SkipFrame,
206
207    /// Instructs egui to recreate the surface, then skip the current frame.
208    RecreateSurface,
209}
210
211/// Configuration for using wgpu with eframe or the egui-wgpu winit feature.
212///
213/// This can also be configured with the environment variables:
214/// * `WGPU_BACKEND`: `vulkan`, `dx11`, `dx12`, `metal`, `opengl`, `webgpu`
215/// * `WGPU_POWER_PREF`: `low`, `high` or `none`
216#[derive(Clone)]
217pub struct WgpuConfiguration {
218    /// Backends that should be supported (wgpu will pick one of these).
219    ///
220    /// For instance, if you only want to support WebGL (and not WebGPU),
221    /// you can set this to [`wgpu::Backends::GL`].
222    ///
223    /// By default on web, WebGPU will be used if available.
224    /// WebGL will only be used as a fallback,
225    /// and only if you have enabled the `webgl` feature of crate `wgpu`.
226    pub supported_backends: wgpu::Backends,
227
228    /// Configuration passed on device request, given an adapter
229    pub device_descriptor: Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static>>,
230
231    /// Present mode used for the primary surface.
232    pub present_mode: wgpu::PresentMode,
233
234    /// Desired maximum number of frames that the presentation engine should queue in advance.
235    ///
236    /// Use `1` for low-latency, and `2` for high-throughput.
237    ///
238    /// See [`wgpu::SurfaceConfiguration::desired_maximum_frame_latency`] for details.
239    ///
240    /// `None` = `wgpu` default.
241    pub desired_maximum_frame_latency: Option<u32>,
242
243    /// Power preference for the adapter.
244    pub power_preference: wgpu::PowerPreference,
245
246    /// Callback for surface errors.
247    pub on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
248}
249
250impl std::fmt::Debug for WgpuConfiguration {
251    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        let Self {
253            supported_backends,
254            device_descriptor: _,
255            present_mode,
256            desired_maximum_frame_latency,
257            power_preference,
258            on_surface_error: _,
259        } = self;
260        f.debug_struct("WgpuConfiguration")
261            .field("supported_backends", &supported_backends)
262            .field("present_mode", &present_mode)
263            .field(
264                "desired_maximum_frame_latency",
265                &desired_maximum_frame_latency,
266            )
267            .field("power_preference", &power_preference)
268            .finish_non_exhaustive()
269    }
270}
271
272impl Default for WgpuConfiguration {
273    fn default() -> Self {
274        Self {
275            // Add GL backend, primarily because WebGPU is not stable enough yet.
276            // (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl")
277            supported_backends: wgpu::util::backend_bits_from_env()
278                .unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL),
279
280            device_descriptor: Arc::new(|adapter| {
281                let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
282                    wgpu::Limits::downlevel_webgl2_defaults()
283                } else {
284                    wgpu::Limits::default()
285                };
286
287                wgpu::DeviceDescriptor {
288                    label: Some("egui wgpu device"),
289                    required_features: wgpu::Features::default(),
290                    required_limits: wgpu::Limits {
291                        // When using a depth buffer, we have to be able to create a texture
292                        // large enough for the entire surface, and we want to support 4k+ displays.
293                        max_texture_dimension_2d: 8192,
294                        ..base_limits
295                    },
296                }
297            }),
298
299            present_mode: wgpu::PresentMode::AutoVsync,
300
301            desired_maximum_frame_latency: None,
302
303            power_preference: wgpu::util::power_preference_from_env()
304                .unwrap_or(wgpu::PowerPreference::HighPerformance),
305
306            on_surface_error: Arc::new(|err| {
307                if err == wgpu::SurfaceError::Outdated {
308                    // This error occurs when the app is minimized on Windows.
309                    // Silently return here to prevent spamming the console with:
310                    // "The underlying surface has changed, and therefore the swap chain must be updated"
311                } else {
312                    log::warn!("Dropped frame with error: {err}");
313                }
314                SurfaceErrorAction::SkipFrame
315            }),
316        }
317    }
318}
319
320/// Find the framebuffer format that egui prefers
321///
322/// # Errors
323/// Returns [`WgpuError::NoSurfaceFormatsAvailable`] if the given list of formats is empty.
324pub fn preferred_framebuffer_format(
325    formats: &[wgpu::TextureFormat],
326) -> Result<wgpu::TextureFormat, WgpuError> {
327    for &format in formats {
328        if matches!(
329            format,
330            wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
331        ) {
332            return Ok(format);
333        }
334    }
335
336    formats
337        .first()
338        .copied()
339        .ok_or(WgpuError::NoSurfaceFormatsAvailable)
340}
341
342/// Take's epi's depth/stencil bits and returns the corresponding wgpu format.
343pub fn depth_format_from_bits(depth_buffer: u8, stencil_buffer: u8) -> Option<wgpu::TextureFormat> {
344    match (depth_buffer, stencil_buffer) {
345        (0, 8) => Some(wgpu::TextureFormat::Stencil8),
346        (16, 0) => Some(wgpu::TextureFormat::Depth16Unorm),
347        (24, 0) => Some(wgpu::TextureFormat::Depth24Plus),
348        (24, 8) => Some(wgpu::TextureFormat::Depth24PlusStencil8),
349        (32, 0) => Some(wgpu::TextureFormat::Depth32Float),
350        (32, 8) => Some(wgpu::TextureFormat::Depth32FloatStencil8),
351        _ => None,
352    }
353}
354
355// ---------------------------------------------------------------------------
356
357/// A human-readable summary about an adapter
358pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
359    let wgpu::AdapterInfo {
360        name,
361        vendor,
362        device,
363        device_type,
364        driver,
365        driver_info,
366        backend,
367    } = &info;
368
369    // Example values:
370    // > 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)"
371    // > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: ""
372    // > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: ""
373
374    let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}");
375
376    if !name.is_empty() {
377        summary += &format!(", name: {name:?}");
378    }
379    if !driver.is_empty() {
380        summary += &format!(", driver: {driver:?}");
381    }
382    if !driver_info.is_empty() {
383        summary += &format!(", driver_info: {driver_info:?}");
384    }
385    if *vendor != 0 {
386        // TODO(emilk): decode using https://github.com/gfx-rs/wgpu/blob/767ac03245ee937d3dc552edc13fe7ab0a860eec/wgpu-hal/src/auxil/mod.rs#L7
387        summary += &format!(", vendor: 0x{vendor:04X}");
388    }
389    if *device != 0 {
390        summary += &format!(", device: 0x{device:02X}");
391    }
392
393    summary
394}
395
396// ---------------------------------------------------------------------------
397
398mod profiling_scopes {
399    #![allow(unused_macros)]
400    #![allow(unused_imports)]
401
402    /// Profiling macro for feature "puffin"
403    macro_rules! profile_function {
404        ($($arg: tt)*) => {
405            #[cfg(feature = "puffin")]
406            #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
407            puffin::profile_function!($($arg)*);
408        };
409    }
410    pub(crate) use profile_function;
411
412    /// Profiling macro for feature "puffin"
413    macro_rules! profile_scope {
414        ($($arg: tt)*) => {
415            #[cfg(feature = "puffin")]
416            #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
417            puffin::profile_scope!($($arg)*);
418        };
419    }
420    pub(crate) use profile_scope;
421}
422
423#[allow(unused_imports)]
424pub(crate) use profiling_scopes::*;