Skip to content

perf(ui): drop unconditional per-frame repaint — idle window goes to ~0% CPU - #86

Merged
weter11 merged 1 commit into
mainfrom
fix/event-driven-repaint
Aug 16, 2026
Merged

perf(ui): drop unconditional per-frame repaint — idle window goes to ~0% CPU#86
weter11 merged 1 commit into
mainfrom
fix/event-driven-repaint

Conversation

@weter11

@weter11 weter11 commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Summary

update() ended with an unconditional ctx.request_repaint(), forcing egui to render a full frame every vsync. On a 165 Hz display that pegs the UI thread even when the window sits untouched — the other half of the idle-CPU burn alongside #85's cover-fetch gate (this stacks on top of it).

What changed

The four channel drains in update() now return whether they drained anything; the unconditional repaint is replaced by a scoped policy:

Dependency (per-frame tick) Resolution
image_rx drain (cover art) repaint on drain; poster Spinner self-repaints while fetch pending
per-task download rx drain repaint on drain + request_repaint_after(250ms) while tasks active (ProgressBar is passive; ETA is Instant-based and counts by wall-clock)
play_result_rx drain repaint on drain + request_repaint_after(1s) while rx is Some (game exit / login-required can arrive while idle)
operation_rx drain (45 async ops) repaint on drain
Spinners (account, proton list, steamguard, poster) egui 0.33 Spinner self-requests repaint while visible — no change needed
TextEdit cursor blink / scroll inertia egui internal — no change needed

With nothing pending, no repaint is scheduled and the main thread blocks in the event loop until the next input event (egui repaints on input itself).

Measured verification (idle window, not just compile)

Same binary pair, same Xvfb display, fresh config (no Steam session), 10 s samples:

Metric before after
UI thread, idle 353% of one core 0% of one core
whole process (top-style) 331% 1.6%
input wake (40 XTEST keys) 288% burst, settles back to 0%

The window was verified alive and input-responsive after the change (focus + synthetic input → repaint burst → returns to idle), not assumed.

Test Plan

  • cargo check --all-targets clean
  • cargo test --lib — 141 passed
  • Live UI smoke test on real display (user)

…~0% CPU

update() ended with an unconditional ctx.request_repaint(), forcing egui to
render a full frame every vsync. On a 165 Hz display that pegs the UI thread
(~65% of one core idle; 353% under unthrottled Xvfb) even when the window
sits untouched.

Replace it with a scoped repaint policy. All four channel drains in update()
now report whether they drained anything, and repaints are only requested
when the UI actually has new state to show:

- any drained message (cover art, download progress, play result, async op)
  -> render immediately
- active downloads -> request_repaint_after(250 ms) so the passive
  ProgressBar and the Instant-based ETA countdown keep ticking between
  channel messages (egui::Spinner self-repaints while visible, so cover
  loading / account / proton / steamguard spinners cover themselves)
- pending play result -> request_repaint_after(1 s) so a game exit or
  login-required event is picked up while the window is idle

With nothing pending the UI schedules no repaint and the main thread blocks
in the event loop until the next input event (which egui repaints on
itself).

Measured idle (10 s samples, Xvfb :99, fresh config, no Steam session):
  before: 353% of one core (UI thread), 331% whole-process (top-style)
  after:    0% of one core (UI thread),   1.6% whole-process
Input wake: 40 XTEST key events -> 288% burst, then settles back to 0%.
Same binary pair, same display, only the repaint policy differs.
@weter11
weter11 merged commit 92396b2 into main Aug 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant