1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 05:10:03 -04:00
Files
egui/crates/egui-wgpu/src/winit.rs
Vitaly Kravchenko a8d09eb60d Fix macOS wgpu live resize with low-latency surfaces (#8229)
## Summary

This fixes macOS live-resize behavior for the `eframe`/`egui-wgpu` path
when using the low-latency wgpu surface configuration.

The problem I was seeing is that native window resize can look visibly
below the baseline expected from a desktop GUI: stale or stretched
frames (manifesting as wobble/jitter), or severe lag while dragging a
window edge.

The fix has three parts:

- use `CAMetalLayer.presentsWithTransaction` during live resize to avoid
stale/stretched frames
- temporarily use at least `desired_maximum_frame_latency = 2` while
live resize is active, so transaction presentation does not stall when
the app normally uses `SurfaceConfig::LOW_LATENCY`
- treat macOS `WindowEvent::Moved` as part of the live-resize event
stream, since resizing from the top or left edge changes the window
origin

This PR depends on the winit-side AppKit live-resize timing fix in
[rust-windowing/winit#4588](https://github.com/rust-windowing/winit/pull/4588)

A renderer-only frame-latency change is not enough by itself. The
temporary latency bump only solves the drawable starvation caused by
combining `presentsWithTransaction` with `SurfaceConfig::LOW_LATENCY`.
It does not change when winit emits resize/redraw events, whether
redraws are delivered during AppKit's live-resize event-tracking loop,
or whether the surface size is derived from the current backing rect.

That is why the winit fix is needed first: it makes the windowing layer
report the current AppKit backing size and request redraws from the
live-resize/display callbacks. egui-wgpu still needs this PR on top
because winit does not own the wgpu `Surface` or the underlying
`CAMetalLayer` presentation policy.

In other words: winit fixes when the windowing layer reports
resize/redraw work, while this PR fixes how egui-wgpu presents
Metal-backed wgpu frames during that resize.

## Why change the existing feature?

The existing `macos-window-resize-jitter-fix` feature addresses one
symptom by enabling transaction presentation during resize, but it is
not enough for the low-latency wgpu path.

In particular, `presentsWithTransaction` and
`SurfaceConfig::LOW_LATENCY` interact poorly during AppKit live resize.
The old code avoids that by [skipping transaction presentation when
latency is
`1`](71c4ff3c33/crates/egui-wgpu/src/winit.rs (L417)),
but that means low-latency users get the resize jitter/wobble back.

This PR keeps the low-latency path normally, but temporarily bumps frame
latency only while live resize is active. That gives the resize path
enough drawable slack without changing normal interaction latency.

I removed the `macos-window-resize-jitter-fix` feature because this
seems like the behavior the macOS wgpu path should have by default, not
a separate opt-in. If keeping the feature as a no-op compatibility alias
is preferred, I can adjust the PR.

## Validation

I created a small demo app that somewhat resembles the layout of my
actual app and highlights both horizontal and vertical resize jitter:

- a borderless macOS window
- a simple toolbar
- a scrolling side list
- `SurfaceConfig::LOW_LATENCY`

The toolbar and list make stale or stretched frames easy to see during
native resize. The jitter is visible even on the traffic light buttons.

Recordings:

### Before 1: no transaction presentation, low latency

Shows jitter/wobble and stale/stretched frames during live resize.


https://github.com/user-attachments/assets/2cf4467b-e14c-4f41-8021-0b8c23f41004

### Before 2: transaction presentation with low latency

Shows the other failure mode: live resize can become severely laggy when
transaction presentation is used while keeping
`SurfaceConfig::LOW_LATENCY`.


https://github.com/user-attachments/assets/2f866790-f472-4ede-a3c0-480e8f0f041a

### After: patched egui-wgpu + patched winit, low latency

No visible wobble/jitter and no severe live-resize lag.


https://github.com/user-attachments/assets/59e46e9f-7906-4b5c-a6c7-1d09eae644cd

---------

Co-authored-by: lucasmerlin <hi@lucasmerlin.me>
2026-06-25 13:55:36 +00:00

802 lines
31 KiB
Rust

#![expect(clippy::missing_errors_doc)]
#![expect(clippy::undocumented_unsafe_blocks)]
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
#![expect(unsafe_code)]
use crate::{RenderState, SurfaceConfig, SurfaceErrorAction, WgpuConfiguration, renderer};
use crate::{
RendererOptions,
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
};
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
use std::{num::NonZeroU32, sync::Arc};
struct SurfaceState {
surface: wgpu::Surface<'static>,
alpha_mode: wgpu::CompositeAlphaMode,
width: u32,
height: u32,
resizing: bool,
needs_reconfigure: bool,
needs_recreate: bool,
}
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
///
/// Alternatively you can use [`crate::Renderer`] directly.
///
/// NOTE: all egui viewports share the same painter.
pub struct Painter {
context: Context,
config: WgpuConfiguration,
options: RendererOptions,
support_transparent_backbuffer: bool,
screen_capture_state: Option<CaptureState>,
instance: wgpu::Instance,
render_state: Option<RenderState>,
// Per viewport/window:
depth_texture_view: ViewportIdMap<wgpu::TextureView>,
msaa_texture_view: ViewportIdMap<wgpu::TextureView>,
surfaces: ViewportIdMap<SurfaceState>,
capture_tx: CaptureSender,
capture_rx: CaptureReceiver,
}
impl Painter {
/// Manages [`wgpu`] state, including surface state, required to render egui.
///
/// Only the [`wgpu::Instance`] is initialized here. Device selection and the initialization
/// of render + surface state is deferred until the painter is given its first window target
/// via [`set_window()`](Self::set_window). (Ensuring that a device that's compatible with the
/// native window is chosen)
///
/// Before calling [`paint_and_update_textures()`](Self::paint_and_update_textures) a
/// [`wgpu::Surface`] must be initialized (and corresponding render state) by calling
/// [`set_window()`](Self::set_window) once you have
/// a [`winit::window::Window`] with a valid `.raw_window_handle()`
/// associated.
pub async fn new(
context: Context,
config: WgpuConfiguration,
support_transparent_backbuffer: bool,
options: RendererOptions,
) -> Self {
let (capture_tx, capture_rx) = capture_channel();
let instance = config.wgpu_setup.new_instance().await;
Self {
context,
config,
options,
support_transparent_backbuffer,
screen_capture_state: None,
instance,
render_state: None,
depth_texture_view: Default::default(),
surfaces: Default::default(),
msaa_texture_view: Default::default(),
capture_tx,
capture_rx,
}
}
/// Get the [`RenderState`].
///
/// Will return [`None`] if the render state has not been initialized yet.
pub fn render_state(&self) -> Option<RenderState> {
self.render_state.clone()
}
fn configure_surface(
surface_state: &SurfaceState,
render_state: &RenderState,
config: &SurfaceConfig,
) {
profiling::function_scope!();
let SurfaceConfig {
present_mode,
desired_maximum_frame_latency,
} = *config;
// Transaction presentation can hold a drawable during AppKit live resize. Keep the
// configured low-latency path normally, but use three Metal drawables while resizing.
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
let desired_maximum_frame_latency = if surface_state.resizing {
Some(desired_maximum_frame_latency.unwrap_or(2).max(2))
} else {
desired_maximum_frame_latency
};
let width = surface_state.width;
let height = surface_state.height;
let mut surf_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: render_state.target_format,
present_mode,
alpha_mode: surface_state.alpha_mode,
view_formats: vec![render_state.target_format],
..surface_state
.surface
.get_default_config(&render_state.adapter, width, height)
.expect("The surface isn't supported by this adapter")
};
if let Some(desired_maximum_frame_latency) = desired_maximum_frame_latency {
surf_config.desired_maximum_frame_latency = desired_maximum_frame_latency;
}
surface_state
.surface
.configure(&render_state.device, &surf_config);
}
/// Drop the existing [`wgpu::Surface`] for `viewport_id` and create a fresh one for the
/// given window via [`wgpu::Instance::create_surface`], then configure it.
///
/// Used to recover from [`wgpu::CurrentSurfaceTexture::Lost`], where reconfiguring the
/// existing surface object cannot recover.
fn recreate_surface(
&mut self,
viewport_id: ViewportId,
window: &Arc<winit::window::Window>,
) -> Result<(), crate::WgpuError> {
profiling::function_scope!();
let Some(old_state) = self.surfaces.remove(&viewport_id) else {
return Ok(());
};
let surface = self.instance.create_surface(Arc::clone(window))?;
self.install_surface(
surface,
viewport_id,
old_state.width,
old_state.height,
old_state.resizing,
);
Ok(())
}
/// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`]
///
/// This creates a [`wgpu::Surface`] for the given Window (as well as initializing render
/// state if needed) that is used for egui rendering.
///
/// This must be called before trying to render via
/// [`paint_and_update_textures`](Self::paint_and_update_textures)
///
/// # Portability
///
/// _In particular it's important to note that on Android a it's only possible to create
/// a window surface between `Resumed` and `Paused` lifecycle events, and Winit will panic on
/// attempts to query the raw window handle while paused._
///
/// On Android [`set_window`](Self::set_window) should be called with `Some(window)` for each
/// `Resumed` event and `None` for each `Paused` event. Currently, on all other platforms
/// [`set_window`](Self::set_window) may be called with `Some(window)` as soon as you have a
/// valid [`winit::window::Window`].
///
/// # Errors
/// If the provided wgpu configuration does not match an available device.
pub async fn set_window(
&mut self,
viewport_id: ViewportId,
window: Option<Arc<winit::window::Window>>,
) -> Result<(), crate::WgpuError> {
profiling::scope!("Painter::set_window"); // profile_function gives bad names for async functions
if let Some(window) = window {
let size = window.inner_size();
if !self.surfaces.contains_key(&viewport_id) {
let surface = self.instance.create_surface(window)?;
self.add_surface(surface, viewport_id, size).await?;
}
} else {
log::warn!("No window - clearing all surfaces");
self.surfaces.clear();
}
Ok(())
}
/// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`] without taking ownership of the window.
///
/// Like [`set_window`](Self::set_window) except:
///
/// # Safety
/// The user is responsible for ensuring that the window is alive for as long as it is set.
pub async unsafe fn set_window_unsafe(
&mut self,
viewport_id: ViewportId,
window: Option<&winit::window::Window>,
) -> Result<(), crate::WgpuError> {
profiling::scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions
if let Some(window) = window {
let size = window.inner_size();
if !self.surfaces.contains_key(&viewport_id) {
let surface = unsafe {
self.instance
.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(&window)?)?
};
self.add_surface(surface, viewport_id, size).await?;
}
} else {
log::warn!("No window - clearing all surfaces");
self.surfaces.clear();
}
Ok(())
}
async fn add_surface(
&mut self,
surface: wgpu::Surface<'static>,
viewport_id: ViewportId,
size: winit::dpi::PhysicalSize<u32>,
) -> Result<(), crate::WgpuError> {
if self.render_state.is_none() {
let render_state =
RenderState::create(&self.config, &self.instance, Some(&surface), self.options)
.await?;
self.render_state = Some(render_state);
}
self.install_surface(surface, viewport_id, size.width, size.height, false);
Ok(())
}
/// Inserts a freshly created surface into [`Self::surfaces`] and configures it.
///
/// Render state must already be initialised before calling this.
// NOTE: The same assumption is already required by `resize_and_generate_depth_texture_view_and_msaa_view`.
fn install_surface(
&mut self,
surface: wgpu::Surface<'static>,
viewport_id: ViewportId,
width: u32,
height: u32,
resizing: bool,
) {
let alpha_mode = {
// Panic: We use the same failure mode as `resize_and_generate_depth_texture_view_and_msaa_view`
let render_state = self
.render_state
.as_ref()
.expect("install_surface called before render_state initialization");
if self.support_transparent_backbuffer {
let supported_alpha_modes =
surface.get_capabilities(&render_state.adapter).alpha_modes;
// Prefer pre multiplied over post multiplied!
if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
wgpu::CompositeAlphaMode::PreMultiplied
} else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
{
wgpu::CompositeAlphaMode::PostMultiplied
} else {
log::warn!(
"Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."
);
wgpu::CompositeAlphaMode::Auto
}
} else {
wgpu::CompositeAlphaMode::Auto
}
};
self.surfaces.insert(
viewport_id,
SurfaceState {
surface,
width,
height,
alpha_mode,
resizing,
needs_reconfigure: false,
needs_recreate: false,
},
);
let Some(width) = NonZeroU32::new(width) else {
log::debug!("The window width was zero; skipping generate textures");
return;
};
let Some(height) = NonZeroU32::new(height) else {
log::debug!("The window height was zero; skipping generate textures");
return;
};
self.resize_and_generate_depth_texture_view_and_msaa_view(viewport_id, width, height);
}
/// Returns the maximum texture dimension supported if known
///
/// This API will only return a known dimension after `set_window()` has been called
/// at least once, since the underlying device and render state are initialized lazily
/// once we have a window (that may determine the choice of adapter/device).
pub fn max_texture_side(&self) -> Option<usize> {
self.render_state
.as_ref()
.map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
}
fn resize_and_generate_depth_texture_view_and_msaa_view(
&mut self,
viewport_id: ViewportId,
width_in_pixels: NonZeroU32,
height_in_pixels: NonZeroU32,
) {
profiling::function_scope!();
let width = width_in_pixels.get();
let height = height_in_pixels.get();
let render_state = self.render_state.as_ref().unwrap();
let surface_state = self.surfaces.get_mut(&viewport_id).unwrap();
surface_state.width = width;
surface_state.height = height;
Self::configure_surface(surface_state, render_state, &self.config.surface);
if let Some(depth_format) = self.options.depth_stencil_format {
self.depth_texture_view.insert(
viewport_id,
render_state
.device
.create_texture(&wgpu::TextureDescriptor {
label: Some("egui_depth_texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: self.options.msaa_samples.max(1),
dimension: wgpu::TextureDimension::D2,
format: depth_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[depth_format],
})
.create_view(&wgpu::TextureViewDescriptor::default()),
);
}
if let Some(render_state) = (self.options.msaa_samples > 1)
.then_some(self.render_state.as_ref())
.flatten()
{
let texture_format = render_state.target_format;
self.msaa_texture_view.insert(
viewport_id,
render_state
.device
.create_texture(&wgpu::TextureDescriptor {
label: Some("egui_msaa_texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: self.options.msaa_samples.max(1),
dimension: wgpu::TextureDimension::D2,
format: texture_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[texture_format],
})
.create_view(&wgpu::TextureViewDescriptor::default()),
);
}
}
/// Handles changes of the resizing state.
///
/// Should be called prior to the first [`Painter::on_window_resized`] call and after the last in
/// the chain. Used to apply platform-specific logic, e.g. OSX Metal window resize jitter fix.
pub fn on_window_resize_state_change(&mut self, viewport_id: ViewportId, resizing: bool) {
profiling::function_scope!();
let Some(state) = self.surfaces.get_mut(&viewport_id) else {
return;
};
if state.resizing == resizing {
if resizing {
log::debug!(
"Painter::on_window_resize_state_change() redundant call while resizing"
);
} else {
log::debug!(
"Painter::on_window_resize_state_change() redundant call after resizing"
);
}
return;
}
// Set before reconfiguring so macOS live resize uses the temporary latency bump above.
state.resizing = resizing;
// Resizing is a bit tricky on macOS.
// It requires enabling ["present_with_transaction"](https://developer.apple.com/documentation/quartzcore/cametallayer/presentswithtransaction)
// flag to avoid jittering during the resize. Even though resize jittering on macOS
// is common across rendering backends, the solution for wgpu/metal is known.
//
// See https://github.com/emilk/egui/issues/903
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
{
// SAFETY: `as_hal::<Metal>()` returns `None` unless this surface is backed by wgpu's
// Metal backend.
unsafe {
if let (Some(render_state), Some(hal_surface)) = (
self.render_state.as_ref(),
state.surface.as_hal::<wgpu::hal::api::Metal>(),
) {
hal_surface
.render_layer()
.lock()
.setPresentsWithTransaction(resizing);
Self::configure_surface(state, render_state, &self.config.surface);
}
}
}
}
pub fn on_window_resized(
&mut self,
viewport_id: ViewportId,
width_in_pixels: NonZeroU32,
height_in_pixels: NonZeroU32,
) {
profiling::function_scope!();
if self.surfaces.contains_key(&viewport_id) {
self.resize_and_generate_depth_texture_view_and_msaa_view(
viewport_id,
width_in_pixels,
height_in_pixels,
);
} else {
log::warn!(
"Ignoring window resize notification with no surface created via Painter::set_window()"
);
}
}
/// Returns two things:
///
/// The approximate number of seconds spent on vsync-waiting (if any),
/// and the captures captured screenshot if it was requested.
///
/// If `capture_data` isn't empty, a screenshot will be captured.
#[expect(clippy::too_many_arguments)]
pub fn paint_and_update_textures(
&mut self,
viewport_id: ViewportId,
pixels_per_point: f32,
clear_color: [f32; 4],
clipped_primitives: &[epaint::ClippedPrimitive],
textures_delta: &epaint::textures::TexturesDelta,
capture_data: Vec<UserData>,
window: &Arc<winit::window::Window>,
) -> f32 {
profiling::function_scope!();
/// Guard to ensure that commands are always submitted to the renderer queue
/// so that calls to [`write_buffer()`](https://docs.rs/wgpu/latest/wgpu/struct.Queue.html#method.write_buffer)
/// are completed even if we take a codepath which doesn't submit commands and avoids
/// internal buffers growing indefinitely.
///
/// This may happen, for example, if no output frame is resolved.
/// See <https://github.com/emilk/egui/pull/7928> for full context.
struct RendererQueueGuard<'q> {
queue: &'q wgpu::Queue,
commands_submitted: bool,
}
impl Drop for RendererQueueGuard<'_> {
fn drop(&mut self) {
// Only submit an empty command buffer array if no commands were
// explicitly submitted.
if !self.commands_submitted {
self.queue.submit([]);
}
}
}
let capture = !capture_data.is_empty();
let mut vsync_sec = 0.0;
// If the previous frame produced `CurrentSurfaceTexture::Lost`, the action match
// below set `needs_recreate`. Recreate the surface now, before re-borrowing
// `self.render_state` / `self.surfaces` for the rest of the paint.
if self
.surfaces
.get(&viewport_id)
.is_some_and(|s| s.needs_recreate)
&& let Err(err) = self.recreate_surface(viewport_id, window)
{
log::error!("Failed to recreate surface for {viewport_id:?}: {err}");
return vsync_sec;
}
// Apply any runtime changes requested via `RenderState::surface_config`.
// We diff against the already-applied values in `self.config.surface`
// and, if anything differs, mark every surface as needing reconfiguration so
// the existing `needs_reconfigure` pathway below picks them up.
if let Some(render_state) = self.render_state.as_ref()
&& render_state.surface_config != self.config.surface
{
self.config.surface = render_state.surface_config;
#[expect(clippy::iter_over_hash_type)]
for surface in self.surfaces.values_mut() {
surface.needs_reconfigure = true;
}
}
let Some(render_state) = self.render_state.as_mut() else {
return vsync_sec;
};
let mut render_queue_guard = RendererQueueGuard {
queue: &render_state.queue,
commands_submitted: false,
};
{
// Upload textures before the surface-dependent early-returns below:
// uploads only need the device + queue, and the atlas dirty region is
// already consumed, so dropping the delta would desync the font texture.
let mut renderer = render_state.renderer.write();
for (id, image_delta) in &textures_delta.set {
renderer.update_texture(
&render_state.device,
&render_state.queue,
*id,
image_delta,
);
}
}
let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
return vsync_sec;
};
let mut encoder =
render_state
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("encoder"),
});
// Upload all resources for the GPU.
let screen_descriptor = renderer::ScreenDescriptor {
size_in_pixels: [surface_state.width, surface_state.height],
pixels_per_point,
};
let user_cmd_bufs = {
let mut renderer = render_state.renderer.write();
renderer.update_buffers(
&render_state.device,
&render_state.queue,
&mut encoder,
clipped_primitives,
&screen_descriptor,
)
};
if surface_state.needs_reconfigure {
Self::configure_surface(surface_state, render_state, &self.config.surface);
surface_state.needs_reconfigure = false;
}
let output_frame = {
profiling::scope!("get_current_texture");
// This is what vsync-waiting happens on my Mac.
let start = web_time::Instant::now();
let output_frame = surface_state.surface.get_current_texture();
vsync_sec += start.elapsed().as_secs_f32();
output_frame
};
let output_frame = match output_frame {
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
surface_state.needs_reconfigure = true;
frame
}
other => {
match (*self.config.on_surface_status)(&other) {
SurfaceErrorAction::Reconfigure => {
Self::configure_surface(surface_state, render_state, &self.config.surface);
self.context.request_repaint_of(viewport_id);
}
SurfaceErrorAction::RecreateSurface => {
// Because of ownership, I could not find an easy way to do a full recovery here,
// as that would involve dropping the old surface and creating a new one.
// For now, we defer the recreation to the beginning of the next frame (which
// we ensure to arrive via `request_repaint_of`). A cleaner solution would be
// to untangle the ownership of `RenderState`.
surface_state.needs_recreate = true;
self.context.request_repaint_of(viewport_id);
}
SurfaceErrorAction::SkipFrame => {}
}
return vsync_sec;
}
};
let mut capture_buffer = None;
{
let renderer = render_state.renderer.read();
let target_texture = if capture {
let capture_state = self.screen_capture_state.get_or_insert_with(|| {
CaptureState::new(&render_state.device, &output_frame.texture)
});
capture_state.update(&render_state.device, &output_frame.texture);
&capture_state.texture
} else {
&output_frame.texture
};
let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
let (view, resolve_target) = (self.options.msaa_samples > 1)
.then_some(self.msaa_texture_view.get(&viewport_id))
.flatten()
.map_or((&target_view, None), |texture_view| {
(texture_view, Some(&target_view))
});
let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("egui_render"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: clear_color[0] as f64,
g: clear_color[1] as f64,
b: clear_color[2] as f64,
a: clear_color[3] as f64,
}),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: self
.options
.depth_stencil_format
.is_some_and(|depth_stencil_format| {
depth_stencil_format.has_depth_aspect()
})
.then_some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
// It is very unlikely that the depth buffer is needed after egui finished rendering
// so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon)
store: wgpu::StoreOp::Discard,
}),
stencil_ops: self
.options
.depth_stencil_format
.is_some_and(|depth_stencil_format| {
depth_stencil_format.has_stencil_aspect()
})
.then_some(wgpu::Operations {
load: wgpu::LoadOp::Clear(0),
store: wgpu::StoreOp::Discard,
}),
}
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
// Forgetting the pass' lifetime means that we are no longer compile-time protected from
// runtime errors caused by accessing the parent encoder before the render pass is dropped.
// Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here!
renderer.render(
&mut render_pass.forget_lifetime(),
clipped_primitives,
&screen_descriptor,
);
if capture && let Some(capture_state) = &mut self.screen_capture_state {
capture_buffer = Some(capture_state.copy_textures(
&render_state.device,
&output_frame,
&mut encoder,
));
}
}
let encoded = {
profiling::scope!("CommandEncoder::finish");
encoder.finish()
};
// Submit the commands: both the main buffer and user-defined ones.
{
profiling::scope!("Queue::submit");
// wgpu doesn't document where vsync can happen. Maybe here?
let start = web_time::Instant::now();
render_state
.queue
.submit(std::iter::chain(user_cmd_bufs, [encoded]));
vsync_sec += start.elapsed().as_secs_f32();
};
// Ensure that the queue guard does not do unnecessary work when dropped
render_queue_guard.commands_submitted = true;
// Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
// Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
}
}
if let Some(capture_buffer) = capture_buffer
&& let Some(screen_capture_state) = &mut self.screen_capture_state
{
screen_capture_state.read_screen_rgba(
self.context.clone(),
capture_buffer,
capture_data,
self.capture_tx.clone(),
viewport_id,
);
}
window.pre_present_notify();
{
profiling::scope!("present");
// wgpu doesn't document where vsync can happen. Maybe here?
let start = web_time::Instant::now();
output_frame.present();
vsync_sec += start.elapsed().as_secs_f32();
}
vsync_sec
}
/// Call this at the beginning of each frame to receive the requested screenshots.
pub fn handle_screenshots(&self, events: &mut Vec<Event>) {
for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() {
let screenshot = Arc::new(screenshot);
for data in user_data {
events.push(Event::Screenshot {
viewport_id,
user_data: data,
image: Arc::clone(&screenshot),
});
}
}
}
pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) {
self.surfaces.retain(|id, _| active_viewports.contains(id));
self.depth_texture_view
.retain(|id, _| active_viewports.contains(id));
self.msaa_texture_view
.retain(|id, _| active_viewports.contains(id));
}
#[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
pub fn destroy(&mut self) {
// TODO(emilk): something here?
}
}