1#![expect(clippy::missing_errors_doc)]
2#![expect(clippy::undocumented_unsafe_blocks)]
3#![expect(clippy::unwrap_used)] #![expect(unsafe_code)]
5
6use crate::{RenderState, SurfaceConfig, SurfaceErrorAction, WgpuConfiguration, renderer};
7use crate::{RendererOptions, capture::CaptureState};
8use core::num::NonZeroU32;
9use egui::{Context, ViewportId, ViewportIdMap, ViewportIdSet};
10use std::sync::Arc;
11
12struct SurfaceState {
13 surface: wgpu::Surface<'static>,
14 alpha_mode: wgpu::CompositeAlphaMode,
15 width: u32,
16 height: u32,
17 resizing: bool,
18 needs_reconfigure: bool,
19 needs_recreate: bool,
20}
21
22pub struct Painter {
28 context: Context,
29 config: WgpuConfiguration,
30 options: RendererOptions,
31 support_transparent_backbuffer: bool,
32 screen_capture_state: Option<CaptureState>,
33
34 instance: wgpu::Instance,
35 render_state: Option<RenderState>,
36
37 depth_texture_view: ViewportIdMap<wgpu::TextureView>,
39 msaa_texture_view: ViewportIdMap<wgpu::TextureView>,
40 surfaces: ViewportIdMap<SurfaceState>,
41}
42
43impl Painter {
44 pub async fn new(
57 context: Context,
58 config: WgpuConfiguration,
59 support_transparent_backbuffer: bool,
60 options: RendererOptions,
61 ) -> Self {
62 let instance = config.wgpu_setup.new_instance().await;
63
64 Self {
65 context,
66 config,
67 options,
68 support_transparent_backbuffer,
69 screen_capture_state: None,
70
71 instance,
72 render_state: None,
73
74 depth_texture_view: Default::default(),
75 surfaces: Default::default(),
76 msaa_texture_view: Default::default(),
77 }
78 }
79
80 pub fn render_state(&self) -> Option<RenderState> {
84 self.render_state.clone()
85 }
86
87 pub fn surface_config(&self) -> SurfaceConfig {
89 self.config.surface
90 }
91
92 pub fn set_surface_config(&mut self, surface_config: SurfaceConfig) {
98 if self.config.surface == surface_config {
99 return;
100 }
101
102 self.config.surface = surface_config;
103
104 #[expect(clippy::iter_over_hash_type)]
105 for surface in self.surfaces.values_mut() {
106 surface.needs_reconfigure = true;
107 }
108 }
109
110 fn configure_surface(
111 surface_state: &SurfaceState,
112 render_state: &RenderState,
113 config: &SurfaceConfig,
114 ) {
115 profiling::function_scope!();
116
117 let SurfaceConfig {
118 present_mode,
119 desired_maximum_frame_latency,
120 } = *config;
121
122 #[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
125 let desired_maximum_frame_latency = if surface_state.resizing {
126 Some(desired_maximum_frame_latency.unwrap_or(2).max(2))
127 } else {
128 desired_maximum_frame_latency
129 };
130
131 let width = surface_state.width;
132 let height = surface_state.height;
133
134 let mut surf_config = wgpu::SurfaceConfiguration {
135 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
136 format: render_state.target_format,
137 present_mode,
138 alpha_mode: surface_state.alpha_mode,
139 view_formats: vec![render_state.target_format],
140 ..surface_state
141 .surface
142 .get_default_config(&render_state.adapter, width, height)
143 .expect("The surface isn't supported by this adapter")
144 };
145
146 if let Some(desired_maximum_frame_latency) = desired_maximum_frame_latency {
147 surf_config.desired_maximum_frame_latency = desired_maximum_frame_latency;
148 }
149
150 surface_state
151 .surface
152 .configure(&render_state.device, &surf_config);
153 }
154
155 fn recreate_surface(
161 &mut self,
162 viewport_id: ViewportId,
163 window: &Arc<winit::window::Window>,
164 ) -> Result<(), crate::WgpuError> {
165 profiling::function_scope!();
166
167 let Some(old_state) = self.surfaces.remove(&viewport_id) else {
168 return Ok(());
169 };
170
171 let surface = self.instance.create_surface(Arc::clone(window))?;
172 self.install_surface(
173 surface,
174 viewport_id,
175 old_state.width,
176 old_state.height,
177 old_state.resizing,
178 );
179 Ok(())
180 }
181
182 pub async fn set_window(
204 &mut self,
205 viewport_id: ViewportId,
206 window: Option<Arc<winit::window::Window>>,
207 ) -> Result<(), crate::WgpuError> {
208 profiling::scope!("Painter::set_window"); if let Some(window) = window {
211 let size = window.inner_size();
212 if !self.surfaces.contains_key(&viewport_id) {
213 let surface = self.instance.create_surface(window)?;
214 self.add_surface(surface, viewport_id, size).await?;
215 }
216 } else {
217 log::warn!("No window - clearing all surfaces");
218 self.surfaces.clear();
219 }
220 Ok(())
221 }
222
223 pub async unsafe fn set_window_unsafe(
230 &mut self,
231 viewport_id: ViewportId,
232 window: Option<&winit::window::Window>,
233 ) -> Result<(), crate::WgpuError> {
234 profiling::scope!("Painter::set_window_unsafe"); if let Some(window) = window {
237 let size = window.inner_size();
238 if !self.surfaces.contains_key(&viewport_id) {
239 let surface = unsafe {
240 self.instance
241 .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(&window)?)?
242 };
243 self.add_surface(surface, viewport_id, size).await?;
244 }
245 } else {
246 log::warn!("No window - clearing all surfaces");
247 self.surfaces.clear();
248 }
249 Ok(())
250 }
251
252 async fn add_surface(
253 &mut self,
254 surface: wgpu::Surface<'static>,
255 viewport_id: ViewportId,
256 size: winit::dpi::PhysicalSize<u32>,
257 ) -> Result<(), crate::WgpuError> {
258 if self.render_state.is_none() {
259 let render_state =
260 RenderState::create(&self.config, &self.instance, Some(&surface), self.options)
261 .await?;
262 self.render_state = Some(render_state);
263 }
264 self.install_surface(surface, viewport_id, size.width, size.height, false);
265 Ok(())
266 }
267
268 fn install_surface(
273 &mut self,
274 surface: wgpu::Surface<'static>,
275 viewport_id: ViewportId,
276 width: u32,
277 height: u32,
278 resizing: bool,
279 ) {
280 let alpha_mode = {
281 let render_state = self
283 .render_state
284 .as_ref()
285 .expect("install_surface called before render_state initialization");
286 if self.support_transparent_backbuffer {
287 let supported_alpha_modes =
288 surface.get_capabilities(&render_state.adapter).alpha_modes;
289 if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
291 wgpu::CompositeAlphaMode::PreMultiplied
292 } else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
293 {
294 wgpu::CompositeAlphaMode::PostMultiplied
295 } else {
296 log::warn!(
297 "Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."
298 );
299 wgpu::CompositeAlphaMode::Auto
300 }
301 } else {
302 wgpu::CompositeAlphaMode::Auto
303 }
304 };
305 self.surfaces.insert(
306 viewport_id,
307 SurfaceState {
308 surface,
309 width,
310 height,
311 alpha_mode,
312 resizing,
313 needs_reconfigure: false,
314 needs_recreate: false,
315 },
316 );
317 let Some(width) = NonZeroU32::new(width) else {
318 log::debug!("The window width was zero; skipping generate textures");
319 return;
320 };
321 let Some(height) = NonZeroU32::new(height) else {
322 log::debug!("The window height was zero; skipping generate textures");
323 return;
324 };
325 self.resize_and_generate_depth_texture_view_and_msaa_view(viewport_id, width, height);
326 }
327
328 pub fn max_texture_side(&self) -> Option<usize> {
334 self.render_state
335 .as_ref()
336 .map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
337 }
338
339 fn resize_and_generate_depth_texture_view_and_msaa_view(
340 &mut self,
341 viewport_id: ViewportId,
342 width_in_pixels: NonZeroU32,
343 height_in_pixels: NonZeroU32,
344 ) {
345 profiling::function_scope!();
346
347 let width = width_in_pixels.get();
348 let height = height_in_pixels.get();
349
350 let render_state = self.render_state.as_ref().unwrap();
351 let surface_state = self.surfaces.get_mut(&viewport_id).unwrap();
352
353 surface_state.width = width;
354 surface_state.height = height;
355
356 Self::configure_surface(surface_state, render_state, &self.config.surface);
357
358 if let Some(depth_format) = self.options.depth_stencil_format {
359 self.depth_texture_view.insert(
360 viewport_id,
361 render_state
362 .device
363 .create_texture(&wgpu::TextureDescriptor {
364 label: Some("egui_depth_texture"),
365 size: wgpu::Extent3d {
366 width,
367 height,
368 depth_or_array_layers: 1,
369 },
370 mip_level_count: 1,
371 sample_count: self.options.msaa_samples.max(1),
372 dimension: wgpu::TextureDimension::D2,
373 format: depth_format,
374 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
375 | wgpu::TextureUsages::TEXTURE_BINDING,
376 view_formats: &[depth_format],
377 })
378 .create_view(&wgpu::TextureViewDescriptor::default()),
379 );
380 }
381
382 if let Some(render_state) = (self.options.msaa_samples > 1)
383 .then_some(self.render_state.as_ref())
384 .flatten()
385 {
386 let texture_format = render_state.target_format;
387 self.msaa_texture_view.insert(
388 viewport_id,
389 render_state
390 .device
391 .create_texture(&wgpu::TextureDescriptor {
392 label: Some("egui_msaa_texture"),
393 size: wgpu::Extent3d {
394 width,
395 height,
396 depth_or_array_layers: 1,
397 },
398 mip_level_count: 1,
399 sample_count: self.options.msaa_samples.max(1),
400 dimension: wgpu::TextureDimension::D2,
401 format: texture_format,
402 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
403 view_formats: &[texture_format],
404 })
405 .create_view(&wgpu::TextureViewDescriptor::default()),
406 );
407 }
408 }
409
410 pub fn on_window_resize_state_change(&mut self, viewport_id: ViewportId, resizing: bool) {
415 profiling::function_scope!();
416
417 let Some(state) = self.surfaces.get_mut(&viewport_id) else {
418 return;
419 };
420 if state.resizing == resizing {
421 if resizing {
422 log::debug!(
423 "Painter::on_window_resize_state_change() redundant call while resizing"
424 );
425 } else {
426 log::debug!(
427 "Painter::on_window_resize_state_change() redundant call after resizing"
428 );
429 }
430 return;
431 }
432
433 state.resizing = resizing;
435
436 #[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
443 {
444 unsafe {
447 if let (Some(render_state), Some(hal_surface)) = (
448 self.render_state.as_ref(),
449 state.surface.as_hal::<wgpu::hal::api::Metal>(),
450 ) {
451 hal_surface
452 .render_layer()
453 .lock()
454 .setPresentsWithTransaction(resizing);
455
456 Self::configure_surface(state, render_state, &self.config.surface);
457 }
458 }
459 }
460 }
461
462 pub fn on_window_resized(
463 &mut self,
464 viewport_id: ViewportId,
465 width_in_pixels: NonZeroU32,
466 height_in_pixels: NonZeroU32,
467 ) {
468 profiling::function_scope!();
469
470 if self.surfaces.contains_key(&viewport_id) {
471 self.resize_and_generate_depth_texture_view_and_msaa_view(
472 viewport_id,
473 width_in_pixels,
474 height_in_pixels,
475 );
476 } else {
477 log::warn!(
478 "Ignoring window resize notification with no surface created via Painter::set_window()"
479 );
480 }
481 }
482
483 #[expect(clippy::too_many_arguments)]
490 pub fn paint_and_update_textures(
491 &mut self,
492 viewport_id: ViewportId,
493 pixels_per_point: f32,
494 clear_color: [f32; 4],
495 clipped_primitives: &[epaint::ClippedPrimitive],
496 textures_delta: &mut epaint::textures::TexturesDelta,
497 capture_data: Vec<egui::ScreenshotCallback>,
498 window: &Arc<winit::window::Window>,
499 ) -> f32 {
500 profiling::function_scope!();
501
502 struct RendererQueueGuard<'q> {
510 queue: &'q wgpu::Queue,
511 commands_submitted: bool,
512 }
513
514 impl Drop for RendererQueueGuard<'_> {
515 fn drop(&mut self) {
516 if !self.commands_submitted {
519 self.queue.submit([]);
520 }
521 }
522 }
523
524 let capture = !capture_data.is_empty();
525 let mut vsync_sec = 0.0;
526
527 if self
531 .surfaces
532 .get(&viewport_id)
533 .is_some_and(|s| s.needs_recreate)
534 && let Err(err) = self.recreate_surface(viewport_id, window)
535 {
536 log::error!("Failed to recreate surface for {viewport_id:?}: {err}");
537 return vsync_sec;
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 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)] 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 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 {
607 wgpu::CurrentSurfaceTexture::Success(frame) => Some(frame),
608 wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
609 surface_state.needs_reconfigure = true;
610 Some(frame)
611 }
612 other => {
613 match (*self.config.on_surface_status)(&other) {
614 SurfaceErrorAction::Reconfigure => {
615 Self::configure_surface(surface_state, render_state, &self.config.surface);
616 self.context.request_repaint_of(viewport_id);
617 }
618 SurfaceErrorAction::RecreateSurface => {
619 surface_state.needs_recreate = true;
625 self.context.request_repaint_of(viewport_id);
626 }
627 SurfaceErrorAction::SkipFrame => {}
628 }
629 if !capture {
630 return vsync_sec;
631 }
632 None
633 }
634 };
635
636 let mut capture_buffer = None;
637 {
638 let renderer = render_state.renderer.read();
639
640 let target_texture = if capture {
641 let (size, format) = match &output_frame {
645 Some(output_frame) => {
646 (output_frame.texture.size(), output_frame.texture.format())
647 }
648 None => (
649 wgpu::Extent3d {
650 width: surface_state.width,
651 height: surface_state.height,
652 depth_or_array_layers: 1,
653 },
654 render_state.target_format,
655 ),
656 };
657 if size.width == 0 || size.height == 0 {
658 log::warn!("Cannot capture a screenshot of a zero-sized surface");
659 return vsync_sec;
660 }
661
662 let capture_state = self
663 .screen_capture_state
664 .get_or_insert_with(|| CaptureState::new(&render_state.device, size, format));
665 capture_state.update(&render_state.device, size);
666
667 &capture_state.texture
668 } else if let Some(output_frame) = &output_frame {
669 &output_frame.texture
670 } else {
671 return vsync_sec;
674 };
675 let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
676
677 let (view, resolve_target) = (self.options.msaa_samples > 1)
678 .then_some(self.msaa_texture_view.get(&viewport_id))
679 .flatten()
680 .map_or((&target_view, None), |texture_view| {
681 (texture_view, Some(&target_view))
682 });
683
684 let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
685 label: Some("egui_render"),
686 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
687 view,
688 resolve_target,
689 ops: wgpu::Operations {
690 load: wgpu::LoadOp::Clear(wgpu::Color {
691 r: clear_color[0] as f64,
692 g: clear_color[1] as f64,
693 b: clear_color[2] as f64,
694 a: clear_color[3] as f64,
695 }),
696 store: wgpu::StoreOp::Store,
697 },
698 depth_slice: None,
699 })],
700 depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
701 wgpu::RenderPassDepthStencilAttachment {
702 view,
703 depth_ops: self
704 .options
705 .depth_stencil_format
706 .is_some_and(|depth_stencil_format| {
707 depth_stencil_format.has_depth_aspect()
708 })
709 .then_some(wgpu::Operations {
710 load: wgpu::LoadOp::Clear(1.0),
711 store: wgpu::StoreOp::Discard,
714 }),
715 stencil_ops: self
716 .options
717 .depth_stencil_format
718 .is_some_and(|depth_stencil_format| {
719 depth_stencil_format.has_stencil_aspect()
720 })
721 .then_some(wgpu::Operations {
722 load: wgpu::LoadOp::Clear(0),
723 store: wgpu::StoreOp::Discard,
724 }),
725 }
726 }),
727 timestamp_writes: None,
728 occlusion_query_set: None,
729 multiview_mask: None,
730 });
731
732 renderer.render(
736 &mut render_pass.forget_lifetime(),
737 clipped_primitives,
738 &screen_descriptor,
739 );
740
741 if capture && let Some(capture_state) = &mut self.screen_capture_state {
742 capture_buffer = Some(capture_state.copy_textures(
743 &render_state.device,
744 output_frame.as_ref(),
745 &mut encoder,
746 ));
747 }
748 }
749
750 let encoded = {
751 profiling::scope!("CommandEncoder::finish");
752 encoder.finish()
753 };
754
755 {
757 profiling::scope!("Queue::submit");
758 let start = web_time::Instant::now();
760 render_state
761 .queue
762 .submit(core::iter::chain(user_cmd_bufs, [encoded]));
763 vsync_sec += start.elapsed().as_secs_f32();
764 };
765
766 render_queue_guard.commands_submitted = true;
768
769 {
773 let mut renderer = render_state.renderer.write();
774 #[expect(clippy::iter_over_hash_type)] for id in textures_delta.free.drain() {
776 renderer.free_texture(&id);
777 }
778 }
779
780 if let Some(capture_buffer) = capture_buffer
781 && let Some(screen_capture_state) = &mut self.screen_capture_state
782 {
783 screen_capture_state.read_screen_rgba(capture_buffer, capture_data);
784 }
785
786 if let Some(output_frame) = output_frame {
787 window.pre_present_notify();
788
789 profiling::scope!("present");
790 let start = web_time::Instant::now();
792 render_state.queue.present(output_frame);
793 vsync_sec += start.elapsed().as_secs_f32();
794 }
795
796 vsync_sec
797 }
798
799 pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) {
800 self.surfaces.retain(|id, _| active_viewports.contains(id));
801 self.depth_texture_view
802 .retain(|id, _| active_viewports.contains(id));
803 self.msaa_texture_view
804 .retain(|id, _| active_viewports.contains(id));
805 }
806
807 #[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
808 pub fn destroy(&mut self) {
809 }
811}