emilk / egui

egui: an easy-to-use immediate mode GUI in Rust that runs on both web and native
https://www.egui.rs/
Apache License 2.0
22.43k stars 1.6k forks source link

Clear way to set FPS limit #1109

Open dvec opened 2 years ago

dvec commented 2 years ago

I've created a simple app that renders long scrollable images grid with some animations and I'm really excited by performance of this library: on my laptop I got about 1500 FPS using glow backend. But there is one thing bothering me: CPU & GPU usage go up to 90% when I run my app, so I want to set FPS limit to save energy and get rid of cooler noise. For now I'm using std::thread::sleep in update function, but this solution doesn't seems clear.

Describe the solution you'd like Since FPS limiting is not backend-dependent, we can add fps_limit(&self) -> Option<u32> function to App trait.

Describe alternatives you've considered Also, we can add set_fps_limit(&self, limit: u32) to Frame, so it should be able to call it from App::setup.

sourcebox commented 2 years ago

I'm also interested in this, mainly because my applications don't require high frame rates and should be CPU efficient. Should it be up to the user to limit the FPS or will egui implement such a feature?

dvec commented 2 years ago

egui_glow backend is using vsync (as well as egui_glium), so as far as I know, app FPS should be limited to monitor refresh rate. But by some reason egui demo app shows about 1500 FPS, and I was able to verify this number using counter in update function. It seems like egui runs update function 1500 times per second, while GPU renders only 60 frames (on 60 HZ screen), which looks like overkill.

UPD: I was able to fix vsync issue by updating macOS to 12.1 and it seems to be not related to egui (even minecraft was affected), so this issue is not a problem for me anymore. But I still think egui should have a way to control FPS and turn on/off vsync.

wucke13 commented 2 years ago

I oppose the request to this feature, at least for egui (eframe would be a different story). My understanding is, that egui is supposed to generate a list of vertices spanning triangles, maybe some color information for said primitives and that's it. Time is completely out of the equation for egui itself. How should egui enforce the FPS limit? By blocking wait? Because egui doesn't require threads and isn't async, I don't see another obvious way (but just truncating every new call to egui for the time slot of the current frame, which would again create spin-lock like busy waiting).

Also it would be kind of weird if a library which is not at all time-aware starts having a blocking wait function (which is what "enforcing an fps limit" essentially boils down to).

On a side note, using just

const MAX_FPS: f64 = 60;

loop{
  // egui code
  std::thread::sleep(Duration::from_sec(1.0/MAX_FPS));
}

Will result in jitter for the actual frame time. Rather it makes sense to wait only time left for the current frame, something like this (untested, just off my head)

const MAX_FPS: f64 = 60;

let frame_time = Duration::from_secs(1.0/MAX_FPS);
let mut next_frame = Instant::now();
loop{
  // egui code
  next_frame += frame_time;
  std::thread::sleep(next_frame - Instant::now());
}
jgarvin commented 2 years ago

@wucke13 Beware using Instant for this, it tries to prevent subtraction from ever overflowing, and to accomplish this mutex locks in old versions. Newer code is lockless but will still create cache line contention if you have multiple threads calling Instant::now(). It's not ideal for realtime and measuring performance if your app/game is multithreaded.

emilk commented 2 years ago

For most people, vsync is the best choice. If that is broken, it should be fixed. Anything slower than vsync will make animations non-smooth. Of course, if one wants to save CPU, this can still be worthwhile.

As @wucke13 points out, you can always do this sleeping yourself in order to enforce a maximum FPS. Beware though that std::thread::sleep has terrible resolution on windows.

As for turning off vsync: why would you want to do that? I guess it can give you lower latency, but is it really noticeable?

Now: if vsync is broken for more people, then perhaps it makes sense for eframe to throttle the FPS as a fallback, e.g. to 200 FPS by default (since there are high-hz monitors), but it is not obvious how to do that since std::thread::sleep has such terrible resolution on Windows.

Blisto91 commented 2 years ago

Without knowing much about the technical details would the approaches of the spin_sleep crate be able to provide any guidance?

emilk commented 2 years ago

The tricks spin_sleep uses to get more accurate sleeping in Windows (timeBeginPeriod) could be useful, but the spinning part is counter-productive (we want to save CPU after all).

sourcebox commented 2 years ago

@emilk Just for clarification: the maximum framerate is determined by the backend, i.e. 60fps when using glium or glow. If I want to lower this value, then I have to put in some thread::sleep() myself. Is this correct?

sourcebox commented 2 years ago

BTW: is there an internal frame count that can be queried?

Blisto91 commented 2 years ago

The tricks spin_sleep uses to get more accurate sleeping in Windows (timeBeginPeriod) could be useful, but the spinning part is counter-productive (we want to save CPU after all).

Ye spinning is more CPU intensive. Was more thinking of it in the context of the fallback throttle you mentioned if vsync fails. In that case I'm guessing it would (maybe) save CPU resources instead of letting it run wild. But again i don't know the technical details. Just peeking from the shadow here mostly 👀

emilk commented 2 years ago

@emilk Just for clarification: the maximum framerate is determined by the backend, i.e. 60fps when using glium or glow. If I want to lower this value, then I have to put in some thread::sleep() myself. Is this correct?

It should be determined by your display refresh rate, which often is 60Hz. If you want something lower you need to add std::thread::sleep, yes.

vkahl commented 9 months ago

I would like to revive this issue, because I'd really like to set a higher FPS limit than my monitor refresh rate. I am convinced this would improve the general perception of egui desktop apps by a lot! The lag and rubberbanding when turning on vsync on a 60Hz monitor just feels so sluggish (equally bad for glow and wgpu, slightly better in the browser). Maybe I am especially sensitive to input lag, but it can't be just me. I usually turn off vsync because of that, but that's a bad solution since it burns unnecessarily many CPU cycles and it lead to weird crashes on windows in the past (not on linux though!). Setting a limit of something like 120 or 180FPS would be a game changer!

NtLoadDriverEx commented 9 months ago

I am also encountering this, similarly to @vkahl, I would like to disable vsync where possible since I am using egui to render things similar to the fractal clock example, using egui primitives to render interesting graphics in the background of my UI, making them run at a high fps is paramount to making it look good when a monitor supports it. You should have the option to control the fps cap (or if there is one) as well as controlling if vsync is enabled.

iMonZ commented 3 weeks ago

@emilk Just for clarification: the maximum framerate is determined by the backend, i.e. 60fps when using glium or glow. If I want to lower this value, then I have to put in some thread::sleep() myself. Is this correct?

It should be determined by your display refresh rate, which often is 60Hz. If you want something lower you need to add std::thread::sleep, yes.

What if the Display Refresh rate is higher then 60hz? Like for example 120hz. Then all Apps from egui would look clunky while the browser, other websites and apps would look smooth in contrast.