Commit abd584185025076b9d90c92ce7ece802c52e7053

Sam Lantinga 2020-05-26T13:19:19

Make some changes to SDL_SetThreadPriority to try and have SDL transparently handle more of the work. 1. Comment that SDL_SetThreadPriority will make any necessary system changes when applying priority. 2. Add a hint to override SDL's default behavior for scheduler policy. 3. Modify the pthreads SDL_SetThreadPriority so that instead of just using the current thread scheduler policy it will change it to a policy that should work best for the requested priority. 4. Add hint checks in SDL_SetThreadPriority so that #3 can be overridden if desired. 5. Modify the Linux SDL_SetThreadPriority so that in the case that policy, either by SDL defaults or from the hint, is a realtime policy it uses the realtime rtkit API. 6. Prior to calling rtkit on Linux make the necessary thread state changes that rtkit requires. Currently this is done every time as it isn't expected that SDL_SetThreadPriority will be called repeatedly for a thread.

diff --git a/include/SDL_hints.h b/include/SDL_hints.h
index 44e6fd4..f0b7e52 100644
--- a/include/SDL_hints.h
+++ b/include/SDL_hints.h
@@ -764,6 +764,19 @@ extern "C" {
 #define SDL_HINT_THREAD_STACK_SIZE              "SDL_THREAD_STACK_SIZE"
 
 /**
+*  \brief  A string specifying additional information to use with SDL_SetThreadPriority.
+*
+*  By default SDL_SetThreadPriority will make appropriate system changes in order to
+*  apply a thread priority.  For example on systems using pthreads the scheduler policy
+*  is changed automatically to a policy that works well with a given priority.
+*  Code which has specific requirements can override SDL's default behavior with this hint.
+*
+*  pthread hint values are "current", "other", "fifo" and "rr".
+*  Currently no other platform hint values are defined but may be in the future.
+*/
+#define SDL_HINT_THREAD_PRIORITY_POLICY         "SDL_THREAD_PRIORITY_POLICY"
+
+/**
  *  \brief If set to 1, then do not allow high-DPI windows. ("Retina" on Mac and iOS)
  */
 #define SDL_HINT_VIDEO_HIGHDPI_DISABLED "SDL_VIDEO_HIGHDPI_DISABLED"
diff --git a/include/SDL_thread.h b/include/SDL_thread.h
index cb53011..4016358 100644
--- a/include/SDL_thread.h
+++ b/include/SDL_thread.h
@@ -54,6 +54,11 @@ typedef unsigned int SDL_TLSID;
 /**
  *  The SDL thread priority.
  *
+ *  SDL will make system changes as necessary in order to apply the thread priority.
+ *  Code which attempts to control thread state related to priority should be aware
+ *  that calling SDL_SetThreadPriority may alter such state.
+ *  SDL_HINT_THREAD_PRIORITY_POLICY can be used to control aspects of this behavior.
+ *
  *  \note On many systems you require special privileges to set high or time critical priority.
  */
 typedef enum {
diff --git a/src/core/linux/SDL_threadprio.c b/src/core/linux/SDL_threadprio.c
index 2cbc4cb..7141888 100644
--- a/src/core/linux/SDL_threadprio.c
+++ b/src/core/linux/SDL_threadprio.c
@@ -34,6 +34,8 @@
 #include "SDL_dbus.h"
 
 #if SDL_USE_LIBDBUS
+#include <sched.h>
+
 /* d-bus queries to org.freedesktop.RealtimeKit1. */
 #define RTKIT_DBUS_NODE "org.freedesktop.RealtimeKit1"
 #define RTKIT_DBUS_PATH "/org/freedesktop/RealtimeKit1"
@@ -41,6 +43,7 @@
 
 static pthread_once_t rtkit_initialize_once = PTHREAD_ONCE_INIT;
 static Sint32 rtkit_min_nice_level = -20;
+static Sint32 rtkit_max_realtime_priority = 99;
 
 static void
 rtkit_initialize()
@@ -52,10 +55,76 @@ rtkit_initialize()
                                             DBUS_TYPE_INT32, &rtkit_min_nice_level)) {
         rtkit_min_nice_level = -20;
     }
