gallery/docs/BUGS/hypotheses-120hz.md
Greg Pomerantz 8f06b85376 120Hz: re-enable render-ahead under the corrected timing model
The 'callback arrives 3-6 ms late' model that invalidated render-ahead in
earlier sessions was a mislabeled STG field: the Choreographer callback
actually fires at edge+0.5 ms (measured from the VSYNC ftn/mon pair). With
the wake ON the edge, the held frame presented at render START lands at
edge+~1.5 ms (constant phase, 4 ms of deadline margin), and render spikes
(5-10 ms, the measured cause of the residual 1% drops + 1.5 ms phase
spread) only stretch the PREPARE phase of the frame held for the NEXT
slot — spikes stop delaying visible frames.

- lib.rs: RENDER_AHEAD_ACTIVE = locked again (was false)
- lib.rs/stg.rs: IT lines gain rcpu (CPU us inside do_render) — spike
  classifier: rcpu≈render ⇒ Skia/driver CPU work; rcpu≪render ⇒ blocked
  on GPU/driver
- hypotheses-120hz.md: model correction + sltest baseline + results

Emulator behavior verified: scroll, settle (transition flush), mid-drag
pause/resume (no black window), no AlreadyAcquired panics, held-frame
presents at 0.6-2.1 ms. Phone cadence measurement pending (phone out for
user use ~2h).
2026-10-30 12:12:44 -04:00

93 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 120 Hz zero-jank campaign — hypothesis log
Target: sltest (minimal 100-row grid, no textures) on the phone's Mali G715.
Definition of ZERO JANK (measured from PRES mon= + VSYNC ftn= lines):
1. no display-slot skip: for consecutive presents, slot_index(n+1) == slot_index(n)+1 (100%)
2. no same-slot double: two presents never land in the same display slot
3. phase stable: present phase within its slot varies < 0.5 ms
Test: 2x `input swipe 640 1900 640 900 4000` (slow drags) + fling, ~10 s.
A "burst" = consecutive presents with gaps < 20 ms; evaluate inside bursts only.
## Hypotheses (order = priority)
- H6 [sltest confound]: sltest FileLogger is blocking (flush per line) on the
render path (PRES/STG/IT at ~240 lines/s).
FIX: non-blocking drain-thread logger. TEST: rebuild, re-measure baseline.
- H1: early waker misses edges (thread descheduled / nanosleep jitter) →
render starts late (edge+5 late wake wins) → present phase bimodal.
TEST: render-start phase histogram vs edges; early-wake hit rate.
- H2: Skia render core variance → present phase jitter.
TEST: VK SKD distribution in sltest (trivial scene, expect <1.5 ms).
- H3: wgpu/Mali submit+present latency variance (CLR/SKF/ORD/PRE).
TEST: VK PRE / CLR / SKF distributions in sltest.
- H4: event-loop overhead (timers, change tracker, spin iterations) eats the
slot budget.
TEST: IT poll/timers/ev/fe distributions; iters/s during scroll.
- H5: CPU scheduling of the UI thread (deschedule mid-slot, core migration,
frequency).
TEST: wall vs CPU split for the render window (vkprobe thread_cpu_ns).
- H7 [method confound]: adb `input swipe` motion is bursty (~20-60 Hz) →
grid movement itself is non-uniform regardless of render cadence.
TEST: pointer-location overlay + user finger, later.
## Rules
- One hypothesis per build; keep the change minimal; measure before/after
with the identical test; record numbers here.
- A hypothesis is RULED OUT only when the measurement it predicts is absent
(not "the fix didn't help").
- Phone hygiene before each run: clean task (sz=1), ResumedActivity,
dismiss 16KB dialog (sltest is debuggable), record log byte offsets.
## Results
### 2026-07-09 — sltest baseline + model correction (builds slt1-slt3)
- **H6 RULED OUT / FIXED**: sltest's blocking FileLogger replaced with a
non-blocking drain-thread logger. No crash, no behavior change beyond
log I/O coming off the render path.
- **MODEL CORRECTION (major)**: the "Choreographer callback arrives 2.5-6 ms
after the edge" belief from earlier sessions was WRONG — it came from the
STG `wake=` field, which actually measures the poll's block time (first
event), not callback latency. Measured from the VSYNC line itself
(ftn = vsync edge, mon = callback ran): **callback delivery = edge+0.52 ms
median, p90 0.65, max 1.8 ms** (sltest, phone). The callback fires ON the
edge.
- **H5 RULED OUT for the poll phase**: `pcpu` (CPU time inside
poll_events) = 2% of wall — the UI thread sleeps in epoll normally; it is
not spinning or descheduled while waiting. The wake→poll path is healthy:
poll start ≈ edge−2.3, byte at edge+0.5, return at edge+0.5, render 2.3-4.3
ms, present ≈ edge+3-4 ms — INSIDE the 5.5 ms deadline at the median.
(The early-waker's edge−0.8 wake is redundant with the on-edge
Choreographer wake — both are fine; the waker stays as fallback.)
- **Baseline zero-jank metrics (sltest, phone, 991 transitions)**:
DROPPED 0.71%, WASTED 0.91%, phase p10-p90 = 2.64-4.13 ms (1.5 ms
spread), 12.5% consecutive phase jumps >1 ms. Median behavior is correct;
the visible judder is the variance.
- **H2/H3 CONFIRMED (residual cause)**: per-frame render = 3.5 ms median
but spikes to 10.7 ms. Stage breakdown (sltest, phone): SKD (Skia draw)
1.07 ms median / 5.2 max; CLR (wgpu clear submit) 0.8 / 3.2; PRE 1.0 / 2.5;
SKF 0.3; ORD 0.35. Spikes push presents past the deadline (the drops) and
shake the phase (the judder). GPU submit overhead (2.45 ms median) exceeds
the actual scene draw (1.07 ms) — four wgpu submits per frame.
- **Next (implemented, phone-measurement pending)**: RENDER-AHEAD
re-enabled under the corrected model. With the wake on the edge, the held
frame is presented at the START of the render (edge+~1.5 ms, constant
phase, 4 ms margin) and a spike only stretches the PREPARE phase of the
frame held for the next slot — spikes stop delaying visible frames.
Behavior verified on the emulator (scroll, settle, mid-drag pause/resume,
no AlreadyAcquired, no black window). The phone is the only remaining
test: expect drops → ~0 and phase spread → <0.5 ms IF spikes were the
cause; `rcpu` (CPU time inside do_render) classifies any residual spikes
(CPU work vs GPU/driver block).
- **Emulator caveat**: the emulated Choreographer fires 9-10 ms late and the
SF pipeline is slow — the emulator cannot reproduce or validate 120 Hz
cadence; it is for behavior verification only. Phone remains the test
target.
### Pending when the phone is back
1. Install slt3+ (render-ahead + rcpu), clean task, 2×4 s drags.
2. Run sltslot.py: target DROPPED=0, WASTED=0, phase spread <0.5 ms.
3. If residual: classify spikes via rcpu (≈render ⇒ CPU/Skia; ≪render ⇒
GPU/driver block), then attack the top stage (CLR submit back-pressure vs
SKD Skia stalls vs PRE present cost).