- 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.
8.4 KiB
Questshock
An open source project to play the classic 1994 System Shock on a VR headset, built on top of Shockolate, a cross-platform port of the original game.
Design principles
- Standalone-headset-only. The game must run entirely on the headset itself - no PC required, whether tethered or streamed (not PCVR). Any feature or dependency that assumes a host PC is out of scope.
- OpenXR-first. Favor OpenXR-standard APIs over vendor-specific ones (e.g. Meta's Oculus Mobile SDK) wherever there's a choice, so the port isn't locked to Meta Quest and can work across other standalone, Android-based OpenXR headsets (e.g. Pico) too.
Layout
engine/- a vendored snapshot of the Shockolate engine source. Built via Docker; see below.build-image/- the Dockerfile (and supporting scripts) for the engine build environment. Every third-party dependency the engine needs to compile (SDL2, SDL2_mixer, the fluidsynth-lite MIDI synth, a MIDI soundfont) is fetched and built once into this image - compilingengine/itself needs no network access.build-image.sh/run-image.sh/upload-image.sh- build the image, run it to compile the engine, and push it to a registry, respectively.res/assets/- where you place your own purchased copy of the game (see below);res/assets/extract_assets.shextracts it intoss_ee/.res/run.sh- the launcher script, copied intodist/on build.Makefile- assemblesdist/, a self-contained runnable copy of the game, out of the compiled engine and the extracted assets. Also buildsdist/shockolate-<version>-linux-<arch>.tar.gz, a redistributable package that omits the proprietary game assets (make package), anddist/questshock-<version>-android-arm64.apkfor the Quest (make apk).android/- the Quest app (JavaSDLActivityglue, Gradle project).android/engine-patches/holds the one small patch needed to buildengine/as an Android shared library instead of a desktop executable- applied to a scratch copy at build time;
engine/itself is never modified.
- applied to a scratch copy at build time;
Building
# 1. Build the engine build-image (once, or after build-image/ changes)
./build-image.sh
# 2. Get your own copy of the game data (see "Game assets" below), then:
res/assets/extract_assets.sh
# 3. Compile the engine and assemble dist/
make dist
# 4. Play
dist/run.sh
make dist always recompiles the engine from the current engine/
source (via run-image.sh), so a fresh build-image plus a re-run of
make dist is all that's needed after pulling engine changes.
Game assets
System Shock's game data is not included in this repository and cannot
be redistributed - you need to own a copy. Buy System Shock: Enhanced
Edition on gog.com, download the offline
installer (a .exe), and drop it into res/assets/. Then run
res/assets/extract_assets.sh, which pulls the classic game's data and
sound files out of the installer (it's an Inno Setup package; the actual
game data lives inside it in a zip-format sshock.kpf) into
res/assets/ss_ee/. That script needs innoextract and unzip; if
they aren't installed locally it falls back to running the extraction in
a throwaway Docker container instead.
If you already have the game installed instead (on Windows, or via
Wine/Proton on Linux), you don't need the installer or the script at
all - just copy its res/data/ and res/sound/ folders directly into
res/assets/ss_ee/data/ and res/assets/ss_ee/sound/. That's exactly
the same layout extract_assets.sh produces, so make dist/make package/make apk pick it up the same way either way.
Packaging
make package builds dist/shockolate-<version>-linux-<arch>.tar.gz: the
compiled binary, its runtime libraries, shaders, a default MIDI
soundfont, license information, and res/GET_ASSETS.txt in place of the
actual game data (which the tarball never includes). Version comes from
the current git tag (push a vX.Y.Z tag to drive a release); without one
it builds an untagged 0.0.0-dev+<sha> placeholder. make apk (below)
is versioned identically, via the same build-image/version.sh.
A Gitea Actions workflow (.gitea/workflows/build.yml) builds this
package on every push, using the build-image as its container (so no
extra setup is needed in CI beyond the image itself), and publishes the
resulting tarball to dl.ladkau.de.
Playing on Meta Quest
make apk builds dist/questshock-<version>-android-arm64.apk - an
immersive OpenXR app (see android/app/src/main/cpp/xr_session.c): the
game's own rendering is unchanged (still a flat, 2D render, no stereo 3D
scene), but instead of running as a Home-hosted 2D panel it's now shown as
a single head-tracked quad floating in front of the viewer in an otherwise
empty space. There's no controller-driven interaction yet (that's ongoing
work - a laser-pointer-driven menu and on-screen keyboard); for now, play
with a Bluetooth mouse/keyboard connected to the headset same as before.
- Install the APK with SideQuest (or
adb install). - Launch it once. It'll ask for storage permission, then create
/sdcard/questshock/and extract its own bundled files (shaders, a default MIDI soundfont) there -res/data/andres/sound/are deliberately left missing, since that's the proprietary game data. - With the Quest connected to a PC, use SideQuest's file browser (or any
MTP file manager) to copy your own
res/data/andres/sound/(see/sdcard/questshock/GET_ASSETS_QUEST.txt, extracted in step 2, for exactly what's needed and where it comes from) into/sdcard/questshock/res/. - Launch it again.
Building natively in Android Studio
make apk always compiles the engine and links the APK inside the
Docker build-image - convenient for CI/CLI builds, but Android Studio
can't attach a debugger to (or get IDE code-intelligence for) a build
that happens inside a container it isn't running.
To have Android Studio compile and deploy android/ itself instead:
./run-image.sh bash build-image/prepare-android-project.sh --host-paths
This stages everything make apk normally stages (a scratch, patched
copy of engine/; the Android SDL2/SDL2_mixer/fluidsynth-lite/gl4es/openxr
prebuilts; bundled assets) - the same as build-apk.sh's own prep step -
except it writes android/engine.properties with paths that resolve on
your host filesystem, and additionally exports the prebuilt libraries
(otherwise only present inside the image, at /opt/prebuilt/android) to
build/android-prebuilt/ so they're visible outside the container too.
Re-run it whenever engine/, android/engine-patches/, or the
prebuilt-library versions in build-image/Dockerfile change.
Then open android/ as a project in Android Studio (JDK 17, NDK
26.1.10909125, and SDK Platform/Build-Tools 34 installed, matching
build-image/Dockerfile and android/app/build.gradle) and build/run
normally - no Docker involved for this part.
License
The original tooling in this repository (the Docker build image, build
scripts, Makefile, asset extraction script, and the Quest app in
android/ - aside from org/libsdl/app/, see below) is licensed under
the MIT License.
android/app/src/main/java/org/libsdl/app/ is copied from
SDL2's own android-project template and is
zlib-licensed, same as SDL2 itself.
The Quest build also bundles 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.
It also bundles the Khronos Group's own OpenXR-SDK loader (lib/arm64-v8a/libopenxr_loader.so,
prebuilt unmodified into the build image), which is Apache 2.0-licensed.
The vendored engine snapshot in engine/ is
Shockolate, which is licensed
under the GNU GPLv3 (see engine/LICENSE) - it is included unchanged
and is not relicensed by this project's MIT license. Any build or
distribution of the compiled engine must comply with the GPLv3.
The game assets extracted into res/assets/ss_ee/ are proprietary,
copyrighted game data owned by their respective rightsholders - they are
never committed to this repository (see .gitignore) and must be
supplied by each user from their own legitimate purchase.
