Skip to content

Commit

Permalink
Handle all Android lifecycle events on the main thread
Browse files Browse the repository at this point in the history
This restructuring also allows us to wait efficiently in SDL_WaitEvent() on Android
  • Loading branch information
slouken committed Jul 24, 2024
1 parent a7c0192 commit c601120
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 154 deletions.
183 changes: 121 additions & 62 deletions src/core/android/SDL_android.c
Original file line number Diff line number Diff line change
Expand Up @@ -404,8 +404,10 @@ static jobject javaAssetManagerRef = 0;
static SDL_AtomicInt bAllowRecreateActivity;

static SDL_Mutex *Android_ActivityMutex = NULL;
SDL_Semaphore *Android_PauseSem = NULL;
SDL_Semaphore *Android_ResumeSem = NULL;
static SDL_Mutex *Android_LifecycleMutex = NULL;
static SDL_Semaphore *Android_LifecycleEventSem = NULL;
static SDL_AndroidLifecycleEvent Android_LifecycleEvents[SDL_NUM_ANDROID_LIFECYCLE_EVENTS];
static int Android_NumLifecycleEvents;

/*******************************************************************************
Functions called by JNI
Expand Down Expand Up @@ -614,14 +616,14 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cl
__android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to create Android_ActivityMutex mutex");
}

Android_PauseSem = SDL_CreateSemaphore(0);
if (!Android_PauseSem) {
__android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to create Android_PauseSem semaphore");
Android_LifecycleMutex = SDL_CreateMutex();
if (!Android_LifecycleMutex) {
__android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to create Android_LifecycleMutex mutex");
}

Android_ResumeSem = SDL_CreateSemaphore(0);
if (!Android_ResumeSem) {
__android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to create Android_ResumeSem semaphore");
Android_LifecycleEventSem = SDL_CreateSemaphore(0);
if (!Android_LifecycleEventSem) {
__android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to create Android_LifecycleEventSem semaphore");
}

mActivityClass = (jclass)((*env)->NewGlobalRef(env, cls));
Expand Down Expand Up @@ -895,46 +897,120 @@ JNIEXPORT int JNICALL SDL_JAVA_INTERFACE(nativeRunMain)(JNIEnv *env, jclass cls,
return status;
}

/* Drop file */
JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeDropFile)(
JNIEnv *env, jclass jcls,
jstring filename)
static int FindLifecycleEvent(SDL_AndroidLifecycleEvent event)
{
const char *path = (*env)->GetStringUTFChars(env, filename, NULL);
SDL_SendDropFile(NULL, NULL, path);
(*env)->ReleaseStringUTFChars(env, filename, path);
SDL_SendDropComplete(NULL);
for (int index = 0; index < Android_NumLifecycleEvents; ++index) {
if (Android_LifecycleEvents[index] == event) {
return index;
}
}
return -1;
}

/* Lock / Unlock Mutex */
void Android_LockActivityMutex(void)
static void RemoveLifecycleEvent(int index)
{
SDL_LockMutex(Android_ActivityMutex);
if (index < Android_NumLifecycleEvents - 1) {
SDL_memcpy(&Android_LifecycleEvents[index], &Android_LifecycleEvents[index+1], (Android_NumLifecycleEvents - index - 1) * sizeof(Android_LifecycleEvents[index]));
}
--Android_NumLifecycleEvents;
}

void Android_UnlockActivityMutex(void)
void Android_SendLifecycleEvent(SDL_AndroidLifecycleEvent event)
{
SDL_UnlockMutex(Android_ActivityMutex);
SDL_LockMutex(Android_LifecycleMutex);
{
int index;
SDL_bool add_event = SDL_TRUE;

switch (event) {
case SDL_ANDROID_LIFECYCLE_WAKE:
// We don't need more than one wake queued
index = FindLifecycleEvent(SDL_ANDROID_LIFECYCLE_WAKE);
if (index >= 0) {
add_event = SDL_FALSE;
}
break;
case SDL_ANDROID_LIFECYCLE_PAUSE:
// If we have a resume queued, just stay in the paused state
index = FindLifecycleEvent(SDL_ANDROID_LIFECYCLE_RESUME);
if (index >= 0) {
RemoveLifecycleEvent(index);
add_event = SDL_FALSE;
}
break;
case SDL_ANDROID_LIFECYCLE_RESUME:
// If we have a pause queued, just stay in the resumed state
index = FindLifecycleEvent(SDL_ANDROID_LIFECYCLE_PAUSE);
if (index >= 0) {
RemoveLifecycleEvent(index);
add_event = SDL_FALSE;
}
break;
case SDL_ANDROID_LIFECYCLE_LOWMEMORY:
// We don't need more than one low memory event queued
index = FindLifecycleEvent(SDL_ANDROID_LIFECYCLE_LOWMEMORY);
if (index >= 0) {
add_event = SDL_FALSE;
}
break;
case SDL_ANDROID_LIFECYCLE_DESTROY:
// Remove all other events, we're done!
while (Android_NumLifecycleEvents > 0) {
RemoveLifecycleEvent(0);
}
break;
default:
SDL_assert(!"Sending unexpected lifecycle event");
add_event = SDL_FALSE;
break;
}

if (add_event) {
SDL_assert(Android_NumLifecycleEvents < SDL_arraysize(Android_LifecycleEvents));
Android_LifecycleEvents[Android_NumLifecycleEvents++] = event;
SDL_SignalSemaphore(Android_LifecycleEventSem);
}
}
SDL_UnlockMutex(Android_LifecycleMutex);
}

