fyne-io / fyne

Cross platform GUI toolkit in Go inspired by Material Design
https://fyne.io/
Other
24.57k stars 1.37k forks source link

GUI refresh rate is low on Windows based laptop with 144Hz screen #3853

Open Hypnotriod opened 1 year ago

Hypnotriod commented 1 year ago

Checklist

Is your feature request related to a problem?

I have launched the "fyne_demo" layout on my Windows based laptop with 144Hz screen, and have faced the low refresh rate in the application window.

Is it possible to construct a solution with the existing API?

Is there any API that can improve the refresh rate of GUI? For example enable V-Sync, set fps?

Describe the solution you'd like to see.

Improve GUI refresh rate.

andydotxyz commented 1 year ago

We don't have adaptive refresh setup on Windows driver yet. It seems like instead of the hard coded 60 FPS we should read the mode info from GLFW https://www.glfw.org/docs/3.3/window_guide.html

Hypnotriod commented 1 year ago

Hi again.
I have been playing around with the loop.go and runner.go modules and found that animations are running in separate goroutine. Was it made on purpose? Because it actually leads to the racing between drawSingleFrame call and animation next frame calculation (the draw call may be executed twice before the animation is recalculated, and vice versa).
So even after I've replaced the framerate with 125 FPS (time.Millisecond * 8) - animations still feels jittery. But when I started to recalculate animation before drawSingleFrame in the same thread - everything started to look way smoother.

Bluebugs commented 1 year ago

It is known that the animation are not controlled by the same timing as the frame rendering. It would be great to make it work like so.

As a fair warning, frame synchronization is a hard problem. You can spend years tuning things right just for one OS. Once we have the animation synchronized with the rendering, the next step will be to synchronize everything with the display. Right now the timer, like the animation timer, is running on its own. It doesn't get the tick from the screen. Getting that tick information might have different meaning on different OS. For example on Linux with Wayland, it might happen a few ms ahead of the next screen synchronization, giving you time to push your frame update, on some other system it might happen just after the screen is refreshed, or just before. Then you will have to care about turning that tick on and off when there is nothing to do to save on battery. Finally at some point along all of that work, it will be important to figure out how to measure our real frame rate, latency and other measurement that give us confidence that all this work it actually delivering the improvement we are trying to do.

Anyway, it is important to make progress along the way in trying to get high framerate nice and smooth. I think everyone will be happy to see small progress here. Maybe starting with synchronization of animation with rendering is a good first step. I will be interested to review a PR that move the animation tick in sync with the rendering.

dweymouth commented 1 year ago

I would just add that I'd argue that before moving beyond 60Hz or maybe 75Hz, getting the main timer to sleep when there is nothing to be done rather than continuous polling would be a prerequisite. Fyne apps already have a bit high background CPU use when idle (#2506), and running the main loop at 144Hz all the time would make it worse.

Hypnotriod commented 1 year ago

Have made the pull request of my solution: https://github.com/fyne-io/fyne/pull/3863
You may consider it as a solution example. I didn't change the fps there, so it remains 60.
Here is the fork I'm playing around with: https://github.com/Hypnotriod/fyne/tree/fps-improve

Hypnotriod commented 1 year ago

I have tried the glfwSwapInterval call with 1 to enable v-sync, so now everything looks like a native windows application. But, CPU and GPU usage grows up 2.5 times. And I guess the only way to fix it is to skip draw calls when nothing changed on screen... If someone wants to test: https://github.com/Hypnotriod/fyne/tree/fps-improve-v-sync

Bluebugs commented 1 year ago

We are using, if I remember correctly, SwapBuffer, which will swap buffer on vsync between the buffer being used to draw and the one used for display. If that wasn't the case we would see horizontal tearing when moving things vertically.

The things with SwapBuffer is that it has different behavior on different os. As far as I remember on Windows, it will block and swap during vsync. On Linux, it will queue the swap for next vsync and it is the next operation that could draw to the buffer that will block. I don't remember for MacOS. But this difference could have an interesting effect between Windows and Linux.

On Windows, as you get blocked and the ticking doesn't happen in sync with the vsync, you would end up with case were we drop a frame, because the tick happen to late, we do all the frame rendering operation and then block and wait for the next frame. So we could span over a vsync during the frame rendering operation.

On Linux, as things get queued, we would only get blocked during the frame rendering operation if the previous swap didn't happen yet, but as soon as it happen, we can continue. So likely on Linux, we might have an extra frame latency to display instead of a frame drop.

I think for working on vsync, we need to have better measurement in place for timing and latency. I haven't looked at what is available in the Go ecosystem, but would be good to figure first how to measure things and then we could look at improvement.

andydotxyz commented 1 year ago

And I guess the only way to fix it is to skip draw calls when nothing changed on screen...

We already don't draw when nothing has changed - I think... Internally we track if the canvas is "dirty" and only paint when we know at least one texture has changed (or, since 2.3.4 if we force a repaint due to a move)

Hypnotriod commented 1 year ago

I have checked the glfwSwapInterval(1) approach on Ubuntu and yeah - it doesn't work...
So my solution now is to add getPrimaryMonitorRefreshRate function which gets refreshRate from videoMode of primary monitor. And to start the drawing timer goroutine with refresh rate + 1 to avoid os draw misses:
https://github.com/Hypnotriod/fyne/tree/fps-improve-refresh-rate