The Android/Quest CMakeLists.txt patch previously reimplemented desktop
build / build (push) Successful in 1m8s

immediate-mode GL (glBegin/glVertex3f/glEnd and friends) from scratch on
top of GLES, since GLES has none of it. That code compiled but was never
actually exercised - no on-screen verification was possible without real
Quest hardware - making it the biggest unverified risk in the port.

Replace it with GL4ES (MIT-licensed, prebuilt into the build image),
a mature desktop-GL-on-GLES translation library used by other
Quest/Android game ports for exactly this problem. This cuts
04-android-opengl-es-render.patch by half, deleting the hand-written
vertex-accumulation emulation entirely; alpha test, point sprites, and
point size stay on their existing GLES-native fixes rather than trusting
GL4ES's shakier custom-shader interop for those.

Bump build-image/VERSION to 3 - a fresh image with GL4ES cross-compiled
for arm64-v8a was built and make apk verified successfully against it.
This commit is contained in:
ml
2026-07-20 12:52:34 +02:00
parent 9e2c9ffbe6
commit 66f02a0c61
8 changed files with 75 additions and 3258 deletions
File diff suppressed because it is too large Load Diff
+3
View File
@@ -7,3 +7,6 @@ under the GNU GPLv3 - see LICENSE.Shockolate. It is included unchanged
and is not relicensed by this project's MIT license. and is not relicensed by this project's MIT license.
Game assets are not included - see res/GET_ASSETS.txt. Game assets are not included - see res/GET_ASSETS.txt.
The Quest build bundles GL4ES (https://github.com/ptitSeb/gl4es),
prebuilt unmodified as lib/arm64-v8a/libGL.so, which is MIT-licensed.
+5
View File
@@ -110,6 +110,11 @@ the [MIT License](LICENSE).
[SDL2](https://www.libsdl.org/)'s own android-project template and is [SDL2](https://www.libsdl.org/)'s own android-project template and is
zlib-licensed, same as SDL2 itself. zlib-licensed, same as SDL2 itself.
The Quest build also bundles [GL4ES](https://github.com/ptitSeb/gl4es)
(`lib/arm64-v8a/libGL.so` in the APK, prebuilt unmodified into the build
image), which translates the engine's desktop-style OpenGL calls into
GLES/EGL and is MIT-licensed.
The vendored engine snapshot in `engine/` is The vendored engine snapshot in `engine/` is
[Shockolate](https://github.com/Interrupt/systemshock), which is licensed [Shockolate](https://github.com/Interrupt/systemshock), which is licensed
under the **GNU GPLv3** (see `engine/LICENSE`) - it is included unchanged under the **GNU GPLv3** (see `engine/LICENSE`) - it is included unchanged
@@ -1,17 +1,19 @@
--- a/CMakeLists.txt --- a/CMakeLists.txt
+++ b/CMakeLists.txt +++ b/CMakeLists.txt
@@ -46,9 +46,15 @@ @@ -46,9 +46,17 @@
add_compile_options(-fsigned-char -fno-strict-aliasing) add_compile_options(-fsigned-char -fno-strict-aliasing)
-# Find OpenGL -# Find OpenGL
+# Find OpenGL. Android has no desktop GL/GLX for CMake's FindOpenGL module +# Find OpenGL. Android has no desktop GL/GLX for CMake's FindOpenGL module
+# to find - just link the NDK's own GLESv2/EGL libraries directly. +# to find - link the prebuilt GL4ES instead (translates this file's
+# desktop-style GL calls into GLES/EGL; it links GLESv2/EGL itself, so
+# the engine doesn't need to link them directly).
if(ENABLE_OPENGL) if(ENABLE_OPENGL)
- find_package(OpenGL REQUIRED) - find_package(OpenGL REQUIRED)
+ if(ANDROID) + if(ANDROID)
+ set(OPENGL_INCLUDE_DIRS "") + set(OPENGL_INCLUDE_DIRS /opt/prebuilt/android/gl4es/include)
+ set(OPENGL_LIBRARIES GLESv2 EGL) + set(OPENGL_LIBRARIES /opt/prebuilt/android/gl4es/lib/libGL.so)
+ else() + else()
+ find_package(OpenGL REQUIRED) + find_package(OpenGL REQUIRED)
+ endif() + endif()
@@ -1,153 +1,55 @@
--- a/src/MacSrc/OpenGL.cc --- a/src/MacSrc/OpenGL.cc
+++ b/src/MacSrc/OpenGL.cc +++ b/src/MacSrc/OpenGL.cc
@@ -7,6 +7,11 @@ @@ -20,6 +20,24 @@
#define GLEW_STATIC 1
#include <SDL.h>
#include <GL/glew.h>
+#elif defined(__ANDROID__)
+// GLES 3.2 (see Shock.c's context creation) - none of the desktop GL
+// headers below exist on Android; EGL only ever gives out GLES contexts.
+#include <SDL.h>
+#include <GLES3/gl32.h>
#else
#define GL_GLEXT_PROTOTYPES
#ifdef __APPLE__
@@ -20,6 +25,112 @@
#include <SDL_opengl.h> #include <SDL_opengl.h>
#endif #endif
+#ifdef __ANDROID__ +#ifdef __ANDROID__
+// GLES has no immediate-mode rendering (glBegin/glVertex3f/glEnd, used +// GL4ES's glPointSize()/custom-shader interop goes through the same kind
+// throughout this file below) at all. These macros transparently replace +// of shader-source rewrite as its alpha-test emulation (see
+// it with an equivalent that accumulates vertex data into arrays and +// init_opengl() below) - avoid relying on it. GLES point size instead
+// issues one glDrawArrays call at glEnd() time - every actual call site +// comes from a "pointSize" uniform, written to gl_PointSize in
+// below (all unchanged) looks identical on both platforms. "position", +// android/gles-shaders/main.vert. opengl_begin_stars() (the only
+// bound to attribute location 0 in CreateShader() below, replaces +// glPointSize() call site) now binds the star shader first, so
+// gl_Vertex (a desktop-only builtin android/gles-shaders/main.vert can't +// GL_CURRENT_PROGRAM here is always correct.
+// use either).
+#define glBegin questshock_glBegin
+#define glVertex3f questshock_glVertex3f
+#define glEnd questshock_glEnd
+#define glVertexAttrib1f questshock_glVertexAttrib1f
+#define glVertexAttrib2f questshock_glVertexAttrib2f
+#define glPointSize questshock_glPointSize +#define glPointSize questshock_glPointSize
+ +static void questshock_glPointSize(GLfloat size) {
+// Generous enough for both the largest opengl_draw_poly() polygon and a
+// full frame's worth of opengl_draw_star() points accumulated between one
+// opengl_begin_stars()/opengl_end_stars() pair.
+static const int QUESTSHOCK_MAX_IM_VERTS = 4096;
+static float questshock_im_pos[QUESTSHOCK_MAX_IM_VERTS * 3];
+static float questshock_im_tc[QUESTSHOCK_MAX_IM_VERTS * 2];
+static float questshock_im_light[QUESTSHOCK_MAX_IM_VERTS];
+static int questshock_im_count;
+static GLenum questshock_im_mode;
+static float questshock_im_cur_tc[2];
+static float questshock_im_cur_light;
+static float questshock_point_size = 1.0f;
+
+// GLES has no glPointSize() at all - point size is instead read from
+// gl_PointSize, set by a "pointSize" uniform in
+// android/gles-shaders/main.vert. Stashed here and applied in
+// questshock_glEnd() below, since opengl_begin_stars() calls this before
+// glUseProgram(starShaderProgram...) - the star shader isn't the current
+// program yet at this point.
+static void questshock_glPointSize(GLfloat size) { questshock_point_size = size; }
+
+static void questshock_glBegin(GLenum mode) {
+ questshock_im_mode = mode;
+ questshock_im_count = 0;
+ questshock_im_cur_tc[0] = questshock_im_cur_tc[1] = 0.0f;
+ questshock_im_cur_light = 0.0f;
+}
+
+static void questshock_glVertexAttrib1f(GLuint index, GLfloat v) {
+ // Only ever called for "light" at these call sites.
+ (void)index;
+ questshock_im_cur_light = v;
+}
+
+static void questshock_glVertexAttrib2f(GLuint index, GLfloat a, GLfloat b) {
+ // Only ever called for "texcoords" at these call sites.
+ (void)index;
+ questshock_im_cur_tc[0] = a;
+ questshock_im_cur_tc[1] = b;
+}
+
+static void questshock_glVertex3f(GLfloat x, GLfloat y, GLfloat z) {
+ if (questshock_im_count >= QUESTSHOCK_MAX_IM_VERTS)
+ return;
+ questshock_im_pos[questshock_im_count * 3 + 0] = x;
+ questshock_im_pos[questshock_im_count * 3 + 1] = y;
+ questshock_im_pos[questshock_im_count * 3 + 2] = z;
+ questshock_im_tc[questshock_im_count * 2 + 0] = questshock_im_cur_tc[0];
+ questshock_im_tc[questshock_im_count * 2 + 1] = questshock_im_cur_tc[1];
+ questshock_im_light[questshock_im_count] = questshock_im_cur_light;
+ questshock_im_count++;
+}
+
+static void questshock_glEnd() {
+ GLint program = 0; + GLint program = 0;
+ glGetIntegerv(GL_CURRENT_PROGRAM, &program); + glGetIntegerv(GL_CURRENT_PROGRAM, &program);
+ GLint posAttrib = glGetAttribLocation(program, "position"); + GLint loc = glGetUniformLocation(program, "pointSize");
+ GLint tcAttrib = glGetAttribLocation(program, "texcoords"); + if (loc >= 0)
+ GLint lightAttrib = glGetAttribLocation(program, "light"); + glUniform1f(loc, size);
+
+ if (questshock_im_mode == GL_POINTS) {
+ GLint pointSizeUniform = glGetUniformLocation(program, "pointSize");
+ if (pointSizeUniform >= 0)
+ glUniform1f(pointSizeUniform, questshock_point_size);
+ }
+
+ if (posAttrib >= 0) {
+ glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 0, questshock_im_pos);
+ glEnableVertexAttribArray(posAttrib);
+ }
+ if (tcAttrib >= 0) {
+ glVertexAttribPointer(tcAttrib, 2, GL_FLOAT, GL_FALSE, 0, questshock_im_tc);
+ glEnableVertexAttribArray(tcAttrib);
+ }
+ if (lightAttrib >= 0) {
+ glVertexAttribPointer(lightAttrib, 1, GL_FLOAT, GL_FALSE, 0, questshock_im_light);
+ glEnableVertexAttribArray(lightAttrib);
+ }
+
+ glDrawArrays(questshock_im_mode, 0, questshock_im_count);
+
+ if (posAttrib >= 0)
+ glDisableVertexAttribArray(posAttrib);
+ if (tcAttrib >= 0)
+ glDisableVertexAttribArray(tcAttrib);
+ if (lightAttrib >= 0)
+ glDisableVertexAttribArray(lightAttrib);
+} +}
+#endif // __ANDROID__ +#endif // __ANDROID__
+ +
extern "C" { extern "C" {
#include "mainloop.h" #include "mainloop.h"
#include "map.h" #include "map.h"
@@ -229,6 +340,12 @@ @@ -229,6 +247,13 @@
GLuint shaderProgram = glCreateProgram(); GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertShader); glAttachShader(shaderProgram, vertShader);
glAttachShader(shaderProgram, fragShader); glAttachShader(shaderProgram, fragShader);
+#ifdef __ANDROID__ +#ifdef __ANDROID__
+ // Must be bound before linking. Matches questshock_glEnd()'s + // Must be bound before linking. GL4ES routes glVertex3f() (used
+ // glGetAttribLocation(program, "position") above and + // throughout this file below) to attribute location 0 of whatever
+ // shader is currently bound - matches
+ // android/gles-shaders/main.vert's "position" attribute. + // android/gles-shaders/main.vert's "position" attribute.
+ glBindAttribLocation(shaderProgram, 0, "position"); + glBindAttribLocation(shaderProgram, 0, "position");
+#endif +#endif
glLinkProgram(shaderProgram); glLinkProgram(shaderProgram);
glUseProgram(shaderProgram); glUseProgram(shaderProgram);
@@ -313,10 +430,17 @@ @@ -313,10 +338,18 @@
glEnable(GL_CULL_FACE); glEnable(GL_CULL_FACE);
glEnable(GL_BLEND); glEnable(GL_BLEND);
+#ifndef __ANDROID__ +#ifndef __ANDROID__
+ // GLES has no fixed-function pipeline: no alpha test (emulated with a + // GL4ES emulates both of these (alpha test, point sprites) for a
+ // discard in android/gles-shaders/texture.frag instead), and point + // custom shader by rewriting its source under the hood - a rougher
+ // sprites are always implicitly on for GL_POINTS rendering (via + // edge of the library. Skip relying on that: alpha test is instead
+ // gl_PointCoord in android/gles-shaders/star.frag) - no enum/enable + // a discard in android/gles-shaders/texture.frag, and point-sprite
+ // for either exists in GLES at all. + // rasterization for GL_POINTS works automatically in GLES with no
+ // enable call at all (see android/gles-shaders/star.frag).
glEnable(GL_ALPHA_TEST); glEnable(GL_ALPHA_TEST);
glEnable(GL_POINT_SPRITE); glEnable(GL_POINT_SPRITE);
- glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@@ -157,3 +59,17 @@
CreateShader("main.vert", "texture.frag", &textureShaderProgram); CreateShader("main.vert", "texture.frag", &textureShaderProgram);
CreateShader("main.vert", "color.frag", &colorShaderProgram); CreateShader("main.vert", "color.frag", &colorShaderProgram);
@@ -908,9 +941,12 @@
void opengl_begin_stars() {
SDL_GL_MakeCurrent(window, context);
+ // Bind the star shader before setting the point size: on Android,
+ // questshock_glPointSize() (see the __ANDROID__ block above) needs
+ // it to already be GL_CURRENT_PROGRAM.
+ glUseProgram(starShaderProgram.shaderProgram);
glPointSize(1.5 * (render_width / 320.0));
- glUseProgram(starShaderProgram.shaderProgram);
glUniformMatrix4fv(starShaderProgram.uniView, 1, false, IdentityMatrix);
glUniformMatrix4fv(starShaderProgram.uniProj, 1, false, IdentityMatrix);
+21
View File
@@ -46,6 +46,10 @@ ARG ANDROID_COMPILE_SDK_VERSION=34
ARG ANDROID_BUILD_TOOLS_VERSION=34.0.0 ARG ANDROID_BUILD_TOOLS_VERSION=34.0.0
ARG ANDROID_NDK_VERSION=26.1.10909125 ARG ANDROID_NDK_VERSION=26.1.10909125
ARG ANDROID_CMAKE_VERSION=3.22.1 ARG ANDROID_CMAKE_VERSION=3.22.1
# GL4ES (https://github.com/ptitSeb/gl4es) - translates the engine's
# desktop-style immediate-mode OpenGL calls into real GLES/EGL calls, so
# engine/src/MacSrc/OpenGL.cc needs no immediate-mode rewrite on Android.
ARG ANDROID_GL4ES_VERSION=1.1.6
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
@@ -225,6 +229,23 @@ RUN git clone https://github.com/EtherTyper/fluidsynth-lite.git fluidsynth-lite-
/opt/prebuilt/android/fluidsynth-lite/include/fluidsynth/version.h \ /opt/prebuilt/android/fluidsynth-lite/include/fluidsynth/version.h \
&& rm -rf fluidsynth-lite-android build-fluidsynth-android && rm -rf fluidsynth-lite-android build-fluidsynth-android
# gl4es's own CMakeLists.txt writes its output straight to
# ${CMAKE_SOURCE_DIR}/lib (the source tree, not the build dir) and gives
# the GL target a ".so.1" suffix - stage explicitly rather than
# `cmake --install` (which it doesn't support for this target anyway).
RUN git clone --branch "v${ANDROID_GL4ES_VERSION}" --depth 1 \
https://github.com/ptitSeb/gl4es.git gl4es-android \
&& rm -rf gl4es-android/.git \
&& cmake -S gl4es-android -B build-gl4es-android \
-DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_TOOLCHAIN}" \
-DANDROID_ABI="${ANDROID_ABI}" -DANDROID_PLATFORM="android-${ANDROID_PLATFORM_VERSION}" \
-DANDROID=ON -DUSE_ANDROID_LOG=ON -DSTATICLIB=OFF \
&& cmake --build build-gl4es-android -j"$(nproc)" \
&& mkdir -p /opt/prebuilt/android/gl4es/lib /opt/prebuilt/android/gl4es/include \
&& cp -a gl4es-android/lib/libGL.so.1 /opt/prebuilt/android/gl4es/lib/libGL.so \
&& cp -a gl4es-android/include/. /opt/prebuilt/android/gl4es/include/ \
&& rm -rf gl4es-android build-gl4es-android
COPY build-image/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh COPY build-image/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
COPY build-image/build-engine.sh /usr/local/bin/build-engine.sh COPY build-image/build-engine.sh /usr/local/bin/build-engine.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/build-engine.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/build-engine.sh
+1 -1
View File
@@ -1 +1 @@
2 3
+2 -1
View File
@@ -42,11 +42,12 @@ cp -a "$ANDROID_DIR/gles-shaders/." "$ASSETS_DIR/shaders/"
cp "/opt/prebuilt/soundfont/default.sf2" "$ASSETS_DIR/res/soundfont.sf2" cp "/opt/prebuilt/soundfont/default.sf2" "$ASSETS_DIR/res/soundfont.sf2"
cp "$REPO_ROOT/res/assets/GET_ASSETS_QUEST.txt" "$ASSETS_DIR/GET_ASSETS_QUEST.txt" cp "$REPO_ROOT/res/assets/GET_ASSETS_QUEST.txt" "$ASSETS_DIR/GET_ASSETS_QUEST.txt"
echo "== Staging prebuilt Android SDL2/SDL2_mixer/fluidsynth-lite .so's into jniLibs ==" echo "== Staging prebuilt Android SDL2/SDL2_mixer/fluidsynth-lite/gl4es .so's into jniLibs =="
JNI_LIBS_DIR="$ANDROID_DIR/app/src/main/jniLibs/arm64-v8a" JNI_LIBS_DIR="$ANDROID_DIR/app/src/main/jniLibs/arm64-v8a"
rm -rf "$ANDROID_DIR/app/src/main/jniLibs" rm -rf "$ANDROID_DIR/app/src/main/jniLibs"
mkdir -p "$JNI_LIBS_DIR" mkdir -p "$JNI_LIBS_DIR"
find /opt/prebuilt/android/sdl2/lib /opt/prebuilt/android/sdl2_mixer/lib /opt/prebuilt/android/fluidsynth-lite/lib \ find /opt/prebuilt/android/sdl2/lib /opt/prebuilt/android/sdl2_mixer/lib /opt/prebuilt/android/fluidsynth-lite/lib \
/opt/prebuilt/android/gl4es/lib \
-name '*.so' -exec cp -a {} "$JNI_LIBS_DIR/" \; -name '*.so' -exec cp -a {} "$JNI_LIBS_DIR/" \;
echo "== Building the APK (gradlew assembleDebug) ==" echo "== Building the APK (gradlew assembleDebug) =="