mirror of
https://github.com/bearstonem/openmohaa-vr.git
synced 2026-10-23 09:00:19 +02:00
Compare commits
43
Commits
main
...
vr-quest3-0.1.0
No files matched your search
@@ -14,6 +14,9 @@ project(${PROJECT_NAME} VERSION ${PROJECT_VERSION} LANGUAGES C CXX)
|
||||
option(BUILD_SERVER "Build dedicated server" ON)
|
||||
option(BUILD_CLIENT "Build client" ON)
|
||||
option(BUILD_RENDERER_GL1 "Build GL1 renderer" ON)
|
||||
# The original fixed function renderer needs a translation layer to reach
|
||||
# OpenGL ES; see cmake/libraries/gl4es.cmake for why it is worth having.
|
||||
option(USE_GL4ES "Translate the GL1 renderer's fixed function calls to OpenGL ES" ${ANDROID})
|
||||
option(BUILD_RENDERER_GL2 "Build GL2 renderer" OFF)
|
||||
option(BUILD_GAME_LIBRARIES "Build game module libraries" ON)
|
||||
option(BUILD_GAME_QVMS "Build game module qvms" ON)
|
||||
@@ -21,6 +24,7 @@ option(BUILD_STANDALONE "Build binaries for standalone games" OFF)
|
||||
|
||||
option(USE_RENDERER_DLOPEN "Dynamically load the renderer(s)" ON)
|
||||
option(USE_OPENAL "OpenAL audio" ON)
|
||||
option(USE_OPENXR "OpenXR stereo rendering and tracking" OFF)
|
||||
option(USE_OPENAL_DLOPEN "Dynamically load OpenAL" ON)
|
||||
option(USE_HTTP "HTTP download support" ON)
|
||||
option(USE_CODEC_VORBIS "Ogg Vorbis support" ON)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+249
@@ -0,0 +1,249 @@
|
||||
# OpenMoHAA on Quest 3 — porting plan
|
||||
|
||||
Getting Medal of Honor: Allied Assault running natively on a Meta Quest 3 in
|
||||
VR, by way of an Android port first.
|
||||
|
||||
Written after surveying the tree at `a2f34019`, and carrying the lessons from
|
||||
a completed Quest 3 VR port of Homeworld (1999) — the mistakes noted at the
|
||||
end are ones already made once and worth not repeating.
|
||||
|
||||
---
|
||||
|
||||
## 1. What we are starting from
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Lineage | ioquake3-derived (`renderergl1`/`renderergl2`, `qcommon`, `botlib`, QVM options) |
|
||||
| Build | CMake ≥ 3.25, modular per-directory `CMakeLists.txt` |
|
||||
| Platform layer | **SDL2** (`code/sdl/`: glimp, input, sound, gamma, mouse) |
|
||||
| Renderers | `renderergl1` (fixed function), `renderergl2` (GLSL) |
|
||||
| Game logic | C++ (`fgame`, `cgame`, `uilib`, `skeletor`, `tiki`) — not Q3-style QVM gameplay |
|
||||
| Android support | **none whatsoever** |
|
||||
| Game data | Retail MOH:AA required (GOG recommended); a demo exists |
|
||||
|
||||
### The finding that shapes everything
|
||||
|
||||
**`renderergl2` already speaks OpenGL ES.** This is inherited from upstream
|
||||
ioquake3 and is in far better shape than expected:
|
||||
|
||||
- `qglesMajorVersion` / `qglesMinorVersion` are real runtime variables
|
||||
(`code/renderercommon/qgl.h`), with a `QGLES_VERSION_ATLEAST` macro
|
||||
- `tr_glsl.c` emits `#version 300 es` when ES 3 is present, `#version 100`
|
||||
for ES 2, and injects `precision mediump float` / `precision mediump
|
||||
sampler2DShadow`
|
||||
- `tr_extensions.c` branches on `qglesMajorVersion` when probing extensions
|
||||
- `sdl_glimp.c` has an **`r_preferOpenGLES` cvar** and will request
|
||||
`SDL_GL_CONTEXT_PROFILE_ES` contexts
|
||||
|
||||
This is the single biggest difference from the Homeworld port, where the
|
||||
engine only spoke fixed-function GL 1.x and needed **gl4es** (a GL-over-GLES2
|
||||
translation layer) underneath everything — which then dictated every later
|
||||
constraint. Here the plan is to drive `renderergl2` at GLES 3 directly, so:
|
||||
|
||||
- no translation layer, no gl4es quirks
|
||||
- real shaders, so normal mapping and post-processing stay on the table
|
||||
- FBOs are already first class (`tr_fbo.c`), which matters enormously for
|
||||
stereo rendering — Homeworld's port never got this and it blocked MSAA
|
||||
|
||||
**`renderergl1` should be considered out of scope.** It is fixed-function and
|
||||
would need the same emulation layer we are avoiding. Build only `renderergl2`
|
||||
(`-DBUILD_RENDERER_GL1=OFF -DBUILD_RENDERER_GL2=ON`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Phase 1 — build for Android
|
||||
|
||||
Goal: an APK that launches, logs, and reaches the main menu. No VR yet.
|
||||
|
||||
1. **Toolchain.** CMake has first-class NDK support via
|
||||
`android.toolchain.cmake`; unlike Homeworld's hand-written meson cross
|
||||
file, this is a supported path. Target `arm64-v8a`, `android-29` or later
|
||||
(Quest 3 is Android 12).
|
||||
2. **SDL2 for Android.** Build SDL2 for `arm64-v8a` and use the stock
|
||||
`org.libsdl.app.SDLActivity` to load the engine as `libmain.so`. The
|
||||
existing `code/sdl/*` should mostly work unchanged — this is the same
|
||||
arrangement the Homeworld port used successfully.
|
||||
3. **Prune the dependency surface.** Decide early, since each is a
|
||||
cross-compile:
|
||||
- `USE_OPENAL` — Quest wants low-latency audio; OpenAL Soft builds for
|
||||
Android, but SDL audio may be simpler for a first pass
|
||||
- `USE_CURL` / `USE_HTTP` — off initially, it is only for downloads
|
||||
- `USE_CODEC_VORBIS` / `OPUS` / `MAD` — MOH:AA music needs at least one;
|
||||
check what the retail data actually ships
|
||||
- `BUILD_SERVER` — off for the client APK
|
||||
4. **Game modules.** `BUILD_GAME_LIBRARIES` vs `BUILD_GAME_QVMS`. Native
|
||||
`.so` modules via `dlopen` work on Android, but Android's linker
|
||||
namespaces are fussy about paths — consider **static linking the game
|
||||
modules into the main binary** for the first port and revisiting later.
|
||||
5. **Filesystem.** Point the base path at the app's external files directory
|
||||
(`SDL_AndroidGetExternalStoragePath()`), and `chdir()` there early —
|
||||
exactly what the Homeworld port does. The user side-loads `main/pak*.pk3`
|
||||
into it.
|
||||
6. **Force the ES path.** Set `r_preferOpenGLES 1` and confirm
|
||||
`qglesMajorVersion` comes back as 3. This is the moment of truth for the
|
||||
whole approach and should be validated before anything else is built on
|
||||
top.
|
||||
|
||||
**Exit criteria:** APK boots, logs to `logcat`, reaches the main menu with
|
||||
retail data side-loaded, renders through `renderergl2` on a GLES 3 context.
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 2 — playable flat on the headset
|
||||
|
||||
Goal: the game is genuinely playable as a 2D panel app before VR is attempted.
|
||||
This is deliberately a separate milestone; debugging engine problems is far
|
||||
easier without a stereo compositor in the way.
|
||||
|
||||
- **Input:** Quest Touch controllers appear to SDL as a game controller.
|
||||
Map to the existing `sdl_input.c` joystick path first; a bespoke mapping
|
||||
comes later with VR.
|
||||
- **Performance baseline:** measure frame time with the flat renderer before
|
||||
stereo triples the cost. Establish what the Quest 3 GPU actually delivers on
|
||||
a large MOH:AA map — this number decides how ambitious VR can be.
|
||||
- **Config and saves:** confirm they write to the external files dir, and that
|
||||
the directory is app-owned (see the ownership trap in §6).
|
||||
- **Audio:** verify no dropouts; Android audio callbacks are less forgiving
|
||||
than desktop.
|
||||
|
||||
**Exit criteria:** a mission is completable, flat, on the headset, at a stable
|
||||
frame rate.
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase 3 — stereo via OpenXR
|
||||
|
||||
Goal: the world renders in tracked stereo at 72 fps.
|
||||
|
||||
1. **Session and swapchains.** Link the Khronos `openxr_loader` for Android,
|
||||
create an instance with `XR_KHR_opengl_es_enable`, and drive
|
||||
`xrWaitFrame` / `xrBeginFrame` / `xrEndFrame` from the engine's frame loop.
|
||||
2. **Two eye passes into FBOs.** `renderergl2` already renders into
|
||||
framebuffer objects (`tr_fbo.c`), so each eye can target an OpenXR
|
||||
swapchain image directly. **This is the piece the Homeworld port never
|
||||
built**, and its absence there blocked MSAA and forced awkward blits — do
|
||||
it properly from the start here.
|
||||
3. **View and projection from OpenXR.** Replace the engine's projection setup
|
||||
with the per-eye FOV and pose from `xrLocateViews`. In id Tech the camera
|
||||
comes from `refdef` — feed eye offsets in there rather than hacking the
|
||||
projection matrix downstream.
|
||||
4. **Do not render the world three times.** The Homeworld port ended up with a
|
||||
mono pass plus two eye passes because the 2D UI and camera capture were
|
||||
entangled with the main view. Design the frame as *exactly two* eye passes
|
||||
from day one; anything needing a flat image should read from an eye buffer
|
||||
or render separately.
|
||||
5. **The 2D UI.** MOH:AA's HUD and menus draw in screen space. Two options,
|
||||
probably both eventually: render them to a texture and present as an
|
||||
OpenXR quad layer (crisp, comfortable, good for menus), or project them
|
||||
into the world (better for the in-game HUD).
|
||||
|
||||
**Exit criteria:** stereo world, correct scale and IPD, head tracking with no
|
||||
mismatch, stable 72 fps, menus readable.
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 4 — VR interaction design
|
||||
|
||||
This is the largest and least mechanical phase, and it is where an FPS differs
|
||||
completely from the RTS port. A wrong choice here causes motion sickness
|
||||
rather than a bug report.
|
||||
|
||||
**Locomotion — the central question.**
|
||||
- Smooth locomotion with comfort options (vignette, snap turn) is the
|
||||
expected default for a shooter, but MOH:AA's level design assumes a fast,
|
||||
free-running player
|
||||
- Teleport is comfortable but destroys the pacing of firefights
|
||||
- Likely: smooth movement, snap *and* smooth turn as options, adjustable
|
||||
vignette. Make all of it a cvar so it can be tuned on device.
|
||||
|
||||
**Weapon handling.**
|
||||
- Aim from the controller, not the head — this is non-negotiable for an FPS
|
||||
- Two-handed hold for rifles is a large fidelity win and a large amount of
|
||||
work; consider one-handed first
|
||||
- Iron sights become genuinely physical: the sight has to line up with the eye
|
||||
- Reloading: gesture vs button. Button first.
|
||||
|
||||
**The player body and camera.**
|
||||
- MOH:AA moves the view for scripted sequences, cutscenes, ladder climbs and
|
||||
death animations. **Any engine-driven camera motion is a nausea risk in
|
||||
VR** and every instance needs auditing. This is likely the single largest
|
||||
source of unpleasant surprises.
|
||||
- Player height: real standing height vs the engine's fixed viewheight
|
||||
- Crouch: physical vs button, ideally both
|
||||
|
||||
**HUD.**
|
||||
- Screen-locked HUD elements are uncomfortable in VR
|
||||
- Ammo and health on the weapon or wrist, as the Homeworld port did with
|
||||
wrist cards — that worked well and is worth reusing
|
||||
- Objectives and subtitles on a comfortable head-relative panel
|
||||
|
||||
**Exit criteria:** a mission playable end to end, comfortably, by someone who
|
||||
is not the developer.
|
||||
|
||||
---
|
||||
|
||||
## 6. Traps carried from the Homeworld Quest port
|
||||
|
||||
Hard-won, all of them cost hours:
|
||||
|
||||
- **Never `adb shell am start` to launch after deploying.** It intermittently
|
||||
strands the headset in Meta's loading environment. Install, then launch from
|
||||
inside the headset.
|
||||
- **`adb push` cannot create directories inside an app's data folder**, and
|
||||
`chmod` must come *after* pushing or adb fails with `remote fchown failed`.
|
||||
- **A directory created by `adb shell mkdir` is mode 2770 owned by `shell`**,
|
||||
which the app cannot even traverse into — it fails *silently*. `chmod 775`
|
||||
afterwards. Files pushed by adb are 644 and world-readable, which is why
|
||||
data files work while directories do not.
|
||||
- **Verify your logging works before relying on it.** A crash handler that has
|
||||
been silently displaced reports nothing, and is indistinguishable from a
|
||||
crash that never happened. Add a startup self-test that exercises the same
|
||||
path.
|
||||
- **`tombstoned` may not write backtraces to logcat.** When it does not,
|
||||
`dumpsys activity exit-info` still reports the signal, and an in-process
|
||||
handler using `_Unwind_Backtrace` + `dladdr` can report the stack itself —
|
||||
but it needs a real `sigaltstack`, or a stack overflow silences it.
|
||||
- **Only one process may hold an OpenXR session.** If the previous run has not
|
||||
fully exited, the new one falls back to flat mono rendering with no error.
|
||||
- **The headset must be awake** or the session never reaches
|
||||
`XR_SESSION_STATE_FOCUSED` and the launch appears to hang.
|
||||
- **Wireless adb ports change** every time debugging is toggled, and mDNS can
|
||||
advertise a dead one.
|
||||
|
||||
---
|
||||
|
||||
## 7. Open questions to settle early
|
||||
|
||||
1. **Does `renderergl2` actually initialise on a Quest GLES 3 context?**
|
||||
Everything above rests on this. Validate in Phase 1 before building more.
|
||||
2. **What frame budget does a real MOH:AA map leave?** id Tech 3 maps are
|
||||
light by modern standards, but MOH:AA's are large and the Quest 3 must
|
||||
render everything twice at 72 fps.
|
||||
3. **Game modules: static or `dlopen`?** Decide before the build is shaped
|
||||
around either.
|
||||
4. **How much engine-driven camera movement is there?** Grep for view
|
||||
overrides early; it drives the Phase 4 comfort work and may be the deciding
|
||||
factor in how pleasant this is to play.
|
||||
5. **Which MOH:AA data edition to target first?** GOG is recommended by the
|
||||
project and is the easiest for testers to obtain legally.
|
||||
6. **Licensing.** OpenMoHAA is GPL; no game assets may be redistributed. Same
|
||||
posture as the Homeworld port: the user supplies their own `pak` files.
|
||||
|
||||
---
|
||||
|
||||
## 8. Suggested order of work
|
||||
|
||||
```
|
||||
1 Android build of the dedicated server or a headless client (proves the toolchain)
|
||||
2 Full client APK, GLES 3 context, main menu (proves the renderer)
|
||||
3 Side-load retail data, reach gameplay, flat (proves the filesystem)
|
||||
4 Controller input, playable flat (baseline + perf numbers)
|
||||
5 OpenXR session, one eye, correct pose (proves the XR path)
|
||||
6 Two eye FBOs, correct IPD and projection (stereo)
|
||||
7 UI as quad layers (menus usable)
|
||||
8 Locomotion and weapon handling (VR design begins)
|
||||
9 Comfort pass, camera audit, HUD (the long tail)
|
||||
```
|
||||
|
||||
Steps 1–3 are mechanical and mostly build engineering. Step 5 onward is where
|
||||
the interesting decisions live.
|
||||
@@ -0,0 +1,366 @@
|
||||
# OpenMoHAA on Quest 3 — where the port stands
|
||||
|
||||
Companion to `VR_PORT_PLAN.md`, which is the original survey and plan, and to
|
||||
`VR_RENDERER_HANDOFF.md`, which covers the move to the fixed function renderer.
|
||||
This records what has actually been built, what was learned that contradicts the
|
||||
plan, and what to do next.
|
||||
|
||||
Branch `vr-quest3`. Everything below is committed.
|
||||
|
||||
**On any problem this port is trying to solve, read RTCWQuest first.** It is
|
||||
cloned at `/home/berkybear/RTCWQuest`, clean at `4c714b9`, and it is finished.
|
||||
That is a standing instruction, not a suggestion; ignoring it has cost more time
|
||||
here than any single bug. It is also not always live code — read what is *called*,
|
||||
not what is present.
|
||||
|
||||
The reusable form of everything below is in `QUEST_PORTING_GUIDE.md`, which is
|
||||
written for the next port rather than this one.
|
||||
|
||||
---
|
||||
|
||||
## 1. What works
|
||||
|
||||
The game runs on a Meta Quest 3 in stereo VR at **90 fps**: 6DoF head tracking,
|
||||
stick locomotion, snap turning, menus on a panel fixed in the room, the weapon in
|
||||
the player's hand aimed by the controller, and a hand on each controller. A
|
||||
mission can be started and played.
|
||||
|
||||
| Phase | State |
|
||||
|---|---|
|
||||
| **1 — Android build** | Done. APK builds, boots, logs to logcat. |
|
||||
| **2 — Playable flat** | Skipped deliberately; went straight to VR at the user's direction. |
|
||||
| **3 — OpenXR stereo** | Done. 90 fps, 4–6 ms per eye in the back end. |
|
||||
| **4 — VR interaction** | Locomotion, snap turn, menu pointer, controller aim, weapon in hand, hands on both controllers. HUD and per-weapon alignment outstanding. |
|
||||
|
||||
Confirmed on the device, in order of when each stopped being a question:
|
||||
|
||||
- the world renders — terrain, BSP, models, textures, lightmaps, fog, sky
|
||||
- 90 fps at 4–6 ms per eye, against rend2's 50–60 (`VR_RENDERER_HANDOFF.md` §2)
|
||||
- `XR_FB_display_refresh_rate` works; the display runs at 90 Hz
|
||||
- colour is correct, and therefore darker than it was — see §2
|
||||
- the start map loads straight into the world without touching a menu
|
||||
- both controllers track 1:1, with no drift on head rotation and no lag on a
|
||||
snap turn
|
||||
- both hands are anchored on their own bones and sit upright at the right size
|
||||
|
||||
### Build
|
||||
|
||||
renderergl1 on gl4es is the default on Android; renderergl2 is still reachable
|
||||
with `-DBUILD_RENDERER_GL1=OFF -DBUILD_RENDERER_GL2=ON -DUSE_GL4ES=OFF`.
|
||||
|
||||
```sh
|
||||
cmake -B build-gl1 -G Ninja \
|
||||
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
|
||||
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-29 -DANDROID_STL=c++_shared \
|
||||
-DCMAKE_BUILD_TYPE=Release -DBUILD_SERVER=OFF
|
||||
cmake --build build-gl1 -j$(nproc)
|
||||
|
||||
cd misc/android && gradle assembleDebug -PnativeLibsDir=../../build-gl1/apk-libs
|
||||
cd ../.. && adb install -r misc/android/app/build/outputs/apk/debug/app-debug.apk
|
||||
adb push misc/android/autoexec.cfg \
|
||||
/sdcard/Android/data/org.openmoh.openmohaa/files/main/autoexec.cfg
|
||||
adb shell am force-stop org.openmoh.openmohaa
|
||||
```
|
||||
|
||||
SDL2, OpenAL Soft, gl4es and the Khronos OpenXR loader are all fetched and built
|
||||
from source; `-DSDL2_SOURCE_PATH=` and friends point at local trees instead. Full
|
||||
instructions and the deployment traps are in
|
||||
`docs/markdown/04-coding/03-android.md`.
|
||||
|
||||
**`adb install -r` does not kill the running process**, so force-stop after
|
||||
installing or the next launch resumes the old build. **Check the APK is newer
|
||||
than the `.so`** — gradle will report success while packaging the previous
|
||||
library. **Never `adb shell am start`**; launch from inside the headset.
|
||||
|
||||
### Game data
|
||||
|
||||
Lives in `/sdcard/Android/data/org.openmoh.openmohaa/files/main/`. Currently
|
||||
populated from a GOG War Chest install.
|
||||
|
||||
**Never create `main/save` or `main/configs` with `adb`.** A directory made by
|
||||
`adb shell mkdir` belongs to `shell`, and no chmod makes it app-writable — the
|
||||
app is `other` there. MOH:AA autosaves on mission start and treats the failure
|
||||
as fatal, so the symptom is a black screen after New Game with
|
||||
`FS_CreatePath: failed to create path` in the log. Let the engine create them.
|
||||
|
||||
### Device configuration
|
||||
|
||||
`misc/android/autoexec.cfg`, deployed next to the game data, not in the APK. It
|
||||
is the only way to set a cvar on a device with no console: exec'd after
|
||||
`omconfig.cfg`, before `R_Init`, so it beats both the stored config and the
|
||||
renderer's own defaults. It cannot reach `VR_TuningCvar` cvars, which force their
|
||||
value at every start.
|
||||
|
||||
The test harness in it ships **off**, so the game starts at its own menu. Filling
|
||||
in `cheats 1`, `vr_testStart "give all"` and `vr_startMap "m4l1"` drops straight
|
||||
into The Bocage with every weapon, which is how the hands were tuned; the values
|
||||
and the reasoning for that map are kept in the comments beside them.
|
||||
|
||||
Nothing the game needs to be correct is only in this file. It is a tuning and
|
||||
testing knob — the defaults compiled into the build are the values measured on
|
||||
device, so an APK installed without it still behaves.
|
||||
|
||||
---
|
||||
|
||||
## 2. Where the plan was wrong
|
||||
|
||||
Worth reading before trusting `VR_PORT_PLAN.md` on these points.
|
||||
|
||||
**"Drive `renderergl2` at GLES 3 directly; `renderergl1` is out of scope" — the
|
||||
opposite happened, and it was the single largest win.** rend2 ran at 10–15 fps
|
||||
against a 90 Hz display: 50–60 ms of *CPU* per eye in the back end binding GLSL
|
||||
programs, while the GPU finished in 3 ms. None of what that buys is visible on
|
||||
art with no normal or specular maps. The port now runs renderergl1 — the plain
|
||||
Quake 3 forward renderer — on gl4es, at 4–6 ms per eye. The whole of
|
||||
`VR_RENDERER_HANDOFF.md` is about that swap and the gl4es boundary it dragged in.
|
||||
|
||||
**"`renderergl2` already speaks OpenGL ES" — nearly, but it could not start.**
|
||||
`QGL_1_1_PROCS` carried five entry points that exist in no version of GLES, and
|
||||
the ES 3.0 context was only requested under `#ifdef __EMSCRIPTEN__`. Both fixed
|
||||
in `f3ca6230`; both would have broken any GLES target.
|
||||
|
||||
**"FBOs are already first class" — they were switched off on GLES**, and are
|
||||
deliberately left off on Android (`r_ext_framebuffer_object 0`): in VR each eye is
|
||||
already a swapchain image, so routing through `tr.renderFbo` first costs a second
|
||||
full-resolution pass and resolve per eye for nothing.
|
||||
|
||||
**"SDL audio may be simpler for a first pass" — there is no SDL audio path.**
|
||||
`snd_local_new.h` hardcodes `#define SOUND_DRIVER OPENAL`, and `USE_OPENAL=OFF`
|
||||
does not compile. OpenAL Soft is built from source on its OpenSL ES backend.
|
||||
|
||||
**"Game modules: static or dlopen?" — static is not an option.** `cgame` and
|
||||
`fgame` both compile `q_shared`, `q_math` and the script system, so linking them
|
||||
into one binary collides. They ship as `libgame.so` / `libcgame.so`.
|
||||
|
||||
**Quest 3 is Android 14 / API 34**, not 12.
|
||||
|
||||
**Do not set the render resolution through cvars.** `r_mode` is `CVAR_LATCH` *and*
|
||||
registered by the renderer itself, so nothing the client sets beforehand can win.
|
||||
The renderer asks the client instead, via `refimport_t::GetVRRenderResolution`.
|
||||
|
||||
**The world is 52.5 units to the metre, not 32.** MOHAA's art is authored at 16
|
||||
units per foot — its tik files say so where they convert from centimetres
|
||||
(`models/weapons/bar.tik`: *"16/30.5 since world is in 16 units per foot"*), and
|
||||
`MAXS_Z 94` / `DEFAULT_VIEWHEIGHT 82` agree at 1.79 m standing. `vr_worldscale`
|
||||
was 32 for months, which is the id Tech number for a game built in inches and
|
||||
simply the wrong engine's. Everything measured in metres was 1.64× too small, and
|
||||
it presented as *the hand models being too large* rather than as a scale error,
|
||||
because a correctly sized model drawn too close is the only clue and the world
|
||||
gives you nothing to compare against.
|
||||
|
||||
---
|
||||
|
||||
## 3. How the VR layer is put together
|
||||
|
||||
`code/vr/vr_openxr.c`, plus `vr_common.h` as the engine-facing API.
|
||||
|
||||
**The session binds to SDL's existing EGL context**, made current against a 16×16
|
||||
pbuffer because SDL unbinds it and reports success when there is no window
|
||||
surface (`VR_RENDERER_HANDOFF.md` §5.6). One GL context in the process.
|
||||
|
||||
**Eyes are rendered by substituting the renderer's idea of the screen.**
|
||||
`re.SetDefaultFramebuffer` points the renderer at the eye's swapchain image, so
|
||||
the entire render path lands in the headset without knowing. Exactly two eye
|
||||
passes per frame; deliberately no third mono pass.
|
||||
|
||||
**The session is torn down and rebuilt around renderer restarts**, and
|
||||
window-resize restarts are declined outright under VR — one of those was
|
||||
destroying the context five seconds into every run and was the black menu.
|
||||
|
||||
**Flat content goes on a quad layer** fixed in the room, drawn into one
|
||||
persistent buffer and blitted into the swapchain image, because a swapchain
|
||||
rotates through three images and the 2D path expects last frame's content to
|
||||
still be there.
|
||||
|
||||
**Colour: the swapchains are `GL_SRGB8_ALPHA8` *and* sRGB write conversion is
|
||||
disabled** via `GL_EXT_sRGB_write_control`. The format alone is half the fix and
|
||||
looks like the whole of it — ES 3.0 converts on write into an sRGB attachment and
|
||||
core ES has no switch to stop it, so the engine's already display-referred colour
|
||||
was encoded a second time. It read as flat lighting. The corrected picture is
|
||||
considerably darker, and that is correct.
|
||||
|
||||
**Orientation travels as axes, never Euler angles**, and position is measured
|
||||
from where the head was when tracking started with the starting heading
|
||||
subtracted.
|
||||
|
||||
**Head pose is applied in `RE_RenderScene`**, so it follows every camera the game
|
||||
has — the player's, cutscenes, ladders, death animations.
|
||||
|
||||
### Placing things in the world, which is not obvious
|
||||
|
||||
`cg.refdef.vieworg` **is not the camera.** `R_VRComposeView` adds the head's play
|
||||
space offset to it, rotated into the body's frame, after cgame is finished:
|
||||
|
||||
```c
|
||||
bodyAngles = (0, viewYaw - baseYaw, 0);
|
||||
fd->vieworg += vrView.origin[i] * bodyAxis[i];
|
||||
```
|
||||
|
||||
Anything anchored on `vieworg` alone is displaced by exactly that offset, which
|
||||
drifts as the head turns and does not survive a snap turn. So both hands are
|
||||
reported measured from the **tracking origin** — the frame `VR_PoseToView` puts
|
||||
the eyes in — and composed the same way the renderer composes the camera.
|
||||
|
||||
RTCWQuest reaches the same place by the other road: a head-relative offset,
|
||||
rotated by `viewangles[YAW] - hmdorientation[YAW]` in `convertFromVR`, with its
|
||||
height replaced by the `-64 + hmdposition[1] * worldScale` rebase. Either works.
|
||||
**Half of each does not**: a head-relative offset composed as play space leaves
|
||||
the hand at `vieworg + R*(hand - head)` while the camera is at `vieworg + R*head`,
|
||||
so the head's motion is counted twice and every hand movement is amplified. That
|
||||
is written up in `vr_common.h` above both accessors.
|
||||
|
||||
### Weapon and hands
|
||||
|
||||
**Aim comes from the controller, the camera stays on the head.**
|
||||
`Weapon::GetMuzzlePosition` takes the controller's pitch and the yaw the hand
|
||||
leads the head by, added to the view yaw the game already has — the controller's
|
||||
absolute yaw is meaningless, since the player can snap turn and the play space
|
||||
has an arbitrary forward. The angles reach `fgame` through `gameImport_t`, which
|
||||
a dedicated server answers false for.
|
||||
|
||||
**The hands are separate surfaces on the first person model** — `triggerhand`,
|
||||
`lefthand`, `garandhand`, `viewsleeves` on `USarmyplyr.skd` — so a hand per
|
||||
controller needs no mesh work. The weapon hand's copy drops the left hand surface
|
||||
and a second copy carries only that. `viewsleeves` is one mesh across both
|
||||
forearms and cannot be split without re-authoring, so it stays with the weapon
|
||||
hand.
|
||||
|
||||
**Each copy is anchored on a tag, not on the model origin**, by inverting the
|
||||
composition `Entity::GetTagPositionAndOrientation` uses. The off hand anchors on
|
||||
`Bip01 L Hand` and the weapon hand on `tag_weapon_right`, since what is held
|
||||
there is the gun and the controller is its grip. Every candidate is measured and
|
||||
logged rather than trusted — `tag_weapon_left` was assumed to be an unused slot
|
||||
at the origin and is a real anchor 23.5 units out.
|
||||
|
||||
What is left after the frame and the anchor are right is the wrist bones' own
|
||||
rotation convention, which is a constant: both hands need a roll of 180 in
|
||||
`vr_weaponAdjust` / `vr_offHandAdjust`. That is the compiled default for both,
|
||||
not something the config has to supply — the two hands are mirrored and were
|
||||
still measured to want the same number, which is why they stay two cvars.
|
||||
|
||||
**Two handed hold**: off hand grip with the hands close together points the
|
||||
weapon along the line between them, roll halved. **Reload** is a tap of the
|
||||
weapon hand's grip. **Weapon switching** is the dominant thumbstick up and down,
|
||||
gated on the sideways component and edge triggered.
|
||||
|
||||
### Debugging
|
||||
|
||||
```
|
||||
vr_traceTracking 1 raw headset pose, tracking origin, world scale
|
||||
r_vrTrace 1 the composed camera the renderer actually uses
|
||||
vr_traceFrame 1 the per frame timing split (on by default; noisy)
|
||||
vr_captureEye 1 writes the eye buffer to main/vrshotN.tga
|
||||
```
|
||||
|
||||
**Start the logcat capture before handing the headset over.** The buffer is
|
||||
shared with a shell that prints every five seconds, so a once-only startup line
|
||||
is gone within a minute or two — that has cost two round trips:
|
||||
|
||||
```sh
|
||||
adb logcat -c && adb logcat -s openmohaa:V > run.log &
|
||||
```
|
||||
|
||||
The crash handler (`code/sys/new/sys_android_new.c`) reports the faulting address
|
||||
and the `pc`/`lr` via `sigaction` + `SA_SIGINFO` on a private `sigaltstack`.
|
||||
`_Unwind_Backtrace` cannot walk out of the kernel's signal trampoline, so before
|
||||
that every crash reported four frames of the handler and nothing else.
|
||||
|
||||
---
|
||||
|
||||
## 4. Known problems
|
||||
|
||||
**It does not hold 90 Hz while moving.** `vr_resolutionScale` is now 1, so the
|
||||
eyes render at the full 1680×1760, and the frame budget is spent:
|
||||
|
||||
| | fps | eye pass |
|
||||
|---|---|---|
|
||||
| standing at spawn | 90.0 | 8–9 ms |
|
||||
| moving in the Bocage | 77–85 | 11–12 ms |
|
||||
|
||||
The display period is 11.1 ms, so while moving most frames overshoot it and the
|
||||
compositor reprojects them. **Kept deliberately** — it plays well, the picture is
|
||||
much sharper, and the reprojection is not felt. But it is a known trade rather
|
||||
than headroom, and `vr_resolutionScale` is the first thing to give back if
|
||||
something later needs the time.
|
||||
|
||||
**There is no budget for MSAA.** It is wired up and switched off (`vr_samples 0`)
|
||||
and it resolves in tile memory on Adreno so it is nearly free on the GPU — but
|
||||
the GPU is not the constraint, and there is nothing spare on the CPU side. This
|
||||
stays off until the frame split below is trustworthy.
|
||||
|
||||
**The frame split cannot be taken at face value.** It reports `world 1ms` against
|
||||
`hud 5ms`, which is not credible for a full world render. `Set2DWindow` begins
|
||||
with `R_IssuePendingRenderCommands()`, so the world's deferred draw calls are
|
||||
attributed to the first 2D call that forces them out — the trap in
|
||||
`QUEST_PORTING_GUIDE.md` §7.1. The one trustworthy figure is `gpuwait`, still
|
||||
**3 ms**: the GPU finishes early and waits, exactly as it did under rend2, so
|
||||
whatever the 11 ms is, it is CPU issuing draw calls and not fill rate.
|
||||
|
||||
**Fixing that attribution is the prerequisite to spending any budget**, because
|
||||
right now nobody can say where half the frame goes.
|
||||
|
||||
**The HUD is still screen space during gameplay**, with the same non-convergence
|
||||
the menus had before the quad layer.
|
||||
|
||||
**Per-weapon alignment is one global setting.** `vr_weaponAdjust` is shared
|
||||
across every weapon, but a Garand, a Colt and a bazooka will not want the same
|
||||
grip offset — the reference keeps `vr_weapon_adjustment_<id>` per weapon. The
|
||||
Bocage hands over a Colt and a Garand at spawn, plus `give all`, which is the
|
||||
place to find out how far apart they need to be.
|
||||
|
||||
**Not confirmed on the device**, and each is only known to compile:
|
||||
|
||||
- the two handed hold and the reload gesture
|
||||
- off hand steering — it was mirrored, and the corrected sign has not been tried
|
||||
- whether the pistol's `lefthand` surface looks right; only the Garand's
|
||||
`garandhand` has been seen
|
||||
|
||||
**Some foliage still rotates with head movement.** `AutospriteDeform` and the
|
||||
`SPRITE_PARALLEL` paths now face the viewer's position rather than the view
|
||||
plane, which fixed the near foliage and the imposters behind it. At least one
|
||||
more path does the same thing. `Autosprite2Deform` was left alone on purpose.
|
||||
|
||||
**A recurring SIGSEGV in the Adreno driver** (`gsl_memory_alloc_pure_64`, fault
|
||||
address `0x90`) on a secondary thread at every launch. It **predates all VR
|
||||
work** — it was in the flat builds too — and the process survives it every time.
|
||||
Establish which faults are background noise before bisecting anything.
|
||||
|
||||
---
|
||||
|
||||
## 5. What to do next
|
||||
|
||||
1. **Fix the frame split**, so `world` and `hud` mean what they say. Half the
|
||||
frame is currently attributed to a flush rather than to the work that caused
|
||||
it, and every performance decision after this one depends on knowing where
|
||||
the 11 ms actually goes. The GPU idling at 3 ms says there is something real
|
||||
to find.
|
||||
2. **Then decide about MSAA**, and about whether the CPU cost can come down far
|
||||
enough to hold 90 Hz at full resolution rather than reprojecting to it.
|
||||
3. **Confirm the unconfirmed** — the two handed hold, the reload tap, off hand
|
||||
steering. All three are one session on the device with the harness already in
|
||||
place.
|
||||
4. **Per-weapon alignment.** Build the per-weapon table once the numbers for two
|
||||
or three weapons are known.
|
||||
5. **The HUD.** Wrist or weapon mounted readouts; `VR_WristPanelEnabled` already
|
||||
exists and is off.
|
||||
6. Remaining foliage paths, and the Adreno fault.
|
||||
|
||||
### Reference
|
||||
|
||||
Team Beef's RTCWQuest, `/home/berkybear/RTCWQuest`, clean at `4c714b9`.
|
||||
`Projects/Android/jni/RTCWVR/`: `TBXR_Common.c` is their OpenXR layer,
|
||||
`VrInputDefault.c` the movement and weapon handling, `VrInputWeaponAlign.c` the
|
||||
per-weapon calibration. `rtcw/src/cgame/cg_weapons.c` has
|
||||
`CG_CalculateVRWeaponPosition` and the offhand equivalents;
|
||||
`rtcw/src/renderer/tr_scene.c` has `convertFromVR`, which is the frame conversion
|
||||
this port arrives at from the other direction.
|
||||
|
||||
Ideas there worth having and not yet taken: **a projection layer and a quad layer
|
||||
in the same frame** (`TBXR_Common.c:1576`), which is how a pointer ray and
|
||||
controller models get drawn on a menu-only frame; and **scope zoom by narrowing
|
||||
the composition layer's FOV** (`TBXR_Common.c:1557`) rather than touching the
|
||||
projection matrix, which applies directly to the sniper scopes and binoculars.
|
||||
|
||||
Not there, so do not go looking: no foveation, no space warp, no performance
|
||||
level hints. `XR_FB_display_refresh_rate` is vestigial in their tree — the
|
||||
working implementation here is original.
|
||||
@@ -0,0 +1,546 @@
|
||||
# Swapping the renderer — where this stands and what to do next
|
||||
|
||||
Companion to `VR_PORT_STATUS.md`. That file describes the VR layer; this one is
|
||||
about the move from `renderergl2` to `renderergl1` on gl4es.
|
||||
|
||||
Branch `vr-quest3`.
|
||||
|
||||
---
|
||||
|
||||
## 0. Status at a glance
|
||||
|
||||
**Working, on the device, with nothing overridden:** renderergl1 on gl4es, the
|
||||
OpenXR session, both eyes and the flat panel, the menus, cutscenes, controllers
|
||||
and pointer beam, **and the world** - terrain, BSP geometry, static models,
|
||||
textures, lightmaps, fog and sky. **89-90 fps at 6 ms per eye**, against rend2's
|
||||
50-60 ms. That is the number this whole swap existed to get.
|
||||
|
||||
Three bugs were behind the black screen, and each is recorded in section 8 with
|
||||
what proved it:
|
||||
|
||||
1. A window-resize `vid_restart` tearing down the GL context and the OpenXR
|
||||
session five seconds into every run. That was the black menu.
|
||||
2. `RB_SurfaceFace` writing 32-bit indices through a 16-bit array. That was the
|
||||
world smeared into streaks.
|
||||
3. **gl4es's cached cull face drifting from the driver's**, so the engine's
|
||||
`glCullFace(GL_FRONT)` was dropped as redundant every frame while the
|
||||
hardware culled `GL_BACK` - exactly the faces Quake keeps. That was the
|
||||
invisible world.
|
||||
|
||||
**The single most important lesson is section 9.** Everything the engine could
|
||||
be asked about itself said the renderer was correct, because under gl4es the
|
||||
engine is largely asking gl4es to repeat the engine's own values back.
|
||||
|
||||
---
|
||||
|
||||
## 1. The reference implementation — read this before solving anything
|
||||
|
||||
**Team Beef's RTCWQuest is cloned at `/home/berkybear/RTCWQuest`**, clean at
|
||||
`4c714b9`.
|
||||
|
||||
Same Quake 3 engine lineage, same genre, same headset, same gl4es — and unlike
|
||||
this port, it is finished. **Go to it first for any problem this port is trying
|
||||
to solve.** That is a standing instruction, not a suggestion. In this session it
|
||||
was violated repeatedly and cost most of the elapsed time: the answer to the
|
||||
single biggest blocker (section 5.8) was sitting in their `Android.mk` the whole
|
||||
time.
|
||||
|
||||
| Path (under `Projects/Android/jni/`) | What |
|
||||
|---|---|
|
||||
| `RTCWVR/TBXR_Common.c` | their OpenXR layer: session, swapchains, composition layers, frame submit |
|
||||
| `RTCWVR/VrInputDefault.c` | movement, gestures, weapon handling |
|
||||
| `RTCWVR/VrInputWeaponAlign.c` | per-weapon aim calibration |
|
||||
| `RTCWVR/VrClientInfo.h` | their whole VR state struct |
|
||||
| `rtcw/src/renderer/` | the fixed function renderer with their GLES changes (`#ifdef HAVE_GLES`) |
|
||||
| `SupportLibs/gl4es/Android.mk` | **the gl4es build flags. These matter. See 5.8.** |
|
||||
|
||||
### Already extracted — do not re-derive
|
||||
|
||||
**Aiming** (`rtcw/src/game/g_weapon.c:1904`). Shot direction is computed in the
|
||||
*game*, where the trace is fired:
|
||||
|
||||
```c
|
||||
VectorCopy(gVR->weaponangles, viewang);
|
||||
viewang[YAW] = ent->client->ps.viewangles[YAW]
|
||||
+ (gVR->weaponangles[YAW] - gVR->hmdorientation[YAW]);
|
||||
```
|
||||
|
||||
Pitch and roll come straight from the controller; yaw is the view yaw plus how
|
||||
far the controller leads the head. The controller's absolute yaw is meaningless.
|
||||
The camera stays on the head; only the trace moves to the hand.
|
||||
|
||||
**6DoF** (`rtcw/src/client/cl_input.c:847`). Head position delta is fed into the
|
||||
usercmd as movement. Already ported here.
|
||||
|
||||
**The HUD.** No wrist panel. Screen space, in the eye buffers, re-centred on
|
||||
each eye's off-centre frustum, given stereo parallax, scaled in from the edges.
|
||||
|
||||
**Hands.** No controller models. Their existing view-model hand, moved to the
|
||||
controller's real-world offset from the head.
|
||||
|
||||
**Ladders.** Nothing at all — RTCW's are contents-based so it works for free.
|
||||
**Does not transfer**: MOH:AA is entity-based with a view clamp in
|
||||
`PmoveAdjustViewAngleSettings_OnLadder`.
|
||||
|
||||
**Not there, so do not go looking**: no foveation, no space warp, no performance
|
||||
level hints. `XR_FB_display_refresh_rate` is vestigial in their tree.
|
||||
|
||||
---
|
||||
|
||||
## 2. Why the renderer was swapped
|
||||
|
||||
renderergl2 ran at **10–15 fps against a 90 Hz display**. Taken apart on the
|
||||
device:
|
||||
|
||||
| Phase | Cost |
|
||||
|---|---|
|
||||
| `xrWaitFrame` | 0 ms |
|
||||
| Building the 3D scene | 2–3 ms |
|
||||
| **Issuing the scene to GL** | **50–60 ms, per eye** |
|
||||
| GPU, measured with `glFinish` | **3 ms** |
|
||||
|
||||
The GPU finishes in 3 ms and waits. Rendering at 36% of the pixels bought only
|
||||
23% back, so it is not fill rate. It is CPU in the back end issuing draw calls:
|
||||
rend2 puts every surface through a GLSL program with dozens of uniforms, 139
|
||||
`GLSL_BindProgram`/`GLSL_SetUniform` call sites in the surface path, once per
|
||||
surface per eye.
|
||||
|
||||
None of what that buys is visible here — this art has no normal or specular
|
||||
maps. RTCWQuest's renderer contains no GLSL at all: the original Quake 3 forward
|
||||
renderer on gl4es. That is the stack being copied.
|
||||
|
||||
---
|
||||
|
||||
## 3. Measurement traps in the *old* renderer investigation
|
||||
|
||||
Kept because they are still true of the code.
|
||||
|
||||
**`backEnd.pc.msec` is assigned, not accumulated** (`tr_backend.c`). Later empty
|
||||
flushes overwrite the scene's real number with 0.
|
||||
|
||||
**`Set2DWindow` begins with `R_IssuePendingRenderCommands()`**. The 3D scene is
|
||||
*built* during the world pass but only *executed* when something forces a flush,
|
||||
and the first thing that does is the HUD. The whole frame's rendering therefore
|
||||
appears to any timer to be HUD cost.
|
||||
|
||||
**`tr.frontEndMsec` only covers `RE_RenderScene`**, not the cgame work around it.
|
||||
|
||||
---
|
||||
|
||||
## 4. Build
|
||||
|
||||
```sh
|
||||
cmake -B build-gl1 -G Ninja \
|
||||
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
|
||||
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-29 -DANDROID_STL=c++_shared \
|
||||
-DCMAKE_BUILD_TYPE=Release -DBUILD_SERVER=OFF
|
||||
cmake --build build-gl1 -j$(nproc)
|
||||
|
||||
cd misc/android && gradle assembleDebug -PnativeLibsDir=../../build-gl1/apk-libs
|
||||
cd ../.. && adb install -r misc/android/app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
renderergl1 + gl4es is the default on Android. renderergl2 is still reachable
|
||||
with `-DBUILD_RENDERER_GL1=OFF -DBUILD_RENDERER_GL2=ON -DUSE_GL4ES=OFF` and
|
||||
still builds.
|
||||
|
||||
---
|
||||
|
||||
## 5. What was wrong, and what fixing it required
|
||||
|
||||
All committed. Each of these was a hard blocker in its own right.
|
||||
|
||||
### 5.1 renderergl1 had never compiled
|
||||
|
||||
`cmake/platforms/android.cmake` held `set(BUILD_RENDERER_GL1 OFF CACHE INTERNAL "")`.
|
||||
**`CACHE INTERNAL` implies `FORCE`**, so the `-D` on the command line was
|
||||
overwritten on every configure. The cache read `BUILD_RENDERER_GL1:INTERNAL=OFF`
|
||||
and `build.ninja` never mentioned renderergl1. A previous handoff claimed this
|
||||
renderer "compiles clean"; it had never been compiled at all. **Check the cache,
|
||||
not the command line.**
|
||||
|
||||
### 5.2 gl4es was building into the source tree, under a name Android cannot load
|
||||
|
||||
gl4es's own CMakeLists sets `CMAKE_LIBRARY_OUTPUT_DIRECTORY` to
|
||||
`${CMAKE_SOURCE_DIR}/lib`, which under FetchContent is *this* project's source
|
||||
root. It also forces the suffix `.so.1`. Android has no versioned sonames and
|
||||
the package manager extracts only `lib*.so`. Redirected into the build tree as
|
||||
`libgl4es.so` and staged with the other APK libraries.
|
||||
|
||||
### 5.3 The renderer's GL entry points must come from gl4es
|
||||
|
||||
renderergl1 does not call GL by name. `qgl_linked.h` is dead code that nothing
|
||||
includes; `renderercommon/qgl.h` declares every entry point as a **function
|
||||
pointer** filled by `GLimp_GetProcAddresses` from `SDL_GL_GetProcAddress`.
|
||||
|
||||
On **EGL 1.5, which the Quest is**, `SDL_EGL_GetProcAddress` tries
|
||||
`eglGetProcAddress` *before* the library it loaded. The driver answers for every
|
||||
name OpenGL 1.x and ES have in common — `glEnable`, `glBindTexture`,
|
||||
`glDrawArrays`, `glTexImage2D`, ~80 more — so only desktop-only names
|
||||
(`glBegin`, `glMatrixMode`) would reach gl4es. The renderer now resolves through
|
||||
`GLimp_GetProcAddress`, straight to the gl4es handle.
|
||||
|
||||
The **extension string** has the same problem: `SDL_GL_ExtensionSupported` reads
|
||||
the driver's ES list, which names none of the desktop extensions the renderer
|
||||
asks after, so `GL_ARB_multitexture` read as absent and the renderer would have
|
||||
dropped to one texture unit.
|
||||
|
||||
### 5.4 The fixed function path asked for a context that does not exist
|
||||
|
||||
`GLimp_SetMode`'s `fixedFunction` branch offered exactly one context: **desktop
|
||||
OpenGL 1.1**. There is no such thing on Android; window creation failed and
|
||||
R_Init gave up with `Couldn't get a visual`. renderergl2 never met this because
|
||||
it takes the other branch.
|
||||
|
||||
Under `USE_GL4ES` that branch now asks for **ES 3.0 then ES 2.0**. The renderer
|
||||
is never told: it reads its version through gl4es, which reports desktop GL
|
||||
(`2.1 gl4es wrapper 1.1.6`), and takes the desktop fixed function path on the
|
||||
strength of it. ES underneath, desktop GL above — the whole point of the layer.
|
||||
|
||||
### 5.5 gl4es answers `glGetString` from constants
|
||||
|
||||
`GL_VERSION`, `GL_VENDOR` and `GL_RENDERER` come out of gl4es's own globals
|
||||
without the driver being asked. The engine's only check that a context is live is
|
||||
`GL_VERSION` coming back non-NULL, so under gl4es that check passes
|
||||
unconditionally and a context created but never bound sails through `R_Init`.
|
||||
`GLimp_SetMode` now asks SDL directly instead.
|
||||
|
||||
### 5.6 SDL unbinds the context and reports success
|
||||
|
||||
`SDL_EGL_MakeCurrent`, handed no surface and without `gl_allow_no_surface`:
|
||||
|
||||
```c
|
||||
if (!egl_context || (!egl_surface && !_this->gl_allow_no_surface)) {
|
||||
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
}
|
||||
return 0; /* success, either way */
|
||||
```
|
||||
|
||||
In a headset there is no window being presented, so that surface never arrives.
|
||||
SDL then records the context as current, and SDL and EGL disagree permanently.
|
||||
Asking SDL to bind it again does nothing — `SDL_GL_MakeCurrent` sees its own
|
||||
bookkeeping agree and returns early with `/* We're already current. */`.
|
||||
|
||||
RTCWQuest never meets this because it never depends on a window surface: it makes
|
||||
its context current against a **16×16 pbuffer** (`TBXR_Common.c`,
|
||||
`egl->TinySurface`). Same here, with the context SDL already made. The pbuffer
|
||||
outlives the session deliberately and is released in `VR_Shutdown`.
|
||||
|
||||
### 5.7 gl4es must own the `GL_FRAMEBUFFER` binding — and only that
|
||||
|
||||
gl4es keeps its own table of framebuffers it created and **renders from that,
|
||||
not from the driver's binding**:
|
||||
|
||||
```c
|
||||
gles_glBindFramebuffer(GL_FRAMEBUFFER, current_fb->id ? current_fb->id : mainfbo_fbo);
|
||||
```
|
||||
|
||||
A framebuffer from the driver's `glGenFramebuffers` is not in that table, so
|
||||
`gl4es_glBindFramebuffer` raises `GL_INVALID_VALUE`, **returns without binding**,
|
||||
and leaves `current_fb` at zero. Measured directly:
|
||||
|
||||
```
|
||||
fbo: gl4es says 0, driver says 10 | driver viewport 756 -71 403 1126
|
||||
```
|
||||
|
||||
Zero in this process is the 16×16 pbuffer, and the viewport lies entirely outside
|
||||
it — so geometry landed in nothing, with no error raised anywhere.
|
||||
|
||||
The VR layer now has gl4es generate, bind and delete these. **Three things must
|
||||
stay with the driver:**
|
||||
|
||||
- the **swapchain texture attachment** — gl4es cannot attach a texture the
|
||||
OpenXR runtime created. RTCWQuest hits this and documents it at
|
||||
`TBXR_Common.c:290`, describing our exact symptom: *"the engine then renders
|
||||
into nothing -> black eye buffer"*. (They must bypass gl4es explicitly because
|
||||
ndk-build puts gl4es first in their link order; ours is already native because
|
||||
`libGLESv3` precedes `libgl4es` in `DT_NEEDED`.)
|
||||
- the **blit's `GL_READ_FRAMEBUFFER` / `GL_DRAW_FRAMEBUFFER` bindings** — gl4es
|
||||
takes `GL_READ_FRAMEBUFFER` as a note to itself and `return`s without binding
|
||||
(`framebuffers.c:238`). Routing the blit through it silently empties the blit
|
||||
and **kills the cutscenes**. This was done once by accident with an
|
||||
over-broad macro; do not repeat it.
|
||||
- `glFramebufferTexture2D`, per the first point.
|
||||
|
||||
`RE_SetDefaultFramebuffer` in renderergl1 binds **nothing**. It exists only to
|
||||
flush: gl4es batches geometry and issues it lazily, so work built for one target
|
||||
would otherwise arrive in whichever is bound when it finally goes out. The VR
|
||||
layer calls it *before* its own bind for that reason.
|
||||
|
||||
### 5.8 gl4es must be built the way the reference builds it
|
||||
|
||||
**This was the largest single blocker and it is the thing to remember.**
|
||||
|
||||
With CMake's own flags, gl4es **rendered no geometry whatsoever** while clearing
|
||||
the same framebuffer perfectly. Measured, in the same buffer microseconds apart:
|
||||
|
||||
```
|
||||
gl4es draw test: 144 red (clear), 0 green (drawn), 0 other
|
||||
```
|
||||
|
||||
Matching RTCWQuest's `Android.mk` fixed it outright, with no engine-side change:
|
||||
|
||||
```
|
||||
gl4es draw test: 108 red (clear), 36 green (drawn), 0 other
|
||||
```
|
||||
|
||||
(36/144 is exactly the quarter the test quad covers.)
|
||||
|
||||
Their flags, now in `cmake/libraries/gl4es.cmake`:
|
||||
|
||||
```
|
||||
-DBCMHOST -DNOX11 -DNO_GBM -DDEFAULT_ES=2 -DNO_INIT_CONSTRUCTOR
|
||||
-O3 -fcommon -fvisibility=hidden -funwind-tables
|
||||
```
|
||||
|
||||
**`ANDROID` is deliberately not among them.** gl4es's own CMakeLists adds
|
||||
`-DANDROID` whenever the NDK toolchain is in use, and ndk-build never does — so
|
||||
the reference has only ever been exercised without it. It is not cosmetic: it
|
||||
selects different code in the loader, in glx, and in `hardext`, which is where
|
||||
gl4es compiles a probe shader (`testGLSL`) to decide what its fixed function
|
||||
pipeline may emit. It comes from `add_definitions()`, a **directory** property,
|
||||
so it must be removed at the directory it was set on — overriding it on the
|
||||
target does not work.
|
||||
|
||||
**Still to do: bisect which flag actually mattered.** `-DANDROID` is the strong
|
||||
suspicion. This is currently convergence on a known-good configuration, not a
|
||||
diagnosis.
|
||||
|
||||
---
|
||||
|
||||
## 6. The trap that cost this session, in bold
|
||||
|
||||
**Every engine-side experiment run before 5.8 was worthless, because gl4es could
|
||||
not draw anything at all.**
|
||||
|
||||
Under that broken gl4es, *any* test that ends "and nothing was drawn" returns the
|
||||
same answer regardless of what it is testing. On the strength of such results
|
||||
this session wrongly concluded, and acted on:
|
||||
|
||||
- that 32-bit indices were the problem (changed to 16-bit — see 8.1)
|
||||
- that the `glDrawElements` client-array path was broken (switched to
|
||||
`qglArrayElement`; no change)
|
||||
- that compiled vertex arrays were broken (disabled them — see 8.2)
|
||||
- that `glDrawBuffer(GL_BACK)` on an FBO was discarding everything (stubbed it —
|
||||
see 8.3)
|
||||
- that immediate mode worked while arrays did not, and later that *neither*
|
||||
worked — two contradictory conclusions, both from invalid data
|
||||
|
||||
**Before believing any negative rendering result, prove the renderer can draw at
|
||||
all.** The cheapest possible check, and the one that finally cracked this:
|
||||
ask gl4es for a `glClear` and then a flat untextured quad into the same buffer,
|
||||
and read the pixels back. A clear needs no shaders; a draw needs the whole
|
||||
pipeline. If the clear lands and the draw does not, stop looking at the engine.
|
||||
|
||||
A second, dumber version of the same mistake: a probe gated on
|
||||
`primitives == 2` silently stopped running when a separate change flipped
|
||||
`primitives` to 1, and two rounds of "nothing drawn" results were actually
|
||||
"the probe never executed". **Verify instrumentation still fires after changing
|
||||
anything it depends on.**
|
||||
|
||||
---
|
||||
|
||||
## 7. What is now positively proven working
|
||||
|
||||
Measured on the device, after 5.8. Do not re-investigate these.
|
||||
|
||||
- The OpenXR session, both eye swapchains and the flat quad layer. 90 fps.
|
||||
- The panel framebuffer is real, complete (`GL_FRAMEBUFFER_COMPLETE`), writable
|
||||
and composited — a clear to blue showed as blue in the headset.
|
||||
- gl4es reaches that framebuffer: a clear to red showed as red.
|
||||
- gl4es renders geometry: a flat quad rendered, verified by pixel readback.
|
||||
- ~~**The engine's own geometry renders** — with a flat-red override, the intro
|
||||
logo screen before the cutscene appeared red in the headset.~~ **Weaker than
|
||||
it reads.** The override had disabled the colour array, and the route it went
|
||||
down was `qglArrayElement`, not `glDrawElements`. It proves one array reaches
|
||||
gl4es on one route, not that the renderer's geometry path works. See 8.1.
|
||||
- Cutscenes render (immediate mode, `RE_StretchRaw`).
|
||||
- Controllers and the pointer beam render (VR layer's own GLES).
|
||||
- Both layers agree on the framebuffer: `gl4es says 10, driver says 10`.
|
||||
- The 2D setup is correct: viewport `0 0 1008 1056`, ortho `0 1008 1056 0`,
|
||||
full scissor, `vrView.active` false during menus (so the HUD convergence code
|
||||
in `R_VRAdjust2DOrtho` is *not* involved in the menu at all).
|
||||
- The geometry submitted for menu widgets is correct: 4 verts, 6 strip indices,
|
||||
sensible coordinates, identity modelview, a correct ortho matching the
|
||||
widget's own viewport, `glGetError() == 0` on every draw.
|
||||
|
||||
---
|
||||
|
||||
## 8. Fixed this session
|
||||
|
||||
**8.1 The menu was a renderer restart.** `sdl_input.c`'s `SDL_WINDOWEVENT_RESIZED`
|
||||
handler compares the window against `cls.glconfig.vidWidth/Height`, which in VR
|
||||
is the *eye buffer*, never the Android surface. It can never match, so it set
|
||||
`r_customwidth`/`r_customheight`/`r_mode -1` and scheduled a `vid_restart` about
|
||||
five seconds in - cvars the renderer then ignores, because the VR resolution
|
||||
comes from `GetVRRenderResolution`. The restart destroyed and rebuilt the GL
|
||||
context and the OpenXR session under a running frame loop, and section 5.7's
|
||||
framebuffer agreement did not survive it. Measured: the menu drew a full
|
||||
`256/256` panel samples right up to `CL_Vid_Restart_f` and nothing ever after.
|
||||
Window-resize restarts are now declined under `VR_Enabled()`.
|
||||
|
||||
`VR_PORT_STATUS.md` called this restart "harmless ... but wasteful". It was the
|
||||
entire bug.
|
||||
|
||||
**8.2 `RB_SurfaceFace` wrote 32-bit indices into a 16-bit array.** It declared
|
||||
`unsigned *tessIndexes` and assigned `tess.indexes` to it - which is
|
||||
`glIndex_t *`, i.e. `unsigned short *` under gl4es (9.1). Every index went in
|
||||
four bytes apart in a two-byte array, so half the slots held the zero high
|
||||
halves of their neighbours and every triangle picked up whatever vertex that
|
||||
landed on. That was the stretched, smeared world that dragged its textures with
|
||||
it. Models never come through this function, which is why they were perfect
|
||||
throughout.
|
||||
|
||||
`ProjectDlightTexture`'s `unsigned hitIndexes[SHADER_MAX_INDEXES]`, passed
|
||||
straight to `R_DrawElements`, had the same fault. Both fixed; a full rebuild now
|
||||
reports no index-type mismatches in the renderer.
|
||||
|
||||
**The compiler had been saying so all along**, under sixty-odd other warnings:
|
||||
|
||||
```
|
||||
tr_surface.c:401: warning: incompatible pointer types assigning to 'unsigned int *'
|
||||
from 'glIndex_t *' (aka 'unsigned short *')
|
||||
```
|
||||
|
||||
This is the cost of 9.1 that nobody checked for: changing `glIndex_t` is not
|
||||
local to the draw call, and the only thing that finds the other users is the
|
||||
warning log. **Read it after touching that typedef.**
|
||||
|
||||
**8.3 The frame budget, which is what the swap was for.** In-game, on the
|
||||
device: **90 fps at 4 ms per eye**, against rend2's 50-60 ms (section 2). That
|
||||
question is answered and `vr_resolutionScale` can go back to 1.
|
||||
|
||||
**8.4 Smaller, all real.** renderergl1's `R_SetupFrustum` culled against the
|
||||
game's symmetric fov rather than the runtime's asymmetric per-eye frustum
|
||||
(renderergl2 does not have this bug - its `R_SetupFrustum` takes the projection
|
||||
edges). Every `glViewport`, `glEnable/glDisable`, `glDepthMask`, `glDepthFunc`,
|
||||
`glBlendFunc`, `glColorMask`, `glCullFace` and `glScissor` in the VR layer now
|
||||
goes through gl4es, because gl4es caches all of them and silently drops the
|
||||
engine's next request for a value its stale copy already claims. `GfxInfo_f`
|
||||
printed a different draw route than `R_DrawElements` actually took.
|
||||
|
||||
---
|
||||
|
||||
## 9. Ask the driver, not gl4es
|
||||
|
||||
**Written up properly in `docs/markdown/04-coding/04-gl4es.md`.** That file is
|
||||
the reference; this section is the short form.
|
||||
|
||||
**gl4es answers state queries from its own tables.** `glGetIntegerv` for
|
||||
`GL_CULL_FACE_MODE`, `GL_FRONT_FACE` and `GL_MODELVIEW_MATRIX` all come out of
|
||||
`glstate`, not the driver (`src/gl/getter.c:636-902`) - the same way `glGetString`
|
||||
comes from constants (5.5). So reading state back through `qgl` and comparing it
|
||||
against the engine compares the engine with gl4es's copy of the engine. It
|
||||
agrees by construction and proves nothing.
|
||||
|
||||
Two measurements in this session did exactly that and were reported as findings:
|
||||
"the matrices are identical, `maxdiff 0.000`" and "GL and the engine agree on
|
||||
culling". Both were vacuous. The second was actively harmful - the driver was
|
||||
culling `GL_BACK` while gl4es reported the `GL_FRONT` the engine had asked for,
|
||||
and that *was* the bug being hunted.
|
||||
|
||||
**gl4es also drops calls its cache judges redundant** - `glCullFace`
|
||||
(`face.c:14`), `glDepthMask`, `glDepthFunc`, `glBlendFunc`, `glColorMask`,
|
||||
`glScissor`, `glViewport`. Once its copy drifts, the engine can never correct it:
|
||||
it asks for the right value, gl4es compares against the wrong copy, and the
|
||||
driver never hears. Nothing raises an error and nothing looks wrong from inside.
|
||||
|
||||
**So: resolve `glGetIntegerv`/`glIsEnabled` out of `libGLESv3` and ask the
|
||||
hardware.** A temporary `r_traceSurf` probe did this, printing both answers
|
||||
side by side; it has since been removed, but the technique is
|
||||
what cracked the cull bug in one run after many that measured nothing.
|
||||
|
||||
The same applies to setting state, which is what `GL_Cull` now does: name a
|
||||
throwaway value first so the real one cannot match a stale cache.
|
||||
|
||||
### 9.1 Probes must be shown to reach the thing they measure
|
||||
|
||||
Three probes in this session had blind spots exactly where the fault was, and
|
||||
each produced a confident wrong conclusion:
|
||||
|
||||
- the panel readback counted samples brighter than 8 against a buffer that is
|
||||
never cleared, so "nothing drawn" and "drawn black" were the same number;
|
||||
- `r_flatColor` did not apply to multitextured surfaces, so it covered models
|
||||
only - every "the world is not drawing" reading from it was about geometry
|
||||
that was never in question;
|
||||
- the flat-red override restored its state inside `if (primitives == 2)`, a
|
||||
branch that was never taken, so it corrupted the renderer it was measuring.
|
||||
|
||||
**Before believing a negative result, prove the probe reached the surfaces in
|
||||
question.** This is section 6's lesson in a second form and it cost more here
|
||||
than the bugs did.
|
||||
|
||||
### 9.2 Single runs are not evidence
|
||||
|
||||
The same configuration produced different results on different runs more than
|
||||
once, and a bisect was built on top of that before it was noticed. Repeat a
|
||||
result before building on it, and never change code and configuration in the
|
||||
same step - doing so destroyed the only known-good baseline and cost several
|
||||
round trips to rebuild.
|
||||
|
||||
## 10. Working notes
|
||||
|
||||
**The device configuration lives at `misc/android/autoexec.cfg`.** It holds the
|
||||
nothing load bearing any more - the overrides it used to carry went with the
|
||||
bugs they worked around. It is not part of the APK; deploy it next to the game
|
||||
data:
|
||||
|
||||
```sh
|
||||
adb push misc/android/autoexec.cfg \
|
||||
/sdcard/Android/data/org.openmoh.openmohaa/files/main/autoexec.cfg
|
||||
adb shell am force-stop org.openmoh.openmohaa
|
||||
```
|
||||
|
||||
**`autoexec.cfg` is the only way to set a cvar on this device.** There is no
|
||||
console in a headset. It is exec'd by `Com_ExecuteCfg` after `omconfig.cfg`,
|
||||
cannot be loaded out of a pk3 (`FS_FOpenFileRead`'s `isLocalConfig`), and lands
|
||||
*before* `R_Init` - so the renderer's `Cvar_Get` finds each cvar already present
|
||||
and keeps this value instead of its own default. That covers `CVAR_LATCH` and
|
||||
`CVAR_ARCHIVE` cvars too. It cannot reach `VR_TuningCvar` cvars, which force
|
||||
their value at every start, and `CVAR_CHEAT` cvars get reset on map load.
|
||||
|
||||
**Set every knob explicitly, never by omission.** A value left out is whatever
|
||||
`omconfig.cfg` last archived. `cg_shadows` cost a whole round trip that way: it
|
||||
was already 0 in the stored config, so the run that was supposed to test
|
||||
disabling shadows tested nothing at all. **Read the stored config before
|
||||
believing a negative result** - `adb pull .../files/main/configs/omconfig.cfg`.
|
||||
|
||||
**`adb install -r` does not kill a running process.** The old code keeps running
|
||||
and the next launch resumes it, so a run can silently test the previous build.
|
||||
Always `adb shell am force-stop org.openmoh.openmohaa` after installing.
|
||||
|
||||
**Pull captures individually, not the directory.** `adb pull .../files/main/`
|
||||
drags 1.6 GB of pk3s. `adb pull .../main/vrshot0.tga` is what you want.
|
||||
|
||||
**Device.** `adb connect 192.168.1.92:43105`. It sleeps and the connection dies;
|
||||
`adb disconnect` then `adb connect`. **Launch from inside the headset** — never
|
||||
`adb shell am start`.
|
||||
|
||||
A logcat wrapper that survives the sleep/wake cycle is worth keeping around; the
|
||||
connection drops constantly and a plain `adb logcat` dies with it.
|
||||
|
||||
**Archived cvars beat a changed default.** The config is exec'd before `VR_Init`,
|
||||
so `Cvar_Get` finds the cvar already there and keeps the old value. `VR_TuningCvar`
|
||||
in `vr_openxr.c` exists for this: it registers unarchived and forces the default
|
||||
every start. There is no console in a headset to correct a stale value with.
|
||||
|
||||
**There is no console in a headset.** Every question has to be answered by
|
||||
something written to logcat, or by something visible enough to describe. Prefer
|
||||
pixel readbacks logged as numbers over anything that has to be seen — asking
|
||||
someone to catch a one-frame flash is a bad test, and was one here.
|
||||
|
||||
**Settled, so not re-investigated:**
|
||||
|
||||
- `XR_FB_display_refresh_rate` works; the display runs at 90 Hz. Ask for it from
|
||||
the frame loop with the two-call enumeration, not at `xrBeginSession`.
|
||||
- MSAA via `GL_EXT_multisampled_render_to_texture` works and is off
|
||||
(`vr_samples 0`).
|
||||
- `vr_resolutionScale` is at **0.6** and should go back to 1 once the picture is
|
||||
up — fill rate was never the problem.
|
||||
- A recurring `Fatal signal 11` at address `0x90` on a secondary thread at every
|
||||
launch **predates all VR work** and the process survives it every time.
|
||||
|
||||
**Once the menu draws**, the number this whole swap was for is still unmeasured:
|
||||
renderergl1's back-end cost per eye against rend2's 50–60 ms. Section 2 is the
|
||||
baseline to beat. The instrumentation for splitting a VR frame into
|
||||
wait/scene/issue/gpu is described in `VR_PORT_STATUS.md`.
|
||||
@@ -81,6 +81,12 @@ if(BUILD_GAME_LIBRARIES)
|
||||
set_target_properties( ${CGAME_MODULE_BINARY_BASEGAME} PROPERTIES OUTPUT_NAME ${CGAME_MODULE_BINARY})
|
||||
set_output_dirs( ${CGAME_MODULE_BINARY_BASEGAME} SUBDIRECTORY ${BASEGAME})
|
||||
|
||||
if(ANDROID)
|
||||
# Shipped in the APK, so it needs the prefix to be extracted at all
|
||||
# (see DLL_PREFIX in q_platform.h)
|
||||
set_target_properties( ${CGAME_MODULE_BINARY_BASEGAME} PROPERTIES PREFIX ${ANDROID_APK_LIBRARY_PREFIX})
|
||||
endif()
|
||||
|
||||
INSTALL(TARGETS ${CGAME_MODULE_BINARY_BASEGAME} DESTINATION ${INSTALL_LIBDIR_FULL})
|
||||
|
||||
if(MSVC)
|
||||
@@ -94,6 +100,10 @@ if(BUILD_GAME_LIBRARIES)
|
||||
set_target_properties( ${GAME_MODULE_BINARY_BASEGAME} PROPERTIES OUTPUT_NAME ${GAME_MODULE_BINARY})
|
||||
set_output_dirs( ${GAME_MODULE_BINARY_BASEGAME} SUBDIRECTORY ${BASEGAME})
|
||||
|
||||
if(ANDROID)
|
||||
set_target_properties( ${GAME_MODULE_BINARY_BASEGAME} PROPERTIES PREFIX ${ANDROID_APK_LIBRARY_PREFIX})
|
||||
endif()
|
||||
|
||||
INSTALL(TARGETS ${GAME_MODULE_BINARY_BASEGAME} DESTINATION ${INSTALL_LIBDIR_FULL})
|
||||
|
||||
if(MSVC)
|
||||
|
||||
+9
-2
@@ -42,7 +42,6 @@ set(CLIENT_SOURCES
|
||||
${SOURCE_DIR}/client/cl_uisoundpicker.cpp
|
||||
${SOURCE_DIR}/client/cl_uistd.cpp
|
||||
${SOURCE_DIR}/client/cl_uiview3d.cpp
|
||||
${SOURCE_DIR}/client/libmumblelink.c
|
||||
${SOURCE_DIR}/client/qal.c
|
||||
${SOURCE_DIR}/client/snd_codec_mp3.c
|
||||
${SOURCE_DIR}/client/snd_codec_ogg.c
|
||||
@@ -106,7 +105,15 @@ list(APPEND CLIENT_BINARY_SOURCES
|
||||
${ASM_SOURCES}
|
||||
${CLIENT_LIBRARY_SOURCES})
|
||||
|
||||
add_executable(${CLIENT_BINARY} ${CLIENT_EXECUTABLE_OPTIONS} ${CLIENT_BINARY_SOURCES})
|
||||
if(ANDROID)
|
||||
# SDLActivity dlopen()s the engine and calls SDL_main, so the client is a
|
||||
# shared library rather than an executable. It also has to carry the lib
|
||||
# prefix: the package manager only extracts lib*.so out of an APK.
|
||||
add_library(${CLIENT_BINARY} SHARED ${CLIENT_BINARY_SOURCES})
|
||||
set_target_properties(${CLIENT_BINARY} PROPERTIES PREFIX ${ANDROID_APK_LIBRARY_PREFIX})
|
||||
else()
|
||||
add_executable(${CLIENT_BINARY} ${CLIENT_EXECUTABLE_OPTIONS} ${CLIENT_BINARY_SOURCES})
|
||||
endif()
|
||||
|
||||
target_include_directories( ${CLIENT_BINARY} PRIVATE ${CLIENT_INCLUDE_DIRS})
|
||||
target_include_directories( ${CLIENT_BINARY} PRIVATE ${SOURCE_DIR}/client)
|
||||
|
||||
@@ -43,8 +43,9 @@ list(APPEND SERVER_LIBRARIES m)
|
||||
if(COMPILER_OUTPUT MATCHES "musl|freebsd|openbsd|netbsd")
|
||||
list(APPEND CLIENT_LIBRARIES execinfo)
|
||||
list(APPEND SERVER_LIBRARIES execinfo)
|
||||
elseif (NOT APPLE)
|
||||
elseif (NOT APPLE AND NOT ANDROID)
|
||||
# For when using GLIBC versions older than 2.34
|
||||
# (bionic has no librt; the same symbols live in libc)
|
||||
list(APPEND CLIENT_LIBRARIES rt)
|
||||
list(APPEND SERVER_LIBRARIES rt)
|
||||
endif()
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
if(NOT APPLE)
|
||||
if(NOT APPLE AND NOT ANDROID)
|
||||
# Configure the .desktop entries with the arch suffix
|
||||
configure_file(
|
||||
misc/linux/org.openmoh.openmohaa.desktop.in
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
if(ANDROID)
|
||||
# The launcher picks a game binary to exec; on Android the activity is
|
||||
# the entry point instead.
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(LAUNCHER_SOURCES "${SOURCE_DIR}/Launcher/launch_main.cpp")
|
||||
|
||||
if (WIN32)
|
||||
|
||||
@@ -4,6 +4,8 @@ include(libraries/jpeg)
|
||||
include(libraries/ogg)
|
||||
include(libraries/opus)
|
||||
include(libraries/openal)
|
||||
include(libraries/openxr)
|
||||
include(libraries/gl4es)
|
||||
include(libraries/sdl)
|
||||
include(libraries/vorbis)
|
||||
include(libraries/zlib)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
if(NOT ANDROID OR NOT BUILD_CLIENT OR NOT USE_GL4ES)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# gl4es translates the fixed function OpenGL the original renderer speaks into
|
||||
# the OpenGL ES the device actually has.
|
||||
#
|
||||
# This is here because renderergl1 is the renderer this game's art was authored
|
||||
# for. renderergl2 is the rend2 rewrite: every surface goes through a GLSL
|
||||
# program with dozens of uniforms set on it, once per surface per eye, and the
|
||||
# frame timing found tens of milliseconds of CPU going into that while the GPU
|
||||
# finished in three. None of what it buys is visible on content that has no
|
||||
# normal or specular maps for it to read - so it costs the frame and returns a
|
||||
# picture further from the original than the plain path gives.
|
||||
#
|
||||
# The alternative was to port renderergl1's fixed function calls to ES by hand.
|
||||
# That is the same work gl4es already does, more thoroughly, and it is the same
|
||||
# choice RTCWQuest made for the same reason.
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
set(GL4ES_VERSION 1.1.6)
|
||||
|
||||
set(GL4ES_SOURCE_PATH "" CACHE PATH
|
||||
"Existing gl4es source tree to build against; downloaded if empty")
|
||||
|
||||
if(GL4ES_SOURCE_PATH)
|
||||
FetchContent_Declare(gl4es SOURCE_DIR ${GL4ES_SOURCE_PATH})
|
||||
else()
|
||||
FetchContent_Declare(gl4es
|
||||
GIT_REPOSITORY https://github.com/ptitSeb/gl4es.git
|
||||
GIT_TAG v${GL4ES_VERSION}
|
||||
GIT_SHALLOW TRUE)
|
||||
endif()
|
||||
|
||||
# No X11 and no GBM on Android, and ES 2 as the backend. NO_INIT_CONSTRUCTOR
|
||||
# keeps it from initialising itself behind the engine's back: the context is
|
||||
# created by SDL and handed to OpenXR, and gl4es has to attach to that one
|
||||
# rather than make its own. These are the flags RTCWQuest ships.
|
||||
set(NOX11 ON CACHE BOOL "" FORCE)
|
||||
set(GBM OFF CACHE BOOL "" FORCE)
|
||||
set(STATICLIB OFF CACHE BOOL "" FORCE)
|
||||
|
||||
FetchContent_MakeAvailable(gl4es)
|
||||
|
||||
# The target is called GL, because that is what gl4es presents itself as - it
|
||||
# takes over the OpenGL entry points the renderer calls. Named back to gl4es on
|
||||
# disk so the APK does not ship something calling itself libGL.so, which is a
|
||||
# name the platform has its own ideas about.
|
||||
if(NOT TARGET GL)
|
||||
message(FATAL_ERROR
|
||||
"gl4es did not declare its GL target; nothing would translate the GL1 "
|
||||
"renderer's calls. Configure with -DUSE_GL4ES=OFF to build without it.")
|
||||
endif()
|
||||
|
||||
# Built the way RTCWQuest builds it, because that combination is known to render
|
||||
# on this device and this one does not.
|
||||
#
|
||||
# Their Android.mk (SupportLibs/gl4es/Android.mk) compiles with exactly:
|
||||
#
|
||||
# -DBCMHOST -DNOX11 -DNO_GBM -DDEFAULT_ES=2 -DNO_INIT_CONSTRUCTOR
|
||||
# -O3 -fcommon -fvisibility=hidden -funwind-tables
|
||||
#
|
||||
# Two differences from what CMake produces on its own are worth naming.
|
||||
#
|
||||
# ANDROID is *not* among them. gl4es's own CMakeLists adds it whenever it is
|
||||
# built with the NDK toolchain, and it is not a harmless label: it selects
|
||||
# different code in the loader, in glx, and in hardext - which is where gl4es
|
||||
# compiles a probe shader to decide what its whole fixed function pipeline is
|
||||
# allowed to emit. ndk-build never defines it, so the reference has been
|
||||
# exercised without it and this has not.
|
||||
#
|
||||
# BCMHOST is the Raspberry Pi switch, which looks wrong on a Quest and is
|
||||
# carried anyway, because everything it gates is additionally guarded by
|
||||
# !defined(ANDROID). Matching the reference matters more here than tidiness.
|
||||
#
|
||||
# add_definitions() in gl4es's CMakeLists is a directory property, so the
|
||||
# ANDROID it adds has to be removed at the directory it was set on rather than
|
||||
# overridden on the target.
|
||||
foreach(GL4ES_DIR ${gl4es_SOURCE_DIR} ${gl4es_SOURCE_DIR}/src)
|
||||
set_property(DIRECTORY ${GL4ES_DIR} PROPERTY COMPILE_DEFINITIONS
|
||||
BCMHOST
|
||||
NOX11
|
||||
NO_GBM
|
||||
DEFAULT_ES=2
|
||||
NO_INIT_CONSTRUCTOR)
|
||||
endforeach()
|
||||
|
||||
target_compile_options(GL PRIVATE
|
||||
-O3
|
||||
# gl4es is old C that expects tentative definitions to be merged. Clang has
|
||||
# defaulted to -fno-common since 15; the NDK here is newer than that and the
|
||||
# reference build is not.
|
||||
-fcommon
|
||||
-fvisibility=hidden
|
||||
-funwind-tables)
|
||||
|
||||
# What their LOCAL_LDLIBS names. gl4es dlopen()s the driver itself, so these are
|
||||
# not needed to resolve anything; they are here so the library is built against
|
||||
# the same set the reference links.
|
||||
target_link_libraries(GL PRIVATE EGL GLESv3 dl log)
|
||||
|
||||
set_target_properties(GL PROPERTIES
|
||||
# Shipped inside the APK, so it needs the prefix the top level CMakeLists
|
||||
# cleared for the engine's own libraries.
|
||||
OUTPUT_NAME gl4es
|
||||
PREFIX ${ANDROID_APK_LIBRARY_PREFIX}
|
||||
|
||||
# gl4es builds itself as libGL.so.1 in the manner of a system OpenGL
|
||||
# (src/CMakeLists.txt forces the suffix). Android has no versioned sonames:
|
||||
# the package manager only extracts lib*.so out of an APK, and the linker
|
||||
# would not load the result if it did. Back to a plain suffix.
|
||||
SUFFIX ".so"
|
||||
|
||||
# gl4es's own CMakeLists points CMAKE_LIBRARY_OUTPUT_DIRECTORY at
|
||||
# ${CMAKE_SOURCE_DIR}/lib, and under FetchContent that is *this* project's
|
||||
# source root - so without this the artifact is written into the engine's
|
||||
# source tree, where nobody thinks to look for it and git has to ignore it.
|
||||
LIBRARY_OUTPUT_DIRECTORY ${gl4es_BINARY_DIR}/lib)
|
||||
|
||||
# The renderer includes <GL/gl.h>, which on Android only exists because gl4es
|
||||
# provides it.
|
||||
list(APPEND RENDERER_INCLUDE_DIRS ${gl4es_SOURCE_DIR}/include)
|
||||
list(APPEND RENDERER_LIBRARIES GL)
|
||||
list(APPEND RENDERER_DEFINITIONS USE_GL4ES)
|
||||
@@ -6,7 +6,49 @@ if(NOT BUILD_CLIENT)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(INTERNAL_OPENAL_DIR ${SOURCE_DIR}/thirdparty/openal-soft-1.24.3)
|
||||
set(INTERNAL_OPENAL_VERSION 1.24.3)
|
||||
set(INTERNAL_OPENAL_DIR ${SOURCE_DIR}/thirdparty/openal-soft-${INTERNAL_OPENAL_VERSION})
|
||||
|
||||
if(ANDROID)
|
||||
# There is no system OpenAL on Android, and OpenAL is not optional here:
|
||||
# it is the engine's only sound driver (see SOUND_DRIVER in
|
||||
# snd_local_new.h). Build OpenAL Soft from source; its OpenSL ES backend is
|
||||
# what actually reaches the headset.
|
||||
include(FetchContent)
|
||||
|
||||
set(OPENAL_SOURCE_PATH "" CACHE PATH
|
||||
"Existing OpenAL Soft source tree to build against; downloaded if empty")
|
||||
|
||||
if(OPENAL_SOURCE_PATH)
|
||||
FetchContent_Declare(OpenAL SOURCE_DIR ${OPENAL_SOURCE_PATH})
|
||||
else()
|
||||
FetchContent_Declare(OpenAL
|
||||
URL https://github.com/kcat/openal-soft/archive/refs/tags/${INTERNAL_OPENAL_VERSION}.tar.gz
|
||||
URL_HASH SHA256=7e1fecdeb45e7f78722b776c5cf30bd33934b961d7fd2a11e0494e064cc631ce)
|
||||
endif()
|
||||
|
||||
set(ALSOFT_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(ALSOFT_UTILS OFF CACHE BOOL "" FORCE)
|
||||
set(ALSOFT_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(ALSOFT_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
set(ALSOFT_INSTALL_CONFIG OFF CACHE BOOL "" FORCE)
|
||||
set(ALSOFT_INSTALL_HRTF_DATA OFF CACHE BOOL "" FORCE)
|
||||
set(ALSOFT_INSTALL_AMBDEC_PRESETS OFF CACHE BOOL "" FORCE)
|
||||
set(ALSOFT_INSTALL_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(ALSOFT_INSTALL_UTILS OFF CACHE BOOL "" FORCE)
|
||||
|
||||
FetchContent_MakeAvailable(OpenAL)
|
||||
|
||||
# See the same note in sdl.cmake: the cleared prefix is inherited from the
|
||||
# top level and has to be put back for anything shipped inside the APK.
|
||||
set_target_properties(OpenAL PROPERTIES PREFIX ${ANDROID_APK_LIBRARY_PREFIX})
|
||||
|
||||
# OpenAL::OpenAL carries its own include directory, so <AL/al.h> resolves
|
||||
# without the vendored copy.
|
||||
list(APPEND CLIENT_DEFINITIONS USE_OPENAL)
|
||||
list(APPEND CLIENT_LIBRARIES OpenAL::OpenAL)
|
||||
return()
|
||||
endif()
|
||||
|
||||
find_package(OpenAL QUIET)
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
if(NOT USE_OPENXR OR NOT BUILD_CLIENT)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# The Khronos loader is the vendor-neutral one: it finds whatever runtime the
|
||||
# device provides (Meta, Pico, SteamVR) rather than binding us to a vendor SDK.
|
||||
include(FetchContent)
|
||||
|
||||
set(OPENXR_VERSION 1.1.43)
|
||||
|
||||
set(OPENXR_SOURCE_PATH "" CACHE PATH
|
||||
"Existing OpenXR-SDK source tree to build against; downloaded if empty")
|
||||
|
||||
if(OPENXR_SOURCE_PATH)
|
||||
FetchContent_Declare(OpenXR SOURCE_DIR ${OPENXR_SOURCE_PATH})
|
||||
else()
|
||||
FetchContent_Declare(OpenXR
|
||||
URL https://github.com/KhronosGroup/OpenXR-SDK/archive/refs/tags/release-${OPENXR_VERSION}.tar.gz
|
||||
URL_HASH SHA256=fd0834e4bc75c935248d82ac044f472ab3af4d8ff3b191a3ccace3436975a4cf)
|
||||
endif()
|
||||
|
||||
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_API_LAYERS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_LOADER ON CACHE BOOL "" FORCE)
|
||||
set(DYNAMIC_LOADER ON CACHE BOOL "" FORCE)
|
||||
|
||||
FetchContent_MakeAvailable(OpenXR)
|
||||
|
||||
if(ANDROID)
|
||||
# Shipped in the APK, so it needs the prefix the top level CMakeLists cleared
|
||||
set_target_properties(openxr_loader PROPERTIES PREFIX ${ANDROID_APK_LIBRARY_PREFIX})
|
||||
endif()
|
||||
|
||||
list(APPEND CLIENT_DEFINITIONS USE_OPENXR)
|
||||
list(APPEND CLIENT_LIBRARIES OpenXR::openxr_loader)
|
||||
list(APPEND CLIENT_LIBRARY_SOURCES ${SOURCE_DIR}/vr/vr_openxr.c)
|
||||
|
||||
if(ANDROID)
|
||||
# The VR layer owns the eye framebuffers and hands the session its EGL
|
||||
# context, so unlike the renderer it calls GL and EGL directly rather than
|
||||
# through SDL's loader.
|
||||
list(APPEND CLIENT_LIBRARIES EGL GLESv3)
|
||||
endif()
|
||||
|
||||
# The renderer needs to know which framebuffer to draw an eye into
|
||||
list(APPEND RENDERER_DEFINITIONS USE_OPENXR)
|
||||
@@ -9,10 +9,51 @@ if(EMSCRIPTEN)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(INTERNAL_SDL_DIR ${SOURCE_DIR}/thirdparty/SDL2-2.32.8)
|
||||
set(INTERNAL_SDL_VERSION 2.32.8)
|
||||
set(INTERNAL_SDL_DIR ${SOURCE_DIR}/thirdparty/SDL2-${INTERNAL_SDL_VERSION})
|
||||
|
||||
include(utils/arch)
|
||||
|
||||
if(ANDROID)
|
||||
# There is no prebuilt SDL for Android, so it is built from source next to
|
||||
# the engine. The same tree also carries the org.libsdl.app Java classes
|
||||
# the APK needs, so the unpacked source is worth keeping hold of.
|
||||
include(FetchContent)
|
||||
|
||||
set(SDL2_SOURCE_PATH "" CACHE PATH
|
||||
"Existing SDL2 source tree to build against; downloaded if empty")
|
||||
|
||||
if(SDL2_SOURCE_PATH)
|
||||
FetchContent_Declare(SDL2 SOURCE_DIR ${SDL2_SOURCE_PATH})
|
||||
else()
|
||||
FetchContent_Declare(SDL2
|
||||
URL https://github.com/libsdl-org/SDL/releases/download/release-${INTERNAL_SDL_VERSION}/SDL2-${INTERNAL_SDL_VERSION}.tar.gz
|
||||
URL_HASH SHA256=0ca83e9c9b31e18288c7ec811108e58bac1f1bb5ec6577ad386830eac51c787e)
|
||||
endif()
|
||||
|
||||
# SDLActivity loads libSDL2.so from the APK by name, so it has to be shared.
|
||||
set(SDL_SHARED ON CACHE BOOL "" FORCE)
|
||||
set(SDL_STATIC OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_TEST OFF CACHE BOOL "" FORCE)
|
||||
set(SDL2_DISABLE_INSTALL ON CACHE BOOL "" FORCE)
|
||||
set(SDL2_DISABLE_UNINSTALL ON CACHE BOOL "" FORCE)
|
||||
|
||||
FetchContent_MakeAvailable(SDL2)
|
||||
|
||||
# The top level CMakeLists clears the shared library prefix, which the
|
||||
# fetched project inherits. Put it back, or the APK ships an SDL2.so the
|
||||
# package manager will not extract and SDLActivity cannot load.
|
||||
set_target_properties(SDL2 PROPERTIES PREFIX ${ANDROID_APK_LIBRARY_PREFIX})
|
||||
|
||||
# Where the APK build picks up SDLActivity and friends
|
||||
set(SDL2_JAVA_DIR ${sdl2_SOURCE_DIR}/android-project/app/src/main/java
|
||||
CACHE INTERNAL "")
|
||||
|
||||
list(APPEND CLIENT_LIBRARIES SDL2::SDL2)
|
||||
list(APPEND RENDERER_LIBRARIES SDL2::SDL2)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(WIN32 OR APPLE)
|
||||
# On Windows and macOS we have internal SDL binaries we can use
|
||||
set(HAVE_INTERNAL_SDL true)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
include(platforms/android)
|
||||
include(platforms/emscripten)
|
||||
include(platforms/linux)
|
||||
include(platforms/macos)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# Android specific settings.
|
||||
#
|
||||
# Configured with the NDK's android.toolchain.cmake, e.g.
|
||||
#
|
||||
# cmake -B build-android \
|
||||
# -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
|
||||
# -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-29
|
||||
#
|
||||
# Nothing here is Quest specific; a phone build works the same way.
|
||||
|
||||
if(NOT ANDROID)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# The renderer choice is left to the top level options, which default to
|
||||
# renderergl1. That is the fixed function renderer this game's art was authored
|
||||
# for, and USE_GL4ES puts the translation layer underneath it that lets it reach
|
||||
# OpenGL ES; see cmake/libraries/gl4es.cmake for why it is worth the layer.
|
||||
#
|
||||
# renderergl2 speaks GLES natively and was the first thing to run here, but it
|
||||
# spends tens of milliseconds of CPU per eye binding GLSL programs for lighting
|
||||
# this content has no maps to feed. It is still reachable with
|
||||
#
|
||||
# -DBUILD_RENDERER_GL1=OFF -DBUILD_RENDERER_GL2=ON -DUSE_GL4ES=OFF
|
||||
#
|
||||
# Nothing may be forced into the cache here: these are declared with option(),
|
||||
# so a `set(... CACHE INTERNAL "")` implies FORCE and would silently overwrite
|
||||
# whatever was asked for on the command line.
|
||||
|
||||
# Android's linker namespaces are particular about where a dlopen'd library
|
||||
# may live. Link the renderer into the client rather than fight them.
|
||||
set(USE_RENDERER_DLOPEN OFF CACHE INTERNAL "")
|
||||
set(USE_OPENAL_DLOPEN OFF CACHE INTERNAL "")
|
||||
|
||||
# The QVM toolchain is not part of an Android build.
|
||||
set(BUILD_GAME_QVMS OFF CACHE INTERNAL "")
|
||||
|
||||
# Only for downloading content off a server; not needed to get the game up.
|
||||
set(USE_HTTP OFF CACHE INTERNAL "")
|
||||
|
||||
# The whole point of the Android build here is the headset.
|
||||
set(USE_OPENXR ON CACHE INTERNAL "")
|
||||
|
||||
# There is no bundled SDL binary for Android; it is built from source.
|
||||
set(USE_INTERNAL_SDL OFF CACHE INTERNAL "")
|
||||
|
||||
# No shell to attach a console to, and no desktop to install into.
|
||||
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/install CACHE PATH "" FORCE)
|
||||
|
||||
# Everything the engine prints ends up in logcat.
|
||||
list(APPEND COMMON_LIBRARIES log)
|
||||
|
||||
# Android extracts only lib*.so out of an APK, so everything shipped inside one
|
||||
# needs the prefix - but the top level CMakeLists clears it for the whole
|
||||
# project. Targets bound for the APK opt back in individually with this.
|
||||
set(ANDROID_APK_LIBRARY_PREFIX "lib")
|
||||
|
||||
# Gradle wants every native library for an ABI in one directory, so gather
|
||||
# them there once everything has been declared.
|
||||
list(APPEND POST_CONFIGURE_FUNCTIONS android_stage_apk_libraries)
|
||||
|
||||
function(android_stage_apk_libraries)
|
||||
set(APK_LIB_DIR ${CMAKE_BINARY_DIR}/apk-libs/${ANDROID_ABI})
|
||||
|
||||
# Several of our libraries use the STL, so it is the shared one and has to
|
||||
# be shipped alongside them.
|
||||
set(APK_LIBRARIES ${CMAKE_SYSROOT}/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}/libc++_shared.so)
|
||||
set(APK_TARGETS)
|
||||
|
||||
foreach(APK_TARGET IN ITEMS ${CLIENT_NAME} SDL2 OpenAL openxr_loader GL ${CGAME_MODULE} ${GAME_MODULE})
|
||||
if(TARGET ${APK_TARGET})
|
||||
list(APPEND APK_TARGETS ${APK_TARGET})
|
||||
list(APPEND APK_LIBRARIES $<TARGET_FILE:${APK_TARGET}>)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(NOT APK_TARGETS)
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_custom_target(apk-libs ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${APK_LIB_DIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${APK_LIBRARIES} ${APK_LIB_DIR}
|
||||
COMMENT "Staging APK libraries in ${APK_LIB_DIR}"
|
||||
VERBATIM)
|
||||
|
||||
add_dependencies(apk-libs ${APK_TARGETS})
|
||||
|
||||
# The APK needs the org.libsdl.app Java classes belonging to the very SDL
|
||||
# the engine was linked against, so the gradle build reads the path from
|
||||
# here rather than guessing.
|
||||
file(WRITE ${CMAKE_BINARY_DIR}/android-build.properties
|
||||
"# Written by the CMake build; consumed by misc/android\n"
|
||||
"sdlJavaDir=${SDL2_JAVA_DIR}\n")
|
||||
endfunction()
|
||||
@@ -1,6 +1,6 @@
|
||||
# Linux-like specific settings, i.e. including FreeBSD etc.
|
||||
|
||||
if(NOT UNIX OR APPLE)
|
||||
if(NOT UNIX OR APPLE OR ANDROID)
|
||||
return()
|
||||
endif()
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ endif()
|
||||
|
||||
list(APPEND SYSTEM_PLATFORM_SOURCES ${SOURCE_DIR}/sys/sys_unix.c)
|
||||
|
||||
if(EMSCRIPTEN)
|
||||
if(ANDROID)
|
||||
# No tty; the console goes to logcat
|
||||
list(APPEND SYSTEM_PLATFORM_SOURCES ${SOURCE_DIR}/sys/con_android.c)
|
||||
elseif(EMSCRIPTEN)
|
||||
list(APPEND SYSTEM_PLATFORM_SOURCES ${SOURCE_DIR}/sys/con_passive.c)
|
||||
else()
|
||||
list(APPEND SYSTEM_PLATFORM_SOURCES ${SOURCE_DIR}/sys/con_tty.c)
|
||||
@@ -21,7 +24,12 @@ list(APPEND COMMON_LIBRARIES
|
||||
m # Math library
|
||||
)
|
||||
|
||||
list(APPEND SYSTEM_PLATFORM_SOURCES ${SOURCE_DIR}/sys/new/sys_unix_new.c)
|
||||
if(ANDROID)
|
||||
# bionic has no <execinfo.h>, so the backtrace is walked differently
|
||||
list(APPEND SYSTEM_PLATFORM_SOURCES ${SOURCE_DIR}/sys/new/sys_android_new.c)
|
||||
else()
|
||||
list(APPEND SYSTEM_PLATFORM_SOURCES ${SOURCE_DIR}/sys/new/sys_unix_new.c)
|
||||
endif()
|
||||
|
||||
find_package(Threads)
|
||||
list(APPEND COMMON_LIBRARIES ${CMAKE_DL_LIBS} ${CMAKE_THREAD_LIBS_INIT})
|
||||
|
||||
@@ -37,6 +37,7 @@ set(RENDERER_GL1_SOURCES
|
||||
${SOURCE_DIR}/renderergl1/tr_terrain.c
|
||||
${SOURCE_DIR}/renderergl1/tr_util.cpp
|
||||
${SOURCE_DIR}/renderergl1/tr_vis.cpp
|
||||
${SOURCE_DIR}/renderergl1/tr_vr.c
|
||||
${SOURCE_DIR}/renderergl1/tr_world.c
|
||||
)
|
||||
|
||||
|
||||
+31
-14
@@ -1526,18 +1526,35 @@ CG_Draw2D
|
||||
*/
|
||||
void CG_Draw2D(void)
|
||||
{
|
||||
CG_UpdateCountdown();
|
||||
CG_DrawZoomOverlay();
|
||||
CG_DrawLagometer();
|
||||
CG_HudDrawElements();
|
||||
CG_DrawObjectives();
|
||||
CG_DrawIcons();
|
||||
CG_DrawStopwatch();
|
||||
CG_DrawSpectatorView();
|
||||
CG_DrawPlayerTeam();
|
||||
CG_DrawPlayerEntInfo();
|
||||
CG_UpdateAttackerDisplay();
|
||||
CG_DrawVote();
|
||||
CG_DrawInstantMessageMenu();
|
||||
CG_DrawCrosshair();
|
||||
//
|
||||
// Added in OPM
|
||||
// In VR this is called twice with the frame split in half: once for the
|
||||
// panel on the player's wrist, and once more with the world, for the
|
||||
// couple of things that would be useless anywhere else. 0 is every flat
|
||||
// frame, where it is called once and draws the lot.
|
||||
//
|
||||
const int pass = vr_hudPass ? vr_hudPass->integer : 0;
|
||||
|
||||
if (pass != 2) {
|
||||
CG_UpdateCountdown();
|
||||
CG_DrawLagometer();
|
||||
CG_HudDrawElements();
|
||||
CG_DrawObjectives();
|
||||
CG_DrawIcons();
|
||||
CG_DrawStopwatch();
|
||||
CG_DrawSpectatorView();
|
||||
CG_DrawPlayerTeam();
|
||||
CG_DrawPlayerEntInfo();
|
||||
CG_UpdateAttackerDisplay();
|
||||
CG_DrawVote();
|
||||
CG_DrawInstantMessageMenu();
|
||||
}
|
||||
|
||||
if (pass != 1) {
|
||||
// The reticle is aimed at things out in the world and the scope
|
||||
// surround has to sit around the magnified view, so both stay with the
|
||||
// world rather than travelling to the wrist.
|
||||
CG_DrawZoomOverlay();
|
||||
CG_DrawCrosshair();
|
||||
}
|
||||
}
|
||||
@@ -513,6 +513,8 @@ extern "C" {
|
||||
//
|
||||
extern cvar_t *cg_fov;
|
||||
extern cvar_t *cg_cheats;
|
||||
// Which half of a split VR frame is being drawn; see CG_Draw2D.
|
||||
extern cvar_t *vr_hudPass;
|
||||
|
||||
//
|
||||
// cg_main.c
|
||||
|
||||
@@ -116,6 +116,7 @@ cvar_t *ui_timemessage;
|
||||
// Added in OPM
|
||||
//
|
||||
cvar_t *cg_fov;
|
||||
cvar_t *vr_hudPass;
|
||||
cvar_t *cg_cheats;
|
||||
|
||||
/*
|
||||
@@ -212,6 +213,8 @@ void CG_RegisterCvars(void)
|
||||
//
|
||||
|
||||
cg_fov = cgi.Cvar_Get("cg_fov", "80", CVAR_ARCHIVE);
|
||||
// Set by the client around each half of a split VR frame; 0 on flat frames.
|
||||
vr_hudPass = cgi.Cvar_Get("vr_hudPass", "0", 0);
|
||||
cg_cheats = cgi.Cvar_Get("cheats", "0", CVAR_USERINFO | CVAR_SERVERINFO | CVAR_LATCH);
|
||||
}
|
||||
/*
|
||||
|
||||
+524
-2
@@ -983,6 +983,459 @@ void CG_ProcessPlayerModel()
|
||||
cg_forceModel->modified = qfalse;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
===============
|
||||
CG_VRHandTag
|
||||
|
||||
The bone or tag a hand's mesh hangs from, so the model can be moved to put
|
||||
*that* where the controller is rather than the model origin.
|
||||
|
||||
The two hands want different anchors. The off hand wants its bone, because the
|
||||
hand itself is the thing being placed. The weapon hand wants tag_weapon_right,
|
||||
because what the player is holding is the gun: the controller is a grip, and
|
||||
putting the weapon's own attachment point on it is what makes the gun sit in the
|
||||
hand rather than near it.
|
||||
|
||||
Every candidate is measured, not just the one taken. Whether an anchor is *at*
|
||||
the hand is the whole question and its tag number does not answer it - a
|
||||
distance of nearly nothing from the model origin means the tag is unused,
|
||||
however successfully it resolved. tag_weapon_left is exactly that trap on this
|
||||
model: it resolves, and it anchors nothing, because the first person model never
|
||||
carries a weapon in its left hand.
|
||||
===============
|
||||
*/
|
||||
static int CG_VRHandTag(refEntity_t *model, int hand)
|
||||
{
|
||||
static const char *const offHand[] = {"Bip01 L Hand", "tag_weapon_left"};
|
||||
static const char *const weapHand[] = {"tag_weapon_right", "Bip01 R Hand"};
|
||||
static qboolean reported[2];
|
||||
|
||||
const char *const *candidates = hand ? weapHand : offHand;
|
||||
int chosen = -1;
|
||||
int i;
|
||||
|
||||
//
|
||||
// Says why, rather than returning -1 in silence. A silent -1 anchors the
|
||||
// model on its own origin, which looks exactly like an anchor that was
|
||||
// found and happened to be at the origin - and this cost a whole round trip
|
||||
// once already, when the weapon hand printed nothing at all and there was
|
||||
// no way to tell "never called" from "returned early" from "found nothing".
|
||||
//
|
||||
if (!model || !model->tiki || !cgi.Tag_NumForName || hand < 0 || hand > 1) {
|
||||
if (hand >= 0 && hand <= 1 && !reported[hand]) {
|
||||
cgi.Printf("VR: %s anchor unavailable (model %s, tiki %s, Tag_NumForName %s)\n",
|
||||
hand ? "weapon hand" : "off hand",
|
||||
model ? "ok" : "null",
|
||||
(model && model->tiki) ? "ok" : "null",
|
||||
cgi.Tag_NumForName ? "ok" : "null");
|
||||
reported[hand] = qtrue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < 2; i++) {
|
||||
const int tagNum = cgi.Tag_NumForName(model->tiki, candidates[i]);
|
||||
|
||||
if (!reported[hand] && cgi.TIKI_Orientation) {
|
||||
if (tagNum < 0) {
|
||||
cgi.Printf("VR: %s candidate \"%s\": absent\n",
|
||||
hand ? "weapon hand" : "off hand", candidates[i]);
|
||||
} else {
|
||||
const orientation_t o = cgi.TIKI_Orientation(model, tagNum);
|
||||
|
||||
cgi.Printf("VR: %s candidate \"%s\": tag %d at %.1f %.1f %.1f (%.1f units out)\n",
|
||||
hand ? "weapon hand" : "off hand", candidates[i], tagNum,
|
||||
o.origin[0], o.origin[1], o.origin[2], VectorLength(o.origin));
|
||||
}
|
||||
}
|
||||
|
||||
if (tagNum >= 0 && chosen < 0) {
|
||||
chosen = tagNum;
|
||||
}
|
||||
}
|
||||
|
||||
if (!reported[hand]) {
|
||||
if (chosen < 0) {
|
||||
cgi.Printf("VR: no %s anchor on this model; using the model origin instead\n",
|
||||
hand ? "weapon hand" : "off hand");
|
||||
}
|
||||
reported[hand] = qtrue;
|
||||
}
|
||||
|
||||
return chosen;
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
CG_VRAnchorOnTag
|
||||
|
||||
Takes a model whose origin and axes describe where the *hand* should be, and
|
||||
rewrites them so it is the named tag that ends up there instead.
|
||||
|
||||
The engine composes a tag into the world as (entity.cpp, GetTagPositionAndOrientation):
|
||||
|
||||
world.origin = model.origin + tag.origin through model.axis
|
||||
world.axis = tag.axis composed with model.axis
|
||||
|
||||
so this is that, inverted. An orientation's axes are orthonormal, so undoing the
|
||||
tag's rotation is a transpose.
|
||||
|
||||
A tagNum of -1 leaves the model where it is, which anchors it on its own origin -
|
||||
wrong by however far the animation puts the hand out in front, but visible and
|
||||
debuggable rather than absent.
|
||||
===============
|
||||
*/
|
||||
static void CG_VRAnchorOnTag(refEntity_t *model, int tagNum)
|
||||
{
|
||||
orientation_t tag;
|
||||
vec3_t handOrigin, handAxis[3];
|
||||
vec3_t inverse[3];
|
||||
int i, j;
|
||||
|
||||
if (tagNum < 0 || !cgi.TIKI_Orientation) {
|
||||
return;
|
||||
}
|
||||
|
||||
VectorCopy(model->origin, handOrigin);
|
||||
AxisCopy(model->axis, handAxis);
|
||||
|
||||
tag = cgi.TIKI_Orientation(model, tagNum);
|
||||
|
||||
for (i = 0; i < 3; i++) {
|
||||
for (j = 0; j < 3; j++) {
|
||||
inverse[i][j] = tag.axis[j][i];
|
||||
}
|
||||
}
|
||||
|
||||
MatrixMultiply(inverse, handAxis, model->axis);
|
||||
|
||||
VectorCopy(handOrigin, model->origin);
|
||||
for (i = 0; i < 3; i++) {
|
||||
VectorMA(model->origin, -tag.origin[i], model->axis[i], model->origin);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
CG_VRPlaceViewModel
|
||||
|
||||
Puts the first person model where the player's hand is.
|
||||
|
||||
The offset arrives relative to the head, in engine units, already turned into
|
||||
the engine's frame and de-rotated by whatever heading the play space happened
|
||||
to have. Adding it to the view origin puts the weapon in the room where the
|
||||
hand is.
|
||||
|
||||
The height needs one more step. The view origin is at the eyes, so adding a
|
||||
hand-relative-to-head offset to it would hang the weapon off the *camera*, and
|
||||
crouching or looking down would drag it about. The reference drops to the feet
|
||||
and adds the measured head height back (cg_weapons.c: origin[2] -= 64, then
|
||||
+= hmdposition[1] * worldScale), which leaves the weapon at the height the
|
||||
player is really holding their hand at. 64 is Quake's eye height above the
|
||||
feet, and MOHAA inherits it.
|
||||
|
||||
Yaw is rebased the same way the aim is: the controller's absolute yaw is
|
||||
meaningless to the game, so what arrives is how far the hand leads the head and
|
||||
the view yaw is added back here.
|
||||
|
||||
Returns false when there is no headset, and the caller falls back to the flat
|
||||
placement.
|
||||
===============
|
||||
*/
|
||||
static qboolean CG_VRPlaceViewModel(refEntity_t *model)
|
||||
{
|
||||
vec3_t trackingOffset, angles;
|
||||
float baseYaw = 0.0f;
|
||||
vec3_t bodyAngles, bodyAxis[3];
|
||||
int tagNum;
|
||||
int i;
|
||||
|
||||
if (!cgi.VR_GetWeaponPose || !cgi.VR_GetWeaponPose(trackingOffset, angles, &baseYaw)) {
|
||||
return qfalse;
|
||||
}
|
||||
|
||||
//
|
||||
// Composed exactly the way R_VRComposeView composes the camera, for the
|
||||
// reason set out at length in CG_VRPlaceOffHandModel: cg.refdef.vieworg is
|
||||
// not the camera, and anything anchored on it alone is left behind by the
|
||||
// head's offset within the play space.
|
||||
//
|
||||
// This path used to add a head-relative offset to vieworg and then correct
|
||||
// the height by dropping to the feet and adding the measured head height
|
||||
// back. That is RTCWQuest's way of doing without this composition, and it
|
||||
// has two failure modes this does not: the weapon slid whenever the head
|
||||
// turned, and a snap turn left it behind entirely, because a stick turn
|
||||
// moves the body's frame and a head-relative offset knows nothing about it.
|
||||
//
|
||||
VectorSet(bodyAngles, 0.0f, cg.refdefViewAngles[YAW] - baseYaw, 0.0f);
|
||||
AnglesToAxis(bodyAngles, bodyAxis);
|
||||
|
||||
VectorCopy(cg.refdef.vieworg, model->origin);
|
||||
for (i = 0; i < 3; i++) {
|
||||
VectorMA(model->origin, trackingOffset[i], bodyAxis[i], model->origin);
|
||||
}
|
||||
|
||||
angles[YAW] = AngleNormalize360(angles[YAW] + cg.refdefViewAngles[YAW]);
|
||||
|
||||
//
|
||||
// Per weapon alignment.
|
||||
//
|
||||
// The view model is authored to sit at the camera with the arms reaching
|
||||
// forward, so anchoring its origin at the hand leaves the model's own
|
||||
// internal offset in place and the gun ends up somewhere near the elbow.
|
||||
// Every weapon needs its own correction, and no amount of getting the hand
|
||||
// pose right removes the need for one.
|
||||
//
|
||||
// Same shape as the reference's vr_weapon_adjustment_<id>
|
||||
// (rtcw cg_weapons.c, VrInputWeaponAlign.c):
|
||||
//
|
||||
// scale,right,up,forward,pitch,yaw,roll
|
||||
//
|
||||
// The rotation is composed in the weapon's own frame rather than added to
|
||||
// the angles, which is only correct for one axis at a time. The offset is
|
||||
// then applied along the *corrected* axes - right, up and forward as the
|
||||
// weapon now points - which is why a model that rotates correctly can still
|
||||
// sit in the wrong place: the two have to be done in that order.
|
||||
//
|
||||
{
|
||||
//
|
||||
// The roll defaults to 180 because that is what the model needs, not
|
||||
// because it is a nice starting point. Anchoring forces the *bone* to
|
||||
// take the controller's orientation, and a wrist bone's local axes do
|
||||
// not run the way a controller is held - the mesh inherits that
|
||||
// mismatch as a constant half turn about the aim axis, which reads as
|
||||
// the gun being upside down.
|
||||
//
|
||||
// It lives here rather than only in misc/android/autoexec.cfg because
|
||||
// that file is not packaged into the APK - it goes next to the game
|
||||
// data. A default of zero here means anyone who installs the build
|
||||
// without also pushing that config gets the wrong answer.
|
||||
//
|
||||
cvar_t *adjust = cgi.Cvar_Get("vr_weaponAdjust", "1,0,0,0,0,0,180", CVAR_ARCHIVE);
|
||||
vec3_t off = {0.0f, 0.0f, 0.0f};
|
||||
vec3_t adjustAng = {0.0f, 0.0f, 0.0f};
|
||||
float adjustScale = 1.0f;
|
||||
vec3_t baseAxis[3], adjAxis[3];
|
||||
|
||||
if (adjust && adjust->string[0]) {
|
||||
sscanf(adjust->string, "%f,%f,%f,%f,%f,%f,%f",
|
||||
&adjustScale, &off[0], &off[1], &off[2],
|
||||
&adjustAng[PITCH], &adjustAng[YAW], &adjustAng[ROLL]);
|
||||
}
|
||||
|
||||
//
|
||||
// The first field is the model's scale, not just the offset's. That is
|
||||
// what it is called and what the reference does with it
|
||||
// (VrInputWeaponAlign.c writes it into test_scale, and cg_weapons.c
|
||||
// returns it as the entity scale) - here it was only ever scaling the
|
||||
// offset, which made it a knob that appeared to do nothing to a model
|
||||
// that was the wrong size.
|
||||
//
|
||||
// Applied before anything asks for a tag, because TIKI_Orientation
|
||||
// multiplies the tag's origin by the entity scale. Anchoring first and
|
||||
// scaling afterwards would move the hand off the controller by however
|
||||
// much the scale changed.
|
||||
//
|
||||
if (adjustScale > 0.0f) {
|
||||
model->scale *= adjustScale;
|
||||
}
|
||||
|
||||
VectorScale(off, adjustScale, off);
|
||||
|
||||
AnglesToAxis(angles, baseAxis);
|
||||
AnglesToAxis(adjustAng, adjAxis);
|
||||
MatrixMultiply(adjAxis, baseAxis, model->axis);
|
||||
|
||||
// AnglesToAxis gives forward, left, up - so right is -axis[1].
|
||||
VectorMA(model->origin, off[2], model->axis[0], model->origin);
|
||||
VectorMA(model->origin, off[1], model->axis[2], model->origin);
|
||||
VectorMA(model->origin, -off[0], model->axis[1], model->origin);
|
||||
}
|
||||
|
||||
//
|
||||
// model->origin and model->axis now hold where the *hand* should be. Turn
|
||||
// that into where the model should be, so the grip lands in the hand rather
|
||||
// than the model origin doing - the same inversion the off hand uses, and
|
||||
// the reason it stopped needing a large vr_offHandAdjust.
|
||||
//
|
||||
// tag_weapon_right is preferred over the bone here because it is where the
|
||||
// weapon is actually attached: anchoring it on the controller puts the
|
||||
// gun's grip in the player's hand, which is the thing being held.
|
||||
//
|
||||
tagNum = CG_VRHandTag(model, 1);
|
||||
CG_VRAnchorOnTag(model, tagNum);
|
||||
|
||||
return qtrue;
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
CG_VRPlaceOffHandModel
|
||||
|
||||
Puts a second copy of the view model where the *other* controller is, so the
|
||||
hand that is not holding the weapon follows its own controller.
|
||||
|
||||
The pose arrives the same way the weapon hand's does and is rebased the same
|
||||
way - see CG_VRPlaceViewModel for why the height and the yaw both need work.
|
||||
|
||||
What is different is the anchor. The weapon hand gets away with hanging the
|
||||
model off its origin because vr_weaponAdjust then corrects for wherever the
|
||||
weapon sits within the model. The off hand cannot: the animation puts that hand
|
||||
a long way out in front of the model origin, and by a distance that changes with
|
||||
the weapon. So the hand's own bone is asked where it is *within* the model and
|
||||
that is taken back off, which lands the mesh on the controller whatever the
|
||||
animation is doing.
|
||||
|
||||
tagWorld = model.origin + tagLocal.origin through model.axis
|
||||
tagWorld.axis = tagLocal.axis composed with model.axis
|
||||
|
||||
so to put the tag at the hand, invert both: the model's axes become the hand's
|
||||
with the bone's own rotation taken off, and its origin the hand's position with
|
||||
the bone's offset taken back off along those axes.
|
||||
|
||||
vr_offHandAdjust is the residual, in the same seven-field form as
|
||||
vr_weaponAdjust, because a bone is not a wrist and the difference has to be
|
||||
found on the device.
|
||||
|
||||
Returns false when there is no headset or that hand is not tracked, and the
|
||||
caller does not draw the off hand at all.
|
||||
===============
|
||||
*/
|
||||
static qboolean CG_VRPlaceOffHandModel(refEntity_t *model)
|
||||
{
|
||||
vec3_t trackingOffset, angles;
|
||||
float baseYaw = 0.0f;
|
||||
vec3_t handOrigin, handAxis[3];
|
||||
vec3_t bodyAngles, bodyAxis[3];
|
||||
int tagNum;
|
||||
int i;
|
||||
|
||||
if (!cgi.VR_GetHandPose || !cgi.VR_GetHandPose(0, trackingOffset, angles, &baseYaw)) {
|
||||
return qfalse;
|
||||
}
|
||||
|
||||
//
|
||||
// Composed exactly the way R_VRComposeView composes the camera
|
||||
// (renderergl1/tr_vr.c), because the hand has to land in the same space the
|
||||
// camera ends up in.
|
||||
//
|
||||
// The renderer does not use cg.refdef.vieworg as it stands: it adds the
|
||||
// head's play space offset to it, rotated into the body's frame. So
|
||||
// anchoring anything on cg.refdef.vieworg alone leaves it behind by exactly
|
||||
// that offset - and because the head swings about the neck, every look
|
||||
// around moves it. That was the "hands drift away when you turn your head".
|
||||
//
|
||||
// The body's frame is the view yaw with the head's own contribution taken
|
||||
// back off, which is what baseYaw carries.
|
||||
//
|
||||
VectorSet(bodyAngles, 0.0f, cg.refdefViewAngles[YAW] - baseYaw, 0.0f);
|
||||
AnglesToAxis(bodyAngles, bodyAxis);
|
||||
|
||||
VectorCopy(cg.refdef.vieworg, handOrigin);
|
||||
for (i = 0; i < 3; i++) {
|
||||
VectorMA(handOrigin, trackingOffset[i], bodyAxis[i], handOrigin);
|
||||
}
|
||||
|
||||
//
|
||||
// No eye-height fudge here. Dropping to the feet and adding the measured
|
||||
// head height back is how the weapon hand gets by without this composition;
|
||||
// once the hand is placed in the play space properly its height is already
|
||||
// right, and doing both would move it twice.
|
||||
//
|
||||
angles[YAW] = AngleNormalize360(angles[YAW] + cg.refdefViewAngles[YAW]);
|
||||
|
||||
{
|
||||
//
|
||||
// 180 for the same reason as the weapon hand, and measured to be the
|
||||
// same number despite the two hands being mirrored - which was not a
|
||||
// given, and is why this is its own cvar rather than a shared one.
|
||||
//
|
||||
cvar_t *adjust = cgi.Cvar_Get("vr_offHandAdjust", "1,0,0,0,0,0,180", CVAR_ARCHIVE);
|
||||
vec3_t off = {0.0f, 0.0f, 0.0f};
|
||||
vec3_t adjustAng = {0.0f, 0.0f, 0.0f};
|
||||
float adjustScale = 1.0f;
|
||||
vec3_t baseAxis[3], adjAxis[3];
|
||||
|
||||
if (adjust && adjust->string[0]) {
|
||||
sscanf(adjust->string, "%f,%f,%f,%f,%f,%f,%f",
|
||||
&adjustScale, &off[0], &off[1], &off[2],
|
||||
&adjustAng[PITCH], &adjustAng[YAW], &adjustAng[ROLL]);
|
||||
}
|
||||
|
||||
//
|
||||
// The first field is the model's scale, not just the offset's. That is
|
||||
// what it is called and what the reference does with it
|
||||
// (VrInputWeaponAlign.c writes it into test_scale, and cg_weapons.c
|
||||
// returns it as the entity scale) - here it was only ever scaling the
|
||||
// offset, which made it a knob that appeared to do nothing to a model
|
||||
// that was the wrong size.
|
||||
//
|
||||
// Applied before anything asks for a tag, because TIKI_Orientation
|
||||
// multiplies the tag's origin by the entity scale. Anchoring first and
|
||||
// scaling afterwards would move the hand off the controller by however
|
||||
// much the scale changed.
|
||||
//
|
||||
if (adjustScale > 0.0f) {
|
||||
model->scale *= adjustScale;
|
||||
}
|
||||
|
||||
VectorScale(off, adjustScale, off);
|
||||
|
||||
AnglesToAxis(angles, baseAxis);
|
||||
AnglesToAxis(adjustAng, adjAxis);
|
||||
MatrixMultiply(adjAxis, baseAxis, handAxis);
|
||||
|
||||
// AnglesToAxis gives forward, left, up - so right is -axis[1].
|
||||
VectorMA(handOrigin, off[2], handAxis[0], handOrigin);
|
||||
VectorMA(handOrigin, off[1], handAxis[2], handOrigin);
|
||||
VectorMA(handOrigin, -off[0], handAxis[1], handOrigin);
|
||||
}
|
||||
|
||||
VectorCopy(handOrigin, model->origin);
|
||||
AxisCopy(handAxis, model->axis);
|
||||
|
||||
tagNum = CG_VRHandTag(model, 0);
|
||||
CG_VRAnchorOnTag(model, tagNum);
|
||||
|
||||
return qtrue;
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
CG_VRShowOnlySurface
|
||||
|
||||
Leaves one surface of the view model drawn and hides the rest.
|
||||
|
||||
The first person model is four surfaces - triggerhand, lefthand, garandhand and
|
||||
viewsleeves - so a hand can be drawn on its own without any mesh work. Passing
|
||||
NULL hides all four, which is how the weapon hand's copy is told to stop drawing
|
||||
the hand that has gone to the other controller.
|
||||
|
||||
viewsleeves is deliberately not separable: it is one mesh covering both
|
||||
forearms, so it stays with whichever copy asks for it rather than being split.
|
||||
===============
|
||||
*/
|
||||
static void CG_VRShowOnlySurface(refEntity_t *model, const char *keep)
|
||||
{
|
||||
static const char *const parts[] = {"triggerhand", "lefthand", "garandhand", "viewsleeves"};
|
||||
int i;
|
||||
|
||||
for (i = 0; i < (int)(sizeof(parts) / sizeof(parts[0])); i++) {
|
||||
const int surfaceNum = cgi.Surface_NameToNum(model->tiki, parts[i]);
|
||||
|
||||
if (surfaceNum < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keep && !Q_stricmp(parts[i], keep)) {
|
||||
model->surfaces[surfaceNum] &= ~MDL_SURFACE_NODRAW;
|
||||
} else {
|
||||
model->surfaces[surfaceNum] |= MDL_SURFACE_NODRAW;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
CG_ModelAnim
|
||||
@@ -998,6 +1451,15 @@ void CG_ModelAnim(centity_t *cent, qboolean bDoShaderTime)
|
||||
const char *szTagName;
|
||||
int iAnimFlags;
|
||||
qboolean bThirdPerson = qfalse;
|
||||
//
|
||||
// Added in OPM
|
||||
//
|
||||
// Set when the view model went to the weapon hand's controller, and to
|
||||
// whichever of the two left hand surfaces the weapon called for. Together
|
||||
// they say a second copy should follow the other controller.
|
||||
//
|
||||
qboolean bVRHandTracked = qfalse;
|
||||
const char *pszVROffHandSurf = NULL;
|
||||
|
||||
s1 = ¢->currentState;
|
||||
|
||||
@@ -1476,6 +1938,8 @@ void CG_ModelAnim(centity_t *cent, qboolean bDoShaderTime)
|
||||
if (iSurfaceNum >= 0) {
|
||||
model.surfaces[iSurfaceNum] &= ~MDL_SURFACE_NODRAW;
|
||||
}
|
||||
|
||||
pszVROffHandSurf = "garandhand";
|
||||
} else {
|
||||
// hide the garand hands
|
||||
|
||||
@@ -1488,6 +1952,8 @@ void CG_ModelAnim(centity_t *cent, qboolean bDoShaderTime)
|
||||
if (iSurfaceNum >= 0) {
|
||||
model.surfaces[iSurfaceNum] &= ~MDL_SURFACE_NODRAW;
|
||||
}
|
||||
|
||||
pszVROffHandSurf = "lefthand";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1497,8 +1963,44 @@ void CG_ModelAnim(centity_t *cent, qboolean bDoShaderTime)
|
||||
}
|
||||
|
||||
if (!(cg.predicted_player_state.pm_flags & PMF_CAMERA_VIEW)) {
|
||||
if (cg.snap->ps.stats[STAT_HEALTH] > 0 && !cg_animationviewmodel->integer) {
|
||||
CG_OffsetFirstPersonView(&model, qfalse);
|
||||
//
|
||||
// Added in OPM
|
||||
//
|
||||
// In VR the weapon belongs in the player's hand, not bolted to
|
||||
// the middle of the screen. CG_OffsetFirstPersonView places it
|
||||
// against the camera and adds the walk bob and the landing dip,
|
||||
// all of which are wrong here: the hand has its own position,
|
||||
// and shaking the view model when the camera is the player's
|
||||
// head is a way to make people ill.
|
||||
//
|
||||
// This is RTCWQuest's CG_CalculateVRWeaponPosition
|
||||
// (rtcw/src/cgame/cg_weapons.c), which does the same three
|
||||
// things: put the model at the hand's offset from the head,
|
||||
// re-base its height so it hangs at the player's real hand
|
||||
// height rather than at eye level, and rebase its yaw by how
|
||||
// far the hand leads the head.
|
||||
//
|
||||
if (CG_VRPlaceViewModel(&model)) {
|
||||
//
|
||||
// The hand that is not holding the weapon belongs on its
|
||||
// own controller, so it comes off this copy and is drawn
|
||||
// again below at the other hand. The sleeves stay here:
|
||||
// they are one mesh across both forearms and cannot be
|
||||
// told apart without re-authoring the model.
|
||||
//
|
||||
bVRHandTracked = qtrue;
|
||||
|
||||
if (pszVROffHandSurf) {
|
||||
const int iOffHandSurf = cgi.Surface_NameToNum(model.tiki, pszVROffHandSurf);
|
||||
|
||||
if (iOffHandSurf >= 0) {
|
||||
model.surfaces[iOffHandSurf] |= MDL_SURFACE_NODRAW;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (cg.snap->ps.stats[STAT_HEALTH] > 0 && !cg_animationviewmodel->integer) {
|
||||
CG_OffsetFirstPersonView(&model, qfalse);
|
||||
}
|
||||
}
|
||||
|
||||
AnglesToAxis(cg.refdefViewAngles, cg.refdef.viewaxis);
|
||||
@@ -1520,6 +2022,26 @@ void CG_ModelAnim(centity_t *cent, qboolean bDoShaderTime)
|
||||
|
||||
// add to refresh list
|
||||
cgi.R_AddRefEntityToScene(&model, s1->parent);
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
//
|
||||
// The off hand, on its own controller. The same model again with
|
||||
// everything but that one hand hidden, anchored so the hand's own bone
|
||||
// lands on the controller rather than the model origin.
|
||||
//
|
||||
// It is a second entity rather than a second pose because the skeleton
|
||||
// is shared: both copies are the same animation at the same frame, and
|
||||
// only the transform differs.
|
||||
//
|
||||
if (bVRHandTracked && pszVROffHandSurf) {
|
||||
refEntity_t offHand = model;
|
||||
|
||||
if (CG_VRPlaceOffHandModel(&offHand)) {
|
||||
CG_VRShowOnlySurface(&offHand, pszVROffHandSurf);
|
||||
cgi.R_AddRefEntityToScene(&offHand, s1->parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CG_UpdateEntityEmitters(s1->number, &model, cent);
|
||||
|
||||
@@ -440,6 +440,33 @@ functions exported to the main executable
|
||||
|
||||
size_t (*getConfigStringIdNormalized)(size_t num);
|
||||
|
||||
/**
|
||||
* Where the weapon hand is, when a headset is driving the game.
|
||||
*
|
||||
* offset is the hand relative to the head, in engine units and the
|
||||
* engine's frame - add it to the view origin. angles carry the
|
||||
* controller's pitch and roll, and in YAW how far the hand leads the
|
||||
* head, to be added to the view yaw. headHeight is the head above the
|
||||
* floor in metres, so the weapon can sit at the player's real hand
|
||||
* height rather than at eye level.
|
||||
*
|
||||
* False when there is no headset or the hand is untracked, in which
|
||||
* case the caller keeps the placement it had.
|
||||
*/
|
||||
qboolean (*VR_GetWeaponPose)(vec3_t trackingOffset, vec3_t angles, float *baseYaw);
|
||||
|
||||
/**
|
||||
* @brief Where one controller is, for a hand to be drawn on it.
|
||||
*
|
||||
* hand is 0 for the left and 1 for the right. Same frame, units and
|
||||
* conventions as VR_GetWeaponPose, but the bare hand pose - none of
|
||||
* what the weapon does to its own aim is folded in.
|
||||
*
|
||||
* False when there is no headset or that hand is untracked, in which
|
||||
* case the caller should not draw that hand at all.
|
||||
*/
|
||||
qboolean (*VR_GetHandPose)(int hand, vec3_t trackingOffset, vec3_t angles, float *baseYaw);
|
||||
|
||||
cvar_t *fsDebug;
|
||||
hdelement_t *HudDrawElements;
|
||||
clientAnim_t *anim;
|
||||
|
||||
@@ -516,6 +516,41 @@ static int CG_CalcFov(void)
|
||||
inwater = qfalse;
|
||||
}
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// In VR the eye frustum comes from the headset rather than from this fov,
|
||||
// so a scope or a pair of binoculars would otherwise change nothing at all.
|
||||
// What the renderer wants is the factor rather than the angle: it narrows
|
||||
// the headset's own frustum by this much and leaves the composition layer
|
||||
// claiming the full field of view, so the compositor magnifies the result
|
||||
// back out to fill the display. That is a scope which costs no extra
|
||||
// drawing and loses no resolution - the narrow view was rendered into the
|
||||
// whole eye texture, so it comes out sharper than the unzoomed one.
|
||||
//
|
||||
// Published as a cvar because it is the ratio of two numbers that only
|
||||
// exist here. The aspect correction above cancels between them, so this is
|
||||
// simply the two camera fovs against each other.
|
||||
//
|
||||
{
|
||||
static float lastZoom = -1.0f;
|
||||
float zoom = 1.0f;
|
||||
|
||||
if (cg.camera_fov > 1.0f && cg_fov->value > 1.0f) {
|
||||
zoom = tan(DEG2RAD(cg_fov->value) * 0.5f) / tan(DEG2RAD(cg.camera_fov) * 0.5f);
|
||||
}
|
||||
|
||||
// Only ever narrower. Widening the view in a headset is a good way to
|
||||
// make someone ill, and nothing asks for it deliberately.
|
||||
if (zoom < 1.0f) {
|
||||
zoom = 1.0f;
|
||||
}
|
||||
|
||||
if (fabs(zoom - lastZoom) > 0.001f) {
|
||||
lastZoom = zoom;
|
||||
cgi.Cvar_Set("vr_fovZoom", va("%f", zoom));
|
||||
}
|
||||
}
|
||||
|
||||
// set it
|
||||
cg.refdef.fov_x = fov_x;
|
||||
cg.refdef.fov_y = fov_y;
|
||||
|
||||
@@ -22,6 +22,9 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
// cl_cgame.c -- client system interaction with client game
|
||||
|
||||
#include "client.h"
|
||||
#ifdef USE_OPENXR
|
||||
#include "../vr/vr_common.h"
|
||||
#endif
|
||||
#include "cl_ui.h"
|
||||
#include "cl_uiradar.h"
|
||||
#include "../corepp/tiki.h"
|
||||
@@ -517,6 +520,44 @@ void CL_R_AddPolyToScene( qhandle_t hShader, int numVerts, const polyVert_t *ver
|
||||
re.AddPolyToScene( hShader, numVerts, verts, 1 );
|
||||
}
|
||||
|
||||
/*
|
||||
====================
|
||||
CL_VR_GetWeaponPose
|
||||
|
||||
Hands the weapon hand's pose to cgame, so the view model sits where the
|
||||
controller is rather than bolted to the middle of the screen.
|
||||
|
||||
Answers false without a headset, and cgame keeps its own placement.
|
||||
====================
|
||||
*/
|
||||
static qboolean CL_VR_GetWeaponPose( vec3_t trackingOffset, vec3_t angles, float *baseYaw ) {
|
||||
#ifdef USE_OPENXR
|
||||
return VR_GetWeaponPose( trackingOffset, angles, baseYaw );
|
||||
#else
|
||||
return qfalse;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
====================
|
||||
CL_VR_GetHandPose
|
||||
|
||||
Hands one controller's pose to cgame, so a hand can be drawn on it. Unlike the
|
||||
weapon pose this is the bare hand, with nothing the weapon does to its own aim
|
||||
folded in.
|
||||
|
||||
Answers false without a headset or for an untracked hand, and cgame does not
|
||||
draw that hand at all.
|
||||
====================
|
||||
*/
|
||||
static qboolean CL_VR_GetHandPose( int hand, vec3_t trackingOffset, vec3_t angles, float *baseYaw ) {
|
||||
#ifdef USE_OPENXR
|
||||
return VR_GetHandPose( hand, trackingOffset, angles, baseYaw );
|
||||
#else
|
||||
return qfalse;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
====================
|
||||
CL_GetShaderPointer
|
||||
@@ -827,6 +868,8 @@ void CL_InitCGameDLL( clientGameImport_t *cgi, clientGameExport_t **cge ) {
|
||||
cgi->CL_ClearSavedCgameState = CL_ClearSavedCgameState;
|
||||
|
||||
cgi->getConfigStringIdNormalized = CPT_NormalizeConfigstring;
|
||||
cgi->VR_GetWeaponPose = CL_VR_GetWeaponPose;
|
||||
cgi->VR_GetHandPose = CL_VR_GetHandPose;
|
||||
|
||||
cgi->fsDebug = fs_debug;
|
||||
cgi->HudDrawElements = cls.HudDrawElements;
|
||||
@@ -909,6 +952,7 @@ void CL_InitClientSavedData( void ) {
|
||||
CL_InitializeStopwatch();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
====================
|
||||
CL_InitCGame
|
||||
|
||||
@@ -23,6 +23,9 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
#include "client.h"
|
||||
#include "cl_ui.h"
|
||||
#ifdef USE_OPENXR
|
||||
#include "../vr/vr_common.h"
|
||||
#endif
|
||||
|
||||
unsigned frame_msec;
|
||||
int old_com_frameTime;
|
||||
@@ -54,6 +57,15 @@ kbutton_t in_strafe, in_speed;
|
||||
|
||||
qboolean in_guimouse;
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
cvar_t *cl_vrSnapAngle;
|
||||
cvar_t *cl_vrSmoothTurn;
|
||||
cvar_t *cl_vrTurnSpeed;
|
||||
cvar_t *cl_vrDeadzone;
|
||||
cvar_t *cl_vrWalkWithView;
|
||||
cvar_t *cl_vrStepScale;
|
||||
#endif
|
||||
|
||||
kbutton_t in_up, in_down;
|
||||
|
||||
#ifdef USE_VOIP
|
||||
@@ -723,6 +735,154 @@ void CL_FinishMove( usercmd_t *cmd ) {
|
||||
CL_CreateCmd
|
||||
=================
|
||||
*/
|
||||
#ifdef USE_OPENXR
|
||||
/*
|
||||
=================
|
||||
CL_VRMove
|
||||
|
||||
Turns the headset and its sticks into a usercmd.
|
||||
|
||||
Until now head tracking only moved the camera the renderer drew from, which
|
||||
left the game itself unaware of it - objectives that ask the player to look at
|
||||
something, and anything else that consults the player's view angles, never saw
|
||||
it. Setting the view angles here is what makes the game agree with what the
|
||||
player can see.
|
||||
|
||||
Aim comes from the head for the moment. Section 5 of the porting plan is right
|
||||
that a shooter wants it on the controller instead, but that belongs with weapon
|
||||
handling; looking at things has to work first.
|
||||
=================
|
||||
*/
|
||||
static void CL_VRMove( usercmd_t *cmd ) {
|
||||
static float snapYaw;
|
||||
static qboolean turnCentred = qtrue;
|
||||
vrInput_t in;
|
||||
float deadzone, turn;
|
||||
|
||||
if ( !VR_Enabled() || !VR_GetInput( &in ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
deadzone = cl_vrDeadzone ? cl_vrDeadzone->value : 0.2f;
|
||||
|
||||
// Turning. Snap by default: a stick that rotates the world smoothly while
|
||||
// the inner ear insists otherwise is the classic way to make people ill,
|
||||
// and it costs nothing to offer both.
|
||||
turn = in.turn;
|
||||
|
||||
if ( fabs( turn ) < deadzone ) {
|
||||
turnCentred = qtrue;
|
||||
} else if ( cl_vrSmoothTurn && cl_vrSmoothTurn->integer ) {
|
||||
snapYaw -= turn * ( cl_vrTurnSpeed ? cl_vrTurnSpeed->value : 90.0f ) * (float)cls.frametime * 0.001f;
|
||||
} else if ( turnCentred ) {
|
||||
// One step per flick, not per frame.
|
||||
snapYaw -= ( turn > 0 ? 1.0f : -1.0f ) * ( cl_vrSnapAngle ? cl_vrSnapAngle->value : 45.0f );
|
||||
turnCentred = qfalse;
|
||||
}
|
||||
|
||||
cl.viewangles[YAW] = snapYaw + in.headYaw;
|
||||
cl.viewangles[PITCH] = in.headPitch;
|
||||
|
||||
// Tell the renderer how much of the head's heading it is about to see come
|
||||
// back the other way. These angles reach the camera through the player
|
||||
// state, and the renderer composes the live headset pose on top of that
|
||||
// camera - so without this the heading is applied twice.
|
||||
//
|
||||
// Nothing to take off during a cutscene: the camera then comes from the
|
||||
// game's own camera angles rather than the player's view angles, so the
|
||||
// head's heading never entered it.
|
||||
if ( cl.snap.valid && ( cl.snap.ps.pm_flags & PMF_CAMERA_VIEW ) ) {
|
||||
VR_SetBaseYaw( 0.0f );
|
||||
} else {
|
||||
VR_SetBaseYaw( in.headYaw );
|
||||
}
|
||||
|
||||
// Walking follows the off hand rather than the gaze. Tying travel to where
|
||||
// the player is looking means they cannot glance sideways without veering,
|
||||
// which is what makes room scale movement feel like being dragged around;
|
||||
// steering with the free hand lets them look wherever they like and keep
|
||||
// walking in a straight line.
|
||||
{
|
||||
float forward = in.moveForward;
|
||||
float side = in.moveRight;
|
||||
float heading = 0.0f;
|
||||
|
||||
// Radial, and taken before the rotation below mixes the two axes.
|
||||
// Measuring the components separately afterwards cuts a dead cross out
|
||||
// of the middle of the stick's travel: a gentle push at forty five
|
||||
// degrees splits into two components that are each under the threshold,
|
||||
// so the player leans on the stick and nothing happens.
|
||||
if ( sqrt( forward * forward + side * side ) < deadzone ) {
|
||||
forward = 0.0f;
|
||||
side = 0.0f;
|
||||
}
|
||||
|
||||
if ( in.offhandTracked && !( cl_vrWalkWithView && cl_vrWalkWithView->integer ) ) {
|
||||
heading = in.offhandYaw - in.headYaw;
|
||||
}
|
||||
|
||||
if ( heading != 0.0f ) {
|
||||
// forwardmove and rightmove are resolved against the view angles, so
|
||||
// what goes in the usercmd is the stick expressed in the head's
|
||||
// frame - which means rotating it back by however far the hand leads
|
||||
// the head, not forward by it. The wrong sign here mirrors the
|
||||
// steering rather than offsetting it: the player points the hand
|
||||
// left and walks right, and only dead ahead behaves.
|
||||
float radians = DEG2RAD( heading );
|
||||
float c = cos( radians );
|
||||
float sn = sin( radians );
|
||||
float f = forward;
|
||||
|
||||
forward = f * c + side * sn;
|
||||
side = side * c - f * sn;
|
||||
}
|
||||
|
||||
// Room scale, added on top of the stick. The headset moving is only a
|
||||
// camera offset until it reaches the usercmd: the view goes where the
|
||||
// player walked and the character stays behind, so they lean through
|
||||
// walls and never actually travel. Handing the step to the same pmove
|
||||
// the stick feeds means the body goes too, and is stopped by the same
|
||||
// things that stop it when walking on the stick.
|
||||
{
|
||||
float stepForward = in.stepForward;
|
||||
float stepSide = in.stepRight;
|
||||
|
||||
// Into the view's frame, exactly as the stick is, and for the same
|
||||
// reason: the step is measured in the play space, but forwardmove
|
||||
// and rightmove are resolved against where the player is looking.
|
||||
if ( in.headYaw != 0.0f ) {
|
||||
float radians = DEG2RAD( -in.headYaw );
|
||||
float c = cos( radians );
|
||||
float sn = sin( radians );
|
||||
float f = stepForward;
|
||||
|
||||
stepForward = f * c + stepSide * sn;
|
||||
stepSide = stepSide * c - f * sn;
|
||||
}
|
||||
|
||||
if ( cls.frametime > 0 ) {
|
||||
// Units this frame into units a second, then into the fraction
|
||||
// of a run that represents, which is what the usercmd carries.
|
||||
const float perSecond = 1000.0f / (float)cls.frametime;
|
||||
const float runSpeed = 250.0f;
|
||||
const float factor = cl_vrStepScale ? cl_vrStepScale->value : 1.0f;
|
||||
|
||||
forward += stepForward * perSecond / runSpeed * factor;
|
||||
side += stepSide * perSecond / runSpeed * factor;
|
||||
}
|
||||
}
|
||||
|
||||
if ( forward != 0.0f ) {
|
||||
cmd->forwardmove = ClampChar( cmd->forwardmove + (int)( forward * 127.0f ) );
|
||||
}
|
||||
|
||||
if ( side != 0.0f ) {
|
||||
cmd->rightmove = ClampChar( cmd->rightmove + (int)( side * 127.0f ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
usercmd_t CL_CreateCmd( void ) {
|
||||
usercmd_t cmd;
|
||||
vec3_t oldAngles;
|
||||
@@ -745,6 +905,10 @@ usercmd_t CL_CreateCmd( void ) {
|
||||
// get basic movement from joystick
|
||||
CL_JoystickMove( &cmd );
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
CL_VRMove( &cmd );
|
||||
#endif
|
||||
|
||||
// check to make sure the angles haven't wrapped
|
||||
if ( cl.viewangles[PITCH] - oldAngles[PITCH] > 90 ) {
|
||||
cl.viewangles[PITCH] = oldAngles[PITCH] + 90;
|
||||
@@ -1210,6 +1374,18 @@ void CL_InitInput( void ) {
|
||||
|
||||
cl_nodelta = Cvar_Get ("cl_nodelta", "0", 0);
|
||||
cl_debugMove = Cvar_Get ("cl_debugMove", "0", 0);
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
// Comfort options, all tunable on the headset rather than at build time.
|
||||
cl_vrSnapAngle = Cvar_Get( "cl_vrSnapAngle", "45", CVAR_ARCHIVE );
|
||||
cl_vrSmoothTurn = Cvar_Get( "cl_vrSmoothTurn", "0", CVAR_ARCHIVE );
|
||||
cl_vrTurnSpeed = Cvar_Get( "cl_vrTurnSpeed", "90", CVAR_ARCHIVE );
|
||||
cl_vrDeadzone = Cvar_Get( "cl_vrDeadzone", "0.2", CVAR_ARCHIVE );
|
||||
cl_vrWalkWithView = Cvar_Get( "cl_vrWalkWithView", "0", CVAR_ARCHIVE );
|
||||
// How much of a real step becomes a step in the game. 1 is one for one at
|
||||
// vr_worldscale; higher covers more ground than the room has.
|
||||
cl_vrStepScale = Cvar_Get( "cl_vrStepScale", "1", CVAR_ARCHIVE );
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -29,6 +29,9 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
#include "../qcommon/localization.h"
|
||||
#include "../qcommon/bg_compat.h"
|
||||
#include "../sys/sys_local.h"
|
||||
#ifdef USE_OPENXR
|
||||
#include "../vr/vr_common.h"
|
||||
#endif
|
||||
#include "../sys/sys_update_checker.h"
|
||||
#include "../uilib/uimessage.h"
|
||||
|
||||
@@ -2677,12 +2680,121 @@ CL_Frame
|
||||
|
||||
==================
|
||||
*/
|
||||
/*
|
||||
==================
|
||||
CL_RunStartupCommand
|
||||
|
||||
Runs vr_testStart once, a moment after the player is actually in the world.
|
||||
|
||||
Setting up a weapon test otherwise means playing through to wherever the game
|
||||
hands one over, every single build. There is no console in a headset to type
|
||||
"give" into either, so the command has to come from the config - and it cannot
|
||||
simply go in autoexec.cfg, which runs before there is a map, let alone a player
|
||||
to give anything to.
|
||||
|
||||
The delay is because CA_ACTIVE arrives before the player entity has finished
|
||||
spawning, and a give aimed at nothing is silently dropped.
|
||||
==================
|
||||
*/
|
||||
static void CL_RunStartupCommand( void ) {
|
||||
static qboolean fired;
|
||||
static qboolean mapFired;
|
||||
static int activeSince;
|
||||
static int idleSince;
|
||||
cvar_t *cmd;
|
||||
|
||||
// Loading the map from autoexec.cfg does not survive: that runs during
|
||||
// Com_Init, and the game's own startup - the intro stages and then the
|
||||
// menu - comes afterwards and tears it down again. So it waits here until
|
||||
// the client is actually sitting at the menu doing nothing.
|
||||
//
|
||||
// CA_CINEMATIC counts as sitting at the menu. This is not obvious and it
|
||||
// cost a launch: the enum reads "playing a cinematic or a static pic, not
|
||||
// connected to a server", and MOHAA's main menu is drawn over a static pic -
|
||||
// so the client is CA_CINEMATIC there, never CA_DISCONNECTED. Waiting only
|
||||
// on the latter meant idleSince was cleared every frame the menu was up and
|
||||
// the wait below could never accumulate, so the map simply never loaded.
|
||||
if ( clc.state == CA_DISCONNECTED || clc.state == CA_CINEMATIC ) {
|
||||
cvar_t *startMap;
|
||||
|
||||
activeSince = 0;
|
||||
fired = qfalse;
|
||||
|
||||
if ( mapFired ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !idleSince ) {
|
||||
idleSince = cls.realtime;
|
||||
return;
|
||||
}
|
||||
|
||||
// Long enough for the intro stages to have finished handing the client
|
||||
// around; starting a map underneath them leaves the menu on top of it.
|
||||
if ( cls.realtime - idleSince < 4000 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
startMap = Cvar_Get( "vr_startMap", "", CVAR_TEMP );
|
||||
mapFired = qtrue;
|
||||
|
||||
// Printed either way, and with the state that let it through, for the
|
||||
// same reason vr_testStart below prints either way: a hook that silently
|
||||
// does nothing looks exactly like one that never ran, and telling those
|
||||
// two apart is the whole cost of a device round trip.
|
||||
Com_Printf( "vr_startMap: idle in state %d, map is \"%s\"\n",
|
||||
(int)clc.state, ( startMap && startMap->string[0] ) ? startMap->string : "" );
|
||||
|
||||
if ( startMap && startMap->string[0] ) {
|
||||
Cbuf_AddText( va( "map %s\n", startMap->string ) );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
idleSince = 0;
|
||||
|
||||
if ( clc.state != CA_ACTIVE ) {
|
||||
// Armed again for the next map, so this works on every load rather
|
||||
// than only the first.
|
||||
activeSince = 0;
|
||||
fired = qfalse;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( fired ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !activeSince ) {
|
||||
activeSince = cls.realtime;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( cls.realtime - activeSince < 1500 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
cmd = Cvar_Get( "vr_testStart", "", CVAR_TEMP );
|
||||
fired = qtrue;
|
||||
|
||||
// Printed either way. A hook that silently does nothing is indistinguishable
|
||||
// from one that never ran, and telling those apart has cost enough already.
|
||||
Com_Printf( "vr_testStart: in world, command is \"%s\"\n",
|
||||
( cmd && cmd->string[0] ) ? cmd->string : "" );
|
||||
|
||||
if ( cmd && cmd->string[0] ) {
|
||||
Cbuf_AddText( va( "%s\n", cmd->string ) );
|
||||
}
|
||||
}
|
||||
|
||||
void CL_Frame ( int msec ) {
|
||||
|
||||
if ( !com_cl_running->integer ) {
|
||||
return;
|
||||
}
|
||||
|
||||
CL_RunStartupCommand();
|
||||
|
||||
#ifdef USE_CURL
|
||||
if(clc.downloadCURLM) {
|
||||
CL_cURL_PerformDownload();
|
||||
@@ -2898,6 +3010,13 @@ CL_ShutdownRef
|
||||
============
|
||||
*/
|
||||
void CL_ShutdownRef( void ) {
|
||||
#ifdef USE_OPENXR
|
||||
// The session and its swapchains belong to the GL context that is about to
|
||||
// be destroyed; left alone they would outlive it and every swapchain
|
||||
// acquire afterwards would fail.
|
||||
VR_DestroySession();
|
||||
#endif
|
||||
|
||||
if ( re.Shutdown ) {
|
||||
re.Shutdown( qtrue );
|
||||
}
|
||||
@@ -2932,6 +3051,12 @@ void CL_StartHunkUsers( qboolean rendererOnly ) {
|
||||
if ( !cls.rendererRegistered ) {
|
||||
cls.rendererRegistered = qtrue;
|
||||
CL_BeginRegistration();
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
// The GL context exists from here, which is what the session binds to.
|
||||
VR_CreateSession();
|
||||
#endif
|
||||
|
||||
UI_ResolutionChange();
|
||||
}
|
||||
|
||||
@@ -3187,6 +3312,22 @@ qboolean CL_IsRendererLoaded(void) {
|
||||
CL_InitRef
|
||||
============
|
||||
*/
|
||||
/*
|
||||
============
|
||||
CL_GetVRRenderResolution
|
||||
============
|
||||
*/
|
||||
static qboolean CL_GetVRRenderResolution( int *width, int *height ) {
|
||||
#ifdef USE_OPENXR
|
||||
if ( VR_Enabled() ) {
|
||||
VR_GetRenderResolution( width, height );
|
||||
return ( *width > 0 && *height > 0 ) ? qtrue : qfalse;
|
||||
}
|
||||
#endif
|
||||
|
||||
return qfalse;
|
||||
}
|
||||
|
||||
void CL_InitRef( void ) {
|
||||
refimport_t ri;
|
||||
refexport_t *ret;
|
||||
@@ -3335,6 +3476,7 @@ void CL_InitRef( void ) {
|
||||
ri.TIKI_GetLocalFromGlobal = CL_RefTIKI_GetLocalFromGlobal;
|
||||
|
||||
ri.SKEL_GetBoneParent = CL_RefSKEL_GetBoneParent;
|
||||
ri.GetVRRenderResolution = CL_GetVRRenderResolution;
|
||||
ri.SKEL_GetMorphWeightFrame = CL_RefSKEL_GetMorphWeightFrame;
|
||||
|
||||
ret = GetRefAPI( REF_API_VERSION, &ri );
|
||||
@@ -3554,6 +3696,12 @@ void CL_Init( void ) {
|
||||
|
||||
Com_Printf( "----- Client Initialization -----\n" );
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
// Before the renderer, because the eye resolution the runtime asks for is
|
||||
// what the window and the render targets are sized to.
|
||||
VR_Init();
|
||||
#endif
|
||||
|
||||
start = Sys_Milliseconds();
|
||||
|
||||
CL_ClearState ();
|
||||
|
||||
@@ -23,10 +23,15 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
#include "client.h"
|
||||
#include "cl_ui.h"
|
||||
#ifdef USE_OPENXR
|
||||
#include "../vr/vr_common.h"
|
||||
#endif
|
||||
|
||||
qboolean scr_initialized; // ready to draw
|
||||
stereoFrame_t s_scr_stereoFrame;
|
||||
|
||||
vrDrawMode_t cl_vrDrawMode = VRDRAW_ALL;
|
||||
|
||||
cvar_t *cl_timegraph;
|
||||
cvar_t *cl_debuggraph;
|
||||
cvar_t *cl_graphheight;
|
||||
@@ -494,6 +499,101 @@ SCR_SimpleUpdateScreen
|
||||
==================
|
||||
*/
|
||||
void SCR_SimpleUpdateScreen( void ) {
|
||||
#ifdef USE_OPENXR
|
||||
// In VR the frame is drawn once per eye, each into its own swapchain image,
|
||||
// and handed to the compositor rather than to the window. Deliberately two
|
||||
// passes and no more: a third, mono pass to produce a flat image would cost
|
||||
// half the frame again.
|
||||
if( VR_Enabled() ) {
|
||||
if( VR_BeginFrame() ) {
|
||||
if( VR_UseScreenLayer() ) {
|
||||
// Flat content: draw it once onto a panel fixed in front of the
|
||||
// viewer. Drawing it into both eyes instead would give it no
|
||||
// stereo disparity while each eye still warps it through its own
|
||||
// asymmetric frustum, and the two images would never fuse.
|
||||
VR_PrepareScreenLayer();
|
||||
|
||||
UpdateStereoSide( STEREO_CENTER );
|
||||
|
||||
if( com_speeds->integer ) {
|
||||
re.EndFrame( &time_frontend, &time_backend );
|
||||
}
|
||||
else {
|
||||
re.EndFrame( NULL, NULL );
|
||||
}
|
||||
|
||||
VR_FinishScreenLayer();
|
||||
}
|
||||
else {
|
||||
int eye;
|
||||
// Only worth splitting the frame when something else is going to
|
||||
// draw the flat half. Otherwise the eye passes carry it, the way
|
||||
// a flat frame does, and RB_SetGL2D converges it.
|
||||
const qboolean split = VR_WristPanelEnabled();
|
||||
|
||||
cl_vrDrawMode = split ? VRDRAW_WORLD : VRDRAW_ALL;
|
||||
Cvar_Set( "vr_hudPass", split ? "2" : "0" );
|
||||
|
||||
for( eye = 0; eye < VR_MAX_EYES; eye++ ) {
|
||||
VR_PrepareEye( eye );
|
||||
|
||||
// Deliberately STEREO_CENTER for both eyes.
|
||||
// STEREO_LEFT/RIGHT select the GL_BACK_LEFT/RIGHT draw
|
||||
// buffers of quad buffered stereo, a different mechanism
|
||||
// entirely: here each eye already has its own framebuffer,
|
||||
// and its viewpoint comes from the headset pose.
|
||||
{
|
||||
const int sceneStart = Sys_Milliseconds();
|
||||
int issueStart;
|
||||
|
||||
UpdateStereoSide( STEREO_CENTER );
|
||||
|
||||
issueStart = Sys_Milliseconds();
|
||||
|
||||
// Always asked for here, not only under com_speeds: the
|
||||
// frame timing report wants the split, and there is no
|
||||
// console in a headset to turn com_speeds on with.
|
||||
re.EndFrame( &time_frontend, &time_backend );
|
||||
VR_TraceRenderTimes( time_frontend, time_backend );
|
||||
VR_TraceSceneTimes( issueStart - sceneStart,
|
||||
Sys_Milliseconds() - issueStart );
|
||||
}
|
||||
|
||||
VR_FinishEye( eye );
|
||||
}
|
||||
|
||||
// Then the HUD and any menus, once, onto the panel on the
|
||||
// player's wrist. Once rather than per eye because it is flat:
|
||||
// the compositor gives it the same disparity in both eyes by
|
||||
// placing the quad in the room.
|
||||
if( VR_WristPanelVisible() ) {
|
||||
cl_vrDrawMode = VRDRAW_PANEL;
|
||||
Cvar_Set( "vr_hudPass", "1" );
|
||||
|
||||
VR_PrepareWristPanel();
|
||||
|
||||
UpdateStereoSide( STEREO_CENTER );
|
||||
|
||||
if( com_speeds->integer ) {
|
||||
re.EndFrame( &time_frontend, &time_backend );
|
||||
}
|
||||
else {
|
||||
re.EndFrame( NULL, NULL );
|
||||
}
|
||||
|
||||
VR_FinishWristPanel();
|
||||
}
|
||||
|
||||
cl_vrDrawMode = VRDRAW_ALL;
|
||||
Cvar_Set( "vr_hudPass", "0" );
|
||||
}
|
||||
}
|
||||
|
||||
VR_SubmitFrame();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// if running in stereo, we need to draw the frame twice
|
||||
if( cls.glconfig.stereoEnabled ) {
|
||||
UpdateStereoSide( STEREO_LEFT );
|
||||
|
||||
@@ -29,6 +29,9 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
#include "../qcommon/q_version.h"
|
||||
|
||||
#include "cl_ui.h"
|
||||
#ifdef USE_OPENXR
|
||||
#include "../vr/vr_common.h"
|
||||
#endif
|
||||
#include "cl_uigamespy.h"
|
||||
|
||||
#include <chrono>
|
||||
@@ -1835,6 +1838,15 @@ void UI_Update(void)
|
||||
frame = uWinMan.getFrame();
|
||||
view3d->Display(frame, 1.0);
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// The health, ammo and compass widgets are flat, so in VR they belong
|
||||
// on the wrist panel rather than in the eye buffers.
|
||||
//
|
||||
if (cl_vrDrawMode == VRDRAW_WORLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ui_hud && !view3d->LetterboxActive()) {
|
||||
// draw the health hud
|
||||
if (hud_health) {
|
||||
@@ -2393,6 +2405,24 @@ void UI_Update(void)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// An eye pass wants the 3D view and none of the widgets around it, so it
|
||||
// goes straight at the view rather than walking the whole tree. Everything
|
||||
// else in the tree is flat and is drawn in the panel pass instead.
|
||||
//
|
||||
if (cl_vrDrawMode == VRDRAW_WORLD) {
|
||||
if (view3d && view3d->getShow()) {
|
||||
view3d->Display(uWinMan.getFrame(), 1.0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
VR_TraceEvent(VRTRACE_UPDATEVIEWS);
|
||||
VR_TraceState((int)cl_vrDrawMode, Cvar_Get("vr_hudPass", "0", 0)->integer, cls.no_menus);
|
||||
#endif
|
||||
|
||||
uWinMan.UpdateViews();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "cl_ui.h"
|
||||
#ifdef USE_OPENXR
|
||||
#include "../vr/vr_common.h"
|
||||
#endif
|
||||
#include "../qcommon/localization.h"
|
||||
|
||||
#include "../server/server.h"
|
||||
@@ -571,36 +574,77 @@ void View3D::DrawNetProfile(void)
|
||||
|
||||
void View3D::Draw2D(void)
|
||||
{
|
||||
#ifdef USE_OPENXR
|
||||
VR_TraceEvent(VRTRACE_DRAW2D);
|
||||
#endif
|
||||
#ifdef USE_OPENXR
|
||||
const int fadeStart = Sys_Milliseconds();
|
||||
int printStart = fadeStart;
|
||||
int overlayStart = fadeStart;
|
||||
int fadeEnd;
|
||||
int overlayEnd = fadeStart;
|
||||
#endif
|
||||
|
||||
if (!cls.no_menus) {
|
||||
DrawFades();
|
||||
}
|
||||
|
||||
DrawLetterbox();
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
fadeEnd = Sys_Milliseconds();
|
||||
#endif
|
||||
|
||||
if ((cl_debuggraph->integer || cl_timegraph->integer) && !cls.no_menus) {
|
||||
SCR_DrawDebugGraph();
|
||||
} else if (!cls.no_menus) {
|
||||
if (cge) {
|
||||
#ifdef USE_OPENXR
|
||||
const int cgameStart = Sys_Milliseconds();
|
||||
VR_TraceEvent(VRTRACE_CGAME_HUD);
|
||||
#endif
|
||||
cge->CG_Draw2D();
|
||||
#ifdef USE_OPENXR
|
||||
VR_TraceHudTimes(Sys_Milliseconds() - cgameStart);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
printStart = Sys_Milliseconds();
|
||||
#endif
|
||||
|
||||
if (m_locationprint) {
|
||||
LocationPrint();
|
||||
} else {
|
||||
CenterPrint();
|
||||
}
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
overlayStart = Sys_Milliseconds();
|
||||
#endif
|
||||
|
||||
if (!cls.no_menus) {
|
||||
DrawSoundOverlay();
|
||||
DrawNetProfile();
|
||||
DrawSubtitleOverlay();
|
||||
}
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
overlayEnd = Sys_Milliseconds();
|
||||
#endif
|
||||
}
|
||||
|
||||
if (fps->integer && !cls.no_menus) {
|
||||
DrawFPS();
|
||||
DrawProf();
|
||||
}
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
// Everything from here accounted for: whatever the parts do not add up to
|
||||
// is not in this function at all.
|
||||
VR_TraceHudParts(0, fadeEnd - fadeStart, overlayStart - printStart,
|
||||
overlayEnd - overlayStart, Sys_Milliseconds() - overlayEnd);
|
||||
#endif
|
||||
}
|
||||
|
||||
void View3D::CenterPrint(void)
|
||||
@@ -777,15 +821,62 @@ void View3D::DrawFades(void)
|
||||
|
||||
void View3D::Draw(void)
|
||||
{
|
||||
if (clc.state != CA_DISCONNECTED) {
|
||||
const int drawStart = Sys_Milliseconds();
|
||||
#ifdef USE_OPENXR
|
||||
VR_TraceEvent(VRTRACE_VIEW3D);
|
||||
#endif
|
||||
int hudStart;
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// The one place the 3D scene and the flat content over it meet, and so the
|
||||
// one place a VR frame has to be able to take them apart - see
|
||||
// vrDrawMode_t. The eye passes want the scene and nothing else; the wrist
|
||||
// panel wants everything else and not the scene.
|
||||
//
|
||||
if (clc.state != CA_DISCONNECTED && cl_vrDrawMode != VRDRAW_PANEL) {
|
||||
SCR_DrawScreenField();
|
||||
}
|
||||
|
||||
hudStart = Sys_Milliseconds();
|
||||
|
||||
if (cl_vrDrawMode == VRDRAW_WORLD) {
|
||||
//
|
||||
// Added in OPM
|
||||
// Everything flat has gone to the wrist panel except the reticle and
|
||||
// the scope surround, which have to stay with the world - see
|
||||
// CG_Draw2D, which draws only those while vr_hudPass says so.
|
||||
//
|
||||
if (cge && !cls.no_menus) {
|
||||
set2D();
|
||||
cge->CG_Draw2D();
|
||||
}
|
||||
#ifdef USE_OPENXR
|
||||
VR_TraceViewTimes(hudStart - drawStart, Sys_Milliseconds() - hudStart);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
{
|
||||
const int setupStart = Sys_Milliseconds();
|
||||
|
||||
set2D();
|
||||
re.SavePerformanceCounters();
|
||||
|
||||
VR_TraceHudParts(Sys_Milliseconds() - setupStart, 0, 0, 0, 0);
|
||||
}
|
||||
#else
|
||||
set2D();
|
||||
|
||||
re.SavePerformanceCounters();
|
||||
#endif
|
||||
|
||||
Draw2D();
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
VR_TraceViewTimes(hudStart - drawStart, Sys_Milliseconds() - hudStart);
|
||||
#endif
|
||||
}
|
||||
|
||||
float avWidth = 0.0;
|
||||
|
||||
@@ -406,6 +406,29 @@ typedef struct {
|
||||
|
||||
extern clientStatic_t cls;
|
||||
|
||||
/*
|
||||
Which half of the frame is being drawn.
|
||||
|
||||
In VR the world and the flat content that sits over it cannot share a pass. The
|
||||
world has to be drawn once per eye, from that eye's viewpoint; the HUD and the
|
||||
menus have to be drawn exactly once, flat, onto a panel the compositor places in
|
||||
the room - drawing them into the eye buffers instead leaves them with no stereo
|
||||
disparity while each eye still warps them through its own asymmetric frustum, so
|
||||
the two images never fuse. The screen space HUD looking wrong in a headset is
|
||||
the same problem the menus had before they moved to a quad.
|
||||
|
||||
So a VR frame is three passes rather than two: the world into each eye, then the
|
||||
flat content into the panel. This says which one is running. VRDRAW_ALL is every
|
||||
non VR frame, where the two are drawn together as they always were.
|
||||
*/
|
||||
typedef enum {
|
||||
VRDRAW_ALL, // everything, in one pass - the flat path
|
||||
VRDRAW_WORLD, // the 3D scene only, once per eye
|
||||
VRDRAW_PANEL // the HUD and menus only, once, onto the wrist panel
|
||||
} vrDrawMode_t;
|
||||
|
||||
extern vrDrawMode_t cl_vrDrawMode;
|
||||
|
||||
//=============================================================================
|
||||
|
||||
//extern vm_t *cgvm; // interface to cgame dll or vm
|
||||
@@ -701,6 +724,11 @@ void SCR_DrawSmallChar( int x, int y, int ch );
|
||||
|
||||
void UpdateStereoSide( stereoFrame_t s );
|
||||
|
||||
// Whether a menu covering the whole screen is up. True during the mission
|
||||
// briefing and anything else that takes over the view while the player is
|
||||
// still in the world.
|
||||
qboolean UI_FullscreenMenuUp( void );
|
||||
|
||||
//
|
||||
// cl_cin.c
|
||||
//
|
||||
|
||||
@@ -497,6 +497,19 @@ typedef struct gameImport_s {
|
||||
int (*pvssoundindex)(const char* name, int streamed);
|
||||
void (*KickClientForReason)(int clientNum, const char *reason);
|
||||
cvar_t *(*Cvar_Find)(const char *varName);
|
||||
/**
|
||||
* Where the weapon hand is pointing, when a headset is driving the game.
|
||||
*
|
||||
* out[PITCH] is the controller's own pitch; out[YAW] is how far the hand
|
||||
* leads the head, to be added to the view yaw the game already has;
|
||||
* out[ROLL] is unused. False when there is no headset or the hand is not
|
||||
* tracked, in which case the caller keeps the aim it had.
|
||||
*
|
||||
* The angles come from the client, so this is a single player facility:
|
||||
* a dedicated server has no headset to ask and always answers false.
|
||||
*/
|
||||
qboolean (*VR_GetWeaponAim)(vec3_t out);
|
||||
|
||||
|
||||
unsigned int (*Client_NumPendingCommands)(int clientNum);
|
||||
unsigned int (*Client_MaxPendingCommands)(int clientNum);
|
||||
|
||||
@@ -7132,8 +7132,32 @@ void Player::PlayerAngles(void)
|
||||
angles[1] = portableTurret->GetStartYaw();
|
||||
}
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// PmoveAdjustAngleSettings clamps where the player may look while they are
|
||||
// on a ladder - pitch to 73 degrees, yaw to 70 either side of the ladder's
|
||||
// facing. That is fine on a monitor, where the game owns the camera and can
|
||||
// simply refuse to turn it. In a headset it is not: the camera is the
|
||||
// player's head, and no amount of clamping stops them turning their neck.
|
||||
// The game would hold its idea of the view still while the headset kept
|
||||
// reporting the truth, and the two disagreeing is what makes a world
|
||||
// appear to swim.
|
||||
//
|
||||
// So the body still gets adjusted - it is what climbs, and what everyone
|
||||
// else sees on the ladder - and the view is put back afterwards. Looking
|
||||
// over your shoulder halfway up a ladder is worth more than the clamp was.
|
||||
//
|
||||
static cvar_t *pVRFreeLook = gi.Cvar_Get("vr_freeLook", "0", 0);
|
||||
vec3_t vUnclamped;
|
||||
|
||||
VectorCopy(v_angle, vUnclamped);
|
||||
|
||||
PmoveAdjustAngleSettings(v_angle, angles, &client->ps, &edict->s);
|
||||
|
||||
if (pVRFreeLook->integer) {
|
||||
VectorCopy(vUnclamped, v_angle);
|
||||
}
|
||||
|
||||
SetViewAngles(v_angle);
|
||||
setAngles(angles);
|
||||
}
|
||||
|
||||
+25
-1
@@ -1233,7 +1233,31 @@ void Weapon::GetMuzzlePosition(vec3_t position, vec3_t vBarrelPos, vec3_t forwar
|
||||
player = static_cast<Player *>(owner.Pointer());
|
||||
|
||||
if (forward || right || up) {
|
||||
AngleVectors(player->m_vViewAng, forward, right, up);
|
||||
Vector aimAng = player->m_vViewAng;
|
||||
vec3_t vrAim;
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
//
|
||||
// In VR the shot follows the weapon hand, not the head. The
|
||||
// camera stays where the player is looking - moving it to the
|
||||
// controller would be unusable - so only the trace moves.
|
||||
//
|
||||
// Pitch comes straight from the controller; the head
|
||||
// contributes nothing to it, and blending the two would fight
|
||||
// the player's own wrist. Yaw is the view yaw plus how far the
|
||||
// hand leads the head, because the controller's absolute yaw
|
||||
// means nothing to the game: the player can turn on the spot
|
||||
// with the stick, and the play space has an arbitrary forward.
|
||||
//
|
||||
// This is what RTCWQuest does, at rtcw/src/game/g_weapon.c:1904.
|
||||
//
|
||||
if (gi.VR_GetWeaponAim && gi.VR_GetWeaponAim(vrAim)) {
|
||||
aimAng[PITCH] = vrAim[PITCH];
|
||||
aimAng[YAW] = AngleNormalize360(aimAng[YAW] + vrAim[YAW]);
|
||||
}
|
||||
|
||||
AngleVectors(aimAng, forward, right, up);
|
||||
}
|
||||
|
||||
VectorCopy(player->m_vViewPos, position);
|
||||
|
||||
@@ -54,11 +54,20 @@ void gsiCancelThread(GSIThreadID id)
|
||||
//should i destroy the attributes here?
|
||||
pthread_attr_destroy(&id.attr);
|
||||
|
||||
#ifdef __ANDROID__
|
||||
// bionic has no thread cancellation at all. Detach instead, so the thread
|
||||
// releases its own resources once it returns on its own.
|
||||
if (pthread_detach(id.thread) != PTHREAD_NO_ERROR) {
|
||||
gsDebugFormat(GSIDebugCat_Common, GSIDebugType_Misc, GSIDebugLevel_WarmError,
|
||||
"Failed to detach thread\r\n");
|
||||
}
|
||||
#else
|
||||
if (pthread_cancel(id.thread) != PTHREAD_NO_ERROR) {
|
||||
//there was an error - how should we handle these? or should we?
|
||||
gsDebugFormat(GSIDebugCat_Common, GSIDebugType_Misc, GSIDebugLevel_WarmError,
|
||||
"Failed to cancel thread\r\n");
|
||||
}
|
||||
#endif
|
||||
//free up memory and set to NULL
|
||||
|
||||
gsifree(&id.thread);
|
||||
|
||||
@@ -84,6 +84,14 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
#define DLL_SUFFIX
|
||||
#endif
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
// Android only extracts lib*.so out of an APK, so the game modules carry the
|
||||
// prefix and are then found on the loader's own search path.
|
||||
#define DLL_PREFIX "lib"
|
||||
#else
|
||||
#define DLL_PREFIX
|
||||
#endif
|
||||
|
||||
#if defined(TARGET_LOCAL_SYSTEM)
|
||||
# define ARCH_SUFFIX
|
||||
#elif defined(TARGET_MULTIPLE_ARCHITECTURES)
|
||||
|
||||
@@ -49,7 +49,6 @@ extern void (APIENTRYP qglUnlockArraysEXT) (void);
|
||||
#define QGL_1_1_PROCS \
|
||||
GLE(void, BindTexture, GLenum target, GLuint texture) \
|
||||
GLE(void, BlendFunc, GLenum sfactor, GLenum dfactor) \
|
||||
GLE(void, CallList, GLuint list) \
|
||||
GLE(void, ClearColor, GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha) \
|
||||
GLE(void, Clear, GLbitfield mask) \
|
||||
GLE(void, ClearStencil, GLint s) \
|
||||
@@ -62,21 +61,17 @@ extern void (APIENTRYP qglUnlockArraysEXT) (void);
|
||||
GLE(void, Disable, GLenum cap) \
|
||||
GLE(void, DrawArrays, GLenum mode, GLint first, GLsizei count) \
|
||||
GLE(void, DrawElements, GLenum mode, GLsizei count, GLenum type, const GLvoid *indices) \
|
||||
GLE(void, DrawPixels, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid *pixels) \
|
||||
GLE(void, Enable, GLenum cap) \
|
||||
GLE(void, Finish, void) \
|
||||
GLE(void, Flush, void) \
|
||||
GLE(void, Fogf, GLenum pname, GLfloat param) \
|
||||
GLE(void, Fogfv, GLenum pname, const GLfloat *params) \
|
||||
GLE(void, Fogi, GLenum pname, GLint param) \
|
||||
GLE(void, GenTextures, GLsizei n, GLuint *textures ) \
|
||||
GLE(void, GetBooleanv, GLenum pname, GLboolean *params) \
|
||||
GLE(GLenum, GetError, void) \
|
||||
GLE(void, GetIntegerv, GLenum pname, GLint *params) \
|
||||
GLE(const GLubyte *, GetString, GLenum name) \
|
||||
GLE(void, LineStipple, GLint factor, GLushort pattern) \
|
||||
GLE(void, LineWidth, GLfloat width) \
|
||||
GLE(void, PixelZoom, GLfloat xfactor, GLfloat yfactor) \
|
||||
GLE(void, PolygonOffset, GLfloat factor, GLfloat units) \
|
||||
GLE(void, ReadPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid *pixels) \
|
||||
GLE(void, Scissor, GLint x, GLint y, GLsizei width, GLsizei height) \
|
||||
@@ -108,10 +103,22 @@ extern void (APIENTRYP qglUnlockArraysEXT) (void);
|
||||
GLE(void, VertexPointer, GLint size, GLenum type, GLsizei stride, const GLvoid *ptr) \
|
||||
|
||||
// OpenGL 1.0/1.1 and 3.2 core profile but not OpenGL ES 1.x
|
||||
//
|
||||
// CallList, DrawPixels, Fogi, LineStipple and PixelZoom exist in no version of
|
||||
// OpenGL ES. They belong here rather than in QGL_1_1_PROCS: that list is shared
|
||||
// with the ES paths, and requiring them there makes GLimp_GetProcAddresses fail
|
||||
// on any ES context, which is what kept the GL2 renderer from starting on
|
||||
// Android. Only renderergl1 calls them - every use in renderergl2 is inside an
|
||||
// "#if 0 // FIXME: unimplemented (GL2)".
|
||||
#define QGL_DESKTOP_1_1_PROCS \
|
||||
GLE(void, CallList, GLuint list) \
|
||||
GLE(void, ClearDepth, GLclampd depth) \
|
||||
GLE(void, DepthRange, GLclampd near_val, GLclampd far_val) \
|
||||
GLE(void, DrawBuffer, GLenum mode) \
|
||||
GLE(void, DrawPixels, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid *pixels) \
|
||||
GLE(void, Fogi, GLenum pname, GLint param) \
|
||||
GLE(void, LineStipple, GLint factor, GLushort pattern) \
|
||||
GLE(void, PixelZoom, GLfloat xfactor, GLfloat yfactor) \
|
||||
GLE(void, PolygonMode, GLenum face, GLenum mode) \
|
||||
|
||||
// OpenGL 1.0/1.1 but not OpenGL 3.2 core profile or OpenGL ES 1.x
|
||||
|
||||
@@ -176,6 +176,22 @@ void GLimp_Init( qboolean fixedFunction );
|
||||
void GLimp_Shutdown( void );
|
||||
void GLimp_EndFrame( void );
|
||||
|
||||
// When something other than the window presents the finished frame - a VR
|
||||
// compositor reading the eye textures - swapping the window as well presents
|
||||
// a surface nothing is looking at, and blocks on the display's own refresh
|
||||
// while doing it. Two presentation paths then run at unrelated rates, which
|
||||
// shows up as flicker.
|
||||
void GLimp_SetPresentsToWindow( qboolean presents );
|
||||
|
||||
// The renderer's one way of asking for a GL entry point. Everything the
|
||||
// renderer calls has to come from the same implementation - see
|
||||
// GLimp_GL4ES_GetProcAddress for what goes wrong when it does not.
|
||||
void *GLimp_GetProcAddress( const char *name );
|
||||
|
||||
// Bind the window's GL context to the calling thread again. The VR layer needs
|
||||
// one current to give the OpenXR session; see GLimp_MakeCurrent.
|
||||
qboolean GLimp_MakeCurrent( void );
|
||||
|
||||
void GLimp_LogComment( char *comment );
|
||||
void GLimp_Minimize(void);
|
||||
|
||||
|
||||
@@ -88,6 +88,23 @@ typedef struct {
|
||||
// if the pointers are not NULL, timing info will be returned
|
||||
void (*EndFrame)( int *frontEndMsec, int *backEndMsec );
|
||||
|
||||
// Redirects everything the renderer would draw to the window into another
|
||||
// framebuffer instead. Used to render an eye into an OpenXR swapchain
|
||||
// image; zero restores the window. May be NULL on renderers that do not
|
||||
// support it.
|
||||
void (*SetDefaultFramebuffer)( unsigned int framebuffer );
|
||||
|
||||
// The eye being rendered, for stereo from a headset. origin and angles are
|
||||
// the head pose relative to the game's camera, in engine units and degrees;
|
||||
// the tangents are the eye's asymmetric frustum edges at unit distance.
|
||||
// baseYaw is how much of the head's heading the game's camera already
|
||||
// carries, and is taken back off before the head is composed onto it - see
|
||||
// R_ApplyVRView. Passing NULL for origin returns to the ordinary flat
|
||||
// projection.
|
||||
void (*SetVRView)( const float *origin, const vec3_t *axis,
|
||||
float tanLeft, float tanRight, float tanUp, float tanDown,
|
||||
float baseYaw, int eye );
|
||||
|
||||
|
||||
int (*MarkFragments)( int numPoints, const vec3_t *points, const vec3_t projection,
|
||||
int maxPoints, vec3_t pointBuffer, int maxFragments, markFragment_t *fragmentBuffer, float fRadiusSquared );
|
||||
@@ -331,6 +348,13 @@ typedef struct {
|
||||
//
|
||||
int (*SKEL_GetMorphWeightFrame)(void *skeletor, int index, float time, int *data);
|
||||
int (*SKEL_GetBoneParent)(void *skeletor, int boneIndex);
|
||||
|
||||
// The per-eye render size a headset requires, or false when not in VR.
|
||||
// Asked for directly rather than passed through cvars: r_mode and friends
|
||||
// are latched and are registered by the renderer itself, so a value set
|
||||
// before the renderer starts is not in effect for its first mode change -
|
||||
// which left the whole frame drawn into one corner of the eye texture.
|
||||
qboolean (*GetVRRenderResolution)(int *width, int *height);
|
||||
} refimport_t;
|
||||
|
||||
|
||||
|
||||
@@ -147,30 +147,46 @@ void GL_Cull( int cullType ) {
|
||||
}
|
||||
else
|
||||
{
|
||||
GLenum face;
|
||||
|
||||
qglEnable( GL_CULL_FACE );
|
||||
|
||||
if ( cullType == CT_BACK_SIDED )
|
||||
{
|
||||
if ( backEnd.viewParms.isMirror )
|
||||
{
|
||||
qglCullFace( GL_FRONT );
|
||||
}
|
||||
else
|
||||
{
|
||||
qglCullFace( GL_BACK );
|
||||
}
|
||||
face = backEnd.viewParms.isMirror ? GL_FRONT : GL_BACK;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( backEnd.viewParms.isMirror )
|
||||
{
|
||||
qglCullFace( GL_BACK );
|
||||
}
|
||||
else
|
||||
{
|
||||
qglCullFace( GL_FRONT );
|
||||
}
|
||||
face = backEnd.viewParms.isMirror ? GL_BACK : GL_FRONT;
|
||||
}
|
||||
|
||||
#ifdef USE_GL4ES
|
||||
// Name a third face first, so the one we actually want cannot be
|
||||
// mistaken for a no-op.
|
||||
//
|
||||
// gl4es keeps its own copy of the cull face and returns early from
|
||||
// glCullFace whenever the requested mode matches it
|
||||
// (src/gl/face.c:14). Measured on the device, past gl4es and straight
|
||||
// out of libGLESv3: its copy said GL_FRONT while the driver was
|
||||
// culling GL_BACK. So the engine asked for GL_FRONT every frame, gl4es
|
||||
// judged that redundant against a copy that had drifted, and the
|
||||
// hardware went on removing precisely the faces Quake wants kept -
|
||||
// its world polygons are back facing in GL's terms, which is why
|
||||
// CT_FRONT_SIDED culls GL_FRONT in the first place.
|
||||
//
|
||||
// The symptom was the whole world invisible while models stayed put,
|
||||
// because model shaders are CT_TWO_SIDED and never enable culling at
|
||||
// all. Inverting the culled face did not help either: asking for
|
||||
// GL_BACK matched the driver's real state, so the picture never
|
||||
// changed. Only disabling culling outright brought the world back.
|
||||
//
|
||||
// GL_FRONT_AND_BACK is safe here - nothing is drawn between these two
|
||||
// calls - and it guarantees the second differs from whatever gl4es
|
||||
// believes, so the driver is always told.
|
||||
qglCullFace( GL_FRONT_AND_BACK );
|
||||
#endif
|
||||
|
||||
qglCullFace( face );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -440,6 +440,11 @@ void Set2DWindow(int x, int y, int w, int h, float left, float right, float bott
|
||||
qglScissor(x, y, w, h);
|
||||
qglMatrixMode(GL_PROJECTION);
|
||||
qglLoadIdentity();
|
||||
|
||||
// Everything 2D in this renderer comes through here, RB_SetGL2D included,
|
||||
// so this is the one place the headset's correction has to be applied.
|
||||
R_VRAdjust2DOrtho(&left, &right, &bottom, &top);
|
||||
|
||||
qglOrtho(left, right, bottom, top, n, f);
|
||||
qglMatrixMode(GL_MODELVIEW);
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@ cvar_t *r_skipBackEnd;
|
||||
cvar_t *r_ignorehwgamma;
|
||||
cvar_t *r_measureOverdraw;
|
||||
|
||||
cvar_t *r_vrTrace;
|
||||
cvar_t *vr_fovZoom;
|
||||
cvar_t *vr_hudScale;
|
||||
cvar_t *vr_hudDepth;
|
||||
|
||||
cvar_t *r_inGameVideo;
|
||||
cvar_t *r_fastsky;
|
||||
cvar_t *r_fastdlights;
|
||||
@@ -405,6 +410,15 @@ static int s_numVidModes = ( sizeof( r_vidModes ) / sizeof( r_vidModes[0] ) );
|
||||
qboolean R_GetModeInfo( int *width, int *height, float *windowAspect, int mode ) {
|
||||
vidmode_t *vm;
|
||||
|
||||
// A headset dictates the render size; whatever r_mode says is irrelevant.
|
||||
// Asked for directly rather than read from a cvar because r_mode is latched
|
||||
// and is registered by this renderer, so a value the client sets beforehand
|
||||
// is not in effect for the first mode change.
|
||||
if ( ri.GetVRRenderResolution && ri.GetVRRenderResolution( width, height ) ) {
|
||||
*windowAspect = (float)*width / (float)*height;
|
||||
return qtrue;
|
||||
}
|
||||
|
||||
if ( mode < -1 ) {
|
||||
return qfalse;
|
||||
}
|
||||
@@ -1201,11 +1215,20 @@ void BuildGfxInfo(char* dest, size_t destsize) {
|
||||
PrintAndAppendString( dest, destsize, "rendering primitives: " );
|
||||
primitives = r_primitives->integer;
|
||||
if ( primitives == 0 ) {
|
||||
// Kept in step with R_DrawElements deliberately. This is a second
|
||||
// copy of that decision, and while it disagreed it reported
|
||||
// "multiple glArrayElement" for a renderer that was issuing
|
||||
// glDrawElements - which is the only place anyone can read the
|
||||
// answer on a device with no console.
|
||||
#ifdef USE_GL4ES
|
||||
primitives = 2;
|
||||
#else
|
||||
if ( qglLockArraysEXT ) {
|
||||
primitives = 2;
|
||||
} else {
|
||||
primitives = 1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if ( primitives == -1 ) {
|
||||
PrintAndAppendString( dest, destsize, "none\n" );
|
||||
@@ -1560,6 +1583,13 @@ void R_Register( void )
|
||||
|
||||
r_ext_multisample = ri.Cvar_Get("r_ext_multisample", "0", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
r_noborder = ri.Cvar_Get("r_noborder", "0", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
r_vrTrace = ri.Cvar_Get( "r_vrTrace", "0", 0 );
|
||||
// Set by cgame from the fov the game asked for; see CG_CalcFov.
|
||||
vr_fovZoom = ri.Cvar_Get( "vr_fovZoom", "1", 0 );
|
||||
// See R_VRAdjust2DOrtho. Scale is the share of the display the HUD is drawn
|
||||
// across; depth is how far out it is made to converge.
|
||||
vr_hudScale = ri.Cvar_Get( "vr_hudScale", "0.55", CVAR_ARCHIVE );
|
||||
vr_hudDepth = ri.Cvar_Get( "vr_hudDepth", "2.0", CVAR_ARCHIVE );
|
||||
r_ext_texture_filter_anisotropic = ri.Cvar_Get("r_ext_texture_filter_anisotropic",
|
||||
"0", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
r_stereoEnabled = ri.Cvar_Get("r_stereoEnabled", "0", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
@@ -1878,6 +1908,8 @@ refexport_t *GetRefAPI ( int apiVersion, refimport_t *rimp ) {
|
||||
|
||||
re.BeginFrame = RE_BeginFrame;
|
||||
re.EndFrame = RE_EndFrame;
|
||||
re.SetDefaultFramebuffer = RE_SetDefaultFramebuffer;
|
||||
re.SetVRView = RE_SetVRView;
|
||||
|
||||
re.MarkFragments = R_MarkFragments;
|
||||
re.LerpTag = R_LerpTag;
|
||||
|
||||
@@ -49,8 +49,24 @@ QGL_3_0_PROCS;
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifdef USE_GL4ES
|
||||
// 16 bit indices on OpenGL ES, which is what gl4es is running on underneath.
|
||||
// ES 2.0 has no 32 bit element indices at all without GL_OES_element_index_uint,
|
||||
// so glDrawElements with GL_UNSIGNED_INT draws nothing and reports nothing.
|
||||
// Immediate mode is unaffected - gl4es collects glBegin/glEnd itself and picks
|
||||
// its own index type - which is why a build with this wrong renders cinematics
|
||||
// and the loading screen perfectly while every menu, every HUD element and the
|
||||
// entire world are missing.
|
||||
//
|
||||
// Costs nothing here. Indices only ever address within one tess batch, and
|
||||
// SHADER_MAX_VERTEXES is 2048; RTCWQuest runs the same renderer at 6000 with
|
||||
// shorts (their tr_local.h, under HAVE_GLES).
|
||||
#define GL_INDEX_TYPE GL_UNSIGNED_SHORT
|
||||
typedef unsigned short glIndex_t;
|
||||
#else
|
||||
#define GL_INDEX_TYPE GL_UNSIGNED_INT
|
||||
typedef unsigned int glIndex_t;
|
||||
#endif
|
||||
|
||||
// fast float to int conversion
|
||||
#if id386 && !( (defined __linux__ || defined __FreeBSD__ ) && (defined __i386__ ) ) // rb010123
|
||||
@@ -1409,6 +1425,33 @@ typedef struct {
|
||||
int farclip;
|
||||
} trGlobals_t;
|
||||
|
||||
/*
|
||||
The eye currently being rendered, when driven by a headset.
|
||||
|
||||
Kept outside trGlobals_t for the same reason as glState: it describes the
|
||||
display rather than the scene, and must survive a renderer re-init.
|
||||
*/
|
||||
typedef struct {
|
||||
qboolean active;
|
||||
vec3_t origin; // head offset from the game's camera, engine units
|
||||
vec3_t axis[3]; // head orientation as forward/left/up
|
||||
float tanLeft; // asymmetric frustum edges at unit distance
|
||||
float tanRight;
|
||||
float tanUp;
|
||||
float tanDown;
|
||||
float baseYaw; // head heading the game's camera already carries
|
||||
int eye; // which eye, for the HUD's stereo offset
|
||||
} vrViewState_t;
|
||||
|
||||
extern vrViewState_t vrView;
|
||||
|
||||
void RE_SetVRView( const float *origin, const vec3_t *axis,
|
||||
float tanLeft, float tanRight, float tanUp, float tanDown,
|
||||
float baseYaw, int eye );
|
||||
void RE_SetDefaultFramebuffer( unsigned int framebuffer );
|
||||
void R_ApplyVRView( refdef_t *fd );
|
||||
void R_VRAdjust2DOrtho( float *left, float *right, float *bottom, float *top );
|
||||
|
||||
extern backEndState_t backEnd;
|
||||
extern trGlobals_t tr;
|
||||
extern glconfig_t glConfig; // outside of TR since it shouldn't be cleared during ref re-init
|
||||
@@ -1451,6 +1494,11 @@ extern cvar_t *r_texturebits; // number of desired texture bits
|
||||
|
||||
extern cvar_t *r_measureOverdraw; // enables stencil buffer overdraw measurement
|
||||
|
||||
extern cvar_t *r_vrTrace; // log the composed VR camera once a second
|
||||
extern cvar_t *vr_fovZoom; // scope magnification, published by cgame
|
||||
extern cvar_t *vr_hudScale; // how much of the display the HUD covers
|
||||
extern cvar_t *vr_hudDepth; // metres out the HUD is made to converge
|
||||
|
||||
extern cvar_t *r_lodscale;
|
||||
|
||||
extern cvar_t *r_primitives; // "0" = based on compiled vertex array existance
|
||||
|
||||
@@ -569,11 +569,40 @@ void R_SetupProjection( void ) {
|
||||
zNear = r_znear->value;
|
||||
zFar = tr.viewParms.zFar;
|
||||
|
||||
ymax = zNear * tan( tr.refdef.fov_y * M_PI / 360.0f );
|
||||
ymin = -ymax;
|
||||
if ( vrView.active ) {
|
||||
// A headset's per-eye frustum is not centred on the view axis - each
|
||||
// eye sees further towards its own side than towards the nose. Using a
|
||||
// symmetric approximation misaligns the two images by a few degrees,
|
||||
// which reads as an image the eyes cannot fuse. The matrix built below
|
||||
// already carries the off-centre terms in its [8] and [9] elements, so
|
||||
// the runtime's own edges go straight in.
|
||||
//
|
||||
// Scopes and binoculars. The game narrows its fov to zoom, but in a
|
||||
// headset the frustum comes from the runtime, so that request would
|
||||
// otherwise be dropped on the floor. Narrowing the eye frustum by the
|
||||
// same factor while the composition layer keeps claiming the headset's
|
||||
// full field of view leaves the compositor to magnify the result, which
|
||||
// is both free and sharper than zooming after the fact: the narrow view
|
||||
// was rendered into the whole eye texture.
|
||||
float zoom = vr_fovZoom ? vr_fovZoom->value : 1.0f;
|
||||
|
||||
xmax = zNear * tan( tr.refdef.fov_x * M_PI / 360.0f );
|
||||
xmin = -xmax;
|
||||
if ( zoom < 1.0f ) {
|
||||
zoom = 1.0f;
|
||||
} else if ( zoom > 16.0f ) {
|
||||
zoom = 16.0f;
|
||||
}
|
||||
|
||||
xmin = zNear * vrView.tanLeft / zoom;
|
||||
xmax = zNear * vrView.tanRight / zoom;
|
||||
ymin = zNear * vrView.tanDown / zoom;
|
||||
ymax = zNear * vrView.tanUp / zoom;
|
||||
} else {
|
||||
ymax = zNear * tan( tr.refdef.fov_y * M_PI / 360.0f );
|
||||
ymin = -ymax;
|
||||
|
||||
xmax = zNear * tan( tr.refdef.fov_x * M_PI / 360.0f );
|
||||
xmin = -xmax;
|
||||
}
|
||||
|
||||
width = xmax - xmin;
|
||||
height = ymax - ymin;
|
||||
@@ -612,6 +641,47 @@ void R_SetupFrustum (void) {
|
||||
float xs, xc;
|
||||
float ang;
|
||||
|
||||
if ( vrView.active ) {
|
||||
// Cull against the frustum actually being rendered, not the one the
|
||||
// game asked for.
|
||||
//
|
||||
// R_SetupProjection already builds the projection from the runtime's
|
||||
// four tangents, which are both wider than the game's fov and not
|
||||
// centred on the view axis - each eye sees further towards its own side
|
||||
// than towards the nose. The planes below were derived from
|
||||
// viewParms.fovX/fovY instead, which is the game's symmetric cone, so
|
||||
// everything between the two was culled while still being inside the
|
||||
// picture: geometry disappearing towards the outer edge of each eye,
|
||||
// and whole surfaces at once, since culling is per surface. Large flat
|
||||
// ones - terrain patches - go all or nothing.
|
||||
//
|
||||
// renderergl2 does not have this bug, which is why it never showed up
|
||||
// while that was the renderer: its R_SetupFrustum takes xmin/xmax/ymax
|
||||
// from the projection and handles the asymmetric case explicitly.
|
||||
//
|
||||
// Q3's axes are forward, left, up. A boundary ray on the right edge is
|
||||
// forward - tanRight*left, and the inward normal of the plane holding it
|
||||
// is tanRight*forward + left; the other three follow the same way. Not
|
||||
// normalised on purpose - R_CullLocalBox and friends only ever test the
|
||||
// sign of the dot product against dist, and dist is built from the same
|
||||
// unnormalised normal below.
|
||||
VectorScale( tr.viewParms.ori.axis[0], vrView.tanRight, tr.viewParms.frustum[0].normal );
|
||||
VectorMA( tr.viewParms.frustum[0].normal, 1.0f, tr.viewParms.ori.axis[1], tr.viewParms.frustum[0].normal );
|
||||
VectorNormalize( tr.viewParms.frustum[0].normal );
|
||||
|
||||
VectorScale( tr.viewParms.ori.axis[0], -vrView.tanLeft, tr.viewParms.frustum[1].normal );
|
||||
VectorMA( tr.viewParms.frustum[1].normal, -1.0f, tr.viewParms.ori.axis[1], tr.viewParms.frustum[1].normal );
|
||||
VectorNormalize( tr.viewParms.frustum[1].normal );
|
||||
|
||||
VectorScale( tr.viewParms.ori.axis[0], vrView.tanUp, tr.viewParms.frustum[2].normal );
|
||||
VectorMA( tr.viewParms.frustum[2].normal, -1.0f, tr.viewParms.ori.axis[2], tr.viewParms.frustum[2].normal );
|
||||
VectorNormalize( tr.viewParms.frustum[2].normal );
|
||||
|
||||
VectorScale( tr.viewParms.ori.axis[0], -vrView.tanDown, tr.viewParms.frustum[3].normal );
|
||||
VectorMA( tr.viewParms.frustum[3].normal, 1.0f, tr.viewParms.ori.axis[2], tr.viewParms.frustum[3].normal );
|
||||
VectorNormalize( tr.viewParms.frustum[3].normal );
|
||||
} else {
|
||||
|
||||
ang = tr.viewParms.fovX / 180 * M_PI * 0.5f;
|
||||
xs = sin( ang );
|
||||
xc = cos( ang );
|
||||
@@ -632,6 +702,8 @@ void R_SetupFrustum (void) {
|
||||
VectorScale( tr.viewParms.ori.axis[0], xs, tr.viewParms.frustum[3].normal );
|
||||
VectorMA( tr.viewParms.frustum[3].normal, -xc, tr.viewParms.ori.axis[2], tr.viewParms.frustum[3].normal );
|
||||
|
||||
}
|
||||
|
||||
for (i=0 ; i<4 ; i++) {
|
||||
tr.viewParms.frustum[i].type = PLANE_NON_AXIAL;
|
||||
tr.viewParms.frustum[i].dist = DotProduct (tr.viewParms.ori.origin, tr.viewParms.frustum[i].normal);
|
||||
|
||||
@@ -442,6 +442,7 @@ to handle mirrors,
|
||||
void RE_RenderScene( const refdef_t *fd ) {
|
||||
viewParms_t parms;
|
||||
int startTime;
|
||||
refdef_t vrRefdef;
|
||||
|
||||
if ( !tr.registered ) {
|
||||
return;
|
||||
@@ -452,6 +453,15 @@ void RE_RenderScene( const refdef_t *fd ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Every camera the game has arrives here, so composing the head pose on at
|
||||
// this point is what makes head tracking follow cutscenes and ladders as
|
||||
// well as the player's own view. See R_ApplyVRView.
|
||||
if ( vrView.active ) {
|
||||
vrRefdef = *fd;
|
||||
R_ApplyVRView( &vrRefdef );
|
||||
fd = &vrRefdef;
|
||||
}
|
||||
|
||||
startTime = ri.Milliseconds();
|
||||
|
||||
if (!tr.world && !( fd->rdflags & RDF_NOWORLDMODEL ) ) {
|
||||
|
||||
@@ -167,16 +167,34 @@ static void R_DrawElements( int numIndexes, const glIndex_t *indexes ) {
|
||||
|
||||
// default is to use triangles if compiled vertex arrays are present
|
||||
if ( primitives == 0 ) {
|
||||
#ifdef USE_GL4ES
|
||||
// glDrawElements, always, which is what the reference does: RTCWQuest
|
||||
// compiles R_DrawStripElements and R_ArrayElementDiscrete out of this
|
||||
// file entirely under HAVE_GLES and sends every surface through the one
|
||||
// call.
|
||||
//
|
||||
// This is not a preference, it is the only route on this stack that is
|
||||
// known to work. The other two rebuild each strip as
|
||||
// glBegin/glArrayElement/glEnd, which under gl4es is its immediate mode
|
||||
// emulation reading back out of the client arrays - vertices at stride
|
||||
// 16, colours as packed bytes, texcoords from a third array - and the
|
||||
// working reference has never once exercised it.
|
||||
//
|
||||
// This port was on that route by accident rather than by choice:
|
||||
// compiled vertex arrays were switched off (see GLimp_InitExtensions),
|
||||
// so qglLockArraysEXT is NULL, so this fell through to 1.
|
||||
primitives = 2;
|
||||
#else
|
||||
if ( qglLockArraysEXT ) {
|
||||
primitives = 2;
|
||||
} else {
|
||||
primitives = 1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
if ( primitives == 2 ) {
|
||||
qglDrawElements( GL_TRIANGLES,
|
||||
qglDrawElements( GL_TRIANGLES,
|
||||
numIndexes,
|
||||
GL_INDEX_TYPE,
|
||||
indexes );
|
||||
@@ -517,7 +535,11 @@ static void ProjectDlightTexture( void ) {
|
||||
byte clipBits[SHADER_MAX_VERTEXES];
|
||||
MAC_STATIC float texCoordsArray[SHADER_MAX_VERTEXES][2];
|
||||
byte colorArray[SHADER_MAX_VERTEXES][4];
|
||||
unsigned hitIndexes[SHADER_MAX_INDEXES];
|
||||
// glIndex_t, for the same reason as tessIndexes in RB_SurfaceFace: this is
|
||||
// handed straight to R_DrawElements, so it has to be the type GL is told
|
||||
// the indices are. As unsigned it was read two indices at a time under
|
||||
// gl4es, which put the dynamic lights on the wrong triangles.
|
||||
glIndex_t hitIndexes[SHADER_MAX_INDEXES];
|
||||
int numIndexes;
|
||||
int planetype;
|
||||
float scale;
|
||||
|
||||
@@ -595,12 +595,25 @@ static void AutospriteDeform(void) {
|
||||
tess.numVertexes = 0;
|
||||
tess.numIndexes = 0;
|
||||
|
||||
if (backEnd.currentEntity != &tr.worldEntity) {
|
||||
GlobalVectorToLocal(backEnd.viewParms.ori.axis[1], leftDir);
|
||||
GlobalVectorToLocal(backEnd.viewParms.ori.axis[2], upDir);
|
||||
} else {
|
||||
VectorCopy(backEnd.viewParms.ori.axis[1], leftDir);
|
||||
VectorCopy(backEnd.viewParms.ori.axis[2], upDir);
|
||||
{
|
||||
vec3_t viewLeft, viewUp;
|
||||
|
||||
VectorCopy(backEnd.viewParms.ori.axis[1], viewLeft);
|
||||
VectorCopy(backEnd.viewParms.ori.axis[2], viewUp);
|
||||
|
||||
if (vrView.active) {
|
||||
// World up, so the sprites stay upright rather than inheriting the
|
||||
// head's roll.
|
||||
VectorSet(viewUp, 0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
if (backEnd.currentEntity != &tr.worldEntity) {
|
||||
GlobalVectorToLocal(viewLeft, leftDir);
|
||||
GlobalVectorToLocal(viewUp, upDir);
|
||||
} else {
|
||||
VectorCopy(viewLeft, leftDir);
|
||||
VectorCopy(viewUp, upDir);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < oldVerts; i += 4) {
|
||||
@@ -614,9 +627,37 @@ static void AutospriteDeform(void) {
|
||||
VectorSubtract(xyz, mid, delta);
|
||||
radius = VectorLength(delta) * 0.707f; // / sqrt(2)
|
||||
|
||||
// Quake's autosprites align to the view plane: every sprite takes the
|
||||
// camera's own axes, so they all sit parallel to the screen. On a
|
||||
// monitor that is indistinguishable from facing the viewer, but in VR
|
||||
// turning the head swings the whole set round to stay parallel to the
|
||||
// view plane, and they visibly spin in place. Point each one at where
|
||||
// the viewer actually is instead, so head rotation leaves them alone
|
||||
// and only moving changes what they present.
|
||||
if (vrView.active) {
|
||||
vec3_t toViewer, spriteLeft, spriteUp;
|
||||
|
||||
VectorSubtract(backEnd.ori.viewOrigin, mid, toViewer);
|
||||
CrossProduct(upDir, toViewer, spriteLeft);
|
||||
|
||||
// Degenerate directly above or below the sprite, where there is no
|
||||
// heading to align to; the shared axes are as good as anything.
|
||||
if (VectorNormalize(spriteLeft) > 0.001f) {
|
||||
CrossProduct(toViewer, spriteLeft, spriteUp);
|
||||
|
||||
if (VectorNormalize(spriteUp) > 0.001f) {
|
||||
VectorScale(spriteLeft, radius, left);
|
||||
VectorScale(spriteUp, radius, up);
|
||||
goto haveAxes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VectorScale(leftDir, radius, left);
|
||||
VectorScale(upDir, radius, up);
|
||||
|
||||
haveAxes:
|
||||
|
||||
if (backEnd.viewParms.isMirror) {
|
||||
VectorSubtract(vec3_origin, left, left);
|
||||
}
|
||||
@@ -748,6 +789,27 @@ static void Autosprite2Deform( void ) {
|
||||
VectorSubtract( mid[1], mid[0], major );
|
||||
|
||||
// cross this with the view direction to get minor axis
|
||||
if ( vrView.active ) {
|
||||
// Towards the viewer, not along the view axis. On a monitor the two
|
||||
// amount to the same thing - the eye is effectively infinitely far
|
||||
// away down the middle of the screen - but in a headset they are
|
||||
// not, and taking the view axis swings every one of these sprites
|
||||
// as the head turns while the geometry it stands beside holds
|
||||
// still. That is the second, worse tree leaning out of every real
|
||||
// one: the imposter and the model disagreeing about where the
|
||||
// viewer is.
|
||||
//
|
||||
// Per sprite rather than per batch for the same reason: each one
|
||||
// has to face its own line to the eye, and a single direction for
|
||||
// the whole surface is only right for a sprite dead ahead.
|
||||
vec3_t centre;
|
||||
|
||||
VectorAdd( mid[0], mid[1], centre );
|
||||
VectorScale( centre, 0.5f, centre );
|
||||
VectorSubtract( centre, backEnd.ori.viewOrigin, forward );
|
||||
VectorNormalize( forward );
|
||||
}
|
||||
|
||||
CrossProduct( major, forward, minor );
|
||||
VectorNormalize( minor );
|
||||
|
||||
|
||||
@@ -121,6 +121,28 @@ void RB_DrawSprite( const refSprite_t *spr ) {
|
||||
}
|
||||
break;
|
||||
case SPRITE_PARALLEL:
|
||||
// In VR, face where the viewer is rather than lying parallel to the
|
||||
// view plane. Parallel to the view plane means every sprite in the
|
||||
// world swings round together the moment the head turns, which is
|
||||
// invisible on a monitor and glaring in a headset.
|
||||
if (vrView.active) {
|
||||
vec3_t toViewer;
|
||||
|
||||
VectorSubtract(backEnd.viewParms.ori.origin, spr->origin, toViewer);
|
||||
|
||||
if (VectorNormalize(toViewer) > 0.001f) {
|
||||
vec3_t worldUp = { 0.0f, 0.0f, 1.0f };
|
||||
|
||||
CrossProduct(worldUp, toViewer, right);
|
||||
|
||||
if (VectorNormalize(right) > 0.001f) {
|
||||
CrossProduct(toViewer, right, up);
|
||||
VectorNormalize(up);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VectorCopy(backEnd.viewParms.ori.axis[2], up);
|
||||
if (!backEnd.viewParms.isMirror) {
|
||||
VectorNegate(backEnd.viewParms.ori.axis[1], right);
|
||||
@@ -143,8 +165,28 @@ void RB_DrawSprite( const refSprite_t *spr ) {
|
||||
}
|
||||
|
||||
VectorSet(up, 0.0f, 0.0f, 1.0f);
|
||||
VectorSet(right, backEnd.viewParms.ori.axis[0][1], -backEnd.viewParms.ori.axis[0][0], 0.0f);
|
||||
VectorNormalize(right);
|
||||
|
||||
// Upright sprites already ignore the view's roll and pitch, but they
|
||||
// still turn with the view's heading rather than towards the viewer -
|
||||
// so in VR they spin in place as the head turns. Aim each one at where
|
||||
// the viewer actually stands instead.
|
||||
if (vrView.active) {
|
||||
vec3_t toViewer;
|
||||
|
||||
VectorSubtract(backEnd.viewParms.ori.origin, spr->origin, toViewer);
|
||||
toViewer[2] = 0.0f;
|
||||
|
||||
if (VectorNormalize(toViewer) > 0.001f) {
|
||||
VectorSet(right, -toViewer[1], toViewer[0], 0.0f);
|
||||
} else {
|
||||
VectorSet(right, backEnd.viewParms.ori.axis[0][1], -backEnd.viewParms.ori.axis[0][0], 0.0f);
|
||||
VectorNormalize(right);
|
||||
}
|
||||
} else {
|
||||
VectorSet(right, backEnd.viewParms.ori.axis[0][1], -backEnd.viewParms.ori.axis[0][0], 0.0f);
|
||||
VectorNormalize(right);
|
||||
}
|
||||
|
||||
VectorSet(norm, -right[1], right[0], 0.0f);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -378,7 +378,21 @@ RB_SurfaceFace
|
||||
void RB_SurfaceFace( srfSurfaceFace_t *surf ) {
|
||||
int i;
|
||||
qboolean needsNormal;
|
||||
unsigned *indices, *tessIndexes;
|
||||
unsigned *indices;
|
||||
// glIndex_t, not unsigned. These two are different types the moment
|
||||
// GL_INDEX_TYPE is GL_UNSIGNED_SHORT, which it is under gl4es, and writing
|
||||
// tess.indexes through an unsigned* puts every index four bytes apart in a
|
||||
// two byte array. The engine's own indices come out interleaved with the
|
||||
// high halves of the previous ones, so each triangle picks up whichever
|
||||
// vertex that lands on - which is what dragged the world's geometry, and
|
||||
// its textures with it, into long streaks while models rendered perfectly.
|
||||
// Models never come through here.
|
||||
//
|
||||
// The compiler said so all along:
|
||||
// incompatible pointer types assigning to 'unsigned int *' from
|
||||
// 'glIndex_t *' (aka 'unsigned short *') [-Wincompatible-pointer-types]
|
||||
// under sixty four other warnings in this file alone.
|
||||
glIndex_t *tessIndexes;
|
||||
float *v;
|
||||
float *normal;
|
||||
int ndx;
|
||||
|
||||
@@ -1179,6 +1179,53 @@ void R_TessellateTerrain()
|
||||
R_DoTriMerging();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
================
|
||||
R_TerrainViewFov
|
||||
|
||||
The horizontal field of view the terrain system should size itself against.
|
||||
|
||||
In VR this is not tr.refdef.fov_x. The projection comes from the runtime's four
|
||||
tangents (R_SetupProjection) and the culling frustum is built from the same
|
||||
(R_SetupFrustum), so refdef.fov_x is no longer what is on screen - it is
|
||||
whatever the game happened to ask for, around 80 degrees, while a Quest 3 eye
|
||||
sees closer to 100.
|
||||
|
||||
That matters here because terrain visibility is a cone test. R_MarkTerrainPatch
|
||||
rejects any patch further off the view axis than g_fClipDotSquared allows, and
|
||||
at fov 80 that half angle is about 46 degrees. Terrain inside the headset's view
|
||||
but outside an 80 degree cone is therefore never marked visible, and never
|
||||
drawn - silently, because nothing else in the VR path reads fov_x any more.
|
||||
|
||||
Taken from the widest of the two horizontal tangents rather than their sum: the
|
||||
per-eye frustum is asymmetric, and the cone is symmetric about the view axis, so
|
||||
it has to cover the wider side.
|
||||
|
||||
MOHAA specific. RTCWQuest has no terrain, so the reference cannot help here.
|
||||
================
|
||||
*/
|
||||
static float R_TerrainViewFov(void)
|
||||
{
|
||||
float tanMax;
|
||||
|
||||
if (!vrView.active) {
|
||||
return tr.refdef.fov_x;
|
||||
}
|
||||
|
||||
tanMax = fabs(vrView.tanLeft);
|
||||
if (fabs(vrView.tanRight) > tanMax) {
|
||||
tanMax = fabs(vrView.tanRight);
|
||||
}
|
||||
|
||||
if (tanMax < 0.01f) {
|
||||
return tr.refdef.fov_x;
|
||||
}
|
||||
|
||||
// Full angle, both sides of the axis.
|
||||
return RAD2DEG(atan(tanMax)) * 2.0f;
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
R_TerrainPrepareFrame
|
||||
@@ -1208,7 +1255,7 @@ void R_TerrainPrepareFrame()
|
||||
}
|
||||
|
||||
distance = 1.0;
|
||||
fFov = tr.refdef.fov_x;
|
||||
fFov = R_TerrainViewFov();
|
||||
if (fFov < 1.0) {
|
||||
fFov = 1.0;
|
||||
}
|
||||
@@ -1251,7 +1298,18 @@ void R_TerrainPrepareFrame()
|
||||
g_fCheck = fCheck;
|
||||
|
||||
if (fDistBound != 0.0) {
|
||||
index = (int)tr.refdef.fov_x;
|
||||
// Clamped, and never zero. These are raw table indices: index 0 gives
|
||||
// g_fDistanceTable[0] = 443.5 / sqrt(1 - 1), which is infinity, and an
|
||||
// infinite distance poisons g_vClipOrigin below and culls every patch
|
||||
// in the level. A fov of zero is not reachable today but nothing here
|
||||
// checked, and the failure is total and silent.
|
||||
index = (int)R_TerrainViewFov();
|
||||
if (index < 1) {
|
||||
index = 1;
|
||||
} else if (index >= TERRAIN_TABLE_SIZE) {
|
||||
index = TERRAIN_TABLE_SIZE - 1;
|
||||
}
|
||||
|
||||
distance = g_fDistanceTable[index];
|
||||
|
||||
g_fClipDotSquared = g_fClipDotSquaredTable[index];
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
===========================================================================
|
||||
Copyright (C) 2025 the OpenMoHAA team
|
||||
|
||||
This file is part of OpenMoHAA source code.
|
||||
|
||||
OpenMoHAA source code 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.
|
||||
|
||||
OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
===========================================================================
|
||||
*/
|
||||
|
||||
// tr_vr.c -- everything the fixed function renderer needs to know about a
|
||||
// headset, which is deliberately very little. The VR layer owns the session,
|
||||
// the poses and the swapchains; this is the surface where it meets the
|
||||
// renderer.
|
||||
|
||||
#include "tr_local.h"
|
||||
|
||||
vrViewState_t vrView;
|
||||
|
||||
|
||||
/*
|
||||
============
|
||||
RE_SetVRView
|
||||
============
|
||||
*/
|
||||
void RE_SetVRView(
|
||||
const float *origin,
|
||||
const vec3_t *axis,
|
||||
float tanLeft,
|
||||
float tanRight,
|
||||
float tanUp,
|
||||
float tanDown,
|
||||
float baseYaw,
|
||||
int eye
|
||||
)
|
||||
{
|
||||
if (!origin || !axis) {
|
||||
vrView.active = qfalse;
|
||||
return;
|
||||
}
|
||||
|
||||
vrView.baseYaw = baseYaw;
|
||||
vrView.eye = eye;
|
||||
|
||||
VectorCopy(origin, vrView.origin);
|
||||
VectorCopy(axis[0], vrView.axis[0]);
|
||||
VectorCopy(axis[1], vrView.axis[1]);
|
||||
VectorCopy(axis[2], vrView.axis[2]);
|
||||
|
||||
vrView.tanLeft = tanLeft;
|
||||
vrView.tanRight = tanRight;
|
||||
vrView.tanUp = tanUp;
|
||||
vrView.tanDown = tanDown;
|
||||
vrView.active = qtrue;
|
||||
}
|
||||
|
||||
/*
|
||||
============
|
||||
RE_SetDefaultFramebuffer
|
||||
|
||||
Called when the VR layer is about to move the target out from under the
|
||||
renderer, so that anything still owed to the old one is paid first.
|
||||
|
||||
Deliberately does not bind anything. This renderer predates framebuffer objects,
|
||||
has none of its own, and never touches the binding - so there is nothing here to
|
||||
redirect, and the VR layer's own bind is what selects the eye.
|
||||
|
||||
Binding here would in fact be worse than useless. The renderer's GL goes through
|
||||
gl4es, and gl4es can only bind framebuffers it created itself: its
|
||||
glBindFramebuffer looks the name up in its own table, and for a name that came
|
||||
from the driver's glGenFramebuffers - which is where every one of the VR layer's
|
||||
framebuffers comes from - the lookup misses, it raises GL_INVALID_VALUE and
|
||||
returns without binding. Worse, it leaves gl4es believing framebuffer 0 is still
|
||||
current. That was a black headset with the frame loop running at a contented 90
|
||||
fps, everything drawn neatly into the window nobody was looking at.
|
||||
|
||||
What is needed instead is the flush. gl4es batches geometry and issues it
|
||||
lazily, so work built for one target would otherwise arrive in whichever is
|
||||
bound when it finally goes out. Flushing while the old target is still bound is
|
||||
what keeps one eye's contents out of the next.
|
||||
============
|
||||
*/
|
||||
void RE_SetDefaultFramebuffer(unsigned int framebuffer)
|
||||
{
|
||||
if (qglFlush) {
|
||||
qglFlush();
|
||||
}
|
||||
|
||||
// Drawing into a framebuffer means something else is presenting it, so the
|
||||
// window must not also be swapped - two presentation paths running at
|
||||
// unrelated rates is what flicker looks like.
|
||||
GLimp_SetPresentsToWindow(framebuffer == 0 ? qtrue : qfalse);
|
||||
}
|
||||
|
||||
/*
|
||||
====================
|
||||
R_ApplyVRView
|
||||
|
||||
Puts the headset on top of whatever camera the game asked for.
|
||||
|
||||
Done in the renderer rather than the game because every camera reaches it
|
||||
through RE_RenderScene - the player's view, cutscene cameras, ladder climbs,
|
||||
death animations - so head tracking follows all of them without each having to
|
||||
know about VR.
|
||||
|
||||
The game's camera decides where the body is and which way it faces. Everything
|
||||
else comes from the headset: pitch and roll are taken from the head alone,
|
||||
since blending them with the game's would fight the viewer's own neck.
|
||||
====================
|
||||
*/
|
||||
void R_ApplyVRView(refdef_t *fd)
|
||||
{
|
||||
vec3_t baseAngles, bodyAngles, bodyAxis[3];
|
||||
int i;
|
||||
|
||||
if (!vrView.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Refuse a head pose that is not a rotation.
|
||||
//
|
||||
// These axes are written straight into refdef.viewaxis, and R_RotateForViewer
|
||||
// copies them into the modelview without checking anything. A zeroed or
|
||||
// degenerate set therefore produces a modelview whose determinant is zero -
|
||||
// a matrix that collapses three dimensions into a plane - and everything
|
||||
// drawn through it disappears while every individual number upstream of it
|
||||
// still looks correct. Measured directly: world surfaces reporting
|
||||
// "mvdet 0.000" in the same frame as others reporting 1.000.
|
||||
//
|
||||
// The game's own camera is a perfectly good fallback for a frame; a
|
||||
// collapsed one is not.
|
||||
{
|
||||
const float len0 = VectorLength(vrView.axis[0]);
|
||||
const float len1 = VectorLength(vrView.axis[1]);
|
||||
const float len2 = VectorLength(vrView.axis[2]);
|
||||
|
||||
if (len0 < 0.5f || len1 < 0.5f || len2 < 0.5f) {
|
||||
static int lastWarn;
|
||||
int now = ri.Milliseconds();
|
||||
|
||||
if (now - lastWarn > 1000) {
|
||||
lastWarn = now;
|
||||
ri.Printf(
|
||||
PRINT_WARNING,
|
||||
"VR: degenerate head pose (axis lengths %.3f %.3f %.3f); keeping the game's camera\n",
|
||||
len0,
|
||||
len1,
|
||||
len2
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The game's camera decides where the body stands and which way it faces;
|
||||
// the headset decides everything about where the eyes are relative to that.
|
||||
// Only the heading is taken from the game - its pitch and roll would fight
|
||||
// the viewer's own neck.
|
||||
//
|
||||
// Minus whatever heading the client already fed into the view angles. The
|
||||
// game is told where the player is looking so that objectives and anything
|
||||
// else consulting the view angles agree with what is in front of them, but
|
||||
// that same heading then comes back through the player state and into this
|
||||
// camera - and composing the head onto it again turns it twice. Yaw ends up
|
||||
// doubled while pitch, which the game's camera does not carry here, does
|
||||
// not, and half of the doubled yaw arrives a frame late through prediction.
|
||||
// The result reads as a world that swims when the head turns.
|
||||
//
|
||||
// Taking it back off leaves the game's own contributions - recoil, lean,
|
||||
// cutscene cameras, ladders - composed with the current headset pose, which
|
||||
// is what this function is for.
|
||||
vectoangles(fd->viewaxis[0], baseAngles);
|
||||
VectorSet(bodyAngles, 0, baseAngles[YAW] - vrView.baseYaw, 0);
|
||||
AnglesToAxis(bodyAngles, bodyAxis);
|
||||
|
||||
// Rotate the head's axes into the body's frame. Composed as a rotation
|
||||
// rather than by adding Euler angles: adding them only happens to work
|
||||
// while pitch and roll are small, and turns every head movement into a
|
||||
// twist once they are not.
|
||||
for (i = 0; i < 3; i++) {
|
||||
vec3_t rotated;
|
||||
|
||||
VectorScale(bodyAxis[0], vrView.axis[i][0], rotated);
|
||||
VectorMA(rotated, vrView.axis[i][1], bodyAxis[1], rotated);
|
||||
VectorMA(rotated, vrView.axis[i][2], bodyAxis[2], rotated);
|
||||
VectorCopy(rotated, fd->viewaxis[i]);
|
||||
}
|
||||
|
||||
if (r_vrTrace && r_vrTrace->integer) {
|
||||
static int lastTrace;
|
||||
int now = ri.Milliseconds();
|
||||
|
||||
if (now - lastTrace > 1000) {
|
||||
lastTrace = now;
|
||||
ri.Printf(
|
||||
PRINT_ALL,
|
||||
"VR view: org %.0f %.0f %.0f body yaw %.1f | head off %.1f %.1f %.1f fwd %.2f %.2f %.2f\n",
|
||||
fd->vieworg[0],
|
||||
fd->vieworg[1],
|
||||
fd->vieworg[2],
|
||||
baseAngles[YAW],
|
||||
vrView.origin[0],
|
||||
vrView.origin[1],
|
||||
vrView.origin[2],
|
||||
vrView.axis[0][0],
|
||||
vrView.axis[0][1],
|
||||
vrView.axis[0][2]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The head's offset within the play space, turned to face the same way the
|
||||
// body does before it is added. Leaning left stays leaning left whichever
|
||||
// way the player has turned.
|
||||
VectorMA(fd->vieworg, vrView.origin[0], bodyAxis[0], fd->vieworg);
|
||||
VectorMA(fd->vieworg, vrView.origin[1], bodyAxis[1], fd->vieworg);
|
||||
VectorMA(fd->vieworg, vrView.origin[2], bodyAxis[2], fd->vieworg);
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
R_VRAdjust2DOrtho
|
||||
|
||||
Bends a 2D ortho window to suit the eye it is about to be drawn into.
|
||||
|
||||
Flat content in a headset needs three corrections that no screen ever does.
|
||||
Each eye looks through a frustum that is not centred on its own axis, so
|
||||
identical 2D lands in a different place in each eye; there is no disparity, so
|
||||
what does line up sits at infinity; and the edges of the display are outside
|
||||
what can comfortably be read.
|
||||
|
||||
Expressed as a change to the ortho window rather than a transform on every
|
||||
vertex, because the window is one matrix and the vertices are thousands.
|
||||
|
||||
Everything 2D in this renderer goes through Set2DWindow, including RB_SetGL2D,
|
||||
so unlike rend2 there is a single place to apply it.
|
||||
================
|
||||
*/
|
||||
void R_VRAdjust2DOrtho(float *left, float *right, float *bottom, float *top)
|
||||
{
|
||||
float scale, depth;
|
||||
float tanWidth, tanHeight;
|
||||
float offX, offY, parallax;
|
||||
float windowWidth, windowHeight;
|
||||
|
||||
if (!vrView.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
tanWidth = vrView.tanRight - vrView.tanLeft;
|
||||
tanHeight = vrView.tanUp - vrView.tanDown;
|
||||
|
||||
// No frustum yet means nothing to centre on, and dividing by it would leave
|
||||
// a NaN in the projection - which does not look like an error, it looks like
|
||||
// the HUD simply not being there.
|
||||
if (tanWidth < 0.0001f || tanHeight < 0.0001f) {
|
||||
return;
|
||||
}
|
||||
|
||||
scale = vr_hudScale ? vr_hudScale->value : 1.0f;
|
||||
depth = vr_hudDepth ? vr_hudDepth->value : 2.0f;
|
||||
|
||||
if (scale < 0.1f) {
|
||||
scale = 0.1f;
|
||||
} else if (scale > 1.0f) {
|
||||
scale = 1.0f;
|
||||
}
|
||||
if (depth < 0.5f) {
|
||||
depth = 0.5f;
|
||||
}
|
||||
|
||||
windowWidth = *right - *left;
|
||||
windowHeight = *bottom - *top;
|
||||
|
||||
offX = -(vrView.tanRight + vrView.tanLeft) / tanWidth;
|
||||
offY = -(vrView.tanUp + vrView.tanDown) / tanHeight;
|
||||
|
||||
// Half an interpupillary distance at the given range, as a share of the
|
||||
// eye's half width, opposite ways round for the two eyes. This is what puts
|
||||
// the HUD at that range instead of infinitely far away.
|
||||
parallax = (2.0f * 0.032f) / (depth * tanWidth);
|
||||
offX += (vrView.eye == 0) ? parallax : -parallax;
|
||||
|
||||
// A window wider than the content draws the content smaller and keeps it
|
||||
// centred; the offsets then slide it bodily.
|
||||
*left = *left - windowWidth * (1.0f + offX - scale) / (2.0f * scale);
|
||||
*right = *left + windowWidth / scale;
|
||||
|
||||
*top = *top + windowHeight * (offY + scale - 1.0f) / (2.0f * scale);
|
||||
*bottom = *top + windowHeight / scale;
|
||||
}
|
||||
@@ -669,11 +669,91 @@ RB_SetGL2D
|
||||
|
||||
================
|
||||
*/
|
||||
/*
|
||||
================
|
||||
R_VRAdjust2DOrtho
|
||||
|
||||
Bends a 2D ortho window to suit the eye it is about to be drawn into.
|
||||
|
||||
Flat content in a headset needs three corrections that no screen ever does.
|
||||
Each eye looks through a frustum that is not centred on its own axis, so
|
||||
identical 2D lands in a different place in each eye; there is no disparity, so
|
||||
what does line up sits at infinity; and the edges of the display are outside
|
||||
what can comfortably be read.
|
||||
|
||||
Expressed as a change to the ortho window rather than a transform on every
|
||||
vertex, because the window is one matrix and the vertices are thousands.
|
||||
|
||||
Lives here, and is called from both places that set up a 2D projection, because
|
||||
Set2DWindow builds its own matrix rather than going through RB_SetGL2D - so
|
||||
correcting only one of them corrects nothing that the UI actually draws.
|
||||
================
|
||||
*/
|
||||
void R_VRAdjust2DOrtho( float *left, float *right, float *bottom, float *top )
|
||||
{
|
||||
float scale, depth;
|
||||
float tanWidth, tanHeight;
|
||||
float offX, offY, parallax;
|
||||
float windowWidth, windowHeight;
|
||||
|
||||
if ( !vrView.active ) {
|
||||
return;
|
||||
}
|
||||
|
||||
tanWidth = vrView.tanRight - vrView.tanLeft;
|
||||
tanHeight = vrView.tanUp - vrView.tanDown;
|
||||
|
||||
// No frustum yet means nothing to centre on, and dividing by it would leave
|
||||
// a NaN in the projection - which does not look like an error, it looks like
|
||||
// the HUD simply not being there.
|
||||
if ( tanWidth < 0.0001f || tanHeight < 0.0001f ) {
|
||||
return;
|
||||
}
|
||||
|
||||
scale = vr_hudScale ? vr_hudScale->value : 1.0f;
|
||||
depth = vr_hudDepth ? vr_hudDepth->value : 2.0f;
|
||||
|
||||
if ( scale < 0.1f ) {
|
||||
scale = 0.1f;
|
||||
} else if ( scale > 1.0f ) {
|
||||
scale = 1.0f;
|
||||
}
|
||||
if ( depth < 0.5f ) {
|
||||
depth = 0.5f;
|
||||
}
|
||||
|
||||
windowWidth = *right - *left;
|
||||
windowHeight = *bottom - *top;
|
||||
|
||||
offX = -( vrView.tanRight + vrView.tanLeft ) / tanWidth;
|
||||
offY = -( vrView.tanUp + vrView.tanDown ) / tanHeight;
|
||||
|
||||
// Half an interpupillary distance at the given range, as a share of the
|
||||
// eye's half width, opposite ways round for the two eyes. This is what puts
|
||||
// the HUD at that range instead of infinitely far away.
|
||||
parallax = ( 2.0f * 0.032f ) / ( depth * tanWidth );
|
||||
offX += ( vrView.eye == 0 ) ? parallax : -parallax;
|
||||
|
||||
// A window wider than the content draws the content smaller and keeps it
|
||||
// centred; the offsets then slide it bodily.
|
||||
*left = *left - windowWidth * ( 1.0f + offX - scale ) / ( 2.0f * scale );
|
||||
*right = *left + windowWidth / scale;
|
||||
|
||||
*top = *top + windowHeight * ( offY + scale - 1.0f ) / ( 2.0f * scale );
|
||||
*bottom = *top + windowHeight / scale;
|
||||
}
|
||||
|
||||
void RB_SetGL2D (void) {
|
||||
mat4_t matrix;
|
||||
int width, height;
|
||||
float left, right, bottom, top;
|
||||
|
||||
if (backEnd.projection2D && backEnd.last2DFBO == glState.currentFBO)
|
||||
// The early out keys on the bound framebuffer, which on a build with the
|
||||
// renderer's own framebuffer support switched off is always NULL - so once
|
||||
// this has run it never runs again. Harmless when the 2D projection is a
|
||||
// constant, and not harmless in VR, where it differs per eye.
|
||||
if (backEnd.projection2D && backEnd.last2DFBO == glState.currentFBO
|
||||
&& !vrView.active)
|
||||
return;
|
||||
|
||||
backEnd.projection2D = qtrue;
|
||||
@@ -694,7 +774,13 @@ void RB_SetGL2D (void) {
|
||||
qglViewport( 0, 0, width, height );
|
||||
qglScissor( 0, 0, width, height );
|
||||
|
||||
Mat4Ortho(0, width, height, 0, 0, 1, matrix);
|
||||
left = 0;
|
||||
right = width;
|
||||
bottom = height;
|
||||
top = 0;
|
||||
R_VRAdjust2DOrtho( &left, &right, &bottom, &top );
|
||||
|
||||
Mat4Ortho(left, right, bottom, top, 0, 1, matrix);
|
||||
GL_SetProjectionMatrix(matrix);
|
||||
Mat4Identity(matrix);
|
||||
GL_SetModelviewMatrix(matrix);
|
||||
|
||||
@@ -507,6 +507,11 @@ void Set2DWindow(int x, int y, int w, int h, float left, float right, float bott
|
||||
qglViewport(x, y, w, h);
|
||||
qglScissor(x, y, w, h);
|
||||
|
||||
// Everything the UI draws comes through here rather than RB_SetGL2D, so
|
||||
// this is where the headset's correction has to be applied or none of it
|
||||
// reaches the HUD.
|
||||
R_VRAdjust2DOrtho(&left, &right, &bottom, &top);
|
||||
|
||||
Mat4Ortho(left, right, bottom, top, n, f, matrix);
|
||||
GL_SetProjectionMatrix(matrix);
|
||||
Mat4Identity(matrix);
|
||||
|
||||
@@ -159,6 +159,33 @@ void GLimp_InitExtraExtensions(void)
|
||||
ri.Printf(PRINT_ALL, result[2], extension);
|
||||
}
|
||||
|
||||
// Framebuffer objects.
|
||||
//
|
||||
// These are core in OpenGL ES - the whole of
|
||||
// QGL_ARB_framebuffer_object_PROCS is core in 3.0, with only blit and
|
||||
// multisampled renderbuffers missing from 2.0. The desktop probing
|
||||
// below is skipped entirely on ES, so without loading them here the
|
||||
// renderer runs with framebuffers disabled and, worse, leaves every one
|
||||
// of those entry points NULL for anything that calls them directly.
|
||||
extension = "framebuffer object";
|
||||
if (qglesMajorVersion >= 3)
|
||||
{
|
||||
glRefConfig.framebufferObject = !!r_ext_framebuffer_object->integer;
|
||||
glRefConfig.framebufferBlit = qtrue;
|
||||
glRefConfig.framebufferMultisample = qtrue;
|
||||
|
||||
qglGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &glRefConfig.maxRenderbufferSize);
|
||||
qglGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &glRefConfig.maxColorAttachments);
|
||||
|
||||
QGL_ARB_framebuffer_object_PROCS;
|
||||
|
||||
ri.Printf(PRINT_ALL, result[glRefConfig.framebufferObject], extension);
|
||||
}
|
||||
else
|
||||
{
|
||||
ri.Printf(PRINT_ALL, result[2], extension);
|
||||
}
|
||||
|
||||
goto done;
|
||||
}
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ void FBO_Bind(FBO_t * fbo)
|
||||
GLimp_LogComment(va("--- FBO_Bind( %s ) ---\n", fbo ? fbo->name : "NULL"));
|
||||
}
|
||||
|
||||
GL_BindFramebuffer(GL_FRAMEBUFFER, fbo ? fbo->frameBuffer : 0);
|
||||
GL_BindFramebuffer(GL_FRAMEBUFFER, fbo ? fbo->frameBuffer : glState.defaultFBO);
|
||||
glState.currentFBO = fbo;
|
||||
}
|
||||
|
||||
@@ -416,7 +416,7 @@ void FBO_Init(void)
|
||||
|
||||
GL_CheckErrors();
|
||||
|
||||
GL_BindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
GL_BindFramebuffer(GL_FRAMEBUFFER, glState.defaultFBO);
|
||||
glState.currentFBO = NULL;
|
||||
}
|
||||
|
||||
@@ -681,6 +681,71 @@ void FBO_FastBlit(const FBO_t *src, ivec4_t srcBox, FBO_t *dst, ivec4_t dstBox,
|
||||
dstBoxFinal[0], dstBoxFinal[1], dstBoxFinal[2], dstBoxFinal[3],
|
||||
buffers, filter);
|
||||
|
||||
GL_BindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
GL_BindFramebuffer(GL_FRAMEBUFFER, glState.defaultFBO);
|
||||
glState.currentFBO = NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
============
|
||||
RE_SetDefaultFramebuffer
|
||||
|
||||
Substitutes the framebuffer the renderer treats as the screen. In VR each eye
|
||||
is an OpenXR swapchain image, so the whole render path - including the
|
||||
post-processing resolve, which is what actually reaches the display - lands
|
||||
there instead of in the window. Zero restores the window.
|
||||
============
|
||||
*/
|
||||
void RE_SetDefaultFramebuffer(unsigned int framebuffer)
|
||||
{
|
||||
if (glState.defaultFBO == framebuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
glState.defaultFBO = framebuffer;
|
||||
|
||||
// Rendering into a framebuffer means something else is presenting it, so
|
||||
// the window must not also be swapped.
|
||||
GLimp_SetPresentsToWindow(framebuffer == 0 ? qtrue : qfalse);
|
||||
|
||||
// Whatever was bound describes the old target, so force the next FBO_Bind
|
||||
// to issue a real bind rather than believe its cached state.
|
||||
glState.currentFBO = NULL;
|
||||
|
||||
// Only the renderer's own binding is conditional here. The caller has
|
||||
// already bound the target itself, because when framebuffers are disabled
|
||||
// this entry point does not exist to be called.
|
||||
if (qglBindFramebuffer) {
|
||||
GL_BindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
============
|
||||
RE_SetVRView
|
||||
============
|
||||
*/
|
||||
vrViewState_t vrView;
|
||||
|
||||
void RE_SetVRView(const float *origin, const vec3_t *axis,
|
||||
float tanLeft, float tanRight, float tanUp, float tanDown,
|
||||
float baseYaw, int eye)
|
||||
{
|
||||
if (!origin || !axis) {
|
||||
vrView.active = qfalse;
|
||||
return;
|
||||
}
|
||||
|
||||
vrView.baseYaw = baseYaw;
|
||||
vrView.eye = eye;
|
||||
|
||||
VectorCopy(origin, vrView.origin);
|
||||
VectorCopy(axis[0], vrView.axis[0]);
|
||||
VectorCopy(axis[1], vrView.axis[1]);
|
||||
VectorCopy(axis[2], vrView.axis[2]);
|
||||
|
||||
vrView.tanLeft = tanLeft;
|
||||
vrView.tanRight = tanRight;
|
||||
vrView.tanUp = tanUp;
|
||||
vrView.tanDown = tanDown;
|
||||
vrView.active = qtrue;
|
||||
}
|
||||
@@ -57,6 +57,8 @@ void FBO_Bind(FBO_t *fbo);
|
||||
void FBO_Init(void);
|
||||
void FBO_Shutdown(void);
|
||||
|
||||
void RE_SetDefaultFramebuffer(unsigned int framebuffer);
|
||||
|
||||
void FBO_BlitFromTexture(struct image_s *src, vec4_t inSrcTexCorners, vec2_t inSrcTexScale, FBO_t *dst, ivec4_t inDstBox, struct shaderProgram_s *shaderProgram, vec4_t inColor, int blend);
|
||||
void FBO_Blit(const FBO_t *src, ivec4_t srcBox, vec2_t srcTexScale, FBO_t *dst, ivec4_t dstBox, struct shaderProgram_s *shaderProgram, vec4_t color, int blend);
|
||||
void FBO_FastBlit(const FBO_t *src, ivec4_t srcBox, FBO_t *dst, ivec4_t dstBox, int buffers, int filter);
|
||||
|
||||
@@ -548,6 +548,43 @@ void R_LoadFontShader(fontheader_sgl_t* font)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
================
|
||||
R_FontTraceBegin / R_FontTraceEnd
|
||||
|
||||
Temporary instrumentation, to settle whether the cost of the HUD is the strings
|
||||
themselves. Both string paths flush the render command buffer and then drive the
|
||||
back end directly, once per call - on a tiler with a framebuffer bound that can
|
||||
end the render pass and force a resolve every time. Counting the calls alongside
|
||||
the time spent in them says whether that is where the frame is going.
|
||||
================
|
||||
*/
|
||||
static int s_fontMsec;
|
||||
static int s_fontCalls;
|
||||
static int s_fontLastReport;
|
||||
|
||||
static int R_FontTraceBegin( void ) {
|
||||
return ri.Milliseconds();
|
||||
}
|
||||
|
||||
static void R_FontTraceEnd( int start ) {
|
||||
int now = ri.Milliseconds();
|
||||
|
||||
s_fontMsec += now - start;
|
||||
s_fontCalls++;
|
||||
|
||||
if ( now - s_fontLastReport < 1000 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
ri.Printf( PRINT_ALL, "VR font: %d calls, %dms a second\n", s_fontCalls, s_fontMsec );
|
||||
|
||||
s_fontLastReport = now;
|
||||
s_fontCalls = 0;
|
||||
s_fontMsec = 0;
|
||||
}
|
||||
|
||||
void R_DrawString_sgl(fontheader_sgl_t* font, const char* text, float x, float y, int maxlen, const float *pvVirtualScreen) {
|
||||
float charHeight;
|
||||
float startx, starty;
|
||||
@@ -575,6 +612,8 @@ void R_DrawString_sgl(fontheader_sgl_t* font, const char* text, float x, float y
|
||||
return;
|
||||
}
|
||||
|
||||
const int fontTraceStart = R_FontTraceBegin();
|
||||
|
||||
R_IssuePendingRenderCommands();
|
||||
|
||||
if (font->trhandle != r_sequencenumber) {
|
||||
@@ -694,6 +733,8 @@ void R_DrawString_sgl(fontheader_sgl_t* font, const char* text, float x, float y
|
||||
}
|
||||
|
||||
RB_EndSurface();
|
||||
|
||||
R_FontTraceEnd( fontTraceStart );
|
||||
}
|
||||
|
||||
void R_DrawString(fontheader_t* font, const char* text, float x, float y, int maxlen, const float *pvVirtualScreen) {
|
||||
|
||||
@@ -2247,6 +2247,26 @@ image_t *R_CreateImage2( const char *name, byte *pic, int width, int height, GLe
|
||||
dataFormat = GL_RGBA;
|
||||
dataType = GL_UNSIGNED_SHORT_4_4_4_4;
|
||||
break;
|
||||
// Depth textures are the render target formats, needed once
|
||||
// framebuffer objects are in use. ES 3.0 takes sized internal
|
||||
// formats directly, so these pass through unchanged and only the
|
||||
// data type has to be spelled out.
|
||||
case GL_DEPTH_COMPONENT16:
|
||||
dataFormat = GL_DEPTH_COMPONENT;
|
||||
dataType = GL_UNSIGNED_SHORT;
|
||||
break;
|
||||
case GL_DEPTH_COMPONENT24:
|
||||
dataFormat = GL_DEPTH_COMPONENT;
|
||||
dataType = GL_UNSIGNED_INT;
|
||||
break;
|
||||
case GL_DEPTH_COMPONENT32F:
|
||||
dataFormat = GL_DEPTH_COMPONENT;
|
||||
dataType = GL_FLOAT;
|
||||
break;
|
||||
case GL_DEPTH24_STENCIL8:
|
||||
dataFormat = GL_DEPTH_STENCIL;
|
||||
dataType = GL_UNSIGNED_INT_24_8;
|
||||
break;
|
||||
default:
|
||||
ri.Error( ERR_DROP, "Missing OpenGL ES support for image '%s' with internal format 0x%X\n", name, internalFormat );
|
||||
}
|
||||
|
||||
@@ -207,6 +207,10 @@ cvar_t *r_lodCurveError;
|
||||
cvar_t *r_fullscreen;
|
||||
cvar_t *r_noborder;
|
||||
|
||||
cvar_t *r_vrTrace;
|
||||
cvar_t *vr_fovZoom;
|
||||
cvar_t *vr_hudScale;
|
||||
cvar_t *vr_hudDepth;
|
||||
cvar_t *r_customwidth;
|
||||
cvar_t *r_customheight;
|
||||
cvar_t *r_customPixelAspect;
|
||||
@@ -483,6 +487,12 @@ qboolean R_GetModeInfo( int *width, int *height, float *windowAspect, int mode )
|
||||
vidmode_t *vm;
|
||||
float pixelAspect;
|
||||
|
||||
// A headset dictates the render size; whatever r_mode says is irrelevant.
|
||||
if ( ri.GetVRRenderResolution && ri.GetVRRenderResolution( width, height ) ) {
|
||||
*windowAspect = (float)*width / (float)*height;
|
||||
return qtrue;
|
||||
}
|
||||
|
||||
if ( mode < -1 ) {
|
||||
return qfalse;
|
||||
}
|
||||
@@ -1341,7 +1351,16 @@ void R_Register( void )
|
||||
r_ext_compiled_vertex_array = ri.Cvar_Get( "r_ext_compiled_vertex_array", "1", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
r_ext_texture_env_add = ri.Cvar_Get( "r_ext_texture_env_add", "1", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
|
||||
#ifdef __ANDROID__
|
||||
// The renderer's own render targets buy nothing in VR - each eye is already
|
||||
// an OpenXR swapchain image, which is what the compositor displays - and
|
||||
// they would cost a second full resolution pass and resolve per eye at
|
||||
// 1680x1760. The entry points are still loaded, so the eye framebuffers
|
||||
// work; this only turns off the renderer's internal use of them.
|
||||
r_ext_framebuffer_object = ri.Cvar_Get( "r_ext_framebuffer_object", "0", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
#else
|
||||
r_ext_framebuffer_object = ri.Cvar_Get( "r_ext_framebuffer_object", "1", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
#endif
|
||||
r_ext_texture_float = ri.Cvar_Get( "r_ext_texture_float", "1", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
r_ext_framebuffer_multisample = ri.Cvar_Get( "r_ext_framebuffer_multisample", "0", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
r_arb_seamless_cube_map = ri.Cvar_Get( "r_arb_seamless_cube_map", "0", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
@@ -1368,6 +1387,13 @@ void R_Register( void )
|
||||
r_mode = ri.Cvar_Get( "r_mode", "-2", CVAR_ARCHIVE | CVAR_LATCH );
|
||||
r_fullscreen = ri.Cvar_Get( "r_fullscreen", "1", CVAR_ARCHIVE );
|
||||
r_noborder = ri.Cvar_Get("r_noborder", "0", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
r_vrTrace = ri.Cvar_Get( "r_vrTrace", "0", 0 );
|
||||
// Set by cgame from the fov the game asked for; see CG_CalcFov.
|
||||
vr_fovZoom = ri.Cvar_Get( "vr_fovZoom", "1", 0 );
|
||||
// See RB_SetGL2D. Scale is the share of the display the HUD is drawn across;
|
||||
// depth is how far out it is made to converge.
|
||||
vr_hudScale = ri.Cvar_Get( "vr_hudScale", "0.55", CVAR_ARCHIVE );
|
||||
vr_hudDepth = ri.Cvar_Get( "vr_hudDepth", "2.0", CVAR_ARCHIVE );
|
||||
r_customwidth = ri.Cvar_Get( "r_customwidth", "1600", CVAR_ARCHIVE | CVAR_LATCH );
|
||||
r_customheight = ri.Cvar_Get( "r_customheight", "1024", CVAR_ARCHIVE | CVAR_LATCH );
|
||||
r_customPixelAspect = ri.Cvar_Get( "r_customPixelAspect", "1", CVAR_ARCHIVE | CVAR_LATCH );
|
||||
@@ -1989,6 +2015,8 @@ refexport_t *GetRefAPI ( int apiVersion, refimport_t *rimp ) {
|
||||
|
||||
re.BeginFrame = RE_BeginFrame;
|
||||
re.EndFrame = RE_EndFrame;
|
||||
re.SetDefaultFramebuffer = RE_SetDefaultFramebuffer;
|
||||
re.SetVRView = RE_SetVRView;
|
||||
|
||||
re.MarkFragments = R_MarkFragments;
|
||||
re.LerpTag = R_LerpTag;
|
||||
|
||||
@@ -1876,6 +1876,11 @@ typedef struct {
|
||||
mat4_t boneMatrix[IQM_MAX_JOINTS];
|
||||
uint32_t vertexAttribsEnabled; // global if no VAOs, tess only otherwise
|
||||
FBO_t *currentFBO;
|
||||
|
||||
// What "no FBO" resolves to. Zero is the window, which is the normal case;
|
||||
// in VR each eye's swapchain image is substituted here so that the whole
|
||||
// renderer draws into the headset without knowing about it.
|
||||
GLuint defaultFBO;
|
||||
vao_t *currentVao;
|
||||
mat4_t modelview;
|
||||
mat4_t projection;
|
||||
@@ -2215,6 +2220,32 @@ typedef struct {
|
||||
int farclip;
|
||||
} trGlobals_t;
|
||||
|
||||
/*
|
||||
The eye currently being rendered, when driven by a headset.
|
||||
|
||||
Kept outside trGlobals_t for the same reason as glState: it describes the
|
||||
display rather than the scene, and must survive a renderer re-init.
|
||||
*/
|
||||
typedef struct {
|
||||
qboolean active;
|
||||
vec3_t origin; // head offset from the game's camera, engine units
|
||||
vec3_t axis[3]; // head orientation as forward/left/up
|
||||
float tanLeft; // asymmetric frustum edges at unit distance
|
||||
float tanRight;
|
||||
float tanUp;
|
||||
float tanDown;
|
||||
float baseYaw; // head heading the game's camera already carries
|
||||
int eye; // which eye, for the HUD's stereo offset
|
||||
} vrViewState_t;
|
||||
|
||||
extern vrViewState_t vrView;
|
||||
|
||||
void R_VRAdjust2DOrtho( float *left, float *right, float *bottom, float *top );
|
||||
|
||||
void RE_SetVRView( const float *origin, const vec3_t *axis,
|
||||
float tanLeft, float tanRight, float tanUp, float tanDown,
|
||||
float baseYaw, int eye );
|
||||
|
||||
extern backEndState_t backEnd;
|
||||
extern trGlobals_t tr;
|
||||
extern glstate_t glState; // outside of TR since it shouldn't be cleared during ref re-init
|
||||
@@ -2242,6 +2273,10 @@ extern cvar_t *r_stereoSeparation; // separation of cameras for stereo renderi
|
||||
|
||||
extern cvar_t *r_measureOverdraw; // enables stencil buffer overdraw measurement
|
||||
|
||||
extern cvar_t *r_vrTrace; // log the composed VR camera once a second
|
||||
extern cvar_t *vr_fovZoom; // scope magnification, published by cgame
|
||||
extern cvar_t *vr_hudScale; // how much of the display the HUD covers
|
||||
extern cvar_t *vr_hudDepth; // metres out the HUD is made to converge
|
||||
extern cvar_t *r_lodbias; // push/pull LOD transitions
|
||||
extern cvar_t *r_lodscale;
|
||||
|
||||
|
||||
@@ -851,11 +851,46 @@ void R_SetupProjection(viewParms_t *dest, float zProj, float zFar, qboolean comp
|
||||
stereoSep = 0;
|
||||
}
|
||||
|
||||
ymax = zProj * tan(dest->fovY * M_PI / 360.0f);
|
||||
ymin = -ymax;
|
||||
if (vrView.active)
|
||||
{
|
||||
// A headset's per-eye frustum is not centred on the view axis - each
|
||||
// eye sees further towards its own side than towards the nose. Using a
|
||||
// symmetric approximation misaligns the two images by a few degrees,
|
||||
// which reads as an image the eyes cannot fuse. The matrix below
|
||||
// already carries the off-centre terms, so the runtime's own edges go
|
||||
// straight in.
|
||||
// Scopes and binoculars. The game narrows its fov to zoom, but in a
|
||||
// headset the frustum comes from the runtime, so that request would
|
||||
// otherwise be dropped on the floor. Narrowing the eye frustum by the
|
||||
// same factor while the composition layer keeps claiming the headset's
|
||||
// full field of view leaves the compositor to magnify the result, which
|
||||
// is both free and sharper than zooming after the fact: the narrow view
|
||||
// was rendered into the whole eye texture.
|
||||
float zoom = vr_fovZoom ? vr_fovZoom->value : 1.0f;
|
||||
|
||||
xmax = zProj * tan(dest->fovX * M_PI / 360.0f);
|
||||
xmin = -xmax;
|
||||
if ( zoom < 1.0f ) {
|
||||
zoom = 1.0f;
|
||||
} else if ( zoom > 16.0f ) {
|
||||
zoom = 16.0f;
|
||||
}
|
||||
|
||||
xmin = zProj * vrView.tanLeft / zoom;
|
||||
xmax = zProj * vrView.tanRight / zoom;
|
||||
ymin = zProj * vrView.tanDown / zoom;
|
||||
ymax = zProj * vrView.tanUp / zoom;
|
||||
|
||||
// Stereo separation is the flat renderer's way of faking two eyes, and
|
||||
// would be applied on top of a real one.
|
||||
stereoSep = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
ymax = zProj * tan(dest->fovY * M_PI / 360.0f);
|
||||
ymin = -ymax;
|
||||
|
||||
xmax = zProj * tan(dest->fovX * M_PI / 360.0f);
|
||||
xmin = -xmax;
|
||||
}
|
||||
|
||||
width = xmax - xmin;
|
||||
height = ymax - ymin;
|
||||
|
||||
@@ -518,9 +518,90 @@ Rendering a scene may require multiple views to be rendered
|
||||
to handle mirrors,
|
||||
@@@@@@@@@@@@@@@@@@@@@
|
||||
*/
|
||||
/*
|
||||
====================
|
||||
R_ApplyVRView
|
||||
|
||||
Puts the headset on top of whatever camera the game asked for.
|
||||
|
||||
Done here rather than in the game code because every camera reaches the
|
||||
renderer through this one function - the player's view, cutscene cameras,
|
||||
ladder climbs, death animations - so head tracking follows all of them without
|
||||
each having to know about VR.
|
||||
|
||||
The game's camera decides where the body is and which way it faces. Everything
|
||||
else comes from the headset: pitch and roll are taken from the head alone,
|
||||
since blending them with the game's would fight the viewer's own neck.
|
||||
====================
|
||||
*/
|
||||
static void R_ApplyVRView( refdef_t *fd ) {
|
||||
vec3_t baseAngles, bodyAngles, bodyAxis[3];
|
||||
int i;
|
||||
|
||||
if ( !vrView.active ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The game's camera decides where the body stands and which way it faces;
|
||||
// the headset decides everything about where the eyes are relative to that.
|
||||
// Only the heading is taken from the game - its pitch and roll would fight
|
||||
// the viewer's own neck.
|
||||
//
|
||||
// Minus whatever heading the client already fed into the view angles. The
|
||||
// game is told where the player is looking so that objectives and anything
|
||||
// else consulting the view angles agree with what is in front of them, but
|
||||
// that same heading then comes back through the player state and into this
|
||||
// camera - and composing the head onto it again turns it twice. Yaw ends up
|
||||
// doubled while pitch, which the game's camera does not carry here, does
|
||||
// not, and half of the doubled yaw arrives a frame late through prediction.
|
||||
// The result reads as a world that swims when the head turns.
|
||||
//
|
||||
// Taking it back off leaves the game's own contributions - recoil, lean,
|
||||
// cutscene cameras, ladders - composed with the current headset pose, which
|
||||
// is what this function is for.
|
||||
vectoangles( fd->viewaxis[0], baseAngles );
|
||||
VectorSet( bodyAngles, 0, baseAngles[YAW] - vrView.baseYaw, 0 );
|
||||
AnglesToAxis( bodyAngles, bodyAxis );
|
||||
|
||||
// Rotate the head's axes into the body's frame. Composed as a rotation
|
||||
// rather than by adding Euler angles: adding them only happens to work
|
||||
// while pitch and roll are small, and turns every head movement into a
|
||||
// twist once they are not.
|
||||
for ( i = 0; i < 3; i++ ) {
|
||||
vec3_t rotated;
|
||||
|
||||
VectorScale( bodyAxis[0], vrView.axis[i][0], rotated );
|
||||
VectorMA( rotated, vrView.axis[i][1], bodyAxis[1], rotated );
|
||||
VectorMA( rotated, vrView.axis[i][2], bodyAxis[2], rotated );
|
||||
VectorCopy( rotated, fd->viewaxis[i] );
|
||||
}
|
||||
|
||||
if ( r_vrTrace && r_vrTrace->integer ) {
|
||||
static int lastTrace;
|
||||
int now = ri.Milliseconds();
|
||||
|
||||
if ( now - lastTrace > 1000 ) {
|
||||
lastTrace = now;
|
||||
ri.Printf( PRINT_ALL,
|
||||
"VR view: org %.0f %.0f %.0f body yaw %.1f | head off %.1f %.1f %.1f fwd %.2f %.2f %.2f\n",
|
||||
fd->vieworg[0], fd->vieworg[1], fd->vieworg[2], baseAngles[YAW],
|
||||
vrView.origin[0], vrView.origin[1], vrView.origin[2],
|
||||
vrView.axis[0][0], vrView.axis[0][1], vrView.axis[0][2] );
|
||||
}
|
||||
}
|
||||
|
||||
// The head's offset within the play space, turned to face the same way the
|
||||
// body does before it is added. Leaning left stays leaning left whichever
|
||||
// way the player has turned.
|
||||
VectorMA( fd->vieworg, vrView.origin[0], bodyAxis[0], fd->vieworg );
|
||||
VectorMA( fd->vieworg, vrView.origin[1], bodyAxis[1], fd->vieworg );
|
||||
VectorMA( fd->vieworg, vrView.origin[2], bodyAxis[2], fd->vieworg );
|
||||
}
|
||||
|
||||
void RE_RenderScene( const refdef_t *fd ) {
|
||||
viewParms_t parms;
|
||||
int startTime;
|
||||
refdef_t vrRefdef;
|
||||
|
||||
if ( !tr.registered ) {
|
||||
return;
|
||||
@@ -531,6 +612,12 @@ void RE_RenderScene( const refdef_t *fd ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( vrView.active ) {
|
||||
vrRefdef = *fd;
|
||||
R_ApplyVRView( &vrRefdef );
|
||||
fd = &vrRefdef;
|
||||
}
|
||||
|
||||
startTime = ri.Milliseconds();
|
||||
|
||||
if (!tr.world && !( fd->rdflags & RDF_NOWORLDMODEL ) ) {
|
||||
|
||||
@@ -372,12 +372,25 @@ static void AutospriteDeform( void ) {
|
||||
tess.numIndexes = 0;
|
||||
tess.firstIndex = 0;
|
||||
|
||||
if ( backEnd.currentEntity != &tr.worldEntity ) {
|
||||
GlobalVectorToLocal( backEnd.viewParms.ori.axis[1], leftDir );
|
||||
GlobalVectorToLocal( backEnd.viewParms.ori.axis[2], upDir );
|
||||
} else {
|
||||
VectorCopy( backEnd.viewParms.ori.axis[1], leftDir );
|
||||
VectorCopy( backEnd.viewParms.ori.axis[2], upDir );
|
||||
{
|
||||
vec3_t viewLeft, viewUp;
|
||||
|
||||
VectorCopy( backEnd.viewParms.ori.axis[1], viewLeft );
|
||||
VectorCopy( backEnd.viewParms.ori.axis[2], viewUp );
|
||||
|
||||
if ( vrView.active ) {
|
||||
// World up, so the sprites stay upright rather than inheriting the
|
||||
// head's roll.
|
||||
VectorSet( viewUp, 0.0f, 0.0f, 1.0f );
|
||||
}
|
||||
|
||||
if ( backEnd.currentEntity != &tr.worldEntity ) {
|
||||
GlobalVectorToLocal( viewLeft, leftDir );
|
||||
GlobalVectorToLocal( viewUp, upDir );
|
||||
} else {
|
||||
VectorCopy( viewLeft, leftDir );
|
||||
VectorCopy( viewUp, upDir );
|
||||
}
|
||||
}
|
||||
|
||||
for ( i = 0 ; i < oldVerts ; i+=4 ) {
|
||||
@@ -392,9 +405,37 @@ static void AutospriteDeform( void ) {
|
||||
VectorSubtract( xyz, mid, delta );
|
||||
radius = VectorLength( delta ) * 0.707f; // / sqrt(2)
|
||||
|
||||
// Quake's autosprites align to the view plane: every sprite takes the
|
||||
// camera's own axes, so they all sit parallel to the screen. On a
|
||||
// monitor that is indistinguishable from facing the viewer, but in VR
|
||||
// turning the head swings the whole set round to stay parallel to the
|
||||
// view plane, and they visibly spin in place. Point each one at where
|
||||
// the viewer actually is instead, so head rotation leaves them alone
|
||||
// and only moving changes what they present.
|
||||
if ( vrView.active ) {
|
||||
vec3_t toViewer, spriteLeft, spriteUp;
|
||||
|
||||
VectorSubtract( backEnd.ori.viewOrigin, mid, toViewer );
|
||||
CrossProduct( upDir, toViewer, spriteLeft );
|
||||
|
||||
// Degenerate directly above or below the sprite, where there is no
|
||||
// heading to align to; the shared axes are as good as anything.
|
||||
if ( VectorNormalize( spriteLeft ) > 0.001f ) {
|
||||
CrossProduct( toViewer, spriteLeft, spriteUp );
|
||||
|
||||
if ( VectorNormalize( spriteUp ) > 0.001f ) {
|
||||
VectorScale( spriteLeft, radius, left );
|
||||
VectorScale( spriteUp, radius, up );
|
||||
goto haveAxes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VectorScale( leftDir, radius, left );
|
||||
VectorScale( upDir, radius, up );
|
||||
|
||||
haveAxes:
|
||||
|
||||
if ( backEnd.viewParms.isMirror ) {
|
||||
VectorSubtract( vec3_origin, left, left );
|
||||
}
|
||||
@@ -514,6 +555,27 @@ static void Autosprite2Deform( void ) {
|
||||
VectorSubtract( mid[1], mid[0], major );
|
||||
|
||||
// cross this with the view direction to get minor axis
|
||||
if ( vrView.active ) {
|
||||
// Towards the viewer, not along the view axis. On a monitor the two
|
||||
// amount to the same thing - the eye is effectively infinitely far
|
||||
// away down the middle of the screen - but in a headset they are
|
||||
// not, and taking the view axis swings every one of these sprites
|
||||
// as the head turns while the geometry it stands beside holds
|
||||
// still. That is the second, worse tree leaning out of every real
|
||||
// one: the imposter and the model disagreeing about where the
|
||||
// viewer is.
|
||||
//
|
||||
// Per sprite rather than per batch for the same reason: each one
|
||||
// has to face its own line to the eye, and a single direction for
|
||||
// the whole surface is only right for a sprite dead ahead.
|
||||
vec3_t centre;
|
||||
|
||||
VectorAdd( mid[0], mid[1], centre );
|
||||
VectorScale( centre, 0.5f, centre );
|
||||
VectorSubtract( centre, backEnd.ori.viewOrigin, forward );
|
||||
VectorNormalize( forward );
|
||||
}
|
||||
|
||||
CrossProduct( major, forward, minor );
|
||||
VectorNormalize( minor );
|
||||
|
||||
|
||||
@@ -122,6 +122,28 @@ void RB_DrawSprite( const refSprite_t *spr ) {
|
||||
}
|
||||
break;
|
||||
case SPRITE_PARALLEL:
|
||||
// In VR, face where the viewer is rather than lying parallel to the
|
||||
// view plane. Parallel to the view plane means every sprite in the
|
||||
// world swings round together the moment the head turns, which is
|
||||
// invisible on a monitor and glaring in a headset.
|
||||
if (vrView.active) {
|
||||
vec3_t toViewer;
|
||||
|
||||
VectorSubtract(backEnd.viewParms.ori.origin, spr->origin, toViewer);
|
||||
|
||||
if (VectorNormalize(toViewer) > 0.001f) {
|
||||
vec3_t worldUp = { 0.0f, 0.0f, 1.0f };
|
||||
|
||||
CrossProduct(worldUp, toViewer, right);
|
||||
|
||||
if (VectorNormalize(right) > 0.001f) {
|
||||
CrossProduct(toViewer, right, up);
|
||||
VectorNormalize(up);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VectorCopy(backEnd.viewParms.ori.axis[2], up);
|
||||
if (!backEnd.viewParms.isMirror) {
|
||||
VectorNegate(backEnd.viewParms.ori.axis[1], right);
|
||||
@@ -144,8 +166,28 @@ void RB_DrawSprite( const refSprite_t *spr ) {
|
||||
}
|
||||
|
||||
VectorSet(up, 0.0f, 0.0f, 1.0f);
|
||||
VectorSet(right, backEnd.viewParms.ori.axis[0][1], -backEnd.viewParms.ori.axis[0][0], 0.0f);
|
||||
VectorNormalize(right);
|
||||
|
||||
// Upright sprites already ignore the view's roll and pitch, but they
|
||||
// still turn with the view's heading rather than towards the viewer -
|
||||
// so in VR they spin in place as the head turns. Aim each one at where
|
||||
// the viewer actually stands instead.
|
||||
if (vrView.active) {
|
||||
vec3_t toViewer;
|
||||
|
||||
VectorSubtract(backEnd.viewParms.ori.origin, spr->origin, toViewer);
|
||||
toViewer[2] = 0.0f;
|
||||
|
||||
if (VectorNormalize(toViewer) > 0.001f) {
|
||||
VectorSet(right, -toViewer[1], toViewer[0], 0.0f);
|
||||
} else {
|
||||
VectorSet(right, backEnd.viewParms.ori.axis[0][1], -backEnd.viewParms.ori.axis[0][0], 0.0f);
|
||||
VectorNormalize(right);
|
||||
}
|
||||
} else {
|
||||
VectorSet(right, backEnd.viewParms.ori.axis[0][1], -backEnd.viewParms.ori.axis[0][0], 0.0f);
|
||||
VectorNormalize(right);
|
||||
}
|
||||
|
||||
VectorSet(norm, -right[1], right[0], 0.0f);
|
||||
break;
|
||||
}
|
||||
|
||||
+336
-20
@@ -35,6 +35,11 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
#include "../sys/sys_local.h"
|
||||
#include "sdl_icon.h"
|
||||
|
||||
#ifdef USE_GL4ES
|
||||
#include <dlfcn.h>
|
||||
#include <EGL/egl.h>
|
||||
#endif
|
||||
|
||||
typedef enum
|
||||
{
|
||||
RSERR_OK,
|
||||
@@ -252,6 +257,137 @@ static void APIENTRY GLimp_GLES_PolygonMode( GLenum face, GLenum mode ) {
|
||||
// unsupported
|
||||
}
|
||||
|
||||
#ifdef USE_GL4ES
|
||||
/*
|
||||
===============
|
||||
GLimp_GL4ES_GetProcAddress
|
||||
|
||||
The fixed function renderer's entry points have to come from gl4es and from
|
||||
nowhere else.
|
||||
|
||||
SDL cannot be asked for them. On EGL 1.5 - which the Quest is -
|
||||
SDL_EGL_GetProcAddress tries eglGetProcAddress before it tries the library it
|
||||
loaded, and the driver answers for every name OpenGL 1.x and ES have in common:
|
||||
glEnable, glBindTexture, glDrawArrays, glTexImage2D and eighty more. Only the
|
||||
names that exist nowhere but desktop GL - glBegin, glMatrixMode - would reach
|
||||
gl4es. That split is worse than either half on its own, because gl4es batches
|
||||
geometry and tracks the matrix stack against calls the driver would then never
|
||||
be told about.
|
||||
|
||||
So resolve against the gl4es handle directly. It is already in the process, in
|
||||
DT_NEEDED, so this dlopen only takes a reference to it.
|
||||
===============
|
||||
*/
|
||||
static void *GLimp_GL4ES_GetProcAddress( const char *name ) {
|
||||
static void *gl4es = NULL;
|
||||
|
||||
if ( !gl4es ) {
|
||||
gl4es = dlopen( "libgl4es.so", RTLD_NOW | RTLD_LOCAL );
|
||||
|
||||
if ( !gl4es ) {
|
||||
Com_Error( ERR_FATAL, "Could not open libgl4es.so: %s", dlerror() );
|
||||
}
|
||||
|
||||
// NO_INIT_CONSTRUCTOR is set, so gl4es does not attach itself to the
|
||||
// context behind our back; it is told to, once the context is current.
|
||||
void ( *initialize_gl4es )( void ) = dlsym( gl4es, "initialize_gl4es" );
|
||||
|
||||
if ( !initialize_gl4es ) {
|
||||
Com_Error( ERR_FATAL, "libgl4es.so has no initialize_gl4es" );
|
||||
}
|
||||
|
||||
initialize_gl4es();
|
||||
}
|
||||
|
||||
return dlsym( gl4es, name );
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
GLimp_ExtensionSupported
|
||||
|
||||
And the extension *string* has to come from gl4es too. SDL_GL_ExtensionSupported
|
||||
resolves glGetString through SDL, so it reads the driver's list - which is the
|
||||
ES one, and names none of the desktop extensions the renderer asks after. Left
|
||||
to SDL, GL_ARB_multitexture reads as absent and the renderer quietly drops to a
|
||||
single texture unit.
|
||||
|
||||
qglGetString is gl4es's by this point; GLimp_GetProcAddresses has already run.
|
||||
===============
|
||||
*/
|
||||
static qboolean GLimp_ExtensionSupported( const char *extension ) {
|
||||
const char *extensions = (const char *)qglGetString( GL_EXTENSIONS );
|
||||
const char *p;
|
||||
size_t len = strlen( extension );
|
||||
|
||||
if ( !extensions ) {
|
||||
return qfalse;
|
||||
}
|
||||
|
||||
// Whole tokens only, or GL_EXT_texture_compression_s3tc would answer for a
|
||||
// query about GL_EXT_texture_compression.
|
||||
for ( p = extensions; ( p = strstr( p, extension ) ) != NULL; p += len ) {
|
||||
if ( ( p == extensions || p[-1] == ' ' ) && ( p[len] == ' ' || p[len] == '\0' ) ) {
|
||||
return qtrue;
|
||||
}
|
||||
}
|
||||
|
||||
return qfalse;
|
||||
}
|
||||
#else
|
||||
#define GLimp_ExtensionSupported( name ) SDL_GL_ExtensionSupported( name )
|
||||
#endif
|
||||
|
||||
/*
|
||||
===============
|
||||
GLimp_GetProcAddress
|
||||
|
||||
The renderer's one way of asking for a GL entry point, so that everything it
|
||||
calls comes from the same implementation. Everything above resolves through
|
||||
this, and so does anything a renderer needs to load for itself - renderergl1
|
||||
has no extension probe of its own and asks for the framebuffer entry points
|
||||
here.
|
||||
===============
|
||||
*/
|
||||
void *GLimp_GetProcAddress( const char *name ) {
|
||||
#ifdef USE_GL4ES
|
||||
return GLimp_GL4ES_GetProcAddress( name );
|
||||
#else
|
||||
return SDL_GL_GetProcAddress( name );
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
GLimp_MakeCurrent
|
||||
|
||||
Bind the window's GL context to the calling thread again.
|
||||
|
||||
The VR layer hands the OpenXR session the EGL context the engine is already
|
||||
using, so it needs one to be current at the moment it asks. Whether it is
|
||||
depends on what SDL and the platform have done with the window since - a hidden
|
||||
window on Android has no surface to be current against yet, and being current
|
||||
is per thread in any case. Rather than assume, the session asks for it.
|
||||
|
||||
Returns false if there is no context to bind, which is a different thing from
|
||||
failing to bind one.
|
||||
===============
|
||||
*/
|
||||
qboolean GLimp_MakeCurrent( void ) {
|
||||
if ( !SDL_window || !SDL_glContext ) {
|
||||
ri.Printf( PRINT_ALL, "GLimp_MakeCurrent: no window (%p) or context (%p)\n",
|
||||
(void *)SDL_window, (void *)SDL_glContext );
|
||||
return qfalse;
|
||||
}
|
||||
|
||||
if ( SDL_GL_MakeCurrent( SDL_window, SDL_glContext ) < 0 ) {
|
||||
ri.Printf( PRINT_ALL, "SDL_GL_MakeCurrent failed: %s\n", SDL_GetError() );
|
||||
return qfalse;
|
||||
}
|
||||
|
||||
return qtrue;
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
GLimp_GetProcAddresses
|
||||
@@ -259,12 +395,96 @@ GLimp_GetProcAddresses
|
||||
Get addresses for OpenGL functions.
|
||||
===============
|
||||
*/
|
||||
|
||||
#ifdef USE_GL4ES
|
||||
/*
|
||||
===============
|
||||
GLimp_BindContextToPbuffer
|
||||
|
||||
Give the GL context something to be current against, before anything asks it a
|
||||
question.
|
||||
|
||||
SDL binds the context to the window's EGL surface, and in a headset no such
|
||||
surface ever arrives - so SDL_EGL_MakeCurrent unbinds everything and returns
|
||||
success, leaving SDL certain a context is current while EGL reports none.
|
||||
|
||||
That was already known; what was missed is how early it matters. gl4es
|
||||
initialises on the first proc address request, which is the next thing that
|
||||
happens after the context is created. With nothing current, its hardware
|
||||
detection and its shader pipeline are set up against no GL at all. The result
|
||||
is not a failure anyone sees: clears go straight through to the driver and work
|
||||
perfectly, while every piece of geometry gl4es is asked to draw disappears in
|
||||
silence, no error raised.
|
||||
|
||||
A 16x16 pbuffer is enough to be current against, which is what RTCWQuest uses
|
||||
(TBXR_Common.c, egl->TinySurface). The VR layer does the same thing later for
|
||||
the OpenXR session; this is the same fix moved to where it has to happen first.
|
||||
===============
|
||||
*/
|
||||
static void GLimp_BindContextToPbuffer( void )
|
||||
{
|
||||
static const EGLint pbufferAttribs[] = { EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE };
|
||||
EGLDisplay display;
|
||||
EGLContext context;
|
||||
EGLConfig config = NULL;
|
||||
EGLint configId = 0, numConfigs = 0;
|
||||
EGLint configAttribs[3] = { EGL_CONFIG_ID, 0, EGL_NONE };
|
||||
EGLSurface pbuffer;
|
||||
|
||||
if ( eglGetCurrentContext() != EGL_NO_CONTEXT ) {
|
||||
return;
|
||||
}
|
||||
|
||||
display = eglGetDisplay( EGL_DEFAULT_DISPLAY );
|
||||
context = (EGLContext)SDL_glContext;
|
||||
|
||||
if ( display == EGL_NO_DISPLAY || !context ) {
|
||||
ri.Printf( PRINT_ALL, "gl4es: no EGL context to bind; geometry will not draw\n" );
|
||||
return;
|
||||
}
|
||||
|
||||
eglQueryContext( display, context, EGL_CONFIG_ID, &configId );
|
||||
configAttribs[1] = configId;
|
||||
|
||||
if ( !eglChooseConfig( display, configAttribs, &config, 1, &numConfigs ) || numConfigs < 1 ) {
|
||||
ri.Printf( PRINT_ALL, "gl4es: could not recover EGLConfig %d\n", configId );
|
||||
return;
|
||||
}
|
||||
|
||||
pbuffer = eglCreatePbufferSurface( display, config, pbufferAttribs );
|
||||
|
||||
if ( pbuffer == EGL_NO_SURFACE ) {
|
||||
ri.Printf( PRINT_ALL, "gl4es: could not create the pbuffer (0x%x)\n", eglGetError() );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !eglMakeCurrent( display, pbuffer, pbuffer, context ) ) {
|
||||
ri.Printf( PRINT_ALL, "gl4es: eglMakeCurrent on the pbuffer failed (0x%x)\n", eglGetError() );
|
||||
return;
|
||||
}
|
||||
|
||||
ri.Printf( PRINT_ALL, "gl4es: context bound to a 16x16 pbuffer before init\n" );
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_GL4ES
|
||||
static const qboolean usingGL4ES = qtrue;
|
||||
#else
|
||||
static const qboolean usingGL4ES = qfalse;
|
||||
#endif
|
||||
|
||||
static qboolean GLimp_GetProcAddresses( qboolean fixedFunction ) {
|
||||
qboolean success = qtrue;
|
||||
const char *version;
|
||||
|
||||
#ifdef __SDL_NOGETPROCADDR__
|
||||
#define GLE( ret, name, ... ) qgl##name = gl#name;
|
||||
#elif defined( USE_GL4ES )
|
||||
#define GLE( ret, name, ... ) qgl##name = (name##proc *) GLimp_GL4ES_GetProcAddress("gl" #name); \
|
||||
if ( qgl##name == NULL ) { \
|
||||
ri.Printf( PRINT_ALL, "ERROR: Missing OpenGL function %s\n", "gl" #name ); \
|
||||
success = qfalse; \
|
||||
}
|
||||
#else
|
||||
#define GLE( ret, name, ... ) qgl##name = (name##proc *) SDL_GL_GetProcAddress("gl" #name); \
|
||||
if ( qgl##name == NULL ) { \
|
||||
@@ -320,6 +540,22 @@ static qboolean GLimp_GetProcAddresses( qboolean fixedFunction ) {
|
||||
if ( QGL_VERSION_ATLEAST( 1, 3 ) ) {
|
||||
QGL_1_3_PROCS;
|
||||
}
|
||||
|
||||
#ifdef USE_GL4ES
|
||||
// gl4es reports a desktop version, so this takes the desktop branch
|
||||
// above and picks up the real glDrawBuffer and glPolygonMode. Neither
|
||||
// means anything underneath, where the context is OpenGL ES - which is
|
||||
// why the ES branch below stubs exactly these two out.
|
||||
//
|
||||
// glDrawBuffer is the one that matters. The back end names GL_BACK once
|
||||
// a frame in RB_DrawBuffer, and GL_BACK is not a buffer a framebuffer
|
||||
// object has; asking for it while an eye or the panel is bound leaves
|
||||
// the draw buffer in a state where everything afterwards is discarded.
|
||||
// Correct geometry, correct matrices, no error on the draw itself, and
|
||||
// nothing written anywhere - which is precisely what was happening.
|
||||
qglDrawBuffer = GLimp_GLES_DrawBuffer;
|
||||
qglPolygonMode = GLimp_GLES_PolygonMode;
|
||||
#endif
|
||||
} else {
|
||||
if ( QGL_VERSION_ATLEAST( 2, 0 ) ) {
|
||||
QGL_1_1_PROCS;
|
||||
@@ -549,13 +785,14 @@ static int GLimp_SetMode(int mode, qboolean fullscreen, qboolean noborder, qbool
|
||||
( r_preferOpenGLES->integer == -1 && profileMask == SDL_GL_CONTEXT_PROFILE_ES ) );
|
||||
|
||||
if ( preferOpenGLES ) {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// WebGL 2.0 isn't fully backward compatible so you have to ask for it specifically
|
||||
// ES 3 has to be asked for by name. WebGL 2.0 isn't fully backward
|
||||
// compatible, and EGL hands back a real ES 2.0 context when that is
|
||||
// what was requested - so asking for 2.0 first would settle for it
|
||||
// even where 3 is available.
|
||||
contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_ES;
|
||||
contexts[numContexts].majorVersion = 3;
|
||||
contexts[numContexts].minorVersion = 0;
|
||||
numContexts++;
|
||||
#endif
|
||||
|
||||
contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_ES;
|
||||
contexts[numContexts].majorVersion = 2;
|
||||
@@ -574,12 +811,10 @@ static int GLimp_SetMode(int mode, qboolean fullscreen, qboolean noborder, qbool
|
||||
numContexts++;
|
||||
|
||||
if ( !preferOpenGLES ) {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_ES;
|
||||
contexts[numContexts].majorVersion = 3;
|
||||
contexts[numContexts].minorVersion = 0;
|
||||
numContexts++;
|
||||
#endif
|
||||
|
||||
contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_ES;
|
||||
contexts[numContexts].majorVersion = 2;
|
||||
@@ -587,10 +822,37 @@ static int GLimp_SetMode(int mode, qboolean fullscreen, qboolean noborder, qbool
|
||||
numContexts++;
|
||||
}
|
||||
} else {
|
||||
#ifdef USE_GL4ES
|
||||
// The fixed function renderer asks for a fixed function context, and on
|
||||
// this platform there is no such thing - the request fails at
|
||||
// SDL_CreateWindow with "Couldn't get a visual" and takes the renderer
|
||||
// down with it.
|
||||
//
|
||||
// gl4es is what makes the request answerable, and it wants the opposite
|
||||
// of what the renderer thinks it is getting: it presents desktop GL 1.x
|
||||
// upwards while running on OpenGL ES underneath. So ask for ES here.
|
||||
// The renderer is never told - it reads its version through gl4es, which
|
||||
// reports desktop GL, and GLimp_GetProcAddresses takes the desktop
|
||||
// fixed function branch on the strength of it.
|
||||
//
|
||||
// ES 3 first and by name, for the reason given above: EGL hands back a
|
||||
// real ES 2.0 context when 2.0 is what was asked for, so asking for it
|
||||
// first would settle for it even where 3 is available.
|
||||
contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_ES;
|
||||
contexts[numContexts].majorVersion = 3;
|
||||
contexts[numContexts].minorVersion = 0;
|
||||
numContexts++;
|
||||
|
||||
contexts[numContexts].profileMask = SDL_GL_CONTEXT_PROFILE_ES;
|
||||
contexts[numContexts].majorVersion = 2;
|
||||
contexts[numContexts].minorVersion = 0;
|
||||
numContexts++;
|
||||
#else
|
||||
contexts[numContexts].profileMask = 0;
|
||||
contexts[numContexts].majorVersion = 1;
|
||||
contexts[numContexts].minorVersion = 1;
|
||||
numContexts++;
|
||||
#endif
|
||||
}
|
||||
|
||||
for (i = 0; i < 16; i++)
|
||||
@@ -732,6 +994,32 @@ static int GLimp_SetMode(int mode, qboolean fullscreen, qboolean noborder, qbool
|
||||
continue;
|
||||
}
|
||||
|
||||
#ifdef USE_GL4ES
|
||||
// The usual proof that a context is live is GL_VERSION coming back
|
||||
// non-NULL from GLimp_GetProcAddresses below. That test does not
|
||||
// work here: gl4es answers glGetString for VERSION, VENDOR and
|
||||
// RENDERER out of its own constants, without asking the driver
|
||||
// anything, so a context that was created but never bound sails
|
||||
// through it and is only noticed much later - when the VR layer
|
||||
// asks EGL for the current context and finds none.
|
||||
if ( !SDL_GL_GetCurrentContext() )
|
||||
{
|
||||
ri.Printf( PRINT_ALL, "%s context was created but is not current: %s\n",
|
||||
contextName, SDL_GetError() );
|
||||
SDL_GL_DeleteContext( SDL_glContext );
|
||||
SDL_glContext = NULL;
|
||||
SDL_DestroyWindow( SDL_window );
|
||||
SDL_window = NULL;
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_GL4ES
|
||||
// Before GLimp_GetProcAddresses, because that is what initialises
|
||||
// gl4es and gl4es needs a context to look at.
|
||||
GLimp_BindContextToPbuffer();
|
||||
#endif
|
||||
|
||||
if ( !GLimp_GetProcAddresses( fixedFunction ) )
|
||||
{
|
||||
ri.Printf( PRINT_ALL, "GLimp_GetProcAddresses() for %s context failed\n", contextName );
|
||||
@@ -907,8 +1195,8 @@ static void GLimp_InitExtensions( qboolean fixedFunction )
|
||||
glConfig.textureCompression = TC_NONE;
|
||||
|
||||
// GL_EXT_texture_compression_s3tc
|
||||
if ( ( QGLES_VERSION_ATLEAST( 2, 0 ) || SDL_GL_ExtensionSupported( "GL_ARB_texture_compression" ) ) &&
|
||||
SDL_GL_ExtensionSupported( "GL_EXT_texture_compression_s3tc" ) )
|
||||
if ( ( QGLES_VERSION_ATLEAST( 2, 0 ) || GLimp_ExtensionSupported( "GL_ARB_texture_compression" ) ) &&
|
||||
GLimp_ExtensionSupported( "GL_EXT_texture_compression_s3tc" ) )
|
||||
{
|
||||
if ( r_ext_compressed_textures->value )
|
||||
{
|
||||
@@ -928,7 +1216,7 @@ static void GLimp_InitExtensions( qboolean fixedFunction )
|
||||
// GL_S3_s3tc ... legacy extension before GL_EXT_texture_compression_s3tc.
|
||||
if (glConfig.textureCompression == TC_NONE)
|
||||
{
|
||||
if ( SDL_GL_ExtensionSupported( "GL_S3_s3tc" ) )
|
||||
if ( GLimp_ExtensionSupported( "GL_S3_s3tc" ) )
|
||||
{
|
||||
if ( r_ext_compressed_textures->value )
|
||||
{
|
||||
@@ -951,7 +1239,7 @@ static void GLimp_InitExtensions( qboolean fixedFunction )
|
||||
{
|
||||
// GL_EXT_texture_env_add
|
||||
glConfig.textureEnvAddAvailable = qfalse;
|
||||
if ( SDL_GL_ExtensionSupported( "GL_EXT_texture_env_add" ) )
|
||||
if ( GLimp_ExtensionSupported( "GL_EXT_texture_env_add" ) )
|
||||
{
|
||||
if ( r_ext_texture_env_add->integer )
|
||||
{
|
||||
@@ -973,13 +1261,13 @@ static void GLimp_InitExtensions( qboolean fixedFunction )
|
||||
qglMultiTexCoord2fARB = NULL;
|
||||
qglActiveTextureARB = NULL;
|
||||
qglClientActiveTextureARB = NULL;
|
||||
if ( SDL_GL_ExtensionSupported( "GL_ARB_multitexture" ) )
|
||||
if ( GLimp_ExtensionSupported( "GL_ARB_multitexture" ) )
|
||||
{
|
||||
if ( r_ext_multitexture->value )
|
||||
{
|
||||
qglMultiTexCoord2fARB = SDL_GL_GetProcAddress( "glMultiTexCoord2fARB" );
|
||||
qglActiveTextureARB = SDL_GL_GetProcAddress( "glActiveTextureARB" );
|
||||
qglClientActiveTextureARB = SDL_GL_GetProcAddress( "glClientActiveTextureARB" );
|
||||
qglMultiTexCoord2fARB = GLimp_GetProcAddress( "glMultiTexCoord2fARB" );
|
||||
qglActiveTextureARB = GLimp_GetProcAddress( "glActiveTextureARB" );
|
||||
qglClientActiveTextureARB = GLimp_GetProcAddress( "glClientActiveTextureARB" );
|
||||
|
||||
if ( qglActiveTextureARB )
|
||||
{
|
||||
@@ -1010,13 +1298,28 @@ static void GLimp_InitExtensions( qboolean fixedFunction )
|
||||
}
|
||||
|
||||
// GL_EXT_compiled_vertex_array
|
||||
if ( SDL_GL_ExtensionSupported( "GL_EXT_compiled_vertex_array" ) )
|
||||
//
|
||||
// Declined on gl4es. This was first adopted on evidence that turned out
|
||||
// to be worthless - it was measured against a gl4es that could not draw
|
||||
// a triangle at all - so it is kept on its own merits, not on that.
|
||||
//
|
||||
// The merits are thin but real: the extension is a 1997 hint about
|
||||
// re-transforming shared vertices between draws, gl4es's glLockArrays
|
||||
// only records first and count and sets a flag, and it re-uploads the
|
||||
// arrays per batch regardless. There is nothing here to win.
|
||||
//
|
||||
// This no longer decides which route R_DrawElements takes. It used to,
|
||||
// by accident: a NULL qglLockArraysEXT sent every surface down the
|
||||
// glBegin/glArrayElement path, which is not a path the reference has
|
||||
// ever run. That renderer now asks for glDrawElements under gl4es
|
||||
// outright, the way RTCWQuest does under HAVE_GLES.
|
||||
if ( !usingGL4ES && GLimp_ExtensionSupported( "GL_EXT_compiled_vertex_array" ) )
|
||||
{
|
||||
if ( r_ext_compiled_vertex_array->value )
|
||||
{
|
||||
ri.Printf( PRINT_ALL, "...using GL_EXT_compiled_vertex_array\n" );
|
||||
qglLockArraysEXT = ( void ( APIENTRY * )( GLint, GLint ) ) SDL_GL_GetProcAddress( "glLockArraysEXT" );
|
||||
qglUnlockArraysEXT = ( void ( APIENTRY * )( void ) ) SDL_GL_GetProcAddress( "glUnlockArraysEXT" );
|
||||
qglLockArraysEXT = ( void ( APIENTRY * )( GLint, GLint ) ) GLimp_GetProcAddress( "glLockArraysEXT" );
|
||||
qglUnlockArraysEXT = ( void ( APIENTRY * )( void ) ) GLimp_GetProcAddress( "glUnlockArraysEXT" );
|
||||
if (!qglLockArraysEXT || !qglUnlockArraysEXT)
|
||||
{
|
||||
ri.Error (ERR_FATAL, "bad getprocaddress");
|
||||
@@ -1034,7 +1337,7 @@ static void GLimp_InitExtensions( qboolean fixedFunction )
|
||||
}
|
||||
|
||||
textureFilterAnisotropic = qfalse;
|
||||
if ( SDL_GL_ExtensionSupported( "GL_EXT_texture_filter_anisotropic" ) )
|
||||
if ( GLimp_ExtensionSupported( "GL_EXT_texture_filter_anisotropic" ) )
|
||||
{
|
||||
if ( r_ext_texture_filter_anisotropic->integer ) {
|
||||
qglGetIntegerv( GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, (GLint *)&maxAnisotropy );
|
||||
@@ -1059,7 +1362,7 @@ static void GLimp_InitExtensions( qboolean fixedFunction )
|
||||
}
|
||||
|
||||
haveClampToEdge = qfalse;
|
||||
if ( QGL_VERSION_ATLEAST( 1, 2 ) || QGLES_VERSION_ATLEAST( 1, 0 ) || SDL_GL_ExtensionSupported( "GL_SGIS_texture_edge_clamp" ) )
|
||||
if ( QGL_VERSION_ATLEAST( 1, 2 ) || QGLES_VERSION_ATLEAST( 1, 0 ) || GLimp_ExtensionSupported( "GL_SGIS_texture_edge_clamp" ) )
|
||||
{
|
||||
ri.Printf( PRINT_ALL, "...using GL_SGIS_texture_edge_clamp\n" );
|
||||
haveClampToEdge = qtrue;
|
||||
@@ -1088,7 +1391,12 @@ void GLimp_Init( qboolean fixedFunction )
|
||||
r_sdlDriver = ri.Cvar_Get( "r_sdlDriver", "", CVAR_ROM );
|
||||
r_allowResize = ri.Cvar_Get( "r_allowResize", "0", CVAR_ARCHIVE | CVAR_LATCH );
|
||||
r_centerWindow = ri.Cvar_Get( "r_centerWindow", "0", CVAR_ARCHIVE | CVAR_LATCH );
|
||||
#ifdef __ANDROID__
|
||||
// There is no desktop GL to fall back to, so don't leave it to detection
|
||||
r_preferOpenGLES = ri.Cvar_Get( "r_preferOpenGLES", "1", CVAR_ARCHIVE | CVAR_LATCH );
|
||||
#else
|
||||
r_preferOpenGLES = ri.Cvar_Get( "r_preferOpenGLES", "-1", CVAR_ARCHIVE | CVAR_LATCH );
|
||||
#endif
|
||||
|
||||
if( ri.Cvar_VariableIntegerValue( "com_abnormalExit" ) )
|
||||
{
|
||||
@@ -1187,10 +1495,18 @@ GLimp_EndFrame
|
||||
Responsible for doing a swapbuffers
|
||||
===============
|
||||
*/
|
||||
static qboolean glimp_presentsToWindow = qtrue;
|
||||
|
||||
void GLimp_SetPresentsToWindow( qboolean presents )
|
||||
{
|
||||
glimp_presentsToWindow = presents;
|
||||
}
|
||||
|
||||
void GLimp_EndFrame( void )
|
||||
{
|
||||
// don't flip if drawing to front buffer
|
||||
if ( Q_stricmp( r_drawBuffer->string, "GL_FRONT" ) != 0 )
|
||||
// don't flip if drawing to front buffer, or if the window is not what the
|
||||
// viewer is actually looking at
|
||||
if ( glimp_presentsToWindow && Q_stricmp( r_drawBuffer->string, "GL_FRONT" ) != 0 )
|
||||
{
|
||||
SDL_GL_SwapWindow( SDL_window );
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
#include "../client/client.h"
|
||||
#include "../sys/sys_local.h"
|
||||
#ifdef USE_OPENXR
|
||||
#include "../vr/vr_common.h"
|
||||
#endif
|
||||
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 17)
|
||||
#define KMOD_SCROLL KMOD_RESERVED
|
||||
@@ -1186,6 +1189,30 @@ static void IN_ProcessEvents( void )
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
// In a headset the window is not what is being
|
||||
// presented and its size decides nothing. The render
|
||||
// resolution comes from the runtime, through
|
||||
// refimport_t::GetVRRenderResolution, and the
|
||||
// r_custom* cvars set below are ignored - so this
|
||||
// restart achieves nothing at all except destroying
|
||||
// and rebuilding the GL context and the OpenXR
|
||||
// session underneath a running frame loop.
|
||||
//
|
||||
// It is not harmless. glconfig.vidWidth/Height is the
|
||||
// eye buffer, never the Android surface, so the
|
||||
// equality test below can never pass and the restart
|
||||
// fires every time the surface settles - about five
|
||||
// seconds in. After it the engine's 2D stopped
|
||||
// reaching the flat panel entirely: the menu was
|
||||
// drawing perfectly (256/256 panel samples) right up
|
||||
// to CL_Vid_Restart_f and drew nothing ever after.
|
||||
if( VR_Enabled() )
|
||||
{
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
// check if size actually changed
|
||||
if( cls.glconfig.vidWidth == width && cls.glconfig.vidHeight == height )
|
||||
{
|
||||
|
||||
@@ -25,6 +25,9 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
#include "../client/snd_public.h"
|
||||
#include "../client/client.h"
|
||||
#if defined(USE_OPENXR) && !defined(DEDICATED)
|
||||
#include "../vr/vr_common.h"
|
||||
#endif
|
||||
#include "../corepp/tiki.h"
|
||||
#include "../qcommon/localization.h"
|
||||
#include "../qcommon/crc.h"
|
||||
@@ -1734,6 +1737,26 @@ unsigned int PF_SV_Client_MaxPendingCommands(int clientNum)
|
||||
return SV_Client_GetMaxPendingCommands(svs.clients + clientNum);
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
SV_VR_GetWeaponAim
|
||||
|
||||
Hands the weapon hand's aim to the game, so a shot goes where the controller is
|
||||
pointing rather than where the player is looking.
|
||||
|
||||
Single player only, and deliberately so: the angles come from a headset attached
|
||||
to this process, and a dedicated server has none. It answers false there and the
|
||||
game keeps the view aim it already had.
|
||||
==================
|
||||
*/
|
||||
static qboolean SV_VR_GetWeaponAim( vec3_t out ) {
|
||||
#if defined(USE_OPENXR) && !defined(DEDICATED)
|
||||
return VR_GetWeaponAim( out );
|
||||
#else
|
||||
return qfalse;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
SV_InitGameProgs
|
||||
@@ -1950,6 +1973,8 @@ void SV_InitGameProgs( void ) {
|
||||
|
||||
import.Cvar_Find = Cvar_FindVar;
|
||||
|
||||
import.VR_GetWeaponAim = SV_VR_GetWeaponAim;
|
||||
|
||||
import.Client_NumPendingCommands = PF_SV_Client_NumPendingCommands;
|
||||
import.Client_MaxPendingCommands = PF_SV_Client_MaxPendingCommands;
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
===========================================================================
|
||||
Copyright (C) 2026 the OpenMoHAA team
|
||||
|
||||
This file is part of OpenMoHAA source code.
|
||||
|
||||
OpenMoHAA source code 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.
|
||||
|
||||
OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
===========================================================================
|
||||
*/
|
||||
|
||||
#include "../qcommon/q_shared.h"
|
||||
#include "../qcommon/qcommon.h"
|
||||
#include "sys_local.h"
|
||||
#include "sys_android.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/*
|
||||
There is no terminal to talk to, so the console goes to logcat instead.
|
||||
|
||||
Engine output arrives through CON_Print and is written directly. Anything
|
||||
that bypasses it and writes to stdout or stderr - the signal handler, the
|
||||
C library, third party code - would otherwise be discarded by Android, so
|
||||
those two descriptors are pumped into logcat as well. Losing that output
|
||||
is worse than it sounds: a diagnostic that never appears is indistinguishable
|
||||
from one that was never reached.
|
||||
*/
|
||||
|
||||
/*
|
||||
==================
|
||||
CON_FlushLine
|
||||
|
||||
logcat is line oriented, Com_Printf is not, so lines are assembled here and
|
||||
emitted one at a time.
|
||||
==================
|
||||
*/
|
||||
static char con_lineBuffer[1024];
|
||||
static size_t con_lineLength;
|
||||
|
||||
static void CON_FlushLine(void)
|
||||
{
|
||||
if (!con_lineLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
con_lineBuffer[con_lineLength] = '\0';
|
||||
Sys_AndroidLog(con_lineBuffer);
|
||||
con_lineLength = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
CON_StdioPump
|
||||
|
||||
Reads whatever was written to stdout/stderr and re-emits it, one line per
|
||||
log entry.
|
||||
==================
|
||||
*/
|
||||
static int con_stdioPipe[2] = {-1, -1};
|
||||
static pthread_t con_stdioThread;
|
||||
|
||||
static void *CON_StdioPump(void *arg)
|
||||
{
|
||||
char line[1024];
|
||||
size_t length = 0;
|
||||
char c;
|
||||
|
||||
while (read(con_stdioPipe[0], &c, 1) == 1) {
|
||||
if (c == '\n' || length == sizeof(line) - 1) {
|
||||
line[length] = '\0';
|
||||
Sys_AndroidLog(line);
|
||||
length = 0;
|
||||
} else if (c != '\r') {
|
||||
line[length++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
CON_Init
|
||||
==================
|
||||
*/
|
||||
void CON_Init(void)
|
||||
{
|
||||
if (pipe(con_stdioPipe) == -1) {
|
||||
Sys_AndroidLog("Failed to create the stdio pipe; stdout and stderr will be lost");
|
||||
return;
|
||||
}
|
||||
|
||||
// Unbuffered, so output shows up when it is written rather than when the
|
||||
// process happens to flush - which on a crash is never.
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
setvbuf(stderr, NULL, _IONBF, 0);
|
||||
|
||||
dup2(con_stdioPipe[1], STDOUT_FILENO);
|
||||
dup2(con_stdioPipe[1], STDERR_FILENO);
|
||||
|
||||
if (pthread_create(&con_stdioThread, NULL, CON_StdioPump, NULL) != 0) {
|
||||
Sys_AndroidLog("Failed to start the stdio pump; stdout and stderr will be lost");
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_detach(con_stdioThread);
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
CON_Shutdown
|
||||
==================
|
||||
*/
|
||||
void CON_Shutdown(void)
|
||||
{
|
||||
CON_FlushLine();
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
CON_Input
|
||||
==================
|
||||
*/
|
||||
char *CON_Input(void)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
CON_Print
|
||||
==================
|
||||
*/
|
||||
void CON_Print(const char *msg)
|
||||
{
|
||||
while (*msg) {
|
||||
if (Q_IsColorString(msg)) {
|
||||
// logcat has no colours, so drop the escape rather than print it
|
||||
msg += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (*msg == '\n') {
|
||||
CON_FlushLine();
|
||||
} else if (*msg != '\r') {
|
||||
con_lineBuffer[con_lineLength++] = *msg;
|
||||
|
||||
if (con_lineLength == sizeof(con_lineBuffer) - 1) {
|
||||
CON_FlushLine();
|
||||
}
|
||||
}
|
||||
|
||||
msg++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
===========================================================================
|
||||
Copyright (C) 2026 the OpenMoHAA team
|
||||
|
||||
This file is part of OpenMoHAA source code.
|
||||
|
||||
OpenMoHAA source code 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.
|
||||
|
||||
OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
===========================================================================
|
||||
*/
|
||||
|
||||
#include "../sys_local.h"
|
||||
#include "../sys_android.h"
|
||||
|
||||
#include <android/log.h>
|
||||
#include <dlfcn.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
#include <unwind.h>
|
||||
|
||||
#ifndef DEDICATED
|
||||
# ifdef USE_INTERNAL_SDL_HEADERS
|
||||
# include "SDL.h"
|
||||
# else
|
||||
# include <SDL.h>
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/*
|
||||
Android counterpart to sys_unix_new.c.
|
||||
|
||||
bionic has no <execinfo.h>, so the backtrace is walked with the unwinder
|
||||
directly and the addresses resolved through dladdr. This matters more here
|
||||
than on the desktop: tombstoned does not reliably put a backtrace in logcat,
|
||||
and when it does not, this is the only account of where the engine died.
|
||||
*/
|
||||
|
||||
/*
|
||||
==================
|
||||
Sys_AndroidLog
|
||||
==================
|
||||
*/
|
||||
void Sys_AndroidLog(const char *message)
|
||||
{
|
||||
__android_log_write(ANDROID_LOG_INFO, SYS_ANDROID_LOG_TAG, message);
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
Sys_PlatformInit_New
|
||||
==================
|
||||
*/
|
||||
void Sys_PlatformInit_New() {
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
Sys_AndroidSetupPaths
|
||||
==================
|
||||
*/
|
||||
void Sys_AndroidSetupPaths(void)
|
||||
{
|
||||
#ifndef DEDICATED
|
||||
const char *path = SDL_AndroidGetExternalStoragePath();
|
||||
char message[MAX_OSPATH + 64];
|
||||
|
||||
if (!path) {
|
||||
Com_sprintf(message, sizeof(message),
|
||||
"No external files directory (%s); the game data will not be found",
|
||||
SDL_GetError());
|
||||
Sys_AndroidLog(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// The engine resolves relative paths against the working directory, and an
|
||||
// Android process starts in "/" where it can neither read nor write.
|
||||
if (chdir(path) != 0) {
|
||||
Com_sprintf(message, sizeof(message), "Could not enter %s", path);
|
||||
Sys_AndroidLog(message);
|
||||
return;
|
||||
}
|
||||
|
||||
Sys_SetDefaultInstallPath(path);
|
||||
|
||||
Com_sprintf(message, sizeof(message), "Game data directory: %s", path);
|
||||
Sys_AndroidLog(message);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
Sys_PrepareBackTrace
|
||||
==================
|
||||
*/
|
||||
void Sys_PrepareBackTrace() {
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
Sys_UnwindCallback
|
||||
|
||||
Collects the program counter of each frame as the unwinder walks up the
|
||||
stack.
|
||||
==================
|
||||
*/
|
||||
typedef struct {
|
||||
void **frames;
|
||||
size_t count;
|
||||
size_t max;
|
||||
} backTraceState_t;
|
||||
|
||||
static _Unwind_Reason_Code Sys_UnwindCallback(struct _Unwind_Context *context, void *arg)
|
||||
{
|
||||
backTraceState_t *state = (backTraceState_t *)arg;
|
||||
uintptr_t pc = _Unwind_GetIP(context);
|
||||
|
||||
if (!pc) {
|
||||
return _URC_NO_REASON;
|
||||
}
|
||||
|
||||
if (state->count >= state->max) {
|
||||
return _URC_END_OF_STACK;
|
||||
}
|
||||
|
||||
state->frames[state->count++] = (void *)pc;
|
||||
return _URC_NO_REASON;
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
Sys_PrintBackTrace
|
||||
|
||||
Written straight to logcat rather than to stderr: this runs from the signal
|
||||
handler, and anything still sitting in a buffer when the process aborts is
|
||||
lost.
|
||||
==================
|
||||
*/
|
||||
void Sys_PrintBackTrace() {
|
||||
void *frames[128];
|
||||
backTraceState_t state;
|
||||
char line[1024];
|
||||
size_t i;
|
||||
|
||||
state.frames = frames;
|
||||
state.count = 0;
|
||||
state.max = ARRAY_LEN(frames);
|
||||
|
||||
_Unwind_Backtrace(Sys_UnwindCallback, &state);
|
||||
|
||||
for (i = 0; i < state.count; i++) {
|
||||
Dl_info info;
|
||||
const char *module = "?";
|
||||
const char *symbol = NULL;
|
||||
uintptr_t offset = 0;
|
||||
|
||||
if (dladdr(frames[i], &info) && info.dli_fname) {
|
||||
const char *slash = strrchr(info.dli_fname, '/');
|
||||
|
||||
module = slash ? slash + 1 : info.dli_fname;
|
||||
symbol = info.dli_sname;
|
||||
|
||||
if (info.dli_saddr) {
|
||||
offset = (uintptr_t)frames[i] - (uintptr_t)info.dli_saddr;
|
||||
}
|
||||
}
|
||||
|
||||
if (symbol) {
|
||||
Com_sprintf(line, sizeof(line), " #%02u %p %s %s+%u",
|
||||
(unsigned int)i, frames[i], module, symbol, (unsigned int)offset);
|
||||
} else {
|
||||
Com_sprintf(line, sizeof(line), " #%02u %p %s",
|
||||
(unsigned int)i, frames[i], module);
|
||||
}
|
||||
|
||||
Sys_AndroidLog(line);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
Sys_AndroidDescribeAddress
|
||||
|
||||
Names whatever the address falls inside, so a bare pointer becomes a library
|
||||
and a symbol.
|
||||
==================
|
||||
*/
|
||||
static void Sys_AndroidDescribeAddress(const void *address, char *out, size_t outSize)
|
||||
{
|
||||
Dl_info info;
|
||||
|
||||
if (dladdr(address, &info) && info.dli_fname) {
|
||||
const char *slash = strrchr(info.dli_fname, '/');
|
||||
const char *module = slash ? slash + 1 : info.dli_fname;
|
||||
|
||||
if (info.dli_sname && info.dli_saddr) {
|
||||
Com_sprintf(out, outSize, "%p %s %s+%u", address, module, info.dli_sname,
|
||||
(unsigned int)((uintptr_t)address - (uintptr_t)info.dli_saddr));
|
||||
return;
|
||||
}
|
||||
|
||||
Com_sprintf(out, outSize, "%p %s+%u", address, module,
|
||||
(unsigned int)((uintptr_t)address - (uintptr_t)info.dli_fbase));
|
||||
return;
|
||||
}
|
||||
|
||||
Com_sprintf(out, outSize, "%p <unmapped>", address);
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
Sys_AndroidCrashHandler
|
||||
|
||||
Reports the fault before handing over to the engine's own handler. The register
|
||||
state is the part that matters: the unwinder stops at the signal frame, so the
|
||||
program counter carried in the context is the only reliable account of where
|
||||
the process actually died.
|
||||
==================
|
||||
*/
|
||||
static void Sys_AndroidCrashHandler(int signum, siginfo_t *info, void *contextPtr)
|
||||
{
|
||||
char line[512];
|
||||
char described[256];
|
||||
|
||||
Com_sprintf(line, sizeof(line), "Fatal signal %d at address %p", signum,
|
||||
info ? info->si_addr : NULL);
|
||||
Sys_AndroidLog(line);
|
||||
|
||||
#if defined(__aarch64__)
|
||||
if (contextPtr) {
|
||||
const ucontext_t *context = (const ucontext_t *)contextPtr;
|
||||
|
||||
Sys_AndroidDescribeAddress((const void *)context->uc_mcontext.pc, described, sizeof(described));
|
||||
Com_sprintf(line, sizeof(line), " pc %s", described);
|
||||
Sys_AndroidLog(line);
|
||||
|
||||
// x30 is the link register: where the faulting function would have
|
||||
// returned to, which identifies the caller when pc is in a stripped or
|
||||
// JIT-generated region.
|
||||
Sys_AndroidDescribeAddress((const void *)context->uc_mcontext.regs[30], described, sizeof(described));
|
||||
Com_sprintf(line, sizeof(line), " lr %s", described);
|
||||
Sys_AndroidLog(line);
|
||||
}
|
||||
#endif
|
||||
|
||||
Sys_SigHandler(signum);
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
Sys_AndroidInstallCrashHandler
|
||||
==================
|
||||
*/
|
||||
void Sys_AndroidInstallCrashHandler(void)
|
||||
{
|
||||
static char signalStack[SIGSTKSZ * 4];
|
||||
const int signals[] = { SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGABRT };
|
||||
struct sigaction action;
|
||||
stack_t altStack;
|
||||
size_t i;
|
||||
|
||||
// A stack overflow faults again the moment the handler pushes a frame, and
|
||||
// the second fault is delivered as an unrecoverable kill with nothing
|
||||
// logged. Give the handler its own stack so it survives to report.
|
||||
altStack.ss_sp = signalStack;
|
||||
altStack.ss_size = sizeof(signalStack);
|
||||
altStack.ss_flags = 0;
|
||||
sigaltstack(&altStack, NULL);
|
||||
|
||||
memset(&action, 0, sizeof(action));
|
||||
action.sa_sigaction = Sys_AndroidCrashHandler;
|
||||
action.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESETHAND;
|
||||
sigemptyset(&action.sa_mask);
|
||||
|
||||
for (i = 0; i < ARRAY_LEN(signals); i++) {
|
||||
sigaction(signals[i], &action, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
==============
|
||||
Sys_DebugPrint
|
||||
==============
|
||||
*/
|
||||
void Sys_DebugPrint(const char* message) {
|
||||
Sys_AndroidLog(message);
|
||||
}
|
||||
|
||||
/*
|
||||
==============
|
||||
Sys_PumpMessageLoop
|
||||
==============
|
||||
*/
|
||||
void Sys_PumpMessageLoop(void)
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
SetNormalThreadPriority
|
||||
==================
|
||||
*/
|
||||
void SetNormalThreadPriority(void)
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
SetBelowNormalThreadPriority
|
||||
==================
|
||||
*/
|
||||
void SetBelowNormalThreadPriority(void)
|
||||
{
|
||||
}
|
||||
@@ -32,6 +32,15 @@ static void* clipboard_text = NULL;
|
||||
|
||||
static void* game_library = NULL;
|
||||
static void* cgame_library = NULL;
|
||||
|
||||
#ifdef __ANDROID__
|
||||
// The modules ship inside the APK, where they sit on the loader's own search
|
||||
// path rather than beside a binary or under fs_basepath.
|
||||
#define GAME_MODULE_USE_SYSTEM_PATH qtrue
|
||||
#else
|
||||
#define GAME_MODULE_USE_SYSTEM_PATH qfalse
|
||||
#endif
|
||||
|
||||
qboolean GLimp_SpawnRenderThread(void (*function)(void))
|
||||
{
|
||||
return qfalse;
|
||||
@@ -238,12 +247,12 @@ Sys_GetGameAPI
|
||||
void* Sys_GetGameAPI(void* parms)
|
||||
{
|
||||
void* (*GetGameAPI) (void*);
|
||||
const char* gamename = "game" DLL_SUFFIX DLL_EXT;
|
||||
const char* gamename = DLL_PREFIX "game" DLL_SUFFIX DLL_EXT;
|
||||
|
||||
if (game_library)
|
||||
Com_Error(ERR_FATAL, "Sys_GetGameAPI without calling Sys_UnloadGame");
|
||||
|
||||
game_library = Sys_LoadDll(gamename, qfalse);
|
||||
game_library = Sys_LoadDll(gamename, GAME_MODULE_USE_SYSTEM_PATH);
|
||||
|
||||
//Still couldn't find it.
|
||||
if (!game_library) {
|
||||
@@ -287,12 +296,12 @@ Sys_GetCGameAPI
|
||||
void* Sys_GetCGameAPI(void* parms)
|
||||
{
|
||||
void* (*GetCGameAPI) (void*);
|
||||
const char* gamename = "cgame" DLL_SUFFIX DLL_EXT;
|
||||
const char* gamename = DLL_PREFIX "cgame" DLL_SUFFIX DLL_EXT;
|
||||
|
||||
if (cgame_library)
|
||||
Com_Error(ERR_FATAL, "Sys_GetCGameAPI without calling Sys_UnloadCGame");
|
||||
|
||||
cgame_library = Sys_LoadDll(gamename, qfalse);
|
||||
cgame_library = Sys_LoadDll(gamename, GAME_MODULE_USE_SYSTEM_PATH);
|
||||
|
||||
//Still couldn't find it.
|
||||
if (!cgame_library) {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
===========================================================================
|
||||
Copyright (C) 2026 the OpenMoHAA team
|
||||
|
||||
This file is part of OpenMoHAA source code.
|
||||
|
||||
OpenMoHAA source code 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.
|
||||
|
||||
OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
===========================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// The tag every log entry is filed under; `adb logcat -s openmohaa` shows
|
||||
// only ours.
|
||||
#define SYS_ANDROID_LOG_TAG "openmohaa"
|
||||
|
||||
// Writes a single line to logcat. Safe to call before the engine has started
|
||||
// and from a signal handler.
|
||||
void Sys_AndroidLog(const char *message);
|
||||
|
||||
// Installs handlers for the fatal signals that report the faulting address and
|
||||
// program counter. The generic Unix handler cannot: it is installed with
|
||||
// signal(), so it never sees siginfo, and _Unwind_Backtrace cannot walk out of
|
||||
// the kernel's signal trampoline - which leaves a crash reported as four frames
|
||||
// of the handler itself and nothing about where the fault actually happened.
|
||||
void Sys_AndroidInstallCrashHandler(void);
|
||||
|
||||
// Points the install path at the app's external files directory and makes it
|
||||
// the working directory. Must run before Com_Init, which is where FS_Startup
|
||||
// reads the install path. A no-op in the dedicated server, which has no
|
||||
// activity to ask.
|
||||
void Sys_AndroidSetupPaths(void);
|
||||
+25
-3
@@ -48,6 +48,10 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
#include "sys_local.h"
|
||||
#include "sys_loadlib.h"
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include "sys_android.h"
|
||||
#endif
|
||||
|
||||
#include "../qcommon/q_shared.h"
|
||||
#include "../qcommon/qcommon.h"
|
||||
|
||||
@@ -835,6 +839,14 @@ int main( int argc, char **argv )
|
||||
Sys_SetBinaryPath( Sys_Dirname( argv[ 0 ] ) );
|
||||
Sys_SetDefaultInstallPath( DEFAULT_BASEDIR );
|
||||
|
||||
#ifdef __ANDROID__
|
||||
// There is no meaningful binary path on Android - argv[0] is "app_process"
|
||||
// and the process starts in "/". Point the engine at the app's own external
|
||||
// files directory instead, which is where the user side-loads the game data
|
||||
// and the only place it can also write.
|
||||
Sys_AndroidSetupPaths();
|
||||
#endif
|
||||
|
||||
// Concatenate the command line for passing to Com_Init
|
||||
for( i = 1; i < argc; i++ )
|
||||
{
|
||||
@@ -875,16 +887,26 @@ int main( int argc, char **argv )
|
||||
#endif
|
||||
|
||||
CON_Init( );
|
||||
Com_Init( commandLine );
|
||||
Sys_InitEx(); // Added in OPM
|
||||
NET_Init( );
|
||||
|
||||
// Installed before Com_Init rather than after it: startup is exactly when
|
||||
// renderer and filesystem bring-up crash on a new platform, and a fault
|
||||
// there used to die with no backtrace at all.
|
||||
#ifdef __ANDROID__
|
||||
// Installs the fatal signals itself, with siginfo and a private stack so a
|
||||
// crash can report where it happened rather than only that it happened.
|
||||
Sys_AndroidInstallCrashHandler();
|
||||
#else
|
||||
signal( SIGILL, Sys_SigHandler );
|
||||
signal( SIGFPE, Sys_SigHandler );
|
||||
signal( SIGSEGV, Sys_SigHandler );
|
||||
#endif
|
||||
signal( SIGTERM, Sys_SigHandler );
|
||||
signal( SIGINT, Sys_SigHandler );
|
||||
|
||||
Com_Init( commandLine );
|
||||
Sys_InitEx(); // Added in OPM
|
||||
NET_Init( );
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_set_main_loop( Com_Frame, 0, 1 );
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
===========================================================================
|
||||
Copyright (C) 2026 the OpenMoHAA team
|
||||
|
||||
This file is part of OpenMoHAA source code.
|
||||
|
||||
OpenMoHAA source code 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.
|
||||
|
||||
OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
===========================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../qcommon/q_shared.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define VR_EYE_LEFT 0
|
||||
#define VR_EYE_RIGHT 1
|
||||
#define VR_MAX_EYES 2
|
||||
|
||||
/*
|
||||
The headset's view of a frame.
|
||||
|
||||
Angles are the engine's (pitch, yaw, roll) in degrees; origin is in engine
|
||||
units. The four tangents describe the eye's frustum: OpenXR gives an
|
||||
asymmetric field of view per eye, and using a symmetric approximation instead
|
||||
misaligns the two images enough to be uncomfortable, so they are carried
|
||||
through to the projection matrix rather than reduced to a single fov value.
|
||||
*/
|
||||
typedef struct {
|
||||
vec3_t origin;
|
||||
|
||||
// Forward, left and up, in the engine's convention - the same layout as
|
||||
// refdef_t's viewaxis. Kept as axes rather than pitch/yaw/roll because the
|
||||
// round trip through Euler angles loses the distinction between rotations
|
||||
// that differ only near the poles, and reintroduces it as a twist.
|
||||
vec3_t axis[3];
|
||||
|
||||
float tanLeft;
|
||||
float tanRight;
|
||||
float tanUp;
|
||||
float tanDown;
|
||||
} vrEyeView_t;
|
||||
|
||||
// Brings up the OpenXR instance and picks a system. Must run before the GL
|
||||
// context exists, and returns false if there is no headset or no runtime, in
|
||||
// which case the engine carries on as an ordinary flat application.
|
||||
qboolean VR_Init(void);
|
||||
void VR_Shutdown(void);
|
||||
|
||||
// True once the instance is up; the engine uses this to decide whether to
|
||||
// render in stereo at all.
|
||||
qboolean VR_Enabled(void);
|
||||
|
||||
// The session and its swapchains are bound to the GL context that is current
|
||||
// when they are created, so both are torn down and rebuilt whenever the engine
|
||||
// restarts the renderer.
|
||||
void VR_CreateSession(void);
|
||||
void VR_DestroySession(void);
|
||||
|
||||
// The per-eye render target size the runtime asks for.
|
||||
void VR_GetRenderResolution(int *width, int *height);
|
||||
|
||||
// Begins a frame and updates the tracked poses. Returns false when the session
|
||||
// is not running, meaning nothing should be rendered or submitted this frame.
|
||||
qboolean VR_BeginFrame(void);
|
||||
|
||||
// Bracket each eye's rendering. Between them the engine draws its scene into
|
||||
// the framebuffer VR_PrepareEye bound.
|
||||
void VR_PrepareEye(int eye);
|
||||
void VR_FinishEye(int eye);
|
||||
|
||||
// True when the frame is flat content - menus, the loading screen, a cinematic
|
||||
// - which is shown on a quad fixed in front of the viewer rather than rendered
|
||||
// into both eyes. Drawing screen space content identically into two eyes gives
|
||||
// it zero disparity while each eye still reprojects through its own asymmetric
|
||||
// frustum, so the two images never line up and the result cannot be fused.
|
||||
qboolean VR_UseScreenLayer(void);
|
||||
|
||||
// Bracket the single pass that draws the flat content, in place of the two eye
|
||||
// passes.
|
||||
void VR_PrepareScreenLayer(void);
|
||||
void VR_FinishScreenLayer(void);
|
||||
|
||||
// The panel on the player's wrist, which carries the HUD and any in game menu
|
||||
// while the world is being drawn behind it. Shown by raising the off hand and
|
||||
// turning it towards the face, the way a watch is read.
|
||||
qboolean VR_WristPanelEnabled(void);
|
||||
qboolean VR_WristPanelVisible(void);
|
||||
void VR_PrepareWristPanel(void);
|
||||
void VR_FinishWristPanel(void);
|
||||
|
||||
// Hands the finished images to the compositor.
|
||||
void VR_SubmitFrame(void);
|
||||
|
||||
// Where the headset is, for the frame that VR_BeginFrame set up.
|
||||
void VR_GetEyeView(int eye, vrEyeView_t *view);
|
||||
|
||||
// Reads the controllers and, while flat content is on screen, points the
|
||||
// engine's cursor at wherever the player is aiming on the panel.
|
||||
void VR_UpdateInput(void);
|
||||
|
||||
/*
|
||||
What the player is doing, for the client to turn into a usercmd.
|
||||
*/
|
||||
typedef struct {
|
||||
qboolean valid;
|
||||
|
||||
float moveForward; // -1..1, left stick
|
||||
float moveRight;
|
||||
float turn; // -1..1, right stick
|
||||
|
||||
// How far the head moved this frame, in engine units, in the play space's
|
||||
// own frame. Fed into the usercmd as movement so that walking about the room
|
||||
// takes the character with it - see CL_VRMove.
|
||||
float stepForward;
|
||||
float stepRight;
|
||||
|
||||
float headYaw; // degrees, relative to where they started
|
||||
float headPitch;
|
||||
|
||||
// Where each hand points, same reference as the head. The off hand steers
|
||||
// walking so the player can look around without veering.
|
||||
float offhandYaw;
|
||||
float weaponYaw;
|
||||
float weaponPitch;
|
||||
|
||||
// Separately, because they are used for different things and either hand can
|
||||
// drop out on its own - the off hand steering walking must not stop working
|
||||
// because the weapon hand went out of view.
|
||||
qboolean offhandTracked;
|
||||
qboolean weaponTracked;
|
||||
} vrInput_t;
|
||||
|
||||
qboolean VR_GetInput(vrInput_t *input);
|
||||
|
||||
/*
|
||||
Where the weapon hand is pointing, for the game to fire along.
|
||||
|
||||
Not an absolute direction. The controller's own yaw is meaningless to the game -
|
||||
the player can turn on the spot with the stick, and the play space has an
|
||||
arbitrary forward - so what travels is how far the hand *leads the head*, which
|
||||
the game then adds to the view yaw it already has. That is what RTCWQuest does
|
||||
(rtcw/src/game/g_weapon.c:1904):
|
||||
|
||||
viewang[YAW] = ent->client->ps.viewangles[YAW]
|
||||
+ (gVR->weaponangles[YAW] - gVR->hmdorientation[YAW]);
|
||||
|
||||
out[PITCH] the controller's pitch, absolute - the head contributes nothing
|
||||
to it, and blending the two would fight the player's own wrist
|
||||
out[YAW] degrees the hand leads the head by
|
||||
out[ROLL] unused; roll does not change a forward vector
|
||||
|
||||
False when there is no headset, no tracked weapon hand, or VR is off - the
|
||||
caller must keep its existing aim in that case rather than firing at zero.
|
||||
*/
|
||||
qboolean VR_GetWeaponAim(vec3_t out);
|
||||
|
||||
/*
|
||||
Where the weapon hand is, for the view model to hang off.
|
||||
|
||||
Reports the weapon hand in exactly the frame VR_GetHandPose reports the other
|
||||
one - measured from the tracking origin, not from the head - so both hands are
|
||||
composed the same way and the long note below applies to both. What is different
|
||||
is only the angles: these carry the weapon's aim, so the two handed hold has
|
||||
already had its say on them, which is right for the thing being held and wrong
|
||||
for the hand that is not holding it.
|
||||
|
||||
The reference keeps its equivalent (calculated_weaponoffset) relative to the
|
||||
head, but then rotates it and replaces its height in convertFromVR and the -64
|
||||
rebase, which comes to the same place by a longer road. Measuring from the
|
||||
tracking origin does it in one step. What must never happen is half of each: a
|
||||
head-relative offset composed as though it were play-space puts the hand at
|
||||
vieworg + R*(hand - head) while the camera is at vieworg + R*head, so the head's
|
||||
own movement is counted twice and every hand movement comes out amplified.
|
||||
*/
|
||||
qboolean VR_GetWeaponPose(vec3_t trackingOffset, vec3_t angles, float *baseYaw);
|
||||
|
||||
/*
|
||||
Where one hand is, for something to be drawn on it. hand is 0 for the left and
|
||||
1 for the right, which is the order OpenXR's subaction paths are set up in here
|
||||
and has nothing to do with which one holds the weapon.
|
||||
|
||||
trackingOffset is the hand measured from the **tracking origin**, in engine units
|
||||
and the engine's frame - not from the head. That is deliberate and it is the
|
||||
whole reason this is not shaped like VR_GetWeaponPose.
|
||||
|
||||
The renderer does not take its camera from cg.refdef.vieworg as cgame leaves it.
|
||||
R_VRComposeView adds the head's play space offset to it, rotated into the body's
|
||||
frame (tr_vr.c). So the camera the player actually looks through is
|
||||
|
||||
vieworg + bodyAxis * headPlaySpaceOffset
|
||||
|
||||
and cgame never sees the second term. Anything anchored on cg.refdef.vieworg
|
||||
alone is therefore displaced by exactly the head's offset within the play space -
|
||||
and since turning the head swings it about the neck, every look around moves it.
|
||||
|
||||
Reporting the hand in the play space lets the caller compose it the same way the
|
||||
renderer composes the camera, which puts the two in the same place by
|
||||
construction:
|
||||
|
||||
bodyAngles = (0, viewYaw - baseYaw, 0)
|
||||
handWorld = vieworg + bodyAxis * trackingOffset
|
||||
|
||||
baseYaw is how much of the head's heading the client has already written into the
|
||||
view angles; subtracting it is what leaves the body's frame rather than the
|
||||
head's. It is out here because cgame has no other way to know it.
|
||||
|
||||
angles carry the controller's own pitch and roll, and in YAW how far the hand
|
||||
leads the head. It is the *hand's* pose: the two handed hold and the per weapon
|
||||
adjustment move the weapon, not the hand that is not holding it.
|
||||
|
||||
False when there is no headset or that hand is not tracked - a controller that
|
||||
has been put down keeps its last drawn position rather than snapping to the
|
||||
player's feet, so the caller should skip drawing instead of using zeroes.
|
||||
*/
|
||||
qboolean VR_GetHandPose(int hand, vec3_t trackingOffset, vec3_t angles, float *baseYaw);
|
||||
|
||||
// How much of the head's heading the client has already written into the game's
|
||||
// view angles. The renderer takes it back off before composing the headset onto
|
||||
// the game's camera, so the same rotation is not applied twice.
|
||||
void VR_SetBaseYaw(float yaw);
|
||||
|
||||
// The renderer's own split of the frame, for the timing report: how long was
|
||||
// spent building the scene against how long was spent issuing it.
|
||||
void VR_TraceRenderTimes(int frontEndMsec, int backEndMsec);
|
||||
|
||||
// How long the game spent building the frame, against how long the renderer
|
||||
// spent issuing it. The first covers everything cgame does per frame, which is
|
||||
// far more than drawing.
|
||||
void VR_TraceSceneTimes(int sceneMsec, int issueMsec);
|
||||
|
||||
// Inside the scene: the 3D half against the flat half. Splits cgame's own per
|
||||
// frame work from the 2D path, which flushes the render command buffer once per
|
||||
// string drawn and is a suspect in its own right.
|
||||
void VR_TraceViewTimes(int worldMsec, int hudMsec);
|
||||
|
||||
// Of the flat half, how much is the game's own HUD as against the engine's
|
||||
// overlays around it.
|
||||
void VR_TraceHudTimes(int cgameMsec);
|
||||
|
||||
// Counters for the paths that draw the flat content, so a path that is never
|
||||
// reached can be told apart from one that is reached and draws nothing.
|
||||
#define VRTRACE_DRAW2D 0 // View3D::Draw2D entered
|
||||
#define VRTRACE_CGAME_HUD 1 // cge->CG_Draw2D entered
|
||||
#define VRTRACE_UPDATEVIEWS 2 // uWinMan.UpdateViews entered
|
||||
#define VRTRACE_VIEW3D 3 // View3D::Draw entered
|
||||
#define VRTRACE_COUNT 4
|
||||
void VR_TraceEvent(int which);
|
||||
|
||||
// The client's idea of the frame, so the report can say what mode it was in.
|
||||
void VR_TraceState(int drawMode, int hudPass, int noMenus);
|
||||
|
||||
// The flat pass broken down further: the fade and letterbox overlays, the
|
||||
// centre print, and the sound/net/subtitle overlays, against the set2D that
|
||||
// precedes them all.
|
||||
void VR_TraceHudParts(int setup, int fades, int prints, int overlays, int tail);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
+4703
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,201 @@
|
||||
# Building for Android
|
||||
|
||||
This covers building OpenMoHAA as an Android APK. It targets `arm64-v8a` and
|
||||
was brought up against a Meta Quest 3, but nothing here is headset specific —
|
||||
a phone or tablet builds the same way.
|
||||
|
||||
VR itself is not part of this. What you get is the ordinary game running as a
|
||||
normal Android app.
|
||||
|
||||
|
||||
> **The gl4es boundary.** On Android the fixed function renderer runs on top of
|
||||
> gl4es, which keeps its own copy of GL state that can disagree with the driver.
|
||||
> If you are debugging anything that looks like *the geometry is correct and
|
||||
> nothing is drawn*, read [the gl4es boundary](04-gl4es.md) first.
|
||||
|
||||
## What you need
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Android NDK | r26 or later |
|
||||
| Android SDK | platform 34, build-tools 34 |
|
||||
| JDK | 17 (required by the Android Gradle Plugin) |
|
||||
| Gradle | 8.0 or later |
|
||||
| CMake | 3.25 or later |
|
||||
| flex, bison | on the host, to generate the script parser |
|
||||
|
||||
Set `ANDROID_NDK_HOME` and `ANDROID_HOME` before you start.
|
||||
|
||||
## 1. Build the engine
|
||||
|
||||
Gradle does not drive the native build; CMake does, and gradle only packages
|
||||
the result. Configure with the NDK toolchain file:
|
||||
|
||||
```sh
|
||||
cmake -B build-android-arm64 -G Ninja \
|
||||
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
|
||||
-DANDROID_ABI=arm64-v8a \
|
||||
-DANDROID_PLATFORM=android-29 \
|
||||
-DANDROID_STL=c++_shared \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SERVER=OFF
|
||||
|
||||
cmake --build build-android-arm64 -j$(nproc)
|
||||
```
|
||||
|
||||
`ANDROID_STL=c++_shared` matters: the APK ships several libraries that use the
|
||||
C++ standard library, and they have to share one copy of it.
|
||||
|
||||
SDL2, OpenAL Soft and gl4es have no Android binaries to fall back on, so all
|
||||
are downloaded and built from source. To build against trees you already have,
|
||||
pass `-DSDL2_SOURCE_PATH=...`, `-DOPENAL_SOURCE_PATH=...` and
|
||||
`-DGL4ES_SOURCE_PATH=...`.
|
||||
|
||||
Several defaults change on Android, in `cmake/platforms/android.cmake`:
|
||||
|
||||
- **`renderergl1` is the renderer**, with **`USE_GL4ES=ON`** underneath it.
|
||||
renderergl1 is fixed function, which is what this game's art was authored
|
||||
for, and gl4es translates its OpenGL 1.x calls to the OpenGL ES the device
|
||||
actually has. renderergl2 speaks GLES directly and was the first thing to run
|
||||
here, but it spends tens of milliseconds of CPU per eye binding GLSL programs
|
||||
for lighting this content has no maps to feed. It is still reachable with
|
||||
`-DBUILD_RENDERER_GL1=OFF -DBUILD_RENDERER_GL2=ON -DUSE_GL4ES=OFF`.
|
||||
- **`USE_RENDERER_DLOPEN=OFF`** — the renderer is linked into the client
|
||||
rather than loaded from a path the Android linker may object to.
|
||||
- **`USE_HTTP=OFF`** — only used for downloading content from a server.
|
||||
- **`BUILD_GAME_QVMS=OFF`** — the QVM toolchain is not part of this build.
|
||||
|
||||
The build stages everything the APK needs into
|
||||
`build-android-arm64/apk-libs/arm64-v8a/`.
|
||||
|
||||
## 2. Build the APK
|
||||
|
||||
```sh
|
||||
cd misc/android
|
||||
gradle assembleDebug
|
||||
```
|
||||
|
||||
The result is `misc/android/app/build/outputs/apk/debug/app-debug.apk`.
|
||||
|
||||
If your build tree is somewhere else, point gradle at it:
|
||||
|
||||
```sh
|
||||
gradle assembleDebug -PnativeLibsDir=/path/to/build/apk-libs
|
||||
```
|
||||
|
||||
Gradle picks up SDL's `org.libsdl.app` Java classes from the SDL tree CMake
|
||||
built against — it reads the path out of `android-build.properties` in the
|
||||
build directory. They have to match the `libSDL2.so` that was linked, so do
|
||||
not point them at a different SDL.
|
||||
|
||||
## 3. Install and add the game data
|
||||
|
||||
```sh
|
||||
adb install -r misc/android/app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
No game data ships with the app. It goes in the app's own external files
|
||||
directory, which is also where the engine writes its config and saves:
|
||||
|
||||
```
|
||||
/sdcard/Android/data/org.openmoh.openmohaa/files/
|
||||
```
|
||||
|
||||
Copy your retail `main/` directory there:
|
||||
|
||||
```sh
|
||||
adb shell mkdir -p /sdcard/Android/data/org.openmoh.openmohaa/files/main
|
||||
adb push main/pak0.pk3 /sdcard/Android/data/org.openmoh.openmohaa/files/main/
|
||||
```
|
||||
|
||||
### Directory permissions
|
||||
|
||||
`adb push` **cannot create directories** under an app's data directory — it
|
||||
fails with `secure_mkdirs failed: Operation not permitted` partway through the
|
||||
transfer. Create them first, in one go:
|
||||
|
||||
```sh
|
||||
cd /path/to/mohaa/main
|
||||
adb shell mkdir -p $(find . -type d | sed 's|^\./|/sdcard/Android/data/org.openmoh.openmohaa/files/main/|' | tr '\n' ' ')
|
||||
adb push --sync . /sdcard/Android/data/org.openmoh.openmohaa/files/main/
|
||||
adb shell chmod -R 775 /sdcard/Android/data/org.openmoh.openmohaa/files
|
||||
```
|
||||
|
||||
`chmod` has to come *after* `adb push`, or push fails with
|
||||
`remote fchown failed`.
|
||||
|
||||
**Only push directories the game reads.** A directory created by
|
||||
`adb shell mkdir` belongs to `shell`, not to the app, and no amount of
|
||||
chmod'ing changes that — 775 lets the app *read* through it, but the app is
|
||||
`other` there and still cannot *write*. Pre-creating `main/configs` or
|
||||
`main/save` therefore leaves the game unable to save its config or your
|
||||
progress, reporting only `Couldn't write omconfig.cfg`. Leave those two out
|
||||
and the engine creates them itself, owned by the app and writable:
|
||||
|
||||
```sh
|
||||
adb shell rm -rf /sdcard/Android/data/org.openmoh.openmohaa/files/main/configs \
|
||||
/sdcard/Android/data/org.openmoh.openmohaa/files/main/save
|
||||
```
|
||||
|
||||
The failure mode to watch for is silence: files pushed by adb are 644 and
|
||||
world readable, so game data loads perfectly while writes fail, which reads
|
||||
as "the data is fine".
|
||||
|
||||
## 4. Run it
|
||||
|
||||
Launch it from the device. Do not use `adb shell am start`; on a Quest it
|
||||
intermittently strands the headset in Meta's loading environment.
|
||||
|
||||
## Logging
|
||||
|
||||
Everything the engine prints goes to logcat under the `openmohaa` tag:
|
||||
|
||||
```sh
|
||||
adb logcat -s openmohaa
|
||||
```
|
||||
|
||||
`stdout` and `stderr` are redirected there too, so output from the C library
|
||||
and from third party code is not lost.
|
||||
|
||||
A crash reports its own stack: bionic has no `<execinfo.h>`, so the backtrace
|
||||
is walked with `_Unwind_Backtrace` and resolved through `dladdr`
|
||||
(`code/sys/new/sys_android_new.c`). This matters because `tombstoned` does not
|
||||
reliably put a backtrace in logcat, and when it does not, that handler is the
|
||||
only account of where the engine died. When even that is silent,
|
||||
`adb shell dumpsys activity exit-info` still reports the signal.
|
||||
|
||||
Verify that logging works *before* you rely on it. A crash handler that has
|
||||
been quietly displaced reports nothing, which is indistinguishable from a
|
||||
crash that never happened.
|
||||
|
||||
## How it fits together
|
||||
|
||||
`SDLActivity` loads `libSDL2.so` and `libopenmohaa.so`, then calls `SDL_main`
|
||||
in the latter — which is the engine's `main`, renamed by `SDL_main.h`. The
|
||||
engine then `dlopen`s the game modules by name.
|
||||
|
||||
Those modules ship inside the APK as `libgame.so` and `libcgame.so` rather
|
||||
than beside the game data. The prefix is not cosmetic: Android extracts only
|
||||
`lib*.so` out of an APK, and having them in the native library directory means
|
||||
the loader finds them by name, with no path to get wrong. `DLL_PREFIX` in
|
||||
`q_platform.h` is what keeps the engine's name for them in step.
|
||||
|
||||
| Library | Loaded by |
|
||||
|---|---|
|
||||
| `libSDL2.so` | `SDLActivity`, by name |
|
||||
| `libopenmohaa.so` | `SDLActivity`, then `SDL_main` is called in it |
|
||||
| `libopenal.so`, `libc++_shared.so` | the linker, as dependencies |
|
||||
| `libgame.so`, `libcgame.so` | the engine, at runtime |
|
||||
|
||||
## Dedicated server
|
||||
|
||||
The server builds too, and needs neither SDL nor a renderer:
|
||||
|
||||
```sh
|
||||
cmake -B build-android-server \
|
||||
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
|
||||
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-29 \
|
||||
-DBUILD_SERVER=ON -DBUILD_CLIENT=OFF
|
||||
```
|
||||
|
||||
It produces an ordinary `omohaaded` executable rather than an APK.
|
||||
@@ -0,0 +1,177 @@
|
||||
# The gl4es boundary
|
||||
|
||||
On Android the fixed function renderer (`renderergl1`) does not talk to the GPU.
|
||||
It talks to [gl4es](https://github.com/ptitSeb/gl4es), which translates desktop
|
||||
OpenGL 1.x into OpenGL ES. The engine never learns this: gl4es reports itself as
|
||||
`2.1 gl4es wrapper 1.1.6`, so the renderer takes its desktop path and gl4es
|
||||
turns `glBegin`, `glMatrixMode` and the rest into ES underneath.
|
||||
|
||||
That works. What is not obvious, and what has now cost several days of debugging
|
||||
across separate sessions, is that **gl4es keeps its own copy of GL state, and
|
||||
that copy can disagree with the driver.** Everything below follows from that one
|
||||
fact.
|
||||
|
||||
Read this before debugging anything that looks like "the geometry is correct and
|
||||
nothing is drawn".
|
||||
|
||||
---
|
||||
|
||||
## 1. gl4es answers state queries from its own tables
|
||||
|
||||
`glGetIntegerv(GL_CULL_FACE_MODE)`, `GL_FRONT_FACE`, `GL_MODELVIEW_MATRIX` and
|
||||
friends are answered out of gl4es's `glstate`, not by asking the driver
|
||||
(`src/gl/getter.c`). `glGetString` is the same - `GL_VERSION`, `GL_VENDOR` and
|
||||
`GL_RENDERER` come from gl4es's constants without the driver being consulted.
|
||||
|
||||
So this proves nothing:
|
||||
|
||||
```c
|
||||
qglGetIntegerv( GL_CULL_FACE_MODE, &mode );
|
||||
/* compare mode against what the engine set */
|
||||
```
|
||||
|
||||
The engine set that value *through gl4es*. Reading it back through gl4es returns
|
||||
the engine's own value. It agrees by construction whatever the hardware is doing.
|
||||
|
||||
Two measurements taken this way were reported as findings and were worthless -
|
||||
one of them ("GL and the engine agree on culling") was actively concealing the
|
||||
bug being hunted.
|
||||
|
||||
**To ask the hardware, resolve the entry point out of the driver:**
|
||||
|
||||
```c
|
||||
void *lib = dlopen( "libGLESv3.so", RTLD_NOW | RTLD_LOCAL );
|
||||
void (*driverGetIntegerv)(GLenum, GLint *) = dlsym( lib, "glGetIntegerv" );
|
||||
```
|
||||
|
||||
A temporary `r_traceSurf` probe did this - printing gl4es's answer and the
|
||||
driver's side by side - and found the cull bug in one run, after many runs that
|
||||
measured nothing. It has since been removed; the technique is the point, and it
|
||||
is four lines to put back.
|
||||
|
||||
## 2. gl4es drops calls it judges redundant
|
||||
|
||||
Each of these returns early when the requested value matches gl4es's copy:
|
||||
|
||||
| Call | Where |
|
||||
|---|---|
|
||||
| `glCullFace` | `src/gl/face.c:14` |
|
||||
| `glDepthMask`, `glDepthFunc`, `glDepthRangef` | `src/gl/depth.c` |
|
||||
| `glBlendFunc` | `src/gl/blend.c` |
|
||||
| `glColorMask` | `src/gl/gl4es.c` |
|
||||
| `glViewport`, `glScissor` | `src/gl/raster.c:61` |
|
||||
| `glEnable`/`glDisable` for `GL_CULL_FACE`, `GL_DEPTH_TEST`, `GL_STENCIL_TEST`, `GL_POLYGON_OFFSET_FILL` | `src/gl/enable.c`, `proxy_glEnable` |
|
||||
|
||||
`GL_SCISSOR_TEST` is *not* filtered - it falls through `proxy_glEnable`'s default
|
||||
branch to the driver every time.
|
||||
|
||||
**Once gl4es's copy drifts from the driver, the engine can never correct it.**
|
||||
It asks for the right value, gl4es compares against the wrong copy, calls it a
|
||||
no-op, and the driver never hears. No error is raised. Nothing looks wrong from
|
||||
inside the engine, because the engine's own state tracker and gl4es's tracker
|
||||
agree with each other - they are just both wrong about the hardware.
|
||||
|
||||
## 3. Anything the VR layer sets natively must go through gl4es
|
||||
|
||||
`code/vr/vr_openxr.c` links `libGLESv3` directly, so a bare `glViewport` there
|
||||
reaches the driver without gl4es seeing it. gl4es's copy is then stale, and by
|
||||
§2 the engine's next request for that value is silently dropped.
|
||||
|
||||
This is inverted from the reference. RTCWQuest's ndk-build puts gl4es first in
|
||||
its link order, so *their* VR layer's GL calls go through gl4es by default and
|
||||
they bypass it explicitly only where they must. Here the default is the other
|
||||
way round, so the routing has to be done by hand.
|
||||
|
||||
The VR layer therefore wraps every state call gl4es caches - `VR_Viewport`,
|
||||
`VR_GLEnable`, `VR_GLDisable`, `VR_DepthMask`, `VR_DepthFunc`, `VR_BlendFunc`,
|
||||
`VR_ColorMask`, `VR_CullFace`, `VR_Scissor`, `VR_BindFramebuffer` - and resolves
|
||||
them out of `libgl4es.so` by name.
|
||||
|
||||
**Three things deliberately stay with the driver**, and must not be routed:
|
||||
|
||||
- the swapchain texture attachment (`glFramebufferTexture2D`) - gl4es cannot
|
||||
attach a texture the OpenXR runtime created;
|
||||
- the blit's `GL_READ_FRAMEBUFFER` / `GL_DRAW_FRAMEBUFFER` bindings - gl4es
|
||||
treats `GL_READ_FRAMEBUFFER` as a note to itself and returns without binding
|
||||
(`framebuffers.c:238`), which silently empties the blit;
|
||||
- the VR layer's own pipeline: its shader program, vertex attributes, uniforms
|
||||
and draw calls. gl4es has no business knowing about those.
|
||||
|
||||
## 4. Setting state that must not be dropped
|
||||
|
||||
Where a value has to reach the driver regardless of what gl4es believes, name a
|
||||
throwaway value first so the real one cannot match a stale cache. `GL_Cull` in
|
||||
`renderergl1/tr_backend.c`:
|
||||
|
||||
```c
|
||||
#ifdef USE_GL4ES
|
||||
qglCullFace( GL_FRONT_AND_BACK ); /* cannot be mistaken for a no-op */
|
||||
#endif
|
||||
qglCullFace( face );
|
||||
```
|
||||
|
||||
Nothing is drawn between the two calls, so the throwaway value is harmless.
|
||||
|
||||
---
|
||||
|
||||
## 5. What this actually cost
|
||||
|
||||
Three bugs on this boundary, each of which presented as "the renderer is
|
||||
correct and nothing is drawn":
|
||||
|
||||
**The framebuffer binding.** gl4es renders from its own table, not the driver's
|
||||
binding, so a framebuffer created by `glGenFramebuffers` on the driver is not in
|
||||
that table - `gl4es_glBindFramebuffer` raises `GL_INVALID_VALUE`, returns
|
||||
without binding, and leaves gl4es drawing into the 16x16 pbuffer. Measured as
|
||||
`fbo: gl4es says 0, driver says 10`.
|
||||
|
||||
**The cull face.** gl4es's copy said `GL_FRONT`; the driver was culling
|
||||
`GL_BACK`. The engine asked for `GL_FRONT` every frame and gl4es dropped it as
|
||||
redundant. Quake's world polygons are back facing in GL's terms - which is why
|
||||
`CT_FRONT_SIDED` culls `GL_FRONT` in the first place - so the hardware was
|
||||
removing precisely the faces that should have been visible.
|
||||
|
||||
The symptom was **the entire world invisible while models rendered perfectly**,
|
||||
because model shaders are `CT_TWO_SIDED` and never enable culling at all.
|
||||
Inverting the culled face did not help either: asking for `GL_BACK` matched what
|
||||
the driver was already doing. Only disabling culling outright brought the world
|
||||
back, which made it look like a winding fault for a very long time.
|
||||
|
||||
**The viewport.** Set natively by the VR layer, cached by gl4es, so the engine's
|
||||
next request for the same value was dropped.
|
||||
|
||||
### The shape to recognise
|
||||
|
||||
> Geometry submitted. Vertex data correct. Indices in range. Matrices correct.
|
||||
> On screen, unclipped, unculled by the frustum. No GL error. Nothing drawn.
|
||||
|
||||
That is what a dropped state call looks like from inside the engine, every time.
|
||||
When you see it, **stop measuring the engine and ask the driver.**
|
||||
|
||||
---
|
||||
|
||||
## 6. Two rules that are not about gl4es
|
||||
|
||||
Learned the hard way alongside the above, and they apply to any device work
|
||||
here.
|
||||
|
||||
**A probe must be shown to reach the thing it measures.** Three separate probes
|
||||
had blind spots exactly where the fault was and each produced a confident wrong
|
||||
conclusion: a panel readback that counted "brighter than 8" against a buffer
|
||||
that is never cleared, so *nothing drawn* and *drawn black* were the same
|
||||
number; an `r_flatColor` override that did not apply to multitextured surfaces,
|
||||
so it only ever covered models; and a flat-red override that restored its state
|
||||
inside a branch that was never taken, corrupting the renderer it was measuring.
|
||||
|
||||
**Verify the change landed.** `r_noDepth` was declared, registered, documented
|
||||
and tested across several device runs - and never actually used, because the
|
||||
edit that was supposed to read it silently failed to apply and nothing checked.
|
||||
Every result attributed to it was a no-op, and it sat in the "known good"
|
||||
configuration taking credit for what disabling culling was doing. Grep for the
|
||||
symbol after editing, or read the warning log; a cvar that reads nothing looks
|
||||
exactly like a cvar that changes nothing.
|
||||
|
||||
**Look at the frame.** `vr_captureEye` writes the eye buffer to
|
||||
`main/vrshotN.tga` every two seconds. One image settled questions that six
|
||||
rounds of verbal description could not. Pixel counts bound the answer; a picture
|
||||
gives it.
|
||||
@@ -0,0 +1,4 @@
|
||||
.gradle/
|
||||
local.properties
|
||||
*.apk
|
||||
*.aab
|
||||
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:installLocation="auto">
|
||||
|
||||
<!-- renderergl2 is driven as GLES 3; renderergl1 is not built for Android -->
|
||||
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
|
||||
|
||||
<!--
|
||||
Together with the VR intent category below, this is what makes the
|
||||
headset launch the app immersively instead of as a flat panel.
|
||||
-->
|
||||
<uses-feature
|
||||
android:name="android.hardware.vr.headtracking"
|
||||
android:required="true"
|
||||
android:version="1" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<!-- The Touch controllers arrive as a game controller -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.gamepad"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
android:required="false" />
|
||||
|
||||
<!-- Multiplayer, and the master server browser -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<!--
|
||||
Game data lives in the app's own external files directory, which needs
|
||||
no permission. The user side-loads main/pak*.pk3 into it; no assets are
|
||||
shipped with the app.
|
||||
-->
|
||||
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:allowBackup="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
|
||||
|
||||
<activity
|
||||
android:name=".OpenMoHAAActivity"
|
||||
android:label="@string/app_name"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
|
||||
android:preferMinimalPostProcessing="true"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="com.oculus.intent.category.VR" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.openmoh.openmohaa;
|
||||
|
||||
import org.libsdl.app.SDLActivity;
|
||||
|
||||
/**
|
||||
* Entry point for the Android build.
|
||||
*
|
||||
* SDLActivity loads the libraries named below in order, then dlopen()s the last
|
||||
* one and calls SDL_main in it - which is the engine's main(), renamed by
|
||||
* SDL_main.h. libc++_shared.so, libopenal.so and libgl4es.so are pulled in
|
||||
* automatically as dependencies of libopenmohaa.so, and libgame.so /
|
||||
* libcgame.so are dlopen()ed later by the engine itself, so none of them are
|
||||
* listed here.
|
||||
*/
|
||||
public class OpenMoHAAActivity extends SDLActivity {
|
||||
@Override
|
||||
protected String[] getLibraries() {
|
||||
return new String[] {
|
||||
"SDL2",
|
||||
"openmohaa",
|
||||
};
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">OpenMoHAA</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,140 @@
|
||||
// Deploy with:
|
||||
//
|
||||
// adb push misc/android/autoexec.cfg \
|
||||
// /sdcard/Android/data/org.openmoh.openmohaa/files/main/autoexec.cfg
|
||||
// adb shell am force-stop org.openmoh.openmohaa
|
||||
//
|
||||
// Then launch from inside the headset. Never adb shell am start.
|
||||
//
|
||||
// Not packaged into the APK - it goes next to the game data, which is where the
|
||||
// filesystem looks for it. Reinstalling the APK keeps it; clearing app data or
|
||||
// reflashing the game files loses it, which is why it lives here.
|
||||
//
|
||||
// autoexec.cfg is exec'd after omconfig.cfg, cannot be loaded from a pk3, and
|
||||
// lands before R_Init - so it beats both the stored config and the renderer's
|
||||
// own defaults, latched and archived cvars included. It is the only way to set
|
||||
// a cvar on a device with no console. See docs/markdown/04-coding/04-gl4es.md.
|
||||
|
||||
// Where the weapon model sits in the hand:
|
||||
//
|
||||
// scale, right, up, forward, pitch, yaw, roll
|
||||
//
|
||||
// The view model is authored to sit at the camera with the arms reaching
|
||||
// forward, so putting its origin at the controller leaves that built-in offset
|
||||
// in place. This takes it back out. Right/up/forward are in engine units along
|
||||
// the weapon's own axes after any rotation correction, so they mean what they
|
||||
// say however the gun is pointing.
|
||||
//
|
||||
// To dial it in: push a new value, force-stop, relaunch. No rebuild needed.
|
||||
// Negative forward pulls the model back towards you, negative up drops it,
|
||||
// negative right moves it to the off hand's side.
|
||||
//
|
||||
// The reference keeps one of these per weapon (vr_weapon_adjustment_<id>);
|
||||
// this is a single global one until the numbers for each are known.
|
||||
// The offsets are back to zero: the -10 right and -6 up were pulling the model
|
||||
// back from an origin anchored on the model itself, and the weapon is now
|
||||
// anchored on tag_weapon_right, so the gun's own attachment point lands on the
|
||||
// controller and there is nothing left for those to cancel. Retune from here
|
||||
// rather than from the old numbers.
|
||||
//
|
||||
// The roll is not zero, and is not a fudge. Anchoring forces the *bone* to take
|
||||
// the controller's orientation, and a wrist bone's local axes do not run the way
|
||||
// a controller is held - the mesh inherits that mismatch as a constant twist.
|
||||
// 180 is the first thing to try because upside down is what a half turn about
|
||||
// the aim axis looks like. If the model ends up on its side instead, the
|
||||
// mismatch is in pitch or yaw; those are the two fields before this one.
|
||||
set vr_weaponAdjust "1,0,0,0,0,0,180"
|
||||
|
||||
// The same seven fields for the hand that is not holding the weapon, which is
|
||||
// drawn as a second copy of the view model on the off hand's controller.
|
||||
//
|
||||
// The offsets are zero because this hand is anchored on its own bone
|
||||
// (Bip01 L Hand, measured 26.7 units out from the model origin), so there is no
|
||||
// built-in offset left to cancel - only the wrist's own rotation convention,
|
||||
// which is what the roll is for. Same reasoning as the weapon above.
|
||||
//
|
||||
// The two hands are mirrored, so do not assume the correction is identical:
|
||||
// if one comes out right at 180 and the other is still wrong, try -180 here.
|
||||
//
|
||||
// If the hand is nowhere near the controller rather than merely turned, read the
|
||||
// log instead of tuning: the candidate lines name every anchor considered and
|
||||
// how far out it sits, which says whether the bone was found at all.
|
||||
set vr_offHandAdjust "1,0,0,0,0,0,180"
|
||||
|
||||
// How many game units make a metre. Not a feel value - it is this game's own
|
||||
// scale, and getting it wrong makes everything measured against the player the
|
||||
// wrong size.
|
||||
//
|
||||
// MOHAA's art is authored at 16 units per foot; its tik files say so where they
|
||||
// convert from centimetres (models/weapons/bar.tik: "16/30.5 since world is in
|
||||
// 16 units per foot"). 16 / 0.3048 = 52.5, and the player dimensions agree -
|
||||
// MAXS_Z 94 and DEFAULT_VIEWHEIGHT 82 are 1.79 m standing, 1.56 m to the eye.
|
||||
//
|
||||
// This is set here as well as changed in code because vr_worldscale is
|
||||
// CVAR_ARCHIVE: the value already stored in omconfig.cfg wins over any new
|
||||
// default, so a code change alone would do nothing on a device that has run
|
||||
// before.
|
||||
set vr_worldscale 52.5
|
||||
|
||||
// How long the weapon hand grip may be held and still count as a tap for
|
||||
// reload, rather than being ignored.
|
||||
set vr_reloadTapMs 400
|
||||
|
||||
// Dumps the left eye to main/vrshotN.tga every two seconds. Pull them one at a
|
||||
// time - pulling the directory drags the pk3s with it.
|
||||
// The per frame timing report is three lines a second, which rolls anything
|
||||
// useful out of logcat within a minute or two. Off while hunting anything that
|
||||
// prints once.
|
||||
set vr_traceFrame 1
|
||||
|
||||
set vr_captureEye 0
|
||||
|
||||
// --- weapon test harness ---
|
||||
//
|
||||
// Playing through to wherever the game hands over a gun, on every build, is not
|
||||
// a workable way to tune weapon alignment. There is no console in a headset to
|
||||
// type "give" into either, and autoexec runs before there is a map - let alone
|
||||
// a player to give anything to.
|
||||
//
|
||||
// All three are off on purpose. Filled in, they skip the game: cheats on,
|
||||
// every weapon handed over, and a map loaded straight past the menu. That is a
|
||||
// harness for tuning alignment, not how anyone should be handed the game.
|
||||
//
|
||||
// set cheats 1
|
||||
// set vr_testStart "give all"
|
||||
// set vr_startMap "m4l1"
|
||||
//
|
||||
// Each needs its own timing, which is why they are three cvars and not one.
|
||||
// vr_testStart runs once, a second and a half after the player is actually in
|
||||
// the world - autoexec is far too early, there is no map yet, let alone a
|
||||
// player to give anything to. vr_startMap waits until the client is sitting
|
||||
// idle at the menu, because a map loaded from autoexec is torn back down by the
|
||||
// game's own startup.
|
||||
set cheats 0
|
||||
set vr_testStart ""
|
||||
|
||||
// Drop straight into a map on launch. Empty is the menu, which is how the game
|
||||
// is meant to start.
|
||||
//
|
||||
// Not "map m4l1" directly: autoexec runs during Com_Init, and the game's own
|
||||
// startup - the intro stages, then the menu - comes afterwards and tears any
|
||||
// map loaded here straight back down. This waits until the client is sitting
|
||||
// idle at the menu and then loads it.
|
||||
//
|
||||
// m4l1 is the weapon test map, chosen off the game's own data rather than by
|
||||
// reputation:
|
||||
//
|
||||
// - its script calls itself THE BOCAGE, and it opens on the Normandy
|
||||
// hedgerows with Manon, who is spawned with her AI disabled and set
|
||||
// nodamage - so nothing is shooting at you while you look at your hands;
|
||||
// - it has 2 spawn references in 538 lines of script, against m1l1's crowd
|
||||
// and m1l2a's 59 in 3660. Nearly nothing is scripted to happen at the
|
||||
// start;
|
||||
// - global/loadout.scr hands the player a colt45 and an m1_garand and selects
|
||||
// the rifle, so there is a weapon in hand at spawn without waiting for one.
|
||||
//
|
||||
// That last point is what makes it the right map for the hands specifically:
|
||||
// the Garand is the weapon that swaps the left hand surface to "garandhand",
|
||||
// so this level exercises both hand meshes rather than just the common one.
|
||||
// Switch to the pistol to see the other.
|
||||
set vr_startMap ""
|
||||
@@ -0,0 +1,14 @@
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
# Where the CMake build staged the native libraries, relative to this
|
||||
# directory. Overridable so a build tree anywhere can be packaged:
|
||||
#
|
||||
# gradle assembleDebug -PnativeLibsDir=/path/to/build/apk-libs
|
||||
#
|
||||
nativeLibsDir=../../build-android-arm64/apk-libs
|
||||
|
||||
# The SDL source tree the engine was built against; the APK needs the
|
||||
# org.libsdl.app Java classes from it, and they must match libSDL2.so. The
|
||||
# CMake build reports the path it used as SDL2_JAVA_DIR.
|
||||
sdlJavaDir=
|
||||
@@ -0,0 +1,18 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "openmohaa"
|
||||
|
||||
include ":app"
|
||||
Reference in new issue
Block a user