Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
adenine-dev opened this issue Apr 21, 2024 · 14 comments
Labels
B - bug Dang, that shouldn't have happened DS - macos

Comments

@adenine-dev
Copy link

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

@adenine-dev adenine-dev added B - bug Dang, that shouldn't have happened DS - macos labels Apr 21, 2024
@kchibisov
Copy link
Member

Does it happen in the example we use?

@adenine-dev
Copy link
Author

adenine-dev commented Apr 21, 2024

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.

@kchibisov
Copy link
Member

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.

@adenine-dev
Copy link
Author

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).

@adenine-dev
Copy link
Author

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.

@kchibisov
Copy link
Member

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?

@madsmtm
Copy link
Member

madsmtm commented Apr 21, 2024

I think this might be a duplicate of #1737.

@adenine-dev
Copy link
Author

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.

@adenine-dev
Copy link
Author

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.

@kchibisov
Copy link
Member

@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.

@adenine-dev
Copy link
Author

adenine-dev commented Apr 22, 2024

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

@bschwind
Copy link

bschwind commented May 3, 2024

@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.

@adenine-dev
Copy link
Author

@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 ^^

@bschwind
Copy link

bschwind commented May 4, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
B - bug Dang, that shouldn't have happened DS - macos
Development

No branches or pull requests

4 participants