/* Lock the Mutex when the Activity is in its 'Running' state */
void Android_LockActivityMutexOnceRunning(void)
SDL_bool Android_WaitLifecycleEvent(SDL_AndroidLifecycleEvent *event, Sint64 timeoutNS)
{
int pauseSignaled = 0;
int resumeSignaled = 0;
SDL_bool got_event = SDL_FALSE;

retry:
while (!got_event && SDL_WaitSemaphoreTimeoutNS(Android_LifecycleEventSem, timeoutNS) == 0) {
SDL_LockMutex(Android_LifecycleMutex);
{
if (Android_NumLifecycleEvents > 0) {
*event = Android_LifecycleEvents[0];
RemoveLifecycleEvent(0);
got_event = SDL_TRUE;
}
}
SDL_UnlockMutex(Android_LifecycleMutex);
}
return got_event;
}

void Android_LockActivityMutex(void)
{
SDL_LockMutex(Android_ActivityMutex);
}

pauseSignaled = SDL_GetSemaphoreValue(Android_PauseSem);
resumeSignaled = SDL_GetSemaphoreValue(Android_ResumeSem);
void Android_UnlockActivityMutex(void)
{
SDL_UnlockMutex(Android_ActivityMutex);
}

if (pauseSignaled > resumeSignaled) {
SDL_UnlockMutex(Android_ActivityMutex);
SDL_Delay(50);
goto retry;
}
/* Drop file */
JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeDropFile)(
JNIEnv *env, jclass jcls,
jstring filename)
{
const char *path = (*env)->GetStringUTFChars(env, filename, NULL);
SDL_SendDropFile(NULL, NULL, path);
(*env)->ReleaseStringUTFChars(env, filename, path);
SDL_SendDropComplete(NULL);
}

/* Set screen resolution */
Expand Down Expand Up @@ -1335,7 +1411,7 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeClipboardChanged)(
JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeLowMemory)(
JNIEnv *env, jclass cls)
{
SDL_SendAppEvent(SDL_EVENT_LOW_MEMORY);
Android_SendLifecycleEvent(SDL_ANDROID_LIFECYCLE_LOWMEMORY);
}

/* Locale
Expand All @@ -1357,20 +1433,7 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeDarkModeChanged)(
JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSendQuit)(
JNIEnv *env, jclass cls)
{
/* Discard previous events. The user should have handled state storage
* in SDL_EVENT_WILL_ENTER_BACKGROUND. After nativeSendQuit() is called, no
* events other than SDL_EVENT_QUIT and SDL_EVENT_TERMINATING should fire */
SDL_FlushEvents(SDL_EVENT_FIRST, SDL_EVENT_LAST);
/* Inject a SDL_EVENT_QUIT event */
SDL_SendQuit();
SDL_SendAppEvent(SDL_EVENT_TERMINATING);
/* Robustness: clear any pending Pause */
while (SDL_TryWaitSemaphore(Android_PauseSem) == 0) {
/* empty */
}
/* Resume the event loop so that the app can catch SDL_EVENT_QUIT which
* should now be the top event in the event queue. */
SDL_SignalSemaphore(Android_ResumeSem);
Android_SendLifecycleEvent(SDL_ANDROID_LIFECYCLE_DESTROY);
}

/* Activity ends */
Expand All @@ -1384,16 +1447,18 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeQuit)(
Android_ActivityMutex = NULL;
}

