tinne26/tps-vs-fps

Explanation gap for dealing with high refresh rate displays

tinne26 opened this issue · 4 comments

In non-pixel-art games, smooth interpolation of game elements at >60Hz (e.g: 144Hz and 240Hz displays) is not uncommon. The current explanation doesn't really explain how to deal with this, and kinda brushes it away as if saying "just stick to 60 ticks per second and draw accordingly".

Adjusting TPS based on the screen's refresh rate is not a good idea, you are basically just doing Draw logic inside Update but with an extra artificial split, and you can no longer use fixed delta values in Update. Additionally, Ebitengine API doesn't provide any function to get the display's maximum refresh rate.

On practical solutions:

  • The main option is to take the current position as the next position and interpolate positions on Draw based on time. This is the safest way to go, but it introduces a 1 tick delay, which can become a real issue for fast-paced games.
  • Another option is predicting future positions and drawing that, but this can easily cause movement and animation artifacts.
  • Another option is going for a time-based approach that ignores Update and does all the work on Draw. This is not even incorrect, but it does kinda go against the ethos of Ebitengine.
  • The final option is the hybrid usage of both the fixed and variable timestep loops like many AAA games do, but this does again get quite complex and it doesn't fall in line with Ebitengine usage expectations.

I don't think Ebitengine has an "official" or recommended way to deal with these situations (TPS != FPS without lag or vsync off being the causes) or acknowledges it as a significant concern. Otherwise I believe at least a DisplayRefreshRate function would most likely exist.

There are also optimization concerns. When handling consecutive draws on pixel-art games, in many cases (when not using time-dependent shaders or similar effects) you can use SetScreenClearedEveryFrame(false) and ignore the draws, as nothing has changed. Otherwise high refresh rate displays can start burning quite a lot of processing time for no good reason. We also have no function to limit the FPS and let Ebitengine do this internally for us.

Summarizing, there's definitely a gap in the documentation around this issue, but the situation is unclear for Ebitengine itself too. But now at least anyone interested in this will know about some practical options or can start bothering Hajime Hoshi on my behalf.

For FPS < TPS, we have the following cases:

  • The game is lagging. Little to do about it except profiling and optimizing.
  • FPSModeVsyncOffMinimum is being used.
  • TPS have been intentionally and temporarily raised to create a "turbo mode" or similar.
  • TPS are unusually high for some reason (e.g. 80 TPS were deemed convenient for some physics simulation, but the display we are using only runs at 60Hz).

None of these are really a problem.

For FPS > TPS, we have the following cases:

  • TPS are intentionally low (e.g. 30 TPS, or even 2 TPS) because no more is needed for the game.
  • TPS have been intentionally and temporarily lowered to create a "slowmo effect" or similar. We will need to interpolate if we want graphics to remain smooth.
  • The screen has a high refresh rate that exceeds the typical TPS. This is becoming increasingly common.
  • FPSModeVsyncOffMaximum is being used. This is the only case where there's no problem, as this is typically done to keep track of the game's performance.

In some of these cases, redrawing each time is wasteful and there's nothing to interpolate to make the game look better. In others, redrawing each time will normally result in the same image unless you are interpolating element positions. Interpolating introduces lag so it's not always so great.

Some more thoughts...

The new SkipDraw feature (see hajimehoshi/ebiten#2341) will be very helpful to spare GPU power for those applications that do not really take advantage of high refresh rates and are redrawing the same screen again and again.

Also, I'm starting to believe that real-time position interpolation is ok for high refresh rate displays, but only because the extra frame delay is much smaller than on regular 60Hz displays. On 60Hz displays it really hurts latency. At 120Hz, latency is ok because you are at a very similar place as with 60Hz without interpolation, but then one might also be polling input events more often, which can help make the system more responsive. And at those rates, prediction failures (if using that) will be less noticeable. So... best support requires using different strategies for different refresh rates.

Is that correct?

No, not fully correct: on a fixed-timestep loop, input polling will be delayed one tick anyway, no matter the display refresh rate.

I have another solution that seems better than anything else I've previously thought of:

  • Use 240TPS as the base rate for animations. Always assume a second has 240 ticks.
  • Allow three TPS levels for the game: 240TPS, 120TPS, 60TPS. We could also have other divisions like 80 or 30, but I don't think those are so useful.
  • Now we can have high(er) refresh rates based on responsive simulation at 240TPS and we only need a light wrapper to get the "animation ticks advance". If we are running at 60TPS, we advance ticks by 4, at 120TPS we advance ticks by 2, and at 240 we advance ticks by 1. Logically, we are always running at 240TPS, and with a bit of care we can keep the system deterministic.

This system remains simple to use (tick based), easy to test with only 3 main levels, but supporting higher refresh rates and input responsivity for the computers that can handle that. This seems like a decent compromise. The main downside is that we may break determinism in some cases if we are not careful, so that's one extra thing to keep in mind when relevant.

Another downside is that then we can't use TPS for turbo/slow modes so freely. That was always kind of a hacky way to implement such things, though, so it's not a big deal. You can still set TPS to 480 anyway, but then you will need yet a few more hacks to compensate the main usage of TPS. A more thorough model for implementing turbo modes / slowdowns that doesn't impact input responsivity should be explored. It's fairly clear, though, that it should be applied in its own logical layer.

To be seen: whether to configure the TPS manually or automatically based on empirical FPS measures.