Repository: libsdl-org/SDL (C)

Issue:
Add TimerID as parameter to SDL_TimerCallback(...) callback function

# This bug report was migrated from our old Bugzilla tracker.

These attachments are available in the static archive:

* [Solves the issue. But there might be stuff I'm not seeing (3802_timerid_in_timercallback.patch, text/plain, 2018-10-16 18:43:53 +0000, 1809 bytes)](https://bugzilla.libsdl.org/attachment.cgi?id=3376)

**Reported in version:** HG 2.1
**Reported for operating system, platform:** All, All

# Comments on the original bug report:

On 2017-09-06 13:29:06 +0000,  wrote:

> Would be nice to get the TimerID as a parameter to the timer callback function.

On 2017-09-06 17:32:21 +0000, Sam Lantinga wrote:

> I agree, added for consideration for SDL 2.1.

On 2018-10-15 21:07:17 +0000, Linus Probert wrote:

> I was having a look at the SDL code and made a quick implementation of this issue just to get some practice navigating the code and... well I like to code. :D
> 
> Anywho, is this issue still relevant? Would you like to see my patch or is there already a solution in the pipe?
> 
> I haven't spent much time browsing through the documentation and stuff, but if you want my patch but want me to read through some code of conduct or format guides or whatever then please point me in the right direction.
> 
> Br,
> Linus

On 2018-10-16 16:43:16 +0000,  wrote:

> @linus.probert@gmail.com
> Sure I would like to see the patch/fix.

On 2018-10-16 18:43:53 +0000, Linus Probert wrote:

> Created attachment 3376
> Solves the issue. But there might be stuff I'm not seeing
> 
> Attaching a patch. Perhaps (likely) there is a ton of things that I should have thought of but didn't. Just get back to me with all of it and I will try to adapt.
> 
> I use SDL _alot_ so it would be wonderful to get into the source of it a bit more.

On 2018-10-17 07:49:37 +0000,  wrote:

> I haven't tested with this patch, but the fix should solve this bug. Thank you!

On 2018-10-17 09:24:40 +0000, Linus Probert wrote:

> It worked in the testbed I set up at least. My concern is how "true" you decide to be to backwards compatibility. It will require code changes for those who decide to upgrade. But perhaps that is acceptable for a minor version upgrade?

On 2018-10-17 16:05:27 +0000,  wrote:

> I reported this "bug" (more of a missing feature) as I needed this fix. Of course it will break backward compatibility but this is something you have to decide if it is acceptable in the given context.
> I assume nobody else complained about this point until now and the code is pretty old; on the other hand the fix is trivial.

On 2018-11-03 00:27:45 +0000, Sam Lantinga wrote:

> This is a good idea, and we should probably accept this patch for SDL 2.1, where we can change the ABI.





Fix (patch):
diff --git a/docs/README-migration.md b/docs/README-migration.md
index bbb8f943efaf3..16dc9ef3c50eb 100644
--- a/docs/README-migration.md
+++ b/docs/README-migration.md
@@ -1669,6 +1669,13 @@ If you were using this macro for other things besides SDL ticks values, you can
 #define SDL_TICKS_PASSED(A, B)  ((Sint32)((B) - (A)) <= 0)
 ```
 
+The callback passed to SDL_AddTimer() has changed parameters to:
+```c
+Uint32 SDLCALL TimerCallback(void *userdata, SDL_TimerID timerID, Uint32 interval);
+````
+
+The return value of SDL_RemoveTimer() has changed to the standard int error code.
+
 ## SDL_touch.h
 
 SDL_GetTouchName is replaced with SDL_GetTouchDeviceName(), which takes an SDL_TouchID instead of an index.
diff --git a/include/SDL3/SDL_timer.h b/include/SDL3/SDL_timer.h
index ecaa305c08371..5efecb34824d9 100644
--- a/include/SDL3/SDL_timer.h
+++ b/include/SDL3/SDL_timer.h
@@ -125,16 +125,23 @@ extern SDL_DECLSPEC void SDLCALL SDL_Delay(Uint32 ms);
 extern SDL_DECLSPEC void SDLCALL SDL_DelayNS(Uint64 ns);
 
 /**
- * Function prototype for the timer callback function.
+ * Definition of the timer ID type.
+ *
+ * \since This datatype is available since SDL 3.0.0.
+ */
+typedef Uint32 SDL_TimerID;
+
+/**
+ * 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
  * 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.
- * \param param an arbitrary pointer provided by the app through SDL_AddTimer,
- *              for its own use.
  * \returns the new callback time interval, or 0 to disable further runs of
  *          the callback.
  *
@@ -146,14 +153,68 @@ extern SDL_DECLSPEC void SDLCALL SDL_DelayNS(Uint64 ns);
  *
  * \sa SDL_AddTimer
  */
-typedef Uint32 (SDLCALL *SDL_TimerCallback)(Uint32 interval, void *param);
+typedef Uint32 (SDLCALL *SDL_TimerCallback)(void *userdata, SDL_TimerID timerID, Uint32 interval);
 
 /**
- * Definition of the timer ID type.
+ * 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_AddTimer() 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, and for short timeouts can potentially be called before this function returns.
+ *
+ * Timers take into account the amount of time it took to execute the
+ * callback. For example, if the callback took 250 ms to execute and returned
+ * 1000 (ms), the timer would only wait another 750 ms 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 milliseconds, 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_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 Uint32 SDL_TimerID;
+typedef Uint64 (SDLCALL *SDL_NSTimerCallback)(void *userdata, SDL_TimerID timerID, Uint64 interval);
 
 /**
  * Call a callback function at a future time.
@@ -161,25 +222,25 @@ typedef Uint32 SDL_TimerID;
  * 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_AddTimer() call and should return the next
+ * 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.
+ * The callback is run on a separate thread, and for short timeouts can potentially be called before this function returns.
  *
  * Timers take into account the amount of time it took to execute the
- * callback. For example, if the callback took 250 ms to execute and returned
- * 1000 (ms), the timer would only wait another 750 ms before its next
+ * 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 milliseconds, passed to `callback`
+ * \param interval the timer delay, in nanoseconds, passed to `callback`
  * \param callback the SDL_TimerCallback function to call when the specified
  *                 `interval` elapses
- * \param param a pointer that is passed to `callback`
+ * \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.
  *
@@ -187,24 +248,23 @@ typedef Uint32 SDL_TimerID;
  *
  * \since This function is available since SDL 3.0.0.
  *
+ * \sa SDL_AddTimer
  * \sa SDL_RemoveTimer
  */
-extern SDL_DECLSPEC SDL_TimerID SDLCALL SDL_AddTimer(Uint32 interval,
-                                                 SDL_TimerCallback callback,
-                                                 void *param);
+extern SDL_DECLSPEC SDL_TimerID SDLCALL SDL_AddTimerNS(Uint64 interval, SDL_NSTimerCallback callback, void *userdata);
 
 /**
  * Remove a timer created with SDL_AddTimer().
  *
  * \param id the ID of the timer to remove
- * \returns SDL_TRUE if the timer is removed or SDL_FALSE if the timer wasn't
- *          found.
+ * \returns 0 on success or a negative error code on failure; call
+ *          SDL_GetError() for more information.
  *
  * \since This function is available since SDL 3.0.0.
  *
  * \sa SDL_AddTimer
  */
-extern SDL_DECLSPEC SDL_bool SDLCALL SDL_RemoveTimer(SDL_TimerID id);
+extern SDL_DECLSPEC int SDLCALL SDL_RemoveTimer(SDL_TimerID id);
 
 
 /* Ends C function definitions when using C++ */
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 c69b84796c309..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),(),)
@@ -662,7 +663,7 @@ SDL_DYNAPI_PROC(int,SDL_ReleaseCameraFrame,(SDL_Camera *a, SDL_Surface *b),(a,b)
 SDL_DYNAPI_PROC(int,SDL_ReloadGamepadMappings,(void),(),return)
 SDL_DYNAPI_PROC(int,SDL_RemovePath,(const char *a),(a),return)
 SDL_DYNAPI_PROC(int,SDL_RemoveStoragePath,(SDL_Storage *a, const char *b),(a,b),return)
-SDL_DYNAPI_PROC(SDL_bool,SDL_RemoveTimer,(SDL_TimerID a),(a),return)
+SDL_DYNAPI_PROC(int,SDL_RemoveTimer,(SDL_TimerID a),(a),return)
 SDL_DYNAPI_PROC(int,SDL_RenamePath,(const char *a, const char *b),(a,b),return)
 SDL_DYNAPI_PROC(int,SDL_RenameStoragePath,(SDL_Storage *a, const char *b, const char *c),(a,b,c),return)
 SDL_DYNAPI_PROC(int,SDL_RenderClear,(SDL_Renderer *a),(a),return)
diff --git a/src/timer/SDL_timer.c b/src/timer/SDL_timer.c
index b779c7c3f3fc8..ca98271d66de1 100644
--- a/src/timer/SDL_timer.c
+++ b/src/timer/SDL_timer.c
@@ -30,8 +30,9 @@
 typedef struct SDL_Timer
 {
     SDL_TimerID timerID;
-    SDL_TimerCallback callback;
-    void *param;
+    SDL_TimerCallback callback_ms;
+    SDL_NSTimerCallback callback_ns;
+    void *userdata;
     Uint64 interval;
     Uint64 scheduled;
     SDL_AtomicInt canceled;
@@ -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((Uint32)SDL_NS_TO_MS(current->interval), current->param));
+                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,12 +273,17 @@ void SDL_QuitTimers(void)
     }
 }
 
-SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *param)
+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;
     SDL_TimerMap *entry;
 
+    if (!callback_ms && !callback_ns) {
+        SDL_InvalidParamError("callback");
+        return 0;
+    }
+
     SDL_LockSpinlock(&data->lock);
     if (!SDL_AtomicGet(&data->active)) {
         if (SDL_InitTimers() < 0) {
@@ -298,9 +307,10 @@ SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *para
         }
     }
     timer->timerID = SDL_GetNextObjectID();
-    timer->callback = callback;
-    timer->param = param;
-    timer->interval = SDL_MS_TO_NS(interval);
+    timer->callback_ms = callback_ms;
+    timer->callback_ns = callback_ns;
+    timer->userdata = userdata;
+    timer->interval = interval;
     timer->scheduled = SDL_GetTicksNS() + timer->interval;
     SDL_AtomicSet(&timer->canceled, 0);
 
@@ -329,12 +339,26 @@ SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *para
     return entry->timerID;
 }
 
-SDL_bool SDL_RemoveTimer(SDL_TimerID id)
+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;
     SDL_TimerMap *prev, *entry;
     SDL_bool canceled = SDL_FALSE;
 
+    if (!id) {
+        return SDL_InvalidParamError("id");
+    }
+
     /* Find the timer */
     SDL_LockMutex(data->timermap_lock);
     prev = NULL;