+
+    /* Try getting maximum realtime priority: this can be less than the POSIX default (99). */
+    if (!dbus || !SDL_DBus_QueryPropertyOnConnection(dbus->system_conn, RTKIT_DBUS_NODE, RTKIT_DBUS_PATH, RTKIT_DBUS_INTERFACE, "MaxRealtimePriority",
+                                            DBUS_TYPE_INT32, &rtkit_max_realtime_priority)) {
+        rtkit_max_realtime_priority = 99;
+    }
 }
 
 static SDL_bool
-rtkit_setpriority(pid_t thread, int nice_level)
+rtkit_initialize_thread()
+{
+    // Following is an excerpt from rtkit README that outlines the requirements
+    // a thread must meet before making rtkit requests:
+    //
+    //   * Only clients with RLIMIT_RTTIME set will get RT scheduling
+    //
+    //   * RT scheduling will only be handed out to processes with
+    //     SCHED_RESET_ON_FORK set to guarantee that the scheduling
+    //     settings cannot 'leak' to child processes, thus making sure
+    //     that 'RT fork bombs' cannot be used to bypass RLIMIT_RTTIME
+    //     and take the system down.
+    //
+    //   * Limits are enforced on all user controllable resources, only
+    //     a maximum number of users, processes, threads can request RT
+    //     scheduling at the same time.
+    //
+    //   * Only a limited number of threads may be made RT in a
+    //     specific time frame.
+    //
+    //   * Client authorization is verified with PolicyKit
+
+    int err;
+    struct rlimit rlimit;
+    int nLimit = RLIMIT_RTTIME;
+    pid_t nPid = 0; //self
+    int nSchedPolicy = sched_getscheduler(nPid) | SCHED_RESET_ON_FORK;
+    struct sched_param schedParam = {};
+
+    // Requirement #1: Set RLIMIT_RTTIME
+    err = getrlimit(nLimit, &rlimit);
+    if (err)
+    {
+        return SDL_FALSE;
+    }
+
+    rlimit.rlim_cur = rlimit.rlim_max;
+    err = setrlimit(nLimit, &rlimit);
+    if (err)
+    {
+        return SDL_FALSE;
+    }
+
+    // Requirement #2: Add SCHED_RESET_ON_FORK to the scheduler policy
+    err = sched_getparam(nPid, &schedParam);
+    if (err)
+    {
+        return SDL_FALSE;
+    }
+
+    err = sched_setscheduler(nPid, nSchedPolicy, &schedParam);
+    if (err)
+    {
+        return SDL_FALSE;
+    }
+
+    return SDL_TRUE;
+}
+
+static SDL_bool
+rtkit_setpriority_nice(pid_t thread, int nice_level)
 {
     Uint64 ui64 = (Uint64)thread;
     Sint32 si32 = (Sint32)nice_level;
@@ -66,6 +135,14 @@ rtkit_setpriority(pid_t thread, int nice_level)
     if (si32 < rtkit_min_nice_level)
         si32 = rtkit_min_nice_level;
 
+    // We always perform the thread state changes necessary for rtkit.
+    // This wastes some system calls if the state is already set but
+    // typically code sets a thread priority and leaves it so it's
+    // not expected that this wasted effort will be an issue.
+    // We also do not quit if this fails, we let the rtkit request
+    // go through to determine whether it really needs to fail or not.
+    rtkit_initialize_thread();
+
     if (!dbus || !SDL_DBus_CallMethodOnConnection(dbus->system_conn,
             RTKIT_DBUS_NODE, RTKIT_DBUS_PATH, RTKIT_DBUS_INTERFACE, "MakeThreadHighPriority",
             DBUS_TYPE_UINT64, &ui64, DBUS_TYPE_INT32, &si32, DBUS_TYPE_INVALID,
@@ -74,10 +151,38 @@ rtkit_setpriority(pid_t thread, int nice_level)
     }
     return SDL_TRUE;
 }
+
+static SDL_bool
+rtkit_setpriority_realtime(pid_t thread, int rt_priority)
+{
+    Uint64 ui64 = (Uint64)thread;
+    Sint32 si32 = (Sint32)rt_priority;
+    SDL_DBusContext *dbus = SDL_DBus_GetContext();
+
+    pthread_once(&rtkit_initialize_once, rtkit_initialize);
+
+    if (si32 > rtkit_max_realtime_priority)
+        si32 = rtkit_max_realtime_priority;
+
+    // We always perform the thread state changes necessary for rtkit.
+    // This wastes some system calls if the state is already set but
+    // typically code sets a thread priority and leaves it so it's
+    // not expected that this wasted effort will be an issue.
+    // We also do not quit if this fails, we let the rtkit request
+    // go through to determine whether it really needs to fail or not.
+    rtkit_initialize_thread();
+
+    if (!dbus || !SDL_DBus_CallMethodOnConnection(dbus->system_conn,
+            RTKIT_DBUS_NODE, RTKIT_DBUS_PATH, RTKIT_DBUS_INTERFACE, "MakeThreadRealtime",
+            DBUS_TYPE_UINT64, &ui64, DBUS_TYPE_INT32, &si32, DBUS_TYPE_INVALID,
+            DBUS_TYPE_INVALID)) {
+        return SDL_FALSE;
+    }
+    return SDL_TRUE;
+}
 #endif /* dbus */
 #endif /* threads */
 
-
 /* this is a public symbol, so it has to exist even if threads are disabled. */
 int
 SDL_LinuxSetThreadPriority(Sint64 threadID, int priority)
@@ -102,7 +207,7 @@ SDL_LinuxSetThreadPriority(Sint64 threadID, int priority)
 
        README and sample code at: http://git.0pointer.net/rtkit.git
     */
-    if (rtkit_setpriority((pid_t)threadID, priority)) {
+    if (rtkit_setpriority_nice((pid_t)threadID, priority)) {
         return 0;
     }
 #endif
@@ -111,6 +216,69 @@ SDL_LinuxSetThreadPriority(Sint64 threadID, int priority)
 #endif
 }
 
+/* this is a public symbol, so it has to exist even if threads are disabled. */
+int
+SDL_LinuxSetThreadPriorityAndPolicy(Sint64 threadID, int sdlPriority, int schedPolicy)
+{
+#if SDL_THREADS_DISABLED
+    return SDL_Unsupported();
+#else
+    if (schedPolicy != SCHED_RR && schedPolicy != SCHED_FIFO && setpriority(PRIO_PROCESS, (id_t)threadID, priority) == 0) {
+        return 0;
+    }
+
+#if SDL_USE_LIBDBUS
+    /* Note that this fails you most likely:
+         * Have your process's scheduler incorrectly configured.
+           See the requirements at:
+           http://git.0pointer.net/rtkit.git/tree/README#n16
+         * Encountered dbus/polkit security restrictions. Note
+           that the RealtimeKit1 dbus endpoint is inaccessible
+           over ssh connections for most common distro configs.
+           You might want to check your local config for details:
+           /usr/share/polkit-1/actions/org.freedesktop.RealtimeKit1.policy
+
+       README and sample code at: http://git.0pointer.net/rtkit.git
+    */
+    if (schedPolicy == SCHED_RR || schedPolicy == SCHED_FIFO) {
+        int rtPriority;
+
+        if (sdlPriority == SDL_THREAD_PRIORITY_LOW) {
+            rtPriority = 1;
+        } else if (sdlPriority == SDL_THREAD_PRIORITY_HIGH) {
+            rtPriority = rtkit_max_realtime_priority * 3 / 4;
+        } else if (sdlPriority == SDL_THREAD_PRIORITY_TIME_CRITICAL) {
+            rtPriority = rtkit_max_realtime_priority;
+        } else {
+            rtPriority = rtkit_max_realtime_priority / 2;
+        }
+
+        if (rtkit_setpriority_realtime((pid_t)threadID, rtPriority)) {
+            return 0;
+        }
+    } else {
+        int niceLevel;
+
+        if (sdlPriority == SDL_THREAD_PRIORITY_LOW) {
+            niceLevel = 19;
+        } else if (sdlPriority == SDL_THREAD_PRIORITY_HIGH) {
+            niceLevel = -10;
+        } else if (sdlPriority == SDL_THREAD_PRIORITY_TIME_CRITICAL) {
+            niceLevel = -20;
+        } else {
+            niceLevel = 0;
+        }
+
+        if (rtkit_setpriority_nice((pid_t)threadID, niceLevel)) {
+            return 0;
+        }
+    }
+#endif
+
+    return SDL_SetError("setpriority() failed");
+#endif
+}
+
 #endif  /* __LINUX__ */
 
 /* vi: set ts=4 sw=4 expandtab: */
diff --git a/src/thread/pthread/SDL_systhread.c b/src/thread/pthread/SDL_systhread.c
index fb96a40..2dc6024 100644
--- a/src/thread/pthread/SDL_systhread.c
+++ b/src/thread/pthread/SDL_systhread.c
@@ -184,34 +184,71 @@ SDL_ThreadID(void)
     return ((SDL_threadID) pthread_self());
 }
 