if (Android_PauseSem) {
SDL_DestroySemaphore(Android_PauseSem);
Android_PauseSem = NULL;
if (Android_LifecycleMutex) {
SDL_DestroyMutex(Android_LifecycleMutex);
Android_LifecycleMutex = NULL;
}

if (Android_ResumeSem) {
SDL_DestroySemaphore(Android_ResumeSem);
Android_ResumeSem = NULL;
if (Android_LifecycleEventSem) {
SDL_DestroySemaphore(Android_LifecycleEventSem);
Android_LifecycleEventSem = NULL;
}

Android_NumLifecycleEvents = 0;

Internal_Android_Destroy_AssetManager();

str = SDL_GetError();
Expand All @@ -1410,9 +1475,7 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativePause)(
{
__android_log_print(ANDROID_LOG_VERBOSE, "SDL", "nativePause()");

/* Signal the pause semaphore so the event loop knows to pause and (optionally) block itself.
* Sometimes 2 pauses can be queued (eg pause/resume/pause), so it's always increased. */
SDL_SignalSemaphore(Android_PauseSem);
Android_SendLifecycleEvent(SDL_ANDROID_LIFECYCLE_PAUSE);
}

/* Resume */
Expand All @@ -1421,11 +1484,7 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeResume)(
{
__android_log_print(ANDROID_LOG_VERBOSE, "SDL", "nativeResume()");

/* Signal the resume semaphore so the event loop knows to resume and restore the GL Context
* We can't restore the GL Context here because it needs to be done on the SDL main thread
* and this function will be called from the Java thread instead.
*/
SDL_SignalSemaphore(Android_ResumeSem);
Android_SendLifecycleEvent(SDL_ANDROID_LIFECYCLE_RESUME);
}

JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeFocusChanged)(
Expand Down
33 changes: 22 additions & 11 deletions src/core/android/SDL_android.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
*/
#include "SDL_internal.h"

#ifndef SDL_android_h
#define SDL_android_h

/* Set up for C function definitions, even when using C++ */
#ifdef __cplusplus
/* *INDENT-OFF* */
Expand All @@ -35,6 +38,23 @@ extern "C" {
// this appears to be broken right now (on Android, not SDL, I think...?).
#define ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES 0

/* Life cycle */
typedef enum
{
SDL_ANDROID_LIFECYCLE_WAKE,
SDL_ANDROID_LIFECYCLE_PAUSE,
SDL_ANDROID_LIFECYCLE_RESUME,
SDL_ANDROID_LIFECYCLE_LOWMEMORY,
SDL_ANDROID_LIFECYCLE_DESTROY,
SDL_NUM_ANDROID_LIFECYCLE_EVENTS
} SDL_AndroidLifecycleEvent;

void Android_SendLifecycleEvent(SDL_AndroidLifecycleEvent event);
SDL_bool Android_WaitLifecycleEvent(SDL_AndroidLifecycleEvent *event, Sint64 timeoutNS);

void Android_LockActivityMutex(void);
void Android_UnlockActivityMutex(void);

/* Interface from the SDL library into the Android Java activity */
extern void Android_JNI_SetActivityTitle(const char *title);
extern void Android_JNI_SetWindowStyle(SDL_bool fullscreen);
Expand Down Expand Up @@ -111,9 +131,6 @@ int Android_JNI_GetLocale(char *buf, size_t buflen);
/* Generic messages */
int Android_JNI_SendMessage(int command, int param);

/* Init */
JNIEXPORT void JNICALL SDL_Android_Init(JNIEnv *mEnv, jclass cls);

/* MessageBox */
int Android_JNI_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID);

Expand All @@ -139,22 +156,16 @@ SDL_bool SDL_IsAndroidTV(void);
SDL_bool SDL_IsChromebook(void);
SDL_bool SDL_IsDeXMode(void);

void Android_LockActivityMutex(void);
void Android_UnlockActivityMutex(void);
void Android_LockActivityMutexOnceRunning(void);

/* File Dialogs */
SDL_bool Android_JNI_OpenFileDialog(SDL_DialogFileCallback callback, void* userdata,
const SDL_DialogFileFilter *filters, int nfilters, SDL_bool forwrite,
SDL_bool multiple);

/* Semaphores for event state processing */
extern SDL_Semaphore *Android_PauseSem;
extern SDL_Semaphore *Android_ResumeSem;

/* Ends C function definitions when using C++ */
#ifdef __cplusplus
/* *INDENT-OFF* */
}
/* *INDENT-ON* */
#endif

#endif // SDL_android_h
Loading

0 comments on commit c601120

Please sign in to comment.