diff --git a/README.md b/README.md index 9f81f06..1c3aa17 100644 --- a/README.md +++ b/README.md @@ -402,6 +402,10 @@ each one does: `SDL_SetWindowFullscreen`/`SetWindowSize`/`SetWindowPosition` calls on Android, since there's no real desktop-style window to resize and calling them desyncs SDL's cached window size from the real surface. + Also calls `opengl_resize()` directly in `ChangeScreenSize()` + (see README 5.5.6) - skipping `SDL_SetWindowSize()` means no native + resize event ever fires on Android to trigger it when the in-game + resolution changes, so it would otherwise never re-run there. - **`10-android-openxr-cmake.patch`** - wires up the OpenXR loader plus EGL as link/include dependencies for the CMake build. - **`11-android-openxr-present.patch`** - the core OpenXR present-path @@ -464,6 +468,46 @@ each one does: silently forever instead of falling back. Adds an `#else` branch that returns failure and logs a `WARN`, so `InitDecXMI()`'s existing fallback-to-dummy-driver logic actually triggers. +- **`18-android-opengl-immediate-mode-attribs.patch`** - fixes a black (and, + after this patch's first revision, flat-colored/untextured) screen when + switching to the OpenGL/3D render mode in-game (see README 5.5.5). Every + `glBegin(GL_TRIANGLE_STRIP)`-based draw in `OpenGL.cc` - + `opengl_swap_and_restore()`'s screen-composite quad, + `opengl_light_tmap()` (every textured wall/floor polygon, via + `draw_vertex()`), `opengl_bitmap()` (sprites), and `opengl_draw_poly()` + (flat/gouraud polygons, also via `draw_vertex()`) - used + `glVertexAttrib2f`/`glVertexAttrib1f`/`glVertex3f` immediate mode to set + per-vertex texcoords/light, which gl4es only captures per-vertex for + attribute 0 (position); custom attributes end up constant for the whole + draw call. Adds `android_imm_begin()`/`android_imm_vertex()`/ + `android_imm_end()`, a small shared immediate-mode-emulation shim that + buffers vertices and flushes them as one real `glDrawArrays()` call, and + switches every one of those call sites to it, Android-only. The + starfield (`opengl_begin_stars()`/`opengl_draw_star()`/ + `opengl_end_stars()`, `GL_POINTS` rather than `GL_TRIANGLE_STRIP`, up to + `NUM_STARS`=1000 per frame) had the same bug for its per-star brightness + and gets its own similarly-shaped `android_stars_begin()`/ + `android_stars_vertex()`/`android_stars_end()` buffer. Also skips the + unconditional `SDL_GL_MakeCurrent(window, context)` call every + rendering function in this file had at its top, Android-only - a + worthwhile cleanup on its own merits, but *not* the source of the + persistent per-frame `GL_INVALID_OPERATION` (1282) logcat spam during + 3D gameplay that this was first suspected to be (see README 5.5.8 for + that bug's actual root cause and fix, in patch 20 below). +- **`19-android-fix-hackcam-opengl-frame.patch`** - `fr_start_view()`/ + `fr_send_view()` (`frsetup.c`) called `opengl_start_frame()`/ + `opengl_end_frame()` again for hack-camera security-monitor renders + (which target a private software bitmap, not the GL-backed screen), + since `should_opengl_swap()` doesn't exclude them the way + `use_opengl()` already does. Adds the same exclusion to both call + sites (see README 5.5.8). +- **`20-android-no-default-framebuffer.patch`** - the actual fix for the + persistent per-frame `GL_INVALID_OPERATION` (1282) in README 5.5.8: + `opengl_end_frame()`'s `BindFrameBuffer(nullptr)` bind to framebuffer/ + renderbuffer id 0 has no valid target on Android (real presentation + goes through the OpenXR swapchain's own FBOs, set up separately in + `xr_swapchain.c`) and is also functionally dead there regardless - + skipped entirely, Android-only. `android/gl4es-patches/`: @@ -600,6 +644,189 @@ path (still silent, but with a clear `WARN` in logcat) instead of silently reporting success while never opening anything - a much faster diagnosis if this ever recurs. +#### 5.5.5. Black, then flat-colored/untextured screen after switching to OpenGL rendering in-game + +The engine has a `Ctrl+G`-bound in-game toggle (`toggle_opengl()`, +`Prefs.c`) that cycles between the plain software renderer and OpenGL +rendering. On Quest, switching to OpenGL rendering (either mode: +unfiltered or bilinear) produced a black screen for the 3D view, with +`OpenGL error: 1282` (`GL_INVALID_OPERATION`) in logcat right at the switch. + +**Root cause:** `opengl_swap_and_restore()` draws the offscreen-rendered +3D view onto a screen-filling quad using `glBegin(GL_TRIANGLE_STRIP)` / +`glVertexAttrib2f()`/`glVertexAttrib1f()` / `glVertex3f()` immediate +mode to supply per-vertex texcoords and light. This is exactly the same +bug already found and fixed for the UI-overlay quad in +`11-android-openxr-present.patch`'s `android_draw_surface_as_quad()`: +under gl4es's GLES2 immediate-mode emulation, `glVertexAttrib*()` sets a +*constant* value for the whole draw call, not a fresh value per vertex +the way desktop GL's immediate mode does - only attribute 0 (position, +driven by `glVertex3f`) gets real per-vertex capture. The 3D-view quad +draw predates that fix and was never updated to match, so it kept using +the broken pattern. + +This patch's first revision only fixed that one quad, which did clear the +black screen - but the next on-device test showed a "blocky" world +instead: every wall/floor polygon rendered as a single solid color +instead of its texture. The same broken immediate-mode pattern turned out +to be used by every other textured/lit polygon draw in the file too: +`opengl_light_tmap()` (every textured wall/floor polygon in the 3D view, +via the shared `draw_vertex()` helper), `opengl_bitmap()` (sprites), and +`opengl_draw_poly()` (flat/gouraud-shaded polygons, also via +`draw_vertex()`) - each polygon's texcoord/light attribute ended up +constant across all of its vertices (whatever was last set), instead of +varying per vertex, hence the flat, solid-colored blocks. + +**Fix:** `18-android-opengl-immediate-mode-attribs.patch` adds +`android_imm_begin()`/`android_imm_vertex()`/`android_imm_end()`, a small +shared immediate-mode-emulation shim (buffers vertices, then issues one +real `glDrawArrays()` call), and switches every glBegin/glVertexAttrib/ +glVertex3f call site above to it, Android-only - the desktop path is +unaffected and keeps the original immediate-mode code, which works +correctly under real desktop OpenGL. + +The same patch also fixes the starfield +(`opengl_begin_stars()`/`opengl_draw_star()`/`opengl_end_stars()`), found +while auditing the file for every other instance of this pattern rather +than through an on-device symptom: each star is a separate `GL_POINTS` +vertex with its own brightness set via the same +`glVertexAttrib1f`/`glVertex3f` immediate-mode pair, so every star drawn +between one begin/end call would have rendered at whatever brightness was +set for the last one. Gets its own `android_stars_begin()`/ +`android_stars_vertex()`/`android_stars_end()` buffer, shaped for +`GL_POINTS` and up to `NUM_STARS` (`GameSrc/rendtool.c`) vertices rather +than `android_imm_*()`'s small triangle-strip buffer. + +#### 5.5.6. Display breaks and stops responding to input after changing resolution in-game + +Picking a different resolution from the in-game options menu (e.g. +`1024x768`) produced a single `OpenGL error: 1282` in logcat and left the +display frozen on the last frame - `VrApi`'s FPS counter kept running at +90fps (the OpenXR compositor was still submitting frames), but nothing +in the menu responded to further input afterward. + +**Root cause:** `ChangeScreenSize()` (`ShockBitmap.c`) calls +`SDL_RenderSetLogicalSize()` to the new resolution and reallocates +`drawSurface`/`offscreenDrawSurface` accordingly, but on desktop the +actual GL-side work - `opengl_resize()` in `OpenGL.cc`, which +(re)creates `backupBuffer` (the offscreen render target the 3D view +draws into) at the new size - only runs later, from the +`SDL_WINDOWEVENT_RESIZED`/`SIZE_CHANGED` event that +`SDL_SetWindowSize()` generates. `09-android-no-window-resize.patch` +skips that `SDL_SetWindowSize()` call on Android for an unrelated +reason (see its own entry above) - which also means no such event, and +therefore no `opengl_resize()` call, ever happens on Android when the +in-game resolution changes. `backupBuffer` stayed stuck at whatever size +it was created with at launch while the rest of the game moved on to +the new resolution, and that mismatch is what broke rendering and left +the menu unable to correctly hit-test further clicks. + +**Fix:** `ChangeScreenSize()` now calls `opengl_resize()` directly on +Android, right after updating `gScreenWide`/`gScreenHigh` (matching the +desktop event's actual timing, since `SDL_PollEvent()` always delivers +it after `ChangeScreenSize()` has already returned) - added to +`09-android-no-window-resize.patch` since this is a direct, foreseeable +consequence of that patch's own change, not a new mechanism. + +#### 5.5.7. Laser-pointer cursor disappears after changing resolution in-game + +After 5.5.6's fix, changing resolution stopped breaking the display, but +the laser-pointer-driven mouse cursor (see section 5's architecture +overview) would disappear and stop being able to click anything +afterward. + +**Root cause:** `xr_get_game_resolution()` (`xr_session.c`), which +`xr_input.c` uses to convert a game-quad ray hit's normalized u,v into +the pixel coordinates it feeds the engine via `xr_mouse_move()`, returned +the game quad's OpenXR swapchain's fixed pixel size +(`g_game_swapchain.width/height`) - set once at launch and never resized. +`android_draw_surface_as_quad()` (`OpenGL.cc`, +`11-android-openxr-present.patch`) always stretch-blits `drawSurface` to +fill that swapchain image via a single full-quad draw, regardless of +`drawSurface`'s own pixel size, so the swapchain's fixed size has no +actual relationship to the coordinate space `MouseX`/`MouseY` live in - +that's `drawSurface`'s own current size (`gScreenWide`/`gScreenHigh`), +which `ChangeScreenSize()` can change at runtime independently of the +swapchain. Using the stale swapchain size there mapped every ray hit +into the wrong coordinate space, moving the mouse position out of the +engine's actual screen bounds. + +**Fix:** `xr_get_game_resolution()` now returns `gScreenWide`/ +`gScreenHigh` instead. `xr_get_game_quad_extent()` (the quad's shape and +distance in the VR world) is intentionally left as-is, still tied to the +swapchain's fixed size - that's a real, unchanging property of the +swapchain image itself, unrelated to this bug. + +#### 5.5.8. Persistent `OpenGL error: 1282` logcat spam during 3D gameplay + +Every logcat capture during actual 3D gameplay showed a continuous +stream of `OpenGL error: 1282` (roughly twice per frame) from the one +pre-existing `glGetError()` check in `opengl_swap_and_restore()`, with +no visible effect on rendering (textures, resolution changes, and the +cursor all worked correctly per sections 5.5.5-5.5.7's fixes). 1282 is +`GL_INVALID_OPERATION` (0x0502) - not `GL_INVALID_ENUM` (0x0500/1280), +which earlier debugging had mistakenly assumed given the visual +symptoms resembled the immediate-mode bug in 5.5.5. + +Two real, unrelated bugs turned out to be involved, found by adding a +temporary chain of `glGetError()` checkpoints across the whole +per-frame render path (`mainloop()` → `render_run()` → `fr_rend()` → +`fr_prepare_view()`/`fr_start_view()`/`fr_send_view()`) and bisecting +which ones read clean: + +**xrPollEvent() leaves a stray error.** `xr_poll_events()` +(`xr_session.c`) left gl4es reporting `GL_INVALID_OPERATION` on every +call while the session is running, regardless of what this app's own +code does. Fixed by draining it (`while (glGetError() != GL_NO_ERROR)`) +right after the polling loop. + +**A hack-camera render re-entered the OpenXR frame setup.** +`render_hack_cameras()` (`render.c`) calls `fr_rend()` - and so +`fr_start_view()`/`fr_send_view()` - a second time per real frame for +each active security-camera monitor, rendering into a private software +(`FR_DOUBLEB_MASK`) bitmap rather than the actual GL-backed screen. +`use_opengl()` already excludes hack-camera/pickup-mode views from the +GL texture-mapping functions (`FR_PICKUPM_MASK|FR_HACKCAM_MASK`), but +`should_opengl_swap()` - the separate check gating +`opengl_start_frame()`/`opengl_end_frame()` in those two functions - +didn't, so those ran again mid-frame for a render that was never +GL-backed. `19-android-fix-hackcam-opengl-frame.patch` adds the same +exclusion to both call sites. This is a genuine bug fixed on its own +merits, but bisecting further showed it wasn't actually the source of +the 1282 spam - removing it just relocated the checkpoint that caught +the error, without changing whether it fired. + +**Actual root cause: binding "framebuffer 0" on Android has no valid +target.** `opengl_end_frame()`'s `BindFrameBuffer(nullptr)` binds +framebuffer/renderbuffer id 0 to mean "the real window surface" - true +on desktop, where that's SDL's actual GL window. On Android, this app's +real presentation target is never that window's own EGL surface at +all - it's the OpenXR swapchain's per-image FBOs +(`xr_swapchain_create()`/`xr_swapchain_acquire()`, `xr_swapchain.c`). +Whatever framebuffer/renderbuffer 0 actually resolves to for gl4es in +this configuration, binding it reliably set a persistent +`GL_INVALID_OPERATION` - confirmed by bisecting through the *entire* +render path with per-call checkpoints, all clean right up until the one +placed immediately after this exact call. The bind is also functionally +dead regardless of the error: by the time anything next draws, Shock.c's +`SDLDraw()` has already run `xr_frame_begin()` → +`xr_swapchain_acquire()`, which rebinds to that frame's real swapchain +FBO before `opengl_swap_and_restore()` (the only thing that draws before +the next `opengl_start_frame()`'s own `BindFrameBuffer(&backupBuffer)`) +runs - so nothing ever actually renders while framebuffer 0 is bound. +`20-android-no-default-framebuffer.patch` skips the call on Android +entirely. Confirmed via logcat: this fully eliminated the error across +an extended play session with 3D rendering active throughout, with no +change in what actually renders. + +An earlier theory (that the OpenXR runtime's own GL work inside +`xrAcquireSwapchainImage`/`xrWaitSwapchainImage`/`xrEndFrame` was +leaving stray driver-level errors on the shared EGL context) prompted a +dlsym'd, gl4es-bypassing `glGetError()` drain at those call sites. Once +the above was fixed, testing showed that drain was never actually +necessary, so it was removed rather than kept as unproven defensive +code. + ## 6. License The original tooling in this repository (the Docker build image, build diff --git a/android/app/src/main/cpp/xr_session.c b/android/app/src/main/cpp/xr_session.c index d785a7b..fb7adfa 100644 --- a/android/app/src/main/cpp/xr_session.c +++ b/android/app/src/main/cpp/xr_session.c @@ -319,6 +319,14 @@ void xr_poll_events(void) { } } } + + // xrPollEvent() leaves gl4es reporting a stray GL_INVALID_OPERATION on + // every call while the session is running - drain it (looped, since + // more than one can accumulate across xrPollEvent()'s possibly-multiple + // iterations above) so it doesn't surface as a false positive at the + // next unrelated glGetError() check in this app's own rendering code. + while (glGetError() != GL_NO_ERROR) { + } } bool xr_is_session_running(void) { return g_session_running; } @@ -434,6 +442,7 @@ void xr_frame_end(void) { endInfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; endInfo.layerCount = layerCount; endInfo.layers = layerCount > 0 ? layers : NULL; + xr_check(xrEndFrame(g_session, &endInfo), "xrEndFrame"); } @@ -471,6 +480,21 @@ void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half } void xr_get_game_resolution(int *width, int *height) { - *width = g_game_swapchain.width; - *height = g_game_swapchain.height; + // Not g_game_swapchain.width/height: that's the fixed physical size of + // the swapchain image the game is composited into (see xr_init()) - + // android_draw_surface_as_quad() (OpenGL.cc, engine-patches/11) always + // stretch-blits drawSurface to fill that image regardless of + // drawSurface's own pixel size, so the swapchain's size has no relation + // to the coordinate space MouseX/MouseY actually live in. gScreenWide/ + // gScreenHigh (InitMac.h) is drawSurface's current size - the + // resolution the in-game options menu can change at runtime + // (ShockBitmap.c's ChangeScreenSize()) independently of the swapchain, + // which is created once at launch and never resized. Using the fixed + // swapchain size here after such a change mapped the laser pointer's + // quad-space hit into the wrong coordinate space entirely, moving + // MouseX/MouseY out of the engine's actual screen bounds - which is + // what made the cursor disappear and become unclickable. + extern int32_t gScreenWide, gScreenHigh; + *width = gScreenWide; + *height = gScreenHigh; } diff --git a/android/app/src/main/cpp/xr_session.h b/android/app/src/main/cpp/xr_session.h index 943c213..6b39b50 100644 --- a/android/app/src/main/cpp/xr_session.h +++ b/android/app/src/main/cpp/xr_session.h @@ -74,11 +74,13 @@ void xr_shutdown(void); // laser pointer always matches whatever's actually visible. void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m); -// Pixel resolution of the game quad's own swapchain - i.e. game_width/ -// game_height as passed to xr_init() (grd_cap->w/h, see Shock.c's -// InitSDL()) - the logical coordinate space xr_input.c needs to convert a -// game-quad ray hit's normalized u,v into engine mouse coordinates (see -// xr_mouse.h). +// The engine's current screen resolution (gScreenWide/gScreenHigh, +// InitMac.h) - not the game quad's own swapchain size, which is fixed at +// launch and never changes - the logical coordinate space xr_input.c needs +// to convert a game-quad ray hit's normalized u,v into engine mouse +// coordinates (see xr_mouse.h). Can change at runtime via the in-game +// options menu (ShockBitmap.c's ChangeScreenSize()), independently of the +// swapchain. void xr_get_game_resolution(int *width, int *height); #ifdef __cplusplus diff --git a/android/app/src/main/cpp/xr_swapchain.c b/android/app/src/main/cpp/xr_swapchain.c index d99ca7b..0e5cc5e 100644 --- a/android/app/src/main/cpp/xr_swapchain.c +++ b/android/app/src/main/cpp/xr_swapchain.c @@ -149,6 +149,7 @@ bool xr_swapchain_acquire(XrInstance instance, XrSwapchainState *state) { // (android_draw_surface_as_quad()) land in the right place. glBindFramebuffer(GL_FRAMEBUFFER, state->fbos[imageIndex]); glViewport(0, 0, state->width, state->height); + return true; } diff --git a/android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java b/android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java index 97326dd..d2fa75a 100644 --- a/android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java +++ b/android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java @@ -6,8 +6,6 @@ import android.content.Context; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.util.Log; import android.view.SurfaceHolder; import androidx.core.app.ActivityCompat; diff --git a/android/engine-patches/09-android-no-window-resize.patch b/android/engine-patches/09-android-no-window-resize.patch index 9d5b6bb..a2da2f6 100644 --- a/android/engine-patches/09-android-no-window-resize.patch +++ b/android/engine-patches/09-android-no-window-resize.patch @@ -1,6 +1,16 @@ --- a/src/MacSrc/ShockBitmap.c +++ b/src/MacSrc/ShockBitmap.c -@@ -42,11 +42,26 @@ +@@ -27,6 +27,9 @@ + #include "Shock.h" + #include "ShockBitmap.h" + #include "2d.h" ++#ifdef __ANDROID__ ++#include "OpenGL.h" ++#endif + + //-------------------- + // Globals +@@ -42,11 +45,26 @@ SDL_RenderClear(renderer); @@ -27,3 +37,32 @@ SDL_RenderSetLogicalSize(renderer, width, height); +@@ -54,6 +72,28 @@ + + gScreenWide = width; + gScreenHigh = height; ++ ++#ifdef __ANDROID__ ++ // Skipping SDL_SetWindowSize() above (see the comment for why) also ++ // means no native resize event ever fires on Android when the in-game ++ // resolution changes - opengl_resize() (OpenGL.cc) only ever runs from ++ // that event (see sdl_events.c) or once at OpenGL init, and it's what ++ // (re)creates backupBuffer, the offscreen 3D-view render target, sized ++ // to the logical resolution just changed above. Without this, it stays ++ // stuck at whatever size it was at launch while the rest of the game ++ // (drawSurface/gScreenWide/gScreenHigh, just updated above) moves on to ++ // the new resolution - the actual cause of the display breaking and ++ // becoming unresponsive to further input after changing resolution ++ // in-game. Call it directly instead - after gScreenWide/gScreenHigh ++ // update above, same as the desktop resize event (delivered via ++ // SDL_PollEvent(), so always processed after this function has already ++ // returned) - using the same physical window size init_opengl() itself ++ // used (SDL_GetWindowSize()); that never changes on Android, only the ++ // logical size set above does. ++ int physWidth, physHeight; ++ SDL_GetWindowSize(window, &physWidth, &physHeight); ++ opengl_resize(physWidth, physHeight); ++#endif + } + + //------------------------------------------------------------------------------------ diff --git a/android/engine-patches/18-android-opengl-immediate-mode-attribs.patch b/android/engine-patches/18-android-opengl-immediate-mode-attribs.patch new file mode 100644 index 0000000..cc1b509 --- /dev/null +++ b/android/engine-patches/18-android-opengl-immediate-mode-attribs.patch @@ -0,0 +1,455 @@ +--- a/src/MacSrc/OpenGL.cc ++++ b/src/MacSrc/OpenGL.cc +@@ -391,7 +391,16 @@ + } + + void opengl_resize(int width, int height) { ++#ifndef __ANDROID__ ++ // Skipped on Android - see opengl_swap_and_restore()'s comment below for ++ // why re-asserting an already-current context/surface every call breaks ++ // gl4es's own "current framebuffer" bookkeeping, even though the real ++ // driver binding never actually changes. This turned out to be the ++ // actual source of the persistent per-frame GL_INVALID_OPERATION (1282) ++ // that plagued 3D rendering: this same unconditional call was present at ++ // the top of nearly every function in this file, not just here. + SDL_GL_MakeCurrent(window, context); ++#endif + + int logical_width, logical_height; + SDL_RenderGetLogicalSize(renderer, &logical_width, &logical_height); +@@ -443,7 +452,10 @@ + } + + void opengl_end_frame() { ++#ifndef __ANDROID__ ++ // See opengl_resize()'s comment above. + SDL_GL_MakeCurrent(window, context); ++#endif + + // Done rendering to the frame buffer, reset back to normal + BindFrameBuffer(nullptr); +@@ -470,7 +482,10 @@ + static bool nightsight_active() { return WareActive(player_struct.hardwarez_status[HARDWARE_GOGGLE_INFRARED]); } + + void opengl_start_frame() { ++#ifndef __ANDROID__ ++ // See opengl_resize()'s comment above. + SDL_GL_MakeCurrent(window, context); ++#endif + + // Start rendering to our frame buffer canvas + BindFrameBuffer(&backupBuffer); +@@ -500,6 +515,130 @@ + } + + #ifdef __ANDROID__ ++// 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. That means every one of this file's ++// glBegin(GL_TRIANGLE_STRIP)-based draws - not just the UI-overlay quad ++// this was first found on, but also opengl_light_tmap() (every textured ++// wall/floor polygon), opengl_bitmap() (sprites) and opengl_draw_poly() ++// (flat/gouraud polygons) via draw_vertex() - draws every vertex with ++// whatever texcoord/light was set for the *last* vertex instead of its ++// own, which is what actually produced flat-colored/untextured world ++// geometry after this patch's first revision only fixed the final ++// screen-composite quad (the one bug this was originally found on). ++// ++// android_imm_begin()/android_imm_vertex()/android_imm_end() below emulate ++// just enough of glBegin(GL_TRIANGLE_STRIP)/glVertexAttrib*()/glVertex3f() ++// immediate mode to replace every such call site on Android, by buffering ++// vertices and flushing them as one real glDrawArrays() call - the engine's ++// other rendering still uses real immediate mode for attribute 0 elsewhere ++// on desktop builds, which depends on it not having an enabled array bound, ++// so android_imm_end() always disables the arrays it enables before ++// returning. ++struct AndroidImmVertex { ++ float x, y, z; ++ float u, v; ++ float light; ++}; ++ ++// Arbitrarily-sized, but generous: the engine's polygons (room walls/ ++// floors, sprites, screen-filling quads) are all small, hand-authored ++// shapes, never anywhere near this many vertices. ++static AndroidImmVertex s_immVerts[64]; ++static int s_immVertCount; ++static GLint s_immTcAttrib; ++static GLint s_immLightAttrib; ++ ++// tcAttrib < 0 means this draw has no texcoord attribute (flat/gouraud ++// polygons via opengl_draw_poly()) - matches draw_vertex()'s own ++// `tcAttrib >= 0` check below. ++static void android_imm_begin(GLint tcAttrib, GLint lightAttrib) { ++ s_immVertCount = 0; ++ s_immTcAttrib = tcAttrib; ++ s_immLightAttrib = lightAttrib; ++} ++ ++static void android_imm_vertex(float x, float y, float z, float u, float v, float light) { ++ if (s_immVertCount >= (int)(sizeof(s_immVerts) / sizeof(s_immVerts[0]))) { ++ WARN("android_imm_vertex: dropping vertex past the %d-vertex limit", s_immVertCount); ++ return; ++ } ++ AndroidImmVertex &vert = s_immVerts[s_immVertCount++]; ++ vert.x = x; ++ vert.y = y; ++ vert.z = z; ++ vert.u = u; ++ vert.v = v; ++ vert.light = light; ++} ++ ++static void android_imm_end() { ++ if (s_immVertCount < 3) ++ return; ++ glEnableVertexAttribArray(0); ++ if (s_immTcAttrib >= 0) ++ glEnableVertexAttribArray(s_immTcAttrib); ++ glEnableVertexAttribArray(s_immLightAttrib); ++ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(AndroidImmVertex), &s_immVerts[0].x); ++ if (s_immTcAttrib >= 0) ++ glVertexAttribPointer(s_immTcAttrib, 2, GL_FLOAT, GL_FALSE, sizeof(AndroidImmVertex), ++ &s_immVerts[0].u); ++ glVertexAttribPointer(s_immLightAttrib, 1, GL_FLOAT, GL_FALSE, sizeof(AndroidImmVertex), ++ &s_immVerts[0].light); ++ glDrawArrays(GL_TRIANGLE_STRIP, 0, s_immVertCount); ++ glDisableVertexAttribArray(0); ++ if (s_immTcAttrib >= 0) ++ glDisableVertexAttribArray(s_immTcAttrib); ++ glDisableVertexAttribArray(s_immLightAttrib); ++} ++ ++// Same underlying bug as android_imm_*() above, for opengl_draw_star(): ++// opengl_begin_stars()/opengl_draw_star()/opengl_end_stars() draw up to ++// NUM_STARS (rendtool.c) individual GL_POINTS between one begin/end pair, ++// each with its own per-star brightness via glVertexAttrib1f(lightAttrib, ++// ...)/glVertex3f() immediate mode - under gl4es that made every star on ++// screen render at whatever brightness was set for the *last* one. Shaped ++// differently enough from android_imm_*() above (GL_POINTS instead of ++// GL_TRIANGLE_STRIP, far more vertices, no texcoord) to warrant its own ++// small buffer rather than overloading that one. ++static AndroidImmVertex s_starVerts[1000]; // NUM_STARS, GameSrc/rendtool.c ++static int s_starVertCount; ++static GLint s_starLightAttrib; ++ ++static void android_stars_begin(GLint lightAttrib) { ++ s_starVertCount = 0; ++ s_starLightAttrib = lightAttrib; ++} ++ ++static void android_stars_vertex(float x, float y, float z, float light) { ++ if (s_starVertCount >= (int)(sizeof(s_starVerts) / sizeof(s_starVerts[0]))) { ++ WARN("android_stars_vertex: dropping star past the %d-star limit", s_starVertCount); ++ return; ++ } ++ AndroidImmVertex &vert = s_starVerts[s_starVertCount++]; ++ vert.x = x; ++ vert.y = y; ++ vert.z = z; ++ vert.light = light; ++} ++ ++static void android_stars_end() { ++ if (s_starVertCount == 0) ++ return; ++ glEnableVertexAttribArray(0); ++ glEnableVertexAttribArray(s_starLightAttrib); ++ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(AndroidImmVertex), &s_starVerts[0].x); ++ glVertexAttribPointer(s_starLightAttrib, 1, GL_FLOAT, GL_FALSE, sizeof(AndroidImmVertex), ++ &s_starVerts[0].light); ++ glDrawArrays(GL_POINTS, 0, s_starVertCount); ++ glDisableVertexAttribArray(0); ++ glDisableVertexAttribArray(s_starLightAttrib); ++} ++ + // 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 +@@ -537,42 +676,15 @@ + + 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); ++ android_imm_begin(tcAttrib, lightAttrib); ++ android_imm_vertex(1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 1.0f); ++ android_imm_vertex(1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f); ++ android_imm_vertex(-1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f); ++ android_imm_vertex(-1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f); ++ android_imm_end(); + // glFinish(), not glFlush(): xr_swapchain_release() (called by our + // caller right after this returns) hands the image to the OpenXR + // runtime for cross-process consumption, which requires the GPU write +@@ -642,6 +754,16 @@ + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + ++#ifdef __ANDROID__ ++ // Real per-vertex arrays, not glBegin/glVertexAttrib/glVertex3f ++ // immediate mode - see android_imm_begin()'s comment above for why. ++ android_imm_begin(tcAttrib, lightAttrib); ++ android_imm_vertex(1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f); ++ android_imm_vertex(1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f); ++ android_imm_vertex(-1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f); ++ android_imm_vertex(-1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f); ++ android_imm_end(); ++#else + glBegin(GL_TRIANGLE_STRIP); + glVertexAttrib2f(tcAttrib, 1.0f, 0.0f); + glVertexAttrib1f(lightAttrib, 1.0f); +@@ -656,6 +778,7 @@ + glVertexAttrib1f(lightAttrib, 1.0f); + glVertex3f(-1.0f, 1.0f, 0.0f); + glEnd(); ++#endif + + // Finish drawing the 3d view + glFlush(); +@@ -709,7 +832,10 @@ + render_width = width; + render_height = height; + ++#ifndef __ANDROID__ ++ // See opengl_resize()'s comment above. + SDL_GL_MakeCurrent(window, context); ++#endif + + int lw, lh; + SDL_RenderGetLogicalSize(renderer, &lw, &lh); +@@ -729,7 +855,10 @@ + } + + static bool opengl_cache_texture(CachedTexture toCache, grs_bitmap *bm) { ++#ifndef __ANDROID__ ++ // See opengl_resize()'s comment above. + SDL_GL_MakeCurrent(window, context); ++#endif + + // GL_UNPACK_ROW_LENGTH must describe the buffer actually passed to + // glTexImage2D() below (toCache.converted->pixels, a separately +@@ -771,7 +900,10 @@ + + DEBUG("Clearing OpenGL texture cache."); + ++#ifndef __ANDROID__ ++ // See opengl_resize()'s comment above. + SDL_GL_MakeCurrent(window, context); ++#endif + + for (auto iter = texturesByBitsPtr.begin(); iter != texturesByBitsPtr.end();) { + // don't free locked surfaces +@@ -896,10 +1028,16 @@ + light = 1.0f - (clut - grd_screen->ltab) / 4096.0f; + } + ++#ifdef __ANDROID__ ++ (void)tcAttrib; // captured by android_imm_begin() instead - see its comment above ++ android_imm_vertex(vertex.x / 65536.0f, vertex.y / 65536.0f, -vertex.z / 65536.0f, ++ vertex.uv.u / 256.0f, vertex.uv.v / 256.0f, light); ++#else + if (tcAttrib >= 0) + glVertexAttrib2f(tcAttrib, vertex.uv.u / 256.0, vertex.uv.v / 256.0); + glVertexAttrib1f(lightAttrib, light); + glVertex3f(vertex.x / 65536.0f, vertex.y / 65536.0f, -vertex.z / 65536.0f); ++#endif + } + + int opengl_draw_tmap(int n, g3s_phandle *vp, grs_bitmap *bm) { return opengl_light_tmap(n, vp, bm); } +@@ -910,7 +1048,10 @@ + return CLIP_ALL; + } + ++#ifndef __ANDROID__ ++ // See opengl_resize()'s comment above. + SDL_GL_MakeCurrent(window, context); ++#endif + set_blend_mode(bm->flags & BMF_TRANS); + + glUseProgram(textureShaderProgram.shaderProgram); +@@ -928,13 +1069,21 @@ + glUniformMatrix4fv(textureShaderProgram.uniView, 1, false, ViewMatrix); + glUniformMatrix4fv(textureShaderProgram.uniProj, 1, false, ProjectionMatrix); + ++#ifdef __ANDROID__ ++ android_imm_begin(tcAttrib, lightAttrib); ++#else + glBegin(GL_TRIANGLE_STRIP); ++#endif + draw_vertex(*(vp[1]), tcAttrib, lightAttrib); + draw_vertex(*(vp[0]), tcAttrib, lightAttrib); + draw_vertex(*(vp[2]), tcAttrib, lightAttrib); + if (n > 3) + draw_vertex(*(vp[3]), tcAttrib, lightAttrib); ++#ifdef __ANDROID__ ++ android_imm_end(); ++#else + glEnd(); ++#endif + + return CLIP_NONE; + } +@@ -949,7 +1098,10 @@ + return CLIP_ALL; + } + ++#ifndef __ANDROID__ ++ // See opengl_resize()'s comment above. + SDL_GL_MakeCurrent(window, context); ++#endif + set_blend_mode(bm->flags & BMF_TRANS); + + glUseProgram(textureShaderProgram.shaderProgram); +@@ -976,6 +1128,14 @@ + + glUniform1i(textureShaderProgram.uniMutant, (bm->type == BMT_TLUC8) || (bm->flags == BMF_TLUC8)); + ++#ifdef __ANDROID__ ++ android_imm_begin(tcAttrib, lightAttrib); ++ android_imm_vertex(convx(vpl[1]->x), convy(vpl[1]->y), 0.0f, 1.0f, 0.0f, light); ++ android_imm_vertex(convx(vpl[0]->x), convy(vpl[0]->y), 0.0f, 0.0f, 0.0f, light); ++ android_imm_vertex(convx(vpl[2]->x), convy(vpl[2]->y), 0.0f, 1.0f, 1.0f, light); ++ android_imm_vertex(convx(vpl[3]->x), convy(vpl[3]->y), 0.0f, 0.0f, 1.0f, light); ++ android_imm_end(); ++#else + glBegin(GL_TRIANGLE_STRIP); + glVertexAttrib2f(tcAttrib, 1.0f, 0.0f); + glVertexAttrib1f(lightAttrib, light); +@@ -990,6 +1150,7 @@ + glVertexAttrib1f(lightAttrib, light); + glVertex3f(convx(vpl[3]->x), convy(vpl[3]->y), 0.0f); + glEnd(); ++#endif + + glUniform1i(textureShaderProgram.uniMutant, false); + +@@ -1007,7 +1168,10 @@ + return CLIP_ALL; + } + ++#ifndef __ANDROID__ ++ // See opengl_resize()'s comment above. + SDL_GL_MakeCurrent(window, context); ++#endif + + glUseProgram(colorShaderProgram.shaderProgram); + glUniformMatrix4fv(colorShaderProgram.uniView, 1, false, ViewMatrix); +@@ -1064,7 +1228,11 @@ + GLint lightAttrib = colorShaderProgram.lightAttrib; + + long a = 0, b = n_verts - 1; ++#ifdef __ANDROID__ ++ android_imm_begin(-1, lightAttrib); ++#else + glBegin(GL_TRIANGLE_STRIP); ++#endif + while (true) { + draw_vertex(*(p[a]), -1, lightAttrib); + if (a++ == b) +@@ -1073,7 +1241,11 @@ + if (b-- == a) + break; + } ++#ifdef __ANDROID__ ++ android_imm_end(); ++#else + glEnd(); ++#endif + + return CLIP_NONE; + } +@@ -1085,7 +1257,10 @@ + } + + void opengl_begin_stars() { ++#ifndef __ANDROID__ ++ // See opengl_resize()'s comment above. + SDL_GL_MakeCurrent(window, context); ++#endif + + // Bind the star shader before setting the point size: on Android, + // questshock_glPointSize() (see the __ANDROID__ block above) needs +@@ -1108,11 +1283,19 @@ + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + ++#ifdef __ANDROID__ ++ android_stars_begin(starShaderProgram.lightAttrib); ++#else + glBegin(GL_POINTS); ++#endif + } + + void opengl_end_stars() { ++#ifdef __ANDROID__ ++ android_stars_end(); ++#else + glEnd(); ++#endif + + // Turn off the stencil test and blending + opengl_set_stencil(0x00); +@@ -1120,9 +1303,11 @@ + } + + int opengl_draw_star(fix star_x, fix star_y, int c, bool anti_alias) { ++#ifndef __ANDROID__ ++ // See opengl_resize()'s comment above. + SDL_GL_MakeCurrent(window, context); +- + GLint lightAttrib = starShaderProgram.lightAttrib; ++#endif + + float x = fix_float(star_x); + float y = fix_float(star_y); +@@ -1142,8 +1327,12 @@ + int color = (std_color_base + std_color_range - 1 - c); + color = (255 * color) / (std_color_range + 1); + ++#ifdef __ANDROID__ ++ android_stars_vertex(x, y, -0.25f, color / 255.0f); ++#else + glVertexAttrib1f(lightAttrib, color / 255.0f); + glVertex3f(x, y, -0.25f); ++#endif + + return CLIP_NONE; + } diff --git a/android/engine-patches/19-android-fix-hackcam-opengl-frame.patch b/android/engine-patches/19-android-fix-hackcam-opengl-frame.patch new file mode 100644 index 0000000..26cc4cc --- /dev/null +++ b/android/engine-patches/19-android-fix-hackcam-opengl-frame.patch @@ -0,0 +1,45 @@ +--- a/src/GameSrc/frsetup.c ++++ b/src/GameSrc/frsetup.c +@@ -635,7 +635,27 @@ + uchar old_cam_type; + int detail; + ++#ifdef __ANDROID__ ++ // should_opengl_swap() (unlike use_opengl(), see its own ++ // FR_PICKUPM_MASK|FR_HACKCAM_MASK exclusion just below and the ++ // matching one used for the STEREO_SUPPORT path in fr_send_view() ++ // below) doesn't know which _fr context is currently being prepared - ++ // it's a frame-level check, also used by Shock.c's SDLDraw() to pick ++ // the frame's overall compositing path. render_hack_cameras() (see ++ // render.c) calls fr_rend() - and so this function - a second time per ++ // real frame for each active hack-camera monitor, rendering into a ++ // private software (FR_DOUBLEB_MASK) canvas rather than the actual ++ // GL-backed screen. Without this exclusion, opengl_start_frame() runs ++ // again mid-frame for that render even though its own polygon draw ++ // calls correctly stay on the software path (use_opengl() already ++ // excludes them) - re-entering the OpenXR swapchain/framebuffer setup ++ // a second time before the first call's frame has ended. Not known to ++ // be exercised on desktop (no OpenXR swapchain there), so this fix is ++ // kept Android-only rather than changing should_opengl_swap() itself. ++ if (should_opengl_swap() && !(_fr_curflags & (FR_PICKUPM_MASK | FR_HACKCAM_MASK))) { ++#else + if(should_opengl_swap()) { ++#endif + opengl_start_frame(); + } + +@@ -823,7 +843,14 @@ + + g3_end_frame(); + ++#ifdef __ANDROID__ ++ // Mirrors the same fix in fr_start_view() above - see its comment. ++ // opengl_start_frame()/opengl_end_frame() must stay paired per real ++ // GL frame, so this exclusion has to match that one exactly. ++ if (should_opengl_swap() && !(_fr_curflags & (FR_PICKUPM_MASK | FR_HACKCAM_MASK))) { ++#else + if(should_opengl_swap()) { ++#endif + opengl_end_frame(); + } + diff --git a/android/engine-patches/20-android-no-default-framebuffer.patch b/android/engine-patches/20-android-no-default-framebuffer.patch new file mode 100644 index 0000000..8bff8b6 --- /dev/null +++ b/android/engine-patches/20-android-no-default-framebuffer.patch @@ -0,0 +1,34 @@ +--- a/src/MacSrc/OpenGL.cc ++++ b/src/MacSrc/OpenGL.cc +@@ -455,10 +455,30 @@ + #ifndef __ANDROID__ + // See opengl_resize()'s comment above. + SDL_GL_MakeCurrent(window, context); +-#endif + + // Done rendering to the frame buffer, reset back to normal + BindFrameBuffer(nullptr); ++#else ++ // BindFrameBuffer(nullptr) (glBindFramebuffer/glBindRenderbuffer to id ++ // 0) means "the real window surface" - on desktop that's SDL's actual ++ // GL window, but this app's real presentation target is never that ++ // SDLSurface's own EGL surface at all - it's the OpenXR swapchain's ++ // per-image FBOs, set up separately in xr_swapchain.c. Whatever ++ // framebuffer/renderbuffer 0 actually resolves to for gl4es in this ++ // configuration, binding it here reliably left gl4es reporting a ++ // persistent GL_INVALID_OPERATION (1282) - confirmed by bisecting ++ // through this whole render path with per-call checkpoints, all clean ++ // right up until "after opengl_end_frame", the one immediately after ++ // this exact call. ++ // ++ // The bind is also functionally dead on Android regardless of that: ++ // by the time anything next draws, Shock.c's SDLDraw() has already ++ // run xr_frame_begin() -> xr_swapchain_acquire(), which rebinds to ++ // that frame's real swapchain FBO before opengl_swap_and_restore() ++ // (the only thing that draws before the next opengl_start_frame()'s ++ // own BindFrameBuffer(&backupBuffer)) runs - so skipping it changes ++ // nothing about what actually gets rendered where. ++#endif + palette_dirty = false; + } + diff --git a/res/screenshots/gallery_010.jpg b/res/screenshots/gallery_010.jpg new file mode 100644 index 0000000..1f2eaf0 Binary files /dev/null and b/res/screenshots/gallery_010.jpg differ diff --git a/res/screenshots/gallery_011.jpg b/res/screenshots/gallery_011.jpg new file mode 100644 index 0000000..6762e7a Binary files /dev/null and b/res/screenshots/gallery_011.jpg differ