- xr_input.c/h: an OpenXR action set (aim pose, trigger/select click, a menu-toggle button) plus ray/quad hit-testing, and a visible laser-pointer line drawn from the controller toward the hit point. - xr_menu.c/h + MenuOverlay.java: a second composition-layer quad, toggled by a controller button, showing an off-screen, never-attached Android View tree (one "Keyboard" button so far) driven by synthetic touch events computed from the laser ray's hit UV. - Migrate gl4es from a prebuilt binary baked into the Docker build-image to a vendored source snapshot (android/gl4es-src/) compiled from source at APK build time via CMake add_subdirectory(), patched through a new android/gl4es-patches/ (mirrors the existing engine-patches/ pattern). This was needed to track down and fix a bug hit while building the menu: gl4es's fixed-pipeline emulation (fpe.c) was unconditionally substituting its own shader onto the menu's draw call (fpe_ReleventState() always sets alphafunc to a nonzero sentinel, so its fpe_IsEmpty() check could never see the state as empty), making the menu quad show the game's own rendering instead of its own content. Fixed by routing the menu's blit through a real glBlitFramebuffer() call instead of a shader-based draw, which gl4es's fpe.c has nothing to intercept - documented in README.md's new "Debugging notes" section. - Renumber android/engine-patches/ to close the gap left by removing an unrelated diagnostic-only patch, and strip investigation-journal comments and dead diagnostic code (temporary tracing, env-var probes) left over from finding the bug above. - Restructure README.md into numbered sections, document every engine/gl4es patch, add Android Studio dev/testing-cycle instructions, and generalize Quest-specific wording to any OpenXR headset. Update NOTICE.txt to match (gl4es is now vendored/patched source, not a prebuilt binary; add the OpenXR-SDK loader).
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
package de.ladkau.questshock;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* An off-screen, never-attached-to-a-window View tree for questshock's
|
||||
* OpenXR menu quad (see android/app/src/main/cpp/xr_menu.c, which owns the
|
||||
* quad's swapchain and drives this class entirely via JNI - construction,
|
||||
* touch input, and pixel readback). Rendering to a Bitmap and dispatching
|
||||
* synthetic MotionEvents into an unattached hierarchy both work the same
|
||||
* way they would for an attached View - draw(Canvas)/dispatchTouchEvent()
|
||||
* don't require a ViewRootImpl/window, just a measured+laid-out tree.
|
||||
*/
|
||||
public class MenuOverlay {
|
||||
private static final String TAG = "QuestShock";
|
||||
|
||||
// Matches xr_menu.c's MENU_WIDTH/MENU_HEIGHT swapchain size - fixed,
|
||||
// not tied to any real display metric, since this tree is never shown
|
||||
// in a real window.
|
||||
static final int WIDTH = 1024;
|
||||
static final int HEIGHT = 768;
|
||||
|
||||
private static MenuOverlay instance;
|
||||
|
||||
private final Activity activity;
|
||||
private final FrameLayout root;
|
||||
private final Button keyboardButton;
|
||||
private final Bitmap bitmap;
|
||||
private final Canvas canvas;
|
||||
|
||||
private final Object pixelLock = new Object();
|
||||
private byte[] pendingPixels;
|
||||
|
||||
private MenuOverlay(Activity activity) {
|
||||
this.activity = activity;
|
||||
|
||||
root = new FrameLayout(activity);
|
||||
root.setBackgroundColor(0xFF202020);
|
||||
|
||||
keyboardButton = new Button(activity);
|
||||
keyboardButton.setText("Keyboard");
|
||||
keyboardButton.setOnClickListener(v -> Log.i(TAG, "MenuOverlay: Keyboard button clicked"));
|
||||
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(420, 140);
|
||||
lp.gravity = Gravity.CENTER;
|
||||
root.addView(keyboardButton, lp);
|
||||
|
||||
bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
|
||||
canvas = new Canvas(bitmap);
|
||||
|
||||
int widthSpec = View.MeasureSpec.makeMeasureSpec(WIDTH, View.MeasureSpec.EXACTLY);
|
||||
int heightSpec = View.MeasureSpec.makeMeasureSpec(HEIGHT, View.MeasureSpec.EXACTLY);
|
||||
root.measure(widthSpec, heightSpec);
|
||||
root.layout(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
forceRedraw();
|
||||
}
|
||||
|
||||
// Called from xr_menu.c's xr_menu_init(), on the render thread, right
|
||||
// after it resolves this class via the activity's own ClassLoader
|
||||
// (plain FindClass() can't see app classes from a thread that was
|
||||
// attached to the JVM rather than spawned from Java - see that file's
|
||||
// xr_menu_find_class()).
|
||||
public static void nativeInit(Activity activity) {
|
||||
if (instance == null) {
|
||||
instance = new MenuOverlay(activity);
|
||||
}
|
||||
}
|
||||
|
||||
// Polled once per frame while the menu quad is visible (see
|
||||
// xr_menu_render_if_visible()) - returns the current pixels only once
|
||||
// per redraw (null otherwise), so the native side knows when it can
|
||||
// skip re-uploading a texture that hasn't actually changed.
|
||||
public static byte[] nativeTakePixelsIfDirty() {
|
||||
if (instance == null) {
|
||||
return null;
|
||||
}
|
||||
synchronized (instance.pixelLock) {
|
||||
byte[] pixels = instance.pendingPixels;
|
||||
instance.pendingPixels = null;
|
||||
return pixels;
|
||||
}
|
||||
}
|
||||
|
||||
// u/v are the menu quad's own hit-test coordinates (0..1, top-left
|
||||
// origin) - computed by xr_input.c's ray/quad intersection against
|
||||
// xr_menu.c's reported quad geometry, forwarded here as a synthetic
|
||||
// tap. Runs on the UI thread since the View/Bitmap/Canvas objects here
|
||||
// are otherwise only ever touched from there.
|
||||
public static void nativeDispatchTouch(final float u, final float v, final boolean down) {
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
instance.activity.runOnUiThread(() -> instance.handleTouch(u, v, down));
|
||||
}
|
||||
|
||||
private void handleTouch(float u, float v, boolean down) {
|
||||
float x = u * WIDTH;
|
||||
float y = v * HEIGHT;
|
||||
long time = SystemClock.uptimeMillis();
|
||||
MotionEvent event = MotionEvent.obtain(
|
||||
time, time, down ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, x, y, 0);
|
||||
try {
|
||||
root.dispatchTouchEvent(event);
|
||||
} finally {
|
||||
event.recycle();
|
||||
}
|
||||
forceRedraw();
|
||||
}
|
||||
|
||||
private void forceRedraw() {
|
||||
canvas.drawColor(0xFF202020);
|
||||
root.draw(canvas);
|
||||
|
||||
byte[] pixels = new byte[WIDTH * HEIGHT * 4];
|
||||
// ARGB_8888's actual in-memory byte order is R,G,B,A - matches
|
||||
// GL_RGBA/GL_UNSIGNED_BYTE on the native side with no swizzling.
|
||||
bitmap.copyPixelsToBuffer(ByteBuffer.wrap(pixels));
|
||||
synchronized (pixelLock) {
|
||||
pendingPixels = pixels;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public class QuestShockActivity extends SDLActivity {
|
||||
// getLibraries()/loadLibraries(), which only runs from super.onCreate())
|
||||
// purely to get nativeChdir() below - Android's dynamic linker resolves
|
||||
// "main"'s own dependencies (SDL2, SDL2_mixer, fluidsynth) from the
|
||||
// APK's native library directory regardless of Java-side load order, so
|
||||
// APK's native library directory regardless of Java-side load order, sox
|
||||
// loading it early here is safe. SDLActivity loading "main" again later
|
||||
// is a harmless no-op (System.loadLibrary is idempotent per
|
||||
// ClassLoader).
|
||||
@@ -74,11 +74,9 @@ public class QuestShockActivity extends SDLActivity {
|
||||
// onWindowFocusChanged(), it never checks SDLActivity.mBrokenLibraries
|
||||
// first. Since surfaceChanged() fires on essentially every launch
|
||||
// regardless of that flag, setting mBrokenLibraries alone (see onCreate()/
|
||||
// setUpGameDirAndContinue()) does NOT actually stop the engine from
|
||||
// starting - confirmed on-device: the missing-assets crash still happened
|
||||
// with mBrokenLibraries set, from exactly this path. Route through a
|
||||
// subclass that adds the missing check instead of patching the vendored
|
||||
// file directly.
|
||||
// setUpGameDirAndContinue()) does not actually stop the engine from
|
||||
// starting. Route through a subclass that adds the missing check instead
|
||||
// of patching the vendored file directly.
|
||||
private static class GameSurface extends SDLSurface {
|
||||
GameSurface(Context context) {
|
||||
super(context);
|
||||
@@ -91,20 +89,6 @@ public class QuestShockActivity extends SDLActivity {
|
||||
}
|
||||
super.surfaceChanged(holder, format, width, height);
|
||||
}
|
||||
|
||||
// Diagnostic only (see OpenGL.cc's matching log) - Android's real,
|
||||
// legitimate signal for "this View's laid-out size actually
|
||||
// changed" is onSizeChanged(), fired by the framework itself, not
|
||||
// something we have to poll or guess about. If Quest's Home shell
|
||||
// settles a freshly-launched panel into its final <layout> size via
|
||||
// a genuine later layout pass, this is where that would show up -
|
||||
// confirming whether a real event exists to hook, before writing
|
||||
// any fix around it.
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
Log.i(TAG, "GameSurface.onSizeChanged() " + oldw + "x" + oldh + " -> " + w + "x" + h);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user