mirror of
https://github.com/bearstonem/openmohaa-vr.git
synced 2026-10-18 06:00:24 +02:00
Compare commits
53
Commits
main
...
vr-quest3-0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaf287d1fb | ||
|
|
d9968210aa | ||
|
|
bd72599ab7 | ||
|
|
e6a7c76f84 | ||
|
|
bec1f4a8d5 | ||
|
|
6f443949ae | ||
|
|
7bb71e7550 | ||
|
|
711cad20e6 | ||
|
|
15471f7578 | ||
|
|
74dc964962 | ||
|
|
b51ed2e430 | ||
|
|
aa2fbeaf94 | ||
|
|
e45388bd01 | ||
|
|
dcd85654a4 | ||
|
|
214933c5fe | ||
|
|
fb2855f664 | ||
|
|
649f8f682d | ||
|
|
7ce4e07352 | ||
|
|
43e29a300a | ||
|
|
f52d6e5163 | ||
|
|
4f5b54c74c | ||
|
|
48b5876e3f | ||
|
|
bd480975a3 | ||
|
|
6e86e55c08 | ||
|
|
013b2f5bbf | ||
|
|
ce91fd1bf7 | ||
|
|
db8dfae0bd | ||
|
|
046e2dd9a6 | ||
|
|
411c3d9bc2 | ||
|
|
862d36a9ba | ||
|
|
11b1597f14 | ||
|
|
5e7e907792 | ||
|
|
e967529ec6 | ||
|
|
5267bc66c0 | ||
|
|
a3e3d04a5a | ||
|
|
b49e34a9ed | ||
|
|
27011bd3eb | ||
|
|
1249e897c6 | ||
|
|
6e369175be | ||
|
|
17771047c5 | ||
|
|
d9960e1857 | ||
|
|
a8a1314c2d | ||
|
|
aae8dff0f2 | ||
|
|
d2d52a5af4 | ||
|
|
6f34f4524c | ||
|
|
633221c57d | ||
|
|
7f794abbb4 | ||
|
|
7872929f32 | ||
|
|
393b0e3e3d | ||
|
|
5aff8d336a | ||
|
|
eea9112be9 | ||
|
|
f3ca623053 | ||
|
|
6517d02dfe |
No files matched your search
@@ -1,4 +1,6 @@
|
||||
build*
|
||||
!/misc/android/build.gradle
|
||||
!/misc/android/app/build.gradle
|
||||
*.swp
|
||||
*tags
|
||||
*~
|
||||
|
||||
@@ -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,410 @@
|
||||
# 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`. The historical status below records previously committed work.
|
||||
|
||||
### Quest release 0.2.0 — October 14, 2026
|
||||
|
||||
The Android package uses version code 2 and version name `0.83.0-vr0.2.0`.
|
||||
The supplied `autoexec.cfg` starts at the normal menu, with cheats, automatic
|
||||
weapon grants, test-map loading, and diagnostic traces disabled. Replace the
|
||||
external config when upgrading: reinstalling the APK preserves the old one.
|
||||
Keep `r_worldDlights 0` until the lighting defect is resolved; that workaround
|
||||
is supplied by the config, not by the APK's defaults. This remains a prerelease,
|
||||
and the September review changes still need headset validation.
|
||||
|
||||
### Code review update — September 29, 2026
|
||||
|
||||
These fixes remove the unconditional GPU wait after each eye;
|
||||
`vr_traceGpu 1` restores that wait only for diagnostics. Normal submission flushes
|
||||
commands and discards the eye's transient depth/stencil buffers. Swapchain timeouts
|
||||
now retain the acquired image for retry, and stereo submission requires both eyes.
|
||||
World/HUD timing now charges the first 2D transition's world flush to the world,
|
||||
and backend timing accumulates all command batches in an eye.
|
||||
|
||||
The review also fixes desktop HUD suppression, objective initialization, HUD
|
||||
resource resets, health colors, weapon-wheel placement, stereo text lifetimes,
|
||||
font shader passes, and sprite culling and upright sprite disappearance when
|
||||
looking vertically. CMake now tracks additions to the client-game source list.
|
||||
No headset was connected for this review: the measured frame rates and visual
|
||||
observations below predate these changes. Device validation is still needed.
|
||||
The existing font-atlas texture issue and dynamic-light darkening remain
|
||||
unresolved; `r_worldDlights 0` in `autoexec.cfg` remains the lighting workaround.
|
||||
|
||||
**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
|
||||
|
||||
**A muzzle flash makes the ground darker, so the world's dynamic lighting is
|
||||
off.** `r_worldDlights 0` in `autoexec.cfg`. Firing multiplies roughly a sixth
|
||||
of the screen down by about 4× for as long as the trigger is held — the ground
|
||||
plane, with hard polygon edges. Unsolved after seven wrong theories; every
|
||||
component of the dynamic lightmap path measures correct end to end, and yet
|
||||
switching the path off makes the symptom stop. Losing flashes lighting the
|
||||
scenery is the smaller loss. The measurements, what is ruled out and where to
|
||||
resume are in `docs/markdown/04-coding/05-dlights.md`.
|
||||
|
||||
The lesson worth carrying beyond this bug: **difference the frames first.**
|
||||
Pulling a capture, extracting every frame and differencing a flash frame against
|
||||
its neighbour gave the cadence, the 4× ratio and the affected geometry in one
|
||||
step, after six rounds of reading the renderer had given nothing. Also, never
|
||||
record video and read the log in the same run — the Quest's recorder floods
|
||||
logcat and evicts everything.
|
||||
|
||||
**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`.
|
||||
+11
-1
@@ -12,7 +12,7 @@ file(GLOB_RECURSE BG_SOURCES
|
||||
${SOURCE_DIR}/fgame/bg_voteoptions.cpp
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE CGAME_SOURCES
|
||||
file(GLOB_RECURSE CGAME_SOURCES CONFIGURE_DEPENDS
|
||||
${SOURCE_DIR}/cgame/*.c ${SOURCE_DIR}/cgame/*.cpp
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
+207
-28
@@ -683,8 +683,9 @@ void CG_InitializeObjectives()
|
||||
cg.ObjectivesCurrentAlpha = 0.0;
|
||||
|
||||
for (i = 0; i < MAX_OBJECTIVES; i++) {
|
||||
cg.Objectives[i].flags = 0;
|
||||
cg.Objectives[i].text[0] = 0;
|
||||
// A new gamestate may already contain objectives (for example after
|
||||
// loading a save). Subsequent changes arrive through configstrings.
|
||||
CG_ProcessConfigString(CS_OBJECTIVES + i, qfalse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -703,12 +704,9 @@ void CG_DrawObjectives()
|
||||
float fTimeDelta;
|
||||
const char *pszLocalizedText;
|
||||
const char *pszLine;
|
||||
float fAlpha;
|
||||
|
||||
iTotalNumLines = 0;
|
||||
for (i = CS_OBJECTIVES; i < CS_OBJECTIVES + MAX_OBJECTIVES; ++i) {
|
||||
CG_ProcessConfigString(i, qfalse);
|
||||
}
|
||||
|
||||
iCurrentObjective = atoi(CG_ConfigString(CS_CURRENT_OBJECTIVE));
|
||||
fTimeDelta = cg.ObjectivesAlphaTime - cg.time;
|
||||
cg.ObjectivesCurrentAlpha = cg.ObjectivesDesiredAlpha;
|
||||
@@ -718,7 +716,71 @@ void CG_DrawObjectives()
|
||||
+ cg.ObjectivesDesiredAlpha;
|
||||
}
|
||||
|
||||
if (cg.ObjectivesCurrentAlpha < 0.02) {
|
||||
//
|
||||
// Added in OPM
|
||||
// Full on the wrist panel, whatever the fade says. In the flat game the
|
||||
// objectives are a hold-the-key overlay - CG_ScoresDown_f fades them in
|
||||
// while the key is held - and a transient overlay on a panel that is
|
||||
// itself only visible while the player is looking at their wrist would
|
||||
// almost never be seen at all. Turning your wrist up *is* the gesture
|
||||
// that asks for them, so the fade has nothing left to do there.
|
||||
//
|
||||
// Held in a local rather than written back to cg.ObjectivesCurrentAlpha:
|
||||
// CG_DrawStopwatch reads that field to decide the stopwatch is covered
|
||||
// and removes it, and CG_SetDesiredObjectiveAlpha animates from it.
|
||||
//
|
||||
fAlpha = cg.ObjectivesCurrentAlpha;
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// Always up in VR, whatever the fade says. In the flat game the objectives
|
||||
// are a hold-the-key overlay - CG_ScoresDown_f fades them in while the key
|
||||
// is held - and there is no key to hold in a headset.
|
||||
//
|
||||
// Its own switch rather than a test on the draw pass. It was written as
|
||||
// pass == 1 while the HUD lived on the wrist panel; when the HUD moved
|
||||
// back into the eye buffers the pass became 0 and the objectives went
|
||||
// straight back to invisible, which took a device run to notice.
|
||||
//
|
||||
if (vr_hudPass && vr_hudPass->integer && vr_objectivesAlways && vr_objectivesAlways->integer) {
|
||||
fAlpha = 1.0f;
|
||||
}
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// The objectives were reported missing from the wrist panel and there are
|
||||
// three quite different reasons they could be, so this says which. The
|
||||
// pass names where it is drawing; alpha under 0.02 is the fade still
|
||||
// holding them off; and a set count of zero means the level never sent any
|
||||
// - which a map entered straight through vr_startMap, past the scripting
|
||||
// that hands them out, is a very good way to arrange.
|
||||
//
|
||||
if (vr_traceObjectives && vr_traceObjectives->integer) {
|
||||
static int lastReport;
|
||||
|
||||
if (!lastReport || cg.time - lastReport > 2000) {
|
||||
int iSet = 0;
|
||||
int k;
|
||||
|
||||
for (k = 0; k < MAX_OBJECTIVES; k++) {
|
||||
if (cg.Objectives[k].flags != OBJ_FLAG_NONE && !(cg.Objectives[k].flags & OBJ_FLAG_HIDDEN)) {
|
||||
iSet++;
|
||||
}
|
||||
}
|
||||
|
||||
lastReport = cg.time;
|
||||
cgi.Printf(
|
||||
"objectives: pass %d alpha %.2f (raw %.2f) set %d current %d\n",
|
||||
vr_hudPass ? vr_hudPass->integer : -1,
|
||||
fAlpha,
|
||||
cg.ObjectivesCurrentAlpha,
|
||||
iSet,
|
||||
iCurrentObjective
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (fAlpha < 0.02) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -749,7 +811,7 @@ void CG_DrawObjectives()
|
||||
vColor[2] = 0.2f;
|
||||
vColor[1] = 0.2f;
|
||||
vColor[0] = 0.2f;
|
||||
vColor[3] = cg.ObjectivesCurrentAlpha * 0.75;
|
||||
vColor[3] = fAlpha * 0.75;
|
||||
cgi.R_SetColor(vColor);
|
||||
cgi.R_DrawStretchPic(
|
||||
fX,
|
||||
@@ -768,7 +830,7 @@ void CG_DrawObjectives()
|
||||
vColor[0] = 1.0;
|
||||
vColor[1] = 1.0;
|
||||
vColor[2] = 1.0;
|
||||
vColor[3] = cg.ObjectivesCurrentAlpha;
|
||||
vColor[3] = fAlpha;
|
||||
cgi.R_SetColor(vColor);
|
||||
cgi.R_DrawString(
|
||||
cgs.media.objectiveFont,
|
||||
@@ -801,26 +863,26 @@ void CG_DrawObjectives()
|
||||
vColor[0] = 1.0;
|
||||
vColor[1] = 1.0;
|
||||
vColor[2] = 1.0;
|
||||
vColor[3] = cg.ObjectivesCurrentAlpha;
|
||||
vColor[3] = fAlpha;
|
||||
hBoxShader = cgs.media.uncheckedBoxShader;
|
||||
} else if ((cg.Objectives[i].flags & OBJ_FLAG_COMPLETED) != 0) {
|
||||
vColor[0] = 0.75;
|
||||
vColor[1] = 0.75;
|
||||
vColor[2] = 0.75;
|
||||
vColor[3] = cg.ObjectivesCurrentAlpha;
|
||||
vColor[3] = fAlpha;
|
||||
hBoxShader = cgs.media.checkedBoxShader;
|
||||
} else {
|
||||
vColor[0] = 1.0;
|
||||
vColor[1] = 1.0;
|
||||
vColor[2] = 1.0;
|
||||
vColor[3] = cg.ObjectivesCurrentAlpha;
|
||||
vColor[3] = fAlpha;
|
||||
hBoxShader = cgs.media.uncheckedBoxShader;
|
||||
}
|
||||
if (i == iCurrentObjective && !(cg.Objectives[i].flags & OBJ_FLAG_COMPLETED)) {
|
||||
vColor[0] = 1.0;
|
||||
vColor[1] = 1.0;
|
||||
vColor[2] = 0.0;
|
||||
vColor[3] = cg.ObjectivesCurrentAlpha;
|
||||
vColor[3] = fAlpha;
|
||||
}
|
||||
|
||||
cgi.R_SetColor(vColor);
|
||||
@@ -840,7 +902,7 @@ void CG_DrawObjectives()
|
||||
vColor[0] = 1.0;
|
||||
vColor[1] = 1.0;
|
||||
vColor[2] = 1.0;
|
||||
vColor[3] = cg.ObjectivesCurrentAlpha;
|
||||
vColor[3] = fAlpha;
|
||||
cgi.R_SetColor(vColor);
|
||||
cgi.R_DrawStretchPic(
|
||||
fX * cgs.uiHiResScale[0],
|
||||
@@ -1524,20 +1586,137 @@ void CG_DrawVote()
|
||||
CG_Draw2D
|
||||
==============
|
||||
*/
|
||||
/*
|
||||
=================
|
||||
CG_DrawVRWheel
|
||||
|
||||
The weapon wheel, while the player is holding it open.
|
||||
|
||||
Deliberately plain: names in a ring with the one under the controller lit up.
|
||||
A wheel is only worth having if it can be read at a glance while something is
|
||||
shooting at you, and the thing that makes it readable is the highlight moving
|
||||
the moment the wrist does - not what the segments are made of.
|
||||
|
||||
Drawn with the world rather than on the wrist panel, because the hand aiming it
|
||||
is out in front of the player and a menu that lives somewhere else would have
|
||||
them looking in two places at once.
|
||||
=================
|
||||
*/
|
||||
static void CG_DrawVRWheel(void)
|
||||
{
|
||||
const char *names[8];
|
||||
int selection = -1;
|
||||
int count = 0;
|
||||
int i;
|
||||
float cx, cy, radius;
|
||||
vec2_t textScale;
|
||||
|
||||
if (!cgi.VR_GetWheel || !cgi.VR_GetWheel(&selection, names, (int)ARRAY_LEN(names), &count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count <= 0 || !cgs.media.objectiveFont) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lay out the ring in eye pixels, then undo the text scale on both axes.
|
||||
// Mixing virtual x with pixel y placed it at y=240 in a 1760-line eye.
|
||||
textScale[0] = textScale[1] = cgs.glconfig.vidHeight / 1080.0f;
|
||||
if (textScale[0] <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
cx = cgs.glconfig.vidWidth * 0.5f;
|
||||
cy = cgs.glconfig.vidHeight * 0.5f;
|
||||
radius = 120.0f * textScale[0];
|
||||
if (count > (int)ARRAY_LEN(names)) {
|
||||
count = (int)ARRAY_LEN(names);
|
||||
}
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
// Clockwise from straight up, matching the order the VR layer numbers
|
||||
// the segments in - the two have to agree or the highlight lands on
|
||||
// the wrong word.
|
||||
const float angle = (float)i * (2.0f * M_PI / (float)count);
|
||||
const float width = cgi.UI_FontStringWidth(cgs.media.objectiveFont, names[i], -1) * textScale[0];
|
||||
const float x = cx + sinf(angle) * radius - width * 0.5f;
|
||||
const float y = cy - cosf(angle) * radius;
|
||||
vec4_t colour;
|
||||
|
||||
if (i == selection) {
|
||||
colour[0] = 1.0f; colour[1] = 0.85f; colour[2] = 0.4f; colour[3] = 1.0f;
|
||||
} else {
|
||||
colour[0] = 0.75f; colour[1] = 0.75f; colour[2] = 0.75f; colour[3] = 0.6f;
|
||||
}
|
||||
|
||||
cgi.R_SetColor(colour);
|
||||
cgi.R_DrawString(cgs.media.objectiveFont, names[i], x / textScale[0], y / textScale[1], -1, textScale);
|
||||
}
|
||||
|
||||
cgi.R_SetColor(NULL);
|
||||
}
|
||||
|
||||
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
|
||||
// 0 is the flat frame, 1 the wrist panel, 2 the world without its HUD,
|
||||
// and 3 a VR eye carrying both the world and the HUD.
|
||||
//
|
||||
const int pass = vr_hudPass ? vr_hudPass->integer : 0;
|
||||
|
||||
if (pass) {
|
||||
// Keep damage feedback current even when the wrist panel is hidden.
|
||||
CG_UpdateVRHud();
|
||||
}
|
||||
|
||||
if (pass != 2) {
|
||||
CG_UpdateCountdown();
|
||||
CG_DrawLagometer();
|
||||
CG_HudDrawElements();
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// The gathered VR HUD carries the objectives itself, laid out beside
|
||||
// the compass rather than across the top left of a screen nobody in a
|
||||
// headset is looking at. Drawing both would put the same text on the
|
||||
// view twice, in two places, at two sizes.
|
||||
//
|
||||
if (pass && vr_hud && vr_hud->integer) {
|
||||
CG_DrawVRHud();
|
||||
} else {
|
||||
CG_DrawObjectives();
|
||||
}
|
||||
|
||||
CG_DrawIcons();
|
||||
CG_DrawStopwatch();
|
||||
CG_DrawSpectatorView();
|
||||
CG_DrawPlayerTeam();
|
||||
CG_DrawPlayerEntInfo();
|
||||
CG_UpdateAttackerDisplay();
|
||||
CG_DrawVote();
|
||||
CG_DrawInstantMessageMenu();
|
||||
}
|
||||
|
||||
if (pass != 1) {
|
||||
CG_DrawVRWheel();
|
||||
|
||||
// The scope surround has to sit around the magnified view, so it stays
|
||||
// with the world rather than travelling to the wrist.
|
||||
CG_DrawZoomOverlay();
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// The reticle is drawn at the centre of the screen, which in a headset
|
||||
// is the centre of the player's head - and the weapon is aimed by the
|
||||
// controller, not by where they are looking. So it marks a spot the
|
||||
// gun is not pointing at, pinned to their face wherever they turn.
|
||||
//
|
||||
// pass 0 is the flat frame, where the reticle means what it always
|
||||
// meant and is drawn as before. vr_crosshair brings it back in VR for
|
||||
// anyone who wants it.
|
||||
//
|
||||
if (!pass || (vr_crosshair && vr_crosshair->integer)) {
|
||||
CG_DrawCrosshair();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -513,6 +513,19 @@ 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;
|
||||
extern cvar_t *vr_crosshair;
|
||||
extern cvar_t *vr_traceObjectives;
|
||||
extern cvar_t *vr_objectivesAlways;
|
||||
// The gathered VR HUD; see cg_vrhud.cpp.
|
||||
extern cvar_t *vr_hud;
|
||||
extern cvar_t *vr_hudGroupX;
|
||||
extern cvar_t *vr_hudGroupY;
|
||||
extern cvar_t *vr_hudGroupScale;
|
||||
extern cvar_t *vr_hudAlpha;
|
||||
extern cvar_t *vr_hudTime;
|
||||
extern cvar_t *vr_hudShow;
|
||||
|
||||
//
|
||||
// cg_main.c
|
||||
@@ -600,6 +613,13 @@ extern "C" {
|
||||
void CG_ZoomDown_f(void);
|
||||
void CG_ZoomUp_f(void);
|
||||
|
||||
//
|
||||
// cg_vrhud.cpp
|
||||
//
|
||||
void CG_InitVRHud(void);
|
||||
void CG_UpdateVRHud(void);
|
||||
void CG_DrawVRHud(void);
|
||||
|
||||
void CG_EyePosition(vec3_t *o_vPos);
|
||||
void CG_EyeOffset(vec3_t *o_vOfs);
|
||||
void CG_EyeAngles(vec3_t *o_vAngles);
|
||||
|
||||
@@ -116,6 +116,25 @@ cvar_t *ui_timemessage;
|
||||
// Added in OPM
|
||||
//
|
||||
cvar_t *cg_fov;
|
||||
cvar_t *vr_hudPass;
|
||||
// Added in OPM
|
||||
// Whether the reticle is drawn in VR. Off: it sits at the centre of the head
|
||||
// while the weapon is aimed by the controller, so it marks a spot the gun is
|
||||
// not pointing at. See CG_Draw2D.
|
||||
cvar_t *vr_crosshair;
|
||||
cvar_t *vr_traceObjectives;
|
||||
cvar_t *vr_objectivesAlways;
|
||||
// Added in OPM
|
||||
// The gathered VR HUD - compass, objectives, health, ammo and the weapon
|
||||
// strip drawn as one group in the middle of the view instead of as five
|
||||
// widgets in five corners of a lens. See cg_vrhud.cpp.
|
||||
cvar_t *vr_hud;
|
||||
cvar_t *vr_hudGroupX;
|
||||
cvar_t *vr_hudGroupY;
|
||||
cvar_t *vr_hudGroupScale;
|
||||
cvar_t *vr_hudAlpha;
|
||||
cvar_t *vr_hudTime;
|
||||
cvar_t *vr_hudShow;
|
||||
cvar_t *cg_cheats;
|
||||
|
||||
/*
|
||||
@@ -212,6 +231,32 @@ void CG_RegisterCvars(void)
|
||||
//
|
||||
|
||||
cg_fov = cgi.Cvar_Get("cg_fov", "80", CVAR_ARCHIVE);
|
||||
// 0 flat, 1 wrist panel, 2 world only, 3 combined VR world and HUD.
|
||||
vr_hudPass = cgi.Cvar_Get("vr_hudPass", "0", 0);
|
||||
vr_crosshair = cgi.Cvar_Get("vr_crosshair", "0", CVAR_ARCHIVE);
|
||||
vr_traceObjectives = cgi.Cvar_Get("vr_traceObjectives", "0", 0);
|
||||
vr_objectivesAlways = cgi.Cvar_Get("vr_objectivesAlways", "1", CVAR_ARCHIVE);
|
||||
// Archived, and placed as fractions of the display rather than in pixels,
|
||||
// so the cluster can be moved and sized from autoexec.cfg. There is no
|
||||
// console in a headset and this is a thing only judged while wearing one.
|
||||
// 0 off, 1 up on the off hand's X button for vr_hudTime seconds, 2 always.
|
||||
vr_hud = cgi.Cvar_Get("vr_hud", "1", CVAR_ARCHIVE);
|
||||
vr_hudTime = cgi.Cvar_Get("vr_hudTime", "3", CVAR_ARCHIVE);
|
||||
// Written by the VR layer when the button goes down; read here. Not
|
||||
// archived - a timestamp from the last run means nothing to this one.
|
||||
vr_hudShow = cgi.Cvar_Get("vr_hudShow", "0", 0);
|
||||
vr_hudGroupX = cgi.Cvar_Get("vr_hudGroupX", "0.5", CVAR_ARCHIVE);
|
||||
// Not where it looks like it is. This places the cluster in the flat frame,
|
||||
// and the renderer then scales that frame to vr_hudScale about the middle
|
||||
// of the display and lifts it by the eye's vertical frustum asymmetry - on
|
||||
// a Quest 3, 170 pixels of 1760. Both of those pull the number back towards
|
||||
// the middle of the view, so the range that reaches the eye at all is about
|
||||
// 0.13 to 0.68 of it: 0.62 lands just above the middle, 0.73 just below,
|
||||
// and 0.92 - this - well down at 0.63, which is where it wants to be for
|
||||
// something checked without breaking aim.
|
||||
vr_hudGroupY = cgi.Cvar_Get("vr_hudGroupY", "0.92", CVAR_ARCHIVE);
|
||||
vr_hudGroupScale = cgi.Cvar_Get("vr_hudGroupScale", "1.0", CVAR_ARCHIVE);
|
||||
vr_hudAlpha = cgi.Cvar_Get("vr_hudAlpha", "1.0", CVAR_ARCHIVE);
|
||||
cg_cheats = cgi.Cvar_Get("cheats", "0", CVAR_USERINFO | CVAR_SERVERINFO | CVAR_LATCH);
|
||||
}
|
||||
/*
|
||||
@@ -667,6 +712,7 @@ void CG_GameStateReceived(void)
|
||||
CG_InitializeSpecialEffectsManager();
|
||||
|
||||
CG_InitializeObjectives();
|
||||
CG_InitVRHud();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -698,6 +744,7 @@ void CG_ServerRestarted(void)
|
||||
CG_ResetVSSSources();
|
||||
// Reset objectives
|
||||
CG_InitializeObjectives();
|
||||
CG_InitVRHud();
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
+588
-5
@@ -983,6 +983,480 @@ 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_VRHandsTracked
|
||||
|
||||
Whether there is a headset with a tracked weapon hand, asked before deciding
|
||||
what the first person model is for.
|
||||
|
||||
The surface selection happens well before CG_VRPlaceViewModel is reached, and
|
||||
it has to know then whether these are a pair of tracked hands or a flat view
|
||||
model - the flat game hides the arms when the player has no weapon, and in VR
|
||||
that is the one case where they should stay.
|
||||
===============
|
||||
*/
|
||||
static qboolean CG_VRHandsTracked(void)
|
||||
{
|
||||
vec3_t trackingOffset, angles;
|
||||
float baseYaw = 0.0f;
|
||||
|
||||
return (cgi.VR_GetWeaponPose && cgi.VR_GetWeaponPose(trackingOffset, angles, &baseYaw)) ? qtrue : qfalse;
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
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 +1472,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;
|
||||
|
||||
@@ -1448,9 +1931,43 @@ void CG_ModelAnim(centity_t *cent, qboolean bDoShaderTime)
|
||||
|
||||
if ((cent->currentState.eFlags & EF_UNARMED) || cg_drawviewmodel->integer <= 1
|
||||
|| cg.snap->ps.stats[STAT_INZOOM] || cg.snap->ps.stats[STAT_HEALTH] <= 0) {
|
||||
// unarmed or zooming, hide the arms
|
||||
for (i = 0; i < MAX_MODEL_SURFACES; i++) {
|
||||
model.surfaces[i] |= MDL_SURFACE_NODRAW;
|
||||
//
|
||||
// Added in OPM
|
||||
//
|
||||
// Being unarmed is not a reason to have no hands in VR. Flat,
|
||||
// this model exists only to hold a weapon, so putting the gun
|
||||
// away hides the arms with it and nobody notices. In a headset
|
||||
// the player is holding two controllers whether or not there
|
||||
// is anything in them, and hands that vanish when the weapon
|
||||
// does read as the tracking having dropped out.
|
||||
//
|
||||
// No mesh work is needed for this: the bare hands are already
|
||||
// separate surfaces - triggerhand is the right, lefthand the
|
||||
// left. Only garandhand goes, being a left hand posed around a
|
||||
// rifle that is not there.
|
||||
//
|
||||
// The other three reasons to hide still hide. Zoom and death
|
||||
// are not about whether a weapon is held, and
|
||||
// cg_drawviewmodel is the player asking for no view model at
|
||||
// all - none of those should be overridden here.
|
||||
//
|
||||
if ((cent->currentState.eFlags & EF_UNARMED) && cg_drawviewmodel->integer > 1
|
||||
&& !cg.snap->ps.stats[STAT_INZOOM] && cg.snap->ps.stats[STAT_HEALTH] > 0
|
||||
&& CG_VRHandsTracked()) {
|
||||
const int iGarandHand = cgi.Surface_NameToNum(model.tiki, "garandhand");
|
||||
|
||||
if (iGarandHand >= 0) {
|
||||
model.surfaces[iGarandHand] |= MDL_SURFACE_NODRAW;
|
||||
}
|
||||
|
||||
// Which sends the left hand to its own controller below,
|
||||
// exactly as it does when a weapon is held.
|
||||
pszVROffHandSurf = "lefthand";
|
||||
} else {
|
||||
// unarmed or zooming, hide the arms
|
||||
for (i = 0; i < MAX_MODEL_SURFACES; i++) {
|
||||
model.surfaces[i] |= MDL_SURFACE_NODRAW;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// show/hide the garand hand depending if it's a rifle or not
|
||||
@@ -1476,6 +1993,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 +2007,8 @@ void CG_ModelAnim(centity_t *cent, qboolean bDoShaderTime)
|
||||
if (iSurfaceNum >= 0) {
|
||||
model.surfaces[iSurfaceNum] &= ~MDL_SURFACE_NODRAW;
|
||||
}
|
||||
|
||||
pszVROffHandSurf = "lefthand";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1497,8 +2018,50 @@ 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)) {
|
||||
//
|
||||
// This copy is the weapon hand and nothing else.
|
||||
//
|
||||
// The other hand comes off it and is drawn again below on
|
||||
// its own controller. The sleeves come off too, which is
|
||||
// the part that is not obvious: viewsleeves is a single
|
||||
// mesh carrying *both* forearms - weighted 27% to
|
||||
// Bip01 R Forearm and 27% to Bip01 L Forearm, with both
|
||||
// elbows, shoulders and clavicles - so drawing it on the
|
||||
// hand that is anchored to the right controller hangs a
|
||||
// disembodied left sleeve in the air beside it.
|
||||
//
|
||||
// What is left is triggerhand, which is a whole hand and
|
||||
// not a stump: it is weighted to Bip01 R Hand, every R
|
||||
// finger bone, and helper Rwrist. The wrist is included,
|
||||
// the forearm is not, and there is no way to get part of
|
||||
// one without re-authoring the mesh - the forearm exists
|
||||
// only inside the two-armed sleeve.
|
||||
//
|
||||
bVRHandTracked = qtrue;
|
||||
|
||||
CG_VRShowOnlySurface(&model, "triggerhand");
|
||||
} else {
|
||||
if (cg.snap->ps.stats[STAT_HEALTH] > 0 && !cg_animationviewmodel->integer) {
|
||||
CG_OffsetFirstPersonView(&model, qfalse);
|
||||
}
|
||||
}
|
||||
|
||||
AnglesToAxis(cg.refdefViewAngles, cg.refdef.viewaxis);
|
||||
@@ -1520,6 +2083,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);
|
||||
|
||||
@@ -331,6 +331,13 @@ functions exported to the main executable
|
||||
int (*R_GetShaderWidth)(qhandle_t shader);
|
||||
int (*R_GetShaderHeight)(qhandle_t shader);
|
||||
void (*R_DrawBox)(float x, float y, float w, float h);
|
||||
// Added in OPM
|
||||
// A textured triangle at arbitrary screen coordinates, which is the
|
||||
// only way to draw anything rotated - R_DrawStretchPic is axis
|
||||
// aligned. The UI has had this all along as re.DrawTrianglePic and it
|
||||
// is how the retail compass turns its face; cgame needed it once the
|
||||
// compass moved here. See CG_VRHudRotatedPic.
|
||||
void (*R_DrawTrianglePic)(const vec2_t vPoints[3], const vec2_t vTexCoords[3], qhandle_t hShader);
|
||||
|
||||
// =========== data shared with the client =============
|
||||
void (*GetGameState)(gameState_t *gamestate);
|
||||
@@ -440,6 +447,40 @@ 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);
|
||||
|
||||
/*
|
||||
* The weapon wheel, for drawing it. True while it is held open, with
|
||||
* the segment the controller is pointing at - or -1 for the dead zone
|
||||
* in the middle, which chooses nothing.
|
||||
*/
|
||||
qboolean (*VR_GetWheel)(int *selection, const char **names, int maxNames, int *count);
|
||||
|
||||
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;
|
||||
@@ -726,6 +761,46 @@ void CG_EyeOffset(vec3_t *o_vOfs)
|
||||
|
||||
void CG_EyeAngles(vec3_t *o_vAngles)
|
||||
{
|
||||
//
|
||||
// Added in OPM
|
||||
//
|
||||
// These angles are what the *game* thinks the player is looking along -
|
||||
// they travel up in the usercmd as usereyes_t and end up as
|
||||
// Player::m_vViewAng, which is what the use trace is aimed with.
|
||||
//
|
||||
// cg.refdefViewAngles is the wrong source for that in VR. It does not
|
||||
// only hold the view: CG_OffsetFirstPersonView overwrites it with the
|
||||
// player model's animated *head bone* orientation on the paths that place
|
||||
// the view from the world position, and whatever is left there when this
|
||||
// is sampled is what the game gets. Measured on device, that left a pitch
|
||||
// pinned between 73 and 86 degrees - very nearly straight down - so every
|
||||
// use press traced into the floor about 77 units below the player and
|
||||
// caught whatever trigger happened to be underfoot. It worked often
|
||||
// enough to look intermittent rather than broken.
|
||||
//
|
||||
// The player state's view angles are the head's, and only the head's:
|
||||
// the client writes the headset's yaw and pitch into them every frame
|
||||
// (CL_VRMove), and nothing animated gets a say. No recoil kick or lean in
|
||||
// them either, which is right for reaching at a door handle.
|
||||
//
|
||||
// Flat play keeps the old source. There the two agree except for kick and
|
||||
// lean, and changing where a desktop player's use is aimed is not what
|
||||
// this is for.
|
||||
//
|
||||
{
|
||||
vec3_t vTracking, vHandAngles;
|
||||
float fBaseYaw = 0.0f;
|
||||
|
||||
if (cgi.VR_GetHandPose
|
||||
&& (cgi.VR_GetHandPose(0, vTracking, vHandAngles, &fBaseYaw)
|
||||
|| cgi.VR_GetHandPose(1, vTracking, vHandAngles, &fBaseYaw))) {
|
||||
(*o_vAngles)[0] = cg.predicted_player_state.viewangles[0];
|
||||
(*o_vAngles)[1] = cg.predicted_player_state.viewangles[1];
|
||||
(*o_vAngles)[2] = cg.predicted_player_state.viewangles[2];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
(*o_vAngles)[0] = cg.refdefViewAngles[0];
|
||||
(*o_vAngles)[1] = cg.refdefViewAngles[1];
|
||||
(*o_vAngles)[2] = cg.refdefViewAngles[2];
|
||||
|
||||
@@ -0,0 +1,797 @@
|
||||
/*
|
||||
===========================================================================
|
||||
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
|
||||
===========================================================================
|
||||
*/
|
||||
|
||||
// DESCRIPTION:
|
||||
// The heads up display, gathered into one group for VR.
|
||||
//
|
||||
// Added in OPM.
|
||||
//
|
||||
// The retail HUD is five separate menus loaded from ui/hud_*.urc - health,
|
||||
// ammo, compass, weapons, items - each aligned to a different corner of a 4:3
|
||||
// screen. On a monitor those corners are a glance away. In a headset they are
|
||||
// the far edge of the lens: technically visible, practically not, and no amount
|
||||
// of moving them solves it, because they are five widgets that each carry their
|
||||
// own frame, their own alignment, and their own 2D window.
|
||||
//
|
||||
// So this does not move them. It draws the same information again, in one
|
||||
// place, from the same source they read from.
|
||||
//
|
||||
// That source turns out to be almost nothing: every one of those widgets is a
|
||||
// formatter over cl.snap.ps.stats[], a flat array of thirty-two integers, and
|
||||
// the .urc files are pure layout. hud_health.urc is "playerstat 0, maxplayerstat
|
||||
// 1, draw it as a vertical bar with this shader". cgame has the same array as
|
||||
// cg.snap->ps.stats[], so nothing has to be read back out of the UI at all.
|
||||
//
|
||||
// Drawn from cgame rather than as another widget for one reason beyond taste:
|
||||
// cgame's 2D goes through the single full screen window that UIView3D::set2D
|
||||
// sets up, and R_VRAdjust2DRect converges that window as a whole. A widget
|
||||
// would arrive with its own window and land back in the bug that made the HUD
|
||||
// impossible to fuse in the first place.
|
||||
//
|
||||
// The compass was an upright ring with letters orbiting it to begin with,
|
||||
// because turning the retail card needs rotated geometry and cgame had no
|
||||
// import that could draw any. It read as a blank plate, which is what a compass
|
||||
// is without its dial, so R_DrawTrianglePic was added to the cgame imports - the
|
||||
// UI has always had it, as re.DrawTrianglePic, and it is what the retail compass
|
||||
// turns its own face with - and the three real layers are drawn here now.
|
||||
//
|
||||
// The damage ring and objective arrow share the card's origin and scale.
|
||||
|
||||
#include "cg_local.h"
|
||||
|
||||
//
|
||||
// The layout, in design units. The cluster is authored in a box this size and
|
||||
// then scaled to the display, so the numbers below can be read as a picture
|
||||
// rather than as a resolution.
|
||||
//
|
||||
// +------------------------------------------+
|
||||
// | (compass) Mission Objectives: |
|
||||
// | ring - take the bunker |
|
||||
// | - find the radio |
|
||||
// | HEALTH [====----] AMMO [======--] 8|64|
|
||||
// | M1 GARAND |
|
||||
// | [pistol][rifle][smg][mg] |
|
||||
// +------------------------------------------+
|
||||
//
|
||||
#define VRHUD_DESIGN_W 420.0f
|
||||
#define VRHUD_DESIGN_H 190.0f
|
||||
|
||||
// The design box is authored against a 1080 line display and scaled from there,
|
||||
// so it keeps the same angular size on any eye resolution.
|
||||
#define VRHUD_DESIGN_REF 1080.0f
|
||||
|
||||
#define VRHUD_RING_R 50.0f // compass ring radius
|
||||
#define VRHUD_RING_CX -150.0f
|
||||
#define VRHUD_RING_CY -50.0f
|
||||
|
||||
#define VRHUD_OBJ_X -92.0f // objectives column, left edge
|
||||
#define VRHUD_OBJ_TOP -92.0f
|
||||
|
||||
#define VRHUD_BAR_H 14.0f
|
||||
#define VRHUD_BAR_W 104.0f
|
||||
#define VRHUD_BAR_Y 26.0f
|
||||
#define VRHUD_HEALTH_X -196.0f
|
||||
#define VRHUD_AMMO_X 36.0f
|
||||
|
||||
#define VRHUD_NAME_Y 54.0f
|
||||
#define VRHUD_WEAP_Y 72.0f
|
||||
#define VRHUD_WEAP_W 42.0f
|
||||
#define VRHUD_WEAP_H 21.0f
|
||||
|
||||
// Text is drawn by the font at its own size, so it is scaled separately from
|
||||
// the boxes and bars - a 20 pixel font shrunk to fit a design box is not
|
||||
// legible through a lens.
|
||||
#define VRHUD_TEXT_SCALE 1.15f
|
||||
|
||||
// How long the group takes to go once its time is up. Long enough to read as a
|
||||
// fade rather than a cut, short enough not to sit in the way.
|
||||
#define VRHUD_FADE_MS 600.0f
|
||||
|
||||
typedef struct {
|
||||
qboolean registered;
|
||||
qhandle_t compassBack;
|
||||
qhandle_t compassFace;
|
||||
qhandle_t compassNeedle;
|
||||
qhandle_t compassObj;
|
||||
qhandle_t compassPain;
|
||||
qhandle_t weapPistol;
|
||||
qhandle_t weapRifle;
|
||||
qhandle_t weapSmg;
|
||||
qhandle_t weapMg;
|
||||
} vrHudMedia_t;
|
||||
|
||||
static vrHudMedia_t vrHud;
|
||||
static qboolean vrHudPainInitialized;
|
||||
static int vrHudPainStat;
|
||||
static int vrHudPainTime = -1;
|
||||
static int vrHudLastTime;
|
||||
|
||||
void CG_InitVRHud(void)
|
||||
{
|
||||
memset(&vrHud, 0, sizeof(vrHud));
|
||||
vrHudPainInitialized = qfalse;
|
||||
vrHudPainTime = -1;
|
||||
}
|
||||
|
||||
void CG_UpdateVRHud(void)
|
||||
{
|
||||
if (!cg.snap || (cg.snap->ps.pm_flags & PMF_INTERMISSION)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int stat = cg.snap->ps.stats[STAT_DAMAGEDIR];
|
||||
|
||||
// Observe hits even while the requested HUD is hidden. Otherwise opening
|
||||
// it replays the last hit, however long ago it happened.
|
||||
if (!vrHudPainInitialized || cg.time < vrHudLastTime) {
|
||||
vrHudPainInitialized = qtrue;
|
||||
vrHudPainTime = -1;
|
||||
} else if (stat != vrHudPainStat) {
|
||||
vrHudPainTime = cg.time;
|
||||
}
|
||||
vrHudPainStat = stat;
|
||||
vrHudLastTime = cg.time;
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
CG_VRHudRegister
|
||||
|
||||
Deliberately lazy rather than in CG_RegisterGraphics. This draws nothing until
|
||||
the player is in a map with a snapshot, by which time the shaders are loadable;
|
||||
registering from the media pass would put these in front of every level load
|
||||
whether the HUD is switched on or not.
|
||||
|
||||
CG_InitVRHud drops the handles when a map or subsystem restarts. A map's start
|
||||
time is not a renderer sequence: two loads can have the same start time.
|
||||
================
|
||||
*/
|
||||
static void CG_VRHudRegister(void)
|
||||
{
|
||||
if (vrHud.registered) {
|
||||
return;
|
||||
}
|
||||
|
||||
vrHud.compassBack = cgi.R_RegisterShaderNoMip("textures/hud/compassback");
|
||||
vrHud.compassFace = cgi.R_RegisterShaderNoMip("textures/hud/compassface");
|
||||
vrHud.compassNeedle = cgi.R_RegisterShaderNoMip("textures/hud/compassneedle");
|
||||
vrHud.compassObj = cgi.R_RegisterShaderNoMip("textures/hud/compassobjarrow");
|
||||
vrHud.compassPain = cgi.R_RegisterShaderNoMip("textures/hud/compasspain");
|
||||
vrHud.weapPistol = cgi.R_RegisterShaderNoMip("textures/hud/weap_pistol");
|
||||
vrHud.weapRifle = cgi.R_RegisterShaderNoMip("textures/hud/weap_rifle");
|
||||
vrHud.weapSmg = cgi.R_RegisterShaderNoMip("textures/hud/weap_smg");
|
||||
vrHud.weapMg = cgi.R_RegisterShaderNoMip("textures/hud/weap_mg");
|
||||
|
||||
vrHud.registered = qtrue;
|
||||
}
|
||||
|
||||
//
|
||||
// The mapping from design units to pixels for this frame. Held in file statics
|
||||
// because every helper below needs them and threading them through each call
|
||||
// would be four more arguments saying the same thing.
|
||||
//
|
||||
static float vrHudScale; // pixels per design unit
|
||||
static float vrHudOriginX; // pixels, centre of the cluster
|
||||
static float vrHudOriginY;
|
||||
|
||||
static float CG_VRHudX(float designX)
|
||||
{
|
||||
return vrHudOriginX + designX * vrHudScale;
|
||||
}
|
||||
|
||||
static float CG_VRHudY(float designY)
|
||||
{
|
||||
return vrHudOriginY + designY * vrHudScale;
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
CG_VRHudFill
|
||||
|
||||
A rectangle in design units, in a colour. Everything here that is not a piece of
|
||||
retail art is one of these - bars, pips, the marker on the compass, the backing
|
||||
plate - because a flat quad needs no asset to go looking for and stays legible
|
||||
at any size.
|
||||
|
||||
Drawn with R_DrawBox, which is untextured and takes the colour straight from
|
||||
R_SetColor, rather than by stretching a white shader.
|
||||
|
||||
That was the first attempt and it was wrong twice over. gfx/2d/blank - the
|
||||
obvious candidate, and what cgame already registers for the lagometer - is
|
||||
declared in scripts/gfx.shader as
|
||||
|
||||
alphaGen oneMinusVertex
|
||||
rgbGen vertex
|
||||
|
||||
and that alphaGen is not a detail. It inverts the alpha: a bar asked for at
|
||||
alpha 1 comes out at 0 and a backing plate asked for at 0.35 comes out at 0.65,
|
||||
so the things meant to be solid were the faintest and the thing meant to be
|
||||
faint was the most solid. Anything else built to be tinted has its own opinion
|
||||
of the same sort. R_DrawBox has none - it is qglColor4ubv and a quad.
|
||||
================
|
||||
*/
|
||||
static void CG_VRHudFill(float x, float y, float w, float h, float r, float g, float b, float a)
|
||||
{
|
||||
vec4_t color;
|
||||
|
||||
color[0] = r;
|
||||
color[1] = g;
|
||||
color[2] = b;
|
||||
color[3] = a;
|
||||
|
||||
cgi.R_SetColor(color);
|
||||
cgi.R_DrawBox(CG_VRHudX(x), CG_VRHudY(y), w * vrHudScale, h * vrHudScale);
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
CG_VRHudPic
|
||||
|
||||
A shader stretched over a rectangle in design units, at full brightness or
|
||||
dimmed. Dimming rather than hiding is what lets the weapon strip show which
|
||||
slots exist as well as which are filled.
|
||||
================
|
||||
*/
|
||||
static void CG_VRHudPic(float x, float y, float w, float h, qhandle_t shader, float bright, float alpha)
|
||||
{
|
||||
vec4_t color;
|
||||
|
||||
if (!shader) {
|
||||
return;
|
||||
}
|
||||
|
||||
color[0] = bright;
|
||||
color[1] = bright;
|
||||
color[2] = bright;
|
||||
color[3] = alpha;
|
||||
|
||||
cgi.R_SetColor(color);
|
||||
cgi.R_DrawStretchPic(
|
||||
CG_VRHudX(x), CG_VRHudY(y), w * vrHudScale, h * vrHudScale, 0.0, 0.0, 1.0, 1.0, shader
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
CG_VRHudRotatedPic
|
||||
|
||||
A square of art centred on a point in design units, turned clockwise by an angle
|
||||
in degrees.
|
||||
|
||||
R_DrawStretchPic cannot do this - it takes a rectangle and draws it square to
|
||||
the screen - so the quad is built by hand as two triangles and handed to
|
||||
R_DrawTrianglePic, which is the same call the retail compass uses to turn its
|
||||
own face.
|
||||
|
||||
Screen space has y running down, so a clockwise turn is
|
||||
|
||||
x' = dx cos - dy sin
|
||||
y' = dx sin + dy cos
|
||||
|
||||
which sends the top corner to the right, as a compass card should when the
|
||||
player turns to the left.
|
||||
================
|
||||
*/
|
||||
static void CG_VRHudRotatedPic(float cx, float cy, float radius, float degrees, qhandle_t shader, float alpha)
|
||||
{
|
||||
static const float corner[4][2] = {
|
||||
{-1.0f, -1.0f},
|
||||
{ 1.0f, -1.0f},
|
||||
{-1.0f, 1.0f},
|
||||
{ 1.0f, 1.0f}
|
||||
};
|
||||
static const float uv[4][2] = {
|
||||
{0.0f, 0.0f},
|
||||
{1.0f, 0.0f},
|
||||
{0.0f, 1.0f},
|
||||
{1.0f, 1.0f}
|
||||
};
|
||||
vec2_t pts[4];
|
||||
vec2_t tri[3], triUV[3];
|
||||
vec4_t color;
|
||||
float s, c;
|
||||
int i;
|
||||
|
||||
if (!shader) {
|
||||
return;
|
||||
}
|
||||
|
||||
s = sin(DEG2RAD(degrees));
|
||||
c = cos(DEG2RAD(degrees));
|
||||
|
||||
for (i = 0; i < 4; i++) {
|
||||
const float dx = corner[i][0] * radius;
|
||||
const float dy = corner[i][1] * radius;
|
||||
|
||||
pts[i][0] = CG_VRHudX(cx + (dx * c - dy * s));
|
||||
pts[i][1] = CG_VRHudY(cy + (dx * s + dy * c));
|
||||
}
|
||||
|
||||
color[0] = color[1] = color[2] = 1.0f;
|
||||
color[3] = alpha;
|
||||
cgi.R_SetColor(color);
|
||||
|
||||
// Two triangles, wound the same way round: 0-1-2 and 1-3-2.
|
||||
for (i = 0; i < 2; i++) {
|
||||
static const int index[2][3] = {
|
||||
{0, 1, 2},
|
||||
{1, 3, 2}
|
||||
};
|
||||
int k;
|
||||
|
||||
for (k = 0; k < 3; k++) {
|
||||
tri[k][0] = pts[index[i][k]][0];
|
||||
tri[k][1] = pts[index[i][k]][1];
|
||||
triUV[k][0] = uv[index[i][k]][0];
|
||||
triUV[k][1] = uv[index[i][k]][1];
|
||||
}
|
||||
|
||||
cgi.R_DrawTrianglePic(tri, triUV, shader);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
CG_VRHudText
|
||||
|
||||
A string with its top left at a point in design units.
|
||||
|
||||
R_DrawString multiplies both the glyph size and the position by the scale vector
|
||||
it is handed, so the position has to be divided back out by it - which is why
|
||||
the coordinates given here are not the coordinates it receives. The same idiom
|
||||
is in CG_DrawObjectives.
|
||||
================
|
||||
*/
|
||||
static void CG_VRHudText(float x, float y, const char *text, float r, float g, float b, float a)
|
||||
{
|
||||
vec4_t color;
|
||||
vec2_t textScale;
|
||||
float s;
|
||||
|
||||
if (!text || !text[0] || !cgs.media.objectiveFont) {
|
||||
return;
|
||||
}
|
||||
|
||||
s = vrHudScale * VRHUD_TEXT_SCALE;
|
||||
textScale[0] = s;
|
||||
textScale[1] = s;
|
||||
|
||||
color[0] = r;
|
||||
color[1] = g;
|
||||
color[2] = b;
|
||||
color[3] = a;
|
||||
|
||||
cgi.R_SetColor(color);
|
||||
cgi.R_DrawString(cgs.media.objectiveFont, text, CG_VRHudX(x) / s, CG_VRHudY(y) / s, -1, textScale);
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
CG_VRHudBar
|
||||
|
||||
A meter: a dark trough with a coloured fill and a thin outline, in design units.
|
||||
|
||||
Drawn as flat quads rather than with textures/hud/healthmeter because the retail
|
||||
meters are tall narrow art built to stand on end at the edge of a screen, and
|
||||
this cluster wants them lying down next to each other.
|
||||
================
|
||||
*/
|
||||
static void CG_VRHudBar(float x, float y, float frac, float r, float g, float b, float alpha)
|
||||
{
|
||||
if (frac < 0.0f) {
|
||||
frac = 0.0f;
|
||||
} else if (frac > 1.0f) {
|
||||
frac = 1.0f;
|
||||
}
|
||||
|
||||
CG_VRHudFill(x - 1.0f, y - 1.0f, VRHUD_BAR_W + 2.0f, VRHUD_BAR_H + 2.0f, 0.0f, 0.0f, 0.0f, alpha * 0.55f);
|
||||
CG_VRHudFill(x, y, VRHUD_BAR_W, VRHUD_BAR_H, 0.12f, 0.12f, 0.10f, alpha * 0.85f);
|
||||
|
||||
if (frac > 0.0f) {
|
||||
CG_VRHudFill(x, y, VRHUD_BAR_W * frac, VRHUD_BAR_H, r, g, b, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
CG_VRHudCompass
|
||||
|
||||
An upright ring with a turning compass card, damage ring and objective arrow.
|
||||
|
||||
Two angles do all of it, and both are decoded exactly the way the retail widgets
|
||||
decode them, so they agree with the game rather than with a guess:
|
||||
|
||||
- Heading. UIFakkLabel::DrawStatCompass takes the eye angles and subtracts
|
||||
stats[STAT_COMPASSNORTH] / 182, which is a sixteen bit angle - 65536 over
|
||||
360. That difference is how far the player is turned from the map's north.
|
||||
- The objective. hud_compass.urc draws it as "statbar spinner 0 3600" with
|
||||
end angles -180 to 180, and DrawStatSpinner turns that into
|
||||
stat / 3600 * 360 - 180 degrees. It is a plain spinner and not a
|
||||
headingspinner, which is the variant that subtracts the view angles - so
|
||||
the game has already made it relative to where the player is looking, and
|
||||
it can be used as a screen angle directly.
|
||||
|
||||
Screen angles here are measured clockwise from the top of the ring. A direction
|
||||
whose world yaw is a sits at yaw - a, because yaw increases anticlockwise in
|
||||
this engine and clockwise is what a compass face reads.
|
||||
================
|
||||
*/
|
||||
static void CG_VRHudCompass(float alpha)
|
||||
{
|
||||
vec3_t eyeAngles;
|
||||
float heading;
|
||||
|
||||
//
|
||||
// Three layers, the same three and in the same order as hud_compass.urc:
|
||||
// the brass plate, the card turning on it, and the needle over the top.
|
||||
//
|
||||
CG_VRHudPic(
|
||||
VRHUD_RING_CX - VRHUD_RING_R,
|
||||
VRHUD_RING_CY - VRHUD_RING_R,
|
||||
VRHUD_RING_R * 2.0f,
|
||||
VRHUD_RING_R * 2.0f,
|
||||
vrHud.compassBack,
|
||||
1.0f,
|
||||
alpha
|
||||
);
|
||||
|
||||
CG_EyeAngles(&eyeAngles);
|
||||
heading = eyeAngles[YAW] - cg.snap->ps.stats[STAT_COMPASSNORTH] / 182.0f;
|
||||
|
||||
//
|
||||
// The card, turned clockwise by the heading.
|
||||
//
|
||||
// North is at the top of the texture, so turning the whole face by the
|
||||
// heading puts north wherever north actually is - and because the engine's
|
||||
// yaw increases anticlockwise while a compass reads clockwise, turning the
|
||||
// player to the left walks the card to the right, which is what a compass
|
||||
// held in the hand does.
|
||||
//
|
||||
CG_VRHudRotatedPic(
|
||||
VRHUD_RING_CX, VRHUD_RING_CY, VRHUD_RING_R * 0.94f, heading, vrHud.compassFace, alpha
|
||||
);
|
||||
|
||||
//
|
||||
// Which way the last hit came from, fading out over a second and a half.
|
||||
//
|
||||
// The retail ring is "statbar headingspinner 0 3600" over stats
|
||||
// [STAT_DAMAGEDIR], and headingspinner is the variant that does subtract
|
||||
// the view angles - unlike the objective below - so the direction is in
|
||||
// world terms and has to be brought round to the head. DrawStatSpinner
|
||||
// does it as AngleSubtract(yaw, raw*360 + 180), and the +180 is because the
|
||||
// stat points at the attacker while the ring marks where the damage
|
||||
// arrived.
|
||||
//
|
||||
// The fade is its own timer rather than anything the stat carries: the
|
||||
// widget starts it whenever the value changes and runs it down over 1500ms.
|
||||
//
|
||||
{
|
||||
const int stat = cg.snap->ps.stats[STAT_DAMAGEDIR];
|
||||
|
||||
if (vrHudPainTime >= 0 && cg.time - vrHudPainTime < 1500) {
|
||||
const float fade = 1.0f - (cg.time - vrHudPainTime) / 1500.0f;
|
||||
float frac = (AngleSubtract(eyeAngles[YAW], (stat / 3600.0f) * 360.0f + 180.0f) + 180.0f) / 360.0f;
|
||||
|
||||
if (frac >= 0.0f && frac <= 1.0f) {
|
||||
CG_VRHudRotatedPic(
|
||||
VRHUD_RING_CX,
|
||||
VRHUD_RING_CY,
|
||||
VRHUD_RING_R * 0.94f,
|
||||
frac * 360.0f - 180.0f,
|
||||
vrHud.compassPain,
|
||||
alpha * fade
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The objective, before the needle so the needle stays on top of it.
|
||||
//
|
||||
// stats[STAT_OBJECTIVECENTER] is already relative to where the player is
|
||||
// looking - hud_compass.urc draws it as a plain spinner rather than the
|
||||
// headingspinner variant, and it is the headingspinner that subtracts the
|
||||
// view angles - so this is a screen angle as it stands. The decode is the
|
||||
// one DrawStatSpinner does for "statbar spinner 0 3600" with end angles
|
||||
// -180 to 180.
|
||||
//
|
||||
if (cg.snap->ps.stats[STAT_OBJECTIVECENTER]) {
|
||||
CG_VRHudRotatedPic(
|
||||
VRHUD_RING_CX,
|
||||
VRHUD_RING_CY,
|
||||
VRHUD_RING_R * 0.94f,
|
||||
cg.snap->ps.stats[STAT_OBJECTIVECENTER] / 10.0f - 180.0f,
|
||||
vrHud.compassObj,
|
||||
alpha
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// The needle, which does not move. It marks the way the player is facing,
|
||||
// and everything else turns underneath it.
|
||||
//
|
||||
CG_VRHudPic(
|
||||
VRHUD_RING_CX - VRHUD_RING_R,
|
||||
VRHUD_RING_CY - VRHUD_RING_R,
|
||||
VRHUD_RING_R * 2.0f,
|
||||
VRHUD_RING_R * 2.0f,
|
||||
vrHud.compassNeedle,
|
||||
1.0f,
|
||||
alpha
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
CG_VRHudWeapons
|
||||
|
||||
The four weapon classes, dim for a slot the player does not have and lit for one
|
||||
they do, with the equipped class boxed.
|
||||
|
||||
stats[STAT_WEAPONS] is a bitmask of WEAPON_CLASS_*, and stats[STAT_EQUIPPED_WEAPON]
|
||||
is the same bitmask with the class in hand set - which is how the retail bar in
|
||||
cl_ui.cpp decides what to enable, only it does it by passing enable and disable
|
||||
events to named widgets in hud_weapons.urc.
|
||||
================
|
||||
*/
|
||||
static void CG_VRHudWeapons(float alpha)
|
||||
{
|
||||
const int owned = cg.snap->ps.stats[STAT_WEAPONS];
|
||||
const int equipped = cg.snap->ps.stats[STAT_EQUIPPED_WEAPON];
|
||||
int i;
|
||||
|
||||
struct {
|
||||
int bit;
|
||||
qhandle_t shader;
|
||||
} slots[4] = {
|
||||
{WEAPON_CLASS_PISTOL, vrHud.weapPistol},
|
||||
{WEAPON_CLASS_RIFLE, vrHud.weapRifle },
|
||||
{WEAPON_CLASS_SMG, vrHud.weapSmg },
|
||||
{WEAPON_CLASS_MG, vrHud.weapMg },
|
||||
};
|
||||
|
||||
for (i = 0; i < 4; i++) {
|
||||
const float x = (i - 2) * (VRHUD_WEAP_W + 6.0f) + 3.0f;
|
||||
const qboolean have = (owned & slots[i].bit) ? qtrue : qfalse;
|
||||
|
||||
if (equipped & slots[i].bit) {
|
||||
CG_VRHudFill(
|
||||
x - 2.0f, VRHUD_WEAP_Y - 2.0f, VRHUD_WEAP_W + 4.0f, VRHUD_WEAP_H + 4.0f,
|
||||
1.0f, 0.85f, 0.2f, alpha * 0.35f
|
||||
);
|
||||
}
|
||||
|
||||
CG_VRHudPic(
|
||||
x, VRHUD_WEAP_Y, VRHUD_WEAP_W, VRHUD_WEAP_H, slots[i].shader,
|
||||
have ? 1.0f : 0.35f, have ? alpha : alpha * 0.4f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
CG_DrawVRHud
|
||||
|
||||
One cluster, in the middle of the view, carrying everything the corners used to.
|
||||
|
||||
Placed and sized entirely from cvars so it can be dialled in from autoexec.cfg
|
||||
with a push and a force-stop: there is no console in a headset, and a HUD is
|
||||
exactly the kind of thing that can only be judged with it on.
|
||||
================
|
||||
*/
|
||||
void CG_DrawVRHud(void)
|
||||
{
|
||||
float alpha;
|
||||
float fade;
|
||||
float health, maxHealth;
|
||||
float clip, maxClip;
|
||||
int i;
|
||||
float objY;
|
||||
|
||||
if (!vr_hudPass || !vr_hudPass->integer || !vr_hud || !vr_hud->integer
|
||||
|| !cg_hud || !cg_hud->integer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cg.snap || (cg.snap->ps.pm_flags & (PMF_NO_HUD | PMF_INTERMISSION))) {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// Up on request, not all the time.
|
||||
//
|
||||
// A HUD that is always there is always in the way, and almost all of what
|
||||
// this one carries is only wanted at the moment it is asked for - how much
|
||||
// ammo is left, which way the objective is. So it is hidden, and the off
|
||||
// hand's X button brings it up for vr_hudTime seconds before it fades.
|
||||
//
|
||||
// vr_hudShow is a timestamp the VR layer writes when the button goes down,
|
||||
// on the same clock cgi.Milliseconds reads, so nothing has to be latched
|
||||
// or cleared here and a press during the fade simply starts it again.
|
||||
//
|
||||
// vr_hud 2 pins it up, which is what to use while arguing with the layout.
|
||||
//
|
||||
fade = 1.0f;
|
||||
|
||||
if (vr_hud->integer != 2) {
|
||||
const float hold = (vr_hudTime ? vr_hudTime->value : 3.0f) * 1000.0f;
|
||||
const int shown = vr_hudShow ? vr_hudShow->integer : 0;
|
||||
const int since = cgi.Milliseconds() - shown;
|
||||
|
||||
if (!shown || since < 0 || since > hold + VRHUD_FADE_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (since > hold) {
|
||||
fade = 1.0f - (since - hold) / VRHUD_FADE_MS;
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to report before there is a player to report on, and dead men
|
||||
// read no compasses.
|
||||
if (cg.snap->ps.stats[STAT_HEALTH] <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
alpha = vr_hudAlpha ? vr_hudAlpha->value : 1.0f;
|
||||
if (alpha > 1.0f) {
|
||||
alpha = 1.0f;
|
||||
}
|
||||
|
||||
alpha *= fade;
|
||||
|
||||
if (alpha <= 0.01f) {
|
||||
return;
|
||||
}
|
||||
|
||||
CG_VRHudRegister();
|
||||
|
||||
//
|
||||
// Design units to pixels. The reference height keeps the cluster the same
|
||||
// angular size whatever the eye buffer turns out to be; the anchor is a
|
||||
// fraction of the display so it means the same thing there too.
|
||||
//
|
||||
vrHudScale = (cgs.glconfig.vidHeight / VRHUD_DESIGN_REF)
|
||||
* (vr_hudGroupScale ? vr_hudGroupScale->value : 1.0f);
|
||||
if (vrHudScale < 0.1f) {
|
||||
vrHudScale = 0.1f;
|
||||
}
|
||||
|
||||
vrHudOriginX = cgs.glconfig.vidWidth * (vr_hudGroupX ? vr_hudGroupX->value : 0.5f);
|
||||
vrHudOriginY = cgs.glconfig.vidHeight * (vr_hudGroupY ? vr_hudGroupY->value : 0.92f);
|
||||
|
||||
//
|
||||
// A backing plate, so the whole group reads as one object against whatever
|
||||
// the world happens to be doing behind it. Without it the text disappears
|
||||
// over snow and the bars disappear over mud.
|
||||
//
|
||||
CG_VRHudFill(
|
||||
-VRHUD_DESIGN_W * 0.5f,
|
||||
-VRHUD_DESIGN_H * 0.5f,
|
||||
VRHUD_DESIGN_W,
|
||||
VRHUD_DESIGN_H,
|
||||
0.0f,
|
||||
0.0f,
|
||||
0.0f,
|
||||
alpha * 0.35f
|
||||
);
|
||||
|
||||
CG_VRHudCompass(alpha);
|
||||
|
||||
//
|
||||
// Objectives. The text is already gathered by CG_DrawObjectives from
|
||||
// cg.Objectives[], which the configstrings fill; this only lays it out
|
||||
// narrower and next to everything else.
|
||||
//
|
||||
objY = VRHUD_OBJ_TOP;
|
||||
CG_VRHudText(VRHUD_OBJ_X, objY, cgi.LV_ConvertString("Mission Objectives:"), 0.85f, 0.75f, 0.2f, alpha);
|
||||
objY += 20.0f;
|
||||
|
||||
for (i = 0; i < MAX_OBJECTIVES; i++) {
|
||||
if (cg.Objectives[i].flags == OBJ_FLAG_NONE || (cg.Objectives[i].flags & OBJ_FLAG_HIDDEN)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cg.Objectives[i].flags & OBJ_FLAG_COMPLETED) {
|
||||
CG_VRHudText(VRHUD_OBJ_X, objY, "x", 0.6f, 0.6f, 0.6f, alpha);
|
||||
CG_VRHudText(
|
||||
VRHUD_OBJ_X + 14.0f, objY, cgi.LV_ConvertString(cg.Objectives[i].text), 0.6f, 0.6f, 0.6f, alpha
|
||||
);
|
||||
} else {
|
||||
CG_VRHudText(VRHUD_OBJ_X, objY, "-", 1.0f, 1.0f, 1.0f, alpha);
|
||||
CG_VRHudText(
|
||||
VRHUD_OBJ_X + 14.0f, objY, cgi.LV_ConvertString(cg.Objectives[i].text), 1.0f, 1.0f, 1.0f, alpha
|
||||
);
|
||||
}
|
||||
|
||||
objY += 17.0f;
|
||||
|
||||
// The cluster is a fixed box and a level can set a great many
|
||||
// objectives; the ones that do not fit are better dropped than drawn
|
||||
// through the health bar.
|
||||
if (objY > VRHUD_BAR_Y - 22.0f) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Health. stats[0] over stats[1], which is what hud_health.urc reads.
|
||||
//
|
||||
health = (float)cg.snap->ps.stats[STAT_HEALTH];
|
||||
maxHealth = (float)cg.snap->ps.stats[STAT_MAXHEALTH];
|
||||
if (maxHealth < 1.0f) {
|
||||
maxHealth = 100.0f;
|
||||
}
|
||||
|
||||
{
|
||||
const float frac = Com_Clamp(0.0f, 1.0f, health / maxHealth);
|
||||
|
||||
CG_VRHudText(VRHUD_HEALTH_X, VRHUD_BAR_Y - 17.0f, "HEALTH", 0.7f, 0.6f, 0.05f, alpha);
|
||||
// Green through amber to red, so the colour says as much as the length.
|
||||
CG_VRHudBar(
|
||||
VRHUD_HEALTH_X, VRHUD_BAR_Y, frac, frac > 0.5f ? (2.0f - frac * 2.0f) : 1.0f,
|
||||
Com_Clamp(0.0f, 1.0f, frac * 1.6f), 0.12f, alpha
|
||||
);
|
||||
CG_VRHudText(
|
||||
VRHUD_HEALTH_X + VRHUD_BAR_W + 8.0f, VRHUD_BAR_Y - 2.0f, va("%d", (int)health), 1.0f, 1.0f, 1.0f, alpha
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Ammo. The clip bar is stats[6] over stats[7]; the numbers beside it are
|
||||
// the clip and the reserve, which is how every hud_ammo_*.urc reads them.
|
||||
//
|
||||
clip = (float)cg.snap->ps.stats[STAT_CLIPAMMO];
|
||||
maxClip = (float)cg.snap->ps.stats[STAT_MAXCLIPAMMO];
|
||||
|
||||
if (maxClip >= 1.0f) {
|
||||
CG_VRHudText(VRHUD_AMMO_X, VRHUD_BAR_Y - 17.0f, "AMMO", 0.7f, 0.6f, 0.05f, alpha);
|
||||
CG_VRHudBar(VRHUD_AMMO_X, VRHUD_BAR_Y, clip / maxClip, 0.95f, 0.8f, 0.25f, alpha);
|
||||
CG_VRHudText(
|
||||
VRHUD_AMMO_X + VRHUD_BAR_W + 8.0f,
|
||||
VRHUD_BAR_Y - 2.0f,
|
||||
va("%d|%d", (int)clip, cg.snap->ps.stats[STAT_AMMO]),
|
||||
1.0f,
|
||||
1.0f,
|
||||
1.0f,
|
||||
alpha
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// What is in the player's hands. activeItems[ITEM_WEAPON] indexes the
|
||||
// weapon configstrings, which is where cg_viewmodelanim.c gets the same
|
||||
// name from.
|
||||
//
|
||||
if (cg.snap->ps.activeItems[ITEM_WEAPON] >= 0) {
|
||||
const char *name = CG_ConfigString(CS_WEAPONS + cg.snap->ps.activeItems[ITEM_WEAPON]);
|
||||
|
||||
if (name && name[0]) {
|
||||
CG_VRHudText(-40.0f, VRHUD_NAME_Y, name, 0.7f, 0.6f, 0.05f, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
CG_VRHudWeapons(alpha);
|
||||
|
||||
cgi.R_SetColor(NULL);
|
||||
}
|
||||
@@ -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,60 @@ 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_VR_GetWheel
|
||||
|
||||
The weapon wheel's state, so cgame can draw it. Nothing but a pass through -
|
||||
the wheel is tracked where the controller is read.
|
||||
====================
|
||||
*/
|
||||
static qboolean CL_VR_GetWheel( int *selection, const char **names, int maxNames, int *count ) {
|
||||
#if defined(USE_OPENXR)
|
||||
return VR_GetWheel( selection, names, maxNames, count );
|
||||
#else
|
||||
return qfalse;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
====================
|
||||
CL_GetShaderPointer
|
||||
@@ -747,6 +804,8 @@ void CL_InitCGameDLL( clientGameImport_t *cgi, clientGameExport_t **cge ) {
|
||||
cgi->R_GetShaderHeight = re.GetShaderHeight;
|
||||
|
||||
cgi->R_DrawBox = re.DrawBox;
|
||||
// Added in OPM, so the gathered VR HUD can turn the compass face.
|
||||
cgi->R_DrawTrianglePic = re.DrawTrianglePic;
|
||||
cgi->R_SwipeBegin = re.SwipeBegin;
|
||||
cgi->R_SwipePoint = re.SwipePoint;
|
||||
cgi->R_SwipeEnd = re.SwipeEnd;
|
||||
@@ -827,6 +886,9 @@ 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->VR_GetWheel = CL_VR_GetWheel;
|
||||
|
||||
cgi->fsDebug = fs_debug;
|
||||
cgi->HudDrawElements = cls.HudDrawElements;
|
||||
@@ -909,6 +971,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,162 @@ 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 );
|
||||
}
|
||||
|
||||
// The grips hold rungs on a ladder rather than steadying the weapon and
|
||||
// reloading, and the VR layer cannot see the player state to know that.
|
||||
VR_SetClimbing( ( cl.snap.valid && cl.snap.ps.pm_type == PM_CLIMBWALL ) ? qtrue : qfalse );
|
||||
|
||||
// And whether there is anything in the player's hand, which decides
|
||||
// whether a grip closing on a holster draws from it or puts back into it.
|
||||
VR_SetArmed( ( cl.snap.valid && cl.snap.ps.activeItems[1] >= 0 ) ? qtrue : qfalse );
|
||||
|
||||
// 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 +913,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 +1382,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,103 @@ 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" : "3" );
|
||||
|
||||
for( eye = 0; eye < VR_MAX_EYES; eye++ ) {
|
||||
if( !VR_PrepareEye( eye ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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 );
|
||||
|
||||
+141
-2
@@ -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>
|
||||
@@ -120,6 +123,13 @@ cvar_t *ui_gmboxspam;
|
||||
cvar_t *ui_debugload;
|
||||
cvar_t *sound_overlay;
|
||||
cvar_t *ui_compass_scale;
|
||||
// Added in OPM
|
||||
// Where the compass sits, as fractions of the flat screen. hud_compass.urc
|
||||
// aligns it left top, which in a headset is the far corner of the lens - it is
|
||||
// there, and it is barely visible. See UI_PlaceVRCompass.
|
||||
cvar_t *vr_compassX;
|
||||
cvar_t *vr_compassY;
|
||||
static cvar_t *ui_vrHud;
|
||||
|
||||
static intro_stage_t intro_stage;
|
||||
static char server_mapname[64];
|
||||
@@ -1818,10 +1828,78 @@ UI_Update
|
||||
Updates the UI.
|
||||
====================
|
||||
*/
|
||||
/*
|
||||
====================
|
||||
UI_PlaceVRCompass
|
||||
|
||||
Added in OPM.
|
||||
|
||||
hud_compass.urc aligns the compass left top, so it is drawn at 0,0. On a flat
|
||||
screen that is a corner you can glance at; in a headset it is the far edge of
|
||||
the lens, where it is technically visible and practically not.
|
||||
|
||||
Centred horizontally and set a little below the middle, which is where a thing
|
||||
you check without breaking your aim wants to be. Fractions of the screen rather
|
||||
than pixels so it means the same at any eye resolution, and the widget's own
|
||||
size is taken off so the fraction places its centre rather than its corner.
|
||||
|
||||
Applied only when the numbers change. setFrame propagates the coordinate system
|
||||
and fires signals, which is not something to do every frame for a widget that is
|
||||
not moving - and the alignment flags are cleared at the same time, so nothing
|
||||
puts it back.
|
||||
====================
|
||||
*/
|
||||
static void UI_PlaceVRCompass(void)
|
||||
{
|
||||
UIWidget *widget;
|
||||
UIRect2D frame;
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
if (!VR_Enabled()) {
|
||||
return;
|
||||
}
|
||||
#else
|
||||
return;
|
||||
#endif
|
||||
|
||||
if (!hud_compass || !vr_compassX || !vr_compassY) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget = hud_compass->GetContainerWidget();
|
||||
|
||||
//
|
||||
// The alignment flags have to go first, or none of this survives.
|
||||
// hud_compass.urc says "align left top", and UIWidget::AlignPosition
|
||||
// turns that into a flat "pos.x = 0; pos.y = 0" every time it runs -
|
||||
// which it does on Realign and ResetMotion, after this. Setting the frame
|
||||
// without clearing the flags puts the compass back in the corner a moment
|
||||
// later, which is exactly what it did.
|
||||
//
|
||||
widget->setAlign(0);
|
||||
|
||||
frame = widget->getFrame();
|
||||
|
||||
// Compare the actual frame so a resize or recreated menu is repositioned
|
||||
// even when the cvars have not changed. Avoid relayout for an unchanged frame.
|
||||
const float x = uid.vidWidth * vr_compassX->value - frame.size.width * 0.5f;
|
||||
const float y = uid.vidHeight * vr_compassY->value - frame.size.height * 0.5f;
|
||||
if (frame.pos.x != x || frame.pos.y != y) {
|
||||
frame.pos.x = x;
|
||||
frame.pos.y = y;
|
||||
widget->setFrame(frame);
|
||||
}
|
||||
}
|
||||
|
||||
void UI_Update(void)
|
||||
{
|
||||
Menu *currentMenu;
|
||||
UIRect2D frame;
|
||||
qboolean vrHudActive = qfalse;
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
vrHudActive = VR_Enabled() && ui_vrHud && ui_vrHud->integer;
|
||||
#endif
|
||||
|
||||
re.SetRenderTime(cls.realtime);
|
||||
CL_FillUIDef();
|
||||
@@ -1835,7 +1913,30 @@ void UI_Update(void)
|
||||
frame = uWinMan.getFrame();
|
||||
view3d->Display(frame, 1.0);
|
||||
|
||||
if (ui_hud && !view3d->LetterboxActive()) {
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// The retail corner HUD, off while the gathered one is up.
|
||||
//
|
||||
// These five menus are aligned to the four corners of a 4:3 screen -
|
||||
// hud_health.urc says "align left bottom", hud_compass.urc "align left
|
||||
// top". On a monitor that is a glance away; through a lens it is the
|
||||
// far edge, where they were reported as visible and unreadable. They
|
||||
// are drawn again, together and in the middle, by CG_DrawVRHud, and
|
||||
// leaving these up as well would draw everything twice.
|
||||
//
|
||||
// Read the current cvar value so the retail HUD can be restored
|
||||
// without a rebuild. Desktop frames always keep their own HUD.
|
||||
//
|
||||
if (ui_hud && !view3d->LetterboxActive() && !vrHudActive) {
|
||||
// draw the health hud
|
||||
if (hud_health) {
|
||||
hud_health->ForceShow();
|
||||
@@ -1969,8 +2070,23 @@ void UI_Update(void)
|
||||
}
|
||||
|
||||
// Hide the HUD when necessary
|
||||
//
|
||||
// Added in OPM
|
||||
// ...including whenever the gathered VR HUD is up, which is the condition
|
||||
// that actually turns the retail corner HUD off.
|
||||
//
|
||||
// Gating the explicit Display calls in the cls.no_menus branch above was
|
||||
// not enough and looked like it was: the widgets kept drawing, measured
|
||||
// off an eye dump with hud_health's meter still at the bottom left corner
|
||||
// and hud_ammo's clip still at the bottom right, both exactly where their
|
||||
// .urc alignment puts them. That branch returns early and is not the path
|
||||
// taken here - when it is not, UI_Update falls through to
|
||||
// uWinMan.UpdateViews(), which walks the whole widget tree and draws every
|
||||
// shown menu whether anything asked it to or not. Hiding them is the only
|
||||
// thing that covers both routes.
|
||||
if (!ui_hud || clc.state != CA_ACTIVE || view3d->LetterboxActive() || (currentMenu && currentMenu->isFullscreen())
|
||||
|| server_loading || ((cl.snap.ps.pm_flags & PMF_NO_HUD) || (cl.snap.ps.pm_flags & PMF_INTERMISSION))) {
|
||||
|| server_loading || ((cl.snap.ps.pm_flags & PMF_NO_HUD) || (cl.snap.ps.pm_flags & PMF_INTERMISSION))
|
||||
|| vrHudActive) {
|
||||
if (crosshairhud) {
|
||||
crosshairhud->ForceHide();
|
||||
}
|
||||
@@ -2393,6 +2509,25 @@ 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
|
||||
|
||||
UI_PlaceVRCompass();
|
||||
uWinMan.UpdateViews();
|
||||
}
|
||||
|
||||
@@ -3887,6 +4022,10 @@ void UI_ResolutionChange(void)
|
||||
ui_compass_scale = Cvar_Get("ui_compass_scale", "0.55", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
}
|
||||
|
||||
vr_compassX = Cvar_Get("vr_compassX", "0.5", CVAR_ARCHIVE);
|
||||
vr_compassY = Cvar_Get("vr_compassY", "0.62", CVAR_ARCHIVE);
|
||||
ui_vrHud = Cvar_Get("vr_hud", "1", CVAR_ARCHIVE);
|
||||
|
||||
CL_FillUIImports();
|
||||
CL_FillUIDef();
|
||||
|
||||
|
||||
+133
-35
@@ -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"
|
||||
@@ -47,9 +50,11 @@ View3D::View3D()
|
||||
setBorderStyle(border_none);
|
||||
AllowActivate(true);
|
||||
|
||||
m_printfadetime = 0.0;
|
||||
m_print_mat = NULL;
|
||||
m_locationprint = qfalse;
|
||||
m_printfadetime = 0.0;
|
||||
m_printUpdateTime = cls.realtime;
|
||||
m_subtitleUpdateTime = cls.realtime;
|
||||
m_print_mat = NULL;
|
||||
m_locationprint = qfalse;
|
||||
}
|
||||
|
||||
void View3D::UpdateCenterPrint(const char *s, float alpha)
|
||||
@@ -62,19 +67,21 @@ void View3D::UpdateCenterPrint(const char *s, float alpha)
|
||||
m_print_mat = NULL;
|
||||
}
|
||||
|
||||
m_printalpha = alpha;
|
||||
m_printfadetime = 4000.0;
|
||||
m_locationprint = qfalse;
|
||||
m_printalpha = alpha;
|
||||
m_printfadetime = 4000.0;
|
||||
m_printUpdateTime = cls.realtime;
|
||||
m_locationprint = qfalse;
|
||||
}
|
||||
|
||||
void View3D::UpdateLocationPrint(int x, int y, const char *s, float alpha)
|
||||
{
|
||||
m_printstring = s;
|
||||
m_printalpha = alpha;
|
||||
m_printfadetime = 4000.0;
|
||||
m_x_coord = x;
|
||||
m_y_coord = y;
|
||||
m_locationprint = qtrue;
|
||||
m_printstring = s;
|
||||
m_printalpha = alpha;
|
||||
m_printfadetime = 4000.0;
|
||||
m_printUpdateTime = cls.realtime;
|
||||
m_x_coord = x;
|
||||
m_y_coord = y;
|
||||
m_locationprint = qtrue;
|
||||
}
|
||||
|
||||
void View3D::FrameInitialized(void)
|
||||
@@ -571,36 +578,88 @@ 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
|
||||
}
|
||||
|
||||
// Fixed in OPM
|
||||
// Advance by elapsed client time, not by the number of rendered eyes.
|
||||
void View3D::UpdatePrintTime(void)
|
||||
{
|
||||
const int elapsed = cls.realtime - m_printUpdateTime;
|
||||
m_printUpdateTime = cls.realtime;
|
||||
if (elapsed > 0) {
|
||||
m_printfadetime = Q_max(0.0f, m_printfadetime - elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
void View3D::CenterPrint(void)
|
||||
@@ -611,6 +670,7 @@ void View3D::CenterPrint(void)
|
||||
float x, y;
|
||||
float w, h;
|
||||
|
||||
UpdatePrintTime();
|
||||
if (!m_printfadetime) {
|
||||
return;
|
||||
}
|
||||
@@ -659,12 +719,6 @@ void View3D::CenterPrint(void)
|
||||
|
||||
re.DrawStretchPic(x, y, w, h, 0, 0, 1, 1, mat);
|
||||
}
|
||||
|
||||
m_printfadetime -= cls.frametime;
|
||||
|
||||
if (m_printfadetime < 0) {
|
||||
m_printfadetime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void View3D::LocationPrint(void)
|
||||
@@ -676,6 +730,7 @@ void View3D::LocationPrint(void)
|
||||
float alpha;
|
||||
UIRect2D frame;
|
||||
|
||||
UpdatePrintTime();
|
||||
if (!m_printfadetime) {
|
||||
m_locationprint = false;
|
||||
return;
|
||||
@@ -730,11 +785,6 @@ void View3D::LocationPrint(void)
|
||||
);
|
||||
|
||||
m_font->setColor(UBlack);
|
||||
m_printfadetime -= cls.frametime;
|
||||
|
||||
if (m_printfadetime < 0) {
|
||||
m_printfadetime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void View3D::DrawLetterbox(void)
|
||||
@@ -777,15 +827,61 @@ void View3D::DrawFades(void)
|
||||
|
||||
void View3D::Draw(void)
|
||||
{
|
||||
if (clc.state != CA_DISCONNECTED) {
|
||||
#ifdef USE_OPENXR
|
||||
const int drawStart = Sys_Milliseconds();
|
||||
VR_TraceEvent(VRTRACE_VIEW3D);
|
||||
#endif
|
||||
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
|
||||
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.
|
||||
//
|
||||
#ifdef USE_OPENXR
|
||||
int hudStart = Sys_Milliseconds();
|
||||
#endif
|
||||
if (cge && !cls.no_menus) {
|
||||
set2D();
|
||||
// Set2DWindow flushes the queued world commands. Charge that
|
||||
// work to the world before starting the HUD timer.
|
||||
#ifdef USE_OPENXR
|
||||
hudStart = Sys_Milliseconds();
|
||||
#endif
|
||||
cge->CG_Draw2D();
|
||||
}
|
||||
#ifdef USE_OPENXR
|
||||
VR_TraceViewTimes(hudStart - drawStart, Sys_Milliseconds() - hudStart);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
set2D();
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
// The transition issues the pending world draw calls; starting the timer
|
||||
// before it incorrectly reports those calls as HUD setup time.
|
||||
const int hudStart = Sys_Milliseconds();
|
||||
#endif
|
||||
re.SavePerformanceCounters();
|
||||
|
||||
Draw2D();
|
||||
|
||||
#ifdef USE_OPENXR
|
||||
VR_TraceViewTimes(hudStart - drawStart, Sys_Milliseconds() - hudStart);
|
||||
#endif
|
||||
}
|
||||
|
||||
float avWidth = 0.0;
|
||||
@@ -794,6 +890,7 @@ void View3D::InitSubtitle(void)
|
||||
{
|
||||
float totalWidth;
|
||||
|
||||
m_subtitleUpdateTime = cls.realtime;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
subs[i] = Cvar_Get(va("subtitle%d", i), "", 0);
|
||||
teams[i] = Cvar_Get(va("subteam%d", i), "0", 0);
|
||||
@@ -816,6 +913,9 @@ void View3D::DrawSubtitleOverlay(void)
|
||||
int i;
|
||||
float minX, maxX;
|
||||
int line;
|
||||
const int elapsed = Q_max(0, cls.realtime - m_subtitleUpdateTime);
|
||||
|
||||
m_subtitleUpdateTime = cls.realtime;
|
||||
|
||||
subAlpha = Cvar_Get("subAlpha", "0.5", 0);
|
||||
|
||||
@@ -827,6 +927,15 @@ void View3D::DrawSubtitleOverlay(void)
|
||||
fadeTime[i] = 2500 * ((strlen(subs[i]->string) / 68) + 1.f) + 1500;
|
||||
subLife[i] = fadeTime[i];
|
||||
Q_strncpyz(oldStrings[i], subs[i]->string, sizeof(oldStrings[i]));
|
||||
} else {
|
||||
fadeTime[i] = Q_max(0.0f, fadeTime[i] - elapsed);
|
||||
}
|
||||
|
||||
if (fadeTime[i] <= 0) {
|
||||
oldStrings[i][0] = 0;
|
||||
if (subs[i]->string && subs[i]->string[0]) {
|
||||
Cvar_Set(va("subtitle%d", i), "");
|
||||
}
|
||||
}
|
||||
|
||||
if (fadeTime[i] > subLife[i] - 750.f) {
|
||||
@@ -836,17 +945,6 @@ void View3D::DrawSubtitleOverlay(void)
|
||||
} else {
|
||||
alpha[i] = 1.f;
|
||||
}
|
||||
|
||||
fadeTime[i] -= cls.frametime;
|
||||
if (fadeTime[i] < 0) {
|
||||
// Clear the subtitle
|
||||
fadeTime[i] = 0;
|
||||
oldStrings[i][0] = 0;
|
||||
|
||||
if (subs[i]->string && subs[i]->string[0]) {
|
||||
Cvar_Set(va("subtitle%d", i), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
minX = m_screenframe.size.height - m_font->getHeight(getHighResScale()) * 10;
|
||||
|
||||
@@ -25,6 +25,8 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
class View3D : public UIWidget
|
||||
{
|
||||
float m_printfadetime;
|
||||
int m_printUpdateTime;
|
||||
int m_subtitleUpdateTime;
|
||||
float m_printalpha;
|
||||
str m_printstring;
|
||||
UIReggedMaterial *m_print_mat;
|
||||
@@ -47,6 +49,7 @@ protected:
|
||||
void DrawSoundOverlay(void);
|
||||
void CenterPrint(void);
|
||||
void LocationPrint(void);
|
||||
void UpdatePrintTime(void);
|
||||
void OnActivate(Event *ev);
|
||||
void OnDeactivate(Event *ev);
|
||||
void DrawSubtitleOverlay(void);
|
||||
|
||||
@@ -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,22 @@ 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);
|
||||
qboolean (*VR_GetHeadOffset)(vec3_t out, float *baseYaw);
|
||||
qboolean (*VR_GetClimbPull)(vec3_t out);
|
||||
qboolean (*VR_GetThrow)(vec3_t velocity, float *baseYaw);
|
||||
|
||||
|
||||
unsigned int (*Client_NumPendingCommands)(int clientNum);
|
||||
unsigned int (*Client_MaxPendingCommands)(int clientNum);
|
||||
|
||||
+166
-1
@@ -4179,6 +4179,53 @@ void Player::ClientMove(usercmd_t *ucmd)
|
||||
setOrigin(origin);
|
||||
CheckGround();
|
||||
}
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
//
|
||||
// Climbing a ladder by hand in VR.
|
||||
//
|
||||
// A ladder here is not moved along by velocity - the pmove is
|
||||
// skipped entirely on a wall, and the player is carried by the
|
||||
// root motion of the climb animation above. That is the right
|
||||
// behaviour for a stick, and it is the wrong one for a pair of
|
||||
// hands: the animation decides how fast the player goes, and a
|
||||
// drag has to be the player deciding.
|
||||
//
|
||||
// So this runs after the animation, not instead of it. The climb
|
||||
// animation still plays, so the arms on the ladder still look
|
||||
// like arms on a ladder, and the distance actually travelled is
|
||||
// the distance the hand pulled.
|
||||
//
|
||||
// The hand that has hold of a rung is not moving through the
|
||||
// world; the world is moving past it. Hence the negation - the
|
||||
// hand goes down and the player goes up.
|
||||
//
|
||||
// It goes through AnimMove for the same reason the animation does:
|
||||
// the ladder has a top and a wall behind it, and a drag that
|
||||
// ignored them would pull the player through both.
|
||||
//
|
||||
if (movecontrol == MOVECONTROL_CLIMBWALL && gi.VR_GetClimbPull) {
|
||||
vec3_t vPull;
|
||||
|
||||
if (gi.VR_GetClimbPull(vPull)) {
|
||||
Vector vMove;
|
||||
|
||||
// The play space offset is already in the engine's frame
|
||||
// and its units, and a ladder is climbed vertically. Only
|
||||
// the height is taken: the horizontal components of a pull
|
||||
// are the player's arm swinging around the rung rather
|
||||
// than any intent to travel, and acting on them would peel
|
||||
// them off the ladder sideways.
|
||||
vMove = Vector(0.0f, 0.0f, -vPull[2]);
|
||||
|
||||
if (vMove != vec_zero) {
|
||||
AnimMove(vMove, &origin);
|
||||
setOrigin(origin);
|
||||
CheckGround();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7132,8 +7179,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);
|
||||
}
|
||||
@@ -12385,6 +12456,13 @@ int Player::getUseableEntities(int *touch, int maxcount, bool requiresLookAt)
|
||||
Vector offset;
|
||||
Vector max;
|
||||
Vector min;
|
||||
vec3_t vBodyEye;
|
||||
|
||||
static cvar_t *g_vrTraceUse = NULL;
|
||||
|
||||
if (!g_vrTraceUse) {
|
||||
g_vrTraceUse = gi.Cvar_Get("vr_traceUse", "0", 0);
|
||||
}
|
||||
|
||||
if ((g_gametype->integer != GT_SINGLE_PLAYER && IsSpectator()) || IsDead()) {
|
||||
return 0;
|
||||
@@ -12403,6 +12481,52 @@ int Player::getUseableEntities(int *touch, int maxcount, bool requiresLookAt)
|
||||
|
||||
start = m_vViewPos;
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
//
|
||||
// In VR, start the look-at trace from where the player is actually
|
||||
// looking from.
|
||||
//
|
||||
// m_vViewPos is the body's eye - the entity origin plus a fixed view
|
||||
// height - and the camera is not there. R_VRComposeView adds the head's
|
||||
// offset within the play space on top of it, so the two agree only while
|
||||
// the body is keeping up with the head. They come apart in exactly the
|
||||
// case this function exists to serve: leaning through a doorway or over a
|
||||
// crate to reach the thing on the other side, where the body is stopped
|
||||
// by the geometry and the head is not.
|
||||
//
|
||||
// The direction needs no such correction. m_vViewAng is already the
|
||||
// headset's - the client writes the head's yaw and pitch straight into
|
||||
// the view angles - so this trace has always pointed where the player is
|
||||
// looking, and only ever started from the wrong place.
|
||||
//
|
||||
VectorCopy(start, vBodyEye);
|
||||
|
||||
{
|
||||
vec3_t vHeadOffset;
|
||||
float fBaseYaw = 0.0f;
|
||||
|
||||
if (gi.VR_GetHeadOffset && gi.VR_GetHeadOffset(vHeadOffset, &fBaseYaw)) {
|
||||
vec3_t vEye, vBodyAngles, vBodyAxis[3];
|
||||
int i;
|
||||
|
||||
VectorCopy(start, vEye);
|
||||
|
||||
// The same composition the renderer uses for the camera. The
|
||||
// offset is in the play space, so it turns with the body's
|
||||
// heading - which is the view yaw less however much of it came
|
||||
// from the head, or a snap turn would leave it behind.
|
||||
VectorSet(vBodyAngles, 0.0f, m_vViewAng[YAW] - fBaseYaw, 0.0f);
|
||||
AnglesToAxis(vBodyAngles, vBodyAxis);
|
||||
|
||||
for (i = 0; i < 3; i++) {
|
||||
VectorMA(vEye, vHeadOffset[i], vBodyAxis[i], vEye);
|
||||
}
|
||||
|
||||
start = Vector(vEye);
|
||||
}
|
||||
}
|
||||
|
||||
if (requiresLookAt) {
|
||||
min = Vector(-4.f, -4.f, -4.f);
|
||||
max = Vector(4.f, 4.f, 4.f);
|
||||
@@ -12427,7 +12551,48 @@ int Player::getUseableEntities(int *touch, int maxcount, bool requiresLookAt)
|
||||
max = start + Vector(31.f, 31.f, 31.f);
|
||||
}
|
||||
|
||||
return gi.AreaEntities(min, max, touch, maxcount);
|
||||
{
|
||||
const int num = gi.AreaEntities(min, max, touch, maxcount);
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
//
|
||||
// "Use works about half the time" is not something that can be
|
||||
// reasoned to an answer - the button, the trace and the box each have
|
||||
// a way of coming up empty, and they all look the same from inside a
|
||||
// headset. So this says which one it was.
|
||||
//
|
||||
// Everything needed to tell them apart is here: the body's eye, the
|
||||
// eye actually used after the play space offset, how far the trace
|
||||
// got, and what the box caught. A press that prints nothing at all
|
||||
// did not reach the game and the fault is in the binding.
|
||||
//
|
||||
if (g_vrTraceUse && g_vrTraceUse->integer) {
|
||||
int i;
|
||||
|
||||
gi.Printf(
|
||||
"VR use: body eye %.0f %.0f %.0f -> traced from %.0f %.0f %.0f | ang %.0f %.0f | "
|
||||
"frac %.2f end %.0f %.0f %.0f | %d in box\n",
|
||||
vBodyEye[0], vBodyEye[1], vBodyEye[2],
|
||||
start[0], start[1], start[2],
|
||||
m_vViewAng[0], m_vViewAng[1],
|
||||
requiresLookAt ? trace.fraction : 1.0f,
|
||||
offset[0], offset[1], offset[2],
|
||||
num
|
||||
);
|
||||
|
||||
for (i = 0; i < num && i < 8; i++) {
|
||||
gentity_t *hit = &g_entities[touch[i]];
|
||||
|
||||
gi.Printf("VR use: [%d] %s%s\n",
|
||||
touch[i],
|
||||
(hit->entity && hit->entity->getClassname()) ? hit->entity->getClassname() : "?",
|
||||
hit->inuse ? "" : " (not in use)");
|
||||
}
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
}
|
||||
|
||||
bool Player::IsPrimaryWeaponValid() const
|
||||
|
||||
+298
-2
@@ -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);
|
||||
@@ -1416,7 +1440,279 @@ void Weapon::Shoot(Event *ev)
|
||||
|
||||
switch (firetype[mode]) {
|
||||
case FT_PROJECTILE:
|
||||
ProjectileAttack(pos, forward, owner, projectileModel[mode], charge_fraction, 0, this);
|
||||
{
|
||||
//
|
||||
// Added in OPM
|
||||
//
|
||||
// A thrown weapon in VR goes where the arm sent it.
|
||||
//
|
||||
// The flat game throws along the view at a speed set by how
|
||||
// long the button was held, because a mouse has no other way
|
||||
// to say "over there, gently". A hand does: the swing carries
|
||||
// both the direction and the strength, and using anything
|
||||
// else means the player winds up, lets go, and watches the
|
||||
// grenade leave along their nose instead.
|
||||
//
|
||||
// Only for grenades. Rockets and the like are projectiles
|
||||
// too, and they are fired rather than thrown - those keep the
|
||||
// weapon's own aim.
|
||||
//
|
||||
Vector vThrowDir = forward;
|
||||
float fThrowFraction = 0.0f;
|
||||
qboolean bThrown = qfalse;
|
||||
|
||||
if ((GetWeaponClass() & WEAPON_CLASS_GRENADE) && gi.VR_GetThrow) {
|
||||
vec3_t vVel;
|
||||
float fBaseYaw = 0.0f;
|
||||
|
||||
if (gi.VR_GetThrow(vVel, &fBaseYaw)) {
|
||||
Sentient *pOwner = (Sentient *)(Entity *)owner;
|
||||
vec3_t vBodyAngles, vBodyAxis[3], vWorld;
|
||||
float fSpeed;
|
||||
int i;
|
||||
|
||||
// Into the world the same way every other play space
|
||||
// vector here is: rotated by the heading the body has
|
||||
// that the head did not give it.
|
||||
VectorSet(vBodyAngles, 0.0f,
|
||||
(pOwner ? pOwner->GetViewAngles()[YAW] : 0.0f) - fBaseYaw, 0.0f);
|
||||
AnglesToAxis(vBodyAngles, vBodyAxis);
|
||||
|
||||
VectorClear(vWorld);
|
||||
for (i = 0; i < 3; i++) {
|
||||
VectorMA(vWorld, vVel[i], vBodyAxis[i], vWorld);
|
||||
}
|
||||
|
||||
fSpeed = VectorLength(vWorld);
|
||||
|
||||
if (fSpeed > 1.0f) {
|
||||
static cvar_t *vr_throwFull = NULL;
|
||||
float fReference;
|
||||
|
||||
VectorScale(vWorld, 1.0f / fSpeed, vWorld);
|
||||
|
||||
//
|
||||
// Both the direction and the strength come from
|
||||
// the swing. This is a physical throw: the grenade
|
||||
// leaves along the line the hand was travelling,
|
||||
// at a speed set by how fast it was going.
|
||||
//
|
||||
// Which instant of the swing to read is the whole
|
||||
// problem, and it took three tries to get right.
|
||||
// Averaging across the release blends the wind up
|
||||
// with the follow through and aims between them.
|
||||
// Taking the peak of the whole swing catches the
|
||||
// backswing - measured on device, four throws all
|
||||
// came out around 0.95 *backwards*. What works is
|
||||
// the peak of a short window ending at release,
|
||||
// which is what the hand was doing as it let go;
|
||||
// the same four throws then measured 0.89 to 0.94
|
||||
// straight ahead, and an underhand lob came out
|
||||
// genuinely upward at 0.58.
|
||||
//
|
||||
// RTCWQuest does not do this at all - its grenade
|
||||
// (VrInputDefault.c, the backpack mode) is only a
|
||||
// selection gesture, thrown afterwards by the
|
||||
// game's own fire along the controller. Aiming by
|
||||
// the controller is steadier and it is not what a
|
||||
// throw is. This port does the harder thing on
|
||||
// purpose.
|
||||
//
|
||||
|
||||
//
|
||||
// Strength as a fraction of the game's own throw,
|
||||
// not as an absolute speed.
|
||||
//
|
||||
// An absolute number would have to be invented
|
||||
// here and would then disagree with whatever the
|
||||
// grenade was authored to do - and every thrown
|
||||
// weapon has its own. The fraction is the knob the
|
||||
// engine already uses for a held throw, so feeding
|
||||
// it from the arm keeps all of that tuning and
|
||||
// only changes what decides it.
|
||||
//
|
||||
// The reference is the peak speed of a hard throw,
|
||||
// measured on device: four throws came in between
|
||||
// 298 and 338 units a second, so 340 is about what
|
||||
// a full effort looks like. Below that the throw
|
||||
// scales down; a gentle lob stays a gentle lob.
|
||||
//
|
||||
if (!vr_throwFull) {
|
||||
vr_throwFull = gi.Cvar_Get("vr_throwFull", "340", CVAR_ARCHIVE);
|
||||
}
|
||||
|
||||
fReference = (vr_throwFull && vr_throwFull->value > 1.0f)
|
||||
? vr_throwFull->value : 340.0f;
|
||||
|
||||
//
|
||||
// Linear, with a floor.
|
||||
//
|
||||
// A square root curve was tried here and made
|
||||
// everything stronger, including the overhand
|
||||
// throw that was already right: the same swing
|
||||
// that measured 89% linear came out at 94%, and
|
||||
// softer ones moved far more - 192 units a second
|
||||
// went from 56% to 75%. It read as the throw
|
||||
// having become too sharp, because it had.
|
||||
//
|
||||
// The strong end was never the problem. Linear is
|
||||
// what the arm expects, and a floor lifts the
|
||||
// gentle throws that would otherwise drop at the
|
||||
// player's feet without touching anything above
|
||||
// it.
|
||||
//
|
||||
//
|
||||
// Raised to a power, and the power is above one on
|
||||
// purpose.
|
||||
//
|
||||
// A lob leaves at around 45 degrees, which is the
|
||||
// angle that carries furthest - so a gentle
|
||||
// underhand at half strength outruns a hard flat
|
||||
// throw, and lobs sailed past where they were
|
||||
// aimed. Linear strength is not enough to stop
|
||||
// that, because the angle is doing the work.
|
||||
//
|
||||
// An exponent above one bends the gentle end down
|
||||
// hard while barely touching the top, which is the
|
||||
// reverse of the square root tried earlier - that
|
||||
// lifted the bottom and made everything too sharp.
|
||||
//
|
||||
{
|
||||
static cvar_t *vr_throwCurve = NULL;
|
||||
float fCurve;
|
||||
|
||||
if (!vr_throwCurve) {
|
||||
vr_throwCurve = gi.Cvar_Get("vr_throwCurve", "1.5", CVAR_ARCHIVE);
|
||||
}
|
||||
|
||||
fCurve = (vr_throwCurve && vr_throwCurve->value > 0.1f)
|
||||
? vr_throwCurve->value : 1.5f;
|
||||
|
||||
fThrowFraction = pow(fSpeed / fReference, fCurve);
|
||||
}
|
||||
|
||||
// A floor rather than zero: the player has decided
|
||||
// to throw, and a grenade that drops at their feet
|
||||
// because the swing was polite is not a mechanic.
|
||||
// Tunable, because where it belongs is a question
|
||||
// about how it feels and every guess so far has
|
||||
// cost a rebuild.
|
||||
{
|
||||
static cvar_t *vr_throwMin = NULL;
|
||||
float fMin;
|
||||
|
||||
if (!vr_throwMin) {
|
||||
vr_throwMin = gi.Cvar_Get("vr_throwMin", "0.2", CVAR_ARCHIVE);
|
||||
}
|
||||
|
||||
fMin = vr_throwMin ? vr_throwMin->value : 0.2f;
|
||||
|
||||
if (fThrowFraction < fMin) {
|
||||
fThrowFraction = fMin;
|
||||
}
|
||||
}
|
||||
|
||||
if (fThrowFraction > 1.0f) {
|
||||
fThrowFraction = 1.0f;
|
||||
}
|
||||
|
||||
//
|
||||
// Damp the sideways component of a gentle throw.
|
||||
//
|
||||
// This is the one place here that deliberately
|
||||
// does not report what the arm did. An underhand
|
||||
// swing comes across the body, so the hand really
|
||||
// is moving sideways as it lets go - measured on
|
||||
// device, lobs drifted left by 0.35 and once by
|
||||
// 0.73, more sideways than forward. That is honest
|
||||
// physics and it is not what the player meant.
|
||||
//
|
||||
// Scaled by how gentle the throw is, so a hard
|
||||
// overhand keeps its true direction - those were
|
||||
// already landing where they were aimed and must
|
||||
// not be touched - while a soft lob is pulled back
|
||||
// towards where the player is facing.
|
||||
//
|
||||
{
|
||||
static cvar_t *vr_throwDamp = NULL;
|
||||
float fDamp, fViewYaw, fRad, fC, fS;
|
||||
float fAheadC, fLeftC;
|
||||
|
||||
if (!vr_throwDamp) {
|
||||
vr_throwDamp = gi.Cvar_Get("vr_throwDamp", "0.8", CVAR_ARCHIVE);
|
||||
}
|
||||
|
||||
fDamp = (vr_throwDamp ? vr_throwDamp->value : 0.8f)
|
||||
* (1.0f - fThrowFraction);
|
||||
|
||||
if (fDamp < 0.0f) {
|
||||
fDamp = 0.0f;
|
||||
} else if (fDamp > 0.95f) {
|
||||
fDamp = 0.95f;
|
||||
}
|
||||
|
||||
fViewYaw = pOwner ? pOwner->GetViewAngles()[YAW] : 0.0f;
|
||||
fRad = -DEG2RAD(fViewYaw);
|
||||
fC = cos(fRad);
|
||||
fS = sin(fRad);
|
||||
|
||||
// Into the player's frame, damp across, and
|
||||
// back out again.
|
||||
fAheadC = vWorld[0] * fC - vWorld[1] * fS;
|
||||
fLeftC = vWorld[0] * fS + vWorld[1] * fC;
|
||||
|
||||
fLeftC *= (1.0f - fDamp);
|
||||
|
||||
vWorld[0] = fAheadC * fC + fLeftC * fS;
|
||||
vWorld[1] = -fAheadC * fS + fLeftC * fC;
|
||||
|
||||
VectorNormalize(vWorld);
|
||||
}
|
||||
|
||||
vThrowDir = Vector(vWorld);
|
||||
|
||||
{
|
||||
// The direction as it reaches the world, which
|
||||
// is the only one worth reading. The play
|
||||
// space vector logged on the VR side is
|
||||
// relative to whichever way the room was
|
||||
// facing when tracking started, so forward and
|
||||
// backward cannot be told apart in it.
|
||||
static cvar_t *vr_traceThrow = NULL;
|
||||
|
||||
if (!vr_traceThrow) {
|
||||
vr_traceThrow = gi.Cvar_Get("vr_traceThrow", "0", 0);
|
||||
}
|
||||
|
||||
if (vr_traceThrow && vr_traceThrow->integer) {
|
||||
// Relative to where the player is facing,
|
||||
// which is the only frame that answers the
|
||||
// question being asked. World axes belong
|
||||
// to the map and play space axes to the
|
||||
// room; in neither can "straight ahead" be
|
||||
// told from "ninety degrees off". Here
|
||||
// ahead is +1 0 0, the player's left is
|
||||
// 0 +1 0, and up is up.
|
||||
const float fViewYaw = pOwner ? pOwner->GetViewAngles()[YAW] : 0.0f;
|
||||
const float fRad = -DEG2RAD(fViewYaw);
|
||||
const float fC = cos(fRad);
|
||||
const float fS = sin(fRad);
|
||||
const float fAhead = vWorld[0] * fC - vWorld[1] * fS;
|
||||
const float fLeft = vWorld[0] * fS + vWorld[1] * fC;
|
||||
|
||||
gi.Printf("VR throw: ahead %.2f left %.2f up %.2f | speed %.0f -> %.0f%% of full\n",
|
||||
fAhead, fLeft, vWorld[2], fSpeed, fThrowFraction * 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
bThrown = qtrue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProjectileAttack(pos, vThrowDir, owner, projectileModel[mode],
|
||||
bThrown ? fThrowFraction : charge_fraction, 0, this);
|
||||
}
|
||||
break;
|
||||
case FT_LANDMINE:
|
||||
PlaceLandmine(pos, owner, projectileModel[mode], this);
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1489,7 +1505,7 @@ void RB_ExecuteRenderCommands( const void *data ) {
|
||||
default:
|
||||
// stop rendering
|
||||
t2 = ri.Milliseconds ();
|
||||
backEnd.pc.msec = t2 - t1;
|
||||
backEnd.pc.msec += t2 - t1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,10 @@ void R_IssueRenderCommands( qboolean runPerformanceCounters ) {
|
||||
cmdList->used = 0;
|
||||
|
||||
if ( runPerformanceCounters ) {
|
||||
int backEndMsec = backEnd.pc.msec;
|
||||
R_PerformanceCounters();
|
||||
// EndFrame still needs the time spent in earlier command batches.
|
||||
backEnd.pc.msec = backEndMsec;
|
||||
}
|
||||
|
||||
// actually start the commands going
|
||||
@@ -127,7 +130,7 @@ Issue any pending commands and wait for them to complete.
|
||||
====================
|
||||
*/
|
||||
void R_IssuePendingRenderCommands( void ) {
|
||||
if ( !tr.registered ) {
|
||||
if ( !tr.registered || !backEndData->commands.used ) {
|
||||
return;
|
||||
}
|
||||
R_IssueRenderCommands( qfalse );
|
||||
|
||||
@@ -436,10 +436,20 @@ Set2DWindow
|
||||
*/
|
||||
void Set2DWindow(int x, int y, int w, int h, float left, float right, float bottom, float top, float n, float f) {
|
||||
R_IssuePendingRenderCommands();
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Applied to the rectangle and not to the ortho window, because the window
|
||||
// is usually a single widget's and the correction is the whole display's.
|
||||
// See R_VRAdjust2DRect.
|
||||
R_VRAdjust2DRect(&x, &y, &w, &h);
|
||||
|
||||
qglViewport(x, y, w, h);
|
||||
qglScissor(x, y, w, h);
|
||||
qglMatrixMode(GL_PROJECTION);
|
||||
qglLoadIdentity();
|
||||
|
||||
qglOrtho(left, right, bottom, top, n, f);
|
||||
qglMatrixMode(GL_MODELVIEW);
|
||||
|
||||
@@ -471,6 +481,11 @@ RE_Scissor
|
||||
================
|
||||
*/
|
||||
void RE_Scissor(int x, int y, int width, int height) {
|
||||
// The UI clips each widget to its own frame right after setting the window
|
||||
// for it, so this has to move with the window or a widget the headset's
|
||||
// correction has shifted is clipped away at where it used to be.
|
||||
R_VRAdjust2DRect(&x, &y, &width, &height);
|
||||
|
||||
qglEnable(GL_SCISSOR_TEST);
|
||||
qglScissor(x, y, width, height);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
static fontheader_sgl_t s_loadedFonts_sgl[MAX_LOADED_FONTS];
|
||||
static int s_numLoadedFonts_sgl = 0;
|
||||
static fontheader_t s_loadedFonts[MAX_LOADED_FONTS];
|
||||
|
||||
cvar_t *vr_traceFont;
|
||||
static int s_numLoadedFonts = 0;
|
||||
static float s_fontHeightScale = 1.0;
|
||||
static float s_fontGeneralScale = 1.0;
|
||||
@@ -533,10 +535,10 @@ void R_LoadFontShader(fontheader_sgl_t* font)
|
||||
{
|
||||
for (i = 0; i < fontshader->numUnfoggedPasses; i++)
|
||||
{
|
||||
if (fontshader->unfoggedStages[0] != NULL && fontshader->unfoggedStages[0]->active)
|
||||
if (fontshader->unfoggedStages[i] != NULL && fontshader->unfoggedStages[i]->active)
|
||||
{
|
||||
fontshader->unfoggedStages[0]->rgbGen = CGEN_GLOBAL_COLOR;
|
||||
fontshader->unfoggedStages[0]->alphaGen = AGEN_GLOBAL_ALPHA;
|
||||
fontshader->unfoggedStages[i]->rgbGen = CGEN_GLOBAL_COLOR;
|
||||
fontshader->unfoggedStages[i]->alphaGen = AGEN_GLOBAL_ALPHA;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +548,139 @@ void R_LoadFontShader(fontheader_sgl_t* font)
|
||||
{
|
||||
font->trhandle = r_sequencenumber;
|
||||
}
|
||||
|
||||
//
|
||||
// Fixed in OPM
|
||||
// Keep the atlas alive, and fetch it again if it has already gone.
|
||||
//
|
||||
// Every string in the game drew as a solid black rectangle - not a black
|
||||
// box with faint glyphs, which is the missing-alpha symptom, but flat
|
||||
// 0,0,0 with no glyph structure in it at all. Measured at the draw call,
|
||||
// the shader's own stage reported texnum 0: no texture. Sampling texture
|
||||
// zero returns opaque black, and opaque black is exactly what the screen
|
||||
// had.
|
||||
//
|
||||
// The lifetime is the cause. R_FreeUnusedImages runs at the end of every
|
||||
// registration - RE_EndRegistration, so once per map load - and frees
|
||||
// every image whose r_sequence is neither the current one nor -1, where
|
||||
// freeing means memset of the whole image_t. The font atlas is loaded
|
||||
// once, early, by whoever draws text first; the shader that refers to it
|
||||
// is then found in the shader hash on every later call to this function
|
||||
// and handed straight back, images untouched. So the shader survives a map
|
||||
// load and the image it points at does not, and the pointer is left aimed
|
||||
// at a zeroed slot.
|
||||
//
|
||||
// The trick above of forcing r_sequencenumber to -1 across R_FindShader is
|
||||
// meant to prevent exactly this, and it does - but only for the load that
|
||||
// actually creates the image. It does nothing for an atlas that some other
|
||||
// path brought in first at an ordinary sequence number, which is the usual
|
||||
// case: the menus draw text long before any of this runs.
|
||||
//
|
||||
// So mark it here, every time, where the font is known to still want it.
|
||||
// And if it has already been taken, ask for it again by name - the name is
|
||||
// gone from the image_t along with everything else, but it is recoverable,
|
||||
// because the font's own name is what built the shader's name to begin
|
||||
// with.
|
||||
//
|
||||
for (i = 0; i < fontshader->numUnfoggedPasses; i++)
|
||||
{
|
||||
shaderStage_t *st = fontshader->unfoggedStages[i];
|
||||
image_t *img;
|
||||
|
||||
if (!st || !st->active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
img = st->bundle[0].image[0];
|
||||
|
||||
if (!img || !img->texnum) {
|
||||
char lost[MAX_QPATH];
|
||||
|
||||
//
|
||||
// Asking for it by name is not enough on its own, and that was the
|
||||
// first attempt. R_FindImageFileOld looks in the hash before it
|
||||
// looks on disk, and the entry in the hash is the very image that
|
||||
// has no texture - so it comes straight back, still broken, and
|
||||
// nothing has happened. Measured: name intact, r_sequence pinned,
|
||||
// texnum still zero.
|
||||
//
|
||||
// So take it out of the hash first. R_FreeImage unhooks it and
|
||||
// clears the slot, which is what makes the next lookup miss and go
|
||||
// to disk, where a fresh image_t gets a texture of its own. The
|
||||
// name has to be copied out before that, because clearing the slot
|
||||
// takes the name with it.
|
||||
//
|
||||
if (img && img->imgName[0]) {
|
||||
Q_strncpyz(lost, img->imgName, sizeof(lost));
|
||||
} else {
|
||||
Com_sprintf(lost, sizeof(lost), "gfx/fonts/%s.tga", font->name);
|
||||
}
|
||||
|
||||
if (img) {
|
||||
R_FreeImage(img);
|
||||
}
|
||||
|
||||
img = R_FindImageFileOld(lost, qfalse, qfalse, qfalse, GL_REPEAT, GL_REPEAT);
|
||||
|
||||
if (img) {
|
||||
st->bundle[0].image[0] = img;
|
||||
ri.Printf(PRINT_ALL, "R_LoadFontShader: reloaded the atlas '%s' for '%s', texnum %d\n",
|
||||
lost, font->name, (int)img->texnum);
|
||||
} else {
|
||||
ri.Printf(PRINT_WARNING, "R_LoadFontShader: lost the atlas '%s' for '%s' and could not fetch it back\n",
|
||||
lost, font->name);
|
||||
}
|
||||
}
|
||||
|
||||
if (img) {
|
||||
// Never a candidate for R_FreeUnusedImages again. A font outlives
|
||||
// any one map, so its atlas has to as well.
|
||||
img->r_sequence = -1;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Added in OPM, for the port.
|
||||
//
|
||||
// Every string in the UI draws as an opaque black box with the glyphs
|
||||
// faintly visible in it, while every image beside it is correct. Both go
|
||||
// through RB_BeginSurface/RB_EndSurface on the same immediate path, so the
|
||||
// difference has to be the shader, and there are exactly two ways it can
|
||||
// come out opaque:
|
||||
//
|
||||
// - the lookup missed the script and fell back to a default shader,
|
||||
// which is GLS_DEFAULT and therefore opaque. "default 1" says so;
|
||||
// - the script was found but the atlas lost its alpha on upload, so
|
||||
// GL_SRC_ALPHA blends against 1 everywhere and covers the background.
|
||||
// "internal 0x1907" is GL_RGB and means exactly that; 0x1908 is
|
||||
// GL_RGBA and means the alpha survived.
|
||||
//
|
||||
// gfx/fonts/facfont-20.tga is greyscale in RGB - 0 where alpha is 0, about
|
||||
// 144 on a glyph - so drawn without a working alpha it is a black box with
|
||||
// grey letters, which is what the device shows.
|
||||
//
|
||||
// The stage's blend bits are printed raw. 0x65 is
|
||||
// GLS_SRCBLEND_SRC_ALPHA | GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA, what the
|
||||
// script asks for. 0 is GLS_DEFAULT, which is opaque.
|
||||
//
|
||||
if (vr_traceFont && vr_traceFont->integer) {
|
||||
const shaderStage_t *st = fontshader->numUnfoggedPasses ? fontshader->unfoggedStages[0] : NULL;
|
||||
|
||||
ri.Printf(
|
||||
PRINT_ALL,
|
||||
"font: \"%s\" default %d passes %d | blend 0x%x rgbGen %d alphaGen %d | image \"%s\" internal 0x%x"
|
||||
" texnum %d\n",
|
||||
fontshader->name,
|
||||
(int)fontshader->defaultShader,
|
||||
fontshader->numUnfoggedPasses,
|
||||
st ? (unsigned int)(st->stateBits & (GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS)) : 0u,
|
||||
st ? (int)st->rgbGen : -1,
|
||||
st ? (int)st->alphaGen : -1,
|
||||
(st && st->bundle[0].image[0]) ? st->bundle[0].image[0]->imgName : "none",
|
||||
(st && st->bundle[0].image[0]) ? (unsigned int)st->bundle[0].image[0]->internalFormat : 0u,
|
||||
(st && st->bundle[0].image[0]) ? (int)st->bundle[0].image[0]->texnum : -1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void R_DrawString_sgl(fontheader_sgl_t* font, const char* text, float x, float y, int maxlen, const float *pvVirtualScreen) {
|
||||
@@ -585,6 +720,63 @@ void R_DrawString_sgl(fontheader_sgl_t* font, const char* text, float x, float y
|
||||
R_LoadFontShader(font);
|
||||
}
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// What the GL is actually set to at the moment a string is about to be
|
||||
// drawn, once a second while vr_traceFont is on.
|
||||
//
|
||||
// Every string in the eye buffers comes out as a solid black rectangle -
|
||||
// measured off a real eye dump, not a compositor screenshot, so it is not
|
||||
// a compression artefact: the box is 0,0,0 with no glyph structure in it
|
||||
// at all. Opaque and black together rules out most of the candidates. The
|
||||
// atlas is a 32 bit TGA with a real alpha channel, min 0 max 255, and the
|
||||
// loader supports that; the shader is rgbGen global_color, alphaGen
|
||||
// global_alpha over SRC_ALPHA/ONE_MINUS_SRC_ALPHA, which is right; and
|
||||
// color2D cannot be black, because the bars drawn either side of the text
|
||||
// through the same field come out green and amber.
|
||||
//
|
||||
// What is left is the texture not reaching the fragments, and that is a
|
||||
// GL state question that no amount of reading the engine will answer -
|
||||
// the state the engine believes it set and the state gl4es actually has
|
||||
// are exactly the two things that come apart here. So this asks the
|
||||
// driver instead of the renderer: what is bound, what is enabled, and
|
||||
// what the blend is really doing.
|
||||
//
|
||||
if (vr_traceFont && vr_traceFont->integer) {
|
||||
static int lastReport;
|
||||
int now = ri.Milliseconds();
|
||||
|
||||
if (now - lastReport > 1000) {
|
||||
const shader_t *sh = (const shader_t *)font->shader;
|
||||
const shaderStage_t *st = (sh && sh->numUnfoggedPasses) ? sh->unfoggedStages[0] : NULL;
|
||||
GLint boundTex = 0, srcBlend = 0, dstBlend = 0;
|
||||
GLboolean blendOn = 0, texOn = 0;
|
||||
|
||||
lastReport = now;
|
||||
|
||||
qglGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex);
|
||||
qglGetIntegerv(GL_BLEND_SRC, &srcBlend);
|
||||
qglGetIntegerv(GL_BLEND_DST, &dstBlend);
|
||||
blendOn = glIsEnabled(GL_BLEND);
|
||||
texOn = glIsEnabled(GL_TEXTURE_2D);
|
||||
|
||||
ri.Printf(
|
||||
PRINT_ALL,
|
||||
"font draw: \"%s\" color2D %d %d %d %d | blend %d src 0x%x dst 0x%x | tex2d %d bound %d"
|
||||
" want %d | img \"%s\" seq %d of %d | scale %.2f\n",
|
||||
sh ? sh->name : "none",
|
||||
backEnd.color2D[0], backEnd.color2D[1], backEnd.color2D[2], backEnd.color2D[3],
|
||||
(int)blendOn, (unsigned int)srcBlend, (unsigned int)dstBlend,
|
||||
(int)texOn, (int)boundTex,
|
||||
(st && st->bundle[0].image[0]) ? st->bundle[0].image[0]->texnum : -1,
|
||||
(st && st->bundle[0].image[0]) ? st->bundle[0].image[0]->imgName : "none",
|
||||
(st && st->bundle[0].image[0]) ? st->bundle[0].image[0]->r_sequence : -999,
|
||||
r_sequencenumber,
|
||||
s_fontGeneralScale
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
charHeight = s_fontHeightScale * font->height * s_fontGeneralScale;
|
||||
RB_BeginSurface((shader_t*)font->shader);
|
||||
|
||||
|
||||
@@ -573,7 +573,23 @@ static void Upload32(
|
||||
}
|
||||
else
|
||||
{
|
||||
internalFormat = 3;
|
||||
//
|
||||
// Fixed in OPM
|
||||
// These were the literals 3 and 4 - OpenGL 1.x shorthand
|
||||
// for "three components" and "four components". Legal on
|
||||
// the desktop and meaningless in OpenGL ES, where
|
||||
// internalformat has to be a real enum, so gl4es had
|
||||
// nothing to map and the alpha was dropped on upload.
|
||||
//
|
||||
// It cost every string in the UI. A font atlas is white
|
||||
// glyphs in the alpha channel over a black RGB, so without
|
||||
// alpha each one drew as an opaque black block - measured
|
||||
// on the device as "internal 0x4" against a shader whose
|
||||
// blend and rgbGen were both correct. Images were fine
|
||||
// throughout, because an opaque texture loses nothing when
|
||||
// its alpha goes.
|
||||
//
|
||||
internalFormat = GL_RGB; // was the literal 3
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -603,7 +619,8 @@ static void Upload32(
|
||||
}
|
||||
else
|
||||
{
|
||||
internalFormat = 4;
|
||||
// See the note on GL_RGB above.
|
||||
internalFormat = GL_RGBA; // was the literal 4
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -888,6 +905,39 @@ image_t* R_CreateImageOld(
|
||||
// Here, by using glGenTextures, it will guarantee the uniqueness of the texture
|
||||
qglGenTextures(1, &image->texnum);
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// glGenTextures never legitimately hands back 0 - it is the reserved
|
||||
// "no texture" name - so a zero here means the call did not write, and
|
||||
// everything downstream then works on texture zero without complaining.
|
||||
// Upload32 uploads into it, GL_Bind binds it, and the surface samples
|
||||
// opaque black. That is what every string in the game looked like: a flat
|
||||
// black rectangle with no glyph structure at all, because the font atlas
|
||||
// had been uploaded into the null texture.
|
||||
//
|
||||
// Asking again costs one call on a path that has just read a file off
|
||||
// disk, and it turns a silently broken image into either a working one or
|
||||
// a line in the log.
|
||||
//
|
||||
if (!image->texnum) {
|
||||
qglGenTextures(1, &image->texnum);
|
||||
|
||||
ri.Printf(
|
||||
image->texnum ? PRINT_DEVELOPER : PRINT_WARNING,
|
||||
"R_CreateImage: \"%s\" got no texture name; asked again and got %d\n",
|
||||
name,
|
||||
(int)image->texnum
|
||||
);
|
||||
}
|
||||
|
||||
// Every font atlas, named, as it is created. Which of the two things is
|
||||
// happening to a broken font cannot be told apart from the outside: an
|
||||
// image that was never created, and one that was created without a texture,
|
||||
// look identical by the time anything draws with it.
|
||||
if (vr_traceFont && vr_traceFont->integer && !Q_stricmpn(name, "gfx/fonts/", 10)) {
|
||||
ri.Printf(PRINT_ALL, "image create: \"%s\" slot %d texnum %d\n", name, i, (int)image->texnum);
|
||||
}
|
||||
|
||||
image->numMipmaps = numMipmaps;
|
||||
image->allowPicmip = allowPicmip;
|
||||
image->dynamicallyUpdated = dynamicallyUpdated;
|
||||
|
||||
@@ -50,6 +50,13 @@ 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 *vr_hudOffset;
|
||||
cvar_t *vr_traceHud;
|
||||
|
||||
cvar_t *r_inGameVideo;
|
||||
cvar_t *r_fastsky;
|
||||
cvar_t *r_fastdlights;
|
||||
@@ -405,6 +412,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 +1217,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" );
|
||||
@@ -1400,6 +1425,17 @@ void R_Register( void )
|
||||
r_fastdlights = ri.Cvar_Get("r_fastdlights", "0", CVAR_ARCHIVE | CVAR_LATCH);
|
||||
r_drawSun = ri.Cvar_Get( "r_drawSun", "0", CVAR_ARCHIVE );
|
||||
r_dlightBacks = ri.Cvar_Get( "r_dlightBacks", "1", CVAR_ARCHIVE );
|
||||
// Added in OPM
|
||||
// Deliberately not CVAR_ARCHIVE: a diagnostic that survives into the next
|
||||
// run is one that gets left on. See R_TraceDlight in tr_light.c.
|
||||
vr_traceDlight = ri.Cvar_Get( "vr_traceDlight", "0", 0 );
|
||||
// Added in OPM
|
||||
// Defaults on, which is the engine's own behaviour. It is turned off in
|
||||
// misc/android/autoexec.cfg, and only there, because the bug it works
|
||||
// around has only ever been seen on the device. See
|
||||
// docs/markdown/04-coding/05-dlights.md.
|
||||
r_worldDlights = ri.Cvar_Get( "r_worldDlights", "1", CVAR_ARCHIVE );
|
||||
vr_traceFont = ri.Cvar_Get( "vr_traceFont", "0", 0 );
|
||||
r_finish = ri.Cvar_Get ("r_finish", "0", CVAR_ARCHIVE);
|
||||
r_textureMode = ri.Cvar_Get( "r_textureMode", "gl_linear_mipmap_linear", CVAR_ARCHIVE );
|
||||
r_swapInterval = ri.Cvar_Get( "r_swapInterval", "0", CVAR_ARCHIVE | CVAR_LATCH );
|
||||
@@ -1560,6 +1596,17 @@ 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_VRAdjust2DRect. Scale is the share of the display the HUD is drawn
|
||||
// across; depth is how far out it is made to converge. Offset is how much
|
||||
// of the per-eye optical-axis centring to apply, and wants to be 1 - it is
|
||||
// separable only so that the term can be taken out while looking at it.
|
||||
vr_hudScale = ri.Cvar_Get( "vr_hudScale", "0.55", CVAR_ARCHIVE );
|
||||
vr_hudDepth = ri.Cvar_Get( "vr_hudDepth", "2.0", CVAR_ARCHIVE );
|
||||
vr_hudOffset = ri.Cvar_Get( "vr_hudOffset", "1", CVAR_ARCHIVE );
|
||||
vr_traceHud = ri.Cvar_Get( "vr_traceHud", "0", 0 );
|
||||
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 +1925,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;
|
||||
|
||||
+219
-4
@@ -138,6 +138,25 @@ int R_RealDlightPatch(srfGridMesh_t *srf, int dlightBits)
|
||||
float *origin;
|
||||
drawVert_t *dv;
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// Off, the world stops responding to dynamic lights entirely: no atlas
|
||||
// block for any surface, so tess.dlightMap stays 0 and the texcoords are
|
||||
// never shifted. Lights still exist and still light models and sprites -
|
||||
// only the world is left alone.
|
||||
//
|
||||
// This is a workaround, not a feature. A muzzle flash currently makes the
|
||||
// ground four times *darker* rather than lighting it, and the cause is
|
||||
// not found - see docs/markdown/04-coding/05-dlights.md for what was
|
||||
// measured and what is ruled out. Losing the flash lighting the scenery
|
||||
// is a smaller loss than the ground going black every time a shot is
|
||||
// fired, so it is off on the device until this is understood.
|
||||
//
|
||||
if (!r_worldDlights->integer) {
|
||||
srf->dlightMap[tr.smpFrame] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!srf->lmHeight || !srf->lmWidth) {
|
||||
srf->dlightMap[tr.smpFrame] = 0;
|
||||
return 0;
|
||||
@@ -304,6 +323,25 @@ int R_RealDlightFace(srfSurfaceFace_t *srf, int dlightBits)
|
||||
qboolean added;
|
||||
float *origin;
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// Off, the world stops responding to dynamic lights entirely: no atlas
|
||||
// block for any surface, so tess.dlightMap stays 0 and the texcoords are
|
||||
// never shifted. Lights still exist and still light models and sprites -
|
||||
// only the world is left alone.
|
||||
//
|
||||
// This is a workaround, not a feature. A muzzle flash currently makes the
|
||||
// ground four times *darker* rather than lighting it, and the cause is
|
||||
// not found - see docs/markdown/04-coding/05-dlights.md for what was
|
||||
// measured and what is ruled out. Losing the flash lighting the scenery
|
||||
// is a smaller loss than the ground going black every time a shot is
|
||||
// fired, so it is off on the device until this is understood.
|
||||
//
|
||||
if (!r_worldDlights->integer) {
|
||||
srf->dlightMap[tr.smpFrame] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!srf->lmHeight || !srf->lmWidth) {
|
||||
srf->dlightMap[tr.smpFrame] = 0;
|
||||
return 0;
|
||||
@@ -404,6 +442,25 @@ int R_RealDlightTerrain(cTerraPatchUnpacked_t *srf, int dlightBits)
|
||||
float z00, z01;
|
||||
float z10, z11;
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// Off, the world stops responding to dynamic lights entirely: no atlas
|
||||
// block for any surface, so tess.dlightMap stays 0 and the texcoords are
|
||||
// never shifted. Lights still exist and still light models and sprites -
|
||||
// only the world is left alone.
|
||||
//
|
||||
// This is a workaround, not a feature. A muzzle flash currently makes the
|
||||
// ground four times *darker* rather than lighting it, and the cause is
|
||||
// not found - see docs/markdown/04-coding/05-dlights.md for what was
|
||||
// measured and what is ruled out. Losing the flash lighting the scenery
|
||||
// is a smaller loss than the ground going black every time a shot is
|
||||
// fired, so it is off on the device until this is understood.
|
||||
//
|
||||
if (!r_worldDlights->integer) {
|
||||
srf->drawinfo.dlightMap[tr.smpFrame] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
dli.numLights = 0;
|
||||
|
||||
for (i = 0; i < tr.refdef.num_dlights; i++) {
|
||||
@@ -1471,8 +1528,68 @@ void R_ClearRealDlights()
|
||||
{
|
||||
memset(dli.allocated, 0, sizeof(dli.allocated));
|
||||
dli.dlightMap = 0;
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// See R_TraceDlight. At 2 the whole atlas is stained green before the
|
||||
// front end writes anything into it, and every texel a light reaches is
|
||||
// then painted magenta over the top. Three outcomes, and each names a
|
||||
// different half of the pipe:
|
||||
//
|
||||
// magenta on the walls - the patch, the upload and the sampling all
|
||||
// work, and the black is whatever R_DlightSample
|
||||
// computes for real;
|
||||
// green on the walls - the atlas reaches the surface, but the surface
|
||||
// reads a part of it nothing was written to, so
|
||||
// the fault is in the texcoords or the offset;
|
||||
// still black - the atlas is not reaching the surface at all,
|
||||
// however bright it is, so the fault is the
|
||||
// upload or the bind.
|
||||
//
|
||||
// White, not a colour. A dark green stain was tried first and was a bad
|
||||
// choice in a level made of hedgerows - it could not be told apart from
|
||||
// foliage by eye, and on the device the areas in question turned out to
|
||||
// have R, G and B all roughly equal, which a modulate against a
|
||||
// green-only stain cannot produce. Full white can only make a surface
|
||||
// brighter, so anything reading the atlas outside a written patch becomes
|
||||
// unmissable and nothing else in the frame looks like it.
|
||||
//
|
||||
if (vr_traceDlight && vr_traceDlight->integer >= 2) {
|
||||
int i;
|
||||
|
||||
for (i = 0; i < LIGHTMAP_SIZE * LIGHTMAP_SIZE; i++) {
|
||||
dli.lightmap_buffer[i * 4 + 0] = 255;
|
||||
dli.lightmap_buffer[i * 4 + 1] = 255;
|
||||
dli.lightmap_buffer[i * 4 + 2] = 255;
|
||||
dli.lightmap_buffer[i * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
R_TraceDlight
|
||||
|
||||
Added in OPM, for the port.
|
||||
|
||||
Muzzle flashes and explosions light the world by rewriting its lightmap, not by
|
||||
adding a pass over it - see R_UploadDlights below. So when they come out black
|
||||
on the surrounding surfaces there are only two places the black can be coming
|
||||
from: the patch this builds on the CPU, or what the GPU does with it.
|
||||
|
||||
That is what this splits.
|
||||
|
||||
1 reports what was actually uploaded. On the device that came back bright - peak
|
||||
548 of 765, over half the texels lit, no GL error - which rules the whole CPU
|
||||
half out and says the black is downstream of it.
|
||||
|
||||
2 stains the atlas instead of measuring it, and is read off the walls rather
|
||||
than out of the log: see R_ClearRealDlights for what each colour means.
|
||||
===============
|
||||
*/
|
||||
cvar_t *vr_traceDlight;
|
||||
cvar_t *r_worldDlights;
|
||||
|
||||
/*
|
||||
===============
|
||||
R_UploadDlights
|
||||
@@ -1482,10 +1599,24 @@ void R_UploadDlights()
|
||||
{
|
||||
int i, h;
|
||||
|
||||
if (!tr.pc.c_dlightSurfaces) {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Changed in OPM
|
||||
// There was a "if (!tr.pc.c_dlightSurfaces) return;" here. It is gone
|
||||
// because the height below is the same guard done properly: it comes from
|
||||
// dli.allocated, this allocator's own state, cleared per scene by
|
||||
// R_ClearRealDlights and again at the end of this function, and it is zero
|
||||
// exactly when the front end allocated nothing. tr.pc is the r_speeds
|
||||
// counter block, which R_PerformanceCounters clears on its own schedule -
|
||||
// nothing this depends on should be reading it.
|
||||
//
|
||||
// Recorded because it is a tempting wrong answer: that clear happens in
|
||||
// R_IssueRenderCommands *before* the command list runs, which reads like
|
||||
// it must leave the count at zero by the time RB_BeginDrawingView gets
|
||||
// here, and therefore like the atlas is never uploaded at all. It is not
|
||||
// what happens. Measured on the device, this is reached with the count at
|
||||
// 35 surfaces and the patch bright, so the guard was passing and was never
|
||||
// the reason muzzle flashes come out black.
|
||||
//
|
||||
h = 0;
|
||||
for (i = 0; i < LIGHTMAP_SIZE; i++) {
|
||||
if (h < dli.allocated[i]) {
|
||||
@@ -1501,6 +1632,77 @@ void R_UploadDlights()
|
||||
GL_Bind(tr.dlightImages[dli.dlightMap]);
|
||||
qglTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, LIGHTMAP_SIZE, h, GL_RGBA, GL_UNSIGNED_BYTE, dli.lightmap_buffer);
|
||||
|
||||
if (vr_traceDlight && vr_traceDlight->integer) {
|
||||
//
|
||||
// Added in OPM
|
||||
// This is the one moment in the frame that is known to have a
|
||||
// flash in it. VR_CaptureEye is otherwise on a two second timer
|
||||
// and has missed every flash it has been asked to catch, because
|
||||
// a muzzle flash lasts a frame or two. This tells it to take the
|
||||
// picture now. See VR_CaptureEye in vr_openxr.c.
|
||||
//
|
||||
ri.Cvar_Set("vr_flashFrame", "1");
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// Read after the call and not before, so it belongs to this
|
||||
// upload. GL_RGBA into a texture whose internal format came out
|
||||
// GL_RGB is legal on the desktop and is not on GL ES, and a
|
||||
// rejected sub-upload leaves the atlas holding whatever it had -
|
||||
// which is the difference between "the patch is wrong" and "the
|
||||
// patch never arrived", and cannot be told apart by looking.
|
||||
//
|
||||
const GLenum err = qglGetError();
|
||||
int bright = 0;
|
||||
int peak = 0;
|
||||
int x, y;
|
||||
|
||||
for (y = 0; y < h; y++) {
|
||||
const byte *row = &dli.lightmap_buffer[y * LIGHTMAP_SIZE * 4];
|
||||
|
||||
for (x = 0; x < LIGHTMAP_SIZE; x++) {
|
||||
const int lum = row[x * 4 + 0] + row[x * 4 + 1] + row[x * 4 + 2];
|
||||
|
||||
if (lum > peak) {
|
||||
peak = lum;
|
||||
}
|
||||
if (lum > 96) {
|
||||
bright++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ri.Printf(
|
||||
PRINT_ALL,
|
||||
"dlight: page %d rows %d surfaces %d texels %d | patch peak %d bright %d of %d |"
|
||||
" internal 0x%x err 0x%x | overbright shift %d mult %.3f identity %.3f/%d\n",
|
||||
dli.dlightMap,
|
||||
h,
|
||||
tr.pc.c_dlightSurfaces,
|
||||
tr.pc.c_dlightTexels,
|
||||
peak,
|
||||
bright,
|
||||
h * LIGHTMAP_SIZE,
|
||||
tr.dlightImages[dli.dlightMap]->internalFormat,
|
||||
(unsigned int)err,
|
||||
//
|
||||
// Added in OPM
|
||||
// The dynamic patch is built from the raw BSP lightmap bytes
|
||||
// and shifted by overbrightShift, while the static lightmap it
|
||||
// replaces was scaled by identityLight when it was uploaded.
|
||||
// Those are two different scales for the same quantity, and a
|
||||
// surface that swaps one for the other changes brightness by
|
||||
// the ratio between them - which is what the device shows,
|
||||
// the lit area coming out three to four times darker rather
|
||||
// than black.
|
||||
//
|
||||
tr.overbrightShift,
|
||||
tr.overbrightMult,
|
||||
tr.identityLight,
|
||||
tr.identityLightByte
|
||||
);
|
||||
}
|
||||
|
||||
tr.pc.c_dlightMaps++;
|
||||
memset(dli.allocated, 0, sizeof(dli.allocated));
|
||||
}
|
||||
@@ -1604,6 +1806,19 @@ qboolean R_DlightSample(byte *src, const vec3_t vec, byte *dst)
|
||||
dst[2] = (byte)b;
|
||||
dst[3] = -1;
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// Only the texels a light actually reached, so the magenta traces the
|
||||
// light's own footprint rather than the whole patch. Deliberately after
|
||||
// the real write and not instead of it: everything above still runs, so
|
||||
// this cannot change which surfaces get a dlight map or how big it is.
|
||||
//
|
||||
if (added && vr_traceDlight && vr_traceDlight->integer >= 2) {
|
||||
dst[0] = 255;
|
||||
dst[1] = 0;
|
||||
dst[2] = 255;
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
|
||||
@@ -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_VRAdjust2DRect( int *x, int *y, int *w, int *h );
|
||||
|
||||
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,13 @@ 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 *vr_hudOffset; // share of the optical-axis centring to apply
|
||||
extern cvar_t *vr_traceHud; // report the 2D correction once per eye
|
||||
|
||||
extern cvar_t *r_lodscale;
|
||||
|
||||
extern cvar_t *r_primitives; // "0" = based on compiled vertex array existance
|
||||
@@ -1464,6 +1514,22 @@ extern cvar_t *r_fastsky; // controls whether sky should be cleared or drawn
|
||||
extern cvar_t *r_fastdlights;
|
||||
extern cvar_t *r_drawSun; // controls drawing of sun quad
|
||||
extern cvar_t *r_dlightBacks; // dlight non-facing surfaces for continuity
|
||||
// Added in OPM
|
||||
// Reports what a muzzle flash or an explosion actually wrote into the dynamic
|
||||
// lightmap, and at 2 paints it magenta. See R_TraceDlight in tr_light.c. Named
|
||||
// with the port's other trace switches rather than r_, because it is turned on
|
||||
// and off in misc/android/autoexec.cfg alongside them.
|
||||
extern cvar_t *vr_traceDlight;
|
||||
// Added in OPM
|
||||
// Whether dynamic lights are allowed to rewrite the world's lightmap at all.
|
||||
// A workaround switch, not a feature - see the comment in R_RealDlightFace
|
||||
// and docs/markdown/04-coding/05-dlights.md.
|
||||
extern cvar_t *r_worldDlights;
|
||||
// Added in OPM
|
||||
// One line per font shader loaded, naming what the UI text is actually drawn
|
||||
// with. See R_LoadFontShader in tr_font.cpp.
|
||||
extern cvar_t *vr_traceFont;
|
||||
void RB_TraceDlightBind(const shader_t *shader, const char *what);
|
||||
|
||||
extern cvar_t *r_norefresh; // bypasses the ref rendering
|
||||
extern cvar_t *r_drawentities; // disable/enable entity rendering
|
||||
|
||||
@@ -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);
|
||||
@@ -1448,10 +1520,14 @@ void R_AddSpriteSurfaces()
|
||||
|
||||
tr.currentEntityNum = ENTITYNUM_WORLD;
|
||||
tr.shiftedEntityNum = tr.currentEntityNum << QSORT_ENTITYNUM_SHIFT;
|
||||
if (sprite->hModel && sprite->hModel < tr.numModels) {
|
||||
if (sprite->hModel > 0 && sprite->hModel < tr.numModels) {
|
||||
tr.currentModel = &tr.models[sprite->hModel];
|
||||
} else {
|
||||
tr.currentModel = &tr.models[0];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tr.currentModel->type != MOD_SPRITE || !tr.currentModel->d.sprite) {
|
||||
continue;
|
||||
}
|
||||
|
||||
R_AdjustVisBoundsForSprite(&tr.refdef.sprites[tr.currentSpriteNum], &tr.viewParms, &tr.ori );
|
||||
|
||||
@@ -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 ) ) {
|
||||
|
||||
+114
-3
@@ -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 );
|
||||
@@ -359,6 +377,55 @@ because a surface may be forced to perform a RB_End due
|
||||
to overflow.
|
||||
==============
|
||||
*/
|
||||
/*
|
||||
===================
|
||||
RB_TraceDlightBind
|
||||
|
||||
Added in OPM, for the port. See R_TraceDlight in tr_light.c.
|
||||
|
||||
The lightmap texcoords are rewritten into the dynamic atlas's frame whenever
|
||||
tess.dlightMap is set - RB_SurfaceFace and RB_SurfaceGrid branch on that alone.
|
||||
The atlas is only *bound* in its place if the lightmap is in the particular
|
||||
bundle each stage iterator happens to look at. Where the two disagree, the
|
||||
surface reads the real lightmap atlas at dynamic atlas coordinates: it lands in
|
||||
the parts of it no lightmap was packed into, and those are black.
|
||||
|
||||
That is the shape of what the device shows - large black areas beside small
|
||||
correct ones, when the atlas itself is measured bright and, stained, comes back
|
||||
as magenta and green in exactly the places that are not black.
|
||||
|
||||
Per batch and not per stage: a single stage binding its own texture is normal,
|
||||
and only a batch that finishes without any stage having bound the atlas is
|
||||
wrong. One line per shader per run, because the list of names is the finding.
|
||||
===================
|
||||
*/
|
||||
static qboolean rb_dlightBound;
|
||||
|
||||
void RB_TraceDlightBind(const shader_t *shader, const char *what)
|
||||
{
|
||||
static const shader_t *seen[24];
|
||||
static int numSeen;
|
||||
int i;
|
||||
|
||||
if (!vr_traceDlight || !vr_traceDlight->integer || !shader) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < numSeen; i++) {
|
||||
if (seen[i] == shader) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (numSeen >= (int)(sizeof(seen) / sizeof(seen[0]))) {
|
||||
return;
|
||||
}
|
||||
seen[numSeen++] = shader;
|
||||
|
||||
ri.Printf(PRINT_ALL, "dlight: %s - \"%s\" (%d stages, lightmapIndex %d)\n",
|
||||
what, shader->name, shader->numUnfoggedPasses, shader->lightmapIndex);
|
||||
}
|
||||
|
||||
void RB_BeginSurface( shader_t *shader ) {
|
||||
tess.numIndexes = 0;
|
||||
tess.numVertexes = 0;
|
||||
@@ -369,6 +436,8 @@ void RB_BeginSurface( shader_t *shader ) {
|
||||
tess.numPasses = shader->numUnfoggedPasses;
|
||||
tess.currentStageIteratorFunc = shader->optimalStageIteratorFunc;
|
||||
|
||||
// Added in OPM - see RB_TraceDlightBind.
|
||||
rb_dlightBound = qfalse;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -472,6 +541,7 @@ static void DrawMultitextured( shaderCommands_t *input, int stage ) {
|
||||
R_BindAnimatedImage(&pStage->bundle[1]);
|
||||
} else {
|
||||
GL_Bind(tr.dlightImages[input->dlightMap - 1]);
|
||||
rb_dlightBound = qtrue; // Added in OPM - see RB_TraceDlightBind
|
||||
}
|
||||
|
||||
R_DrawElements( input->numIndexes, input->indexes );
|
||||
@@ -517,7 +587,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;
|
||||
@@ -529,6 +603,32 @@ static void ProjectDlightTexture( void ) {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Added in OPM, for the port. See R_TraceDlight in tr_light.c.
|
||||
//
|
||||
// This is the other dynamic light path, and the one nothing has looked at
|
||||
// yet: Quake 3's, a second additive pass over the surface, used for every
|
||||
// surface the MOHAA atlas did not give a block to - allocation refused, no
|
||||
// light actually landing on a lumel, or a shader with no lightmap at all,
|
||||
// which in the Bocage is most of the foliage.
|
||||
//
|
||||
// It draws the radial falloff texture, which is black at its edges, and it
|
||||
// is only not black on screen because the blend makes black contribute
|
||||
// nothing. If the blend is not in force, those triangles go opaque black -
|
||||
// which is the shape on the device, and the atlas path has now been
|
||||
// measured correct end to end, so this is what is left.
|
||||
//
|
||||
// 3 skips it. If the black goes with it, it is this pass; if it stays,
|
||||
// it is not, and that is worth as much.
|
||||
//
|
||||
if (vr_traceDlight && vr_traceDlight->integer >= 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (vr_traceDlight && vr_traceDlight->integer) {
|
||||
RB_TraceDlightBind(tess.shader, "drawn by the Quake 3 dlight pass, not the atlas");
|
||||
}
|
||||
|
||||
#if idppc_altivec
|
||||
// There has to be a better way to do this so that floatColor
|
||||
// and/or modulate are already 16-byte aligned.
|
||||
@@ -1492,6 +1592,7 @@ static void RB_IterateStagesGeneric( shaderCommands_t *input )
|
||||
else {
|
||||
if (input->dlightMap && tess.xstages[stage]->bundle[0].isLightmap) {
|
||||
GL_Bind(tr.dlightImages[input->dlightMap - 1]);
|
||||
rb_dlightBound = qtrue; // Added in OPM - see RB_TraceDlightBind
|
||||
}
|
||||
else {
|
||||
R_BindAnimatedImage(&pStage->bundle[0]);
|
||||
@@ -1770,6 +1871,7 @@ void RB_StageIteratorLightmappedMultitextureUnfogged( void ) {
|
||||
}
|
||||
if (tess.dlightMap && tess.xstages[0]->bundle[1].isLightmap) {
|
||||
GL_Bind(tr.dlightImages[tess.dlightMap - 1]);
|
||||
rb_dlightBound = qtrue; // Added in OPM - see RB_TraceDlightBind
|
||||
} else {
|
||||
R_BindAnimatedImage(&tess.xstages[0]->bundle[1]);
|
||||
}
|
||||
@@ -1859,6 +1961,15 @@ void RB_EndSurface( void ) {
|
||||
//
|
||||
tess.currentStageIteratorFunc();
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// The texcoords went into the dynamic atlas's frame and nothing put the
|
||||
// atlas under them. See RB_TraceDlightBind.
|
||||
//
|
||||
if (tess.dlightMap && !rb_dlightBound) {
|
||||
RB_TraceDlightBind(tess.shader, "texcoords shifted, atlas never bound");
|
||||
}
|
||||
|
||||
if (!(tr.refdef.rdflags & RDF_NOWORLDMODEL) && !backEnd.in2D)
|
||||
{
|
||||
//
|
||||
|
||||
@@ -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 );
|
||||
|
||||
|
||||
@@ -61,13 +61,13 @@ static int CullSprite(const vec3_t* points) {
|
||||
|
||||
for (i = 0; i < backEnd.viewParms.fog.extrafrustums + 4; i++) {
|
||||
for (j = 0; j < 4; j++) {
|
||||
if (DotProduct(points[j], backEnd.viewParms.frustum[i].normal) - backEnd.viewParms.frustum[i].dist > 0.0f) {
|
||||
if (DotProduct(points[j], backEnd.viewParms.frustum[i].normal) >= backEnd.viewParms.frustum[i].dist) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (j == 4) {
|
||||
return CULL_CLIP;
|
||||
return CULL_OUT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,15 +87,15 @@ void RB_DrawSprite( const refSprite_t *spr ) {
|
||||
float org_x, org_y;
|
||||
float scale;
|
||||
|
||||
if (!spr->hModel)
|
||||
if (spr->hModel <= 0 || spr->hModel >= tr.numModels)
|
||||
{
|
||||
ri.Printf(PRINT_WARNING, "No model found for Sprite\n");
|
||||
return;
|
||||
}
|
||||
|
||||
model = tr.models;
|
||||
if (spr->hModel > 0 && spr->hModel < tr.numModels) {
|
||||
model = &tr.models[spr->hModel];
|
||||
model = &tr.models[spr->hModel];
|
||||
if (model->type != MOD_SPRITE || !model->d.sprite) {
|
||||
return;
|
||||
}
|
||||
|
||||
scale = spr->scale * model->d.sprite->scale;
|
||||
@@ -108,10 +108,16 @@ void RB_DrawSprite( const refSprite_t *spr ) {
|
||||
float cr, sr;
|
||||
int i;
|
||||
|
||||
invmag = 1.0 / sqrt(spr->axis[1][2] * spr->axis[1][2] + spr->axis[1][1] * spr->axis[1][1]);
|
||||
|
||||
cr = invmag * spr->axis[1][1];
|
||||
sr = invmag * spr->axis[1][2];
|
||||
invmag = spr->axis[1][2] * spr->axis[1][2] + spr->axis[1][1] * spr->axis[1][1];
|
||||
if (invmag > 0.0f) {
|
||||
invmag = 1.0f / sqrt(invmag);
|
||||
cr = invmag * spr->axis[1][1];
|
||||
sr = invmag * spr->axis[1][2];
|
||||
} else {
|
||||
// No projected roll axis: keep the quad finite and unrotated.
|
||||
cr = 1.0f;
|
||||
sr = 0.0f;
|
||||
}
|
||||
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
@@ -121,6 +127,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);
|
||||
@@ -136,15 +164,36 @@ void RB_DrawSprite( const refSprite_t *spr ) {
|
||||
VectorCopy(backEnd.viewParms.ori.axis[0], norm);
|
||||
VectorCopy(backEnd.viewParms.ori.axis[1], right);
|
||||
VectorCopy(backEnd.viewParms.ori.axis[2], up);
|
||||
if (backEnd.viewParms.ori.axis[0][2] > 0.999) {
|
||||
return;
|
||||
} else if (backEnd.viewParms.ori.axis[0][2] < -0.999) {
|
||||
// VR facing comes from the viewer's position, so looking vertically
|
||||
// does not make an otherwise visible upright sprite degenerate.
|
||||
if (!vrView.active && fabs(backEnd.viewParms.ori.axis[0][2]) > 0.999f) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
// Directly above/below: use a stable axis, even when the view
|
||||
// direction is vertical too.
|
||||
VectorSet(right, 1.0f, 0.0f, 0.0f);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
@@ -375,10 +375,30 @@ void RB_SurfaceBeam( void )
|
||||
RB_SurfaceFace
|
||||
==============
|
||||
*/
|
||||
// Added in OPM - see the reports in the dlight branches of RB_SurfaceFace and
|
||||
// RB_DrawTerrainTris. Capped rather than deduplicated: the spread across
|
||||
// surfaces is the measurement. Restart to re-arm them.
|
||||
static int rb_terrainSpans;
|
||||
static int rb_faceSpans;
|
||||
|
||||
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;
|
||||
@@ -434,6 +454,41 @@ void RB_SurfaceFace( srfSurfaceFace_t *surf ) {
|
||||
*(unsigned int*)&tess.vertexColors[ndx] = *(unsigned int*)&v[7];
|
||||
tess.vertexDlightBits[ndx] = dlightBits;
|
||||
}
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// Which rows of the atlas this face's texcoords actually reach. Only
|
||||
// the rows R_UploadDlights calls "rows N" are ever sent; past that the
|
||||
// texture keeps what it had, which is black. So a face running beyond
|
||||
// N is reading rows nothing wrote this frame - a black wall. The
|
||||
// offset it is shifted by is printed too, because that is the term
|
||||
// under suspicion. See RB_TraceDlightBind in tr_shade.c.
|
||||
//
|
||||
if (vr_traceDlight && vr_traceDlight->integer && rb_faceSpans < 24 && numPoints > 0) {
|
||||
float sMin = 1.0e9f, sMax = -1.0e9f, tMin = 1.0e9f, tMax = -1.0e9f;
|
||||
int n;
|
||||
|
||||
for (n = 0, ndx = tess.numVertexes; n < numPoints; n++, ndx++) {
|
||||
const float s = tess.texCoords[ndx][1][0];
|
||||
const float t = tess.texCoords[ndx][1][1];
|
||||
|
||||
if (s < sMin) { sMin = s; }
|
||||
if (s > sMax) { sMax = s; }
|
||||
if (t < tMin) { tMin = t; }
|
||||
if (t > tMax) { tMax = t; }
|
||||
}
|
||||
|
||||
rb_faceSpans++;
|
||||
ri.Printf(PRINT_ALL,
|
||||
"dlight: face %d rows %.1f..%.1f cols %.1f..%.1f of 128,"
|
||||
" offset %.4f,%.4f lm %dx%d at %d,%d map %d\n",
|
||||
rb_faceSpans,
|
||||
tMin * LIGHTMAP_SIZE, tMax * LIGHTMAP_SIZE,
|
||||
sMin * LIGHTMAP_SIZE, sMax * LIGHTMAP_SIZE,
|
||||
surf->lightmapOffset[0], surf->lightmapOffset[1],
|
||||
surf->lmWidth, surf->lmHeight, surf->lmX, surf->lmY,
|
||||
tess.dlightMap);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -817,9 +872,31 @@ void RB_DrawTerrainTris(srfTerrain_t* p) {
|
||||
|
||||
dlightBits = p->dlightBits[backEnd.smpFrame];
|
||||
tess.dlightBits |= dlightBits;
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// Terrain picks its texcoords off the patch's own flag while the atlas is
|
||||
// bound off the batch's tess.dlightMap, which comes from the sort key.
|
||||
// Those are two different pieces of state and nothing makes them agree, so
|
||||
// this asks whether they do. See RB_TraceDlightBind in tr_shade.c.
|
||||
//
|
||||
if (p->dlightMap[backEnd.smpFrame] != tess.dlightMap) {
|
||||
RB_TraceDlightBind(tess.shader, "terrain patch and batch disagree");
|
||||
}
|
||||
|
||||
if (p->dlightMap[backEnd.smpFrame])
|
||||
{
|
||||
float lmScale = (1.0 / LIGHTMAP_SIZE) / p->lmapStep;
|
||||
//
|
||||
// Added in OPM
|
||||
// Where in the atlas this patch's texcoords actually land. The atlas is
|
||||
// 128 texels tall but only the rows R_UploadDlights reports as "rows N"
|
||||
// are ever sent; past that the texture holds whatever it held last,
|
||||
// which is black. So a patch whose t runs beyond N/128 is reading rows
|
||||
// nothing wrote to this frame, and that is a black patch of ground.
|
||||
// See RB_TraceDlightBind in tr_shade.c.
|
||||
//
|
||||
float sMin = 1.0e9f, sMax = -1.0e9f, tMin = 1.0e9f, tMax = -1.0e9f;
|
||||
|
||||
for (i = p->iVertHead; i; i = g_pVert[i].iNext) {
|
||||
assert(tess.numVertexes < SHADER_MAX_VERTEXES);
|
||||
@@ -829,6 +906,17 @@ void RB_DrawTerrainTris(srfTerrain_t* p) {
|
||||
tess.texCoords[tess.numVertexes][0][1] = g_pVert[i].texCoords[0][1];
|
||||
tess.texCoords[tess.numVertexes][1][0] = g_pVert[i].xyz[0] * lmScale + p->lmapX;
|
||||
tess.texCoords[tess.numVertexes][1][1] = g_pVert[i].xyz[1] * lmScale + p->lmapY;
|
||||
|
||||
if (vr_traceDlight && vr_traceDlight->integer) {
|
||||
const float s = tess.texCoords[tess.numVertexes][1][0];
|
||||
const float t = tess.texCoords[tess.numVertexes][1][1];
|
||||
|
||||
if (s < sMin) { sMin = s; }
|
||||
if (s > sMax) { sMax = s; }
|
||||
if (t < tMin) { tMin = t; }
|
||||
if (t > tMax) { tMax = t; }
|
||||
}
|
||||
|
||||
tess.normal[tess.numVertexes][0] = 0;
|
||||
tess.normal[tess.numVertexes][1] = 0;
|
||||
tess.normal[tess.numVertexes][2] = 1.0;
|
||||
@@ -840,6 +928,22 @@ void RB_DrawTerrainTris(srfTerrain_t* p) {
|
||||
g_pVert[i].iVertArray = tess.numVertexes;
|
||||
tess.numVertexes++;
|
||||
}
|
||||
|
||||
// Added in OPM - see above. Capped rather than deduplicated by shader:
|
||||
// the spread across patches is the measurement, and every one of them
|
||||
// is the same shader. Restart to re-arm it.
|
||||
if (vr_traceDlight && vr_traceDlight->integer && sMax >= sMin
|
||||
&& rb_terrainSpans < 24) {
|
||||
rb_terrainSpans++;
|
||||
ri.Printf(PRINT_ALL,
|
||||
"dlight: terrain patch %d rows %.1f..%.1f cols %.1f..%.1f of 128,"
|
||||
" step %g size %d, batch map %d patch map %d\n",
|
||||
rb_terrainSpans,
|
||||
tMin * LIGHTMAP_SIZE, tMax * LIGHTMAP_SIZE,
|
||||
sMin * LIGHTMAP_SIZE, sMax * LIGHTMAP_SIZE,
|
||||
p->lmapStep, p->lmapSize,
|
||||
tess.dlightMap, p->dlightMap[backEnd.smpFrame]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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,414 @@
|
||||
/*
|
||||
===========================================================================
|
||||
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_VRGet2DTransform
|
||||
|
||||
The one correction flat content needs in a headset, as a scale about the centre
|
||||
of the eye buffer and an offset in half-widths of it.
|
||||
|
||||
Three things are wrong with 2D in a headset that are not wrong on a screen. 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; identical 2D also has no disparity, so
|
||||
whatever does line up sits at infinity rather than in front of the player; and
|
||||
the edges of the display are outside what can comfortably be read.
|
||||
|
||||
Returns false when there is nothing to correct, which is every flat frame and
|
||||
every pass that draws onto a compositor quad - the wrist panel and the screen
|
||||
layer both have the compositor place them in the room, so applying an eye's
|
||||
frustum to them would be doing the same job twice, oppositely.
|
||||
================
|
||||
*/
|
||||
static qboolean R_VRGet2DTransform(float *scale, float *offX, float *offY)
|
||||
{
|
||||
float tanWidth, tanHeight;
|
||||
float depth, parallax;
|
||||
|
||||
if (!vrView.active) {
|
||||
return qfalse;
|
||||
}
|
||||
|
||||
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 qfalse;
|
||||
}
|
||||
|
||||
*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;
|
||||
}
|
||||
|
||||
// Where the eye's optical axis falls in its own buffer, as a share of half
|
||||
// the buffer. Zero tangent is straight ahead; the runtime's frustum is
|
||||
// asymmetric, so that is not the middle of the image.
|
||||
*offX = -(vrView.tanRight + vrView.tanLeft) / tanWidth;
|
||||
*offY = -(vrView.tanUp + vrView.tanDown) / tanHeight;
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// How much of the frustum centring to apply. 1 puts the content on the
|
||||
// optical axis, which is what makes the two eyes agree; 0 centres it on
|
||||
// the buffer instead, which is wrong but symmetric.
|
||||
//
|
||||
// Kept as a knob because it is the term that decides fusion: it is equal
|
||||
// and opposite in the two eyes, so an error here pulls the images apart
|
||||
// rather than merely misplacing them. Anything other than 1 is a way of
|
||||
// asking whether the tangents are being reported honestly, not a setting.
|
||||
//
|
||||
if (vr_hudOffset) {
|
||||
*offX *= vr_hudOffset->value;
|
||||
*offY *= vr_hudOffset->value;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
//
|
||||
// Added in OPM
|
||||
// One line per eye, once. Convergence cannot be judged from a screenshot -
|
||||
// the Quest's capture is a reprojected composite of one eye, so the two
|
||||
// images that have to fuse are never both in it. These are the numbers
|
||||
// that decide it, and the last of them is the whole answer: the disparity
|
||||
// is how far apart the same pixel lands in the two eyes, and it is either
|
||||
// the half-IPD the depth asks for or it is not.
|
||||
//
|
||||
{
|
||||
static int reported;
|
||||
|
||||
if (vr_traceHud && vr_traceHud->integer) {
|
||||
if (!(reported & (1 << vrView.eye))) {
|
||||
reported |= 1 << vrView.eye;
|
||||
ri.Printf(
|
||||
PRINT_ALL,
|
||||
"vr hud: eye %d tan L%.3f R%.3f U%.3f D%.3f | scale %.2f depth %.2f offsetCvar %.2f"
|
||||
" | offX %.4f offY %.4f parallax %.4f | shift %.1f,%.1f px of %dx%d\n",
|
||||
vrView.eye,
|
||||
vrView.tanLeft, vrView.tanRight, vrView.tanUp, vrView.tanDown,
|
||||
*scale, depth, vr_hudOffset ? vr_hudOffset->value : 1.0f,
|
||||
*offX, *offY, parallax,
|
||||
*offX * glConfig.vidWidth * 0.5f,
|
||||
*offY * glConfig.vidHeight * 0.5f,
|
||||
glConfig.vidWidth, glConfig.vidHeight
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Re-armed while it is off, so turning it on later still says
|
||||
// something. Otherwise the one report is spent during startup.
|
||||
reported = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return qtrue;
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
R_VRAdjust2DRect
|
||||
|
||||
Moves a 2D viewport or scissor rectangle to suit the eye it is about to be drawn
|
||||
into. Coordinates are pixels in the eye buffer, GL's way up.
|
||||
|
||||
Expressed as a change to the rectangle rather than to the ortho window, and this
|
||||
is the whole point of the function.
|
||||
|
||||
The correction is a property of the display: the same number of pixels, in the
|
||||
same direction, for everything flat in the frame. An ortho window is not - in
|
||||
this engine it is usually a single widget. UIWidget::set2D gives every widget a
|
||||
viewport and an ortho window cut to that widget's own frame, and the HUD is
|
||||
built out of widgets: hud_compass, hud_health, hud_ammo, hud_weapons, hud_items
|
||||
are each a menu drawn that way. Correcting the window therefore scaled the
|
||||
correction to the widget, and a hundred and fifty pixel compass in a sixteen
|
||||
hundred pixel eye received about a twelfth of the shift it needed.
|
||||
|
||||
That is what the HUD refusing to fuse actually was. Every element ended up with
|
||||
a disparity in proportion to its own width, so each one sat at a different
|
||||
apparent depth - the objectives, drawn through the full screen window the 3D
|
||||
view sets up, converged where they were asked to; the compass beside them sat
|
||||
near infinity. There is no vergence a pair of eyes can adopt that resolves both,
|
||||
so the HUD reads as doubled no matter what vr_hudDepth is set to.
|
||||
|
||||
Applied to the rectangle it comes out in pixels, which every 2D window shares,
|
||||
so all of it converges together whatever window it was drawn through. It also
|
||||
puts vr_hudScale back in charge of where things are rather than only how big
|
||||
they are: scaling about the centre of the display draws the corners inwards,
|
||||
where before each widget merely shrank where it stood.
|
||||
================
|
||||
*/
|
||||
void R_VRAdjust2DRect(int *x, int *y, int *w, int *h)
|
||||
{
|
||||
float scale, offX, offY;
|
||||
float halfW, halfH;
|
||||
float x0, y0, x1, y1;
|
||||
|
||||
if (!R_VRGet2DTransform(&scale, &offX, &offY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
halfW = glConfig.vidWidth * 0.5f;
|
||||
halfH = glConfig.vidHeight * 0.5f;
|
||||
|
||||
// Both edges are mapped and then rounded, rather than rounding the origin
|
||||
// and scaling the size, so that neighbouring rectangles stay neighbouring.
|
||||
x0 = halfW + (*x - halfW) * scale + offX * halfW;
|
||||
y0 = halfH + (*y - halfH) * scale + offY * halfH;
|
||||
x1 = x0 + *w * scale;
|
||||
y1 = y0 + *h * scale;
|
||||
|
||||
*x = (int)floor(x0 + 0.5f);
|
||||
*y = (int)floor(y0 + 0.5f);
|
||||
*w = (int)floor(x1 + 0.5f) - *x;
|
||||
*h = (int)floor(y1 + 0.5f) - *y;
|
||||
|
||||
// A zero width viewport is undefined and a zero width scissor draws
|
||||
// nothing; a widget small enough to round away is better drawn thin.
|
||||
if (*w < 1) {
|
||||
*w = 1;
|
||||
}
|
||||
if (*h < 1) {
|
||||
*h = 1;
|
||||
}
|
||||
}
|
||||
@@ -669,11 +669,113 @@ RB_SetGL2D
|
||||
|
||||
================
|
||||
*/
|
||||
/*
|
||||
================
|
||||
R_VRAdjust2DRect
|
||||
|
||||
Moves a 2D viewport or scissor rectangle to suit the eye it is about to be drawn
|
||||
into. Coordinates are pixels in the eye buffer, GL's way up.
|
||||
|
||||
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; identical 2D also has no disparity, so
|
||||
whatever 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 rectangle rather than to the ortho window, because
|
||||
the correction belongs to the display and the window usually does not. See the
|
||||
long note on the same function in renderergl1/tr_vr.c: UIWidget::set2D gives
|
||||
every widget an ortho window cut to its own frame, so correcting the window
|
||||
scaled the correction to the widget and left each HUD element converging at a
|
||||
different depth.
|
||||
|
||||
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_VRAdjust2DRect( int *x, int *y, int *w, int *h )
|
||||
{
|
||||
float scale, depth;
|
||||
float tanWidth, tanHeight;
|
||||
float offX, offY, parallax;
|
||||
float halfW, halfH;
|
||||
float x0, y0, x1, y1;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Where the eye's optical axis falls in its own buffer, as a share of half
|
||||
// the buffer. Zero tangent is straight ahead; the runtime's frustum is
|
||||
// asymmetric, so that is not the middle of the image.
|
||||
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;
|
||||
|
||||
halfW = glConfig.vidWidth * 0.5f;
|
||||
halfH = glConfig.vidHeight * 0.5f;
|
||||
|
||||
// Both edges are mapped and then rounded, rather than rounding the origin
|
||||
// and scaling the size, so that neighbouring rectangles stay neighbouring.
|
||||
x0 = halfW + ( *x - halfW ) * scale + offX * halfW;
|
||||
y0 = halfH + ( *y - halfH ) * scale + offY * halfH;
|
||||
x1 = x0 + *w * scale;
|
||||
y1 = y0 + *h * scale;
|
||||
|
||||
*x = (int)floor( x0 + 0.5f );
|
||||
*y = (int)floor( y0 + 0.5f );
|
||||
*w = (int)floor( x1 + 0.5f ) - *x;
|
||||
*h = (int)floor( y1 + 0.5f ) - *y;
|
||||
|
||||
// A zero width viewport is undefined and a zero width scissor draws
|
||||
// nothing; a widget small enough to round away is better drawn thin.
|
||||
if ( *w < 1 ) {
|
||||
*w = 1;
|
||||
}
|
||||
if ( *h < 1 ) {
|
||||
*h = 1;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -691,10 +793,20 @@ void RB_SetGL2D (void) {
|
||||
}
|
||||
|
||||
// set 2D virtual screen size
|
||||
qglViewport( 0, 0, width, height );
|
||||
qglScissor( 0, 0, width, height );
|
||||
{
|
||||
int vx = 0, vy = 0, vw = width, vh = height;
|
||||
|
||||
Mat4Ortho(0, width, height, 0, 0, 1, matrix);
|
||||
R_VRAdjust2DRect( &vx, &vy, &vw, &vh );
|
||||
qglViewport( vx, vy, vw, vh );
|
||||
qglScissor( vx, vy, vw, vh );
|
||||
}
|
||||
|
||||
left = 0;
|
||||
right = width;
|
||||
bottom = height;
|
||||
top = 0;
|
||||
|
||||
Mat4Ortho(left, right, bottom, top, 0, 1, matrix);
|
||||
GL_SetProjectionMatrix(matrix);
|
||||
Mat4Identity(matrix);
|
||||
GL_SetModelviewMatrix(matrix);
|
||||
@@ -1934,7 +2046,7 @@ void RB_ExecuteRenderCommands( const void *data ) {
|
||||
|
||||
// stop rendering
|
||||
t2 = ri.Milliseconds ();
|
||||
backEnd.pc.msec = t2 - t1;
|
||||
backEnd.pc.msec += t2 - t1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,10 @@ void R_IssueRenderCommands( qboolean runPerformanceCounters ) {
|
||||
cmdList->used = 0;
|
||||
|
||||
if ( runPerformanceCounters ) {
|
||||
int backEndMsec = backEnd.pc.msec;
|
||||
R_PerformanceCounters();
|
||||
// EndFrame still needs the time spent in earlier command batches.
|
||||
backEnd.pc.msec = backEndMsec;
|
||||
}
|
||||
|
||||
// actually start the commands going
|
||||
@@ -135,7 +138,7 @@ Issue any pending commands and wait for them to complete.
|
||||
====================
|
||||
*/
|
||||
void R_IssuePendingRenderCommands( void ) {
|
||||
if ( !tr.registered ) {
|
||||
if ( !tr.registered || (!backEndData->commands.used && !tess.numIndexes) ) {
|
||||
return;
|
||||
}
|
||||
R_IssueRenderCommands( qfalse );
|
||||
|
||||
@@ -504,6 +504,15 @@ void Set2DWindow(int x, int y, int w, int h, float left, float right, float bott
|
||||
backEnd.projection2D = qtrue;
|
||||
backEnd.last2DFBO = glState.currentFBO;
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Applied to the rectangle and not to the ortho window, because the window
|
||||
// is usually a single widget's and the correction is the whole display's.
|
||||
// See R_VRAdjust2DRect.
|
||||
R_VRAdjust2DRect(&x, &y, &w, &h);
|
||||
|
||||
qglViewport(x, y, w, h);
|
||||
qglScissor(x, y, w, h);
|
||||
|
||||
@@ -530,6 +539,11 @@ RE_Scissor
|
||||
================
|
||||
*/
|
||||
void RE_Scissor(int x, int y, int width, int height) {
|
||||
// The UI clips each widget to its own frame right after setting the window
|
||||
// for it, so this has to move with the window or a widget the headset's
|
||||
// correction has shifted is clipped away at where it used to be.
|
||||
R_VRAdjust2DRect(&x, &y, &width, &height);
|
||||
|
||||
qglEnable(GL_SCISSOR_TEST);
|
||||
qglScissor(x, y, width, height);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -533,10 +533,10 @@ void R_LoadFontShader(fontheader_sgl_t* font)
|
||||
{
|
||||
for (i = 0; i < fontshader->numUnfoggedPasses; i++)
|
||||
{
|
||||
if (fontshader->stages[0] != NULL && fontshader->stages[0]->active)
|
||||
if (fontshader->stages[i] != NULL && fontshader->stages[i]->active)
|
||||
{
|
||||
fontshader->stages[0]->rgbGen = CGEN_ENTITY;
|
||||
fontshader->stages[0]->alphaGen = AGEN_ENTITY;
|
||||
fontshader->stages[i]->rgbGen = CGEN_ENTITY;
|
||||
fontshader->stages[i]->alphaGen = AGEN_ENTITY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_VRAdjust2DRect( int *x, int *y, int *w, int *h );
|
||||
|
||||
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;
|
||||
@@ -3119,13 +3154,13 @@ void R_AddSpriteSurfaces()
|
||||
|
||||
tr.currentEntityNum = ENTITYNUM_WORLD;
|
||||
tr.shiftedEntityNum = tr.currentEntityNum << QSORT_REFENTITYNUM_SHIFT;
|
||||
if (sprite->hModel && sprite->hModel < tr.numModels) {
|
||||
if (sprite->hModel > 0 && sprite->hModel < tr.numModels) {
|
||||
tr.currentModel = tr.models[sprite->hModel];
|
||||
} else {
|
||||
tr.currentModel = tr.models[0];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tr.currentModel->type != MOD_SPRITE) {
|
||||
if (tr.currentModel->type != MOD_SPRITE || !tr.currentModel->d.sprite) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 );
|
||||
|
||||
|
||||
@@ -61,13 +61,13 @@ static int CullSprite(const vec3_t* points) {
|
||||
|
||||
for (i = 0; i < backEnd.viewParms.fog.extrafrustums + 4; i++) {
|
||||
for (j = 0; j < 4; j++) {
|
||||
if (DotProduct(points[j], backEnd.viewParms.frustum[i].normal) - backEnd.viewParms.frustum[i].dist > 0.0f) {
|
||||
if (DotProduct(points[j], backEnd.viewParms.frustum[i].normal) >= backEnd.viewParms.frustum[i].dist) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (j == 4) {
|
||||
return CULL_CLIP;
|
||||
return CULL_OUT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,15 +88,15 @@ void RB_DrawSprite( const refSprite_t *spr ) {
|
||||
float scale;
|
||||
int i;
|
||||
|
||||
if (!spr->hModel)
|
||||
if (spr->hModel <= 0 || spr->hModel >= tr.numModels)
|
||||
{
|
||||
ri.Printf(PRINT_WARNING, "No model found for Sprite\n");
|
||||
return;
|
||||
}
|
||||
|
||||
model = tr.models;
|
||||
if (spr->hModel > 0 && spr->hModel < tr.numModels) {
|
||||
model = tr.models[spr->hModel];
|
||||
model = tr.models[spr->hModel];
|
||||
if (model->type != MOD_SPRITE || !model->d.sprite) {
|
||||
return;
|
||||
}
|
||||
|
||||
scale = spr->scale * model->d.sprite->scale;
|
||||
@@ -109,10 +109,16 @@ void RB_DrawSprite( const refSprite_t *spr ) {
|
||||
float cr, sr;
|
||||
int i;
|
||||
|
||||
invmag = 1.0 / sqrt(spr->axis[1][2] * spr->axis[1][2] + spr->axis[1][1] * spr->axis[1][1]);
|
||||
|
||||
cr = invmag * spr->axis[1][1];
|
||||
sr = invmag * spr->axis[1][2];
|
||||
invmag = spr->axis[1][2] * spr->axis[1][2] + spr->axis[1][1] * spr->axis[1][1];
|
||||
if (invmag > 0.0f) {
|
||||
invmag = 1.0f / sqrt(invmag);
|
||||
cr = invmag * spr->axis[1][1];
|
||||
sr = invmag * spr->axis[1][2];
|
||||
} else {
|
||||
// No projected roll axis: keep the quad finite and unrotated.
|
||||
cr = 1.0f;
|
||||
sr = 0.0f;
|
||||
}
|
||||
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
@@ -122,6 +128,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);
|
||||
@@ -137,15 +165,36 @@ void RB_DrawSprite( const refSprite_t *spr ) {
|
||||
VectorCopy(backEnd.viewParms.ori.axis[0], norm);
|
||||
VectorCopy(backEnd.viewParms.ori.axis[1], right);
|
||||
VectorCopy(backEnd.viewParms.ori.axis[2], up);
|
||||
if (backEnd.viewParms.ori.axis[0][2] > 0.999) {
|
||||
return;
|
||||
} else if (backEnd.viewParms.ori.axis[0][2] < -0.999) {
|
||||
// VR facing comes from the viewer's position, so looking vertically
|
||||
// does not make an otherwise visible upright sprite degenerate.
|
||||
if (!vrView.active && fabs(backEnd.viewParms.ori.axis[0][2]) > 0.999f) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
// Directly above/below: use a stable axis, even when the view
|
||||
// direction is vertical too.
|
||||
VectorSet(right, 1.0f, 0.0f, 0.0f);
|
||||
}
|
||||
} 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,82 @@ 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_VR_GetHeadOffset
|
||||
|
||||
Hands the head's offset within the play space to the game, so that a trace from
|
||||
the player's eye starts where the player is actually looking from.
|
||||
|
||||
Single player only, for the same reason as the aim above: a dedicated server has
|
||||
no headset, answers false, and the game keeps the body's eye position.
|
||||
==================
|
||||
*/
|
||||
static qboolean SV_VR_GetHeadOffset( vec3_t out, float *baseYaw ) {
|
||||
#if defined(USE_OPENXR) && !defined(DEDICATED)
|
||||
return VR_GetHeadOffset( out, baseYaw );
|
||||
#else
|
||||
return qfalse;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
SV_VR_GetClimbPull
|
||||
|
||||
Hands the ladder drag to the game, which is the only place that can act on it:
|
||||
the climb moves the player's origin, and where the player may be moved to is a
|
||||
question about the world.
|
||||
|
||||
Single player only, as above.
|
||||
==================
|
||||
*/
|
||||
static qboolean SV_VR_GetClimbPull( vec3_t out ) {
|
||||
#if defined(USE_OPENXR) && !defined(DEDICATED)
|
||||
return VR_GetClimbPull( out );
|
||||
#else
|
||||
return qfalse;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
SV_VR_GetThrow
|
||||
|
||||
Hands the weapon hand's velocity to the game, so a thrown grenade leaves along
|
||||
the arm's own swing.
|
||||
|
||||
Single player only, as with the rest of these.
|
||||
==================
|
||||
*/
|
||||
static qboolean SV_VR_GetThrow( vec3_t velocity, float *baseYaw ) {
|
||||
#if defined(USE_OPENXR) && !defined(DEDICATED)
|
||||
return VR_GetThrow( velocity, baseYaw );
|
||||
#else
|
||||
return qfalse;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
SV_InitGameProgs
|
||||
@@ -1950,6 +2029,11 @@ void SV_InitGameProgs( void ) {
|
||||
|
||||
import.Cvar_Find = Cvar_FindVar;
|
||||
|
||||
import.VR_GetWeaponAim = SV_VR_GetWeaponAim;
|
||||
import.VR_GetHeadOffset = SV_VR_GetHeadOffset;
|
||||
import.VR_GetClimbPull = SV_VR_GetClimbPull;
|
||||
import.VR_GetThrow = SV_VR_GetThrow;
|
||||
|
||||
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,344 @@
|
||||
/*
|
||||
===========================================================================
|
||||
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. False means skip rendering
|
||||
// (inactive session, hidden frame, or unavailable tracking). Still call
|
||||
// VR_SubmitFrame to end any begun frame with no layers.
|
||||
qboolean VR_BeginFrame(void);
|
||||
|
||||
// Bracket each eye's rendering. Between them the engine draws its scene into
|
||||
// the framebuffer VR_PrepareEye bound.
|
||||
// False if the swapchain image is not ready; skip drawing and finishing it.
|
||||
qboolean 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 head is within the play space, from the tracking origin, in engine
|
||||
units and the engine's frame, with the base yaw the caller needs to rotate it
|
||||
into the body's frame:
|
||||
|
||||
bodyAngles = (0, viewYaw - baseYaw, 0);
|
||||
eye = playerStateEyePos + offset[i] * bodyAxis[i];
|
||||
|
||||
That is the same composition R_VRComposeView uses to make the camera, and it
|
||||
has to be, or the answer is a point the player is not looking from.
|
||||
|
||||
For the game rather than the renderer: the eye position in the player state is
|
||||
the body's, and the camera has the play space offset on top of it. A trace from
|
||||
the body's eye is aimed from the wrong place whenever the two have come apart,
|
||||
which is precisely when the body is against something and the head is not.
|
||||
|
||||
False when there is no headset, and the caller keeps the eye position it has.
|
||||
*/
|
||||
qboolean VR_GetHeadOffset(vec3_t out, float *baseYaw);
|
||||
|
||||
/*
|
||||
Whether the player is on a ladder. The client reads it out of the player state
|
||||
and tells the VR layer, which cannot see the player state and needs to know:
|
||||
the grips hold rungs there instead of steadying the rifle and reloading.
|
||||
*/
|
||||
void VR_SetClimbing(qboolean climbing);
|
||||
|
||||
/*
|
||||
Whether the player has a weapon in hand. The client reads it out of the player
|
||||
state; the VR layer uses it to tell a hand reaching for a holster from a hand
|
||||
bringing something back to one.
|
||||
*/
|
||||
void VR_SetArmed(qboolean armed);
|
||||
|
||||
/*
|
||||
The weapon hand's velocity, in engine units a second and the play space's
|
||||
frame, with the base yaw needed to rotate it into the world. For throwing:
|
||||
a grenade should leave the hand going where the hand was going.
|
||||
|
||||
False when the hand is not moving enough to be a throw.
|
||||
*/
|
||||
qboolean VR_GetThrow(vec3_t velocity, float *baseYaw);
|
||||
|
||||
/*
|
||||
How far the hands holding the ladder have dragged since this was last asked, in
|
||||
engine units and the engine's frame, measured in the play space.
|
||||
|
||||
The body moves the other way by this much, which is what makes it a drag: pull
|
||||
down and you rise by the same distance, stop and you stop. Both hands averaged
|
||||
when both are holding on, so gripping with two hands does not climb twice as
|
||||
fast.
|
||||
|
||||
False when nothing is being pulled, and the caller leaves the player where the
|
||||
climb animation put them.
|
||||
*/
|
||||
qboolean VR_GetClimbPull(vec3_t out);
|
||||
|
||||
/*
|
||||
The weapon wheel's state, for cgame to draw. True while it is held open, with
|
||||
the segment the controller is pointing at in selection - or -1 for the dead
|
||||
zone in the middle, which selects nothing and leaves the weapon in hand.
|
||||
|
||||
names is filled with the class name of each segment, clockwise from straight
|
||||
up, so the caller labels them without knowing what they are.
|
||||
*/
|
||||
qboolean VR_GetWheel(int *selection, const char **names, int maxNames, int *count);
|
||||
|
||||
/*
|
||||
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
|
||||
+5546
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,132 @@
|
||||
# A muzzle flash makes the ground darker
|
||||
|
||||
**Status: unsolved, parked, worked around.** `r_worldDlights 0` in
|
||||
`misc/android/autoexec.cfg` turns the world's dynamic lighting off on the
|
||||
device. Flashes no longer light the scenery, which is a smaller loss than the
|
||||
ground going black every time a shot is fired.
|
||||
|
||||
This is the record of what was measured, so that picking it up again does not
|
||||
start from nothing. Seven theories were tried and all seven were wrong. Every
|
||||
one came from reading the renderer; every useful fact came from the device.
|
||||
|
||||
---
|
||||
|
||||
## 1. The symptom, measured
|
||||
|
||||
Firing in `m4l1` makes a large area of ground **darker**, not black, in lockstep
|
||||
with the trigger. From a 30 fps capture, the fraction of the frame darker than
|
||||
luminance 12:
|
||||
|
||||
```
|
||||
206: 0.183 │ 207: 0.035 208: 0.035 209: 0.035
|
||||
210: 0.181 │ 211: 0.175 212: 0.173
|
||||
213: 0.036 │ 214: 0.035 215: 0.036
|
||||
216: 0.179 │ 217: 0.172 218: 0.173
|
||||
219: 0.037 │ 220: 0.037 221: 0.037
|
||||
```
|
||||
|
||||
Three frames on, three off, at the firing cadence. Differencing frame 216
|
||||
against 215:
|
||||
|
||||
- **336,359 pixels — 16.2% of the frame** go from mean luminance 25.1 to 6.2
|
||||
- that is a **4.06× multiply**, and the pixels keep their texture (std 2–8,
|
||||
max 68), so it is a modulate and not a paint-over
|
||||
- the mask is the **ground plane**: the lower half of the frame with hard
|
||||
straight polygon edges following the road and the hedgerow silhouette
|
||||
|
||||
"Renders black" in the original report is a surface being multiplied down, not
|
||||
geometry being covered. Chasing a paint-over cost four rounds.
|
||||
|
||||
## 2. What is ruled out, and by what
|
||||
|
||||
Everything below was measured on the device, not reasoned about.
|
||||
|
||||
| Claim | How it died |
|
||||
|---|---|
|
||||
| `R_UploadDlights` early-returns on a cleared `tr.pc.c_dlightSurfaces`, so the atlas is never sent | The trace prints `surfaces 36–46` *at the point of upload*. The guard was passing. |
|
||||
| `glTexSubImage2D` sends `GL_RGBA` into a `GL_RGB` texture — illegal on ES | `err 0x0`. gl4es accepts it. `internal 0x1907` confirms the format really is `GL_RGB`. |
|
||||
| Terrain is missing the world-origin term in its dynamic texcoords | It is there, `tr_light.c:650-652`. And terrain is **never dlit here** — zero `dlight: terrain` lines in any run. |
|
||||
| Surfaces shift their texcoords into the atlas frame without the atlas being bound | Zero batches reported it. Every batch that shifted, bound. |
|
||||
| Faces address atlas rows past the uploaded height | All 24 sampled faces land exactly on their own block, inside the uploaded rows. Worked example below. |
|
||||
| Quake 3's `ProjectDlightTexture` second pass is drawing black | It **never runs**. Zero lines. |
|
||||
| The dynamic patch and the static lightmap use different brightness scales | `R_ColorShiftLightingBytes` (`tr_bsp.c:132`) is `<< overbrightShift` then normalise-by-max. `R_DlightSample`'s tail is the same two operations, on the same bytes — `lmData` points into `s_worldData.lighting`, the memcpy of the lump `R_LoadLightmaps` built the static lightmaps from. For an unlit texel the two are identical by construction. |
|
||||
|
||||
Worked example of the texcoord check, from `vr_traceDlight 1`:
|
||||
|
||||
```
|
||||
face 1 rows 4.6..6.6 cols 109.7..111.7 offset 0.4062,-0.0234 lm 4x4 at 57,7
|
||||
```
|
||||
|
||||
`offset = x/128 - lmX/128`, so `0.4062 = x/128 - 57/128` gives `x = 109`, and the
|
||||
face covers cols 109.7–111.7. Same for t: `y = 4`, block 4 tall, rows 4.6–6.6.
|
||||
Exact. Face 12 (`22x16 at 49,60`, offset `-0.3828,-0.3750`) gives `x=0, y=12`,
|
||||
covering rows 13–27 of an atlas 28 rows tall. Also exact.
|
||||
|
||||
Also checked and not the cause:
|
||||
|
||||
- **SMP.** `smpFrame` is declared in both `trGlobals_t` and `backEndState_t` and
|
||||
**never assigned anywhere**. Front end and back end are always index 0.
|
||||
- **`texels` exceeding the uploaded area.** `texels 9275` against `rows 28`
|
||||
(3584 texels) looks impossible but is not: `R_RealDlightFace` counts
|
||||
`c_dlightTexels` *before* it knows whether any light landed, then bails at
|
||||
`if (!added)` without committing to `dli.allocated`, so the block is handed
|
||||
straight back out to the next surface. Failed surfaces are counted and take no
|
||||
space. This is not a bug and cost a round.
|
||||
|
||||
## 3. The one thing that is known to matter
|
||||
|
||||
Turning the world's dynamic lighting off — `r_worldDlights 0`, which makes
|
||||
`R_RealDlightFace`, `R_RealDlightPatch` and `R_RealDlightTerrain` return 0 so
|
||||
no surface ever gets an atlas block — **makes the dimming stop**.
|
||||
|
||||
So the subsystem is involved, despite every component of it measuring correct.
|
||||
That is the contradiction to resume on.
|
||||
|
||||
## 4. Where to pick it up
|
||||
|
||||
The next measurement was half-written when this was parked: print, for a few
|
||||
dlit faces, the raw source texel, the texel actually written into the atlas, and
|
||||
what `R_ColorShiftLightingBytes` would produce for that same raw texel. All
|
||||
three should agree for any texel no light reached. If they do not, that is the
|
||||
bug and the numbers say where. If they do agree, the dimming is downstream of
|
||||
the patch contents entirely, and the next suspect is what changes in the *draw*
|
||||
when `tess.dlightMap` becomes non-zero — a different texture object bound on
|
||||
TMU1, with whatever env, filter and wrap state that carries with it under
|
||||
gl4es.
|
||||
|
||||
One specific thing never checked: `tr.dlightImages[]` are created by
|
||||
`R_CreateImageOld` with the name `*lightmapD%i`, and the `isLightmap` test is
|
||||
`strncmp(name, "*lightmap", 9)` — which those names match, so the
|
||||
`else if (!strncmp(name, "*lightmapD", 10))` branch that sets
|
||||
`dynamicallyUpdated` is **dead code**. `dynamicallyUpdated` is passed to
|
||||
`Upload32` and never read there, so it appears inert, but the classification
|
||||
also decides `internalFormat` and `TMU`. Worth an hour before anything else.
|
||||
|
||||
## 5. The diagnostics, and how to use them
|
||||
|
||||
All in `vr_traceDlight`, off by default, registered in `tr_init.c`:
|
||||
|
||||
| Level | What it does |
|
||||
|---|---|
|
||||
| `1` | One line per atlas upload: page, uploaded rows, surface and texel counts, brightest texel of the patch out of 765, the texture's internal format, the GL error, and the overbright and identity scales. Plus per-face and per-terrain-patch reports of exactly which atlas rows and columns each surface's texcoords cover, against the block it was allocated. |
|
||||
| `2` | Stops measuring, starts staining: the whole atlas is painted **white** before the front end writes to it, and every texel a light reaches is painted magenta on top. Read off the walls. White can only brighten, so anything reading unwritten atlas is unmissable. |
|
||||
| `3` | Additionally skips `ProjectDlightTexture`. Measured: it never runs here, so this changes nothing. Kept because proving that took a run. |
|
||||
|
||||
Two lessons about the probes themselves, which cost more than the bug so far:
|
||||
|
||||
**A stain must not look like the level.** The first stain was dark green, in a
|
||||
level made of hedgerows. Neither of us could tell it from foliage by eye, and
|
||||
two rounds were spent on "is that green or is that a bush". White was
|
||||
unambiguous immediately. The pixels also killed it independently: the dimmed
|
||||
areas have R, G and B roughly equal, which a modulate against a green-only
|
||||
stain cannot produce.
|
||||
|
||||
**Screen recording destroys the log.** A whole run was lost because the Quest's
|
||||
recorder floods logcat and evicts everything but the shutdown lines.
|
||||
`adb logcat -G 32M` helps but does not fix it. Record video **or** read the log,
|
||||
never both in one run.
|
||||
|
||||
**Frame differencing beat six rounds of description.** Pulling the recording,
|
||||
extracting every frame, and differencing a flash frame against its neighbour
|
||||
produced the cadence, the 4× ratio and the affected geometry in one step. Do
|
||||
that first next time, not seventh.
|
||||
@@ -0,0 +1,4 @@
|
||||
.gradle/
|
||||
local.properties
|
||||
*.apk
|
||||
*.aab
|
||||
@@ -0,0 +1,78 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
// The native libraries are built by CMake, not by gradle. See
|
||||
// docs/markdown/android.md - build the engine first, then package it here.
|
||||
def nativeLibs = file(rootProject.file(nativeLibsDir).absolutePath)
|
||||
|
||||
def sdlJava = sdlJavaDir?.trim()
|
||||
if (!sdlJava) {
|
||||
// The CMake build records the SDL tree it used next to the staged libraries
|
||||
def fromCMake = file("${nativeLibs}/../android-build.properties")
|
||||
if (fromCMake.exists()) {
|
||||
def props = new Properties()
|
||||
fromCMake.withInputStream { props.load(it) }
|
||||
sdlJava = props.getProperty('sdlJavaDir')
|
||||
}
|
||||
}
|
||||
|
||||
if (!sdlJava || !file(sdlJava).exists()) {
|
||||
throw new GradleException(
|
||||
"Could not find SDL's Java sources. Build the engine with CMake first, " +
|
||||
"or pass -PsdlJavaDir=<SDL source>/android-project/app/src/main/java. " +
|
||||
"They must come from the same SDL the engine was linked against.")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'org.openmoh.openmohaa'
|
||||
compileSdk 34
|
||||
|
||||
// Only so AGP can find the NDK's strip tool; the engine itself is built by
|
||||
// CMake outside gradle. Without it the libraries ship with full debug info,
|
||||
// which works but makes for a much larger APK.
|
||||
def ndkFromEnv = System.getenv('ANDROID_NDK_HOME')
|
||||
if (ndkFromEnv) {
|
||||
ndkPath = ndkFromEnv
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.openmoh.openmohaa"
|
||||
// Quest 3 runs Android 12 (API 32)
|
||||
minSdk 29
|
||||
targetSdk 32
|
||||
versionCode 2
|
||||
versionName "0.83.0-vr0.2.0"
|
||||
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs += sdlJava
|
||||
jniLibs.srcDirs = [nativeLibs]
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// The engine is already built optimised; nothing here to shrink
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
// The engine binaries are large and already compressed poorly; leaving
|
||||
// them uncompressed lets the loader mmap them straight out of the APK.
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
useLegacyPackaging false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,406 @@
|
||||
// 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
|
||||
|
||||
// The HUD and the objectives live on a panel on the player's left wrist, shown
|
||||
// by turning the wrist up to look at it, and the laser pointer already reaches
|
||||
// it. Flat content in the eye buffers has no stereo disparity to fuse while
|
||||
// each eye still warps it through its own asymmetric frustum, which is the
|
||||
// problem the quad layers exist to solve.
|
||||
//
|
||||
// The compass is deliberately not on it - it is drawn heads up in both eyes
|
||||
// every frame instead, because it is how the player knows which way the next
|
||||
// objective is and a heading you have to ask for is one you navigate without.
|
||||
// See UI_DrawVRCompass in cl_ui.cpp.
|
||||
//
|
||||
// Set here as well as defaulted in code because it is CVAR_ARCHIVE: a value
|
||||
// already in omconfig.cfg would otherwise win over the new default.
|
||||
// The wrist panel is off. The HUD - compass, ammo, health, objectives - is
|
||||
// drawn into both eye buffers instead and floated in front of the player by
|
||||
// R_VRAdjust2DRect, at vr_hudDepth metres. That is the same thing RTCWQuest
|
||||
// does with cg_hudDepth and cg_hudScale, and it is what "a floating 2D panel
|
||||
// half a metre away" actually is.
|
||||
//
|
||||
// It was on for a while, which put the HUD on a quad strapped to the left
|
||||
// wrist. That works, but it is a glance away rather than always there, and the
|
||||
// compass has to be always there to be worth having.
|
||||
set vr_wristPanel 0
|
||||
|
||||
// The gathered VR HUD: compass, objectives, health, ammo, weapon strip, drawn
|
||||
// as one group in the middle of the view by CG_DrawVRHud.
|
||||
//
|
||||
// The retail HUD is five menus loaded from ui/hud_*.urc, each aligned to a
|
||||
// different corner of a 4:3 screen. On a monitor those corners are a glance
|
||||
// away; through a lens they are the far edge, where all five were reported as
|
||||
// present and unreadable. This draws the same numbers again in one place,
|
||||
// reading them from cl.snap.ps.stats[] - the same array the widgets read - so
|
||||
// nothing has to be recovered out of the UI.
|
||||
//
|
||||
// 0 puts the retail corner HUD back, without a rebuild. 1 keeps the group
|
||||
// hidden and brings it up on the off hand's X button for vr_hudTime seconds
|
||||
// before it fades - almost everything it carries is only wanted at the moment
|
||||
// it is asked for, and a HUD that is always there is always in the way. 2 pins
|
||||
// it up, which is what to use while arguing with the layout.
|
||||
set vr_hud 1
|
||||
set vr_hudTime 3
|
||||
|
||||
// Where the group sits and how big it is. X and Y are fractions of the display,
|
||||
// so they mean the same thing at any eye resolution; scale is a multiplier on a
|
||||
// cluster authored against a 1080 line display.
|
||||
//
|
||||
// Y is not where it looks like it is, and it is worth knowing why before
|
||||
// dialling it. It places the cluster in the flat frame, and the renderer then
|
||||
// takes that frame, scales it to vr_hudScale about the middle of the display,
|
||||
// and lifts it by the eye's vertical frustum asymmetry - 170 pixels of 1760 on
|
||||
// this headset. Both of those pull the number towards the middle of the view,
|
||||
// so the range that reaches the eye at all is about 0.13 to 0.68 of it:
|
||||
//
|
||||
// 0.62 -> 0.47 of the eye, just above the middle
|
||||
// 0.73 -> 0.53, just below it
|
||||
// 0.85 -> 0.60
|
||||
// 0.92 -> 0.63
|
||||
// 1.00 -> 0.68, as low as it goes
|
||||
//
|
||||
// 0.92 puts it well down the view, which is where something you check without
|
||||
// breaking your aim belongs.
|
||||
set vr_hudGroupX 0.5
|
||||
set vr_hudGroupY 0.92
|
||||
set vr_hudGroupScale 1.0
|
||||
|
||||
// How solid the group is over the world. Worth pulling down once the layout
|
||||
// settles - a HUD you can see through is less in the way of a game that is
|
||||
// mostly about looking at things.
|
||||
set vr_hudAlpha 1.0
|
||||
|
||||
// Where the retail compass widget sits, for vr_hud 0. Unused while the gathered
|
||||
// HUD is up, since that draws its own compass ring.
|
||||
//
|
||||
// hud_compass.urc aligns it left top, which is 0,0. On a monitor that is a
|
||||
// corner you can glance at; in a headset it is the far edge of the lens.
|
||||
set vr_compassX 0.5
|
||||
set vr_compassY 0.62
|
||||
|
||||
// How far in front of the player the flat HUD floats, in metres. Back to the
|
||||
// reference's default: at 0.5 the eyes have to converge hard on something
|
||||
// filling the middle of the view, and it came back as double vision that would
|
||||
// not fuse.
|
||||
set vr_hudDepth 2.0
|
||||
|
||||
// How much of the frustum centring to apply to the flat HUD. 0 centres it on
|
||||
// the buffer, 1 centres it on the eye's optical axis.
|
||||
//
|
||||
// Back to 1, and it has to be 1. This was set to 0 on the reading that the
|
||||
// centring was overshooting - a marker box came out at (595,530) of a 1680x1760
|
||||
// eye instead of (840,880) - but that measurement was of the old bug, which
|
||||
// scaled the correction to whichever widget's ortho window it was handed rather
|
||||
// than to the display. See R_VRAdjust2DRect.
|
||||
//
|
||||
// 0 is the one setting that cannot fuse at all. The centre of the left eye's
|
||||
// buffer is about seven degrees left of straight ahead and the centre of the
|
||||
// right eye's is about seven degrees right, because the runtime hands out a
|
||||
// frustum that is wider towards each temple. Putting the HUD at both buffer
|
||||
// centres therefore asks the eyes to diverge by fourteen degrees, and eyes do
|
||||
// not diverge - they hold two images instead, which is the double vision. 1
|
||||
// puts it on both optical axes, where the only disparity left is the half IPD
|
||||
// the parallax deliberately adds.
|
||||
set vr_hudOffset 1
|
||||
|
||||
// One line per eye naming the tangents, the centring, the parallax and the
|
||||
// resulting shift in pixels. Convergence cannot be read off a screenshot - the
|
||||
// headset capture is one reprojected eye, so the two images that have to fuse
|
||||
// are never both in it. The two lines have to report shifts that are equal and
|
||||
// opposite; if they do not, nothing downstream of them will fuse.
|
||||
set vr_traceHud 0
|
||||
|
||||
// How much of the display the flat HUD is drawn across. This and vr_hudDepth
|
||||
// are the mechanism RTCWQuest uses for its in-game HUD (cg_hudScale,
|
||||
// cg_hudDepth) and R_VRAdjust2DRect is the same maths: the 2D is scaled into
|
||||
// the readable middle of the view, recentred per eye to undo the asymmetric
|
||||
// frustum, then given a horizontal parallax of half an IPD at vr_hudDepth so
|
||||
// the two eyes fuse it at that range instead of at infinity.
|
||||
//
|
||||
// Everything flat in the eye buffers rides on this - compass, ammo, health,
|
||||
// objectives.
|
||||
set vr_hudScale 0.55
|
||||
|
||||
// The wrist panel's geometry, in metres. All archived rather than forced from
|
||||
// code, so these can be dialled in with a push and a force-stop instead of a
|
||||
// rebuild - which matters, because every one of them is a feel value that can
|
||||
// only be judged with the thing on your arm.
|
||||
//
|
||||
// vr_wristSize how big the quad is
|
||||
// vr_wristDistance how far it stands off the back of the hand
|
||||
// vr_wristBack how far back along the forearm, towards where a watch
|
||||
// sits - the controller's own pose is out at the fingers
|
||||
//
|
||||
// It was 0.26 across held 0.16 out, which is a billboard floating above the
|
||||
// arm rather than something worn on it. Smaller and closer now.
|
||||
set vr_wristSize 0.24
|
||||
set vr_wristDistance 0.05
|
||||
set vr_wristBack 0.10
|
||||
|
||||
// Which part of the 1680x1760 screen the panel shows, as fractions. The HUD is
|
||||
// laid out for a whole screen and uses a small share of it, so showing less is
|
||||
// what makes what is left big enough to read - the compositor crops for free.
|
||||
// The default is the top band at full width, which is where the compass and
|
||||
// the objectives are.
|
||||
// Framed on the objectives block. CG_DrawObjectives puts it at x 25..475, and
|
||||
// its top comes from UI_GetObjectivesTop, which now keeps it clear of the
|
||||
// enlarged compass - so at vr_compassScale 3 it starts at y 389 and runs to
|
||||
// about 490 of the 1680x1760 buffer.
|
||||
//
|
||||
// At full width it was a twentieth of the panel and far too small to read;
|
||||
// this fills the panel with it. The quad takes the shape of the crop, so a wide
|
||||
// short crop is a wide short panel and nothing is stretched.
|
||||
//
|
||||
// If vr_compassScale changes, this has to follow it: the objectives start
|
||||
// directly below the compass, at 128 times that scale.
|
||||
set vr_wristCropX 0
|
||||
set vr_wristCropY 0.21
|
||||
set vr_wristCropW 0.32
|
||||
set vr_wristCropH 0.10
|
||||
|
||||
// One line every two seconds naming why the objectives are or are not on the
|
||||
// panel: which pass drew them, the alpha after the panel override, and how many
|
||||
// the level has actually set. Zero set means the level never sent any, which a
|
||||
// map entered straight through vr_startMap - past the scripting that hands them
|
||||
// out - is a very good way to arrange.
|
||||
set vr_traceObjectives 0
|
||||
|
||||
// The objectives stay up instead of being a hold-the-key overlay. There is no
|
||||
// key to hold in a headset.
|
||||
set vr_objectivesAlways 1
|
||||
|
||||
// The reticle, off. It is drawn at the centre of the screen, which in a headset
|
||||
// is the centre of the player's head - and the weapon is aimed by the
|
||||
// controller, not by where they are looking, so it marks a spot the gun is not
|
||||
// pointing at and follows the face around. 1 puts it back.
|
||||
set vr_crosshair 0
|
||||
|
||||
// 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 0
|
||||
|
||||
// Opt-in synchronous GPU timing. This waits for GPU completion after each eye
|
||||
// and changes frame performance; leave it off for normal play and CPU profiling.
|
||||
set vr_traceGpu 0
|
||||
|
||||
set vr_captureEye 0
|
||||
|
||||
// One line per use press, naming which of the three ways it can come up empty
|
||||
// actually happened: the body's eye, the eye the trace really started from
|
||||
// after the play space offset, how far the trace got, and what the box at the
|
||||
// end of it caught. A press that prints nothing never reached the game at all,
|
||||
// which points at the binding rather than at the reach.
|
||||
set vr_traceUse 0
|
||||
|
||||
// Weapons live on the body: reach to a hip or over a shoulder and squeeze the
|
||||
// grip to draw, the way Half-Life: Alyx does it. Right hip is the pistol, left
|
||||
// hip grenades, and the long guns are slung over the shoulders - rifle right,
|
||||
// smg left. Either hand can draw from any of them.
|
||||
set vr_holsters 1
|
||||
|
||||
// One line per grip squeeze, giving where the hand was relative to the head
|
||||
// when it closed and which zone that landed in - or "nothing", which is the
|
||||
// interesting case. The zone boxes in vr_openxr.c are a first guess at where
|
||||
// people actually reach; this is how to replace them with measurements.
|
||||
set vr_traceHolster 0
|
||||
|
||||
// One line per upload of the dynamic lightmap - which is how a muzzle flash or
|
||||
// an explosion lights the world in this engine: it rewrites the lightmap of
|
||||
// every surface the light reaches rather than adding a pass over them.
|
||||
//
|
||||
// At 1, "patch peak" is the brightest texel uploaded, out of 765. Measured on
|
||||
// the device: peak 548, over half the texels lit, no GL error. So the patch the
|
||||
// CPU builds is bright and it reaches GL without complaint, and whatever turns
|
||||
// the walls black is downstream of both.
|
||||
//
|
||||
// At 2 it stops measuring and starts staining: the whole atlas goes white
|
||||
// before the front end touches it, and every texel a light reaches is painted
|
||||
// magenta over the top. Read off the walls, not out of the log. White can only
|
||||
// brighten, so anything reading the atlas outside a written patch is
|
||||
// unmissable. A dark green stain was tried first and was useless in a level
|
||||
// made of hedgerows.
|
||||
//
|
||||
// 3 additionally skips ProjectDlightTexture, Quake 3's second-pass dynamic
|
||||
// light. Measured: it never runs here, so this changes nothing.
|
||||
//
|
||||
// Off, because none of it is needed to play. Turn it on to resume the hunt.
|
||||
set vr_traceDlight 0
|
||||
|
||||
// One line per font shader loaded, naming what the UI text is actually drawn
|
||||
// with - whether the lookup found gfx.shader's entry or fell back to an opaque
|
||||
// default, and whether the atlas kept its alpha through the upload. Both would
|
||||
// produce the black boxes the UI currently draws instead of text.
|
||||
set vr_traceFont 0
|
||||
|
||||
// The world does not respond to dynamic lights. This is a workaround for an
|
||||
// unsolved bug and not a preference: a muzzle flash makes the ground about
|
||||
// four times *darker* rather than lighting it, over roughly a sixth of the
|
||||
// screen, for as long as the trigger is held. Losing flashes lighting the
|
||||
// scenery is the smaller loss.
|
||||
//
|
||||
// Lights still exist and still light models and sprites - only the world's
|
||||
// lightmap is left alone. Set it back to 1 to see the bug.
|
||||
//
|
||||
// docs/markdown/04-coding/05-dlights.md has the measurements, the seven
|
||||
// theories that were wrong, and where to pick it up.
|
||||
set r_worldDlights 0
|
||||
|
||||
// One line per throw: the peak speed of the swing and the direction it was
|
||||
// going, in the play space's own frame. The scale and clamp applied to that in
|
||||
// weapon.cpp are guesses about how hard a person throws in a living room, and
|
||||
// this is what replaces them with a measurement.
|
||||
set vr_traceThrow 0
|
||||
|
||||
// Grenade throwing, all four archived - which is exactly why they are set here
|
||||
// as well as defaulted in code. An archived cvar keeps whatever omconfig.cfg
|
||||
// already has, so changing a default in the source does nothing at all on a
|
||||
// device that has run before. That trap cost a build here: the floor stayed at
|
||||
// its old 0.45 while the curve moved under it, and every throw came out
|
||||
// clamped to the same 45% regardless of how hard it was thrown.
|
||||
//
|
||||
// vr_throwFull hand speed, in units a second, counting as a full throw.
|
||||
// Measured: hard overhand 278-338, gentle lob 145-192.
|
||||
// vr_throwCurve above 1 bends the gentle end down without touching the top,
|
||||
// which is what stops a 45 degree lob outrunning a flat throw.
|
||||
// vr_throwMin floor, so a polite swing still leaves the hand.
|
||||
// vr_throwDamp how much of the sideways drift to take out of a slow throw,
|
||||
// scaled by how gentle it is. 0 is honest physics and throws
|
||||
// lobs where the arm actually went, which is not where the
|
||||
// player meant. The only deliberate lie in here.
|
||||
set vr_throwFull 340
|
||||
set vr_throwCurve 1.5
|
||||
set vr_throwMin 0.2
|
||||
set vr_throwDamp 0.8
|
||||
|
||||
// --- 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,3 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '8.1.1' apply false
|
||||
}
|
||||
@@ -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