Skip to content

Add framelimiter winit#23486

Draft
joelawm wants to merge 4 commits intobevyengine:mainfrom
joelawm:add-framelimiter-winit
Draft

Add framelimiter winit#23486
joelawm wants to merge 4 commits intobevyengine:mainfrom
joelawm:add-framelimiter-winit

Conversation

@joelawm
Copy link
Copy Markdown

@joelawm joelawm commented Mar 24, 2026

Objective

Allow developers to limit an application's frame rate when using bevy_winit.

This closes #1343.

Fixes the inability to limit the framerate of the game.

Solution

This PR adds a new windowed update mode for bevy_winit:

UpdateMode::ContinuousCapped { wait: Duration }

It also adds convenience constructors:

WinitSettings::continuous_capped(wait)
WinitSettings::game_with_max_fps(max_fps)

The new mode behaves like a continuous game loop, but instead of running as fast as possible, it advances on a scheduled timeout cadence. Unlike Reactive, it does not wake early for user, window, or device events. Those events are batched and handled on the next scheduled update.

Note:
One thing to note when I was looking into this PR, I noticed several reworks made for winit runner based on this PR#9304. Mine utilizes this older version of the system. I could take a crack at reviving that PR and modernizing it.

Testing

Yes, I did.
Manual testing:

  • I ran the new frame_limiter example
  • Then I verified that changing the target FPS changes the observed frame rate
    PR Reviewers can test with:
cargo run --release -p bevy --example frame_limiter

I added --release because it helps reduce performance issues when limiting the frame rate, particularly with 240fps. I am slightly concerned that my method could be somehow making the game update slower, hence the framerate issue. The only thing I can think of is the timers limiting the frame pacing. Which is why I have a cap of 180fps while running the demo in debug, but that's not the case in release.

Showcase

Click to view showcase This PR adds a new public API for capped continuous updates:
use bevy::prelude::*;
use bevy::winit::WinitSettings;

App::new()
    .insert_resource(WinitSettings::game_with_max_fps(120.0))
    .add_plugins(DefaultPlugins)
    .run();

For more direct control:

use bevy::prelude::*;
use bevy::winit::WinitSettings;
use core::time::Duration;

App::new()
    .insert_resource(WinitSettings::continuous_capped(
        Duration::from_secs_f64(1.0 / 60.0),
    ))
    .add_plugins(DefaultPlugins)
    .run();

joelawm added 2 commits March 23, 2026 23:20
I wanted to remove vsync since it has little to do with the example.
@github-actions
Copy link
Copy Markdown
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide, as well as our policy regarding AI usage, and we look forward to reviewing your pull request shortly ✨

fn main() {
App::new()
.insert_resource(FrameLimiterSettings::default())
.insert_resource(WinitSettings::game_with_max_fps(DEFAULT_FPS as f64))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I really don't like defaulting to 60 fps locked. Winit exposes the monitor refresh rate which should always be used when it's available.

Copy link
Copy Markdown
Member

@aevyrie aevyrie Mar 24, 2026

Choose a reason for hiding this comment

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

Additionally, winit only provides monitor Hz to the nearest integer. If you don't round your fps up, you'll end up accumulating frames and latency, which defeats the point of pacing.

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.

Copy link
Copy Markdown
Contributor

@IceSentry IceSentry left a comment

Choose a reason for hiding this comment

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

Frame pacing is already done in browsers so this should not be enabled when compiled for wasm

Copy link
Copy Markdown
Member

@aevyrie aevyrie left a comment

Choose a reason for hiding this comment

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

Frame limiting should probably not be tied to winit, and be based around the render/vblank timing so we can do proper frame pacing, not just limit the event loop poll.

Here's some prior art that's existed since 0.6: https://github.com/aevyrie/bevy_framepace

See also: https://www.activision.com/cdn/research/Hogge_Akimitsu_Controller_to_display.pdf

It's possible that limiting at the event loop poll could give us better results, but from what I can tell the speed of the polling is a black box, and I don't know if we can rely on it for stable timings. Another consideration is power use. One of the motivating factors for bevy_framepace, and framelimiting in general is reducing power use for non game apps. This PR doesn't address this in two ways:

  • There is no testing to show this is as efficient as, say spin_sleep
  • This is coupled to update modes, for app use at work we usually use reactive rendering with framelimiting/pacing. Frame limiting should be orthogonal to the event loop mode, not baked into it. These two things combined are the only way I've found to get bevy's power use down to the low single digits.

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-Windowing Platform-agnostic interface layer to run your app in D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Nominated-To-Close A triage team member thinks this PR or issue should be closed out. labels Mar 24, 2026
@alice-i-cecile
Copy link
Copy Markdown
Member

alice-i-cecile commented Mar 24, 2026

I would really love work here, but I think the correct strategy is to upstream bevy_mod_framepace, possibly with improvements.

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

Labels

A-Windowing Platform-agnostic interface layer to run your app in C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Nominated-To-Close A triage team member thinks this PR or issue should be closed out.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants