bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
36.7k stars 3.61k forks source link

Frame rate limiting #1343

Open inodentry opened 3 years ago

inodentry commented 3 years ago

Add a way to limit the frame rate.

Ideally, allow for more flexible control over updates, to support non-game gui applications that should sleep when idle.

alice-i-cecile commented 3 years ago

In this discussion, I think it's important to clearly distinguish between "frame rate" (how often your screen draws), "system tick rate" (how often a given system runs) and "game tick rate" (how often the main game loop runs). I think these are all important to be able to control and limit ergonomically, but the details are a bit distinct.

Currently, as I understand it, there are three ways to tackle this sort of functionality:

  1. Adding a Timer to each of the relevant criteria. Controls system tick rate.
  2. The FixedTimeStep run criteria. Controls system tick rate.
  3. Enabling vsync. Controls frame rate, and incidentally limits game tick rate.

(Chime in if my understanding is wrong; I haven't poked at this much myself.)

inodentry commented 3 years ago

"system tick rate"

This is not relevant to this issue. That's something to be discussed with Run Criteria (as you point out).

Here we are talking about the global refresh rate.

"game tick rate" is coupled with rendering frame rate, by design, so they are practically synonymous.

vsync

VSync is orthogonal. There needs to be a frame rate limiter independent of vsync. VSync is about presenting the frames with the correct timing (synchronized to the monitor), not about framerate. It is to ensure the frames are displayed correctly on the screen, without visual artifacts (tearing).

Vsync can (and preferably should, when supported; we are already doing this in bevy) be implemented in a way that does not limit frame rate. See "mailbox vsync". Also the various vendor/driver provided solutions that go by various names like "adaptive sync", "fast sync", whatever.


As for GUI applications, you don't want to refresh at all if there is nothing to do. Unlike games, they are mostly idle. Therefore, for proper power savings for GUI apps, there needs to be more flexible and aggressive throttle control, to only refresh when there are things to do: handle external events / input, ongoing animations, any change that needs to update the UI / screen.

alice-i-cecile commented 3 years ago

"game tick rate" is coupled with rendering frame rate, by design, so they are practically synonymous.

I don't think this is a particularly good design, merely a convenient one.

If we're discussing ways in which users can control and limit the performance of their game, I think that drawing this distinction and working towards the decoupling of the concepts is an important part of the puzzle.

For example, relevant to your point on non-game applications: we may want to allow the main logic loop to proceed unimpeded, while the rendering is paused completely until there is something new to draw (or the window is focused).

cart commented 3 years ago

Yeah I think the next step forward here is decoupling rendering from "game updates", which we have discussed a bit here: https://github.com/bevyengine/bevy/issues/1098#issuecomment-759047862

This opens up the door to "frame rate limiting", "pipelined rendering", disabling rendering when minimized / unfocused, etc.

Toqozz commented 3 years ago

I'm assuming that by "decoupling rendering" you are referring to a separate render thread.

Please note that rendering in a separate thread (or in many cases, decoupling rendering in general) from game updates will inherently introduce a degree of latency:

Objects move in systems -> wait some time -> picked up by the renderer and frame is rendered with that data.

I think anything that adds latency is important to consider carefully. Personally, I feel that decoupling rendering may be over-engineering here and serves mostly to confuse users with another layer to think about.

I understand that there are advantages as well, so it may be worthwhile, but so far I don't see huge benefit.

forbjok commented 3 years ago

but so far I don't see huge benefit

One significant benefit is that if you can run non-rendering systems at a much higher framerate, it reduces the chance of ignored inputs. This is already an issue in Bevy, even when running at a full 60fps, and the lower the framerate is the worse it will become. Maybe there are other better ways to fix this (some way of guaranteeing that that the .pressed and .just_pressed checks return true whenever the key was pressed at any point since the previous update cycle, even if it was released before the current?), but being able to run update cycles at for example 120fps instead of 60, while still rendering at 60, would mean keypresses are checked more frequently, making the problem less noticeable and annoying to the player.

Toqozz commented 3 years ago

One significant benefit is that if you can run non-rendering systems at a much higher framerate ...

Input is definitely an interesting case here, and I agree that there should be a way to poll input at a faster rate than the framerate. I'm still unconvinced that decoupled rendering is the answer here though. To me it feels like more of a specific problem which should be solved independently.

aevyrie commented 2 years ago

I created a prototype framerate limiter here https://github.com/bevyengine/bevy/issues/3317#issuecomment-997513775 with the goal of reducing input latency.

mwbryant commented 2 years ago

This is the (janky) solution I've used for my project so far, just a nothing system that sleeps to hold up the frame for a set period of time. This is my temp fix until something better comes along (the change to fifo present mode doesn't seem to work on my laptop)


fn frame_limiter_system() {
    use std::{thread, time};
    thread::sleep(time::Duration::from_millis(10));
}
aevyrie commented 2 years ago

Shameless plug for https://github.com/aevyrie/bevy_framepace, since my last post.

I've done quite a bit of experimentation in this area for the work we are doing at Foresight with desktop applications. The nice thing about frame pacing combined with framerate limiting, is that it has much better latency than Fifo vsync alone, latency as good as Immediate, with no frame tearing, and much less CPU/GPU use than any of Fifo/Mailbox/Immediate.

aevyrie commented 2 years ago

support non-game gui applications that should sleep when idle.

The quoted part of the issue is solved by #3974.

alexniver commented 1 year ago

Can we define some thing like Update in app.add_systems(Update, xx_xx);

such as UpdateByDuration(Duration), then use it by app.add_systems(UpdateByDuration(Duration::from_seconds(1.0), xx_xx))

siler commented 10 months ago

I would like a way to globally cap the rate at which the main schedule runs similar to FixedUpdate (or lock it in sync with FixedUpdate).

My use case is: I have a headless server running MinimalPlugins which shares Bevy code with a client through a library including some plugins and systems. While there is FixedUpdate which some systems use, there are also shared systems that use Update via plugin configuration. The client would like these systems to run at frame speed while the server would like these systems to run at fixed update, and it would be nice if I could just configure the current schedule to run less often.

I'm mostly reporting this here because the bevy_framepace plugin is currently being considered as a solution for this, but it doesn't work for my use case because it depends on the render subsystem existing.

tbillington commented 10 months ago

@siler does this not work for you?

MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(
    std::time::Duration::from_secs_f64(1.0 / UPDATES_PER_SECOND),
))
Maximetinu commented 7 months ago

JMS55 removed this from the 0.14 milestone 2 days ago

@JMS55, does that mean this will go into 0.15?

JMS55 commented 7 months ago

There's no set date I can give you.

At the start of a release cycle I typically add tasks I think would be good to do to the milestone. As we get towards the end of a cycle, I remove the ones that won't realistically be completed in time or aren't even started.

No one's taken up frame limiting, so I've removed it from the milestone.