Slint-on-Mali was conclusively janky at its base level (sltest: 0.33-0.83%
slot drops on a scene with no app logic); egui on the same phone measured
3-7x cleaner (egtest: 0.12%, same referee/scene).
Architecture (see HANDOFF §8.11-8.12):
- winit 0.30 ApplicationHandler on android-activity 0.6, raw EGL (ES3),
egui_glow::EguiGlow. winit 0.30 never delivers RedrawRequested on
Android → the frame driver is app-owned: World::tick runs every loop
iteration from about_to_wait (liveness), render when dirty or
animating, park ~8 ms (one vblank) at rest.
- Virtualized grid painter (3 cols, visible rows ±4), GPU-resident
textures, turbojpeg worker pool decoding off-thread.
- CRITICAL pixel-pipeline fix: RGB8→Vec<Color32> conversion happens on
the WORKER (egui 0.31's from_rgba_unmultiplied is O(n) per-pixel,
8-60 ms/2 MB on the UI thread → 9.57% drops + phone OOM crash).
UI thread only MOVES the vector into ColorImage.
- Fling: per-event EMA velocity (frame-based dies at slow touch sampling),
clamp 14000 px/s; decay exp(-7.42 dt).
- Painter::image uv is NORMALIZED (0..1) — a screen-space uv made every
cell a solid rectangle (the emulator's solid-color test images hid it).
- Memory caps for a personal phone: full cache 6 entries / 16 MP.
Verified on emulator: grid, scroll, fling, viewer, back-key. Phone
deploy: thumbnails render, 8580 files. Pending: user finger test +
fr_capture phone run (per-pixel fix expected to cut render cost
18.8 ms → <8.3 ms).
Deletes: vendor/ (instrumented Slint tree), shim/, build.rs, gputex,
vkshim, baseline60, main.slint — the Slint zero-jank campaign is
concluded in docs/BUGS/.
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).