Compare commits
28 Commits
2300d081c3
..
v0.5.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 790b4ffd21 | |||
| 135e474a14 | |||
| 295f4869c1 | |||
| f0a83df667 | |||
| 9586c58a67 | |||
| 5d7564c3b5 | |||
| 70c43240bc | |||
| d8324d409e | |||
| c24b7f15cb | |||
| e1e6a006d4 | |||
| 5fe4cdf7f8 | |||
| 369555efc7 | |||
| 360a1f7e3d | |||
| c0619a55ab | |||
| 09280dd410 | |||
| c4fd4eb3dd | |||
| e0a2e1f4bf | |||
| 504ed3e1cb | |||
| d6e438a541 | |||
| a2fb95e57b | |||
| 581af95e7b | |||
| 824b4a2506 | |||
| 920842a465 | |||
| 3be310ade6 | |||
| 2d8fb49bf5 | |||
| 1be0bebda2 | |||
| 2b52b818e8 | |||
| d2dc58391b |
@@ -0,0 +1,2 @@
|
||||
* text=auto
|
||||
*.sh text eol=lf
|
||||
@@ -1,14 +1,16 @@
|
||||
name: build
|
||||
|
||||
# Builds a versioned Linux release tarball (see `make package`) on every
|
||||
# push/PR, plus on-demand via the Gitea "Run workflow" button. Runs
|
||||
# Builds versioned Linux and Windows release packages (see `make
|
||||
# package`, which builds both - a .tar.gz and a .zip) on every push/PR,
|
||||
# plus on-demand via the Gitea "Run workflow" button. Runs
|
||||
# inside the build-image (see ../../build-image/Dockerfile, built/pushed
|
||||
# via ../../build-image.sh and ../../upload-image.sh), which bundles
|
||||
# every dependency engine/ needs to compile - no network access needed
|
||||
# at job runtime. Since the job's container already *is* the build-image
|
||||
# (QUESTSHOCK_BUILD_IMAGE=1), `make package`'s `engine` prerequisite
|
||||
# compiles directly instead of trying to docker-run it again, which
|
||||
# wouldn't work here (no nested docker).
|
||||
# every dependency engine/ needs to compile (for both platforms,
|
||||
# including the Windows cross-toolchain) - no network access needed at
|
||||
# job runtime. Since the job's container already *is* the build-image
|
||||
# (QUESTSHOCK_BUILD_IMAGE=1), `make package`'s `engine`/`engine-win`
|
||||
# prerequisites compile directly instead of trying to docker-run it
|
||||
# again, which wouldn't work here (no nested docker).
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
@@ -45,20 +47,47 @@ jobs:
|
||||
command -v cmake >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: cmake not in PATH" >&2; exit 1; }
|
||||
[ -n "${QUESTSHOCK_BUILD_IMAGE:-}" ] || { echo "PREFLIGHT FAIL: QUESTSHOCK_BUILD_IMAGE not set - not running inside the questshock build-image?" >&2; exit 1; }
|
||||
[ -d /opt/prebuilt/built_sdl ] || { echo "PREFLIGHT FAIL: /opt/prebuilt/built_sdl missing" >&2; exit 1; }
|
||||
[ -d /opt/prebuilt/android/gl4es ] || { echo "PREFLIGHT FAIL: /opt/prebuilt/android/gl4es missing - runner is using a build-image older than the Android/Quest layer" >&2; exit 1; }
|
||||
# gl4es itself is no longer prebuilt here (compiled from source at
|
||||
# APK-build time instead - see build-image/Dockerfile's gl4es
|
||||
# comment) - openxr was added to this image in the same
|
||||
# Android/Quest layer, so it's an equally good marker of that.
|
||||
[ -d /opt/prebuilt/android/openxr ] || { echo "PREFLIGHT FAIL: /opt/prebuilt/android/openxr missing - runner is using a build-image older than the Android/Quest layer" >&2; exit 1; }
|
||||
[ -d /opt/prebuilt/win/sdl2 ] || { echo "PREFLIGHT FAIL: /opt/prebuilt/win/sdl2 missing - runner is using a build-image older than the Windows cross-build layer" >&2; exit 1; }
|
||||
command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: x86_64-w64-mingw32-gcc not in PATH" >&2; exit 1; }
|
||||
command -v zip >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: zip not in PATH (needed by make package-win)" >&2; exit 1; }
|
||||
|
||||
- name: Build package
|
||||
run: make package
|
||||
|
||||
- name: Upload build artifact
|
||||
- name: Upload build artifacts
|
||||
# v4 uses the newer @actions/artifact backend, which this Gitea
|
||||
# instance's artifact storage doesn't support (GHESNotSupportedError) - v3 works.
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: shockolate
|
||||
path: dist/shockolate-*-linux-*.tar.gz
|
||||
path: |
|
||||
dist/shockolate-*-linux-*.tar.gz
|
||||
dist/shockolate-*-windows-*.zip
|
||||
|
||||
# The release key is stored base64-encoded in ANDROID_KEYSTORE_BASE64
|
||||
# (see README section 4.3). Without it the APK is a debug build.
|
||||
- name: Prepare release signing
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$KEYSTORE_BASE64" ]; then
|
||||
echo "No signing key: the APK will be a debug build"
|
||||
exit 0
|
||||
fi
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > "${RUNNER_TEMP:-/tmp}/questshock-release.jks"
|
||||
echo "QUESTSHOCK_KEYSTORE=${RUNNER_TEMP:-/tmp}/questshock-release.jks" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build Quest APK
|
||||
env:
|
||||
QUESTSHOCK_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
QUESTSHOCK_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
QUESTSHOCK_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
# QUESTSHOCK_BUILD_IMAGE is already set (see Preflight above), so
|
||||
# this runs build-apk.sh directly instead of trying to docker-run
|
||||
# the image again - same reasoning as `make package` above.
|
||||
@@ -74,13 +103,14 @@ jobs:
|
||||
path: dist/questshock-*-android-*.apk
|
||||
|
||||
- name: Publish to dl.ladkau.de
|
||||
# Uploads the tarball and APK over SFTP instead of using
|
||||
# Uploads the tarball, zip, and APK over SFTP instead of using
|
||||
# actions/upload-artifact (whose zip wrapping can't be disabled).
|
||||
# Only runs on push so PR builds don't publish.
|
||||
if: gitea.event_name == 'push'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARBALL="$(ls dist/shockolate-*-linux-*.tar.gz)"
|
||||
ZIP="$(ls dist/shockolate-*-windows-*.zip)"
|
||||
APK="$(ls dist/questshock-*-android-*.apk)"
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.DL_SFTP_KEY }}" > ~/.ssh/dl_sftp_key
|
||||
@@ -90,5 +120,6 @@ jobs:
|
||||
uploader@dl.ladkau.de <<EOF
|
||||
-mkdir files/questshock
|
||||
put $TARBALL files/questshock/$(basename "$TARBALL")
|
||||
put $ZIP files/questshock/$(basename "$ZIP")
|
||||
put $APK files/questshock/$(basename "$APK")
|
||||
EOF
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
name: release
|
||||
|
||||
# GitHub's build. The Gitea build is .gitea/workflows/build.yml.
|
||||
#
|
||||
# Pushing a tag vX.Y.Z builds the Linux and Windows packages and the Quest APK
|
||||
# inside the build-image and attaches them to a GitHub release. A manual run
|
||||
# builds the same files and keeps them as a workflow artifact without
|
||||
# releasing them.
|
||||
#
|
||||
# The build-image (build-image/Dockerfile) is published to GHCR, tagged with a
|
||||
# hash of the files that define it, and only built when that tag does not
|
||||
# exist yet.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
outputs:
|
||||
name: ${{ steps.name.outputs.name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Name the image after the files that define it
|
||||
id: name
|
||||
run: |
|
||||
set -euo pipefail
|
||||
hash="$(cat build-image/Dockerfile build-image/docker-entrypoint.sh build-image/build-engine.sh | sha256sum | cut -c1-16)"
|
||||
echo "name=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/questshock-builder:${hash}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push the image if it does not exist yet
|
||||
env:
|
||||
IMAGE: ${{ steps.name.outputs.name }}
|
||||
REGISTRY_USER: ${{ github.actor }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "$REGISTRY_PASSWORD" | docker login ghcr.io -u "$REGISTRY_USER" --password-stdin
|
||||
if docker manifest inspect "$IMAGE" >/dev/null 2>&1; then
|
||||
echo "$IMAGE already exists"
|
||||
exit 0
|
||||
fi
|
||||
# The image is large and the runner's disk is small.
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
|
||||
# The label links the package to this repository, so the build job
|
||||
# can pull it with the workflow's token.
|
||||
docker build -f build-image/Dockerfile \
|
||||
--label "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}" \
|
||||
-t "$IMAGE" .
|
||||
docker push "$IMAGE"
|
||||
|
||||
build:
|
||||
needs: image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
# The job runs inside the build-image, so `make` compiles directly instead
|
||||
# of starting the image itself (QUESTSHOCK_BUILD_IMAGE is set in it).
|
||||
container:
|
||||
image: ${{ needs.image.outputs.name }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# The version comes from `git describe --tags`.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build the Linux and Windows packages
|
||||
run: make package
|
||||
|
||||
# The release key is stored base64-encoded in ANDROID_KEYSTORE_BASE64; see
|
||||
# README section 4.3. A tag build must be signed with it.
|
||||
- name: Prepare release signing
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$KEYSTORE_BASE64" ]; then
|
||||
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
||||
echo "::error::ANDROID_KEYSTORE_BASE64 is not set: a release APK must be signed"
|
||||
exit 1
|
||||
fi
|
||||
echo "No signing key: the APK will be a debug build"
|
||||
exit 0
|
||||
fi
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > "${RUNNER_TEMP:-/tmp}/questshock-release.jks"
|
||||
echo "QUESTSHOCK_KEYSTORE=${RUNNER_TEMP:-/tmp}/questshock-release.jks" >> "$GITHUB_ENV"
|
||||
|
||||
# Needs network access for Gradle's own dependency resolution.
|
||||
- name: Build the Quest APK
|
||||
env:
|
||||
QUESTSHOCK_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
QUESTSHOCK_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
QUESTSHOCK_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: make apk
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: questshock
|
||||
path: |
|
||||
dist/shockolate-*-linux-*.tar.gz
|
||||
dist/shockolate-*-windows-*.zip
|
||||
dist/questshock-*-android-*.apk
|
||||
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: questshock
|
||||
path: dist
|
||||
|
||||
- name: Create the release, or update its files if it exists
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
|
||||
gh release upload "$GITHUB_REF_NAME" dist/* --clobber
|
||||
else
|
||||
gh release create "$GITHUB_REF_NAME" dist/* \
|
||||
--title "$GITHUB_REF_NAME" \
|
||||
--generate-notes \
|
||||
--notes "Sideload the APK onto a Meta Quest (see README section 5.1). The game data is not included: you need your own copy of System Shock: Enhanced Edition."
|
||||
fi
|
||||
@@ -3,16 +3,22 @@
|
||||
|
||||
# Game assets are not included in the repo
|
||||
/res/assets/setup_system_shock_enhanced*
|
||||
/res/assets/setup_system_shock_classic*
|
||||
/res/assets/*.zip
|
||||
/res/assets/ss_ee/
|
||||
|
||||
# Build output
|
||||
/dist/
|
||||
/dist-win/
|
||||
/build/
|
||||
|
||||
# Engine build artifacts (engine/ is committed as a source snapshot; these
|
||||
# are generated by run-image.sh / build-image/build-engine.sh)
|
||||
# are generated by run-image.sh / build-image/build-engine.sh /
|
||||
# build-image/build-engine-win.sh - the Windows cross-build's own scratch
|
||||
# copy of engine/ lives under /build/win-engine/, already covered above)
|
||||
/engine/build_ext/
|
||||
/engine/.build-output/
|
||||
/engine/.build-output-win/
|
||||
/engine/CMakeCache.txt
|
||||
/engine/CMakeFiles/
|
||||
/engine/cmake_install.cmake
|
||||
@@ -32,3 +38,7 @@
|
||||
|
||||
# Local registry credentials (see registry.env.example)
|
||||
/registry.env
|
||||
|
||||
# Signing keys
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidance for AI coding agents (and new contributors) working on Questshock.
|
||||
README.md is the user-facing build/usage guide and CONFIG.md documents the
|
||||
configuration file; this file covers how the code is organised and the rules to
|
||||
follow when changing it.
|
||||
|
||||
## 1. What this project is
|
||||
|
||||
Questshock plays the 1994 game System Shock on a standalone VR headset (Meta
|
||||
Quest 2 is the tested device). It wraps [Shockolate](https://github.com/Interrupt/systemshock),
|
||||
a cross-platform port of the original engine, in an Android app that:
|
||||
|
||||
- runs the unmodified 2D engine (software or OpenGL renderer) and shows its
|
||||
frame on a head-tracked virtual screen (an OpenXR quad layer);
|
||||
- shows a menu and an on-screen keyboard as separate OpenXR quads, operated by
|
||||
a laser pointer from either controller;
|
||||
- maps controller inputs to game actions through a configurable binding layer;
|
||||
- persists settings in `/sdcard/QuestShock/questshock.config.json`.
|
||||
|
||||
A desktop (Linux/Windows) build of the same engine exists for development
|
||||
without a headset; it has none of the Android/OpenXR code.
|
||||
|
||||
## 2. Hard rules
|
||||
|
||||
These come from the maintainer. Follow them without being asked again.
|
||||
|
||||
1. **Never commit.** Do not run `git commit` (or amend, rebase, push), even if
|
||||
asked to commit. Draft the commit message and show it; the maintainer runs
|
||||
the commit. Mention any new untracked file that has to be `git add`ed
|
||||
(typically a new patch under `android/engine-patches/`).
|
||||
2. **Do not build.** Do not run `make apk`, `./gradlew`, or `make android-studio`
|
||||
to check that a change compiles. The maintainer deploys from Android Studio.
|
||||
`make apk` and `make android-studio` overwrite `android/engine.properties`
|
||||
with container-only paths and break an open Android Studio project. What you
|
||||
can do is dry-run patches (section 6) and run static consistency checks
|
||||
(section 13). Say plainly in your report that the change was not built or
|
||||
tested.
|
||||
3. **`engine/` and `android/gl4es-src/` are never edited.** They are vendored
|
||||
snapshots. Every change to them is a numbered patch under
|
||||
`android/engine-patches/` or `android/gl4es-patches/`, applied to a scratch
|
||||
copy at build time. (`engine/` is GPLv3, see README section 6.)
|
||||
4. **Patch numbers stay contiguous** (`01`, `02`, ... with no gap). When you
|
||||
insert or delete a patch, renumber the rest and update every reference to
|
||||
the numbers (README patch reference, code comments).
|
||||
5. **Comments and documentation describe the current state and why it has to be
|
||||
that way.** No history or narrative: no "used to", "originally", "no
|
||||
longer", "now", "this was fixed", "confirmed on device", incident stories or
|
||||
dropped alternatives. History belongs in commit messages. A comment that
|
||||
would read oddly to someone who never saw an earlier version should be
|
||||
rewritten or cut. Default to one or two lines. (README section 5.5, the
|
||||
debugging notes, is the one deliberate exception.)
|
||||
6. **Standalone headset only, OpenXR first.** No feature may need a host PC
|
||||
(no PCVR, tethering or streaming). Prefer OpenXR APIs over vendor-specific
|
||||
ones (Meta's Oculus Mobile SDK and the like) so the port can run on other
|
||||
Android OpenXR headsets. Flag a request that would break either rule
|
||||
instead of quietly doing it.
|
||||
7. **Do not bump `build-image/VERSION`** just because `build-image/Dockerfile`
|
||||
changed. Bump it only once a build with the current number has succeeded
|
||||
end to end (for example a CI run). Ask if unsure.
|
||||
8. **Keep docs in sync with behaviour.** A user-visible change updates
|
||||
README.md and/or CONFIG.md in the same change; a new or renumbered engine
|
||||
patch updates the README patch reference (section 5.4).
|
||||
|
||||
## 3. Repository layout
|
||||
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| `engine/` | Vendored Shockolate source. Read-only for us. |
|
||||
| `android/engine-patches/` | Numbered `patch -p1` files applied to a scratch copy of `engine/`. |
|
||||
| `android/gl4es-src/`, `android/gl4es-patches/` | Vendored GL4ES (desktop GL to GLES) and its patches. |
|
||||
| `android/app/src/main/cpp/` | First-party native code (section 4). |
|
||||
| `android/app/src/main/java/de/ladkau/questshock/` | First-party Java (section 4). `org/libsdl/app/` is vendored SDL2 glue (zlib). |
|
||||
| `android/app/build.gradle` | Gradle project; the `stageEngine` task stages the patched engine. |
|
||||
| `android/force-rebuild.sh` | Stops Gradle daemons and wipes staged/build output for a fully fresh native build. |
|
||||
| `build-image/` | Docker build environment (`Dockerfile`, `VERSION`, `prepare-android-project.sh`, `build-apk.sh`, engine build scripts). |
|
||||
| `res/` | Bundled resources: `questshock.config.default.json`, `controller_profiles/` (JSON + PNG per controller), `splash.png` (startup splash), `logo.png` (1024x1024 logo, master of the launcher icons in `android/app/src/main/res/mipmap-*/`), `logo_small.png` (the README header), `screenshots/` (headset captures and videos named `questshock_v<version>_<nnn>.<ext>`, numbered per version), `assets/` (where the user's own game data goes, git-ignored), `run.sh`/`run.bat`. |
|
||||
| `.gitea/workflows/build.yml`, `.github/workflows/release.yml` | CI. Gitea is the primary repository and builds on every push; GitHub is a mirror whose workflow builds and publishes a release when a `vX.Y.Z` tag is pushed. Each platform has its own file. |
|
||||
| `Makefile` | `engine`, `dist`, `package`, `apk`, `android-studio`, `clean`, ... (see README section 2). |
|
||||
| `README.md`, `CONFIG.md`, `NOTICE.txt` | User docs, config reference, license notice. |
|
||||
|
||||
`build/`, `dist/`, `dist-win/` and `res/assets/ss_ee/` are generated or
|
||||
proprietary and git-ignored. `registry.env` holds local registry settings; do
|
||||
not print it or commit it.
|
||||
|
||||
## 4. Android app architecture
|
||||
|
||||
### 4.1 Native modules (`android/app/src/main/cpp/`)
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `xr_session.c/.h` | OpenXR instance and session lifecycle, frame loop, composition layers (game quad, menu, keyboard, laser beams). |
|
||||
| `xr_swapchain.c/.h` | Swapchain creation, acquire and release, per-image FBOs. |
|
||||
| `xr_overlay.c/.h` | Generic "off-screen Android `View` rendered into an OpenXR quad" module, hit-tested by a laser pointer. The menu and the keyboard are two instances. |
|
||||
| `xr_input.c/.h` | Controller input: OpenXR actions, raw-input layer, action bindings, capture-mode rebinding, key injection into the engine, free view, cyberspace scaling, snap-turn/smooth-move of the player's position, pointer rays. The largest file. |
|
||||
| `xr_mouse.c/.h` | Injects mouse motion and clicks into the engine. |
|
||||
| `qs_config.c/.h` | Configuration: JSON parse, apply, write; screen profiles; menu-driven requests; cheats; menu-triggered pause. |
|
||||
| `qs_splash.c/.h` | Startup splash: `qs_splash_show()` fades the image (`res/splash.png`, decoded by Java and passed in through `nativeSetSplashImage`) in and out on the game screen. Called from engine patch 27. |
|
||||
| `questshock_native.c` | JNI exports and small native helpers (`nativeChdir`, `nativeSendPrintableChar`, ...). |
|
||||
|
||||
### 4.2 Java classes
|
||||
|
||||
| Class | Role |
|
||||
|---|---|
|
||||
| `QuestShockActivity` | `SDLActivity` subclass. Creates `/sdcard/QuestShock/`, extracts bundled files, blocks engine start until the game data exists. |
|
||||
| `OverlayPanel` | Base for the off-screen panels: builds the view tree, renders it to a texture, turns ray hits into touch events. |
|
||||
| `MenuOverlay` | The menu: Main Menu, Video Options, Controller Options and Cheats tabs. Singleton. |
|
||||
| `KeyboardOverlay` | The on-screen keyboard. |
|
||||
| `Strings` | All menu and keyboard text in English, French and German. |
|
||||
|
||||
### 4.3 How the pieces fit
|
||||
|
||||
- The engine's present path is redirected (engine patch 11) so that
|
||||
`SDLDraw()` submits into the OpenXR game-quad swapchain. The per-frame
|
||||
entry point of `xr_input.c` is `xr_input_sync_and_draw()`, called from the
|
||||
frame path in `xr_session.c` on the engine's own thread.
|
||||
- The engine's own log output goes to logcat (patch 08); Java and native
|
||||
first-party code log under the tag `QuestShock`.
|
||||
- Bindings, config and menu state are owned natively. Java only displays and
|
||||
requests.
|
||||
- `gScreenWide`/`gScreenHigh` are only set by the engine's
|
||||
`ChangeScreenSize()`, so they are 0 until its first mode change. Code that
|
||||
runs during startup (such as `qs_splash_show()`) takes the screen size from
|
||||
`xr_get_game_swapchain_size()` instead.
|
||||
|
||||
### 4.4 Threads and cross-thread requests
|
||||
|
||||
- Engine state (player struct, game loop, cheats, config apply) must only be
|
||||
touched on the engine thread.
|
||||
- Menu code calls native functions through JNI. Anything that changes engine
|
||||
state must be enqueued, not executed inline: the JNI export calls a
|
||||
`qs_config_request_*()` function, which pushes an SDL custom event
|
||||
(`SDL_RegisterEvents`); `qs_config_handle_event()` handles it on the engine
|
||||
thread (it is called from `pump_events()`, engine patch 21). To add a new
|
||||
request, add an event type, the `qs_config_request_*()` enqueue function and
|
||||
a branch in `qs_config_handle_event()`.
|
||||
- Getters that Java calls (`qs_config_cheat_unlimited_mask()`, binding-name
|
||||
getters, ...) must be plain scalar or string reads that are safe from any
|
||||
thread.
|
||||
- Per-frame work that must run on the engine thread (such as
|
||||
`qs_config_frame_tick()`) is called from `xr_input.c`.
|
||||
- JNI symbol names must match the Java `native` declarations exactly
|
||||
(`Java_de_ladkau_questshock_<Class>_<method>`). A mismatch only fails at
|
||||
runtime, so check it (section 13).
|
||||
|
||||
### 4.5 Off-screen views (`OverlayPanel`, `MenuOverlay`, `KeyboardOverlay`)
|
||||
|
||||
The view tree is built and drawn from native code and is never attached to a
|
||||
window. Consequences that shape the UI code:
|
||||
|
||||
- Only `ACTION_DOWN`/`ACTION_UP` touches arrive (the laser trigger). There is no
|
||||
drag, fling or multi-touch. Scrolling is done by a self-drawn scroll bar
|
||||
(`ScrollBarView` in `MenuOverlay`), not by the framework.
|
||||
- `View.post()` never runs, so anything that defers work through it does not
|
||||
work. `ProgressBar` and `SeekBar` are unusable for the same reason.
|
||||
- Hover and hit-testing use `OverlayPanel.absoluteBounds()`, which subtracts
|
||||
ancestor scroll offsets.
|
||||
- Toggling a child's visibility needs an explicit re-measure and re-layout
|
||||
(see `relayoutAndRedraw()`).
|
||||
- All user-visible text goes through `Strings.get(key, language)` with an
|
||||
English, French and German entry, and is re-applied in `applyTranslations()`
|
||||
when the language changes. "OpenGL" is a brand name and is not translated.
|
||||
|
||||
## 5. Configuration system
|
||||
|
||||
- `qs_config.c` holds one parsed struct per group (`video_options`, `general`,
|
||||
`player_position`, `player_controls`), a parse function, an apply function,
|
||||
and the writer `qs_config_write_file()`. Groups are tagged live-appliable or
|
||||
restart-required (`QS_*_REQUIRES_RESTART`).
|
||||
- Load order at startup: `questshock.config.json`, else
|
||||
`questshock.config.default.json` (bundled, refreshed on every launch), else
|
||||
the engine's own `prefs.txt` behaviour. Save is only ever an explicit menu
|
||||
action.
|
||||
- Invalid values are logged and skipped individually; unknown keys are ignored.
|
||||
|
||||
**Adding an option:** parsed-struct field, parse, apply (with validation and a
|
||||
`WARN` on invalid input), write in `qs_config_write_file()`, restart flag,
|
||||
`CONFIG.md`, and (if it should have a shipped default) `res/questshock.config.default.json`.
|
||||
|
||||
The virtual screen has two profiles, "initial" (splash, cutscenes, main menu)
|
||||
and "game", switched by `qs_screen_profile_notify_loop_mode()` from engine
|
||||
patch 22. `s_screen_profile_is_game` in `qs_config.c` doubles as "a game is
|
||||
running" and gates the cheats and the menu pause.
|
||||
|
||||
## 6. Engine patches
|
||||
|
||||
Rules:
|
||||
|
||||
- Guard every engine change with `#ifdef __ANDROID__` so the desktop build is
|
||||
unaffected, and keep hunks minimal.
|
||||
- Patches are applied in filename order with `patch -p1` from the engine root
|
||||
(paths look like `a/src/GameSrc/hkeyfunc.c`). Patch N is generated against
|
||||
the tree with patches `01..N-1` applied.
|
||||
- Some engine files use CRLF line endings (for example `mouselook.c`). Edits
|
||||
and generated patches must preserve them byte for byte; check with `file`
|
||||
or `cat -A`. An editor or script that rewrites line endings produces a
|
||||
whole-file diff.
|
||||
- Do not hand-edit hunk headers. Regenerate the patch from a scratch copy.
|
||||
|
||||
Creating or changing a patch:
|
||||
|
||||
1. Make a scratch copy of `engine/` (for example under the session scratch
|
||||
directory) and apply patches `01..N-1` to it (`patch -p1 -d <scratch> < ...`).
|
||||
2. Copy the target file(s) to a second location, edit them in the scratch copy.
|
||||
3. Produce the patch with `diff -u` using `a/` and `b/` path prefixes relative
|
||||
to the engine root, and save it as `android/engine-patches/NN-android-<topic>.patch`.
|
||||
4. Dry-run the whole series on a fresh copy of `engine/`
|
||||
(`patch -p1 --dry-run`, or apply for real on the copy) and confirm every
|
||||
patch applies cleanly, with the patched file byte-identical to what you
|
||||
edited.
|
||||
5. Keep numbering contiguous, and update the README patch reference (5.4) and
|
||||
any code comments that cite patch numbers.
|
||||
|
||||
Gradle's `stageEngine` task re-stages and re-patches on build, but it can wrongly
|
||||
report `UP-TO-DATE` when a **new** patch file appears. If a native change does
|
||||
not show up on the device, tell the maintainer to run `./gradlew --stop`, or
|
||||
`android/force-rebuild.sh`, or delete `build/android-engine`, before assuming
|
||||
the change is wrong.
|
||||
|
||||
Engine symbols used from first-party code (for example `game_paused`,
|
||||
`pause_game_func`, `toggle_giveall_func`, `qs_cheat_refill`,
|
||||
`qs_in_cyberspace`) are declared `extern` next to their use in `qs_config.c` or
|
||||
`xr_input.c`, with a comment naming the engine file and patch.
|
||||
|
||||
## 7. Controller mapping (`xr_input.c`)
|
||||
|
||||
Structure:
|
||||
|
||||
- Physical controls are OpenXR actions, suggested once against three
|
||||
interaction profiles: Meta Touch, HTC Vive wand and the core
|
||||
`khr/simple_controller`. A single wrong path makes the whole
|
||||
`xrSuggestInteractionProfileBindings` call fail, so each profile lists only
|
||||
components it really has. A failure calls
|
||||
`qs_config_report_fatal_input_error()` and aborts input init.
|
||||
- The **raw-input layer** (`QsRawInput`) digitizes every physical control
|
||||
(buttons, triggers, squeezes, thumbstick clicks, and eight thumbstick
|
||||
directions with hysteresis) each frame. `QS_RAW_NONE` means unbound.
|
||||
- **Actions** (`QsControlAction`) are bound to raw inputs through `s_binding[]`.
|
||||
The default bindings are `kDefaults[]`.
|
||||
- Contexts: `ALWAYS`, `NORMAL` (move, strafe, turn, jump, stand, crouch, prone)
|
||||
and `CYBER` (the eleven `cyber_*` actions). The active set is chosen with
|
||||
`qs_in_cyberspace()` (patch 24). Two actions conflict only if their contexts
|
||||
can be active together (`xr_input_contexts_overlap()`).
|
||||
- Capture mode (`xr_input_process_capture()`): the next fresh raw down-edge is
|
||||
assigned to the chosen action. Any other action in an overlapping context that
|
||||
held that input (or its symmetric alias) is set to `none`. The reset and the
|
||||
timeout (8 s) paths never leave a half-assigned state.
|
||||
- The menu button is not a raw input and can never be bound, so the menu can
|
||||
always be reached to press **Reset to Defaults**.
|
||||
- While the menu is open, `mouse_click` is always the trigger (the controller
|
||||
used to operate the menu); after it closes, the bound `mouse_click` drives
|
||||
both the keyboard and the game quad.
|
||||
- While the Controller Options tab is open, the menu's own thumbstick
|
||||
snap-turn and smooth-move are disabled
|
||||
(`g_controller_options_tab_open`, set through
|
||||
`nativeSetControllerOptionsTabOpen`).
|
||||
- Free view: holding a `free_*_thumbstick_view` binding turns that stick into
|
||||
the engine's mouse look (cursor forced hidden, stick direction bindings
|
||||
suspended). It uses `SDL_ShowCursor(SDL_DISABLE)` because the engine zeroes
|
||||
mouse-look velocity while the cursor is visible.
|
||||
- Cyberspace pitch and bank scale with stick deflection through the globals
|
||||
from patch 25 (`qs_cyber_pitch_scale`, `qs_cyber_bank_scale`).
|
||||
|
||||
### Adding or changing a bindable action (checklist)
|
||||
|
||||
All of these lists share one order and must stay identical:
|
||||
|
||||
1. `QsControlAction` enum in `xr_input.c` (and append before `QS_ACTION_COUNT`).
|
||||
2. `kActionNames[]` in `xr_input.c`.
|
||||
3. `s_binding[]` initial values and `kDefaults[]` in `xr_input.c`.
|
||||
4. If it injects a key: a `kKeyActions[]` entry (keycode, modifiers, whether it
|
||||
needs `SDL_TEXTINPUT`); add the key to the stuck-key watchdog list
|
||||
(`kWatchedKeys`) if it is held continuously.
|
||||
5. `ACTION_KEYS` in `MenuOverlay.java`, and `FIRST_ROW_ACTIONS` if it should be
|
||||
listed first.
|
||||
6. `Strings.java`: `action_<key>` in EN/FR/DE.
|
||||
7. `QS_MAX_PLAYER_CONTROL_ACTIONS` in `qs_config.c` if the count outgrows it.
|
||||
8. `CONFIG.md`: the action tables and the count in the Player Controls intro;
|
||||
README default-controls table if it has a default binding.
|
||||
9. Check that no two actions that can be active together share a default input.
|
||||
|
||||
### Injecting keys into the engine
|
||||
|
||||
- Plain printable keys (letters, digits, Space, punctuation) need an
|
||||
`SDL_TEXTINPUT` event plus `SDL_KEYDOWN`/`SDL_KEYUP`. Modifier, control-range,
|
||||
F and keypad keys use `SDL_KEYDOWN`/`SDL_KEYUP` with the right `mod`.
|
||||
- **Send the release on the real up edge, never in the same call as the
|
||||
press.** The engine polls movement keys once per frame before it drains
|
||||
events, so a press and release drained together are never seen and the key
|
||||
silently does nothing. Tie the release to the physical button's up edge (or
|
||||
the touch's `ACTION_UP`/`ACTION_CANCEL`).
|
||||
- Held keys are tracked in the engine's `sshockKeyStates[]`; `xr_input.c`
|
||||
keeps a watchdog that clears a watched key the engine still thinks is down
|
||||
after its binding is released.
|
||||
- Actions that share an engine key (for example the several ways to fire, or
|
||||
the movement keys) are handled with sibling logic in the dispatch loop
|
||||
(`wantDown[]`); do not press or release the shared key independently.
|
||||
|
||||
## 8. Cheats tab
|
||||
|
||||
- Buttons and switches live in `MenuOverlay.buildCheatsTab()`; ids are
|
||||
`QS_CHEAT_*` in `qs_config.c` and `CHEAT_*` in `MenuOverlay.java` (keep them
|
||||
identical).
|
||||
- The engine's own cheat handlers (`toggle_giveall_func`, `toggle_physics_func`,
|
||||
`toggle_up_level_func`, `toggle_down_level_func`) are called on the engine
|
||||
thread through `qs_apply_cheat()`, and only while a game is running.
|
||||
- The four unlimited switches are a bitmask in `qs_config.c`
|
||||
(`QS_CHEAT_MASK_*`: health 1, energy 2, ammo 4, items 8). Every frame
|
||||
`qs_config_frame_tick()` calls `qs_cheat_refill(mask)` (patch 26). The mask
|
||||
values must match the patch. The switches are not persisted.
|
||||
- The four `cheat_*` hotkeys are deliberately not bindable in
|
||||
`player_controls`. New cheats go on the Cheats tab.
|
||||
|
||||
## 9. Menu pause
|
||||
|
||||
Opening the menu pauses a running game (`qs_apply_menu_pause()` through the
|
||||
menu-visibility event); closing it resumes only if the menu did the pausing and
|
||||
the game is still paused. Do not toggle pause blindly, since the pause function
|
||||
toggles.
|
||||
|
||||
## 10. Bundled files on the device
|
||||
|
||||
`QuestShockActivity` extracts these into `/sdcard/QuestShock/`:
|
||||
|
||||
- shaders, the default soundfont and `GET_ASSETS_QUEST.txt` (once);
|
||||
- `questshock.config.default.json` and `controller_profiles/` (every launch,
|
||||
overwriting only the bundled filenames so user-added profiles survive).
|
||||
|
||||
`res/splash.png` stays in the APK assets and is not extracted to the device;
|
||||
`QuestShockActivity` decodes it and hands the pixels to `qs_splash.c`.
|
||||
|
||||
`res/controller_profiles/`, `res/splash.png` and the default config are staged
|
||||
into APK assets by `build-image/prepare-android-project.sh`; every staged file
|
||||
or directory must also be declared as `inputs.file(...)`/`inputs.dir(...)` of
|
||||
`stageEngine` in `android/app/build.gradle`.
|
||||
|
||||
The proprietary game data (`res/data/`, `res/sound/`) is never bundled or
|
||||
committed. The app refuses to start the engine until it is present.
|
||||
|
||||
## 11. Signing
|
||||
|
||||
Distributed APKs are release builds signed with one fixed key, so users can
|
||||
update in place. `build-image/build-apk.sh` reads the key from
|
||||
`QUESTSHOCK_KEYSTORE`, `QUESTSHOCK_KEYSTORE_PASSWORD`, `QUESTSHOCK_KEY_ALIAS`
|
||||
and `QUESTSHOCK_KEY_PASSWORD`; `android/app/build.gradle` applies it to the
|
||||
`release` build type only when `QUESTSHOCK_KEYSTORE` is set. Both CI workflows
|
||||
decode it from the `ANDROID_KEYSTORE_BASE64` secret. Without a key the build is
|
||||
a debug APK (`...-debug.apk`), which is only for testing. Never commit a
|
||||
keystore or a password, print them, or put them in a workflow file (README
|
||||
section 4.3 lists the secrets).
|
||||
|
||||
## 12. Code and documentation style
|
||||
|
||||
- Match the surrounding code: naming, comment density, idiom. C is plain C with
|
||||
`static` helpers and `//` comments; Java targets Android API 34 and uses only the SDK plus
|
||||
`androidx.core`.
|
||||
- Keep functions and file-level constants near their use. Put magic numbers in
|
||||
named constants (see `QS_FREE_VIEW_*`, `QS_CYBER_ROTATION_*`).
|
||||
- Keep the `xr_input.h` and `qs_config.h` declarations documented with a short
|
||||
comment saying which thread may call them.
|
||||
- Comments: what and why, current state only (rule 5 in section 2).
|
||||
- Docs: README.md is organised by numbered sections that other text cites
|
||||
("5.5.3"); do not renumber existing sections. Add new material as a new
|
||||
subsection instead.
|
||||
- English, French and German strings live in `Strings.java`; keep the three
|
||||
entries of each key on the same `put(...)`.
|
||||
|
||||
## 13. Verifying a change without building
|
||||
|
||||
Because you may not build (rule 2), check statically and say so in the report:
|
||||
|
||||
- **Patches:** apply the whole series to a fresh copy of `engine/` and confirm
|
||||
no rejects. Confirm the edited file matches the patched result byte for byte.
|
||||
- **JNI:** every `native` method in the Java classes has a
|
||||
`Java_de_ladkau_questshock_<Class>_<method>` export with a matching signature,
|
||||
and no export is left without a Java declaration.
|
||||
- **Ordered lists:** `QsControlAction`, `kActionNames[]`, `s_binding[]`,
|
||||
`kDefaults[]` and `ACTION_KEYS` have the same length and order.
|
||||
- **Strings:** every `Strings.get("...")` key exists in `Strings.java`.
|
||||
- **Balance:** braces and parentheses balance. `qs_config.c` reports a
|
||||
brace-count offset of +5 from character literals containing `'{'`; that is
|
||||
not a bug.
|
||||
- **Docs:** README and CONFIG.md agree with the code (action counts, default
|
||||
bindings, patch list).
|
||||
|
||||
Then report what was checked, what was not (build, on-device behaviour), and
|
||||
any device check the maintainer should do (for example a new patch that may
|
||||
need `android/force-rebuild.sh`).
|
||||
|
||||
## 14. Commit messages
|
||||
|
||||
Draft only (rule 1). Use a short imperative subject (about 70 characters), a
|
||||
blank line, then a body that explains what the change does and why: the
|
||||
user-visible behaviour, the mechanism, and any constraint worth remembering.
|
||||
Mention new files that need `git add`.
|
||||
@@ -0,0 +1,458 @@
|
||||
# Persistent configuration (`questshock.config.json`)
|
||||
|
||||
QuestShock can load a persistent, human-editable configuration file at
|
||||
startup, in addition to (and independent of) the engine's own `prefs.txt`.
|
||||
|
||||
- **Location**: `/sdcard/QuestShock/questshock.config.json` - the same
|
||||
folder you already copy `res/data`/`res/sound` into (see README.md
|
||||
section 5.1).
|
||||
- **Format**: plain JSON, grouped by concern. Each top-level key is a
|
||||
*group*; each key inside a group is one *option*.
|
||||
- **Loading**: read automatically at app startup, before the splash screen,
|
||||
if the file exists. If it doesn't, `questshock.config.default.json` (same
|
||||
folder) is used instead - a copy bundled with the app, refreshed on every
|
||||
launch, never overwriting your own saved `questshock.config.json`. If
|
||||
neither file exists, the engine behaves as it did before this file
|
||||
existed (driven by `prefs.txt` as usual).
|
||||
- **Saving**: never automatic. In-headset, open the menu (controller menu
|
||||
button) and press **Save Configuration** to write the file from your
|
||||
current live settings, overwriting it if present. Press **Load
|
||||
Configuration** to re-read the file and apply it without restarting the
|
||||
app.
|
||||
- **Live vs. restart-required**: every option below is tagged as either
|
||||
*live-appliable* (applying it while already playing takes effect
|
||||
immediately) or *restart-required*. Pressing **Load Configuration**
|
||||
applies every live-appliable change straight away; if the file also
|
||||
changes something restart-required, a dialog lists exactly which
|
||||
option(s) need it and offers:
|
||||
- **Dismiss** - keep playing with the live-appliable changes already
|
||||
applied; the restart-required option(s) stay as they were.
|
||||
- **Restart Now** - relaunches the app, which re-runs this same startup
|
||||
load and so applies every group in the file, including the
|
||||
restart-required ones.
|
||||
|
||||
(Every option currently documented below is live-appliable, so this
|
||||
dialog won't actually appear yet - it's built in for options added in a
|
||||
future version that do need a restart.)
|
||||
- Unrecognized groups/keys are ignored (harmless if you're editing the file
|
||||
in advance for a future version). An invalid value for a *specific*
|
||||
option (see each option's possible values below) is logged and that one
|
||||
option is skipped - every other valid option in the file still applies.
|
||||
A file that isn't valid JSON at all is ignored entirely, with nothing
|
||||
applied.
|
||||
- `questshock.config.json` and `prefs.txt` are fully independent - loading
|
||||
or saving one never reads or writes the other.
|
||||
|
||||
## Example
|
||||
|
||||
```json
|
||||
{
|
||||
"video_options": {
|
||||
"renderer": "opengl",
|
||||
"resolution": "1024x768",
|
||||
"initial_screen_width_m": 1.6,
|
||||
"initial_screen_distance_m": 2.0,
|
||||
"game_screen_width_m": 2.8,
|
||||
"game_screen_distance_m": 2.0,
|
||||
"screen_width_offset_m": 1.0,
|
||||
"screen_width_step_m": 0.6,
|
||||
"screen_distance_offset_m": 1.0,
|
||||
"screen_distance_step_m": 0.5
|
||||
},
|
||||
"general": {
|
||||
"language": "english"
|
||||
},
|
||||
"player_position": {
|
||||
"x": 0.0,
|
||||
"z": 0.0,
|
||||
"yaw_degrees": 0.0
|
||||
},
|
||||
"player_controls": {
|
||||
"move_forward": "left_thumbstick_up",
|
||||
"move_back": "left_thumbstick_down",
|
||||
"strafe_left": "left_thumbstick_left",
|
||||
"strafe_right": "left_thumbstick_right",
|
||||
"turn_left": "right_thumbstick_left",
|
||||
"turn_right": "right_thumbstick_right",
|
||||
"jump": "left_thumbstick_click",
|
||||
"mouse_click": "right_a",
|
||||
"mouse_double_click": "right_b",
|
||||
"stand": "right_thumbstick_up",
|
||||
"prone": "right_thumbstick_down",
|
||||
"map_view": "right_thumbstick_click",
|
||||
"fire_weapon": "right_trigger"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Video Options (`video_options`)
|
||||
|
||||
### `renderer`
|
||||
|
||||
Which renderer the game uses to draw the 3D view.
|
||||
|
||||
- **Type**: enum
|
||||
- **Possible values**: `"software"`, `"opengl"`
|
||||
- **Restart required?**: No (live-appliable)
|
||||
|
||||
### `resolution`
|
||||
|
||||
The game's internal rendering resolution.
|
||||
|
||||
- **Type**: enum
|
||||
- **Possible values**: `"320x200"`, `"320x400"`, `"640x400"`, `"640x480"`, `"1024x768"`
|
||||
- **Restart required?**: No (live-appliable)
|
||||
|
||||
### `initial_screen_width_m`, `initial_screen_distance_m`, `game_screen_width_m`, `game_screen_distance_m`
|
||||
|
||||
The virtual screen's physical width and distance from the player (both in
|
||||
meters) - the game shows two independently configurable screen
|
||||
"profiles", switched automatically based on what's currently on screen:
|
||||
|
||||
- **Initial Screen** (`initial_screen_*`) - the splash screen, intro
|
||||
cutscenes, and the game's own main menu.
|
||||
- **Game Screen** (`game_screen_*`) - actual gameplay (the 3D view,
|
||||
automap, cyberspace, etc.).
|
||||
|
||||
Switching happens automatically - no player action needed - the instant
|
||||
the game itself transitions between these (e.g. starting/loading a game,
|
||||
or returning to the menu).
|
||||
|
||||
- **Type**: number (meters)
|
||||
- **Possible values**: the in-headset Video Options tab offers 5 presets
|
||||
each - widths `1.0`, `1.6` (the original, pre-this-feature default),
|
||||
`2.2`, `2.8`, `3.4`; distances `1.0`, `1.5`, `2.0` (the original,
|
||||
pre-this-feature default), `2.5`, `3.0` - but a hand-edited value
|
||||
doesn't need to match a preset exactly (it's clamped to a sane range -
|
||||
width `[0.2, 10.0]`, distance `[0.5, 10.0]` - rather than snapped or
|
||||
rejected; the Video Options tab just shows no button selected for an
|
||||
in-between value).
|
||||
- **Restart required?**: No (live-appliable)
|
||||
- **Note**: the screen is always flat - there's no curve option.
|
||||
|
||||
### `screen_width_offset_m`, `screen_width_step_m`, `screen_distance_offset_m`, `screen_distance_step_m`
|
||||
|
||||
Reshapes the 5 preset buttons the Video Options tab offers for width and
|
||||
distance (above) - config-file-only, there's no in-headset control for
|
||||
these. Shared by both **Initial Screen** and **Game Screen** (both
|
||||
columns' buttons show the same 5 values per row) - this only changes
|
||||
what the buttons offer, not either profile's current value.
|
||||
|
||||
Preset button `index` (0-4) shows `offset + step * index`. For example,
|
||||
a distance offset of `0` and step of `2` shows buttons `0`, `2`, `4`,
|
||||
`6`, `8`; the defaults below reproduce this feature's original launch
|
||||
presets exactly.
|
||||
|
||||
- **Type**: number (meters)
|
||||
- **Default values**: width offset `1.0`, step `0.6` (buttons `1.0`,
|
||||
`1.6`, `2.2`, `2.8`, `3.4`); distance offset `1.0`, step `0.5` (buttons
|
||||
`1.0`, `1.5`, `2.0`, `2.5`, `3.0`).
|
||||
- **Restart required?**: No (live-appliable)
|
||||
- **Note**: not clamped - an unusual offset/step (e.g. negative step) is
|
||||
accepted as-is. A resulting preset value outside the current value's
|
||||
own clamp range (see above) still clamps normally when that preset
|
||||
button is actually pressed.
|
||||
|
||||
## General (`general`)
|
||||
|
||||
### `language`
|
||||
|
||||
The game's UI/text language - both the game's own resource strings and this
|
||||
project's own menu/keyboard chrome text (title bar, buttons, etc.).
|
||||
|
||||
- **Type**: enum
|
||||
- **Possible values**: `"english"`, `"french"`, `"german"`
|
||||
- **Restart required?**: No (live-appliable) - applies the same way the
|
||||
game's own in-game options menu applies a language change (reloads the
|
||||
language-specific resource files and refreshes the inventory/MFD/side-icon
|
||||
UI immediately).
|
||||
- **Note**: French and German both need their own full resource pack
|
||||
(`frnstrng.res`/`mfdfrn.res`, `gerstrng.res`/`mfdger.res` under
|
||||
`res/data/`) - if any of those files aren't present, the switch is
|
||||
refused (logged, `questshock.config.json` and the current language are
|
||||
left unchanged) rather than leaving the game with no valid resource file
|
||||
open. See README.md section 3 ("Game assets") for where `mfdfrn.res`/
|
||||
`mfdger.res` come from - the GOG Enhanced Edition installer alone doesn't
|
||||
include them.
|
||||
- The intro cutscene is also played in the selected language when its
|
||||
voice-over file (`svfrintr.res`/`svgeintr.res` under `res/data/`) is
|
||||
present; otherwise the English intro plays.
|
||||
|
||||
## Player Position (`player_position`)
|
||||
|
||||
Where the player is virtually standing/facing, independent of - and not
|
||||
reset by - starting a new game or loading a save (same independence from
|
||||
`prefs.txt`/savegames as every other group here). Set by moving around
|
||||
in-headset (menu open, left thumbstick snap-turns, right thumbstick smooth-
|
||||
moves) to line yourself up with the game quad ("virtual screen"). Height is
|
||||
never included here - it always comes from your real tracked headset
|
||||
height, not this file.
|
||||
|
||||
**Startup fallback**: only applied at startup if all three fields (`x`,
|
||||
`z`, `yaw_degrees`) are present - a config file with none of them (or none
|
||||
at all), or a partial/hand-edited one missing any of the three, falls back
|
||||
to an automatic recenter instead (see **Reset Position** below), using
|
||||
your real head pose at that moment. A complete saved position is applied
|
||||
as-is, with no such recenter.
|
||||
|
||||
**Reset Position** (Main Menu tab) is a live-only action, not a config
|
||||
value: it moves you to the centre in front of the game quad, based on where
|
||||
your head actually is right now. It resets only your *position*, not which
|
||||
way you're facing, so the screen does not rotate to follow your gaze - press
|
||||
it while looking off to one side and the screen stays off to the other side.
|
||||
The automatic recenter at startup (when there's no saved position) does
|
||||
face the screen towards where you're looking. Reset Position never reads or
|
||||
writes `questshock.config.json`.
|
||||
|
||||
### `x`, `z`
|
||||
|
||||
Horizontal position, in meters, on the play space's floor plane.
|
||||
|
||||
- **Type**: number
|
||||
- **Restart required?**: No (live-appliable)
|
||||
|
||||
### `yaw_degrees`
|
||||
|
||||
Which way the player is virtually facing, in degrees.
|
||||
|
||||
- **Type**: number
|
||||
- **Restart required?**: No (live-appliable)
|
||||
|
||||
## Player Controls (`player_controls`)
|
||||
|
||||
Maps each of 57 in-game actions to a physical controller input: 9
|
||||
movement/click actions, 2 free-view actions, 11 cyberspace actions, and 35
|
||||
for the engine's own keyboard hotkeys and firing (posture, save/load, weapon
|
||||
select, MFD panels, etc. - see the hotkey table below). Some actions are bound by default (see the default-input
|
||||
columns); the rest are unbound and can be bound on demand. Assign them in the
|
||||
**Controller Options** menu tab: tap an action's binding button, then press
|
||||
the button or stick direction to use ("Press a button…"); tap it again, or
|
||||
wait 8 seconds, to cancel without changing it.
|
||||
|
||||
- **Type**: one enum-string field per action (below)
|
||||
- **Restart required?**: No (live-appliable) - a rebind takes effect
|
||||
immediately, and persists across restarts once saved (**Save
|
||||
Configuration**, same as every other group here).
|
||||
- **Menu movement**: while the Controller Options tab is open, the menu's own
|
||||
thumbstick snap-turn and smooth-move (see `player_position`) are disabled, so
|
||||
pressing inputs to assign them doesn't move you away from the screen.
|
||||
- **Not included here**: opening/closing the VR menu itself is a fixed,
|
||||
non-remappable button (see "Menu button" below) - there's no
|
||||
`menu_toggle` field, and the Controller Options tab never lets you
|
||||
reassign it. This guarantees you can always reach this tab and press
|
||||
**Reset to Defaults** to recover from any bad rebind, no matter what else
|
||||
you've reassigned.
|
||||
- **Reset to Defaults**: restores the "Default input" column below for
|
||||
every action not overridden by a `player_controls` group in
|
||||
`questshock.config.default.json` - if that file defines one (it doesn't,
|
||||
out of the box), the values it specifies are applied on top of the table
|
||||
below instead, so you can customize what "reset" means without a rebuild.
|
||||
This is independent of your own `questshock.config.json` - resetting
|
||||
doesn't read or write that file, just the live bindings (**Save
|
||||
Configuration** afterward to persist them).
|
||||
|
||||
### Actions
|
||||
|
||||
| Action key | Default input | Effect |
|
||||
|---|---|---|
|
||||
| `move_forward` | `left_thumbstick_up` | Walk forward |
|
||||
| `move_back` | `left_thumbstick_down` | Walk backward |
|
||||
| `strafe_left` | `left_thumbstick_left` | Step sideways, left |
|
||||
| `strafe_right` | `left_thumbstick_right` | Step sideways, right |
|
||||
| `turn_left` | `right_thumbstick_left` | Turn left |
|
||||
| `turn_right` | `right_thumbstick_right` | Turn right |
|
||||
| `jump` | `left_thumbstick_click` | Jump |
|
||||
| `mouse_click` | `right_a` | Click - operates the keyboard/game quad's cursor, whichever the pointing hand's ray is over |
|
||||
| `mouse_double_click` | `right_b` | Double-click - game quad only (e.g. using an inventory item); has no effect over the menu or keyboard |
|
||||
| `free_left_thumbstick_view` | `left_squeeze` | Hold to look around with the left thumbstick (see below) |
|
||||
| `free_right_thumbstick_view` | `none` | Hold to look around with the right thumbstick (see below) |
|
||||
|
||||
Movement actions bound to a thumbstick direction are suppressed while the
|
||||
VR menu is open, since the same physical stick already snap-turns/smooth-
|
||||
moves the player in that state (see `player_position` above) - this is the
|
||||
one case where a binding's effect depends on whether the menu is open, and
|
||||
it only ever affects the 8 thumbstick-direction inputs below.
|
||||
|
||||
**Free view** (`free_left_thumbstick_view`, `free_right_thumbstick_view`) is
|
||||
the engine's mouse look driven by a thumbstick. While the bound input is held,
|
||||
that stick looks around instead of doing its normal job: left/right turns the
|
||||
view, up/down looks up and down, in proportion to how far the stick is pushed.
|
||||
The stick's own direction bindings (move, strafe, turn, stand/prone) are
|
||||
suspended while it is held, and the cursor stays centred in the view. Release
|
||||
the input to return to normal. It has no effect while the VR menu is open.
|
||||
|
||||
### Cyberspace actions
|
||||
|
||||
Cyberspace has its own set of movement actions, active only while the player
|
||||
is in cyberspace. While in cyberspace the normal movement actions
|
||||
(`move_*`, `strafe_*`, `turn_*`, `jump`, `stand`, `crouch`, `prone`) are
|
||||
inactive, and the cyberspace actions are inactive everywhere else. Each set
|
||||
has its own bindings, so one input can be bound in both (for example the left
|
||||
stick up is `move_forward` on foot and `cyber_climb` in cyberspace);
|
||||
the Controller Options tab only reports a conflict between actions that can
|
||||
be active at the same time. Free view, clicking and firing work in both.
|
||||
|
||||
Pitch and bank (`cyber_climb`, `cyber_dive`, `cyber_bank_*` and the four
|
||||
diagonals) are proportional when bound to a thumbstick direction: pushing the
|
||||
stick further turns faster, from 20% of full speed just past the trigger point
|
||||
to 60% at full deflection. Bound to a button they run at full speed. Thrust and
|
||||
roll are always full speed.
|
||||
|
||||
| Action key | Default input | Keyboard equivalent | Effect |
|
||||
|---|---|---|---|
|
||||
| `cyber_thrust` | `right_thumbstick_up` | `s` | Thrust forward |
|
||||
| `cyber_climb` | `left_thumbstick_up` | `w` | Pitch up |
|
||||
| `cyber_dive` | `left_thumbstick_down` | `x` | Pitch down |
|
||||
| `cyber_bank_left` | `left_thumbstick_left` | `a` | Bank left |
|
||||
| `cyber_bank_right` | `left_thumbstick_right` | `d` | Bank right |
|
||||
| `cyber_roll_left` | `right_thumbstick_left` | `e` | Roll left |
|
||||
| `cyber_roll_right` | `right_thumbstick_right` | `q` | Roll right |
|
||||
| `cyber_climb_left` | `none` | Keypad 7 | Pitch up and bank left |
|
||||
| `cyber_climb_right` | `none` | Keypad 9 | Pitch up and bank right |
|
||||
| `cyber_dive_left` | `none` | Keypad 1 | Pitch down and bank left |
|
||||
| `cyber_dive_right` | `none` | Keypad 3 | Pitch down and bank right |
|
||||
|
||||
`mouse_click` additionally behaves differently depending on the menu:
|
||||
while the VR menu is open, its "click" is always whichever hand's trigger
|
||||
is used to interact with the menu (so you can never lock yourself out of
|
||||
this settings screen by rebinding `mouse_click` elsewhere); once the menu
|
||||
is closed, both the on-screen keyboard (if it's still open) and the game
|
||||
quad use your actual `mouse_click` binding. The default (`right_a`) is a
|
||||
specific button on the right controller, so only the right hand's ray clicks;
|
||||
bind the `trigger` identifier (see below) for either hand to click.
|
||||
|
||||
### Hotkey actions
|
||||
|
||||
The engine's own keyboard hotkeys (`HotKeyLookup[]` in `Prefs.c`) plus
|
||||
firing, as 35 bindable actions. `stand` (`right_thumbstick_up`), `prone`
|
||||
(`right_thumbstick_down`), `map_view` (`right_thumbstick_click`) and
|
||||
`fire_weapon` (`right_trigger`) are bound by default; the rest are unbound
|
||||
(`none`). Each fires once per press, like pressing the key on a real
|
||||
keyboard (there is no hold behavior).
|
||||
|
||||
| Action key | Keyboard equivalent | Effect |
|
||||
|---|---|---|
|
||||
| `stand` | `t` | Stand up |
|
||||
| `crouch` | `g` | Crouch |
|
||||
| `prone` | `b` | Go prone |
|
||||
| `pause` | `p` | Pause the game |
|
||||
| `full_view` | Ctrl+F | Switch to the fullscreen view loop |
|
||||
| `normal_view` | Ctrl+D | Switch to the normal gameplay view |
|
||||
| `map_view` | Ctrl+A | Switch to the automap view |
|
||||
| `save_game` | Ctrl+S | Save the game |
|
||||
| `load_game` | Ctrl+L | Load the game |
|
||||
| `select_grenade` | Ctrl+' | Select the next grenade type |
|
||||
| `arm_grenade` | Alt+' | Arm the selected grenade |
|
||||
| `select_drug` | Ctrl+; | Select the next drug/patch |
|
||||
| `use_drug` | Alt+; | Use the selected drug/patch |
|
||||
| `toggle_online_help` | Ctrl+H | Toggle the online help overlay |
|
||||
| `show_help` | Alt+O | Show the help screen |
|
||||
| `toggle_music` | Ctrl+M | Toggle music |
|
||||
| `quit` | Ctrl+Q | Quit the game - opt-in only; nothing binds this by default |
|
||||
| `cycle_weapon_next` | Tab | Ready the next weapon |
|
||||
| `cycle_weapon_prev` | Shift+Tab | Ready the previous weapon |
|
||||
| `cycle_detail` | Ctrl+1 | Cycle the graphics detail level |
|
||||
| `reload_weapon_primary` | Ctrl+Backspace | Reload the current weapon's primary ammo |
|
||||
| `reload_weapon_secondary` | Alt+Backspace | Reload the current weapon's secondary ammo |
|
||||
| `hud_color` | Alt+H | Cycle the HUD color scheme |
|
||||
| `clear_fullscreen` | Backspace | Close a fullscreen view (e.g. a datapad or email) |
|
||||
| `mfd_left_1`-`mfd_left_5` | F1-F5 | Set the left MFD panel to Weapon/Item/Map/Target/Info |
|
||||
| `mfd_right_1`-`mfd_right_5` | F6-F10 | Set the right MFD panel to Weapon/Item/Map/Target/Info |
|
||||
| `fire_weapon` | Enter | Fire the readied weapon at the cursor (the engine's default fire key). Clicking in the 3D view with `mouse_click` also fires; this is a separate button for it |
|
||||
|
||||
**Not exposed** (other `HotKeyLookup[]` entries):
|
||||
- `keypad 0`-`keypad 9`: a controller button per digit is not useful; use the
|
||||
on-screen keyboard.
|
||||
- `toggle_freelook` (F): the free-view actions cover mouse look.
|
||||
- The 4 `cheat_*` entries: debug cheats. They are on the menu's Cheats tab
|
||||
instead, along with four switches for unlimited health, energy, ammo and
|
||||
items. Those are not saved to the config.
|
||||
- `mac_help` (disabled) and `audiolog_cancel` (its build flag is not
|
||||
defined): they are not real hotkeys in this build.
|
||||
- `toggle_opengl` (Ctrl+G): the Video Options tab's Renderer buttons already
|
||||
toggle the same setting.
|
||||
- The 10 numeric hardware-item hotkeys (bio scan, fullscreen, 360 view,
|
||||
lantern, shield, infrared, nav unit, data reader, booster, jumpjets): their
|
||||
registered keys never match a real keypress in the engine.
|
||||
|
||||
### Raw input identifiers
|
||||
|
||||
Every raw physical input available to bind an action to, with its generic
|
||||
OpenXR interaction-profile component name and its Meta Quest Touch control
|
||||
name. Two symmetric identifiers (`trigger`, `squeeze`) are also accepted, but
|
||||
only for `mouse_click`/`mouse_double_click`: they mean "either hand's own
|
||||
trigger/grip, whichever hand is pointing". Every other identifier below
|
||||
refers to one specific control.
|
||||
|
||||
| Identifier | OpenXR component (Touch) | Meta Quest control |
|
||||
|---|---|---|
|
||||
| `left_trigger` | `/user/hand/left/input/trigger/value` | Left Trigger |
|
||||
| `right_trigger` | `/user/hand/right/input/trigger/value` | Right Trigger |
|
||||
| `left_squeeze` | `/user/hand/left/input/squeeze/value` | Left Grip |
|
||||
| `right_squeeze` | `/user/hand/right/input/squeeze/value` | Right Grip |
|
||||
| `left_x` | `/user/hand/left/input/x/click` | X Button |
|
||||
| `left_y` | `/user/hand/left/input/y/click` | Y Button |
|
||||
| `right_a` | `/user/hand/right/input/a/click` | A Button |
|
||||
| `right_b` | `/user/hand/right/input/b/click` | B Button |
|
||||
| `left_thumbstick_click` | `/user/hand/left/input/thumbstick/click` | Left Thumbstick Click |
|
||||
| `right_thumbstick_click` | `/user/hand/right/input/thumbstick/click` | Right Thumbstick Click |
|
||||
| `left_thumbstick_up/down/left/right` | `/user/hand/left/input/thumbstick` (digitized) | Left Thumbstick, tilted |
|
||||
| `right_thumbstick_up/down/left/right` | `/user/hand/right/input/thumbstick` (digitized) | Right Thumbstick, tilted |
|
||||
| `trigger` | either hand's trigger | Either Trigger (`mouse_click`/`mouse_double_click` only) |
|
||||
| `squeeze` | either hand's grip | Either Grip (`mouse_click`/`mouse_double_click` only) |
|
||||
| `none` | - | Unbound - the action never fires |
|
||||
|
||||
**Menu button** (left controller, Touch/Vive/generic alike) is not in this
|
||||
list - it always opens/closes the VR menu and can never be reassigned.
|
||||
|
||||
**Other supported controllers**: HTC Vive wands have no A/B/X/Y and no
|
||||
analog grip - `left_x`/`left_y`/`right_a`/`right_b` simply never fire on
|
||||
that profile, and `left/right_squeeze` are a plain click rather than
|
||||
analog. Their trackpad stands in for the thumbstick identifiers above
|
||||
(same digitization, just a trackpad instead of a stick). A bare-minimum
|
||||
OpenXR controller (`khr/simple_controller`, used as a fallback when
|
||||
neither Touch nor Vive bindings are suggested) only has `left_trigger`/
|
||||
`right_trigger` (via its `select/click` component) and the menu button -
|
||||
every other identifier above simply never fires on it, which is why the
|
||||
Controller Options tab shows a note and hides irrelevant rows when this
|
||||
profile is active.
|
||||
|
||||
### Controller profile files (`controller_profiles/`)
|
||||
|
||||
The Controller Options tab shows a labeled illustration of whichever
|
||||
controller is actually active, driven by `res/controller_profiles/*.json`
|
||||
(bundled with the app, copied to `/sdcard/QuestShock/controller_profiles/`
|
||||
on every launch, refreshing the known bundled files without touching any
|
||||
extra ones you add there yourself). Each file:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Meta Quest Touch",
|
||||
"interaction_profile": "/interaction_profiles/oculus/touch_controller",
|
||||
"image": "touch.png",
|
||||
"controls": [
|
||||
{"id": "right_b", "label": "B", "anchor_x": 0.76, "anchor_y": 0.22}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `interaction_profile` - the OpenXR interaction profile path this file
|
||||
describes; matched against whichever one is actually bound at runtime.
|
||||
Falls back to `generic.json` if nothing matches.
|
||||
- `image` - filename (same folder) of the controller illustration.
|
||||
- `controls` - one entry per labeled point on that illustration: `id` is a
|
||||
raw input identifier from the table above (thumbstick directions share
|
||||
one `left_thumbstick`/`right_thumbstick` entry, since they're the same
|
||||
physical stick); `anchor_x`/`anchor_y` are 0-1 fractions of the image for
|
||||
positioning that label's pointer line.
|
||||
|
||||
This is presentation data only - adding real support for a new controller
|
||||
still needs matching bindings in the app itself; a profile file with no
|
||||
corresponding code-level support just never gets matched.
|
||||
|
||||
### Fatal startup error
|
||||
|
||||
If the OpenXR runtime rejects any of the three supported controller
|
||||
profiles' bindings at launch (an app bug, not something you did), a
|
||||
non-cancelable dialog explains this and exits - this should never happen
|
||||
in a normal build.
|
||||
@@ -1,20 +1,28 @@
|
||||
# Assembles dist/ - a self-contained, runnable copy of System Shock -
|
||||
# from the compiled engine (built via Docker, see build-image.sh/
|
||||
# run-image.sh) and the game assets extracted from a purchased copy (see
|
||||
# Assembles dist/ (Linux) and dist-win/ (Windows, cross-compiled via
|
||||
# MinGW) - self-contained, runnable copies of System Shock - from the
|
||||
# compiled engine (built via Docker, see build-image.sh/run-image.sh) and
|
||||
# the game assets extracted from a purchased copy (see
|
||||
# res/assets/extract_assets.sh). Building the engine needs the build-image
|
||||
# (./build-image.sh, once); everything else here is plain file copying.
|
||||
# `dist-linux`/`dist-win` build just one platform; plain `dist` builds
|
||||
# both.
|
||||
#
|
||||
# `make package` instead builds a redistributable tarball that omits the
|
||||
# proprietary game assets entirely (see res/assets/GET_ASSETS.txt, which
|
||||
# it ships in their place) - this is what CI publishes.
|
||||
# `make package` instead builds redistributable archives (a .tar.gz for
|
||||
# Linux, a .zip for Windows) that omit the proprietary game assets
|
||||
# entirely (see res/assets/GET_ASSETS.txt, which they ship in their
|
||||
# place) - this is what CI publishes. Same dist-linux/dist-win/(both)
|
||||
# split via package-linux/package-win/package.
|
||||
|
||||
DIST_DIR := dist
|
||||
DIST_WIN_DIR := dist-win
|
||||
ENGINE_OUT := engine/.build-output
|
||||
ENGINE_OUT_WIN := engine/.build-output-win
|
||||
ASSETS_DIR := res/assets/ss_ee
|
||||
BUILD_DIR := build
|
||||
ARCH := $(shell uname -m)
|
||||
|
||||
.PHONY: all dist build-image engine assets package apk clean
|
||||
.PHONY: all dist dist-linux dist-win build-image engine engine-win assets \
|
||||
package package-linux package-win apk android-studio clean
|
||||
|
||||
all: dist
|
||||
|
||||
@@ -40,6 +48,16 @@ engine:
|
||||
./run-image.sh; \
|
||||
fi
|
||||
|
||||
# Cross-compiles engine/ for Windows via MinGW - see build-engine-win.sh
|
||||
# for why this can't share build-engine.sh's engine/build_ext/ (a scratch
|
||||
# copy is used instead). Same QUESTSHOCK_BUILD_IMAGE detection as `engine`.
|
||||
engine-win:
|
||||
@if [ -n "$$QUESTSHOCK_BUILD_IMAGE" ]; then \
|
||||
bash build-image/build-engine-win.sh; \
|
||||
else \
|
||||
./run-image.sh bash build-image/build-engine-win.sh; \
|
||||
fi
|
||||
|
||||
# Fails with a pointer to extract_assets.sh if the purchased game assets
|
||||
# haven't been extracted yet.
|
||||
assets:
|
||||
@@ -49,7 +67,10 @@ assets:
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
dist: engine assets
|
||||
# Builds both platforms. `dist-linux`/`dist-win` build just one.
|
||||
dist: dist-linux dist-win
|
||||
|
||||
dist-linux: engine assets
|
||||
@echo "== Assembling $(DIST_DIR) =="
|
||||
rm -rf "$(DIST_DIR)/systemshock" "$(DIST_DIR)/lib" "$(DIST_DIR)/res" \
|
||||
"$(DIST_DIR)/shaders" "$(DIST_DIR)/run.sh"
|
||||
@@ -64,6 +85,27 @@ dist: engine assets
|
||||
chmod +x "$(DIST_DIR)/run.sh"
|
||||
@echo "== Done - run $(DIST_DIR)/run.sh to play =="
|
||||
|
||||
# Windows counterpart of dist-linux. Unlike dist/, the DLLs sit flat
|
||||
# alongside systemshock.exe instead of in a lib/ subdirectory - Windows'
|
||||
# default DLL search order already checks the executable's own directory
|
||||
# first, so (unlike run.sh's LD_LIBRARY_PATH) run.bat needs no extra
|
||||
# wiring for that.
|
||||
dist-win: engine-win assets
|
||||
@echo "== Assembling $(DIST_WIN_DIR) =="
|
||||
rm -rf "$(DIST_WIN_DIR)"
|
||||
mkdir -p "$(DIST_WIN_DIR)/res/data" "$(DIST_WIN_DIR)/res/sound"
|
||||
cp "$(ENGINE_OUT_WIN)/systemshock.exe" "$(ENGINE_OUT_WIN)"/*.dll "$(DIST_WIN_DIR)/"
|
||||
cp "$(ENGINE_OUT_WIN)/soundfont.sf2" "$(DIST_WIN_DIR)/res/"
|
||||
cp -a engine/shaders "$(DIST_WIN_DIR)/shaders"
|
||||
cp -a "$(ASSETS_DIR)/data/." "$(DIST_WIN_DIR)/res/data/"
|
||||
cp -a "$(ASSETS_DIR)/sound/." "$(DIST_WIN_DIR)/res/sound/"
|
||||
cp res/run.bat "$(DIST_WIN_DIR)/run.bat"
|
||||
@echo "== Done - run $(DIST_WIN_DIR)/run.bat (or systemshock.exe) to play =="
|
||||
|
||||
# Builds both platforms' redistributable archives. `package-linux`/
|
||||
# `package-win` build just one.
|
||||
package: package-linux package-win
|
||||
|
||||
# Builds a versioned, redistributable Linux release tarball at
|
||||
# dist/shockolate-<version>-linux-<arch>.tar.gz - everything needed to
|
||||
# run except the proprietary game assets (res/GET_ASSETS.txt explains how
|
||||
@@ -72,7 +114,7 @@ dist: engine assets
|
||||
# tag to drive a release. Override with `make package VERSION=1.2.3`, or
|
||||
# just run it untagged for a local dev build (gets a 0.0.0-dev+<sha>
|
||||
# placeholder version, with a warning).
|
||||
package: engine
|
||||
package-linux: engine
|
||||
@git config --global --add safe.directory "$$(pwd)" 2>/dev/null || true
|
||||
@V="$$(VERSION="$(VERSION)" ./build-image/version.sh)"; \
|
||||
PKG_NAME="shockolate-$$V-linux-$(ARCH)"; \
|
||||
@@ -95,6 +137,33 @@ package: engine
|
||||
rm -rf "$(BUILD_DIR)/package"; \
|
||||
echo "Wrote $(DIST_DIR)/$$PKG_NAME.tar.gz"
|
||||
|
||||
# Windows counterpart of package-linux: dist/shockolate-<version>-windows-
|
||||
# x86_64.zip. Same versioning (build-image/version.sh) and VERSION=
|
||||
# override. Needs `zip` on whatever host runs `make package-win` itself
|
||||
# (unlike engine-win's own build, packaging isn't run inside the
|
||||
# build-image) - see README's Desktop build prerequisites.
|
||||
package-win: engine-win
|
||||
@git config --global --add safe.directory "$$(pwd)" 2>/dev/null || true
|
||||
@command -v zip >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: zip not found in PATH" >&2; exit 1; }
|
||||
@V="$$(VERSION="$(VERSION)" ./build-image/version.sh)"; \
|
||||
PKG_NAME="shockolate-$$V-windows-x86_64"; \
|
||||
PKG_STAGE="$(BUILD_DIR)/package/$$PKG_NAME"; \
|
||||
echo "Packaging $$PKG_NAME"; \
|
||||
rm -rf "$$PKG_STAGE"; \
|
||||
mkdir -p "$$PKG_STAGE/res"; \
|
||||
cp "$(ENGINE_OUT_WIN)/systemshock.exe" "$(ENGINE_OUT_WIN)"/*.dll "$$PKG_STAGE/"; \
|
||||
cp -a engine/shaders "$$PKG_STAGE/shaders"; \
|
||||
cp "$(ENGINE_OUT_WIN)/soundfont.sf2" "$$PKG_STAGE/res/"; \
|
||||
cp res/assets/GET_ASSETS.txt "$$PKG_STAGE/res/GET_ASSETS.txt"; \
|
||||
cp res/run.bat "$$PKG_STAGE/run.bat"; \
|
||||
cp LICENSE "$$PKG_STAGE/LICENSE"; \
|
||||
cp engine/LICENSE "$$PKG_STAGE/LICENSE.Shockolate"; \
|
||||
cp NOTICE.txt "$$PKG_STAGE/NOTICE.txt"; \
|
||||
mkdir -p "$(DIST_DIR)"; \
|
||||
(cd "$(BUILD_DIR)/package" && zip -rq "$(CURDIR)/$(DIST_DIR)/$$PKG_NAME.zip" "$$PKG_NAME"); \
|
||||
rm -rf "$(BUILD_DIR)/package"; \
|
||||
echo "Wrote $(DIST_DIR)/$$PKG_NAME.zip"
|
||||
|
||||
# Builds the Quest APK (see build-image/build-apk.sh and
|
||||
# android/engine-patches/ - engine/ itself is never modified; a patch is
|
||||
# applied to a scratch copy at build time instead). Same
|
||||
@@ -108,8 +177,23 @@ apk:
|
||||
./run-image.sh bash build-image/build-apk.sh; \
|
||||
fi
|
||||
|
||||
# Stages a scratch, patched copy of engine/ and android/gl4es-src/, plus
|
||||
# the Android SDL2/SDL2_mixer/fluidsynth-lite/openxr prebuilts, with
|
||||
# host-resolvable paths (writes android/engine.properties) so Android
|
||||
# Studio can compile and deploy android/ natively - see README's "5.2.
|
||||
# Building natively in Android Studio". `make apk` doesn't need this; it
|
||||
# stages the same things itself inside the build-image. Re-run after
|
||||
# `make clean` (which deletes android/engine.properties) or after
|
||||
# changing engine/, android/engine-patches/, android/gl4es-src/,
|
||||
# android/gl4es-patches/, or the prebuilt-library versions in
|
||||
# build-image/Dockerfile - Android Studio's own Gradle build already
|
||||
# does this automatically via its stageEngine task, so this is mainly
|
||||
# useful for first-time setup or confirming staging succeeded on its own.
|
||||
android-studio:
|
||||
./run-image.sh bash build-image/prepare-android-project.sh --host-paths
|
||||
|
||||
clean:
|
||||
rm -rf "$(DIST_DIR)" "$(BUILD_DIR)" "$(ENGINE_OUT)" \
|
||||
rm -rf "$(DIST_DIR)" "$(DIST_WIN_DIR)" "$(BUILD_DIR)" "$(ENGINE_OUT)" "$(ENGINE_OUT_WIN)" \
|
||||
engine/build_ext engine/CMakeCache.txt engine/CMakeFiles \
|
||||
engine/cmake_install.cmake engine/Makefile engine/systemshock \
|
||||
engine/src/Libraries/CMakeFiles \
|
||||
|
||||
@@ -8,5 +8,30 @@ and is not relicensed by this project's MIT license.
|
||||
|
||||
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.
|
||||
System Shock is a trademark of its respective owners. This is an unofficial
|
||||
fan project and is not affiliated with, endorsed by or supported by them.
|
||||
|
||||
The Android build additionally bundles:
|
||||
|
||||
- GL4ES (https://github.com/ptitSeb/gl4es), vendored and patched (see
|
||||
android/gl4es-patches/) and compiled from source at APK build time as
|
||||
lib/arm64-v8a/libGL.so. It is MIT-licensed.
|
||||
- The Khronos Group's OpenXR-SDK loader
|
||||
(https://github.com/KhronosGroup/OpenXR-SDK), prebuilt unmodified as
|
||||
lib/arm64-v8a/libopenxr_loader.so. It is Apache 2.0-licensed.
|
||||
|
||||
The Windows build (cross-compiled via MinGW - see README's "4.2. Windows
|
||||
cross-build") additionally bundles:
|
||||
|
||||
- GLEW (http://glew.sourceforge.net/), unmodified, as glew32.dll - used
|
||||
for OpenGL extension loading, which Windows' own opengl32.dll doesn't
|
||||
provide past OpenGL 1.1 (Linux instead gets this straight from Mesa's
|
||||
headers, needing no separate loader library - see
|
||||
engine/src/MacSrc/OpenGL.cc). It is licensed under a combination of the
|
||||
Modified BSD License, the MIT License, and the Khronos License, all
|
||||
permissive.
|
||||
- The MinGW-w64 runtime's winpthreads library, as libwinpthread-1.dll. It
|
||||
is MIT-licensed. (GCC's own runtime, libgcc/libstdc++, is linked
|
||||
statically into systemshock.exe instead of shipped as a DLL, under the
|
||||
GCC Runtime Library Exception - this doesn't subject the rest of the
|
||||
binary to the GPL.)
|
||||
|
||||
@@ -4,37 +4,83 @@ apply plugin: 'com.android.application'
|
||||
// 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.
|
||||
// engineDir is the scratch, patched copy of engine/; gl4esDir is the
|
||||
// scratch, patched copy of android/gl4es-src/ (built from source as a
|
||||
// CMake subdirectory of engine/'s own native build below, not prebuilt);
|
||||
// prebuiltDir is the Android SDL2/SDL2_mixer/fluidsynth-lite/openxr
|
||||
// prebuilts - all 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 gl4esDir = engineProps.getProperty('gl4esDir')
|
||||
if (gl4esDir == null) {
|
||||
throw new GradleException("android/engine.properties is missing 'gl4esDir' - 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'
|
||||
// desktop tarball's versioning) via -PquestshockVersionName/Code. Building
|
||||
// directly from Android Studio (the normal day-to-day workflow - see
|
||||
// README) never passes that property, so the default below has to carry
|
||||
// its own weight rather than just being a fallback for some rare, other,
|
||||
// unsupported path: it runs `git describe` itself, the same way
|
||||
// engine/CMakeLists.txt's own PROJECT_REVERSION_STRING does for the
|
||||
// engine's *own* separate version string - "vX.Y.Z" on a clean tagged
|
||||
// commit, or "vX.Y.Z-N-gHASH[-dirty]" otherwise, so a build between
|
||||
// releases is still identifiable from the in-headset menu (see
|
||||
// MenuOverlay.java's header, which reads BuildConfig.VERSION_NAME).
|
||||
def gitDescribeVersionName = {
|
||||
try {
|
||||
def proc = ["git", "-C", projectDir.toString(), "describe", "--tags", "--always", "--dirty"].execute()
|
||||
proc.waitFor()
|
||||
if (proc.exitValue() == 0) {
|
||||
def out = proc.text.trim()
|
||||
if (out) {
|
||||
return out
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// git not on PATH, or this isn't a git checkout at all (e.g. a
|
||||
// source tarball) - fall through to the placeholder below.
|
||||
}
|
||||
return "0.0.0-dev"
|
||||
}
|
||||
def questshockVersionName = project.hasProperty('questshockVersionName') ? project.property('questshockVersionName') : gitDescribeVersionName()
|
||||
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.
|
||||
// Auto-refreshes the staged, patched engine/ and gl4es-src/ copies and
|
||||
// prebuilt libraries (see build-image/prepare-android-project.sh
|
||||
// --host-paths, and the engineDir/gl4esDir/prebuiltDir properties read
|
||||
// above) so a plain Android Studio build/run can never silently compile
|
||||
// against a stale scratch copy after engine/, android/engine-patches/,
|
||||
// android/gl4es-src/, or android/gl4es-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.
|
||||
//
|
||||
// This step only re-stages whatever is already baked into the build-image
|
||||
// (it re-runs prepare-android-project.sh inside the existing image, which
|
||||
// is fast - no compilation happens here) - engine/ and gl4es-src/ are both
|
||||
// then compiled from these scratch copies by the externalNativeBuild CMake
|
||||
// configure below (see android/engine-patches/02-android-opengl-es.patch's
|
||||
// add_subdirectory of gl4esDir), so Gradle's own incremental CMake/Ninja
|
||||
// build only recompiles what actually changed, same as any other native
|
||||
// source edit. This task does NOT rebuild the image itself: if you changed
|
||||
// build-image/Dockerfile (a real toolchain/dependency change, not
|
||||
// engine-patches/ or gl4es-patches/, which this task re-stages on its own),
|
||||
// you still need to run ./build-image.sh yourself first (a slow, deliberate
|
||||
// step - see the memory on build-image versioning) so the image actually
|
||||
// contains your changes before this task re-stages them.
|
||||
def repoRoot = file("${projectDir}/../..")
|
||||
def stageEngineTask = tasks.register("stageEngine", Exec) {
|
||||
group = "build setup"
|
||||
@@ -55,10 +101,18 @@ def stageEngineTask = tasks.register("stageEngine", Exec) {
|
||||
|
||||
inputs.dir("${repoRoot}/engine")
|
||||
inputs.dir("${projectDir}/../engine-patches")
|
||||
inputs.dir("${projectDir}/../gl4es-src")
|
||||
inputs.dir("${projectDir}/../gl4es-patches")
|
||||
inputs.file("${repoRoot}/build-image/Dockerfile")
|
||||
// Staged into assets/ by prepare-android-project.sh.
|
||||
inputs.file("${repoRoot}/res/questshock.config.default.json")
|
||||
inputs.dir("${repoRoot}/res/controller_profiles")
|
||||
inputs.file("${repoRoot}/res/splash.png")
|
||||
outputs.dir("${repoRoot}/build/android-engine")
|
||||
outputs.dir("${repoRoot}/build/android-gl4es")
|
||||
outputs.dir("${repoRoot}/build/android-prebuilt")
|
||||
outputs.file("${projectDir}/../engine.properties")
|
||||
outputs.dir("${projectDir}/src/main/assets")
|
||||
}
|
||||
|
||||
// preBuild is what every variant's compile/native-build tasks already
|
||||
@@ -85,7 +139,7 @@ android {
|
||||
// 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
|
||||
// /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
|
||||
@@ -102,8 +156,12 @@ android {
|
||||
// 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.
|
||||
// prebuiltDir). ANDROID_GL4ES_DIR is read by
|
||||
// android/engine-patches/02-android-opengl-es.patch, which
|
||||
// add_subdirectory()'s it to compile gl4es from source as
|
||||
// part of this same CMake configure, instead of linking a
|
||||
// prebuilt .so - so gl4es-patches/ changes just need a normal
|
||||
// rebuild here, not a build-image rebuild.
|
||||
// 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,
|
||||
@@ -113,17 +171,28 @@ android {
|
||||
// -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.
|
||||
// the same for the prebuilt SDL2/SDL2_mixer/fluidsynth-lite
|
||||
// .so's this links against (gl4es is compiled here with the
|
||||
// same flag passed straight through, see its add_subdirectory
|
||||
// above).
|
||||
// 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,
|
||||
// android/engine-patches/10-android-openxr-cmake.patch,
|
||||
// which adds it to include_directories()).
|
||||
// USE_ANDROID_LOG/STATICLIB used to be passed to gl4es's own,
|
||||
// separate cmake invocation in build-image/Dockerfile - now
|
||||
// that it's add_subdirectory()'d into this same configure,
|
||||
// they need to be set here instead, or gl4es silently falls
|
||||
// back to stdio logging, which never reaches logcat. ANDROID
|
||||
// itself doesn't need setting here - the NDK toolchain file
|
||||
// already defines it before any CMakeLists.txt runs.
|
||||
arguments "-DENABLE_SDL2=ON", "-DENABLE_SOUND=BUNDLED", "-DENABLE_FLUIDSYNTH=BUNDLED", \
|
||||
"-DANDROID_PREBUILT_DIR=${prebuiltDir}", \
|
||||
"-DANDROID_GL4ES_DIR=${gl4esDir}", \
|
||||
"-DUSE_ANDROID_LOG=ON", "-DSTATICLIB=OFF", \
|
||||
"-DANDROID_EXTRA_INCLUDE_DIRS=${projectDir}/src/main/cpp", \
|
||||
"-DCMAKE_PREFIX_PATH=${prebuiltDir}/sdl2", \
|
||||
"-DCMAKE_FIND_ROOT_PATH=${prebuiltDir}/sdl2", \
|
||||
@@ -131,7 +200,7 @@ android {
|
||||
"-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"
|
||||
"-DANDROID_EXTRA_SOURCES=${projectDir}/src/main/cpp/questshock_native.c;${projectDir}/src/main/cpp/xr_session.c;${projectDir}/src/main/cpp/xr_input.c;${projectDir}/src/main/cpp/xr_overlay.c;${projectDir}/src/main/cpp/xr_swapchain.c;${projectDir}/src/main/cpp/xr_mouse.c;${projectDir}/src/main/cpp/qs_config.c;${projectDir}/src/main/cpp/qs_splash.c"
|
||||
abiFilters 'arm64-v8a'
|
||||
}
|
||||
}
|
||||
@@ -143,11 +212,37 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// The release key comes from the environment (build-image/build-apk.sh,
|
||||
// CI secrets). Without it there is no release signing, and the release
|
||||
// build type is left unsigned.
|
||||
def releaseKeystore = System.getenv("QUESTSHOCK_KEYSTORE")
|
||||
def releaseStorePassword = System.getenv("QUESTSHOCK_KEYSTORE_PASSWORD")
|
||||
signingConfigs {
|
||||
release {
|
||||
if (releaseKeystore) {
|
||||
storeFile file(releaseKeystore)
|
||||
storePassword releaseStorePassword
|
||||
keyAlias System.getenv("QUESTSHOCK_KEY_ALIAS") ?: "questshock"
|
||||
keyPassword System.getenv("QUESTSHOCK_KEY_PASSWORD") ?: releaseStorePassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
if (releaseKeystore) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generates BuildConfig (specifically BuildConfig.VERSION_NAME, read by
|
||||
// MenuOverlay.java's header) - off by default since AGP 8.0, so this
|
||||
// has to be requested explicitly.
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError false
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
OpenXR session to actually start. -->
|
||||
<meta-data android:name="com.oculus.vr.focusaware" android:value="true" />
|
||||
|
||||
<!-- Meta-specific: the headsets this app is built for. Other OpenXR
|
||||
runtimes ignore it. -->
|
||||
<meta-data android:name="com.oculus.supportedDevices"
|
||||
android:value="quest2|questpro|quest3|quest3s" />
|
||||
|
||||
<activity android:name="de.ladkau.questshock.QuestShockActivity"
|
||||
android:label="@string/app_name"
|
||||
android:alwaysRetainTaskState="true"
|
||||
@@ -51,13 +56,10 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- The vendor-neutral, Khronos-defined way to declare an
|
||||
immersive OpenXR entry point (vs. e.g. Meta's own
|
||||
com.oculus.intent.category.VR) - see this project's
|
||||
"favor OpenXR" rule in the README. If Horizon OS still
|
||||
launches this as a flat panel on-device, the next thing
|
||||
to try is adding a com.oculus.supportedDevices meta-data
|
||||
listing the target Quest models - not confirmed
|
||||
necessary from static analysis alone. -->
|
||||
immersive OpenXR entry point, plus Meta's own VR
|
||||
category, which Horizon OS looks for to treat the app as
|
||||
a VR app. Other runtimes ignore the Meta one. -->
|
||||
<category android:name="com.oculus.intent.category.VR" />
|
||||
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
#ifndef QUESTSHOCK_QS_CONFIG_H
|
||||
#define QUESTSHOCK_QS_CONFIG_H
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Persistent, human-editable JSON config file - see CONFIG.md at the repo
|
||||
// root for the documented schema. Lives at a plain relative path
|
||||
// ("questshock.config.json") since the whole engine already runs with its
|
||||
// cwd chdir()'d to the user-facing /sdcard/QuestShock/ folder (see
|
||||
// QuestShockActivity.java's nativeChdir()) before main() ever runs - the
|
||||
// same way prefs.txt/keybinds.txt (Prefs.c) already do.
|
||||
//
|
||||
// Deliberately independent of prefs.txt: this file is never read or written
|
||||
// as a side effect of prefs.txt load/save, and vice versa.
|
||||
|
||||
// Parses the config file if it exists (silent no-op if it doesn't - this
|
||||
// never creates the file itself) and applies every recognized option
|
||||
// directly into gShockPrefs/mode_id. Call once, early in main() - see
|
||||
// android/engine-patches/21-android-config.patch - after LoadPrefs() (so the
|
||||
// config file can override prefs.txt) and before init_all() (so there's no
|
||||
// live-vs-restart distinction to worry about: nothing is initialized yet, so
|
||||
// every field takes full effect as part of this same startup).
|
||||
void qs_config_load_startup(void);
|
||||
|
||||
// Called from the "Save Configuration"/"Load Configuration" button JNI
|
||||
// handlers (questshock_native.c), which run on Android's UI thread. These
|
||||
// only enqueue a custom SDL event - the actual file I/O and any
|
||||
// gShockPrefs/mode_id/message_info() work happens on the engine's own SDL
|
||||
// main thread instead (see qs_config_handle_event() below), matching this
|
||||
// codebase's existing convention for crossing this exact thread boundary
|
||||
// (xr_session.c's xr_poll_events(), questshock_native.c's SDL_TEXTINPUT
|
||||
// push for keyboard input).
|
||||
void qs_config_request_save(void);
|
||||
void qs_config_request_load(void);
|
||||
|
||||
// Same cross-thread-dispatch reasoning as qs_config_request_save()/_load()
|
||||
// above - called from the Video Options tab's controls (MenuOverlay.java),
|
||||
// which are just as much on Android's UI thread as the Save/Load buttons.
|
||||
// opengl=true/false selects "opengl"/"software"; resolution_index is 0-4,
|
||||
// indexing the same table qs_config_current_resolution_index() below
|
||||
// reads. Applies live immediately (no restart involved - see CONFIG.md);
|
||||
// unlike Load Configuration, these don't post a message_info() HUD
|
||||
// notification for every single tap - the tab's own selection highlight is
|
||||
// the feedback.
|
||||
void qs_config_request_set_renderer(bool opengl);
|
||||
void qs_config_request_set_resolution(int resolution_index);
|
||||
|
||||
// Same cross-thread-dispatch reasoning as the two above - called from the
|
||||
// Main Menu tab's Language row (MenuOverlay.java). language_index is 0
|
||||
// English/1 French/2 German (gShockPrefs.goLanguage's own numbering,
|
||||
// Prefs.h). Applies live immediately, the same way the game's own options
|
||||
// menu applies a language change (see qs_config.c's qs_apply_language()).
|
||||
void qs_config_request_set_language(int language_index);
|
||||
|
||||
// Same cross-thread dispatch as the three above - called from the Main Menu
|
||||
// tab's "Reset Position" button. Re-anchors the player's position in front of
|
||||
// the game quad (see xr_input_reset_player_position()); never reads or writes
|
||||
// the config file.
|
||||
void qs_config_request_reset_position(void);
|
||||
|
||||
// Same cross-thread-dispatch reasoning as the four above - called from the
|
||||
// Video Options tab's "Initial Screen"/"Game Screen" width/distance
|
||||
// preset rows (MenuOverlay.java). preset_index indexes qs_config.c's own
|
||||
// shared, offset+step-generated width/distance preset lists (both in
|
||||
// meters; the offset/step themselves are config-file-only, not exposed
|
||||
// as buttons), read back via the four qs_config_current_*_screen_*_index()
|
||||
// getters below. Applies live immediately if the just-changed profile is
|
||||
// the currently active one (see CONFIG.md) - otherwise just updates the
|
||||
// stored preference, invisibly, until that profile becomes active (e.g.
|
||||
// tweaking "Initial Screen" mid-game).
|
||||
void qs_config_request_set_initial_screen_width(int preset_index);
|
||||
void qs_config_request_set_initial_screen_distance(int preset_index);
|
||||
void qs_config_request_set_game_screen_width(int preset_index);
|
||||
void qs_config_request_set_game_screen_distance(int preset_index);
|
||||
|
||||
// Cross-thread requests from the Controller Options tab (MenuOverlay.java),
|
||||
// enqueued like every request_*() above. action_index indexes
|
||||
// xr_input_action_name()'s list. Begin/cancel start and stop rebind capture;
|
||||
// reset restores all bindings (the built-in defaults, then
|
||||
// questshock.config.default.json's player_controls group if it has one).
|
||||
void qs_config_request_begin_control_capture(int action_index);
|
||||
void qs_config_request_cancel_control_capture(void);
|
||||
void qs_config_request_reset_player_controls(void);
|
||||
|
||||
// Called from xr_input.c's xr_input_process_capture() on the engine's own
|
||||
// thread, so no cross-thread dispatch is needed (as with
|
||||
// qs_screen_profile_notify_loop_mode() below). Resolves
|
||||
// MenuOverlay.onControlCaptureResult(int, String) for action_index (raw_name
|
||||
// NULL on timeout or cancel), and again for displaced_action_index if the new
|
||||
// binding took the input from another action (-1/NULL when none), so both
|
||||
// rows can be updated together.
|
||||
void qs_config_notify_control_capture_result(int action_index, const char *raw_name,
|
||||
int displaced_action_index,
|
||||
const char *displaced_raw_name);
|
||||
|
||||
// Called from xr_input.c whenever the VR menu's visibility changes. Showing
|
||||
// the menu pauses a running game (not the main menu, intro or cutscenes) that
|
||||
// is not already paused; hiding it unpauses only if the menu did the pausing.
|
||||
// Enqueued like the request_*() functions above; the pause itself runs on the
|
||||
// engine's thread at pump_events() time.
|
||||
void qs_config_notify_menu_visibility(bool visible);
|
||||
|
||||
// Cheats tab (MenuOverlay.java). Same enqueue-only requests as above; the work
|
||||
// runs on the engine's thread. cheat_id: 0 give everything, 1 toggle physics,
|
||||
// 2 next level, 3 previous level, 4-7 toggle unlimited health, energy, ammo,
|
||||
// items (grenades and drugs). Cheats need a running game (the unlimited
|
||||
// switches can be set anywhere). The getters are plain scalar reads, safe from
|
||||
// the UI thread; the unlimited mask has bit 0 health, 1 energy, 2 ammo, 3 items.
|
||||
void qs_config_request_cheat(int cheat_id);
|
||||
int qs_config_cheat_unlimited_mask(void);
|
||||
bool qs_config_cheat_physics_running(void);
|
||||
|
||||
// Called once per frame from xr_input.c on the engine's thread; applies the
|
||||
// unlimited-resource cheats that are switched on.
|
||||
void qs_config_frame_tick(void);
|
||||
|
||||
// Called from xr_input.c's xr_input_suggest_bindings() when the OpenXR runtime
|
||||
// rejects a supported interaction profile's suggested bindings at startup.
|
||||
// That is an app bug, not a config problem, so it shows
|
||||
// MenuOverlay.showFatalControllerBindingError()'s non-cancelable "Exit" dialog;
|
||||
// xr_input_init() aborts right after.
|
||||
void qs_config_report_fatal_input_error(const char *profile_display_name);
|
||||
|
||||
// Returns true and handles the event if it is one of the custom events the
|
||||
// request_*()/notify functions above enqueue. Called from pump_events()
|
||||
// (sdl_events.c, see android/engine-patches/21-android-config.patch) on the
|
||||
// engine's thread, once per polled event. Returns false for any other event
|
||||
// type, including before the custom types are registered.
|
||||
bool qs_config_handle_event(const SDL_Event *event);
|
||||
|
||||
// Called directly (no event-queue dispatch - see
|
||||
// android/engine-patches/22-android-screen-profile.patch and this
|
||||
// function's own definition in qs_config.c for why) from the engine's
|
||||
// mainloop.c whenever the loop mode changes, and once from
|
||||
// qs_config_load_startup() - picks between the "Initial Screen" (menu/
|
||||
// intro) and "Game Screen" (actual gameplay) presets based on new_mode
|
||||
// (mainloop.h's SETUP_LOOP/CUTSCENE_LOOP vs. everything else) and applies
|
||||
// the newly-active one's shape to xr_session.c, but only if it's actually
|
||||
// different from what was already active.
|
||||
void qs_screen_profile_notify_loop_mode(short new_mode);
|
||||
|
||||
// Read-only queries for the Video Options tab's own selection highlight
|
||||
// (MenuOverlay.java re-reads these whenever the tab is (re)shown, so it
|
||||
// reflects the live value even if it was last changed some other way, e.g.
|
||||
// the Ctrl+G hotkey or Load Configuration). Safe to call directly from
|
||||
// Android's UI thread despite not going through the event-queue dispatch
|
||||
// the mutating calls above use - same low-risk reasoning already used for
|
||||
// Save Configuration's own reads of gShockPrefs/mode_id: these are simple,
|
||||
// independent POD scalar reads, not a sequence that needs to be atomic
|
||||
// with anything else.
|
||||
bool qs_config_current_renderer_is_opengl(void);
|
||||
int qs_config_current_resolution_index(void);
|
||||
|
||||
// -1 (not just an unlikely fallback, a routine, expected result here -
|
||||
// unlike qs_config_current_resolution_index() above) if the profile's
|
||||
// current value doesn't exactly match one of qs_config.c's own
|
||||
// offset+step-generated width/distance presets, e.g. after a hand-edited
|
||||
// config file sets an in-between value - MenuOverlay.java's own
|
||||
// setSelected() naturally shows no button selected in that case, no
|
||||
// special-casing needed.
|
||||
int qs_config_current_initial_screen_width_index(void);
|
||||
int qs_config_current_initial_screen_distance_index(void);
|
||||
int qs_config_current_game_screen_width_index(void);
|
||||
int qs_config_current_game_screen_distance_index(void);
|
||||
|
||||
// Returns the value preset button `index` (0..4) currently shows for the
|
||||
// width/distance option - shared by both "Initial Screen" and "Game
|
||||
// Screen" columns (their offset/step is config-file-only, not per-profile
|
||||
// - see qs_config.c's own comment on the underlying statics), so
|
||||
// MenuOverlay.java can label its preset buttons correctly even after a
|
||||
// Load Configuration changes the offset/step. Index is clamped into
|
||||
// range, not validated - every index 0..4 always has a value.
|
||||
float qs_config_screen_width_preset_value(int index);
|
||||
float qs_config_screen_distance_preset_value(int index);
|
||||
|
||||
// gShockPrefs.goLanguage (0 English/1 French/2 German, Prefs.h) - read by
|
||||
// MenuOverlay.java's/KeyboardOverlay.java's periodic header-refresh timer
|
||||
// to pick which language's chrome text (Strings.java) to display, so a
|
||||
// language change made mid-session via the game's own options menu (or our
|
||||
// own Language row, or Load Configuration) is picked up without needing to
|
||||
// reopen either panel; also doubles as the Main Menu tab's Language row's
|
||||
// own selection highlight, same as qs_config_current_renderer_is_opengl()/
|
||||
// qs_config_current_resolution_index() above. Same off-thread-scalar-read
|
||||
// reasoning as those two getters.
|
||||
int qs_config_current_language(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // QUESTSHOCK_QS_CONFIG_H
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "qs_splash.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include <android/log.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "xr_session.h"
|
||||
|
||||
#define QS_SPLASH_HOLD_MS 2500
|
||||
#define QS_SPLASH_FADE_MS 500
|
||||
#define QS_SPLASH_TOTAL_MS (QS_SPLASH_HOLD_MS + 2 * QS_SPLASH_FADE_MS)
|
||||
// The XR session only starts once frames are polled, so the display time is
|
||||
// counted from the first submitted frame; this bounds the wait for it.
|
||||
#define QS_SPLASH_SESSION_TIMEOUT_MS 5000
|
||||
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "QuestShock", __VA_ARGS__)
|
||||
|
||||
extern void pump_events(void);
|
||||
// OpenGL.cc (engine patch 11): draws a software frame into the XR game swapchain.
|
||||
extern void android_composite_software_frame(SDL_Surface *ui);
|
||||
|
||||
static SDL_Surface *s_image = NULL;
|
||||
|
||||
void qs_splash_set_image(const uint32_t *argb, int width, int height) {
|
||||
if (s_image != NULL) {
|
||||
SDL_FreeSurface(s_image);
|
||||
s_image = NULL;
|
||||
}
|
||||
if (argb == NULL || width <= 0 || height <= 0)
|
||||
return;
|
||||
s_image = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
if (s_image == NULL)
|
||||
return;
|
||||
for (int y = 0; y < height; y++)
|
||||
memcpy((uint8_t *)s_image->pixels + (size_t)y * s_image->pitch, argb + (size_t)y * width,
|
||||
(size_t)width * sizeof(uint32_t));
|
||||
SDL_SetSurfaceBlendMode(s_image, SDL_BLENDMODE_BLEND);
|
||||
}
|
||||
|
||||
// 0-255 brightness at `elapsed` ms after the first frame.
|
||||
static int qs_splash_level(uint32_t elapsed) {
|
||||
uint32_t edge = elapsed < QS_SPLASH_TOTAL_MS - elapsed ? elapsed : QS_SPLASH_TOTAL_MS - elapsed;
|
||||
if (edge >= QS_SPLASH_FADE_MS)
|
||||
return 255;
|
||||
return (int)(255 * edge / QS_SPLASH_FADE_MS);
|
||||
}
|
||||
|
||||
void qs_splash_show(void) {
|
||||
if (s_image == NULL) {
|
||||
LOGI("Startup splash: no image");
|
||||
return;
|
||||
}
|
||||
int width, height;
|
||||
xr_get_game_swapchain_size(&width, &height);
|
||||
if (width <= 0 || height <= 0) {
|
||||
LOGI("Startup splash: no XR game swapchain");
|
||||
return;
|
||||
}
|
||||
|
||||
// The image is stretched over the whole swapchain image, so it is
|
||||
// letterboxed into a canvas of that size to keep its aspect ratio.
|
||||
SDL_Surface *canvas =
|
||||
SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_RGBA32);
|
||||
if (canvas == NULL)
|
||||
return;
|
||||
const float scale = SDL_min((float)width / s_image->w, (float)height / s_image->h);
|
||||
SDL_Rect dst;
|
||||
dst.w = (int)(s_image->w * scale);
|
||||
dst.h = (int)(s_image->h * scale);
|
||||
dst.x = (width - dst.w) / 2;
|
||||
dst.y = (height - dst.h) / 2;
|
||||
|
||||
const uint32_t waitStart = SDL_GetTicks();
|
||||
uint32_t shownStart = 0;
|
||||
uint32_t elapsed = 0;
|
||||
int drawnLevel = -1;
|
||||
bool done = false;
|
||||
|
||||
while (!done) {
|
||||
pump_events();
|
||||
xr_poll_events();
|
||||
if (xr_frame_begin()) {
|
||||
if (shownStart == 0)
|
||||
shownStart = SDL_GetTicks();
|
||||
elapsed = SDL_GetTicks() - shownStart;
|
||||
if (elapsed >= QS_SPLASH_TOTAL_MS) {
|
||||
elapsed = QS_SPLASH_TOTAL_MS;
|
||||
done = true;
|
||||
}
|
||||
const int level = qs_splash_level(elapsed);
|
||||
if (level != drawnLevel) {
|
||||
drawnLevel = level;
|
||||
SDL_FillRect(canvas, NULL, SDL_MapRGB(canvas->format, 0, 0, 0));
|
||||
SDL_SetSurfaceColorMod(s_image, level, level, level);
|
||||
SDL_BlitScaled(s_image, NULL, canvas, &dst);
|
||||
}
|
||||
android_composite_software_frame(canvas);
|
||||
} else {
|
||||
SDL_Delay(10);
|
||||
if (shownStart == 0 && SDL_GetTicks() - waitStart > QS_SPLASH_SESSION_TIMEOUT_MS)
|
||||
done = true;
|
||||
}
|
||||
xr_frame_end();
|
||||
}
|
||||
|
||||
LOGI("Startup splash: %s after %u ms", shownStart != 0 ? "shown" : "XR session did not start",
|
||||
(unsigned)(SDL_GetTicks() - waitStart));
|
||||
SDL_FreeSurface(canvas);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Startup splash: the QuestShock title, shown on the game screen before the
|
||||
// engine's own splash sequence.
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Stores a copy of the splash image (straight-alpha ARGB, one int per pixel).
|
||||
// Called from JNI before the engine starts; replaces any earlier image. Without
|
||||
// an image qs_splash_show() does nothing.
|
||||
void qs_splash_set_image(const uint32_t *argb, int width, int height);
|
||||
|
||||
// Engine thread only (called from engine patch 27). Fades the image in and out
|
||||
// on the game screen and returns when done, or after a few seconds if the XR
|
||||
// session never starts.
|
||||
void qs_splash_show(void);
|
||||
@@ -4,12 +4,295 @@
|
||||
// needs, compiled into the same "main" library as engine/ (see
|
||||
// android/engine-patches/01-android-shared-lib.patch's ANDROID_EXTRA_SOURCES)
|
||||
// - engine/ itself is never modified.
|
||||
#include <ctype.h>
|
||||
#include <jni.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include "qs_config.h"
|
||||
#include "qs_splash.h"
|
||||
#include "xr_input.h"
|
||||
#include "xr_overlay.h"
|
||||
#include "xr_session.h"
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_QuestShockActivity_nativeChdir(JNIEnv *env, jclass clazz, jstring path) {
|
||||
const char *cpath = (*env)->GetStringUTFChars(env, path, NULL);
|
||||
chdir(cpath);
|
||||
(*env)->ReleaseStringUTFChars(env, path, cpath);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_de_ladkau_questshock_QuestShockActivity_nativeSetSplashImage(
|
||||
JNIEnv *env, jclass clazz, jintArray pixels, jint width, jint height) {
|
||||
jint *data = (*env)->GetIntArrayElements(env, pixels, NULL);
|
||||
if (data == NULL)
|
||||
return;
|
||||
qs_splash_set_image((const uint32_t *)data, width, height);
|
||||
(*env)->ReleaseIntArrayElements(env, pixels, data, JNI_ABORT);
|
||||
}
|
||||
|
||||
// KeyboardOverlay's hand-built on-screen keyboard (see
|
||||
// KeyboardOverlay.handleCharKeyDown()/handleCharKeyUp()) has no real
|
||||
// Android IME session behind it, so printable characters can't reach the
|
||||
// game via Android's usual IME path - instead this builds and pushes an
|
||||
// SDL_TEXTINPUT event directly, the same event type/shape SDL's own
|
||||
// Android backend pushes for typed text, using only the public
|
||||
// SDL_Event/SDL_PushEvent API. The engine's pump_events() (sdl_events.c)
|
||||
// only picks up printable characters from this event, not from
|
||||
// SDL_KEYDOWN.
|
||||
//
|
||||
// pump_events()'s SDL_TEXTINPUT handling sets sshockKeyStates[code] for
|
||||
// this character and only ever clears it again on a matching SDL_KEYUP.
|
||||
// Any character bound to a continuously-polled motion key (space -> jump,
|
||||
// w -> forward, etc., see MacSrc/Prefs.c's default keybinds.txt) is
|
||||
// polled every physics tick for as long as that state stays set, exactly
|
||||
// like a real held key - so nativeSendPrintableCharDown()/Up() are called
|
||||
// from the key button's own ACTION_DOWN/ACTION_UP (see
|
||||
// KeyboardOverlay.charKeyTouchListener()), not both at once: pushing the
|
||||
// release in the same call as the press would set and clear
|
||||
// sshockKeyStates[code] within a single pump_events() call, before the
|
||||
// game loop's poll (which runs once per frame, before pump_events - see
|
||||
// GameSrc/mainloop.c) ever has a chance to observe the press.
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_KeyboardOverlay_nativeSendPrintableCharDown(JNIEnv *env, jclass clazz,
|
||||
jchar c) {
|
||||
SDL_Event event;
|
||||
SDL_zero(event);
|
||||
event.type = SDL_TEXTINPUT;
|
||||
event.text.timestamp = SDL_GetTicks();
|
||||
event.text.windowID = 0;
|
||||
event.text.text[0] = (char)c;
|
||||
event.text.text[1] = '\0';
|
||||
SDL_PushEvent(&event);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_KeyboardOverlay_nativeSendPrintableCharUp(JNIEnv *env, jclass clazz,
|
||||
jchar c) {
|
||||
SDL_Event releaseEvent;
|
||||
SDL_zero(releaseEvent);
|
||||
releaseEvent.type = SDL_KEYUP;
|
||||
releaseEvent.key.timestamp = SDL_GetTicks();
|
||||
releaseEvent.key.windowID = 0;
|
||||
releaseEvent.key.state = SDL_RELEASED;
|
||||
releaseEvent.key.repeat = 0;
|
||||
// SDL keycodes for printable characters are the character's own
|
||||
// (lowercased) byte value - see sdl_events.c's own comment on this.
|
||||
releaseEvent.key.keysym.sym = (SDL_Keycode)tolower((unsigned char)c);
|
||||
releaseEvent.key.keysym.mod = KMOD_NONE;
|
||||
SDL_PushEvent(&releaseEvent);
|
||||
}
|
||||
|
||||
// KeyboardOverlay's title-bar Close button - the keyboard is a fully
|
||||
// independent overlay quad from the menu launcher (see xr_overlay.h), so
|
||||
// this is the only way to hide it once it's open; the controller's menu
|
||||
// button only ever affects the menu launcher.
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_KeyboardOverlay_nativeRequestClose(JNIEnv *env, jclass clazz) {
|
||||
xr_overlay_set_visible(xr_session_get_keyboard_overlay(), false);
|
||||
}
|
||||
|
||||
// MenuOverlay's "Keyboard" button - only opens the keyboard overlay, never
|
||||
// touches the menu launcher's own visibility, so both can be shown
|
||||
// together.
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeShowKeyboard(JNIEnv *env, jclass clazz) {
|
||||
xr_overlay_set_visible(xr_session_get_keyboard_overlay(), true);
|
||||
}
|
||||
|
||||
// MenuOverlay's "Save Configuration"/"Load Configuration" buttons - see
|
||||
// qs_config.h for why these only enqueue an event rather than doing the
|
||||
// actual file/engine-state work here directly (this runs on Android's UI
|
||||
// thread, not the engine's own).
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSaveConfig(JNIEnv *env, jclass clazz) {
|
||||
qs_config_request_save();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestLoadConfig(JNIEnv *env, jclass clazz) {
|
||||
qs_config_request_load();
|
||||
}
|
||||
|
||||
// MenuOverlay's Video Options tab - live-appliable controls, see
|
||||
// qs_config.h. Same enqueue-only reasoning as the Save/Load buttons above.
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetRenderer(JNIEnv *env, jclass clazz,
|
||||
jboolean opengl) {
|
||||
qs_config_request_set_renderer(opengl);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetResolution(JNIEnv *env, jclass clazz,
|
||||
jint resolutionIndex) {
|
||||
qs_config_request_set_resolution(resolutionIndex);
|
||||
}
|
||||
|
||||
// MenuOverlay's Main Menu tab's Language row - same enqueue-only reasoning as above.
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetLanguage(JNIEnv *env, jclass clazz,
|
||||
jint languageIndex) {
|
||||
qs_config_request_set_language(languageIndex);
|
||||
}
|
||||
|
||||
// MenuOverlay's Main Menu tab's "Reset Position" button - same enqueue-only
|
||||
// reasoning as above.
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestResetPosition(JNIEnv *env, jclass clazz) {
|
||||
qs_config_request_reset_position();
|
||||
}
|
||||
|
||||
// MenuOverlay's Video Options tab - "Initial Screen"/"Game Screen" width/
|
||||
// distance preset rows, same enqueue-only reasoning as above.
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetInitialScreenWidth(JNIEnv *env, jclass clazz,
|
||||
jint presetIndex) {
|
||||
qs_config_request_set_initial_screen_width(presetIndex);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetGameScreenWidth(JNIEnv *env, jclass clazz,
|
||||
jint presetIndex) {
|
||||
qs_config_request_set_game_screen_width(presetIndex);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetInitialScreenDistance(JNIEnv *env,
|
||||
jclass clazz,
|
||||
jint presetIndex) {
|
||||
qs_config_request_set_initial_screen_distance(presetIndex);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetGameScreenDistance(JNIEnv *env, jclass clazz,
|
||||
jint presetIndex) {
|
||||
qs_config_request_set_game_screen_distance(presetIndex);
|
||||
}
|
||||
|
||||
// Read-only queries - safe to call directly (no enqueue needed, see
|
||||
// qs_config.h's own comment on each of these).
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetRendererIsOpenGl(JNIEnv *env, jclass clazz) {
|
||||
return qs_config_current_renderer_is_opengl();
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetResolutionIndex(JNIEnv *env, jclass clazz) {
|
||||
return qs_config_current_resolution_index();
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetInitialScreenWidthIndex(JNIEnv *env, jclass clazz) {
|
||||
return qs_config_current_initial_screen_width_index();
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetGameScreenWidthIndex(JNIEnv *env, jclass clazz) {
|
||||
return qs_config_current_game_screen_width_index();
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetInitialScreenDistanceIndex(JNIEnv *env,
|
||||
jclass clazz) {
|
||||
return qs_config_current_initial_screen_distance_index();
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetGameScreenDistanceIndex(JNIEnv *env, jclass clazz) {
|
||||
return qs_config_current_game_screen_distance_index();
|
||||
}
|
||||
|
||||
// Preset button *labels* (not the current value) - shared by both
|
||||
// "Initial Screen" and "Game Screen" columns, since their offset/step is
|
||||
// config-file-only, not per-profile (see qs_config.h's own comment).
|
||||
JNIEXPORT jfloat JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetScreenWidthPresetValue(JNIEnv *env, jclass clazz,
|
||||
jint index) {
|
||||
return qs_config_screen_width_preset_value(index);
|
||||
}
|
||||
|
||||
JNIEXPORT jfloat JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetScreenDistancePresetValue(JNIEnv *env, jclass clazz,
|
||||
jint index) {
|
||||
return qs_config_screen_distance_preset_value(index);
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetLanguage(JNIEnv *env, jclass clazz) {
|
||||
return qs_config_current_language();
|
||||
}
|
||||
|
||||
// MenuOverlay's header - polled on a periodic timer (see its own comment),
|
||||
// not event-driven, so these are plain synchronous reads too.
|
||||
JNIEXPORT jfloat JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetFps(JNIEnv *env, jclass clazz) {
|
||||
return xr_session_get_fps();
|
||||
}
|
||||
|
||||
JNIEXPORT jfloat JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetFrameTimeMs(JNIEnv *env, jclass clazz) {
|
||||
return xr_session_get_frame_ms();
|
||||
}
|
||||
|
||||
JNIEXPORT jfloat JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetCpuUtilization(JNIEnv *env, jclass clazz) {
|
||||
return xr_session_get_cpu_utilization();
|
||||
}
|
||||
|
||||
JNIEXPORT jfloat JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetGpuUtilization(JNIEnv *env, jclass clazz) {
|
||||
return xr_session_get_gpu_utilization();
|
||||
}
|
||||
|
||||
// Controller Options tab: actionIndex follows xr_input.c's action order
|
||||
// (MenuOverlay.java and Strings.java hold the matching labels).
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestBeginControlCapture(JNIEnv *env, jclass clazz,
|
||||
jint actionIndex) {
|
||||
qs_config_request_begin_control_capture(actionIndex);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestCancelControlCapture(JNIEnv *env, jclass clazz) {
|
||||
qs_config_request_cancel_control_capture();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestResetPlayerControls(JNIEnv *env, jclass clazz) {
|
||||
qs_config_request_reset_player_controls();
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetControlBindingKey(JNIEnv *env, jclass clazz,
|
||||
jint actionIndex) {
|
||||
return (*env)->NewStringUTF(env, xr_input_get_action_binding_name(actionIndex));
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetActiveInteractionProfilePath(JNIEnv *env,
|
||||
jclass clazz) {
|
||||
return (*env)->NewStringUTF(env, xr_input_get_active_interaction_profile_path());
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeSetControllerOptionsTabOpen(JNIEnv *env, jclass clazz,
|
||||
jboolean open) {
|
||||
xr_input_set_controller_options_tab_open(open == JNI_TRUE);
|
||||
}
|
||||
|
||||
// Cheats tab: same enqueue-only pattern as the other requests.
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestCheat(JNIEnv *env, jclass clazz, jint cheatId) {
|
||||
qs_config_request_cheat(cheatId);
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetCheatUnlimitedMask(JNIEnv *env, jclass clazz) {
|
||||
return qs_config_cheat_unlimited_mask();
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetCheatPhysicsRunning(JNIEnv *env, jclass clazz) {
|
||||
return qs_config_cheat_physics_running();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// Controller input for questshock's immersive Quest build: one OpenXR
|
||||
// action set (aim pose + trigger click per hand, a menu-toggle button on
|
||||
// the left controller) plus ray/quad hit-testing. Each hand's ray is
|
||||
// tried against the keyboard overlay, then the menu overlay (see
|
||||
// xr_overlay.h, xr_session.c), then - only if neither actually claims it
|
||||
// this frame (landing on it now, or continuing a touch/drag begun on a
|
||||
// previous frame - not merely because that overlay happens to be visible)
|
||||
// - the game quad xr_session.c submits, forwarding trigger edges to
|
||||
// whichever overlay claims the ray as synthetic touches. The game quad
|
||||
// itself acts as a plain desktop-style mouse pointer into the engine's
|
||||
// own UI (see xr_mouse.h) - absolute cursor position while the ray hits
|
||||
// it, plus a left click on the trigger edge; a pending click there
|
||||
// likewise keeps claiming the ray ahead of the keyboard/menu until
|
||||
// released, so the eventual mouse-up isn't lost if the ray strays. Since
|
||||
// none of the three targets' own feedback (the overlays' on-quad cursor,
|
||||
// drawn by their Java views; the engine's own mouse cursor sprite for the
|
||||
// game quad) gives any indication while the ray is short of actually
|
||||
// landing on something, every claimed target also builds a thin,
|
||||
// billboarded laser-beam quad per hand from the controller to wherever
|
||||
// the ray currently crosses that target's plane (see
|
||||
// xr_input_get_beam_layer()) - so aiming looks and behaves the same
|
||||
// whether the target is an overlay or the game quad itself.
|
||||
#ifndef QUESTSHOCK_XR_INPUT_H
|
||||
#define QUESTSHOCK_XR_INPUT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define XR_USE_PLATFORM_ANDROID 1
|
||||
#define XR_USE_GRAPHICS_API_OPENGL_ES 1
|
||||
#include <openxr/openxr.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Call once, right after the session is created (see xr_session.c's
|
||||
// xr_create_instance_and_session()) - creates the action set/actions,
|
||||
// suggests Touch controller bindings, creates the per-hand aim action
|
||||
// spaces and a view-space reference (for the laser-beam billboard math),
|
||||
// creates the two per-hand beam swapchains (swapchain_format - the same
|
||||
// format shared by the game swapchain and both overlays), and attaches
|
||||
// the action set to the session. Returns false (logged, non-fatal - the
|
||||
// caller keeps rendering without input) if any of that fails.
|
||||
bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_format);
|
||||
|
||||
// Call once per frame from xr_frame_end(), before releasing the acquired
|
||||
// swapchain image - syncs this frame's action states (always, so edge
|
||||
// detection stays correct even on frames with nothing to draw), handles
|
||||
// the menu_toggle button's edge, and hit-tests/dispatches each hand's ray
|
||||
// against keyboard/menu/game quad in that priority order (see the file
|
||||
// comment above). draw gates only the laser-beam visuals (and the
|
||||
// head-pose locate that feeds them) - if false (nothing to draw into this
|
||||
// frame, e.g. no swapchain image was acquired), hit-testing and
|
||||
// touch-forwarding still run. baseSpace/time must match whatever
|
||||
// xr_frame_begin() used to predict this frame.
|
||||
void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw);
|
||||
|
||||
// Call once per hand (hand: 0=left, 1=right) after xr_input_sync_and_draw()
|
||||
// in the same frame - if that hand's ray claimed the keyboard, menu, or
|
||||
// game quad this frame (see the file comment above), fills *out_quad's
|
||||
// subImage/pose/size for its laser-beam ribbon and returns true.
|
||||
// space/eyeVisibility/layerFlags are left for the caller to set, same
|
||||
// convention as xr_overlay_render_and_build_layer(). Returns false
|
||||
// (out_quad untouched) if no beam should be shown for that hand this
|
||||
// frame.
|
||||
bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad);
|
||||
|
||||
// Recenters the player so the game quad ("virtual screen") appears directly
|
||||
// ahead of wherever they are standing and facing, from their current real head
|
||||
// pose. Never reads or writes questshock.config.json. Called automatically
|
||||
// once, from xr_input_sync_and_draw(), on the first frame with a valid head
|
||||
// pose after startup, unless a complete saved player_position was loaded (see
|
||||
// xr_input_skip_startup_recenter()): a saved position is relative to a
|
||||
// previous session's real-space origin, which is not stable across launches.
|
||||
// Returns false (no-op) if no valid head pose is available yet.
|
||||
bool xr_input_recenter_player_to_screen(void);
|
||||
|
||||
// The Main Menu tab's "Reset Position" button (via qs_config.c). Like
|
||||
// xr_input_recenter_player_to_screen(), but re-anchors only the player's
|
||||
// position to the centre in front of the game quad; the current facing (yaw)
|
||||
// is kept, so the screen does not rotate to follow the gaze. Returns false
|
||||
// (no-op) if no valid head pose is available yet.
|
||||
bool xr_input_reset_player_position(void);
|
||||
|
||||
// Called once, from qs_config_load_startup() (qs_config.c), when a
|
||||
// complete saved player_position was actually loaded from
|
||||
// questshock.config.json - cancels the pending automatic startup recenter
|
||||
// above so it doesn't immediately override the value that was just
|
||||
// applied. Safe to call regardless of whether xr_input_init() has even run
|
||||
// yet (this only touches the plain file-local flag, no XR calls).
|
||||
void xr_input_skip_startup_recenter(void);
|
||||
|
||||
// ===========================================================================
|
||||
// Remappable controls ("player_controls" config group; see qs_config.c and
|
||||
// CONFIG.md). menu_toggle is not among them and has no getter/setter: it is
|
||||
// fixed (see xr_input_sync_and_draw()).
|
||||
// ===========================================================================
|
||||
|
||||
// Number of remappable actions and each one's index<->name mapping, which
|
||||
// qs_config.c uses to parse/write the player_controls group without its own
|
||||
// copy of the list. The names are the keys used in questshock.config.json;
|
||||
// index_from_name() returns false for an unrecognized name.
|
||||
int xr_input_action_count(void);
|
||||
const char *xr_input_action_name(int action_index);
|
||||
bool xr_input_action_index_from_name(const char *name, int *out_index);
|
||||
|
||||
// Current binding for one action, as an opaque int and as its string form
|
||||
// (e.g. "right_b", "trigger", "none" for unbound), the same strings used in
|
||||
// questshock.config.json and reported by capture.
|
||||
int xr_input_get_action_binding(int action_index);
|
||||
const char *xr_input_get_action_binding_name(int action_index);
|
||||
|
||||
// Sets one action's binding from a config-file string. Returns false (binding
|
||||
// unchanged) for an unrecognized name, or for the symmetric "trigger"/
|
||||
// "squeeze" on an action other than mouse_click/mouse_double_click. Does not
|
||||
// enforce one-action-per-input the way capture does: a hand-edited file that
|
||||
// binds two actions to one input is accepted as-is.
|
||||
bool xr_input_set_action_binding(int action_index, const char *raw_name);
|
||||
|
||||
// Restores every action to its built-in default (the table xr_input_init()
|
||||
// starts from). qs_config.c calls this first for "Reset to Defaults", then
|
||||
// applies questshock.config.default.json's player_controls group on top if it
|
||||
// has one.
|
||||
void xr_input_reset_player_controls(void);
|
||||
|
||||
// Begins/cancels rebind capture for one action (an index into
|
||||
// xr_input_action_name()'s list): the next fresh button or thumbstick-direction
|
||||
// edge on any hand becomes that action's binding and is reported via
|
||||
// qs_config_notify_control_capture_result(); with no press, capture cancels
|
||||
// after 8 seconds. Called via qs_config.c's cross-thread request functions.
|
||||
// The menu button is never captured.
|
||||
void xr_input_capture_begin(int action_index);
|
||||
void xr_input_capture_cancel(void);
|
||||
|
||||
// The OpenXR interaction profile currently bound (e.g.
|
||||
// "/interaction_profiles/oculus/touch_controller"), refreshed once per frame.
|
||||
// MenuOverlay.java's Controller Options tab uses it to pick a file from
|
||||
// controller_profiles/ (see CONFIG.md). Empty until the first successful
|
||||
// query.
|
||||
const char *xr_input_get_active_interaction_profile_path(void);
|
||||
|
||||
// Set by MenuOverlay.java when the Controller Options tab becomes or stops
|
||||
// being the menu's current tab. While it is open, the menu's thumbstick
|
||||
// locomotion (snap turn/smooth move) is disabled.
|
||||
void xr_input_set_controller_options_tab_open(bool open);
|
||||
|
||||
void xr_input_shutdown(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "xr_mouse.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include "mouse.h" // engine/src/Libraries/INPUT/Source/mouse.h - mouse_put_xy()
|
||||
|
||||
void xr_mouse_move(int x, int y) { mouse_put_xy((short)x, (short)y); }
|
||||
|
||||
void xr_mouse_click(bool down) {
|
||||
SDL_Event event;
|
||||
SDL_zero(event);
|
||||
event.type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP;
|
||||
event.button.timestamp = SDL_GetTicks();
|
||||
event.button.windowID = 0;
|
||||
event.button.which = 0;
|
||||
event.button.button = SDL_BUTTON_LEFT;
|
||||
event.button.state = down ? SDL_PRESSED : SDL_RELEASED;
|
||||
event.button.clicks = 1;
|
||||
SDL_PushEvent(&event);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Bridges VR aim-ray hit-testing (xr_input.c) to the vendored engine's
|
||||
// mouse input (engine/src/Libraries/INPUT/Source/mouse.h) - lets the
|
||||
// controller's laser pointer drive the game's own UI (inventory, menus,
|
||||
// dialogs) exactly like a desktop mouse: absolute cursor position plus a
|
||||
// single left button, no relative/mouselook mode, no right button.
|
||||
#ifndef QUESTSHOCK_XR_MOUSE_H
|
||||
#define QUESTSHOCK_XR_MOUSE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Sets the engine's absolute mouse position (mouse_put_xy()) - call every
|
||||
// frame the aim ray is actually hitting the game quad; x/y are pixel
|
||||
// coordinates in the game's own logical resolution (see
|
||||
// xr_get_game_resolution() in xr_session.h), already clamped by the
|
||||
// caller. Do not call on frames the ray isn't hitting the game quad - the
|
||||
// cursor should hold its last position, not snap elsewhere.
|
||||
void xr_mouse_move(int x, int y);
|
||||
|
||||
// Pushes a synthetic SDL_MOUSEBUTTONDOWN/UP (SDL_BUTTON_LEFT) - matches
|
||||
// KeyboardOverlay's nativeSendPrintableCharDown()/Up() in
|
||||
// questshock_native.c: a plain SDL_Event built by hand and handed to the
|
||||
// public SDL_PushEvent(),
|
||||
// no engine patch needed. pump_events() (sdl_events.c) reads whatever
|
||||
// mouse_put_xy() last set, not any x/y carried on the event itself - call
|
||||
// xr_mouse_move() first if position needs updating this frame.
|
||||
void xr_mouse_click(bool down);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,503 @@
|
||||
#include "xr_overlay.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <android/log.h>
|
||||
#include <jni.h>
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl3.h>
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include "xr_swapchain.h"
|
||||
|
||||
#define TAG "QuestShock"
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
||||
|
||||
// Same constant (deliberately not <math.h>'s M_PI, which isn't used
|
||||
// anywhere else in this codebase and isn't guaranteed by the C standard -
|
||||
// only POSIX) as xr_session.c's own play-space rebuild math.
|
||||
#define QS_DEG_TO_RAD (3.14159265358979323846f / 180.0f)
|
||||
|
||||
struct XrOverlay {
|
||||
XrInstance instance; // for error logging only
|
||||
int width, height;
|
||||
float distance_m, half_width_m, half_height_m;
|
||||
float center_x_m, center_y_m;
|
||||
// The *authored* distance_m (config->distance_m at creation time),
|
||||
// never touched again afterward by anything, including
|
||||
// xr_overlay_set_pose() - kept separate from the live, mutable
|
||||
// distance_m above because that one stops meaning "how far in front of
|
||||
// the origin" once a rotated set_pose() call (the menu recenter)
|
||||
// repurposes it as a raw base-space Z coordinate; reusing *that* as a
|
||||
// "how far in front of me" magnitude on a later recenter would drift
|
||||
// further with every reopen instead of staying at a stable distance -
|
||||
// see xr_overlay_get_default_distance_m().
|
||||
float default_distance_m;
|
||||
// Rotation about the vertical axis, applied on top of center_x_m/
|
||||
// center_y_m/distance_m (see xr_overlay_set_pose()) - 0 for every
|
||||
// overlay except the menu launcher immediately after it's freshly
|
||||
// reopened (xr_input.c re-centers it in front of wherever the player
|
||||
// is currently facing, which needs this to keep the quad actually
|
||||
// facing them instead of whatever direction it started out at).
|
||||
float yaw_deg;
|
||||
bool visible;
|
||||
|
||||
XrSwapchainState swapchain;
|
||||
|
||||
jclass java_class;
|
||||
jmethodID native_init_method;
|
||||
jmethodID take_pixels_method;
|
||||
jmethodID dispatch_touch_method;
|
||||
jmethodID update_cursor_method;
|
||||
|
||||
// This overlay's content lives in this texture, uploaded by
|
||||
// xr_overlay_upload_source_texture_if_dirty() below. It's created
|
||||
// through gl4es (glGenTextures/glBindTexture/glTexImage2D), like any
|
||||
// other texture the engine itself creates, so ordinary
|
||||
// glTexSubImage2D uploads against it work the normal way.
|
||||
GLuint source_texture;
|
||||
// A framebuffer with source_texture as its only color attachment, used
|
||||
// as the read source for the blit in
|
||||
// xr_overlay_render_and_build_layer() below. Created via the real
|
||||
// (non-gl4es) GLES entry points below: that blit bypasses gl4es
|
||||
// entirely (see xr_overlay_render_and_build_layer() for why), so its
|
||||
// source framebuffer has to be a real GL object rather than one gl4es
|
||||
// tracks.
|
||||
GLuint source_fbo;
|
||||
|
||||
uint32_t pixel_generation;
|
||||
uint32_t source_uploaded_generation;
|
||||
uint8_t *pixel_cache; // width*height*4 bytes
|
||||
};
|
||||
|
||||
// This file's other GL calls otherwise resolve to gl4es (the only GL
|
||||
// symbol provider linked into this binary - see android/engine-patches/
|
||||
// 02-android-opengl-es.patch), fine for anything shared with the engine's
|
||||
// own gl4es-routed rendering. But the source FBO's blit (see
|
||||
// xr_overlay_render_and_build_layer()) needs the real driver directly:
|
||||
// gl4es's fixed-pipeline emulation unconditionally substitutes its own
|
||||
// shader onto any gl4es-routed draw call, which would silently replace an
|
||||
// overlay's content with whatever the game itself last rendered. A
|
||||
// framebuffer blit has no shader stage at all, so going through the real
|
||||
// driver for it sidesteps the problem entirely - resolved once here and
|
||||
// shared by every XrOverlay instance, since they're process-wide function
|
||||
// pointers regardless of how many overlays exist.
|
||||
typedef void (*PFNQSGETINTEGERV)(GLenum, GLint *);
|
||||
typedef void (*PFNQSGENFRAMEBUFFERS)(GLsizei, GLuint *);
|
||||
typedef void (*PFNQSDELETEFRAMEBUFFERS)(GLsizei, const GLuint *);
|
||||
typedef void (*PFNQSBINDFRAMEBUFFER)(GLenum, GLuint);
|
||||
typedef void (*PFNQSFRAMEBUFFERTEXTURE2D)(GLenum, GLenum, GLenum, GLuint, GLint);
|
||||
typedef void (*PFNQSBLITFRAMEBUFFER)(GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLint,
|
||||
GLbitfield, GLenum);
|
||||
|
||||
static PFNQSGETINTEGERV real_glGetIntegerv;
|
||||
static PFNQSGENFRAMEBUFFERS real_glGenFramebuffers;
|
||||
static PFNQSDELETEFRAMEBUFFERS real_glDeleteFramebuffers;
|
||||
static PFNQSBINDFRAMEBUFFER real_glBindFramebuffer;
|
||||
static PFNQSFRAMEBUFFERTEXTURE2D real_glFramebufferTexture2D;
|
||||
// glBlitFramebuffer is a GLES 3.0 addition; resolved via eglGetProcAddress
|
||||
// rather than dlsym, since Android's driver dispatch doesn't guarantee ES3+
|
||||
// symbols are dlsym-able by name from libGLESv2.so, unlike the GLES2-core
|
||||
// functions above.
|
||||
static PFNQSBLITFRAMEBUFFER real_glBlitFramebuffer;
|
||||
static bool g_real_gles_loaded = false;
|
||||
|
||||
static bool xr_overlay_load_real_gles(void) {
|
||||
if (g_real_gles_loaded)
|
||||
return true;
|
||||
void *lib = dlopen("libGLESv2.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (lib == NULL) {
|
||||
LOGE("XR: overlay dlopen(libGLESv2.so) failed: %s", dlerror());
|
||||
return false;
|
||||
}
|
||||
real_glGetIntegerv = (PFNQSGETINTEGERV)dlsym(lib, "glGetIntegerv");
|
||||
real_glGenFramebuffers = (PFNQSGENFRAMEBUFFERS)dlsym(lib, "glGenFramebuffers");
|
||||
real_glDeleteFramebuffers = (PFNQSDELETEFRAMEBUFFERS)dlsym(lib, "glDeleteFramebuffers");
|
||||
real_glBindFramebuffer = (PFNQSBINDFRAMEBUFFER)dlsym(lib, "glBindFramebuffer");
|
||||
real_glFramebufferTexture2D = (PFNQSFRAMEBUFFERTEXTURE2D)dlsym(lib, "glFramebufferTexture2D");
|
||||
real_glBlitFramebuffer = (PFNQSBLITFRAMEBUFFER)eglGetProcAddress("glBlitFramebuffer");
|
||||
g_real_gles_loaded = real_glGetIntegerv && real_glGenFramebuffers &&
|
||||
real_glDeleteFramebuffers && real_glBindFramebuffer &&
|
||||
real_glFramebufferTexture2D && real_glBlitFramebuffer;
|
||||
return g_real_gles_loaded;
|
||||
}
|
||||
|
||||
// FindClass() from this thread (SDL's native thread, attached to the JVM
|
||||
// via AttachCurrentThread rather than spawned from Java) resolves against
|
||||
// the bootstrap classloader, which only knows framework classes - it can't
|
||||
// see app classes like de.ladkau.questshock.KeyboardOverlay at all.
|
||||
// Routing through the activity's own classloader is the standard,
|
||||
// documented workaround.
|
||||
static jclass xr_overlay_find_class(JNIEnv *env, jobject activity, const char *name) {
|
||||
jclass activityClass = (*env)->GetObjectClass(env, activity);
|
||||
jmethodID getClassLoader =
|
||||
(*env)->GetMethodID(env, activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
|
||||
jobject classLoader = (*env)->CallObjectMethod(env, activity, getClassLoader);
|
||||
jclass classLoaderClass = (*env)->FindClass(env, "java/lang/ClassLoader");
|
||||
jmethodID loadClass = (*env)->GetMethodID(env, classLoaderClass, "loadClass",
|
||||
"(Ljava/lang/String;)Ljava/lang/Class;");
|
||||
jstring className = (*env)->NewStringUTF(env, name);
|
||||
jclass result = (jclass)(*env)->CallObjectMethod(env, classLoader, loadClass, className);
|
||||
(*env)->DeleteLocalRef(env, className);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool xr_overlay_init_jni(XrOverlay *overlay, const char *java_class_name) {
|
||||
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
||||
jobject activity = (jobject)SDL_AndroidGetActivity();
|
||||
if (env == NULL || activity == NULL) {
|
||||
LOGE("XR: overlay - no JNIEnv/Activity from SDL");
|
||||
return false;
|
||||
}
|
||||
|
||||
jclass localClass = xr_overlay_find_class(env, activity, java_class_name);
|
||||
if (localClass == NULL) {
|
||||
LOGE("XR: overlay - could not find class %s", java_class_name);
|
||||
return false;
|
||||
}
|
||||
overlay->java_class = (jclass)(*env)->NewGlobalRef(env, localClass);
|
||||
|
||||
overlay->native_init_method = (*env)->GetStaticMethodID(env, overlay->java_class, "nativeInit",
|
||||
"(Landroid/app/Activity;)V");
|
||||
overlay->take_pixels_method =
|
||||
(*env)->GetStaticMethodID(env, overlay->java_class, "nativeTakePixelsIfDirty", "()[B");
|
||||
overlay->dispatch_touch_method =
|
||||
(*env)->GetStaticMethodID(env, overlay->java_class, "nativeDispatchTouch", "(FFZ)V");
|
||||
overlay->update_cursor_method =
|
||||
(*env)->GetStaticMethodID(env, overlay->java_class, "nativeUpdateCursor", "(FFZ)V");
|
||||
if (!overlay->native_init_method || !overlay->take_pixels_method ||
|
||||
!overlay->dispatch_touch_method || !overlay->update_cursor_method) {
|
||||
LOGE("XR: overlay - could not resolve %s JNI methods", java_class_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, overlay->java_class, overlay->native_init_method, activity);
|
||||
// CallStaticVoidMethod doesn't surface Java exceptions on its own - if
|
||||
// the constructor throws (it runs its View measure/layout/draw calls
|
||||
// on this native render thread rather than the Android UI thread, a
|
||||
// plausible crash vector), the exception would otherwise be left
|
||||
// silently pending and corrupt whatever JNI call runs next.
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
LOGE("XR: overlay %s.nativeInit() threw a pending Java exception:", java_class_name);
|
||||
(*env)->ExceptionDescribe(env);
|
||||
(*env)->ExceptionClear(env);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool xr_overlay_create_source_texture(XrOverlay *overlay) {
|
||||
glGenTextures(1, &overlay->source_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, overlay->source_texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, overlay->width, overlay->height, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, NULL);
|
||||
|
||||
GLint prevFbo = 0;
|
||||
real_glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFbo);
|
||||
real_glGenFramebuffers(1, &overlay->source_fbo);
|
||||
real_glBindFramebuffer(GL_FRAMEBUFFER, overlay->source_fbo);
|
||||
real_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
overlay->source_texture, 0);
|
||||
real_glBindFramebuffer(GL_FRAMEBUFFER, (GLuint)prevFbo);
|
||||
|
||||
return overlay->source_texture != 0;
|
||||
}
|
||||
|
||||
XrOverlay *xr_overlay_create(XrInstance instance, XrSession session, int64_t swapchain_format,
|
||||
const XrOverlayConfig *config) {
|
||||
if (!xr_overlay_load_real_gles()) {
|
||||
LOGE("XR: overlay couldn't resolve real GLES functions via dlsym/eglGetProcAddress");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
XrOverlay *overlay = (XrOverlay *)calloc(1, sizeof(XrOverlay));
|
||||
if (overlay == NULL)
|
||||
return NULL;
|
||||
|
||||
overlay->instance = instance;
|
||||
overlay->width = config->width;
|
||||
overlay->height = config->height;
|
||||
overlay->distance_m = config->distance_m;
|
||||
overlay->default_distance_m = config->distance_m;
|
||||
overlay->half_width_m = config->width_m * 0.5f;
|
||||
overlay->half_height_m = config->width_m * 0.5f * (float)config->height / (float)config->width;
|
||||
overlay->center_x_m = config->default_center_x_m;
|
||||
overlay->center_y_m = config->default_center_y_m;
|
||||
|
||||
if (!xr_swapchain_create(instance, session, swapchain_format, config->width, config->height,
|
||||
&overlay->swapchain)) {
|
||||
LOGE("XR: overlay %s swapchain setup failed", config->java_class_name);
|
||||
free(overlay);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!xr_overlay_create_source_texture(overlay)) {
|
||||
xr_swapchain_destroy(&overlay->swapchain);
|
||||
free(overlay);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!xr_overlay_init_jni(overlay, config->java_class_name)) {
|
||||
xr_swapchain_destroy(&overlay->swapchain);
|
||||
free(overlay);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
overlay->pixel_cache = (uint8_t *)malloc((size_t)config->width * config->height * 4);
|
||||
if (overlay->pixel_cache == NULL) {
|
||||
xr_swapchain_destroy(&overlay->swapchain);
|
||||
free(overlay);
|
||||
return NULL;
|
||||
}
|
||||
overlay->source_uploaded_generation = (uint32_t)-1;
|
||||
|
||||
LOGI("XR: overlay %s ready (%dx%d)", config->java_class_name, config->width, config->height);
|
||||
return overlay;
|
||||
}
|
||||
|
||||
void xr_overlay_destroy(XrOverlay *overlay) {
|
||||
if (overlay == NULL)
|
||||
return;
|
||||
|
||||
if (overlay->source_texture != 0)
|
||||
glDeleteTextures(1, &overlay->source_texture);
|
||||
if (overlay->source_fbo != 0 && real_glDeleteFramebuffers != NULL)
|
||||
real_glDeleteFramebuffers(1, &overlay->source_fbo);
|
||||
|
||||
if (overlay->java_class != NULL) {
|
||||
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
||||
if (env != NULL)
|
||||
(*env)->DeleteGlobalRef(env, overlay->java_class);
|
||||
}
|
||||
|
||||
xr_swapchain_destroy(&overlay->swapchain);
|
||||
free(overlay->pixel_cache);
|
||||
free(overlay);
|
||||
}
|
||||
|
||||
// Every function below tolerates overlay == NULL (a no-op, or the
|
||||
// obvious "not visible"/zeroed-extent default) - xr_overlay_create() can
|
||||
// fail (e.g. its backing Java class couldn't be resolved), and unlike the
|
||||
// single static-global menu quad this replaced, an overlay instance is
|
||||
// otherwise a heap pointer callers (xr_session.c, questshock_native.c) can
|
||||
// hold onto and use across frames without re-checking for failure every
|
||||
// time.
|
||||
void xr_overlay_toggle_visible(XrOverlay *overlay) {
|
||||
if (overlay != NULL)
|
||||
overlay->visible = !overlay->visible;
|
||||
}
|
||||
|
||||
void xr_overlay_set_visible(XrOverlay *overlay, bool visible) {
|
||||
if (overlay != NULL)
|
||||
overlay->visible = visible;
|
||||
}
|
||||
|
||||
bool xr_overlay_is_visible(const XrOverlay *overlay) { return overlay != NULL && overlay->visible; }
|
||||
|
||||
void xr_overlay_get_quad_extent(const XrOverlay *overlay, float *center_x_m, float *center_y_m,
|
||||
float *distance_m, float *half_width_m, float *half_height_m,
|
||||
float *yaw_deg) {
|
||||
if (overlay == NULL) {
|
||||
*center_x_m = *center_y_m = *distance_m = *half_width_m = *half_height_m = *yaw_deg = 0.0f;
|
||||
return;
|
||||
}
|
||||
*center_x_m = overlay->center_x_m;
|
||||
*center_y_m = overlay->center_y_m;
|
||||
*distance_m = overlay->distance_m;
|
||||
*half_width_m = overlay->half_width_m;
|
||||
*half_height_m = overlay->half_height_m;
|
||||
*yaw_deg = overlay->yaw_deg;
|
||||
}
|
||||
|
||||
void xr_overlay_set_position(XrOverlay *overlay, float center_x_m, float center_y_m) {
|
||||
if (overlay == NULL)
|
||||
return;
|
||||
overlay->center_x_m = center_x_m;
|
||||
overlay->center_y_m = center_y_m;
|
||||
}
|
||||
|
||||
void xr_overlay_set_pose(XrOverlay *overlay, float center_x_m, float center_y_m, float distance_m,
|
||||
float yaw_deg) {
|
||||
if (overlay == NULL)
|
||||
return;
|
||||
overlay->center_x_m = center_x_m;
|
||||
overlay->center_y_m = center_y_m;
|
||||
overlay->distance_m = distance_m;
|
||||
overlay->yaw_deg = yaw_deg;
|
||||
}
|
||||
|
||||
float xr_overlay_get_default_distance_m(const XrOverlay *overlay) {
|
||||
return overlay == NULL ? 0.0f : overlay->default_distance_m;
|
||||
}
|
||||
|
||||
void xr_overlay_touch(XrOverlay *overlay, float u, float v, bool down) {
|
||||
if (overlay == NULL)
|
||||
return;
|
||||
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
||||
if (env == NULL)
|
||||
return;
|
||||
// Passed via CallStaticVoidMethodA/jvalue rather than the variadic
|
||||
// Call*Method form - deliberately sidesteps relying on JNI
|
||||
// implementations correctly un-doing C's float-to-double default
|
||||
// argument promotion for varargs float parameters (a well-known JNI
|
||||
// footgun; Android's ART handles it correctly, but there's no reason
|
||||
// to depend on that when the jvalue form is unambiguous either way).
|
||||
jvalue args[3];
|
||||
args[0].f = u;
|
||||
args[1].f = v;
|
||||
args[2].z = (jboolean)down;
|
||||
(*env)->CallStaticVoidMethodA(env, overlay->java_class, overlay->dispatch_touch_method, args);
|
||||
}
|
||||
|
||||
void xr_overlay_update_cursor(XrOverlay *overlay, float u, float v, bool visible) {
|
||||
if (overlay == NULL)
|
||||
return;
|
||||
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
||||
if (env == NULL)
|
||||
return;
|
||||
// Same jvalue-form rationale as xr_overlay_touch() above.
|
||||
jvalue args[3];
|
||||
args[0].f = u;
|
||||
args[1].f = v;
|
||||
args[2].z = (jboolean)visible;
|
||||
(*env)->CallStaticVoidMethodA(env, overlay->java_class, overlay->update_cursor_method, args);
|
||||
}
|
||||
|
||||
// Pulls the Java view's latest pixels (if it redrew since the last check)
|
||||
// into pixel_cache and bumps pixel_generation - called once per rendered
|
||||
// frame, before deciding whether source_texture needs a fresh upload.
|
||||
static void xr_overlay_refresh_pixel_cache(XrOverlay *overlay) {
|
||||
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
||||
if (env == NULL)
|
||||
return;
|
||||
jbyteArray pixels = (jbyteArray)(*env)->CallStaticObjectMethod(
|
||||
env, overlay->java_class, overlay->take_pixels_method);
|
||||
// CallStaticObjectMethod also doesn't surface Java exceptions on its
|
||||
// own - if nativeTakePixelsIfDirty() itself throws, it would otherwise
|
||||
// silently look identical to "nothing changed yet" (a null return).
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
LOGE("XR: overlay nativeTakePixelsIfDirty() threw a pending Java exception:");
|
||||
(*env)->ExceptionDescribe(env);
|
||||
(*env)->ExceptionClear(env);
|
||||
return;
|
||||
}
|
||||
if (pixels == NULL)
|
||||
return;
|
||||
(*env)->GetByteArrayRegion(env, pixels, 0, overlay->width * overlay->height * 4,
|
||||
(jbyte *)overlay->pixel_cache);
|
||||
(*env)->DeleteLocalRef(env, pixels);
|
||||
overlay->pixel_generation++;
|
||||
}
|
||||
|
||||
// Uploads pixel_cache into source_texture via plain (gl4es-routed)
|
||||
// glBindTexture/glTexSubImage2D, matching how the texture was created.
|
||||
// Skipped when nothing changed since the last upload. Texture-unit-0 state
|
||||
// is saved and restored around the upload, since this call is gl4es-routed
|
||||
// and the engine's own next-frame rendering assumes nothing touched its
|
||||
// texture bindings since it last drew.
|
||||
static void xr_overlay_upload_source_texture_if_dirty(XrOverlay *overlay) {
|
||||
if (overlay->source_uploaded_generation == overlay->pixel_generation)
|
||||
return;
|
||||
|
||||
GLint prevActiveTexture = GL_TEXTURE0;
|
||||
glGetIntegerv(GL_ACTIVE_TEXTURE, &prevActiveTexture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
GLint prevTexture2D = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &prevTexture2D);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, overlay->source_texture);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, overlay->width, overlay->height, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, overlay->pixel_cache);
|
||||
overlay->source_uploaded_generation = overlay->pixel_generation;
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, (GLuint)prevTexture2D);
|
||||
if (prevActiveTexture != GL_TEXTURE0)
|
||||
glActiveTexture((GLenum)prevActiveTexture);
|
||||
}
|
||||
|
||||
bool xr_overlay_render_and_build_layer(XrOverlay *overlay, XrCompositionLayerQuad *out_quad) {
|
||||
if (overlay == NULL || !overlay->visible)
|
||||
return false;
|
||||
if (!xr_swapchain_acquire(overlay->instance, &overlay->swapchain))
|
||||
return false;
|
||||
|
||||
xr_overlay_refresh_pixel_cache(overlay);
|
||||
xr_overlay_upload_source_texture_if_dirty(overlay);
|
||||
|
||||
// Copies source_texture (via source_fbo) directly into the swapchain
|
||||
// image xr_swapchain_acquire() just bound - sized to exactly
|
||||
// width x height, so the destination is always the whole image - using
|
||||
// the real GLES3 hardware blit (glBlitFramebuffer) rather than a
|
||||
// shader-based full-screen-quad draw. A blit has no vertex/fragment
|
||||
// shading stage, no shader program, and no texture units involved at
|
||||
// all, so gl4es's fixed-pipeline-emulation layer - which
|
||||
// unconditionally substitutes a customized shader (reproducing the
|
||||
// game's own last-bound texture/fixed-function state) onto any
|
||||
// gl4es-routed draw call - has nothing to intercept here. It also
|
||||
// never touches gl4es's own tracked program/vertex-array/
|
||||
// texture-binding shadow state, so the only piece of state that needs
|
||||
// save/restore is the READ framebuffer binding; binding only
|
||||
// GL_READ_FRAMEBUFFER, rather than the combined GL_FRAMEBUFFER target,
|
||||
// leaves the DRAW side (the swapchain image itself) untouched
|
||||
// throughout.
|
||||
//
|
||||
// Y is flipped between source and destination: source_texture's texel
|
||||
// row v=0 holds the Java view's Bitmap row 0 (the top of the rendered
|
||||
// content, standard top-row-first raster order), and reading that same
|
||||
// texture via an attached FBO, framebuffer y=0 accesses that identical
|
||||
// row. The destination swapchain framebuffer follows the opposite
|
||||
// convention - its own y=0 is its bottom edge - so swapping dstY0/dstY1
|
||||
// keeps the content right-side up (glBlitFramebuffer supports inverted
|
||||
// src/dst rects for exactly this).
|
||||
GLint prevReadFbo = 0;
|
||||
real_glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevReadFbo);
|
||||
|
||||
real_glBindFramebuffer(GL_READ_FRAMEBUFFER, overlay->source_fbo);
|
||||
real_glBlitFramebuffer(0, 0, overlay->width, overlay->height, 0, overlay->height,
|
||||
overlay->width, 0, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
real_glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)prevReadFbo);
|
||||
|
||||
// Without this, nothing here waits for the blit to actually finish on
|
||||
// the GPU before xr_swapchain_release() hands the image back to the
|
||||
// compositor - glBlitFramebuffer only queues the copy. A cross-process
|
||||
// consumer reading the image before the driver has finished writing it
|
||||
// is a textbook source of the diagonal-banded/torn corruption seen on
|
||||
// device (see xr_swapchain_acquire()'s counterpart in OpenGL.cc, which
|
||||
// had the same gap).
|
||||
glFinish();
|
||||
|
||||
xr_swapchain_release(overlay->instance, &overlay->swapchain);
|
||||
|
||||
memset(out_quad, 0, sizeof(*out_quad));
|
||||
out_quad->type = XR_TYPE_COMPOSITION_LAYER_QUAD;
|
||||
out_quad->subImage.swapchain = overlay->swapchain.swapchain;
|
||||
out_quad->subImage.imageRect.extent.width = overlay->swapchain.width;
|
||||
out_quad->subImage.imageRect.extent.height = overlay->swapchain.height;
|
||||
// Rotation about the vertical axis only (see the struct field's own
|
||||
// comment) - same R_y(-yaw_deg) quaternion construction as
|
||||
// xr_session.c's own play-space rebuild, for the same reason: this
|
||||
// quad's pose is expressed as "yaw_deg turned from its own local -Z",
|
||||
// so a point/ray in that rotated frame maps back into the (unrotated)
|
||||
// base space by rotating by -yaw_deg.
|
||||
float half = -(overlay->yaw_deg * QS_DEG_TO_RAD) * 0.5f;
|
||||
out_quad->pose.orientation.x = 0.0f;
|
||||
out_quad->pose.orientation.y = sinf(half);
|
||||
out_quad->pose.orientation.z = 0.0f;
|
||||
out_quad->pose.orientation.w = cosf(half);
|
||||
out_quad->pose.position.x = overlay->center_x_m;
|
||||
out_quad->pose.position.y = overlay->center_y_m;
|
||||
out_quad->pose.position.z = -overlay->distance_m;
|
||||
out_quad->size.width = overlay->half_width_m * 2.0f;
|
||||
out_quad->size.height = overlay->half_height_m * 2.0f;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Generic "off-screen Android View rendered into its own OpenXR quad,
|
||||
// hit-tested/touched via a laser pointer" module - each instance owns its
|
||||
// own swapchain (via xr_swapchain.h), its own visible/position state, and
|
||||
// the JNI glue to a backing Java class (menu launcher, keyboard, or any
|
||||
// future such panel) that must expose these static methods, matching the
|
||||
// pattern MenuOverlay.java already established:
|
||||
// static void nativeInit(Activity activity)
|
||||
// static byte[] nativeTakePixelsIfDirty()
|
||||
// static void nativeDispatchTouch(float u, float v, boolean down)
|
||||
// static void nativeUpdateCursor(float u, float v, boolean visible)
|
||||
//
|
||||
// xr_input.c drives touch/cursor dispatch and (for panels that support it,
|
||||
// like the keyboard's title bar) dragging via xr_overlay_set_position();
|
||||
// xr_session.c owns instance lifetime and calls
|
||||
// xr_overlay_render_and_build_layer() once per frame per instance.
|
||||
#ifndef QUESTSHOCK_XR_OVERLAY_H
|
||||
#define QUESTSHOCK_XR_OVERLAY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define XR_USE_PLATFORM_ANDROID 1
|
||||
#define XR_USE_GRAPHICS_API_OPENGL_ES 1
|
||||
#include <openxr/openxr.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct XrOverlay XrOverlay;
|
||||
|
||||
typedef struct {
|
||||
// Fully-qualified Java class name backing this overlay, e.g.
|
||||
// "de.ladkau.questshock.KeyboardOverlay" - resolved via the activity's
|
||||
// own ClassLoader (see xr_overlay_find_class() in xr_overlay.c).
|
||||
const char *java_class_name;
|
||||
// Pixel size of the overlay's own content/swapchain.
|
||||
int width;
|
||||
int height;
|
||||
// Initial local-space pose (meters, in front of the viewer along -Z) -
|
||||
// see xr_overlay_get_quad_extent(). width_m is the quad's physical
|
||||
// width; its height is derived to preserve width/height's aspect.
|
||||
float distance_m;
|
||||
float width_m;
|
||||
float default_center_x_m;
|
||||
float default_center_y_m;
|
||||
} XrOverlayConfig;
|
||||
|
||||
// instance/session are used to create this overlay's own swapchain
|
||||
// (format shared with every other quad this app submits - see
|
||||
// xr_session.c's chosenFormat). Returns NULL (logged) on failure - the
|
||||
// caller just has no such panel.
|
||||
XrOverlay *xr_overlay_create(XrInstance instance, XrSession session, int64_t swapchain_format,
|
||||
const XrOverlayConfig *config);
|
||||
void xr_overlay_destroy(XrOverlay *overlay);
|
||||
|
||||
void xr_overlay_toggle_visible(XrOverlay *overlay);
|
||||
void xr_overlay_set_visible(XrOverlay *overlay, bool visible);
|
||||
bool xr_overlay_is_visible(const XrOverlay *overlay);
|
||||
|
||||
// Base-space pose/size of the quad, valid regardless of visibility - shared
|
||||
// with xr_input.c's ray/quad hit-testing. yaw_deg is rotation about the
|
||||
// vertical axis (see xr_overlay_set_pose()) - 0 unless something has
|
||||
// called that.
|
||||
void xr_overlay_get_quad_extent(const XrOverlay *overlay, float *center_x_m, float *center_y_m,
|
||||
float *distance_m, float *half_width_m, float *half_height_m,
|
||||
float *yaw_deg);
|
||||
|
||||
// Repositions the quad (base-space X/Y offset, same units/space as
|
||||
// xr_overlay_get_quad_extent()'s center_x_m/center_y_m) - e.g. driven by
|
||||
// xr_input.c while the user drags a panel's title bar. Distance from the
|
||||
// viewer and rotation aren't adjustable this way - only left/right/up/down
|
||||
// repositioning, not push/pull or turning - see xr_overlay_set_pose() for
|
||||
// that.
|
||||
void xr_overlay_set_position(XrOverlay *overlay, float center_x_m, float center_y_m);
|
||||
|
||||
// Full reposition: X/Y offset, distance ("push/pull"), and rotation about
|
||||
// the vertical axis (degrees, same sign convention as
|
||||
// xr_session_set_player_transform()'s yaw_deg) - used by xr_input.c to
|
||||
// re-center the menu launcher in front of wherever the player currently
|
||||
// is/facing each time it's freshly reopened, unlike
|
||||
// xr_overlay_set_position()'s drag-only X/Y-at-fixed-distance-and-facing.
|
||||
void xr_overlay_set_pose(XrOverlay *overlay, float center_x_m, float center_y_m, float distance_m,
|
||||
float yaw_deg);
|
||||
|
||||
// The overlay's original, never-mutated distance_m (from XrOverlayConfig at
|
||||
// creation time) - unlike xr_overlay_get_quad_extent()'s live distance_m,
|
||||
// which xr_overlay_set_pose() repurposes as a raw base-space Z coordinate
|
||||
// once any rotation is involved (see xr_overlay_set_pose()'s own comment),
|
||||
// this stays a stable "how far in front" magnitude across repeated
|
||||
// recenters. 0 for a NULL overlay.
|
||||
float xr_overlay_get_default_distance_m(const XrOverlay *overlay);
|
||||
|
||||
// Forwards a hit on the quad (in the quad's own 0..1 u/v, top-left origin)
|
||||
// to the backing Java view as a synthetic touch down/up.
|
||||
void xr_overlay_touch(XrOverlay *overlay, float u, float v, bool down);
|
||||
|
||||
// Updates the on-quad cursor the Java view draws at the current aim hit
|
||||
// point (same u/v convention as xr_overlay_touch()) - called once per
|
||||
// frame regardless of click state, so the cursor tracks the ray
|
||||
// continuously. visible=false hides it.
|
||||
void xr_overlay_update_cursor(XrOverlay *overlay, float u, float v, bool visible);
|
||||
|
||||
// If visible: acquires this overlay's next swapchain image, blits the
|
||||
// Java view's latest rendered pixels into it, releases the image, and
|
||||
// fills *out_quad's subImage/pose/size. space/eyeVisibility/layerFlags are
|
||||
// left for the caller to set (shared/policy choices across every layer
|
||||
// this app submits, e.g. translucency - not this module's concern).
|
||||
// Returns false (out_quad untouched) if not visible or something failed -
|
||||
// the caller should skip submitting a layer for it this frame.
|
||||
bool xr_overlay_render_and_build_layer(XrOverlay *overlay, XrCompositionLayerQuad *out_quad);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "xr_session.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <ctype.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -17,27 +18,84 @@
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include "xr_input.h"
|
||||
#include "xr_overlay.h"
|
||||
#include "xr_swapchain.h"
|
||||
|
||||
#define TAG "QuestShock"
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
||||
|
||||
// Fixed pose/size for the single game quad - 2m in front of the local space
|
||||
// origin, sized to preserve the game's own aspect ratio at a comfortable
|
||||
// visual angle. No per-eye view/projection work is needed for a quad layer
|
||||
// at all - the compositor handles reprojecting it per eye itself, which is
|
||||
// exactly why this milestone doesn't need xrLocateViews or any stereo
|
||||
// rendering (see the plan's step A notes).
|
||||
// Original, pre-screen-profile-feature default distance for the game
|
||||
// screen - 2m in front of the local space origin - now just the initial
|
||||
// value of g_screen_distance_m (below) rather than a fixed constant.
|
||||
// Width/curve/distance are all configurable (see g_screen_width_m/
|
||||
// g_screen_curve_deg/g_screen_distance_m above and CONFIG.md's Video
|
||||
// Options "Initial Screen"/"Game Screen" settings), sized to preserve the
|
||||
// game's own pixel aspect ratio at whatever physical width is currently
|
||||
// configured. No per-eye view/projection work is needed for a quad/
|
||||
// cylinder layer at all - the compositor handles reprojecting it per eye
|
||||
// itself, which is exactly why this milestone doesn't need xrLocateViews
|
||||
// or any stereo rendering (see the plan's step A notes).
|
||||
#define QUAD_DISTANCE_METERS 2.0f
|
||||
#define QUAD_WIDTH_METERS 1.6f
|
||||
|
||||
// Up to this many swapchain images/FBOs - real runtimes report small counts
|
||||
// (2-4); this is just a fixed upper bound for the cache arrays below.
|
||||
#define MAX_SWAPCHAIN_IMAGES 8
|
||||
// The menu launcher (just a "Keyboard" button - see MenuOverlay.java) is
|
||||
// small and sits above where the keyboard defaults to; the keyboard keeps
|
||||
// its previous size/position. They're independent XrOverlay instances (see
|
||||
// xr_overlay.h) so both can be shown/hidden/dragged separately - deliberately
|
||||
// positioned apart by default so they don't start out overlapping.
|
||||
#define MENU_JAVA_CLASS "de.ladkau.questshock.MenuOverlay"
|
||||
// 768x800 (was 512x256, then 768x512, then 768x552) - grown to fit the
|
||||
// header/tabs/Video Options content added on top of the original single
|
||||
// "Keyboard" button, then again to fit the stats ribbon below the tab
|
||||
// bar, then again to fit the Video Options tab's screen-size grid; taller
|
||||
// in world space too since MENU_WIDTH_METERS (the physical size) stays
|
||||
// fixed and height_m is derived from it by aspect ratio (see
|
||||
// xr_overlay.c's xr_overlay_init()). Must match MenuOverlay.java's own
|
||||
// WIDTH/HEIGHT, and xr_input.c's MENU_TITLE_BAR_V_FRACTION denominator.
|
||||
#define MENU_WIDTH 768
|
||||
#define MENU_HEIGHT 800
|
||||
#define MENU_DISTANCE_METERS 1.3f
|
||||
// Physical size - was 0.35m, too small to read comfortably at
|
||||
// MENU_DISTANCE_METERS given how much text the header/tabs/stats ribbon
|
||||
// pack in; same pixel resolution (MENU_WIDTH/HEIGHT above) just displayed
|
||||
// bigger, not a layout change.
|
||||
#define MENU_WIDTH_METERS 0.5f
|
||||
#define MENU_DEFAULT_CENTER_X_METERS 0.0f
|
||||
#define MENU_DEFAULT_CENTER_Y_METERS 0.15f
|
||||
|
||||
#define KEYBOARD_JAVA_CLASS "de.ladkau.questshock.KeyboardOverlay"
|
||||
#define KEYBOARD_WIDTH 1024
|
||||
#define KEYBOARD_HEIGHT 768
|
||||
#define KEYBOARD_DISTANCE_METERS 1.5f
|
||||
#define KEYBOARD_WIDTH_METERS 0.8f
|
||||
#define KEYBOARD_DEFAULT_CENTER_X_METERS 0.0f
|
||||
#define KEYBOARD_DEFAULT_CENTER_Y_METERS -0.45f
|
||||
|
||||
static XrInstance g_instance = XR_NULL_HANDLE;
|
||||
static XrSystemId g_system_id = XR_NULL_SYSTEM_ID;
|
||||
static XrSession g_session = XR_NULL_HANDLE;
|
||||
static XrSpace g_local_space = XR_NULL_HANDLE;
|
||||
|
||||
// Everything this app submits/locates (game/menu/keyboard/beam quads, hand
|
||||
// and head poses) is expressed relative to g_play_space, not g_local_space
|
||||
// directly - it's g_local_space translated/rotated by the inverse of the
|
||||
// player's own virtual transform (see xr_session_set_player_transform()'s
|
||||
// own comment in xr_session.h and xr_session_rebuild_play_space() below),
|
||||
// so moving/turning the player moves/turns everything else relative to
|
||||
// them without touching any actual pose-building or hit-testing math
|
||||
// elsewhere in the app - it's all still "relative to the base space",
|
||||
// which just happens to no longer be g_local_space itself.
|
||||
static XrSpace g_play_space = XR_NULL_HANDLE;
|
||||
static float g_player_x = 0.0f;
|
||||
static float g_player_z = 0.0f;
|
||||
static float g_player_yaw_deg = 0.0f;
|
||||
// Set by xr_session_set_player_transform() (including before xr_init() has
|
||||
// even run - see its own comment), cleared once xr_session_rebuild_play_space()
|
||||
// actually rebuilds g_play_space from the current x/z/yaw_deg - checked
|
||||
// once per frame in xr_frame_end() so a stick held at rest costs nothing.
|
||||
static bool g_player_transform_dirty = false;
|
||||
|
||||
static XrSessionState g_session_state = XR_SESSION_STATE_UNKNOWN;
|
||||
// Distinct from g_session_state - true from a successful xrBeginSession
|
||||
// until xrEndSession/loss/exit. Many runtimes only advance the *state*
|
||||
@@ -51,44 +109,47 @@ static XrSessionState g_session_state = XR_SESSION_STATE_UNKNOWN;
|
||||
// later state.
|
||||
static bool g_session_running = false;
|
||||
|
||||
static XrSwapchain g_game_swapchain = XR_NULL_HANDLE;
|
||||
static int g_game_width = 0;
|
||||
static int g_game_height = 0;
|
||||
static GLuint g_swapchain_fbos[MAX_SWAPCHAIN_IMAGES];
|
||||
static uint32_t g_swapchain_image_count = 0;
|
||||
static XrSwapchainState g_game_swapchain;
|
||||
static XrOverlay *g_menu_overlay = NULL;
|
||||
static XrOverlay *g_keyboard_overlay = NULL;
|
||||
|
||||
static XrTime g_predicted_display_time = 0;
|
||||
static bool g_frame_should_render = false;
|
||||
static bool g_have_acquired_image = false;
|
||||
static bool g_have_acquired_game_image = false;
|
||||
|
||||
// This file's GL calls otherwise resolve to gl4es (the only GL symbol
|
||||
// provider linked into this binary - see android/engine-patches/
|
||||
// 02-android-opengl-es.patch), which is fine for anything shared with the
|
||||
// engine's own gl4es-routed rendering. But the swapchain images OpenXR
|
||||
// hands us are real driver texture objects gl4es never created itself,
|
||||
// and gl4es's own glFramebufferTexture2D can't attach a texture it has no
|
||||
// tracked metadata for. So the FBO *container* is created via gl4es's own
|
||||
// glGenFramebuffers/glBindFramebuffer (so gl4es recognizes the id as its
|
||||
// own and its own per-frame glBindFramebuffer succeeds), while the
|
||||
// texture-attach step - the specifically foreign part - goes through the
|
||||
// real driver directly, via dlsym against libGLESv2.so.
|
||||
typedef void (*PFNQSFRAMEBUFFERTEXTURE2D)(GLenum, GLenum, GLenum, GLuint, GLint);
|
||||
typedef GLenum (*PFNQSCHECKFRAMEBUFFERSTATUS)(GLenum);
|
||||
// XR_META_performance_metrics support (see xr_session_init_performance_metrics()
|
||||
// below) - only present if the runtime advertised it at xrCreateInstance
|
||||
// time (checked via xr_instance_extension_supported()), since unlike the
|
||||
// two always-required KHR extensions above, this one is a device-capability
|
||||
// extra that a build shouldn't hard-depend on.
|
||||
static bool g_performance_metrics_supported = false;
|
||||
static PFN_xrSetPerformanceMetricsStateMETA g_xrSetPerformanceMetricsStateMETA = NULL;
|
||||
static PFN_xrQueryPerformanceMetricsCounterMETA g_xrQueryPerformanceMetricsCounterMETA = NULL;
|
||||
// Resolved once, at startup, by string-matching the runtime's own
|
||||
// human-readable counter path names (see xr_session_init_performance_metrics())
|
||||
// rather than hardcoding the exact path strings the spec defines - a
|
||||
// substring match is robust to spec/runtime wording we can't verify without
|
||||
// on-device testing. XR_NULL_PATH (0) if no match was found.
|
||||
static XrPath g_cpu_utilization_path = XR_NULL_PATH;
|
||||
static XrPath g_gpu_utilization_path = XR_NULL_PATH;
|
||||
|
||||
static PFNQSFRAMEBUFFERTEXTURE2D real_glFramebufferTexture2D;
|
||||
static PFNQSCHECKFRAMEBUFFERSTATUS real_glCheckFramebufferStatus;
|
||||
// XR_KHR_composition_layer_cylinder support - same "only if the runtime
|
||||
// advertised it" reasoning as g_performance_metrics_supported above. If
|
||||
// unsupported, the curve is forced to 0 (flat) everywhere regardless of
|
||||
// configuration (see xr_get_game_quad_extent()) so the game screen just
|
||||
// silently stays flat instead of failing to submit a layer at all.
|
||||
static bool g_cylinder_layer_supported = false;
|
||||
|
||||
static bool xr_load_real_gles(void) {
|
||||
void *lib = dlopen("libGLESv2.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (lib == NULL) {
|
||||
LOGE("XR: dlopen(libGLESv2.so) failed: %s", dlerror());
|
||||
return false;
|
||||
}
|
||||
real_glFramebufferTexture2D = (PFNQSFRAMEBUFFERTEXTURE2D)dlsym(lib, "glFramebufferTexture2D");
|
||||
real_glCheckFramebufferStatus =
|
||||
(PFNQSCHECKFRAMEBUFFERSTATUS)dlsym(lib, "glCheckFramebufferStatus");
|
||||
return real_glFramebufferTexture2D && real_glCheckFramebufferStatus;
|
||||
}
|
||||
// The game screen's currently-configured shape (see qs_config.c's
|
||||
// qs_screen_profile_notify_loop_mode(), which is what actually calls
|
||||
// xr_session_set_game_screen_shape() below - this file has no notion of
|
||||
// "which profile" is active, only "what shape is current"). Defaults
|
||||
// match the game quad's own original, pre-this-feature hardcoded width
|
||||
// (1.6m) and lack of curve, so a build that never touches the new config
|
||||
// options renders identically to before.
|
||||
static float g_screen_width_m = 1.6f;
|
||||
static float g_screen_curve_deg = 0.0f;
|
||||
static float g_screen_distance_m = QUAD_DISTANCE_METERS;
|
||||
|
||||
static bool xr_check(XrResult result, const char *what) {
|
||||
if (XR_SUCCEEDED(result))
|
||||
@@ -110,6 +171,106 @@ static PFN_xrVoidFunction xr_get_proc(const char *name) {
|
||||
return fn;
|
||||
}
|
||||
|
||||
// Queried before xrCreateInstance (which is why this takes no instance
|
||||
// handle - xrEnumerateInstanceExtensionProperties is one of the few
|
||||
// functions that works without one) to decide whether it's safe to add
|
||||
// `name` to enabledExtensionNames - passing an unsupported extension there
|
||||
// would fail instance creation outright, unlike a missing optional feature
|
||||
// discovered afterward.
|
||||
static bool xr_instance_extension_supported(const char *name) {
|
||||
uint32_t count = 0;
|
||||
if (!xr_check(xrEnumerateInstanceExtensionProperties(NULL, 0, &count, NULL),
|
||||
"xrEnumerateInstanceExtensionProperties"))
|
||||
return false;
|
||||
XrExtensionProperties *props = (XrExtensionProperties *)malloc(sizeof(XrExtensionProperties) * count);
|
||||
for (uint32_t i = 0; i < count; i++)
|
||||
props[i].type = XR_TYPE_EXTENSION_PROPERTIES;
|
||||
bool supported = false;
|
||||
if (xr_check(xrEnumerateInstanceExtensionProperties(NULL, count, &count, props),
|
||||
"xrEnumerateInstanceExtensionProperties")) {
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
if (strcmp(props[i].extensionName, name) == 0) {
|
||||
supported = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
free(props);
|
||||
return supported;
|
||||
}
|
||||
|
||||
// Case-insensitively checks whether `needle` occurs anywhere in `haystack` -
|
||||
// used to identify a counter path (e.g. ".../device/cpu_utilization_average")
|
||||
// by a short, stable substring instead of matching its exact full spelling.
|
||||
static bool xr_path_name_contains(const char *haystack, const char *needle) {
|
||||
size_t hlen = strlen(haystack);
|
||||
char lower[256];
|
||||
if (hlen >= sizeof(lower))
|
||||
hlen = sizeof(lower) - 1;
|
||||
for (size_t i = 0; i < hlen; i++)
|
||||
lower[i] = (char)tolower((unsigned char)haystack[i]);
|
||||
lower[hlen] = '\0';
|
||||
return strstr(lower, needle) != NULL;
|
||||
}
|
||||
|
||||
// Enables XR_META_performance_metrics (if the runtime advertised it - see
|
||||
// xr_instance_extension_supported() above) and resolves the CPU/GPU
|
||||
// device-utilization counter paths xr_session_update_frame_stats() polls
|
||||
// every frame. Called once, right after xrCreateSession succeeds. Leaves
|
||||
// g_cpu_utilization_path/g_gpu_utilization_path at XR_NULL_PATH (their
|
||||
// getters then report -1.0f) if anything here doesn't pan out - this is a
|
||||
// nice-to-have stats display, not something worth failing session bring-up
|
||||
// over.
|
||||
static void xr_session_init_performance_metrics(void) {
|
||||
if (!g_performance_metrics_supported)
|
||||
return;
|
||||
|
||||
PFN_xrEnumeratePerformanceMetricsCounterPathsMETA enumeratePaths =
|
||||
(PFN_xrEnumeratePerformanceMetricsCounterPathsMETA)xr_get_proc(
|
||||
"xrEnumeratePerformanceMetricsCounterPathsMETA");
|
||||
g_xrSetPerformanceMetricsStateMETA = (PFN_xrSetPerformanceMetricsStateMETA)xr_get_proc(
|
||||
"xrSetPerformanceMetricsStateMETA");
|
||||
g_xrQueryPerformanceMetricsCounterMETA = (PFN_xrQueryPerformanceMetricsCounterMETA)xr_get_proc(
|
||||
"xrQueryPerformanceMetricsCounterMETA");
|
||||
if (enumeratePaths == NULL || g_xrSetPerformanceMetricsStateMETA == NULL ||
|
||||
g_xrQueryPerformanceMetricsCounterMETA == NULL)
|
||||
return;
|
||||
|
||||
XrPerformanceMetricsStateMETA state = {XR_TYPE_PERFORMANCE_METRICS_STATE_META};
|
||||
state.enabled = XR_TRUE;
|
||||
if (!xr_check(g_xrSetPerformanceMetricsStateMETA(g_session, &state),
|
||||
"xrSetPerformanceMetricsStateMETA"))
|
||||
return;
|
||||
|
||||
uint32_t pathCount = 0;
|
||||
if (!xr_check(enumeratePaths(g_instance, 0, &pathCount, NULL),
|
||||
"xrEnumeratePerformanceMetricsCounterPathsMETA"))
|
||||
return;
|
||||
XrPath *paths = (XrPath *)malloc(sizeof(XrPath) * pathCount);
|
||||
if (!xr_check(enumeratePaths(g_instance, pathCount, &pathCount, paths),
|
||||
"xrEnumeratePerformanceMetricsCounterPathsMETA")) {
|
||||
free(paths);
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < pathCount; i++) {
|
||||
char pathName[256];
|
||||
uint32_t pathNameLen = 0;
|
||||
if (!xr_check(xrPathToString(g_instance, paths[i], sizeof(pathName), &pathNameLen, pathName),
|
||||
"xrPathToString"))
|
||||
continue;
|
||||
if (g_cpu_utilization_path == XR_NULL_PATH && xr_path_name_contains(pathName, "cpu_utilization"))
|
||||
g_cpu_utilization_path = paths[i];
|
||||
if (g_gpu_utilization_path == XR_NULL_PATH && xr_path_name_contains(pathName, "gpu_utilization"))
|
||||
g_gpu_utilization_path = paths[i];
|
||||
}
|
||||
free(paths);
|
||||
|
||||
LOGI("XR: performance metrics cpu_utilization=%s gpu_utilization=%s",
|
||||
g_cpu_utilization_path != XR_NULL_PATH ? "found" : "not found",
|
||||
g_gpu_utilization_path != XR_NULL_PATH ? "found" : "not found");
|
||||
}
|
||||
|
||||
// The Android OpenXR loader needs the JavaVM/context before anything else -
|
||||
// separate from XR_KHR_android_create_instance below, which only affects
|
||||
// xrCreateInstance itself. Skipping this is a common cause of
|
||||
@@ -144,6 +305,65 @@ static EGLConfig xr_get_current_egl_config(EGLDisplay display, EGLContext contex
|
||||
return config;
|
||||
}
|
||||
|
||||
#define QS_DEG_TO_RAD (3.14159265358979323846f / 180.0f)
|
||||
|
||||
// Destroys/recreates g_play_space from g_local_space, offset by the
|
||||
// inverse of the player's current virtual transform (g_player_x/z/yaw_deg) -
|
||||
// see xr_session_set_player_transform()'s comment in xr_session.h for why
|
||||
// this is the inverse rather than the transform itself. Given
|
||||
// V = Translate(x, 0, z) * Rotate_Y(yaw_deg) (the player's own transform,
|
||||
// "apparent pose in play space = V composed with real pose in local
|
||||
// space"), the new space's poseInReferenceSpace must be V's inverse:
|
||||
// orientation = Rotate_Y(-yaw_deg), position = Rotate_Y(-yaw_deg) * (-x, 0, -z).
|
||||
// Called once, right after g_local_space itself is created (picking up
|
||||
// whatever qs_config_load_startup() already seeded before xr_init() ever
|
||||
// ran), and once per frame from xr_frame_end() whenever
|
||||
// g_player_transform_dirty says the transform actually changed since the
|
||||
// last rebuild - a plain stick-at-rest frame costs nothing beyond that one
|
||||
// bool check. g_session must already exist.
|
||||
static void xr_session_rebuild_play_space(void) {
|
||||
if (g_play_space != XR_NULL_HANDLE) {
|
||||
xrDestroySpace(g_play_space);
|
||||
g_play_space = XR_NULL_HANDLE;
|
||||
}
|
||||
|
||||
float theta = g_player_yaw_deg * QS_DEG_TO_RAD;
|
||||
float half = -theta * 0.5f;
|
||||
float c = cosf(-theta), s = sinf(-theta);
|
||||
float vx = -g_player_x, vz = -g_player_z;
|
||||
|
||||
XrReferenceSpaceCreateInfo spaceInfo = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
||||
spaceInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
|
||||
spaceInfo.poseInReferenceSpace.orientation.x = 0.0f;
|
||||
spaceInfo.poseInReferenceSpace.orientation.y = sinf(half);
|
||||
spaceInfo.poseInReferenceSpace.orientation.z = 0.0f;
|
||||
spaceInfo.poseInReferenceSpace.orientation.w = cosf(half);
|
||||
spaceInfo.poseInReferenceSpace.position.x = vx * c + vz * s;
|
||||
spaceInfo.poseInReferenceSpace.position.y = 0.0f;
|
||||
spaceInfo.poseInReferenceSpace.position.z = -vx * s + vz * c;
|
||||
xr_check(xrCreateReferenceSpace(g_session, &spaceInfo, &g_play_space),
|
||||
"xrCreateReferenceSpace(play)");
|
||||
|
||||
g_player_transform_dirty = false;
|
||||
}
|
||||
|
||||
void xr_session_set_player_transform(float x, float z, float yaw_deg) {
|
||||
g_player_x = x;
|
||||
g_player_z = z;
|
||||
g_player_yaw_deg = yaw_deg;
|
||||
g_player_transform_dirty = true;
|
||||
}
|
||||
|
||||
void xr_session_get_player_transform(float *x, float *z, float *yaw_deg) {
|
||||
*x = g_player_x;
|
||||
*z = g_player_z;
|
||||
*yaw_deg = g_player_yaw_deg;
|
||||
}
|
||||
|
||||
XrSpace xr_session_get_local_space(void) { return g_local_space; }
|
||||
|
||||
XrTime xr_session_get_predicted_display_time(void) { return g_predicted_display_time; }
|
||||
|
||||
static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
||||
jobject activity = (jobject)SDL_AndroidGetActivity();
|
||||
@@ -157,10 +377,26 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
if (!xr_initialize_android_loader(vm, activity))
|
||||
return false;
|
||||
|
||||
const char *extensions[] = {
|
||||
g_performance_metrics_supported =
|
||||
xr_instance_extension_supported(XR_META_PERFORMANCE_METRICS_EXTENSION_NAME);
|
||||
g_cylinder_layer_supported =
|
||||
xr_instance_extension_supported(XR_KHR_COMPOSITION_LAYER_CYLINDER_EXTENSION_NAME);
|
||||
|
||||
// The two KHR extensions here are always required; the other two are
|
||||
// each independently optional device capabilities (see
|
||||
// g_performance_metrics_supported/g_cylinder_layer_supported's own
|
||||
// comments) - appended only if actually advertised, since listing an
|
||||
// unsupported extension fails xrCreateInstance entirely, not just that
|
||||
// one feature.
|
||||
const char *extensions[4] = {
|
||||
XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME,
|
||||
XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME,
|
||||
};
|
||||
uint32_t extensionCount = 2;
|
||||
if (g_performance_metrics_supported)
|
||||
extensions[extensionCount++] = XR_META_PERFORMANCE_METRICS_EXTENSION_NAME;
|
||||
if (g_cylinder_layer_supported)
|
||||
extensions[extensionCount++] = XR_KHR_COMPOSITION_LAYER_CYLINDER_EXTENSION_NAME;
|
||||
|
||||
XrInstanceCreateInfoAndroidKHR androidInfo = {XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR};
|
||||
androidInfo.applicationVM = vm;
|
||||
@@ -168,7 +404,7 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
|
||||
XrInstanceCreateInfo createInfo = {XR_TYPE_INSTANCE_CREATE_INFO};
|
||||
createInfo.next = &androidInfo;
|
||||
createInfo.enabledExtensionCount = sizeof(extensions) / sizeof(extensions[0]);
|
||||
createInfo.enabledExtensionCount = extensionCount;
|
||||
createInfo.enabledExtensionNames = extensions;
|
||||
strncpy(createInfo.applicationInfo.applicationName, "QuestShock",
|
||||
XR_MAX_APPLICATION_NAME_SIZE - 1);
|
||||
@@ -188,10 +424,12 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
// Required by spec before creating an OpenGL ES-backed session, even
|
||||
// though we don't gate on the returned min/max API version here.
|
||||
PFN_xrGetOpenGLESGraphicsRequirementsKHR getGLESRequirements =
|
||||
(PFN_xrGetOpenGLESGraphicsRequirementsKHR)xr_get_proc("xrGetOpenGLESGraphicsRequirementsKHR");
|
||||
(PFN_xrGetOpenGLESGraphicsRequirementsKHR)xr_get_proc(
|
||||
"xrGetOpenGLESGraphicsRequirementsKHR");
|
||||
if (getGLESRequirements == NULL)
|
||||
return false;
|
||||
XrGraphicsRequirementsOpenGLESKHR glesRequirements = {XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR};
|
||||
XrGraphicsRequirementsOpenGLESKHR glesRequirements = {
|
||||
XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR};
|
||||
if (!xr_check(getGLESRequirements(g_instance, g_system_id, &glesRequirements),
|
||||
"xrGetOpenGLESGraphicsRequirementsKHR"))
|
||||
return false;
|
||||
@@ -215,6 +453,8 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
if (!xr_check(xrCreateSession(g_instance, &sessionInfo, &g_session), "xrCreateSession"))
|
||||
return false;
|
||||
|
||||
xr_session_init_performance_metrics();
|
||||
|
||||
XrReferenceSpaceCreateInfo spaceInfo = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
||||
spaceInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
|
||||
spaceInfo.poseInReferenceSpace.orientation.w = 1.0f;
|
||||
@@ -222,13 +462,24 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
"xrCreateReferenceSpace"))
|
||||
return false;
|
||||
|
||||
// Picks up whatever qs_config_load_startup() already stored into
|
||||
// g_player_x/z/yaw_deg via xr_session_set_player_transform() before
|
||||
// this function ever ran (that call is pure state, no XR API calls, so
|
||||
// it works fine even this early) - so a saved position is already in
|
||||
// effect from the very first frame, not just after the player first
|
||||
// moves a thumbstick.
|
||||
xr_session_rebuild_play_space();
|
||||
|
||||
// Prefer a plain linear 8-bit format over GL_SRGB8_ALPHA8: the source
|
||||
// SDL surface pixels are already sRGB-encoded (as ordinary 8-bit image
|
||||
// data conventionally is) and get uploaded/sampled as plain linear
|
||||
// GL_RGBA with no decode step anywhere in this path, matching the
|
||||
// desktop SDL_RenderCopy path this replaces - an sRGB swapchain format
|
||||
// would auto-gamma-encode on write and double-encode already-encoded
|
||||
// data.
|
||||
// data. Every swapchain below (game quad, menu/keyboard overlays, and
|
||||
// - via xr_input_init() - the per-hand laser-beam overlays) shares
|
||||
// this one choice - the compositor's supported format set doesn't
|
||||
// depend on swapchain size.
|
||||
uint32_t formatCount = 0;
|
||||
xrEnumerateSwapchainFormats(g_session, 0, &formatCount, NULL);
|
||||
int64_t *formats = (int64_t *)malloc(sizeof(int64_t) * formatCount);
|
||||
@@ -243,69 +494,42 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
}
|
||||
free(formats);
|
||||
|
||||
XrSwapchainCreateInfo swapchainInfo = {XR_TYPE_SWAPCHAIN_CREATE_INFO};
|
||||
swapchainInfo.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT;
|
||||
swapchainInfo.format = chosenFormat;
|
||||
swapchainInfo.sampleCount = 1;
|
||||
swapchainInfo.width = (uint32_t)game_width;
|
||||
swapchainInfo.height = (uint32_t)game_height;
|
||||
swapchainInfo.faceCount = 1;
|
||||
swapchainInfo.arraySize = 1;
|
||||
swapchainInfo.mipCount = 1;
|
||||
if (!xr_check(xrCreateSwapchain(g_session, &swapchainInfo, &g_game_swapchain),
|
||||
"xrCreateSwapchain"))
|
||||
return false;
|
||||
g_game_width = game_width;
|
||||
g_game_height = game_height;
|
||||
// Non-fatal if it fails (e.g. no controllers bound yet) - rendering
|
||||
// keeps working either way, just without the laser pointer/beams.
|
||||
xr_input_init(g_instance, g_session, chosenFormat);
|
||||
|
||||
uint32_t imageCount = 0;
|
||||
xrEnumerateSwapchainImages(g_game_swapchain, 0, &imageCount, NULL);
|
||||
if (imageCount > MAX_SWAPCHAIN_IMAGES) {
|
||||
LOGE("XR: swapchain reports %u images, only room for %d", imageCount,
|
||||
MAX_SWAPCHAIN_IMAGES);
|
||||
return false;
|
||||
}
|
||||
// Zero-initialized, not just `.type` set per element - these structs
|
||||
// also carry a `next` field the runtime may read, and an
|
||||
// uninitialized stack array would leave it as garbage.
|
||||
XrSwapchainImageOpenGLESKHR images[MAX_SWAPCHAIN_IMAGES] = {0};
|
||||
for (uint32_t i = 0; i < imageCount; i++)
|
||||
images[i].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR;
|
||||
if (!xr_check(xrEnumerateSwapchainImages(g_game_swapchain, imageCount, &imageCount,
|
||||
(XrSwapchainImageBaseHeader *)images),
|
||||
"xrEnumerateSwapchainImages"))
|
||||
return false;
|
||||
g_swapchain_image_count = imageCount;
|
||||
|
||||
if (!xr_load_real_gles()) {
|
||||
LOGE("XR: couldn't resolve real GLES FBO functions via dlsym");
|
||||
if (!xr_swapchain_create(g_instance, g_session, chosenFormat, game_width, game_height,
|
||||
&g_game_swapchain)) {
|
||||
LOGE("XR: game swapchain setup failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wraps each swapchain-provided texture in its own framebuffer, matching
|
||||
// how OpenGL.cc's own CreateFrameBuffer() wraps backupBuffer - just
|
||||
// without a depth/stencil attachment, since the final composite draw
|
||||
// (see opengl_swap_and_restore) never needs one. The Gen/Bind calls are
|
||||
// gl4es's own (linked, not dlsym'd) - see the comment above
|
||||
// real_glFramebufferTexture2D for why.
|
||||
bool all_complete = true;
|
||||
for (uint32_t i = 0; i < imageCount; i++) {
|
||||
glGenFramebuffers(1, &g_swapchain_fbos[i]);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, g_swapchain_fbos[i]);
|
||||
real_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
images[i].image, 0);
|
||||
GLenum status = real_glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
LOGE("XR: swapchain FBO %u incomplete: 0x%x", i, status);
|
||||
all_complete = false;
|
||||
}
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (!all_complete)
|
||||
return false;
|
||||
// Both non-fatal - the game keeps rendering with no such panel if
|
||||
// either fails (e.g. the Java class couldn't be resolved via JNI).
|
||||
XrOverlayConfig menuConfig = {
|
||||
.java_class_name = MENU_JAVA_CLASS,
|
||||
.width = MENU_WIDTH,
|
||||
.height = MENU_HEIGHT,
|
||||
.distance_m = MENU_DISTANCE_METERS,
|
||||
.width_m = MENU_WIDTH_METERS,
|
||||
.default_center_x_m = MENU_DEFAULT_CENTER_X_METERS,
|
||||
.default_center_y_m = MENU_DEFAULT_CENTER_Y_METERS,
|
||||
};
|
||||
g_menu_overlay = xr_overlay_create(g_instance, g_session, chosenFormat, &menuConfig);
|
||||
|
||||
LOGI("XR: instance/session/swapchain ready (%dx%d, %u images)", game_width, game_height,
|
||||
imageCount);
|
||||
XrOverlayConfig keyboardConfig = {
|
||||
.java_class_name = KEYBOARD_JAVA_CLASS,
|
||||
.width = KEYBOARD_WIDTH,
|
||||
.height = KEYBOARD_HEIGHT,
|
||||
.distance_m = KEYBOARD_DISTANCE_METERS,
|
||||
.width_m = KEYBOARD_WIDTH_METERS,
|
||||
.default_center_x_m = KEYBOARD_DEFAULT_CENTER_X_METERS,
|
||||
.default_center_y_m = KEYBOARD_DEFAULT_CENTER_Y_METERS,
|
||||
};
|
||||
g_keyboard_overlay = xr_overlay_create(g_instance, g_session, chosenFormat, &keyboardConfig);
|
||||
|
||||
LOGI("XR: instance/session ready (game %dx%d, menu %dx%d, keyboard %dx%d)", game_width,
|
||||
game_height, MENU_WIDTH, MENU_HEIGHT, KEYBOARD_WIDTH, KEYBOARD_HEIGHT);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -318,6 +542,9 @@ bool xr_init(int game_width, int game_height) {
|
||||
return true;
|
||||
}
|
||||
|
||||
XrOverlay *xr_session_get_menu_overlay(void) { return g_menu_overlay; }
|
||||
XrOverlay *xr_session_get_keyboard_overlay(void) { return g_keyboard_overlay; }
|
||||
|
||||
void xr_poll_events(void) {
|
||||
if (g_instance == XR_NULL_HANDLE)
|
||||
return;
|
||||
@@ -338,7 +565,8 @@ void xr_poll_events(void) {
|
||||
if (g_session_state == XR_SESSION_STATE_READY) {
|
||||
XrSessionBeginInfo beginInfo = {XR_TYPE_SESSION_BEGIN_INFO};
|
||||
beginInfo.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
g_session_running = xr_check(xrBeginSession(g_session, &beginInfo), "xrBeginSession");
|
||||
g_session_running =
|
||||
xr_check(xrBeginSession(g_session, &beginInfo), "xrBeginSession");
|
||||
} else if (g_session_state == XR_SESSION_STATE_STOPPING) {
|
||||
xr_check(xrEndSession(g_session), "xrEndSession");
|
||||
g_session_running = false;
|
||||
@@ -348,12 +576,20 @@ 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; }
|
||||
|
||||
bool xr_frame_begin(void) {
|
||||
g_have_acquired_image = false;
|
||||
g_have_acquired_game_image = false;
|
||||
if (!xr_is_session_running())
|
||||
return false;
|
||||
|
||||
@@ -371,78 +607,213 @@ bool xr_frame_begin(void) {
|
||||
if (!g_frame_should_render)
|
||||
return false;
|
||||
|
||||
uint32_t imageIndex = 0;
|
||||
XrSwapchainImageAcquireInfo acquireInfo = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
|
||||
if (!xr_check(xrAcquireSwapchainImage(g_game_swapchain, &acquireInfo, &imageIndex),
|
||||
"xrAcquireSwapchainImage"))
|
||||
return false;
|
||||
|
||||
XrSwapchainImageWaitInfo waitImageInfo = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
|
||||
waitImageInfo.timeout = XR_INFINITE_DURATION;
|
||||
if (!xr_check(xrWaitSwapchainImage(g_game_swapchain, &waitImageInfo), "xrWaitSwapchainImage"))
|
||||
return false;
|
||||
|
||||
// gl4es's own (linked, not dlsym'd) bind - this targets an FBO id
|
||||
// gl4es itself created (see xr_create_instance_and_session()), so its
|
||||
// own "current FBO" bookkeeping updates correctly and its immediate-
|
||||
// mode draw calls in android_draw_surface_as_quad() land in the right
|
||||
// place.
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, g_swapchain_fbos[imageIndex]);
|
||||
glViewport(0, 0, g_game_width, g_game_height);
|
||||
|
||||
g_have_acquired_image = true;
|
||||
return true;
|
||||
g_have_acquired_game_image = xr_swapchain_acquire(g_instance, &g_game_swapchain);
|
||||
return g_have_acquired_game_image;
|
||||
}
|
||||
|
||||
// Backs xr_session_get_fps()/get_frame_ms() (see xr_session.h) - exponential
|
||||
// smoothing (rather than a plain per-frame instantaneous value) so
|
||||
// MenuOverlay's header display doesn't visibly jitter every single frame.
|
||||
// Called once per xr_frame_end() that reaches its own end (i.e. once per
|
||||
// real XR frame, whether or not the game itself actually rendered anything
|
||||
// new into it this frame - matches the OpenXR compositor's own frame
|
||||
// cadence, which is what "FPS" means to someone looking at this display).
|
||||
static float s_fps = 0.0f;
|
||||
static float s_frame_ms = 0.0f;
|
||||
static float s_cpu_utilization = -1.0f;
|
||||
static float s_gpu_utilization = -1.0f;
|
||||
|
||||
// Reads one XR_META_performance_metrics counter (if it was found at
|
||||
// startup - see xr_session_init_performance_metrics()) and folds it into
|
||||
// *smoothed with the same exponential smoothing as FPS/frame time above.
|
||||
// Leaves *smoothed at -1.0f (its "unsupported" sentinel) untouched if the
|
||||
// path wasn't found, the query fails, or the runtime didn't mark its
|
||||
// floatValue valid this call.
|
||||
static void xr_session_update_utilization(XrPath path, float *smoothed) {
|
||||
if (path == XR_NULL_PATH || g_xrQueryPerformanceMetricsCounterMETA == NULL)
|
||||
return;
|
||||
XrPerformanceMetricsCounterMETA counter = {XR_TYPE_PERFORMANCE_METRICS_COUNTER_META};
|
||||
if (!xr_check(g_xrQueryPerformanceMetricsCounterMETA(g_session, path, &counter),
|
||||
"xrQueryPerformanceMetricsCounterMETA"))
|
||||
return;
|
||||
if (!(counter.counterFlags & XR_PERFORMANCE_METRICS_COUNTER_FLOAT_VALUE_VALID_BIT_META))
|
||||
return;
|
||||
const float kSmoothing = 0.1f;
|
||||
*smoothed =
|
||||
(*smoothed < 0.0f) ? counter.floatValue : *smoothed * (1.0f - kSmoothing) + counter.floatValue * kSmoothing;
|
||||
}
|
||||
|
||||
static void xr_session_update_frame_stats(void) {
|
||||
static Uint32 s_last_ticks = 0;
|
||||
Uint32 now = SDL_GetTicks();
|
||||
if (s_last_ticks != 0) {
|
||||
Uint32 delta_ms = now - s_last_ticks;
|
||||
if (delta_ms > 0) {
|
||||
float instant_fps = 1000.0f / (float)delta_ms;
|
||||
const float kSmoothing = 0.1f;
|
||||
s_fps = (s_fps == 0.0f) ? instant_fps : s_fps * (1.0f - kSmoothing) + instant_fps * kSmoothing;
|
||||
s_frame_ms =
|
||||
(s_frame_ms == 0.0f) ? (float)delta_ms : s_frame_ms * (1.0f - kSmoothing) + (float)delta_ms * kSmoothing;
|
||||
}
|
||||
}
|
||||
s_last_ticks = now;
|
||||
|
||||
xr_session_update_utilization(g_cpu_utilization_path, &s_cpu_utilization);
|
||||
xr_session_update_utilization(g_gpu_utilization_path, &s_gpu_utilization);
|
||||
}
|
||||
|
||||
float xr_session_get_fps(void) { return s_fps; }
|
||||
|
||||
float xr_session_get_frame_ms(void) { return s_frame_ms; }
|
||||
|
||||
float xr_session_get_cpu_utilization(void) { return s_cpu_utilization; }
|
||||
|
||||
float xr_session_get_gpu_utilization(void) { return s_gpu_utilization; }
|
||||
|
||||
void xr_frame_end(void) {
|
||||
if (g_session == XR_NULL_HANDLE)
|
||||
return;
|
||||
|
||||
if (g_have_acquired_image) {
|
||||
XrSwapchainImageReleaseInfo releaseInfo = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
||||
xr_check(xrReleaseSwapchainImage(g_game_swapchain, &releaseInfo),
|
||||
"xrReleaseSwapchainImage");
|
||||
}
|
||||
// Rebuilt before xr_input_sync_and_draw() below so this same frame's
|
||||
// hit-testing and layer submission both see whatever the player's
|
||||
// thumbstick input (or a config-driven "Reset Position"/Load
|
||||
// Configuration) changed last frame - a no-op check on every frame
|
||||
// nothing moved.
|
||||
if (g_player_transform_dirty)
|
||||
xr_session_rebuild_play_space();
|
||||
|
||||
// Syncs actions and hit-tests/dispatches each hand's ray against
|
||||
// keyboard/menu/game quad (see xr_input.c) before the game swapchain
|
||||
// image is released below - this always runs, even when there's
|
||||
// nothing to draw (no acquired image this frame), so edge detection
|
||||
// (trigger/menu-button clicks) doesn't miss a frame. Any resulting
|
||||
// laser-beam quads are submitted as their own composition layers
|
||||
// further down, not drawn into the game swapchain itself.
|
||||
if (xr_is_session_running())
|
||||
xr_input_sync_and_draw(g_play_space, g_predicted_display_time,
|
||||
g_have_acquired_game_image);
|
||||
|
||||
if (g_have_acquired_game_image)
|
||||
xr_swapchain_release(g_instance, &g_game_swapchain);
|
||||
|
||||
if (!xr_is_session_running())
|
||||
return;
|
||||
|
||||
XrCompositionLayerQuad quad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||
quad.space = g_local_space;
|
||||
quad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
quad.subImage.swapchain = g_game_swapchain;
|
||||
quad.subImage.imageRect.extent.width = g_game_width;
|
||||
quad.subImage.imageRect.extent.height = g_game_height;
|
||||
quad.pose.orientation.w = 1.0f;
|
||||
quad.pose.position.z = -QUAD_DISTANCE_METERS;
|
||||
quad.size.width = QUAD_WIDTH_METERS;
|
||||
quad.size.height = QUAD_WIDTH_METERS * (float)g_game_height / (float)g_game_width;
|
||||
// Up to 5 layers: the game screen (if a frame was actually rendered) -
|
||||
// a flat quad, or a curved cylinder if the currently-active screen
|
||||
// profile has a nonzero curve (see xr_get_game_quad_extent()'s own
|
||||
// comment; xr_input.c's xr_input_try_game_quad() hit-tests whichever
|
||||
// shape is actually submitted here, always agreeing since both read
|
||||
// the same getter) - the menu launcher and keyboard overlays while
|
||||
// visible - each gets its own acquire/render/release cycle against
|
||||
// its own swapchain (see xr_overlay_render_and_build_layer()) any
|
||||
// time before xrEndFrame, unlike the game screen there's no per-frame
|
||||
// engine rendering to wrap around here, just each overlay's own blit -
|
||||
// and, per hand, a laser-beam quad while that hand's ray is aimed at
|
||||
// the keyboard, menu, or the game screen itself (see
|
||||
// xr_input_get_beam_layer()).
|
||||
float gameDistance, gameHalfWidth, gameHalfHeight, gameRadius, gameCentralAngle;
|
||||
xr_get_game_quad_extent(&gameDistance, &gameHalfWidth, &gameHalfHeight, &gameRadius,
|
||||
&gameCentralAngle);
|
||||
bool gameScreenCurved = gameCentralAngle > 0.0f;
|
||||
|
||||
const XrCompositionLayerBaseHeader *layers[1] = {(XrCompositionLayerBaseHeader *)&quad};
|
||||
XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||
XrCompositionLayerCylinderKHR gameCylinder = {XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR};
|
||||
if (gameScreenCurved) {
|
||||
gameCylinder.space = g_play_space;
|
||||
gameCylinder.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
gameCylinder.subImage.swapchain = g_game_swapchain.swapchain;
|
||||
gameCylinder.subImage.imageRect.extent.width = g_game_swapchain.width;
|
||||
gameCylinder.subImage.imageRect.extent.height = g_game_swapchain.height;
|
||||
gameCylinder.pose.orientation.w = 1.0f;
|
||||
gameCylinder.pose.position.z = -gameDistance;
|
||||
gameCylinder.radius = gameRadius;
|
||||
gameCylinder.centralAngle = gameCentralAngle;
|
||||
gameCylinder.aspectRatio = (float)g_game_swapchain.width / (float)g_game_swapchain.height;
|
||||
} else {
|
||||
gameQuad.space = g_play_space;
|
||||
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
gameQuad.subImage.swapchain = g_game_swapchain.swapchain;
|
||||
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width;
|
||||
gameQuad.subImage.imageRect.extent.height = g_game_swapchain.height;
|
||||
gameQuad.pose.orientation.w = 1.0f;
|
||||
gameQuad.pose.position.z = -gameDistance;
|
||||
gameQuad.size.width = gameHalfWidth * 2.0f;
|
||||
gameQuad.size.height = gameHalfHeight * 2.0f;
|
||||
}
|
||||
|
||||
const XrCompositionLayerBaseHeader *layers[5];
|
||||
uint32_t layerCount = 0;
|
||||
if (g_have_acquired_game_image)
|
||||
layers[layerCount++] = gameScreenCurved ? (XrCompositionLayerBaseHeader *)&gameCylinder
|
||||
: (XrCompositionLayerBaseHeader *)&gameQuad;
|
||||
|
||||
XrCompositionLayerQuad menuQuad;
|
||||
if (g_frame_should_render && xr_overlay_render_and_build_layer(g_menu_overlay, &menuQuad)) {
|
||||
menuQuad.space = g_play_space;
|
||||
menuQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&menuQuad;
|
||||
}
|
||||
|
||||
XrCompositionLayerQuad keyboardQuad;
|
||||
if (g_frame_should_render &&
|
||||
xr_overlay_render_and_build_layer(g_keyboard_overlay, &keyboardQuad)) {
|
||||
keyboardQuad.space = g_play_space;
|
||||
keyboardQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
// Lets the keyboard sit on screen translucently while playing
|
||||
// rather than fully hiding whatever's behind it - KeyboardOverlay
|
||||
// writes real sub-255 alpha into the swapchain texture (see
|
||||
// OverlayPanel.compositeAndPublish()), already in Android's
|
||||
// default premultiplied format, which is what this flag's absence
|
||||
// of UNPREMULTIPLIED_ALPHA_BIT assumes. The menu launcher above
|
||||
// deliberately doesn't get this flag - it stays fully opaque.
|
||||
keyboardQuad.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
|
||||
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&keyboardQuad;
|
||||
}
|
||||
|
||||
// Submitted last (frontmost) so a beam doesn't z-fight against the
|
||||
// panel surface it's aimed at/terminates on - see
|
||||
// xr_input_get_beam_layer(). Translucent for the same reason the
|
||||
// keyboard is (see comment above).
|
||||
XrCompositionLayerQuad beamQuad[2];
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
if (xr_input_get_beam_layer(hand, &beamQuad[hand])) {
|
||||
beamQuad[hand].space = g_play_space;
|
||||
beamQuad[hand].eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
beamQuad[hand].layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
|
||||
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&beamQuad[hand];
|
||||
}
|
||||
}
|
||||
|
||||
XrFrameEndInfo endInfo = {XR_TYPE_FRAME_END_INFO};
|
||||
endInfo.displayTime = g_predicted_display_time;
|
||||
endInfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||
endInfo.layerCount = g_have_acquired_image ? 1 : 0;
|
||||
endInfo.layers = g_have_acquired_image ? layers : NULL;
|
||||
endInfo.layerCount = layerCount;
|
||||
endInfo.layers = layerCount > 0 ? layers : NULL;
|
||||
|
||||
xr_check(xrEndFrame(g_session, &endInfo), "xrEndFrame");
|
||||
|
||||
xr_session_update_frame_stats();
|
||||
}
|
||||
|
||||
void xr_shutdown(void) {
|
||||
for (uint32_t i = 0; i < g_swapchain_image_count; i++) {
|
||||
if (g_swapchain_fbos[i] != 0)
|
||||
glDeleteFramebuffers(1, &g_swapchain_fbos[i]);
|
||||
}
|
||||
g_swapchain_image_count = 0;
|
||||
|
||||
if (g_game_swapchain != XR_NULL_HANDLE)
|
||||
xrDestroySwapchain(g_game_swapchain);
|
||||
g_game_swapchain = XR_NULL_HANDLE;
|
||||
xr_swapchain_destroy(&g_game_swapchain);
|
||||
|
||||
if (g_local_space != XR_NULL_HANDLE)
|
||||
xrDestroySpace(g_local_space);
|
||||
g_local_space = XR_NULL_HANDLE;
|
||||
|
||||
if (g_play_space != XR_NULL_HANDLE)
|
||||
xrDestroySpace(g_play_space);
|
||||
g_play_space = XR_NULL_HANDLE;
|
||||
|
||||
// Before the session/instance they were created from.
|
||||
xr_input_shutdown();
|
||||
xr_overlay_destroy(g_menu_overlay);
|
||||
g_menu_overlay = NULL;
|
||||
xr_overlay_destroy(g_keyboard_overlay);
|
||||
g_keyboard_overlay = NULL;
|
||||
|
||||
if (g_session != XR_NULL_HANDLE)
|
||||
xrDestroySession(g_session);
|
||||
g_session = XR_NULL_HANDLE;
|
||||
@@ -453,4 +824,74 @@ void xr_shutdown(void) {
|
||||
|
||||
g_session_state = XR_SESSION_STATE_UNKNOWN;
|
||||
g_session_running = false;
|
||||
|
||||
g_performance_metrics_supported = false;
|
||||
g_xrSetPerformanceMetricsStateMETA = NULL;
|
||||
g_xrQueryPerformanceMetricsCounterMETA = NULL;
|
||||
g_cpu_utilization_path = XR_NULL_PATH;
|
||||
g_gpu_utilization_path = XR_NULL_PATH;
|
||||
|
||||
g_cylinder_layer_supported = false;
|
||||
// g_screen_width_m/g_screen_curve_deg/g_screen_distance_m deliberately
|
||||
// NOT reset here - same reasoning as xr_session_set_player_transform()'s
|
||||
// own state: it's meant to persist across a shutdown/reinit within the
|
||||
// same process (e.g. an Android lifecycle pause/resume), not just
|
||||
// within one XR session.
|
||||
}
|
||||
|
||||
void xr_session_set_game_screen_shape(float width_m, float curve_deg, float distance_m) {
|
||||
g_screen_width_m = width_m;
|
||||
g_screen_curve_deg = curve_deg;
|
||||
g_screen_distance_m = distance_m;
|
||||
}
|
||||
|
||||
// Single source of truth for the game screen's current shape, read by
|
||||
// both xr_frame_end() (to build/submit the actual layer) and
|
||||
// xr_input_try_game_quad() (xr_input.c, to hit-test the laser pointer
|
||||
// against it). half_width_m is meaningful only when *central_angle_rad is
|
||||
// 0 (flat); a curved screen's horizontal extent is bounded by
|
||||
// *central_angle_rad instead, with *half_height_m always the vertical
|
||||
// bound either way.
|
||||
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m,
|
||||
float *radius_m, float *central_angle_rad) {
|
||||
float curveDeg = g_cylinder_layer_supported ? g_screen_curve_deg : 0.0f;
|
||||
float aspect = (float)g_game_swapchain.height / (float)g_game_swapchain.width;
|
||||
|
||||
*distance_m = g_screen_distance_m;
|
||||
*half_width_m = g_screen_width_m * 0.5f;
|
||||
*half_height_m = g_screen_width_m * 0.5f * aspect;
|
||||
|
||||
if (curveDeg > 0.0f) {
|
||||
float theta = curveDeg * QS_DEG_TO_RAD;
|
||||
*central_angle_rad = theta;
|
||||
*radius_m = g_screen_width_m / theta;
|
||||
} else {
|
||||
*central_angle_rad = 0.0f;
|
||||
*radius_m = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void xr_get_game_swapchain_size(int *width, int *height) {
|
||||
*width = g_game_swapchain.width;
|
||||
*height = g_game_swapchain.height;
|
||||
}
|
||||
|
||||
void xr_get_game_resolution(int *width, int *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;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,43 @@
|
||||
// Minimal OpenXR bring-up for questshock's immersive Quest build. Keeps the
|
||||
// game's own rendering untouched (see android/engine-patches/
|
||||
// 12-android-openxr-present.patch) - this module only owns the OpenXR
|
||||
// instance/session/swapchain and the final "submit a quad layer instead of
|
||||
// presenting to a window" step. No stereo rendering, no controller input
|
||||
// yet (see the plan's steps B/C/D for those) - the game composite is shown
|
||||
// as a single flat quad floating in front of the viewer.
|
||||
// 11-android-openxr-present.patch) - this module only owns the OpenXR
|
||||
// instance/session/swapchains and the final "submit quad layers instead of
|
||||
// presenting to a window" step. No stereo rendering - the game composite is
|
||||
// shown as a single flat quad floating in front of the viewer.
|
||||
#ifndef QUESTSHOCK_XR_SESSION_H
|
||||
#define QUESTSHOCK_XR_SESSION_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "xr_overlay.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Call once, right after init_opengl() (OpenGL.cc) has created and made
|
||||
// current the GL context SDL/gl4es already use - creates the OpenXR
|
||||
// instance/session sharing that same EGL display/context, plus one
|
||||
// swapchain sized to the game's logical resolution (game_width/height, i.e.
|
||||
// grd_cap->w/h - see Shock.c's InitSDL()). Returns false if OpenXR bring-up
|
||||
// failed (e.g. no runtime installed) - callers should fall back to the
|
||||
// existing window-present path in that case.
|
||||
// instance/session sharing that same EGL display/context, plus the game
|
||||
// quad's own swapchain, sized to the game's logical resolution
|
||||
// (game_width/height, i.e. grd_cap->w/h - see Shock.c's InitSDL()). Also
|
||||
// creates the menu launcher and keyboard overlay quads (see xr_overlay.h) -
|
||||
// two independent XrOverlay instances, each with their own swapchain,
|
||||
// visibility, and position, retrievable via xr_session_get_menu_overlay()/
|
||||
// xr_session_get_keyboard_overlay() below. Returns false if OpenXR
|
||||
// bring-up failed (e.g. no runtime installed) - callers should fall back
|
||||
// to the existing window-present path in that case.
|
||||
bool xr_init(int game_width, int game_height);
|
||||
|
||||
// The menu launcher (small "Keyboard" button panel, toggled by the
|
||||
// controller's menu button) and keyboard (the on-screen key grid, shown/
|
||||
// hidden independently via MenuOverlay's "Keyboard" button and
|
||||
// KeyboardOverlay's own Close button) overlay quads - used by xr_input.c
|
||||
// for ray/quad hit-testing and touch/cursor dispatch, and by
|
||||
// questshock_native.c's JNI glue (nativeShowKeyboard()/
|
||||
// nativeRequestClose()). NULL if xr_init() failed before creating them.
|
||||
XrOverlay *xr_session_get_menu_overlay(void);
|
||||
XrOverlay *xr_session_get_keyboard_overlay(void);
|
||||
|
||||
// Pumps XR session-state events. Call once per frame, before
|
||||
// xr_frame_begin(). Must still be called even when xr_init() returned
|
||||
// false (no-op in that case).
|
||||
@@ -34,21 +49,123 @@ void xr_poll_events(void);
|
||||
bool xr_is_session_running(void);
|
||||
|
||||
// Begins the XR frame (xrWaitFrame/xrBeginFrame) and, if the runtime wants
|
||||
// this frame rendered, acquires the next swapchain image and binds its
|
||||
// framebuffer as the current render target - ready for the caller to draw
|
||||
// into exactly as it would have drawn to the default framebuffer. Returns
|
||||
// true if the caller should draw this frame; xr_frame_end() must be called
|
||||
// unconditionally afterward either way (a begun XR frame must always be
|
||||
// ended, rendered or not).
|
||||
// this frame rendered, acquires the game quad's next swapchain image and
|
||||
// binds its framebuffer as the current render target - ready for the
|
||||
// caller to draw into exactly as it would have drawn to the default
|
||||
// framebuffer. Returns true if the caller should draw this frame;
|
||||
// xr_frame_end() must be called unconditionally afterward either way (a
|
||||
// begun XR frame must always be ended, rendered or not).
|
||||
bool xr_frame_begin(void);
|
||||
|
||||
// Releases the swapchain image (if one was acquired this frame) and
|
||||
// submits it as a single XrCompositionLayerQuad positioned in front of the
|
||||
// local reference space's origin, then ends the XR frame.
|
||||
// Releases the game quad's swapchain image (if one was acquired this
|
||||
// frame), then does the same acquire/render/release cycle for the menu
|
||||
// launcher and keyboard overlay quads via xr_overlay_render_and_build_layer()
|
||||
// for whichever of them is currently visible, and submits whichever of the
|
||||
// three quads actually rendered this frame as composition layers
|
||||
// positioned in front of the play space's origin (see
|
||||
// xr_session_set_player_transform() above), then ends the XR frame.
|
||||
void xr_frame_end(void);
|
||||
|
||||
void xr_shutdown(void);
|
||||
|
||||
// Play-space geometry of the single game screen xr_frame_end() submits
|
||||
// (distance in front of the play space origin, half-width/half-height,
|
||||
// all in meters) - shared with xr_input.c's ray/quad hit-testing so the
|
||||
// laser pointer always matches whatever's actually visible. *radius_m/
|
||||
// *central_angle_rad are 0 when flat, in which case *half_width_m is the
|
||||
// horizontal extent; otherwise the screen is a curved cylinder segment
|
||||
// and the caller uses *central_angle_rad instead of *half_width_m for the
|
||||
// horizontal bound (*half_height_m stays meaningful either way). Curving
|
||||
// is currently unused - qs_config.c has no config surface for it - but
|
||||
// kept working so it can be re-enabled later.
|
||||
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m,
|
||||
float *radius_m, float *central_angle_rad);
|
||||
|
||||
// Sets the game screen's current physical width (meters) and curve (wrap
|
||||
// angle in degrees, 0 = flat) - a plain state setter, no XR calls, same
|
||||
// category as xr_session_set_player_transform() above (safe to call
|
||||
// before xr_init() has even run). Read by both xr_frame_end() (to build/
|
||||
// submit the actual layer) and, via xr_get_game_quad_extent() above,
|
||||
// xr_input.c's hit-testing. Driven by qs_config.c's
|
||||
// qs_screen_profile_notify_loop_mode(), which decides which of the
|
||||
// "Initial Screen"/"Game Screen" config profiles (see CONFIG.md) is
|
||||
// currently active - this function itself has no notion of "profiles",
|
||||
// only "what shape is current".
|
||||
void xr_session_set_game_screen_shape(float width_m, float curve_deg, float distance_m);
|
||||
|
||||
// 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);
|
||||
|
||||
// Pixel size of the game quad's swapchain image, fixed at launch (0, 0 before
|
||||
// xr_init()). The quad's aspect ratio is this size, so full-quad content
|
||||
// drawn outside the engine's own frame path (the startup splash) is laid out
|
||||
// against it.
|
||||
void xr_get_game_swapchain_size(int *width, int *height);
|
||||
|
||||
// Exponentially-smoothed frame rate/frame time, updated once per real
|
||||
// rendered frame in xr_frame_end() - read by MenuOverlay's header (see
|
||||
// questshock_native.c's nativeGetFps()/nativeGetFrameTimeMs()). 0 before
|
||||
// the second frame has run (nothing to compute a delta from yet).
|
||||
float xr_session_get_fps(void);
|
||||
float xr_session_get_frame_ms(void);
|
||||
|
||||
// Device-wide CPU/GPU utilization percentage (0-100), via the
|
||||
// XR_META_performance_metrics extension - same per-frame update cadence as
|
||||
// the FPS/frame-time getters above. Returns -1.0f if the extension isn't
|
||||
// supported by the runtime, or its cpu_utilization/gpu_utilization counter
|
||||
// couldn't be found among the ones it advertises.
|
||||
float xr_session_get_cpu_utilization(void);
|
||||
float xr_session_get_gpu_utilization(void);
|
||||
|
||||
// The player's virtual position/orientation - horizontal-plane only (x/z
|
||||
// plus a yaw rotation about the vertical axis); height always comes from
|
||||
// the real tracked HMD pose, never from this. Everything the app submits
|
||||
// (game/menu/keyboard/beam quads) and every hand/head pose it locates is
|
||||
// expressed relative to a second reference space, derived from the real
|
||||
// LOCAL space by this transform's inverse (see xr_session.c's own
|
||||
// xr_session_rebuild_play_space()) - so changing it moves/turns the player
|
||||
// relative to those otherwise-fixed quads, letting them walk up to and
|
||||
// square up with the game quad ("virtual screen") without needing any
|
||||
// physical room-scale space to do it in.
|
||||
//
|
||||
// xr_session_set_player_transform() only stores the three floats (no XR
|
||||
// calls) - safe to call before xr_init() has even run, which is how
|
||||
// qs_config_load_startup() seeds the saved position before the OpenXR
|
||||
// session/spaces exist yet (xr_init() picks it up when it creates its own
|
||||
// spaces); also used live by xr_input.c's thumbstick locomotion and
|
||||
// "Reset Position" recenter (xr_input_recenter_player_to_screen()) and
|
||||
// qs_config.c's "Load Configuration", and read back by "Save Configuration"
|
||||
// via the getter.
|
||||
void xr_session_set_player_transform(float x, float z, float yaw_deg);
|
||||
void xr_session_get_player_transform(float *x, float *z, float *yaw_deg);
|
||||
|
||||
// The real, physical tracking-origin reference space - used only by
|
||||
// xr_input_recenter_player_to_screen() (xr_input.c), which needs the
|
||||
// player's CURRENT real head pose (not one already bent through the
|
||||
// existing play-space transform - see xr_session_set_player_transform()'s
|
||||
// own comment above) to compute a brand new one. Every other consumer in
|
||||
// this app (rendering, hit-testing) uses the play space instead - see
|
||||
// xr_session_rebuild_play_space()'s own comment for why this one stays a
|
||||
// separate, never-swapped-out space.
|
||||
XrSpace xr_session_get_local_space(void);
|
||||
|
||||
// The most recent xrWaitFrame-predicted display time (xr_frame_begin()'s
|
||||
// own g_predicted_display_time) - for the same reason as
|
||||
// xr_session_get_local_space() above, xr_input_recenter_player_to_screen()
|
||||
// needs a valid XrTime to locate a space with but isn't itself called from
|
||||
// the normal per-frame xr_frame_end()/xr_input_sync_and_draw() call chain
|
||||
// that already threads one through (it's triggered by a button press,
|
||||
// dispatched via qs_config.c's cross-thread event queue instead). A frame
|
||||
// or so of staleness doesn't matter for a one-shot recenter. 0 before the
|
||||
// first frame has ever begun.
|
||||
XrTime xr_session_get_predicted_display_time(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
#include "xr_swapchain.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
#define TAG "QuestShock"
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
||||
|
||||
static bool xr_check(XrInstance instance, XrResult result, const char *what) {
|
||||
if (XR_SUCCEEDED(result))
|
||||
return true;
|
||||
char resultString[XR_MAX_RESULT_STRING_SIZE] = {0};
|
||||
if (instance != XR_NULL_HANDLE)
|
||||
xrResultToString(instance, result, resultString);
|
||||
LOGE("XR: %s failed: %s (%d)", what, resultString[0] ? resultString : "?", (int)result);
|
||||
return false;
|
||||
}
|
||||
|
||||
// This file's other GL calls otherwise resolve to gl4es (the only GL
|
||||
// symbol provider linked into this binary - see android/engine-patches/
|
||||
// 02-android-opengl-es.patch), which is fine for anything shared with the
|
||||
// engine's own gl4es-routed rendering. But the swapchain images OpenXR
|
||||
// hands us are real driver texture objects gl4es never created itself,
|
||||
// and gl4es's own glFramebufferTexture2D can't attach a texture it has no
|
||||
// tracked metadata for. So the FBO *container* is created via gl4es's own
|
||||
// glGenFramebuffers/glBindFramebuffer (so gl4es recognizes the id as its
|
||||
// own and its own per-frame glBindFramebuffer succeeds), while the
|
||||
// texture-attach step - the specifically foreign part - goes through the
|
||||
// real driver directly, via dlsym against libGLESv2.so.
|
||||
typedef void (*PFNQSFRAMEBUFFERTEXTURE2D)(GLenum, GLenum, GLenum, GLuint, GLint);
|
||||
typedef GLenum (*PFNQSCHECKFRAMEBUFFERSTATUS)(GLenum);
|
||||
|
||||
static PFNQSFRAMEBUFFERTEXTURE2D real_glFramebufferTexture2D;
|
||||
static PFNQSCHECKFRAMEBUFFERSTATUS real_glCheckFramebufferStatus;
|
||||
static bool g_real_gles_loaded = false;
|
||||
|
||||
static bool xr_swapchain_load_real_gles(void) {
|
||||
if (g_real_gles_loaded)
|
||||
return true;
|
||||
void *lib = dlopen("libGLESv2.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (lib == NULL) {
|
||||
LOGE("XR: swapchain dlopen(libGLESv2.so) failed: %s", dlerror());
|
||||
return false;
|
||||
}
|
||||
real_glFramebufferTexture2D = (PFNQSFRAMEBUFFERTEXTURE2D)dlsym(lib, "glFramebufferTexture2D");
|
||||
real_glCheckFramebufferStatus =
|
||||
(PFNQSCHECKFRAMEBUFFERSTATUS)dlsym(lib, "glCheckFramebufferStatus");
|
||||
g_real_gles_loaded = real_glFramebufferTexture2D && real_glCheckFramebufferStatus;
|
||||
return g_real_gles_loaded;
|
||||
}
|
||||
|
||||
bool xr_swapchain_create(XrInstance instance, XrSession session, int64_t format, int width,
|
||||
int height, XrSwapchainState *out) {
|
||||
if (!xr_swapchain_load_real_gles()) {
|
||||
LOGE("XR: swapchain couldn't resolve real GLES FBO functions via dlsym");
|
||||
return false;
|
||||
}
|
||||
|
||||
out->width = width;
|
||||
out->height = height;
|
||||
|
||||
XrSwapchainCreateInfo swapchainInfo = {XR_TYPE_SWAPCHAIN_CREATE_INFO};
|
||||
swapchainInfo.usageFlags =
|
||||
XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT;
|
||||
swapchainInfo.format = format;
|
||||
swapchainInfo.sampleCount = 1;
|
||||
swapchainInfo.width = (uint32_t)width;
|
||||
swapchainInfo.height = (uint32_t)height;
|
||||
swapchainInfo.faceCount = 1;
|
||||
swapchainInfo.arraySize = 1;
|
||||
swapchainInfo.mipCount = 1;
|
||||
if (!xr_check(instance, xrCreateSwapchain(session, &swapchainInfo, &out->swapchain),
|
||||
"xrCreateSwapchain"))
|
||||
return false;
|
||||
|
||||
uint32_t imageCount = 0;
|
||||
xrEnumerateSwapchainImages(out->swapchain, 0, &imageCount, NULL);
|
||||
if (imageCount > XR_SWAPCHAIN_MAX_IMAGES) {
|
||||
LOGE("XR: swapchain reports %u images, only room for %d", imageCount,
|
||||
XR_SWAPCHAIN_MAX_IMAGES);
|
||||
return false;
|
||||
}
|
||||
// Zero-initialized, not just `.type` set per element - these structs
|
||||
// also carry a `next` field the runtime may read, and an uninitialized
|
||||
// stack array would leave it as garbage.
|
||||
XrSwapchainImageOpenGLESKHR images[XR_SWAPCHAIN_MAX_IMAGES] = {0};
|
||||
for (uint32_t i = 0; i < imageCount; i++)
|
||||
images[i].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR;
|
||||
if (!xr_check(instance,
|
||||
xrEnumerateSwapchainImages(out->swapchain, imageCount, &imageCount,
|
||||
(XrSwapchainImageBaseHeader *)images),
|
||||
"xrEnumerateSwapchainImages"))
|
||||
return false;
|
||||
out->image_count = imageCount;
|
||||
|
||||
// Wraps each swapchain-provided texture in its own framebuffer, matching
|
||||
// how OpenGL.cc's own CreateFrameBuffer() wraps backupBuffer - just
|
||||
// without a depth/stencil attachment, since the final composite draw
|
||||
// (see opengl_swap_and_restore) never needs one. The Gen/Bind calls are
|
||||
// gl4es's own (linked, not dlsym'd) - see the comment above
|
||||
// real_glFramebufferTexture2D for why.
|
||||
bool all_complete = true;
|
||||
for (uint32_t i = 0; i < imageCount; i++) {
|
||||
glGenFramebuffers(1, &out->fbos[i]);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, out->fbos[i]);
|
||||
real_glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
images[i].image, 0);
|
||||
GLenum status = real_glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
LOGE("XR: swapchain FBO %u incomplete: 0x%x", i, status);
|
||||
all_complete = false;
|
||||
}
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return all_complete;
|
||||
}
|
||||
|
||||
void xr_swapchain_destroy(XrSwapchainState *state) {
|
||||
for (uint32_t i = 0; i < state->image_count; i++) {
|
||||
if (state->fbos[i] != 0)
|
||||
glDeleteFramebuffers(1, &state->fbos[i]);
|
||||
}
|
||||
state->image_count = 0;
|
||||
if (state->swapchain != XR_NULL_HANDLE)
|
||||
xrDestroySwapchain(state->swapchain);
|
||||
state->swapchain = XR_NULL_HANDLE;
|
||||
}
|
||||
|
||||
// Acquires the next image of the given swapchain, waits for the runtime to
|
||||
// finish with it, and binds its framebuffer (sized to exactly fill it) as
|
||||
// the current render target.
|
||||
bool xr_swapchain_acquire(XrInstance instance, XrSwapchainState *state) {
|
||||
uint32_t imageIndex = 0;
|
||||
XrSwapchainImageAcquireInfo acquireInfo = {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
|
||||
if (!xr_check(instance, xrAcquireSwapchainImage(state->swapchain, &acquireInfo, &imageIndex),
|
||||
"xrAcquireSwapchainImage"))
|
||||
return false;
|
||||
|
||||
XrSwapchainImageWaitInfo waitImageInfo = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
|
||||
waitImageInfo.timeout = XR_INFINITE_DURATION;
|
||||
if (!xr_check(instance, xrWaitSwapchainImage(state->swapchain, &waitImageInfo),
|
||||
"xrWaitSwapchainImage"))
|
||||
return false;
|
||||
|
||||
// gl4es's own (linked, not dlsym'd) bind - this targets an FBO id gl4es
|
||||
// itself created (see xr_swapchain_create()), so its own "current FBO"
|
||||
// bookkeeping updates correctly and its immediate-mode draw calls
|
||||
// (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;
|
||||
}
|
||||
|
||||
void xr_swapchain_release(XrInstance instance, XrSwapchainState *state) {
|
||||
XrSwapchainImageReleaseInfo releaseInfo = {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
||||
xr_check(instance, xrReleaseSwapchainImage(state->swapchain, &releaseInfo),
|
||||
"xrReleaseSwapchainImage");
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Generic OpenXR swapchain + per-image GL framebuffer helper, shared by
|
||||
// every quad this app submits - the game quad (xr_session.c) and every
|
||||
// xr_overlay.c instance (menu launcher, keyboard) - since they all need
|
||||
// exactly the same setup, just at their own size/format.
|
||||
#ifndef QUESTSHOCK_XR_SWAPCHAIN_H
|
||||
#define QUESTSHOCK_XR_SWAPCHAIN_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <jni.h>
|
||||
|
||||
#define XR_USE_PLATFORM_ANDROID 1
|
||||
#define XR_USE_GRAPHICS_API_OPENGL_ES 1
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_platform.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Up to this many swapchain images/FBOs - real runtimes report small counts
|
||||
// (2-4); this is just a fixed upper bound for the cache arrays below.
|
||||
#define XR_SWAPCHAIN_MAX_IMAGES 8
|
||||
|
||||
typedef struct {
|
||||
XrSwapchain swapchain;
|
||||
GLuint fbos[XR_SWAPCHAIN_MAX_IMAGES];
|
||||
uint32_t image_count;
|
||||
int width;
|
||||
int height;
|
||||
} XrSwapchainState;
|
||||
|
||||
// Creates the swapchain and wraps each of its images in its own GL
|
||||
// framebuffer, ready to bind and render into directly. instance is only
|
||||
// used to log a human-readable error string on failure.
|
||||
bool xr_swapchain_create(XrInstance instance, XrSession session, int64_t format, int width,
|
||||
int height, XrSwapchainState *out);
|
||||
|
||||
void xr_swapchain_destroy(XrSwapchainState *state);
|
||||
|
||||
// Acquires the next image, waits for the runtime to finish with it, and
|
||||
// binds its framebuffer (sized to exactly fill it) as the current render
|
||||
// target.
|
||||
bool xr_swapchain_acquire(XrInstance instance, XrSwapchainState *state);
|
||||
|
||||
void xr_swapchain_release(XrInstance instance, XrSwapchainState *state);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,586 @@
|
||||
package de.ladkau.questshock;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.libsdl.app.SDLActivity;
|
||||
|
||||
/**
|
||||
* The on-screen keyboard quad (see OverlayPanel for the shared off-screen-
|
||||
* render/touch/cursor plumbing this builds on) - a hand-built, full
|
||||
* US-layout key grid (there's no system IME to borrow once immersive) plus
|
||||
* a text field echoing what's been sent. Every key that reaches the game
|
||||
* (everything but Close/Shift/Ctrl/Alt, which are local UI actions - see
|
||||
* handleKeyDown()/handleKeyUp() and handleCharKeyDown()/handleCharKeyUp())
|
||||
* is driven by that key button's own ACTION_DOWN/ACTION_UP (see
|
||||
* charKeyTouchListener()/controlKeyTouchListener()), which
|
||||
* OverlayPanel.handleTouch() dispatches for exactly as long as the
|
||||
* controller's trigger is physically held while aimed at it - so a key
|
||||
* held on this keyboard behaves like a real key held on a physical one,
|
||||
* not an instant tap. Non-printable keys go through an
|
||||
* SDLActivity.onNativeKeyDown()/onNativeKeyUp() pair, the same one a
|
||||
* physical Bluetooth keyboard's presses already drive; printable keys go
|
||||
* through a synthesized SDL_TEXTINPUT/SDL_KEYUP pair pushed from native
|
||||
* code (see nativeSendPrintableCharDown()/Up() in questshock_native.c) -
|
||||
* except while a Ctrl/Alt "armed" modifier is active, when they go through
|
||||
* the same onNativeKeyDown()/onNativeKeyUp() path instead, so the modifier
|
||||
* carries across (see toggleModifier()). Shift is a purely local layer
|
||||
* toggle (uppercase/symbols vs lowercase/numbers), never itself sent to the
|
||||
* game.
|
||||
*
|
||||
* This is a fully independent overlay quad from MenuOverlay's small
|
||||
* launcher - opened via that launcher's "Keyboard" button
|
||||
* (nativeShowKeyboard()) but otherwise self-contained: once open, the
|
||||
* controller's menu button (which only ever affects the menu launcher, see
|
||||
* xr_input.c) doesn't hide it, so it stays up as a standing input panel
|
||||
* while actually playing rather than a modal you open and close. The only
|
||||
* way to hide it is its own title bar's Close button (buildTitleBar()),
|
||||
* which also doubles as a drag handle for repositioning it (handled
|
||||
* entirely on the native side - see xr_input.c - since it never reaches
|
||||
* this class's own touch dispatch). contentAlpha() renders the whole panel
|
||||
* semi-transparent so whatever's behind it stays visible while it's up.
|
||||
*/
|
||||
public class KeyboardOverlay extends OverlayPanel {
|
||||
// Matches xr_session.c's KEYBOARD_WIDTH/KEYBOARD_HEIGHT swapchain size -
|
||||
// fixed, not tied to any real display metric, since this tree is never
|
||||
// shown in a real window.
|
||||
private static final int WIDTH = 1024;
|
||||
private static final int HEIGHT = 768;
|
||||
|
||||
private static final int TYPED_TEXT_HEIGHT = 100;
|
||||
|
||||
// Height of the title bar (buildTitleBar()) and width of its Close
|
||||
// button - xr_input.c's KEYBOARD_TITLE_BAR_V_FRACTION/
|
||||
// KEYBOARD_CLOSE_BUTTON_U_FRACTION must be kept in sync with these (as
|
||||
// fractions of WIDTH/HEIGHT above), since the drag-handle-vs-Close-
|
||||
// button-vs-rest-of-the-keyboard decision happens entirely on the
|
||||
// native side, before a hit ever reaches this class's own touch
|
||||
// dispatch.
|
||||
private static final int TITLE_BAR_HEIGHT = 90;
|
||||
private static final int CLOSE_BUTTON_WIDTH = 160;
|
||||
|
||||
// ~70% opaque - readable but lets whatever's behind the panel (the game
|
||||
// quad, or empty space) show through while it's left up during play.
|
||||
private static final int PANEL_ALPHA = 180;
|
||||
|
||||
private static KeyboardOverlay instance;
|
||||
|
||||
private TextView typedTextView;
|
||||
private final StringBuilder typedText = new StringBuilder();
|
||||
|
||||
// Title bar chrome text - the only pieces of this panel that get
|
||||
// translated (see Strings.java's own comment on why the actual key
|
||||
// labels don't). Set from MenuOverlay.nativeGetLanguage() at the top of
|
||||
// buildContent() (see OverlayPanel's constructor note on why a field a
|
||||
// subclass's buildContent() depends on can't rely on normal field-
|
||||
// initializer timing) and kept in sync afterward by applyTranslations()
|
||||
// below, called from MenuOverlay's own periodic language-refresh check.
|
||||
private int currentLanguage = Strings.ENGLISH;
|
||||
private TextView titleLabel;
|
||||
private Button closeButton;
|
||||
|
||||
// Shift is a local-only layer toggle (never itself sent to the game -
|
||||
// see updateShiftVisual()); Ctrl/Alt are one-shot "armed" modifiers,
|
||||
// sent to the game the moment they're pressed and released again as
|
||||
// soon as the next key consumes them (see toggleModifier()/
|
||||
// releaseArmedModifiers()) - there's no press-and-hold gesture with a
|
||||
// laser pointer + trigger click, so armed-then-consumed stands in for
|
||||
// holding the key down.
|
||||
private boolean shiftActive = false;
|
||||
private boolean ctrlArmed = false;
|
||||
private boolean altArmed = false;
|
||||
private Button shiftButton;
|
||||
private Button ctrlButton;
|
||||
private Button altButton;
|
||||
|
||||
// Every CharKey created by addRow(), so updateShiftVisual() can relabel
|
||||
// all of them at once when Shift toggles. Assigned (not a field
|
||||
// initializer) at the top of buildContent() - see OverlayPanel's
|
||||
// constructor note on why a field a subclass's buildContent() depends
|
||||
// on can't rely on normal field-initializer timing.
|
||||
private List<CharKey> charKeys;
|
||||
|
||||
// A key whose printed character (and, while unmodified, whose game
|
||||
// input) depends on the Shift layer - covers letters, digits, and
|
||||
// punctuation uniformly (e.g. 'q'/'Q', '1'/'!', '-'/'_'). androidKeyCode
|
||||
// is only used for the Ctrl/Alt-combo path (see handleCharKeyDown()) -
|
||||
// the unmodified path goes through nativeSendPrintableCharDown()/Up()
|
||||
// instead, which doesn't need an Android keycode at all.
|
||||
private static final class CharKey {
|
||||
final char base;
|
||||
final char shifted;
|
||||
final int androidKeyCode;
|
||||
Button button;
|
||||
|
||||
CharKey(char base, char shifted, int androidKeyCode) {
|
||||
this.base = base;
|
||||
this.shifted = shifted;
|
||||
this.androidKeyCode = androidKeyCode;
|
||||
}
|
||||
}
|
||||
|
||||
private static CharKey charKey(char base, char shifted, int androidKeyCode) {
|
||||
return new CharKey(base, shifted, androidKeyCode);
|
||||
}
|
||||
|
||||
// Space is a CharKey like any other (see addRow()), but a literal
|
||||
// space character makes for an unreadable, blank-looking button - keep
|
||||
// showing the word "Space" instead, same as before it became a CharKey.
|
||||
private static String charKeyLabel(CharKey key, boolean shiftActive) {
|
||||
if (key.base == ' ') {
|
||||
return "Space";
|
||||
}
|
||||
return String.valueOf(shiftActive ? key.shifted : key.base);
|
||||
}
|
||||
|
||||
private KeyboardOverlay(Activity activity) {
|
||||
super(activity, WIDTH, HEIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int contentAlpha() {
|
||||
return PANEL_ALPHA;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View buildContent() {
|
||||
charKeys = new ArrayList<>();
|
||||
currentLanguage = MenuOverlay.nativeGetLanguage();
|
||||
|
||||
LinearLayout panel = new LinearLayout(activity);
|
||||
panel.setOrientation(LinearLayout.VERTICAL);
|
||||
panel.setBackgroundColor(0xFF202020);
|
||||
|
||||
panel.addView(buildTitleBar(),
|
||||
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, TITLE_BAR_HEIGHT));
|
||||
|
||||
typedTextView = new TextView(activity);
|
||||
typedTextView.setTextColor(0xFFFFFFFF);
|
||||
typedTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 28);
|
||||
typedTextView.setBackgroundColor(0xFF303030);
|
||||
typedTextView.setPadding(20, 20, 20, 20);
|
||||
panel.addView(typedTextView, new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, TYPED_TEXT_HEIGHT));
|
||||
|
||||
// Rows follow real keyboard geometry where it helps (Tab starting
|
||||
// the QWERTY row, Enter ending the home row, Shift starting the
|
||||
// bottom-letter row, a compact arrow cluster bottom-right) so the
|
||||
// layout reads as a familiar keyboard rather than an arbitrary grid.
|
||||
addRow(panel, "Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10",
|
||||
"F11", "F12", "Back");
|
||||
addRow(panel,
|
||||
charKey('`', '~', KeyEvent.KEYCODE_GRAVE),
|
||||
charKey('1', '!', KeyEvent.KEYCODE_1),
|
||||
charKey('2', '@', KeyEvent.KEYCODE_2),
|
||||
charKey('3', '#', KeyEvent.KEYCODE_3),
|
||||
charKey('4', '$', KeyEvent.KEYCODE_4),
|
||||
charKey('5', '%', KeyEvent.KEYCODE_5),
|
||||
charKey('6', '^', KeyEvent.KEYCODE_6),
|
||||
charKey('7', '&', KeyEvent.KEYCODE_7),
|
||||
charKey('8', '*', KeyEvent.KEYCODE_8),
|
||||
charKey('9', '(', KeyEvent.KEYCODE_9),
|
||||
charKey('0', ')', KeyEvent.KEYCODE_0),
|
||||
charKey('-', '_', KeyEvent.KEYCODE_MINUS),
|
||||
charKey('=', '+', KeyEvent.KEYCODE_EQUALS));
|
||||
addRow(panel, "Tab",
|
||||
charKey('q', 'Q', KeyEvent.KEYCODE_Q), charKey('w', 'W', KeyEvent.KEYCODE_W),
|
||||
charKey('e', 'E', KeyEvent.KEYCODE_E), charKey('r', 'R', KeyEvent.KEYCODE_R),
|
||||
charKey('t', 'T', KeyEvent.KEYCODE_T), charKey('y', 'Y', KeyEvent.KEYCODE_Y),
|
||||
charKey('u', 'U', KeyEvent.KEYCODE_U), charKey('i', 'I', KeyEvent.KEYCODE_I),
|
||||
charKey('o', 'O', KeyEvent.KEYCODE_O), charKey('p', 'P', KeyEvent.KEYCODE_P),
|
||||
charKey('[', '{', KeyEvent.KEYCODE_LEFT_BRACKET),
|
||||
charKey(']', '}', KeyEvent.KEYCODE_RIGHT_BRACKET),
|
||||
charKey('\\', '|', KeyEvent.KEYCODE_BACKSLASH));
|
||||
addRow(panel, "Ctrl",
|
||||
charKey('a', 'A', KeyEvent.KEYCODE_A), charKey('s', 'S', KeyEvent.KEYCODE_S),
|
||||
charKey('d', 'D', KeyEvent.KEYCODE_D), charKey('f', 'F', KeyEvent.KEYCODE_F),
|
||||
charKey('g', 'G', KeyEvent.KEYCODE_G), charKey('h', 'H', KeyEvent.KEYCODE_H),
|
||||
charKey('j', 'J', KeyEvent.KEYCODE_J), charKey('k', 'K', KeyEvent.KEYCODE_K),
|
||||
charKey('l', 'L', KeyEvent.KEYCODE_L),
|
||||
charKey(';', ':', KeyEvent.KEYCODE_SEMICOLON),
|
||||
charKey('\'', '"', KeyEvent.KEYCODE_APOSTROPHE),
|
||||
"Enter");
|
||||
addRow(panel, "Shift",
|
||||
charKey('z', 'Z', KeyEvent.KEYCODE_Z), charKey('x', 'X', KeyEvent.KEYCODE_X),
|
||||
charKey('c', 'C', KeyEvent.KEYCODE_C), charKey('v', 'V', KeyEvent.KEYCODE_V),
|
||||
charKey('b', 'B', KeyEvent.KEYCODE_B), charKey('n', 'N', KeyEvent.KEYCODE_N),
|
||||
charKey('m', 'M', KeyEvent.KEYCODE_M),
|
||||
charKey(',', '<', KeyEvent.KEYCODE_COMMA),
|
||||
charKey('.', '>', KeyEvent.KEYCODE_PERIOD),
|
||||
charKey('/', '?', KeyEvent.KEYCODE_SLASH),
|
||||
"Up");
|
||||
addRow(panel, "Alt", charKey(' ', ' ', KeyEvent.KEYCODE_SPACE), "Left", "Down", "Right");
|
||||
return panel;
|
||||
}
|
||||
|
||||
// The title bar: a decorative "drag here" label (not itself
|
||||
// interactive - see the class doc, dragging is handled entirely by
|
||||
// xr_input.c before a hit ever reaches here) plus a real Close button,
|
||||
// which is dispatched as an ordinary click like any other key. Its
|
||||
// height and the Close button's width must stay in sync with
|
||||
// xr_input.c's KEYBOARD_TITLE_BAR_V_FRACTION/
|
||||
// KEYBOARD_CLOSE_BUTTON_U_FRACTION.
|
||||
private LinearLayout buildTitleBar() {
|
||||
LinearLayout bar = new LinearLayout(activity);
|
||||
bar.setOrientation(LinearLayout.HORIZONTAL);
|
||||
// Matches MenuOverlay's own header bar style.
|
||||
bar.setBackgroundColor(0xFF000080);
|
||||
|
||||
titleLabel = new TextView(activity);
|
||||
titleLabel.setText(Strings.get("keyboard_drag_title", currentLanguage));
|
||||
titleLabel.setTextColor(0xFFFFFFFF);
|
||||
titleLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
||||
titleLabel.setGravity(Gravity.CENTER_VERTICAL);
|
||||
titleLabel.setPadding(24, 0, 0, 0);
|
||||
bar.addView(titleLabel, new LinearLayout.LayoutParams(
|
||||
0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||
|
||||
closeButton = new Button(activity);
|
||||
closeButton.setText(Strings.get("close", currentLanguage));
|
||||
closeButton.setOnClickListener(v -> nativeRequestClose());
|
||||
closeButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
||||
closeButton.setBackground(buttonBackground());
|
||||
closeButton.setTextColor(buttonTextColors());
|
||||
registerHoverable(closeButton);
|
||||
bar.addView(closeButton,
|
||||
new LinearLayout.LayoutParams(CLOSE_BUTTON_WIDTH, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
return bar;
|
||||
}
|
||||
|
||||
// Each row fills the panel's remaining height evenly (weight 1 on the
|
||||
// row itself). Accepts a mix of String (a fixed-action control key,
|
||||
// handled by handleKeyDown()/handleKeyUp()) and CharKey (a
|
||||
// Shift-sensitive character key, handled by handleCharKeyDown()/
|
||||
// handleCharKeyUp()) per item, so a row can match real keyboard
|
||||
// geometry (e.g. "Tab" followed by a run of CharKeys).
|
||||
// Within a row, CharKeys and most control keys share width equally
|
||||
// except Space (wider, like a real keyboard) and Esc/Back/Enter/Tab/
|
||||
// Shift/Ctrl/Alt (narrower, see keyWeight()).
|
||||
private void addRow(LinearLayout panel, Object... items) {
|
||||
LinearLayout row = new LinearLayout(activity);
|
||||
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||
for (Object item : items) {
|
||||
Button key = new Button(activity);
|
||||
key.setBackground(buttonBackground());
|
||||
key.setTextColor(buttonTextColors());
|
||||
registerHoverable(key);
|
||||
float weight;
|
||||
if (item instanceof CharKey) {
|
||||
CharKey charKey = (CharKey) item;
|
||||
charKey.button = key;
|
||||
key.setText(charKeyLabel(charKey, shiftActive));
|
||||
key.setOnTouchListener(charKeyTouchListener(charKey));
|
||||
charKeys.add(charKey);
|
||||
// Space is a CharKey too (base == shifted == ' '), but keeps
|
||||
// its traditional wide key like the other control keys do.
|
||||
weight = (charKey.base == ' ') ? 4f : 1f;
|
||||
} else {
|
||||
String label = (String) item;
|
||||
key.setText(label);
|
||||
weight = keyWeight(label);
|
||||
// Shift/Ctrl/Alt are local UI toggles with their own
|
||||
// instantaneous press/release semantics (see
|
||||
// toggleModifier()), not a value forwarded to the game for
|
||||
// as long as the key is held - a plain click is enough.
|
||||
// Every other control key reaches the game and needs real
|
||||
// hold-duration semantics (see controlKeyTouchListener()).
|
||||
if (label.equals("Shift") || label.equals("Ctrl") || label.equals("Alt")) {
|
||||
key.setOnClickListener(v -> handleToggleKey(label));
|
||||
key.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
||||
} else {
|
||||
key.setOnTouchListener(controlKeyTouchListener(label));
|
||||
}
|
||||
if (label.equals("Shift")) {
|
||||
shiftButton = key;
|
||||
} else if (label.equals("Ctrl")) {
|
||||
ctrlButton = key;
|
||||
} else if (label.equals("Alt")) {
|
||||
altButton = key;
|
||||
}
|
||||
}
|
||||
row.addView(key, new LinearLayout.LayoutParams(
|
||||
0, ViewGroup.LayoutParams.MATCH_PARENT, weight));
|
||||
}
|
||||
panel.addView(row, new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
}
|
||||
|
||||
// Drives a control key (everything controlKeyCode() maps, i.e. every
|
||||
// key in addRow() except CharKeys and the Shift/Ctrl/Alt toggles) from
|
||||
// its button's own touch lifecycle, so it reaches the game for exactly
|
||||
// as long as the controller's trigger is held on it (see the class
|
||||
// doc). ACTION_CANCEL (the ray straying off the button, or another
|
||||
// overlay claiming it - see xr_input.c) releases the key the same as
|
||||
// ACTION_UP, so a key can never be left stuck down.
|
||||
private View.OnTouchListener controlKeyTouchListener(String label) {
|
||||
return (v, event) -> {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
v.setPressed(true);
|
||||
handleKeyDown(label);
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
v.setPressed(false);
|
||||
handleKeyUp(label);
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
v.setPressed(false);
|
||||
handleKeyUp(label);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// Same as controlKeyTouchListener() above, for a CharKey.
|
||||
private View.OnTouchListener charKeyTouchListener(CharKey key) {
|
||||
return (v, event) -> {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
v.setPressed(true);
|
||||
handleCharKeyDown(key);
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
v.setPressed(false);
|
||||
handleCharKeyUp(key);
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
v.setPressed(false);
|
||||
handleCharKeyUp(key);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
private static float keyWeight(String label) {
|
||||
if (label.equals("Back") || label.equals("Esc") || label.equals("Enter")
|
||||
|| label.equals("Tab") || label.equals("Shift") || label.equals("Ctrl")
|
||||
|| label.equals("Alt")) {
|
||||
return 1.5f;
|
||||
}
|
||||
return 1f;
|
||||
}
|
||||
|
||||
// Maps a control key's label to the Android keycode forwarded via
|
||||
// SDLActivity.onNativeKeyDown()/onNativeKeyUp() - the same pair a
|
||||
// physical Bluetooth keyboard's key events already drive (see
|
||||
// SDLActivity.handleKeyEvent()). F-keys are contiguous in
|
||||
// android.view.KeyEvent (KEYCODE_F1..KEYCODE_F12), so "F1".."F12" are
|
||||
// computed rather than listed individually.
|
||||
private static int controlKeyCode(String label) {
|
||||
switch (label) {
|
||||
case "Back": return KeyEvent.KEYCODE_DEL;
|
||||
case "Esc": return KeyEvent.KEYCODE_ESCAPE;
|
||||
case "Enter": return KeyEvent.KEYCODE_ENTER;
|
||||
case "Tab": return KeyEvent.KEYCODE_TAB;
|
||||
case "Up": return KeyEvent.KEYCODE_DPAD_UP;
|
||||
case "Down": return KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
case "Left": return KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
case "Right": return KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
default:
|
||||
if (label.charAt(0) == 'F') {
|
||||
int n = Integer.parseInt(label.substring(1));
|
||||
return KeyEvent.KEYCODE_F1 + (n - 1);
|
||||
}
|
||||
throw new IllegalArgumentException("no keycode for " + label);
|
||||
}
|
||||
}
|
||||
|
||||
// Shift is a local layer toggle; Ctrl/Alt are one-shot armed modifiers
|
||||
// (see toggleModifier()) - neither is forwarded to the game as a key of
|
||||
// its own, so a plain click (rather than the hold-duration dispatch
|
||||
// every other key below uses) is enough.
|
||||
private void handleToggleKey(String label) {
|
||||
if (label.equals("Shift")) {
|
||||
shiftActive = !shiftActive;
|
||||
updateShiftVisual();
|
||||
return;
|
||||
}
|
||||
toggleModifier(label);
|
||||
}
|
||||
|
||||
// Everything controlKeyCode() maps (Back/Esc/Enter/Tab/arrows/F1-F12)
|
||||
// is non-printable, so it's forwarded as a real Android key event via
|
||||
// that code - down on this key's own ACTION_DOWN, up on ACTION_UP/
|
||||
// CANCEL (see controlKeyTouchListener()), the same down/up split a
|
||||
// physical Bluetooth keyboard's own key events already carry. This all
|
||||
// matters beyond just the visible typedTextView below: the engine's
|
||||
// cutscene skip handler (cutsloop.c's cutscene_key_handler()) only
|
||||
// reacts to Esc/Enter/Space, so those need to reach the game as actual
|
||||
// game input, not just be echoed locally.
|
||||
private void handleKeyDown(String label) {
|
||||
int keyCode = controlKeyCode(label);
|
||||
SDLActivity.onNativeKeyDown(keyCode);
|
||||
|
||||
if (label.equals("Back")) {
|
||||
if (typedText.length() > 0) {
|
||||
typedText.setLength(typedText.length() - 1);
|
||||
}
|
||||
} else {
|
||||
typedText.append('[').append(label).append(']');
|
||||
}
|
||||
typedTextView.setText(typedText.toString());
|
||||
}
|
||||
|
||||
private void handleKeyUp(String label) {
|
||||
SDLActivity.onNativeKeyUp(controlKeyCode(label));
|
||||
releaseArmedModifiers();
|
||||
}
|
||||
|
||||
// A CharKey's printed character depends only on the Shift layer
|
||||
// (updateShiftVisual() keeps its button's label in sync). Which game
|
||||
// input it produces depends on whether a modifier is armed: unmodified,
|
||||
// it goes through nativeSendPrintableCharDown()/Up() below like a
|
||||
// normal typed character; with Ctrl/Alt armed, it instead goes through
|
||||
// the same onNativeKeyDown()/onNativeKeyUp() path handleKeyDown()/
|
||||
// handleKeyUp() use for control keys, since that's the only path that
|
||||
// carries ev.key.keysym.mod through to the engine (sdl_events.c's
|
||||
// pump_events() reads Ctrl/Alt only off that path, never off
|
||||
// SDL_TEXTINPUT).
|
||||
private void handleCharKeyDown(CharKey key) {
|
||||
char c = shiftActive ? key.shifted : key.base;
|
||||
if (ctrlArmed || altArmed) {
|
||||
SDLActivity.onNativeKeyDown(key.androidKeyCode);
|
||||
} else {
|
||||
nativeSendPrintableCharDown(c);
|
||||
}
|
||||
typedText.append(c);
|
||||
typedTextView.setText(typedText.toString());
|
||||
}
|
||||
|
||||
private void handleCharKeyUp(CharKey key) {
|
||||
char c = shiftActive ? key.shifted : key.base;
|
||||
if (ctrlArmed || altArmed) {
|
||||
SDLActivity.onNativeKeyUp(key.androidKeyCode);
|
||||
releaseArmedModifiers();
|
||||
} else {
|
||||
nativeSendPrintableCharUp(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl/Alt are "armed" rather than held: pressing one immediately sends
|
||||
// its keydown (so SDL's own modifier tracking picks it up for whatever
|
||||
// key comes next - see handleCharKeyDown()/handleKeyDown()) and
|
||||
// brackets its label for feedback; pressing the same key again before
|
||||
// it's been used cancels it (sends the matching keyup, un-brackets).
|
||||
// The normal case - actually being consumed by the next key press - is
|
||||
// handled by releaseArmedModifiers() below, not here.
|
||||
private void toggleModifier(String label) {
|
||||
boolean ctrl = label.equals("Ctrl");
|
||||
Button button = ctrl ? ctrlButton : altButton;
|
||||
int keyCode = ctrl ? KeyEvent.KEYCODE_CTRL_LEFT : KeyEvent.KEYCODE_ALT_LEFT;
|
||||
boolean nowArmed = ctrl ? !ctrlArmed : !altArmed;
|
||||
if (ctrl) {
|
||||
ctrlArmed = nowArmed;
|
||||
} else {
|
||||
altArmed = nowArmed;
|
||||
}
|
||||
if (nowArmed) {
|
||||
SDLActivity.onNativeKeyDown(keyCode);
|
||||
} else {
|
||||
SDLActivity.onNativeKeyUp(keyCode);
|
||||
}
|
||||
button.setText(nowArmed ? "[" + label + "]" : label);
|
||||
}
|
||||
|
||||
// Called after any key actually reaches the game (a control key's
|
||||
// handleKeyUp(), or a CharKey's handleCharKeyUp()) so an armed Ctrl/Alt
|
||||
// only ever applies to the very next key, then releases - matching a
|
||||
// real Ctrl/Alt+key combo's keyup once the combo is done.
|
||||
private void releaseArmedModifiers() {
|
||||
if (ctrlArmed) {
|
||||
SDLActivity.onNativeKeyUp(KeyEvent.KEYCODE_CTRL_LEFT);
|
||||
ctrlArmed = false;
|
||||
ctrlButton.setText("Ctrl");
|
||||
}
|
||||
if (altArmed) {
|
||||
SDLActivity.onNativeKeyUp(KeyEvent.KEYCODE_ALT_LEFT);
|
||||
altArmed = false;
|
||||
altButton.setText("Alt");
|
||||
}
|
||||
}
|
||||
|
||||
// Shift never itself reaches the game (see class doc) - it only flips
|
||||
// which character each CharKey shows/sends, including its own label.
|
||||
// Note this means a Ctrl+Shift+key chord won't carry Shift to the
|
||||
// engine (Ctrl/Alt are real forwarded modifier keys, Shift here isn't) -
|
||||
// not worth solving unless it actually comes up.
|
||||
private void updateShiftVisual() {
|
||||
shiftButton.setText(shiftActive ? "[Shift]" : "Shift");
|
||||
for (CharKey key : charKeys) {
|
||||
key.button.setText(charKeyLabel(key, shiftActive));
|
||||
}
|
||||
}
|
||||
|
||||
// Synthesizes the SDL_TEXTINPUT/SDL_KEYUP pair pump_events()
|
||||
// (sdl_events.c) requires for printable characters (see
|
||||
// handleCharKeyDown()/handleCharKeyUp() above), by building and
|
||||
// pushing them directly in native code rather than through Android's
|
||||
// IME (there's no real IME session behind this off-screen,
|
||||
// never-attached grid for that plumbing to hook into).
|
||||
private static native void nativeSendPrintableCharDown(char c);
|
||||
|
||||
private static native void nativeSendPrintableCharUp(char c);
|
||||
|
||||
// Hides this overlay's quad (xr_overlay_set_visible(..., false)) - the
|
||||
// only way to do so, since the controller's menu button doesn't affect
|
||||
// the keyboard (see this class's doc comment and xr_input.c).
|
||||
private static native void nativeRequestClose();
|
||||
|
||||
// Called from MenuOverlay's own periodic header-refresh timer when it
|
||||
// detects the game's language preference changed mid-session (via its
|
||||
// own options menu) - re-labels this panel's title bar chrome to match.
|
||||
// A plain Java call (both classes are in this same package), not JNI -
|
||||
// runOnUiThread() here is defensive/self-documenting more than
|
||||
// strictly needed today, since MenuOverlay's own timer already runs on
|
||||
// the UI thread itself (Activity.runOnUiThread() is a no-op wrapper
|
||||
// when already on that thread).
|
||||
public static void applyTranslations(final int language) {
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
instance.activity.runOnUiThread(() -> {
|
||||
instance.currentLanguage = language;
|
||||
instance.titleLabel.setText(Strings.get("keyboard_drag_title", language));
|
||||
instance.closeButton.setText(Strings.get("close", language));
|
||||
instance.redrawContent();
|
||||
});
|
||||
}
|
||||
|
||||
// Called from xr_overlay.c's xr_overlay_init_jni(), on the render
|
||||
// thread, right after it resolves this class via the activity's own
|
||||
// ClassLoader (plain FindClass() can't see app classes from a thread
|
||||
// that was attached to the JVM rather than spawned from Java).
|
||||
public static void nativeInit(Activity activity) {
|
||||
if (instance == null) {
|
||||
instance = new KeyboardOverlay(activity);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] nativeTakePixelsIfDirty() {
|
||||
return instance == null ? null : instance.takePixelsIfDirty();
|
||||
}
|
||||
|
||||
public static void nativeDispatchTouch(final float u, final float v, final boolean down) {
|
||||
if (instance != null) {
|
||||
instance.dispatchTouch(u, v, down);
|
||||
}
|
||||
}
|
||||
|
||||
public static void nativeUpdateCursor(final float u, final float v, final boolean visible) {
|
||||
if (instance != null) {
|
||||
instance.dispatchUpdateCursor(u, v, visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
package de.ladkau.questshock;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.StateListDrawable;
|
||||
import android.os.SystemClock;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Shared machinery for an off-screen, never-attached-to-a-window View tree
|
||||
* rendered into its own OpenXR quad (see android/app/src/main/cpp/
|
||||
* xr_overlay.c, which owns the quad's swapchain and drives whichever
|
||||
* concrete subclass via JNI - construction, touch input, and pixel
|
||||
* readback). Rendering to a Bitmap and dispatching synthetic MotionEvents
|
||||
* into an unattached hierarchy both work the same way they would for an
|
||||
* attached View - draw(Canvas)/dispatchTouchEvent() don't require a
|
||||
* ViewRootImpl/window, just a measured+laid-out tree.
|
||||
*
|
||||
* Subclasses (MenuOverlay, KeyboardOverlay) provide their own content via
|
||||
* buildContent() and, if they want translucency, contentAlpha() - and, since
|
||||
* Java has no static virtual dispatch and each overlay needs its own
|
||||
* singleton, each subclass still declares its own thin nativeInit()/
|
||||
* nativeTakePixelsIfDirty()/nativeDispatchTouch()/nativeUpdateCursor()
|
||||
* static methods (matching what xr_overlay.c's JNI glue resolves by class
|
||||
* name) that just delegate into the instance methods here
|
||||
* (takePixelsIfDirty()/dispatchTouch()/dispatchUpdateCursor()).
|
||||
*/
|
||||
abstract class OverlayPanel {
|
||||
private static final float CURSOR_RADIUS = 10f;
|
||||
|
||||
// Button's own click handling (View.onTouchEvent()'s ACTION_UP case)
|
||||
// calls View.post(mPerformClick) rather than invoking performClick()
|
||||
// directly; post() queues the runnable to run once the view is attached
|
||||
// to a window and returns true immediately even when unattached, so on
|
||||
// this permanently-unattached tree that queued click silently never
|
||||
// fires - dispatchTouchEvent() still delivers the down/up events
|
||||
// correctly, only the click callback is swallowed. Registering this as
|
||||
// each button's OnTouchListener bypasses that path entirely: returning
|
||||
// true here skips View's internal onTouchEvent() (see
|
||||
// ViewGroup/View#dispatchTouchEvent), so performClick() (which still
|
||||
// runs any OnClickListener set via setOnClickListener()) is called
|
||||
// directly instead.
|
||||
protected static final View.OnTouchListener CLICK_ON_TOUCH_UP = (v, event) -> {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
v.setPressed(true);
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
v.setPressed(false);
|
||||
v.performClick();
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
v.setPressed(false);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// A plain, instantly-drawn StateListDrawable rather than the platform
|
||||
// default Button background (a Material ripple): ripples animate via
|
||||
// continuous invalidation callbacks tied to a real attached window's
|
||||
// vsync, which never fires here (this tree is never attached to a
|
||||
// window) - so setPressed()/setHovered() state changes would otherwise
|
||||
// produce no visible change at all, independent of this project's own
|
||||
// touch handling (which already calls setPressed() correctly - see
|
||||
// CLICK_ON_TOUCH_UP above). Every button this project creates
|
||||
// (MenuOverlay, KeyboardOverlay) should use this (or
|
||||
// MenuOverlay.selectableButtonBackground(), which adds a persistent
|
||||
// "selected" state on top of the same normal/hovered/pressed colors) -
|
||||
// paired with buttonTextColors() below, since the normal/hovered state
|
||||
// is light enough to need dark text while pressed/selected need white.
|
||||
protected static Drawable buttonBackground() {
|
||||
StateListDrawable drawable = new StateListDrawable();
|
||||
drawable.addState(new int[]{android.R.attr.state_pressed}, new ColorDrawable(PRESSED_COLOR));
|
||||
drawable.addState(new int[]{android.R.attr.state_hovered}, new ColorDrawable(HOVERED_COLOR));
|
||||
drawable.addState(new int[]{}, new ColorDrawable(NORMAL_COLOR));
|
||||
return drawable;
|
||||
}
|
||||
|
||||
// See buttonBackground() above - light gray (normal/hovered) needs dark
|
||||
// text; the accent colors (pressed, and MenuOverlay's own "selected"
|
||||
// state) need white. Every button should get both this and
|
||||
// buttonBackground()/selectableButtonBackground() together.
|
||||
protected static ColorStateList buttonTextColors() {
|
||||
return new ColorStateList(
|
||||
new int[][]{
|
||||
new int[]{android.R.attr.state_pressed},
|
||||
new int[]{android.R.attr.state_selected},
|
||||
new int[]{},
|
||||
},
|
||||
new int[]{0xFFFFFFFF, 0xFFFFFFFF, 0xFF202020});
|
||||
}
|
||||
|
||||
protected static final int NORMAL_COLOR = 0xFFC8C8C8;
|
||||
protected static final int HOVERED_COLOR = 0xFFAEC2E8;
|
||||
protected static final int PRESSED_COLOR = 0xFF3355AA;
|
||||
|
||||
protected final Activity activity;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final View root;
|
||||
|
||||
// Views registered via registerHoverable() (every button, from both
|
||||
// buildContent() and any later-added content) - hit-tested against the
|
||||
// cursor position on every updateCursor() call so aiming at (not
|
||||
// clicking) one visibly highlights it via android.R.attr.state_hovered
|
||||
// (a real View state since API 14, paired with buttonBackground()
|
||||
// above).
|
||||
private final List<View> hoverableViews = new ArrayList<>();
|
||||
private View hoveredView = null;
|
||||
|
||||
// contentBitmap holds just the panel's own rendered content (no cursor),
|
||||
// redrawn only when that content actually changes (redrawContent(), via
|
||||
// handleTouch() or a subclass's own key handling) via root.draw() - a
|
||||
// full off-screen View-tree traversal that gets noticeably more
|
||||
// expensive on a busier content tree. bitmap is what's actually
|
||||
// published to native (see takePixelsIfDirty()): compositeAndPublish()
|
||||
// cheaply blits contentBitmap plus the cursor circle into it. Splitting
|
||||
// these two apart matters because updateCursor() below fires every
|
||||
// single frame (~90Hz) while aiming at the panel - if it called the
|
||||
// expensive root.draw() path every time (as an earlier version did), a
|
||||
// busy content tree made each redraw slow enough that runOnUiThread()
|
||||
// posts piled up faster than the UI thread could drain them, so the
|
||||
// cursor visibly lagged behind the controller's actual aim instead of
|
||||
// tracking it.
|
||||
private final Bitmap contentBitmap;
|
||||
private final Canvas contentCanvas;
|
||||
private final Bitmap bitmap;
|
||||
private final Canvas canvas;
|
||||
private final Paint cursorPaint;
|
||||
// Draws contentBitmap at contentAlpha() - see compositeAndPublish().
|
||||
private final Paint contentAlphaPaint;
|
||||
|
||||
private final Object pixelLock = new Object();
|
||||
private byte[] pendingPixels;
|
||||
|
||||
// Only ever touched on the UI thread (both dispatchUpdateCursor() and
|
||||
// compositeAndPublish() run/are posted there) - no lock needed, unlike
|
||||
// pendingPixels above.
|
||||
private boolean cursorVisible = false;
|
||||
private float cursorX = 0f;
|
||||
private float cursorY = 0f;
|
||||
|
||||
protected OverlayPanel(Activity activity, int width, int height) {
|
||||
this.activity = activity;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
root = buildContent();
|
||||
int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
|
||||
int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
|
||||
root.measure(widthSpec, heightSpec);
|
||||
root.layout(0, 0, width, height);
|
||||
|
||||
contentBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
contentCanvas = new Canvas(contentBitmap);
|
||||
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
canvas = new Canvas(bitmap);
|
||||
|
||||
cursorPaint = new Paint();
|
||||
cursorPaint.setColor(0xFFFFFFFF);
|
||||
cursorPaint.setAntiAlias(true);
|
||||
|
||||
// contentAlpha() must not depend on any subclass instance state -
|
||||
// it's called here, from the superclass constructor, before the
|
||||
// subclass's own field initializers/constructor body have run (the
|
||||
// usual Java construction order: this class's fields/constructor
|
||||
// first, then the subclass's). A static constant is the only thing
|
||||
// that's safe to return.
|
||||
contentAlphaPaint = new Paint();
|
||||
contentAlphaPaint.setAlpha(contentAlpha());
|
||||
|
||||
redrawContent();
|
||||
}
|
||||
|
||||
// Builds this overlay's content View tree - called once, from this
|
||||
// class's own constructor (see the note on contentAlpha() above: don't
|
||||
// rely on subclass instance fields being initialized yet here; if a
|
||||
// subclass needs its own mutable state available while building its
|
||||
// content, e.g. a list of buttons to later relabel, initialize that
|
||||
// field's value at the top of this method instead of via a field
|
||||
// initializer).
|
||||
protected abstract View buildContent();
|
||||
|
||||
// 0-255; defaults to fully opaque. Override for translucency (see the
|
||||
// constructor's note on what's safe to depend on here).
|
||||
protected int contentAlpha() {
|
||||
return 255;
|
||||
}
|
||||
|
||||
// Polled once per frame while this overlay's quad is visible (see
|
||||
// xr_overlay_render_and_build_layer()) - returns the current pixels
|
||||
// only once per redraw (null otherwise), so the native side knows when
|
||||
// it can skip re-uploading a texture that hasn't actually changed.
|
||||
protected final byte[] takePixelsIfDirty() {
|
||||
synchronized (pixelLock) {
|
||||
byte[] pixels = pendingPixels;
|
||||
pendingPixels = null;
|
||||
return pixels;
|
||||
}
|
||||
}
|
||||
|
||||
// u/v are this quad's own hit-test coordinates (0..1, top-left origin)
|
||||
// - computed by xr_input.c's ray/quad intersection against xr_overlay.c's
|
||||
// reported quad geometry, forwarded here as a synthetic tap. Runs on the
|
||||
// UI thread since the View/Bitmap/Canvas objects here are otherwise only
|
||||
// ever touched from there.
|
||||
protected final void dispatchTouch(final float u, final float v, final boolean down) {
|
||||
activity.runOnUiThread(() -> handleTouch(u, v, down));
|
||||
}
|
||||
|
||||
// u/v are the same hit-test coordinates dispatchTouch() uses, but
|
||||
// polled once per frame regardless of click state rather than only on
|
||||
// click edges - lets the cursor track the aim ray continuously instead
|
||||
// of only jumping when a trigger is pressed. visible=false (no hand's
|
||||
// ray currently on the quad) hides it.
|
||||
protected final void dispatchUpdateCursor(final float u, final float v,
|
||||
final boolean visible) {
|
||||
activity.runOnUiThread(() -> updateCursor(u, v, visible));
|
||||
}
|
||||
|
||||
private void updateCursor(float u, float v, boolean visible) {
|
||||
float x = u * width;
|
||||
float y = v * height;
|
||||
if (visible == cursorVisible && x == cursorX && y == cursorY) {
|
||||
return;
|
||||
}
|
||||
cursorVisible = visible;
|
||||
cursorX = x;
|
||||
cursorY = y;
|
||||
if (updateHoverState(x, y, visible)) {
|
||||
// redrawContent() (called inside updateHoverState()) already
|
||||
// composited and published this frame's pixels - nothing more
|
||||
// to do.
|
||||
return;
|
||||
}
|
||||
compositeAndPublish();
|
||||
}
|
||||
|
||||
// Registers a button (or any View) to be hit-tested against the cursor
|
||||
// position - called once per button, alongside buttonBackground()/
|
||||
// setOnTouchListener(), from buildContent() (or any later content
|
||||
// rebuild).
|
||||
protected final void registerHoverable(View view) {
|
||||
hoverableViews.add(view);
|
||||
}
|
||||
|
||||
// Clears whatever's currently hovered without waiting for the cursor to
|
||||
// actually move off of it - call after hiding/showing content (e.g. a
|
||||
// tab switch), since a view that was hovered right before becoming
|
||||
// GONE would otherwise still be internally tracked as hovered (and
|
||||
// wrongly show that way again) if it's later made VISIBLE again before
|
||||
// the cursor position next changes.
|
||||
protected final void resetHover() {
|
||||
if (hoveredView != null) {
|
||||
hoveredView.setHovered(false);
|
||||
hoveredView = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Absolute bounds of view within this panel's own coordinate space
|
||||
// (i.e. relative to root, which is always laid out at (0,0,width,
|
||||
// height) - see the constructor) - View's own getLeft()/getTop() are
|
||||
// parent-relative, so this walks up the parent chain summing them,
|
||||
// additionally subtracting each ancestor's own getScrollX()/getScrollY()
|
||||
// (0 for a plain ViewGroup, non-zero for e.g. a ScrollView - see
|
||||
// MenuOverlay's Controller Options tab) so a view inside a scrolled
|
||||
// container reports its actual on-screen position, not its laid-out one.
|
||||
// Assumes view is actually part of this panel's tree (every registered
|
||||
// view always is, since registerHoverable() is only ever called from
|
||||
// buildContent()).
|
||||
private Rect absoluteBounds(View view) {
|
||||
int left = 0;
|
||||
int top = 0;
|
||||
View current = view;
|
||||
while (true) {
|
||||
left += current.getLeft();
|
||||
top += current.getTop();
|
||||
if (current == root || !(current.getParent() instanceof View)) {
|
||||
break;
|
||||
}
|
||||
View parent = (View) current.getParent();
|
||||
left -= parent.getScrollX();
|
||||
top -= parent.getScrollY();
|
||||
current = parent;
|
||||
}
|
||||
return new Rect(left, top, left + view.getWidth(), top + view.getHeight());
|
||||
}
|
||||
|
||||
// Returns true if the hovered view changed (and so redrawContent() -
|
||||
// which always publishes too - already ran); false if there's nothing
|
||||
// more for updateCursor() to do beyond its own compositeAndPublish().
|
||||
private boolean updateHoverState(float x, float y, boolean visible) {
|
||||
View newHovered = null;
|
||||
if (visible) {
|
||||
for (View candidate : hoverableViews) {
|
||||
if (candidate.getVisibility() != View.VISIBLE) {
|
||||
continue;
|
||||
}
|
||||
if (absoluteBounds(candidate).contains((int) x, (int) y)) {
|
||||
newHovered = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newHovered == hoveredView) {
|
||||
return false;
|
||||
}
|
||||
if (hoveredView != null) {
|
||||
hoveredView.setHovered(false);
|
||||
}
|
||||
if (newHovered != null) {
|
||||
newHovered.setHovered(true);
|
||||
}
|
||||
hoveredView = newHovered;
|
||||
redrawContent();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleTouch(float u, float v, boolean down) {
|
||||
float x = u * width;
|
||||
float y = v * height;
|
||||
long time = SystemClock.uptimeMillis();
|
||||
MotionEvent event = MotionEvent.obtain(
|
||||
time, time, down ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, x, y, 0);
|
||||
try {
|
||||
root.dispatchTouchEvent(event);
|
||||
} finally {
|
||||
event.recycle();
|
||||
}
|
||||
redrawContent();
|
||||
}
|
||||
|
||||
// Re-runs the full off-screen View-tree draw (expensive - see the
|
||||
// contentBitmap field comment above) - call only when the panel's
|
||||
// actual content changed, not for the cursor-only updates
|
||||
// compositeAndPublish() below handles on its own.
|
||||
protected final void redrawContent() {
|
||||
contentCanvas.drawColor(0xFF202020);
|
||||
root.draw(contentCanvas);
|
||||
compositeAndPublish();
|
||||
}
|
||||
|
||||
// Cheap per-frame path: blits the last-rendered contentBitmap (no View
|
||||
// traversal) plus the cursor circle into bitmap and publishes it.
|
||||
// contentBitmap itself is fully opaque (redrawContent() draws an opaque
|
||||
// background first); contentAlpha() translucency is applied right here
|
||||
// instead, via contentAlphaPaint - which needs bitmap cleared to fully
|
||||
// transparent first, since otherwise each frame's alpha-blended draw
|
||||
// would blend against whatever was left over in bitmap from the
|
||||
// previous frame rather than against nothing, compounding into a
|
||||
// ghosting trail across frames.
|
||||
private void compositeAndPublish() {
|
||||
bitmap.eraseColor(0);
|
||||
canvas.drawBitmap(contentBitmap, 0, 0, contentAlphaPaint);
|
||||
if (cursorVisible) {
|
||||
canvas.drawCircle(cursorX, cursorY, CURSOR_RADIUS, cursorPaint);
|
||||
}
|
||||
|
||||
byte[] pixels = new byte[width * height * 4];
|
||||
// ARGB_8888's actual in-memory byte order is R,G,B,A - matches
|
||||
// GL_RGBA/GL_UNSIGNED_BYTE on the native side with no swizzling.
|
||||
bitmap.copyPixelsToBuffer(ByteBuffer.wrap(pixels));
|
||||
synchronized (pixelLock) {
|
||||
pendingPixels = pixels;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
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;
|
||||
@@ -20,7 +20,7 @@ import org.libsdl.app.SDLActivity;
|
||||
import org.libsdl.app.SDLSurface;
|
||||
|
||||
/**
|
||||
* Prepares /sdcard/questshock/ the way Shockolate's plain relative-path
|
||||
* Prepares /sdcard/QuestShock/ the way Shockolate's plain relative-path
|
||||
* file I/O ("res/data/...", confirmed via grep - none of it goes through
|
||||
* SDL_RWops, so Android's asset-manager fallback for SDL_RWFromFile
|
||||
* doesn't apply here) expects to find it - see android/engine-patches/ for
|
||||
@@ -41,13 +41,14 @@ import org.libsdl.app.SDLSurface;
|
||||
public class QuestShockActivity extends SDLActivity {
|
||||
private static final String TAG = "QuestShock";
|
||||
private static final int PERMISSION_REQUEST_STORAGE = 1;
|
||||
private static final String GAME_DIR = "/sdcard/questshock";
|
||||
// Package-visible: MenuOverlay reads controller_profiles/ from here.
|
||||
static final String GAME_DIR = "/sdcard/QuestShock";
|
||||
|
||||
// Loaded here (rather than waiting for SDLActivity's own
|
||||
// getLibraries()/loadLibraries(), which only runs from super.onCreate())
|
||||
// purely to get nativeChdir() below - Android's dynamic linker resolves
|
||||
// "main"'s own dependencies (SDL2, SDL2_mixer, fluidsynth) from the
|
||||
// APK's native library directory regardless of Java-side load order, so
|
||||
// APK's native library directory regardless of Java-side load order, sox
|
||||
// loading it early here is safe. SDLActivity loading "main" again later
|
||||
// is a harmless no-op (System.loadLibrary is idempotent per
|
||||
// ClassLoader).
|
||||
@@ -59,6 +60,9 @@ public class QuestShockActivity extends SDLActivity {
|
||||
// has no public chdir() (confirmed against the actual API 34 stub jar).
|
||||
private static native void nativeChdir(String path);
|
||||
|
||||
// Straight-alpha ARGB pixels of the startup splash (qs_splash.c).
|
||||
private static native void nativeSetSplashImage(int[] pixels, int width, int height);
|
||||
|
||||
@Override
|
||||
protected String[] getLibraries() {
|
||||
return new String[] {
|
||||
@@ -74,11 +78,9 @@ public class QuestShockActivity extends SDLActivity {
|
||||
// onWindowFocusChanged(), it never checks SDLActivity.mBrokenLibraries
|
||||
// first. Since surfaceChanged() fires on essentially every launch
|
||||
// regardless of that flag, setting mBrokenLibraries alone (see onCreate()/
|
||||
// setUpGameDirAndContinue()) does NOT actually stop the engine from
|
||||
// starting - confirmed on-device: the missing-assets crash still happened
|
||||
// with mBrokenLibraries set, from exactly this path. Route through a
|
||||
// subclass that adds the missing check instead of patching the vendored
|
||||
// file directly.
|
||||
// setUpGameDirAndContinue()) does not actually stop the engine from
|
||||
// starting. Route through a subclass that adds the missing check instead
|
||||
// of patching the vendored file directly.
|
||||
private static class GameSurface extends SDLSurface {
|
||||
GameSurface(Context context) {
|
||||
super(context);
|
||||
@@ -91,20 +93,6 @@ public class QuestShockActivity extends SDLActivity {
|
||||
}
|
||||
super.surfaceChanged(holder, format, width, height);
|
||||
}
|
||||
|
||||
// Diagnostic only (see OpenGL.cc's matching log) - Android's real,
|
||||
// legitimate signal for "this View's laid-out size actually
|
||||
// changed" is onSizeChanged(), fired by the framework itself, not
|
||||
// something we have to poll or guess about. If Quest's Home shell
|
||||
// settles a freshly-launched panel into its final <layout> size via
|
||||
// a genuine later layout pass, this is where that would show up -
|
||||
// confirming whether a real event exists to hook, before writing
|
||||
// any fix around it.
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
Log.i(TAG, "GameSurface.onSizeChanged() " + oldw + "x" + oldh + " -> " + w + "x" + h);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -161,11 +149,25 @@ public class QuestShockActivity extends SDLActivity {
|
||||
// is already there, assume a previous run already extracted everything.
|
||||
File marker = new File(gameDir, "GET_ASSETS_QUEST.txt");
|
||||
if (!marker.exists()) {
|
||||
copyAssetDir("shaders", new File(gameDir, "shaders"));
|
||||
copyAssetFile("res/soundfont.sf2", new File(gameDir, "res/soundfont.sf2"));
|
||||
copyAssetFile("GET_ASSETS_QUEST.txt", marker);
|
||||
copyAssetDir("shaders", new File(gameDir, "shaders"), false);
|
||||
copyAssetFile("res/soundfont.sf2", new File(gameDir, "res/soundfont.sf2"), false);
|
||||
copyAssetFile("GET_ASSETS_QUEST.txt", marker, false);
|
||||
}
|
||||
|
||||
// Refreshed on every launch (unlike the marker-gated block above) so
|
||||
// it always matches the installed build - qs_config.c reads this as
|
||||
// the initial config only when questshock.config.json doesn't exist
|
||||
// yet, never overwriting a user's own saved config.
|
||||
copyAssetFile("questshock.config.default.json",
|
||||
new File(gameDir, "questshock.config.default.json"), true);
|
||||
|
||||
// Refreshed on every launch. MenuOverlay's Controller Options tab
|
||||
// matches the active OpenXR interaction profile against these files'
|
||||
// "interaction_profile" and shows the paired image (see CONFIG.md).
|
||||
// Only the bundled filenames are overwritten, so profiles a user adds
|
||||
// to this folder are kept.
|
||||
copyAssetDir("controller_profiles", new File(gameDir, "controller_profiles"), true);
|
||||
|
||||
// Shockolate's engine/ is never patched to check this itself (see the
|
||||
// class doc above) - it just does plain fopen("res/data/...", ...)
|
||||
// and, on a fresh install with no game data copied in yet, that
|
||||
@@ -190,11 +192,31 @@ public class QuestShockActivity extends SDLActivity {
|
||||
// before clearing mBrokenLibraries below so the engine can never
|
||||
// start pre-chdir.
|
||||
nativeChdir(GAME_DIR);
|
||||
loadSplashImage();
|
||||
|
||||
// Assets confirmed present - safe to let the native engine start now.
|
||||
SDLActivity.mBrokenLibraries = false;
|
||||
}
|
||||
|
||||
// The engine shows this at startup (qs_splash.c); without it there is no splash.
|
||||
private void loadSplashImage() {
|
||||
try (InputStream in = getAssets().open("splash.png")) {
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(in);
|
||||
if (bitmap == null) {
|
||||
Log.e(TAG, "Could not decode splash.png");
|
||||
return;
|
||||
}
|
||||
int width = bitmap.getWidth();
|
||||
int height = bitmap.getHeight();
|
||||
int[] pixels = new int[width * height];
|
||||
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
|
||||
bitmap.recycle();
|
||||
nativeSetSplashImage(pixels, width, height);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Could not read splash.png", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isNonEmptyDir(File dir) {
|
||||
String[] entries = dir.list();
|
||||
return entries != null && entries.length > 0;
|
||||
@@ -222,15 +244,15 @@ public class QuestShockActivity extends SDLActivity {
|
||||
+ "merging or extraction required.\n\n"
|
||||
+ "Then relaunch.\n\n"
|
||||
+ "(These instructions are also in GET_ASSETS_QUEST.txt in the "
|
||||
+ "questshock folder on this device, if you need to read them again.)")
|
||||
+ "QuestShock folder on this device, if you need to read them again.)")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Exit", (dialog, which) -> finish())
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void copyAssetFile(String assetPath, File dest) {
|
||||
if (dest.exists()) {
|
||||
private void copyAssetFile(String assetPath, File dest, boolean overwrite) {
|
||||
if (dest.exists() && !overwrite) {
|
||||
return;
|
||||
}
|
||||
File parent = dest.getParentFile();
|
||||
@@ -250,7 +272,7 @@ public class QuestShockActivity extends SDLActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void copyAssetDir(String assetDir, File destDir) {
|
||||
private void copyAssetDir(String assetDir, File destDir, boolean overwrite) {
|
||||
AssetManager assets = getAssets();
|
||||
String[] entries;
|
||||
try {
|
||||
@@ -264,7 +286,7 @@ public class QuestShockActivity extends SDLActivity {
|
||||
}
|
||||
destDir.mkdirs();
|
||||
for (String entry : entries) {
|
||||
copyAssetFile(assetDir + "/" + entry, new File(destDir, entry));
|
||||
copyAssetFile(assetDir + "/" + entry, new File(destDir, entry), overwrite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package de.ladkau.questshock;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Best-effort (not professionally reviewed) translations for this app's own
|
||||
* UI chrome text - the menu/keyboard panels this project draws itself, not
|
||||
* the game's own resource-string text (which already has its own separate
|
||||
* English/French/German data files). Selected by gShockPrefs.goLanguage (0
|
||||
* English/1 French/2 German, Prefs.h) via MenuOverlay's native language
|
||||
* query (qs_config_current_language()) - see MenuOverlay's periodic header-
|
||||
* refresh timer, which also pushes it to KeyboardOverlay.
|
||||
*
|
||||
* Deliberately excludes the keyboard's own alphanumeric key labels (a-z,
|
||||
* Tab, Space, Esc, ...) - those represent physical key positions/characters
|
||||
* rather than translatable UI copy - and the restart-confirmation dialog's
|
||||
* message text, which is built in native code (qs_config.c's
|
||||
* qs_show_restart_dialog()) with an interpolated option-name list and is
|
||||
* not currently reachable in practice (see CONFIG.md - neither Video
|
||||
* Options option is flagged restart-required today).
|
||||
*/
|
||||
final class Strings {
|
||||
static final int ENGLISH = 0;
|
||||
static final int FRENCH = 1;
|
||||
static final int GERMAN = 2;
|
||||
|
||||
private static final Map<String, String[]> TABLE = new HashMap<>();
|
||||
|
||||
private static void put(String key, String en, String fr, String de) {
|
||||
TABLE.put(key, new String[]{en, fr, de});
|
||||
}
|
||||
|
||||
static {
|
||||
put("keyboard", "Keyboard", "Clavier", "Tastatur");
|
||||
put("save_configuration", "Save Configuration", "Enregistrer la configuration",
|
||||
"Konfiguration speichern");
|
||||
put("load_configuration", "Load Configuration", "Charger la configuration",
|
||||
"Konfiguration laden");
|
||||
put("reset_position", "Reset Position", "Réinitialiser la position",
|
||||
"Position zurücksetzen");
|
||||
put("main_menu", "Main Menu", "Menu principal", "Hauptmenü");
|
||||
put("video_options", "Video Options", "Options vidéo", "Videooptionen");
|
||||
put("dismiss", "Dismiss", "Fermer", "Schließen");
|
||||
put("restart_now", "Restart Now", "Redémarrer maintenant", "Jetzt neu starten");
|
||||
put("renderer", "Renderer", "Moteur de rendu", "Renderer");
|
||||
put("resolution", "Resolution", "Résolution", "Auflösung");
|
||||
put("software", "Software", "Logiciel", "Software");
|
||||
put("language", "Language", "Langue", "Sprache");
|
||||
put("english", "English", "Anglais", "Englisch");
|
||||
put("french", "French", "Français", "Französisch");
|
||||
put("german", "German", "Allemand", "Deutsch");
|
||||
put("initial_screen", "Initial Screen", "Écran initial", "Startbildschirm");
|
||||
put("game_screen", "Game Screen", "Écran de jeu", "Spielbildschirm");
|
||||
put("width", "Width", "Largeur", "Breite");
|
||||
put("distance", "Distance", "Distance", "Abstand");
|
||||
// "OpenGL" is a proper/brand name - not translated, same in every
|
||||
// language, so it has no entry here; call sites just use it as a
|
||||
// literal.
|
||||
put("keyboard_drag_title", "Keyboard - drag here to move",
|
||||
"Clavier - faites glisser ici pour déplacer",
|
||||
"Tastatur - hier ziehen zum Verschieben");
|
||||
put("close", "Close", "Fermer", "Schließen");
|
||||
|
||||
// Controller Options tab: action row labels, keyed by xr_input.c's
|
||||
// action names (see MenuOverlay.ACTION_KEYS).
|
||||
put("controller_options", "Controller Options", "Options de manette", "Controller-Optionen");
|
||||
put("action_move_forward", "Move Forward", "Avancer", "Vorwärts gehen");
|
||||
put("action_move_back", "Move Back", "Reculer", "Rückwärts gehen");
|
||||
put("action_strafe_left", "Strafe Left", "Pas latéral gauche", "Seitwärts links");
|
||||
put("action_strafe_right", "Strafe Right", "Pas latéral droit", "Seitwärts rechts");
|
||||
put("action_turn_left", "Turn Left", "Tourner à gauche", "Nach links drehen");
|
||||
put("action_turn_right", "Turn Right", "Tourner à droite", "Nach rechts drehen");
|
||||
put("action_jump", "Jump", "Sauter", "Springen");
|
||||
put("action_mouse_click", "Click", "Cliquer", "Klicken");
|
||||
put("action_mouse_double_click", "Double-Click", "Double-clic", "Doppelklick");
|
||||
|
||||
// Hotkey actions (see xr_input.c); all unbound by default.
|
||||
put("action_stand", "Stand", "Debout", "Stehen");
|
||||
put("action_crouch", "Crouch", "Accroupi", "Ducken");
|
||||
put("action_prone", "Prone", "Couché", "Liegen");
|
||||
put("action_pause", "Pause Game", "Mettre en pause", "Spiel pausieren");
|
||||
put("action_full_view", "Fullscreen View", "Vue plein écran", "Vollbildansicht");
|
||||
put("action_normal_view", "Normal View", "Vue normale", "Normale Ansicht");
|
||||
put("action_map_view", "Automap View", "Vue de la carte", "Automap-Ansicht");
|
||||
put("action_save_game", "Save Game", "Sauvegarder", "Spiel speichern");
|
||||
put("action_load_game", "Load Game", "Charger", "Spiel laden");
|
||||
put("action_select_grenade", "Select Grenade", "Sélectionner une grenade",
|
||||
"Granate auswählen");
|
||||
put("action_arm_grenade", "Arm Grenade", "Armer la grenade", "Granate scharfmachen");
|
||||
put("action_select_drug", "Select Drug", "Sélectionner une drogue", "Droge auswählen");
|
||||
put("action_use_drug", "Use Drug", "Utiliser la drogue", "Droge benutzen");
|
||||
put("action_toggle_online_help", "Toggle Online Help", "Basculer l'aide en ligne",
|
||||
"Online-Hilfe umschalten");
|
||||
put("action_show_help", "Show Help", "Afficher l'aide", "Hilfe anzeigen");
|
||||
put("action_toggle_music", "Toggle Music", "Basculer la musique", "Musik umschalten");
|
||||
put("action_quit", "Quit Game", "Quitter le jeu", "Spiel beenden");
|
||||
put("action_cycle_weapon_next", "Next Weapon", "Arme suivante", "Nächste Waffe");
|
||||
put("action_cycle_weapon_prev", "Previous Weapon", "Arme précédente", "Vorherige Waffe");
|
||||
put("action_cycle_detail", "Cycle Detail Level", "Changer le niveau de détail",
|
||||
"Detailgrad wechseln");
|
||||
put("action_reload_weapon_primary", "Reload Weapon (Primary)",
|
||||
"Recharger l'arme (principal)", "Waffe nachladen (primär)");
|
||||
put("action_reload_weapon_secondary", "Reload Weapon (Secondary)",
|
||||
"Recharger l'arme (secondaire)", "Waffe nachladen (sekundär)");
|
||||
put("action_hud_color", "Cycle HUD Color", "Changer la couleur de l'interface",
|
||||
"HUD-Farbe wechseln");
|
||||
put("action_clear_fullscreen", "Clear Fullscreen View", "Quitter la vue plein écran",
|
||||
"Vollbildansicht schließen");
|
||||
put("action_mfd_left_1", "Left MFD: Weapon", "MFD gauche : arme", "Linkes MFD: Waffe");
|
||||
put("action_mfd_left_2", "Left MFD: Item", "MFD gauche : objet", "Linkes MFD: Gegenstand");
|
||||
put("action_mfd_left_3", "Left MFD: Map", "MFD gauche : carte", "Linkes MFD: Karte");
|
||||
put("action_mfd_left_4", "Left MFD: Target", "MFD gauche : cible", "Linkes MFD: Ziel");
|
||||
put("action_mfd_left_5", "Left MFD: Info", "MFD gauche : info", "Linkes MFD: Info");
|
||||
put("action_mfd_right_1", "Right MFD: Weapon", "MFD droit : arme", "Rechtes MFD: Waffe");
|
||||
put("action_mfd_right_2", "Right MFD: Item", "MFD droit : objet",
|
||||
"Rechtes MFD: Gegenstand");
|
||||
put("action_mfd_right_3", "Right MFD: Map", "MFD droit : carte", "Rechtes MFD: Karte");
|
||||
put("action_mfd_right_4", "Right MFD: Target", "MFD droit : cible", "Rechtes MFD: Ziel");
|
||||
put("action_mfd_right_5", "Right MFD: Info", "MFD droit : info", "Rechtes MFD: Info");
|
||||
put("action_fire_weapon", "Fire Weapon", "Tirer", "Waffe abfeuern");
|
||||
// Cheats tab.
|
||||
put("cheats", "Cheats", "Triches", "Cheats");
|
||||
put("cheats_note", "Cheats need a running game.", "Les triches nécessitent une partie en cours.",
|
||||
"Cheats benötigen ein laufendes Spiel.");
|
||||
put("cheat_give_all", "Give All Items & Weapons", "Tout obtenir", "Alles erhalten");
|
||||
put("cheat_physics", "Physics", "Physique", "Physik");
|
||||
put("cheat_level_up", "Next Level", "Niveau suivant", "Nächstes Level");
|
||||
put("cheat_level_down", "Previous Level", "Niveau précédent", "Vorheriges Level");
|
||||
put("cheat_unlimited_health", "Unlimited Health", "Santé illimitée", "Unbegrenzt Leben");
|
||||
put("cheat_unlimited_energy", "Unlimited Energy", "Énergie illimitée", "Unbegrenzt Energie");
|
||||
put("cheat_unlimited_ammo", "Unlimited Ammo", "Munitions illimitées", "Unbegrenzt Munition");
|
||||
put("cheat_unlimited_items", "Unlimited Items", "Objets illimités", "Unbegrenzt Items");
|
||||
|
||||
put("action_free_left_thumbstick_view", "Free View (Left Stick)",
|
||||
"Vue libre (stick gauche)", "Freie Ansicht (linker Stick)");
|
||||
put("action_free_right_thumbstick_view", "Free View (Right Stick)",
|
||||
"Vue libre (stick droit)", "Freie Ansicht (rechter Stick)");
|
||||
|
||||
// Cyberspace actions (active only in cyberspace, see xr_input.c).
|
||||
put("action_cyber_thrust", "Cyberspace: Thrust", "Cyberespace : poussée",
|
||||
"Cyberspace: Schub");
|
||||
put("action_cyber_climb", "Cyberspace: Climb", "Cyberespace : monter",
|
||||
"Cyberspace: Steigen");
|
||||
put("action_cyber_dive", "Cyberspace: Dive", "Cyberespace : plonger",
|
||||
"Cyberspace: Sinken");
|
||||
put("action_cyber_bank_left", "Cyberspace: Bank Left", "Cyberespace : virage à gauche",
|
||||
"Cyberspace: Links kippen");
|
||||
put("action_cyber_bank_right", "Cyberspace: Bank Right", "Cyberespace : virage à droite",
|
||||
"Cyberspace: Rechts kippen");
|
||||
put("action_cyber_roll_left", "Cyberspace: Roll Left", "Cyberespace : roulis gauche",
|
||||
"Cyberspace: Links rollen");
|
||||
put("action_cyber_roll_right", "Cyberspace: Roll Right", "Cyberespace : roulis droit",
|
||||
"Cyberspace: Rechts rollen");
|
||||
put("action_cyber_climb_left", "Cyberspace: Climb Left", "Cyberespace : monter à gauche",
|
||||
"Cyberspace: Links steigen");
|
||||
put("action_cyber_climb_right", "Cyberspace: Climb Right", "Cyberespace : monter à droite",
|
||||
"Cyberspace: Rechts steigen");
|
||||
put("action_cyber_dive_left", "Cyberspace: Dive Left", "Cyberespace : plonger à gauche",
|
||||
"Cyberspace: Links sinken");
|
||||
put("action_cyber_dive_right", "Cyberspace: Dive Right", "Cyberespace : plonger à droite",
|
||||
"Cyberspace: Rechts sinken");
|
||||
put("capture_prompt", "Press a button…", "Appuyez sur un bouton…", "Taste drücken…");
|
||||
put("reset_to_defaults", "Reset to Defaults", "Réinitialiser", "Auf Standard zurücksetzen");
|
||||
put("generic_profile_note",
|
||||
"This controller only exposes a trigger and a menu button - most actions above have "
|
||||
+ "nothing to bind to.",
|
||||
"Cette manette n'expose qu'une gâchette et un bouton menu - la plupart des actions "
|
||||
+ "ci-dessus n'ont rien à quoi être assignées.",
|
||||
"Dieser Controller bietet nur einen Trigger und eine Menütaste - die meisten "
|
||||
+ "Aktionen oben haben nichts, woran sie gebunden werden können.");
|
||||
|
||||
// Raw physical-input display names (xr_input.c's kRawInputNames);
|
||||
// "none" is the unbound sentinel.
|
||||
put("raw_left_trigger", "Left Trigger", "Gâchette gauche", "Linker Trigger");
|
||||
put("raw_right_trigger", "Right Trigger", "Gâchette droite", "Rechter Trigger");
|
||||
put("raw_left_squeeze", "Left Grip", "Préhension gauche", "Linker Griff");
|
||||
put("raw_right_squeeze", "Right Grip", "Préhension droite", "Rechter Griff");
|
||||
put("raw_left_x", "X Button", "Bouton X", "X-Taste");
|
||||
put("raw_left_y", "Y Button", "Bouton Y", "Y-Taste");
|
||||
put("raw_right_a", "A Button", "Bouton A", "A-Taste");
|
||||
put("raw_right_b", "B Button", "Bouton B", "B-Taste");
|
||||
put("raw_left_thumbstick_click", "Left Stick Click", "Clic du stick gauche",
|
||||
"Linker Stick-Klick");
|
||||
put("raw_right_thumbstick_click", "Right Stick Click", "Clic du stick droit",
|
||||
"Rechter Stick-Klick");
|
||||
put("raw_left_thumbstick_up", "Left Stick Up", "Stick gauche haut", "Linker Stick hoch");
|
||||
put("raw_left_thumbstick_down", "Left Stick Down", "Stick gauche bas", "Linker Stick runter");
|
||||
put("raw_left_thumbstick_left", "Left Stick Left", "Stick gauche à gauche",
|
||||
"Linker Stick links");
|
||||
put("raw_left_thumbstick_right", "Left Stick Right", "Stick gauche à droite",
|
||||
"Linker Stick rechts");
|
||||
put("raw_right_thumbstick_up", "Right Stick Up", "Stick droit haut", "Rechter Stick hoch");
|
||||
put("raw_right_thumbstick_down", "Right Stick Down", "Stick droit bas",
|
||||
"Rechter Stick runter");
|
||||
put("raw_right_thumbstick_left", "Right Stick Left", "Stick droit à gauche",
|
||||
"Rechter Stick links");
|
||||
put("raw_right_thumbstick_right", "Right Stick Right", "Stick droit à droite",
|
||||
"Rechter Stick rechts");
|
||||
put("raw_trigger", "Either Trigger", "L'une ou l'autre gâchette", "Beliebiger Trigger");
|
||||
put("raw_squeeze", "Either Grip", "L'une ou l'autre préhension", "Beliebiger Griff");
|
||||
put("raw_none", "(unbound)", "(non assigné)", "(nicht zugewiesen)");
|
||||
}
|
||||
|
||||
private Strings() {}
|
||||
|
||||
static String get(String key, int language) {
|
||||
String[] variants = TABLE.get(key);
|
||||
if (variants == null) {
|
||||
return key;
|
||||
}
|
||||
if (language < 0 || language >= variants.length) {
|
||||
language = ENGLISH;
|
||||
}
|
||||
return variants[language];
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">System Shock</string>
|
||||
<string name="app_name">QuestShock</string>
|
||||
</resources>
|
||||
|
||||
@@ -9,7 +9,18 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.1.1'
|
||||
// 8.5.1+ needed for AGP to automatically 16 KB-align native
|
||||
// libraries' zip entries within the APK - the actual .so's are
|
||||
// already ELF-segment-aligned for this regardless of AGP version
|
||||
// (see build-image/Dockerfile's ANDROID_16KB_LDFLAGS and
|
||||
// app/build.gradle's own CMAKE_SHARED_LINKER_FLAGS), but that alone
|
||||
// doesn't get them 16 KB-aligned *within the APK's zip*, which is
|
||||
// what Android Studio's "not compatible with 16 KB devices"
|
||||
// warning is actually about. Needs Gradle 8.7+ (see
|
||||
// gradle/wrapper/gradle-wrapper.properties) and JDK 17 in whatever
|
||||
// runs gradlew (already the case - see build-image/Dockerfile's
|
||||
// openjdk-17-jdk-headless).
|
||||
classpath 'com.android.tools.build:gradle:8.5.2'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -46,9 +46,17 @@
|
||||
diff -ru a/CMakeLists.txt b/CMakeLists.txt
|
||||
--- a/CMakeLists.txt 2026-07-31 14:04:53.195646458 +0200
|
||||
+++ b/CMakeLists.txt 2026-07-31 14:05:05.703429751 +0200
|
||||
@@ -46,9 +46,27 @@
|
||||
|
||||
add_compile_options(-fsigned-char -fno-strict-aliasing)
|
||||
|
||||
-# Find OpenGL
|
||||
+# Find OpenGL. Android has no desktop GL/GLX for CMake's FindOpenGL module
|
||||
+# to find - link the prebuilt GL4ES instead (translates this file's
|
||||
+# to find - build GL4ES from source 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).
|
||||
+# the engine doesn't need to link them directly), via add_subdirectory
|
||||
+# rather than a prebuilt .so: ANDROID_GL4ES_DIR (passed in by
|
||||
+# android/app/build.gradle, same as ANDROID_PREBUILT_DIR) is a scratch,
|
||||
+# patched copy of android/gl4es-src/ staged by
|
||||
+# build-image/prepare-android-project.sh (mirrors how this file itself is
|
||||
+# staged/patched before Gradle ever sees it) - building it as part of this
|
||||
+# same CMake configure means Gradle's own incremental CMake/Ninja build
|
||||
+# only recompiles it when gl4es-src/gl4es-patches actually changed, same
|
||||
+# as this file, instead of needing a full build-image rebuild for every
|
||||
+# gl4es-patches change.
|
||||
if(ENABLE_OPENGL)
|
||||
- find_package(OpenGL REQUIRED)
|
||||
+ if(ANDROID)
|
||||
+ set(OPENGL_INCLUDE_DIRS ${ANDROID_PREBUILT_DIR}/gl4es/include)
|
||||
+ set(OPENGL_LIBRARIES ${ANDROID_PREBUILT_DIR}/gl4es/lib/libGL.so)
|
||||
+ add_subdirectory(${ANDROID_GL4ES_DIR} ${CMAKE_BINARY_DIR}/gl4es-build)
|
||||
+ set(OPENGL_INCLUDE_DIRS ${ANDROID_GL4ES_DIR}/include)
|
||||
+ set(OPENGL_LIBRARIES GL)
|
||||
+ else()
|
||||
+ find_package(OpenGL REQUIRED)
|
||||
+ endif()
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
+ // Android - EGL only ever gives out GLES contexts. Quest hardware
|
||||
+ // (Adreno on the XR2/XR2 Gen 2) supports GLES 3.2, which - unlike
|
||||
+ // GLES 2.0 - has GL_UNPACK_ROW_LENGTH and GL_CLAMP_TO_BORDER as core
|
||||
+ // (see android/engine-patches/03-android-opengl-es-render.patch),
|
||||
+ // (see android/engine-patches/04-android-opengl-es-render.patch),
|
||||
+ // avoiding needing workarounds for either in OpenGL.cc.
|
||||
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
--- a/src/MacSrc/Shock.c
|
||||
+++ b/src/MacSrc/Shock.c
|
||||
@@ -162,6 +162,17 @@
|
||||
@@ -162,9 +162,26 @@
|
||||
void InitSDL() {
|
||||
SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1");
|
||||
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl");
|
||||
@@ -12,9 +12,18 @@
|
||||
+ // src/audio/aaudio/SDL_aaudio.c). SDLSound.c opens two: one directly via
|
||||
+ // SDL_OpenAudioDevice() for cutscene audio, one via Mix_OpenAudio() for
|
||||
+ // SFX/MIDI. The older OpenSL ES backend has no such limitation, so force
|
||||
+ // it instead of AAudio.
|
||||
+ SDL_SetHint(SDL_HINT_AUDIODRIVER, "openslES");
|
||||
+ // it instead of AAudio. Uses SDL_HINT_OVERRIDE priority so nothing set
|
||||
+ // earlier (or by SDL/the platform) can take precedence over it.
|
||||
+ SDL_SetHintWithPriority(SDL_HINT_AUDIODRIVER, "openslES", SDL_HINT_OVERRIDE);
|
||||
+#endif
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO) < 0) {
|
||||
DEBUG("%s: Init failed", __FUNCTION__);
|
||||
}
|
||||
+#ifdef __ANDROID__
|
||||
+ // Standing diagnostic: reports which audio driver SDL actually selected,
|
||||
+ // to confirm the openslES override above took effect on-device.
|
||||
+ INFO("SDL audio driver: %s", SDL_GetCurrentAudioDriver());
|
||||
+#endif
|
||||
|
||||
// TODO: figure out some universal set of settings that work...
|
||||
#ifdef __ANDROID__
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
--- a/src/MacSrc/OpenGL.cc
|
||||
+++ b/src/MacSrc/OpenGL.cc
|
||||
@@ -332,6 +332,21 @@
|
||||
|
||||
int width, height;
|
||||
SDL_GetWindowSize(window, &width, &height);
|
||||
+#ifdef __ANDROID__
|
||||
+ // Temporary diagnostic: the reported panel/surface size is correct
|
||||
+ // (confirmed via SDLSurface's own "Window size" log and a Java-side
|
||||
+ // onSizeChanged() probe showing no later relayout), but the rendered
|
||||
+ // content still only fills a small corner of it. That means the
|
||||
+ // divergence is somewhere below the Java/Activity layer - between what
|
||||
+ // SDL_GetWindowSize() (the value used for opengl_resize() below) reports
|
||||
+ // and what SDL/EGL/gl4es actually believe the live drawable size is.
|
||||
+ // Compare all three directly instead of guessing further.
|
||||
+ int drawable_w, drawable_h, output_w, output_h;
|
||||
+ SDL_GL_GetDrawableSize(window, &drawable_w, &drawable_h);
|
||||
+ SDL_GetRendererOutputSize(renderer, &output_w, &output_h);
|
||||
+ INFO("Android size diag: SDL_GetWindowSize=%dx%d SDL_GL_GetDrawableSize=%dx%d SDL_GetRendererOutputSize=%dx%d",
|
||||
+ width, height, drawable_w, drawable_h, output_w, output_h);
|
||||
+#endif
|
||||
opengl_resize(width, height);
|
||||
|
||||
// Now make the palettes
|
||||
@@ -0,0 +1,68 @@
|
||||
--- a/src/MacSrc/ShockBitmap.c
|
||||
+++ b/src/MacSrc/ShockBitmap.c
|
||||
@@ -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);
|
||||
|
||||
+#ifndef __ANDROID__
|
||||
+ // On Android there's exactly one OS-controlled-size surface - no
|
||||
+ // desktop-style window to resize, move, or toggle fullscreen on.
|
||||
+ // SDL_SetWindowSize() still "succeeds" there (Android has no
|
||||
+ // SetWindowSize driver hook, so SDL's generic layer just overwrites its
|
||||
+ // own cached window->w/h to the requested game resolution, e.g.
|
||||
+ // 640x480, and synthesizes a resize event from that) - which desyncs
|
||||
+ // SDL's own notion of the window size from the real, unchanged Android
|
||||
+ // surface size (e.g. 1600x1200), and that desync is exactly what then
|
||||
+ // makes both SDL's internal renderer viewport and this engine's own
|
||||
+ // custom GL viewport (OpenGL.cc's opengl_resize(), driven by that same
|
||||
+ // now-wrong cached size) shrink to the game's resolution instead of
|
||||
+ // filling the real surface. Skip the desktop-only calls on Android
|
||||
+ // entirely, leaving SDL's window-size bookkeeping untouched.
|
||||
extern bool fullscreenActive;
|
||||
SDL_SetWindowFullscreen(window, fullscreenActive ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
|
||||
|
||||
SDL_SetWindowSize(window, width, height);
|
||||
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||
+#endif
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------------
|
||||
@@ -1,31 +0,0 @@
|
||||
--- a/src/MacSrc/ShockBitmap.c
|
||||
+++ b/src/MacSrc/ShockBitmap.c
|
||||
@@ -42,11 +42,28 @@
|
||||
|
||||
SDL_RenderClear(renderer);
|
||||
|
||||
+#ifndef __ANDROID__
|
||||
+ // On Android there's exactly one OS-controlled-size surface - no
|
||||
+ // desktop-style window to resize, move, or toggle fullscreen on.
|
||||
+ // SDL_SetWindowSize() still "succeeds" there (Android has no
|
||||
+ // SetWindowSize driver hook, so SDL's generic layer just overwrites its
|
||||
+ // own cached window->w/h to the requested game resolution, e.g.
|
||||
+ // 640x480, and synthesizes a resize event from that) - which desyncs
|
||||
+ // SDL's own notion of the window size from the real, unchanged Android
|
||||
+ // surface size (e.g. 1600x1200), and that desync is exactly what then
|
||||
+ // makes both SDL's internal renderer viewport and this engine's own
|
||||
+ // custom GL viewport (OpenGL.cc's opengl_resize(), driven by that same
|
||||
+ // now-wrong cached size) shrink to the game's resolution instead of
|
||||
+ // filling the real surface - confirmed on-device via added logcat
|
||||
+ // diagnostics (see android/engine-patches/07-09) showing the correct
|
||||
+ // 1600x1200 size at startup, then this exact call sequence collapsing
|
||||
+ // it to 640x480 the moment the splash screen sets its video mode.
|
||||
extern bool fullscreenActive;
|
||||
SDL_SetWindowFullscreen(window, fullscreenActive ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
|
||||
|
||||
SDL_SetWindowSize(window, width, height);
|
||||
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||
+#endif
|
||||
|
||||
SDL_RenderSetLogicalSize(renderer, width, height);
|
||||
|
||||
@@ -199,11 +199,18 @@
|
||||
#include "InitMac.h"
|
||||
#include "Modding.h"
|
||||
#include "OpenGL.h"
|
||||
@@ -301,6 +305,21 @@
|
||||
@@ -301,6 +305,28 @@
|
||||
}
|
||||
|
||||
void SDLDraw() {
|
||||
+#ifdef __ANDROID__
|
||||
+ // Diagnostic: logs once per transition (including the very first
|
||||
+ // frame) between the OpenGL 3D render path and the plain
|
||||
+ // software-composited 2D path (splash/menus/cutscenes) below - so a
|
||||
+ // logcat capture immediately shows which one is active without
|
||||
+ // needing to infer it from screenshots.
|
||||
+ static int s_last_render_mode = -1; // -1 = not yet logged
|
||||
+
|
||||
+ // The true once-a-frame present hook for the immersive OpenXR path -
|
||||
+ // unlike opengl_swap_and_restore() below, this runs every frame
|
||||
+ // regardless of should_opengl_swap() (e.g. also during the splash
|
||||
@@ -221,17 +228,25 @@
|
||||
if (should_opengl_swap()) {
|
||||
// We want the UI background to be transparent!
|
||||
sdlPalette->colors[255].a = 0x00;
|
||||
@@ -310,14 +329,27 @@
|
||||
@@ -310,14 +336,35 @@
|
||||
|
||||
// Set the palette back, and we are done
|
||||
sdlPalette->colors[255].a = 0xff;
|
||||
+#ifdef __ANDROID__
|
||||
+ if (s_last_render_mode != 1) {
|
||||
+ s_last_render_mode = 1;
|
||||
+ INFO("Game rendering active: OpenGL (3D) mode");
|
||||
+ }
|
||||
+ xr_frame_end();
|
||||
+#endif
|
||||
return;
|
||||
}
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+ if (s_last_render_mode != 0) {
|
||||
+ s_last_render_mode = 0;
|
||||
+ INFO("Game rendering active: software (2D) mode");
|
||||
+ }
|
||||
+ // Same software-rendered frame the desktop path below would present
|
||||
+ // via SDL_RenderCopy/SDL_RenderPresent, composited into the XR
|
||||
+ // swapchain image xr_frame_begin() bound above instead - SDL's
|
||||
@@ -0,0 +1,30 @@
|
||||
--- a/src/MacSrc/OpenGL.cc 2026-08-22 09:00:00.000000000 +0200
|
||||
+++ b/src/MacSrc/OpenGL.cc 2026-08-22 09:05:00.000000000 +0200
|
||||
@@ -562,7 +562,14 @@
|
||||
static bool opengl_cache_texture(CachedTexture toCache, grs_bitmap *bm) {
|
||||
SDL_GL_MakeCurrent(window, context);
|
||||
|
||||
- glPixelStorei(GL_UNPACK_ROW_LENGTH, bm->row);
|
||||
+ // GL_UNPACK_ROW_LENGTH must describe the buffer actually passed to
|
||||
+ // glTexImage2D() below (toCache.converted->pixels, a separately
|
||||
+ // allocated SDL_PIXELFORMAT_RGBA32 surface from
|
||||
+ // SDL_ConvertSurfaceFormat()) - not bm->row, the original 8-bit
|
||||
+ // source bitmap's own unrelated row stride. Using bm->row here fed
|
||||
+ // gl4es's GL_UNPACK_ROW_LENGTH emulation the wrong stride whenever
|
||||
+ // the two differed, corrupting the texture upload.
|
||||
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, toCache.converted->pitch / 4);
|
||||
|
||||
if (texturesByBitsPtr.size() < MAX_CACHED_TEXTURES) {
|
||||
// We have enough room, generate the new texture
|
||||
@@ -696,7 +703,10 @@
|
||||
t->lastDrawTime = *tmd_ticks;
|
||||
|
||||
if (isDirty) {
|
||||
- glPixelStorei(GL_UNPACK_ROW_LENGTH, bm->row);
|
||||
+ // Same fix as opengl_cache_texture() above - t->converted->pixels
|
||||
+ // is what's actually uploaded, so its own pitch is what
|
||||
+ // GL_UNPACK_ROW_LENGTH needs to describe, not bm->row.
|
||||
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, t->converted->pitch / 4);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bm->w, bm->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, t->converted->pixels);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
--- a/src/MacSrc/OpenGL.cc
|
||||
+++ b/src/MacSrc/OpenGL.cc
|
||||
@@ -566,7 +566,11 @@
|
||||
glDisableVertexAttribArray(0);
|
||||
glDisableVertexAttribArray(tcAttrib);
|
||||
glDisableVertexAttribArray(lightAttrib);
|
||||
- glFlush();
|
||||
+ // 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
|
||||
+ // to have actually landed, not just be queued.
|
||||
+ glFinish();
|
||||
|
||||
SDL_FreeSurface(uiRgba);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
--- a/src/MacSrc/OpenGL.cc
|
||||
+++ b/src/MacSrc/OpenGL.cc
|
||||
@@ -525,6 +525,13 @@
|
||||
bind_texture(s_uiTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
+ // uiRgba is tightly packed, but gl4es implements GL_UNPACK_ROW_LENGTH/
|
||||
+ // GL_UNPACK_ALIGNMENT client-side and persists whatever was last set
|
||||
+ // across draw calls in the same context. Reset both to the packed
|
||||
+ // defaults here so this upload doesn't inherit unrelated unpack state
|
||||
+ // set elsewhere in the frame.
|
||||
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, uiRgba->w, uiRgba->h, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
uiRgba->pixels);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
--- a/src/MacSrc/SDLSound.c
|
||||
+++ b/src/MacSrc/SDLSound.c
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
|
||||
if (Mix_OpenAudio(48000, AUDIO_S16SYS, 2, 2048) < 0) {
|
||||
- ERROR("%s: Couldn't open audio device", __FUNCTION__);
|
||||
+ ERROR("%s: Couldn't open audio device: %s", __FUNCTION__, SDL_GetError());
|
||||
}
|
||||
|
||||
Mix_AllocateChannels(SND_MAX_SAMPLES);
|
||||
@@ -0,0 +1,181 @@
|
||||
--- a/src/MacSrc/SDLSound.c
|
||||
+++ b/src/MacSrc/SDLSound.c
|
||||
@@ -19,24 +19,6 @@
|
||||
|
||||
// Startup the sound system
|
||||
|
||||
- SDL_AudioSpec spec, obtained;
|
||||
- spec.freq = 48000;
|
||||
- spec.format = AUDIO_S16SYS;
|
||||
- spec.channels = 2;
|
||||
- spec.samples = 2048;
|
||||
- spec.callback = AudioStreamCallback;
|
||||
- spec.userdata = (void *)&cutscene_audiostream;
|
||||
-
|
||||
- extern SDL_AudioDeviceID device;
|
||||
- device = SDL_OpenAudioDevice(NULL, 0, &spec, &obtained, 0);
|
||||
-
|
||||
- if (device == 0) {
|
||||
- ERROR("Could not open SDL audio: %s", SDL_GetError());
|
||||
- } else {
|
||||
- INFO("Opened Music Stream, deviceID %d, freq %d, size %d, format %d, channels %d, samples %d", device,
|
||||
- obtained.freq, obtained.size, obtained.format, obtained.channels, obtained.samples);
|
||||
- }
|
||||
-
|
||||
if (Mix_Init(MIX_INIT_MP3) < 0) {
|
||||
ERROR("%s: Init failed", __FUNCTION__);
|
||||
}
|
||||
@@ -50,10 +32,18 @@
|
||||
Mix_HookMusic(MusicCallback, (void *)&MusicDev);
|
||||
Mix_VolumeMusic(MIX_MAX_VOLUME); // use max volume for music stream
|
||||
|
||||
+ // Cutscene/audiolog playback (AudioStreamCallback, see cutsloop.c) used to
|
||||
+ // run through its own SDL_OpenAudioDevice() device, separate from this
|
||||
+ // one - SDL only ever allows one open non-capture device on Android's
|
||||
+ // audio backends, so that second open always failed with "Audio device
|
||||
+ // already open" and both SFX and MIDI music (both routed through this
|
||||
+ // Mix_OpenAudio device) silently never played. Mixed into this device's
|
||||
+ // post-mix stage instead, so there is only ever one device open.
|
||||
+ Mix_SetPostMix(AudioStreamCallback, (void *)&cutscene_audiostream);
|
||||
+
|
||||
InitReadXMI();
|
||||
|
||||
atexit(Mix_CloseAudio);
|
||||
- atexit(SDL_CloseAudio);
|
||||
|
||||
return OK;
|
||||
}
|
||||
--- a/src/MacSrc/Shock.c
|
||||
+++ b/src/MacSrc/Shock.c
|
||||
@@ -60,8 +60,6 @@
|
||||
SDL_Palette *sdlPalette;
|
||||
SDL_Renderer *renderer;
|
||||
|
||||
-SDL_AudioDeviceID device;
|
||||
-
|
||||
int num_args;
|
||||
char **arg_values;
|
||||
|
||||
--- a/src/GameSrc/cutsloop.c
|
||||
+++ b/src/GameSrc/cutsloop.c
|
||||
@@ -83,15 +83,25 @@
|
||||
extern char FrnSubtitle[256];
|
||||
extern char GerSubtitle[256];
|
||||
|
||||
-extern SDL_AudioDeviceID device;
|
||||
-
|
||||
-
|
||||
+// Runs as SDL_mixer's post-mix hook (see snd_start_digital() in
|
||||
+// SDLSound.c) rather than a dedicated SDL audio device's callback, so
|
||||
+// SFX/MIDI music sharing the same device are already mixed into `stream`
|
||||
+// by the time this runs - mix in rather than overwrite, so they aren't
|
||||
+// silenced whenever a cutscene or audiolog is playing.
|
||||
void AudioStreamCallback(void *userdata, unsigned char *stream, int len)
|
||||
{
|
||||
+ static Uint8 mixbuf[MOVIE_DEFAULT_BLOCKLEN * 4];
|
||||
SDL_AudioStream *as = *(SDL_AudioStream **)userdata;
|
||||
|
||||
- if (as != NULL && SDL_AudioStreamAvailable(as) > 0)
|
||||
- SDL_AudioStreamGet(as, stream, len);
|
||||
+ if (as == NULL || SDL_AudioStreamAvailable(as) <= 0)
|
||||
+ return;
|
||||
+
|
||||
+ if (len > (int)sizeof(mixbuf))
|
||||
+ len = (int)sizeof(mixbuf);
|
||||
+
|
||||
+ int got = SDL_AudioStreamGet(as, mixbuf, len);
|
||||
+ if (got > 0)
|
||||
+ SDL_MixAudioFormat(stream, mixbuf, AUDIO_S16SYS, got, SDL_MIX_MAXVOLUME);
|
||||
}
|
||||
|
||||
|
||||
@@ -153,11 +163,13 @@
|
||||
|
||||
if (cutscene_audiostream != NULL)
|
||||
{
|
||||
- SDL_PauseAudioDevice(device, 1);
|
||||
- SDL_Delay(1);
|
||||
-
|
||||
+ // AudioStreamCallback (SDL_mixer's post-mix hook) runs continuously on
|
||||
+ // SDL_mixer's own audio thread and reads this pointer - lock around
|
||||
+ // freeing it so that thread can't be mid-read when it's freed.
|
||||
+ SDL_LockAudio();
|
||||
SDL_FreeAudioStream(cutscene_audiostream);
|
||||
cutscene_audiostream = NULL;
|
||||
+ SDL_UnlockAudio();
|
||||
|
||||
if (cutscene_audiobuffer)
|
||||
{
|
||||
@@ -184,8 +196,6 @@
|
||||
|
||||
if (cutscene_audiostream)
|
||||
{
|
||||
- SDL_PauseAudioDevice(device, 0);
|
||||
-
|
||||
if (cutscene_audiobuffer_size > 0)
|
||||
{
|
||||
// === adjust volume in buffer here ===
|
||||
@@ -325,10 +335,9 @@
|
||||
|
||||
if (sfx_on)
|
||||
{
|
||||
- SDL_PauseAudioDevice(device, 1);
|
||||
- SDL_Delay(1);
|
||||
-
|
||||
+ SDL_LockAudio();
|
||||
cutscene_audiostream = SDL_NewAudioStream(AUDIO_U8, 1, fix_int(amovie->a.sampleRate), AUDIO_S16SYS, 2, 48000);
|
||||
+ SDL_UnlockAudio();
|
||||
|
||||
cutscene_audiobuffer_pos = cutscene_audiobuffer;
|
||||
}
|
||||
--- a/src/GameSrc/audiolog.c
|
||||
+++ b/src/GameSrc/audiolog.c
|
||||
@@ -61,8 +61,6 @@
|
||||
extern uchar curr_alog_vol;
|
||||
extern char which_lang;
|
||||
|
||||
-extern SDL_AudioDeviceID device;
|
||||
-
|
||||
errtype audiolog_init(void) { return OK; }
|
||||
|
||||
errtype audiolog_play(int email_id) {
|
||||
@@ -114,10 +112,9 @@
|
||||
|
||||
DEBUG("%s: Playing email", __FUNCTION__);
|
||||
|
||||
- SDL_PauseAudioDevice(device, 1);
|
||||
- SDL_Delay(1);
|
||||
-
|
||||
+ SDL_LockAudio();
|
||||
cutscene_audiostream = SDL_NewAudioStream(AUDIO_U8, 1, fix_int(palog->a.sampleRate), AUDIO_S16SYS, 2, 48000);
|
||||
+ SDL_UnlockAudio();
|
||||
|
||||
audiolog_audiobuffer_pos = audiolog_audiobuffer;
|
||||
|
||||
@@ -150,11 +147,14 @@
|
||||
}
|
||||
|
||||
if (cutscene_audiostream != NULL) {
|
||||
- SDL_PauseAudioDevice(device, 1);
|
||||
- SDL_Delay(1);
|
||||
-
|
||||
+ // AudioStreamCallback (SDL_mixer's post-mix hook, see cutsloop.c)
|
||||
+ // runs continuously on SDL_mixer's own audio thread and reads this
|
||||
+ // pointer - lock around freeing it so that thread can't be
|
||||
+ // mid-read when it's freed.
|
||||
+ SDL_LockAudio();
|
||||
SDL_FreeAudioStream(cutscene_audiostream);
|
||||
cutscene_audiostream = NULL;
|
||||
+ SDL_UnlockAudio();
|
||||
|
||||
if (audiolog_audiobuffer) {
|
||||
free(audiolog_audiobuffer);
|
||||
@@ -177,8 +177,6 @@
|
||||
|
||||
errtype audiolog_loop_callback(void) {
|
||||
if (cutscene_audiostream) {
|
||||
- SDL_PauseAudioDevice(device, 0);
|
||||
-
|
||||
if (audiolog_audiobuffer_size > 0) {
|
||||
int i, vol = curr_alog_vol * 127 / 100; // convert from 0-100 to 0-127
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
--- a/src/MusicSrc/MusicDevice.c
|
||||
+++ b/src/MusicSrc/MusicDevice.c
|
||||
@@ -569,6 +569,14 @@
|
||||
// connected
|
||||
ndev->dev.isOpen = 1;
|
||||
ndev->dev.outputIndex = outputIndex;
|
||||
+#else
|
||||
+ // No native MIDI backend implemented for this platform (e.g. Android) -
|
||||
+ // report failure so InitDecXMI()'s caller falls back to another device
|
||||
+ // instead of treating an unopened device as successfully initialized.
|
||||
+ (void)outputIndex;
|
||||
+ (void)samplerate;
|
||||
+ WARN("NativeMidiInit(): native MIDI is not implemented on this platform");
|
||||
+ return -1;
|
||||
#endif
|
||||
// suppress compiler warnings
|
||||
(void)outputIndex;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
--- a/src/MacSrc/Shock.c
|
||||
+++ b/src/MacSrc/Shock.c
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <SDL.h>
|
||||
|
||||
#ifdef __ANDROID__
|
||||
+#include "qs_config.h"
|
||||
#include "xr_session.h"
|
||||
#endif
|
||||
|
||||
@@ -109,6 +110,15 @@
|
||||
LoadHotkeyKeybinds();
|
||||
LoadMoveKeybinds();
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+ // Persistent, human-editable JSON config (questshock.config.json) - see
|
||||
+ // qs_config.h/CONFIG.md. After prefs.txt is loaded (above) so this file
|
||||
+ // can override it, before init_all() below so there's no live-vs-
|
||||
+ // restart distinction to worry about yet - every field just takes full
|
||||
+ // effect as part of this same startup.
|
||||
+ qs_config_load_startup();
|
||||
+#endif
|
||||
+
|
||||
// Process some startup arguments
|
||||
|
||||
bool show_splash = !CheckArgument("-nosplash");
|
||||
--- a/src/Libraries/INPUT/Source/sdl_events.c
|
||||
+++ b/src/Libraries/INPUT/Source/sdl_events.c
|
||||
@@ -29,6 +29,10 @@
|
||||
#include <SDL.h>
|
||||
#include <OpenGL.h>
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+#include "qs_config.h"
|
||||
+#endif
|
||||
+
|
||||
extern SDL_Window *window;
|
||||
extern SDL_Renderer *renderer;
|
||||
|
||||
@@ -481,6 +485,14 @@
|
||||
SDL_Event ev;
|
||||
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
+#ifdef __ANDROID__
|
||||
+ // Custom event types qs_config_request_save()/_load() enqueue from
|
||||
+ // the "Save Configuration"/"Load Configuration" menu buttons (see
|
||||
+ // qs_config.h) - handled here, on this thread, rather than directly
|
||||
+ // from those JNI calls (which run on Android's UI thread).
|
||||
+ if (qs_config_handle_event(&ev))
|
||||
+ continue;
|
||||
+#endif
|
||||
switch (ev.type) {
|
||||
case SDL_QUIT:
|
||||
// a bit hacky at this place, but this would allow exiting the game via the window's [x] button
|
||||
@@ -0,0 +1,30 @@
|
||||
--- a/src/GameSrc/mainloop.c
|
||||
+++ b/src/GameSrc/mainloop.c
|
||||
@@ -57,6 +57,10 @@
|
||||
#include "tools.h"
|
||||
#include "wrapper.h"
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+#include "qs_config.h"
|
||||
+#endif
|
||||
+
|
||||
// how is the game doing, anyway, set to true at end of time
|
||||
uchar cit_success = FALSE;
|
||||
|
||||
@@ -103,6 +107,16 @@
|
||||
_last_mode = *cmode;
|
||||
(*exit_modes[_last_mode])();
|
||||
*cmode = _new_mode;
|
||||
+#ifdef __ANDROID__
|
||||
+ // Notifies qs_config.c of every loop-mode transition so it can pick
|
||||
+ // between the "Initial Screen" and "Game Screen" virtual-screen
|
||||
+ // presets - see qs_config.h/CONFIG.md. Called synchronously, directly,
|
||||
+ // on this thread (mainloop()'s own) - unlike the UI-thread-triggered
|
||||
+ // config actions elsewhere in qs_config.c, this doesn't need the
|
||||
+ // cross-thread SDL-event dance since it already originates on the
|
||||
+ // right thread.
|
||||
+ qs_screen_profile_notify_loop_mode(*cmode);
|
||||
+#endif
|
||||
_static_change = 0;
|
||||
if (*cmode >= 0)
|
||||
(*enter_modes[*cmode])();
|
||||
@@ -0,0 +1,42 @@
|
||||
--- a/src/GameSrc/cutsloop.c
|
||||
+++ b/src/GameSrc/cutsloop.c
|
||||
@@ -292,11 +292,38 @@
|
||||
|
||||
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+// The intro (cutscene 0) ships in one file per language - localized
|
||||
+// voice-over - each holding the same resource id (cutscene_anims[0]), so
|
||||
+// opening the current language's file instead is all it takes. The death
|
||||
+// and ending cutscenes only have one version.
|
||||
+static char *intro_files_by_language[3] =
|
||||
+{
|
||||
+ "res/data/svgaintr.res", // English
|
||||
+ "res/data/svfrintr.res", // French
|
||||
+ "res/data/svgeintr.res" // German
|
||||
+};
|
||||
+
|
||||
+static int open_cutscene_file(int id)
|
||||
+{
|
||||
+ if (id == 0 && which_lang > 0 && which_lang < 3)
|
||||
+ {
|
||||
+ int handle = ResOpenFile(intro_files_by_language[(int)which_lang]);
|
||||
+ if (handle > 0)
|
||||
+ return handle;
|
||||
+ // Not installed for this language - fall back to English below.
|
||||
+ }
|
||||
+ return ResOpenFile(cutscene_files[id]);
|
||||
+}
|
||||
+#else
|
||||
+#define open_cutscene_file(id) ResOpenFile(cutscene_files[id])
|
||||
+#endif
|
||||
+
|
||||
short play_cutscene(int id, bool show_credits)
|
||||
{
|
||||
MacTuneKillCurrentTheme();
|
||||
|
||||
- cutscene_filehandle = ResOpenFile(cutscene_files[id]);
|
||||
+ cutscene_filehandle = open_cutscene_file(id);
|
||||
if (cutscene_filehandle <= 0) {
|
||||
// If we failed to play the cutscene, go to setup / credits.
|
||||
_new_mode = SETUP_LOOP;
|
||||
@@ -0,0 +1,13 @@
|
||||
--- a/src/GameSrc/mouselook.c
|
||||
+++ b/src/GameSrc/mouselook.c
|
||||
@@ -121,3 +121,10 @@
|
||||
get_mouselook_vel(&mvelx, &mvely);
|
||||
}
|
||||
}
|
||||
+
|
||||
+#ifdef __ANDROID__
|
||||
+// Whether the player is currently in cyberspace; read by the Android
|
||||
+// controller input code (android/app/src/main/cpp/xr_input.c) to switch
|
||||
+// between the normal and cyberspace action sets.
|
||||
+int qs_in_cyberspace(void) { return global_fullmap != NULL && global_fullmap->cyber; }
|
||||
+#endif
|
||||
@@ -0,0 +1,110 @@
|
||||
--- a/src/GameSrc/movekeys.c
|
||||
+++ b/src/GameSrc/movekeys.c
|
||||
@@ -44,6 +44,12 @@
|
||||
extern bool gKeypadOverride;
|
||||
|
||||
uchar parse_motion_key(ushort code, short *cnum, short *cval);
|
||||
+#ifdef __ANDROID__
|
||||
+// Set every frame by android/app/src/main/cpp/xr_input.c.
|
||||
+float qs_cyber_pitch_scale = 1.0f;
|
||||
+float qs_cyber_bank_scale = 1.0f;
|
||||
+#endif
|
||||
+
|
||||
uchar parse_motion_key_cyber(ushort code, short *cnum, short *cval);
|
||||
void init_motion_polling(void);
|
||||
|
||||
@@ -173,6 +179,20 @@
|
||||
*cnum = -1;
|
||||
*cval = 0;
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+ // Pitch and bank strength, scaled by the controller thumbstick's
|
||||
+ // deflection (see android/app/src/main/cpp/xr_input.c); 1.0 for a key.
|
||||
+ short pitchMax = (short)(CONTROL_MAX_VAL * qs_cyber_pitch_scale + 0.5f);
|
||||
+ short bankMax = (short)(CONTROL_MAX_VAL * qs_cyber_bank_scale + 0.5f);
|
||||
+ if (pitchMax < 1)
|
||||
+ pitchMax = 1;
|
||||
+ if (bankMax < 1)
|
||||
+ bankMax = 1;
|
||||
+#else
|
||||
+ short pitchMax = CONTROL_MAX_VAL;
|
||||
+ short bankMax = CONTROL_MAX_VAL;
|
||||
+#endif
|
||||
+
|
||||
while (MoveCyberKeybinds[i].code != 255) {
|
||||
if (code == MoveCyberKeybinds[i].code) {
|
||||
move = MoveCyberKeybinds[i].move;
|
||||
@@ -189,22 +209,22 @@
|
||||
|
||||
case M_CLIMB:
|
||||
*cnum = CONTROL_YVEL;
|
||||
- *cval = -CONTROL_MAX_VAL;
|
||||
+ *cval = -pitchMax;
|
||||
break;
|
||||
|
||||
case M_BANKLEFT:
|
||||
*cnum = CONTROL_XYROT;
|
||||
- *cval = -CONTROL_MAX_VAL;
|
||||
+ *cval = -bankMax;
|
||||
break;
|
||||
|
||||
case M_BANKRIGHT:
|
||||
*cnum = CONTROL_XYROT;
|
||||
- *cval = CONTROL_MAX_VAL;
|
||||
+ *cval = bankMax;
|
||||
break;
|
||||
|
||||
case M_DIVE:
|
||||
*cnum = CONTROL_YVEL;
|
||||
- *cval = CONTROL_MAX_VAL;
|
||||
+ *cval = pitchMax;
|
||||
break;
|
||||
|
||||
case M_ROLLRIGHT:
|
||||
@@ -219,38 +239,38 @@
|
||||
|
||||
case M_CLIMBLEFT:
|
||||
*cnum = CONTROL_YVEL;
|
||||
- *cval = -CONTROL_MAX_VAL;
|
||||
+ *cval = -pitchMax;
|
||||
if (abs(poll_controls[*cnum]) < abs(*cval))
|
||||
poll_controls[*cnum] = *cval;
|
||||
*cnum = CONTROL_XYROT;
|
||||
- *cval = -CONTROL_MAX_VAL;
|
||||
+ *cval = -bankMax;
|
||||
break;
|
||||
|
||||
case M_CLIMBRIGHT:
|
||||
*cnum = CONTROL_YVEL;
|
||||
- *cval = -CONTROL_MAX_VAL;
|
||||
+ *cval = -pitchMax;
|
||||
if (abs(poll_controls[*cnum]) < abs(*cval))
|
||||
poll_controls[*cnum] = *cval;
|
||||
*cnum = CONTROL_XYROT;
|
||||
- *cval = CONTROL_MAX_VAL;
|
||||
+ *cval = bankMax;
|
||||
break;
|
||||
|
||||
case M_DIVERIGHT:
|
||||
*cnum = CONTROL_YVEL;
|
||||
- *cval = CONTROL_MAX_VAL;
|
||||
+ *cval = pitchMax;
|
||||
if (abs(poll_controls[*cnum]) < abs(*cval))
|
||||
poll_controls[*cnum] = *cval;
|
||||
*cnum = CONTROL_XYROT;
|
||||
- *cval = CONTROL_MAX_VAL;
|
||||
+ *cval = bankMax;
|
||||
break;
|
||||
|
||||
case M_DIVELEFT:
|
||||
*cnum = CONTROL_YVEL;
|
||||
- *cval = CONTROL_MAX_VAL;
|
||||
+ *cval = pitchMax;
|
||||
if (abs(poll_controls[*cnum]) < abs(*cval))
|
||||
poll_controls[*cnum] = *cval;
|
||||
*cnum = CONTROL_XYROT;
|
||||
- *cval = -CONTROL_MAX_VAL;
|
||||
+ *cval = -bankMax;
|
||||
break;
|
||||
default:
|
||||
// Unhandled movement. What I gonna do?
|
||||
@@ -0,0 +1,74 @@
|
||||
--- a/src/GameSrc/hkeyfunc.c
|
||||
+++ b/src/GameSrc/hkeyfunc.c
|
||||
@@ -1465,3 +1465,71 @@
|
||||
#pragma enable_message(202)
|
||||
|
||||
#endif // NOT_YET
|
||||
+
|
||||
+#ifdef __ANDROID__
|
||||
+// Tops up parts of the player's state; android/app/src/main/cpp/qs_config.c
|
||||
+// calls it once per frame with the "unlimited" cheats that are switched on.
|
||||
+// `what` is a bitmask: 1 health, 2 energy, 4 ammo, 8 items (the same values as
|
||||
+// QS_CHEAT_MASK_* in qs_config.c). Only conventional guns use ammo: beam and
|
||||
+// melee weapons keep heat in the same field, so they are left alone (beams draw
|
||||
+// on energy). Spare ammo clips, grenades and drugs are only refilled for kinds
|
||||
+// the player already holds, so the cheat never creates items.
|
||||
+void qs_cheat_refill(int what) {
|
||||
+ const int kAmmoFloor = 99;
|
||||
+ const int kItemFloor = 99;
|
||||
+ uchar vitalsChanged = FALSE;
|
||||
+ uchar inventoryChanged = FALSE;
|
||||
+
|
||||
+ if (player_struct.dead)
|
||||
+ return;
|
||||
+ if ((what & 1) && player_struct.hit_points < PLAYER_MAX_HP) {
|
||||
+ player_struct.hit_points = PLAYER_MAX_HP;
|
||||
+ vitalsChanged = TRUE;
|
||||
+ }
|
||||
+ if ((what & 1) && player_struct.cspace_hp < PLAYER_MAX_HP) {
|
||||
+ player_struct.cspace_hp = PLAYER_MAX_HP;
|
||||
+ vitalsChanged = TRUE;
|
||||
+ }
|
||||
+ if ((what & 2) && player_struct.energy < 255) {
|
||||
+ player_struct.energy = 255;
|
||||
+ vitalsChanged = TRUE;
|
||||
+ }
|
||||
+ if (vitalsChanged)
|
||||
+ chg_set_flg(VITALS_UPDATE);
|
||||
+
|
||||
+ if (what & 4) {
|
||||
+ for (int i = 0; i < NUM_WEAPON_SLOTS; i++) {
|
||||
+ weapon_slot *slot = &player_struct.weapons[i];
|
||||
+ if (slot->type != EMPTY_WEAPON_SLOT && slot->type <= GUN_SUBCLASS_SPECIAL &&
|
||||
+ slot->ammo < kAmmoFloor) {
|
||||
+ slot->ammo = kAmmoFloor;
|
||||
+ inventoryChanged = TRUE;
|
||||
+ }
|
||||
+ }
|
||||
+ for (int i = 0; i < NUM_AMMO_TYPES; i++) {
|
||||
+ if (player_struct.cartridges[i] > 0 && player_struct.cartridges[i] < kItemFloor) {
|
||||
+ player_struct.cartridges[i] = kItemFloor;
|
||||
+ inventoryChanged = TRUE;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ if (what & 8) {
|
||||
+ for (int i = 0; i < NUM_GRENADEZ; i++) {
|
||||
+ if (player_struct.grenades[i] > 0 && player_struct.grenades[i] < kItemFloor) {
|
||||
+ player_struct.grenades[i] = kItemFloor;
|
||||
+ inventoryChanged = TRUE;
|
||||
+ }
|
||||
+ }
|
||||
+ for (int i = 0; i < NUM_DRUGZ; i++) {
|
||||
+ if (player_struct.drugs[i] > 0 && player_struct.drugs[i] < kItemFloor) {
|
||||
+ player_struct.drugs[i] = kItemFloor;
|
||||
+ inventoryChanged = TRUE;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ if (inventoryChanged) {
|
||||
+ chg_set_flg(INVENTORY_UPDATE);
|
||||
+ mfd_force_update();
|
||||
+ }
|
||||
+}
|
||||
+#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
--- a/src/GameSrc/setup.c 2026-09-20 16:04:46.197310679 +0200
|
||||
+++ b/src/GameSrc/setup.c 2026-09-20 16:05:08.400330327 +0200
|
||||
@@ -58,6 +58,9 @@
|
||||
#include "gamestrn.h"
|
||||
|
||||
#include "mainloop.h"
|
||||
+#ifdef __ANDROID__
|
||||
+#include "qs_splash.h"
|
||||
+#endif
|
||||
#include "tools.h"
|
||||
#include "input.h"
|
||||
#include "game_screen.h"
|
||||
@@ -1300,6 +1303,11 @@
|
||||
if (!show_splash)
|
||||
return;
|
||||
|
||||
+#ifdef __ANDROID__
|
||||
+ // The QuestShock title, before the engine's own splash screens.
|
||||
+ qs_splash_show();
|
||||
+#endif
|
||||
+
|
||||
// Need to load the splash palette file
|
||||
|
||||
INFO("Loading splshpal.res");
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Forces a fully fresh native build - works around stageEngine sometimes
|
||||
# wrongly reporting UP-TO-DATE after android/engine-patches/ or
|
||||
# android/gl4es-patches/ changes (see gradle_stage_engine_stale_cache memory
|
||||
# note). Run from anywhere. Needs JAVA_HOME set to a JDK 17+; defaults to
|
||||
# Android Studio's bundled JDK if found.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
if [ -z "${JAVA_HOME:-}" ]; then
|
||||
if [ -d /opt/android-studio/jbr ]; then
|
||||
export JAVA_HOME=/opt/android-studio/jbr
|
||||
else
|
||||
echo "JAVA_HOME not set and no JDK found at /opt/android-studio/jbr - export it yourself" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
./gradlew --stop
|
||||
rm -rf \
|
||||
../build/android-engine \
|
||||
../build/android-gl4es \
|
||||
../build/android-prebuilt \
|
||||
app/.cxx \
|
||||
app/build
|
||||
echo "Done - next Gradle build/Run will rebuild from scratch."
|
||||
@@ -0,0 +1,47 @@
|
||||
diff -ru a/CMakeLists.txt b/CMakeLists.txt
|
||||
--- a/CMakeLists.txt 2026-07-31 13:25:14.739711912 +0200
|
||||
+++ b/CMakeLists.txt 2026-07-31 13:25:29.283619378 +0200
|
||||
@@ -2,11 +2,22 @@
|
||||
|
||||
project(gl4es LANGUAGES C)
|
||||
|
||||
+# questshock: on Android this project is add_subdirectory()'d straight into
|
||||
+# the engine's own CMake configure (see android/engine-patches/
|
||||
+# 02-android-opengl-es.patch), not built standalone - CMAKE_SOURCE_DIR then
|
||||
+# points at the *outer* project's root, not here, so writing there would
|
||||
+# scatter build output outside this project entirely. Leaving these unset
|
||||
+# for Android just falls back to CMake's normal per-target build-tree
|
||||
+# output, which AGP's own native-build packaging finds regardless of path;
|
||||
+# link_directories() is also unneeded there since the GL/EGL targets link
|
||||
+# each other by CMake target name, not by search path.
|
||||
+if(NOT ANDROID)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)
|
||||
|
||||
link_directories(${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
|
||||
+endif()
|
||||
|
||||
option(PANDORA "Set to ON if targeting an OpenPandora device" ${PANDORA})
|
||||
option(PYRA "Set to ON if targeting an Dragonbox Pyra device" ${PYRA})
|
||||
diff -ru a/src/CMakeLists.txt b/src/CMakeLists.txt
|
||||
--- a/src/CMakeLists.txt 2026-07-31 13:25:14.747711861 +0200
|
||||
+++ b/src/CMakeLists.txt 2026-07-31 13:25:44.043526054 +0200
|
||||
@@ -220,7 +220,16 @@
|
||||
target_link_libraries(GL ${log-lib})
|
||||
endif()
|
||||
|
||||
- if(CMAKE_SHARED_LIBRARY_SUFFIX MATCHES ".so")
|
||||
+ # questshock: on Android, AGP's native-build packaging only bundles a
|
||||
+ # jniLibs .so whose *filename* literally ends in ".so" (and the dynamic
|
||||
+ # linker resolves NEEDED entries by embedded SONAME, which CMake derives
|
||||
+ # from this same filename) - versioning it to "libGL.so.1" like the
|
||||
+ # desktop build does would need a separate patchelf rename step, which
|
||||
+ # only build-image's Docker environment could guarantee has this tool.
|
||||
+ # Skipping the override on Android instead so CMake's own default
|
||||
+ # SHARED library naming (plain "libGL.so", correct SONAME included)
|
||||
+ # already comes out right with no extra step.
|
||||
+ if(CMAKE_SHARED_LIBRARY_SUFFIX MATCHES ".so" AND NOT ANDROID)
|
||||
set_target_properties(GL PROPERTIES SUFFIX ".so.1")
|
||||
endif()
|
||||
install(TARGETS GL
|
||||
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: ptitSeb
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: paypal.me/0ptitSeb
|
||||
@@ -0,0 +1,11 @@
|
||||
.gdb_history
|
||||
.vscode/
|
||||
CMakeFiles/
|
||||
lib/
|
||||
Makefile
|
||||
CMakeCache.txt
|
||||
CTestTestfile.cmake
|
||||
cmake_install.cmake
|
||||
spec/yml/gles-1.1-full.yml
|
||||
/build/
|
||||
/tests/*.png
|
||||
@@ -0,0 +1,20 @@
|
||||
language: c
|
||||
sudo: false
|
||||
dist: buster
|
||||
|
||||
compiler:
|
||||
- gcc
|
||||
|
||||
#Build steps
|
||||
before_script:
|
||||
- mkdir build
|
||||
- cd build
|
||||
- cmake .. -DODROID=1
|
||||
|
||||
script:
|
||||
- make
|
||||
|
||||
#after_script:
|
||||
# - sudo apt-get install apitrace-gl-frontend=5\* imagemagick xvfb -y
|
||||
# - cd ../tests
|
||||
# - xvfb-run ./tests.sh ../lib
|
||||
@@ -0,0 +1,104 @@
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
###########################
|
||||
#
|
||||
# GL static library
|
||||
#
|
||||
###########################
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE := GL
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
|
||||
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES) -DBCMHOST
|
||||
|
||||
LOCAL_SRC_FILES := \
|
||||
src/gl/arbconverter.c \
|
||||
src/gl/arbgenerator.c \
|
||||
src/gl/arbhelper.c \
|
||||
src/gl/arbparser.c \
|
||||
src/gl/array.c \
|
||||
src/gl/blend.c \
|
||||
src/gl/blit.c \
|
||||
src/gl/buffers.c \
|
||||
src/gl/build_info.c \
|
||||
src/gl/debug.c \
|
||||
src/gl/decompress.c \
|
||||
src/gl/depth.c \
|
||||
src/gl/directstate.c \
|
||||
src/gl/drawing.c \
|
||||
src/gl/enable.c \
|
||||
src/gl/envvars.c \
|
||||
src/gl/eval.c \
|
||||
src/gl/face.c \
|
||||
src/gl/fog.c \
|
||||
src/gl/fpe.c \
|
||||
src/gl/fpe_cache.c \
|
||||
src/gl/fpe_shader.c \
|
||||
src/gl/framebuffers.c \
|
||||
src/gl/gl_lookup.c \
|
||||
src/gl/getter.c \
|
||||
src/gl/gl4es.c \
|
||||
src/gl/glstate.c \
|
||||
src/gl/hint.c \
|
||||
src/gl/init.c \
|
||||
src/gl/light.c \
|
||||
src/gl/line.c \
|
||||
src/gl/list.c \
|
||||
src/gl/listdraw.c \
|
||||
src/gl/listrl.c \
|
||||
src/gl/loader.c \
|
||||
src/gl/logs.c \
|
||||
src/gl/matrix.c \
|
||||
src/gl/matvec.c \
|
||||
src/gl/oldprogram.c \
|
||||
src/gl/pixel.c \
|
||||
src/gl/planes.c \
|
||||
src/gl/pointsprite.c \
|
||||
src/gl/preproc.c \
|
||||
src/gl/program.c \
|
||||
src/gl/queries.c \
|
||||
src/gl/raster.c \
|
||||
src/gl/render.c \
|
||||
src/gl/samplers.c \
|
||||
src/gl/shader.c \
|
||||
src/gl/shaderconv.c \
|
||||
src/gl/shader_hacks.c \
|
||||
src/gl/stack.c \
|
||||
src/gl/stencil.c \
|
||||
src/gl/string_utils.c \
|
||||
src/gl/stubs.c \
|
||||
src/gl/texenv.c \
|
||||
src/gl/texgen.c \
|
||||
src/gl/texture.c \
|
||||
src/gl/texture_compressed.c \
|
||||
src/gl/texture_params.c \
|
||||
src/gl/texture_read.c \
|
||||
src/gl/texture_3d.c \
|
||||
src/gl/uniform.c \
|
||||
src/gl/vertexattrib.c \
|
||||
src/gl/wrap/gl4eswraps.c \
|
||||
src/gl/wrap/gles.c \
|
||||
src/gl/wrap/glstub.c \
|
||||
src/gl/math/matheval.c \
|
||||
src/glx/hardext.c \
|
||||
src/glx/glx.c \
|
||||
src/glx/lookup.c \
|
||||
src/glx/gbm.c \
|
||||
src/glx/streaming.c \
|
||||
|
||||
LOCAL_CFLAGS += -g -std=gnu99 -funwind-tables -O3 -fvisibility=hidden -include include/android_debug.h
|
||||
LOCAL_CFLAGS += -DNOX11
|
||||
LOCAL_CFLAGS += -DNO_GBM
|
||||
#LOCAL_CFLAGS += -DNO_INIT_CONSTRUCTOR
|
||||
LOCAL_CFLAGS += -DDEFAULT_ES=2
|
||||
//TODO: maybe temporary?
|
||||
LOCAL_CFLAGS += -Wno-typedef-redefinition -Wno-dangling-else
|
||||
|
||||
LOCAL_LDLIBS := -llog
|
||||
#building as a static lib
|
||||
|
||||
LOCAL_CFLAGS += -DSTATICLIB
|
||||
include $(BUILD_STATIC_LIBRARY)
|
||||
@@ -0,0 +1,184 @@
|
||||
Version history
|
||||
----
|
||||
##### v1.1.6
|
||||
* Improve glGetError() handling
|
||||
* Improve LIBGL_FB=3 handling
|
||||
* Added some ShaderHacks
|
||||
* Better Renderbuffer handling
|
||||
* Fixed many issues with VAO and VBO
|
||||
* Improved ARB shader support
|
||||
* optimized some format conversion
|
||||
* Improve mipmap handling
|
||||
* Improve Android support
|
||||
* Added sampler handling
|
||||
* Improve texture format handling
|
||||
* some fixes to a few matrix computation
|
||||
* Added support for a new platform: Windows (thx to @yjh-styx)
|
||||
* Added some more extension to GLES2 backend
|
||||
|
||||
##### v1.1.4
|
||||
* Fixed some regression, and foobillard++ is working fine now
|
||||
* Set default GL version to 2.1 (instead of 2.0) for GLES2.0 backend
|
||||
* Added ARB_vertex_program and ARB_fragment_program
|
||||
* Improved fpe with more compatibility fixes
|
||||
* Improved Handling of DXTc textures
|
||||
* Improve handling of partially mipmap'd textures
|
||||
* Added PYRA Profile
|
||||
* Added support for Texture LOD access in Fragment Shaders
|
||||
* Increased maximum Texture Unit support to 16 (from 8)
|
||||
* Added support for program without Vertex Shader
|
||||
* Some fixes for program that switch GLX Context frequently.
|
||||
* Fixes and improvements in fpe_shaders (less array access)
|
||||
* Added support for Clipping Plane in custom shaders (help OpenMW water reflection)
|
||||
* Added support for multiple FBO attachement (if hardware support it)
|
||||
|
||||
##### v1.1.2
|
||||
* Improved a bit the merger (wich merge subsequent `glBegin(...)`/`glEnd()`) efficiency
|
||||
* Removed LIBGL_BEGINEND=2 (that was not working correctly, and it complexify the code too much)
|
||||
* Added some Direct Access function (from EXT_direct_access)
|
||||
* Fixes and Improvements on depth (and depth_Stencil) Texture handling
|
||||
* Handling of GL_BGRA color size in VA (for HumandRessourceMachine)
|
||||
* Some fixes to fpe_shader (for Neverwinter Night)
|
||||
* Rework of Header structure (from @rajdakin)
|
||||
* Better test, using "make test" (from @rajdakin, still using apitrace for replay)
|
||||
* Added PSA: Precompiled Shader Archive, to store (and fast retrieve) FPE shader (and avoid some pauses when complex FPE shaders are computed)
|
||||
* Numerous optimization and changes in Batch mode.
|
||||
* Fixes to GL_BGRA handling
|
||||
* Improvement in texture format handling, to limit number of conversions
|
||||
* Added handling of VBO. Real VBO are used (in GLES2+ only) for GL_ARRAY_BUFFER and GL_ELEMENT_ARRAY_BUFFER
|
||||
* Added VBO when processing glList (only in GLES2+)
|
||||
* Added a few more GL3.x functions (like glTexStorage). No real GL3.0 support yet
|
||||
* Can now use GBM/DRM, using LIBGL_FB=4. Still WIP, but now it start to work (thanks to @icecream95)
|
||||
* Preliminary Emscripten support (thanks to @CecilHarvey)
|
||||
* AmigaOS4 support is now live (thanks to @kas1e)
|
||||
* Some fixes to texture handling, when uploading (or modifying) a texture in a multi-texture context
|
||||
* Small optimization some of the FPE Generated program (many fragment shaders that do texturing)
|
||||
* Added support to glGetProgramBinary extension
|
||||
* GL4ES has a logo now :) !
|
||||
* AmigaOS4 is now fully supported! And SDK (with libs and samples) is available on os4depot
|
||||
* Emscripten is now supported! A first example of a game using gl4es in a web build can be found [here](http://ptitseb.github.io/stuntcarremake/) with [Stunt Car Remake](https://github.com/ptitSeb/stuntcarremake)
|
||||
|
||||
|
||||
##### v1.1.0
|
||||
* Default backend is now GLES2 (but not on Pandora, still GLES1.1 for compatibility reasons)
|
||||
* Added LIBGL_SHADERNOGLES to remove the GLES part of shaders (if any).
|
||||
* Various RPi improvements, mainly in the context creation.
|
||||
* Various AmigaOS4 improvement and workaround (but still in a beta state)
|
||||
* Various improvement in GLX function, with a more accurate way to emulate GLXFBConfig
|
||||
* Improved the way eglSurface are created in glx.c, to avoid try to create 2 on the same window (EGL doesn't allow that)
|
||||
* Added LIBGL_GLXRECYCLE to not delete eglSurface and recycle them
|
||||
* Added tracking of Framebuffers Object and Renderbuffers Objects
|
||||
* Added (real) support for Float and Half-float Texture (including has attachement to FBO, emulating it if not supported in Hardware)
|
||||
* Added support for Depth Stencil texture when attached to an FBO
|
||||
* Added LIBGL_FBO=WxH for all platform
|
||||
* Fixed some issue when resized textures attached to an FBO
|
||||
* Added LIBGL_NODEPTHTEX to avoid using Depth Texture when available (using renderbuffer can be faster)
|
||||
* Added support for Depth Texture when attached to an FBO (if supported by Hardware)
|
||||
* Fixes some isue with blitting of FBO when size of Main Framebuffer changed
|
||||
* Added option LIBGL_LOGSHADERERROR to get Shader compiler log and error
|
||||
* Added support for (emulated) Hardware Instancing
|
||||
* Added support for GL_ARB_draw_elements_base_vertex
|
||||
|
||||
##### v1.0.8
|
||||
* Fixes and improvments to avoid unnecessary GLES state changes
|
||||
* Fixes some memory issues with glBitmap
|
||||
* Fixes to FPE (when using multitexture and GL_COMBINE)
|
||||
* Added some TexEnv extension for GLES2 backend
|
||||
* Fixes to Batch / Merger on GL_POLYGON primitives
|
||||
* Fixes to LineStipple
|
||||
* Pandora only: Fixed use of Texture Streaming on GLES2 backend
|
||||
|
||||
##### v1.0.6
|
||||
* Factorised "Blit" function, and implemented `glBlitFramebuffer`
|
||||
* Optimized `glBitmap`
|
||||
* Added (limited) direct support to `GL_UNSIGNED_INT` for `glDrawElements` for hardware that support it
|
||||
* Improved (a lot) `glBegin`/`glEnd` merger
|
||||
* Added Anisotropic filtering support, for hardware that support it.
|
||||
* Changed `LIBGL_BATCH`. It will now try to merge small (parametrable) subsequent `glDrawXXXX`
|
||||
* Changed (simplified) the way texture "0" is handled
|
||||
* Improvement to `glDrawArrays` and `glDrawElement`, with less copy of data
|
||||
* WIP AmigaOS4 support (and BigEndian architecture)
|
||||
* Improved NPOT support for "Limited NPOT" hardware
|
||||
* Lots of FPE fixes
|
||||
* Improve the way multi glX Context are handled
|
||||
* Added basic pre-proc (only handle comments for now)
|
||||
* Small optimization on when using `glDrawArrays` with GL_QUADS
|
||||
* Improvement to GL_RENDER
|
||||
* Improvement to line stipple
|
||||
* Improvement to glPolygonMode(GL_LINE)
|
||||
|
||||
##### v1.0.4
|
||||
* FPE is now usable. Most function are implemented
|
||||
* ShaderConv in now usable. Basic GL 2 shader are supported
|
||||
* Refactored Texture cache and handling, for better Tex1D/Tex3D/TexRectangle handling
|
||||
|
||||
##### v1.0.2
|
||||
* Added GLES2 backend infrastructure
|
||||
* Begin GLES2 backend
|
||||
* Infrastructure for FPE (Fixed Pipeline Emulator)
|
||||
* Basic and Crude Shader convertor
|
||||
* Added blit function for GLES2 backend
|
||||
* Added some OpenGL Builtin VAs and Uniforms
|
||||
|
||||
##### 1.0.0
|
||||
* Removed old ES2 defines (ES2 will be dynamic later)
|
||||
|
||||
##### 0.9.8
|
||||
* Added TravisCI build on github
|
||||
* Added some optimization when to discard call to glBindTexture if useless (same texture) between 2 glBegin/glEnd blocks
|
||||
* Stubbed glGet with GL_DRAW_BUFFER
|
||||
* Improvement to Raster operations
|
||||
* Factorised Blit function (only 1 function for that now)
|
||||
* Tracking ShadeModel
|
||||
* Tracking TexEnv
|
||||
|
||||
##### 0.9.7
|
||||
* Tracking Clip Planes
|
||||
* Refactor Blitting function, and use glDrawTex extension if present
|
||||
* Restructured README and split in several files
|
||||
* Some improvments and fixes to LIBGL_BEGINEND=2 mode
|
||||
* Some improvments to some LIBGL_SHRINK mode
|
||||
* Proper support for DOT3 extension
|
||||
* Some fixes to Read/Draw Buffer handling
|
||||
* Some fix with the PixMap glX context creation
|
||||
|
||||
##### 0.9.6
|
||||
* Some fixes in GL_TEXTURE_RECTANGLE_ARB handling
|
||||
* Some other fixes in texture handling (unpack and glList related)
|
||||
* Some fix with the PBuffer glX context creation
|
||||
* Tracking of glFog
|
||||
* Exposed glBlendEquation if supported
|
||||
* New LIBGL_AVOID16BITS parameter to prefer 32bits texture (useful on ODroid)
|
||||
* Some optimisations in texture conversion
|
||||
|
||||
##### 0.9.5
|
||||
* Added some optimizations for sequential glBegin/glEnd blocks, with a switch to control them
|
||||
* Fixed many issue with Lights introduced with 0.9.4
|
||||
* Fixed Android build introduced with 0.9.4
|
||||
|
||||
##### 0.9.4
|
||||
* Fixed some extended functions (like glBlendFuncSeparate) not working inside list (fixing some issues with Batch mode)
|
||||
* Added back GL_TEXTURE_RECTANGLE_ARB handling (and using npot texture, even limited, if available)
|
||||
* Added tracking of Lights and Materials
|
||||
* Fixed (Added in fact) support for Multisampling on the GLX Context creation (a bit hacky, but seems to works)
|
||||
* Added LIBGL_NODOWNSAMPLING and associated Hint
|
||||
* Try to implement some caching on VAO to avoid some memcpy in renderlist (with a way to disable it just in case)
|
||||
|
||||
##### 0.9.3
|
||||
* Added support for Cube Mapping (with hardware support)
|
||||
* Improved Texture state tracking
|
||||
* Added LIBGL_NOTEXMAT env. var. switch for Texture Matrix handling
|
||||
* Added GL_EXT_vertex_array_bgra (and NEONinzed some loop)
|
||||
* Finished GL_EXT_direct_state_access extension
|
||||
* Mangled glX function (to be able to use apitrace to capture GL frames)
|
||||
* Return some values in glXQueryServerString, coherent with glXGetClientString
|
||||
|
||||
##### 0.9.2
|
||||
* All matrix are tracked now
|
||||
* Texture Matrix are 100% handled by gl4es. GLES Hardware keep an Identity matrix (TexCoord are transformed if needed). This allows a better handling of NPOT texture on hardware that doesn't support Full NPOT (fixed movies being horizontally shifted in openmw with LIBGL_NPOT=1 for example)
|
||||
|
||||
##### 0.9.1
|
||||
* Added gl4es specifics glHint capabilities. If the extension GL_GL4ES_hint is present, than a few Hint are accessible. Look in include/gl4eshint.h for the list.
|
||||
|
||||
##### 0.9.0
|
||||
* New name: gl4es
|
||||
@@ -0,0 +1,300 @@
|
||||
cmake_minimum_required(VERSION 2.8.12)
|
||||
|
||||
project(gl4es LANGUAGES C)
|
||||
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)
|
||||
|
||||
link_directories(${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
|
||||
|
||||
option(PANDORA "Set to ON if targeting an OpenPandora device" ${PANDORA})
|
||||
option(PYRA "Set to ON if targeting an Dragonbox Pyra device" ${PYRA})
|
||||
option(BCMHOST "Set to ON if targeting an RPi(2) device" ${BCMHOST})
|
||||
option(ODROID "Set to ON if targeting an ODroid device" ${ODROID})
|
||||
option(GOA_CLONE "Set to ON if targeting GO Advance clones, like RG351p/v, Gameforce Chi, RGB10..." ${GOA_CLONE})
|
||||
option(ANDROID "Set to ON if targeting an Android device" ${ANDROID})
|
||||
option(CHIP "Set to ON if targeting an C.H.I.P. device" ${CHIP})
|
||||
option(AMIGAOS4 "Set to ON if targeting an AmigaOS4/Warp3D platform (activate NOEGL and NOX11)" ${AMIGAOS4})
|
||||
option(NOX11 "Set to ON to not use X11 (creation of context has to be done outside gl4es)" ${NOX11})
|
||||
option(NOEGL "Set to ON to not use EGL (all functions are taken in GLES library)" ${NOEGL})
|
||||
option(STATICLIB "Set to ON to build a static version of gl4es" ${STATICLIB})
|
||||
option(GBM "Set to ON to not build GBM interface" ${GBM})
|
||||
option(USE_CCACHE "Set to ON to use ccache if present in the system" ${USE_CCACHE})
|
||||
option(USE_CLOCK "Set to ON to use clock_gettime instead of gttimeofday for LIBGL_FPS" ${USE_CLOCK})
|
||||
option(NO_LOADER "disable library loader (useful for static library with NOEGL, NOX11, use include/gl4esinit.h)" ${NO_LOADER})
|
||||
option(NO_INIT_CONSTRUCTOR "disable automatic initialization (useful for static library, use include/gl4esinit.h)" ${NO_INIT_CONSTRUCTOR})
|
||||
option(USE_ANDROID_LOG "Set to ON to use Android log instead of stdio" ${USE_ANDROID_LOG})
|
||||
option(EGL_WRAPPER "Set to ON to build EGL wrapper" ${EGL_WRAPPER})
|
||||
option(GLX_STUBS "Set to ON to build GLX function stubs" ${GLX_STUBS})
|
||||
|
||||
include(CheckSymbolExists)
|
||||
check_symbol_exists(backtrace "execinfo.h" HAS_BACKTRACE)
|
||||
if (HAS_BACKTRACE)
|
||||
add_definitions(-DHAS_BACKTRACE)
|
||||
endif()
|
||||
|
||||
if(CMAKE_SYSTEM_NAME MATCHES "Windows" OR CMAKE_SYSTEM_NAME MATCHES "MSYS")
|
||||
set(NOX11 ON)
|
||||
set(NO_GBM ON)
|
||||
set(WIN32_PLATFORM ON)
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "MSVC" OR "x${CMAKE_C_SIMULATE_ID}" STREQUAL "xMSVC")
|
||||
set(WIN32_MSVC ON) #msvc or icl or clang-cl
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Pandora
|
||||
if(PANDORA)
|
||||
add_definitions(-DPANDORA)
|
||||
add_definitions(-DTEXSTREAM)
|
||||
add_definitions(-mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp -ftree-vectorize -fsingle-precision-constant -ffast-math)
|
||||
set(NO_GBM ON)
|
||||
endif()
|
||||
|
||||
if((NOT GBM) OR PANDORA)
|
||||
set(NO_GBM ON)
|
||||
else()
|
||||
set(NO_GBM OFF)
|
||||
endif()
|
||||
|
||||
|
||||
if(NOT DEFAULT_ES)
|
||||
if(PANDORA)
|
||||
set(DEFAULT_ES 1)
|
||||
else()
|
||||
set(DEFAULT_ES 2)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(STATICLIB)
|
||||
add_definitions(-DSTATICLIB)
|
||||
endif(STATICLIB)
|
||||
|
||||
# Pyra
|
||||
if(PYRA)
|
||||
add_definitions(-DPYRA)
|
||||
set(USE_CLOCK ON)
|
||||
#add_definitions(-DTEXSTREAM)
|
||||
add_definitions(-mcpu=cortex-a15 -mfpu=neon-vfpv4 -mfloat-abi=hard -ftree-vectorize -fsingle-precision-constant -ffast-math)
|
||||
set(NO_GBM ON)
|
||||
endif()
|
||||
|
||||
# Raspberry PI
|
||||
if(BCMHOST)
|
||||
include_directories(/opt/vc/include /opt/vc/include/interface/vcos/pthreads /opt/vc/include/interface/vmcs_host/linux)
|
||||
link_directories(/opt/vc/lib)
|
||||
add_definitions(-DBCMHOST)
|
||||
endif()
|
||||
|
||||
# ODROID
|
||||
if(ODROID)
|
||||
add_definitions(-DODROID)
|
||||
endif()
|
||||
|
||||
# GOA_CLONE
|
||||
if(GOA_CLONE)
|
||||
add_definitions(-DGOA_CLONE)
|
||||
add_definitions(-mcpu=cortex-a35 -mfpu=neon-vfpv3 -march=armv8-a+crc+simd+crypto -mfloat-abi=hard -ftree-vectorize -fsingle-precision-constant -ffast-math)
|
||||
set(EGL_WRAPPER ON)
|
||||
set(GLX_STUBS ON)
|
||||
endif()
|
||||
|
||||
# Android
|
||||
if(ANDROID)
|
||||
add_definitions(-DANDROID)
|
||||
add_definitions(-DNOX11 -DNO_GBM -DDEFAULT_ES=2)
|
||||
endif()
|
||||
|
||||
if(USE_ANDROID_LOG)
|
||||
add_definitions(-DUSE_ANDROID_LOG)
|
||||
find_library(log-lib log)
|
||||
endif()
|
||||
|
||||
#PocketCHIP
|
||||
if(CHIP)
|
||||
add_definitions(-DCHIP)
|
||||
add_definitions(-mcpu=cortex-a8 -mfpu=neon -mfloat-abi=hard -ftree-vectorize -fsingle-precision-constant -ffast-math)
|
||||
endif()
|
||||
|
||||
# AmigaOS4
|
||||
if(AMIGAOS4)
|
||||
set(CMAKE_C_COMPILER "ppc-amigaos-gcc")
|
||||
set(CMAKE_CXX_COMPILER "ppc-amigaos-g++")
|
||||
set(CMAKE_LINKER "ppc-amigaos-ld")
|
||||
set(CMAKE_AR "ppc-amigaos-ar")
|
||||
set(CMAKE_RANLIB "ppc-amigaos-ranlib")
|
||||
add_definitions(-DAMIGAOS4)
|
||||
set(NOX11 ON)
|
||||
set(NOEGL ON)
|
||||
set(NO_GBM ON)
|
||||
set(CMAKE_C_FLAGS_DEBUG "-O1 -gstabs")
|
||||
set(CMAKE_C_FLAGS_RELWITHDEBINFO "-O2 -gstabs")
|
||||
endif()
|
||||
|
||||
#NOX11
|
||||
if(NOX11)
|
||||
add_definitions(-DEGL_NO_X11)
|
||||
add_definitions(-DNOX11)
|
||||
endif()
|
||||
|
||||
#NOEGL
|
||||
if(NOEGL)
|
||||
add_definitions(-DNOEGL)
|
||||
add_definitions(-DNOX11)
|
||||
endif()
|
||||
|
||||
if(NOT NO_GBM)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(KMSDRM REQUIRED libdrm gbm egl)
|
||||
link_directories(${KMSDRM_LIBRARY_DIRS})
|
||||
include_directories(${KMSDRM_INCLUDE_DIRS})
|
||||
endif()
|
||||
|
||||
if(NO_GBM)
|
||||
add_definitions(-DNO_GBM)
|
||||
endif()
|
||||
|
||||
#NOX11
|
||||
if(USE_CLOCK)
|
||||
add_definitions(-DUSE_CLOCK)
|
||||
endif()
|
||||
|
||||
if(NO_LOADER)
|
||||
add_definitions(-DNO_LOADER)
|
||||
endif()
|
||||
|
||||
if(NO_INIT_CONSTRUCTOR)
|
||||
add_definitions(-DNO_INIT_CONSTRUCTOR)
|
||||
endif()
|
||||
|
||||
if(GLX_STUBS)
|
||||
add_definitions(-DGLX_STUBS)
|
||||
endif()
|
||||
|
||||
#DEFAULT_ES=2
|
||||
if(DEFAULT_ES EQUAL 2)
|
||||
add_definitions(-DDEFAULT_ES=2)
|
||||
endif()
|
||||
if(DEFAULT_ES EQUAL 1)
|
||||
add_definitions(-DDEFAULT_ES=1)
|
||||
endif()
|
||||
if(USE_CCACHE)
|
||||
find_program(CCACHE_FOUND ccache)
|
||||
if(CCACHE_FOUND)
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
link_directories(${CMAKE_SOURCE_DIR}/lib)
|
||||
if(NOT WIN32_MSVC)
|
||||
add_definitions(-std=gnu11 -funwind-tables)
|
||||
if(NOT WIN32_PLATFORM)
|
||||
add_definitions(-fvisibility=hidden)
|
||||
else()
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--exclude-all-symbols,--kill-at")
|
||||
endif()
|
||||
elseif(CMAKE_C_COMPILER_ID MATCHES "Clang")
|
||||
add_definitions(-Wno-deprecated-declarations) #strdup
|
||||
add_definitions(-Wno-unused-function -Wno-unused-variable -Wno-dangling-else)
|
||||
add_definitions(-Wno-implicit-const-int-float-conversion)
|
||||
add_definitions(-Wno-visibility)
|
||||
else()
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "Intel")
|
||||
add_definitions(-wd1786 -wd589 -wd537 -wd118 -wd2722)
|
||||
else()
|
||||
add_definitions(-wd4996 -wd4244 -wd4267 -wd4098 -wd4018)
|
||||
endif()
|
||||
add_definitions("-Dinline=__inline" "-D__func__=__FUNCTION__") # for VC<=13
|
||||
endif()
|
||||
|
||||
if (CMAKE_CC_COMPILER_ID MATCHES "Clang" OR CMAKE_SYSTEM_NAME MATCHES "Emscripten")
|
||||
add_definitions(-Wno-pointer-sign -Wno-dangling-else)
|
||||
endif()
|
||||
|
||||
include_directories(include)
|
||||
add_subdirectory(src)
|
||||
|
||||
enable_testing()
|
||||
|
||||
macro(create_test test_name test_filename calls_count tolerance)
|
||||
if (${ARGC} EQUAL 5)
|
||||
add_test(${test_name}
|
||||
${CMAKE_COMMAND}
|
||||
-D LIBRARY_FOLDER=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
|
||||
-D TESTS_DIRECTORY=${CMAKE_SOURCE_DIR}/tests
|
||||
-D TEST_FILENAME=${test_filename}
|
||||
-D CALLS=${calls_count}
|
||||
-D TOLERANCE=${tolerance}
|
||||
-D EXTRACT_RANGE=${ARGV4}
|
||||
-P ${CMAKE_SOURCE_DIR}/test.cmake)
|
||||
elseif (${ARGC} EQUAL 6)
|
||||
if (${ARGV4} STREQUAL "NOEXTRACT_RANGE")
|
||||
add_test(${test_name}
|
||||
${CMAKE_COMMAND}
|
||||
-D LIBRARY_FOLDER=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
|
||||
-D TESTS_DIRECTORY=${CMAKE_SOURCE_DIR}/tests
|
||||
-D TEST_FILENAME=${test_filename}
|
||||
-D CALLS=${calls_count}
|
||||
-D TOLERANCE_GLES1=${tolerance}
|
||||
-D TOLERANCE_GLES2=${ARGV5}
|
||||
-P ${CMAKE_SOURCE_DIR}/test.cmake)
|
||||
else (${ARGV4} STREQUAL "NOEXTRACT_RANGE")
|
||||
add_test(${test_name}
|
||||
${CMAKE_COMMAND}
|
||||
-D LIBRARY_FOLDER=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
|
||||
-D TESTS_DIRECTORY=${CMAKE_SOURCE_DIR}/tests
|
||||
-D TEST_FILENAME=${test_filename}
|
||||
-D CALLS=${calls_count}
|
||||
-D TOLERANCE_GLES1=${tolerance}
|
||||
-D TOLERANCE_GLES2=${ARGV5}
|
||||
-D EXTRACT_RANGE=${ARGV4}
|
||||
-P ${CMAKE_SOURCE_DIR}/test.cmake)
|
||||
endif (${ARGV4} STREQUAL "NOEXTRACT_RANGE")
|
||||
else (${ARGC} EQUAL 5)
|
||||
add_test(${test_name}
|
||||
${CMAKE_COMMAND}
|
||||
-D LIBRARY_FOLDER=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
|
||||
-D TESTS_DIRECTORY=${CMAKE_SOURCE_DIR}/tests
|
||||
-D TEST_FILENAME=${test_filename}
|
||||
-D CALLS=${calls_count}
|
||||
-D TOLERANCE=${tolerance}
|
||||
-P ${CMAKE_SOURCE_DIR}/test.cmake)
|
||||
endif (${ARGC} EQUAL 5)
|
||||
endmacro(create_test)
|
||||
|
||||
macro(create_test_GLES test_name GLES test_filename calls_count tolerance)
|
||||
if (${ARGC} EQUAL 5)
|
||||
add_test(${test_name}_GLES${GLES}
|
||||
${CMAKE_COMMAND}
|
||||
-D LIBRARY_FOLDER=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
|
||||
-D TESTS_DIRECTORY=${CMAKE_SOURCE_DIR}/tests
|
||||
-D TEST_FILENAME=${test_filename}
|
||||
-D CALLS=${calls_count}
|
||||
-D TOLERANCE=${tolerance}
|
||||
-D EXTRACT_RANGE=${ARGV4}
|
||||
-D GLES_FORCED=${GLES}
|
||||
-P ${CMAKE_SOURCE_DIR}/test.cmake)
|
||||
else (${ARGC} EQUAL 5)
|
||||
add_test(${test_name}_GLES${GLES}
|
||||
${CMAKE_COMMAND}
|
||||
-D LIBRARY_FOLDER=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
|
||||
-D TESTS_DIRECTORY=${CMAKE_SOURCE_DIR}/tests
|
||||
-D TEST_FILENAME=${test_filename}
|
||||
-D CALLS=${calls_count}
|
||||
-D TOLERANCE=${tolerance}
|
||||
-D GLES_FORCED=${GLES}
|
||||
-P ${CMAKE_SOURCE_DIR}/test.cmake)
|
||||
endif (${ARGC} EQUAL 5)
|
||||
endmacro(create_test_GLES)
|
||||
|
||||
create_test(GLXgears glxgears "0000008203" 25 "NOEXTRACT_RANGE" 700)
|
||||
create_test(StuntCarRacer stuntcarracer "0000118817" 20 "638x478+1+1")
|
||||
create_test(Neverball neverball "0000078750" 20 "798x478+1+1" 200)
|
||||
create_test(FoobillardPlus foobillardplus "0000014748" 50 "798x478+1+1" 60)
|
||||
create_test(Descent3 descent3 "0000284192" 20 "638x478+1+1")
|
||||
create_test(PointSprite pointsprite "0000248810" 20)
|
||||
|
||||
#create_test_GLES(Neverball 2 neverball "0000078750" 200 "798x478+1+1")
|
||||
create_test_GLES(OpenRA 2 openra "0000031249" 20 "638x478+1+1")
|
||||
create_test_GLES(GLSL_lighting 2 glsl_lighting "0000505393" 20)
|
||||
@@ -0,0 +1,133 @@
|
||||
Compiling
|
||||
====
|
||||
It's better to define the CMake Build type, preferably `RelWithDebInfo`, that define a Release build (so with some optimizations) but with debug info (so gdb Backtrace shows infos).
|
||||
|
||||
*Pandora*
|
||||
---
|
||||
|
||||
`mkdir build; cd build; cmake .. -DPANDORA=1 -DCMAKE_BUILD_TYPE=RelWithDebInfo; make`
|
||||
|
||||
*Raspberry Pi*
|
||||
---
|
||||
|
||||
If your are using legacy driver (non-mesa ones)
|
||||
|
||||
`mkdir build; cd build; cmake .. -DBCMHOST=1 -DCMAKE_BUILD_TYPE=RelWithDebInfo; make`
|
||||
|
||||
For Mesa VC4 driver, use the ODROID profile.
|
||||
|
||||
*ODroid*
|
||||
---
|
||||
|
||||
`mkdir build; cd build; cmake .. -DODROID=1 -DCMAKE_BUILD_TYPE=RelWithDebInfo; make`
|
||||
|
||||
*Tinker Board 1/1S or RK3288*
|
||||
---
|
||||
|
||||
use ODROID profile.
|
||||
|
||||
*OrangePI*
|
||||
---
|
||||
|
||||
use ODROID profile.
|
||||
|
||||
*CHIP machines*
|
||||
---
|
||||
|
||||
`mkdir build; cd build; cmake .. -DCHIP=1 -DCMAKE_BUILD_TYPE=RelWithDebInfo; make`
|
||||
|
||||
*Emscripten*
|
||||
---
|
||||
|
||||
`mkdir build; cd build; emcmake cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo -DNOX11=ON -DNOEGL=ON -DSTATICLIB=ON; make`
|
||||
|
||||
*Android*
|
||||
---
|
||||
|
||||
An Android.mk is provided that should compile with an NDK
|
||||
|
||||
*iOS*
|
||||
---
|
||||
|
||||
```
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -G Xcode -DCMAKE_TOOLCHAIN_FILE={where ios.toolchain.cmake is located} -DNOX11=ON -DNOEGL=ON -DSTATICLIB=ON -DPLATFORM=OS
|
||||
cmake --build . --config Release --target GL
|
||||
```
|
||||
|
||||
Use with [ios-cmake](https://github.com/leetal/ios-cmake)
|
||||
|
||||
*Custom build*
|
||||
---
|
||||
|
||||
Alternatively, you can use the curses-bases ccmake (or any other gui frontend for cmake) to select wich platform to use interactively.
|
||||
|
||||
You can avoid the use of X11 with `NOX11=1` and EGL with `NOEGL=1`, but then, you will need to create the EGL Context yourself (or using SDL for example). Be sure to synchronize the context you create with the Backend you use. By default GLES 1.1 backend is used. To used GLES2 by default, use `DEFAULT_ES=2`
|
||||
|
||||
You can use USE_CLOCK to use `clock_gettime(...)` instead of `gettimeofday(...)` for LIBGL_FPS. It can be more precise on some platform. Add `-DUSE_CLOCK=ON`
|
||||
|
||||
You can use cmake and mix command line argument to custom build your version of GL4ES. For example, for a generic ARM with NEON platform, not using X11 or EGL, defaulting to GLES2 backend, enabling RPi, and using clock_gettime, you would do:
|
||||
|
||||
`mkdir build; cd build; cmake .. -DBCMHOST=1 -DNOEGL=1 -DNOX11=1 -DDEFAULT_ES=2 -DUSE_CLOCK=ON -DCMAKE_C_FLAGS="-marm -mcpu=cortex-a9 -mfpu=neon -mfloat-abi=hard" -DCMAKE_BUILD_TYPE=RelWithDebInfo; make`
|
||||
|
||||
----
|
||||
|
||||
Testing
|
||||
====
|
||||
A few tests are included.
|
||||
They can be launched with `tests/tests.sh`
|
||||
You will need apitrace and imagemagick for them to run. (on debian and friend, it's `sudo apt install apitrace-gl-frontend imagemagick`)
|
||||
The tests use a pre-recorded GL trace that is replayed, then a specific frame is captured and compared to a reference picture.
|
||||
Because each renderer may render slightly differently, there are some fuzz in the comparison, so only significant changes will be detected.
|
||||
For now, 2 tests are done, one with glxgears (basic testing, using mostly glBegin / glEnd) and stuntcarracer (with more GL stuff, textures and lighting).
|
||||
|
||||
----
|
||||
|
||||
Per-platform
|
||||
====
|
||||
|
||||
## OpenPandora
|
||||
|
||||
This is the main developpement platform for now. GL4ES works on all 3 models (CC, Rebirth and Gigahertz), and with all driver version.
|
||||
For the SGX Driver version that doesn't support X11 Windowed mode (i.e. there is no `/usr/lib/libpvrPVR2D_X11WSEGL.so` library in the firmware), you need to use one of the framebuffer output: `LIBGL_FB=1` is the fastest, but you may need `LIBGL_FB=2` if you need 32bits framebuffer, or `LIBGL_FB=3` if you need GL in a Window (but it will be slow, as each frame has to be blitted back in the X11 windows).
|
||||
On the Pandora, the special `LIBGL_GAMMA` can be used also, to boost gamma at load, using firmware command to change LCD gamma.
|
||||
|
||||
## ODroid
|
||||
|
||||
GL4ES works well on ODroid. I can test on XU4 model, but it has been reported to work on all model, including 64bits version.
|
||||
On ODroid, the EGL context can be created with SRGB attribute, by using `LIBGL_SRGB=1`, for a nice boost in the gamma (supported on most ODroid).
|
||||
|
||||
## OrangePI
|
||||
GL4ES works on OrangePI using ODROID profile. GLES Hardware is MALI and is similar to ODroid in many way. I don't own any OrangePI but I have seen many success in compiling and using the lib.
|
||||
|
||||
## C.H.I.P.
|
||||
|
||||
GL4ES should work on CHIP and PocketCHIP. Framebuffers mode will probably not work, with only `LIBGL_FB=3` mode that seems to work fine, and with adequate performances (there is probably no slow blit, as the driver handle directly the GL->X11 blit).
|
||||
Also, on the CHIP, you will probably need to do `sudo apt-get install chip-mali-userspace` to be sure the GLES driver of the CHIP is present.
|
||||
|
||||
## RaspberryPI
|
||||
|
||||
GL4ES works on RaspberryPI, on both legacy driver and mesa opensourced ones.
|
||||
For Legacy driver, the Framebuffer mode should work on this plateform. It seems `LIBGL_FB=3` gives good result and should be used in most cases. `LIBGL_FB=1` try to use `DispManX` for more speed. Be aware that if you use X-less config, many SDL version dedicated to DispManX do not handle GL context creation. So trying to create a GL context with this version of SDL will fail, but GL4ES is never called, so I cannot "fix" that (the fix would be inside SDL/DispManX driver).
|
||||
|
||||
## Android
|
||||
On Android build of GL4ES, no X11 function are called. So most `glX` function are not defined. That means that the GL context (in fact a GLES1.1 context) has to be created by other apps (mainly SDL or SDL2).
|
||||
|
||||
On Android version 4.0 and earlier, there is a bug that prevent dlopen to be called inside dlopen (see [here](http://grokbase.com/t/gg/android-ndk/124bdvscqx/block-with-calling-dlopen-and-dlclose)).
|
||||
GL4ES use a "library constructor" to initialize itself a load, and that constructor do some dlopen (to load EGL and GLES libraries), so it will trigger that bug.
|
||||
If you are targeting a wide range of device, you should probably activate the workaround:
|
||||
1. Modify [Android.mk](Android.mk) to uncomment `#LOCAL_CFLAGS += -DNO_INIT_CONSTRUCTOR` near the end of the file, to prevent the use of the library constructor.
|
||||
2. In your code, call `void initialize_gl4es()` as soon as possible after loading GL4ES, and before using any GL function.
|
||||
|
||||
To try the GLES2 backend, you can compile gl4es with ES2 by default (so you don't have to mess with env. variable). Simply uncomment `#LOCAL_CFLAGS += -DDEFAULT_ES=2`, and create the GL Context as GLES2.
|
||||
|
||||
## Emscripten
|
||||
|
||||
In your code, call `void initialize_gl4es()` as soon as possible after loading GL4ES, and before using any GL function.
|
||||
|
||||
Use `-s FULL_ES2=1 -I[directory where GL4ES source code is located]/gl4es/include -lGL` when compiling your program for Emscripten.
|
||||
|
||||
## iOS
|
||||
|
||||
In your code, call `void initialize_gl4es()` as soon as possible after loading GLES context, and before using any GL function. If you use EAGLContext, call it immediately after `[EAGLContext setCurrentContext:context]`.
|
||||
@@ -0,0 +1,390 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||
<CodeBlocks_project_file>
|
||||
<FileVersion major="1" minor="6" />
|
||||
<Project>
|
||||
<Option title="GL4ES" />
|
||||
<Option pch_mode="0" />
|
||||
<Option compiler="gcc" />
|
||||
<Build>
|
||||
<Target title="Linux Static">
|
||||
<Option output="./libGL4ES.a" prefix_auto="0" extension_auto="0" />
|
||||
<Option working_dir="" />
|
||||
<Option type="2" />
|
||||
<Option compiler="gcc" />
|
||||
<Option projectResourceIncludeDirsRelation="1" />
|
||||
</Target>
|
||||
<Target title="Linux Dynamic">
|
||||
<Option output="./libGL.so" prefix_auto="0" extension_auto="0" />
|
||||
<Option type="3" />
|
||||
<Option compiler="gcc" />
|
||||
<Option projectResourceIncludeDirsRelation="1" />
|
||||
</Target>
|
||||
<Target title="Amiga Dynamic">
|
||||
<Option output="./libGL.so" prefix_auto="0" extension_auto="0" />
|
||||
<Option type="3" />
|
||||
<Option compiler="gcc" />
|
||||
<Option projectResourceIncludeDirsRelation="1" />
|
||||
</Target>
|
||||
</Build>
|
||||
<VirtualTargets>
|
||||
<Add alias="All" targets="Linux Static;" />
|
||||
</VirtualTargets>
|
||||
<Compiler>
|
||||
<Add option="-DNO_GBM" />
|
||||
<Add directory="/home/Dev/libs/game/Irrlicht-SVN/include" />
|
||||
<Add directory="include" />
|
||||
</Compiler>
|
||||
<Linker>
|
||||
<Add directory="/home/Dev/libs/game/Irrlicht-SVN/lib/Linux" />
|
||||
</Linker>
|
||||
<Unit filename="include/EGL/egl.h" />
|
||||
<Unit filename="include/EGL/eglext.h" />
|
||||
<Unit filename="include/EGL/eglplatform.h" />
|
||||
<Unit filename="include/GL/gl.h" />
|
||||
<Unit filename="include/GL/gl_mangle.h" />
|
||||
<Unit filename="include/GL/glext.h" />
|
||||
<Unit filename="include/GL/glu.h" />
|
||||
<Unit filename="include/GL/glu_mangle.h" />
|
||||
<Unit filename="include/GL/glx.h" />
|
||||
<Unit filename="include/GL/glx_mangle.h" />
|
||||
<Unit filename="include/GL/glxext.h" />
|
||||
<Unit filename="include/GL/internal/dri_interface.h" />
|
||||
<Unit filename="include/GLES/egl.h" />
|
||||
<Unit filename="include/GLES/gl.h" />
|
||||
<Unit filename="include/GLES/gl2.h" />
|
||||
<Unit filename="include/GLES/gl2ext.h" />
|
||||
<Unit filename="include/GLES/gl2platform.h" />
|
||||
<Unit filename="include/GLES/gl3.h" />
|
||||
<Unit filename="include/GLES/gl3ext.h" />
|
||||
<Unit filename="include/GLES/gl3platform.h" />
|
||||
<Unit filename="include/GLES/glext.h" />
|
||||
<Unit filename="include/GLES/glplatform.h" />
|
||||
<Unit filename="include/KHR/khrplatform.h" />
|
||||
<Unit filename="include/android_debug.h" />
|
||||
<Unit filename="include/gl4eshint.h" />
|
||||
<Unit filename="include/gl4esinit.h" />
|
||||
<Unit filename="include/khash.h" />
|
||||
<Unit filename="src/agl/agl.c">
|
||||
<Option compilerVar="CC" />
|
||||
<Option target="Amiga Dynamic" />
|
||||
</Unit>
|
||||
<Unit filename="src/agl/agl.h">
|
||||
<Option target="Amiga Dynamic" />
|
||||
</Unit>
|
||||
<Unit filename="src/agl/amigaos.c">
|
||||
<Option compilerVar="CC" />
|
||||
<Option target="Amiga Dynamic" />
|
||||
</Unit>
|
||||
<Unit filename="src/agl/amigaos.h">
|
||||
<Option target="Amiga Dynamic" />
|
||||
</Unit>
|
||||
<Unit filename="src/agl/lookup.c">
|
||||
<Option compilerVar="CC" />
|
||||
<Option target="Amiga Dynamic" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/arbconverter.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/arbconverter.h" />
|
||||
<Unit filename="src/gl/arbgenerator.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/arbgenerator.h" />
|
||||
<Unit filename="src/gl/arbhelper.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/arbhelper.h" />
|
||||
<Unit filename="src/gl/arbparser.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/arbparser.h" />
|
||||
<Unit filename="src/gl/array.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/array.h" />
|
||||
<Unit filename="src/gl/attributes.h" />
|
||||
<Unit filename="src/gl/blend.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/blend.h" />
|
||||
<Unit filename="src/gl/blit.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/blit.h" />
|
||||
<Unit filename="src/gl/buffers.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/buffers.h" />
|
||||
<Unit filename="src/gl/build_info.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/build_info.h" />
|
||||
<Unit filename="src/gl/const.h" />
|
||||
<Unit filename="src/gl/debug.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/debug.h" />
|
||||
<Unit filename="src/gl/decompress.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/decompress.h" />
|
||||
<Unit filename="src/gl/depth.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/depth.h" />
|
||||
<Unit filename="src/gl/directstate.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/directstate.h" />
|
||||
<Unit filename="src/gl/drawing.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/enable.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/enum_info.h" />
|
||||
<Unit filename="src/gl/envvars.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/envvars.h" />
|
||||
<Unit filename="src/gl/eval.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/eval.h" />
|
||||
<Unit filename="src/gl/face.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/face.h" />
|
||||
<Unit filename="src/gl/fog.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/fog.h" />
|
||||
<Unit filename="src/gl/fpe.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/fpe.h" />
|
||||
<Unit filename="src/gl/fpe_cache.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/fpe_cache.h" />
|
||||
<Unit filename="src/gl/fpe_shader.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/fpe_shader.h" />
|
||||
<Unit filename="src/gl/framebuffers.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/framebuffers.h" />
|
||||
<Unit filename="src/gl/getter.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/gl4es.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/gl4es.h" />
|
||||
<Unit filename="src/gl/gl_lookup.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/gl_lookup.h" />
|
||||
<Unit filename="src/gl/glcase.h" />
|
||||
<Unit filename="src/gl/gles.h" />
|
||||
<Unit filename="src/gl/glesfuncs.inc" />
|
||||
<Unit filename="src/gl/glstate.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/glstate.h" />
|
||||
<Unit filename="src/gl/hint.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/hint.h" />
|
||||
<Unit filename="src/gl/init.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/init.h" />
|
||||
<Unit filename="src/gl/light.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/light.h" />
|
||||
<Unit filename="src/gl/line.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/line.h" />
|
||||
<Unit filename="src/gl/list.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/list.h" />
|
||||
<Unit filename="src/gl/listdraw.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/listrl.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/loader.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/loader.h" />
|
||||
<Unit filename="src/gl/logs.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/logs.h" />
|
||||
<Unit filename="src/gl/math/eval.h" />
|
||||
<Unit filename="src/gl/math/matheval.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/matrix.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/matrix.h" />
|
||||
<Unit filename="src/gl/matvec.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/matvec.h" />
|
||||
<Unit filename="src/gl/oldprogram.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/oldprogram.h" />
|
||||
<Unit filename="src/gl/pixel.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/pixel.h" />
|
||||
<Unit filename="src/gl/planes.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/planes.h" />
|
||||
<Unit filename="src/gl/pointsprite.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/pointsprite.h" />
|
||||
<Unit filename="src/gl/preproc.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/preproc.h" />
|
||||
<Unit filename="src/gl/program.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/program.h" />
|
||||
<Unit filename="src/gl/queries.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/queries.h" />
|
||||
<Unit filename="src/gl/raster.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/raster.h" />
|
||||
<Unit filename="src/gl/render.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/render.h" />
|
||||
<Unit filename="src/gl/samplers.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/samplers.h" />
|
||||
<Unit filename="src/gl/shader.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/shader.h" />
|
||||
<Unit filename="src/gl/shader_hacks.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/shader_hacks.h" />
|
||||
<Unit filename="src/gl/shaderconv.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/shaderconv.h" />
|
||||
<Unit filename="src/gl/stack.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/stack.h" />
|
||||
<Unit filename="src/gl/state.h" />
|
||||
<Unit filename="src/gl/stb_dxt_104.h" />
|
||||
<Unit filename="src/gl/stencil.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/stencil.h" />
|
||||
<Unit filename="src/gl/string_utils.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/string_utils.h" />
|
||||
<Unit filename="src/gl/stubs.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/texenv.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/texenv.h" />
|
||||
<Unit filename="src/gl/texgen.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/texgen.h" />
|
||||
<Unit filename="src/gl/texture.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/texture.h" />
|
||||
<Unit filename="src/gl/texture_3d.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/texture_compressed.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/texture_params.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/texture_read.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/uniform.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/uniform.h" />
|
||||
<Unit filename="src/gl/vertexattrib.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/vertexattrib.h" />
|
||||
<Unit filename="src/gl/wrap/gl4es.h" />
|
||||
<Unit filename="src/gl/wrap/gl4eswraps.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/wrap/gles.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/wrap/gles.h" />
|
||||
<Unit filename="src/gl/wrap/glstub.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/gl/wrap/skips.h" />
|
||||
<Unit filename="src/gl/wrap/stub.h" />
|
||||
<Unit filename="src/glx/drmfunc.h" />
|
||||
<Unit filename="src/glx/gbm.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/glx/gbmfunc.h" />
|
||||
<Unit filename="src/glx/glx.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/glx/glx.h" />
|
||||
<Unit filename="src/glx/glx_gbm.h" />
|
||||
<Unit filename="src/glx/glx_stubs.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/glx/hardext.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/glx/hardext.h" />
|
||||
<Unit filename="src/glx/lookup.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/glx/rpi.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/glx/rpi.h" />
|
||||
<Unit filename="src/glx/streaming.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/glx/streaming.h" />
|
||||
<Unit filename="src/glx/utils.c">
|
||||
<Option compilerVar="CC" />
|
||||
</Unit>
|
||||
<Unit filename="src/glx/utils.h" />
|
||||
<Extensions />
|
||||
</Project>
|
||||
</CodeBlocks_project_file>
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2016-2018 Sebastien Chevalier
|
||||
Copyright (c) 2013-2016 Ryan Hileman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,61 @@
|
||||
GL4ES - OpenGL for GLES Hardware
|
||||
====
|
||||
|
||||
Many OpenGL software already works with GL4ES.
|
||||
|
||||

|
||||
|
||||
Complex OpenGL 1.5 games, like Foobillard++, that uses cascaded display list, line stipple and TexGen works fine. Here running on the OpenPandora. It will work with both GLES1.1 and GLES2.0 backend. Most OpenGL 1.x games will runs.
|
||||
|
||||
|
||||
[](https://www.youtube.com/watch?v=75FYb60L7zw)
|
||||
|
||||
The limit is an hardware one: GLES1.1 and GLES2.0 hardware do not allow reading Depth buffer, so some games that use it (like AssaultCube) for game play wont run correctly. Some FPS are using it just for cosmetics (Lens flare) and run fine without, like Serious Sam (both First and Second Encounter), here on the OpenPandora again.
|
||||
|
||||

|
||||
AssaultCube an Android also use gl4es for the rendering
|
||||
|
||||
|
||||
[](https://www.youtube.com/watch?v=kJPb2jYiBoM)
|
||||
|
||||
More complex program, like Blender, can also run.
|
||||
|
||||
|
||||

|
||||
|
||||
But also Minecraft, here on an ODroid in this old video.
|
||||
|
||||
|
||||

|
||||
|
||||
On the OpenGL 2.0, side using GLES2.0 backend, OpenRA can run on the Pandora.
|
||||
|
||||
|
||||
And OpenGL 2.x opens a lot of doors. Many commercial games run just fine on gl4es.
|
||||
|
||||
[](https://www.youtube.com/watch?v=VUoeHWuwlMU)
|
||||
|
||||
Here some FNA games, running on an ODroid XU4 (using method described [there](https://magazine.odroid.com/article/playing-modern-fna-games-on-the-odroid-platform/)). That video show just a few, and even more can be make to run.
|
||||
|
||||
|
||||
|
||||
[](https://www.youtube.com/watch?v=B4YN37z3-ws)
|
||||
|
||||
And with [Box86](https://github.com/ptitSeb/box86), even more commercial games can run, like here NeverWinter Night on an ODroid XU4
|
||||
|
||||
GL4ES also now works on some other platform, like AmigaOS4 (so BigEndian platform)
|
||||
[](https://www.youtube.com/watch?v=hQVabA_ReoQ)
|
||||
|
||||
Here running Foobillard++
|
||||
|
||||
Or even in a web browser (that support WebGL): try it with Stunt Car Remake [here](http://ptitseb.github.io/stuntcarremake/)
|
||||
|
||||
Another web browser example: Serious Sam Engine, try it [here](https://martinmullins.github.io/ssam/) (initial 80MB game files download).
|
||||
[source repo](https://github.com/martinmullins/Serious-Engine)
|
||||
[](https://martinmullins.github.io/ssam/)
|
||||
|
||||
Neverball in a browser also use gl4es for rendering: [neverball in a browser](https://neverball.github.io/)
|
||||
|
||||
About performances: while gl4es is a wrapper, there are many caching and works done in gl4es to avoid slowdown and bottleneck. While those optimization are useless in highly optimized engine, like the Quake3 engine, on other engine and games, gl4es can be (much) faster than straight GLES conversion (especially if there are a lot of `glBegin(...)`/`glEnd()` drawing commands). On an x86 VM, gl4es running on GLES can be faster than direct OpenGL use on some games!
|
||||
Now GL4ES can also use VBO, either when the software use it, or automatically when using glList, giving sometimes a nice boost (the boost depend on the Platform and game)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||

|
||||
|
||||
GL4ES - OpenGL for GLES Hardware
|
||||
====
|
||||
|
||||

|
||||
|
||||
This is a library provide OpenGL 2.x functionality for GLES2.0 accelerated Hardware (and of course also support OpenGL 1.5 function, sometimes better than when using GLES 1.1 backend)
|
||||
There is also support for GLES 1.1 Hardware, emulating OpenGL 1.5, and some OpenGL 2.x+ extensions.
|
||||
|
||||
GL4ES is known to work on many platform: OpenPandora, ODroid, RaspberryPI (2 and 3 at least), PocketCHIP, "otherfruit"PI (like the OrangePI), Android, iOS, x86 and x86_64 Linux (tested using mesa-egl). There is also some WIP support for AmigaOS4, using experimental GLES2 driver for Warp3D.
|
||||
|
||||
This library is based on glshim (https://github.com/lunixbochs/glshim) but as now evolved far from it, with different feature set and objectives. Go check this lib if you need things like RemoteGL or TinyGLES (for software rendering).
|
||||
|
||||
The focus is on compatibility and speed with a wide selection of game and software.
|
||||
|
||||
It has been tested successfully of a large selection of games and software, including: Minecraft, OpenMW, SeriousSam (both First and Second Encounters), RVGL (ReVolt GL), TSMC (The Secret Maryo Chronicles), TORCS, SpeedDreams, GL-117, Foobillard(plus), half life 1&2, Blender 2.68 to name just a few. I have also some success with Linux port of XNA games, using either MonoGame or FNA.
|
||||
|
||||
Most function of OpenGL up to 1.5 are supported, with some notable exceptions:
|
||||
* Reading of Depth or Stencil buffer will not work
|
||||
* GL_FEEDBACK mode is not implemented
|
||||
* No Accum emulation
|
||||
|
||||
Some known general limitations:
|
||||
* GL_SELECT as some limitation in its implementation (for example, current Depth buffer or bounded texture are not taken into account, also custom vertex shader will not work here)
|
||||
* NPOT texture are supported, but not with GL_REPEAT / GL_MIRRORED, only GL_CLAMP will work properly (unless the GLES Hardware support NPOT)
|
||||
* Multiple Color attachment on Framebuffer are not supported
|
||||
* OcclusionQuery is implemented, but with a 0 bits precision
|
||||
* Probably many other things
|
||||
|
||||
Status of the GLES2 backend
|
||||
* The FPE (Fixed Pipeline Emulator) has most OpenGL 1.5 drawing call implemented
|
||||
* The Shader Conversion is really crude, so only simple shaders will work (especially, the implicit conversion float <-> int is not handled)
|
||||
* ARB_program are supported (converted on-the-fly to glsl shaders)
|
||||
* Lighting support double-side and color separation
|
||||
* FogCoord are supported, along with secondary color
|
||||
* An ES2 context should be usable (useful for SDL2)
|
||||
* OpenGL 2.x games that have been tested include: OpenRA, GZDoom, Danger from the Deep, SuperTuxKart 0.8.1, Hammerwatch, OpenMW, half life 2, many FNA & MonoGames games (FEZ, Towerfall Ascension, Stardew Valley, Dust, Owlboy, and many other), even some Unity3D games (Teslagrad, Colin McRea Rally remake and other)...
|
||||
* glxgears works, but FlatShade is not implemented (and will probably never be), so it's slightly different than using GLES1.1 or actual GL hardware
|
||||
* GL_TEXTURE_1D, GL_TEXTURE_3D and GL_TEXTURE_RECTANGLE_ARB are not yet supported in shaders (they are supported in fixed pipeline functions), and texture 3D are just a single 2D layer for now.
|
||||
* Program that link only a GL_FRAGMENT or GL_VERTEX shader are not supported yet.
|
||||
* Some VBO are used.
|
||||
|
||||
Status of the GLES1.1 backend
|
||||
* Framebuffer use FRAMEBUFFER_OES extension (that must be present in the GLES 1.1 stack)
|
||||
* Lighting doesn't support double-side or color separation
|
||||
* FogCoord or Secondary colors are not supported
|
||||
* GL_TEXTURE_3D are just a single 2D layer (the 1st layer).
|
||||
* VBO are supported, but they are emulated, even if VBO if supported in GLES1.1 driver
|
||||
|
||||
If you use gl4es in your project (as a static or dynamic link), please mention gl4es in you readme / about / whatever.
|
||||
|
||||
----
|
||||
|
||||
Compiling
|
||||
----
|
||||
How to compile and per-platform specific comment can be found [here](COMPILE.md)
|
||||
|
||||
----
|
||||
|
||||
GLU
|
||||
----
|
||||
|
||||
Standard GLU do works without any issues. You can find a version [here](https://github.com/ptitSeb/GLU) if you need one.
|
||||
|
||||
----
|
||||
|
||||
Installation
|
||||
----
|
||||
|
||||
Put lib/libGL.so.1 in your `LD_LIBRARY_PATH`.
|
||||
Beware that GL4ES is meant to replace any libGL you can have on your system (like Mesa for example)
|
||||
|
||||
----
|
||||
|
||||
Usage
|
||||
----
|
||||
|
||||
There are many environment variable to control gl4es behavior, also usable at runtime using `glHint(...)`.
|
||||
|
||||
See [here](USAGE.md) for all variables and what they do.
|
||||
|
||||
----
|
||||
|
||||
Media (what is working already)
|
||||
----
|
||||
|
||||
Some screenshot and youtube links of stuffs that works [here](MEDIA.md)
|
||||
|
||||
----
|
||||
|
||||
Version history
|
||||
----
|
||||
|
||||
The change log is [here](CHANGELOG.md)
|
||||
@@ -0,0 +1,387 @@
|
||||
Usage
|
||||
----
|
||||
|
||||
There are many environment variables to control gl4es behavior. All are numeric, except LIBGL_VERSION that take a string, LIBGL_FBO that takes a 2d size (WxH), and both LIBGL_EGL and LIBGL_GLES that take path/filename.
|
||||
You can also change many of this variable at runtime using the `glHint(...)` function. See [gl4eshint.h](include/gl4eshint.h) for the list of #define to use in this function.
|
||||
|
||||
##### LIBGL_FB
|
||||
Controls the Framebuffer output
|
||||
* 0 : Default, using standard x11 rendering
|
||||
* 1 : Use Framebuffer output (x11 bypassed, only fullscreen)
|
||||
* 2 : Use Framebuffer, but also an intermediary FBO
|
||||
* 3 : Use PBuffer, allowing x11 (and windowed) rendering even if driver doesn't support it
|
||||
|
||||
##### LIBGL_FBONOALPHA
|
||||
In case of LIBGL_FB=2, control if FBO is RGBA or RGB
|
||||
* 0 : Default, use RGBA
|
||||
* 1 : Use RGB for FBO
|
||||
|
||||
##### LIBGL_ES
|
||||
Controls the version of GLES to use
|
||||
* 0 : Default, using GLES 2.0 backend (unless built with DEFAULT_ES 1) (not on Pandora, still GLES 1.1 backend by default)
|
||||
* 1 : Use GLES 1.1 backend
|
||||
* 2 : Use GLES 2.0 backend
|
||||
|
||||
##### LIBGL_GL
|
||||
Controls the version of OpenGL exposed
|
||||
* 0 : Default, expose OpenGL 1.5 when using GLES1.1 or OpenGL 2.1 when using GLES2.0
|
||||
* 10..14: Export OpenGL 1.0-1.4
|
||||
* 15: Expose OpenGL 1.5 (default for GLES 1.1 backend)
|
||||
* 20: Expose OpenGL 2.0
|
||||
* 21: Expose OpenGL 2.1 (default for GLES 2.0 backend)
|
||||
|
||||
##### LIBGL_XREFRESH
|
||||
Debug helper in specific cases
|
||||
* 0 : Default, nothing special
|
||||
* 1 : xrefresh will be called on cleanup
|
||||
|
||||
##### LIBGL_STACKTRACE
|
||||
Automatic Backtrace log
|
||||
* 0 : Default, nothing special
|
||||
* 1 : stacktrace will be printed on crash
|
||||
|
||||
##### LIBGL_FPS
|
||||
Print current FPS to the console
|
||||
* 0 : Defaut, don't measure or printf FPS
|
||||
* 1 : Print FPS (on stdout) every second
|
||||
|
||||
##### LIBGL_VSYNC
|
||||
VSync control
|
||||
* 0 : Default, nothing special
|
||||
* 1 : vsync enabled
|
||||
|
||||
##### LIBGL_RECYCLEFBO
|
||||
Recycling FBO special case (don't delete a created FBO, but recycle it if needed)
|
||||
* 0 : Default, nothing special
|
||||
* 1 : Recycling of FBO enabled
|
||||
|
||||
##### LIBGL_MIPMAP
|
||||
Handling of Manual and Automatic MIPMAP
|
||||
* 0 : Default, nothing special
|
||||
* 1 : AutoMipMap forced
|
||||
* 2 : guess AutoMipMap (based on manual mipmaping on textures)
|
||||
* 3 : ignore MipMap (mipmap creation / use entirely disabled)
|
||||
* 4 : ignore AutoMipMap on non-squared textures
|
||||
* 5 : calculate all sub-mipmap one time when uploading level 1
|
||||
|
||||
##### LIBGL_FORCENPOT
|
||||
Forcing NPOT (Non-Power of Two) Texture size.
|
||||
* 0 : Default, nothing special
|
||||
* 1 : If hardware only support Limited NPOT, then disabling MIPMAP (i.e. LIBGL_MIPMAP=3), so all texture can be NPOT.
|
||||
If Hardware support full NPOT, do nothing special. Useful for GLES2 backend where limited NPOT is always supported.
|
||||
|
||||
##### LIBGL_TEXCOPY
|
||||
Make a local copy of every texture for easy glGetTexImage2D
|
||||
* 0 : Default, nothing special
|
||||
* 1 : Texture copy enabled
|
||||
|
||||
##### LIBGL_SHRINK
|
||||
Texture shrinking control
|
||||
* 0 : Default, nothing special
|
||||
* 1 : everything / 2 (using original algorithm for size reduction, all other shrink mode use a refined algorithm)
|
||||
* 2 : only textures which one size > 512 are / 2
|
||||
* 3 : only textures which one size > 256 are / 2
|
||||
* 4 : only textures which one size > 256 are / 2, and the one > 1024 are / 4
|
||||
* 5 : only textures which one size > 256 are resized to 256 (if possible, because only /2 and /4 exists), but empty texture are not shrunken
|
||||
* 6 : only textures which one size > 128 are / 2, those >= 512 are resized to 256 (if possible, because only /2 and /4 exists), but empty texture are not shrunken
|
||||
* 7 : only textures which one size > 512 are / 2, but empty texture are not shrunken
|
||||
* 8 : advertise a max texture size *4, but every texture which one size > 2048 are shrunken to 2048
|
||||
* 9 : advertise a max texture size *4, but every texture which one size > 4096 are / 4 and the one > 512 are / 2, but empty texture are not shrunken
|
||||
* 10: advertise a max texture size *4, but every texture which one size > 2048 are / 4 and the one > 512 are / 2, but empty texture are not shrunken
|
||||
* 11: advertise a max texture size *2, but every texture with one dimension > max texture size will get shrunken to max texture size
|
||||
|
||||
##### LIBGL_TEXDUMP
|
||||
Texture dump
|
||||
* 0 : Default, nothing special
|
||||
* 1 : Texture dump enabled
|
||||
|
||||
##### LIBGL_ALPHAHACK
|
||||
Experimental: enable Alpha test only when using texture that contains an alpha channel
|
||||
* 0 : Default, nothing special
|
||||
* 1 : Alpha Hack enabled
|
||||
|
||||
##### LIBGL_NODOWNSAMPLING
|
||||
Texture downsampling control (deprecated, use LIBGL_AVOID16BITS instead)
|
||||
* 0 : Default, DXTc texture are downsampled to 16bits
|
||||
* 1 : DXTc texture are left as 32bits RGBA
|
||||
|
||||
##### LIBGL_STREAM
|
||||
PANDORA only: enable Texture Streaming (works only on RGB textures)
|
||||
* 0 : Default, nothing special
|
||||
* 1 : Enabled on empty RGB textures
|
||||
* 2 : Enabled on all RGB textures
|
||||
|
||||
##### LIBGL_COPY
|
||||
Removed (Controlled the glCopyTex(Sub)Image2D hack, it's now automatic, depending on how compatible is the read framebuffer)
|
||||
|
||||
##### LIBGL_NOLUMALPHA
|
||||
Control the availability of the LUMINANCE_ALPHA format (can be buggy on Pandora model CC)
|
||||
* 0 : Default,GL_LUMINANCE_ALPHA is available and used if needed
|
||||
* 1 : GL_LUMINANCE_ALPHA hardware support disabled (a GL_RGBA texture will be used instead)
|
||||
|
||||
##### LIBGL_BLENDHACK
|
||||
Experimental: Change Blend GL_SRC_ALPHA, GL_ONE to GL_ONE, GL_ONE
|
||||
* 0 : Default, nothing special
|
||||
* 1 : Change Blend GL_SRC_ALPHA, GL_ONE to GL_ONE, GL_ONE (can be usefull for Xash3D engine)
|
||||
|
||||
##### LIBGL_BLENDCOLOR
|
||||
Hack: Export a (faked) glBlendColor
|
||||
* 0 : Default, don't expose gBlendColor
|
||||
* 1 : Exposed the function (if no hardware support, faked function will be used)
|
||||
|
||||
##### LIBGL_VERSION
|
||||
Hack: Control the glGetString version. Override version string (should be in the form of "1.x")
|
||||
|
||||
##### LIBGL_BATCH
|
||||
This has been changed with v1.0.5.
|
||||
Now BATCH simply try to merge subsequent glDrawXXXXX (glDrawArrays, glDrawElements...). It only try to merge if arrays is between MINBATCH and MAXBATCH (inclusive)
|
||||
The Batching stop when there is a change of GL State, but also if an Array of more then 100*N is encountered.
|
||||
* 0 : Default: don't try to merge glDrawXXXXX
|
||||
* N : Any number: try to merger arrays, 1st must be between 0 and 100*N
|
||||
* MIN-MAX : 2 number separated by minus, to try merge arrays that are between MIN and MAX vertices
|
||||
|
||||
##### LIBGL_NOERROR
|
||||
Hack: glGetError() always return GL_NOERROR
|
||||
* 0 : Default, glGetError behave as it should
|
||||
* 1 : glGetError never fail.
|
||||
|
||||
##### LIBGL_GAMMA
|
||||
Pandora Hack: Set a Gamma value (in decimal formal, 1.0 means no gamma boost)
|
||||
* X.Y : Use X.Y as gamma when creating context (typical value can be 1.6 or 2.0)
|
||||
|
||||
##### LIBGL_SRGB
|
||||
ODROID Hack: Enable sRGB Surface (so Gamma corrected), if Hardware support it
|
||||
* 0 : Default, don't try to use sRGB surface
|
||||
* 1 : Enable sRGB Surface (but support will be tested first, must have EGL_KHR_gl_colorspace extension)
|
||||
|
||||
##### LIBGL_FASTMATH
|
||||
Hack: Activate some Fast Math in processor/co-processor
|
||||
* 0 : Default, nothing special
|
||||
* 1 : On OpenPandora and CHIP, activate "RunFast" on Cortex-A8 (mode default NaN, flush-to-zero)
|
||||
: Not implemented on other platforms (will do nothing)
|
||||
|
||||
##### LIBGL_SILENTSTUB
|
||||
Debug: Hide or Show the Sub / Not found message
|
||||
* 0 : The messages for Stub or absent function are printed
|
||||
* 1 : Default, don't print the STUB or glXGetProcAddress glXXXXX not found message
|
||||
|
||||
##### LIBGL_NOBANNER
|
||||
Show/Hide initial text
|
||||
* 0 : Default, print starting message
|
||||
* 1 : Silent: no LIBGL message at start (combine with LIBGL_SILENTSTUB for more silence)
|
||||
|
||||
##### LIBGL_NPOT
|
||||
Expose NPOT (Non Power of Two) Support
|
||||
* 0 : Default, expose the extension that are available by the GLES backend
|
||||
* 1 : Expose limited NPOT extension
|
||||
* 2 : Expose GL_ARB_texture_non_power_of_two extension
|
||||
|
||||
##### LIBGL_GLQUERIES
|
||||
Expose glQueries functions
|
||||
* 0 : Don't expose the function (fake one will be used if called)
|
||||
* 1 : Default, expose fake functions (always answer 0)
|
||||
|
||||
##### LIBGL_NOTEXMAT
|
||||
Handling of Texture Matrix
|
||||
* 0 : Default, perform handling internally (better handling of NPOT texture on all hardware)
|
||||
* 1 : Let the driver handle texmat (can be faster in some cases, but NPOT texture may be broken)
|
||||
|
||||
##### LIBGL_NOTEST
|
||||
Initial Hardware test
|
||||
* 0 : Default, perform initial hardware testing (using a PBuffer)
|
||||
* 1 : Do not perform test (no extensions tested or used)
|
||||
|
||||
##### LIBGL_NOVAOCACHE
|
||||
VAO Caching
|
||||
* 0 : Default, try to cache vao to avoid memcpy in render list
|
||||
* 1 : Don't cache VAO
|
||||
|
||||
##### LIBGL_VABGRA
|
||||
Vertex Array BGRA extension
|
||||
* 0 : Default, GL_ARB_vertex_array_bgra not exposed (still emulated)
|
||||
* 1 : Extension exposed may be faster in some cases (Arx Libertatis mainly)
|
||||
|
||||
##### LIBGL_BEGINEND
|
||||
Merge of subsequent glBegin/glEnd blocks (will be non-effective if BATCH mode is used)
|
||||
* 0 : Don't try to merge
|
||||
* 1 : Try to merge, even if there is a glColor / glNormal in between (default)
|
||||
|
||||
##### LIBGL_AVOID16BITS
|
||||
Try to avoid 16bits textures
|
||||
* 0 : Default on ImgTec hardware, use 16bits texture if it can avoid a conversion or for DXTc textures
|
||||
* 1 : Default on all other hardware, Use 32bits texture unless specifically requested (using internalformat)
|
||||
|
||||
##### LIBGL_AVOID24BITS
|
||||
Try to avoid 24bits textures (i.e. GL_RGB)
|
||||
* 0 : Default, use 24bits texture when it's possible
|
||||
* 1 : Force 32bits textures when GL_RGB is asked (as internal or not). Not recommended, as it may break some blend functions (especially on GLES 1.1 backend). Does not impact 16bits formats.
|
||||
|
||||
##### LIBGL_FORCE16BITS
|
||||
Try to use 16bits textures
|
||||
* 0 : Default, don't force 16bits texture
|
||||
* 1 : Use 16bits texture instead of 32bits (i.e. use RGBA4 instead of RGBA8 and RGB5 instead of RGB8)
|
||||
|
||||
##### LIBGL_POTFRAMEBUFFER
|
||||
Use only Power Of Two dimension for Framebuffer
|
||||
* 0 : Default, use NPOT dimension if supported
|
||||
* 1 : Force Framebuffer to be created with POT dimension (not advised on GLES2 backend)
|
||||
|
||||
##### LIBGL_NOBGRA
|
||||
Ignore BGRA Texture hardware extension
|
||||
* 0 : Default, use BGRA extension if possible
|
||||
* 1 : Ignore BGRA extension, even if supported by GLES hardware
|
||||
|
||||
##### LIBGL_NOTEXRECT
|
||||
Don't expose Texture rectangle extension (GL_ARB_texture_rectangle)
|
||||
* 0 : Default, the extension is listed
|
||||
* 1 : Don't expose the extension (it's not supported in shaders yet)
|
||||
|
||||
##### LIBGL_NOHIGHP
|
||||
Usage of highp precision in fragment shader (ES2 backend only)
|
||||
* 0 : Default, use highp if available
|
||||
* 1 : Disable usage of highp in Fragment shaders
|
||||
|
||||
##### LIBGL_COMMENTS
|
||||
Comments in shaders are kept (also for generated shaders by fpe_shaders)
|
||||
* 0 : Default, no comments in shaders sent to GLES Hardware
|
||||
* 1 : Comments are left in Shaders sent to GLES Hardware
|
||||
|
||||
##### LIBGL_DEFAULTWRAP
|
||||
Hack to define default WRAP mode for texture
|
||||
* 0 : Default wrap mode is GL_REPEAT (normal OpenGL behavour): default on NPOT hardware
|
||||
* 1 : Default wrap mode is GL_CLAMP_TO_EDGE: default on limited NPOT or non-NPOT hardware
|
||||
* 2 : Default wrap mode is GL_CLAMP_TO_EDGE, enforced (not advised)
|
||||
|
||||
##### LIBGL_FBOUNBIND
|
||||
Workaround on FBO where a bond texture is used for drawing
|
||||
* 0 : Disabled (Default for all other configuration)
|
||||
* 1 : Enabled (Default on ARM and PowerVR hardware)
|
||||
|
||||
##### LIBGL_FBOFORCETEX
|
||||
For the Color Attachment 0 to be a Texture2D (even if program attachs a Renderbuffer) => may speedup glBlitFramebuffer if used
|
||||
* 0 : Don't force (allow Renderbuffer to be used on color attachments)
|
||||
* 1 : Default: For Color Attachment 0 of FBO to be a texture
|
||||
|
||||
##### LIBGL_BLITFULLSCREEN
|
||||
Hack to trigger a SwapBuffers when a Full Framebuffer Blit on default FBO is done
|
||||
* 0 : Don't force
|
||||
* 1 : Default: Activate the hack (usefull with Wine/D3D)
|
||||
|
||||
##### LIBGL_NOARBPROGRAM
|
||||
Don't expose ARB Program extensions (GL_ARB_vertex_program, GL_ARB_fragment_program and GL_EXT_program_parameters)
|
||||
* 0 : Default: expose the ARB Program extensions
|
||||
* 1 : Don't expose the extensions
|
||||
|
||||
##### LIBGL_FBO
|
||||
Hack: define custom dimension for FBO (only used with LIBGL_FBO=2)
|
||||
* WxH : Define FBO of WxH size (ex: LIBGL_FBO=1280x720)
|
||||
|
||||
##### LIBGL_NOTEXARRAY
|
||||
Hack to force using discrete Texture instead of Array in all shader
|
||||
* 0 : Default: Array of texture is used in shaders (not in FPE generated ones)
|
||||
* 1 : Individual texture are forced in shaders (shaders may fail to compile if array are accessed by indice)
|
||||
|
||||
##### LIBGL_LOGSHADERERROR
|
||||
Log to the console Shader Compile error, with initial and ShaderConv'd source of the shader
|
||||
* 0 : Default, don't log
|
||||
* 1 : Log Shader Compilation Errors
|
||||
|
||||
##### LIBGL_SHADERNOGLES
|
||||
Don't use GL_ES part in shaders
|
||||
* 0 : Default, let GL_ES part in shader
|
||||
* 1 : Remove the GL_ES part in shader (useful for Löve for example)
|
||||
|
||||
##### LIBGL_NODEPTHTEX
|
||||
Disable the use of Depth texture
|
||||
* 0 : Default, Use Depth Texture if supported by Hardware
|
||||
* 1 : Disable the use of Depth Texture (renderbuffer will be used in FBO)
|
||||
|
||||
##### LIBGL_FLOAT
|
||||
Expose support for FLOAT and HALF_FLOAT Texture support (and has attachment to FBO)
|
||||
* 0 : Don't exposed, even if supported in hardware
|
||||
* 1 : Default, exposed what is supported by hardware
|
||||
* 2 : Force exposed, even if no supported (will be emulated has GL_UNSIGNED_BYTE if not supported)
|
||||
|
||||
##### LIBGL_GLXRECYCLE
|
||||
Recycle EGLSurface per Drawable, instead of destroying them
|
||||
* 0 : Default, don't recycle
|
||||
* 1 : Don't destroy EGLSurface, per reused them per drawable (can fix EGL_BAD_ALLOC error). EGLSurface are never destroyed in this mode for now.
|
||||
|
||||
##### LIBGL_NOCLEAN
|
||||
Debug: don't clean GLContext when they are destroy
|
||||
* 0 : Default, clean GLContext
|
||||
* 1 : Don't clean GLContext
|
||||
|
||||
##### LIBGL_EGL
|
||||
Define EGL lib to use. Default folder are the standard one for dynamic library loading (LD_LIBRARY_PATH and friend) plus "/opt/vc/lib/", /usr/local/lib/" and "/usr/lib/".
|
||||
* by default try to use libbrcmEGL and libEGL
|
||||
* filename: try to load from the defaults folder (don't forget to use complete filename, with ".so" extension). If not found/loaded, default one will be tried.
|
||||
* /path/to/filename: try to use exact path/filename. If not found/loaded, default one will be tried.
|
||||
|
||||
##### LIBGL_GLES
|
||||
Define GLES(2) lib to use. Default folder are the standard one for dynamic library loading (LD_LIBRARY_PATH and friend) plus "/opt/vc/lib/", /usr/local/lib/" and "/usr/lib/". Be sure to point to correct GLES library depending on wich GLES backend you are using.
|
||||
* by default try to use libGLESv1_CM, libGLES_CM or libbrcmGLESv1_CM for GLES1.1 and libGLESv2_CM, libGLESv2 or libbrcmGLESv2 for GLES2 backend
|
||||
* filename: try to load from the defaults folder (don't forget to use complete filename, with ".so" extension). If not found/loaded, default one will be tried.
|
||||
* /path/to/filename: try to use exact path/filename. If not found/loaded, default one will be tried.
|
||||
|
||||
##### LIBGL_DBGSHADERCONV
|
||||
Log to the console all shaders before and after conversion
|
||||
* 0 : Default: don't log anything
|
||||
* 1 : Log Vertex Shader
|
||||
* 2 : Log Framegent Shader
|
||||
* 4 : Log Shaders before going to Shaderconv
|
||||
* 8 : Log Shaders after going to Shaderconv
|
||||
Note that you can combine (logical or state. So 14 will be only Fragment shader before and after shaderconv)
|
||||
Note also that if neither Fragment and Vertex are defined, both will be selected. Same for Before and After.
|
||||
At last, the value "1" will be changed to "15", to log everything.
|
||||
|
||||
##### LIBGL_NOPSA
|
||||
Disable the use of the Precompiled Shader Archive
|
||||
* 0 : Default: use (and save) the PSA (it's saved on $HOME/.gl4es.psa on linux)
|
||||
* 1 : Don't use PSA.
|
||||
|
||||
##### LIBGL_PSA_FOLDER
|
||||
Set a custom path for Precompile Shader Archive
|
||||
* XXXXX : set that path. Archive will be saved at XXXXX/.gl4es.psa
|
||||
|
||||
##### LIBGL_USEVBO
|
||||
Usage of VBO in certain cases. Only for GLES2+. The 2 and 3 mode are experimental and will probably be slower anyway.
|
||||
* 0 : Disable the use of VBO.
|
||||
* 1 : Default: Use VBO when possible (for Arrays VBO or glList)
|
||||
* 2 : Use VBO when possible (and also on `glLockArrays`).
|
||||
* 3 : Use VBO when possible (and special case on `glLockArrays` for idTech3 engine games).
|
||||
|
||||
##### LIBGL_NOES2COMPAT
|
||||
Don't expose GLX_EXT_create_context_es2_profile extension
|
||||
* 0 : Extension is there
|
||||
* 1 : Don't expose the extension: for SDL2, use it with SDL_VIDEO_GL_DRIVER and SDL_VIDEO_EGL_DRIVER to use GLESv2 driver directly
|
||||
|
||||
##### LIBGL_NOINTOVLHACK
|
||||
Disable the hack in shader converter to define overloaded function with int
|
||||
* 0 : Default: use the hack
|
||||
* 1 : Don't use it (some driver / PVRCapture don't like it much).
|
||||
|
||||
##### LIBGL_GLXNATIVE
|
||||
Disable the filtering of GLXConfig by NATIVE_TYPE
|
||||
* 0 : Default, GLX_X_NATIVE_TYPE attribute are taken into account
|
||||
* 1 : Don't filter GLXConfig by GLX_X_NATIVE_TYPE
|
||||
|
||||
##### LIBGL_NOSHADERLOD
|
||||
Disable GL_EXT_shader_texture_lod
|
||||
* 0 : Default, use the extension if present
|
||||
* 1 : Disable the use of the extension (using crude fallback)
|
||||
|
||||
##### LIBGL_NORMALIZE
|
||||
Force normals to be normliazed in FPE
|
||||
* 0 : Default, don't force normalizations
|
||||
* 1 : Force normalization on normals on FPE, even when it's disabled (workaround for a bug that prevent colors on Minecraft 1.16+)
|
||||
|
||||
###### LIBGL_BLITFB0
|
||||
Blit to FB 0 force a SwapBuffer
|
||||
* 0 : Default, don't force a SwapBuffer when glBlitFramebuffer to draw fb0 is used (unless the full FB0 if blitted)
|
||||
* 1 : Force a SwapBuffer each time glBlitFramebuffer on FB0 is used (can help some windowed Wine games)
|
||||
|
||||
###### LIBGL_DEEPBIND
|
||||
Use RTLD_DEEPBIND when loading EGL and GLES library
|
||||
* 0 : Default except on PYRA, use RTLD_DEEPBIND when loading EGL/GLES libraries
|
||||
* 1 : Default only on PYRA, don't use RTLD_DEEPBIND when loading EGL/GLES libraries
|
||||
@@ -0,0 +1,2 @@
|
||||
theme: jekyll-theme-hacker
|
||||
title: GL4ES - The OpenGL driver for GLES Hardware
|
||||
@@ -0,0 +1,84 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
* Copyright (C) 2009 Texas Instruments Incorporated - http://www.ti.com/
|
||||
* Copyright(c) 2008 Imagination Technologies Ltd. All rights reserved.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope it will be useful but, except
|
||||
* as otherwise stated in writing, without any warranty; without even the
|
||||
* implied warranty of merchantability or fitness for a particular purpose.
|
||||
* See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* The full GNU General Public License is included in this distribution in
|
||||
* the file called "COPYING".
|
||||
*
|
||||
* Contact Information:
|
||||
* Imagination Technologies Ltd. <gpl-support@imgtec.com>
|
||||
* Home Park Estate, Kings Langley, Herts, WD4 8LZ, UK
|
||||
*
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __BC_CAT_H__
|
||||
#define __BC_CAT_H__
|
||||
|
||||
#include <linux/ioctl.h>
|
||||
|
||||
#define BC_FOURCC(a,b,c,d) \
|
||||
((unsigned long) ((a) | (b)<<8 | (c)<<16 | (d)<<24))
|
||||
|
||||
#define BC_PIX_FMT_NV12 BC_FOURCC('N', 'V', '1', '2') /*YUV 4:2:0*/
|
||||
#define BC_PIX_FMT_UYVY BC_FOURCC('U', 'Y', 'V', 'Y') /*YUV 4:2:2*/
|
||||
#define BC_PIX_FMT_YUYV BC_FOURCC('Y', 'U', 'Y', 'V') /*YUV 4:2:2*/
|
||||
#define BC_PIX_FMT_RGB565 BC_FOURCC('R', 'G', 'B', 'P') /*RGB 5:6:5*/
|
||||
|
||||
enum BC_memory {
|
||||
BC_MEMORY_MMAP = 1,
|
||||
BC_MEMORY_USERPTR = 2,
|
||||
};
|
||||
|
||||
typedef struct BCIO_package_TAG {
|
||||
int input;
|
||||
int output;
|
||||
}BCIO_package;
|
||||
|
||||
/*
|
||||
* the following types are tested for fourcc in struct bc_buf_params_t
|
||||
* NV12
|
||||
* UYVY
|
||||
* RGB565 - not tested yet
|
||||
* YUYV
|
||||
*/
|
||||
typedef struct bc_buf_params {
|
||||
int count; /*number of buffers, [in/out]*/
|
||||
int width; /*buffer width in pixel, multiple of 8 or 32*/
|
||||
int height; /*buffer height in pixel*/
|
||||
unsigned int fourcc; /*buffer pixel format*/
|
||||
enum BC_memory type;
|
||||
} bc_buf_params_t;
|
||||
|
||||
typedef struct bc_buf_ptr {
|
||||
unsigned int index;
|
||||
int size;
|
||||
unsigned long pa;
|
||||
} bc_buf_ptr_t;
|
||||
|
||||
#define BCIO_GID 'g'
|
||||
#define BC_IOWR(INDEX) _IOWR(BCIO_GID, INDEX, BCIO_package)
|
||||
|
||||
#define BCIOGET_BUFFERCOUNT BC_IOWR(0) /*obsolete, since BCIOREQ_BUFFERS
|
||||
return the number of buffers*/
|
||||
#define BCIOGET_BUFFERPHYADDR BC_IOWR(1) /*get physical address by index*/
|
||||
#define BCIOGET_BUFFERIDX BC_IOWR(2) /*get index by physical address*/
|
||||
|
||||
#define BCIOREQ_BUFFERS BC_IOWR(3)
|
||||
#define BCIOSET_BUFFERPHYADDR BC_IOWR(4)
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
gl4es for Debian
|
||||
---------------
|
||||
|
||||
|
||||
-- licheng <shiptux@gmail.com> Fri, 30 Jul 2021 14:53:15 +0800
|
||||
@@ -0,0 +1,7 @@
|
||||
gl4es for Debian
|
||||
---------------
|
||||
|
||||
|
||||
|
||||
-- licheng <shiptux@gmail.com> Fri, 30 Jul 2021 14:53:15 +0800
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
gl4es (1.1.4-1) unstable; urgency=medium
|
||||
|
||||
* Initial release
|
||||
|
||||
-- licheng <shiptux@gmail.com> Fri, 30 Jul 2021 14:53:15 +0800
|
||||
@@ -0,0 +1 @@
|
||||
11
|
||||
@@ -0,0 +1,29 @@
|
||||
Source: gl4es
|
||||
Section: libs
|
||||
Priority: optional
|
||||
Maintainer: licheng <shiptux@gmail.com>
|
||||
Build-Depends: debhelper (>= 11),cmake,libx11-dev
|
||||
Standards-Version: 4.1.3
|
||||
Homepage: https://github.com/ptitSeb/gl4es
|
||||
|
||||
Package: libgl4es
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}
|
||||
Multi-arch: no
|
||||
Description: GL4ES - OpenGL for GLES Hardware
|
||||
This is a library provide OpenGL 2.x functionality for GLES2.0 accelerated Hardware
|
||||
(and of course also support OpenGL 1.5 function, so metimes better than when using
|
||||
GLES 1.1 backend) There is also support for GLES 1.1 Hardware, emulating OpenGL 1.5,
|
||||
and some OpenGL 2.x + extensions.
|
||||
This package contains the shared libraries.
|
||||
|
||||
Package: libgl4es-dev
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}
|
||||
Multi-arch: no
|
||||
Description: GL4ES - OpenGL for GLES Hardware
|
||||
This is a headers provide OpenGL 2.x functionality for GLES2.0 accelerated Hardware
|
||||
(and of course also support OpenGL 1.5 function, so metimes better than when using
|
||||
GLES 1.1 backend) There is also support for GLES 1.1 Hardware, emulating OpenGL 1.5,
|
||||
and some OpenGL 2.x + extensions.
|
||||
This package contains the development files.
|
||||
@@ -0,0 +1,46 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: gl4es
|
||||
Source: https://github.com/ptitSeb/gl4es
|
||||
|
||||
Files: *
|
||||
Copyright: Copyright (c) 2016-2018 Sebastien Chevalier
|
||||
Copyright (c) 2013-2016 Ryan Hileman
|
||||
License: Expat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Files: debian/*
|
||||
Copyright: 2021 licheng <shiptux@gmail.com>
|
||||
License: GPL-2+
|
||||
This package is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
include/gl4esinit.h usr/include/gl4es/
|
||||
include/gl4eshint.h usr/include/gl4es/
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
lib/libGL.so.1 usr/lib/gl4es/
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fix lintian error
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -6,6 +6,9 @@
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)
|
||||
|
||||
+set(CMAKE_SKIP_INSTALL_RPATH ON)
|
||||
+set(CMAKE_SKIP_RPATH ON)
|
||||
+
|
||||
link_directories(${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
|
||||
|
||||
option(PANDORA "Set to ON if targeting an OpenPandora device" ${PANDORA})
|
||||
@@ -0,0 +1 @@
|
||||
fix_lintian_error
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_auto_test:
|
||||
# Disable auto tests at build time
|
||||
:
|
||||
|
||||
override_dh_auto_clean:
|
||||
dh_auto_clean
|
||||
rm -f lib/*.so*
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
3.0 (quilt)
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,361 @@
|
||||
#ifndef __egl_h_
|
||||
#define __egl_h_ 1
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
** Copyright (c) 2013-2017 The Khronos Group Inc.
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person obtaining a
|
||||
** copy of this software and/or associated documentation files (the
|
||||
** "Materials"), to deal in the Materials without restriction, including
|
||||
** without limitation the rights to use, copy, modify, merge, publish,
|
||||
** distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
** permit persons to whom the Materials are furnished to do so, subject to
|
||||
** the following conditions:
|
||||
**
|
||||
** The above copyright notice and this permission notice shall be included
|
||||
** in all copies or substantial portions of the Materials.
|
||||
**
|
||||
** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
*/
|
||||
/*
|
||||
** This header is generated from the Khronos EGL XML API Registry.
|
||||
** The current version of the Registry, generator scripts
|
||||
** used to make the header, and the header can be found at
|
||||
** http://www.khronos.org/registry/egl
|
||||
**
|
||||
** Khronos $Git commit SHA1: cb927ca98d $ on $Git commit date: 2019-08-08 01:05:38 -0700 $
|
||||
*/
|
||||
|
||||
#include <EGL/eglplatform.h>
|
||||
|
||||
#ifndef EGL_EGL_PROTOTYPES
|
||||
#define EGL_EGL_PROTOTYPES 1
|
||||
#endif
|
||||
|
||||
/* Generated on date 20190808 */
|
||||
|
||||
/* Generated C header for:
|
||||
* API: egl
|
||||
* Versions considered: .*
|
||||
* Versions emitted: .*
|
||||
* Default extensions included: None
|
||||
* Additional extensions included: _nomatch_^
|
||||
* Extensions removed: _nomatch_^
|
||||
*/
|
||||
|
||||
#ifndef EGL_VERSION_1_0
|
||||
#define EGL_VERSION_1_0 1
|
||||
typedef unsigned int EGLBoolean;
|
||||
typedef void *EGLDisplay;
|
||||
#include <KHR/khrplatform.h>
|
||||
#include <EGL/eglplatform.h>
|
||||
typedef void *EGLConfig;
|
||||
typedef void *EGLSurface;
|
||||
typedef void *EGLContext;
|
||||
typedef void (*__eglMustCastToProperFunctionPointerType)(void);
|
||||
#define EGL_ALPHA_SIZE 0x3021
|
||||
#define EGL_BAD_ACCESS 0x3002
|
||||
#define EGL_BAD_ALLOC 0x3003
|
||||
#define EGL_BAD_ATTRIBUTE 0x3004
|
||||
#define EGL_BAD_CONFIG 0x3005
|
||||
#define EGL_BAD_CONTEXT 0x3006
|
||||
#define EGL_BAD_CURRENT_SURFACE 0x3007
|
||||
#define EGL_BAD_DISPLAY 0x3008
|
||||
#define EGL_BAD_MATCH 0x3009
|
||||
#define EGL_BAD_NATIVE_PIXMAP 0x300A
|
||||
#define EGL_BAD_NATIVE_WINDOW 0x300B
|
||||
#define EGL_BAD_PARAMETER 0x300C
|
||||
#define EGL_BAD_SURFACE 0x300D
|
||||
#define EGL_BLUE_SIZE 0x3022
|
||||
#define EGL_BUFFER_SIZE 0x3020
|
||||
#define EGL_CONFIG_CAVEAT 0x3027
|
||||
#define EGL_CONFIG_ID 0x3028
|
||||
#define EGL_CORE_NATIVE_ENGINE 0x305B
|
||||
#define EGL_DEPTH_SIZE 0x3025
|
||||
#define EGL_DONT_CARE EGL_CAST(EGLint,-1)
|
||||
#define EGL_DRAW 0x3059
|
||||
#define EGL_EXTENSIONS 0x3055
|
||||
#define EGL_FALSE 0
|
||||
#define EGL_GREEN_SIZE 0x3023
|
||||
#define EGL_HEIGHT 0x3056
|
||||
#define EGL_LARGEST_PBUFFER 0x3058
|
||||
#define EGL_LEVEL 0x3029
|
||||
#define EGL_MAX_PBUFFER_HEIGHT 0x302A
|
||||
#define EGL_MAX_PBUFFER_PIXELS 0x302B
|
||||
#define EGL_MAX_PBUFFER_WIDTH 0x302C
|
||||
#define EGL_NATIVE_RENDERABLE 0x302D
|
||||
#define EGL_NATIVE_VISUAL_ID 0x302E
|
||||
#define EGL_NATIVE_VISUAL_TYPE 0x302F
|
||||
#define EGL_NONE 0x3038
|
||||
#define EGL_NON_CONFORMANT_CONFIG 0x3051
|
||||
#define EGL_NOT_INITIALIZED 0x3001
|
||||
#define EGL_NO_CONTEXT EGL_CAST(EGLContext,0)
|
||||
#define EGL_NO_DISPLAY EGL_CAST(EGLDisplay,0)
|
||||
#define EGL_NO_SURFACE EGL_CAST(EGLSurface,0)
|
||||
#define EGL_PBUFFER_BIT 0x0001
|
||||
#define EGL_PIXMAP_BIT 0x0002
|
||||
#define EGL_READ 0x305A
|
||||
#define EGL_RED_SIZE 0x3024
|
||||
#define EGL_SAMPLES 0x3031
|
||||
#define EGL_SAMPLE_BUFFERS 0x3032
|
||||
#define EGL_SLOW_CONFIG 0x3050
|
||||
#define EGL_STENCIL_SIZE 0x3026
|
||||
#define EGL_SUCCESS 0x3000
|
||||
#define EGL_SURFACE_TYPE 0x3033
|
||||
#define EGL_TRANSPARENT_BLUE_VALUE 0x3035
|
||||
#define EGL_TRANSPARENT_GREEN_VALUE 0x3036
|
||||
#define EGL_TRANSPARENT_RED_VALUE 0x3037
|
||||
#define EGL_TRANSPARENT_RGB 0x3052
|
||||
#define EGL_TRANSPARENT_TYPE 0x3034
|
||||
#define EGL_TRUE 1
|
||||
#define EGL_VENDOR 0x3053
|
||||
#define EGL_VERSION 0x3054
|
||||
#define EGL_WIDTH 0x3057
|
||||
#define EGL_WINDOW_BIT 0x0004
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLCHOOSECONFIGPROC) (EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLCOPYBUFFERSPROC) (EGLDisplay dpy, EGLSurface surface, EGLNativePixmapType target);
|
||||
typedef EGLContext (EGLAPIENTRYP PFNEGLCREATECONTEXTPROC) (EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);
|
||||
typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPBUFFERSURFACEPROC) (EGLDisplay dpy, EGLConfig config, const EGLint *attrib_list);
|
||||
typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPIXMAPSURFACEPROC) (EGLDisplay dpy, EGLConfig config, EGLNativePixmapType pixmap, const EGLint *attrib_list);
|
||||
typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEWINDOWSURFACEPROC) (EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYCONTEXTPROC) (EGLDisplay dpy, EGLContext ctx);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYSURFACEPROC) (EGLDisplay dpy, EGLSurface surface);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETCONFIGATTRIBPROC) (EGLDisplay dpy, EGLConfig config, EGLint attribute, EGLint *value);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETCONFIGSPROC) (EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config);
|
||||
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETCURRENTDISPLAYPROC) (void);
|
||||
typedef EGLSurface (EGLAPIENTRYP PFNEGLGETCURRENTSURFACEPROC) (EGLint readdraw);
|
||||
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETDISPLAYPROC) (EGLNativeDisplayType display_id);
|
||||
typedef EGLint (EGLAPIENTRYP PFNEGLGETERRORPROC) (void);
|
||||
typedef __eglMustCastToProperFunctionPointerType (EGLAPIENTRYP PFNEGLGETPROCADDRESSPROC) (const char *procname);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLINITIALIZEPROC) (EGLDisplay dpy, EGLint *major, EGLint *minor);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLMAKECURRENTPROC) (EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYCONTEXTPROC) (EGLDisplay dpy, EGLContext ctx, EGLint attribute, EGLint *value);
|
||||
typedef const char *(EGLAPIENTRYP PFNEGLQUERYSTRINGPROC) (EGLDisplay dpy, EGLint name);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYSURFACEPROC) (EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint *value);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLSWAPBUFFERSPROC) (EGLDisplay dpy, EGLSurface surface);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLTERMINATEPROC) (EGLDisplay dpy);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLWAITGLPROC) (void);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLWAITNATIVEPROC) (EGLint engine);
|
||||
#if EGL_EGL_PROTOTYPES
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglChooseConfig (EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglCopyBuffers (EGLDisplay dpy, EGLSurface surface, EGLNativePixmapType target);
|
||||
EGLAPI EGLContext EGLAPIENTRY eglCreateContext (EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);
|
||||
EGLAPI EGLSurface EGLAPIENTRY eglCreatePbufferSurface (EGLDisplay dpy, EGLConfig config, const EGLint *attrib_list);
|
||||
EGLAPI EGLSurface EGLAPIENTRY eglCreatePixmapSurface (EGLDisplay dpy, EGLConfig config, EGLNativePixmapType pixmap, const EGLint *attrib_list);
|
||||
EGLAPI EGLSurface EGLAPIENTRY eglCreateWindowSurface (EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglDestroyContext (EGLDisplay dpy, EGLContext ctx);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglDestroySurface (EGLDisplay dpy, EGLSurface surface);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglGetConfigAttrib (EGLDisplay dpy, EGLConfig config, EGLint attribute, EGLint *value);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglGetConfigs (EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config);
|
||||
EGLAPI EGLDisplay EGLAPIENTRY eglGetCurrentDisplay (void);
|
||||
EGLAPI EGLSurface EGLAPIENTRY eglGetCurrentSurface (EGLint readdraw);
|
||||
EGLAPI EGLDisplay EGLAPIENTRY eglGetDisplay (EGLNativeDisplayType display_id);
|
||||
EGLAPI EGLint EGLAPIENTRY eglGetError (void);
|
||||
EGLAPI __eglMustCastToProperFunctionPointerType EGLAPIENTRY eglGetProcAddress (const char *procname);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglInitialize (EGLDisplay dpy, EGLint *major, EGLint *minor);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglMakeCurrent (EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglQueryContext (EGLDisplay dpy, EGLContext ctx, EGLint attribute, EGLint *value);
|
||||
EGLAPI const char *EGLAPIENTRY eglQueryString (EGLDisplay dpy, EGLint name);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglQuerySurface (EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint *value);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglSwapBuffers (EGLDisplay dpy, EGLSurface surface);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglTerminate (EGLDisplay dpy);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglWaitGL (void);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglWaitNative (EGLint engine);
|
||||
#endif
|
||||
#endif /* EGL_VERSION_1_0 */
|
||||
|
||||
#ifndef EGL_VERSION_1_1
|
||||
#define EGL_VERSION_1_1 1
|
||||
#define EGL_BACK_BUFFER 0x3084
|
||||
#define EGL_BIND_TO_TEXTURE_RGB 0x3039
|
||||
#define EGL_BIND_TO_TEXTURE_RGBA 0x303A
|
||||
#define EGL_CONTEXT_LOST 0x300E
|
||||
#define EGL_MIN_SWAP_INTERVAL 0x303B
|
||||
#define EGL_MAX_SWAP_INTERVAL 0x303C
|
||||
#define EGL_MIPMAP_TEXTURE 0x3082
|
||||
#define EGL_MIPMAP_LEVEL 0x3083
|
||||
#define EGL_NO_TEXTURE 0x305C
|
||||
#define EGL_TEXTURE_2D 0x305F
|
||||
#define EGL_TEXTURE_FORMAT 0x3080
|
||||
#define EGL_TEXTURE_RGB 0x305D
|
||||
#define EGL_TEXTURE_RGBA 0x305E
|
||||
#define EGL_TEXTURE_TARGET 0x3081
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLBINDTEXIMAGEPROC) (EGLDisplay dpy, EGLSurface surface, EGLint buffer);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLRELEASETEXIMAGEPROC) (EGLDisplay dpy, EGLSurface surface, EGLint buffer);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLSURFACEATTRIBPROC) (EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint value);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLSWAPINTERVALPROC) (EGLDisplay dpy, EGLint interval);
|
||||
#if EGL_EGL_PROTOTYPES
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglBindTexImage (EGLDisplay dpy, EGLSurface surface, EGLint buffer);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglReleaseTexImage (EGLDisplay dpy, EGLSurface surface, EGLint buffer);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglSurfaceAttrib (EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint value);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglSwapInterval (EGLDisplay dpy, EGLint interval);
|
||||
#endif
|
||||
#endif /* EGL_VERSION_1_1 */
|
||||
|
||||
#ifndef EGL_VERSION_1_2
|
||||
#define EGL_VERSION_1_2 1
|
||||
typedef unsigned int EGLenum;
|
||||
typedef void *EGLClientBuffer;
|
||||
#define EGL_ALPHA_FORMAT 0x3088
|
||||
#define EGL_ALPHA_FORMAT_NONPRE 0x308B
|
||||
#define EGL_ALPHA_FORMAT_PRE 0x308C
|
||||
#define EGL_ALPHA_MASK_SIZE 0x303E
|
||||
#define EGL_BUFFER_PRESERVED 0x3094
|
||||
#define EGL_BUFFER_DESTROYED 0x3095
|
||||
#define EGL_CLIENT_APIS 0x308D
|
||||
#define EGL_COLORSPACE 0x3087
|
||||
#define EGL_COLORSPACE_sRGB 0x3089
|
||||
#define EGL_COLORSPACE_LINEAR 0x308A
|
||||
#define EGL_COLOR_BUFFER_TYPE 0x303F
|
||||
#define EGL_CONTEXT_CLIENT_TYPE 0x3097
|
||||
#define EGL_DISPLAY_SCALING 10000
|
||||
#define EGL_HORIZONTAL_RESOLUTION 0x3090
|
||||
#define EGL_LUMINANCE_BUFFER 0x308F
|
||||
#define EGL_LUMINANCE_SIZE 0x303D
|
||||
#define EGL_OPENGL_ES_BIT 0x0001
|
||||
#define EGL_OPENVG_BIT 0x0002
|
||||
#define EGL_OPENGL_ES_API 0x30A0
|
||||
#define EGL_OPENVG_API 0x30A1
|
||||
#define EGL_OPENVG_IMAGE 0x3096
|
||||
#define EGL_PIXEL_ASPECT_RATIO 0x3092
|
||||
#define EGL_RENDERABLE_TYPE 0x3040
|
||||
#define EGL_RENDER_BUFFER 0x3086
|
||||
#define EGL_RGB_BUFFER 0x308E
|
||||
#define EGL_SINGLE_BUFFER 0x3085
|
||||
#define EGL_SWAP_BEHAVIOR 0x3093
|
||||
#define EGL_UNKNOWN EGL_CAST(EGLint,-1)
|
||||
#define EGL_VERTICAL_RESOLUTION 0x3091
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLBINDAPIPROC) (EGLenum api);
|
||||
typedef EGLenum (EGLAPIENTRYP PFNEGLQUERYAPIPROC) (void);
|
||||
typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPBUFFERFROMCLIENTBUFFERPROC) (EGLDisplay dpy, EGLenum buftype, EGLClientBuffer buffer, EGLConfig config, const EGLint *attrib_list);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLRELEASETHREADPROC) (void);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLWAITCLIENTPROC) (void);
|
||||
#if EGL_EGL_PROTOTYPES
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglBindAPI (EGLenum api);
|
||||
EGLAPI EGLenum EGLAPIENTRY eglQueryAPI (void);
|
||||
EGLAPI EGLSurface EGLAPIENTRY eglCreatePbufferFromClientBuffer (EGLDisplay dpy, EGLenum buftype, EGLClientBuffer buffer, EGLConfig config, const EGLint *attrib_list);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglReleaseThread (void);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglWaitClient (void);
|
||||
#endif
|
||||
#endif /* EGL_VERSION_1_2 */
|
||||
|
||||
#ifndef EGL_VERSION_1_3
|
||||
#define EGL_VERSION_1_3 1
|
||||
#define EGL_CONFORMANT 0x3042
|
||||
#define EGL_CONTEXT_CLIENT_VERSION 0x3098
|
||||
#define EGL_MATCH_NATIVE_PIXMAP 0x3041
|
||||
#define EGL_OPENGL_ES2_BIT 0x0004
|
||||
#define EGL_VG_ALPHA_FORMAT 0x3088
|
||||
#define EGL_VG_ALPHA_FORMAT_NONPRE 0x308B
|
||||
#define EGL_VG_ALPHA_FORMAT_PRE 0x308C
|
||||
#define EGL_VG_ALPHA_FORMAT_PRE_BIT 0x0040
|
||||
#define EGL_VG_COLORSPACE 0x3087
|
||||
#define EGL_VG_COLORSPACE_sRGB 0x3089
|
||||
#define EGL_VG_COLORSPACE_LINEAR 0x308A
|
||||
#define EGL_VG_COLORSPACE_LINEAR_BIT 0x0020
|
||||
#endif /* EGL_VERSION_1_3 */
|
||||
|
||||
#ifndef EGL_VERSION_1_4
|
||||
#define EGL_VERSION_1_4 1
|
||||
#define EGL_DEFAULT_DISPLAY EGL_CAST(EGLNativeDisplayType,0)
|
||||
#define EGL_MULTISAMPLE_RESOLVE_BOX_BIT 0x0200
|
||||
#define EGL_MULTISAMPLE_RESOLVE 0x3099
|
||||
#define EGL_MULTISAMPLE_RESOLVE_DEFAULT 0x309A
|
||||
#define EGL_MULTISAMPLE_RESOLVE_BOX 0x309B
|
||||
#define EGL_OPENGL_API 0x30A2
|
||||
#define EGL_OPENGL_BIT 0x0008
|
||||
#define EGL_SWAP_BEHAVIOR_PRESERVED_BIT 0x0400
|
||||
typedef EGLContext (EGLAPIENTRYP PFNEGLGETCURRENTCONTEXTPROC) (void);
|
||||
#if EGL_EGL_PROTOTYPES
|
||||
EGLAPI EGLContext EGLAPIENTRY eglGetCurrentContext (void);
|
||||
#endif
|
||||
#endif /* EGL_VERSION_1_4 */
|
||||
|
||||
#ifndef EGL_VERSION_1_5
|
||||
#define EGL_VERSION_1_5 1
|
||||
typedef void *EGLSync;
|
||||
typedef intptr_t EGLAttrib;
|
||||
typedef khronos_utime_nanoseconds_t EGLTime;
|
||||
typedef void *EGLImage;
|
||||
#define EGL_CONTEXT_MAJOR_VERSION 0x3098
|
||||
#define EGL_CONTEXT_MINOR_VERSION 0x30FB
|
||||
#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
|
||||
#define EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY 0x31BD
|
||||
#define EGL_NO_RESET_NOTIFICATION 0x31BE
|
||||
#define EGL_LOSE_CONTEXT_ON_RESET 0x31BF
|
||||
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
|
||||
#define EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT 0x00000002
|
||||
#define EGL_CONTEXT_OPENGL_DEBUG 0x31B0
|
||||
#define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE 0x31B1
|
||||
#define EGL_CONTEXT_OPENGL_ROBUST_ACCESS 0x31B2
|
||||
#define EGL_OPENGL_ES3_BIT 0x00000040
|
||||
#define EGL_CL_EVENT_HANDLE 0x309C
|
||||
#define EGL_SYNC_CL_EVENT 0x30FE
|
||||
#define EGL_SYNC_CL_EVENT_COMPLETE 0x30FF
|
||||
#define EGL_SYNC_PRIOR_COMMANDS_COMPLETE 0x30F0
|
||||
#define EGL_SYNC_TYPE 0x30F7
|
||||
#define EGL_SYNC_STATUS 0x30F1
|
||||
#define EGL_SYNC_CONDITION 0x30F8
|
||||
#define EGL_SIGNALED 0x30F2
|
||||
#define EGL_UNSIGNALED 0x30F3
|
||||
#define EGL_SYNC_FLUSH_COMMANDS_BIT 0x0001
|
||||
#define EGL_FOREVER 0xFFFFFFFFFFFFFFFFull
|
||||
#define EGL_TIMEOUT_EXPIRED 0x30F5
|
||||
#define EGL_CONDITION_SATISFIED 0x30F6
|
||||
#define EGL_NO_SYNC EGL_CAST(EGLSync,0)
|
||||
#define EGL_SYNC_FENCE 0x30F9
|
||||
#define EGL_GL_COLORSPACE 0x309D
|
||||
#define EGL_GL_COLORSPACE_SRGB 0x3089
|
||||
#define EGL_GL_COLORSPACE_LINEAR 0x308A
|
||||
#define EGL_GL_RENDERBUFFER 0x30B9
|
||||
#define EGL_GL_TEXTURE_2D 0x30B1
|
||||
#define EGL_GL_TEXTURE_LEVEL 0x30BC
|
||||
#define EGL_GL_TEXTURE_3D 0x30B2
|
||||
#define EGL_GL_TEXTURE_ZOFFSET 0x30BD
|
||||
#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x30B3
|
||||
#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x30B4
|
||||
#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x30B5
|
||||
#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x30B6
|
||||
#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x30B7
|
||||
#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x30B8
|
||||
#define EGL_IMAGE_PRESERVED 0x30D2
|
||||
#define EGL_NO_IMAGE EGL_CAST(EGLImage,0)
|
||||
typedef EGLSync (EGLAPIENTRYP PFNEGLCREATESYNCPROC) (EGLDisplay dpy, EGLenum type, const EGLAttrib *attrib_list);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYSYNCPROC) (EGLDisplay dpy, EGLSync sync);
|
||||
typedef EGLint (EGLAPIENTRYP PFNEGLCLIENTWAITSYNCPROC) (EGLDisplay dpy, EGLSync sync, EGLint flags, EGLTime timeout);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETSYNCATTRIBPROC) (EGLDisplay dpy, EGLSync sync, EGLint attribute, EGLAttrib *value);
|
||||
typedef EGLImage (EGLAPIENTRYP PFNEGLCREATEIMAGEPROC) (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLAttrib *attrib_list);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYIMAGEPROC) (EGLDisplay dpy, EGLImage image);
|
||||
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYPROC) (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
|
||||
typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPLATFORMWINDOWSURFACEPROC) (EGLDisplay dpy, EGLConfig config, void *native_window, const EGLAttrib *attrib_list);
|
||||
typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPLATFORMPIXMAPSURFACEPROC) (EGLDisplay dpy, EGLConfig config, void *native_pixmap, const EGLAttrib *attrib_list);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLWAITSYNCPROC) (EGLDisplay dpy, EGLSync sync, EGLint flags);
|
||||
#if EGL_EGL_PROTOTYPES
|
||||
EGLAPI EGLSync EGLAPIENTRY eglCreateSync (EGLDisplay dpy, EGLenum type, const EGLAttrib *attrib_list);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglDestroySync (EGLDisplay dpy, EGLSync sync);
|
||||
EGLAPI EGLint EGLAPIENTRY eglClientWaitSync (EGLDisplay dpy, EGLSync sync, EGLint flags, EGLTime timeout);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglGetSyncAttrib (EGLDisplay dpy, EGLSync sync, EGLint attribute, EGLAttrib *value);
|
||||
EGLAPI EGLImage EGLAPIENTRY eglCreateImage (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLAttrib *attrib_list);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglDestroyImage (EGLDisplay dpy, EGLImage image);
|
||||
EGLAPI EGLDisplay EGLAPIENTRY eglGetPlatformDisplay (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
|
||||
EGLAPI EGLSurface EGLAPIENTRY eglCreatePlatformWindowSurface (EGLDisplay dpy, EGLConfig config, void *native_window, const EGLAttrib *attrib_list);
|
||||
EGLAPI EGLSurface EGLAPIENTRY eglCreatePlatformPixmapSurface (EGLDisplay dpy, EGLConfig config, void *native_pixmap, const EGLAttrib *attrib_list);
|
||||
EGLAPI EGLBoolean EGLAPIENTRY eglWaitSync (EGLDisplay dpy, EGLSync sync, EGLint flags);
|
||||
#endif
|
||||
#endif /* EGL_VERSION_1_5 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,182 @@
|
||||
#ifndef __eglplatform_h_
|
||||
#define __eglplatform_h_
|
||||
|
||||
/*
|
||||
** Copyright (c) 2007-2016 The Khronos Group Inc.
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person obtaining a
|
||||
** copy of this software and/or associated documentation files (the
|
||||
** "Materials"), to deal in the Materials without restriction, including
|
||||
** without limitation the rights to use, copy, modify, merge, publish,
|
||||
** distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
** permit persons to whom the Materials are furnished to do so, subject to
|
||||
** the following conditions:
|
||||
**
|
||||
** The above copyright notice and this permission notice shall be included
|
||||
** in all copies or substantial portions of the Materials.
|
||||
**
|
||||
** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
*/
|
||||
|
||||
/* Platform-specific types and definitions for egl.h
|
||||
* $Revision: 30994 $ on $Date: 2015-04-30 13:36:48 -0700 (Thu, 30 Apr 2015) $
|
||||
*
|
||||
* Adopters may modify khrplatform.h and this file to suit their platform.
|
||||
* You are encouraged to submit all modifications to the Khronos group so that
|
||||
* they can be included in future versions of this file. Please submit changes
|
||||
* by sending them to the public Khronos Bugzilla (http://khronos.org/bugzilla)
|
||||
* by filing a bug against product "EGL" component "Registry".
|
||||
*/
|
||||
|
||||
#include <KHR/khrplatform.h>
|
||||
|
||||
/* Macros used in EGL function prototype declarations.
|
||||
*
|
||||
* EGL functions should be prototyped as:
|
||||
*
|
||||
* EGLAPI return-type EGLAPIENTRY eglFunction(arguments);
|
||||
* typedef return-type (EXPAPIENTRYP PFNEGLFUNCTIONPROC) (arguments);
|
||||
*
|
||||
* KHRONOS_APICALL and KHRONOS_APIENTRY are defined in KHR/khrplatform.h
|
||||
*/
|
||||
|
||||
#ifndef EGLAPI
|
||||
#define EGLAPI KHRONOS_APICALL
|
||||
#endif
|
||||
|
||||
#ifndef EGLAPIENTRY
|
||||
#define EGLAPIENTRY KHRONOS_APIENTRY
|
||||
#endif
|
||||
#define EGLAPIENTRYP EGLAPIENTRY*
|
||||
|
||||
#if defined(MESA_EGL_NO_X11_HEADERS) && !defined(EGL_NO_X11)
|
||||
#warning "`MESA_EGL_NO_X11_HEADERS` is deprecated, and doesn't work with the unmodified Khronos header"
|
||||
#warning "Please use `EGL_NO_X11` instead, as `MESA_EGL_NO_X11_HEADERS` will be removed soon"
|
||||
#define EGL_NO_X11
|
||||
#endif
|
||||
|
||||
/* The types NativeDisplayType, NativeWindowType, and NativePixmapType
|
||||
* are aliases of window-system-dependent types, such as X Display * or
|
||||
* Windows Device Context. They must be defined in platform-specific
|
||||
* code below. The EGL-prefixed versions of Native*Type are the same
|
||||
* types, renamed in EGL 1.3 so all types in the API start with "EGL".
|
||||
*
|
||||
* Khronos STRONGLY RECOMMENDS that you use the default definitions
|
||||
* provided below, since these changes affect both binary and source
|
||||
* portability of applications using EGL running on different EGL
|
||||
* implementations.
|
||||
*/
|
||||
|
||||
#if defined(_WIN32) || defined(__VC32__) && !defined(__CYGWIN__) && !defined(__SCITECH_SNAP__) /* Win32 and WinCE */
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN 1
|
||||
#endif
|
||||
#include <windows.h>
|
||||
|
||||
typedef HDC EGLNativeDisplayType;
|
||||
typedef HBITMAP EGLNativePixmapType;
|
||||
typedef HWND EGLNativeWindowType;
|
||||
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
|
||||
typedef int EGLNativeDisplayType;
|
||||
typedef int EGLNativePixmapType;
|
||||
typedef int EGLNativeWindowType;
|
||||
|
||||
#elif defined(__WINSCW__) || defined(__SYMBIAN32__) /* Symbian */
|
||||
|
||||
typedef int EGLNativeDisplayType;
|
||||
typedef void *EGLNativePixmapType;
|
||||
typedef void *EGLNativeWindowType;
|
||||
|
||||
#elif defined(WL_EGL_PLATFORM)
|
||||
|
||||
typedef struct wl_display *EGLNativeDisplayType;
|
||||
typedef struct wl_egl_pixmap *EGLNativePixmapType;
|
||||
typedef struct wl_egl_window *EGLNativeWindowType;
|
||||
|
||||
#elif defined(__GBM__)
|
||||
|
||||
typedef struct gbm_device *EGLNativeDisplayType;
|
||||
typedef struct gbm_bo *EGLNativePixmapType;
|
||||
typedef void *EGLNativeWindowType;
|
||||
|
||||
#elif defined(__ANDROID__) || defined(ANDROID)
|
||||
|
||||
struct ANativeWindow;
|
||||
struct egl_native_pixmap_t;
|
||||
|
||||
typedef void* EGLNativeDisplayType;
|
||||
typedef struct egl_native_pixmap_t* EGLNativePixmapType;
|
||||
typedef struct ANativeWindow* EGLNativeWindowType;
|
||||
|
||||
#elif defined(USE_OZONE)
|
||||
|
||||
typedef intptr_t EGLNativeDisplayType;
|
||||
typedef intptr_t EGLNativePixmapType;
|
||||
typedef intptr_t EGLNativeWindowType;
|
||||
|
||||
#elif defined(__unix__) && defined(EGL_NO_X11)
|
||||
|
||||
typedef void *EGLNativeDisplayType;
|
||||
typedef khronos_uintptr_t EGLNativePixmapType;
|
||||
typedef khronos_uintptr_t EGLNativeWindowType;
|
||||
|
||||
#elif defined(__unix__) || defined(USE_X11)
|
||||
|
||||
/* X11 (tentative) */
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
|
||||
typedef Display *EGLNativeDisplayType;
|
||||
typedef Pixmap EGLNativePixmapType;
|
||||
typedef Window EGLNativeWindowType;
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
|
||||
typedef int EGLNativeDisplayType;
|
||||
typedef void *EGLNativePixmapType;
|
||||
typedef void *EGLNativeWindowType;
|
||||
|
||||
#elif defined(__HAIKU__)
|
||||
|
||||
#include <kernel/image.h>
|
||||
|
||||
typedef void *EGLNativeDisplayType;
|
||||
typedef khronos_uintptr_t EGLNativePixmapType;
|
||||
typedef khronos_uintptr_t EGLNativeWindowType;
|
||||
|
||||
#else
|
||||
#error "Platform not recognized"
|
||||
#endif
|
||||
|
||||
/* EGL 1.2 types, renamed for consistency in EGL 1.3 */
|
||||
typedef EGLNativeDisplayType NativeDisplayType;
|
||||
typedef EGLNativePixmapType NativePixmapType;
|
||||
typedef EGLNativeWindowType NativeWindowType;
|
||||
|
||||
|
||||
/* Define EGLint. This must be a signed integral type large enough to contain
|
||||
* all legal attribute names and values passed into and out of EGL, whether
|
||||
* their type is boolean, bitmask, enumerant (symbolic constant), integer,
|
||||
* handle, or other. While in general a 32-bit integer will suffice, if
|
||||
* handles are 64 bit types, then EGLint should be defined as a signed 64-bit
|
||||
* integer type.
|
||||
*/
|
||||
typedef khronos_int32_t EGLint;
|
||||
|
||||
|
||||
/* C++ / C typecast macros for special EGL handle values */
|
||||
#if defined(__cplusplus)
|
||||
#define EGL_CAST(type, value) (static_cast<type>(value))
|
||||
#else
|
||||
#define EGL_CAST(type, value) ((type) (value))
|
||||
#endif
|
||||
|
||||
#endif /* __eglplatform_h */
|
||||
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* SGI FREE SOFTWARE LICENSE B (Version 2.0, Sept. 18, 2008)
|
||||
* Copyright (C) 1991-2000 Silicon Graphics, Inc. All Rights Reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice including the dates of first publication and
|
||||
* either this permission notice or a reference to
|
||||
* http://oss.sgi.com/projects/FreeB/
|
||||
* shall be included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* SILICON GRAPHICS, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* Except as contained in this notice, the name of Silicon Graphics, Inc.
|
||||
* shall not be used in advertising or otherwise to promote the sale, use or
|
||||
* other dealings in this Software without prior written authorization from
|
||||
* Silicon Graphics, Inc.
|
||||
*/
|
||||
|
||||
#ifndef __glu_h__
|
||||
#define __glu_h__
|
||||
|
||||
#if defined(USE_MGL_NAMESPACE)
|
||||
#include "glu_mangle.h"
|
||||
#endif
|
||||
|
||||
#include <GL/gl.h>
|
||||
|
||||
#ifndef GLAPIENTRY
|
||||
#if defined(_MSC_VER) || defined(__MINGW32__)
|
||||
#define GLAPIENTRY __stdcall
|
||||
#else
|
||||
#define GLAPIENTRY
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef GLAPIENTRYP
|
||||
#define GLAPIENTRYP GLAPIENTRY *
|
||||
#endif
|
||||
|
||||
#if (defined(_MSC_VER) || defined(__MINGW32__)) && defined(BUILD_GLU32)
|
||||
# undef GLAPI
|
||||
# define GLAPI __declspec(dllexport)
|
||||
#elif (defined(_MSC_VER) || defined(__MINGW32__)) && defined(_DLL)
|
||||
/* tag specifying we're building for DLL runtime support */
|
||||
# undef GLAPI
|
||||
# define GLAPI __declspec(dllimport)
|
||||
#elif !defined(GLAPI)
|
||||
/* for use with static link lib build of Win32 edition only */
|
||||
# define GLAPI extern
|
||||
#endif /* _STATIC_MESA support */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*************************************************************/
|
||||
|
||||
/* Extensions */
|
||||
#define GLU_EXT_object_space_tess 1
|
||||
#define GLU_EXT_nurbs_tessellator 1
|
||||
|
||||
/* Boolean */
|
||||
#define GLU_FALSE 0
|
||||
#define GLU_TRUE 1
|
||||
|
||||
/* Version */
|
||||
#define GLU_VERSION_1_1 1
|
||||
#define GLU_VERSION_1_2 1
|
||||
#define GLU_VERSION_1_3 1
|
||||
|
||||
/* StringName */
|
||||
#define GLU_VERSION 100800
|
||||
#define GLU_EXTENSIONS 100801
|
||||
|
||||
/* ErrorCode */
|
||||
#define GLU_INVALID_ENUM 100900
|
||||
#define GLU_INVALID_VALUE 100901
|
||||
#define GLU_OUT_OF_MEMORY 100902
|
||||
#define GLU_INCOMPATIBLE_GL_VERSION 100903
|
||||
#define GLU_INVALID_OPERATION 100904
|
||||
|
||||
/* NurbsDisplay */
|
||||
/* GLU_FILL */
|
||||
#define GLU_OUTLINE_POLYGON 100240
|
||||
#define GLU_OUTLINE_PATCH 100241
|
||||
|
||||
/* NurbsCallback */
|
||||
#define GLU_NURBS_ERROR 100103
|
||||
#define GLU_ERROR 100103
|
||||
#define GLU_NURBS_BEGIN 100164
|
||||
#define GLU_NURBS_BEGIN_EXT 100164
|
||||
#define GLU_NURBS_VERTEX 100165
|
||||
#define GLU_NURBS_VERTEX_EXT 100165
|
||||
#define GLU_NURBS_NORMAL 100166
|
||||
#define GLU_NURBS_NORMAL_EXT 100166
|
||||
#define GLU_NURBS_COLOR 100167
|
||||
#define GLU_NURBS_COLOR_EXT 100167
|
||||
#define GLU_NURBS_TEXTURE_COORD 100168
|
||||
#define GLU_NURBS_TEX_COORD_EXT 100168
|
||||
#define GLU_NURBS_END 100169
|
||||
#define GLU_NURBS_END_EXT 100169
|
||||
#define GLU_NURBS_BEGIN_DATA 100170
|
||||
#define GLU_NURBS_BEGIN_DATA_EXT 100170
|
||||
#define GLU_NURBS_VERTEX_DATA 100171
|
||||
#define GLU_NURBS_VERTEX_DATA_EXT 100171
|
||||
#define GLU_NURBS_NORMAL_DATA 100172
|
||||
#define GLU_NURBS_NORMAL_DATA_EXT 100172
|
||||
#define GLU_NURBS_COLOR_DATA 100173
|
||||
#define GLU_NURBS_COLOR_DATA_EXT 100173
|
||||
#define GLU_NURBS_TEXTURE_COORD_DATA 100174
|
||||
#define GLU_NURBS_TEX_COORD_DATA_EXT 100174
|
||||
#define GLU_NURBS_END_DATA 100175
|
||||
#define GLU_NURBS_END_DATA_EXT 100175
|
||||
|
||||
/* NurbsError */
|
||||
#define GLU_NURBS_ERROR1 100251
|
||||
#define GLU_NURBS_ERROR2 100252
|
||||
#define GLU_NURBS_ERROR3 100253
|
||||
#define GLU_NURBS_ERROR4 100254
|
||||
#define GLU_NURBS_ERROR5 100255
|
||||
#define GLU_NURBS_ERROR6 100256
|
||||
#define GLU_NURBS_ERROR7 100257
|
||||
#define GLU_NURBS_ERROR8 100258
|
||||
#define GLU_NURBS_ERROR9 100259
|
||||
#define GLU_NURBS_ERROR10 100260
|
||||
#define GLU_NURBS_ERROR11 100261
|
||||
#define GLU_NURBS_ERROR12 100262
|
||||
#define GLU_NURBS_ERROR13 100263
|
||||
#define GLU_NURBS_ERROR14 100264
|
||||
#define GLU_NURBS_ERROR15 100265
|
||||
#define GLU_NURBS_ERROR16 100266
|
||||
#define GLU_NURBS_ERROR17 100267
|
||||
#define GLU_NURBS_ERROR18 100268
|
||||
#define GLU_NURBS_ERROR19 100269
|
||||
#define GLU_NURBS_ERROR20 100270
|
||||
#define GLU_NURBS_ERROR21 100271
|
||||
#define GLU_NURBS_ERROR22 100272
|
||||
#define GLU_NURBS_ERROR23 100273
|
||||
#define GLU_NURBS_ERROR24 100274
|
||||
#define GLU_NURBS_ERROR25 100275
|
||||
#define GLU_NURBS_ERROR26 100276
|
||||
#define GLU_NURBS_ERROR27 100277
|
||||
#define GLU_NURBS_ERROR28 100278
|
||||
#define GLU_NURBS_ERROR29 100279
|
||||
#define GLU_NURBS_ERROR30 100280
|
||||
#define GLU_NURBS_ERROR31 100281
|
||||
#define GLU_NURBS_ERROR32 100282
|
||||
#define GLU_NURBS_ERROR33 100283
|
||||
#define GLU_NURBS_ERROR34 100284
|
||||
#define GLU_NURBS_ERROR35 100285
|
||||
#define GLU_NURBS_ERROR36 100286
|
||||
#define GLU_NURBS_ERROR37 100287
|
||||
|
||||
/* NurbsProperty */
|
||||
#define GLU_AUTO_LOAD_MATRIX 100200
|
||||
#define GLU_CULLING 100201
|
||||
#define GLU_SAMPLING_TOLERANCE 100203
|
||||
#define GLU_DISPLAY_MODE 100204
|
||||
#define GLU_PARAMETRIC_TOLERANCE 100202
|
||||
#define GLU_SAMPLING_METHOD 100205
|
||||
#define GLU_U_STEP 100206
|
||||
#define GLU_V_STEP 100207
|
||||
#define GLU_NURBS_MODE 100160
|
||||
#define GLU_NURBS_MODE_EXT 100160
|
||||
#define GLU_NURBS_TESSELLATOR 100161
|
||||
#define GLU_NURBS_TESSELLATOR_EXT 100161
|
||||
#define GLU_NURBS_RENDERER 100162
|
||||
#define GLU_NURBS_RENDERER_EXT 100162
|
||||
|
||||
/* NurbsSampling */
|
||||
#define GLU_OBJECT_PARAMETRIC_ERROR 100208
|
||||
#define GLU_OBJECT_PARAMETRIC_ERROR_EXT 100208
|
||||
#define GLU_OBJECT_PATH_LENGTH 100209
|
||||
#define GLU_OBJECT_PATH_LENGTH_EXT 100209
|
||||
#define GLU_PATH_LENGTH 100215
|
||||
#define GLU_PARAMETRIC_ERROR 100216
|
||||
#define GLU_DOMAIN_DISTANCE 100217
|
||||
|
||||
/* NurbsTrim */
|
||||
#define GLU_MAP1_TRIM_2 100210
|
||||
#define GLU_MAP1_TRIM_3 100211
|
||||
|
||||
/* QuadricDrawStyle */
|
||||
#define GLU_POINT 100010
|
||||
#define GLU_LINE 100011
|
||||
#define GLU_FILL 100012
|
||||
#define GLU_SILHOUETTE 100013
|
||||
|
||||
/* QuadricCallback */
|
||||
/* GLU_ERROR */
|
||||
|
||||
/* QuadricNormal */
|
||||
#define GLU_SMOOTH 100000
|
||||
#define GLU_FLAT 100001
|
||||
#define GLU_NONE 100002
|
||||
|
||||
/* QuadricOrientation */
|
||||
#define GLU_OUTSIDE 100020
|
||||
#define GLU_INSIDE 100021
|
||||
|
||||
/* TessCallback */
|
||||
#define GLU_TESS_BEGIN 100100
|
||||
#define GLU_BEGIN 100100
|
||||
#define GLU_TESS_VERTEX 100101
|
||||
#define GLU_VERTEX 100101
|
||||
#define GLU_TESS_END 100102
|
||||
#define GLU_END 100102
|
||||
#define GLU_TESS_ERROR 100103
|
||||
#define GLU_TESS_EDGE_FLAG 100104
|
||||
#define GLU_EDGE_FLAG 100104
|
||||
#define GLU_TESS_COMBINE 100105
|
||||
#define GLU_TESS_BEGIN_DATA 100106
|
||||
#define GLU_TESS_VERTEX_DATA 100107
|
||||
#define GLU_TESS_END_DATA 100108
|
||||
#define GLU_TESS_ERROR_DATA 100109
|
||||
#define GLU_TESS_EDGE_FLAG_DATA 100110
|
||||
#define GLU_TESS_COMBINE_DATA 100111
|
||||
|
||||
/* TessContour */
|
||||
#define GLU_CW 100120
|
||||
#define GLU_CCW 100121
|
||||
#define GLU_INTERIOR 100122
|
||||
#define GLU_EXTERIOR 100123
|
||||
#define GLU_UNKNOWN 100124
|
||||
|
||||
/* TessProperty */
|
||||
#define GLU_TESS_WINDING_RULE 100140
|
||||
#define GLU_TESS_BOUNDARY_ONLY 100141
|
||||
#define GLU_TESS_TOLERANCE 100142
|
||||
|
||||
/* TessError */
|
||||
#define GLU_TESS_ERROR1 100151
|
||||
#define GLU_TESS_ERROR2 100152
|
||||
#define GLU_TESS_ERROR3 100153
|
||||
#define GLU_TESS_ERROR4 100154
|
||||
#define GLU_TESS_ERROR5 100155
|
||||
#define GLU_TESS_ERROR6 100156
|
||||
#define GLU_TESS_ERROR7 100157
|
||||
#define GLU_TESS_ERROR8 100158
|
||||
#define GLU_TESS_MISSING_BEGIN_POLYGON 100151
|
||||
#define GLU_TESS_MISSING_BEGIN_CONTOUR 100152
|
||||
#define GLU_TESS_MISSING_END_POLYGON 100153
|
||||
#define GLU_TESS_MISSING_END_CONTOUR 100154
|
||||
#define GLU_TESS_COORD_TOO_LARGE 100155
|
||||
#define GLU_TESS_NEED_COMBINE_CALLBACK 100156
|
||||
|
||||
/* TessWinding */
|
||||
#define GLU_TESS_WINDING_ODD 100130
|
||||
#define GLU_TESS_WINDING_NONZERO 100131
|
||||
#define GLU_TESS_WINDING_POSITIVE 100132
|
||||
#define GLU_TESS_WINDING_NEGATIVE 100133
|
||||
#define GLU_TESS_WINDING_ABS_GEQ_TWO 100134
|
||||
|
||||
/*************************************************************/
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
class GLUnurbs;
|
||||
class GLUquadric;
|
||||
class GLUtesselator;
|
||||
#else
|
||||
typedef struct GLUnurbs GLUnurbs;
|
||||
typedef struct GLUquadric GLUquadric;
|
||||
typedef struct GLUtesselator GLUtesselator;
|
||||
#endif
|
||||
|
||||
typedef GLUnurbs GLUnurbsObj;
|
||||
typedef GLUquadric GLUquadricObj;
|
||||
typedef GLUtesselator GLUtesselatorObj;
|
||||
typedef GLUtesselator GLUtriangulatorObj;
|
||||
|
||||
#define GLU_TESS_MAX_COORD 1.0e150
|
||||
|
||||
/* Internal convenience typedefs */
|
||||
typedef void (GLAPIENTRYP _GLUfuncptr)(void);
|
||||
|
||||
GLAPI void GLAPIENTRY gluBeginCurve (GLUnurbs* nurb);
|
||||
GLAPI void GLAPIENTRY gluBeginPolygon (GLUtesselator* tess);
|
||||
GLAPI void GLAPIENTRY gluBeginSurface (GLUnurbs* nurb);
|
||||
GLAPI void GLAPIENTRY gluBeginTrim (GLUnurbs* nurb);
|
||||
GLAPI GLint GLAPIENTRY gluBuild1DMipmapLevels (GLenum target, GLint internalFormat, GLsizei width, GLenum format, GLenum type, GLint level, GLint base, GLint max, const void *data);
|
||||
GLAPI GLint GLAPIENTRY gluBuild1DMipmaps (GLenum target, GLint internalFormat, GLsizei width, GLenum format, GLenum type, const void *data);
|
||||
GLAPI GLint GLAPIENTRY gluBuild2DMipmapLevels (GLenum target, GLint internalFormat, GLsizei width, GLsizei height, GLenum format, GLenum type, GLint level, GLint base, GLint max, const void *data);
|
||||
GLAPI GLint GLAPIENTRY gluBuild2DMipmaps (GLenum target, GLint internalFormat, GLsizei width, GLsizei height, GLenum format, GLenum type, const void *data);
|
||||
GLAPI GLint GLAPIENTRY gluBuild3DMipmapLevels (GLenum target, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, GLint level, GLint base, GLint max, const void *data);
|
||||
GLAPI GLint GLAPIENTRY gluBuild3DMipmaps (GLenum target, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void *data);
|
||||
GLAPI GLboolean GLAPIENTRY gluCheckExtension (const GLubyte *extName, const GLubyte *extString);
|
||||
GLAPI void GLAPIENTRY gluCylinder (GLUquadric* quad, GLdouble base, GLdouble top, GLdouble height, GLint slices, GLint stacks);
|
||||
GLAPI void GLAPIENTRY gluDeleteNurbsRenderer (GLUnurbs* nurb);
|
||||
GLAPI void GLAPIENTRY gluDeleteQuadric (GLUquadric* quad);
|
||||
GLAPI void GLAPIENTRY gluDeleteTess (GLUtesselator* tess);
|
||||
GLAPI void GLAPIENTRY gluDisk (GLUquadric* quad, GLdouble inner, GLdouble outer, GLint slices, GLint loops);
|
||||
GLAPI void GLAPIENTRY gluEndCurve (GLUnurbs* nurb);
|
||||
GLAPI void GLAPIENTRY gluEndPolygon (GLUtesselator* tess);
|
||||
GLAPI void GLAPIENTRY gluEndSurface (GLUnurbs* nurb);
|
||||
GLAPI void GLAPIENTRY gluEndTrim (GLUnurbs* nurb);
|
||||
GLAPI const GLubyte * GLAPIENTRY gluErrorString (GLenum error);
|
||||
GLAPI void GLAPIENTRY gluGetNurbsProperty (GLUnurbs* nurb, GLenum property, GLfloat* data);
|
||||
GLAPI const GLubyte * GLAPIENTRY gluGetString (GLenum name);
|
||||
GLAPI void GLAPIENTRY gluGetTessProperty (GLUtesselator* tess, GLenum which, GLdouble* data);
|
||||
GLAPI void GLAPIENTRY gluLoadSamplingMatrices (GLUnurbs* nurb, const GLfloat *model, const GLfloat *perspective, const GLint *view);
|
||||
GLAPI void GLAPIENTRY gluLookAt (GLdouble eyeX, GLdouble eyeY, GLdouble eyeZ, GLdouble centerX, GLdouble centerY, GLdouble centerZ, GLdouble upX, GLdouble upY, GLdouble upZ);
|
||||
GLAPI GLUnurbs* GLAPIENTRY gluNewNurbsRenderer (void);
|
||||
GLAPI GLUquadric* GLAPIENTRY gluNewQuadric (void);
|
||||
GLAPI GLUtesselator* GLAPIENTRY gluNewTess (void);
|
||||
GLAPI void GLAPIENTRY gluNextContour (GLUtesselator* tess, GLenum type);
|
||||
GLAPI void GLAPIENTRY gluNurbsCallback (GLUnurbs* nurb, GLenum which, _GLUfuncptr CallBackFunc);
|
||||
GLAPI void GLAPIENTRY gluNurbsCallbackData (GLUnurbs* nurb, GLvoid* userData);
|
||||
GLAPI void GLAPIENTRY gluNurbsCallbackDataEXT (GLUnurbs* nurb, GLvoid* userData);
|
||||
GLAPI void GLAPIENTRY gluNurbsCurve (GLUnurbs* nurb, GLint knotCount, GLfloat *knots, GLint stride, GLfloat *control, GLint order, GLenum type);
|
||||
GLAPI void GLAPIENTRY gluNurbsProperty (GLUnurbs* nurb, GLenum property, GLfloat value);
|
||||
GLAPI void GLAPIENTRY gluNurbsSurface (GLUnurbs* nurb, GLint sKnotCount, GLfloat* sKnots, GLint tKnotCount, GLfloat* tKnots, GLint sStride, GLint tStride, GLfloat* control, GLint sOrder, GLint tOrder, GLenum type);
|
||||
GLAPI void GLAPIENTRY gluOrtho2D (GLdouble left, GLdouble right, GLdouble bottom, GLdouble top);
|
||||
GLAPI void GLAPIENTRY gluPartialDisk (GLUquadric* quad, GLdouble inner, GLdouble outer, GLint slices, GLint loops, GLdouble start, GLdouble sweep);
|
||||
GLAPI void GLAPIENTRY gluPerspective (GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar);
|
||||
GLAPI void GLAPIENTRY gluPickMatrix (GLdouble x, GLdouble y, GLdouble delX, GLdouble delY, GLint *viewport);
|
||||
GLAPI GLint GLAPIENTRY gluProject (GLdouble objX, GLdouble objY, GLdouble objZ, const GLdouble *model, const GLdouble *proj, const GLint *view, GLdouble* winX, GLdouble* winY, GLdouble* winZ);
|
||||
GLAPI void GLAPIENTRY gluPwlCurve (GLUnurbs* nurb, GLint count, GLfloat* data, GLint stride, GLenum type);
|
||||
GLAPI void GLAPIENTRY gluQuadricCallback (GLUquadric* quad, GLenum which, _GLUfuncptr CallBackFunc);
|
||||
GLAPI void GLAPIENTRY gluQuadricDrawStyle (GLUquadric* quad, GLenum draw);
|
||||
GLAPI void GLAPIENTRY gluQuadricNormals (GLUquadric* quad, GLenum normal);
|
||||
GLAPI void GLAPIENTRY gluQuadricOrientation (GLUquadric* quad, GLenum orientation);
|
||||
GLAPI void GLAPIENTRY gluQuadricTexture (GLUquadric* quad, GLboolean texture);
|
||||
GLAPI GLint GLAPIENTRY gluScaleImage (GLenum format, GLsizei wIn, GLsizei hIn, GLenum typeIn, const void *dataIn, GLsizei wOut, GLsizei hOut, GLenum typeOut, GLvoid* dataOut);
|
||||
GLAPI void GLAPIENTRY gluSphere (GLUquadric* quad, GLdouble radius, GLint slices, GLint stacks);
|
||||
GLAPI void GLAPIENTRY gluTessBeginContour (GLUtesselator* tess);
|
||||
GLAPI void GLAPIENTRY gluTessBeginPolygon (GLUtesselator* tess, GLvoid* data);
|
||||
GLAPI void GLAPIENTRY gluTessCallback (GLUtesselator* tess, GLenum which, _GLUfuncptr CallBackFunc);
|
||||
GLAPI void GLAPIENTRY gluTessEndContour (GLUtesselator* tess);
|
||||
GLAPI void GLAPIENTRY gluTessEndPolygon (GLUtesselator* tess);
|
||||
GLAPI void GLAPIENTRY gluTessNormal (GLUtesselator* tess, GLdouble valueX, GLdouble valueY, GLdouble valueZ);
|
||||
GLAPI void GLAPIENTRY gluTessProperty (GLUtesselator* tess, GLenum which, GLdouble data);
|
||||
GLAPI void GLAPIENTRY gluTessVertex (GLUtesselator* tess, GLdouble *location, GLvoid* data);
|
||||
GLAPI GLint GLAPIENTRY gluUnProject (GLdouble winX, GLdouble winY, GLdouble winZ, const GLdouble *model, const GLdouble *proj, const GLint *view, GLdouble* objX, GLdouble* objY, GLdouble* objZ);
|
||||
GLAPI GLint GLAPIENTRY gluUnProject4 (GLdouble winX, GLdouble winY, GLdouble winZ, GLdouble clipW, const GLdouble *model, const GLdouble *proj, const GLint *view, GLdouble nearVal, GLdouble farVal, GLdouble* objX, GLdouble* objY, GLdouble* objZ, GLdouble* objW);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __glu_h__ */
|
||||