gamescope: frames on the display's real vsync, FIFO under a cap, compositor off SurfaceFlinger's cores (payload 20261102.1)

Closed
UT
8d ago
+188-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:

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=mailbox made 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.so is rebuilt from gamenative-drm flip-events-on-the-real-vsync-pr: sha256 d48e27cd… (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 vkWaitForPresentKHR from gamescope's wp_presentation feedback, as Mesa does.

    The payload's gamescope gets nice −20 through RLIMIT_NICE (A57).

  • powercontrol/PowerManager.kt, TaskAffinity.kt, display/GamescopeServerComponent.java, cpp/gnaffinity + prebuilt libgnaffinity.so keep gamescope, its Xwayland and the gndrm threads on the online cores minus SurfaceFlinger's Cpus_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_setaffinity is 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.so sha256 61272eb7…; source, build script and provenance are in app/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.

membersha256from
bin/gamescopeb5b23f33b25a81a57720109109759963e1be1bf8e28d7a3cfc80fcd8f2684ec3gamescope nice-via-rlimit-nice a051351 (CI run 41394426789)
lib/libadrenotools_gamescope.sob9aedd0103a542bf2772b1ae077dc11a82e021fd5ee29e72e1c14a3ad72ac171gamenative-drm flip-events-on-the-real-vsync-pr
bin/Xwayland (unchanged)4dded8566661423ee3c91490523d96240c85906bd00e0774adc912df9dd38698as 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.

GameBeforeAfter
Dave the Diver (DXVK), in-game VSync on35.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 off54-5759.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

  1. 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's files/gamescope/.payload.version should read 20261102.1.
  2. 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.
  3. In logcat, gamescope's No CAP_SYS_NICE; RLIMIT_NICE permits nice -20 … and Nice -20 also applied to 5 thread(s)….
  4. 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.
  5. Check the rate: dumpsys SurfaceFlinger --latency over 30 s reads the panel's rate. Turning in-game VSync on in a DXVK game (Dave the Diver) no longer halves it.
  6. Regression checks:
    • an X11 session shows no Applied affinity … to gamescope line;
    • frame generation x2 at cap 40 still produces 80, with declared=mailbox lsfg=1.

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.so so 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 without CAP_SYS_NICE, and the shim declares FIFO to gamescope under a cap so the limiter is the cap, answering vkWaitForPresentKHR from wp_presentation feedback.
  • Adds libgnaffinity.so plus TaskAffinity/PowerManager changes 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.