# Slint 1.18 Skia renderer: redundant per-frame final `flush()` causes scroll jank on Mali Status: **worked around locally** (vendored one-line deletion, see below). Upstream issue: **NOT filed as of 2026-10-28.** Not found in upstream issues at research time (see "Prior art" below). Re-visit filing if the workaround must be maintained across Slint upgrades. ## Symptom Pixel 9 Pro (Tensor G4, **Mali G715**, 120 Hz), 8,580-item `ListView` grid of GPU-resident wgpu textures: 2–4 deterministic scroll stalls of 19–26 ms at fixed content offsets during scrolling. Same Slint code on an x86_64 emulator (SwiftShader Vulkan): **zero stalls**. ## Root cause (proven by stage-timestamp profiling) The Skia renderer's `render_components_to_canvas` ends with a **CPU-synchronous** ```rust if let Some(ctx) = gr_context.as_mut() { ctx.flush(None); // GrDirectContext::flush } ``` (stage `FL2` in the profiling below). Steady-state cost: 0.3–0.8 ms. When the Mali driver's queue is momentarily busy — new texture uploads landing (`queue.write_texture` copies), first-draw pipeline state, swapchain pressure — that call **blocks the UI thread 19.2–20.4 ms**, producing the visible stall. SwiftShader's `flush()` returns instantly, which is why the emulator never reproduced the bug and every CPU-side hypothesis (item walk, binding re-evaluation, image import, geometry, worker writes) failed. ### Per-stage frame data (Mali G715, programmatic scroll) Stages: ACQ = `get_current_texture`, MKS = swapchain surface create, DC = `draw_contents`, EV = render callback, RCI = item walk + draws, PD = partial-renderer dirty region, FL1 = pre-`BeforeRendering` flush (active only while our rendering notifier was set), **FL2 = the final flush**, SUB = `flush_and_submit` (Skia `submit()`), PRE = `queue.present`. ``` steady frames (y < -2000): SR=4-8ms DC=2-3ms RCI=1-2ms FL2=0.3-0.8ms ev=1 IMG=0-3 stall frames: y=-574.5 : SR=23.9 DC=22.1 RCI=1.0 FL2=20.4 IMG=0 y=-3543 : SR=23.4 DC=21.2 RCI=1.0 FL2=19.8 IMG=0 y=-4347 : SR=24.3 DC=21.9 RCI=2.0 FL2=19.2 IMG=3 (400x301, 301x400, 400x301) ``` Also ruled out by the same data (all small on stall frames): item walk (RCI 1–2 ms; Slint 1.18 prunes the walk to ~133–152 visited items, **not** O(rows) — an earlier O(n) walk diagnosis was wrong), binding re-evaluations (1/frame), new Skia image creation (0.0–0.1 ms each), partial renderer (0 ms), swapchain acquire (0.2 ms), submit (0.3 ms), present (0.6 ms). A second, smaller stall class (22 ms between stage markers, once per run) was wgpu driver back-pressure on a `vkQueueSubmit` (the "Slint frame init" clear pass or the texture-transition submit); it disappeared with the fix and never reappeared in verification runs. ## Why the flush is redundant (for the wgpu backend) Frame order in `i-slint-renderer-skia/wgpu_30_surface.rs::render`: 1. clear pass (wgpu encoder) + submit 2. `callback(...)` → Skia draws the frame (commands pending in Skia) 3. `flush_and_submit(gr_context)` → **`gr_context.submit(None)`** — submits all of Skia's pending commands to the same Vulkan queue 4. `queue.present(frame)` → `vkQueuePresentKHR` on the same device/queue Vulkan guarantees in-order execution on a single queue, so the present cannot run ahead of Skia's work. The final `GrDirectContext::flush()` therefore adds no ordering guarantees — only a CPU-side wait that costs 19–24 ms on Mali. (FL1, the pre-`BeforeRendering` flush, exists to make the window-background clear visible to `BeforeRendering` callbacks; it is only active while a rendering notifier is registered and was left in place.) ## Local workaround `vendor/i-slint-renderer-skia` (pinned 1.18.1, `[patch.crates-io]` in `Cargo.toml`) with that final flush removed (see the "jank fix" comment in `lib.rs::render_components_to_canvas`). The patch set is exactly this one hunk; the rest of `vendor/i-slint-renderer-skia` is byte-identical to the 1.18.1 registry source. Verification on the Pixel 9 Pro (post-fix): | test | before | after | |---|---|---| | slow scroll 5 s (×3 runs) | 2–4 stalls, 20–26 ms | 0 stalls, max 3.6 / 5.8 / 3.9 ms | | fast fling 8× velocity | stalls | 0 stalls, max 7.4 ms, p50 1.0 ms | | display | — | clean (screenshot-verified, no artifacts) | Frame budget at 120 Hz is 8.3 ms. ## Open question before filing upstream We cannot rule out that the final flush exists to bound GPU-side resource lifetime or to serialize against the (non-wgpu) backends that share `render_components_to_canvas` (OpenGL/EGL, where the Skia context and the swapchain may be on different queues). If filing, propose making the flush backend-gated or replacing it with an async submit, and cite the data above. ## Prior art (adjacent, not identical) - slint-ui/slint#12030 (open): rendering notifier + FemtoVG causes per-frame flush → pipeline cache cleared → pipeline recreation every frame. Same *class* of problem (per-frame synchronous flush interacting with GPU pipeline state), different backend and mechanism. - slint-ui/slint#11119 (open): request for a debugging tool to diagnose performance issues (this bug was found with ad-hoc stage timestamps in the vendored crates). - slint-ui/slint#3867 (open): `accessibility` feature perf impact on big listviews — checked and ruled out for this app (accessibility off).