egui_wgpu/
winit.rs

1#![allow(clippy::missing_errors_doc)]
2#![allow(clippy::undocumented_unsafe_blocks)]
3
4use crate::{RenderState, SurfaceErrorAction, WgpuConfiguration, renderer};
5use crate::{
6    RendererOptions,
7    capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
8};
9use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
10use std::{num::NonZeroU32, sync::Arc};
11
12struct SurfaceState {
13    surface: wgpu::Surface<'static>,
14    alpha_mode: wgpu::CompositeAlphaMode,
15    width: u32,
16    height: u32,
17    resizing: bool,
18}
19
20/// Everything you need to paint egui with [`wgpu`] on [`winit`].
21///
22/// Alternatively you can use [`crate::Renderer`] directly.
23///
24/// NOTE: all egui viewports share the same painter.
25pub struct Painter {
26    context: Context,
27    configuration: WgpuConfiguration,
28    options: RendererOptions,
29    support_transparent_backbuffer: bool,
30    screen_capture_state: Option<CaptureState>,
31
32    instance: wgpu::Instance,
33    render_state: Option<RenderState>,
34
35    // Per viewport/window:
36    depth_texture_view: ViewportIdMap<wgpu::TextureView>,
37    msaa_texture_view: ViewportIdMap<wgpu::TextureView>,
38    surfaces: ViewportIdMap<SurfaceState>,
39    capture_tx: CaptureSender,
40    capture_rx: CaptureReceiver,
41}
42
43impl Painter {
44    /// Manages [`wgpu`] state, including surface state, required to render egui.
45    ///
46    /// Only the [`wgpu::Instance`] is initialized here. Device selection and the initialization
47    /// of render + surface state is deferred until the painter is given its first window target
48    /// via [`set_window()`](Self::set_window). (Ensuring that a device that's compatible with the
49    /// native window is chosen)
50    ///
51    /// Before calling [`paint_and_update_textures()`](Self::paint_and_update_textures) a
52    /// [`wgpu::Surface`] must be initialized (and corresponding render state) by calling
53    /// [`set_window()`](Self::set_window) once you have
54    /// a [`winit::window::Window`] with a valid `.raw_window_handle()`
55    /// associated.
56    pub async fn new(
57        context: Context,
58        configuration: WgpuConfiguration,
59        support_transparent_backbuffer: bool,
60        options: RendererOptions,
61    ) -> Self {
62        let (capture_tx, capture_rx) = capture_channel();
63        let instance = configuration.wgpu_setup.new_instance().await;
64
65        Self {
66            context,
67            configuration,
68            options,
69            support_transparent_backbuffer,
70            screen_capture_state: None,
71
72            instance,
73            render_state: None,
74
75            depth_texture_view: Default::default(),
76            surfaces: Default::default(),
77            msaa_texture_view: Default::default(),
78
79            capture_tx,
80            capture_rx,
81        }
82    }
83
84    /// Get the [`RenderState`].
85    ///
86    /// Will return [`None`] if the render state has not been initialized yet.
87    pub fn render_state(&self) -> Option<RenderState> {
88        self.render_state.clone()
89    }
90
91    fn configure_surface(
92        surface_state: &SurfaceState,
93        render_state: &RenderState,
94        config: &WgpuConfiguration,
95    ) {
96        profiling::function_scope!();
97
98        let width = surface_state.width;
99        let height = surface_state.height;
100
101        let mut surf_config = wgpu::SurfaceConfiguration {
102            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
103            format: render_state.target_format,
104            present_mode: config.present_mode,
105            alpha_mode: surface_state.alpha_mode,
106            view_formats: vec![render_state.target_format],
107            ..surface_state
108                .surface
109                .get_default_config(&render_state.adapter, width, height)
110                .expect("The surface isn't supported by this adapter")
111        };
112
113        if let Some(desired_maximum_frame_latency) = config.desired_maximum_frame_latency {
114            surf_config.desired_maximum_frame_latency = desired_maximum_frame_latency;
115        }
116
117        surface_state
118            .surface
119            .configure(&render_state.device, &surf_config);
120    }
121
122    /// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`]
123    ///
124    /// This creates a [`wgpu::Surface`] for the given Window (as well as initializing render
125    /// state if needed) that is used for egui rendering.
126    ///
127    /// This must be called before trying to render via
128    /// [`paint_and_update_textures`](Self::paint_and_update_textures)
129    ///
130    /// # Portability
131    ///
132    /// _In particular it's important to note that on Android a it's only possible to create
133    /// a window surface between `Resumed` and `Paused` lifecycle events, and Winit will panic on
134    /// attempts to query the raw window handle while paused._
135    ///
136    /// On Android [`set_window`](Self::set_window) should be called with `Some(window)` for each
137    /// `Resumed` event and `None` for each `Paused` event. Currently, on all other platforms
138    /// [`set_window`](Self::set_window) may be called with `Some(window)` as soon as you have a
139    /// valid [`winit::window::Window`].
140    ///
141    /// # Errors
142    /// If the provided wgpu configuration does not match an available device.
143    pub async fn set_window(
144        &mut self,
145        viewport_id: ViewportId,
146        window: Option<Arc<winit::window::Window>>,
147    ) -> Result<(), crate::WgpuError> {
148        profiling::scope!("Painter::set_window"); // profile_function gives bad names for async functions
149
150        if let Some(window) = window {
151            let size = window.inner_size();
152            if !self.surfaces.contains_key(&viewport_id) {
153                let surface = self.instance.create_surface(window)?;
154                self.add_surface(surface, viewport_id, size).await?;
155            }
156        } else {
157            log::warn!("No window - clearing all surfaces");
158            self.surfaces.clear();
159        }
160        Ok(())
161    }
162
163    /// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`] without taking ownership of the window.
164    ///
165    /// Like [`set_window`](Self::set_window) except:
166    ///
167    /// # Safety
168    /// The user is responsible for ensuring that the window is alive for as long as it is set.
169    pub async unsafe fn set_window_unsafe(
170        &mut self,
171        viewport_id: ViewportId,
172        window: Option<&winit::window::Window>,
173    ) -> Result<(), crate::WgpuError> {
174        profiling::scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions
175
176        if let Some(window) = window {
177            let size = window.inner_size();
178            if !self.surfaces.contains_key(&viewport_id) {
179                let surface = unsafe {
180                    self.instance
181                        .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(&window)?)?
182                };
183                self.add_surface(surface, viewport_id, size).await?;
184            }
185        } else {
186            log::warn!("No window - clearing all surfaces");
187            self.surfaces.clear();
188        }
189        Ok(())
190    }
191
192    async fn add_surface(
193        &mut self,
194        surface: wgpu::Surface<'static>,
195        viewport_id: ViewportId,
196        size: winit::dpi::PhysicalSize<u32>,
197    ) -> Result<(), crate::WgpuError> {
198        let render_state = if let Some(render_state) = &self.render_state {
199            render_state
200        } else {
201            let render_state = RenderState::create(
202                &self.configuration,
203                &self.instance,
204                Some(&surface),
205                self.options,
206            )
207            .await?;
208            self.render_state.get_or_insert(render_state)
209        };
210        let alpha_mode = if self.support_transparent_backbuffer {
211            let supported_alpha_modes = surface.get_capabilities(&render_state.adapter).alpha_modes;
212
213            // Prefer pre multiplied over post multiplied!
214            if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
215                wgpu::CompositeAlphaMode::PreMultiplied
216            } else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
217                wgpu::CompositeAlphaMode::PostMultiplied
218            } else {
219                log::warn!(
220                    "Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."
221                );
222                wgpu::CompositeAlphaMode::Auto
223            }
224        } else {
225            wgpu::CompositeAlphaMode::Auto
226        };
227        self.surfaces.insert(
228            viewport_id,
229            SurfaceState {
230                surface,
231                width: size.width,
232                height: size.height,
233                alpha_mode,
234                resizing: false,
235            },
236        );
237        let Some(width) = NonZeroU32::new(size.width) else {
238            log::debug!("The window width was zero; skipping generate textures");
239            return Ok(());
240        };
241        let Some(height) = NonZeroU32::new(size.height) else {
242            log::debug!("The window height was zero; skipping generate textures");
243            return Ok(());
244        };
245        self.resize_and_generate_depth_texture_view_and_msaa_view(viewport_id, width, height);
246        Ok(())
247    }
248
249    /// Returns the maximum texture dimension supported if known
250    ///
251    /// This API will only return a known dimension after `set_window()` has been called
252    /// at least once, since the underlying device and render state are initialized lazily
253    /// once we have a window (that may determine the choice of adapter/device).
254    pub fn max_texture_side(&self) -> Option<usize> {
255        self.render_state
256            .as_ref()
257            .map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
258    }
259
260    fn resize_and_generate_depth_texture_view_and_msaa_view(
261        &mut self,
262        viewport_id: ViewportId,
263        width_in_pixels: NonZeroU32,
264        height_in_pixels: NonZeroU32,
265    ) {
266        profiling::function_scope!();
267
268        let width = width_in_pixels.get();
269        let height = height_in_pixels.get();
270
271        let render_state = self.render_state.as_ref().unwrap();
272        let surface_state = self.surfaces.get_mut(&viewport_id).unwrap();
273
274        surface_state.width = width;
275        surface_state.height = height;
276
277        Self::configure_surface(surface_state, render_state, &self.configuration);
278
279        if let Some(depth_format) = self.options.depth_stencil_format {
280            self.depth_texture_view.insert(
281                viewport_id,
282                render_state
283                    .device
284                    .create_texture(&wgpu::TextureDescriptor {
285                        label: Some("egui_depth_texture"),
286                        size: wgpu::Extent3d {
287                            width,
288                            height,
289                            depth_or_array_layers: 1,
290                        },
291                        mip_level_count: 1,
292                        sample_count: self.options.msaa_samples.max(1),
293                        dimension: wgpu::TextureDimension::D2,
294                        format: depth_format,
295                        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
296                            | wgpu::TextureUsages::TEXTURE_BINDING,
297                        view_formats: &[depth_format],
298                    })
299                    .create_view(&wgpu::TextureViewDescriptor::default()),
300            );
301        }
302
303        if let Some(render_state) = (self.options.msaa_samples > 1)
304            .then_some(self.render_state.as_ref())
305            .flatten()
306        {
307            let texture_format = render_state.target_format;
308            self.msaa_texture_view.insert(
309                viewport_id,
310                render_state
311                    .device
312                    .create_texture(&wgpu::TextureDescriptor {
313                        label: Some("egui_msaa_texture"),
314                        size: wgpu::Extent3d {
315                            width,
316                            height,
317                            depth_or_array_layers: 1,
318                        },
319                        mip_level_count: 1,
320                        sample_count: self.options.msaa_samples.max(1),
321                        dimension: wgpu::TextureDimension::D2,
322                        format: texture_format,
323                        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
324                        view_formats: &[texture_format],
325                    })
326                    .create_view(&wgpu::TextureViewDescriptor::default()),
327            );
328        }
329    }
330
331    /// Handles changes of the resizing state.
332    ///
333    /// Should be called prior to the first [`Painter::on_window_resized`] call and after the last in
334    /// the chain. Used to apply platform-specific logic, e.g. OSX Metal window resize jitter fix.
335    pub fn on_window_resize_state_change(&mut self, viewport_id: ViewportId, resizing: bool) {
336        profiling::function_scope!();
337
338        let Some(state) = self.surfaces.get_mut(&viewport_id) else {
339            return;
340        };
341        if state.resizing == resizing {
342            if resizing {
343                log::debug!(
344                    "Painter::on_window_resize_state_change() redundant call while resizing"
345                );
346            } else {
347                log::debug!(
348                    "Painter::on_window_resize_state_change() redundant call after resizing"
349                );
350            }
351            return;
352        }
353
354        // Resizing is a bit tricky on macOS.
355        // It requires enabling ["present_with_transaction"](https://developer.apple.com/documentation/quartzcore/cametallayer/presentswithtransaction)
356        // flag to avoid jittering during the resize. Even though resize jittering on macOS
357        // is common across rendering backends, the solution for wgpu/metal is known.
358        //
359        // See https://github.com/emilk/egui/issues/903
360        #[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
361        {
362            // SAFETY: The cast is checked with if condition. If the used backend is not metal
363            // it gracefully fails. The pointer casts are valid as it's 1-to-1 type mapping.
364            // This is how wgpu currently exposes this backend-specific flag.
365            unsafe {
366                if let Some(hal_surface) = state.surface.as_hal::<wgpu::hal::api::Metal>() {
367                    let raw =
368                        std::ptr::from_ref::<wgpu::hal::metal::Surface>(&*hal_surface).cast_mut();
369
370                    (*raw).present_with_transaction = resizing;
371
372                    Self::configure_surface(
373                        state,
374                        self.render_state.as_ref().unwrap(),
375                        &self.configuration,
376                    );
377                }
378            }
379        }
380
381        state.resizing = resizing;
382    }
383
384    pub fn on_window_resized(
385        &mut self,
386        viewport_id: ViewportId,
387        width_in_pixels: NonZeroU32,
388        height_in_pixels: NonZeroU32,
389    ) {
390        profiling::function_scope!();
391
392        if self.surfaces.contains_key(&viewport_id) {
393            self.resize_and_generate_depth_texture_view_and_msaa_view(
394                viewport_id,
395                width_in_pixels,
396                height_in_pixels,
397            );
398        } else {
399            log::warn!(
400                "Ignoring window resize notification with no surface created via Painter::set_window()"
401            );
402        }
403    }
404
405    /// Returns two things:
406    ///
407    /// The approximate number of seconds spent on vsync-waiting (if any),
408    /// and the captures captured screenshot if it was requested.
409    ///
410    /// If `capture_data` isn't empty, a screenshot will be captured.
411    pub fn paint_and_update_textures(
412        &mut self,
413        viewport_id: ViewportId,
414        pixels_per_point: f32,
415        clear_color: [f32; 4],
416        clipped_primitives: &[epaint::ClippedPrimitive],
417        textures_delta: &epaint::textures::TexturesDelta,
418        capture_data: Vec<UserData>,
419    ) -> f32 {
420        profiling::function_scope!();
421
422        let capture = !capture_data.is_empty();
423        let mut vsync_sec = 0.0;
424
425        let Some(render_state) = self.render_state.as_mut() else {
426            return vsync_sec;
427        };
428        let Some(surface_state) = self.surfaces.get(&viewport_id) else {
429            return vsync_sec;
430        };
431
432        let mut encoder =
433            render_state
434                .device
435                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
436                    label: Some("encoder"),
437                });
438
439        // Upload all resources for the GPU.
440        let screen_descriptor = renderer::ScreenDescriptor {
441            size_in_pixels: [surface_state.width, surface_state.height],
442            pixels_per_point,
443        };
444
445        let user_cmd_bufs = {
446            let mut renderer = render_state.renderer.write();
447            for (id, image_delta) in &textures_delta.set {
448                renderer.update_texture(
449                    &render_state.device,
450                    &render_state.queue,
451                    *id,
452                    image_delta,
453                );
454            }
455
456            renderer.update_buffers(
457                &render_state.device,
458                &render_state.queue,
459                &mut encoder,
460                clipped_primitives,
461                &screen_descriptor,
462            )
463        };
464
465        let output_frame = {
466            profiling::scope!("get_current_texture");
467            // This is what vsync-waiting happens on my Mac.
468            let start = web_time::Instant::now();
469            let output_frame = surface_state.surface.get_current_texture();
470            vsync_sec += start.elapsed().as_secs_f32();
471            output_frame
472        };
473
474        let output_frame = match output_frame {
475            Ok(frame) => frame,
476            Err(err) => match (*self.configuration.on_surface_error)(err) {
477                SurfaceErrorAction::RecreateSurface => {
478                    Self::configure_surface(surface_state, render_state, &self.configuration);
479                    return vsync_sec;
480                }
481                SurfaceErrorAction::SkipFrame => {
482                    return vsync_sec;
483                }
484            },
485        };
486
487        let mut capture_buffer = None;
488        {
489            let renderer = render_state.renderer.read();
490
491            let target_texture = if capture {
492                let capture_state = self.screen_capture_state.get_or_insert_with(|| {
493                    CaptureState::new(&render_state.device, &output_frame.texture)
494                });
495                capture_state.update(&render_state.device, &output_frame.texture);
496
497                &capture_state.texture
498            } else {
499                &output_frame.texture
500            };
501            let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
502
503            let (view, resolve_target) = (self.options.msaa_samples > 1)
504                .then_some(self.msaa_texture_view.get(&viewport_id))
505                .flatten()
506                .map_or((&target_view, None), |texture_view| {
507                    (texture_view, Some(&target_view))
508                });
509
510            let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
511                label: Some("egui_render"),
512                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
513                    view,
514                    resolve_target,
515                    ops: wgpu::Operations {
516                        load: wgpu::LoadOp::Clear(wgpu::Color {
517                            r: clear_color[0] as f64,
518                            g: clear_color[1] as f64,
519                            b: clear_color[2] as f64,
520                            a: clear_color[3] as f64,
521                        }),
522                        store: wgpu::StoreOp::Store,
523                    },
524                    depth_slice: None,
525                })],
526                depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
527                    wgpu::RenderPassDepthStencilAttachment {
528                        view,
529                        depth_ops: Some(wgpu::Operations {
530                            load: wgpu::LoadOp::Clear(1.0),
531                            // It is very unlikely that the depth buffer is needed after egui finished rendering
532                            // so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon)
533                            store: wgpu::StoreOp::Discard,
534                        }),
535                        stencil_ops: None,
536                    }
537                }),
538                timestamp_writes: None,
539                occlusion_query_set: None,
540            });
541
542            // Forgetting the pass' lifetime means that we are no longer compile-time protected from
543            // runtime errors caused by accessing the parent encoder before the render pass is dropped.
544            // Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here!
545            renderer.render(
546                &mut render_pass.forget_lifetime(),
547                clipped_primitives,
548                &screen_descriptor,
549            );
550
551            if capture && let Some(capture_state) = &mut self.screen_capture_state {
552                capture_buffer = Some(capture_state.copy_textures(
553                    &render_state.device,
554                    &output_frame,
555                    &mut encoder,
556                ));
557            }
558        }
559
560        let encoded = {
561            profiling::scope!("CommandEncoder::finish");
562            encoder.finish()
563        };
564
565        // Submit the commands: both the main buffer and user-defined ones.
566        {
567            profiling::scope!("Queue::submit");
568            // wgpu doesn't document where vsync can happen. Maybe here?
569            let start = web_time::Instant::now();
570            render_state
571                .queue
572                .submit(user_cmd_bufs.into_iter().chain([encoded]));
573            vsync_sec += start.elapsed().as_secs_f32();
574        };
575
576        // Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
577        // Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
578        // However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
579        {
580            let mut renderer = render_state.renderer.write();
581            for id in &textures_delta.free {
582                renderer.free_texture(id);
583            }
584        }
585
586        if let Some(capture_buffer) = capture_buffer
587            && let Some(screen_capture_state) = &mut self.screen_capture_state
588        {
589            screen_capture_state.read_screen_rgba(
590                self.context.clone(),
591                capture_buffer,
592                capture_data,
593                self.capture_tx.clone(),
594                viewport_id,
595            );
596        }
597
598        {
599            profiling::scope!("present");
600            // wgpu doesn't document where vsync can happen. Maybe here?
601            let start = web_time::Instant::now();
602            output_frame.present();
603            vsync_sec += start.elapsed().as_secs_f32();
604        }
605
606        vsync_sec
607    }
608
609    /// Call this at the beginning of each frame to receive the requested screenshots.
610    pub fn handle_screenshots(&self, events: &mut Vec<Event>) {
611        for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() {
612            let screenshot = Arc::new(screenshot);
613            for data in user_data {
614                events.push(Event::Screenshot {
615                    viewport_id,
616                    user_data: data,
617                    image: screenshot.clone(),
618                });
619            }
620        }
621    }
622
623    pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) {
624        self.surfaces.retain(|id, _| active_viewports.contains(id));
625        self.depth_texture_view
626            .retain(|id, _| active_viewports.contains(id));
627        self.msaa_texture_view
628            .retain(|id, _| active_viewports.contains(id));
629    }
630
631    #[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
632    pub fn destroy(&mut self) {
633        // TODO(emilk): something here?
634    }
635}