android: A52 — release limited frames on the vblank time grid, not on every Nth wakeup

Closed
UT
11d ago
+100-0

What this fixes

The non-VRR frame-rate limit counts compositor wakeups: a window gets its frame callback on every refresh / targetth wakeup (steamcompmgr_should_vblank_window, vblank_idx % nVblankDivisor). That equals every Nth vblank only while no wakeup is ever late. CVBlankTimer::GetNextVBlank steps to the next vblank after now, so a wakeup that lands more than one vblank late skips the vblanks in between, and vblank_idx never counts them.

A session runs on the 1200 Hz grid (-r 1200, the nested refresh: GetRefresh() returns it, and so does every g_nNestedRefresh ? g_nNestedRefresh : g_nOutputRefresh read in the compositor loop), where a wakeup is late by a vblank after 0.83 ms of scheduling delay. Every late wakeup holds the next release a whole vblank longer, so the cap lands below the target in proportion to the wakeups lost, and it sinks further as the SoC throttles and the compositor thread is scheduled later still. That is the shape of the reports: a 60 cap reading 45-57 and a 30 cap reading ~25 on the same device (both about 83 % of the target), flat 18-21 ms frames with the CPU and the GPU idle, gamescope at the X11 rate for the first minutes and lower afterwards.

The change

A52 keeps the release grid in time instead of in wakeups:

  • g_ulFPSLimitNextRelease is the previous release plus the target period; a frame is released on the first vblank whose nominal time (VBlankTime::schedule.ulTargetVBlank) reaches it, with half a vblank of tolerance so a moving anchor cannot push a release to the next vblank.
  • A late wakeup delays that one release by its lateness and the grid absorbs it on the next, so the average rate is the target.
  • A stall longer than a period is a lost frame, never a debt: the grid re-anchors rather than releasing on back-to-back vblanks.
  • Computed once per vblank in the compositor loop before any window is latched, so every should_vblank_window call in that iteration agrees.

The grid is the nested refresh and the target fps alone. The panel's rate plays no part in the limiter, as before: g_nOutputRefresh keeps its uses (mode, Xwayland re-advertise, mangoapp, feedback atom). The VRR branch is untouched (it already schedules by time), and g_SteamCompMgrLimitedAppRefreshCycle (the cycle hinted to clients) is unchanged.

Patch 0050-A52-fps-limit-on-the-vblank-time-grid.patch applies after A51; the full train applies cleanly on this base.

Verification

  • Train check: all 50 patches apply on a clean checkout of this branch.
  • CI build on this PR.
  • Device: Odin 3 (8 Elite) holds an exact 60 on both the old and the new limiter, because its compositor thread keeps up with the grid; the shortfall reproduces on the Adreno 740 phones in the reports, so that is where this needs to be run (a capped GL/X11-drawn game, since Vulkan games under gamescope are paced by the ICD shim's own cap, fixed separately in GameNative/gamenative-drm#34).

Stacked on #20 (base branch fix/gamescope-resolution-scanout) so it ships in the same payload.