shim: a late start keeps its slot's timeline (FPS limiter ignored at 120 Hz)
Report
App master with payload 20261103.6 (shim 762e559): the FPS limiter set to 20 under gamescope on a 120 Hz panel left MGS V: The Phantom Pain at 40-50 fps. At 60 Hz the cap held.
Reproduced
Retroid Pocket 6 at 120 Hz, DTD 1920x1080, cap 20, container env mailbox:
present mode: declared=mailbox requested=immediate source=env route=gamescope cap=20 gate=vsync ... gate_holds=188 gate_late=8014 skipped=13325 ... spacing=[<15:0 15-16.2:0 16.2-17.2:0 17.2-20:524 >20:4]
present mode: ... gate_holds=188 gate_late=8541 skipped=14000 ...
present mode: ... gate_holds=188 gate_late=9068 skipped=14675 ...
The cap reached the shim. The gate held 188 frames, then never held another one: gate_late grows by one per frame and the game runs at its own ~53 fps.
Cause
GameNative/gamenative-drm#47 (1cc98e1) carried two changes. The fix for JT's 40 fps capture, the gate off under the cap-rate FIFO declaration, stays. The second, "the release after a late start is never more than one grid instant after it", is the bug: a cap below the display's rate holds the guest for a whole slot (six vblanks at 20 on 120 Hz), and after that rule fired once the next release was one vblank after the late start. A guest whose frame outlasts one period plus ON_TIME_NS (0.5 ms) is late for that release too, and for every one after it, so the gate never holds again. At 120 Hz (8.3 ms) that is every game; at 60 Hz a guest faster than ~58 fps recovers, which is why a 60 Hz panel hides it.
Change
Schedule::book (icd_shim/src/vsync_gate.rs): a start late by less than a period is booked to its release point, as before. A start late by a period or more but still inside its slot is booked to the grid instant it is in and keeps the slot's ideal timeline, so the release after it is where the cap puts it, at most the rest of the slot away. A start past the whole slot restarts the timeline from the instant it is in, so a stall carries no debt. The cap-rate cell is unchanged (slot == period), so the #47 capture stays fixed.
Trade: a late frame's successor can be held for the rest of its slot instead of coming out one vblank later; that is what a limiter does on a hitch, and the cap holds on average.
Tests: the late-frame-on-a-divisor-cap test now expects the slot's release (two vblanks at 30 on 60) and covers late by a whole vblank and past the slot; a new test runs a guest with 18 ms frames, 1 ms late wake-ups and one 80 ms hitch under cap 20 on 120 Hz (and 22 ms frames under 20 on 60 Hz) and requires the 6- (3-) vblank cadence apart from the frame after the hitch. The 16 ms guest under cap 60 on 59.97 Hz still gets every vblank.
docs/15 states the rule and why the next-grid-instant release is gone.
Verification
- Android cross-build of the shim: OK.
- Unit tests: 90 of 90 (
adrenotools_gamescopelib suite) on the RP6 through an adb cargo runner. - Device: RP6 at 120 Hz, DTD, caps 20/40/60 live: 20.0 / 40.0 / 53 (game-bound); 100 s run 20.02 fps. See the comments.
Delivery: payload gamescope-gamenative-20261103.7.tzst (sha256 1b2dc730…4f55, shim b10cb509…9122 from 5d1e0ae) is on both CDN hosts; app PR utkarshdalal/GameNative#2336 bumps GamescopePayload.VERSION and depends on this PR.
EOF
)