-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Res<Time> is unreliable / jittery #4669
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
Comments
There is a great Unity article about this in conjunction with pipelined rendering and their very long hunt to straighten it out. |
Awesome, glad the community is taking this seriously. Trying to chase down these kinds of stutter issues at the application level can leave one feeling extremely haunted. I thought I ran into this type of jitter at a visible level in a toy Bevy project where the debug build was running at opt-level 1. Measuring exactly what's going on with this sort of thing is currently a bit beyond my abilities, so I can't say for certain what I was seeing, but as an experiment I tried adding a system to PreUpdate that did a rolling average of delta times (inspired by this ancient post from the Our Machinery folks), and then using the resulting smoothed delta for all my movement simulation. That actually seemed to help a great deal, which is at least some evidence that inaccurate delta was the source of the stutter I was seeing. Later, I ripped that out and just flipped to opt-level 2 for debug profile, which also smoothed out the worst of the stutter. So maybe the inaccuracy gets amplified when runtime performance gets worse, or maybe I was off-base entirely; definitely not a zone where I'm confident in my judgements yet. |
I ran into this when implementing |
This seems to be related to #4691. Since we're setting time in
Feels like it's not very possible to run things before winit events, but if we wanted to, we could fix things a bit in the current model by adding another schedule that is run after sub apps are run. |
What is the title of the thread? I use Discord apps and clicking the link doesn't open them on mobile nor desktop. EDIT: "Timing, stutter, hitching, and jank" in #help. |
Yes, the time jitter is not from the "complexity" of your project, but rather the way Bevy updates its time values. The problem exists even on a minimal blank example app. |
# Objective - The time update is currently done in the wrong part of the schedule. For a single frame the current order of things is update input, update time (First stage), other stages, render stage (frame presentation). So when we update the time it includes the input processing of the current frame and the frame presentation of the previous frame. This is a problem when vsync is on. When input processing takes a longer amount of time for a frame, the vsync wait time gets shorter. So when these are not paired correctly we can potentially have a long input processing time added to the normal vsync wait time in the previous frame. This leads to inaccurate frame time reporting and more variance of the time than actually exists. For more details of why this is an issue see the linked issue below. - Helps with #4669 - Supercedes #4728 and #4735. This PR should be less controversial than those because it doesn't add to the API surface. ## Solution - The most accurate frame time would come from hardware. We currently don't have access to that for multiple reasons, so the next best thing we can do is measure the frame time as close to frame presentation as possible. This PR gets the Instant::now() for the time immediately after frame presentation in the render system and then sends that time to the app world through a channel. - implements suggestion from @aevyrie from here #4728 (comment) ## Statistics  --- ## Changelog - Make frame time reporting more accurate. ## Migration Guide `time.delta()` now reports zero for 2 frames on startup instead of 1 frame.
bevy/examples/2d on main [?] via ⚙️ v1.62.1 took 4m41s
75% ➜ cargo run --example move_sprite --features wayland --release I still see sprite moving not smoothly( Software: Fedora 36 with latest updates |
# Objective - The time update is currently done in the wrong part of the schedule. For a single frame the current order of things is update input, update time (First stage), other stages, render stage (frame presentation). So when we update the time it includes the input processing of the current frame and the frame presentation of the previous frame. This is a problem when vsync is on. When input processing takes a longer amount of time for a frame, the vsync wait time gets shorter. So when these are not paired correctly we can potentially have a long input processing time added to the normal vsync wait time in the previous frame. This leads to inaccurate frame time reporting and more variance of the time than actually exists. For more details of why this is an issue see the linked issue below. - Helps with bevyengine#4669 - Supercedes bevyengine#4728 and bevyengine#4735. This PR should be less controversial than those because it doesn't add to the API surface. ## Solution - The most accurate frame time would come from hardware. We currently don't have access to that for multiple reasons, so the next best thing we can do is measure the frame time as close to frame presentation as possible. This PR gets the Instant::now() for the time immediately after frame presentation in the render system and then sends that time to the app world through a channel. - implements suggestion from @aevyrie from here bevyengine#4728 (comment) ## Statistics  --- ## Changelog - Make frame time reporting more accurate. ## Migration Guide `time.delta()` now reports zero for 2 frames on startup instead of 1 frame.
# Objective - The time update is currently done in the wrong part of the schedule. For a single frame the current order of things is update input, update time (First stage), other stages, render stage (frame presentation). So when we update the time it includes the input processing of the current frame and the frame presentation of the previous frame. This is a problem when vsync is on. When input processing takes a longer amount of time for a frame, the vsync wait time gets shorter. So when these are not paired correctly we can potentially have a long input processing time added to the normal vsync wait time in the previous frame. This leads to inaccurate frame time reporting and more variance of the time than actually exists. For more details of why this is an issue see the linked issue below. - Helps with bevyengine#4669 - Supercedes bevyengine#4728 and bevyengine#4735. This PR should be less controversial than those because it doesn't add to the API surface. ## Solution - The most accurate frame time would come from hardware. We currently don't have access to that for multiple reasons, so the next best thing we can do is measure the frame time as close to frame presentation as possible. This PR gets the Instant::now() for the time immediately after frame presentation in the render system and then sends that time to the app world through a channel. - implements suggestion from @aevyrie from here bevyengine#4728 (comment) ## Statistics  --- ## Changelog - Make frame time reporting more accurate. ## Migration Guide `time.delta()` now reports zero for 2 frames on startup instead of 1 frame.
# Objective - The time update is currently done in the wrong part of the schedule. For a single frame the current order of things is update input, update time (First stage), other stages, render stage (frame presentation). So when we update the time it includes the input processing of the current frame and the frame presentation of the previous frame. This is a problem when vsync is on. When input processing takes a longer amount of time for a frame, the vsync wait time gets shorter. So when these are not paired correctly we can potentially have a long input processing time added to the normal vsync wait time in the previous frame. This leads to inaccurate frame time reporting and more variance of the time than actually exists. For more details of why this is an issue see the linked issue below. - Helps with bevyengine#4669 - Supercedes bevyengine#4728 and bevyengine#4735. This PR should be less controversial than those because it doesn't add to the API surface. ## Solution - The most accurate frame time would come from hardware. We currently don't have access to that for multiple reasons, so the next best thing we can do is measure the frame time as close to frame presentation as possible. This PR gets the Instant::now() for the time immediately after frame presentation in the render system and then sends that time to the app world through a channel. - implements suggestion from @aevyrie from here bevyengine#4728 (comment) ## Statistics  --- ## Changelog - Make frame time reporting more accurate. ## Migration Guide `time.delta()` now reports zero for 2 frames on startup instead of 1 frame.
Jitter still exists in simplest 2d sprite moving demo, even with full screen mode. system:Windows |
Likewise on my macbook, linux machine and built for the web |
This issue snowballs badly due to the lack of proper frame lock and while having something like freesync or g-sync. With low CPU load framerate peaks above monitor refresh rate, then goes lower, then back higher again. Never reaching actual refresh rate. Which causes massive variation of DT and that also results in major stutter. To have production ready engine this has to be solved, combined with:
Right now its really hard to reach smooth gameplay on a decent machine. |
If we accept that [(#4669) Res<Time> is jittery](bevyengine/bevy#4669) then 1. Time is updated in the [TimeSystem set](https://docs.rs/bevy/0.14.2/bevy/time/struct.TimeSystem.html) 2. No work should happen before the Time is updated in a frame This doesn't fix the upstream jitter issue, but we can reduce bevy_ecs_tilemap's potential impact on the issue by making sure any work we do in `First` is done *after* time is updated. \## Solution 1. Create a new `TilemapFirstSet` SystemSet 2. Add bevy_ecs_tilemap `First` systems to `TilemapFirstSet` 3. Order `TilemapFirstSet` systems after the `TimeSystem`. \## Migration This shouldn't require end-user migration, but if you want to run system in First after bevy_ecs_tilemap's work, then the new SystemSet should be used.
While experimenting with different settings, I've stumbled upon Godot suggestions how to handle jitter / stutter which is platform dependent. E.g., see Window platform. https://docs.godotengine.org/en/stable/tutorials/rendering/jitter_stutter.html So, I've tried setting WindowMode::BorderlessFullscreen for the WindowPlugin and compared that to Windowed / Fullscreen. BorderlessFullscreen provide much better experience of them all on Win 11. There's also a weird input lag while using Windowed + 60hz setup on the 144hz monitor. Also, there's this suggestion, which kind of makes sense:
Bevy caps FPS higher than 144 for the 144hz G-Sync / FreeSync monitors, which may lead to unstable framerate spiral downwards and back again to something like 148. Not sure what causes this with PresentMode::AutoVsync though. Unfortunately, there's no way to custom limit the FPS without workaround hacks like loading CPU with workload, so there's no way to test this idea properly. |
This is effectively what The plugin works using |
Bevy version: 0.7 and current
main
.Bevy's
Time
resource, which is the standard and recommended source of timing information in Bevy, that everything should use for anything timing-related, is inaccurate in typical game situations.The issue occurs, because
Time
is updated in a system at the beginning of theFirst
stage in the main app schedule, usingstd
time.This might be fine for a pure cpu simulation use case, where the app just runs as fast as possible on the CPU. However, it is inadequate for games.
The exact instant when Time is updated depends on when the time update system happens to be scheduled to run. It is not tied to actual rendering frame timings in any way.
For animation use cases (this includes most practical uses of delta time! moving objects, moving the camera …), really anything where the timing information is needed to control what is to be displayed on the screen, the
Time
resource will not accurately represent the actual frame timings. This could lead to artifacts, like jitter or hiccups.Usage of "fixed timestep" is also affected, as it derives its fixed updates from
Time
.The issue can be observed if we enable vsync (which should lock Bevy to the display refresh rate and result in identical timings every frame), and print the value of
time.delta()
every frame update. The result is something like this:You can see that these timings vary by at least a few hundred microseconds every frame, sometimes even as much as ~1ms from frame to frame. This is, i guess, small enough that nobody has raised an issue in Bevy yet :D … but definitely large enough to risk causing real issues with animation/physics/etc.
The real solution to this problem would be to use frame presentation timings (time reported by the OS/driver corresponding to when the rendered frame is actually sent to the screen), which requires support from the underlying graphics APIs.
wgpu
does not yet provide anything for this use case. This is understandable, as there is no standard API in Vulkan yet either. AFAIK, only Android, and maybe recent versions of DirectX, and i think some Mesa extensions on linux, provide such functionality.Relevant work in Vulkan: KhronosGroup/Vulkan-Docs#1364
In the meantime, we should explore ways to improve the accuracy of
Time
in any way we can. TheFirst
stage does not seem like the best place to do it.Maybe a value that is much closer to the true frame timings (and likely good enough for most use cases) could be obtained from the Render schedule somehow? Maybe in the Prepare stage when the swapchain texture is obtained (as this is where the "vsync wait" happens)?
Please discuss.
Related issue: #3768
The text was updated successfully, but these errors were encountered: