Skip to content

Add rolling average option to Engine.get_frames_per_second()#63356

Open
Calinou wants to merge 1 commit into
godotengine:masterfrom
Calinou:smooth-fps-counter
Open

Add rolling average option to Engine.get_frames_per_second()#63356
Calinou wants to merge 1 commit into
godotengine:masterfrom
Calinou:smooth-fps-counter

Conversation

@Calinou

@Calinou Calinou commented Jul 23, 2022

Copy link
Copy Markdown
Member

This uses an averaging algorithm to smooth out the FPS counter. To opt into this new behavior, call Engine.get_frames_per_second(Engine.FPS_METRIC_ROLLING_AVERAGE).

The existing behavior is preserved (average over the last second) if no parameter is passed to Engine.get_frames_per_second().

This also improves the documentation a bit.

See also #18998, which is an earlier attempt at providing something like this (with a different formula). The same formula is included in the MRP (but commented out) for reference.

Testing project: test_fps_counter.zip
Press Space to toggle between core and script-based FPS reporting. Use core-based reporting to test this PR (or the current behavior, with a vanilla master build).
Press Up/Down arrows to increase/decrease maximum FPS. Note that it's capped by V-Sync for testing purposes, but you can disable V-Sync in the project settings or run with --disable-vsync CLI argument if needed.

