Replies: 2 comments 5 replies
-
|
@NthTensor and @IceSentry have come to similar conclusions about the need to refactor our windowing loop to fix this I believe. I'm very interested in a fix for this: it's a crucial bit of polish for all kinds of projects. |
Beta Was this translation helpful? Give feedback.
-
|
You've identified part of the issue and one of my plan towards this was to upstream bevy_framepace and then plan from there but one big issue is simply that we don't have access to accurate timings from the gpu which makes things harder. One of the wgpu maintainer has been looking at framepacing from the wgpu side lately so I was mainly waiting on that to see what would come of it. Another issue with refactoring bevy_winit is that there's a lot of small bugfixes in it that have accumulated over the years and we need to be careful about not regressing too much. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
It's relatively well known that the out-of-the-box bevy experience is not as smooth as it could be. Variable frame rates, latency, and jitter are common complaints, and those complaints have been around for years. A very quick sampling of examples:
Time<Real>, missedFixedUpdateΒ #21229I spent weeks trying to get frame pacing working in bevy with something akin to https://github.com/aevyrie/bevy_framepace, but never got results as good as I knew should be possible. I could only manage to get smooth frame rates, or low CPU/GPU utilization, but not both at the same time. Nothing I came up with played well with vsync, and minimizing latency was a challenge.
The Problem
By default, bevy's render loop looks something like this (very simplified):
It's worth noting this is a very common configuration, and
winititself even recommends something like this. However:The main problem with this is the
vsync waitbecause it stalls the event loop. Naive frame pacers create the same issue by sleeping, which also stalls the event loop. When the event loop finally resumes, it then has to process a backlog of all the events that piled up while it was stalled. This creates variable amounts of work depending on how many events are queued, leading to variable frame times, stuttering, jitter, etc. (see Res<Time> is unreliable / jitteryΒ #4669 and Variable frametime during user inputΒ #4691)A secondary issue with the above is the
vsync waitinjects time between when input events are sampled by thewinit event loop, and when the results of those inputs are presented on screen. The very definition of input latency. If you get unlucky, this latency can be nearly an entire frame time. It gets worse though, because vsync typically involves double (bevy's default, I believe) or triple buffering, meaning you get a minimum of 1-3 frames worth of input latency. Even if you do everything else perfectly, the results still feel sluggish. (see Delays in user inputΒ #16335 and High input latencyΒ #21439)Waiting in the Event Loop
Given the above, it would be nice if we could somehow move the
vsync waittime into thewinit event loop. That way, we can keep processing winit events right up until it's time to render the next frame. This would help address both issues:vsync waitis kept as small as possible and inputs are more recent on averageProof of Concept
To figure this out, I ended up building an app outside of bevy, with just
winitandwgpu, but the core implementation isn't terribly complicated. Here are the important bits:winitdoesn't have event loop timeouts (as far as I could find), but you can emulate one withControlFlow::WaitUntil(see rust-windowing/winit#3377). So, the pieces needed for this are:render_target).ControlFlow::WaitUntilto prevent sleeping pastrender_target(about_to_wait). Waking up triggers the next point.render_targettime. If so, request a redraw (new_events).render_targethas already past (window_event). Prevent "catching up" by resettingrender_targettonow + FRAME_TIME.And that's it. On my machine, the above resulted in ~1-2% CPU load (an 1/8th of the best I could get in bevy) and ~1% GPU load (about 1/2 of the best I could get in bevy), is buttery smooth, and input doesn't feel sluggish.
Other benefits
window_eventforcesrender_targetto synchronize with vsync, so this works well and minimizes input latency with both vsync on and off.Caveats
winitevent loop handler.render_targetshould try to account for how long it takes to run the bevy schedules in the worse case. If vsync is enabled,vsync waitwould consume any extra time as usual.wgpu'sdesired_maximum_frame_latencyshould be set as low as possible to minimize vsync related input latencyBeta Was this translation helpful? Give feedback.
All reactions