Added SDL_IsMainThread() and SDL_RunOnMainThread()

This commit is contained in:
Sam Lantinga 2024-12-05 12:35:23 -08:00
parent bc4185c685
commit 23828b62d4
7 changed files with 312 additions and 0 deletions

View file

@ -239,6 +239,53 @@ extern SDL_DECLSPEC SDL_InitFlags SDLCALL SDL_WasInit(SDL_InitFlags flags);
*/ */
extern SDL_DECLSPEC void SDLCALL SDL_Quit(void); 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. * Specify basic metadata about your app.
* *

View file

@ -183,6 +183,7 @@ static bool SDL_MainIsReady = false;
#else #else
static bool SDL_MainIsReady = true; static bool SDL_MainIsReady = true;
#endif #endif
static SDL_ThreadID SDL_MainThreadID = 0;
static bool SDL_bInMainQuit = false; static bool SDL_bInMainQuit = false;
static Uint8 SDL_SubsystemRefCount[32]; static Uint8 SDL_SubsystemRefCount[32];
@ -250,6 +251,22 @@ static bool SDL_InitOrIncrementSubsystem(Uint32 subsystem)
void SDL_SetMainReady(void) void SDL_SetMainReady(void)
{ {
SDL_MainIsReady = true; 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 // Initialize all the subsystems that require initialization before threads start
@ -330,6 +347,11 @@ bool SDL_InitSubSystem(SDL_InitFlags flags)
goto quit_and_error; 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); SDL_IncrementSubsystemRefCount(SDL_INIT_VIDEO);
if (!SDL_VideoInit(NULL)) { if (!SDL_VideoInit(NULL)) {
SDL_DecrementSubsystemRefCount(SDL_INIT_VIDEO); SDL_DecrementSubsystemRefCount(SDL_INIT_VIDEO);

View file

@ -1201,6 +1201,8 @@ SDL3_0.0.0 {
SDL_SignalAsyncIOQueue; SDL_SignalAsyncIOQueue;
SDL_LoadFileAsync; SDL_LoadFileAsync;
SDL_ShowFileDialogWithProperties; SDL_ShowFileDialogWithProperties;
SDL_IsMainThread;
SDL_RunOnMainThread;
# extra symbols go here (don't modify this line) # extra symbols go here (don't modify this line)
local: *; local: *;
}; };

View file

@ -1226,3 +1226,5 @@
#define SDL_SignalAsyncIOQueue SDL_SignalAsyncIOQueue_REAL #define SDL_SignalAsyncIOQueue SDL_SignalAsyncIOQueue_REAL
#define SDL_LoadFileAsync SDL_LoadFileAsync_REAL #define SDL_LoadFileAsync SDL_LoadFileAsync_REAL
#define SDL_ShowFileDialogWithProperties SDL_ShowFileDialogWithProperties_REAL #define SDL_ShowFileDialogWithProperties SDL_ShowFileDialogWithProperties_REAL
#define SDL_IsMainThread SDL_IsMainThread_REAL
#define SDL_RunOnMainThread SDL_RunOnMainThread_REAL

View file

@ -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(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(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(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)

View file

@ -1175,6 +1175,177 @@ void SDL_FlushEvents(Uint32 minType, Uint32 maxType)
SDL_UnlockMutex(SDL_EventQ.lock); 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 // Run the system dependent event loops
static void SDL_PumpEventsInternal(bool push_sentinel) 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 // Release any keys held down from last frame
SDL_ReleaseAutoReleaseKeys(); SDL_ReleaseAutoReleaseKeys();
// Run any pending main thread callbacks
SDL_RunMainThreadCallbacks();
#ifdef SDL_PLATFORM_ANDROID #ifdef SDL_PLATFORM_ANDROID
// Android event processing is independent of the video subsystem // Android event processing is independent of the video subsystem
Android_PumpEvents(0); Android_PumpEvents(0);
@ -1792,6 +1966,7 @@ bool SDL_InitEvents(void)
#endif #endif
SDL_AddHintCallback(SDL_HINT_EVENT_LOGGING, SDL_EventLoggingChanged, NULL); SDL_AddHintCallback(SDL_HINT_EVENT_LOGGING, SDL_EventLoggingChanged, NULL);
SDL_AddHintCallback(SDL_HINT_POLL_SENTINEL, SDL_PollSentinelChanged, NULL); SDL_AddHintCallback(SDL_HINT_POLL_SENTINEL, SDL_PollSentinelChanged, NULL);
SDL_InitMainThreadCallbacks();
if (!SDL_StartEventLoop()) { if (!SDL_StartEventLoop()) {
SDL_RemoveHintCallback(SDL_HINT_EVENT_LOGGING, SDL_EventLoggingChanged, NULL); SDL_RemoveHintCallback(SDL_HINT_EVENT_LOGGING, SDL_EventLoggingChanged, NULL);
return false; return false;
@ -1806,6 +1981,7 @@ void SDL_QuitEvents(void)
{ {
SDL_QuitQuit(); SDL_QuitQuit();
SDL_StopEventLoop(); SDL_StopEventLoop();
SDL_QuitMainThreadCallbacks();
SDL_RemoveHintCallback(SDL_HINT_POLL_SENTINEL, SDL_PollSentinelChanged, NULL); SDL_RemoveHintCallback(SDL_HINT_POLL_SENTINEL, SDL_PollSentinelChanged, NULL);
SDL_RemoveHintCallback(SDL_HINT_EVENT_LOGGING, SDL_EventLoggingChanged, NULL); SDL_RemoveHintCallback(SDL_HINT_EVENT_LOGGING, SDL_EventLoggingChanged, NULL);
#ifndef SDL_JOYSTICK_DISABLED #ifndef SDL_JOYSTICK_DISABLED

View file

@ -203,6 +203,62 @@ static int SDLCALL events_addDelEventWatchWithUserdata(void *arg)
return TEST_COMPLETED; 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 ================== */ /* ================= Test References ================== */
/* Events test cases */ /* 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 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 */ /* Sequence of Events test cases */
static const SDLTest_TestCaseReference *eventsTests[] = { static const SDLTest_TestCaseReference *eventsTests[] = {
&eventsTest_pushPumpAndPollUserevent, &eventsTest_pushPumpAndPollUserevent,
&eventsTest_addDelEventWatch, &eventsTest_addDelEventWatch,
&eventsTest_addDelEventWatchWithUserdata, &eventsTest_addDelEventWatchWithUserdata,
&eventsTest_mainThreadCallbacks,
NULL NULL
}; };