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