+#if __LINUX__
+/**
+   \brief Sets the SDL priority (not nice level) for a thread, using setpriority() if appropriate, and RealtimeKit if available.
+   Differs from SDL_LinuxSetThreadPriority in also taking the desired scheduler policy,
+   such as SCHED_OTHER or SCHED_RR.
+
+   \return 0 on success, or -1 on error.
+ */
+extern DECLSPEC int SDLCALL SDL_LinuxSetThreadPriorityAndPolicy(Sint64 threadID, int sdlPriority, int schedPolicy);
+#endif
+
 int
 SDL_SYS_SetThreadPriority(SDL_ThreadPriority priority)
 {
 #if __NACL__ || __RISCOS__
     /* FIXME: Setting thread priority does not seem to be supported in NACL */
     return 0;
-#elif __LINUX__
-    int value;
-    pid_t thread = syscall(SYS_gettid);
-
-    if (priority == SDL_THREAD_PRIORITY_LOW) {
-        value = 19;
-    } else if (priority == SDL_THREAD_PRIORITY_HIGH) {
-        value = -10;
-    } else if (priority == SDL_THREAD_PRIORITY_TIME_CRITICAL) {
-        value = -20;
-    } else {
-        value = 0;
-    }
-    return SDL_LinuxSetThreadPriority(thread, value);
 #else
     struct sched_param sched;
     int policy;
+    int pri_policy;
     pthread_t thread = pthread_self();
+    const char *policyhint = SDL_GetHint(SDL_HINT_THREAD_PRIORITY_POLICY);
 
     if (pthread_getschedparam(thread, &policy, &sched) != 0) {
         return SDL_SetError("pthread_getschedparam() failed");
     }
+
+    /* Higher priority levels may require changing the pthread scheduler policy
+     * for the thread.  SDL will make such changes by default but there is
+     * also a hint allowing that behavior to be overridden. */
+    switch (priority) {
+    case SDL_THREAD_PRIORITY_LOW:
+    case SDL_THREAD_PRIORITY_NORMAL:
+        pri_policy = SCHED_OTHER;
+        break;
+    case SDL_THREAD_PRIORITY_HIGH:
+    case SDL_THREAD_PRIORITY_TIME_CRITICAL:
+        pri_policy = SCHED_RR;
+        break;
+    default:
+        pri_policy = policy;
+        break;
+    }
+
+    if (policyhint) {
+        if (SDL_strcmp(policyhint, "current") == 0) {
+            /* Leave current thread scheduler policy unchanged */
+        } else if (SDL_strcmp(policyhint, "other") == 0) {
+            policy = SCHED_OTHER;
+        } else if (SDL_strcmp(policyhint, "rr") == 0) {
+            policy = SCHED_RR;
+        } else if (SDL_strcmp(policyhint, "fifo") == 0) {
+            policy = SCHED_FIFO;
+        } else {
+            policy = pri_policy;
+        }
+    } else {
+        policy = pri_policy;
+    }
+
+#if __LINUX__
+    pid_t thread = syscall(SYS_gettid);
+    return SDL_LinuxSetThreadPriorityAndPolicy(thread, priority, policy);
+#else
     if (priority == SDL_THREAD_PRIORITY_LOW) {
         sched.sched_priority = sched_get_priority_min(policy);
     } else if (priority == SDL_THREAD_PRIORITY_TIME_CRITICAL) {
@@ -242,6 +279,7 @@ SDL_SYS_SetThreadPriority(SDL_ThreadPriority priority)
     }
     return 0;
 #endif /* linux */
+#endif /* #if __NACL__ || __RISCOS__ */
 }
 
 void