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