Split the menu quad back onto its own independent OpenXR swapchain
build / build (push) Successful in 1m43s

Replaces the single shared, side-by-side swapchain (game + menu content
packed into one image, needing viewport/scissor juggling to keep the
menu's own glClear from bleeding into the game's region) with two fully
independent XrSwapchainState instances, each sized to exactly its own
content and acquired/released on its own within the same
xrBeginFrame/xrEndFrame pair - an ordinary multi-layer OpenXR setup.

The shared-swapchain design was originally adopted to work around what
looked like a Horizon OS compositor limitation with independent
swapchains, but that was diagnosed before the real root cause of the
"menu shows the game's content" bug was found (gl4es's fpe.c
unconditionally substituting its own shader onto the menu's draw call -
see README's Debugging notes). Since the actual fix routes the menu's
blit through a real, non-gl4es glBlitFramebuffer() call - orthogonal to
how many swapchains exist - splitting back onto two swapchains works
fine and removes the GL_SCISSOR_TEST workaround and sub-rectangle
offset math entirely. Confirmed on-device: both quads render correctly,
hit-testing and 90 FPS unaffected.

Also adds a visible cursor to the menu quad itself: xr_input.c now
tracks whichever hand's aim ray hits the menu each frame and forwards it
to a new MenuOverlay.nativeUpdateCursor(), which draws a small dot at
that position (composited through the same Bitmap the menu's own
content already goes through) - previously there was no visual feedback
at all showing where you were pointing before pulling the trigger.
This commit is contained in:
ml
2026-08-02 08:01:14 +02:00
parent 2b52b818e8
commit 1be0bebda2
7 changed files with 336 additions and 227 deletions
+36 -18
View File
@@ -31,6 +31,7 @@ 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
@@ -138,7 +139,10 @@ static bool xr_menu_init_jni(void) {
(*env)->GetStaticMethodID(env, g_menu_overlay_class, "nativeTakePixelsIfDirty", "()[B");
g_dispatch_touch_method =
(*env)->GetStaticMethodID(env, g_menu_overlay_class, "nativeDispatchTouch", "(FFZ)V");
if (!g_native_init_method || !g_take_pixels_method || !g_dispatch_touch_method) {
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;
}
@@ -235,6 +239,20 @@ void xr_menu_touch(float u, float v, bool 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.
@@ -293,18 +311,20 @@ void xr_menu_render_if_visible(void) {
xr_menu_refresh_pixel_cache();
xr_menu_upload_source_texture_if_dirty();
// Copies g_source_texture (via g_source_fbo) directly into the menu's
// sub-rectangle of the currently-bound shared swapchain framebuffer,
// 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
// 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.
//
@@ -318,13 +338,10 @@ void xr_menu_render_if_visible(void) {
// inverted src/dst rects for exactly this).
GLint prevReadFbo = 0;
real_glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevReadFbo);
GLint viewport[4] = {0, 0, 0, 0};
real_glGetIntegerv(GL_VIEWPORT, viewport);
real_glBindFramebuffer(GL_READ_FRAMEBUFFER, g_source_fbo);
real_glBlitFramebuffer(0, 0, MENU_WIDTH, MENU_HEIGHT, viewport[0], viewport[1] + viewport[3],
viewport[0] + viewport[2], viewport[1], GL_COLOR_BUFFER_BIT,
GL_LINEAR);
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);
}
@@ -347,6 +364,7 @@ void xr_menu_shutdown(void) {
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;