Skip to content

gpui: Park the Wayland frame loop while windows are idle - #60308

Closed
phisch wants to merge 5 commits into
zed-industries:mainfrom
phisch:gpui-wayland-idle-cpu-fix
Closed

phisch wants to merge 5 commits into
zed-industries:mainfrom
phisch:gpui-wayland-idle-cpu-fix

Conversation

@phisch

@phisch phisch commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Objective

GPUI apps on Wayland burn CPU at the display refresh rate while a window is visible (at least partially on an active output), even when nothing changes. The frame callback loop unconditionally re-arms itself: every done requests the next frame callback and ends in a commit, so the compositor wakes us ~60 times (or more on higher refresh rates) per second forever, only to skip drawing.

I noticed this while working on a GPUI application, and found an empty window constantly consuming CPU resources.

Solution

  • New PlatformWindow::request_redraw(), a no-op everywhere except Wayland.
  • A Wayland frame now only commits (arming the next callback) when something was drawn or another frame is wanted, otherwise the loop parks.
  • Windows that still need a frame after an effect flush call request_redraw, which resumes a parked loop with one bare commit. All state changes funnel through the effect flush, so no wake source is missed.
  • The gpui_linux: Fix Wayland flickering under CPU load by skipping redundant surface commit #54214 guard (no bare commit on a frame the renderer presented) is preserved. Idle windows now issue zero bare commits instead of 60/s.

Testing

Measured debug build: an idle window went from 62 wakeups/s and ~2% CPU to 0 wakeups and 0% over 20s, WAYLAND_DEBUG shows the protocol going silent. Verified redraw-on-timer (park, wake, redraw, re-park),
continuous animation at full rate.

X11 is unaffected (tested on bspwm), and cargo test -p gpui and script/clippy still pass.

To test: run any gpui example on Wayland and watch CPU in htop (or any other system monitor).

Self-Review Checklist:

  • I've reviewed my own diff for quality, security, and reliability
  • Unsafe blocks (if any) have justifying comments
  • The content adheres to Zed's UI standards (UX/UI and icon guidelines)
  • Tests cover the new/changed behavior
  • Performance impact has been considered and is acceptable

Release Notes:

  • Fixed idle windows waking at the display's refresh rate on Wayland

phisch added 2 commits July 2, 2026 21:52
Windows ask the platform for a frame when they need one while the
platform frame loop is idle. A no-op on platforms whose loop runs
continuously while visible.
The frame callback loop re-armed itself with a commit on every frame,
waking the process at refresh rate even when nothing changed. Now a
frame only re-arms when something was drawn or another frame is wanted,
and request_redraw resumes the parked loop. Also keeps a forced render
alive when the frame it was requested for got throttled.
@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Jul 2, 2026
@smitbarmase smitbarmase added the platform:linux/wayland happens only on Linux Wayland label Jul 3, 2026
@Anthony-Eid Anthony-Eid self-assigned this Jul 3, 2026
NeelChotai added a commit that referenced this pull request Jul 20, 2026
The explicit `completed_frame` was a code smell and only existed to execute
Wayland's frame-end bookkeeping, it's now inlined in `schedule_frame` so that
becomes the entire render loop contract.

Takes some elements from #60308 and
test instrumentation from #61063.

Co-authored-by: Philipp Schaffrath <philipp.schaffrath@gmail.com>
Co-authored-by: Daan De Meyer <daan@amutable.com>
@iddm

iddm commented Jul 21, 2026

Copy link
Copy Markdown

GPUI apps on Wayland burn CPU at the display refresh rate while a window is visible (at least partially on an active output), even when nothing changes. The frame callback loop unconditionally re-arms itself: every done requests the next frame callback and ends in a commit, so the compositor wakes us ~60 times (or more on higher refresh rates) per second forever, only to skip drawing.

I noticed this while working on a GPUI application, and found an empty window constantly consuming CPU resources.

I found the exact opposite behaviour: when there is nothing to stitch for the compositor, and the Wayland client doesn't draw, the Wayland server does not invoke the app callbacks to draw, and so the drawing freezes completely. So 0 draw callbacks are called; nothing is rendered on the screen. This is why #60690 was created, as well as why I created my PR. Does it work here the same way? Cuz judging by the description, it attempts to solve a different problem.

This behaviour also isn't specific to GPUI and Zed; I ran into it myself when I was also recently creating a game ui in Vulkan and found the exact same issue with Wayland.

@phisch

phisch commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@iddm Before diving into anything, have you tested if this PR solves your problem? Compiling and testing is a bit faster than trying to figure out what causes your problem, understanding that problem, and finding out if this approach fixes it by chance.

Please give it a quick test, and let me know which compositor you are experiencing that issue under.

@iddm

iddm commented Jul 21, 2026

Copy link
Copy Markdown

@iddm Before diving into anything, have you tested if this PR solves your problem? Compiling and testing is a bit faster than trying to figure out what causes your problem, understanding that problem, and finding out if this approach fixes it by chance.

Please give it a quick test, and let me know which compositor you are experiencing that issue under.

Yes, I wanted to test it, but didn't have time today, hopefully, tomorrow.