TODO

  • Fix the reported value being higher than it should be, especially when a FPS cap below the monitor refresh rate is used. This is likely related to what frame_time actually defines (I've also tried process_ticks to no avail). - For comparison, the script approach in the MRP does not have this issue.
    • Thanks @CaelusV for assistance in fixing this 🙂

@Calinou Calinou added this to the 4.0 milestone Jul 23, 2022
Comment thread core/math/math_defs.h Outdated
@Calinou Calinou force-pushed the smooth-fps-counter branch 3 times, most recently from f0d342e to 8682301 Compare July 27, 2022 17:31
Comment thread main/main.cpp Outdated
@Calinou

Calinou commented Jul 22, 2024

Copy link
Copy Markdown
Member Author

Rebased and tested again, it works as expected. I update the MRP in OP.

A strange issue occurs now when the engine starts: the reported FPS is negative for a brief period of time. I don't recall this happening previously, despite having made no code changes to the PR itself.

This is a video of the MRP being run at 120 FPS, slowed down to 10% so you can see it more easily:

negative_fps.mp4

This does not occur with the script-based implementation in the MRP.

@CaelusV

CaelusV commented Jul 23, 2024

Copy link
Copy Markdown

You probably want to make sure the ratio doesn't exceed 1.0 since you're subtracting it from 1.0 on the line below.

@Calinou

Calinou commented Jul 23, 2024

Copy link
Copy Markdown
Member Author

You probably want to make sure the ratio doesn't exceed 1.0 since you're subtracting it from 1.0 on the line below.

That does the trick, thanks 🙂

@Calinou Calinou force-pushed the smooth-fps-counter branch from ad72599 to 7826d68 Compare July 23, 2024 15:26
@Calinou

Calinou commented May 20, 2025

Copy link
Copy Markdown
Member Author

Rebased and tested again, it works as expected. (I've also tested --time-scale and --fixed-fps just to make sure, and it behaves correctly with those options.)

@KeyboardDanni

Copy link
Copy Markdown
Contributor

Personally, I'd prefer if this were user-configurable in some way. The old behavior makes it easier to see when frame drops occur because it's just accumulating the frame count over the span of one second. Granted, there's better ways to detect frame drops, but I do generally prefer the old behavior. It'd be cool if there were an enum option so we could choose between accumulated frames and averaged frames.

I see averaging useful for more intensive applications where the framerate is likely to vary. On the other hand, my main focus is on lightweight graphics where I care about the FPS consistently hitting the max refresh rate of the monitor. So I think there should be options to reflect both of these use case scenarios.

@lawnjelly

Copy link
Copy Markdown
Member

Yeah I wrote the same on RC:

It's tough because the existing average over 1 second is useful too because it shows variation. Wonder if we should have an enum of different values reported?

e.g.
Average FPS over 1 second
Rolling average (your change I'm guessing)
Minimum FPS over 1 second
Maximum FPS over 1 second
Something like that. You could keep all those statistics fairly easily then report them based on a defaulted enum parameter.
And maybe allow the user to change the range of these measurements rather than fix at 1 second.

So although the code here is useful, imo it would be more useful with a switch between the versions e.g.

Engine::get_frames_per_second(FPSEnum p_option=FPSEnum::Average)

@Calinou Calinou changed the title Make the FPS reported by Engine.get_frames_per_second() smoother Add rolling average option to Engine.get_frames_per_second() Feb 19, 2026
@Calinou

Calinou commented Feb 19, 2026

Copy link
Copy Markdown
Member Author

Rebased and tested again, it works as expected.

So although the code here is useful, imo it would be more useful with a switch between the versions e.g.

Engine::get_frames_per_second(FPSEnum p_option=FPSEnum::Average)

Done. This also paves the way to integrate #67136 more cleanly into the engine.

Note that I didn't implement the minimum/maximum metrics in this PR, since the bookkeeping required is also present in #67136. I'll implement them in #67136 after rebasing it on top of this PR.

@Calinou Calinou force-pushed the smooth-fps-counter branch 2 times, most recently from 1590233 to 168537b Compare February 20, 2026 01:31
This uses an averaging algorithm to smooth out the FPS counter.
To opt into this new behavior, call
`Engine.get_frames_per_second(Engine.FPS_METRIC_ROLLING_AVERAGE)`.

The existing behavior is preserved (average over the last second)
if no parameter is passed to `Engine.get_frames_per_second()`.
@Calinou Calinou requested review from a team as code owners February 20, 2026 01:31
Comment thread core/core_bind.h
enum FPSMetric {
FPS_METRIC_AVERAGE,
FPS_METRIC_ROLLING_AVERAGE,
FPS_METRIC_MAX,

@lawnjelly lawnjelly Feb 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A while since I did any enum binding, but do we need to bind a MAX?
Is it useful to users, and does it have any implications for when new values are added to the enum?

(Speaking as a user this was kind of confusing, as if metric MAX might give the max FPS rather than the max of the enum.)

@Calinou Calinou Feb 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A while since I did any enum binding, but do we need to bind a MAX?

It's not a strict requirement, but we generally do it for exposed enums. It allows users to loop over the size of the enum or implement cycling logic without hardcoding the size of the enum, as there is no reflection in GDScript on C++ enums. Reflection is supported on GDScript enums though, since they're internally dictionaries.

for metric in range(Engine.FPS_METRIC_MAX):
	pass

# Or:
var fps_metric: Engine.FPSMetric = Engine.FPS_METRIC_AVERAGE

func _input(event: InputEvent) -> void:
	if event.is_action_pressed("ui_accept"):
	fps_metric = wrapi(fps_metric + 1, 0, Engine.FPS_METRIC_MAX)

and does it have any implications for when new values are added to the enum?

No, we can safely change its value without breaking compatibility. It's one exception to enum value changes since we assume people are not relying on the _MAX value for anything else than looping/iteration.

(Speaking as a user this was kind of confusing, as if metric MAX might give the max FPS rather than the max of the enum.)

When the time comes to add a maximum metric, it'll be FPS_METRIC_MAXIMUM to avoid a conflict, and will be listed before FPS_METRIC_MAX (since that one always remains at the end of the enum).

@lawnjelly

lawnjelly commented Feb 20, 2026

Copy link
Copy Markdown
Member

This looks good.

I did note that Grok calls this technique "exponential moving average" (EMA), and thinks this is the standard term for the exact math here, versus "rolling average" being broader more generic term.

Otoh calling it "rolling average" means we could later change the exact technique. @Calinou any thoughts on this prior to approving?

("Smoothed FPS" is also an extremely common descriptive term apparently.)

@Calinou

Calinou commented Feb 20, 2026

Copy link
Copy Markdown
Member Author

I did note that Grok calls this technique "exponential moving average" (EMA), and thinks this is the standard term for the exact math here, versus "rolling average" being broader more generic term.

Otoh calling it "rolling average" means we could later change the exact technique. @Calinou any thoughts on this prior to approving?

I would prefer keeping the term "rolling average", as I consider the exact smoothing algorithm to be an implementation detail. (This is indeed an exponentional moving average algorithm right now.)

@lawnjelly lawnjelly left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve based on the PR does seem to do as intended (tested the artifact) and the code looks fine as far as I can see.

Could probably do with another reviewer confirming that we want this feature, as I think we generally prefer consensus before adding features (as it hasn't gone through a proposal as far as I can see?).

Otoh having different FPS metrics do seem useful imo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants