Three root causes, all fixed and verified on the Pixel 9 Pro:
1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the
render to the vsync wake; RENDER_AHEAD presents the previous frame at
the start of render() (phase = wake + ~1.1 ms) and holds the new
frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never
merely dropped, before the next acquire (AlreadyAcquired panic at
launch otherwise); discard_pending_frame presents too.
2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled
through a ~20-cell viewport forever (137 decodes/20 s) and each
eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes,
~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone
cooldown on evicted paths (idle prefetch skips, urgent tap bypasses)
+ off-thread reaper for the freed blobs (insert: 21 ms -> 4 us).
3. (earlier) Skia final flush + Mailbox present mode (vendor, M3).
Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK
lines for cross-thread closure timing and push-site tagging.
Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact
120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero
>33 ms); PRES phase median ~994 us, zero late frames. Docs updated
(docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference).
Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch),
vkshim profiling tooling, backend made a non-optional dep, debuggable
flag for simpleperf (REMOVE before shipping).
The 19-26 ms mid-scroll stalls (Mali-only, fixed content offsets) were
the Skia renderer's final synchronous GrDirectContext::flush() blocking
the UI thread on Mali driver back-pressure — proven by per-stage
timestamps in the vendored renderer (stall frames: flush 19-24 ms,
walk 1-3 ms, 1 binding re-eval, image imports 0.1 ms each).
- vendor/i-slint-renderer-skia (pinned 1.18.1, [patch.crates-io]):
remove that one flush. Redundant on the wgpu backend: the surface
already calls flush_and_submit before queue.present, same Vulkan
queue, in-order. Phone: 0 stalls across 4 runs (max 3.6-7.4 ms vs
the 8.3 ms budget), display verified clean.
- docs/BUGS/slint-skia-final-flush-jank.md: full evidence, prior art
(slint-ui/slint#12030 closest), open question for a future
upstream filing (not filed yet).
- Profiling also retired the O(n)-walk theory: Slint 1.18 prunes the
item walk to ~133-152 items; the windowed Flickable built on that
theory was already reverted (ListView is smooth with the fix).
- Strip all jank-hunt instrumentation: FR/SCR/TICK/APLYD/STG logging,
stg markers, .gallery_* file knobs, jsamp thread sampler,
DbgScroll programmatic scroll, nblog perf-file mirroring.
Also fixes the 'launches into unstoppable fast scroll' bug: a
leftover .gallery_cmd was re-read on every launch (file never
deleted) and overrode user swipes — the hook is gone.
- Keep: nblog (logd backpressure is real), zero-dimension decode
guards, keep-alive (default-off).
- HANDOFF: replace stale windowed-Flickable section with the actual
root cause/fix; 15 unit tests pass.
Verified on phone (aarch64, 8580 photos) and emulator (x86_64).