The Choreographer vsync callback reaches the UI thread only 2.5-6 ms
AFTER the vsync edge (measured on the phone). With a 2.5-5 ms Skia
render plus present, that left no phase margin inside the 8.3 ms slot:
51% of frames landed one slot late (judder) and ~4% two slots late
(hitches), on every build tested so far.
Attempted fixes, all measured and retired (see commit history):
- render-ahead hold (present previous frame at next render start):
the held present lands after the NEXT wake's 3.2-6 ms Choreographer
latency -> 52% one-slot late.
- present-at-end + present GATE (sleep on the UI thread to quantize
the present to the vsync grid): the gate sleep (2.5-4 ms) added to
the slot budget (wake 5.3 + core 3 + gate 3 + present 1 = 12.3 ms >
8.3 ms) -> cadence collapsed to 60 Hz.
Working fix (this commit):
- vsync.rs: an 'early waker' thread learns the vsync grid from the
callback's frame_time_nanos (vkprobe::note_vsync_edge) and wakes the
main looper ~0.8 ms BEFORE each edge while animating. Render start
moves from edge+2.5..6 to edge-0.8..+0.5, so the present lands at a
natural ~edge+3..5 ms phase with no UI-thread sleep.
- lib.rs: a render freshness guard (min gap = period - 1.5 ms) makes
the FIRST wake of each slot (early or the late Choreographer
fallback) render and rejects the second (double render = 60 Hz
collapse). Threshold sits between late-after-early (5.8 ms, reject)
and early-after-late (7.5 ms, accept).
- lib.rs: locked renders no longer consume pending_redraw (it carried
the motion invalidations; consuming it starved the follow-up
unlocked iteration -> 60 Hz collapse).
- stg.rs: IT lines now log the render decision (locked/saw_wake/
pr_before/do_render); vkprobe logs gate sleep (GATE_US, now 0).
Measured (phone, 8580-file grid, slow 4 s drags):
in-burst cadence: 8.29-8.44 ms median, p90 8.6-10.2 ms
(was 8.36 median / 10.2 p90 with 3.7-20% two-slot gaps)
in-burst >16.7 ms gaps: 0-2 per burst (was 3-13)
present-to-present: 27% exact one slot, 33% one-slot-late steady
state (imvisible), 3% f-jumps (Skia core 2.5-5 ms variance).
Residual judder lever: Skia core cost variance (next step).
Test hygiene reminders (HANDOFF 8.10): verify ResumedActivity, keep
the task clean (sz=1), record the perf-log byte offset before each
run (the file accumulates across process launches).