On-glass (A024, 120 Hz panel, 120 fps session) the first presenter build
released only 60/s and the HUD display term hit 40 ms. Root cause, in two
layers: Android down-rates a game-category uid's choreographer stream to
60 Hz (frame-rate categories / game default frame rate), and under that
override Display.getRefreshRate REPORTS THE OVERRIDE — so the presenter's
panel grid read 16.67 ms on an 8.33 ms panel and the subdivision became a
no-op, pacing the video at half rate and dropping every other frame.
Three-part fix, verified live on the same device:
- Kotlin passes the panel rate from the supported-modes TABLE
(MainActivity.streamPanelFps — the mode list is not override-filtered)
instead of display.refreshRate, and votes the app's render rate up via
View.requestedFrameRate = streamHz (API 35+) while streaming.
- The native vsync clock LEARNS the panel period from observed timeline
spacing (downward-only: the finest spacing SurfaceFlinger ever reports is
the true grid) and next_target subdivides the reported timeline onto it —
full-rate on down-rated devices, a no-op where callbacks match the panel.
- OnFrameRendered display/latch samples get the e2e clamp (0..10 s): a
vendor's first callbacks can carry a garbage system_nano (observed: an
epoch-sized latch max) that would poison every max it lands in.
pf.present gained panelMs next to vsyncMs, and a one-shot cadence
diagnostic logs Δ/timelines/spacing/panel on the third tick.
After: released=120 displays=120 paced=0, pace p50 <1 ms, latch p50 ~16 ms
idle / ~22 ms under game load (2 refresh intervals at 8.33 — the same
composited-pipeline law the Apple client measured), HUD display ~17-26 ms
vs 40 before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>