Fix persistent GL_INVALID_OPERATION spam and resolution-change bugs on Android
build / build (push) Successful in 2m44s

- opengl_end_frame() bound framebuffer 0 on Android, which has no valid
  target there (real presentation goes through the OpenXR swapchain's own
  FBOs) - gl4es reported this every frame. Skip it there.
- render_hack_cameras() re-entered opengl_start_frame()/opengl_end_frame()
  for camera-monitor renders that never touch the GL screen; excluded them
  to match use_opengl()'s existing behavior. Also drains a stray error left
  by xrPollEvent().
- ChangeScreenSize() never called opengl_resize() on Android, freezing the
  3D render target at launch size after an in-game resolution change; and
  xr_get_game_resolution() used the swapchain's fixed size instead of the
  current screen resolution, misplacing the laser-pointer cursor after such
  a change.
- Removes all temporary bisection diagnostics added while tracking down the
  GL_INVALID_OPERATION source.
This commit is contained in:
ml
2026-09-04 16:34:52 +02:00
parent 504ed3e1cb
commit e0a2e1f4bf
11 changed files with 835 additions and 10 deletions
@@ -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
}
//------------------------------------------------------------------------------------
@@ -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;
}
@@ -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();
}
@@ -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;
}