Files
questshock/android/app/build.gradle
T
ml 2300d081c3
build / build (push) Successful in 2m12s
Add OpenXR bring-up: render the game as a floating quad in an immersive Quest session
- 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.
2026-07-25 07:25:27 +02:00

164 lines
8.4 KiB
Groovy

apply plugin: 'com.android.application'
// Paths written by build-image/prepare-android-project.sh (called by
// build-apk.sh, or standalone with --host-paths to prep for a native
// build in Android Studio - see README) since they're only known at
// prep/build time, not something a checked-in build.gradle can hardcode.
// engineDir is the scratch, patched copy of engine/; prebuiltDir is the
// Android SDL2/SDL2_mixer/fluidsynth-lite/gl4es prebuilts - both point
// into this container's own paths for build-apk.sh's own gradlew call,
// or host-resolvable paths (plus a host-side export of prebuiltDir, since
// /opt/prebuilt/android only exists in the image) when prepared with
// --host-paths for Android Studio.
def engineProps = new Properties()
file("${projectDir}/../engine.properties").withInputStream { engineProps.load(it) }
def engineDir = engineProps.getProperty('engineDir')
if (engineDir == null) {
throw new GradleException("android/engine.properties is missing 'engineDir' - run via build-apk.sh or prepare-android-project.sh, not gradlew directly")
}
def prebuiltDir = engineProps.getProperty('prebuiltDir')
if (prebuiltDir == null) {
throw new GradleException("android/engine.properties is missing 'prebuiltDir' - run via build-apk.sh or prepare-android-project.sh, not gradlew directly")
}
// Set by build-apk.sh (see build-image/version.sh, shared with the
// desktop tarball's versioning) via -PquestshockVersionName/Code. Default
// here only covers a direct, unsupported `gradlew` invocation.
def questshockVersionName = project.hasProperty('questshockVersionName') ? project.property('questshockVersionName') : '0.0.0-dev'
def questshockVersionCode = project.hasProperty('questshockVersionCode') ? project.property('questshockVersionCode').toInteger() : 1
// Auto-refreshes the staged, patched engine/ copy and prebuilt libraries
// (see build-image/prepare-android-project.sh --host-paths, and the
// engineDir/prebuiltDir properties read above) so a plain Android Studio
// build/run can never silently compile against a stale scratch copy after
// engine/ or android/engine-patches/ change - previously a manual, easy to
// forget re-run. inputs/outputs are declared so Gradle skips the (Docker-
// invoking, not free) step entirely when nothing relevant actually changed,
// keeping pure-Java edit/run cycles fast.
def repoRoot = file("${projectDir}/../..")
def stageEngineTask = tasks.register("stageEngine", Exec) {
group = "build setup"
description = "Refreshes the patched engine/ scratch copy and prebuilt libraries for a native Android Studio build (build-image/prepare-android-project.sh --host-paths)."
workingDir repoRoot
commandLine "./run-image.sh", "bash", "build-image/prepare-android-project.sh", "--host-paths"
// /opt/prebuilt/android only ever exists inside the build-image
// container itself (baked in at image-build time, never on the host -
// see build-image/Dockerfile). Its presence means this build is
// build-apk.sh's own gradlew call, running INSIDE that container, which
// already staged everything itself in container mode before invoking
// gradlew - re-running this task there would try to `docker run` from
// inside a container with no docker socket, breaking make apk/CI. Only
// a genuine host-side Android Studio build (where this path is absent)
// needs this task.
onlyIf { !file('/opt/prebuilt/android').isDirectory() }
inputs.dir("${repoRoot}/engine")
inputs.dir("${projectDir}/../engine-patches")
inputs.file("${repoRoot}/build-image/Dockerfile")
outputs.dir("${repoRoot}/build/android-engine")
outputs.dir("${repoRoot}/build/android-prebuilt")
outputs.file("${projectDir}/../engine.properties")
}
// preBuild is what every variant's compile/native-build tasks already
// transitively depend on, regardless of AGP version's exact CMake task
// naming - the simplest reliable hook to run before any of them.
afterEvaluate {
tasks.named("preBuild").configure {
dependsOn stageEngineTask
}
}
android {
namespace "de.ladkau.questshock"
// compileSdk/buildToolsVersion/ndkVersion must all match what
// build-image/Dockerfile actually installs - otherwise AGP defaults to
// whatever version it itself prefers and tries to download it into the
// SDK dir at build time, which isn't writable.
compileSdk 34
buildToolsVersion "34.0.0"
ndkVersion "26.1.10909125"
defaultConfig {
applicationId "de.ladkau.questshock"
// 29, not compileSdk's 34, is deliberate - see build-image/Dockerfile's
// comment on ANDROID_PLATFORM_VERSION: paired with
// requestLegacyExternalStorage in the manifest, this keeps
// /sdcard/questshock/ a plain, unrestricted shared folder instead of
// Android 11+ scoped storage, on the Quest 2/3/3S/Pro headsets this
// targets - not just whatever's the current API level today.
minSdkVersion 24
targetSdkVersion 29
versionCode questshockVersionCode
versionName questshockVersionName
externalNativeBuild {
cmake {
// engine/CMakeLists.txt's own dependency-selection options
// (see engine/CMakeLists.txt near the top): SDL2 via
// find_package (our Android SDL2 build installs a real CMake
// config, unlike the desktop build's older bundled copy);
// SDL2_mixer and FluidSynth via the same build_ext/ BUNDLED
// convention the desktop build already uses (populated in the
// scratch engine copy by prepare-android-project.sh from
// prebuiltDir). ANDROID_PREBUILT_DIR is also read directly by
// android/engine-patches/02-android-opengl-es.patch, for gl4es.
// The CMAKE_FIND_ROOT_PATH* overrides below are needed
// because the NDK toolchain file restricts find_package/
// find_path/find_library to its own sysroot by default,
// which would otherwise miss our custom-installed SDL2 (see
// build-image/Dockerfile's SDL2_mixer build, which hit the
// exact same thing).
// -Wl,-z,max-page-size=16384: NDK 26 doesn't 16 KB-align ELF
// LOAD segments by default (only automatic in NDK 28+) - see
// build-image/Dockerfile's ANDROID_16KB_LDFLAGS, which does
// the same for the prebuilt SDL2/SDL2_mixer/fluidsynth-lite/
// gl4es .so's this links against.
// ANDROID_EXTRA_SOURCES is a semicolon-separated CMake list,
// not shell-split - AGP passes each "arguments" string
// straight through as one argv element to cmake, so the
// ';' below survives intact. ANDROID_EXTRA_INCLUDE_DIRS lets
// engine/'s own OpenGL.cc find "xr_session.h" (see
// android/engine-patches/11-android-openxr-cmake.patch,
// which adds it to include_directories()).
arguments "-DENABLE_SDL2=ON", "-DENABLE_SOUND=BUNDLED", "-DENABLE_FLUIDSYNTH=BUNDLED", \
"-DANDROID_PREBUILT_DIR=${prebuiltDir}", \
"-DANDROID_EXTRA_INCLUDE_DIRS=${projectDir}/src/main/cpp", \
"-DCMAKE_PREFIX_PATH=${prebuiltDir}/sdl2", \
"-DCMAKE_FIND_ROOT_PATH=${prebuiltDir}/sdl2", \
"-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH", \
"-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", \
"-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH", \
"-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384", \
"-DANDROID_EXTRA_SOURCES=${projectDir}/src/main/cpp/questshock_native.c;${projectDir}/src/main/cpp/xr_session.c"
abiFilters 'arm64-v8a'
}
}
}
externalNativeBuild {
cmake {
path file("${engineDir}/CMakeLists.txt")
}
}
buildTypes {
release {
minifyEnabled false
}
}
lint {
abortOnError false
}
}
dependencies {
// ActivityCompat.checkSelfPermission/requestPermissions in
// QuestShockActivity - the plain android.app.Activity APIs work the
// same way without androidx, but ActivityCompat is the standard,
// documented way to request runtime permissions safely.
implementation 'androidx.core:core:1.12.0'
}