Skip to content

Full-rate frames ready early wait for the refresh the others land on - #552

Merged
wingleeio merged 1 commit into
mainfrom
flush/tile-align
Oct 26, 2026
Merged

wingleeio merged 1 commit into
mainfrom
flush/tile-align

Conversation

@wingleeio

@wingleeio wingleeio commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #551, which makes full-rate frames plain tiles. I'll retarget this to main when #551 merges.

What was wrong. At 120 Hz the display link fires about 2.5 ms before a refresh point. A frame takes about 0.8 ms to render and wait for the GPU, so it's ready about 1.5 ms before the point, and the window server shows it on the refresh after. About one frame in twenty is ready earlier, 1.7–3.2 ms before the point, and shows on that refresh instead. Two frames then land in one refresh and the next shows nothing new. That's why tiles alone still read a 17.7 ms p99 interval.

Measured with host-clock stamps of each tile commit against the display link's output grid (93% of commits at 1.4–1.6 ms before a point). The same phase holds on the AV path, where the AV renderer's own copyDisplayedPixelBuffer showed it taking 99.5% of frames: the loss is in which composite a frame reaches.

The change. align_wait: a frame ready 1.85–4.5 ms before a point waits until 1.6 ms before it, in one short sleep. About 5% of frames wait, each under 1.5 ms. Commits then all land 1.0–1.75 ms before a point, and handover intervals run 7.9–8.8 ms (p1–p99).

Applies only when all of these hold:

  • the frame is the one a display refresh asked for (refresh_frame): input's frames are drawn and shown at once, as before;
  • the refresh is under 12 ms: 60 Hz, and the latch's steady app-timer frames (refresh_grid), are untouched;
  • no resize is settling (500 ms): those frames show at once with the window's new size.

Why not a hold on every frame. Holding every frame past the point gave p99 12.7–12.8 ms, but on the AV layer it cost +0.41 points of CPU (two wakeups and a split frame every refresh). Sleeping once didn't change that, and neither did timed AV presentation (PTS on the refresh grid, p99 only −1.4 ms). Only the early tail needs moving.

Measurements. One quiet window, Zeron hidden, one binary (tiles = #551's path), 8 interleaved ScreenCaptureKit captures each (scripts/framepace.swift), and 4 interleaved suite sampler runs (scripts/macsample.swift) each. The desktop was busy, so compare within the run.

variant p99 median [range] p95 fps CPU (ps)
main (AV layer) 20.55 ms [18.9–21.2] 16.81 103.3 5.24%
tiles (#551) 17.69 ms [17.4–17.9] 12.78 107.1 4.47%
tiles + this 13.63 ms [13.3–15.0] 12.48 108.4 4.47%
tiles + hold every frame (diagnostic) 12.79 ms [12.7–13.1] 11.95 107.4 4.53%
GPUI Fast 13.62 ms [13.2–17.0] 12.70 107.5 6.24%

No variant had long intervals (>25 ms).

variant CPU, one core GPU power WindowServer CPU / GPU footprint
main (AV layer) 4.84% 0.1551 W 25.2% / 11.19% 253.3 MB
tiles (#551) 4.03% 0.1557 W 23.8% / 11.19% 241.8 MB
tiles + this 4.06% 0.1544 W 24.5% / 11.19% 241.7 MB
GPUI Fast 6.26% 0.1831 W 27.1% / 6.40% 253.4 MB

Offline (10 s runs, /usr/bin/time -l, 3 alternating rounds), against tiles: involuntary context switches 10.3K vs 10.3K, instructions 2.25 G vs 2.25 G per run.

Checks:

  • only_early_frames_at_a_fast_refresh_wait_for_the_others_point covers align_wait: the early tail waits to ALIGN_AT, the main cluster and late frames don't, frames a refresh early don't, and 60 Hz never waits.
  • Windowed animate resized four times at full rate with ZGX_DAMAGE_CHECK=1: no stale pixels, a full repaint at each resize.
  • No CoreMedia threads at full rate with tiles (sample: 0, against 5 with ZGX_PRESENT_VIDEO=1).
  • fmt, clippy 1.99 -D warnings, and the zgx-desktop tests (176 passed).

The input-latency probe (macos_latency_probe, typing) is in the shared verification window with #551. Typing frames aren't refresh frames, so they never wait.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Instruction counts (cachegrind, exact) against d1212406; the gate fails above +1%.

scene base head change
heavy-both 1,415.7 M 1,415.7 M -0.00%
heavy-stream 1,359.3 M 1,359.3 M -0.00%
heavy-scroll 80.4 M 80.4 M -0.00%
lab-stream 462.1 M 462.2 M +0.03%
lab-scroll 70.3 M 70.3 M +0.01%
animate 397.6 M 397.6 M +0.00%
micro-state 59.7 M 59.7 M -0.00%
micro-vscroll-fixed 48.8 M 48.8 M -0.01%
micro-vscroll-variable 89.1 M 89.0 M -0.05%
micro-state-luau 83.0 M 83.0 M +0.01%
micro-vscroll-fixed-luau 66.3 M 66.3 M -0.01%
micro-vscroll-variable-luau 134.6 M 134.6 M +0.03%
mount 327.0 M 327.1 M +0.04%
script-update 1,700.1 M 1,700.0 M -0.01%
anime 4,791.6 M 4,791.7 M +0.00%
script-layout 4,001.4 M 4,001.9 M +0.01%
list-state 894.4 M 893.7 M -0.07%
style 2,146.7 M 2,146.7 M +0.00%
widgets 2,685.2 M 2,685.2 M +0.00%
app-mail 318.5 M 318.6 M +0.04%
app-chat 53.2 M 53.2 M +0.07%
app-dashboard 189.8 M 189.8 M -0.01%
app-dashboard-sort 126.5 M 126.5 M +0.01%
app-sketch 48.5 M 48.5 M +0.12%
app-code 100.0 M 100.0 M +0.03%
app-code-search 87.6 M 87.7 M +0.04%

At 120 Hz the display link fires about 2.5 ms before a refresh point and
a frame is ready about 1.5 ms before it, which the window server shows on
the refresh after. A few (about one frame in twenty) are ready earlier, up
to 3 ms before, and show on that refresh instead: two frames land in one
refresh, the next shows nothing new, and animate's p99 interval reads
17.7 ms rather than 13.6. Those few now wait until 1.6 ms before the point
(align_wait), one short sleep; the rest go at once. Only frames a display
refresh asked for, at a refresh under 12 ms, outside a resize's settle:
input's frames, 60 Hz and the latch's steady frames are as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wingleeio
wingleeio merged commit 5e59795 into main Oct 26, 2026
4 checks passed
@wingleeio
wingleeio deleted the flush/tile-align branch October 26, 2026 06:17
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