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