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