- 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:
@@ -17,6 +17,9 @@
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include "xr_input.h"
|
||||
#include "xr_menu.h"
|
||||
|
||||
#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__)
|
||||
@@ -51,9 +54,20 @@ static XrSessionState g_session_state = XR_SESSION_STATE_UNKNOWN;
|
||||
// later state.
|
||||
static bool g_session_running = false;
|
||||
|
||||
static XrSwapchain g_game_swapchain = XR_NULL_HANDLE;
|
||||
// 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;
|
||||
|
||||
@@ -222,6 +236,10 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
"xrCreateReferenceSpace"))
|
||||
return false;
|
||||
|
||||
// Non-fatal if it fails (e.g. no controllers bound yet) - rendering
|
||||
// keeps working either way, just without the laser pointer.
|
||||
xr_input_init(g_instance, g_session);
|
||||
|
||||
// Prefer a plain linear 8-bit format over GL_SRGB8_ALPHA8: the source
|
||||
// SDL surface pixels are already sRGB-encoded (as ordinary 8-bit image
|
||||
// data conventionally is) and get uploaded/sampled as plain linear
|
||||
@@ -243,23 +261,30 @@ 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 = (uint32_t)game_width;
|
||||
swapchainInfo.height = (uint32_t)game_height;
|
||||
swapchainInfo.width = sharedWidth;
|
||||
swapchainInfo.height = sharedHeight;
|
||||
swapchainInfo.faceCount = 1;
|
||||
swapchainInfo.arraySize = 1;
|
||||
swapchainInfo.mipCount = 1;
|
||||
if (!xr_check(xrCreateSwapchain(g_session, &swapchainInfo, &g_game_swapchain),
|
||||
if (!xr_check(xrCreateSwapchain(g_session, &swapchainInfo, &g_swapchain),
|
||||
"xrCreateSwapchain"))
|
||||
return false;
|
||||
g_game_width = game_width;
|
||||
g_game_height = game_height;
|
||||
|
||||
uint32_t imageCount = 0;
|
||||
xrEnumerateSwapchainImages(g_game_swapchain, 0, &imageCount, NULL);
|
||||
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);
|
||||
@@ -271,7 +296,7 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
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_game_swapchain, imageCount, &imageCount,
|
||||
if (!xr_check(xrEnumerateSwapchainImages(g_swapchain, imageCount, &imageCount,
|
||||
(XrSwapchainImageBaseHeader *)images),
|
||||
"xrEnumerateSwapchainImages"))
|
||||
return false;
|
||||
@@ -304,8 +329,14 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
if (!all_complete)
|
||||
return false;
|
||||
|
||||
LOGI("XR: instance/session/swapchain ready (%dx%d, %u images)", game_width, game_height,
|
||||
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);
|
||||
|
||||
// Also non-fatal - the game keeps rendering with no menu quad if this
|
||||
// fails (e.g. MenuOverlay couldn't be resolved via JNI).
|
||||
xr_menu_init(g_instance, g_session);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -373,22 +404,28 @@ bool xr_frame_begin(void) {
|
||||
|
||||
uint32_t imageIndex = 0;
|
||||
XrSwapchainImageAcquireInfo acquireInfo = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
|
||||
if (!xr_check(xrAcquireSwapchainImage(g_game_swapchain, &acquireInfo, &imageIndex),
|
||||
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_game_swapchain, &waitImageInfo), "xrWaitSwapchainImage"))
|
||||
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.
|
||||
// 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;
|
||||
@@ -398,33 +435,85 @@ 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.
|
||||
if (xr_is_session_running())
|
||||
xr_input_sync_and_draw(g_local_space, g_predicted_display_time, g_have_acquired_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);
|
||||
xr_menu_render_if_visible();
|
||||
}
|
||||
|
||||
if (g_have_acquired_image) {
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
XrSwapchainImageReleaseInfo releaseInfo = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
||||
xr_check(xrReleaseSwapchainImage(g_game_swapchain, &releaseInfo),
|
||||
"xrReleaseSwapchainImage");
|
||||
xr_check(xrReleaseSwapchainImage(g_swapchain, &releaseInfo), "xrReleaseSwapchainImage");
|
||||
}
|
||||
|
||||
if (!xr_is_session_running())
|
||||
return;
|
||||
|
||||
XrCompositionLayerQuad quad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||
quad.space = g_local_space;
|
||||
quad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
quad.subImage.swapchain = g_game_swapchain;
|
||||
quad.subImage.imageRect.extent.width = g_game_width;
|
||||
quad.subImage.imageRect.extent.height = g_game_height;
|
||||
quad.pose.orientation.w = 1.0f;
|
||||
quad.pose.position.z = -QUAD_DISTANCE_METERS;
|
||||
quad.size.width = QUAD_WIDTH_METERS;
|
||||
quad.size.height = QUAD_WIDTH_METERS * (float)g_game_height / (float)g_game_width;
|
||||
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.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;
|
||||
|
||||
const XrCompositionLayerBaseHeader *layers[1] = {(XrCompositionLayerBaseHeader *)&quad};
|
||||
// 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.
|
||||
const XrCompositionLayerBaseHeader *layers[2];
|
||||
uint32_t layerCount = 0;
|
||||
if (g_have_acquired_image)
|
||||
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&gameQuad;
|
||||
|
||||
XrCompositionLayerQuad menuQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||
if (menuVisible) {
|
||||
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.pose.orientation.w = 1.0f;
|
||||
menuQuad.pose.position.x = centerX;
|
||||
menuQuad.pose.position.y = centerY;
|
||||
menuQuad.pose.position.z = -distance;
|
||||
menuQuad.size.width = halfWidth * 2.0f;
|
||||
menuQuad.size.height = halfHeight * 2.0f;
|
||||
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&menuQuad;
|
||||
}
|
||||
|
||||
XrFrameEndInfo endInfo = {XR_TYPE_FRAME_END_INFO};
|
||||
endInfo.displayTime = g_predicted_display_time;
|
||||
endInfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||
endInfo.layerCount = g_have_acquired_image ? 1 : 0;
|
||||
endInfo.layers = g_have_acquired_image ? layers : NULL;
|
||||
endInfo.layerCount = layerCount;
|
||||
endInfo.layers = layerCount > 0 ? layers : NULL;
|
||||
xr_check(xrEndFrame(g_session, &endInfo), "xrEndFrame");
|
||||
}
|
||||
|
||||
@@ -435,14 +524,18 @@ void xr_shutdown(void) {
|
||||
}
|
||||
g_swapchain_image_count = 0;
|
||||
|
||||
if (g_game_swapchain != XR_NULL_HANDLE)
|
||||
xrDestroySwapchain(g_game_swapchain);
|
||||
g_game_swapchain = XR_NULL_HANDLE;
|
||||
if (g_swapchain != XR_NULL_HANDLE)
|
||||
xrDestroySwapchain(g_swapchain);
|
||||
g_swapchain = XR_NULL_HANDLE;
|
||||
|
||||
if (g_local_space != XR_NULL_HANDLE)
|
||||
xrDestroySpace(g_local_space);
|
||||
g_local_space = XR_NULL_HANDLE;
|
||||
|
||||
// Before the session/instance they were created from.
|
||||
xr_input_shutdown();
|
||||
xr_menu_shutdown();
|
||||
|
||||
if (g_session != XR_NULL_HANDLE)
|
||||
xrDestroySession(g_session);
|
||||
g_session = XR_NULL_HANDLE;
|
||||
@@ -454,3 +547,9 @@ void xr_shutdown(void) {
|
||||
g_session_state = XR_SESSION_STATE_UNKNOWN;
|
||||
g_session_running = false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user