Resizing window is laggy
Description My basic wgpu app is quite laggy when resizing compared to other apps on macos. It's very possible I'm just doing something wrong though.
Repro steps
[dependencies]
futures = { version = "0.3.31", features = ["executor"] }
wgpu = "25.0.0"
winit = { version = "0.30.10", features = ["wayland", "x11"] }
main.rs
use std::iter;
use winit::application::ApplicationHandler;
use winit::event::{Event, WindowEvent, DeviceEvent, DeviceId};
use winit::event_loop::{EventLoop, ActiveEventLoop};
use winit::window::{Window, WindowId};
use futures::executor::block_on;
#[derive(Default)]
struct App {
window_state: Option<WindowState>,
}
struct WindowState {
surface: wgpu::Surface<'static>,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
size: winit::dpi::PhysicalSize<u32>,
device: wgpu::Device,
window: Arc<Window>,
instance: wgpu::Instance,
render_pipeline: wgpu::RenderPipeline,
}
impl WindowState {
fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
let output = self.surface.get_current_texture()?;
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_pipeline(&self.render_pipeline);
render_pass.draw(0..3, 0..1);
}
self.queue.submit(iter::once(encoder.finish()));
output.present();
Ok(())
}
}
impl ApplicationHandler for App {
// This is a common indicator that you can create a window.
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let win_attrs = Window::default_attributes();
let window = Arc::new(event_loop.create_window(win_attrs).unwrap());
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let surface = instance.create_surface(Arc::clone(&window)).unwrap();
let adapter = block_on(instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})).unwrap();
let (device, queue) = block_on(adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu::Features::empty(),
// WebGL doesn't support all of wgpu's features, so if
// we're building for the web we'll have to disable some.
required_limits: wgpu::Limits::default(),
memory_hints: Default::default(),
trace: wgpu::Trace::Off,
}
)).unwrap();
let surface_caps = surface.get_capabilities(&adapter);
// Shader code in this tutorial assumes an Srgb surface texture. Using a different
// one will result all the colors comming out darker. If you want to support non
// Srgb surfaces, you'll need to account for that when drawing to the frame.
let surface_format = surface_caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(surface_caps.formats[0]);
let size = window.inner_size();
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: surface_caps.present_modes[0],
alpha_mode: surface_caps.alpha_modes[0],
desired_maximum_frame_latency: 2,
view_formats: vec![],
};
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[],
push_constant_ranges: &[],
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent::REPLACE,
alpha: wgpu::BlendComponent::REPLACE,
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
// Setting this to anything other than Fill requires Features::POLYGON_MODE_LINE
// or Features::POLYGON_MODE_POINT
polygon_mode: wgpu::PolygonMode::Fill,
// Requires Features::DEPTH_CLIP_CONTROL
unclipped_depth: false,
// Requires Features::CONSERVATIVE_RASTERIZATION
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
// If the pipeline will be used with a multiview render pass, this
// indicates how many array layers the attachments will have.
multiview: None,
// Useful for optimizing shader compilation on Android
cache: None,
});
surface.configure(&device, &config);
window.request_redraw();
self.window_state = Some(WindowState {
surface,
queue,
config,
size,
device,
window,
instance,
render_pipeline
})
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) {
// `unwrap` is fine, the window will always be available when
// receiving a window event.
// Handle window event.
match event {
WindowEvent::CloseRequested => {
println!("The close button was pressed; stopping");
event_loop.exit();
},
_ => (),
}
let window_state = self.window_state.as_mut().unwrap();
window_state.render();
}
fn device_event(&mut self, event_loop: &ActiveEventLoop, device_id: DeviceId, event: DeviceEvent) {
// Handle window event.
}
}
fn main() {
let event_loop = EventLoop::new().unwrap();
let mut state = App::default();
let _ = event_loop.run_app(&mut state);
}
Expected vs observed behavior When resizing the window there is noticable lag, compared to other apps which feel very smooth when resizing, the example being textedit in this case.
Extra materials This video shows the lagginess: https://github.com/user-attachments/assets/1a94e20d-1f37-4c21-a521-41d2fcad23cc
Platform Intel Mac Sequoia 15.0.1
This is because of the way wgpu handles recreating surface textures. When you reconfigure, every pipeline has to be rebuilt iirc(this is a limitation of vulkan and probably some other backends), and every surface texture has to be recreated(this is probably your bottleneck since you have just one simple pipeline).
In general, you shouldn’t be resizing the surface every time the window is resized as that causes this lag. Instead, update it with a lower frequency while the window is being resized or just once when the resizing stops.
A more direct solution is to preemptively create a larger texture than you need for the window size, and using this as your render target. Then you would have to use scissors/viewports that don’t cover the entire image. I’m not sure how this would work with wgpu, but that is the advice with vulkan.
This issue is a duplicate of #3868. Could you close it?
Edit: disregard everything said here.
When you reconfigure, every pipeline has to be rebuilt iirc(this is a limitation of vulkan and probably some other backends),
Pipelines don't need to be recreated, only the surface textures. It would be worth profiling, but surface texture creation I wouldn't expect to be slow.
I think this is likely a result of the interactions with the latency of the swapchain and other specific mac things. In order to get tight with the compositor there are various mac-specific things you need to use which have various downsides.