Skip to content

Commit

Permalink
Added SDL_IsMainThread() and SDL_RunOnMainThread()
Browse files Browse the repository at this point in the history
  • Loading branch information
slouken committed Dec 5, 2024
1 parent bc4185c commit 23828b6
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 0 deletions.
47 changes: 47 additions & 0 deletions include/SDL3/SDL_init.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
22 changes: 22 additions & 0 deletions src/SDL.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/dynapi/SDL_dynapi.sym
Original file line number Diff line number Diff line change
Expand Up @@ -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: *;
};
2 changes: 2 additions & 0 deletions src/dynapi/SDL_dynapi_overrides.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/dynapi/SDL_dynapi_procs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
176 changes: 176 additions & 0 deletions src/events/SDL_events.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
Loading

0 comments on commit 23828b6

Please sign in to comment.