@@ -357,7 +381,11 @@ SDL_bool SDL_RemoveTimer(SDL_TimerID id)
         }
         SDL_free(entry);
     }
-    return canceled;
+    if (canceled) {
+        return 0;
+    } else {
+        return SDL_SetError("Timer not found");
+    }
 }
 
 #else
@@ -369,9 +397,10 @@ typedef struct SDL_TimerMap
 {
     SDL_TimerID timerID;
     int timeoutID;
-    Uint32 interval;
-    SDL_TimerCallback callback;
-    void *param;
+    Uint64 interval;
+    SDL_TimerCallback callback_ms;
+    SDL_NSTimerCallback callback_ns;
+    void *userdata;
     struct SDL_TimerMap *next;
 } SDL_TimerMap;
 
@@ -385,10 +414,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->interval, entry->param);
+    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);
     }
 }
@@ -410,22 +443,28 @@ void SDL_QuitTimers(void)
     }
 }
 
-SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *param)
+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;
 
+    if (!callback_ms && !callback_ns) {
+        SDL_InvalidParamError("callback");
+        return 0;
+    }
+
     entry = (SDL_TimerMap *)SDL_malloc(sizeof(*entry));
     if (!entry) {
         return 0;
     }
     entry->timerID = SDL_GetNextObjectID();
-    entry->callback = callback;
-    entry->param = param;
+    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;
@@ -434,11 +473,25 @@ SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *para
     return entry->timerID;
 }
 
-SDL_bool SDL_RemoveTimer(SDL_TimerID id)
+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;
     SDL_TimerMap *prev, *entry;
 
+    if (!id) {
+        return SDL_InvalidParamError("id");
+    }
+
     /* Find the timer */
     prev = NULL;
     for (entry = data->timermap; entry; prev = entry, entry = entry->next) {
@@ -455,10 +508,10 @@ SDL_bool SDL_RemoveTimer(SDL_TimerID id)
     if (entry) {
         emscripten_clear_timeout(entry->timeoutID);
         SDL_free(entry);
-
-        return SDL_TRUE;
+        return 0;
+    } else {
+        return SDL_SetError("Timer not found");
     }
-    return SDL_FALSE;
 }
 
 #endif /* !defined(SDL_PLATFORM_EMSCRIPTEN) || !SDL_THREADS_DISABLED */