myou-engine/libs/glfm/src/glfm_android.c
Alberto Torres 9af1322937 First commit.
* Incomplete port of myou-engine-js to nimskull, after many months of work, and
  a few extra features that weren't exactly necessary for a "first commit" to
  work. Excuse the lack of commit history up to this point.
* Bare bones structure of the documentation and the process to update it.
* Restructure of the whole project to have a more sensible organization.
* Making submodules of forks of larger libraries.
* README, licenses, AUTHORS.md.
2024-08-20 13:08:19 +02:00

2841 lines
119 KiB
C

// GLFM
// https://github.com/brackeen/glfm
// #if defined(__ANDROID__)
#include "glfm.h"
#include "glfm_internal.h"
#include <EGL/egl.h>
#include <android/configuration.h>
#include <android/sensor.h>
#include <android/window.h>
#include <assert.h>
#include <dlfcn.h>
#include <pthread.h>
#include <unistd.h>
#define GLFM_LOG_LIFECYCLE_ENABLE 1
#ifdef NDEBUG
# define GLFM_LOG(...) do { } while (0)
# define GLFM_LOG_LIFECYCLE(...) do { } while (0)
#else
# include <android/log.h>
# define GLFM_LOG(...) __android_log_print(ANDROID_LOG_DEBUG, "GLFM", __VA_ARGS__)
# if GLFM_LOG_LIFECYCLE_ENABLE
# define GLFM_LOG_LIFECYCLE(...) __android_log_print(ANDROID_LOG_VERBOSE, "GLFM", "Lifecycle: " __VA_ARGS__)
# else
# define GLFM_LOG_LIFECYCLE(...) do { } while (0)
# endif
#endif
#define GLFM_MAX_SIMULTANEOUS_TOUCHES 5
// Same update interval as iOS
#define GLFM_SENSOR_UPDATE_INTERVAL_MICROS ((int)(0.01 * 1000000))
#define GLFM_RESIZE_EVENT_MAX_WAIT_FRAMES 5
// If GLFM_HANDLE_BACK_BUTTON is 1, when the user presses the back button, the task is moved to the
// back. Otherwise, when the user presses the back button, the activity is destroyed.
// On newer API levels (31) this may not be needed.
#define GLFM_HANDLE_BACK_BUTTON 1
// MARK: - Platform data (global singleton)
typedef struct {
ALooper *looper;
pthread_t thread;
pthread_mutex_t mutex;
pthread_cond_t cond;
int commandPipeRead;
int commandPipeWrite;
bool threadRunning;
ALooper *uiLooper;
int uiCommandPipeRead;
int uiCommandPipeWrite;
ANativeWindow *window;
AInputQueue *inputQueue;
ARect contentRectArray[2];
int contentRectIndex;
ANativeWindow *pendingWindow;
AInputQueue *pendingInputQueue;
ANativeActivity *activity;
AConfiguration *config;
bool destroyRequested;
bool multitouchEnabled;
ARect keyboardFrame;
bool keyboardVisible;
bool animating;
bool refreshRequested;
bool swapCalled;
bool surfaceCreatedNotified;
double lastSwapTime;
EGLDisplay eglDisplay;
EGLSurface eglSurface;
EGLConfig eglConfig;
EGLContext eglContext;
bool eglContextCurrent;
int32_t width;
int32_t height;
double scale;
int resizeEventWaitFrames;
struct {
int top, right, bottom, left;
bool valid;
} insets;
GLFMDisplay *display;
GLFMRenderingAPI renderingAPI;
ASensorEventQueue *sensorEventQueue;
GLFMSensorEvent sensorEvent[GLFM_NUM_SENSORS];
bool sensorEventValid[GLFM_NUM_SENSORS];
bool deviceSensorEnabled[GLFM_NUM_SENSORS];
GLFMInterfaceOrientation orientation;
JNIEnv *jniEnv;
} GLFMPlatformData;
static GLFMPlatformData *platformDataGlobal = NULL;
// MARK: - Private function declarations
__attribute__((no_sanitize("hwaddress"))) static void *glfm__mainLoop(void *param);
__attribute__((no_sanitize("hwaddress"))) static int glfm__looperCallback(int pipe, int events, void *userData);
__attribute__((no_sanitize("hwaddress"))) static void glfm__setAllRequestedSensorsEnabled(GLFMDisplay *display, bool enable);
__attribute__((no_sanitize("hwaddress"))) static void glfm__reportOrientationChangeIfNeeded(GLFMDisplay *display);
__attribute__((no_sanitize("hwaddress"))) static void glfm__reportInsetsChangedIfNeeded(GLFMDisplay *display);
__attribute__((no_sanitize("hwaddress"))) static bool glfm__updateSurfaceSizeIfNeeded(GLFMDisplay *display, bool force);
static float glfm__getRefreshRate(const GLFMDisplay *display);
__attribute__((no_sanitize("hwaddress"))) static void glfm__getDisplayChromeInsets(const GLFMDisplay *display, int *top, int *right,
int *bottom, int *left);
__attribute__((no_sanitize("hwaddress"))) static void glfm__resetContentRect(GLFMPlatformData *platformData);
__attribute__((no_sanitize("hwaddress"))) static void glfm__updateKeyboardVisibility(GLFMPlatformData *platformData);
__attribute__((no_sanitize("hwaddress"))) static void glfm__updateUserInterfaceChrome(GLFMPlatformData *platformData);
// MARK: - JNI code
#ifdef NDEBUG
# define glfm__printException(jni) ((void)0)
#else
# define glfm__printException(jni) (*(jni))->ExceptionDescribe(jni)
#endif
#define glfm__wasJavaExceptionThrown(jni) \
((*(jni))->ExceptionCheck(jni) ? (glfm__printException(jni), (*(jni))->ExceptionClear(jni), true) : false)
#define glfm__clearJavaException(jni) \
do { \
if ((*(jni))->ExceptionCheck(jni)) { \
glfm__printException(jni); \
(*(jni))->ExceptionClear(jni); \
} \
} while (0)
__attribute__((no_sanitize("hwaddress"))) static jmethodID glfm__getJavaMethodID(JNIEnv *jni, jobject object, const char *name, const char *sig) {
if (!object) {
return NULL;
}
jclass class = (*jni)->GetObjectClass(jni, object);
jmethodID methodID = (*jni)->GetMethodID(jni, class, name, sig);
(*jni)->DeleteLocalRef(jni, class);
return glfm__wasJavaExceptionThrown(jni) ? NULL : methodID;
}
__attribute__((no_sanitize("hwaddress"))) static jfieldID glfm__getJavaFieldID(JNIEnv *jni, jobject object, const char *name, const char *sig) {
if (!object) {
return NULL;
}
jclass class = (*jni)->GetObjectClass(jni, object);
jfieldID fieldID = (*jni)->GetFieldID(jni, class, name, sig);
(*jni)->DeleteLocalRef(jni, class);
return glfm__wasJavaExceptionThrown(jni) ? NULL : fieldID;
}
__attribute__((no_sanitize("hwaddress"))) static jmethodID glfm__getJavaStaticMethodID(JNIEnv *jni, jclass class, const char *name, const char *sig) {
if (!class) {
return NULL;
}
jmethodID methodID = (*jni)->GetStaticMethodID(jni, class, name, sig);
return glfm__wasJavaExceptionThrown(jni) ? NULL : methodID;
}
__attribute__((no_sanitize("hwaddress"))) static jfieldID glfm__getJavaStaticFieldID(JNIEnv *jni, jclass class, const char *name, const char *sig) {
if (!class) {
return NULL;
}
jfieldID fieldID = (*jni)->GetStaticFieldID(jni, class, name, sig);
return glfm__wasJavaExceptionThrown(jni) ? NULL : fieldID;
}
#define glfm__callJavaMethod(jni, object, methodName, methodSig, returnType) \
(*(jni))->Call##returnType##Method(jni, object, \
glfm__getJavaMethodID(jni, object, methodName, methodSig))
#define glfm__callJavaMethodWithArgs(jni, object, methodName, methodSig, returnType, ...) \
(*(jni))->Call##returnType##Method(jni, object, \
glfm__getJavaMethodID(jni, object, methodName, methodSig), __VA_ARGS__)
#define glfm__callJavaStaticMethod(jni, class, methodName, methodSig, returnType) \
(*(jni))->CallStatic##returnType##Method(jni, class, \
glfm__getJavaStaticMethodID(jni, class, methodName, methodSig))
#define glfm__callJavaStaticMethodWithArgs(jni, class, methodName, methodSig, returnType, ...) \
(*(jni))->CallStatic##returnType##Method(jni, class, \
glfm__getJavaStaticMethodID(jni, class, methodName, methodSig), __VA_ARGS__)
#define glfm__getJavaField(jni, object, fieldName, fieldSig, fieldType) \
(*(jni))->Get##fieldType##Field(jni, object, \
glfm__getJavaFieldID(jni, object, fieldName, fieldSig))
#define glfm__getJavaStaticField(jni, class, fieldName, fieldSig, fieldType) \
(*(jni))->GetStatic##fieldType##Field(jni, class, \
glfm__getJavaStaticFieldID(jni, class, fieldName, fieldSig))
// MARK: - EGL
__attribute__((no_sanitize("hwaddress"))) static bool glfm__eglContextInit(GLFMPlatformData *platformData) {
// Available in eglext.h in API 18
static const int EGL_CONTEXT_MAJOR_VERSION_KHR = 0x3098;
static const int EGL_CONTEXT_MINOR_VERSION_KHR = 0x30FB;
if (!platformData || !platformData->display) {
return false;
}
EGLint majorVersion = 0;
EGLint minorVersion = 0;
bool created = false;
if (platformData->eglContext == EGL_NO_CONTEXT) {
// OpenGL ES 3.2
if (platformData->display->preferredAPI >= GLFMRenderingAPIOpenGLES32) {
majorVersion = 3;
minorVersion = 2;
const EGLint contextAttribList[] = { EGL_CONTEXT_MAJOR_VERSION_KHR, majorVersion,
EGL_CONTEXT_MINOR_VERSION_KHR, minorVersion,
EGL_NONE, EGL_NONE };
platformData->eglContext = eglCreateContext(platformData->eglDisplay,
platformData->eglConfig,
EGL_NO_CONTEXT,
contextAttribList);
created = platformData->eglContext != EGL_NO_CONTEXT;
}
// OpenGL ES 3.1
if (!created && platformData->display->preferredAPI >= GLFMRenderingAPIOpenGLES31) {
majorVersion = 3;
minorVersion = 1;
const EGLint contextAttribList[] = { EGL_CONTEXT_MAJOR_VERSION_KHR, majorVersion,
EGL_CONTEXT_MINOR_VERSION_KHR, minorVersion,
EGL_NONE, EGL_NONE };
platformData->eglContext = eglCreateContext(platformData->eglDisplay,
platformData->eglConfig,
EGL_NO_CONTEXT,
contextAttribList);
created = platformData->eglContext != EGL_NO_CONTEXT;
}
// OpenGL ES 3.0
if (!created && platformData->display->preferredAPI >= GLFMRenderingAPIOpenGLES3) {
majorVersion = 3;
minorVersion = 0;
const EGLint contextAttribList[] = { EGL_CONTEXT_CLIENT_VERSION, majorVersion,
EGL_NONE, EGL_NONE };
platformData->eglContext = eglCreateContext(platformData->eglDisplay,
platformData->eglConfig,
EGL_NO_CONTEXT,
contextAttribList);
created = platformData->eglContext != EGL_NO_CONTEXT;
}
// OpenGL ES 2.0
if (!created) {
majorVersion = 2;
minorVersion = 0;
const EGLint contextAttribList[] = { EGL_CONTEXT_CLIENT_VERSION, majorVersion,
EGL_NONE, EGL_NONE };
platformData->eglContext = eglCreateContext(platformData->eglDisplay,
platformData->eglConfig,
EGL_NO_CONTEXT,
contextAttribList);
created = platformData->eglContext != EGL_NO_CONTEXT;
}
if (created) {
eglQueryContext(platformData->eglDisplay, platformData->eglContext,
EGL_CONTEXT_MAJOR_VERSION_KHR, &majorVersion);
if (majorVersion >= 3) {
// This call fails on many devices.
// When it fails, `minorVersion` is left unchanged.
eglQueryContext(platformData->eglDisplay, platformData->eglContext,
EGL_CONTEXT_MINOR_VERSION_KHR, &minorVersion);
}
if (majorVersion == 3 && minorVersion == 2) {
platformData->renderingAPI = GLFMRenderingAPIOpenGLES32;
} else if (majorVersion == 3 && minorVersion == 1) {
platformData->renderingAPI = GLFMRenderingAPIOpenGLES31;
} else if (majorVersion == 3) {
platformData->renderingAPI = GLFMRenderingAPIOpenGLES3;
} else {
platformData->renderingAPI = GLFMRenderingAPIOpenGLES2;
}
}
}
if (!eglMakeCurrent(platformData->eglDisplay, platformData->eglSurface,
platformData->eglSurface, platformData->eglContext)) {
GLFM_LOG_LIFECYCLE("eglMakeCurrent() failed");
platformData->eglContextCurrent = false;
return false;
}
GLFM_LOG_LIFECYCLE("GL Context made current");
platformData->eglContextCurrent = true;
if (created && !platformData->surfaceCreatedNotified) {
platformData->surfaceCreatedNotified = true;
if (platformData->display && platformData->display->surfaceCreatedFunc) {
platformData->display->surfaceCreatedFunc(platformData->display,
platformData->width,
platformData->height);
}
}
return true;
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__eglContextDisable(GLFMPlatformData *platformData) {
if (platformData->eglDisplay != EGL_NO_DISPLAY) {
eglMakeCurrent(platformData->eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
}
platformData->eglContextCurrent = false;
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__eglSurfaceInit(GLFMPlatformData *platformData) {
if (platformData->eglSurface == EGL_NO_SURFACE) {
platformData->eglSurface = eglCreateWindowSurface(platformData->eglDisplay,
platformData->eglConfig,
platformData->window, NULL);
switch (platformData->display->swapBehavior) {
case GLFMSwapBehaviorPlatformDefault:
// Platform default, do nothing.
break;
case GLFMSwapBehaviorBufferPreserved:
eglSurfaceAttrib(platformData->eglDisplay, platformData->eglSurface, EGL_SWAP_BEHAVIOR, EGL_BUFFER_PRESERVED);
break;
case GLFMSwapBehaviorBufferDestroyed:
eglSurfaceAttrib(platformData->eglDisplay, platformData->eglSurface, EGL_SWAP_BEHAVIOR, EGL_BUFFER_DESTROYED);
}
}
}
#ifndef NDEBUG
__attribute__((no_sanitize("hwaddress"))) static void glfm__eglLogConfig(GLFMPlatformData *platformData, EGLConfig config) {
GLFM_LOG("Config: %p", config);
EGLint value = 0;
eglGetConfigAttrib(platformData->eglDisplay, config, EGL_RENDERABLE_TYPE, &value);
GLFM_LOG(" EGL_RENDERABLE_TYPE %i", value);
eglGetConfigAttrib(platformData->eglDisplay, config, EGL_SURFACE_TYPE, &value);
GLFM_LOG(" EGL_SURFACE_TYPE %i", value);
eglGetConfigAttrib(platformData->eglDisplay, config, EGL_RED_SIZE, &value);
GLFM_LOG(" EGL_RED_SIZE %i", value);
eglGetConfigAttrib(platformData->eglDisplay, config, EGL_GREEN_SIZE, &value);
GLFM_LOG(" EGL_GREEN_SIZE %i", value);
eglGetConfigAttrib(platformData->eglDisplay, config, EGL_BLUE_SIZE, &value);
GLFM_LOG(" EGL_BLUE_SIZE %i", value);
eglGetConfigAttrib(platformData->eglDisplay, config, EGL_ALPHA_SIZE, &value);
GLFM_LOG(" EGL_ALPHA_SIZE %i", value);
eglGetConfigAttrib(platformData->eglDisplay, config, EGL_DEPTH_SIZE, &value);
GLFM_LOG(" EGL_DEPTH_SIZE %i", value);
eglGetConfigAttrib(platformData->eglDisplay, config, EGL_STENCIL_SIZE, &value);
GLFM_LOG(" EGL_STENCIL_SIZE %i", value);
eglGetConfigAttrib(platformData->eglDisplay, config, EGL_SAMPLE_BUFFERS, &value);
GLFM_LOG(" EGL_SAMPLE_BUFFERS %i", value);
eglGetConfigAttrib(platformData->eglDisplay, config, EGL_SAMPLES, &value);
GLFM_LOG(" EGL_SAMPLES %i", value);
}
#endif
__attribute__((no_sanitize("hwaddress"))) static bool glfm__eglInit(GLFMPlatformData *platformData) {
if (platformData->eglDisplay != EGL_NO_DISPLAY) {
glfm__eglSurfaceInit(platformData);
return glfm__eglContextInit(platformData);
}
int rBits, gBits, bBits, aBits;
int depthBits, stencilBits, samples;
switch (platformData->display->colorFormat) {
case GLFMColorFormatRGB565:
rBits = 5;
gBits = 6;
bBits = 5;
aBits = 0;
break;
case GLFMColorFormatRGB888:
rBits = 8;
gBits = 8;
bBits = 8;
aBits = 0;
break;
case GLFMColorFormatRGBA8888:
default:
rBits = 8;
gBits = 8;
bBits = 8;
aBits = 8;
break;
}
switch (platformData->display->depthFormat) {
case GLFMDepthFormatNone:
default:
depthBits = 0;
break;
case GLFMDepthFormat16:
depthBits = 16;
break;
case GLFMDepthFormat24:
depthBits = 24;
break;
}
switch (platformData->display->stencilFormat) {
case GLFMStencilFormatNone:
default:
stencilBits = 0;
break;
case GLFMStencilFormat8:
stencilBits = 8;
if (depthBits > 0) {
// Many implementations only allow 24-bit depth with 8-bit stencil.
depthBits = 24;
}
break;
}
samples = platformData->display->multisample == GLFMMultisample4X ? 4 : 0;
EGLint majorVersion = 0;
EGLint minorVersion = 0;
EGLint format = 0;
EGLint numConfigs = 0;
platformData->eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(platformData->eglDisplay, &majorVersion, &minorVersion);
while (true) {
const EGLint attribList[] = {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RED_SIZE, rBits,
EGL_GREEN_SIZE, gBits,
EGL_BLUE_SIZE, bBits,
EGL_ALPHA_SIZE, aBits,
EGL_DEPTH_SIZE, depthBits,
EGL_STENCIL_SIZE, stencilBits,
EGL_SAMPLE_BUFFERS, samples > 0 ? 1 : 0,
EGL_SAMPLES, samples > 0 ? samples : 0,
EGL_NONE, EGL_NONE
};
eglChooseConfig(platformData->eglDisplay, attribList,
&platformData->eglConfig, 1, &numConfigs);
if (numConfigs) {
// Found!
//glfm__eglLogConfig(platformData, platformData->eglConfig);
break;
}
if (samples > 0) {
// Try 2x multisampling or no multisampling
samples -= 2;
} else if (depthBits > 8) {
// Try 16-bit depth or 8-bit depth
depthBits -= 8;
} else {
// Failure
#ifndef NDEBUG
static bool printedConfigs = false;
if (!printedConfigs) {
printedConfigs = true;
GLFM_LOG("eglChooseConfig() failed");
EGLConfig configs[256];
EGLint numTotalConfigs = 0;
if (eglGetConfigs(platformData->eglDisplay, configs, 256, &numTotalConfigs)) {
GLFM_LOG("Num available configs: %i", numTotalConfigs);
for (int i = 0; i < numTotalConfigs; i++) {
glfm__eglLogConfig(platformData, configs[i]);
}
} else {
GLFM_LOG("Couldn't get any EGL configs");
}
}
#endif
GLFM_LOG("eglChooseConfig() failed");
glfm__reportSurfaceError(platformData->eglDisplay, "eglChooseConfig() failed");
eglTerminate(platformData->eglDisplay);
platformData->eglDisplay = EGL_NO_DISPLAY;
return false;
}
}
glfm__eglSurfaceInit(platformData);
eglQuerySurface(platformData->eglDisplay, platformData->eglSurface, EGL_WIDTH,
&platformData->width);
eglQuerySurface(platformData->eglDisplay, platformData->eglSurface, EGL_HEIGHT,
&platformData->height);
eglGetConfigAttrib(platformData->eglDisplay, platformData->eglConfig, EGL_NATIVE_VISUAL_ID,
&format);
ANativeWindow_setBuffersGeometry(platformData->window, 0, 0, format);
return glfm__eglContextInit(platformData);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__eglSurfaceDestroy(GLFMPlatformData *platformData) {
if (platformData->eglSurface != EGL_NO_SURFACE) {
eglDestroySurface(platformData->eglDisplay, platformData->eglSurface);
platformData->eglSurface = EGL_NO_SURFACE;
}
glfm__eglContextDisable(platformData);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__eglDestroy(GLFMPlatformData *platformData) {
if (platformData->eglDisplay != EGL_NO_DISPLAY) {
eglMakeCurrent(platformData->eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (platformData->eglContext != EGL_NO_CONTEXT) {
eglDestroyContext(platformData->eglDisplay, platformData->eglContext);
GLFM_LOG_LIFECYCLE("GL Context destroyed");
if (platformData->surfaceCreatedNotified) {
platformData->surfaceCreatedNotified = false;
if (platformData->display && platformData->display->surfaceDestroyedFunc) {
platformData->display->surfaceDestroyedFunc(platformData->display);
}
}
}
if (platformData->eglSurface != EGL_NO_SURFACE) {
eglDestroySurface(platformData->eglDisplay, platformData->eglSurface);
}
eglTerminate(platformData->eglDisplay);
}
platformData->eglDisplay = EGL_NO_DISPLAY;
platformData->eglContext = EGL_NO_CONTEXT;
platformData->eglSurface = EGL_NO_SURFACE;
platformData->eglContextCurrent = false;
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__eglCheckError(GLFMPlatformData *platformData) {
EGLint err = eglGetError();
if (err == EGL_BAD_SURFACE) {
glfm__eglSurfaceDestroy(platformData);
glfm__eglSurfaceInit(platformData);
} else if (err == EGL_CONTEXT_LOST || err == EGL_BAD_CONTEXT) {
if (platformData->eglContext != EGL_NO_CONTEXT) {
platformData->eglContext = EGL_NO_CONTEXT;
platformData->eglContextCurrent = false;
GLFM_LOG_LIFECYCLE("GL Context lost");
if (platformData->surfaceCreatedNotified) {
platformData->surfaceCreatedNotified = false;
if (platformData->display && platformData->display->surfaceDestroyedFunc) {
platformData->display->surfaceDestroyedFunc(platformData->display);
}
}
}
glfm__eglContextInit(platformData);
} else {
glfm__eglDestroy(platformData);
glfm__eglInit(platformData);
}
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__drawFrame(GLFMPlatformData *platformData) {
if (!platformData->eglContextCurrent) {
// Probably a bad config (Happens on Android 2.3 emulator)
return;
}
// Check for resize (or rotate)
glfm__updateSurfaceSizeIfNeeded(platformData->display, false);
// Tick and draw
if (platformData->refreshRequested) {
platformData->refreshRequested = false;
if (platformData->display && platformData->display->surfaceRefreshFunc) {
platformData->display->surfaceRefreshFunc(platformData->display);
}
}
if (platformData->display && platformData->display->renderFunc) {
platformData->display->renderFunc(platformData->display);
}
}
// MARK: - ANativeActivity callbacks (UI thread)
enum {
GLFMLooperIDCommand = 1,
GLFMLooperIDInput = 2,
GLFMLooperIDSensor = 3,
};
typedef enum {
GLFMActivityCommandOnStart,
GLFMActivityCommandOnPause,
GLFMActivityCommandOnResume,
GLFMActivityCommandOnStop,
GLFMActivityCommandOnDestroy,
GLFMActivityCommandOnWindowFocusGained,
GLFMActivityCommandOnWindowFocusLost,
GLFMActivityCommandOnNativeWindowCreated,
GLFMActivityCommandOnNativeWindowResized,
GLFMActivityCommandOnNativeWindowRedrawNeeded,
GLFMActivityCommandOnNativeWindowDestroyed,
GLFMActivityCommandOnInputQueueCreated,
GLFMActivityCommandOnInputQueueDestroyed,
GLFMActivityCommandOnContentRectChanged,
GLFMActivityCommandOnConfigurationChanged,
GLFMActivityCommandOnLowMemory,
} GLFMActivityCommand;
__attribute__((no_sanitize("hwaddress"))) static void glfm__sendCommand(ANativeActivity *activity, GLFMActivityCommand command) {
GLFMPlatformData *platformData = activity->instance;
if (!platformData) {
return;
}
uint8_t data = (uint8_t)command;
if (write(platformData->commandPipeWrite, &data, sizeof(data)) != sizeof(data)) {
GLFM_LOG("Couldn't write to pipe");
}
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnStart(ANativeActivity *activity) {
GLFMPlatformData *platformData = activity->instance;
if (platformData && platformData->display) {
glfm__updateUserInterfaceChrome(platformData);
}
glfm__sendCommand(activity, GLFMActivityCommandOnStart);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnPause(ANativeActivity *activity) {
glfm__sendCommand(activity, GLFMActivityCommandOnPause);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnResume(ANativeActivity *activity) {
glfm__sendCommand(activity, GLFMActivityCommandOnResume);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnStop(ANativeActivity *activity) {
glfm__sendCommand(activity, GLFMActivityCommandOnStop);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnWindowFocusChanged(ANativeActivity *activity, int hasFocus) {
glfm__sendCommand(activity, (hasFocus ? GLFMActivityCommandOnWindowFocusGained :
GLFMActivityCommandOnWindowFocusLost));
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnConfigurationChanged(ANativeActivity *activity) {
glfm__sendCommand(activity, GLFMActivityCommandOnConfigurationChanged);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnLowMemory(ANativeActivity *activity) {
glfm__sendCommand(activity, GLFMActivityCommandOnLowMemory);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnNativeWindowResized(ANativeActivity *activity, ANativeWindow *window) {
(void)window;
glfm__sendCommand(activity, GLFMActivityCommandOnNativeWindowResized);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnNativeWindowRedrawNeeded(ANativeActivity *activity, ANativeWindow *window) {
(void)window;
glfm__sendCommand(activity, GLFMActivityCommandOnNativeWindowRedrawNeeded);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnNativeWindowCreated(ANativeActivity *activity, ANativeWindow *window) {
GLFMPlatformData *platformData = activity->instance;
pthread_mutex_lock(&platformData->mutex);
platformData->pendingWindow = window;
glfm__sendCommand(activity, GLFMActivityCommandOnNativeWindowCreated);
while (platformData->window != window) {
pthread_cond_wait(&platformData->cond, &platformData->mutex);
}
pthread_mutex_unlock(&platformData->mutex);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnNativeWindowDestroyed(ANativeActivity *activity, ANativeWindow *window) {
(void)window;
glfm__sendCommand(activity, GLFMActivityCommandOnNativeWindowDestroyed);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnInputQueueCreated(ANativeActivity *activity, AInputQueue *queue) {
GLFMPlatformData *platformData = activity->instance;
pthread_mutex_lock(&platformData->mutex);
platformData->pendingInputQueue = queue;
glfm__sendCommand(activity, GLFMActivityCommandOnInputQueueCreated);
while (platformData->inputQueue != queue) {
pthread_cond_wait(&platformData->cond, &platformData->mutex);
}
pthread_mutex_unlock(&platformData->mutex);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnInputQueueDestroyed(ANativeActivity *activity, AInputQueue *queue) {
(void)queue;
glfm__sendCommand(activity, GLFMActivityCommandOnInputQueueDestroyed);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnContentRectChanged(ANativeActivity *activity, const ARect *rect) {
GLFMPlatformData *platformData = activity->instance;
int nextContentRectIndex = platformData->contentRectIndex ^ 1;
platformData->contentRectArray[nextContentRectIndex] = *rect;
platformData->contentRectIndex = nextContentRectIndex;
glfm__resetContentRect(platformData); // Reset so that onContentRectChanged acts as a global layout listener.
glfm__sendCommand(activity, GLFMActivityCommandOnContentRectChanged);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__activityOnDestroy(ANativeActivity *activity) {
GLFMPlatformData *platformData = activity->instance;
pthread_mutex_lock(&platformData->mutex);
glfm__sendCommand(activity, GLFMActivityCommandOnDestroy);
while (platformData->threadRunning) {
pthread_cond_wait(&platformData->cond, &platformData->mutex);
}
pthread_mutex_unlock(&platformData->mutex);
close(platformData->commandPipeRead);
close(platformData->commandPipeWrite);
pthread_cond_destroy(&platformData->cond);
pthread_mutex_destroy(&platformData->mutex);
close(platformData->uiCommandPipeRead);
close(platformData->uiCommandPipeWrite);
GLFM_LOG_LIFECYCLE("Goodbye");
}
__attribute__((no_sanitize("hwaddress"))) static void *glfm__activityOnSaveInstanceState(ANativeActivity *activity, size_t *outSize) {
(void)activity;
*outSize = 0;
return NULL;
}
__attribute__((no_sanitize("hwaddress"))) JNIEXPORT void ANativeActivity_onCreate(ANativeActivity *activity, void *savedState, size_t savedStateSize)
{
(void)savedState;
(void)savedStateSize;
//int32_t ver = activity->sdkVersion;
//GLFM_LOG_LIFECYCLE("ANativeActivity_onCreate (API %i)", ver);
ALooper *looper = ALooper_forThread();
if (!looper) {
GLFM_LOG("No looper");
return;
}
int commandPipe[2];
int uiCommandPipe[2];
if (pipe(commandPipe)) {
GLFM_LOG("Couldn't create pipe");
return;
}
if (pipe(uiCommandPipe)) {
GLFM_LOG("Couldn't create UI pipe");
return;
}
activity->callbacks->onStart = glfm__activityOnStart;
activity->callbacks->onPause = glfm__activityOnPause;
activity->callbacks->onResume = glfm__activityOnResume;
activity->callbacks->onStop = glfm__activityOnStop;
activity->callbacks->onDestroy = glfm__activityOnDestroy;
activity->callbacks->onWindowFocusChanged = glfm__activityOnWindowFocusChanged;
activity->callbacks->onNativeWindowCreated = glfm__activityOnNativeWindowCreated;
activity->callbacks->onNativeWindowResized = glfm__activityOnNativeWindowResized;
activity->callbacks->onNativeWindowRedrawNeeded = glfm__activityOnNativeWindowRedrawNeeded;
activity->callbacks->onNativeWindowDestroyed = glfm__activityOnNativeWindowDestroyed;
activity->callbacks->onInputQueueCreated = glfm__activityOnInputQueueCreated;
activity->callbacks->onInputQueueDestroyed = glfm__activityOnInputQueueDestroyed;
activity->callbacks->onContentRectChanged = glfm__activityOnContentRectChanged;
activity->callbacks->onConfigurationChanged = glfm__activityOnConfigurationChanged;
activity->callbacks->onLowMemory = glfm__activityOnLowMemory;
activity->callbacks->onSaveInstanceState = glfm__activityOnSaveInstanceState;
if (platformDataGlobal == NULL) {
// ANativeActivity_onCreate can be called multiple times for the same Activity.
// For now, use a global to prevent glfmMain() from being called multiple times.
// This behavior may need to change in the future.
platformDataGlobal = calloc(1, sizeof(GLFMPlatformData));
}
GLFMPlatformData *platformData = platformDataGlobal;
activity->instance = platformData;
platformData->activity = activity;
platformData->window = NULL;
platformData->threadRunning = false;
platformData->destroyRequested = false;
platformData->contentRectArray[0] = (ARect) { 0 };
platformData->contentRectArray[1] = (ARect) { 0 };
platformData->commandPipeRead = commandPipe[0];
platformData->commandPipeWrite = commandPipe[1];
pthread_mutex_init(&platformData->mutex, NULL);
pthread_cond_init(&platformData->cond, NULL);
// Setup UI thread callbacks
platformData->uiLooper = looper;
platformData->uiCommandPipeRead = uiCommandPipe[0];
platformData->uiCommandPipeWrite = uiCommandPipe[1];
ALooper_addFd(platformData->uiLooper, platformData->uiCommandPipeRead,
ALOOPER_POLL_CALLBACK,ALOOPER_EVENT_INPUT,
glfm__looperCallback, platformData);
// Start thread
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&platformData->thread, &attr, glfm__mainLoop,
platformData);
pthread_attr_destroy(&attr);
// Wait for thread to start
pthread_mutex_lock(&platformData->mutex);
while (!platformData->threadRunning) {
pthread_cond_wait(&platformData->cond, &platformData->mutex);
}
pthread_mutex_unlock(&platformData->mutex);
GLFM_LOG_LIFECYCLE("Returning from ANativeActivity_onCreate");
}
// MARK: - UI thread callbacks
typedef void (*GLFMUIThreadFunc)(GLFMPlatformData *platformData, void *userData);
typedef struct {
GLFMUIThreadFunc function;
void *userData;
} GLFMLooperMessage;
// Called from the UI thread
__attribute__((no_sanitize("hwaddress"))) static int glfm__looperCallback(int pipe, int events, void *userData) {
GLFMPlatformData *platformData = userData;
GLFMLooperMessage message;
assert(ALooper_forThread() == platformData->uiLooper);
if ((events & ALOOPER_EVENT_INPUT) != 0) {
if (read(pipe, &message, sizeof(message)) == sizeof(message)) {
message.function(platformData, message.userData);
}
}
return 1;
}
/// Queues a function to execute on the UI thread.
/// Returns true if the function was queued, false otherwise.
__attribute__((no_sanitize("hwaddress"))) static bool glfm__runOnUIThread(GLFMPlatformData *platformData, GLFMUIThreadFunc function,
void *userData) {
assert(platformData->looper == ALooper_forThread());
if (platformData->looper != ALooper_forThread() || !function) {
return false;
}
GLFMLooperMessage message = { 0 };
message.function = function;
message.userData = userData;
if (write(platformData->uiCommandPipeWrite, &message, sizeof(message)) != sizeof(message)) {
// The pipe is full.
return false;
}
return true;
}
// MARK: - App command callback and input callbacks
__attribute__((no_sanitize("hwaddress"))) static void glfm__setAnimating(GLFMPlatformData *platformData, bool animating) {
if (platformData->animating != animating) {
platformData->animating = animating;
platformData->refreshRequested = true;
if (platformData->display && platformData->display->focusFunc) {
platformData->display->focusFunc(platformData->display, animating);
}
glfm__setAllRequestedSensorsEnabled(platformData->display, animating);
}
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__onAppCmd(GLFMPlatformData *platformData, GLFMActivityCommand command) {
switch (command) {
case GLFMActivityCommandOnNativeWindowCreated: {
GLFM_LOG_LIFECYCLE("OnNativeWindowCreated");
pthread_mutex_lock(&platformData->mutex);
platformData->window = platformData->pendingWindow;
pthread_cond_broadcast(&platformData->cond);
pthread_mutex_unlock(&platformData->mutex);
const bool success = glfm__eglInit(platformData);
if (!success) {
glfm__eglCheckError(platformData);
}
platformData->refreshRequested = true;
glfm__drawFrame(platformData);
break;
}
case GLFMActivityCommandOnNativeWindowResized: {
GLFM_LOG_LIFECYCLE("OnNativeWindowResized");
break;
}
case GLFMActivityCommandOnNativeWindowDestroyed: {
GLFM_LOG_LIFECYCLE("OnNativeWindowDestroyed");
platformData->window = NULL;
glfm__eglSurfaceDestroy(platformData);
glfm__setAnimating(platformData, false);
break;
}
case GLFMActivityCommandOnNativeWindowRedrawNeeded: {
GLFM_LOG_LIFECYCLE("OnNativeWindowRedrawNeeded");
platformData->refreshRequested = true;
break;
}
case GLFMActivityCommandOnWindowFocusGained: {
GLFM_LOG_LIFECYCLE("OnWindowFocusGained");
glfm__setAnimating(platformData, true);
break;
}
case GLFMActivityCommandOnWindowFocusLost: {
GLFM_LOG_LIFECYCLE("OnWindowFocusLost");
if (platformData->animating) {
platformData->refreshRequested = true;
glfm__drawFrame(platformData);
glfm__setAnimating(platformData, false);
}
break;
}
case GLFMActivityCommandOnContentRectChanged: {
// NOTE: Content rect might be the same, as this is also used as a global layout listener
#if GLFM_LOG_LIFECYCLE_ENABLE
ARect *oldRect = &platformData->contentRectArray[platformData->contentRectIndex ^ 1];
ARect *newRect = &platformData->contentRectArray[platformData->contentRectIndex];
GLFM_LOG_LIFECYCLE("OnContentRectChanged (from %i,%i,%i,%i to %i,%i,%i,%i)",
oldRect->left, oldRect->top, oldRect->right, oldRect->bottom,
newRect->left, newRect->top, newRect->right, newRect->bottom);
#endif
platformData->refreshRequested = true;
if (platformData->window) {
bool sizedChanged = glfm__updateSurfaceSizeIfNeeded(platformData->display, true);
if (!sizedChanged) {
glfm__reportOrientationChangeIfNeeded(platformData->display);
glfm__reportInsetsChangedIfNeeded(platformData->display);
glfm__updateKeyboardVisibility(platformData);
}
}
break;
}
case GLFMActivityCommandOnLowMemory: {
GLFM_LOG_LIFECYCLE("OnLowMemory");
if (platformData->display && platformData->display->lowMemoryFunc) {
platformData->display->lowMemoryFunc(platformData->display);
}
break;
}
case GLFMActivityCommandOnStart: {
GLFM_LOG_LIFECYCLE("OnStart");
// if (platformData->display && platformData->display->appEventFunc) {
// platformData->display->appEventFunc(platformData->display, GLFMAppEventStart);
// }
break;
}
case GLFMActivityCommandOnResume: {
GLFM_LOG_LIFECYCLE("OnResume");
// if (platformData->display && platformData->display->appEventFunc) {
// platformData->display->appEventFunc(platformData->display, GLFMAppEventResume);
// }
break;
}
case GLFMActivityCommandOnPause: {
GLFM_LOG_LIFECYCLE("OnPause");
// if (platformData->display && platformData->display->appEventFunc) {
// platformData->display->appEventFunc(platformData->display, GLFMAppEventPause);
// }
break;
}
case GLFMActivityCommandOnStop: {
GLFM_LOG_LIFECYCLE("OnStop");
// if (platformData->display && platformData->display->appEventFunc) {
// platformData->display->appEventFunc(platformData->display, GLFMAppEventStop);
// }
break;
}
case GLFMActivityCommandOnDestroy: {
GLFM_LOG_LIFECYCLE("OnDestroy");
glfm__eglDestroy(platformData);
glfm__setAnimating(platformData, false);
platformData->destroyRequested = true;
break;
}
case GLFMActivityCommandOnInputQueueCreated: {
GLFM_LOG_LIFECYCLE("OnInputQueueCreated");
pthread_mutex_lock(&platformData->mutex);
if (platformData->inputQueue) {
AInputQueue_detachLooper(platformData->inputQueue);
}
platformData->inputQueue = platformData->pendingInputQueue;
AInputQueue_attachLooper(platformData->inputQueue, platformData->looper,
GLFMLooperIDInput, NULL, NULL);
pthread_cond_broadcast(&platformData->cond);
pthread_mutex_unlock(&platformData->mutex);
break;
}
case GLFMActivityCommandOnInputQueueDestroyed: {
GLFM_LOG_LIFECYCLE("OnInputQueueDestroyed");
if (platformData->inputQueue) {
AInputQueue_detachLooper(platformData->inputQueue);
platformData->inputQueue = NULL;
}
break;
}
case GLFMActivityCommandOnConfigurationChanged: {
GLFM_LOG_LIFECYCLE("OnConfigurationChanged");
AConfiguration_fromAssetManager(platformData->config,
platformData->activity->assetManager);
break;
}
default: {
// Do nothing
break;
}
}
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__unicodeToUTF8(uint32_t unicode, char utf8[5]) {
if (unicode < 0x80) {
utf8[0] = (char)(unicode & 0x7fu);
utf8[1] = 0;
} else if (unicode < 0x800) {
utf8[0] = (char)(0xc0u | (unicode >> 6u));
utf8[1] = (char)(0x80u | (unicode & 0x3fu));
utf8[2] = 0;
} else if (unicode < 0x10000) {
utf8[0] = (char)(0xe0u | (unicode >> 12u));
utf8[1] = (char)(0x80u | ((unicode >> 6u) & 0x3fu));
utf8[2] = (char)(0x80u | (unicode & 0x3fu));
utf8[3] = 0;
} else if (unicode < 0x110000) {
utf8[0] = (char)(0xf0u | (unicode >> 18u));
utf8[1] = (char)(0x80u | ((unicode >> 12u) & 0x3fu));
utf8[2] = (char)(0x80u | ((unicode >> 6u) & 0x3fu));
utf8[3] = (char)(0x80u | (unicode & 0x3fu));
utf8[4] = 0;
} else {
utf8[0] = 0;
}
}
static uint32_t glfm__getUnicodeChar(GLFMPlatformData *platformData, jint keyCode, jint metaState) {
JNIEnv *jni = platformData->jniEnv;
if ((*jni)->ExceptionCheck(jni)) {
return 0;
}
jclass keyEventClass = (*jni)->FindClass(jni, "android/view/KeyEvent");
if (glfm__wasJavaExceptionThrown(jni) || !keyEventClass) {
return 0;
}
jmethodID getUnicodeChar = (*jni)->GetMethodID(jni, keyEventClass, "getUnicodeChar", "(I)I");
jmethodID eventConstructor = (*jni)->GetMethodID(jni, keyEventClass, "<init>", "(II)V");
jobject eventObject = (*jni)->NewObject(jni, keyEventClass, eventConstructor,
(jint)AKEY_EVENT_ACTION_DOWN, keyCode);
if (glfm__wasJavaExceptionThrown(jni) || !eventObject) {
return 0;
}
jint unicodeKey = (*jni)->CallIntMethod(jni, eventObject, getUnicodeChar, metaState);
(*jni)->DeleteLocalRef(jni, eventObject);
(*jni)->DeleteLocalRef(jni, keyEventClass);
if (glfm__wasJavaExceptionThrown(jni)) {
return 0;
}
return (uint32_t)unicodeKey;
}
/*
* Move task to the back if it is root task. This make the back button have the same behavior
* as the home button.
*
* Without this, when the user presses the back button, the loop in glfm__mainLoop() is exited, the
* OpenGL context is destroyed, and the main thread is destroyed. The glfm__mainLoop() function
* would be called again in the same process if the user returns to the app.
*
* When this, when the app is in the background, the app will pause in the ALooper_pollAll() call.
*/
__attribute__((no_sanitize("hwaddress"))) static bool glfm__handleBackButton(GLFMPlatformData *platformData) {
if (!platformData || !platformData->activity) {
return false;
}
JNIEnv *jni = platformData->jniEnv;
if ((*jni)->ExceptionCheck(jni)) {
return false;
}
jboolean handled = glfm__callJavaMethodWithArgs(jni, platformData->activity->clazz,
"moveTaskToBack", "(Z)Z", Boolean, false);
return !glfm__wasJavaExceptionThrown(jni) && handled;
}
__attribute__((no_sanitize("hwaddress"))) static bool glfm__onKeyEvent(GLFMPlatformData *platformData, AInputEvent *event) {
if (!platformData || !platformData->display) {
return false;
}
GLFMDisplay *display = platformData->display;
int32_t aAction = AKeyEvent_getAction(event);
int32_t aKeyCode = AKeyEvent_getKeyCode(event);
int32_t aMetaState = AKeyEvent_getMetaState(event);
if (aKeyCode == 0) {
// aKeyCode is 0 for many non-ASCII keys from the virtual keyboard.
return false;
}
if (aKeyCode == INT32_MAX) {
// This is a special key code for GLFM where the scancode represents a unicode character.
if (display->charFunc) {
uint32_t unicode = (uint32_t)AKeyEvent_getScanCode(event);
char utf8[5];
glfm__unicodeToUTF8(unicode, utf8);
display->charFunc(display, utf8, 0);
}
return true;
}
bool handled = false;
if (display->keyFunc) {
static const GLFMKeyCode AKEYCODE_MAP[] = {
[AKEYCODE_BACK] = GLFMKeyCodeNavigationBack,
[AKEYCODE_0] = GLFMKeyCode0,
[AKEYCODE_1] = GLFMKeyCode1,
[AKEYCODE_2] = GLFMKeyCode2,
[AKEYCODE_3] = GLFMKeyCode3,
[AKEYCODE_4] = GLFMKeyCode4,
[AKEYCODE_5] = GLFMKeyCode5,
[AKEYCODE_6] = GLFMKeyCode6,
[AKEYCODE_7] = GLFMKeyCode7,
[AKEYCODE_8] = GLFMKeyCode8,
[AKEYCODE_9] = GLFMKeyCode9,
[AKEYCODE_DPAD_UP] = GLFMKeyCodeArrowUp,
[AKEYCODE_DPAD_DOWN] = GLFMKeyCodeArrowDown,
[AKEYCODE_DPAD_LEFT] = GLFMKeyCodeArrowLeft,
[AKEYCODE_DPAD_RIGHT] = GLFMKeyCodeArrowRight,
[AKEYCODE_POWER] = GLFMKeyCodePower,
[AKEYCODE_A] = GLFMKeyCodeA,
[AKEYCODE_B] = GLFMKeyCodeB,
[AKEYCODE_C] = GLFMKeyCodeC,
[AKEYCODE_D] = GLFMKeyCodeD,
[AKEYCODE_E] = GLFMKeyCodeE,
[AKEYCODE_F] = GLFMKeyCodeF,
[AKEYCODE_G] = GLFMKeyCodeG,
[AKEYCODE_H] = GLFMKeyCodeH,
[AKEYCODE_I] = GLFMKeyCodeI,
[AKEYCODE_J] = GLFMKeyCodeJ,
[AKEYCODE_K] = GLFMKeyCodeK,
[AKEYCODE_L] = GLFMKeyCodeL,
[AKEYCODE_M] = GLFMKeyCodeM,
[AKEYCODE_N] = GLFMKeyCodeN,
[AKEYCODE_O] = GLFMKeyCodeO,
[AKEYCODE_P] = GLFMKeyCodeP,
[AKEYCODE_Q] = GLFMKeyCodeQ,
[AKEYCODE_R] = GLFMKeyCodeR,
[AKEYCODE_S] = GLFMKeyCodeS,
[AKEYCODE_T] = GLFMKeyCodeT,
[AKEYCODE_U] = GLFMKeyCodeU,
[AKEYCODE_V] = GLFMKeyCodeV,
[AKEYCODE_W] = GLFMKeyCodeW,
[AKEYCODE_X] = GLFMKeyCodeX,
[AKEYCODE_Y] = GLFMKeyCodeY,
[AKEYCODE_Z] = GLFMKeyCodeZ,
[AKEYCODE_COMMA] = GLFMKeyCodeComma,
[AKEYCODE_PERIOD] = GLFMKeyCodePeriod,
[AKEYCODE_ALT_LEFT] = GLFMKeyCodeAltLeft,
[AKEYCODE_ALT_RIGHT] = GLFMKeyCodeAltRight,
[AKEYCODE_SHIFT_LEFT] = GLFMKeyCodeShiftLeft,
[AKEYCODE_SHIFT_RIGHT] = GLFMKeyCodeShiftRight,
[AKEYCODE_TAB] = GLFMKeyCodeTab,
[AKEYCODE_SPACE] = GLFMKeyCodeSpace,
[AKEYCODE_ENTER] = GLFMKeyCodeEnter,
[AKEYCODE_DEL] = GLFMKeyCodeBackspace,
[AKEYCODE_GRAVE] = GLFMKeyCodeBackquote,
[AKEYCODE_MINUS] = GLFMKeyCodeMinus,
[AKEYCODE_EQUALS] = GLFMKeyCodeEqual,
[AKEYCODE_LEFT_BRACKET] = GLFMKeyCodeBracketLeft,
[AKEYCODE_RIGHT_BRACKET] = GLFMKeyCodeBracketRight,
[AKEYCODE_BACKSLASH] = GLFMKeyCodeBackslash,
[AKEYCODE_SEMICOLON] = GLFMKeyCodeSemicolon,
[AKEYCODE_APOSTROPHE] = GLFMKeyCodeQuote,
[AKEYCODE_SLASH] = GLFMKeyCodeSlash,
[AKEYCODE_MENU] = GLFMKeyCodeMenu,
[AKEYCODE_PAGE_UP] = GLFMKeyCodePageUp,
[AKEYCODE_PAGE_DOWN] = GLFMKeyCodePageDown,
[AKEYCODE_ESCAPE] = GLFMKeyCodeEscape,
[AKEYCODE_FORWARD_DEL] = GLFMKeyCodeDelete,
[AKEYCODE_CTRL_LEFT] = GLFMKeyCodeControlLeft,
[AKEYCODE_CTRL_RIGHT] = GLFMKeyCodeControlRight,
[AKEYCODE_CAPS_LOCK] = GLFMKeyCodeCapsLock,
[AKEYCODE_SCROLL_LOCK] = GLFMKeyCodeScrollLock,
[AKEYCODE_META_LEFT] = GLFMKeyCodeMetaLeft,
[AKEYCODE_META_RIGHT] = GLFMKeyCodeMetaRight,
[AKEYCODE_FUNCTION] = GLFMKeyCodeFunction,
[AKEYCODE_SYSRQ] = GLFMKeyCodePrintScreen,
[AKEYCODE_BREAK] = GLFMKeyCodePause,
[AKEYCODE_MOVE_HOME] = GLFMKeyCodeHome,
[AKEYCODE_MOVE_END] = GLFMKeyCodeEnd,
[AKEYCODE_INSERT] = GLFMKeyCodeInsert,
[AKEYCODE_F1] = GLFMKeyCodeF1,
[AKEYCODE_F2] = GLFMKeyCodeF2,
[AKEYCODE_F3] = GLFMKeyCodeF3,
[AKEYCODE_F4] = GLFMKeyCodeF4,
[AKEYCODE_F5] = GLFMKeyCodeF5,
[AKEYCODE_F6] = GLFMKeyCodeF6,
[AKEYCODE_F7] = GLFMKeyCodeF7,
[AKEYCODE_F8] = GLFMKeyCodeF8,
[AKEYCODE_F9] = GLFMKeyCodeF9,
[AKEYCODE_F10] = GLFMKeyCodeF10,
[AKEYCODE_F11] = GLFMKeyCodeF11,
[AKEYCODE_F12] = GLFMKeyCodeF12,
[AKEYCODE_NUM_LOCK] = GLFMKeyCodeNumLock,
[AKEYCODE_NUMPAD_0] = GLFMKeyCodeNumpad0,
[AKEYCODE_NUMPAD_1] = GLFMKeyCodeNumpad1,
[AKEYCODE_NUMPAD_2] = GLFMKeyCodeNumpad2,
[AKEYCODE_NUMPAD_3] = GLFMKeyCodeNumpad3,
[AKEYCODE_NUMPAD_4] = GLFMKeyCodeNumpad4,
[AKEYCODE_NUMPAD_5] = GLFMKeyCodeNumpad5,
[AKEYCODE_NUMPAD_6] = GLFMKeyCodeNumpad6,
[AKEYCODE_NUMPAD_7] = GLFMKeyCodeNumpad7,
[AKEYCODE_NUMPAD_8] = GLFMKeyCodeNumpad8,
[AKEYCODE_NUMPAD_9] = GLFMKeyCodeNumpad9,
[AKEYCODE_NUMPAD_DIVIDE] = GLFMKeyCodeNumpadDivide,
[AKEYCODE_NUMPAD_MULTIPLY] = GLFMKeyCodeNumpadMultiply,
[AKEYCODE_NUMPAD_SUBTRACT] = GLFMKeyCodeNumpadSubtract,
[AKEYCODE_NUMPAD_ADD] = GLFMKeyCodeNumpadAdd,
[AKEYCODE_NUMPAD_DOT] = GLFMKeyCodeNumpadDecimal,
[AKEYCODE_NUMPAD_ENTER] = GLFMKeyCodeNumpadEnter,
[AKEYCODE_NUMPAD_EQUALS] = GLFMKeyCodeNumpadEqual,
};
GLFMKeyCode keyCode = GLFMKeyCodeUnknown;
if (aKeyCode >= 0 && aKeyCode < (int32_t)(sizeof(AKEYCODE_MAP) / sizeof(*AKEYCODE_MAP))) {
keyCode = AKEYCODE_MAP[aKeyCode];
}
int modifiers = 0;
if ((aMetaState & AMETA_SHIFT_ON) != 0) {
modifiers |= GLFMKeyModifierShift;
}
if ((aMetaState & AMETA_CTRL_ON) != 0) {
modifiers |= GLFMKeyModifierControl;
}
if ((aMetaState & AMETA_ALT_ON) != 0) {
modifiers |= GLFMKeyModifierAlt;
}
if ((aMetaState & AMETA_META_ON) != 0) {
modifiers |= GLFMKeyModifierMeta;
}
if ((aMetaState & AMETA_FUNCTION_ON) != 0) {
modifiers |= GLFMKeyModifierFunction;
}
if (aAction == AKEY_EVENT_ACTION_UP) {
handled = display->keyFunc(display, keyCode, GLFMKeyActionReleased, modifiers);
} else if (aAction == AKEY_EVENT_ACTION_DOWN) {
GLFMKeyAction keyAction;
if (AKeyEvent_getRepeatCount(event) > 0) {
keyAction = GLFMKeyActionRepeated;
} else {
keyAction = GLFMKeyActionPressed;
}
handled = display->keyFunc(display, keyCode, keyAction, modifiers);
} else if (aAction == AKEY_EVENT_ACTION_MULTIPLE) {
for (int i = AKeyEvent_getRepeatCount(event); i > 0; i--) {
if (display->keyFunc) {
handled |= display->keyFunc(display, keyCode, GLFMKeyActionPressed, modifiers);
}
if (display->keyFunc) {
handled |= display->keyFunc(display, keyCode, GLFMKeyActionReleased, modifiers);
}
}
}
}
#if GLFM_HANDLE_BACK_BUTTON
if (!handled && aAction == AKEY_EVENT_ACTION_UP && aKeyCode == AKEYCODE_BACK) {
handled = glfm__handleBackButton(platformData);
}
#endif
if (display->charFunc && (aAction == AKEY_EVENT_ACTION_DOWN || aAction == AKEY_EVENT_ACTION_MULTIPLE)) {
uint32_t unicode = glfm__getUnicodeChar(platformData, aKeyCode, aMetaState);
if (unicode >= ' ') {
char utf8[5];
glfm__unicodeToUTF8(unicode, utf8);
if (aAction == AKEY_EVENT_ACTION_DOWN) {
display->charFunc(display, utf8, 0);
} else {
for (int i = AKeyEvent_getRepeatCount(event); i > 0; i--) {
if (display->charFunc) {
display->charFunc(display, utf8, 0);
}
}
}
}
}
return handled;
}
__attribute__((no_sanitize("hwaddress"))) static bool glfm__onTouchEvent(GLFMPlatformData *platformData, AInputEvent *event) {
if (!platformData || !platformData->display || !platformData->display->touchFunc) {
return false;
}
GLFMDisplay *display = platformData->display;
const int maxTouches = platformData->multitouchEnabled ? GLFM_MAX_SIMULTANEOUS_TOUCHES : 1;
const int32_t action = AMotionEvent_getAction(event);
const uint32_t maskedAction = (uint32_t)action & (uint32_t)AMOTION_EVENT_ACTION_MASK;
GLFMTouchPhase phase;
bool validAction = true;
bool allPointers = false;
switch (maskedAction) {
case AMOTION_EVENT_ACTION_DOWN:
allPointers = true;
case AMOTION_EVENT_ACTION_POINTER_DOWN:
phase = GLFMTouchPhaseBegan;
break;
case AMOTION_EVENT_ACTION_UP:
allPointers = true;
case AMOTION_EVENT_ACTION_POINTER_UP:
case AMOTION_EVENT_ACTION_OUTSIDE:
phase = GLFMTouchPhaseEnded;
break;
case AMOTION_EVENT_ACTION_MOVE:
phase = GLFMTouchPhaseMoved;
allPointers = true;
break;
case AMOTION_EVENT_ACTION_CANCEL:
phase = GLFMTouchPhaseCancelled;
allPointers = true;
break;
default:
phase = GLFMTouchPhaseCancelled;
validAction = false;
break;
}
if (validAction) {
if (allPointers)
{
const size_t count = AMotionEvent_getPointerCount(event);
for (size_t i = 0; i < count; i++) {
const int touchNumber = AMotionEvent_getPointerId(event, i);
if (touchNumber >= 0 && touchNumber < maxTouches && display->touchFunc) {
double x = (double)AMotionEvent_getX(event, i);
double y = (double)AMotionEvent_getY(event, i);
display->touchFunc(display, touchNumber, phase, x, y);
}
}
}
else {
const size_t index = (size_t)(((uint32_t)action &
(uint32_t)AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >>
(uint32_t)AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
const int touchNumber = AMotionEvent_getPointerId(event, index);
if (touchNumber >= 0 && touchNumber < maxTouches && display->touchFunc) {
double x = (double)AMotionEvent_getX(event, index);
double y = (double)AMotionEvent_getY(event, index);
display->touchFunc(display, touchNumber, phase, x, y);
}
}
}
return true;
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__onInputEvent(GLFMPlatformData *platformData) {
AInputEvent *event = NULL;
while (AInputQueue_getEvent(platformData->inputQueue, &event) >= 0) {
if (AInputQueue_preDispatchEvent(platformData->inputQueue, event)) {
continue;
}
bool handled = false;
int32_t eventType = AInputEvent_getType(event);
if (eventType == AINPUT_EVENT_TYPE_KEY) {
handled = glfm__onKeyEvent(platformData, event);
} else if (eventType == AINPUT_EVENT_TYPE_MOTION) {
handled = glfm__onTouchEvent(platformData, event);
}
AInputQueue_finishEvent(platformData->inputQueue, event, (int)handled);
}
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__onSensorEvent(GLFMPlatformData *platformData) {
ASensorEvent event;
bool sensorEventReceived[GLFM_NUM_SENSORS] = { 0 };
while (ASensorEventQueue_getEvents(platformData->sensorEventQueue, &event, 1) > 0) {
if (event.type == ASENSOR_TYPE_ACCELEROMETER) {
// Convert to iOS format
GLFMSensorEvent *sensorEvent = &platformData->sensorEvent[GLFMSensorAccelerometer];
sensorEvent->sensor = GLFMSensorAccelerometer;
sensorEvent->timestamp = (double)event.timestamp / 1000000000.0;
sensorEvent->vector.x = (double)event.acceleration.x / -(double)ASENSOR_STANDARD_GRAVITY;
sensorEvent->vector.y = (double)event.acceleration.y / -(double)ASENSOR_STANDARD_GRAVITY;
sensorEvent->vector.z = (double)event.acceleration.z / -(double)ASENSOR_STANDARD_GRAVITY;
sensorEventReceived[GLFMSensorAccelerometer] = true;
platformData->sensorEventValid[GLFMSensorAccelerometer] = true;
} else if (event.type == ASENSOR_TYPE_MAGNETIC_FIELD) {
GLFMSensorEvent *sensorEvent = &platformData->sensorEvent[GLFMSensorMagnetometer];
sensorEvent->sensor = GLFMSensorMagnetometer;
sensorEvent->timestamp = (double)event.timestamp / 1000000000.0;
sensorEvent->vector.x = (double)event.magnetic.x;
sensorEvent->vector.y = (double)event.magnetic.y;
sensorEvent->vector.z = (double)event.magnetic.z;
sensorEventReceived[GLFMSensorMagnetometer] = true;
platformData->sensorEventValid[GLFMSensorMagnetometer] = true;
} else if (event.type == ASENSOR_TYPE_GYROSCOPE) {
GLFMSensorEvent *sensorEvent = &platformData->sensorEvent[GLFMSensorGyroscope];
sensorEvent->sensor = GLFMSensorGyroscope;
sensorEvent->timestamp = (double)event.timestamp / 1000000000.0;
sensorEvent->vector.x = (double)event.vector.x;
sensorEvent->vector.y = (double)event.vector.y;
sensorEvent->vector.z = (double)event.vector.z;
sensorEventReceived[GLFMSensorGyroscope] = true;
platformData->sensorEventValid[GLFMSensorGyroscope] = true;
} else if (event.type == ASENSOR_TYPE_ROTATION_VECTOR) {
const int SDK_INT = platformData->activity->sdkVersion;
GLFMSensorEvent *sensorEvent = &platformData->sensorEvent[GLFMSensorRotationMatrix];
sensorEvent->sensor = GLFMSensorRotationMatrix;
sensorEvent->timestamp = (double)event.timestamp / 1000000000.0;
// Get unit quaternion
double qx = (double)event.vector.x;
double qy = (double)event.vector.y;
double qz = (double)event.vector.z;
double qw;
if (SDK_INT >= 18) {
qw = (double)event.data[3];
} else {
qw = 1 - (qx * qx + qy * qy + qz * qz);
qw = (qw > 0) ? sqrt(qw) : 0;
}
/*
* Convert unit quaternion to rotation matrix.
*
* First, convert Android's reference frame to the same as iOS.
* Android uses a reference frame where the Y axis points north,
* and iOS uses a reference frame where the X axis points north.
*
* To convert the unit quaternion, pre-multiply the unit quaternion by
* a rotation of -90 degrees around the Z axis.
*
* a=-90
* q1 = cos(a/2) + 0i + 0j + sin(a/2)k
*
* Which is the same as:
*
* f = sqrt(2)/2
* q1 = f + 0i + 0j - fk
*
* Multiplying two quaternions, where q2 is the original Android quaternion:
*
* q1q2 = (w1w2 - x1x2 - y1y2 - z1z2) +
* (w1x2 + x1w2 + y1z2 - z1y2)i +
* (w1y2 + z1x2 + y1w2 - x1z2)j +
* (w1z2 + x1y2 + z1w2 - y1x2)k
*
* Where x1 == 0, y1 == 0, z1 == -f, w1 == f:
*
* q1q2 = (f * (z2 + w2)) +
* (f * (y2 + x2))i +
* (f * (y2 - x2))j +
* (f * (z2 + w2))k
*
* In C:
*
* double f = sqrt(2)/2;
* double qx_ = f * (qy + qx);
* double qy_ = f * (qy - qx);
* double qz_ = f * (qz - qw);
* double qw_ = f * (qz + qw);
*
* However, since f*f == 0.5, and we don't need the converted quaternion,
* we can remove a few multiplications.
*/
#if 0
// Original (no conversion)
double qxx2 = qx * qx * 2;
double qxy2 = qx * qy * 2;
double qxz2 = qx * qz * 2;
double qxw2 = qx * qw * 2;
double qyy2 = qy * qy * 2;
double qyz2 = qy * qz * 2;
double qyw2 = qy * qw * 2;
double qzz2 = qz * qz * 2;
double qzw2 = qz * qw * 2;
#else
// Conversion to the same reference frame as iOS
double qx_ = qy + qx;
double qy_ = qy - qx;
double qz_ = qz - qw;
double qw_ = qz + qw;
double qxx2 = qx_ * qx_;
double qxy2 = qx_ * qy_;
double qxz2 = qx_ * qz_;
double qxw2 = qx_ * qw_;
double qyy2 = qy_ * qy_;
double qyz2 = qy_ * qz_;
double qyw2 = qy_ * qw_;
double qzz2 = qz_ * qz_;
double qzw2 = qz_ * qw_;
#endif
sensorEvent->matrix.m00 = 1 - qyy2 - qzz2;
sensorEvent->matrix.m10 = qxy2 - qzw2;
sensorEvent->matrix.m20 = qxz2 + qyw2;
sensorEvent->matrix.m01 = qxy2 + qzw2;
sensorEvent->matrix.m11 = 1 - qxx2 - qzz2;
sensorEvent->matrix.m21 = qyz2 - qxw2;
sensorEvent->matrix.m02 = qxz2 - qyw2;
sensorEvent->matrix.m12 = qyz2 + qxw2;
sensorEvent->matrix.m22 = 1 - qxx2 - qyy2;
sensorEventReceived[GLFMSensorRotationMatrix] = true;
platformData->sensorEventValid[GLFMSensorRotationMatrix] = true;
}
}
// Send callbacks
for (int i = 0; i < GLFM_NUM_SENSORS; i++) {
GLFMSensorFunc sensorFunc = platformData->display->sensorFuncs[i];
if (sensorFunc && sensorEventReceived[i]) {
sensorFunc(platformData->display, platformData->sensorEvent[i]);
}
}
}
// MARK: - Thread entry point
int pollAll(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result;
do {
result = ALooper_pollOnce(timeoutMillis, outFd, outEvents, outData);
} while (result == ALOOPER_POLL_CALLBACK);
return result;
}
__attribute__((no_sanitize("hwaddress"))) static void *glfm__mainLoop(void *param) {
GLFM_LOG_LIFECYCLE("glfm__mainLoop");
// Init platform data
GLFMPlatformData *platformData = param;
platformData->refreshRequested = true;
platformData->lastSwapTime = glfmGetTime();
platformData->config = AConfiguration_new();
AConfiguration_fromAssetManager(platformData->config, platformData->activity->assetManager);
// Init looper
platformData->looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
ALooper_addFd(platformData->looper, platformData->commandPipeRead,
GLFMLooperIDCommand, ALOOPER_EVENT_INPUT, NULL, NULL);
// Init java env
JavaVM *jvm = platformData->activity->vm;
(*jvm)->AttachCurrentThread(jvm, &platformData->jniEnv, NULL);
// Get display scale
const int ACONFIGURATION_DENSITY_ANY = 0xfffe; // Added in API 21
const int32_t density = AConfiguration_getDensity(platformData->config);
if (density == ACONFIGURATION_DENSITY_DEFAULT || density == ACONFIGURATION_DENSITY_NONE ||
density == ACONFIGURATION_DENSITY_ANY || density <= 0) {
platformData->scale = 1.0;
} else {
platformData->scale = density / 160.0;
}
// Call glfmMain() (once per instance)
if (platformData->display == NULL) {
GLFM_LOG_LIFECYCLE("glfmMain");
platformData->display = calloc(1, sizeof(GLFMDisplay));
platformData->display->platformData = platformData;
platformData->display->supportedOrientations = GLFMInterfaceOrientationAll;
platformData->display->swapBehavior = GLFMSwapBehaviorPlatformDefault;
platformData->resizeEventWaitFrames = GLFM_RESIZE_EVENT_MAX_WAIT_FRAMES;
glfmMain(platformData->display);
}
// Setup window params
int32_t windowFormat;
if (platformData->display->colorFormat == GLFMColorFormatRGB565) {
windowFormat = WINDOW_FORMAT_RGB_565;
} else {
windowFormat = WINDOW_FORMAT_RGBA_8888;
}
bool fullscreen = platformData->display->uiChrome == GLFMUserInterfaceChromeNone;
ANativeActivity_setWindowFormat(platformData->activity, windowFormat);
ANativeActivity_setWindowFlags(platformData->activity,
fullscreen ? AWINDOW_FLAG_FULLSCREEN : 0,
AWINDOW_FLAG_FULLSCREEN);
glfm__updateUserInterfaceChrome(platformData);
if (platformData->activity->sdkVersion >= 28) {
// Allow rendering in cutout areas ("safe area") in both portrait and landscape.
// Test this code in Settings -> Developer Options -> Simulate a display with a cutout.
static const int LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES = 0x00000001;
JNIEnv *jni = platformData->jniEnv;
jobject window = glfm__callJavaMethod(jni, platformData->activity->clazz, "getWindow",
"()Landroid/view/Window;", Object);
jobject attributes = glfm__callJavaMethod(jni, window, "getAttributes",
"()Landroid/view/WindowManager$LayoutParams;",
Object);
jclass clazz = (*jni)->GetObjectClass(jni, attributes);
jfieldID layoutInDisplayCutoutMode = (*jni)->GetFieldID(jni, clazz,
"layoutInDisplayCutoutMode", "I");
(*jni)->SetIntField(jni, attributes, layoutInDisplayCutoutMode,
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES);
(*jni)->DeleteLocalRef(jni, clazz);
(*jni)->DeleteLocalRef(jni, attributes);
(*jni)->DeleteLocalRef(jni, window);
}
// Get initial values for reporting changes. First insets are valid until later.
platformData->orientation = glfmGetInterfaceOrientation(platformData->display);
platformData->insets.valid = false;
// Notify thread running
pthread_mutex_lock(&platformData->mutex);
platformData->threadRunning = true;
pthread_cond_broadcast(&platformData->cond);
pthread_mutex_unlock(&platformData->mutex);
// Run the main loop
while (!platformData->destroyRequested) {
int eventIdentifier;
while ((eventIdentifier = pollAll(platformData->animating ? 0 : -1,
NULL, NULL, NULL)) >= 0) {
if (eventIdentifier == GLFMLooperIDCommand) {
uint8_t cmd = 0;
if (read(platformData->commandPipeRead, &cmd, sizeof(cmd)) == sizeof(cmd)) {
GLFMActivityCommand command = (GLFMActivityCommand)cmd;
glfm__onAppCmd(platformData, command);
} else {
GLFM_LOG("Couldn't read from pipe");
}
} else if (eventIdentifier == GLFMLooperIDInput) {
glfm__onInputEvent(platformData);
} else if (eventIdentifier == GLFMLooperIDSensor) {
glfm__onSensorEvent(platformData);
}
if (platformData->destroyRequested) {
break;
}
}
if (platformData->animating && platformData->display) {
platformData->swapCalled = false;
glfm__drawFrame(platformData);
if (!platformData->swapCalled) {
// Sleep until next swap time (1/60 second after last swap time)
const float refreshRate = glfm__getRefreshRate(platformData->display);
const double sleepUntilTime = platformData->lastSwapTime + 1.0 / (double)refreshRate;
double now = glfmGetTime();
if (now >= sleepUntilTime) {
platformData->lastSwapTime = now;
} else {
// Sleep until 500 microseconds before deadline
const double offset = 0.0005;
while (true) {
double sleepDuration = sleepUntilTime - now - offset;
if (sleepDuration <= 0) {
platformData->lastSwapTime = sleepUntilTime;
break;
}
useconds_t sleepDurationMicroseconds = (useconds_t) (sleepDuration * 1000000);
usleep(sleepDurationMicroseconds);
now = glfmGetTime();
}
}
}
}
}
// Cleanup
GLFM_LOG_LIFECYCLE("Destroying thread");
if (platformData->inputQueue) {
AInputQueue_detachLooper(platformData->inputQueue);
platformData->inputQueue = NULL;
}
if (platformData->sensorEventQueue) {
glfm__setAllRequestedSensorsEnabled(platformData->display, false);
ASensorManager *sensorManager = ASensorManager_getInstance();
ASensorManager_destroyEventQueue(sensorManager, platformData->sensorEventQueue);
platformData->sensorEventQueue = NULL;
}
if (platformData->config) {
AConfiguration_delete(platformData->config);
platformData->config = NULL;
}
glfm__eglDestroy(platformData);
glfm__setAnimating(platformData, false);
(*jvm)->DetachCurrentThread(jvm);
platformData->window = NULL;
platformData->looper = NULL;
// Notify thread no longer running
pthread_mutex_lock(&platformData->mutex);
platformData->threadRunning = false;
pthread_cond_broadcast(&platformData->cond);
pthread_mutex_unlock(&platformData->mutex);
// App is destroyed, but glfm__mainLoop() can be called again in the same process.
// Set GLFM_HANDLE_BACK_BUTTON to 0 to test this code.
return NULL;
}
// MARK: - GLFM private functions
__attribute__((no_sanitize("hwaddress"))) static jobject glfm__getDecorView(JNIEnv *jni, GLFMPlatformData *platformData)
{
if (!platformData || !platformData->activity || (*jni)->ExceptionCheck(jni)) {
return NULL;
}
jobject window = glfm__callJavaMethod(jni, platformData->activity->clazz, "getWindow", "()Landroid/view/Window;", Object);
if (glfm__wasJavaExceptionThrown(jni) || !window) {
return NULL;
}
jobject decorView = glfm__callJavaMethod(jni, window, "getDecorView", "()Landroid/view/View;", Object);
(*jni)->DeleteLocalRef(jni, window);
return glfm__wasJavaExceptionThrown(jni) ? NULL : decorView;
}
__attribute__((no_sanitize("hwaddress"))) static ARect glfm__getDecorViewRect(GLFMPlatformData *platformData, const ARect *defaultRect)
{
JNIEnv *jni = platformData->jniEnv;
if ((*jni)->ExceptionCheck(jni)) {
return *defaultRect;
}
jobject decorView = glfm__getDecorView(jni, platformData);
if (!decorView) {
return *defaultRect;
}
jintArray locationArray = (*jni)->NewIntArray(jni, 2);
if (!locationArray) {
(*jni)->DeleteLocalRef(jni, decorView);
return *defaultRect;
}
jint location[2] = { 0 };
glfm__callJavaMethodWithArgs(jni, decorView, "getLocationInWindow", "([I)V", Void, locationArray);
(*jni)->GetIntArrayRegion(jni, locationArray, 0, 2, location);
(*jni)->DeleteLocalRef(jni, locationArray);
if ((*jni)->ExceptionCheck(jni)) {
(*jni)->DeleteLocalRef(jni, decorView);
return *defaultRect;
}
jint width = glfm__callJavaMethod(jni, decorView, "getWidth", "()I", Int);
jint height = glfm__callJavaMethod(jni, decorView, "getHeight", "()I", Int);
(*jni)->DeleteLocalRef(jni, decorView);
if ((*jni)->ExceptionCheck(jni)) {
return *defaultRect;
}
ARect result;
result.left = location[0];
result.top = location[1];
result.right = location[0] + width;
result.bottom = location[1] + height;
return result;
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__updateUserInterfaceChromeCallback(GLFMPlatformData *platformData, void *userData) {
(void)userData;
glfm__updateUserInterfaceChrome(platformData);
}
// Can be called from either native thread or UI thread
__attribute__((no_sanitize("hwaddress"))) static void glfm__updateUserInterfaceChrome(GLFMPlatformData *platformData) {
static const unsigned int View_STATUS_BAR_HIDDEN = 0x00000001;
static const unsigned int View_SYSTEM_UI_FLAG_LOW_PROFILE = 0x00000001;
static const unsigned int View_SYSTEM_UI_FLAG_HIDE_NAVIGATION = 0x00000002;
static const unsigned int View_SYSTEM_UI_FLAG_FULLSCREEN = 0x00000004;
static const unsigned int View_SYSTEM_UI_FLAG_LAYOUT_STABLE = 0x00000100;
static const unsigned int View_SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION = 0x00000200;
static const unsigned int View_SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 0x00000400;
static const unsigned int View_SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 0x00001000;
if (!platformData || !platformData->activity) {
return;
}
const int SDK_INT = platformData->activity->sdkVersion;
if (SDK_INT < 11) {
return;
}
JavaVM *jvm = platformData->activity->vm;
JNIEnv *jni = NULL;
(*jvm)->GetEnv(jvm, (void **) &jni, JNI_VERSION_1_2);
if (!jni || (*jni)->ExceptionCheck(jni)) {
return;
}
jobject decorView = glfm__getDecorView(jni, platformData);
if (!decorView) {
return;
}
GLFMUserInterfaceChrome uiChrome = platformData->display->uiChrome;
bool setNow = true;
bool isUiThread = ALooper_forThread() == platformData->uiLooper;
if (!isUiThread) {
bool isDecorViewAttached;
if (SDK_INT >= 19) {
isDecorViewAttached = glfm__callJavaMethod(jni, decorView, "isAttachedToWindow", "()Z", Boolean);
} else {
isDecorViewAttached = glfm__callJavaMethod(jni, decorView, "getWindowToken", "()Landroid/os/IBinder;", Object) != NULL;
}
if (glfm__wasJavaExceptionThrown(jni)) {
(*jni)->DeleteLocalRef(jni, decorView);
return;
}
setNow = !isDecorViewAttached;
}
if (!setNow) {
// Set on the UI thread
glfm__runOnUIThread(platformData, glfm__updateUserInterfaceChromeCallback, NULL);
} else {
// Set now
if (SDK_INT >= 30) {
jobject windowInsetsController = glfm__callJavaMethod(jni, decorView, "getWindowInsetsController", "()Landroid/view/WindowInsetsController;", Object);
jclass windowInsetsTypeClass = (*jni)->FindClass(jni, "android/view/WindowInsets$Type");
if (windowInsetsController && windowInsetsTypeClass && !glfm__wasJavaExceptionThrown(jni)) {
static const jint WindowInsetsController_BEHAVIOR_DEFAULT = 1;
static const jint WindowInsetsController_BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2;
const jint systemBars = glfm__callJavaStaticMethod(jni, windowInsetsTypeClass, "systemBars", "()I", Int);
if (uiChrome == GLFMUserInterfaceChromeNone) {
glfm__callJavaMethodWithArgs(jni, windowInsetsController, "setSystemBarsBehavior", "(I)V", Void,
WindowInsetsController_BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
glfm__callJavaMethodWithArgs(jni, windowInsetsController, "hide", "(I)V", Void, systemBars);
} else {
glfm__callJavaMethodWithArgs(jni, windowInsetsController, "setSystemBarsBehavior", "(I)V", Void,
WindowInsetsController_BEHAVIOR_DEFAULT);
if (uiChrome == GLFMUserInterfaceChromeNavigationAndStatusBar) {
glfm__callJavaMethodWithArgs(jni, windowInsetsController, "show", "(I)V", Void, systemBars);
} else if (uiChrome == GLFMUserInterfaceChromeNavigation) {
const jint statusBars = glfm__callJavaStaticMethod(jni, windowInsetsTypeClass, "statusBars", "()I", Int);
glfm__callJavaMethodWithArgs(jni, windowInsetsController, "hide", "(I)V", Void, statusBars);
glfm__callJavaMethodWithArgs(jni, windowInsetsController, "show", "(I)V", Void, systemBars & ~statusBars);
}
}
(*jni)->DeleteLocalRef(jni, windowInsetsController);
(*jni)->DeleteLocalRef(jni, windowInsetsTypeClass);
glfm__clearJavaException(jni);
}
} else {
unsigned int systemUiVisibility = 0;
if (uiChrome == GLFMUserInterfaceChromeNavigationAndStatusBar) {
systemUiVisibility = 0;
} else if (SDK_INT >= 11 && SDK_INT < 14) {
systemUiVisibility = View_STATUS_BAR_HIDDEN;
} else if (SDK_INT >= 14 && SDK_INT < 19) {
if (uiChrome == GLFMUserInterfaceChromeNavigation) {
systemUiVisibility = View_SYSTEM_UI_FLAG_FULLSCREEN;
} else {
systemUiVisibility = (View_SYSTEM_UI_FLAG_LOW_PROFILE | View_SYSTEM_UI_FLAG_FULLSCREEN);
}
} else if (SDK_INT >= 19) {
if (uiChrome == GLFMUserInterfaceChromeNavigation) {
systemUiVisibility = View_SYSTEM_UI_FLAG_FULLSCREEN;
} else {
systemUiVisibility = (View_SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View_SYSTEM_UI_FLAG_FULLSCREEN |
View_SYSTEM_UI_FLAG_LAYOUT_STABLE |
View_SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View_SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View_SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
}
glfm__callJavaMethodWithArgs(jni, decorView, "setSystemUiVisibility", "(I)V", Void,
(jint)systemUiVisibility);
glfm__clearJavaException(jni);
}
}
(*jni)->DeleteLocalRef(jni, decorView);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__resetContentRect(GLFMPlatformData *platformData) {
// Reset's NativeActivity's content rect so that onContentRectChanged acts as a
// OnGlobalLayoutListener. This is needed to detect changes to getWindowVisibleDisplayFrame()
// HACK: This uses undocumented fields.
JavaVM *jvm = platformData->activity->vm;
JNIEnv *jni = NULL;
(*jvm)->GetEnv(jvm, (void **) &jni, JNI_VERSION_1_2);
if (!jni || (*jni)->ExceptionCheck(jni)) {
return;
}
jfieldID field = glfm__getJavaFieldID(jni, platformData->activity->clazz,
"mLastContentWidth", "I");
if (glfm__wasJavaExceptionThrown(jni) || !field) {
return;
}
(*jni)->SetIntField(jni, platformData->activity->clazz, field, -1);
glfm__clearJavaException(jni);
}
__attribute__((no_sanitize("hwaddress"))) static ARect glfm__getWindowVisibleDisplayFrame(GLFMPlatformData *platformData, const ARect *defaultRect)
{
JNIEnv *jni = platformData->jniEnv;
if ((*jni)->ExceptionCheck(jni)) {
return *defaultRect;
}
jobject decorView = glfm__getDecorView(jni, platformData);
if (!decorView) {
return *defaultRect;
}
jclass javaRectClass = (*jni)->FindClass(jni, "android/graphics/Rect");
if (glfm__wasJavaExceptionThrown(jni)) {
return *defaultRect;
}
jobject javaRect = (*jni)->AllocObject(jni, javaRectClass);
if (glfm__wasJavaExceptionThrown(jni)) {
return *defaultRect;
}
glfm__callJavaMethodWithArgs(jni, decorView, "getWindowVisibleDisplayFrame",
"(Landroid/graphics/Rect;)V", Void, javaRect);
if (glfm__wasJavaExceptionThrown(jni)) {
return *defaultRect;
}
ARect rect;
rect.left = glfm__getJavaField(jni, javaRect, "left", "I", Int);
rect.right = glfm__getJavaField(jni, javaRect, "right", "I", Int);
rect.top = glfm__getJavaField(jni, javaRect, "top", "I", Int);
rect.bottom = glfm__getJavaField(jni, javaRect, "bottom", "I", Int);
(*jni)->DeleteLocalRef(jni, javaRect);
(*jni)->DeleteLocalRef(jni, javaRectClass);
if (glfm__wasJavaExceptionThrown(jni)) {
return *defaultRect;
}
jintArray locationArray = (*jni)->NewIntArray(jni, 2);
if (locationArray) {
jint location[2] = { 0 };
glfm__callJavaMethodWithArgs(jni, decorView, "getLocationOnScreen", "([I)V", Void,
locationArray);
(*jni)->GetIntArrayRegion(jni, locationArray, 0, 2, location);
(*jni)->DeleteLocalRef(jni, locationArray);
if (!glfm__wasJavaExceptionThrown(jni)) {
rect.left -= location[0];
rect.top -= location[1];
}
}
(*jni)->DeleteLocalRef(jni, decorView);
return rect;
}
__attribute__((no_sanitize("hwaddress"))) static bool glfm__getSafeInsets(const GLFMDisplay *display, int *top, int *right,
int *bottom, int *left) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
const int SDK_INT = platformData->activity->sdkVersion;
if (SDK_INT < 28) {
return false;
}
JNIEnv *jni = platformData->jniEnv;
jobject decorView = glfm__getDecorView(jni, platformData);
if (!decorView) {
return false;
}
jobject insets = glfm__callJavaMethod(jni, decorView, "getRootWindowInsets",
"()Landroid/view/WindowInsets;", Object);
(*jni)->DeleteLocalRef(jni, decorView);
if (!insets) {
return false;
}
jobject cutouts = glfm__callJavaMethod(jni, insets, "getDisplayCutout", "()Landroid/view/DisplayCutout;", Object);
(*jni)->DeleteLocalRef(jni, insets);
if (!cutouts) {
return false;
}
*top = glfm__callJavaMethod(jni, cutouts, "getSafeInsetTop", "()I", Int);
*right = glfm__callJavaMethod(jni, cutouts, "getSafeInsetRight", "()I", Int);
*bottom = glfm__callJavaMethod(jni, cutouts, "getSafeInsetBottom", "()I", Int);
*left = glfm__callJavaMethod(jni, cutouts, "getSafeInsetLeft", "()I", Int);
(*jni)->DeleteLocalRef(jni, cutouts);
return true;
}
__attribute__((no_sanitize("hwaddress"))) static bool glfm__getSystemWindowInsets(const GLFMDisplay *display, int *top, int *right,
int *bottom, int *left) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
const int SDK_INT = platformData->activity->sdkVersion;
if (SDK_INT < 20) {
return false;
}
JNIEnv *jni = platformData->jniEnv;
jobject decorView = glfm__getDecorView(jni, platformData);
if (!decorView) {
return false;
}
jobject insets = glfm__callJavaMethod(jni, decorView, "getRootWindowInsets",
"()Landroid/view/WindowInsets;", Object);
(*jni)->DeleteLocalRef(jni, decorView);
if (!insets) {
return false;
}
*top = glfm__callJavaMethod(jni, insets, "getSystemWindowInsetTop", "()I", Int);
*right = glfm__callJavaMethod(jni, insets, "getSystemWindowInsetRight", "()I", Int);
*bottom = glfm__callJavaMethod(jni, insets, "getSystemWindowInsetBottom", "()I", Int);
*left = glfm__callJavaMethod(jni, insets, "getSystemWindowInsetLeft", "()I", Int);
(*jni)->DeleteLocalRef(jni, insets);
return true;
}
// Calls activity.getWindow().getWindowManager().getDefaultDisplay()
__attribute__((no_sanitize("hwaddress"))) static jobject glfm__getWindowDisplay(GLFMPlatformData *platformData) {
JNIEnv *jni = platformData->jniEnv;
jobject activity = platformData->activity->clazz;
jobject window = glfm__callJavaMethod(jni, activity, "getWindow",
"()Landroid/view/Window;", Object);
if (glfm__wasJavaExceptionThrown(jni) || !window) {
return NULL;
}
jobject windowManager = glfm__callJavaMethod(jni, window, "getWindowManager",
"()Landroid/view/WindowManager;", Object);
(*jni)->DeleteLocalRef(jni, window);
if (glfm__wasJavaExceptionThrown(jni) || !windowManager) {
return NULL;
}
jobject windowDisplay = glfm__callJavaMethod(jni, windowManager, "getDefaultDisplay",
"()Landroid/view/Display;", Object);
(*jni)->DeleteLocalRef(jni, windowManager);
if (glfm__wasJavaExceptionThrown(jni)) {
return NULL;
}
return windowDisplay;
}
__attribute__((no_sanitize("hwaddress"))) static float glfm__getRefreshRate(const GLFMDisplay *display) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
JNIEnv *jni = platformData->jniEnv;
float refreshRate = -1;
jobject windowDisplay = glfm__getWindowDisplay(platformData);
if (windowDisplay) {
refreshRate = glfm__callJavaMethod(jni, windowDisplay, "getRefreshRate","()F", Float);
(*jni)->DeleteLocalRef(jni, windowDisplay);
}
if (glfm__wasJavaExceptionThrown(jni) || refreshRate <= 0) {
return 60;
}
return refreshRate;
}
__attribute__((no_sanitize("hwaddress"))) static bool glfm__updateSurfaceSizeIfNeeded(GLFMDisplay *display, bool force) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
int32_t width = 0;
int32_t height = 0;
EGLBoolean success = true;
success &= eglQuerySurface(platformData->eglDisplay, platformData->eglSurface, EGL_WIDTH, &width);
success &= eglQuerySurface(platformData->eglDisplay, platformData->eglSurface, EGL_HEIGHT, &height);
if (success && (width != platformData->width || height != platformData->height)) {
if (force || platformData->resizeEventWaitFrames <= 0) {
GLFM_LOG_LIFECYCLE("Resize: %i x %i", width, height);
platformData->resizeEventWaitFrames = GLFM_RESIZE_EVENT_MAX_WAIT_FRAMES;
platformData->refreshRequested = true;
platformData->width = width;
platformData->height = height;
if (platformData->display && platformData->display->surfaceResizedFunc) {
platformData->display->surfaceResizedFunc(platformData->display, width, height);
}
glfm__reportOrientationChangeIfNeeded(platformData->display);
glfm__reportInsetsChangedIfNeeded(platformData->display);
glfm__updateKeyboardVisibility(platformData);
return true;
}
// Prefer to wait until after content rect changed, if possible
platformData->resizeEventWaitFrames--;
}
return false;
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__getDisplayChromeInsets(const GLFMDisplay *display, int *top, int *right,
int *bottom, int *left) {
bool success;
if (glfmGetDisplayChrome(display) == GLFMUserInterfaceChromeNone) {
success = glfm__getSafeInsets(display, top, right, bottom, left);
} else {
success = glfm__getSystemWindowInsets(display, top, right, bottom, left);
}
if (!success) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
const ARect *contentRect = &platformData->contentRectArray[platformData->contentRectIndex];
ARect visibleRect = glfm__getWindowVisibleDisplayFrame(platformData, contentRect);
if (visibleRect.right - visibleRect.left <= 0 || visibleRect.bottom - visibleRect.top <= 0) {
*top = 0;
*right = 0;
*bottom = 0;
*left = 0;
} else {
*top = visibleRect.top;
*right = platformData->width - visibleRect.right;
*bottom = platformData->height - visibleRect.bottom;
*left = visibleRect.left;
}
}
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__reportInsetsChangedIfNeeded(GLFMDisplay *display) {
if (!display) {
return;
}
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
int top, right, bottom, left;
glfm__getDisplayChromeInsets(display, &top, &right, &bottom, &left);
if (platformData->insets.top != top || platformData->insets.right != right ||
platformData->insets.bottom != bottom || platformData->insets.left != left) {
platformData->insets.top = top;
platformData->insets.right = right;
platformData->insets.bottom = bottom;
platformData->insets.left = left;
if (display->displayChromeInsetsChangedFunc && platformData->insets.valid) {
display->displayChromeInsetsChangedFunc(display, (double)top, (double)top,
(double)bottom, (double)left);
}
}
platformData->insets.valid = true;
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__reportOrientationChangeIfNeeded(GLFMDisplay *display) {
if (!display) {
return;
}
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
GLFMInterfaceOrientation orientation = glfmGetInterfaceOrientation(display);
if (platformData->orientation != orientation) {
platformData->orientation = orientation;
platformData->refreshRequested = true;
if (display->orientationChangedFunc) {
display->orientationChangedFunc(display, orientation);
}
}
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__setOrientation(GLFMPlatformData *platformData) {
static const int ActivityInfo_SCREEN_ORIENTATION_SENSOR = 0x00000004;
static const int ActivityInfo_SCREEN_ORIENTATION_SENSOR_LANDSCAPE = 0x00000006;
static const int ActivityInfo_SCREEN_ORIENTATION_SENSOR_PORTRAIT = 0x00000007;
if (!platformData || !platformData->activity) {
return;
}
GLFMInterfaceOrientation orientations = platformData->display->supportedOrientations;
bool portraitRequested = (
((uint8_t)orientations & (uint8_t)GLFMInterfaceOrientationPortrait) ||
((uint8_t)orientations & (uint8_t)GLFMInterfaceOrientationPortraitUpsideDown));
bool landscapeRequested = ((uint8_t)orientations & (uint8_t)GLFMInterfaceOrientationLandscape);
int orientation;
if (portraitRequested && landscapeRequested) {
orientation = ActivityInfo_SCREEN_ORIENTATION_SENSOR;
} else if (landscapeRequested) {
orientation = ActivityInfo_SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
} else {
orientation = ActivityInfo_SCREEN_ORIENTATION_SENSOR_PORTRAIT;
}
JNIEnv *jni = platformData->jniEnv;
if ((*jni)->ExceptionCheck(jni)) {
return;
}
glfm__callJavaMethodWithArgs(jni, platformData->activity->clazz, "setRequestedOrientation", "(I)V", Void, orientation);
glfm__clearJavaException(jni);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__displayChromeUpdated(GLFMDisplay *display) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
glfm__updateUserInterfaceChrome(platformData);
}
static const ASensor *glfm__getDeviceSensor(GLFMSensor sensor) {
ASensorManager *sensorManager = ASensorManager_getInstance();
switch (sensor) {
case GLFMSensorAccelerometer:
return ASensorManager_getDefaultSensor(sensorManager, ASENSOR_TYPE_ACCELEROMETER);
case GLFMSensorMagnetometer:
return ASensorManager_getDefaultSensor(sensorManager, ASENSOR_TYPE_MAGNETIC_FIELD);
case GLFMSensorGyroscope:
return ASensorManager_getDefaultSensor(sensorManager, ASENSOR_TYPE_GYROSCOPE);
case GLFMSensorRotationMatrix:
return ASensorManager_getDefaultSensor(sensorManager, ASENSOR_TYPE_ROTATION_VECTOR);
default:
return NULL;
}
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__setAllRequestedSensorsEnabled(GLFMDisplay *display, bool enabledGlobally) {
if (!display) {
return;
}
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
for (int i = 0; i < GLFM_NUM_SENSORS; i++) {
GLFMSensor sensor = (GLFMSensor)i;
const ASensor *deviceSensor = glfm__getDeviceSensor(sensor);
bool isNeededEnabled = display->sensorFuncs[i] != NULL;
bool shouldEnable = enabledGlobally && isNeededEnabled;
bool isEnabled = platformData->deviceSensorEnabled[i];
if (!shouldEnable) {
platformData->sensorEventValid[i] = false;
}
if (isEnabled == shouldEnable || deviceSensor == NULL) {
continue;
}
if (platformData->sensorEventQueue == NULL) {
ASensorManager *sensorManager = ASensorManager_getInstance();
platformData->sensorEventQueue = ASensorManager_createEventQueue(sensorManager,
ALooper_forThread(), GLFMLooperIDSensor, NULL, NULL);
if (!platformData->sensorEventQueue) {
continue;
}
}
if (shouldEnable && !isEnabled) {
if (ASensorEventQueue_enableSensor(platformData->sensorEventQueue, deviceSensor) == 0) {
int minDelay = ASensor_getMinDelay(deviceSensor);
if (minDelay > 0) {
int delay = GLFM_SENSOR_UPDATE_INTERVAL_MICROS;
if (delay < minDelay) {
delay = minDelay;
}
ASensorEventQueue_setEventRate(platformData->sensorEventQueue, deviceSensor, delay);
}
platformData->deviceSensorEnabled[i] = true;
}
} else if (!shouldEnable && isEnabled) {
if (ASensorEventQueue_disableSensor(platformData->sensorEventQueue, deviceSensor) == 0) {
platformData->deviceSensorEnabled[i] = false;
}
}
}
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__sensorFuncUpdated(GLFMDisplay *display) {
if (display) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
glfm__setAllRequestedSensorsEnabled(display, platformData->animating);
}
}
/// Gets an Android system service. The "serviceName" is a field from android.content.Context,
/// like "INPUT_METHOD_SERVICE" or "VIBRATOR_SERVICE".
///
/// The C code:
/// glfm__getSystemService(platformData, "INPUT_METHOD_SERVICE")
/// will invoke the java code:
/// activity.getSystemService(Context.INPUT_METHOD_SERVICE);
static jobject glfm__getSystemService(GLFMPlatformData *platformData, const char *serviceName) {
JNIEnv *jni = platformData->jniEnv;
jclass contextClass = (*jni)->FindClass(jni, "android/content/Context");
if (glfm__wasJavaExceptionThrown(jni)) {
return NULL;
}
jstring serviceNameString = glfm__getJavaStaticField(jni, contextClass, serviceName,
"Ljava/lang/String;", Object);
(*jni)->DeleteLocalRef(jni, contextClass);
if (glfm__wasJavaExceptionThrown(jni) || !serviceNameString) {
return NULL;
}
jobject service = glfm__callJavaMethodWithArgs(jni, platformData->activity->clazz,
"getSystemService",
"(Ljava/lang/String;)Ljava/lang/Object;",
Object, serviceNameString);
(*jni)->DeleteLocalRef(jni, serviceNameString);
if (glfm__wasJavaExceptionThrown(jni)) {
return NULL;
}
return service;
}
__attribute__((no_sanitize("hwaddress"))) static bool glfm__setKeyboardVisible(GLFMPlatformData *platformData, bool visible) {
static const int InputMethodManager_SHOW_FORCED = 2;
JNIEnv *jni = platformData->jniEnv;
if ((*jni)->ExceptionCheck(jni)) {
return false;
}
jobject decorView = glfm__getDecorView(jni, platformData);
if (!decorView) {
return false;
}
jobject ime = glfm__getSystemService(platformData, "INPUT_METHOD_SERVICE");
if (!ime) {
return false;
}
if (visible) {
int flags = 0;
if (platformData->activity->sdkVersion < 23) {
// This flag was deprecated in API 33. It was required for older versions of Android,
// but it kept the soft keyboard open when leaving the app. At some point, the flag was
// no longer required (possibly for versions prior to 23.)
flags = InputMethodManager_SHOW_FORCED;
}
glfm__callJavaMethodWithArgs(jni, ime, "showSoftInput", "(Landroid/view/View;I)Z", Boolean,
decorView, flags);
} else {
jobject windowToken = glfm__callJavaMethod(jni, decorView, "getWindowToken", "()Landroid/os/IBinder;", Object);
if (glfm__wasJavaExceptionThrown(jni) || !windowToken) {
return false;
}
glfm__callJavaMethodWithArgs(jni, ime, "hideSoftInputFromWindow",
"(Landroid/os/IBinder;I)Z", Boolean, windowToken, 0);
(*jni)->DeleteLocalRef(jni, windowToken);
}
(*jni)->DeleteLocalRef(jni, ime);
(*jni)->DeleteLocalRef(jni, decorView);
return !glfm__wasJavaExceptionThrown(jni);
}
__attribute__((no_sanitize("hwaddress"))) static void glfm__updateKeyboardVisibility(GLFMPlatformData *platformData) {
if (platformData->display) {
const ARect *contentRect = &platformData->contentRectArray[platformData->contentRectIndex];
ARect windowRect = glfm__getDecorViewRect(platformData, contentRect);
ARect visibleRect = glfm__getWindowVisibleDisplayFrame(platformData, &windowRect);
ARect nonVisibleRect[4];
// Left
nonVisibleRect[0].left = windowRect.left;
nonVisibleRect[0].right = visibleRect.left;
nonVisibleRect[0].top = windowRect.top;
nonVisibleRect[0].bottom = windowRect.bottom;
// Right
nonVisibleRect[1].left = visibleRect.right;
nonVisibleRect[1].right = windowRect.right;
nonVisibleRect[1].top = windowRect.top;
nonVisibleRect[1].bottom = windowRect.bottom;
// Top
nonVisibleRect[2].left = windowRect.left;
nonVisibleRect[2].right = windowRect.right;
nonVisibleRect[2].top = windowRect.top;
nonVisibleRect[2].bottom = visibleRect.top;
// Bottom
nonVisibleRect[3].left = windowRect.left;
nonVisibleRect[3].right = windowRect.right;
nonVisibleRect[3].top = visibleRect.bottom;
nonVisibleRect[3].bottom = windowRect.bottom;
// Find largest with minimum keyboard size
const int minimumKeyboardSize = (int)(100 * platformData->scale);
int largestIndex = 0;
int largestArea = -1;
for (int i = 0; i < 4; i++) {
int width = nonVisibleRect[i].right - nonVisibleRect[i].left;
int height = nonVisibleRect[i].bottom - nonVisibleRect[i].top;
int area = width * height;
if (width >= minimumKeyboardSize && height >= minimumKeyboardSize && area > largestArea) {
largestIndex = i;
largestArea = area;
}
}
bool keyboardVisible = largestArea > 0;
ARect keyboardFrame = keyboardVisible ? nonVisibleRect[largestIndex] : (ARect){ 0 };
// Send update notification
if (platformData->keyboardVisible != keyboardVisible ||
platformData->keyboardFrame.left != keyboardFrame.left ||
platformData->keyboardFrame.top != keyboardFrame.top ||
platformData->keyboardFrame.right != keyboardFrame.right ||
platformData->keyboardFrame.bottom != keyboardFrame.bottom) {
if (platformData->keyboardVisible != keyboardVisible) {
glfm__updateUserInterfaceChrome(platformData);
}
platformData->keyboardVisible = keyboardVisible;
platformData->keyboardFrame = keyboardFrame;
platformData->refreshRequested = true;
if (platformData->display->keyboardVisibilityChangedFunc) {
double x = keyboardFrame.left;
double y = keyboardFrame.top;
double width = keyboardFrame.right - keyboardFrame.left;
double height = keyboardFrame.bottom - keyboardFrame.top;
platformData->display->keyboardVisibilityChangedFunc(platformData->display,
keyboardVisible,
x, y, width, height);
}
}
}
}
// MARK: - GLFM public functions
double glfmGetTime(void) {
static int clockID;
static time_t initTime;
static bool initialized = false;
struct timespec time;
if (!initialized) {
if (clock_gettime(CLOCK_MONOTONIC_RAW, &time) == 0) {
clockID = CLOCK_MONOTONIC_RAW;
} else if (clock_gettime(CLOCK_MONOTONIC, &time) == 0) {
clockID = CLOCK_MONOTONIC;
} else {
clock_gettime(CLOCK_REALTIME, &time);
clockID = CLOCK_REALTIME;
}
initTime = time.tv_sec;
initialized = true;
} else {
clock_gettime(clockID, &time);
}
// Subtract by initTime to ensure that conversion to double keeps nanosecond accuracy
return (double)(time.tv_sec - initTime) + (double)time.tv_nsec / 1e9;
}
void glfmSwapBuffers(GLFMDisplay *display) {
if (display) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
EGLBoolean result = eglSwapBuffers(platformData->eglDisplay, platformData->eglSurface);
platformData->swapCalled = true;
platformData->lastSwapTime = glfmGetTime();
if (!result) {
glfm__eglCheckError(platformData);
}
}
}
__attribute__((no_sanitize("hwaddress"))) void glfmSetSupportedInterfaceOrientation(GLFMDisplay *display, GLFMInterfaceOrientation supportedOrientations) {
if (display && display->supportedOrientations != supportedOrientations) {
display->supportedOrientations = supportedOrientations;
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
glfm__setOrientation(platformData);
}
}
__attribute__((no_sanitize("hwaddress"))) GLFMInterfaceOrientation glfmGetInterfaceOrientation(const GLFMDisplay *display) {
enum {
Surface_ROTATION_0 = 0,
Surface_ROTATION_90 = 1,
Surface_ROTATION_180 = 2,
Surface_ROTATION_270 = 3,
};
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
JNIEnv *jni = platformData->jniEnv;
jobject windowDisplay = glfm__getWindowDisplay(platformData);
if (!windowDisplay) {
return GLFMInterfaceOrientationUnknown;
}
int rotation = glfm__callJavaMethod(jni, windowDisplay, "getRotation","()I", Int);
(*jni)->DeleteLocalRef(jni, windowDisplay);
if (glfm__wasJavaExceptionThrown(jni)) {
return GLFMInterfaceOrientationUnknown;
}
switch (rotation) {
case Surface_ROTATION_0:
return GLFMInterfaceOrientationPortrait;
case Surface_ROTATION_90:
return GLFMInterfaceOrientationLandscapeRight;
case Surface_ROTATION_180:
return GLFMInterfaceOrientationPortraitUpsideDown;
case Surface_ROTATION_270:
return GLFMInterfaceOrientationLandscapeLeft;
default:
return GLFMInterfaceOrientationUnknown;
}
}
__attribute__((no_sanitize("hwaddress"))) void glfmGetDisplaySize(const GLFMDisplay *display, int *width, int *height) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
if (width) *width = platformData->width;
if (height) *height = platformData->height;
}
__attribute__((no_sanitize("hwaddress"))) double glfmGetDisplayScale(const GLFMDisplay *display) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
return platformData->scale;
}
__attribute__((no_sanitize("hwaddress"))) void glfmGetDisplayChromeInsets(const GLFMDisplay *display, double *top, double *right,
double *bottom, double *left) {
int intTop, intRight, intBottom, intLeft;
glfm__getDisplayChromeInsets(display, &intTop, &intRight, &intBottom, &intLeft);
if (top) *top = (double)intTop;
if (right) *right = (double)intRight;
if (bottom) *bottom = (double)intBottom;
if (left) *left = (double)intLeft;
}
__attribute__((no_sanitize("hwaddress"))) GLFMRenderingAPI glfmGetRenderingAPI(const GLFMDisplay *display) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
return platformData->renderingAPI;
}
__attribute__((no_sanitize("hwaddress"))) bool glfmHasTouch(const GLFMDisplay *display) {
(void)display;
// This will need to change, for say, TV apps
return true;
}
__attribute__((no_sanitize("hwaddress"))) void glfmSetMouseCursor(GLFMDisplay *display, GLFMMouseCursor mouseCursor) {
(void)display;
(void)mouseCursor;
// Do nothing
}
__attribute__((no_sanitize("hwaddress"))) void glfmSetMultitouchEnabled(GLFMDisplay *display, bool multitouchEnabled) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
platformData->multitouchEnabled = multitouchEnabled;
}
__attribute__((no_sanitize("hwaddress"))) bool glfmGetMultitouchEnabled(const GLFMDisplay *display) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
return platformData->multitouchEnabled;
}
__attribute__((no_sanitize("hwaddress"))) GLFMProc glfmGetProcAddress(const char *functionName) {
GLFMProc function = eglGetProcAddress(functionName);
if (!function) {
static void *handle = NULL;
if (!handle) {
handle = dlopen(NULL, RTLD_LAZY);
}
function = handle ? (GLFMProc)dlsym(handle, functionName) : NULL;
}
return function;
}
__attribute__((no_sanitize("hwaddress"))) bool glfmHasVirtualKeyboard(const GLFMDisplay *display) {
(void)display;
return true;
}
__attribute__((no_sanitize("hwaddress"))) void glfmSetKeyboardVisible(GLFMDisplay *display, bool visible) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
if (glfm__setKeyboardVisible(platformData, visible)) {
glfm__updateUserInterfaceChrome(platformData);
}
}
__attribute__((no_sanitize("hwaddress"))) bool glfmIsKeyboardVisible(const GLFMDisplay *display) {
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
return platformData->keyboardVisible;
}
__attribute__((no_sanitize("hwaddress"))) bool glfmIsSensorAvailable(const GLFMDisplay *display, GLFMSensor sensor) {
(void)display;
return glfm__getDeviceSensor(sensor) != NULL;
}
__attribute__((no_sanitize("hwaddress"))) bool glfmIsHapticFeedbackSupported(const GLFMDisplay *display) {
/*
Vibrator vibrator = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
return vibrator ? vibrator.hasVibrator() : false;
*/
if (!display) {
return false;
}
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
JNIEnv *jni = platformData->jniEnv;
if ((*jni)->ExceptionCheck(jni)) {
return false;
}
jobject vibratorService = glfm__getSystemService(platformData, "VIBRATOR_SERVICE");
if (!vibratorService) {
return false;
}
jboolean result = glfm__callJavaMethod(jni, vibratorService, "hasVibrator", "()Z", Boolean);
(*jni)->DeleteLocalRef(jni, vibratorService);
if (glfm__wasJavaExceptionThrown(jni)) {
return false;
}
return result;
}
__attribute__((no_sanitize("hwaddress"))) void glfmPerformHapticFeedback(GLFMDisplay *display, GLFMHapticFeedbackStyle style) {
// decorView.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP, flags);
static const jint HapticFeedbackConstants_CONTEXT_CLICK = 6; // Light, API 23
static const jint HapticFeedbackConstants_VIRTUAL_KEY = 1; // Medium
static const jint HapticFeedbackConstants_LONG_PRESS = 0; // Heavy
static const jint HapticFeedbackConstants_REJECT = 17; // Heavy, API 30
static const jint HapticFeedbackConstants_FLAG_IGNORE_VIEW_SETTING = 0x01;
static const jint HapticFeedbackConstants_FLAG_IGNORE_GLOBAL_SETTING = 0x02;
if (!display) {
return;
}
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
JNIEnv *jni = platformData->jniEnv;
if ((*jni)->ExceptionCheck(jni)) {
return;
}
jobject decorView = glfm__getDecorView(jni, platformData);
if (!decorView) {
return;
}
const int SDK_INT = platformData->activity->sdkVersion;
jint defaultFeedbackConstant = HapticFeedbackConstants_LONG_PRESS;
jint feedbackConstant;
jint feedbackFlags = HapticFeedbackConstants_FLAG_IGNORE_VIEW_SETTING | HapticFeedbackConstants_FLAG_IGNORE_GLOBAL_SETTING;
switch (style) {
case GLFMHapticFeedbackLight: default:
if (SDK_INT < 23) {
feedbackConstant = HapticFeedbackConstants_VIRTUAL_KEY;
} else {
feedbackConstant = HapticFeedbackConstants_CONTEXT_CLICK;
}
break;
case GLFMHapticFeedbackMedium:
feedbackConstant = HapticFeedbackConstants_VIRTUAL_KEY;
break;
case GLFMHapticFeedbackHeavy:
if (SDK_INT < 30) {
feedbackConstant = HapticFeedbackConstants_LONG_PRESS;
} else {
feedbackConstant = HapticFeedbackConstants_REJECT;
}
break;
}
bool performed = glfm__callJavaMethodWithArgs(jni, decorView, "performHapticFeedback", "(II)Z", Boolean, feedbackConstant, feedbackFlags);
if (!performed) {
// Some devices (Samsung S8) don't support all constants
glfm__callJavaMethodWithArgs(jni, decorView, "performHapticFeedback", "(II)Z", Boolean, defaultFeedbackConstant, feedbackFlags);
}
(*jni)->DeleteLocalRef(jni, decorView);
}
__attribute__((no_sanitize("hwaddress"))) bool glfmHasClipboardText(const GLFMDisplay *display) {
if (!display || !display->platformData) {
return false;
}
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
JNIEnv *jni = platformData->jniEnv;
// ClipboardManager clipboardManager = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
jobject clipboardManager = glfm__getSystemService(platformData, "CLIPBOARD_SERVICE");
if (!clipboardManager) {
return false;
}
// Invoke clipboardManager.getPrimaryClipDescription()
jobject primaryClipDescription = glfm__callJavaMethod(jni, clipboardManager, "getPrimaryClipDescription",
"()Landroid/content/ClipDescription;", Object);
(*jni)->DeleteLocalRef(jni, clipboardManager);
if (glfm__wasJavaExceptionThrown(jni) || !primaryClipDescription) {
return false;
}
// Invoke primaryClipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)
jclass class = (*jni)->GetObjectClass(jni, primaryClipDescription);
jobject mimeType = glfm__getJavaStaticField(jni, class, "MIMETYPE_TEXT_PLAIN",
"Ljava/lang/String;", Object);
(*jni)->DeleteLocalRef(jni, class);
if (glfm__wasJavaExceptionThrown(jni) || !mimeType) {
return false;
}
jboolean hasText = glfm__callJavaMethodWithArgs(jni, primaryClipDescription, "hasMimeType",
"(Ljava/lang/String;)Z", Boolean, mimeType);
(*jni)->DeleteLocalRef(jni, primaryClipDescription);
if (glfm__wasJavaExceptionThrown(jni)) {
return false;
}
return hasText;
}
__attribute__((no_sanitize("hwaddress"))) void glfmRequestClipboardText(GLFMDisplay *display, GLFMClipboardTextFunc clipboardTextFunc) {
if (!clipboardTextFunc) {
return;
}
// First check glfmHasClipboardText(), to prevent a toast from being show if there is something
// other than text in the clipboard
if (!display || !display->platformData || !glfmHasClipboardText(display)) {
clipboardTextFunc(display, NULL);
return;
}
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
JNIEnv *jni = platformData->jniEnv;
// ClipboardManager clipboardManager = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
jobject clipboardManager = glfm__getSystemService(platformData, "CLIPBOARD_SERVICE");
if (!clipboardManager) {
clipboardTextFunc(display, NULL);
return;
}
// Invoke clipboardManager.getPrimaryClip()?.getItemAt(0)?.getText()?.toString()
// Note, there appears no reason to do this asynchronously.
jobject clipData = glfm__callJavaMethod(jni, clipboardManager, "getPrimaryClip",
"()Landroid/content/ClipData;", Object);
(*jni)->DeleteLocalRef(jni, clipboardManager);
if (glfm__wasJavaExceptionThrown(jni) || !clipData) {
clipboardTextFunc(display, NULL);
return;
}
jobject clipDataItem = glfm__callJavaMethodWithArgs(jni, clipData, "getItemAt",
"(I)Landroid/content/ClipData$Item;",
Object, 0);
(*jni)->DeleteLocalRef(jni, clipData);
if (glfm__wasJavaExceptionThrown(jni) || !clipDataItem) {
clipboardTextFunc(display, NULL);
return;
}
jobject clipDataItemText = glfm__callJavaMethod(jni, clipDataItem, "getText",
"()Ljava/lang/CharSequence;", Object);
(*jni)->DeleteLocalRef(jni, clipDataItem);
if (glfm__wasJavaExceptionThrown(jni) || !clipDataItemText) {
clipboardTextFunc(display, NULL);
return;
}
jstring javaString = glfm__callJavaMethod(jni, clipDataItemText, "toString",
"()Ljava/lang/String;", Object);
(*jni)->DeleteLocalRef(jni, clipDataItemText);
if (glfm__wasJavaExceptionThrown(jni) || !javaString) {
clipboardTextFunc(display, NULL);
return;
}
// Convert Java string to C string
const char *cString = (*jni)->GetStringUTFChars(jni, javaString, NULL);
if (glfm__wasJavaExceptionThrown(jni) || !cString) {
clipboardTextFunc(display, NULL);
return;
}
// Call
clipboardTextFunc(display, cString);
// Cleanup
(*jni)->ReleaseStringUTFChars(jni, javaString, cString);
(*jni)->DeleteLocalRef(jni, javaString);
}
__attribute__((no_sanitize("hwaddress"))) bool glfmSetClipboardText(GLFMDisplay *display, const char *string) {
if (!string || !display || !display->platformData) {
return false;
}
GLFMPlatformData *platformData = (GLFMPlatformData *)display->platformData;
JNIEnv *jni = platformData->jniEnv;
// Convert C string to java String
jstring javaString = (*jni)->NewStringUTF(jni, string);
if (glfm__wasJavaExceptionThrown(jni) || !javaString) {
return false;
}
// Create ClipData
// ClipData clipData = ClipData.newPlainText("simple text", javaString);
jclass clipDataClass = (*jni)->FindClass(jni, "android/content/ClipData");
if (glfm__wasJavaExceptionThrown(jni) || !clipDataClass) {
(*jni)->DeleteLocalRef(jni, javaString);
return false;
}
jstring label = (*jni)->NewStringUTF(jni, "simple text");
if (glfm__wasJavaExceptionThrown(jni) || !label) {
(*jni)->DeleteLocalRef(jni, clipDataClass);
(*jni)->DeleteLocalRef(jni, javaString);
return false;
}
jobject clipData = glfm__callJavaStaticMethodWithArgs(jni, clipDataClass, "newPlainText",
"(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Landroid/content/ClipData;",
Object, label, javaString);
(*jni)->DeleteLocalRef(jni, clipDataClass);
(*jni)->DeleteLocalRef(jni, javaString);
if (glfm__wasJavaExceptionThrown(jni) || !clipData) {
return false;
}
// Set the clipboard text
// ClipboardManager clipboardManager = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
// clipboardManager.setPrimaryClip(clipData);
jobject clipboardManager = glfm__getSystemService(platformData, "CLIPBOARD_SERVICE");
if (glfm__wasJavaExceptionThrown(jni) || !clipboardManager) {
(*jni)->DeleteLocalRef(jni, clipData);
return false;
}
glfm__callJavaMethodWithArgs(jni, clipboardManager, "setPrimaryClip",
"(Landroid/content/ClipData;)V", Void, clipData);
(*jni)->DeleteLocalRef(jni, clipData);
(*jni)->DeleteLocalRef(jni, clipboardManager);
return !glfm__wasJavaExceptionThrown(jni);
}
// MARK: - Platform-specific functions
__attribute__((no_sanitize("hwaddress"))) bool glfmIsMetalSupported(const GLFMDisplay *display) {
(void)display;
return false;
}
__attribute__((no_sanitize("hwaddress"))) ANativeActivity *glfmAndroidGetActivity(void) {
if (!platformDataGlobal) {
return NULL;
}
return platformDataGlobal->activity;
}
__attribute__((no_sanitize("hwaddress"))) void *glfmGetAndroidActivity(const GLFMDisplay *display) {
if (!display || !display->platformData) {
return NULL;
}
GLFMPlatformData *platformData = display->platformData;
return platformData->activity;
}
// #endif // __ANDROID__