Skip to content

android: disable the A52 vblank time-grid fps limiter patch - #26

Merged
utkarshdalal merged 2 commits into
gamenativefrom
fix/fps-limiter
Nov 1, 2026
Merged

utkarshdalal merged 2 commits into
gamenativefrom
fix/fps-limiter

Conversation

@joshuatam

@joshuatam joshuatam commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Moves 0050-A52-fps-limit-on-the-vblank-time-grid.patch out of android/patches/gamescope/ into android/patches/disabled/, where android/build-gamescope.sh does not apply it.

With the patch gone, the non-VRR frame-rate limit counts compositor wakeups again (steamcompmgr_should_vblank_window, vblank_idx % nVblankDivisor) instead of releasing on the vblank time grid. That equals every Nth vblank only while no wakeup is ever late: CVBlankTimer::GetNextVBlank steps to the next vblank after now, so a wakeup landing more than one vblank late skips the vblanks in between and vblank_idx never counts them. The finer the session grid, the sooner a wakeup is late — a sub-millisecond scheduling delay already cost a whole vblank — so each late wakeup held the next release a vblank longer and the cap read below its target: the 18-21 ms frames with the CPU and the GPU idle that #22 measured at 54 of 60. The launcher no longer passes a fine grid, which is why the patch is disabled rather than merely unapplied.

One file, a rename, so re-enabling is the reverse move. The VRR path is untouched either way, and no other patch depends on it.

Paired changes

With A52 out of the payload, the limiter the app drives is the compositor's own divisor-based
one; the app-side cap and frame generation are unchanged by this rename.

The A47 patch still said `-r` was the session's vblank grid and that a `-r 1200`
grid matched no mode; the A52 patch, disabled, diagnosed the cap against the same
grid and its 0.83 ms lateness. Neither is true of a session any more: the launcher
passes no `-r`, so the mode's rate is the only rate in the session and the grid
derives from it.

- `android/patches/gamescope/0034-A47-drm-mode-lookup-by-size.patch`: the header
  and the comment the hunk inserts state the mechanism that still matters — a
  preferred refresh the connector does not carry would find no mode at all, so
  the lookup re-finds by size and the launch line stays free to pass one. The
  hunk header is recounted after the comment shrank by a line; the patch still
  reverse-applies against the built tree, so the applied copy and the patch agree.
- `android/patches/disabled/0050-A52-fps-limit-on-the-vblank-time-grid.patch`:
  the diagnosis says the finer the session grid the sooner a wakeup is late, and
  that the launcher no longer passes one, instead of naming the grid and its
  period.

