Split the menu quad back onto its own independent OpenXR swapchain
build / build (push) Successful in 1m43s
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:
@@ -140,12 +140,17 @@ can be sideloaded onto any Android-based VR headset with OpenXR support
|
||||
vendor's store. The game's own rendering is unchanged (still a flat, 2D
|
||||
render, no stereo 3D scene), but instead of running as a Home-hosted 2D
|
||||
panel it's now shown as a single head-tracked quad floating in front of
|
||||
the viewer in an otherwise empty space. There's no controller-driven
|
||||
interaction yet (that's ongoing work - a laser-pointer-driven menu and
|
||||
on-screen keyboard); for now, play with a Bluetooth mouse/keyboard
|
||||
connected to the headset same as before. (Only tested on Meta Quest so
|
||||
far - the steps below use Quest-specific tool names where relevant, but
|
||||
the same `adb install` flow applies to any Android headset with USB
|
||||
the viewer, with a separate menu quad (currently just one "Keyboard"
|
||||
button - the actual on-screen keyboard behind it is still ongoing work)
|
||||
toggled by a controller button and driven by a laser-pointer-style aim
|
||||
ray from each hand (`android/app/src/main/cpp/xr_input.c`) - point and
|
||||
pull the trigger to interact with the menu, same as the game's own
|
||||
Bluetooth mouse/keyboard input otherwise works unchanged. The laser
|
||||
pointer/cursor is currently only visible while actually aiming at the
|
||||
game or menu quad respectively - there's no visual feedback yet while
|
||||
aiming at empty space between them. (Only tested on Meta Quest so far -
|
||||
the steps below use Quest-specific tool names where relevant, but the
|
||||
same `adb install` flow applies to any Android headset with USB
|
||||
debugging enabled.)
|
||||
|
||||
### 5.1. Installing and playing
|
||||
|
||||
@@ -244,16 +244,23 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
||||
{1.0f, 0.85f, 0.1f, 1.0f}, // right: amber
|
||||
};
|
||||
|
||||
// The reticle is drawn directly into whatever framebuffer is currently
|
||||
// bound - the game quad's swapchain image (see xr_frame_end(), which
|
||||
// calls this while that image is still bound). That only makes sense
|
||||
// while aiming at the game quad; the menu quad's content comes from
|
||||
// MenuOverlay's rendered Bitmap instead (xr_menu.c), uploaded on a
|
||||
// separate swapchain, so no reticle is drawn for it here.
|
||||
// The GL reticle is drawn directly into whatever framebuffer is
|
||||
// currently bound - the game quad's swapchain image (see xr_frame_end(),
|
||||
// which calls this while that image is still bound). That only makes
|
||||
// sense while aiming at the game quad; the menu quad's own cursor is
|
||||
// drawn by MenuOverlay itself instead (see xr_menu_update_cursor()
|
||||
// below), composited into its Bitmap the same way its other content is.
|
||||
bool drawReticle = draw && !menuVisible;
|
||||
if (drawReticle)
|
||||
glUseProgram(g_reticle_program);
|
||||
|
||||
// Fed to xr_menu_update_cursor() after the loop below - whichever hand's
|
||||
// ray hits the menu quad last wins if both do, good enough for a single
|
||||
// on-quad cursor (only ever updated when hit is true, so a later hand
|
||||
// that misses doesn't hide an earlier hand's hit).
|
||||
bool menuCursorHit = false;
|
||||
float menuCursorU = 0.0f, menuCursorV = 0.0f;
|
||||
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
XrActionStateGetInfo selectInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
||||
selectInfo.action = g_select_click_action;
|
||||
@@ -335,6 +342,11 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
||||
hit ? "entered" : "left", u, v);
|
||||
g_prev_hit[hand] = hit;
|
||||
}
|
||||
if (hit) {
|
||||
menuCursorHit = true;
|
||||
menuCursorU = u;
|
||||
menuCursorV = v;
|
||||
}
|
||||
if (selectDownEdge && hit) {
|
||||
xr_menu_touch(u, v, true);
|
||||
g_menu_touch_active[hand] = true;
|
||||
@@ -364,6 +376,9 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
||||
glDrawArrays(GL_LINES, 0, 4);
|
||||
glDisableVertexAttribArray(RETICLE_POSITION_LOC);
|
||||
}
|
||||
|
||||
if (menuVisible)
|
||||
xr_menu_update_cursor(menuCursorU, menuCursorV, menuCursorHit);
|
||||
}
|
||||
|
||||
void xr_input_shutdown(void) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,14 +4,9 @@
|
||||
// toggled by xr_input.c's menu_toggle action and hit-tested by the same ray
|
||||
// xr_input.c already computes per hand.
|
||||
//
|
||||
// The menu's content shares xr_session.c's single OpenXR swapchain (see
|
||||
// that file for why: two independently-created swapchains submitted as
|
||||
// separate composition layers make the Horizon OS compositor show the
|
||||
// game's own content on the menu quad instead of the menu's - a
|
||||
// compositor-level limitation, not anything under this app's control).
|
||||
// xr_session.c sizes its shared swapchain to fit both the game's and the
|
||||
// menu's content side by side, and calls into this file to render the
|
||||
// menu's own sub-rectangle each visible frame.
|
||||
// The menu has its own independent OpenXR swapchain, sized to exactly
|
||||
// xr_menu_get_content_size()'s dimensions (see xr_session.c, which creates
|
||||
// it and calls into this file to render its content each visible frame).
|
||||
#ifndef QUESTSHOCK_XR_MENU_H
|
||||
#define QUESTSHOCK_XR_MENU_H
|
||||
|
||||
@@ -26,13 +21,12 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
// Pixel size of the menu's own content region - xr_session.c needs this
|
||||
// before it creates its shared swapchain, to size it to fit both the
|
||||
// game's and the menu's content. Has no OpenXR/GL dependency, safe to call
|
||||
// before xr_menu_init().
|
||||
// before it creates the menu's own swapchain, to size it correctly. Has no
|
||||
// OpenXR/GL dependency, safe to call before xr_menu_init().
|
||||
void xr_menu_get_content_size(int *width, int *height);
|
||||
|
||||
// Call once, after xr_session.c has created its shared swapchain - sets up
|
||||
// the MenuOverlay Java-side singleton and the gl4es-owned texture its
|
||||
// Call once, after xr_session.c has created the menu's own swapchain - sets
|
||||
// up the MenuOverlay Java-side singleton and the gl4es-owned texture its
|
||||
// pixels get uploaded into. Returns false (logged, non-fatal - the caller
|
||||
// just has no menu) on failure.
|
||||
bool xr_menu_init(XrInstance instance, XrSession session);
|
||||
@@ -54,11 +48,18 @@ void xr_menu_get_quad_extent(float *center_x_m, float *center_y_m, float *distan
|
||||
// hand's aim ray currently hits the menu quad.
|
||||
void xr_menu_touch(float u, float v, bool down);
|
||||
|
||||
// Updates the on-quad cursor MenuOverlay draws at the current aim hit point
|
||||
// (in the quad's own 0..1 u/v, top-left origin) - called once per frame
|
||||
// while xr_menu_is_visible(), regardless of click state, so the cursor
|
||||
// tracks the ray continuously rather than only jumping on click edges like
|
||||
// xr_menu_touch() above. visible=false hides it (no hand's ray currently
|
||||
// hits the quad).
|
||||
void xr_menu_update_cursor(float u, float v, bool visible);
|
||||
|
||||
// Refreshes MenuOverlay's pixels from Java if they changed since the last
|
||||
// call, and blits them into whichever framebuffer is currently bound -
|
||||
// xr_session.c binds its shared swapchain image and sets the
|
||||
// viewport/scissor to the menu's own sub-rectangle within it before calling
|
||||
// this. Only called while xr_menu_is_visible().
|
||||
// xr_session.c binds the menu's own swapchain image before calling this.
|
||||
// Only called while xr_menu_is_visible().
|
||||
void xr_menu_render_if_visible(void);
|
||||
|
||||
void xr_menu_shutdown(void);
|
||||
|
||||
@@ -54,26 +54,28 @@ static XrSessionState g_session_state = XR_SESSION_STATE_UNKNOWN;
|
||||
// later state.
|
||||
static bool g_session_running = false;
|
||||
|
||||
// A single shared swapchain, wide enough to hold the game's own content
|
||||
// (left) and the menu's content (right) side by side - see xr_frame_end()
|
||||
// for how each gets its own imageRect/viewport/scissor sub-rectangle within
|
||||
// it. Deliberately not two independent swapchains: submitting two
|
||||
// separately-created swapchains as two XrCompositionLayerQuads makes the
|
||||
// Horizon OS compositor show the game's own content on the menu quad
|
||||
// instead of the menu's - a compositor-level limitation with that
|
||||
// configuration, not something fixable from app-side GL/OpenXR code.
|
||||
// Sharing one swapchain avoids it entirely.
|
||||
static XrSwapchain g_swapchain = XR_NULL_HANDLE;
|
||||
static int g_game_width = 0;
|
||||
static int g_game_height = 0;
|
||||
static int g_menu_width = 0;
|
||||
static int g_menu_height = 0;
|
||||
static GLuint g_swapchain_fbos[MAX_SWAPCHAIN_IMAGES];
|
||||
static uint32_t g_swapchain_image_count = 0;
|
||||
// One independent swapchain per quad (game, menu), each sized to exactly
|
||||
// its own content and acquired/released on its own within the same
|
||||
// xrBeginFrame/xrEndFrame pair - a completely ordinary multi-layer OpenXR
|
||||
// setup. A GL framebuffer wrapping each swapchain image is created once, up
|
||||
// front, and reused every frame, since a swapchain's images are a small,
|
||||
// fixed, runtime-owned pool (MAX_SWAPCHAIN_IMAGES above), not something
|
||||
// recreated per frame.
|
||||
typedef struct {
|
||||
XrSwapchain swapchain;
|
||||
GLuint fbos[MAX_SWAPCHAIN_IMAGES];
|
||||
uint32_t image_count;
|
||||
int width;
|
||||
int height;
|
||||
} XrSwapchainState;
|
||||
|
||||
static XrSwapchainState g_game_swapchain;
|
||||
static XrSwapchainState g_menu_swapchain;
|
||||
|
||||
static XrTime g_predicted_display_time = 0;
|
||||
static bool g_frame_should_render = false;
|
||||
static bool g_have_acquired_image = false;
|
||||
static bool g_have_acquired_game_image = false;
|
||||
static bool g_have_acquired_menu_image = false;
|
||||
|
||||
// This file's GL calls otherwise resolve to gl4es (the only GL symbol
|
||||
// provider linked into this binary - see android/engine-patches/
|
||||
@@ -158,6 +160,111 @@ static EGLConfig xr_get_current_egl_config(EGLDisplay display, EGLContext contex
|
||||
return config;
|
||||
}
|
||||
|
||||
// Creates a swapchain at the given size/format and wraps each of its images
|
||||
// in its own GL framebuffer, ready to bind and render into directly -
|
||||
// shared by the game and menu swapchains below, since both need exactly the
|
||||
// same setup, just at a different size.
|
||||
static bool xr_create_swapchain_state(int64_t format, int width, int height,
|
||||
XrSwapchainState *out) {
|
||||
out->width = width;
|
||||
out->height = height;
|
||||
|
||||
XrSwapchainCreateInfo swapchainInfo = {XR_TYPE_SWAPCHAIN_CREATE_INFO};
|
||||
swapchainInfo.usageFlags =
|
||||
XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT;
|
||||
swapchainInfo.format = format;
|
||||
swapchainInfo.sampleCount = 1;
|
||||
swapchainInfo.width = (uint32_t)width;
|
||||
swapchainInfo.height = (uint32_t)height;
|
||||
swapchainInfo.faceCount = 1;
|
||||
swapchainInfo.arraySize = 1;
|
||||
swapchainInfo.mipCount = 1;
|
||||
if (!xr_check(xrCreateSwapchain(g_session, &swapchainInfo, &out->swapchain),
|
||||
"xrCreateSwapchain"))
|
||||
return false;
|
||||
|
||||
uint32_t imageCount = 0;
|
||||
xrEnumerateSwapchainImages(out->swapchain, 0, &imageCount, NULL);
|
||||
if (imageCount > MAX_SWAPCHAIN_IMAGES) {
|
||||
LOGE("XR: swapchain reports %u images, only room for %d", imageCount,
|
||||
MAX_SWAPCHAIN_IMAGES);
|
||||
return false;
|
||||
}
|
||||
// Zero-initialized, not just `.type` set per element - these structs
|
||||
// also carry a `next` field the runtime may read, and an uninitialized
|
||||
// stack array would leave it as garbage.
|
||||
XrSwapchainImageOpenGLESKHR images[MAX_SWAPCHAIN_IMAGES] = {0};
|
||||
for (uint32_t i = 0; i < imageCount; i++)
|
||||
images[i].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR;
|
||||
if (!xr_check(xrEnumerateSwapchainImages(out->swapchain, imageCount, &imageCount,
|
||||
(XrSwapchainImageBaseHeader *)images),
|
||||
"xrEnumerateSwapchainImages"))
|
||||
return false;
|
||||
out->image_count = imageCount;
|
||||
|
||||
// Wraps each swapchain-provided texture in its own framebuffer, matching
|
||||
// how OpenGL.cc's own CreateFrameBuffer() wraps backupBuffer - just
|
||||
// without a depth/stencil attachment, since the final composite draw
|
||||
// (see opengl_swap_and_restore) never needs one. The Gen/Bind calls are
|
||||
// gl4es's own (linked, not dlsym'd) - see the comment above
|
||||
// real_glFramebufferTexture2D for why.
|
||||
bool all_complete = true;
|
||||
for (uint32_t i = 0; i < imageCount; i++) {
|
||||
glGenFramebuffers(1, &out->fbos[i]);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, out->fbos[i]);
|
||||
real_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
images[i].image, 0);
|
||||
GLenum status = real_glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
LOGE("XR: swapchain FBO %u incomplete: 0x%x", i, status);
|
||||
all_complete = false;
|
||||
}
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return all_complete;
|
||||
}
|
||||
|
||||
static void xr_destroy_swapchain_state(XrSwapchainState *state) {
|
||||
for (uint32_t i = 0; i < state->image_count; i++) {
|
||||
if (state->fbos[i] != 0)
|
||||
glDeleteFramebuffers(1, &state->fbos[i]);
|
||||
}
|
||||
state->image_count = 0;
|
||||
if (state->swapchain != XR_NULL_HANDLE)
|
||||
xrDestroySwapchain(state->swapchain);
|
||||
state->swapchain = XR_NULL_HANDLE;
|
||||
}
|
||||
|
||||
// Acquires the next image of the given swapchain, waits for the runtime to
|
||||
// finish with it, and binds its framebuffer (sized to exactly fill it) as
|
||||
// the current render target.
|
||||
static bool xr_acquire_swapchain_image(XrSwapchainState *state) {
|
||||
uint32_t imageIndex = 0;
|
||||
XrSwapchainImageAcquireInfo acquireInfo = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
|
||||
if (!xr_check(xrAcquireSwapchainImage(state->swapchain, &acquireInfo, &imageIndex),
|
||||
"xrAcquireSwapchainImage"))
|
||||
return false;
|
||||
|
||||
XrSwapchainImageWaitInfo waitImageInfo = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
|
||||
waitImageInfo.timeout = XR_INFINITE_DURATION;
|
||||
if (!xr_check(xrWaitSwapchainImage(state->swapchain, &waitImageInfo), "xrWaitSwapchainImage"))
|
||||
return false;
|
||||
|
||||
// gl4es's own (linked, not dlsym'd) bind - this targets an FBO id gl4es
|
||||
// itself created (see xr_create_swapchain_state()), so its own "current
|
||||
// FBO" bookkeeping updates correctly and its immediate-mode draw calls
|
||||
// (android_draw_surface_as_quad(), the laser reticle) land in the right
|
||||
// place.
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, state->fbos[imageIndex]);
|
||||
glViewport(0, 0, state->width, state->height);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void xr_release_swapchain_image(XrSwapchainState *state) {
|
||||
XrSwapchainImageReleaseInfo releaseInfo = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
||||
xr_check(xrReleaseSwapchainImage(state->swapchain, &releaseInfo), "xrReleaseSwapchainImage");
|
||||
}
|
||||
|
||||
static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
||||
jobject activity = (jobject)SDL_AndroidGetActivity();
|
||||
@@ -202,10 +309,12 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
// Required by spec before creating an OpenGL ES-backed session, even
|
||||
// though we don't gate on the returned min/max API version here.
|
||||
PFN_xrGetOpenGLESGraphicsRequirementsKHR getGLESRequirements =
|
||||
(PFN_xrGetOpenGLESGraphicsRequirementsKHR)xr_get_proc("xrGetOpenGLESGraphicsRequirementsKHR");
|
||||
(PFN_xrGetOpenGLESGraphicsRequirementsKHR)xr_get_proc(
|
||||
"xrGetOpenGLESGraphicsRequirementsKHR");
|
||||
if (getGLESRequirements == NULL)
|
||||
return false;
|
||||
XrGraphicsRequirementsOpenGLESKHR glesRequirements = {XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR};
|
||||
XrGraphicsRequirementsOpenGLESKHR glesRequirements = {
|
||||
XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR};
|
||||
if (!xr_check(getGLESRequirements(g_instance, g_system_id, &glesRequirements),
|
||||
"xrGetOpenGLESGraphicsRequirementsKHR"))
|
||||
return false;
|
||||
@@ -246,7 +355,8 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
// GL_RGBA with no decode step anywhere in this path, matching the
|
||||
// desktop SDL_RenderCopy path this replaces - an sRGB swapchain format
|
||||
// would auto-gamma-encode on write and double-encode already-encoded
|
||||
// data.
|
||||
// data. Both swapchains below share this one choice - the compositor's
|
||||
// supported format set doesn't depend on swapchain size.
|
||||
uint32_t formatCount = 0;
|
||||
xrEnumerateSwapchainFormats(g_session, 0, &formatCount, NULL);
|
||||
int64_t *formats = (int64_t *)malloc(sizeof(int64_t) * formatCount);
|
||||
@@ -261,77 +371,25 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
}
|
||||
free(formats);
|
||||
|
||||
g_game_width = game_width;
|
||||
g_game_height = game_height;
|
||||
xr_menu_get_content_size(&g_menu_width, &g_menu_height);
|
||||
// The game's own content occupies (0,0)-(game_width,game_height); the
|
||||
// menu's occupies (game_width,0)-(game_width+menu_width,menu_height) -
|
||||
// side by side in one image, wide enough and tall enough for both.
|
||||
uint32_t sharedWidth = (uint32_t)(g_game_width + g_menu_width);
|
||||
uint32_t sharedHeight = (uint32_t)(g_game_height > g_menu_height ? g_game_height : g_menu_height);
|
||||
|
||||
XrSwapchainCreateInfo swapchainInfo = {XR_TYPE_SWAPCHAIN_CREATE_INFO};
|
||||
swapchainInfo.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT;
|
||||
swapchainInfo.format = chosenFormat;
|
||||
swapchainInfo.sampleCount = 1;
|
||||
swapchainInfo.width = sharedWidth;
|
||||
swapchainInfo.height = sharedHeight;
|
||||
swapchainInfo.faceCount = 1;
|
||||
swapchainInfo.arraySize = 1;
|
||||
swapchainInfo.mipCount = 1;
|
||||
if (!xr_check(xrCreateSwapchain(g_session, &swapchainInfo, &g_swapchain),
|
||||
"xrCreateSwapchain"))
|
||||
return false;
|
||||
|
||||
uint32_t imageCount = 0;
|
||||
xrEnumerateSwapchainImages(g_swapchain, 0, &imageCount, NULL);
|
||||
if (imageCount > MAX_SWAPCHAIN_IMAGES) {
|
||||
LOGE("XR: swapchain reports %u images, only room for %d", imageCount,
|
||||
MAX_SWAPCHAIN_IMAGES);
|
||||
return false;
|
||||
}
|
||||
// Zero-initialized, not just `.type` set per element - these structs
|
||||
// also carry a `next` field the runtime may read, and an
|
||||
// uninitialized stack array would leave it as garbage.
|
||||
XrSwapchainImageOpenGLESKHR images[MAX_SWAPCHAIN_IMAGES] = {0};
|
||||
for (uint32_t i = 0; i < imageCount; i++)
|
||||
images[i].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR;
|
||||
if (!xr_check(xrEnumerateSwapchainImages(g_swapchain, imageCount, &imageCount,
|
||||
(XrSwapchainImageBaseHeader *)images),
|
||||
"xrEnumerateSwapchainImages"))
|
||||
return false;
|
||||
g_swapchain_image_count = imageCount;
|
||||
|
||||
if (!xr_load_real_gles()) {
|
||||
LOGE("XR: couldn't resolve real GLES FBO functions via dlsym");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wraps each swapchain-provided texture in its own framebuffer, matching
|
||||
// how OpenGL.cc's own CreateFrameBuffer() wraps backupBuffer - just
|
||||
// without a depth/stencil attachment, since the final composite draw
|
||||
// (see opengl_swap_and_restore) never needs one. The Gen/Bind calls are
|
||||
// gl4es's own (linked, not dlsym'd) - see the comment above
|
||||
// real_glFramebufferTexture2D for why.
|
||||
bool all_complete = true;
|
||||
for (uint32_t i = 0; i < imageCount; i++) {
|
||||
glGenFramebuffers(1, &g_swapchain_fbos[i]);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, g_swapchain_fbos[i]);
|
||||
real_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
images[i].image, 0);
|
||||
GLenum status = real_glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
LOGE("XR: swapchain FBO %u incomplete: 0x%x", i, status);
|
||||
all_complete = false;
|
||||
}
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (!all_complete)
|
||||
if (!xr_create_swapchain_state(chosenFormat, game_width, game_height, &g_game_swapchain)) {
|
||||
LOGE("XR: game swapchain setup failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGI("XR: instance/session/swapchain ready (%ux%u shared, %dx%d game + %dx%d menu, %u images)",
|
||||
sharedWidth, sharedHeight, g_game_width, g_game_height, g_menu_width, g_menu_height,
|
||||
imageCount);
|
||||
int menu_width, menu_height;
|
||||
xr_menu_get_content_size(&menu_width, &menu_height);
|
||||
if (!xr_create_swapchain_state(chosenFormat, menu_width, menu_height, &g_menu_swapchain)) {
|
||||
LOGE("XR: menu swapchain setup failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGI("XR: instance/session ready (game %dx%d, menu %dx%d)", game_width, game_height,
|
||||
menu_width, menu_height);
|
||||
|
||||
// Also non-fatal - the game keeps rendering with no menu quad if this
|
||||
// fails (e.g. MenuOverlay couldn't be resolved via JNI).
|
||||
@@ -369,7 +427,8 @@ void xr_poll_events(void) {
|
||||
if (g_session_state == XR_SESSION_STATE_READY) {
|
||||
XrSessionBeginInfo beginInfo = {XR_TYPE_SESSION_BEGIN_INFO};
|
||||
beginInfo.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
g_session_running = xr_check(xrBeginSession(g_session, &beginInfo), "xrBeginSession");
|
||||
g_session_running =
|
||||
xr_check(xrBeginSession(g_session, &beginInfo), "xrBeginSession");
|
||||
} else if (g_session_state == XR_SESSION_STATE_STOPPING) {
|
||||
xr_check(xrEndSession(g_session), "xrEndSession");
|
||||
g_session_running = false;
|
||||
@@ -384,7 +443,7 @@ void xr_poll_events(void) {
|
||||
bool xr_is_session_running(void) { return g_session_running; }
|
||||
|
||||
bool xr_frame_begin(void) {
|
||||
g_have_acquired_image = false;
|
||||
g_have_acquired_game_image = false;
|
||||
if (!xr_is_session_running())
|
||||
return false;
|
||||
|
||||
@@ -402,66 +461,40 @@ bool xr_frame_begin(void) {
|
||||
if (!g_frame_should_render)
|
||||
return false;
|
||||
|
||||
uint32_t imageIndex = 0;
|
||||
XrSwapchainImageAcquireInfo acquireInfo = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
|
||||
if (!xr_check(xrAcquireSwapchainImage(g_swapchain, &acquireInfo, &imageIndex),
|
||||
"xrAcquireSwapchainImage"))
|
||||
return false;
|
||||
|
||||
XrSwapchainImageWaitInfo waitImageInfo = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
|
||||
waitImageInfo.timeout = XR_INFINITE_DURATION;
|
||||
if (!xr_check(xrWaitSwapchainImage(g_swapchain, &waitImageInfo), "xrWaitSwapchainImage"))
|
||||
return false;
|
||||
|
||||
// gl4es's own (linked, not dlsym'd) bind - this targets an FBO id
|
||||
// gl4es itself created (see xr_create_instance_and_session()), so its
|
||||
// own "current FBO" bookkeeping updates correctly and its immediate-
|
||||
// mode draw calls in android_draw_surface_as_quad() land in the right
|
||||
// place. Scissor-limited to the game's own sub-rectangle of the shared
|
||||
// image (see g_swapchain's comment) - glViewport alone would already
|
||||
// keep the engine's draw calls within that rectangle, but glClear
|
||||
// ignores viewport and would otherwise wipe the menu's own region of
|
||||
// the same image whenever the engine clears before drawing.
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, g_swapchain_fbos[imageIndex]);
|
||||
glViewport(0, 0, g_game_width, g_game_height);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(0, 0, g_game_width, g_game_height);
|
||||
|
||||
g_have_acquired_image = true;
|
||||
return true;
|
||||
g_have_acquired_game_image = xr_acquire_swapchain_image(&g_game_swapchain);
|
||||
return g_have_acquired_game_image;
|
||||
}
|
||||
|
||||
void xr_frame_end(void) {
|
||||
if (g_session == XR_NULL_HANDLE)
|
||||
return;
|
||||
|
||||
// Draw the laser-pointer reticle into the still-bound swapchain
|
||||
// framebuffer before releasing it, so it composites on top of
|
||||
// whatever this frame's game content already drew there. Syncing
|
||||
// actions happens even when there's nothing to draw (no acquired
|
||||
// image this frame), so edge detection (trigger/menu-button clicks)
|
||||
// doesn't miss a frame. Only ever draws while the menu isn't visible
|
||||
// (see xr_input.c), so it never needs to know about the menu's own
|
||||
// sub-rectangle below.
|
||||
// Draw the laser-pointer reticle into the still-bound game swapchain
|
||||
// framebuffer before releasing it, so it composites on top of whatever
|
||||
// this frame's game content already drew there. Syncing actions
|
||||
// happens even when there's nothing to draw (no acquired image this
|
||||
// frame), so edge detection (trigger/menu-button clicks) doesn't miss a
|
||||
// frame. Only ever draws while the menu isn't visible (see xr_input.c) -
|
||||
// the menu renders into its own independent swapchain image below.
|
||||
if (xr_is_session_running())
|
||||
xr_input_sync_and_draw(g_local_space, g_predicted_display_time, g_have_acquired_image);
|
||||
xr_input_sync_and_draw(g_local_space, g_predicted_display_time,
|
||||
g_have_acquired_game_image);
|
||||
|
||||
bool menuVisible = g_have_acquired_image && xr_menu_is_visible();
|
||||
if (menuVisible) {
|
||||
// Switch viewport/scissor to the menu's own sub-rectangle of the
|
||||
// same shared image (to the right of the game's own region, see
|
||||
// g_swapchain's comment) and let xr_menu.c draw its content there -
|
||||
// still the same framebuffer bound in xr_frame_begin(), just a
|
||||
// different sub-rectangle of it.
|
||||
glViewport(g_game_width, 0, g_menu_width, g_menu_height);
|
||||
glScissor(g_game_width, 0, g_menu_width, g_menu_height);
|
||||
if (g_have_acquired_game_image)
|
||||
xr_release_swapchain_image(&g_game_swapchain);
|
||||
|
||||
// The menu gets its own acquire/render/release cycle against its own
|
||||
// swapchain - unlike the game quad, there's no per-frame engine
|
||||
// rendering to wrap around here, just xr_menu.c's own blit, so this
|
||||
// can happen any time before xrEndFrame rather than needing to bracket
|
||||
// anything.
|
||||
g_have_acquired_menu_image = false;
|
||||
if (g_frame_should_render && xr_menu_is_visible()) {
|
||||
g_have_acquired_menu_image = xr_acquire_swapchain_image(&g_menu_swapchain);
|
||||
if (g_have_acquired_menu_image) {
|
||||
xr_menu_render_if_visible();
|
||||
xr_release_swapchain_image(&g_menu_swapchain);
|
||||
}
|
||||
|
||||
if (g_have_acquired_image) {
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
XrSwapchainImageReleaseInfo releaseInfo = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
||||
xr_check(xrReleaseSwapchainImage(g_swapchain, &releaseInfo), "xrReleaseSwapchainImage");
|
||||
}
|
||||
|
||||
if (!xr_is_session_running())
|
||||
@@ -470,36 +503,33 @@ void xr_frame_end(void) {
|
||||
XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||
gameQuad.space = g_local_space;
|
||||
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
gameQuad.subImage.swapchain = g_swapchain;
|
||||
gameQuad.subImage.imageRect.offset.x = 0;
|
||||
gameQuad.subImage.imageRect.offset.y = 0;
|
||||
gameQuad.subImage.imageRect.extent.width = g_game_width;
|
||||
gameQuad.subImage.imageRect.extent.height = g_game_height;
|
||||
gameQuad.subImage.swapchain = g_game_swapchain.swapchain;
|
||||
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width;
|
||||
gameQuad.subImage.imageRect.extent.height = g_game_swapchain.height;
|
||||
gameQuad.pose.orientation.w = 1.0f;
|
||||
gameQuad.pose.position.z = -QUAD_DISTANCE_METERS;
|
||||
gameQuad.size.width = QUAD_WIDTH_METERS;
|
||||
gameQuad.size.height = QUAD_WIDTH_METERS * (float)g_game_height / (float)g_game_width;
|
||||
gameQuad.size.height =
|
||||
QUAD_WIDTH_METERS * (float)g_game_swapchain.height / (float)g_game_swapchain.width;
|
||||
|
||||
// Up to 2 layers: the game quad (if a frame was actually rendered) and,
|
||||
// while toggled on, the menu quad in front of it - see xr_input.c's
|
||||
// menu_toggle handling. Both reference the SAME g_swapchain, just
|
||||
// different imageRect sub-rectangles within it.
|
||||
// while toggled on and rendered this frame, the menu quad in front of
|
||||
// it - see xr_input.c's menu_toggle handling. Each references its own
|
||||
// independent swapchain (see XrSwapchainState above).
|
||||
const XrCompositionLayerBaseHeader *layers[2];
|
||||
uint32_t layerCount = 0;
|
||||
if (g_have_acquired_image)
|
||||
if (g_have_acquired_game_image)
|
||||
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&gameQuad;
|
||||
|
||||
XrCompositionLayerQuad menuQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||
if (menuVisible) {
|
||||
if (g_have_acquired_menu_image) {
|
||||
float centerX, centerY, distance, halfWidth, halfHeight;
|
||||
xr_menu_get_quad_extent(¢erX, ¢erY, &distance, &halfWidth, &halfHeight);
|
||||
menuQuad.space = g_local_space;
|
||||
menuQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
menuQuad.subImage.swapchain = g_swapchain;
|
||||
menuQuad.subImage.imageRect.offset.x = g_game_width;
|
||||
menuQuad.subImage.imageRect.offset.y = 0;
|
||||
menuQuad.subImage.imageRect.extent.width = g_menu_width;
|
||||
menuQuad.subImage.imageRect.extent.height = g_menu_height;
|
||||
menuQuad.subImage.swapchain = g_menu_swapchain.swapchain;
|
||||
menuQuad.subImage.imageRect.extent.width = g_menu_swapchain.width;
|
||||
menuQuad.subImage.imageRect.extent.height = g_menu_swapchain.height;
|
||||
menuQuad.pose.orientation.w = 1.0f;
|
||||
menuQuad.pose.position.x = centerX;
|
||||
menuQuad.pose.position.y = centerY;
|
||||
@@ -518,15 +548,8 @@ void xr_frame_end(void) {
|
||||
}
|
||||
|
||||
void xr_shutdown(void) {
|
||||
for (uint32_t i = 0; i < g_swapchain_image_count; i++) {
|
||||
if (g_swapchain_fbos[i] != 0)
|
||||
glDeleteFramebuffers(1, &g_swapchain_fbos[i]);
|
||||
}
|
||||
g_swapchain_image_count = 0;
|
||||
|
||||
if (g_swapchain != XR_NULL_HANDLE)
|
||||
xrDestroySwapchain(g_swapchain);
|
||||
g_swapchain = XR_NULL_HANDLE;
|
||||
xr_destroy_swapchain_state(&g_game_swapchain);
|
||||
xr_destroy_swapchain_state(&g_menu_swapchain);
|
||||
|
||||
if (g_local_space != XR_NULL_HANDLE)
|
||||
xrDestroySpace(g_local_space);
|
||||
@@ -551,5 +574,6 @@ void xr_shutdown(void) {
|
||||
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m) {
|
||||
*distance_m = QUAD_DISTANCE_METERS;
|
||||
*half_width_m = QUAD_WIDTH_METERS * 0.5f;
|
||||
*half_height_m = QUAD_WIDTH_METERS * 0.5f * (float)g_game_height / (float)g_game_width;
|
||||
*half_height_m =
|
||||
QUAD_WIDTH_METERS * 0.5f * (float)g_game_swapchain.height / (float)g_game_swapchain.width;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Minimal OpenXR bring-up for questshock's immersive Quest build. Keeps the
|
||||
// game's own rendering untouched (see android/engine-patches/
|
||||
// 11-android-openxr-present.patch) - this module only owns the OpenXR
|
||||
// instance/session/swapchain and the final "submit a quad layer instead of
|
||||
// presenting to a window" step. No stereo rendering, no controller input
|
||||
// yet (see the plan's steps B/C/D for those) - the game composite is shown
|
||||
// as a single flat quad floating in front of the viewer.
|
||||
// instance/session/swapchains and the final "submit quad layers instead of
|
||||
// presenting to a window" step. No stereo rendering - the game composite is
|
||||
// shown as a single flat quad floating in front of the viewer.
|
||||
#ifndef QUESTSHOCK_XR_SESSION_H
|
||||
#define QUESTSHOCK_XR_SESSION_H
|
||||
|
||||
@@ -16,11 +15,13 @@ extern "C" {
|
||||
|
||||
// Call once, right after init_opengl() (OpenGL.cc) has created and made
|
||||
// current the GL context SDL/gl4es already use - creates the OpenXR
|
||||
// instance/session sharing that same EGL display/context, plus one
|
||||
// swapchain sized to the game's logical resolution (game_width/height, i.e.
|
||||
// grd_cap->w/h - see Shock.c's InitSDL()). Returns false if OpenXR bring-up
|
||||
// failed (e.g. no runtime installed) - callers should fall back to the
|
||||
// existing window-present path in that case.
|
||||
// instance/session sharing that same EGL display/context, plus the game
|
||||
// quad's own swapchain, sized to the game's logical resolution
|
||||
// (game_width/height, i.e. grd_cap->w/h - see Shock.c's InitSDL()). The
|
||||
// menu quad's own, independently-sized swapchain (see xr_menu.h) is created
|
||||
// alongside it. Returns false if OpenXR bring-up failed (e.g. no runtime
|
||||
// installed) - callers should fall back to the existing window-present path
|
||||
// in that case.
|
||||
bool xr_init(int game_width, int game_height);
|
||||
|
||||
// Pumps XR session-state events. Call once per frame, before
|
||||
@@ -34,17 +35,19 @@ void xr_poll_events(void);
|
||||
bool xr_is_session_running(void);
|
||||
|
||||
// Begins the XR frame (xrWaitFrame/xrBeginFrame) and, if the runtime wants
|
||||
// this frame rendered, acquires the next swapchain image and binds its
|
||||
// framebuffer as the current render target - ready for the caller to draw
|
||||
// into exactly as it would have drawn to the default framebuffer. Returns
|
||||
// true if the caller should draw this frame; xr_frame_end() must be called
|
||||
// unconditionally afterward either way (a begun XR frame must always be
|
||||
// ended, rendered or not).
|
||||
// this frame rendered, acquires the game quad's next swapchain image and
|
||||
// binds its framebuffer as the current render target - ready for the
|
||||
// caller to draw into exactly as it would have drawn to the default
|
||||
// framebuffer. Returns true if the caller should draw this frame;
|
||||
// xr_frame_end() must be called unconditionally afterward either way (a
|
||||
// begun XR frame must always be ended, rendered or not).
|
||||
bool xr_frame_begin(void);
|
||||
|
||||
// Releases the swapchain image (if one was acquired this frame) and
|
||||
// submits it as a single XrCompositionLayerQuad positioned in front of the
|
||||
// local reference space's origin, then ends the XR frame.
|
||||
// Releases the game quad's swapchain image (if one was acquired this
|
||||
// frame), acquires/renders/releases the menu quad's own swapchain image if
|
||||
// it's currently visible (see xr_menu.h), submits whichever of the two
|
||||
// quads actually rendered this frame as composition layers positioned in
|
||||
// front of the local reference space's origin, then ends the XR frame.
|
||||
void xr_frame_end(void);
|
||||
|
||||
void xr_shutdown(void);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user