rust-windowing / winit

Window handling library in pure Rust

Home Page:https://docs.rs/winit/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Resizing/repositioning with rectangle on macos is very slow for continuously rendering applications

adenine-dev opened this issue · comments

Description

On macos when resizing windows with the rectangle utility, winit windows that request_redraw continuously with rendering, lag when resizing/repositioning with rectangle. This lag varies but is typically ~200ms-500ms.

While rendering is required (namely SurfaceTexture::present or equivalent), the bug occurs with multiple backends (tested with wgpu and vulkano), on my machine no other application exhibits this behavior, including ones that render continuously so I don't think its a rectangle bug either.

This bug occurs no matter where I put the rendering code, or how I update it, I've tested with rendering in Event::AboutToWait and WindowEvent::RedrawRequested I've also tested with with ControlFlow::Poll and ControlFlow::WaitUntil manually targeting 60fps, all cases I could find exhibit this behavior.

Minimum reproducible: wgpu triangle example, modified to render continuously:

use std::borrow::Cow;
use winit::{
    event::{Event, WindowEvent},
    event_loop::{ControlFlow, EventLoop},
    window::Window,
};

async fn run(event_loop: EventLoop<()>, window: Window) {
    let mut size = window.inner_size();
    size.width = size.width.max(1);
    size.height = size.height.max(1);

    let instance = wgpu::Instance::default();

    let surface = instance.create_surface(&window).unwrap();
    let adapter = instance
        .request_adapter(&wgpu::RequestAdapterOptions {
            power_preference: wgpu::PowerPreference::default(),
            force_fallback_adapter: false,
            // Request an adapter which can render to our surface
            compatible_surface: Some(&surface),
        })
        .await
        .expect("Failed to find an appropriate adapter");

    // Create the logical device and command queue
    let (device, queue) = adapter
        .request_device(
            &wgpu::DeviceDescriptor {
                label: None,
                required_features: wgpu::Features::empty(),
                // Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain.
                required_limits: wgpu::Limits::downlevel_webgl2_defaults()
                    .using_resolution(adapter.limits()),
            },
            None,
        )
        .await
        .expect("Failed to create device");

    // Load the shaders from disk
    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
        label: None,
        source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
    });

    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
        label: None,
        bind_group_layouts: &[],
        push_constant_ranges: &[],
    });

    let swapchain_capabilities = surface.get_capabilities(&adapter);
    let swapchain_format = swapchain_capabilities.formats[0];

    let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
        label: None,
        layout: Some(&pipeline_layout),
        vertex: wgpu::VertexState {
            module: &shader,
            entry_point: "vs_main",
            buffers: &[],
            compilation_options: Default::default(),
        },
        fragment: Some(wgpu::FragmentState {
            module: &shader,
            entry_point: "fs_main",
            compilation_options: Default::default(),
            targets: &[Some(swapchain_format.into())],
        }),
        primitive: wgpu::PrimitiveState::default(),
        depth_stencil: None,
        multisample: wgpu::MultisampleState::default(),
        multiview: None,
    });

    let mut config = surface
        .get_default_config(&adapter, size.width, size.height)
        .unwrap();
    surface.configure(&device, &config);

    event_loop.set_control_flow(ControlFlow::Poll);
    let window = &window;
    event_loop
        .run(move |event, target| {
            // Have the closure take ownership of the resources.
            // `event_loop.run` never returns, therefore we must do this to ensure
            // the resources are properly cleaned up.
            let _ = (&instance, &adapter, &shader, &pipeline_layout);

            if let Event::WindowEvent {
                window_id: _,
                event,
            } = event
            {
                match event {
                    WindowEvent::Resized(new_size) => {
                        // Reconfigure the surface with the new size
                        config.width = new_size.width.max(1);
                        config.height = new_size.height.max(1);
                        surface.configure(&device, &config);
                        // On macos the window needs to be redrawn manually after resizing
                        window.request_redraw();
                    }
                    WindowEvent::RedrawRequested => {
                        let frame = surface
                            .get_current_texture()
                            .expect("Failed to acquire next swap chain texture");
                        let view = frame
                            .texture
                            .create_view(&wgpu::TextureViewDescriptor::default());
                        let mut encoder =
                            device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
                                label: None,
                            });
                        {
                            let mut rpass =
                                encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                                    label: None,
                                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                                        view: &view,
                                        resolve_target: None,
                                        ops: wgpu::Operations {
                                            load: wgpu::LoadOp::Clear(wgpu::Color::GREEN),
                                            store: wgpu::StoreOp::Store,
                                        },
                                    })],
                                    depth_stencil_attachment: None,
                                    timestamp_writes: None,
                                    occlusion_query_set: None,
                                });
                            rpass.set_pipeline(&render_pipeline);
                            rpass.draw(0..3, 0..1);
                        }

                        queue.submit(Some(encoder.finish()));
                        frame.present();
                    }
                    WindowEvent::CloseRequested => target.exit(),
                    _ => {}
                };
            /* ------------------------------------------------   change here   --------------------------------------*/
            } else if event == Event::AboutToWait {
                window.request_redraw();
            }
        })
        .unwrap();
}

