gamescope: frames on the display's real vsync, FIFO under a cap, compositor off SurfaceFlinger's cores (payload 20261102.1)
Description
Gamescope sessions now hold the cap on the display's own vsync. Before this, they ran 1-3 % under it, and DXVK games with in-game VSync on ran at about half the rate. This PR goes together with:
- GameNative/gamenative-drm branch
flip-events-on-the-real-vsync-pr(GameNative/gamenative-drm#43); - GameNative/gamescope branch
nice-via-rlimit-nice(GameNative/gamescope#30).
What was wrong
Measured on a Retroid Pocket 6, cap 60, 60 Hz:
- Three clocks. The panel latches at 59.97-59.99 Hz. The daemon stamped gamescope's flip events on a walk of exactly 60.000 Hz, and the shim's acquire gate released frames on its own 60.000 Hz clock. Both slid across SurfaceFlinger's latch, which dropped a frame each time two buffers landed in one latch window.
- Mailbox to gamescope. The container's
MESA_VK_WSI_PRESENT_MODE=mailboxmade the shim declare every frame mailbox to gamescope, so gamescope's limiter could not hold it. - VSync on. With in-game VSync on, DXVK waits on every present. The shim answered that wait a vblank late, so the game ran at 35 fps.
- Priority. gamescope ran at nice 0 below the game's threads, because it only raises priority with CAP_SYS_NICE.
- Shared cores. The compositor chain shared CPUs 0-2 with SurfaceFlinger's and the audio stack's realtime threads, and was preempted there right around the latch.
What it does
-
jniLibs/arm64-v8a/libgamenative_drm.sois rebuilt from gamenative-drmflip-events-on-the-real-vsync-pr: sha256d48e27cd…(bc30175e…in the APK after stripping). The daemon now:- stamps flip events on the display's measured vsync, from Choreographer, on SurfaceFlinger's latch grid minus a measured lead, as a KMS driver stamps the hardware vblank;
- claims the latch from the instant the apply started;
- keeps its thread names across app upcalls, so the affinity below can find them.
-
GamescopePayload.VERSION=20261102.1(the payload is listed below). The payload's shim now:- declares FIFO to gamescope while a cap is set, as gamescope's WSI layer does on a PC, so gamescope's limiter is the cap;
- answers
vkWaitForPresentKHRfrom gamescope'swp_presentationfeedback, as Mesa does.
The payload's gamescope gets nice −20 through RLIMIT_NICE (A57).
-
powercontrol/PowerManager.kt,TaskAffinity.kt,display/GamescopeServerComponent.java,cpp/gnaffinity+ prebuiltlibgnaffinity.sokeep gamescope, its Xwayland and the gndrm threads on the online cores minus SurfaceFlinger'sCpus_allowed_list(3-7 on the RP6).- This is applied at session connect and whenever the game pin re-applies, and released at session end.
- It needs no privilege:
sched_setaffinityis allowed on any task of the app's uid. - The measurement behind it: in a 30 s trace, 269 of the 283 preemptions over 1 ms on the compositor path were on CPU 2, clustered around the latch; none over 0.7 ms on CPUs 3-6. 10 of the 20 doubled intervals in that trace had such a stall.
libgnaffinity.sosha25661272eb7…; source, build script and provenance are inapp/src/main/cpp/gnaffinity.
Payload 20261102.1
gamescope-gamenative-20261102.1.tzst, 14295767 B, sha256 5616837d976f3d4cfaaaa096529276d5f35b13f01d2ac161464c1418f7d365b4. It is 20261101.6 with two members replaced; every other member is byte-identical.
| member | sha256 | from |
|---|---|---|
bin/gamescope | b5b23f33b25a81a57720109109759963e1be1bf8e28d7a3cfc80fcd8f2684ec3 | gamescope nice-via-rlimit-nice a051351 (CI run 41394426789) |
lib/libadrenotools_gamescope.so | b9aedd0103a542bf2772b1ae077dc11a82e021fd5ee29e72e1c14a3ad72ac171 | gamenative-drm flip-events-on-the-real-vsync-pr |
bin/Xwayland (unchanged) | 4dded8566661423ee3c91490523d96240c85906bd00e0774adc912df9dd38698 | as in 20261101.6 |
Measured
Retroid Pocket 6 (Android 13, Adreno 740), gamescope, cap 60, 60 Hz mode, same scene per game, frames counted at SurfaceFlinger. Before = master with payload 20261101.6. After = this branch with payload 20261102.1.
| Game | Before | After |
|---|---|---|
| Dave the Diver (DXVK), in-game VSync on | 35.3 fps (every other frame two vblanks) | 59.95 at a 59.99 Hz panel; 0 discarded / 0 fallback present-wait answers over 120 s |
| Dave the Diver, VSync off | 54-57 | 59.4 (limited by the game's own 59.55 Hz timer) |
| SteamWorld Dig 2 (GL) | 57.1 (periodic drop zones) | 59.5, drop zones gone |
SteamWorld Dig 2's remaining ~1 % of doubled intervals is not fixed here and is being investigated separately.
Behaviour changes to know about:
- A cap that does not divide the display's rate (40 or 45 on 60 Hz) now runs at gamescope's next divisor rate (60), which is upstream gamescope's limiter.
- Under a cap, frames queue FIFO in gamescope as on a PC.
- Frame generation keeps its own path. It was not re-measured in this round.
Test steps
- Install the APK, open a game with the display server set to gamescope, and set the FPS limit to 60 on a 60 Hz panel. The payload 20261102.1 downloads on first launch. In logcat, expect
Gamescope payload 20261102.1 ready, and the app'sfiles/gamescope/.payload.versionshould read20261102.1. - In logcat:
gndrm-daemon: vsync: flip events on the display's vsync: source=choreographer-deadline period=… (59.9xx Hz, mode 60.000 Hz);- every 10 s,
vsync: check … publishes≈vsyncs late=0 … margin p10≈+3.0ms; gn-vkshim: present mode: declared=fifo … cap=60 … feedback=on presented=… discarded=0 fallback=0.
- In logcat, gamescope's
No CAP_SYS_NICE; RLIMIT_NICE permits nice -20 …andNice -20 also applied to 5 thread(s)…. - In logcat,
PowerManager: Applied affinity 3, 4, 5, 6, 7 (mask 0xf8) to gamescope (PID: …), Xwayland (PID: …), gndrm threads (TIDs: …)on the RP6. The core list follows SurfaceFlinger's mask on other devices. - Check the rate:
dumpsys SurfaceFlinger --latencyover 30 s reads the panel's rate. Turning in-game VSync on in a DXVK game (Dave the Diver) no longer halves it. - Regression checks:
- an X11 session shows no
Applied affinity … to gamescopeline; - frame generation x2 at cap 40 still produces 80, with
declared=mailbox lsfg=1.
- an X11 session shows no
Recording
Type of Change
- Bug fix
- Performance / stability improvement
- Compatibility improvements
- Other (requires prior approval)
Checklist
- If I have access to
#code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed. - This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
- I have attached a recording of the change.
- I have read and agree to the contribution guidelines in
CONTRIBUTING.md.
Summary by cubic
Gamescope sessions now hold the FPS cap on the display's own vsync instead of running just under it, and the compositor chain is moved off the cores SurfaceFlinger uses.
- Rebuilds
libgamenative_drm.soso flip events are stamped on the display's measured vsync (Choreographer, on SurfaceFlinger's latch grid) rather than a 60.000 Hz walk of the daemon's own clock. - Bumps the payload to
20261102.1: gamescope gets nice −20 via RLIMIT_NICE withoutCAP_SYS_NICE, and the shim declares FIFO to gamescope under a cap so the limiter is the cap, answeringvkWaitForPresentKHRfromwp_presentationfeedback. - Adds
libgnaffinity.soplusTaskAffinity/PowerManagerchanges that pin every gamescope, Xwayland, and gndrm thread to the online cores minus SurfaceFlinger's mask (3–7 on the RP6), applied at session connect and on game pin, released at session end; no privilege needed since all threads run under the app's uid. - Measured on a Retroid Pocket 6 at cap 60 on 60 Hz: Dave the Diver (DXVK) with VSync on went 35.3 → 59.95 fps, VSync off 54–57 → 59.4; SteamWorld Dig 2 (GL) 57.1 → 59.5.
Behavior changes to know about
- A cap that does not divide the display rate (40 or 45 on 60 Hz) now runs at gamescope's next divisor rate (60), matching upstream gamescope's limiter.
- Under a cap, frames queue FIFO in gamescope as on a PC.
- Frame generation keeps its own path and was not re-measured.
Summary by CodeRabbit
- New Features
- Game sessions now manage CPU assignments for the compositor and related processes, applying them during startup and restoring access to available cores when the session ends.
- Updates
- Updated the bundled Gamescope payload. The app now uses the updated version when checking payload readiness.