No diff body changes in either file.
@utkarshdalal
utkarshdalal merged commit 907a20c into gamenative Nov 1, 2026
1 check passed
utkarshdalal pushed a commit to utkarshdalal/GameNative that referenced this pull request Nov 1, 2026
… cap and the multiplier (#2313)

* gamescope: advertise a cap that is below the panel, and the panel otherwise

The daemon reports the panel's own rate as the session's emulated mode. That is
right for everything at or above the panel — the cap is enforced at acquire, so
the compositor's grid should stay at the rate the display can latch. Below the
panel it is not: a 30 cap on a 120 Hz panel had the connector advertise 120, the
compositor paint at 120 and the divisor base be 120, so the session ran a display
three times the rate it was asked to produce.

`applyPanelRefreshForCap` decides one number per apply: the cap when it is below
the panel, the panel otherwise, and it is called from the gamescope arm of the
limiter whenever a target changes. `refreshFpsLimit` passes the current cap too,
so a display change (and session start, which already sets the rate before
`nativeStart`) re-evaluates rather than re-reporting the panel blindly. X11
sessions are untouched: they pace through the renderer and the present extension,
not through the emulated display.

The jniLibs daemon is rebuilt from gamenative-drm `5ecab43`, which is the code
this behaviour needs: the mode is the panel's rate, the frame ring counts what
was produced, and a scan-out subscription now outlives the allocator role in both
directions.

* gamescope: the mode follows the cap and the multiplier, live

The connector advertises the rate the session is asked to produce — the cap times
the frame-generation multiplier, clamped to the panel — and gamescope has to take
it mid-session: at a 40 cap, x2 must run an 80 Hz mode and show 80 fps, x3 120
and 120, x4 120 and 160, on the QuickMenu's change alone.

`applyPanelRefreshForCap` derives that rate from the multiplier as well as the
cap, reads the multiplier from the container rather than `isLsfgAvailable` (that
is a composition-time value, and the QuickMenu's switch changes the container
without recomposing this screen), reports it, and asks gamescope to re-read the
connector when it changes — only then, because a nudge forces a modeset.
`applyLsfgMultiplier` reports the rate before the LSFG settings, so the config
the shim opens the generator from carries the rate in force instead of the one
from before the change.

- `display/GamescopeConVar.kt`: `BACKEND_SET_DIRTY`, gamescope's own re-poll.
- `display/GamescopeCommandQueue.kt`: `nudgeDisplayMode()`, a one-shot action.
- `ui/screen/xserver/XServerScreen.kt`: the produced rate in
  `applyPanelRefreshForCap`, the change-only nudge, the multiplier documented,
  and the rate applied before the LSFG settings.
- `display/GamescopePayload.kt`: payload 20261031.6.
- `jniLibs/arm64-v8a/libgamenative_drm.so`: rebuilt from the daemon commit that
  serves the rate live (`69f03ef6`).

Measured on the Retroid Pocket Nova (120 Hz panel), Hades II, one session, cap
40, changing only the multiplier: 80 Hz mode / 80.2 fps at x2, 120 / 120.0 at
x3, 120 / 160.0 at x4, back to 80 / 80.0 at x2.

* gamescope: the mode is the display's, not the session's

`applyPanelRefresh` reports the display's own rate and nothing else. The cap and the
frame-generation multiplier never enter it: the mode is what the compositor runs at, so it
changes when the display changes and at no other time. At a 40 cap the mode stays at the
panel's rate while x2 produces 80 frames a second, x3 120 and x4 160, which is what the frame
counter reports.

The mode nudge stays, and is now asked for only by the display listener: a display change
reaches the running session through it (gamescope re-reads the connector and re-selects the
mode), while a QuickMenu change moves nothing. The first report does not nudge — it is the rate
the session starts on anyway.

- `ui/screen/xserver/XServerScreen.kt`: `applyPanelRefreshForCap` becomes `applyPanelRefresh`,
  reports the display's rate, and nudges only on a change after the first report.
- `display/GamescopePayload.kt`: payload 20261031.7, whose shim no longer halves the source
  rate on a slow panel.
- `jniLibs/arm64-v8a/libgamenative_drm.so`: rebuilt (`aa33a6a4`).

Measured on the Retroid Pocket Nova, Hades II, cap 40, one session per display: 120 Hz display
80.0 / 120.0 / 160.0 at x2 / x3 / x4 with a single `selecting mode 1280x960@120Hz` and no
nudges; 60 Hz display 160.0 at x4, 120.0 at x3, 79.8 at x2 with 40.3 acquires a second;
forcing the display to 60 Hz mid-session re-selected the mode once.

* gamescope: payload 20261101.1, the shim leaves the source rate at the cap

The payload carries the gamescope build with the A52 vblank time-grid limiter
patch disabled (GameNative/gamescope#26) and the shim built from the daemon
change in GameNative/gamenative-drm#41, where `source_cap` is the user's cap as
asked: the display no longer halves it on a slow panel, and generation runs at
`multiplier - 1` frames per source frame whatever the mode.

`GamescopePayload.VERSION` must name the archive that ships with the build, so
it moves with it.

* update drm library

* Gamescope payload 20261101.4: Xwayland shared-memory attach via the sysvshm server, on the fps-limiter stack

Payload = shipped 20261031.3 + bin/gamescope (gamescope#26, A52 disabled)
+ bin/Xwayland (GameNative/xserver#1 + #2) + lib/libadrenotools_gamescope.so
(gamenative-drm#41 shim, e0f9c23). Daemon in jniLibs is JT's from #2313.

* Gamescope payload 20261101.5: merged fps-limiter daemon and shim, Xwayland shared-memory attach

Payload = shipped 20261031.3 + bin/gamescope and bin/Xwayland from the
gamescope gamenative CI (A52 disabled, #26; Xwayland from GameNative/xserver
gamenative with #1 + #2) + lib/libadrenotools_gamescope.so from
gamenative-drm master (#41 + #42). Daemon in jniLibs from the same master.

* Gamescope payload 20261101.6: Xwayland linked against the payload's libxcvt.so

20261101.5's Xwayland was built by the gamenative workflow without the
unversioned-libxcvt fix, so it needed libxcvt.so.0 and never loaded
("Xwayland server 0 did not signal ready"). 20261101.6 is the same
payload with the Xwayland built from the identical xserver source
(bd9c2cdd4 = merged gamenative) on the fixed workflow.

---------

Co-authored-by: Utkarsh Dalal <utkarsh.dalal@toptal.com>
@joshuatam
joshuatam deleted the fix/fps-limiter branch November 2, 2026 00:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants