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: &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        {
549            // Upload textures before the surface-dependent early-returns below:
550            // uploads only need the device + queue, and the atlas dirty region is
551            // already consumed, so dropping the delta would desync the font texture.
552            let mut renderer = render_state.renderer.write();
553            for (id, image_delta) in &textures_delta.set {
554                renderer.update_texture(
555                    &render_state.device,
556                    &render_state.queue,
557                    *id,
558                    image_delta,
559                );
560            }
561        }
562
563        let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
564            return vsync_sec;
565        };
566
567        let mut encoder =
568            render_state
569                .device
570                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
571                    label: Some("encoder"),
572                });
573
574        // Upload all resources for the GPU.
575        let screen_descriptor = renderer::ScreenDescriptor {
576            size_in_pixels: [surface_state.width, surface_state.height],
577            pixels_per_point,
578        };
579
580        let user_cmd_bufs = {
581            let mut renderer = render_state.renderer.write();
582            renderer.update_buffers(
583                &render_state.device,
584                &render_state.queue,
585                &mut encoder,
586                clipped_primitives,
587                &screen_descriptor,
588            )
589        };
590
591        if surface_state.needs_reconfigure {
592            Self::configure_surface(surface_state, render_state, &self.config.surface);
593            surface_state.needs_reconfigure = false;
594        }
595
596        let output_frame = {
597            profiling::scope!("get_current_texture");
598            // This is what vsync-waiting happens on my Mac.
599            let start = web_time::Instant::now();
600            let output_frame = surface_state.surface.get_current_texture();
601            vsync_sec += start.elapsed().as_secs_f32();
602            output_frame
603        };
604
605        let output_frame = match output_frame {
606            wgpu::CurrentSurfaceTexture::Success(frame) => frame,
607            wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
608                surface_state.needs_reconfigure = true;
609                frame
610            }
611            other => {
612                match (*self.config.on_surface_status)(&other) {
613                    SurfaceErrorAction::Reconfigure => {
614                        Self::configure_surface(surface_state, render_state, &self.config.surface);
615                        self.context.request_repaint_of(viewport_id);
616                    }
617                    SurfaceErrorAction::RecreateSurface => {
618                        // Because of ownership, I could not find an easy way to do a full recovery here,
619                        // as that would involve dropping the old surface and creating a new one.
620                        // For now, we defer the recreation to the beginning of the next frame (which
621                        // we ensure to arrive via `request_repaint_of`). A cleaner solution would be
622                        // to untangle the ownership of `RenderState`.
623                        surface_state.needs_recreate = true;
624                        self.context.request_repaint_of(viewport_id);
625                    }
626                    SurfaceErrorAction::SkipFrame => {}
627                }
628                return vsync_sec;
629            }
630        };
631
632        let mut capture_buffer = None;
633        {
634            let renderer = render_state.renderer.read();
635
636            let target_texture = if capture {
637                let capture_state = self.screen_capture_state.get_or_insert_with(|| {
638                    CaptureState::new(&render_state.device, &output_frame.texture)
639                });
640                capture_state.update(&render_state.device, &output_frame.texture);
641
642                &capture_state.texture
643            } else {
644                &output_frame.texture
645            };
646            let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
647
648            let (view, resolve_target) = (self.options.msaa_samples > 1)
649                .then_some(self.msaa_texture_view.get(&viewport_id))
650                .flatten()
651                .map_or((&target_view, None), |texture_view| {
652                    (texture_view, Some(&target_view))
653                });
654
655            let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
656                label: Some("egui_render"),
657                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
658                    view,
659                    resolve_target,
660                    ops: wgpu::Operations {
661                        load: wgpu::LoadOp::Clear(wgpu::Color {
662                            r: clear_color[0] as f64,
663                            g: clear_color[1] as f64,
664                            b: clear_color[2] as f64,
665                            a: clear_color[3] as f64,
666                        }),
667                        store: wgpu::StoreOp::Store,
668                    },
669                    depth_slice: None,
670                })],
671                depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
672                    wgpu::RenderPassDepthStencilAttachment {
673                        view,
674                        depth_ops: self
675                            .options
676                            .depth_stencil_format
677                            .is_some_and(|depth_stencil_format| {
678                                depth_stencil_format.has_depth_aspect()
679                            })
680                            .then_some(wgpu::Operations {
681                                load: wgpu::LoadOp::Clear(1.0),
682                                // It is very unlikely that the depth buffer is needed after egui finished rendering
683                                // so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon)
684                                store: wgpu::StoreOp::Discard,
685                            }),
686                        stencil_ops: self
687                            .options
688                            .depth_stencil_format
689                            .is_some_and(|depth_stencil_format| {
690                                depth_stencil_format.has_stencil_aspect()
691                            })
692                            .then_some(wgpu::Operations {
693                                load: wgpu::LoadOp::Clear(0),
694                                store: wgpu::StoreOp::Discard,
695                            }),
696                    }
697                }),
698                timestamp_writes: None,
699                occlusion_query_set: None,
700                multiview_mask: None,
701            });
702
703            // Forgetting the pass' lifetime means that we are no longer compile-time protected from
704            // runtime errors caused by accessing the parent encoder before the render pass is dropped.
705            // Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here!
706            renderer.render(
707                &mut render_pass.forget_lifetime(),
708                clipped_primitives,
709                &screen_descriptor,
710            );
711
712            if capture && let Some(capture_state) = &mut self.screen_capture_state {
713                capture_buffer = Some(capture_state.copy_textures(
714                    &render_state.device,
715                    &output_frame,
716                    &mut encoder,
717                ));
718            }
719        }
720
721        let encoded = {
722            profiling::scope!("CommandEncoder::finish");
723            encoder.finish()
724        };
725
726        // Submit the commands: both the main buffer and user-defined ones.
727        {
728            profiling::scope!("Queue::submit");
729            // wgpu doesn't document where vsync can happen. Maybe here?
730            let start = web_time::Instant::now();
731            render_state
732                .queue
733                .submit(std::iter::chain(user_cmd_bufs, [encoded]));
734            vsync_sec += start.elapsed().as_secs_f32();
735        };
736
737        // Ensure that the queue guard does not do unnecessary work when dropped
738        render_queue_guard.commands_submitted = true;
739
740        // Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
741        // Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
742        // However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
743        {
744            let mut renderer = render_state.renderer.write();
745            for id in &textures_delta.free {
746                renderer.free_texture(id);
747            }
748        }
749
750        if let Some(capture_buffer) = capture_buffer
751            && let Some(screen_capture_state) = &mut self.screen_capture_state
752        {
753            screen_capture_state.read_screen_rgba(
754                self.context.clone(),
755                capture_buffer,
756                capture_data,
757                self.capture_tx.clone(),
758                viewport_id,
759            );
760        }
761
762        window.pre_present_notify();
763
764        {
765            profiling::scope!("present");
766            // wgpu doesn't document where vsync can happen. Maybe here?
767            let start = web_time::Instant::now();
768            output_frame.present();
769            vsync_sec += start.elapsed().as_secs_f32();
770        }
771
772        vsync_sec
773    }
774
775    /// Call this at the beginning of each frame to receive the requested screenshots.
776    pub fn handle_screenshots(&self, events: &mut Vec<Event>) {
777        for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() {
778            let screenshot = Arc::new(screenshot);
779            for data in user_data {
780                events.push(Event::Screenshot {
781                    viewport_id,
782                    user_data: data,
783                    image: Arc::clone(&screenshot),
784                });
785            }
786        }
787    }
788
789    pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) {
790        self.surfaces.retain(|id, _| active_viewports.contains(id));
791        self.depth_texture_view
792            .retain(|id, _| active_viewports.contains(id));
793        self.msaa_texture_view
794            .retain(|id, _| active_viewports.contains(id));
795    }
796
797    #[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
798    pub fn destroy(&mut self) {
799        // TODO(emilk): something here?
800    }
801}