wgpu icon indicating copy to clipboard operation
wgpu copied to clipboard

Resizing window is laggy

Open finnnnnnnnnnnnnnnnn opened this issue 7 months ago • 2 comments

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

finnnnnnnnnnnnnnnnn avatar May 05 '25 14:05 finnnnnnnnnnnnnnnnn

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.

inner-daemons avatar May 07 '25 16:05 inner-daemons

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.

cwfitzgerald avatar May 07 '25 18:05 cwfitzgerald