#include "xr_menu.h" #include #include #include #include #include #include #include #define TAG "QuestShock" #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) #define MENU_WIDTH 1024 #define MENU_HEIGHT 768 // Below the game quad (QUAD_DISTANCE_METERS/QUAD_WIDTH_METERS in // xr_session.c, 2m out) so both are visible together without overlapping. #define MENU_DISTANCE_METERS 1.5f #define MENU_WIDTH_METERS 0.8f #define MENU_CENTER_X_METERS 0.0f #define MENU_CENTER_Y_METERS -0.45f static bool g_visible = false; static jclass g_menu_overlay_class = NULL; static jmethodID g_native_init_method = NULL; static jmethodID g_take_pixels_method = NULL; static jmethodID g_dispatch_touch_method = NULL; static jmethodID g_update_cursor_method = NULL; // The menu overlay's content lives in this texture, uploaded by // xr_menu_upload_source_texture_if_dirty() below. It's created through // gl4es (glGenTextures/glBindTexture/glTexImage2D), like any other texture // the engine itself creates, so ordinary glTexSubImage2D uploads against it // work the normal way. static GLuint g_source_texture = 0; // A framebuffer with g_source_texture as its only color attachment, used as // the read source for xr_menu_render_if_visible()'s glBlitFramebuffer() // call. Created via the real (non-gl4es) GLES entry points below: that blit // bypasses gl4es entirely (see xr_menu_render_if_visible() for why), so its // source framebuffer has to be a real GL object rather than one gl4es // tracks. static GLuint g_source_fbo = 0; typedef void (*PFNQSGETINTEGERV)(GLenum, GLint *); typedef void (*PFNQSGENFRAMEBUFFERS)(GLsizei, GLuint *); typedef void (*PFNQSDELETEFRAMEBUFFERS)(GLsizei, const GLuint *); typedef void (*PFNQSBINDFRAMEBUFFER)(GLenum, GLuint); typedef void (*PFNQSFRAMEBUFFERTEXTURE2D)(GLenum, GLenum, GLenum, GLuint, GLint); typedef void (*PFNQSBLITFRAMEBUFFER)(GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLbitfield, GLenum); // The menu's source framebuffer and its glBlitFramebuffer() copy into the // swapchain image go through the real GLES driver rather than gl4es: gl4es's // fixed-pipeline emulation unconditionally substitutes its own shader onto // any gl4es-routed draw call, which would silently replace the menu's // content with whatever the game itself last rendered (see // xr_menu_render_if_visible()). A framebuffer blit has no shader stage at // all, so going through the real driver for it sidesteps the problem // entirely. static PFNQSGETINTEGERV real_glGetIntegerv; static PFNQSGENFRAMEBUFFERS real_glGenFramebuffers; static PFNQSDELETEFRAMEBUFFERS real_glDeleteFramebuffers; static PFNQSBINDFRAMEBUFFER real_glBindFramebuffer; static PFNQSFRAMEBUFFERTEXTURE2D real_glFramebufferTexture2D; // glBlitFramebuffer is a GLES 3.0 addition; resolved via eglGetProcAddress // rather than dlsym, since Android's driver dispatch doesn't guarantee ES3+ // symbols are dlsym-able by name from libGLESv2.so, unlike the GLES2-core // functions above. static PFNQSBLITFRAMEBUFFER real_glBlitFramebuffer; static bool xr_menu_load_real_gles(void) { void *lib = dlopen("libGLESv2.so", RTLD_NOW | RTLD_LOCAL); if (lib == NULL) { LOGE("XR: menu dlopen(libGLESv2.so) failed: %s", dlerror()); return false; } real_glGetIntegerv = (PFNQSGETINTEGERV)dlsym(lib, "glGetIntegerv"); real_glGenFramebuffers = (PFNQSGENFRAMEBUFFERS)dlsym(lib, "glGenFramebuffers"); real_glDeleteFramebuffers = (PFNQSDELETEFRAMEBUFFERS)dlsym(lib, "glDeleteFramebuffers"); real_glBindFramebuffer = (PFNQSBINDFRAMEBUFFER)dlsym(lib, "glBindFramebuffer"); real_glFramebufferTexture2D = (PFNQSFRAMEBUFFERTEXTURE2D)dlsym(lib, "glFramebufferTexture2D"); real_glBlitFramebuffer = (PFNQSBLITFRAMEBUFFER)eglGetProcAddress("glBlitFramebuffer"); return real_glGetIntegerv && real_glGenFramebuffers && real_glDeleteFramebuffers && real_glBindFramebuffer && real_glFramebufferTexture2D && real_glBlitFramebuffer; } static uint32_t g_pixel_generation = 0; static uint32_t g_source_uploaded_generation = (uint32_t)-1; static uint8_t *g_pixel_cache = NULL; // MENU_WIDTH*MENU_HEIGHT*4 bytes void xr_menu_get_content_size(int *width, int *height) { *width = MENU_WIDTH; *height = MENU_HEIGHT; } // FindClass() from this thread (SDL's native thread, attached to the JVM // via AttachCurrentThread rather than spawned from Java) resolves against // the bootstrap classloader, which only knows framework classes - it can't // see de.ladkau.questshock.MenuOverlay at all. Routing through the // activity's own classloader is the standard, documented workaround. static jclass xr_menu_find_class(JNIEnv *env, jobject activity, const char *name) { jclass activityClass = (*env)->GetObjectClass(env, activity); jmethodID getClassLoader = (*env)->GetMethodID(env, activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;"); jobject classLoader = (*env)->CallObjectMethod(env, activity, getClassLoader); jclass classLoaderClass = (*env)->FindClass(env, "java/lang/ClassLoader"); jmethodID loadClass = (*env)->GetMethodID(env, classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); jstring className = (*env)->NewStringUTF(env, name); jclass result = (jclass)(*env)->CallObjectMethod(env, classLoader, loadClass, className); (*env)->DeleteLocalRef(env, className); return result; } static bool xr_menu_init_jni(void) { JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv(); jobject activity = (jobject)SDL_AndroidGetActivity(); if (env == NULL || activity == NULL) { LOGE("XR: menu - no JNIEnv/Activity from SDL"); return false; } jclass localClass = xr_menu_find_class(env, activity, "de.ladkau.questshock.MenuOverlay"); if (localClass == NULL) { LOGE("XR: menu - could not find MenuOverlay class"); return false; } g_menu_overlay_class = (jclass)(*env)->NewGlobalRef(env, localClass); g_native_init_method = (*env)->GetStaticMethodID(env, g_menu_overlay_class, "nativeInit", "(Landroid/app/Activity;)V"); g_take_pixels_method = (*env)->GetStaticMethodID(env, g_menu_overlay_class, "nativeTakePixelsIfDirty", "()[B"); g_dispatch_touch_method = (*env)->GetStaticMethodID(env, g_menu_overlay_class, "nativeDispatchTouch", "(FFZ)V"); g_update_cursor_method = (*env)->GetStaticMethodID(env, g_menu_overlay_class, "nativeUpdateCursor", "(FFZ)V"); if (!g_native_init_method || !g_take_pixels_method || !g_dispatch_touch_method || !g_update_cursor_method) { LOGE("XR: menu - could not resolve MenuOverlay JNI methods"); return false; } (*env)->CallStaticVoidMethod(env, g_menu_overlay_class, g_native_init_method, activity); // CallStaticVoidMethod doesn't surface Java exceptions on its own - if // MenuOverlay's constructor throws (it runs its View measure/layout/ // draw calls on this native render thread rather than the Android UI // thread, a plausible crash vector), the exception would otherwise be // left silently pending and corrupt whatever JNI call runs next. if ((*env)->ExceptionCheck(env)) { LOGE("XR: menu MenuOverlay.nativeInit() threw a pending Java exception:"); (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); return false; } return true; } static bool xr_menu_create_source_texture(void) { glGenTextures(1, &g_source_texture); glBindTexture(GL_TEXTURE_2D, g_source_texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, MENU_WIDTH, MENU_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); GLint prevFbo = 0; real_glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFbo); real_glGenFramebuffers(1, &g_source_fbo); real_glBindFramebuffer(GL_FRAMEBUFFER, g_source_fbo); real_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, g_source_texture, 0); real_glBindFramebuffer(GL_FRAMEBUFFER, (GLuint)prevFbo); return g_source_texture != 0; } bool xr_menu_init(XrInstance instance, XrSession session) { (void)instance; (void)session; if (!xr_menu_load_real_gles()) { LOGE("XR: menu couldn't resolve real GLES functions via dlsym/eglGetProcAddress"); return false; } if (!xr_menu_create_source_texture()) return false; if (!xr_menu_init_jni()) return false; g_pixel_cache = (uint8_t *)malloc((size_t)MENU_WIDTH * MENU_HEIGHT * 4); if (g_pixel_cache == NULL) return false; LOGI("XR: menu overlay ready (%dx%d)", MENU_WIDTH, MENU_HEIGHT); return true; } void xr_menu_toggle_visible(void) { g_visible = !g_visible; LOGI("XR: menu quad now %s", g_visible ? "visible" : "hidden"); } bool xr_menu_is_visible(void) { return g_visible; } void xr_menu_get_quad_extent(float *center_x_m, float *center_y_m, float *distance_m, float *half_width_m, float *half_height_m) { *center_x_m = MENU_CENTER_X_METERS; *center_y_m = MENU_CENTER_Y_METERS; *distance_m = MENU_DISTANCE_METERS; *half_width_m = MENU_WIDTH_METERS * 0.5f; *half_height_m = MENU_WIDTH_METERS * 0.5f * (float)MENU_HEIGHT / (float)MENU_WIDTH; } void xr_menu_touch(float u, float v, bool down) { if (g_menu_overlay_class == NULL) return; JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv(); if (env == NULL) return; // Passed via CallStaticVoidMethodA/jvalue rather than the variadic // Call*Method form - deliberately sidesteps relying on JNI // implementations correctly un-doing C's float-to-double default // argument promotion for varargs float parameters (a well-known JNI // footgun; Android's ART handles it correctly, but there's no reason // to depend on that when the jvalue form is unambiguous either way). jvalue args[3]; args[0].f = u; args[1].f = v; args[2].z = (jboolean)down; (*env)->CallStaticVoidMethodA(env, g_menu_overlay_class, g_dispatch_touch_method, args); } void xr_menu_update_cursor(float u, float v, bool visible) { if (g_menu_overlay_class == NULL) return; JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv(); if (env == NULL) return; // Same jvalue-form rationale as xr_menu_touch() above. jvalue args[3]; args[0].f = u; args[1].f = v; args[2].z = (jboolean)visible; (*env)->CallStaticVoidMethodA(env, g_menu_overlay_class, g_update_cursor_method, args); } // Pulls MenuOverlay's latest pixels (if it redrew since the last check) // into g_pixel_cache and bumps g_pixel_generation - called once per visible // frame, before deciding whether g_source_texture needs a fresh upload. static void xr_menu_refresh_pixel_cache(void) { JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv(); if (env == NULL) return; jbyteArray pixels = (jbyteArray)(*env)->CallStaticObjectMethod(env, g_menu_overlay_class, g_take_pixels_method); // CallStaticObjectMethod also doesn't surface Java exceptions on its // own - if nativeTakePixelsIfDirty() itself throws, it would otherwise // silently look identical to "nothing changed yet" (a null return). if ((*env)->ExceptionCheck(env)) { LOGE("XR: menu nativeTakePixelsIfDirty() threw a pending Java exception:"); (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); return; } if (pixels == NULL) return; (*env)->GetByteArrayRegion(env, pixels, 0, MENU_WIDTH * MENU_HEIGHT * 4, (jbyte *)g_pixel_cache); (*env)->DeleteLocalRef(env, pixels); g_pixel_generation++; } // Uploads g_pixel_cache into g_source_texture via plain (gl4es-routed) // glBindTexture/glTexSubImage2D, matching how the texture was created. // Skipped when nothing changed since the last upload. Texture-unit-0 state // is saved and restored around the upload, since this call is gl4es-routed // and the engine's own next-frame rendering assumes nothing touched its // texture bindings since it last drew. static void xr_menu_upload_source_texture_if_dirty(void) { if (g_source_uploaded_generation == g_pixel_generation) return; GLint prevActiveTexture = GL_TEXTURE0; glGetIntegerv(GL_ACTIVE_TEXTURE, &prevActiveTexture); glActiveTexture(GL_TEXTURE0); GLint prevTexture2D = 0; glGetIntegerv(GL_TEXTURE_BINDING_2D, &prevTexture2D); glBindTexture(GL_TEXTURE_2D, g_source_texture); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, MENU_WIDTH, MENU_HEIGHT, GL_RGBA, GL_UNSIGNED_BYTE, g_pixel_cache); g_source_uploaded_generation = g_pixel_generation; glBindTexture(GL_TEXTURE_2D, (GLuint)prevTexture2D); if (prevActiveTexture != GL_TEXTURE0) glActiveTexture((GLenum)prevActiveTexture); } void xr_menu_render_if_visible(void) { if (g_source_fbo == 0) return; xr_menu_refresh_pixel_cache(); xr_menu_upload_source_texture_if_dirty(); // Copies g_source_texture (via g_source_fbo) directly into the // currently-bound framebuffer - the menu's own dedicated swapchain // image, sized to exactly MENU_WIDTH x MENU_HEIGHT (see xr_session.c), // so the destination is always the whole image - using the real GLES3 // hardware blit (glBlitFramebuffer) rather than a shader-based // full-screen-quad draw. A blit has no vertex/fragment shading stage, // no shader program, and no texture units involved at all, so gl4es's // fixed-pipeline-emulation layer - which unconditionally substitutes a // customized shader (reproducing the game's own last-bound // texture/fixed-function state) onto any gl4es-routed draw call - has // nothing to intercept here. It also never touches gl4es's own tracked // program/vertex-array/texture-binding shadow state, so the only piece // of state that needs save/restore is the READ framebuffer binding; // binding only GL_READ_FRAMEBUFFER, rather than the combined // GL_FRAMEBUFFER target, leaves the DRAW side (the swapchain image // itself) untouched throughout. // // Y is flipped between source and destination: g_source_texture's texel // row v=0 holds MenuOverlay's Bitmap row 0 (the top of the rendered // content, standard top-row-first raster order), and reading that same // texture via an attached FBO, framebuffer y=0 accesses that identical // row. The destination swapchain framebuffer follows the opposite // convention - its own y=0 is its bottom edge - so swapping dstY0/dstY1 // keeps the menu content right-side up (glBlitFramebuffer supports // inverted src/dst rects for exactly this). GLint prevReadFbo = 0; real_glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevReadFbo); real_glBindFramebuffer(GL_READ_FRAMEBUFFER, g_source_fbo); real_glBlitFramebuffer(0, 0, MENU_WIDTH, MENU_HEIGHT, 0, MENU_HEIGHT, MENU_WIDTH, 0, GL_COLOR_BUFFER_BIT, GL_LINEAR); real_glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)prevReadFbo); } void xr_menu_shutdown(void) { if (g_source_texture != 0) glDeleteTextures(1, &g_source_texture); g_source_texture = 0; g_source_uploaded_generation = (uint32_t)-1; if (g_source_fbo != 0 && real_glDeleteFramebuffers != NULL) real_glDeleteFramebuffers(1, &g_source_fbo); g_source_fbo = 0; if (g_menu_overlay_class != NULL) { JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv(); if (env != NULL) (*env)->DeleteGlobalRef(env, g_menu_overlay_class); } g_menu_overlay_class = NULL; g_native_init_method = NULL; g_take_pixels_method = NULL; g_dispatch_touch_method = NULL; g_update_cursor_method = NULL; free(g_pixel_cache); g_pixel_cache = NULL; g_pixel_generation = 0; g_visible = false; }