From f63401b5ca417f2798281900b622d44138db5624 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Mon, 2 Nov 2026 09:36:11 +0530 Subject: [PATCH 1/2] =?UTF-8?q?android:=20A57=20=E2=80=94=20nice=20-20=20w?= =?UTF-8?q?hen=20RLIMIT=5FNICE=20allows=20it,=20without=20CAP=5FSYS=5FNICE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../0053-A57-nice-via-rlimit-nice.patch | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 android/patches/gamescope/0053-A57-nice-via-rlimit-nice.patch diff --git a/android/patches/gamescope/0053-A57-nice-via-rlimit-nice.patch b/android/patches/gamescope/0053-A57-nice-via-rlimit-nice.patch new file mode 100644 index 000000000..c2607931e --- /dev/null +++ b/android/patches/gamescope/0053-A57-nice-via-rlimit-nice.patch @@ -0,0 +1,117 @@ +A57 — nice -20 when RLIMIT_NICE allows it, without CAP_SYS_NICE + +gamescope only lowers its nice value when it holds CAP_SYS_NICE. An Android +app never does, so every gamescope thread ran at nice 0 ("No CAP_SYS_NICE, +falling back to regular-priority compute and threads") while the game's own +threads ran at nice -5 and -2 and Wine's audio threads at -19. On a Retroid +Pocket 6 (Android 13, Dave the Diver at a 60 cap, real-vsync daemon) about 1 % +of frames missed their vblank: 23 of 25 late posts over 44 s were gamescope +draw spikes, gamescope's own commit coming 3.9 to 10.8 ms late because +gamescope-xwm was runnable but not scheduled for 1 to 8 ms. + +The same device grants RLIMIT_NICE = 40 (soft and hard) to the app and every +child, which is how Linux lets an unprivileged process lower its nice value: +setpriority succeeds down to 20 - rlim_cur, so nice -20 here. Realtime is not +granted (RLIMIT_RTPRIO is 0). + +- CanSetNice(n): CAP_SYS_NICE, or an RLIMIT_NICE soft limit that reaches n + (20 - rlim_cur <= n, or RLIM_INFINITY). SetNice uses it. +- main sets nice -20 on that path too, before any gamescope thread exists, so + gamescope-xwm, the vblank and wait threads inherit it. SetRealtime stays on + CAP_SYS_NICE only. +- RestoreNice no longer needs CAP_SYS_NICE: raising the value back needs no + permission, and SetNice only records values when it lowered them. Children + (Xwayland, the game) still start at the nice gamescope was started with. +- The Vulkan queues are unchanged: VK_QUEUE_GLOBAL_PRIORITY_REALTIME is still + requested only with CAP_SYS_NICE (rendervulkan.cpp CreateDevice, with its + VK_ERROR_NOT_PERMITTED_KHR fallback), so without it both queues stay at + regular priority as before. + +Logs: "No CAP_SYS_NICE; RLIMIT_NICE permits nice -20 (threads at nice -20), +realtime compute queue and scheduling unavailable." once at start, instead of +the "falling back to regular-priority" line. + +diff --git a/src/Utils/Process.cpp b/src/Utils/Process.cpp +index 1a82f57..2ba91dd 100644 +--- a/src/Utils/Process.cpp ++++ b/src/Utils/Process.cpp +@@ -900,12 +900,36 @@ namespace gamescope::Process + #endif + } + ++ static bool RlimitNiceAllows( int nNice ) ++ { ++#if defined(__linux__) ++ // Without CAP_SYS_NICE, setpriority may still lower the nice value down to ++ // 20 - RLIMIT_NICE (the soft limit). This is how an unprivileged process is ++ // granted a better priority without the capability. ++ struct rlimit niceLimit{}; ++ if ( getrlimit( RLIMIT_NICE, &niceLimit ) != 0 ) ++ return false; ++ ++ if ( niceLimit.rlim_cur == RLIM_INFINITY ) ++ return true; ++ ++ return 20 - static_cast( niceLimit.rlim_cur ) <= nNice; ++#else ++ return false; ++#endif ++ } ++ ++ bool CanSetNice( int nNice ) ++ { ++ return HasCapSysNice() || RlimitNiceAllows( nNice ); ++ } ++ + std::optional g_oOldNice; + std::optional g_oNewNice; + void SetNice( int nNice ) + { + #if defined(__linux__) +- if ( !HasCapSysNice() ) ++ if ( !CanSetNice( nNice ) ) + return; + + errno = 0; +@@ -927,9 +951,9 @@ namespace gamescope::Process + void RestoreNice() + { + #if defined(__linux__) +- if ( !HasCapSysNice() ) +- return; +- ++ // Raising the nice value back needs no permission, and SetNice only ++ // records values when it was allowed to lower it (by CAP_SYS_NICE or ++ // RLIMIT_NICE), so children must be restored either way. + if ( !g_oOldNice || !g_oNewNice ) + return; + +diff --git a/src/Utils/Process.h b/src/Utils/Process.h +index 1828bce..5456233 100644 +--- a/src/Utils/Process.h ++++ b/src/Utils/Process.h +@@ -78,6 +78,7 @@ namespace gamescope::Process + pid_t SpawnProcessInWatchdog( char **argv, bool bRespawn = false, std::function fnPreambleInChild = nullptr ); + + bool HasCapSysNice(); ++ bool CanSetNice( int nNice ); + void SetNice( int nNice ); + void RestoreNice(); + +diff --git a/src/main.cpp b/src/main.cpp +index 4b51a16..0d13d0b 100644 +--- a/src/main.cpp ++++ b/src/main.cpp +@@ -885,6 +885,12 @@ int main(int argc, char **argv) + if ( g_bRt ) + gamescope::Process::SetRealtime(); + } ++ else if ( gamescope::Process::CanSetNice( -20 ) ) ++ { ++ gamescope::Process::SetNice( -20 ); ++ ++ fprintf( stderr, "No CAP_SYS_NICE; RLIMIT_NICE permits nice -20 (threads at nice %d), realtime compute queue and scheduling unavailable.\n", getpriority( PRIO_PROCESS, 0 ) ); ++ } + else + { + fprintf( stderr, "No CAP_SYS_NICE, falling back to regular-priority compute and threads.\nPerformance will be affected.\n" ); From a051351f9427b4a900f84f776b6947cf097975eb Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Mon, 2 Nov 2026 11:00:30 +0530 Subject: [PATCH 2/2] =?UTF-8?q?android:=20A57=20=E2=80=94=20also=20lower?= =?UTF-8?q?=20threads=20started=20before=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../0053-A57-nice-via-rlimit-nice.patch | 75 ++++++++++++++++--- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/android/patches/gamescope/0053-A57-nice-via-rlimit-nice.patch b/android/patches/gamescope/0053-A57-nice-via-rlimit-nice.patch index c2607931e..d8b4808ab 100644 --- a/android/patches/gamescope/0053-A57-nice-via-rlimit-nice.patch +++ b/android/patches/gamescope/0053-A57-nice-via-rlimit-nice.patch @@ -16,12 +16,24 @@ granted (RLIMIT_RTPRIO is 0). - CanSetNice(n): CAP_SYS_NICE, or an RLIMIT_NICE soft limit that reaches n (20 - rlim_cur <= n, or RLIM_INFINITY). SetNice uses it. -- main sets nice -20 on that path too, before any gamescope thread exists, so - gamescope-xwm, the vblank and wait threads inherit it. SetRealtime stays on - CAP_SYS_NICE only. +- main sets nice -20 on that path too. Threads created after it + (gamescope-xwm, -wl, -kms, -wait, -vrmon, the vblank thread) inherit it. + SetRealtime stays on CAP_SYS_NICE only. +- nice() only changes the calling thread, and two waiter threads are started + by static constructors before main: g_ImageWaiter ("gamescope_img", + steamcompmgr.cpp) and g_LibEisWaiter ("gamescope-eis", wlserver.cpp). They + stayed at nice 0 (upstream has the same gap with CAP_SYS_NICE). On the RP6 + (SteamWorld Dig 2, 30 s live capture) 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 also gives the new value + to every other thread of the process that is still at the old one. - RestoreNice no longer needs CAP_SYS_NICE: raising the value back needs no - permission, and SetNice only records values when it lowered them. Children - (Xwayland, the game) still start at the nice gamescope was started with. + permission, and SetNice only records values when it lowered them. The game + and the other children gamescope spawns still start at the nice gamescope + was started with. Xwayland is forked by wlroots (xwayland/server.c), not + through Process::SpawnProcess, so it inherits nice -20, as it does upstream + on a desktop with CAP_SYS_NICE. - The Vulkan queues are unchanged: VK_QUEUE_GLOBAL_PRIORITY_REALTIME is still requested only with CAP_SYS_NICE (rendervulkan.cpp CreateDevice, with its VK_ERROR_NOT_PERMITTED_KHR fallback), so without it both queues stay at @@ -29,13 +41,14 @@ granted (RLIMIT_RTPRIO is 0). Logs: "No CAP_SYS_NICE; RLIMIT_NICE permits nice -20 (threads at nice -20), realtime compute queue and scheduling unavailable." once at start, instead of -the "falling back to regular-priority" line. +the "falling back to regular-priority" line, and "Nice -20 also applied to +N thread(s) started before it was set." when earlier threads were moved. diff --git a/src/Utils/Process.cpp b/src/Utils/Process.cpp -index 1a82f57..2ba91dd 100644 +index 1a82f57..b94ae48 100644 --- a/src/Utils/Process.cpp +++ b/src/Utils/Process.cpp -@@ -900,12 +900,36 @@ namespace gamescope::Process +@@ -900,12 +900,70 @@ namespace gamescope::Process #endif } @@ -62,6 +75,40 @@ index 1a82f57..2ba91dd 100644 + { + return HasCapSysNice() || RlimitNiceAllows( nNice ); + } ++ ++ // nice() only changes the calling thread. Threads that already exist keep the ++ // nice they were started with, and some are started by static constructors ++ // before main gets here (g_ImageWaiter "gamescope_img", "gamescope-eis"). ++ // Give every other thread still at the old value the new one. ++ static void ApplyNiceToEarlierThreads( int nOldNice, int nNewNice ) ++ { ++#if defined(__linux__) ++ DIR *pTaskDir = opendir( "/proc/self/task" ); ++ if ( !pTaskDir ) ++ return; ++ defer( closedir( pTaskDir ) ); ++ ++ const pid_t nSelf = gettid(); ++ int nApplied = 0; ++ while ( struct dirent *pEntry = readdir( pTaskDir ) ) ++ { ++ std::optional onTid = Parse( pEntry->d_name ); ++ if ( !onTid || *onTid == nSelf ) ++ continue; ++ ++ errno = 0; ++ int nThreadNice = getpriority( PRIO_PROCESS, *onTid ); ++ if ( errno != 0 || nThreadNice != nOldNice ) ++ continue; ++ ++ if ( setpriority( PRIO_PROCESS, *onTid, nNewNice ) == 0 ) ++ nApplied++; ++ } ++ ++ if ( nApplied ) ++ s_ProcessLog.infof( "Nice %d also applied to %d thread(s) started before it was set.", nNewNice, nApplied ); ++#endif ++ } + std::optional g_oOldNice; std::optional g_oNewNice; @@ -73,7 +120,17 @@ index 1a82f57..2ba91dd 100644 return; errno = 0; -@@ -927,9 +951,9 @@ namespace gamescope::Process +@@ -920,6 +978,9 @@ namespace gamescope::Process + if ( nNewNice != -1 || errno == 0 ) + { + g_oNewNice = nNewNice; ++ ++ if ( g_oOldNice && *g_oOldNice != nNewNice ) ++ ApplyNiceToEarlierThreads( *g_oOldNice, nNewNice ); + } + #endif + } +@@ -927,9 +988,9 @@ namespace gamescope::Process void RestoreNice() { #if defined(__linux__)