Skip to main content

egui_wgpu/
winit.rs

1#![expect(clippy::missing_errors_doc)]
2#![expect(clippy::undocumented_unsafe_blocks)]
3#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
4#![expect(unsafe_code)]
5
6use crate::{RenderState, SurfaceConfig, SurfaceErrorAction, WgpuConfiguration, renderer};
7use crate::{RendererOptions, capture::CaptureState};
8use core::num::NonZeroU32;
9use egui::{Context, ViewportId, ViewportIdMap, ViewportIdSet};
10use std::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    needs_reconfigure: bool,
19    needs_recreate: bool,
20}
21
22/// Everything you need to paint egui with [`wgpu`] on [`winit`].
23///
24/// Alternatively you can use [`crate::Renderer`] directly.
25///
26/// NOTE: all egui viewports share the same painter.
27pub struct Painter {
28    context: Context,
29    config: WgpuConfiguration,
30    options: RendererOptions,
31    support_transparent_backbuffer: bool,
32    screen_capture_state: Option<CaptureState>,
33
34    instance: wgpu::Instance,
35    render_state: Option<RenderState>,
36
37    // Per viewport/window:
38    depth_texture_view: ViewportIdMap<wgpu::TextureView>,
39    msaa_texture_view: ViewportIdMap<wgpu::TextureView>,
40    surfaces: ViewportIdMap<SurfaceState>,
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        config: WgpuConfiguration,
59        support_transparent_backbuffer: bool,
60        options: RendererOptions,
61    ) -> Self {
62        let instance = config.wgpu_setup.new_instance().await;
63
64        Self {
65            context,
66            config,
67            options,
68            support_transparent_backbuffer,
69            screen_capture_state: None,
70
71            instance,
72            render_state: None,
73
74            depth_texture_view: Default::default(),
75            surfaces: Default::default(),
76            msaa_texture_view: Default::default(),
77        }
78    }
79
80    /// Get the [`RenderState`].
81    ///
82    /// Will return [`None`] if the render state has not been initialized yet.
83    pub fn render_state(&self) -> Option<RenderState> {
84        self.render_state.clone()
85    }
86
87    /// The surface configuration (present mode, frame latency) the surfaces are configured with.
88    pub fn surface_config(&self) -> SurfaceConfig {
89        self.config.surface
90    }
91
92    /// Change the surface configuration (present mode, frame latency) at runtime.
93    ///
94    /// All surfaces are reconfigured before the next paint.
95    ///
96    /// This is cheap to call every frame.
97    pub fn set_surface_config(&mut self, surface_config: SurfaceConfig) {
98        if self.config.surface == surface_config {
99            return;
100        }
101
102        self.config.surface = surface_config;
103
104        #[expect(clippy::iter_over_hash_type)]
105        for surface in self.surfaces.values_mut() {
106            surface.needs_reconfigure = true;
107        }
108    }
109
110    fn configure_surface(
111        surface_state: &SurfaceState,
112        render_state: &RenderState,
113        config: &SurfaceConfig,
114    ) {
115        profiling::function_scope!();
116
117        let SurfaceConfig {
118            present_mode,
119            desired_maximum_frame_latency,
120        } = *config;
121
122        // Transaction presentation can hold a drawable during AppKit live resize. Keep the
123        // configured low-latency path normally, but use three Metal drawables while resizing.
124        #[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
125        let desired_maximum_frame_latency = if surface_state.resizing {
126            Some(desired_maximum_frame_latency.unwrap_or(2).max(2))
127        } else {
128            desired_maximum_frame_latency
129        };
130
131        let width = surface_state.width;
132        let height = surface_state.height;
133
134        let mut surf_config = wgpu::SurfaceConfiguration {
135            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
136            format: render_state.target_format,
137            present_mode,
138            alpha_mode: surface_state.alpha_mode,
139            view_formats: vec![render_state.target_format],
140            ..surface_state
141                .surface
142                .get_default_config(&render_state.adapter, width, height)
143                .expect("The surface isn't supported by this adapter")
144        };
145
146        if let Some(desired_maximum_frame_latency) = desired_maximum_frame_latency {
147            surf_config.desired_maximum_frame_latency = desired_maximum_frame_latency;
148        }
149
150        surface_state
151            .surface
152            .configure(&render_state.device, &surf_config);
153    }
154
155    /// Drop the existing [`wgpu::Surface`] for `viewport_id` and create a fresh one for the
156    /// given window via [`wgpu::Instance::create_surface`], then configure it.
157    ///
158    /// Used to recover from [`wgpu::CurrentSurfaceTexture::Lost`], where reconfiguring the
159    /// existing surface object cannot recover.
160    fn recreate_surface(
161        &mut self,
162        viewport_id: ViewportId,
163        window: &Arc<winit::window::Window>,
164    ) -> Result<(), crate::WgpuError> {
165        profiling::function_scope!();
166
167        let Some(old_state) = self.surfaces.remove(&viewport_id) else {
168            return Ok(());
169        };
170
171        let surface = self.instance.create_surface(Arc::clone(window))?;
172        self.install_surface(
173            surface,
174            viewport_id,
175            old_state.width,
176            old_state.height,
177            old_state.resizing,
178        );
179        Ok(())
180    }
181
182    /// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`]
183    ///
184    /// This creates a [`wgpu::Surface`] for the given Window (as well as initializing render
185    /// state if needed) that is used for egui rendering.
186    ///
187    /// This must be called before trying to render via
188    /// [`paint_and_update_textures`](Self::paint_and_update_textures)
189    ///
190    /// # Portability
191    ///
192    /// _In particular it's important to note that on Android a it's only possible to create
193    /// a window surface between `Resumed` and `Paused` lifecycle events, and Winit will panic on
194    /// attempts to query the raw window handle while paused._
195    ///
196    /// On Android [`set_window`](Self::set_window) should be called with `Some(window)` for each
197    /// `Resumed` event and `None` for each `Paused` event. Currently, on all other platforms
198    /// [`set_window`](Self::set_window) may be called with `Some(window)` as soon as you have a
199    /// valid [`winit::window::Window`].
200    ///
201    /// # Errors
202    /// If the provided wgpu configuration does not match an available device.
203    pub async fn set_window(
204        &mut self,
205        viewport_id: ViewportId,
206        window: Option<Arc<winit::window::Window>>,
207    ) -> Result<(), crate::WgpuError> {
208        profiling::scope!("Painter::set_window"); // profile_function gives bad names for async functions
209
210        if let Some(window) = window {
211            let size = window.inner_size();
212            if !self.surfaces.contains_key(&viewport_id) {
213                let surface = self.instance.create_surface(window)?;
214                self.add_surface(surface, viewport_id, size).await?;
215            }
216        } else {
217            log::warn!("No window - clearing all surfaces");
218            self.surfaces.clear();
219        }
220        Ok(())
221    }
222
223    /// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`] without taking ownership of the window.
224    ///
225    /// Like [`set_window`](Self::set_window) except:
226    ///
227    /// # Safety
228    /// The user is responsible for ensuring that the window is alive for as long as it is set.
229    pub async unsafe fn set_window_unsafe(
230        &mut self,
231        viewport_id: ViewportId,
232        window: Option<&winit::window::Window>,
233    ) -> Result<(), crate::WgpuError> {
234        profiling::scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions
235
236        if let Some(window) = window {
237            let size = window.inner_size();
238            if !self.surfaces.contains_key(&viewport_id) {
239                let surface = unsafe {
240                    self.instance
241                        .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(&window)?)?
242                };
243                self.add_surface(surface, viewport_id, size).await?;
244            }
245        } else {
246            log::warn!("No window - clearing all surfaces");
247            self.surfaces.clear();
248        }
249        Ok(())
250    }
251
252    async fn add_surface(
253        &mut self,
254        surface: wgpu::Surface<'static>,
255        viewport_id: ViewportId,
256        size: winit::dpi::PhysicalSize<u32>,
257    ) -> Result<(), crate::WgpuError> {
258        if self.render_state.is_none() {
259            let render_state =
260                RenderState::create(&self.config, &self.instance, Some(&surface), self.options)
261                    .await?;
262            self.render_state = Some(render_state);
263        }
264        self.install_surface(surface, viewport_id, size.width, size.height, false);
265        Ok(())
266    }
267
268    /// Inserts a freshly created surface into [`Self::surfaces`] and configures it.
269    ///
270    /// Render state must already be initialised before calling this.
271    // NOTE: The same assumption is already required by `resize_and_generate_depth_texture_view_and_msaa_view`.
272    fn install_surface(
273        &mut self,
274        surface: wgpu::Surface<'static>,
275        viewport_id: ViewportId,
276        width: u32,
277        height: u32,
278        resizing: bool,
279    ) {
280        let alpha_mode = {
281            // Panic: We use the same failure mode as `resize_and_generate_depth_texture_view_and_msaa_view`
282            let render_state = self
283                .render_state
284                .as_ref()
285                .expect("install_surface called before render_state initialization");
286            if self.support_transparent_backbuffer {
287                let supported_alpha_modes =
288                    surface.get_capabilities(&render_state.adapter).alpha_modes;
289                // Prefer pre multiplied over post multiplied!
290                if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
291                    wgpu::CompositeAlphaMode::PreMultiplied
292                } else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
293                {
294                    wgpu::CompositeAlphaMode::PostMultiplied
295                } else {
296                    log::warn!(
297                        "Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."
298                    );
299                    wgpu::CompositeAlphaMode::Auto
300                }
301            } else {
302                wgpu::CompositeAlphaMode::Auto
303            }
304        };
305        self.surfaces.insert(
306            viewport_id,
307            SurfaceState {
308                surface,
309                width,
310                height,
311                alpha_mode,
312                resizing,
313                needs_reconfigure: false,
314                needs_recreate: false,
315            },
316        );
317        let Some(width) = NonZeroU32::new(width) else {
318            log::debug!("The window width was zero; skipping generate textures");
319            return;
320        };
321        let Some(height) = NonZeroU32::new(height) else {
322            log::debug!("The window height was zero; skipping generate textures");
323            return;
324        };
325        self.resize_and_generate_depth_texture_view_and_msaa_view(viewport_id, width, height);
326    }
327
328    /// Returns the maximum texture dimension supported if known
329    ///
330    /// This API will only return a known dimension after `set_window()` has been called
331    /// at least once, since the underlying device and render state are initialized lazily
332    /// once we have a window (that may determine the choice of adapter/device).
333    pub fn max_texture_side(&self) -> Option<usize> {
334        self.render_state
335            .as_ref()
336            .map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
337    }
338
339    fn resize_and_generate_depth_texture_view_and_msaa_view(
340        &mut self,
341        viewport_id: ViewportId,
342        width_in_pixels: NonZeroU32,
343        height_in_pixels: NonZeroU32,
344    ) {
345        profiling::function_scope!();
346
347        let width = width_in_pixels.get();
348        let height = height_in_pixels.get();
349
350        let render_state = self.render_state.as_ref().unwrap();
351        let surface_state = self.surfaces.get_mut(&viewport_id).unwrap();
352
353        surface_state.width = width;
354        surface_state.height = height;
355
356        Self::configure_surface(surface_state, render_state, &self.config.surface);
357
358        if let Some(depth_format) = self.options.depth_stencil_format {
359            self.depth_texture_view.insert(
360                viewport_id,
361                render_state
362                    .device
363                    .create_texture(&wgpu::TextureDescriptor {
364                        label: Some("egui_depth_texture"),
365                        size: wgpu::Extent3d {
366                            width,
367                            height,
368                            depth_or_array_layers: 1,
369                        },
370                        mip_level_count: 1,
371                        sample_count: self.options.msaa_samples.max(1),
372                        dimension: wgpu::TextureDimension::D2,
373                        format: depth_format,
374                        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
375                            | wgpu::TextureUsages::TEXTURE_BINDING,
376                        view_formats: &[depth_format],
377                    })
378                    .create_view(&wgpu::TextureViewDescriptor::default()),
379            );
380        }
381
382        if let Some(render_state) = (self.options.msaa_samples > 1)
383            .then_some(self.render_state.as_ref())
384            .flatten()
385        {
386            let texture_format = render_state.target_format;
387            self.msaa_texture_view.insert(
388                viewport_id,
389                render_state
390                    .device
391                    .create_texture(&wgpu::TextureDescriptor {
392                        label: Some("egui_msaa_texture"),
393                        size: wgpu::Extent3d {
394                            width,
395                            height,
396                            depth_or_array_layers: 1,
397                        },
398                        mip_level_count: 1,
399                        sample_count: self.options.msaa_samples.max(1),
400                        dimension: wgpu::TextureDimension::D2,
401                        format: texture_format,
402                        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
403                        view_formats: &[texture_format],
404                    })
405                    .create_view(&wgpu::TextureViewDescriptor::default()),
406            );
407        }
408    }
409
410    /// Handles changes of the resizing state.
411    ///
412    /// Should be called prior to the first [`Painter::on_window_resized`] call and after the last in
413    /// the chain. Used to apply platform-specific logic, e.g. OSX Metal window resize jitter fix.
414    pub fn on_window_resize_state_change(&mut self, viewport_id: ViewportId, resizing: bool) {
415        profiling::function_scope!();
416
417        let Some(state) = self.surfaces.get_mut(&viewport_id) else {
418            return;
419        };
420        if state.resizing == resizing {
421            if resizing {
422                log::debug!(
423                    "Painter::on_window_resize_state_change() redundant call while resizing"
424                );
425            } else {
426                log::debug!(
427                    "Painter::on_window_resize_state_change() redundant call after resizing"
428                );
429            }
430            return;
431        }
432
433        // Set before reconfiguring so macOS live resize uses the temporary latency bump above.
434        state.resizing = resizing;
435
436        // Resizing is a bit tricky on macOS.
437        // It requires enabling ["present_with_transaction"](https://developer.apple.com/documentation/quartzcore/cametallayer/presentswithtransaction)
438        // flag to avoid jittering during the resize. Even though resize jittering on macOS
439        // is common across rendering backends, the solution for wgpu/metal is known.
440        //
441        // See https://github.com/emilk/egui/issues/903
442        #[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
443        {
444            // SAFETY: `as_hal::<Metal>()` returns `None` unless this surface is backed by wgpu's
445            // Metal backend.
446            unsafe {
447                if let (Some(render_state), Some(hal_surface)) = (
448                    self.render_state.as_ref(),
449                    state.surface.as_hal::<wgpu::hal::api::Metal>(),
450                ) {
451                    hal_surface
452                        .render_layer()
453                        .lock()
454                        .setPresentsWithTransaction(resizing);
455
456                    Self::configure_surface(state, render_state, &self.config.surface);
457                }
458            }
459        }
460    }
461
462    pub fn on_window_resized(
463        &mut self,
464        viewport_id: ViewportId,
465        width_in_pixels: NonZeroU32,
466        height_in_pixels: NonZeroU32,
467    ) {
468        profiling::function_scope!();
469
470        if self.surfaces.contains_key(&viewport_id) {
471            self.resize_and_generate_depth_texture_view_and_msaa_view(
472                viewport_id,
473                width_in_pixels,
474                height_in_pixels,
475            );
476        } else {
477            log::warn!(
478                "Ignoring window resize notification with no surface created via Painter::set_window()"
479            );
480        }
481    }
482
483    /// Returns two things:
484    ///
485    /// The approximate number of seconds spent on vsync-waiting (if any),
486    /// and the captures captured screenshot if it was requested.
487    ///
488    /// If `capture_data` isn't empty, a screenshot will be captured.
489    #[expect(clippy::too_many_arguments)]
490    pub fn paint_and_update_textures(
491        &mut self,
492        viewport_id: ViewportId,
493        pixels_per_point: f32,
494        clear_color: [f32; 4],
495        clipped_primitives: &[epaint::ClippedPrimitive],
496        textures_delta: &mut epaint::textures::TexturesDelta,
497        capture_data: Vec<egui::ScreenshotCallback>,
498        window: &Arc<winit::window::Window>,
499    ) -> f32 {
500        profiling::function_scope!();
501
502        /// Guard to ensure that commands are always submitted to the renderer queue
503        /// so that calls to [`write_buffer()`](https://docs.rs/wgpu/latest/wgpu/struct.Queue.html#method.write_buffer)
504        /// are completed even if we take a codepath which doesn't submit commands and avoids
505        /// internal buffers growing indefinitely.
506        ///
507        /// This may happen, for example, if no output frame is resolved.
508        /// See <https://github.com/emilk/egui/pull/7928> for full context.
509        struct RendererQueueGuard<'q> {
510            queue: &'q wgpu::Queue,
511            commands_submitted: bool,
512        }
513
514        impl Drop for RendererQueueGuard<'_> {
515            fn drop(&mut self) {
516                // Only submit an empty command buffer array if no commands were
517                // explicitly submitted.
518                if !self.commands_submitted {
519                    self.queue.submit([]);
520                }
521            }
522        }
523
524        let capture = !capture_data.is_empty();
525        let mut vsync_sec = 0.0;
526
527        // If the previous frame produced `CurrentSurfaceTexture::Lost`, the action match
528        // below set `needs_recreate`. Recreate the surface now, before re-borrowing
529        // `self.render_state` / `self.surfaces` for the rest of the paint.
530        if self
531            .surfaces
532            .get(&viewport_id)
533            .is_some_and(|s| s.needs_recreate)
534            && let Err(err) = self.recreate_surface(viewport_id, window)
535        {
536            log::error!("Failed to recreate surface for {viewport_id:?}: {err}");
537            return vsync_sec;
538        }
539
540        let Some(render_state) = self.render_state.as_mut() else {
541            return vsync_sec;
542        };
543
544        let mut render_queue_guard = RendererQueueGuard {
545            queue: &render_state.queue,
546            commands_submitted: false,
547        };
548
549        let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
550            return vsync_sec;
551        };
552
553        let mut encoder =
554            render_state
555                .device
556                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
557                    label: Some("encoder"),
558                });
559
560        // Upload all resources for the GPU.
561        let screen_descriptor = renderer::ScreenDescriptor {
562            size_in_pixels: [surface_state.width, surface_state.height],
563            pixels_per_point,
564        };
565
566        let user_cmd_bufs = {
567            let mut renderer = render_state.renderer.write();
568            #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
569            for (id, image_deltas) in textures_delta.set.drain() {
570                for image_delta in image_deltas {
571                    renderer.update_texture(
572                        &render_state.device,
573                        &render_state.queue,
574                        id,
575                        &image_delta,
576                    );
577                }
578            }
579
580            renderer.update_buffers(
581                &render_state.device,
582                &render_state.queue,
583                &mut encoder,
584                clipped_primitives,
585                &screen_descriptor,
586            )
587        };
588
589        if surface_state.needs_reconfigure {
590            Self::configure_surface(surface_state, render_state, &self.config.surface);
591            surface_state.needs_reconfigure = false;
592        }
593
594        let output_frame = {
595            profiling::scope!("get_current_texture");
596            // This is what vsync-waiting happens on my Mac.
597            let start = web_time::Instant::now();
598            let output_frame = surface_state.surface.get_current_texture();
599            vsync_sec += start.elapsed().as_secs_f32();
600            output_frame
601        };
602
603        // `None` when there is no frame to present to. A capture is rendered to its own
604        // texture and copied back to the cpu, so it is served even then — that is how a hidden
605        // window, which the compositor gives no surface texture, is screenshotted at all.
606        let output_frame = match output_frame {
607            wgpu::CurrentSurfaceTexture::Success(frame) => Some(frame),
608            wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
609                surface_state.needs_reconfigure = true;
610                Some(frame)
611            }
612            other => {
613                match (*self.config.on_surface_status)(&other) {
614                    SurfaceErrorAction::Reconfigure => {
615                        Self::configure_surface(surface_state, render_state, &self.config.surface);
616                        self.context.request_repaint_of(viewport_id);
617                    }
618                    SurfaceErrorAction::RecreateSurface => {
619                        // Because of ownership, I could not find an easy way to do a full recovery here,
620                        // as that would involve dropping the old surface and creating a new one.
621                        // For now, we defer the recreation to the beginning of the next frame (which
622                        // we ensure to arrive via `request_repaint_of`). A cleaner solution would be
623                        // to untangle the ownership of `RenderState`.
624                        surface_state.needs_recreate = true;
625                        self.context.request_repaint_of(viewport_id);
626                    }
627                    SurfaceErrorAction::SkipFrame => {}
628                }
629                if !capture {
630                    return vsync_sec;
631                }
632                None
633            }
634        };
635
636        let mut capture_buffer = None;
637        {
638            let renderer = render_state.renderer.read();
639
640            let target_texture = if capture {
641                // The frame we would have presented decides the capture, when there is one.
642                // Without it we have only the surface we configured, which is the same thing
643                // as long as no resize is in flight.
644                let (size, format) = match &output_frame {
645                    Some(output_frame) => {
646                        (output_frame.texture.size(), output_frame.texture.format())
647                    }
648                    None => (
649                        wgpu::Extent3d {
650                            width: surface_state.width,
651                            height: surface_state.height,
652                            depth_or_array_layers: 1,
653                        },
654                        render_state.target_format,
655                    ),
656                };
657                if size.width == 0 || size.height == 0 {
658                    log::warn!("Cannot capture a screenshot of a zero-sized surface");
659                    return vsync_sec;
660                }
661
662                let capture_state = self
663                    .screen_capture_state
664                    .get_or_insert_with(|| CaptureState::new(&render_state.device, size, format));
665                capture_state.update(&render_state.device, size);
666
667                &capture_state.texture
668            } else if let Some(output_frame) = &output_frame {
669                &output_frame.texture
670            } else {
671                // Unreachable: without a frame to present to, and with nothing to capture,
672                // we returned above.
673                return vsync_sec;
674            };
675            let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
676
677            let (view, resolve_target) = (self.options.msaa_samples > 1)
678                .then_some(self.msaa_texture_view.get(&viewport_id))
679                .flatten()
680                .map_or((&target_view, None), |texture_view| {
681                    (texture_view, Some(&target_view))
682                });
683
684            let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
685                label: Some("egui_render"),
686                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
687                    view,
688                    resolve_target,
689                    ops: wgpu::Operations {
690                        load: wgpu::LoadOp::Clear(wgpu::Color {
691                            r: clear_color[0] as f64,
692                            g: clear_color[1] as f64,
693                            b: clear_color[2] as f64,
694                            a: clear_color[3] as f64,
695                        }),
696                        store: wgpu::StoreOp::Store,
697                    },
698                    depth_slice: None,
699                })],
700                depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
701                    wgpu::RenderPassDepthStencilAttachment {
702                        view,
703                        depth_ops: self
704                            .options
705                            .depth_stencil_format
706                            .is_some_and(|depth_stencil_format| {
707                                depth_stencil_format.has_depth_aspect()
708                            })
709                            .then_some(wgpu::Operations {
710                                load: wgpu::LoadOp::Clear(1.0),
711                                // It is very unlikely that the depth buffer is needed after egui finished rendering
712                                // so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon)
713                                store: wgpu::StoreOp::Discard,
714                            }),
715                        stencil_ops: self
716                            .options
717                            .depth_stencil_format
718                            .is_some_and(|depth_stencil_format| {
719                                depth_stencil_format.has_stencil_aspect()
720                            })
721                            .then_some(wgpu::Operations {
722                                load: wgpu::LoadOp::Clear(0),
723                                store: wgpu::StoreOp::Discard,
724                            }),
725                    }
726                }),
727                timestamp_writes: None,
728                occlusion_query_set: None,
729                multiview_mask: None,
730            });
731
732            // Forgetting the pass' lifetime means that we are no longer compile-time protected from
733            // runtime errors caused by accessing the parent encoder before the render pass is dropped.
734            // Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here!
735            renderer.render(
736                &mut render_pass.forget_lifetime(),
737                clipped_primitives,
738                &screen_descriptor,
739            );
740
741            if capture && let Some(capture_state) = &mut self.screen_capture_state {
742                capture_buffer = Some(capture_state.copy_textures(
743                    &render_state.device,
744                    output_frame.as_ref(),
745                    &mut encoder,
746                ));
747            }
748        }
749
750        let encoded = {
751            profiling::scope!("CommandEncoder::finish");
752            encoder.finish()
753        };
754
755        // Submit the commands: both the main buffer and user-defined ones.
756        {
757            profiling::scope!("Queue::submit");
758            // wgpu doesn't document where vsync can happen. Maybe here?
759            let start = web_time::Instant::now();
760            render_state
761                .queue
762                .submit(core::iter::chain(user_cmd_bufs, [encoded]));
763            vsync_sec += start.elapsed().as_secs_f32();
764        };
765
766        // Ensure that the queue guard does not do unnecessary work when dropped
767        render_queue_guard.commands_submitted = true;
768
769        // Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
770        // Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
771        // However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
772        {
773            let mut renderer = render_state.renderer.write();
774            #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
775            for id in textures_delta.free.drain() {
776                renderer.free_texture(&id);
777            }
778        }
779
780        if let Some(capture_buffer) = capture_buffer
781            && let Some(screen_capture_state) = &mut self.screen_capture_state
782        {
783            screen_capture_state.read_screen_rgba(capture_buffer, capture_data);
784        }
785
786        if let Some(output_frame) = output_frame {
787            window.pre_present_notify();
788
789            profiling::scope!("present");
790            // wgpu doesn't document where vsync can happen. Maybe here?
791            let start = web_time::Instant::now();
792            render_state.queue.present(output_frame);
793            vsync_sec += start.elapsed().as_secs_f32();
794        }
795
796        vsync_sec
797    }
798
799    pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) {
800        self.surfaces.retain(|id, _| active_viewports.contains(id));
801        self.depth_texture_view
802            .retain(|id, _| active_viewports.contains(id));
803        self.msaa_texture_view
804            .retain(|id, _| active_viewports.contains(id));
805    }
806
807    #[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
808    pub fn destroy(&mut self) {
809        // TODO(emilk): something here?
810    }
811}