SDL: Added SDL_AddTimerNS()

From 99599d923602c6d6444a316898066ff30b2c0bdd Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 27 May 2024 06:30:37 -0700
Subject: [PATCH] Added SDL_AddTimerNS()

---
 include/SDL3/SDL_timer.h          | 64 ++++++++++++++++++++++++++++++-
 src/dynapi/SDL_dynapi.sym         |  1 +
 src/dynapi/SDL_dynapi_overrides.h |  1 +
 src/dynapi/SDL_dynapi_procs.h     |  1 +
 src/timer/SDL_timer.c             | 57 ++++++++++++++++++++-------
 test/testtimer.c                  | 52 ++++++++++++++++++++-----
 6 files changed, 152 insertions(+), 24 deletions(-)

diff --git a/include/SDL3/SDL_timer.h b/include/SDL3/SDL_timer.h
index 200a7fba0d553..9bac551a599ec 100644
--- a/include/SDL3/SDL_timer.h
+++ b/include/SDL3/SDL_timer.h
@@ -132,7 +132,7 @@ extern SDL_DECLSPEC void SDLCALL SDL_DelayNS(Uint64 ns);
 typedef Uint32 SDL_TimerID;
 
 /**
- * Function prototype for the timer callback function.
+ * Function prototype for the millisecond timer callback function.
  *
  * The callback function is passed the current timer interval and returns the
  * next timer interval, in milliseconds. If the returned value is the same as
@@ -187,10 +187,72 @@ typedef Uint32 (SDLCALL *SDL_TimerCallback)(void *userdata, SDL_TimerID timerID,
  *
  * \since This function is available since SDL 3.0.0.
  *
+ * \sa SDL_AddTimerNS
  * \sa SDL_RemoveTimer
  */
 extern SDL_DECLSPEC SDL_TimerID SDLCALL SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *userdata);
 
