Skip to content

Perf/frame pacing - #15

Merged
kxxoling merged 6 commits into
mainfrom
perf/frame-pacing
Aug 27, 2026
Merged

Perf/frame pacing#15
kxxoling merged 6 commits into
mainfrom
perf/frame-pacing

Conversation

@kxxoling

@kxxoling kxxoling commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Frame pacing overhaul: refresh-rate adaptation + user-selectable rate

Problem

  • Animations advanced by a hardcoded per-frame constant, so they ran
    faster or slower depending on the display's refresh rate and on frame
    drops (a dropped frame slowed the whole scene).
  • The render loop always targeted 60fps: on high-refresh panels the
    wallpaper never used the extra smoothness, and the pacing slept until
    the exact frame boundary, so the next draw + buffer submission landed
    after the intended vblank and presentation slipped a whole refresh
    period (visible judder).

What this PR does

  1. Real frame delta time — sway, lock/unlock transition easing and
    drag inertia are driven by measured elapsed time (exponential
    approach with a 0.2s time constant, framerate-normalized damping),
    so animation speed is identical on any refresh rate and unaffected
    by dropped frames.

  2. Refresh-rate adaptation — the render pace follows the display's
    current refresh rate and votes for the panel's highest supported
    rate via Surface.setFrameRate (API 30+); without the vote the
    compositor keeps high-refresh panels at their default mode.

  3. Frame rate setting — a Material3 dropdown lets users pick
    Auto (max N Hz) (labeled with the device's actual maximum) or fixed
    tiers at 1/2, 1/3 and 1/4 of the panel's max refresh (tiers below
    30fps are hidden). Fraction tiers divide the panel's max evenly, so
    presentation stays vblank-aligned even while the panel still runs at
    max — fixed values like 60 on a 144Hz panel would alternate 2/3
    vblank periods and judder. Changes apply on the very next frame via
    the existing prefs listener.

  4. Vblank-aligned pacing — the sleep target is pulled back by a
    present margin (1–3ms, scaled with the frame interval) so draw +
    submission land before the intended vblank instead of slipping a
    whole period.

Replace hardcoded 0.016f per-frame constants (sway clock, unlock
timer, transition easing, inertia damping) with elapsed-time based
updates so animation speed is identical on 60/90/120Hz screens and
unaffected by dropped frames: exponential approach with a 0.2s time
constant for the lock/unlock transition (equivalent to the old 8% per
frame at 60fps) and framerate-normalized damping exp(-5t).

Unit test assertions updated to the time-based easing (a 16ms step
moves 1 - exp(-0.016/0.2) ~ 7.7% instead of exactly 8%).
Pacing slept until the exact frame boundary, so the next draw and
buffer submission happened after it; the presentation then slipped a
whole vblank period, juddering between paces (e.g. 33/50ms for a
30fps idle target on a 60Hz panel) - visibly choppy on the launcher.

Subtract a 3ms present margin from the sleep target so submission
lands before the intended vblank and presentation snaps onto it.
sleepMillisForFrame takes the margin as a parameter (default 3ms,
0 restores the old behavior) with updated unit tests.
@kxxoling
kxxoling force-pushed the perf/frame-pacing branch 3 times, most recently from a3bf795 to d2ff2c3 Compare August 27, 2026 22:56
The render loop always targeted a fixed 60fps: on high-refresh panels
the wallpaper never used the extra smoothness, and sleeping until the
exact frame boundary meant the next draw + buffer submission landed
after the intended vblank, slipping presentation a whole refresh
period (visible judder).

- Full pace follows the display's current refresh rate, polled every
  ~0.5s.
- Vote for the panel's highest supported refresh rate via
  Surface.setFrameRate (API 30+): without a vote the compositor keeps
  the panel at its default mode, so high-refresh devices never
  actually ran fast.
- The sleep target is pulled back by a present margin scaled with the
  frame interval (1-3ms) so draw + submission land before the intended
  vblank instead of slipping a whole period.
Add a Frame rate setting so users can trade smoothness against
battery on high-refresh panels.

Behavior:
- Auto (default): vote for the panel's highest supported refresh rate
  and pace at whatever the panel currently runs at.
- Fixed tiers at 1/2, 1/3 and 1/4 of the panel's max refresh rate
  (tiers below 30fps are hidden). Fraction rates divide the max
  refresh evenly, so presentation stays vblank-aligned even while the
  panel still runs at max - fixed rates like 60 on a 144Hz panel
  would alternate 2/3 vblank periods and judder. Mainstream panels
  (60/90/120/144) all divide cleanly.
- Pacing uses min(preference, current refresh) while the
  Surface.setFrameRate vote asks the panel to move toward it.

Storage: mode string (auto|half|third|quarter) in DataStore with a
SharedPreferences mirror, like the other wallpaper-visible settings;
resolved against the display's supported modes at runtime so it
adapts across devices. The engine's prefs listener pushes changes to
the GL thread, which applies them on the very next frame (the poll
counter is nudged); restarts restore the preference from prefs.

UI: Material3 ExposedDropdownMenuBox (read-only outlined field with
chevron) listing "Auto (max 144 Hz)" with the device's actual max,
plus plain Hz values for the tiers.
While the screen is off the render loop kept waking at the render
pace (~60x/s) just to run a no-op transition update, burning CPU all
night. Nobody sees intermediate frames while off, so:

- Advance the transition with real measured frame delta (previously
  it reused the stale delta from the last rendered frame) at a coarse
  100ms cadence.
- Once the transition settles, poll at ~1Hz.
- setRendering(true) interrupts the sleep so screen-on still renders
  immediately; all sleeps swallow and clear the interrupt so a stale
  flag cannot busy-loop the pace loop.
@kxxoling
kxxoling merged commit 96f8473 into main Aug 27, 2026
1 check passed
@kxxoling
kxxoling deleted the perf/frame-pacing branch August 27, 2026 23:26
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