SDL: emscripten: Make timers work (if used with emscripten_set_main_loop)

From d950b9e2d9c756c7fc5641d75d9f7dd1a19ceac8 Mon Sep 17 00:00:00 2001
From: Charlie Birks <[EMAIL REDACTED]>
Date: Sun, 7 Nov 2021 20:40:54 +0000
Subject: [PATCH] emscripten: Make timers work (if used with
 emscripten_set_main_loop)

Co-authored-by: aidanhs <aidanhs@cantab.net>
---
 src/timer/SDL_timer.c | 112 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 112 insertions(+)

diff --git a/src/timer/SDL_timer.c b/src/timer/SDL_timer.c
index 559fdf5b2d..7ed1720455 100644
--- a/src/timer/SDL_timer.c
+++ b/src/timer/SDL_timer.c
@@ -28,6 +28,8 @@
 
 /* #define DEBUG_TIMERS */
 
+#if !defined(__EMSCRIPTEN__) || !SDL_THREADS_DISABLED
+
 typedef struct _SDL_Timer
 {
     int timerID;
@@ -370,6 +372,116 @@ SDL_RemoveTimer(SDL_TimerID id)
     return canceled;
 }
 
+#else
+
+#include <emscripten/emscripten.h>
+
+typedef struct _SDL_TimerMap
+{
+    int timerID;
+    int timeoutID;
+    struct _SDL_TimerMap *next;
+} SDL_TimerMap;
+
+typedef struct {
+    int nextID;
+    SDL_TimerMap *timermap;
+} SDL_TimerData;
+
+static SDL_TimerData SDL_timer_data;
+
+static void
+SDL_Emscripten_TimerHelper(SDL_TimerMap *entry, Uint32 interval, SDL_TimerCallback callback, void *param)
+{
+    Uint32 new_timeout;
+
+    new_timeout = callback(interval, param);
+
+    if (new_timeout != 0) {
+        entry->timeoutID = EM_ASM_INT({
+            return Browser.safeSetTimeout(function() {
+                dynCall('viiii', $0, [$1, $2, $3, $4]);
+            }, $2);
+        }, &SDL_Emscripten_TimerHelper, entry, interval, callback, param);
+    }
+}
+
+int
+SDL_TimerInit(void)
+{
+    return 0;
+}
+
+void
+SDL_TimerQuit(void)
+{
+    SDL_TimerData *data = &SDL_timer_data;
+    SDL_TimerMap *entry;
+
+    while (data->timermap) {
+        entry = data->timermap;
+        data->timermap = entry->next;
+        SDL_free(entry);
+    }
+}
+
+SDL_TimerID
+SDL_AddTimer(Uint32 interval, SDL_TimerCallback callback, void *param)
+{
+    SDL_TimerData *data = &SDL_timer_data;
+    SDL_TimerMap *entry;
+
+    entry = (SDL_TimerMap *)SDL_malloc(sizeof(*entry));
+    if (!entry) {
+        SDL_OutOfMemory();
+        return 0;
+    }
+    entry->timerID = ++data->nextID;
+
+    entry->timeoutID = EM_ASM_INT({
+        return Browser.safeSetTimeout(function() {
+            dynCall('viiii', $0, [$1, $2, $3, $4]);
+        }, $2);
+    }, &SDL_Emscripten_TimerHelper, entry, interval, callback, param);
+
+    entry->next = data->timermap;
+    data->timermap = entry;
+
+    return entry->timerID;
+}
+
+SDL_bool
+SDL_RemoveTimer(SDL_TimerID id)
+{
+    SDL_TimerData *data = &SDL_timer_data;
+    SDL_TimerMap *prev, *entry;
+
+    /* Find the timer */
+    prev = NULL;
+    for (entry = data->timermap; entry; prev = entry, entry = entry->next) {
+        if (entry->timerID == id) {
+            if (prev) {
+                prev->next = entry->next;
+            } else {
+                data->timermap = entry->next;
+            }
+            break;
+        }
+    }
+
+    if (entry) {
+        EM_ASM_({
+            window.clearTimeout($0);
+        }, entry->timeoutID);
+        SDL_free(entry);
+
+        return SDL_TRUE;
+    }
+    return SDL_FALSE;
+}
+
+#endif
+
 /* This is a legacy support function; SDL_GetTicks() returns a Uint32,
    which wraps back to zero every ~49 days. The newer SDL_GetTicks64()
    doesn't have this problem, so we just wrap that function and clamp to