I am on Hyprland.

@phisch

phisch commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@iddm Before diving into anything, have you tested if this PR solves your problem? Compiling and testing is a bit faster than trying to figure out what causes your problem, understanding that problem, and finding out if this approach fixes it by chance.
Please give it a quick test, and let me know which compositor you are experiencing that issue under.

Yes, I wanted to test it, but didn't have time today, hopefully, tomorrow.

I am on Hyprland.

Let me know once you had some time to test! Thanks for the help!

@NeelChotai

Copy link
Copy Markdown
Member

Thank you for working on this! Your debug of the issue is spot on; the PR that @iddm referenced tackles the same thing but also gets rid of the bare surface commit: #60690

The difference is whether we rely on compositor wakes or handle that internally, and this PR uses the existing mechanism (the former) but this causes issues on a fullscreen because callbacks only fire when the compositor repaints, and a damageless (i.e. no change) commit on a static surface may never get one.

I've added you as a co-author on #60690, so your contribution will still be reflected. The remaining gaps you identified (broader flush wake, async refresh) are fixed as part of #61063, so we will land one after the other.

However, I'm happy to wait on testing before moving forward in case it turns out we're mistaken about the bufferless commit being the root cause.

@phisch

phisch commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@NeelChotai thanks for taking over! 👍

Since @iddm already confirmed (#60690 (comment)) that their problem is resolved in the new PR, we should just go ahead with the merge instead of waiting to see if this now obsolete PR also solves it.

Closing this PR in favor of #60690

@phisch phisch closed this Jul 23, 2026
@iddm

iddm commented Jul 23, 2026

Copy link
Copy Markdown

@iddm Before diving into anything, have you tested if this PR solves your problem? Compiling and testing is a bit faster than trying to figure out what causes your problem, understanding that problem, and finding out if this approach fixes it by chance.

Please give it a quick test, and let me know which compositor you are experiencing that issue under.

Yes, I wanted to test it, but didn't have time today, hopefully, tomorrow.

I am on Hyprland.

Let me know once you had some time to test! Thanks for the help!

I am sorry for the delay. Legitimately tried to run, but rustc, for some reason, eats out all memory... could not launch it to confirm.

NeelChotai added a commit that referenced this pull request Jul 24, 2026
The explicit `completed_frame` was a code smell and only existed to
execute
Wayland's frame-end bookkeeping, it's now inlined in `schedule_frame` so
that
becomes the entire render loop contract.

Takes some elements from
#60308 and
test instrumentation from
#61063.

Co-authored-by: Philipp Schaffrath <philipp.schaffrath@gmail.com>
Co-authored-by: Daan De Meyer <daan@amutable.com>
NeelChotai added a commit that referenced this pull request Aug 6, 2026
The explicit `completed_frame` was a code smell and only existed to
execute
Wayland's frame-end bookkeeping, it's now inlined in `schedule_frame` so
that
becomes the entire render loop contract.

Takes some elements from
#60308 and
test instrumentation from
#61063.

Co-authored-by: Philipp Schaffrath <philipp.schaffrath@gmail.com>
Co-authored-by: Daan De Meyer <daan@amutable.com>
NeelChotai added a commit that referenced this pull request Aug 6, 2026
The explicit `completed_frame` was a code smell and only existed to
execute
Wayland's frame-end bookkeeping, it's now inlined in `schedule_frame` so
that
becomes the entire render loop contract.

Takes some elements from
#60308 and
test instrumentation from
#61063.

Co-authored-by: Philipp Schaffrath <philipp.schaffrath@gmail.com>
Co-authored-by: Daan De Meyer <daan@amutable.com>

Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
NeelChotai added a commit that referenced this pull request Aug 20, 2026
The explicit `completed_frame` was a code smell and only existed to
execute
Wayland's frame-end bookkeeping, it's now inlined in `schedule_frame` so
that
becomes the entire render loop contract.

Takes some elements from
#60308 and
test instrumentation from
#61063.

Co-authored-by: Philipp Schaffrath <philipp.schaffrath@gmail.com>
Co-authored-by: Daan De Meyer <daan@amutable.com>

Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
NeelChotai added a commit that referenced this pull request Aug 20, 2026
The explicit `completed_frame` was a code smell and only existed to
execute
Wayland's frame-end bookkeeping, it's now inlined in `schedule_frame` so
that
becomes the entire render loop contract.

Takes some elements from
#60308 and
test instrumentation from
#61063.

Co-authored-by: Philipp Schaffrath <philipp.schaffrath@gmail.com>
Co-authored-by: Daan De Meyer <daan@amutable.com>
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
NeelChotai added a commit that referenced this pull request Aug 20, 2026
The explicit `completed_frame` was a code smell and only existed to
execute
Wayland's frame-end bookkeeping, it's now inlined in `schedule_frame` so
that
becomes the entire render loop contract.

Takes some elements from
#60308 and
test instrumentation from
#61063.

Co-authored-by: Philipp Schaffrath <philipp.schaffrath@gmail.com>
Co-authored-by: Daan De Meyer <daan@amutable.com>
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement platform:linux/wayland happens only on Linux Wayland

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants