You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Proposal] - Support high refresh rate (> 60) on Linux #183703
Linux (I tested both on Hyprland (Wayland, with VRR turned on) and LXDE (X11)) is the last platform that doesn't support high refresh rate (> 60 FPS) for Flutter, making scrolling and animation less pleasant.
Since the main window managers and desktop environments on Linux like GNOME, KDE, Sway, Hyprland, niri already support VRR (variable refresh rate or adaptive sync) (Variable refresh rate - ArchWiki), Flutter supporting high refresh rate can make itself more competitive, especially considering that Electron on Linux doesn't support high refresh rate either at this moment (tested Obsidian app on Linux and Windows) and one of selling points of Flutter, compared to React Native and Electron, is performance.
I used the same hardware, monitor, and app to test. On Windows, the max FPS is 6000+, and the avg. is 250+. On Linux, the max is locked to 60. (Testing tool: show_fps | Flutter package).
Proposal
Support high refresh rate (> 60) for Linux (at least on Wayland).
Reacted by Jari Hämäläinen, Felipe Peter, Alexander Kolenaty, Ben Hagen, xiaobai, benfgit, SmallTall, Predidit, lin72h, Hitesh Kumar Saini and 7 more
Reacted by Mazen Omar, Kristijan Žic, lin72h and Friedhold Matz
@misobadev asked above whether this is the same as #49757. Yes, same root cause. Everything in this comment applies to both.
The groundwork is all on that thread already. nortio implemented the waiter and reported that the app claims 144fps while the animation stays at 60 (#49757 (comment)), and knopp's reply is the conclusion that turns out to be right: a vsync waiter is pointless while GTK controls buffer flipping, and the way out is taking over rendering and pacing off Wayland frame callbacks (#49757 (comment)). nortio also noticed that GtkGLArea is capped at 60 on GTK3 while GTK4 is not - what follows is the mechanism-level version of that observation.
@finnvyrn's report is accurate as filed. What I couldn't find written down anywhere is the exact chain, a reproducer with no Flutter in it, and a way to measure this that doesn't produce false positives.
Environment: 3840x2160 @ 143.99Hz, RTX 4070 SUPER (proprietary), KDE Plasma 6.7.4 Wayland, GTK 3.24.52, Flutter master @ e26b384.
Why GDK's frame clock is stuck at 60 for a Flutter window
Flutter's frame is a GTK paint: present_layers → g_idle_add → gtk_widget_queue_draw → draw handler → gdk_cairo_draw_from_gl. queue_draw schedules on GDK's frame clock, and for this window that clock never learns the display's rate:
gdkwindow.c:2965 sets current_paint.use_gl = impl_window->gl_paint_context != NULL. Note this is not a property of the paint: it is the presence of a GL context cached on the window, created by the first gdk_cairo_draw_from_gl (gdkwindow.c:2866) and released only at unrealize (gdkwindow.c:2106-2111). One GL frame in the window's lifetime is enough to take this branch forever after, including for frames that are pure cairo.
gdk_window_impl_wayland_end_paint guards its whole body on !current_paint.use_gl (gdkwindow-wayland.c:1084), so gdk_wayland_window_attach_image and impl->pending_commit = TRUE (:1116) are skipped for those paints
on_frame_clock_after_paint therefore early-returns on if (!impl->pending_commit) (:723)
which also means _gdk_frame_clock_freeze() (:729) never runs. The clock is never throttled by the compositor at all - it free-runs on the idle timer, which is why the measurement below is an exact 60.00 rather than something that jitters.
awaiting_frame (:771) stays FALSE, so GDK never requests a wl_surface_frame
frame_callback() never fires, and it is the only place in GTK3 that sets GdkFrameTimings.complete (:666)
gdkframeclockidle.c:428-432 then reads refresh_interval == 0 and falls back to #define FRAME_INTERVAL 16667 (:37)
the clock ticks at 60Hz, and every paint with it
Instrumented in the Flutter window, on every tick, indefinitely:
GTK3 on Wayland isn't inherently capped at 60. The GL presentation path is.
Measuring this correctly
I implemented a vsync waiter to confirm what knopp said, in two variants. The GdkFrameClock-backed one can't exceed 60 by construction, since it paces off the clock that step 8 above pins at 60. The one driven by wl_surface_frame does lift frame production, and that's the interesting case. Counting frames in present_layers and paints in the renderer's draw handler, in the same binary:
baseline with waiter
engine frames produced 60.00/s 90-95/s
GTK paints (on screen) 60.00/s 60.0/s
Production is load-dependent and moves a lot between runs, anywhere from 77 to 98/s in my measurements. The number that matters is the one that doesn't move: presentation stays at 60.0 in every run. Those extra 30-35 frames per second are rendered and discarded. Strictly worse than the baseline: same 60 on screen, more CPU and GPU burned.
This is the trap in this area. An fps counter wired to Ticker, addTimingsCallback or the engine timeline measures production. On this path that number moves freely without anything reaching the display, which is how a patch here can look like a 40% win while being a regression. Any change claiming to fix this should be measured at the last link, not the first. A counter in the renderer's draw handler is enough.
This matters for how the issue gets closed, so to be concrete: the show_fps package used in the report measures production too. Today that gives the right answer, because production and presentation are both 60. After a waiter-only patch it would read 144 while the screen still showed 60. The tool that correctly demonstrates the problem now would incorrectly demonstrate it fixed.
Notes on things that are easy to get wrong
refresh_interval is only populated once the compositor acknowledges a frame. A window that calls gdk_frame_clock_begin_updating() without ever drawing keeps GDK's 16667us default and looks exactly like a hard 60Hz cap. Any measurement here has to actually draw.
An exact 60.00 does not by itself identify VsyncWaiterFallback: the Flutter-free reproducer above produces the same exact 60.00 straight from GDK's clock.
eglSwapBuffers isn't the bottleneck. GDK calls eglSwapInterval(egl_display, 0) on every make-current (gdkglcontext-wayland.c:574), and __GL_SYNC_TO_VBLANK=0 / vblank_mode=0 change nothing.
None of this applies to X11. Under Xwayland the reproducer gives 60.00fps with complete=7/8 in both modes, the GL path and the cairo control alike. So _NET_WM_FRAME_TIMINGS does arrive and timings do complete; what's missing is a non-zero refresh_interval in it (gdkdisplay-x11.c:1423). Whatever caps X11 is a different mechanism, and since cairo is capped too, the GL path isn't it. I only tested Xwayland, not a native X11 session.
MinikPLayer reported on Desktop app frame rate isn't synced to display #49757 reaching 165Hz with a vsync waiter plus a patched FRAME_INTERVAL in gdk/gdkframeclockidle.c. I haven't reproduced that, but it is consistent with the mechanism here: patching the constant changes the fallback the clock uses precisely because timings never complete. It also shows the cap can be lifted without leaving gdk_cairo_draw_from_gl, just not in a way that can ship, since it requires patching the system GTK.
One more datapoint, because it rules out the obvious workaround: #189584 renders Impeller into a wl_subsurface and paints a cairo background on the parent, which is enough to keep GDK's frame accounting alive. On that branch the toplevel clock runs at refresh_interval=6944us with complete=7/8. The frame-clock half of the problem is solved there. The parent still only commits 48 times a second, below what the OpenGL path manages. So getting GDK to learn the refresh rate is necessary and not sufficient.
Implication
Presentation has to stop depending on GTK's paint cycle for its cadence, which is the direction of #189584 and of the GTK4 migration in #94804. A vsync waiter is necessary but must land with such a path, not before it: on its own it's a regression.
For the pacing source, wp_presentation gives real presentation timestamps for frame_target_time_nanos, which GDK's frame clock can't provide here. KWin, Mutter and wlroots all expose it.
The waiter builds against master and has tests, if any of it is useful once presentation moves. I'd rather shape it to whatever the team wants than push it as-is.
Reacted by Michał Tomecki, mmmpipi, lin72h, Kevin McDole and Ryotaro Onoue
Activity
[-]Support high refresh rate (> 60) on Linux[/-][+][Proposal] - Support high refresh rate (> 60) on Linux[/+]saurabh-mirajkar commentedon Mar 18, 2026
@finnvyrn Thanks for a detailed proposal report. Labelling this to investigate further.
finnvyrn commentedon Mar 28, 2026
Updated: I noticed Electron apps like VS Code and Obsidian have already been supporting VRR on Linux (at least on Wayland).
misobadev commentedon May 10, 2026
is this the same with #49757 (comment) ?
Document why the Linux desktop build must move off Flutter's embedder
ales-drnz commentedon Aug 6, 2026
@misobadev asked above whether this is the same as #49757. Yes, same root cause. Everything in this comment applies to both.
The groundwork is all on that thread already. nortio implemented the waiter and reported that the app claims 144fps while the animation stays at 60 (#49757 (comment)), and knopp's reply is the conclusion that turns out to be right: a vsync waiter is pointless while GTK controls buffer flipping, and the way out is taking over rendering and pacing off Wayland frame callbacks (#49757 (comment)). nortio also noticed that GtkGLArea is capped at 60 on GTK3 while GTK4 is not - what follows is the mechanism-level version of that observation.
@finnvyrn's report is accurate as filed. What I couldn't find written down anywhere is the exact chain, a reproducer with no Flutter in it, and a way to measure this that doesn't produce false positives.
Environment: 3840x2160 @ 143.99Hz, RTX 4070 SUPER (proprietary), KDE Plasma 6.7.4 Wayland, GTK 3.24.52, Flutter master @ e26b384.
Why GDK's frame clock is stuck at 60 for a Flutter window
Flutter's frame is a GTK paint:
present_layers→g_idle_add→gtk_widget_queue_draw→ draw handler →gdk_cairo_draw_from_gl.queue_drawschedules on GDK's frame clock, and for this window that clock never learns the display's rate:gdkwindow.c:2965setscurrent_paint.use_gl = impl_window->gl_paint_context != NULL. Note this is not a property of the paint: it is the presence of a GL context cached on the window, created by the firstgdk_cairo_draw_from_gl(gdkwindow.c:2866) and released only at unrealize (gdkwindow.c:2106-2111). One GL frame in the window's lifetime is enough to take this branch forever after, including for frames that are pure cairo.gdk_window_impl_wayland_end_paintguards its whole body on!current_paint.use_gl(gdkwindow-wayland.c:1084), sogdk_wayland_window_attach_imageandimpl->pending_commit = TRUE(:1116) are skipped for those paintson_frame_clock_after_painttherefore early-returns onif (!impl->pending_commit)(:723)_gdk_frame_clock_freeze()(:729) never runs. The clock is never throttled by the compositor at all - it free-runs on the idle timer, which is why the measurement below is an exact 60.00 rather than something that jitters.awaiting_frame(:771) stays FALSE, so GDK never requests awl_surface_frameframe_callback()never fires, and it is the only place in GTK3 that setsGdkFrameTimings.complete(:666)gdkframeclockidle.c:428-432then readsrefresh_interval == 0and falls back to#define FRAME_INTERVAL 16667(:37)Instrumented in the Flutter window, on every tick, indefinitely:
The frame counter advances normally (120 per 2s). Nothing ever completes.
Reproducer without Flutter
Same window, same frame clock, only the draw path differs. No argument for the GL path,
cairofor the control.GTK3 on Wayland isn't inherently capped at 60. The GL presentation path is.
Measuring this correctly
I implemented a vsync waiter to confirm what knopp said, in two variants. The
GdkFrameClock-backed one can't exceed 60 by construction, since it paces off the clock that step 8 above pins at 60. The one driven bywl_surface_framedoes lift frame production, and that's the interesting case. Counting frames inpresent_layersand paints in the renderer's draw handler, in the same binary:Production is load-dependent and moves a lot between runs, anywhere from 77 to 98/s in my measurements. The number that matters is the one that doesn't move: presentation stays at 60.0 in every run. Those extra 30-35 frames per second are rendered and discarded. Strictly worse than the baseline: same 60 on screen, more CPU and GPU burned.
This is the trap in this area. An fps counter wired to
Ticker,addTimingsCallbackor the engine timeline measures production. On this path that number moves freely without anything reaching the display, which is how a patch here can look like a 40% win while being a regression. Any change claiming to fix this should be measured at the last link, not the first. A counter in the renderer's draw handler is enough.This matters for how the issue gets closed, so to be concrete: the
show_fpspackage used in the report measures production too. Today that gives the right answer, because production and presentation are both 60. After a waiter-only patch it would read 144 while the screen still showed 60. The tool that correctly demonstrates the problem now would incorrectly demonstrate it fixed.Notes on things that are easy to get wrong
refresh_intervalis only populated once the compositor acknowledges a frame. A window that callsgdk_frame_clock_begin_updating()without ever drawing keeps GDK's 16667us default and looks exactly like a hard 60Hz cap. Any measurement here has to actually draw.VsyncWaiterFallback: the Flutter-free reproducer above produces the same exact 60.00 straight from GDK's clock.eglSwapBuffersisn't the bottleneck. GDK callseglSwapInterval(egl_display, 0)on every make-current (gdkglcontext-wayland.c:574), and__GL_SYNC_TO_VBLANK=0/vblank_mode=0change nothing.complete=7/8in both modes, the GL path and the cairo control alike. So_NET_WM_FRAME_TIMINGSdoes arrive and timings do complete; what's missing is a non-zerorefresh_intervalin it (gdkdisplay-x11.c:1423). Whatever caps X11 is a different mechanism, and since cairo is capped too, the GL path isn't it. I only tested Xwayland, not a native X11 session.FRAME_INTERVALingdk/gdkframeclockidle.c. I haven't reproduced that, but it is consistent with the mechanism here: patching the constant changes the fallback the clock uses precisely because timings never complete. It also shows the cap can be lifted without leavinggdk_cairo_draw_from_gl, just not in a way that can ship, since it requires patching the system GTK.One more datapoint, because it rules out the obvious workaround: #189584 renders Impeller into a
wl_subsurfaceand paints a cairo background on the parent, which is enough to keep GDK's frame accounting alive. On that branch the toplevel clock runs atrefresh_interval=6944uswithcomplete=7/8. The frame-clock half of the problem is solved there. The parent still only commits 48 times a second, below what the OpenGL path manages. So getting GDK to learn the refresh rate is necessary and not sufficient.Implication
Presentation has to stop depending on GTK's paint cycle for its cadence, which is the direction of #189584 and of the GTK4 migration in #94804. A vsync waiter is necessary but must land with such a path, not before it: on its own it's a regression.
For the pacing source,
wp_presentationgives real presentation timestamps forframe_target_time_nanos, which GDK's frame clock can't provide here. KWin, Mutter and wlroots all expose it.The waiter builds against master and has tests, if any of it is useful once presentation moves. I'd rather shape it to whatever the team wants than push it as-is.
Smoother Week view, and measure rendering