Add OpenXR bring-up: render the game as a floating quad in an immersive Quest session
build / build (push) Successful in 2m12s
build / build (push) Successful in 2m12s
- New android/app/src/main/cpp/xr_session.c (+.h): owns the OpenXR
instance/session/local-space/swapchain and the per-frame
xrWaitFrame/xrBeginFrame/xrEndFrame loop, submitting the game's
existing flat render as a single head-tracked XrCompositionLayerQuad.
No stereo rendering or controller input yet - that's steps B/C/D of
the plan.
- 11-android-openxr-cmake.patch: links the OpenXR loader as a build
dependency, staged into jniLibs the same way as gl4es/SDL2.
build-image/Dockerfile and prepare-android-project.sh build and stage
it.
- 12-android-openxr-present.patch: redirects OpenGL.cc's final present
step (opengl_swap_and_restore / SDLDraw's software path) into the XR
swapchain FBO instead of the window, on Android only. Two real gl4es
bugs had to be worked around to get pixels on screen at all:
- gl4es's own glBindFramebuffer errors on an FBO id it didn't create
itself, even though the real driver-level bind succeeds - fixed by
creating the swapchain FBO container via gl4es's own
glGenFramebuffers/glBindFramebuffer, and only using the real
(dlsym'd) driver call for attaching OpenXR's foreign swapchain
texture, which gl4es's own attach can't handle.
- gl4es's glBegin/glVertexAttrib/glVertex3f immediate-mode emulation
only captures a fresh per-vertex value for attribute 0 (position,
driven directly by glVertex3f) - custom attributes like this
shader's texcoords/light are GLES2's *constant*-attribute API and
applied once for the whole draw, not per vertex, silently
collapsing the UI-overlay quad to a single sampled texel. Fixed by
switching that one draw call to real vertex arrays
(glVertexAttribPointer/glDrawArrays).
Also flips the V texcoord to match SDL's top-down row order against
GL's texture convention, and reuses a persistent texture object
instead of a fresh gen/upload/delete every frame.
- AndroidManifest.xml: declares the immersive-HMD intent category and
focus-aware metadata, drops the 2D-panel layout hint.
- README: documents the new OpenXR immersive mode.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -64,6 +64,19 @@
|
||||
endif(WIN32)
|
||||
endif(ENABLE_OPENGL)
|
||||
|
||||
+# OpenXR loader (Android only for now - see android/app/src/main/cpp/
|
||||
+# xr_session.c, which submits the game's existing render as a composition
|
||||
+# layer instead of presenting to a normal window/surface). EGL is linked
|
||||
+# explicitly too - xr_session.c calls eglGetCurrentDisplay/Context/
|
||||
+# QueryContext/ChooseConfig directly (to share the GL context gl4es/SDL
|
||||
+# already made), and unlike GL itself (where gl4es's own libGL.so already
|
||||
+# provides every symbol the engine or xr_session.c call), there's no other
|
||||
+# EGL provider in this link.
|
||||
+if(ANDROID)
|
||||
+ set(OPENXR_INCLUDE_DIRS ${ANDROID_PREBUILT_DIR}/openxr/include)
|
||||
+ set(OPENXR_LIBRARIES ${ANDROID_PREBUILT_DIR}/openxr/lib/libopenxr_loader.so EGL)
|
||||
+endif(ANDROID)
|
||||
+
|
||||
if(ENABLE_SDL2 MATCHES "ON")
|
||||
find_package(SDL2 REQUIRED)
|
||||
if(SDL2_FOUND)
|
||||
@@ -109,6 +122,8 @@
|
||||
${SDL2_MIXER_INCLUDE_DIRS}
|
||||
${FLUIDSYNTH_INCLUDE_DIRS}
|
||||
${OPENGL_INCLUDE_DIRS}
|
||||
+ ${OPENXR_INCLUDE_DIRS}
|
||||
+ ${ANDROID_EXTRA_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
if(NOT WIN32)
|
||||
@@ -439,6 +454,7 @@
|
||||
${SDL2_MIXER_LIBRARIES}
|
||||
${FLUIDSYNTH_LIBRARIES}
|
||||
${OPENGL_LIBRARIES}
|
||||
+ ${OPENXR_LIBRARIES}
|
||||
${ALSA_LIBRARIES}
|
||||
$<$<BOOL:${ANDROID}>:log>
|
||||
)
|
||||
@@ -0,0 +1,252 @@
|
||||
--- a/src/MacSrc/OpenGL.cc 2026-07-25 07:10:06.189412183 +0200
|
||||
+++ b/src/MacSrc/OpenGL.cc 2026-07-25 07:12:05.356301052 +0200
|
||||
@@ -36,6 +36,7 @@
|
||||
if (loc >= 0)
|
||||
glUniform1f(loc, size);
|
||||
}
|
||||
+#include "xr_session.h"
|
||||
#endif // __ANDROID__
|
||||
|
||||
extern "C" {
|
||||
@@ -382,6 +383,21 @@
|
||||
#endif
|
||||
opengl_resize(width, height);
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+ // Bring up the OpenXR session now that the shared GL context above is
|
||||
+ // current (xr_init() reuses it via eglGetCurrentContext/Display - see
|
||||
+ // xr_session.c) - sized to the game's own logical resolution, not the
|
||||
+ // physical window, since the swapchain is composited by the XR runtime
|
||||
+ // rather than upscaled by us into a physical-window-sized viewport (see
|
||||
+ // opengl_swap_and_restore()/android_composite_software_frame() below).
|
||||
+ // Failure here (e.g. no OpenXR runtime installed) just means the
|
||||
+ // immersive path never activates - SDLDraw() falls back to the plain
|
||||
+ // window-present path for the rest of the run.
|
||||
+ int xr_logical_width, xr_logical_height;
|
||||
+ SDL_RenderGetLogicalSize(renderer, &xr_logical_width, &xr_logical_height);
|
||||
+ xr_init(xr_logical_width, xr_logical_height);
|
||||
+#endif
|
||||
+
|
||||
// Now make the palettes
|
||||
opaquePalette = SDL_AllocPalette(256);
|
||||
transparentPalette = SDL_AllocPalette(256);
|
||||
@@ -498,7 +514,109 @@
|
||||
*y_scale = output_height / screen_height;
|
||||
}
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+// Shared by opengl_swap_and_restore()'s UI-overlay blit and
|
||||
+// android_composite_software_frame() below - draws an SDL_Surface as a
|
||||
+// fullscreen textured quad via gl4es. SDL's own renderer can't be used
|
||||
+// here: it's tied to the window's own EGL surface, not whatever XR
|
||||
+// swapchain framebuffer the caller already bound via xr_frame_begin().
|
||||
+static void android_draw_surface_as_quad(SDL_Surface *ui, bool blend) {
|
||||
+ SDL_Surface *uiRgba = SDL_ConvertSurfaceFormat(ui, SDL_PIXELFORMAT_RGBA32, 0);
|
||||
+ if (uiRgba == nullptr)
|
||||
+ return;
|
||||
+
|
||||
+ glUseProgram(textureShaderProgram.shaderProgram);
|
||||
+ GLint tcAttrib = textureShaderProgram.tcAttrib;
|
||||
+ GLint lightAttrib = textureShaderProgram.lightAttrib;
|
||||
+ glUniform1i(textureShaderProgram.uniNightSight, false);
|
||||
+ glUniformMatrix4fv(textureShaderProgram.uniView, 1, false, IdentityMatrix);
|
||||
+ glUniformMatrix4fv(textureShaderProgram.uniProj, 1, false, IdentityMatrix);
|
||||
+
|
||||
+ // Persistent texture, reused every call instead of a fresh
|
||||
+ // gen/upload/delete each frame.
|
||||
+ static GLuint s_uiTexture = 0;
|
||||
+ if (s_uiTexture == 0)
|
||||
+ glGenTextures(1, &s_uiTexture);
|
||||
+ bind_texture(s_uiTexture);
|
||||
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, uiRgba->w, uiRgba->h, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
+ uiRgba->pixels);
|
||||
+
|
||||
+ set_blend_mode(blend);
|
||||
+
|
||||
+ // Real per-vertex arrays, not glBegin/glVertexAttrib/glVertex3f
|
||||
+ // immediate mode: glVertexAttrib*() is GLES2's *constant*-attribute
|
||||
+ // API - without an enabled vertex array it sets one value for the
|
||||
+ // whole draw call, not a fresh value per vertex. gl4es's immediate-mode
|
||||
+ // emulation only reproduces desktop GL's per-vertex-capture behavior
|
||||
+ // for attribute 0 (position, driven directly by glVertex3f), not for
|
||||
+ // custom attributes like this shader's texcoords/light.
|
||||
+ //
|
||||
+ // V is flipped (1 at the bottom, 0 at the top): GL texture v=0
|
||||
+ // addresses the first row of data passed to glTexImage2D, but uiRgba
|
||||
+ // (an SDL surface) stores its rows top-down.
|
||||
+ struct QuadVertex {
|
||||
+ float x, y, z;
|
||||
+ float u, v;
|
||||
+ float light;
|
||||
+ };
|
||||
+ static const QuadVertex quadVerts[4] = {
|
||||
+ {1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 1.0f},
|
||||
+ {1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f},
|
||||
+ {-1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f},
|
||||
+ {-1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f},
|
||||
+ };
|
||||
+ glEnableVertexAttribArray(0);
|
||||
+ glEnableVertexAttribArray(tcAttrib);
|
||||
+ glEnableVertexAttribArray(lightAttrib);
|
||||
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(QuadVertex), &quadVerts[0].x);
|
||||
+ glVertexAttribPointer(tcAttrib, 2, GL_FLOAT, GL_FALSE, sizeof(QuadVertex), &quadVerts[0].u);
|
||||
+ glVertexAttribPointer(lightAttrib, 1, GL_FLOAT, GL_FALSE, sizeof(QuadVertex),
|
||||
+ &quadVerts[0].light);
|
||||
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
+ // The engine's other rendering uses glBegin/glVertexAttrib/glVertex3f
|
||||
+ // immediate mode for attribute 0, which depends on it not having an
|
||||
+ // enabled array bound.
|
||||
+ glDisableVertexAttribArray(0);
|
||||
+ glDisableVertexAttribArray(tcAttrib);
|
||||
+ glDisableVertexAttribArray(lightAttrib);
|
||||
+ glFlush();
|
||||
+
|
||||
+ SDL_FreeSurface(uiRgba);
|
||||
+}
|
||||
+
|
||||
+void android_composite_software_frame(SDL_Surface *ui) {
|
||||
+ // Deliberately no SDL_GL_MakeCurrent() here - see the comment in
|
||||
+ // opengl_swap_and_restore() below for why.
|
||||
+ glClear(GL_COLOR_BUFFER_BIT);
|
||||
+ android_draw_surface_as_quad(ui, false);
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
void opengl_swap_and_restore(SDL_Surface *ui) {
|
||||
+#ifdef __ANDROID__
|
||||
+ // No SDL_GL_MakeCurrent() here on Android, deliberately - unlike the
|
||||
+ // desktop path below. init_opengl() already made (window, context)
|
||||
+ // current once; re-asserting it every frame turned out to reset
|
||||
+ // gl4es's own internal "current framebuffer" bookkeeping back to
|
||||
+ // whatever it associates with that context/surface pair (observed
|
||||
+ // on-device: the XR swapchain image reliably received our real
|
||||
+ // glClear/bind from xr_frame_begin(), but every actual gl4es-routed
|
||||
+ // draw call after a fresh SDL_GL_MakeCurrent() here kept landing
|
||||
+ // somewhere else - the quad stayed permanently black). Confirmed this
|
||||
+ // exact class of gl4es/OpenXR interaction against RTCWQuest (Team
|
||||
+ // Beef Studios), a shipped gl4es+Khronos-OpenXR-loader Quest port:
|
||||
+ // it makes its EGL context current exactly once at init and never
|
||||
+ // again per-frame, for the same reason.
|
||||
+ //
|
||||
+ // xr_frame_begin() (called from SDLDraw()) already bound the XR
|
||||
+ // swapchain's framebuffer and a full-size viewport - no physical-
|
||||
+ // window letterboxing needed here either, unlike the desktop/2D-panel
|
||||
+ // path below, since the swapchain is sized to exactly the game's
|
||||
+ // logical resolution (see xr_init()).
|
||||
+ glClear(GL_COLOR_BUFFER_BIT);
|
||||
+#else
|
||||
// restore the view backup (without HUD overlay) for incremental
|
||||
// updates in the subsequent frame
|
||||
SDL_GL_MakeCurrent(window, context);
|
||||
@@ -510,6 +628,7 @@
|
||||
// Set the drawable area for the 3d view
|
||||
glViewport(phys_offset_x * x_hdpi_scale, phys_offset_y * y_hdpi_scale, phys_width * x_hdpi_scale,
|
||||
phys_height * y_hdpi_scale);
|
||||
+#endif
|
||||
set_blend_mode(false);
|
||||
|
||||
// Bind and setup our general shader program
|
||||
@@ -552,6 +671,11 @@
|
||||
if (err != GL_NO_ERROR)
|
||||
ERROR("OpenGL error: %i", err);
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+ // Blit the UI canvas over the 3d view (see android_draw_surface_as_quad()
|
||||
+ // above - SDL's renderer can't target the XR swapchain framebuffer).
|
||||
+ android_draw_surface_as_quad(ui, true);
|
||||
+#else
|
||||
// Blit the UI canvas over the 3d view
|
||||
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, ui);
|
||||
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
|
||||
@@ -561,6 +685,7 @@
|
||||
|
||||
// Finally, swap to the screen
|
||||
SDL_RenderPresent(renderer);
|
||||
+#endif
|
||||
}
|
||||
|
||||
void toggle_opengl() {
|
||||
--- a/src/MacSrc/OpenGL.h 2026-07-19 13:11:51.000000000 +0200
|
||||
+++ b/src/MacSrc/OpenGL.h 2026-07-25 07:10:06.353410581 +0200
|
||||
@@ -22,6 +22,15 @@
|
||||
void opengl_swap_and_restore(SDL_Surface *ui);
|
||||
void opengl_change_palette();
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+// Composites a plain software-rendered frame (the same content SDLDraw's
|
||||
+// non-OpenGL fallback would otherwise present via SDL_RenderCopy) into the
|
||||
+// XR swapchain image already bound by xr_frame_begin() - see Shock.c's
|
||||
+// SDLDraw(), which needs this for the splash screen/cutscenes/menus that
|
||||
+// render this way instead of through opengl_swap_and_restore().
|
||||
+void android_composite_software_frame(SDL_Surface *ui);
|
||||
+#endif
|
||||
+
|
||||
void opengl_set_viewport(int x, int y, int width, int height);
|
||||
int opengl_draw_tmap(int n, g3s_phandle *vp, grs_bitmap *bm);
|
||||
int opengl_light_tmap(int n, g3s_phandle *vp, grs_bitmap *bm);
|
||||
--- a/src/MacSrc/Shock.c 2026-07-25 07:10:06.181412261 +0200
|
||||
+++ b/src/MacSrc/Shock.c 2026-07-25 07:10:06.353410581 +0200
|
||||
@@ -30,6 +30,10 @@
|
||||
#include <math.h>
|
||||
#include <SDL.h>
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+#include "xr_session.h"
|
||||
+#endif
|
||||
+
|
||||
#include "InitMac.h"
|
||||
#include "Modding.h"
|
||||
#include "OpenGL.h"
|
||||
@@ -301,6 +305,21 @@
|
||||
}
|
||||
|
||||
void SDLDraw() {
|
||||
+#ifdef __ANDROID__
|
||||
+ // The true once-a-frame present hook for the immersive OpenXR path -
|
||||
+ // unlike opengl_swap_and_restore() below, this runs every frame
|
||||
+ // regardless of should_opengl_swap() (e.g. also during the splash
|
||||
+ // screen/cutscenes/menus, which render through the plain software
|
||||
+ // path below, not the OpenGL one). xr_poll_events() needs exactly that
|
||||
+ // - called unconditionally - to ever observe the session reach a
|
||||
+ // running state in the first place.
|
||||
+ xr_poll_events();
|
||||
+ if (!xr_frame_begin()) {
|
||||
+ xr_frame_end();
|
||||
+ return;
|
||||
+ }
|
||||
+#endif
|
||||
+
|
||||
if (should_opengl_swap()) {
|
||||
// We want the UI background to be transparent!
|
||||
sdlPalette->colors[255].a = 0x00;
|
||||
@@ -310,14 +329,27 @@
|
||||
|
||||
// Set the palette back, and we are done
|
||||
sdlPalette->colors[255].a = 0xff;
|
||||
+#ifdef __ANDROID__
|
||||
+ xr_frame_end();
|
||||
+#endif
|
||||
return;
|
||||
}
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+ // Same software-rendered frame the desktop path below would present
|
||||
+ // via SDL_RenderCopy/SDL_RenderPresent, composited into the XR
|
||||
+ // swapchain image xr_frame_begin() bound above instead - SDL's
|
||||
+ // renderer is tied to the window's own EGL surface, not that FBO.
|
||||
+ android_composite_software_frame(drawSurface);
|
||||
+ xr_frame_end();
|
||||
+ return;
|
||||
+#endif
|
||||
+
|
||||
// Clear the screen!
|
||||
SDL_RenderClear(renderer);
|
||||
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, drawSurface);
|
||||
|
||||
- // Blit to the screen by drawing the surface
|
||||
+ // Blit to the screen by drawing the surface
|
||||
SDL_Rect srcRect = {0, 0, gScreenWide, gScreenHigh};
|
||||
SDL_RenderCopy(renderer, texture, &srcRect, NULL);
|
||||
SDL_DestroyTexture(texture);
|
||||
Reference in New Issue
Block a user