Skip to content

android: A57 — nice -20 through RLIMIT_NICE, without CAP_SYS_NICE - #30

Closed
utkarshdalal wants to merge 2 commits into
gamenativefrom
nice-via-rlimit-nice
Closed

utkarshdalal wants to merge 2 commits into
gamenativefrom
nice-via-rlimit-nice

Conversation

@utkarshdalal

Copy link
Copy Markdown

Base gamenative (f8edbc8), head nice-via-rlimit-nice (a051351). One new patch, android/patches/gamescope/0053-A57-nice-via-rlimit-nice.patch, in two commits (f63401b, a051351). CI: Android build (test) run 41394426789, green on a051351.

What this fixes

gamescope lowers its nice value only when it holds CAP_SYS_NICE (main.cpp → Process::SetNice(-20), Utils/Process.cpp). An Android app never holds that capability, so gamescope logged No CAP_SYS_NICE, falling back to regular-priority compute and threads and every gamescope thread ran at nice 0. Next to it:

  • the game's own threads ran at nice −5 and −2 (Dave the Diver: tids at NI -5 and NI -2 in top -H);
  • Wine's audio threads ran at −19;
  • SurfaceFlinger runs realtime.

Measured on a Retroid Pocket 6 (Android 13, Dave the Diver, DXVK, cap 60, 60 Hz mode, flip events on the display's real vsync from GameNative/gamenative-drm flip-events-on-the-real-vsync-pr):

  • About 1 % of frames missed their vblank. Over 44 s, 23 of the 25 late posts were gamescope draw spikes: gamescope's own commit came 3.9-10.8 ms late, with gamescope-xwm runnable but not scheduled for 1-8 ms.
  • The proof that the time was spent inside gamescope's draw window: after each spike, gamescope's next wakeup came earlier by about the same amount (slope 0.78, r = 0.78, n = 25). That is vblankmanager.cpp taking the spike as its new draw-time maximum, then decaying it 2 % a frame. Nothing outside gamescope can move its wakeup that way.

The device grants the app and every child RLIMIT_NICE = 40 (soft and hard) and RLIMIT_RTPRIO = 0. From /proc/<gamescope>/limits:

Max nice priority         40                   40
Max realtime priority     0                    0

That is how Linux lets an unprivileged process lower its nice value: setpriority succeeds down to 20 − rlim_cur, which is −20 here. Realtime scheduling is not granted.

The change (src/Utils/Process.cpp)

  • CanSetNice(n) is true with CAP_SYS_NICE, or with an RLIMIT_NICE soft limit that reaches n (20 − rlim_cur <= n, or RLIM_INFINITY). SetNice uses it. main therefore sets nice −20 on this path too, and every thread created after it inherits −20: gamescope-xwm, -wl, -kms, -wait, -vrmon and the vblank thread.
  • Threads started before main. nice() changes only the calling thread, and two waiter threads are started by static constructors before main runs: g_ImageWaiter (gamescope_img, steamcompmgr.cpp) and g_LibEisWaiter (gamescope-eis, wlserver.cpp). They stayed at nice 0, a gap upstream has with CAP_SYS_NICE as well.
    • On the RP6 (SteamWorld Dig 2, 30 s context-switch trace), gamescope_img, the thread that marks an Xwayland commit done, first ran 15.5 ms after the commit arrived, on a CPU 67 % busy with app threads. The commit was not done at the next wakeup, and gamescope skipped a whole period.
    • SetNice now walks /proc/self/task and gives the new value to every other thread still at the old one.
  • RestoreNice no longer needs the capability. Raising the value back needs no permission, and SetNice records values only when it lowered them. The game and the other children gamescope spawns through Process::SpawnProcess still start at the nice gamescope was started with.
  • Xwayland is forked by wlroots (xwayland/server.c), not through SpawnProcess, so it inherits −20, as it does upstream on a desktop with CAP_SYS_NICE.
  • Unchanged:
    • SetRealtime stays on CAP_SYS_NICE only (RLIMIT_RTPRIO is 0).
    • The Vulkan queues still request VK_QUEUE_GLOBAL_PRIORITY_REALTIME only with CAP_SYS_NICE, with the existing VK_ERROR_NOT_PERMITTED_KHR fallback, so both queues stay at regular priority as before.
    • vblankmanager.cpp, steamcompmgr.cpp and DRMBackend.cpp are not touched.

Verified on device

Retroid Pocket 6, payload 20261101.6 with bin/gamescope replaced by this branch's CI artifact (sha256 b5b23f33…), SteamWorld Dig 2 and Dave the Diver.

At start, gamescope logs this instead of the "falling back to regular-priority" line:

No CAP_SYS_NICE; RLIMIT_NICE permits nice -20 (threads at nice -20), realtime compute queue and scheduling unavailable.
[gamescope] [Info]  process: Nice -20 also applied to 5 thread(s) started before it was set.

Per-thread nice values during the session:

process threads at nice −20 others
gamescope gamescope-wl, gamescope_img, gamescope-kms, gamescope-xwm, gamescope-wait, gamescope-vrmon none
Xwayland zfq0, zcq0, zcfq0-2, gdrv0, gl0 disk$N and traceq0 at 19 (Mesa's own background queues, which set their own nice)

Before the patch, every one of those gamescope and Xwayland threads was at nice 0.

This patch ships as part of payload 20261102.1, together with the gamenative-drm changes (flip events on the display's vsync, FIFO under a cap, present wait from wp_presentation) and the app's compositor-chain affinity. With all three, on the RP6 at cap 60:

  • Dave the Diver with VSync on: 35.3 → 59.95 fps;
  • Dave the Diver with VSync off: 54-57 → 59.4;
  • SteamWorld Dig 2: 57.1 → 59.5.

This patch's own share of those numbers has not been isolated. What it targets is the gamescope-side draw spikes and the late gamescope_img described above.

How to check

On any device, cat /proc/$(pidof -s gamescope)/limits | grep -i 'nice\|realtime' shows the limit the patch relies on. Then:

for t in /proc/<gamescope pid>/task/*; do echo "$(cat $t/comm) $(awk '{print $19}' $t/stat)"; done

Every gamescope thread should read −20. Where RLIMIT_NICE is below 40, CanSetNice(-20) is false, nothing changes, and the old log line remains.

Utkarsh Dalal added 2 commits November 2, 2026 09:36
gamescope lowers its nice value only with CAP_SYS_NICE, which an Android
app never holds, so every gamescope thread ran at nice 0 while the game's
threads ran at -5/-2 and Wine's audio threads at -19. On a Retroid Pocket 6
(Android 13, Dave the Diver at a 60 cap) about 1 % of frames missed their
vblank, 23 of 25 late posts over 44 s being gamescope's own commit coming
3.9-10.8 ms late with gamescope-xwm runnable but not scheduled.

The device grants RLIMIT_NICE = 40 to the app and its children, which lets
setpriority reach nice -20 without the capability. Use it for the nice path
only: realtime scheduling and the REALTIME Vulkan queue stay on
CAP_SYS_NICE (RLIMIT_RTPRIO is 0 there). RestoreNice no longer needs the
capability, so Xwayland and the game still start at the inherited nice.
nice() changes only the calling thread, and g_ImageWaiter ("gamescope_img")
and g_LibEisWaiter ("gamescope-eis") are started by static constructors
before main sets nice -20, so they stayed at nice 0. On the RP6 (SteamWorld
Dig 2, 30 s live capture) gamescope_img first ran 15.5 ms after an Xwayland
commit arrived, the commit was not done at the next wakeup, and gamescope
skipped a whole period. SetNice now gives the new value to every other
thread still at the old one.

The header also records that Xwayland is forked by wlroots, not through
Process::SpawnProcess, so it inherits -20 as it does upstream.
@utkarshdalal

Copy link
Copy Markdown
Author

Closing: on its own this measured no fps change on the RP6 (daemon late count fell 4-6 -> 0-3 per 600 vsyncs, SF fps unchanged). The branch stays for a future A/B; the delivery is gamenative-drm commits for the real vsync, FIFO under a cap and the wp_presentation present wait.

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.

1 participant