pub fn main() {
    let event_loop = EventLoop::new().unwrap();
    #[allow(unused_mut)]
    let mut builder = winit::window::WindowBuilder::new();
    #[cfg(target_arch = "wasm32")]
    {
        use wasm_bindgen::JsCast;
        use winit::platform::web::WindowBuilderExtWebSys;
        let canvas = web_sys::window()
            .unwrap()
            .document()
            .unwrap()
            .get_element_by_id("canvas")
            .unwrap()
            .dyn_into::<web_sys::HtmlCanvasElement>()
            .unwrap();
        builder = builder.with_canvas(Some(canvas));
    }
    let window = builder.build(&event_loop).unwrap();

    #[cfg(not(target_arch = "wasm32"))]
    {
        env_logger::init();
        pollster::block_on(run(event_loop, window));
    }
    #[cfg(target_arch = "wasm32")]
    {
        std::panic::set_hook(Box::new(console_error_panic_hook::hook));
        console_log::init().expect("could not initialize logger");
        wasm_bindgen_futures::spawn_local(run(event_loop, window));
    }
}

alternately, the boids example exhibits this behavior without modification.

macOS version

ProductName:	macOS
ProductVersion:	12.6.7
BuildVersion:	21G651

Winit version

0.29.15

Does it happen in the example we use?

Of the examples in the winit repo, control_flow (only when set to 3/poll or when redraw requested is enabled), pump_events, and run_on_demand, exhibit the behavior, while child_window and window do not.

Additionally control_flow when set to poll/with redraw requested, causes multiple seconds of lag, which is weird because in my application when I turn off rendering, even while updating continuously it does not cause this issue.

Only window should not do that, because window has correct render loop, the rest is just to show how things kind of work.

If window doesn't it could be just macOS being slow with hardware acceleration surfaces.

window indeed does not do it, only control_flow, pump_events, and run_on_demand.

Not really sure what you mean abt hardware acceleration surfaces, other applications I know are using metal do not exhibit this behavior. (specifically i tested a C app using metal and sdl3, and it didn't have this issue).

Just tested this using the sdl2 crate, specifically this variant of it since it doesn't work with wgpu on main rn, and it doesn't have this issue, It is slightly more laggy than other applications so wgpu may be partially at fault, but its no where near how laggy winit is.

But winit itself in example is not laggy as you said with the way it's doing things and its window example with the desired control flow is not laggy as well?

I think this might be a duplicate of #1737.

I think this might be a duplicate of #1737.

I saw this too but the last comment says it doesn't apply with rectangle so i figured it was different, magnet and bettertouchtool are both paid so i can't test them unfortunately.

But winit itself in example is not laggy as you said with the way it's doing things and its window example with the desired control flow is not laggy as well?

While the window example in this repo does not exhibit the behavior, the example also does not change the control flow or update continuously, the examples in this repo that do update continuously do show this behavior.

@adenine-dev if you resize it it'll update contiguously, so I don't see an issue here? It's updated as you want when it needs to be, you can also make it update contiguously.

Rectangle snaps to various locations in the screen, similar to how on windows if you drag a window to the side it will snap to filling half of the screen. So its not resizing continuously its snapping and resizing once (or technically twice because of the way rectangle is implemented). Additionally the lag occurs even when repositioning the window (ie when snapping from the left half of the screen to the right half, but maintaining the same size). This lag does not occur when resizing manually, or if it does it is not noticeable.

I hope the attached video clarifies, here it is first in wait mode and I spam the rectangle snap shortcuts, then when i change the control flow to poll and spam them again it is very laggy. I'm spamming the shortcuts at the same speed both times.

*edited to remove the linked video

@adenine-dev I have experienced this as well. I haven't studied it in depth, and for now the main way I've worked around it is to artificially limit my drawing frequency based on time as you can see here.

I don't think this is a great solution though because it probably misses some frames when resizing, which would result in flickering or incorrectly rendered output sizes.

It seems the act of rendering, presumably with vsync enabled, makes the redraw event take quite some time, and maybe multiple RedrawRequested events stack up and cause lag. I'd have to look more into the winit code to see for sure.

@bschwind Thanks, this works really well,,

idk about your system, it looks like you're potentially getting the desired fps from the monitor, but the maximum variance I've noticed when resizing with this method is ~+100% dt at 120fps, which is a lot but seeing as its an "instant" resize, only occurs for one tick, and this behavior does not occur on "normal" drag resize, its more than good enough for my purposes.

this is still probably a bug in winit, so i'll leave the issue open but hopefully anyone who finds this in the future can learn from it

Thanks again ^^

Glad that works for you, at least as a workaround. I decided to start digging into the winit code a little bit but so far I haven't come up with anything.

I did find this post from 2019 which may have some hints on it, though:

https://thume.ca/2019/06/19/glitchless-metal-window-resizing/

In any case, I'll keep looking into it as it would be a satisfying bug to fix.