+/**
+ * Function prototype for the nanosecond timer callback function.
+ *
+ * The callback function is passed the current timer interval and returns the
+ * next timer interval, in nanoseconds. If the returned value is the same as
+ * the one passed in, the periodic alarm continues, otherwise a new alarm is
+ * scheduled. If the callback returns 0, the periodic alarm is cancelled.
+ *
+ * \param userdata an arbitrary pointer provided by the app through SDL_AddTimer, for its own use.
+ * \param timerID the current timer being processed
+ * \param interval the current callback time interval.
+ * \returns the new callback time interval, or 0 to disable further runs of
+ *          the callback.
+ *
+ * \threadsafety SDL may call this callback at any time from a background
+ *               thread; the application is responsible for locking resources
+ *               the callback touches that need to be protected.
+ *
+ * \since This datatype is available since SDL 3.0.0.
+ *
+ * \sa SDL_AddTimerNS
+ */
+typedef Uint64 (SDLCALL *SDL_NSTimerCallback)(void *userdata, SDL_TimerID timerID, Uint64 interval);
+
+/**
+ * Call a callback function at a future time.
+ *
+ * If you use this function, you must pass `SDL_INIT_TIMER` to SDL_Init().
+ *
+ * The callback function is passed the current timer interval and the user
+ * supplied parameter from the SDL_AddTimerNS() call and should return the next
+ * timer interval. If the value returned from the callback is 0, the timer is
+ * canceled.
+ *
+ * The callback is run on a separate thread.
+ *
+ * Timers take into account the amount of time it took to execute the
+ * callback. For example, if the callback took 250 ns to execute and returned
+ * 1000 (ns), the timer would only wait another 750 ns before its next
+ * iteration.
+ *
+ * Timing may be inexact due to OS scheduling. Be sure to note the current
+ * time with SDL_GetTicksNS() or SDL_GetPerformanceCounter() in case your
+ * callback needs to adjust for variances.
+ *
+ * \param interval the timer delay, in nanoseconds, passed to `callback`
+ * \param callback the SDL_TimerCallback function to call when the specified
+ *                 `interval` elapses
+ * \param userdata a pointer that is passed to `callback`
+ * \returns a timer ID or 0 if an error occurs; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_AddTimer
+ * \sa SDL_RemoveTimer
+ */
+extern SDL_DECLSPEC SDL_TimerID SDLCALL SDL_AddTimerNS(Uint64 interval, SDL_NSTimerCallback callback, void *userdata);
+
 /**
  * Remove a timer created with SDL_AddTimer().
  *
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index 744cd0eb7ee9b..f670900605a43 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -9,6 +9,7 @@ SDL3_0.0.0 {
     SDL_AddGamepadMappingsFromIO;
     SDL_AddHintCallback;
     SDL_AddTimer;
+    SDL_AddTimerNS;
     SDL_AddVulkanRenderSemaphores;
     SDL_AllocateEventMemory;
     SDL_AndroidBackButton;
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 2e5558159c268..629a12d8d31a2 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -34,6 +34,7 @@
 #define SDL_AddGamepadMappingsFromIO SDL_AddGamepadMappingsFromIO_REAL
 #define SDL_AddHintCallback SDL_AddHintCallback_REAL
 #define SDL_AddTimer SDL_AddTimer_REAL
+#define SDL_AddTimerNS SDL_AddTimerNS_REAL
 #define SDL_AddVulkanRenderSemaphores SDL_AddVulkanRenderSemaphores_REAL
 #define SDL_AllocateEventMemory SDL_AllocateEventMemory_REAL
 #define SDL_AndroidBackButton SDL_AndroidBackButton_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index a2e016c00f6fb..1147fe02ff305 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -54,6 +54,7 @@ SDL_DYNAPI_PROC(int,SDL_AddGamepadMappingsFromFile,(const char *a),(a),return)
 SDL_DYNAPI_PROC(int,SDL_AddGamepadMappingsFromIO,(SDL_IOStream *a, SDL_bool b),(a,b),return)
 SDL_DYNAPI_PROC(int,SDL_AddHintCallback,(const char *a, SDL_HintCallback b, void *c),(a,b,c),return)
 SDL_DYNAPI_PROC(SDL_TimerID,SDL_AddTimer,(Uint32 a, SDL_TimerCallback b, void *c),(a,b,c),return)
+SDL_DYNAPI_PROC(SDL_TimerID,SDL_AddTimerNS,(Uint64 a, SDL_NSTimerCallback b, void *c),(a,b,c),return)
 SDL_DYNAPI_PROC(int,SDL_AddVulkanRenderSemaphores,(SDL_Renderer *a, Uint32 b, Sint64 c, Sint64 d),(a,b,c,d),return)
 SDL_DYNAPI_PROC(void*,SDL_AllocateEventMemory,(size_t a),(a),return)
 SDL_DYNAPI_PROC(void,SDL_AndroidBackButton,(void),(),)
diff --git a/src/timer/SDL_timer.c b/src/timer/SDL_timer.c
index 9043a2052e750..e6e5094982205 100644
--- a/src/timer/SDL_timer.c
+++ b/src/timer/SDL_timer.c
@@ -30,7 +30,8 @@
 typedef struct SDL_Timer
 {
     SDL_TimerID timerID;
-    SDL_TimerCallback callback;
+    SDL_TimerCallback callback_ms;
+    SDL_NSTimerCallback callback_ns;
     void *userdata;
     Uint64 interval;
     Uint64 scheduled;
@@ -160,8 +161,11 @@ static int SDLCALL SDL_TimerThread(void *_data)
             if (SDL_AtomicGet(&current->canceled)) {
                 interval = 0;
             } else {
-                /* FIXME: We could potentially support sub-millisecond timers now */
-                interval = SDL_MS_TO_NS(current->callback(current->userdata, current->timerID, (Uint32)SDL_NS_TO_MS(current->interval)));
+                if (current->callback_ms) {
+                    interval = SDL_MS_TO_NS(current->callback_ms(current->userdata, current->timerID, (Uint32)SDL_NS_TO_MS(current->interval)));
+                } else {
+                    interval = current->callback_ns(current->userdata, current->timerID, current->interval);
+                }
             }
 
             if (interval > 0) {
@@ -269,7 +273,7 @@ void SDL_QuitTimers(void)
     }
 }
 
-SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *userdata)
+static SDL_TimerID SDL_CreateTimer(Uint64 interval, SDL_TimerCallback callback_ms, SDL_NSTimerCallback callback_ns, void *userdata)
 {
     SDL_TimerData *data = &SDL_timer_data;
     SDL_Timer *timer;
@@ -298,9 +302,10 @@ SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *user
         }
     }
     timer->timerID = SDL_GetNextObjectID();
-    timer->callback = callback;
+    timer->callback_ms = callback_ms;
+    timer->callback_ns = callback_ns;
     timer->userdata = userdata;
-    timer->interval = SDL_MS_TO_NS(interval);
+    timer->interval = interval;
     timer->scheduled = SDL_GetTicksNS() + timer->interval;
     SDL_AtomicSet(&timer->canceled, 0);
 
@@ -329,6 +334,16 @@ SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *user
     return entry->timerID;
 }
 
+SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *userdata)
+{
+    return SDL_CreateTimer(SDL_MS_TO_NS(interval), callback, NULL, userdata);
+}
+
+SDL_TimerID SDL_AddTimerNS(Uint64 interval, SDL_NSTimerCallback callback, void *userdata)
+{
+    return SDL_CreateTimer(interval, NULL, callback, userdata);
+}
+
 int SDL_RemoveTimer(SDL_TimerID id)
 {
     SDL_TimerData *data = &SDL_timer_data;
@@ -373,8 +388,9 @@ typedef struct SDL_TimerMap
 {
     SDL_TimerID timerID;
     int timeoutID;
-    Uint32 interval;
-    SDL_TimerCallback callback;
+    Uint64 interval;
+    SDL_TimerCallback callback_ms;
+    SDL_NSTimerCallback callback_ns;
     void *userdata;
     struct SDL_TimerMap *next;
 } SDL_TimerMap;
@@ -389,10 +405,14 @@ static SDL_TimerData SDL_timer_data;
 static void SDL_Emscripten_TimerHelper(void *userdata)
 {
     SDL_TimerMap *entry = (SDL_TimerMap *)userdata;
-    entry->interval = entry->callback(entry->userdata, entry->timerID, entry->interval);
+    if (entry->callback_ms) {
+        entry->interval = SDL_MS_TO_NS(entry->callback_ms(entry->userdata, entry->timerID, (Uint32)SDL_NS_TO_MS(entry->interval)));
+    } else {
+        entry->interval = entry->callback_ns(entry->userdata, entry->timerID, entry->interval);
+    }
     if (entry->interval > 0) {
         entry->timeoutID = emscripten_set_timeout(&SDL_Emscripten_TimerHelper,
-                                                  entry->interval,
+                                                  SDL_NS_TO_MS(entry->interval),
                                                   entry);
     }
 }
@@ -414,7 +434,7 @@ void SDL_QuitTimers(void)
     }
 }
 
-SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *userdata)
+static SDL_TimerID SDL_CreateTimer(Uint64 interval, SDL_TimerCallback callback_ms, SDL_NSTimerCallback callback_ns, void *userdata)
 {
     SDL_TimerData *data = &SDL_timer_data;
     SDL_TimerMap *entry;
@@ -424,12 +444,13 @@ SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *user
         return 0;
     }
     entry->timerID = SDL_GetNextObjectID();
-    entry->callback = callback;
+    entry->callback_ms = callback_ms;
+    entry->callback_ns = callback_ns;
     entry->userdata = userdata;
     entry->interval = interval;
 
     entry->timeoutID = emscripten_set_timeout(&SDL_Emscripten_TimerHelper,
-                                              entry->interval,
+                                              SDL_NS_TO_MS(entry->interval),
                                               entry);
 
     entry->next = data->timermap;
@@ -438,6 +459,16 @@ SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *user
     return entry->timerID;
 }
 
+SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *userdata)
+{
+    return SDL_CreateTimer(SDL_MS_TO_NS(interval), callback, NULL, userdata);
+}
+
+SDL_TimerID SDL_AddTimerNS(Uint64 interval, SDL_NSTimerCallback callback, void *userdata)
+{
+    return SDL_CreateTimer(interval, NULL, callback, userdata);
+}
+
 int SDL_RemoveTimer(SDL_TimerID id)
 {
     SDL_TimerData *data = &SDL_timer_data;
diff --git a/test/testtimer.c b/test/testtimer.c
index 63456f7fc8a1c..de38842928061 100644
--- a/test/testtimer.c
+++ b/test/testtimer.c
@@ -56,6 +56,13 @@ ticktock(void *param, SDL_TimerID timerID, Uint32 interval)
     return interval;
 }
 
+static Uint64 SDLCALL
+ticktockNS(void *param, SDL_TimerID timerID, Uint64 interval)
+{
+    ++ticks;
+    return interval;
+}
+
 static Uint32 SDLCALL
 callback(void *param, SDL_TimerID timerID, Uint32 interval)
 {
@@ -136,25 +143,49 @@ int main(int argc, char *argv[])
         }
     }
 
-    /* Start the timer */
+    /* Start the millisecond timer */
     if (desired < 0) {
         desired = DEFAULT_RESOLUTION;
     }
+    ticks = 0;
     t1 = SDL_AddTimer(desired, ticktock, NULL);
 
-    /* Wait 10 seconds */
-    SDL_Log("Waiting 10 seconds\n");
-    SDL_Delay(10 * 1000);
+    /* Wait 1 seconds */
+    SDL_Log("Waiting 1 seconds for millisecond timer\n");
+    SDL_Delay(1 * 1000);
 
     /* Stop the timer */
     SDL_RemoveTimer(t1);
 
     /* Print the results */
     if (ticks) {
-        SDL_Log("Timer resolution: desired = %d ms, actual = %f ms\n",
+        SDL_Log("Millisecond timer resolution: desired = %d ms, actual = %f ms\n",
                 desired, (double)(10 * 1000) / ticks);
     }
 
+    /* Wait for the results to be seen */
+    SDL_Delay(1 * 1000);
+
+    /* Start the nanosecond timer */
+    ticks = 0;
+    t1 = SDL_AddTimerNS(desired, ticktockNS, NULL);
+
+    /* Wait 1 seconds */
+    SDL_Log("Waiting 1 seconds for nanosecond timer\n");
+    SDL_Delay(1 * 1000);
+
+    /* Stop the timer */
+    SDL_RemoveTimer(t1);
+
+    /* Print the results */
+    if (ticks) {
+        SDL_Log("Nanosecond timer resolution: desired = %d ns, actual = %f ns\n",
+                desired, (double)(10 * 1000000) / ticks);
+    }
+
+    /* Wait for the results to be seen */
+    SDL_Delay(1 * 1000);
+
     /* Test multiple timers */
     SDL_Log("Testing multiple timers...\n");
     t1 = SDL_AddTimer(100, callback, (void *)1);
@@ -170,18 +201,19 @@ int main(int argc, char *argv[])
         SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Could not create timer 3: %s\n", SDL_GetError());
     }
 
-    /* Wait 10 seconds */
-    SDL_Log("Waiting 10 seconds\n");
-    SDL_Delay(10 * 1000);
+    /* Wait 3 seconds */
+    SDL_Log("Waiting 3 seconds\n");
+    SDL_Delay(3 * 1000);
 
-    SDL_Log("Removing timer 1 and waiting 5 more seconds\n");
+    SDL_Log("Removing timer 1 and waiting 3 more seconds\n");
     SDL_RemoveTimer(t1);
 
-    SDL_Delay(5 * 1000);
+    SDL_Delay(3 * 1000);
 
     SDL_RemoveTimer(t2);
     SDL_RemoveTimer(t3);
 
+    ticks = 0;
     start_perf = SDL_GetPerformanceCounter();
     for (i = 0; i < 1000000; ++i) {
         ticktock(NULL, 0, 0);