From 23828b62d4ca329c0432ddf1e02f829cf4d0d14d Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 5 Dec 2024 12:35:23 -0800
Subject: [PATCH] Added SDL_IsMainThread() and SDL_RunOnMainThread()
---
include/SDL3/SDL_init.h | 47 ++++++++
src/SDL.c | 22 ++++
src/dynapi/SDL_dynapi.sym | 2 +
src/dynapi/SDL_dynapi_overrides.h | 2 +
src/dynapi/SDL_dynapi_procs.h | 2 +
src/events/SDL_events.c | 176 ++++++++++++++++++++++++++++++
test/testautomation_events.c | 61 +++++++++++
7 files changed, 312 insertions(+)
diff --git a/include/SDL3/SDL_init.h b/include/SDL3/SDL_init.h
index 97d08f2723977..ac0329b6ba213 100644
--- a/include/SDL3/SDL_init.h
+++ b/include/SDL3/SDL_init.h
@@ -239,6 +239,53 @@ extern SDL_DECLSPEC SDL_InitFlags SDLCALL SDL_WasInit(SDL_InitFlags flags);
*/
extern SDL_DECLSPEC void SDLCALL SDL_Quit(void);
+/**
+ * Return whether this is the main thread.
+ *
+ * On Apple platforms, the main thread is the thread that runs your program's main() entry point. On other platforms, the main thread is the one that calls SDL_Init(SDL_INIT_VIDEO), which should usually be the one that runs your program's main() entry point. If you are using the main callbacks, SDL_AppInit(), SDL_AppIterate(), and SDL_AppQuit() are all called on the main thread.
+ *
+ * \returns true if this thread is the main thread, or false otherwise.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.1.8.
+ *
+ * \sa SDL_RunOnMainThread
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_IsMainThread(void);
+
+/**
+ * Callback run on the main thread.
+ *
+ * \param userdata an app-controlled pointer that is passed to the callback.
+ *
+ * \since This datatype is available since SDL 3.1.8.
+ *
+ * \sa SDL_RunOnMainThread
+ */
+typedef void (SDLCALL *SDL_MainThreadCallback)(void *userdata);
+
+/**
+ * Call a function on the main thread during event processing.
+ *
+ * If this is called on the main thread, the callback is executed immediately. If this is called on another thread, this callback is queued for execution on the main thread during event processing.
+ *
+ * Be careful of deadlocks when using this functionality. You should not have the main thread wait for the current thread while this function is being called with `wait_complete` true.
+ *
+ * \param callback the callback to call on the main thread.
+ * \param userdata a pointer that is passed to `callback`.
+ * \param wait_complete true to wait for the callback to complete, false to return immediately.
+ * \returns true on success or false on failure; 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.1.8.
+ *
+ * \sa SDL_IsMainThread
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_RunOnMainThread(SDL_MainThreadCallback callback, void *userdata, bool wait_complete);
+
/**
* Specify basic metadata about your app.
*
diff --git a/src/SDL.c b/src/SDL.c
index 11d25bb5f9bb1..5eb9c616a85ae 100644
--- a/src/SDL.c
+++ b/src/SDL.c
@@ -183,6 +183,7 @@ static bool SDL_MainIsReady = false;
#else
static bool SDL_MainIsReady = true;
#endif
+static SDL_ThreadID SDL_MainThreadID = 0;
static bool SDL_bInMainQuit = false;
static Uint8 SDL_SubsystemRefCount[32];
@@ -250,6 +251,22 @@ static bool SDL_InitOrIncrementSubsystem(Uint32 subsystem)
void SDL_SetMainReady(void)
{
SDL_MainIsReady = true;
+
+ if (SDL_MainThreadID == 0) {
+ SDL_MainThreadID = SDL_GetCurrentThreadID();
+ }
+}
+
+bool SDL_IsMainThread(void)
+{
+ if (SDL_MainThreadID == 0) {
+ // Not initialized yet?
+ return true;
+ }
+ if (SDL_MainThreadID == SDL_GetCurrentThreadID()) {
+ return true;
+ }
+ return false;
}
// Initialize all the subsystems that require initialization before threads start
@@ -330,6 +347,11 @@ bool SDL_InitSubSystem(SDL_InitFlags flags)
goto quit_and_error;
}
+ // We initialize video on the main thread
+ // On Apple platforms this is a requirement.
+ // On other platforms, this is the definition.
+ SDL_MainThreadID = SDL_GetCurrentThreadID();
+
SDL_IncrementSubsystemRefCount(SDL_INIT_VIDEO);
if (!SDL_VideoInit(NULL)) {
SDL_DecrementSubsystemRefCount(SDL_INIT_VIDEO);
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index 83280f8919aa4..1ca616f709e40 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1201,6 +1201,8 @@ SDL3_0.0.0 {
SDL_SignalAsyncIOQueue;
SDL_LoadFileAsync;
SDL_ShowFileDialogWithProperties;
+ SDL_IsMainThread;
+ SDL_RunOnMainThread;
# extra symbols go here (don't modify this line)
local: *;
};
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 6d0bab6a77a94..691e7f184c90a 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1226,3 +1226,5 @@
#define SDL_SignalAsyncIOQueue SDL_SignalAsyncIOQueue_REAL
#define SDL_LoadFileAsync SDL_LoadFileAsync_REAL
#define SDL_ShowFileDialogWithProperties SDL_ShowFileDialogWithProperties_REAL
+#define SDL_IsMainThread SDL_IsMainThread_REAL
+#define SDL_RunOnMainThread SDL_RunOnMainThread_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 6f857012f65af..d39ad72331ab9 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1232,3 +1232,5 @@ SDL_DYNAPI_PROC(bool,SDL_WaitAsyncIOResult,(SDL_AsyncIOQueue *a, SDL_AsyncIOOutc
SDL_DYNAPI_PROC(void,SDL_SignalAsyncIOQueue,(SDL_AsyncIOQueue *a),(a),)
SDL_DYNAPI_PROC(bool,SDL_LoadFileAsync,(const char *a, SDL_AsyncIOQueue *b, void *c),(a,b,c),return)
SDL_DYNAPI_PROC(void,SDL_ShowFileDialogWithProperties,(SDL_FileDialogType a, SDL_DialogFileCallback b, void *c, SDL_PropertiesID d),(a,b,c,d),)
+SDL_DYNAPI_PROC(bool,SDL_IsMainThread,(void),(),return)
+SDL_DYNAPI_PROC(bool,SDL_RunOnMainThread,(SDL_MainThreadCallback a,void *b,bool c),(a,b,c),return)
diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c
index a792a305b6e96..f26ec9c0ff026 100644
--- a/src/events/SDL_events.c
+++ b/src/events/SDL_events.c
@@ -1175,6 +1175,177 @@ void SDL_FlushEvents(Uint32 minType, Uint32 maxType)
SDL_UnlockMutex(SDL_EventQ.lock);
}
+typedef enum
+{
+ SDL_MAIN_CALLBACK_WAITING,
+ SDL_MAIN_CALLBACK_COMPLETE,
+ SDL_MAIN_CALLBACK_CANCELED,
+} SDL_MainThreadCallbackState;
+
+typedef struct SDL_MainThreadCallbackEntry
+{
+ SDL_MainThreadCallback callback;
+ void *userdata;
+ SDL_AtomicInt state;
+ SDL_Semaphore *semaphore;
+ struct SDL_MainThreadCallbackEntry *next;
+} SDL_MainThreadCallbackEntry;
+
+static SDL_Mutex *SDL_main_callbacks_lock;
+static SDL_MainThreadCallbackEntry *SDL_main_callbacks_head;
+static SDL_MainThreadCallbackEntry *SDL_main_callbacks_tail;
+
+static SDL_MainThreadCallbackEntry *SDL_CreateMainThreadCallback(SDL_MainThreadCallback callback, void *userdata, bool wait_complete)
+{
+ SDL_MainThreadCallbackEntry *entry = (SDL_MainThreadCallbackEntry *)SDL_malloc(sizeof(*entry));
+ if (!entry) {
+ return NULL;
+ }
+
+ entry->callback = callback;
+ entry->userdata = userdata;
+ SDL_SetAtomicInt(&entry->state, SDL_MAIN_CALLBACK_WAITING);
+ if (wait_complete) {
+ entry->semaphore = SDL_CreateSemaphore(0);
+ if (!entry->semaphore) {
+ SDL_free(entry);
+ return NULL;
+ }
+ } else {
+ entry->semaphore = NULL;
+ }
+ entry->next = NULL;
+
+ return entry;
+}
+
+static void SDL_DestroyMainThreadCallback(SDL_MainThreadCallbackEntry *entry)
+{
+ if (entry->semaphore) {
+ SDL_DestroySemaphore(entry->semaphore);
+ }
+ SDL_free(entry);
+}
+
+static void SDL_InitMainThreadCallbacks(void)
+{
+ SDL_main_callbacks_lock = SDL_CreateMutex();
+ SDL_assert(SDL_main_callbacks_head == NULL &&
+ SDL_main_callbacks_tail == NULL);
+}
+
+static void SDL_QuitMainThreadCallbacks(void)
+{
+ SDL_MainThreadCallbackEntry *entry;
+
+ SDL_LockMutex(SDL_main_callbacks_lock);
+ {
+ entry = SDL_main_callbacks_head;
+ SDL_main_callbacks_head = NULL;
+ SDL_main_callbacks_tail = NULL;
+ }
+ SDL_UnlockMutex(SDL_main_callbacks_lock);
+
+ while (entry) {
+ SDL_MainThreadCallbackEntry *next = entry->next;
+
+ if (entry->semaphore) {
+ // Let the waiting thread know this is canceled
+ SDL_SetAtomicInt(&entry->state, SDL_MAIN_CALLBACK_CANCELED);
+ SDL_SignalSemaphore(entry->semaphore);
+ } else {
+ // Nobody's waiting for this, clean it up
+ SDL_DestroyMainThreadCallback(entry);
+ }
+ entry = next;
+ }
+
+ SDL_DestroyMutex(SDL_main_callbacks_lock);
+ SDL_main_callbacks_lock = NULL;
+}
+
+static void SDL_RunMainThreadCallbacks(void)
+{
+ SDL_MainThreadCallbackEntry *entry;
+
+ SDL_LockMutex(SDL_main_callbacks_lock);
+ {
+ entry = SDL_main_callbacks_head;
+ SDL_main_callbacks_head = NULL;
+ SDL_main_callbacks_tail = NULL;
+ }
+ SDL_UnlockMutex(SDL_main_callbacks_lock);
+
+ while (entry) {
+ SDL_MainThreadCallbackEntry *next = entry->next;
+
+ entry->callback(entry->userdata);
+
+ if (entry->semaphore) {
+ // Let the waiting thread know this is done
+ SDL_SetAtomicInt(&entry->state, SDL_MAIN_CALLBACK_COMPLETE);
+ SDL_SignalSemaphore(entry->semaphore);
+ } else {
+ // Nobody's waiting for this, clean it up
+ SDL_DestroyMainThreadCallback(entry);
+ }
+ entry = next;
+ }
+}
+
+bool SDL_RunOnMainThread(SDL_MainThreadCallback callback, void *userdata, bool wait_complete)
+{
+ if (SDL_IsMainThread() || !SDL_WasInit(SDL_INIT_EVENTS)) {
+ // No need to queue the callback
+ callback(userdata);
+ return true;
+ }
+
+ SDL_MainThreadCallbackEntry *entry = SDL_CreateMainThreadCallback(callback, userdata, wait_complete);
+ if (!entry) {
+ return false;
+ }
+
+ SDL_LockMutex(SDL_main_callbacks_lock);
+ {
+ if (SDL_main_callbacks_tail) {
+ SDL_main_callbacks_tail->next = entry;
+ SDL_main_callbacks_tail = entry;
+ } else {
+ SDL_main_callbacks_head = entry;
+ SDL_main_callbacks_tail = entry;
+ }
+ }
+ SDL_UnlockMutex(SDL_main_callbacks_lock);
+
+ if (!wait_complete) {
+ // Queued for execution, wait not requested
+ return true;
+ }
+
+ // Maximum wait of 30 seconds to prevent deadlocking forever
+ const Sint32 MAX_CALLBACK_WAIT = 30 * 1000;
+ SDL_WaitSemaphoreTimeout(entry->semaphore, MAX_CALLBACK_WAIT);
+
+ switch (SDL_GetAtomicInt(&entry->state)) {
+ case SDL_MAIN_CALLBACK_COMPLETE:
+ // Execution complete!
+ SDL_DestroyMainThreadCallback(entry);
+ return true;
+
+ case SDL_MAIN_CALLBACK_CANCELED:
+ // The callback was canceled on the main thread
+ SDL_DestroyMainThreadCallback(entry);
+ return SDL_SetError("Callback canceled");
+
+ default:
+ // Probably hit a deadlock in the callback
+ // We can't destroy the entry as the semaphore will be signaled
+ // if it ever comes back, just leak it here.
+ return SDL_SetError("Callback timed out");
+ }
+}
+
// Run the system dependent event loops
static void SDL_PumpEventsInternal(bool push_sentinel)
{
@@ -1184,6 +1355,9 @@ static void SDL_PumpEventsInternal(bool push_sentinel)
// Release any keys held down from last frame
SDL_ReleaseAutoReleaseKeys();
+ // Run any pending main thread callbacks
+ SDL_RunMainThreadCallbacks();
+
#ifdef SDL_PLATFORM_ANDROID
// Android event processing is independent of the video subsystem
Android_PumpEvents(0);
@@ -1792,6 +1966,7 @@ bool SDL_InitEvents(void)
#endif
SDL_AddHintCallback(SDL_HINT_EVENT_LOGGING, SDL_EventLoggingChanged, NULL);
SDL_AddHintCallback(SDL_HINT_POLL_SENTINEL, SDL_PollSentinelChanged, NULL);
+ SDL_InitMainThreadCallbacks();
if (!SDL_StartEventLoop()) {
SDL_RemoveHintCallback(SDL_HINT_EVENT_LOGGING, SDL_EventLoggingChanged, NULL);
return false;
@@ -1806,6 +1981,7 @@ void SDL_QuitEvents(void)
{
SDL_QuitQuit();
SDL_StopEventLoop();
+ SDL_QuitMainThreadCallbacks();
SDL_RemoveHintCallback(SDL_HINT_POLL_SENTINEL, SDL_PollSentinelChanged, NULL);
SDL_RemoveHintCallback(SDL_HINT_EVENT_LOGGING, SDL_EventLoggingChanged, NULL);
#ifndef SDL_JOYSTICK_DISABLED
diff --git a/test/testautomation_events.c b/test/testautomation_events.c
index 4eb1f23fc4925..aca6fafb7319c 100644
--- a/test/testautomation_events.c
+++ b/test/testautomation_events.c
@@ -203,6 +203,62 @@ static int SDLCALL events_addDelEventWatchWithUserdata(void *arg)
return TEST_COMPLETED;
}
+/**
+ * Runs callbacks on the main thread.
+ *
+ * \sa SDL_IsMainThread
+ * \sa SDL_RunOnMainThread
+ *
+ */
+
+static void SDLCALL IncrementCounter(void *userdata)
+{
+ int *value = (int *)userdata;
+ *value = *value + 1;
+}
+
+#ifndef SDL_PLATFORM_EMSCRIPTEN /* Emscripten doesn't have threads */
+static int SDLCALL IncrementCounterThread(void *userdata)
+{
+ SDL_assert(!SDL_IsMainThread());
+ SDL_RunOnMainThread(IncrementCounter, userdata, false);
+ SDL_RunOnMainThread(IncrementCounter, userdata, true);
+ return 0;
+}
+#endif /* !SDL_PLATFORM_EMSCRIPTEN */
+
+static int SDLCALL events_mainThreadCallbacks(void *arg)
+{
+ int counter = 0;
+
+ /* Make sure we're on the main thread */
+ SDLTest_AssertCheck(SDL_IsMainThread(), "Verify we're on the main thread");
+
+ SDL_RunOnMainThread(IncrementCounter, &counter, true);
+ SDLTest_AssertCheck(counter == 1, "Incremented counter on main thread, expected 1, got %d", counter);
+
+#ifndef SDL_PLATFORM_EMSCRIPTEN /* Emscripten doesn't have threads */
+ {
+ SDL_Thread *thread;
+
+ thread = SDL_CreateThread(IncrementCounterThread, NULL, &counter);
+ SDLTest_AssertCheck(thread != NULL, "Create counter thread");
+
+ /* Wait for both increment calls to be queued up */
+ SDL_Delay(100);
+
+ /* Run the main callbacks */
+ while (counter < 3) {
+ SDL_PumpEvents();
+ }
+ SDL_WaitThread(thread, NULL);
+ SDLTest_AssertCheck(counter == 3, "Incremented counter on main thread, expected 3, got %d", counter);
+ }
+#endif /* !SDL_PLATFORM_EMSCRIPTEN */
+
+ return TEST_COMPLETED;
+}
+
/* ================= Test References ================== */
/* Events test cases */
@@ -218,11 +274,16 @@ static const SDLTest_TestCaseReference eventsTest_addDelEventWatchWithUserdata =
events_addDelEventWatchWithUserdata, "events_addDelEventWatchWithUserdata", "Adds and deletes an event watch function with userdata", TEST_ENABLED
};
+static const SDLTest_TestCaseReference eventsTest_mainThreadCallbacks = {
+ events_mainThreadCallbacks, "events_mainThreadCallbacks", "Run callbacks on the main thread", TEST_ENABLED
+};
+
/* Sequence of Events test cases */
static const SDLTest_TestCaseReference *eventsTests[] = {
&eventsTest_pushPumpAndPollUserevent,
&eventsTest_addDelEventWatch,
&eventsTest_addDelEventWatchWithUserdata,
+ &eventsTest_mainThreadCallbacks,
NULL
};