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
@@ -3,6 +3,7 @@ package de.ladkau.questshock;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.SystemClock;
import android.util.Log;
import android.view.Gravity;
@@ -30,6 +31,8 @@ public class MenuOverlay {
static final int WIDTH = 1024;
static final int HEIGHT = 768;
private static final float CURSOR_RADIUS = 10f;
private static MenuOverlay instance;
private final Activity activity;
@@ -37,10 +40,18 @@ public class MenuOverlay {
private final Button keyboardButton;
private final Bitmap bitmap;
private final Canvas canvas;
private final Paint cursorPaint;
private final Object pixelLock = new Object();
private byte[] pendingPixels;
// Only ever touched on the UI thread (both nativeUpdateCursor() and
// forceRedraw() run/are posted there) - no lock needed, unlike
// pendingPixels above.
private boolean cursorVisible = false;
private float cursorX = 0f;
private float cursorY = 0f;
private MenuOverlay(Activity activity) {
this.activity = activity;
@@ -57,6 +68,10 @@ public class MenuOverlay {
bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
canvas = new Canvas(bitmap);
cursorPaint = new Paint();
cursorPaint.setColor(0xFFFFFFFF);
cursorPaint.setAntiAlias(true);
int widthSpec = View.MeasureSpec.makeMeasureSpec(WIDTH, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(HEIGHT, View.MeasureSpec.EXACTLY);
root.measure(widthSpec, heightSpec);
@@ -103,6 +118,31 @@ public class MenuOverlay {
instance.activity.runOnUiThread(() -> instance.handleTouch(u, v, down));
}
// u/v are the same menu-quad hit-test coordinates nativeDispatchTouch()
// uses, but polled once per frame regardless of click state (see
// xr_menu_update_cursor()) rather than only on click edges - lets the
// cursor track the aim ray continuously instead of only jumping when a
// trigger is pressed. visible=false (no hand's ray currently on the
// quad) hides it.
public static void nativeUpdateCursor(final float u, final float v, final boolean visible) {
if (instance == null) {
return;
}
instance.activity.runOnUiThread(() -> instance.updateCursor(u, v, visible));
}
private void updateCursor(float u, float v, boolean visible) {
float x = u * WIDTH;
float y = v * HEIGHT;
if (visible == cursorVisible && x == cursorX && y == cursorY) {
return;
}
cursorVisible = visible;
cursorX = x;
cursorY = y;
forceRedraw();
}
private void handleTouch(float u, float v, boolean down) {
float x = u * WIDTH;
float y = v * HEIGHT;
@@ -120,6 +160,9 @@ public class MenuOverlay {
private void forceRedraw() {
canvas.drawColor(0xFF202020);
root.draw(canvas);
if (cursorVisible) {
canvas.drawCircle(cursorX, cursorY, CURSOR_RADIUS, cursorPaint);
}
byte[] pixels = new byte[WIDTH * HEIGHT * 4];
// ARGB_8888's actual in-memory byte order is R,G,B,A - matches