mirror of
https://github.com/bearstonem/openmohaa-vr.git
synced 2026-10-18 03:00:36 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ead71109fc | ||
|
|
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,39 +1,73 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
name: OpenMoHAA VR bug report
|
||||
about: Report a VR, gameplay, rendering, or performance issue in this port
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Report port issues at https://github.com/bearstonem/openmohaa-vr/issues.
|
||||
Meta Quest is the current standalone release target. Steam Frame is a development
|
||||
target; no Steam Frame binary is available yet. Please identify the build you tested.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
What went wrong? For visual issues, describe whether it affects one eye or both,
|
||||
the world, HUD, menus, or controller models.
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
1. Start the game using '...'
|
||||
2. Load mission/map or multiplayer mode '...'
|
||||
3. Use these controls or settings: '...'
|
||||
4. Observe '...'
|
||||
|
||||
Does it happen every time? Did a previous port release work?
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots (OPTIONAL)**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
**Headset and build**
|
||||
|
||||
- Headset model:
|
||||
- Headset OS / firmware version:
|
||||
- OpenXR runtime and version, if known:
|
||||
- Controllers and input mode:
|
||||
- OpenMoHAA VR release / downloaded asset name:
|
||||
- Git commit and build configuration, if built from source:
|
||||
- Engine version from the startup log or `version` console command:
|
||||
- Host OS and GPU, if applicable to a development build:
|
||||
|
||||
**Game and settings**
|
||||
|
||||
- Game edition: Allied Assault / Spearhead / Breakthrough:
|
||||
- Mission/map and single-player or multiplayer:
|
||||
- Mods or custom assets:
|
||||
- Changed VR/render settings and whether the release's `autoexec.cfg` was installed:
|
||||
- For performance issues: refresh rate, render scale, and measured frame time/FPS, if available:
|
||||
|
||||
**Screenshots or recording (optional)**
|
||||
|
||||
Attach a short capture when useful. Describe headset-only symptoms that a recording
|
||||
does not show, such as double vision, flickering in one eye, or tracking drift.
|
||||
|
||||
**Logs**
|
||||
- Paste the output of the console window in a text file, and attach the text file.
|
||||
- If applicable (`logfile` set to 2), attach the `qconsole.log` file. On Windows, it can be found in **%APPDATA%\...\qconsole.log** and on Linux, it can be found on the home directory **~/.openmohaa/.../qconsole.log**.
|
||||
|
||||
**Version information (please complete the following information):**
|
||||
- OS: [e.g. `Debian`]
|
||||
- OS Version [e.g `12`]
|
||||
- OpenMoHAA Version: [e.g `0.70.0-alpha+0.0b1a20dcf win_msvc64-x86_64-debug`, can be found below **Common Initialization** in the console, or by typing `version`]
|
||||
- Attach the startup log and the output around the failure as a text file.
|
||||
- On Quest, start log capture before reproducing: `adb logcat -v threadtime -s openmohaa:V AndroidRuntime:E > openmohaa-vr.log`.
|
||||
- Include `qconsole.log` when available (`logfile 2` enables it), plus crash output or a backtrace for crashes.
|
||||
- For source builds on other targets, include relevant OpenXR/runtime logs and the exact launch command.
|
||||
- Remove personal information or server passwords before attaching logs.
|
||||
|
||||
**Dump (OPTIONAL)**
|
||||
You can include a core dump generated by the OS (`.dmp` file). If you compiled OpenMoHAA yourself, also add all output openmohaa binaries (except libopenal and libSDL).
|
||||
**Crash dump (optional)**
|
||||
|
||||
If available, attach a crash dump or Android tombstone. For a source build, identify
|
||||
the matching commit and retain its binaries/debug symbols for follow-up debugging.
|
||||
|
||||
**Additional context (optional)**
|
||||
|
||||
**Additional context (OPTIONAL)**
|
||||
Add any other context about the problem here.
|
||||
@@ -1,4 +1,6 @@
|
||||
build*
|
||||
!/misc/android/build.gradle
|
||||
!/misc/android/app/build.gradle
|
||||
*.swp
|
||||
*tags
|
||||
*~
|
||||
|
||||
+6
-1
@@ -1,6 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
For **OpenMoHAA VR** changes, see the [VR release notes](https://github.com/bearstonem/openmohaa-vr/releases)
|
||||
and [port status](VR_PORT_STATUS.md). Quest is the current release target;
|
||||
Steam Frame integration is in development.
|
||||
|
||||
The changelog below is inherited from upstream OpenMoHAA and records engine
|
||||
changes. Its release links and version numbers belong to the upstream project.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
@@ -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)
|
||||
|
||||
+45
-57
@@ -1,94 +1,79 @@
|
||||
# Contributing guidelines
|
||||
# Contributing to OpenMoHAA VR
|
||||
|
||||
Whether you're reporting issues or coding, follow the guidelines below to help keep the project consistent and maintainable.
|
||||
OpenMoHAA VR is an independent VR port of [OpenMoHAA](https://github.com/openmoh/openmohaa), with standalone Meta Quest releases and Steam Frame as a development target. There is no Steam Frame binary yet. These guidelines apply to [bearstonem/openmohaa-vr](https://github.com/bearstonem/openmohaa-vr); upstream maintains its own contribution policies and release process.
|
||||
|
||||
Start with the [port status](VR_PORT_STATUS.md), [repository layout](LAYOUT.md), and [renderer handoff](VR_RENDERER_HANDOFF.md). The status document separates device-tested behavior from changes that still need headset validation.
|
||||
|
||||
All contributions, including **issues**, **discussions**, **code comments**, **commit messages**, and **documentation**, must be written in *English*. This ensures clarity and consistency for all contributors.
|
||||
|
||||
There are many ways to contribute. Contributions are welcome in the following areas:
|
||||
|
||||
- Fixing [bugs and crashes](#issues).
|
||||
- Documentation improvements
|
||||
- Performance improvements
|
||||
- Testing
|
||||
- Helping users (issues, discussions)
|
||||
- Suggesting enhancements (via issues only)
|
||||
- VR rendering, tracking, controller interaction, and HUD improvements.
|
||||
- Performance improvements backed by measurements.
|
||||
- Headset testing and reproducible regression reports.
|
||||
- Documentation and build improvements.
|
||||
- Steam Frame development and testing, with support claims kept explicit.
|
||||
- Helping users and proposing enhancements through this repository's issues.
|
||||
|
||||
New features are designed and implemented by maintainers, so the project's direction and maintenance burden stay consistent.
|
||||
|
||||
If you want to propose a new feature, please open an issue first.
|
||||
|
||||
Do not start implementing a new feature before getting approval. Pull requests that introduce unapproved features may be closed without review.
|
||||
Discuss major features or architecture changes in a [port issue](https://github.com/bearstonem/openmohaa-vr/issues) before investing in a large implementation. Focused fixes, tests, and documentation improvements can be submitted directly.
|
||||
|
||||
---
|
||||
|
||||
## Issues
|
||||
|
||||
Before opening a new issue:
|
||||
1. Make sure you're running the [latest development version](https://github.com/openmoh/openmohaa/releases/tag/dev).
|
||||
2. Check if the issue already exists.
|
||||
|
||||
When filing an issue, include as much detail as possible, this helps reproducing and fixing the problem effectively:
|
||||
- Clear description of the bug.
|
||||
- Steps to reproduce.
|
||||
- Log file (`qconsole.log`) and crash output if applicable:
|
||||
- Attach the relevant log file.
|
||||
- For crashes, paste the console output that includes the backtrace.
|
||||
- Operating System and version. Example: `Debian 13`.
|
||||
- Full game version. Example: `0.70.0-alpha+0.0b1a20dcf win_msvc64-x86_64-debug`. The version is obtained at the start of the log file, or by executing the `version` command in console.
|
||||
1. Check the [port releases](https://github.com/bearstonem/openmohaa-vr/releases) and [known issues](VR_PORT_STATUS.md). Upstream desktop releases do not include this port's VR changes.
|
||||
2. Search [existing port issues](https://github.com/bearstonem/openmohaa-vr/issues).
|
||||
|
||||
## Usage of generative AI / Large language models
|
||||
Include the following details to help reproduce and diagnose an issue:
|
||||
|
||||
### Policy
|
||||
- A clear description and reproducible steps, including the mission/map and game edition.
|
||||
- Headset model, headset OS/firmware, controllers, and OpenXR runtime/version when known.
|
||||
- Port release and asset name, or the source commit and build configuration. Include the engine version from the startup log or `version` command as well.
|
||||
- Relevant VR/render settings, mods, and whether the release's external `autoexec.cfg` was installed.
|
||||
- Startup and failure logs, including `qconsole.log`, Android logcat, or runtime logs as appropriate. Include a backtrace for crashes.
|
||||
- For visual or performance issues, whether one or both eyes are affected, and any captures or frame-time measurements.
|
||||
|
||||
This project requires that all contributions be human-authored.
|
||||
For Quest logs, begin capture before reproducing the issue: `adb logcat -v threadtime -s openmohaa:V AndroidRuntime:E > openmohaa-vr.log`. See the [bug report template](.github/ISSUE_TEMPLATE/bug_report.md) for the full checklist.
|
||||
|
||||
Do not submit any code, documentation, issues, pull requests, commit messages or comments that are generated using AI or similar assistive tools (such as Microsoft Copilot, Anthropic Claude, ChatGPT...).
|
||||
Report port problems here first. If a problem also reproduces in upstream OpenMoHAA, provide that evidence and follow upstream's policies when reporting or contributing there.
|
||||
|
||||
- **No AI-derived patches**. If the change was proposed by an AI, it must not be ported over "by hand". If the idea came from AI, treat is as untrusted and do your own independent implementation.
|
||||
- **No AI-assisted refactors**. Mass rewrites, LLM-based formatting, or modernization are not accepted.
|
||||
- **Enforcement**. Maintainers may request clarification. PRs suspected of AI involvement may be rejected or reverted, and may lead to a ban.
|
||||
## Tools and contribution responsibility
|
||||
|
||||
#### Acceptable uses
|
||||
AI-assisted contributions are welcome in this fork. Contributors remain responsible for reviewing, understanding, and validating every submitted change, including generated code and documentation. Explain the problem, the resulting behavior, and the checks performed; distinguish source review, automated tests, and actual headset testing.
|
||||
|
||||
- **Non-generative tools** are allowed: static analyzers, formatters, linters, sanitizers, fuzzers and IDE features are fine as long they do not generate code.
|
||||
- **Translation tools** to help communicating between people.
|
||||
Keep changes focused and reviewable. Preserve license headers, copyright notices, and authorship attribution, and identify the source and license of imported code or assets. Do not submit proprietary game data. Tool use does not change these responsibilities.
|
||||
|
||||
### Reasoning
|
||||
|
||||
This policy exists due to:
|
||||
- **Copyright and licensing risk**. The origin of the outputs may be unclear as these tools plagiarize the original sources without permission.
|
||||
- **Security and authenticity**. AI produces plausible code but may often look wrong. This could work, but would turn into undefined behavior, vulnerabilities, or time spent in a debugger.
|
||||
- **Maintability**. Changes must be explainable and reviewable. Humans can explain tradeoffs they consciously accepted. Generated code can't. If you can't explain why a specific line exists, it shouldn't be proposed at all.
|
||||
- **Respect for maintainers**. Submitting contributions generated by these tools waste maintainers resources to review them.
|
||||
|
||||
### Responsibility
|
||||
|
||||
When submitting, you agree that:
|
||||
|
||||
- The submission **complies with this policy**. Changes must be able to be explained and justified during review.
|
||||
- The submission **can be declined** if it is **suspected to use AI** or is raising reviewability concerns.
|
||||
This policy applies only to OpenMoHAA VR. It does not override upstream OpenMoHAA's independently maintained policies or authorize sending AI-assisted work upstream.
|
||||
|
||||
## Coding
|
||||
|
||||
### Compatibility
|
||||
|
||||
All changes must stay retro-compatible with the original game:
|
||||
Preserve the engine's compatibility with the original game while adapting presentation and input for VR:
|
||||
|
||||
- Assets must be loaded correctly.
|
||||
- Changes related to networking must be compatible with existing MOHAA clients and servers, these changes must not cause an error/disconnect.
|
||||
- Scripts/mods must remain functional.
|
||||
- The singleplayer campaign must be fully playable.
|
||||
- Preserve single-player campaign behavior; report which missions and expansions were actually tested.
|
||||
- Keep desktop and dedicated-server builds working when changing shared code.
|
||||
- Do not change existing package IDs, config/save paths, network identifiers, or binary/module names solely for display branding.
|
||||
|
||||
### Coding style & Guidelines
|
||||
|
||||
- Match the existing code style in any files you modify.
|
||||
- Avoid personal tags or author comments (.e.g., `// @Name: note`). Git already tracks authorship.
|
||||
- Ensure the code builds successfully using CI.
|
||||
- Use **camelCase** notation for all variables, and **PascalCase** notation for functions. Even if some existing code doesn't.
|
||||
- Format code using `clang-format`.
|
||||
- Avoid personal tags or author comments (e.g., `// @Name: note`). Git already tracks authorship.
|
||||
- Build the affected targets and run relevant checks. State when a target or headset was unavailable.
|
||||
- Follow the surrounding naming conventions, including engine subsystem prefixes such as `CG_`, `CL_`, `R_`, and `VR_`.
|
||||
- Format changed code using the repository's `clang-format` configuration; avoid unrelated whole-file formatting.
|
||||
- In VSCode from the command palette: **Format document**
|
||||
|
||||
### Event declaration
|
||||
|
||||
When declaring a new Event, use the following structure:
|
||||
|
||||
```cpp
|
||||
Event EV_YourEventName // Pascal Case naming convention
|
||||
(
|
||||
@@ -133,6 +118,7 @@ When modifying existing code from the original game or adding new classes, annot
|
||||
```
|
||||
|
||||
If referencing a specific game version, replace OPM with that version number. Example:
|
||||
|
||||
```cpp
|
||||
// Added in 2.11
|
||||
// Make sure to clean turret stuff up
|
||||
@@ -141,6 +127,7 @@ If referencing a specific game version, replace OPM with that version number. Ex
|
||||
Known versions are `1.00`, `1.10`, `1.11`, `2.0`, `2.1`, `2.11`, `2.15`, `2.30`, `2.40`.
|
||||
|
||||
You can also group multiple related functions like this:
|
||||
|
||||
```cpp
|
||||
// Added in OPM
|
||||
//====
|
||||
@@ -166,26 +153,27 @@ Pull Requests are the preferred way of submitting code changes.
|
||||
|
||||
### Before you start
|
||||
|
||||
1. **Discuss major changes first**: open an issue or discuss about it.
|
||||
2. **Create a branch** based on the `main` branch (you shouldn't commit directly on `main`)
|
||||
1. **Discuss major changes first** in this port's issue tracker.
|
||||
2. **Create a branch** based on this repository's current default branch, rather than upstream's `main`.
|
||||
3. **Keep PRs as small as possible and focused**: One clear purpose per PR.
|
||||
|
||||
### Submitting a PR
|
||||
|
||||
- Target the correct branch (usually `main`).
|
||||
- Target this repository's current default branch, unless a maintainer requests a different integration branch.
|
||||
- Add a short and clear description of what you changed and why.
|
||||
- Reference matching issues (e.g. `Fixes #123`).
|
||||
- Make sure your code builds and passes tests.
|
||||
- Test locally to ensure your changes don't break functionality or compatibility.
|
||||
- For VR changes, list the headset/runtime, build, and scenarios tested. A successful build alone does not establish device support or a performance improvement.
|
||||
|
||||
### Reviews
|
||||
|
||||
- Maintainers may ask for tweaks or commit changes directly to your PR, that's normal.
|
||||
- Stay polite and technical.
|
||||
- Once approved, your changes will be merged.
|
||||
- Maintainers decide whether and when a change is ready to merge.
|
||||
|
||||
### Additional Notes
|
||||
|
||||
- Contributions follow the same license as the project.
|
||||
- Keep things consistent with existing code and docs.
|
||||
- If you're unsure about anything, you can discuss it on Discord via [this link](https://discord.gg/NYtH58R).
|
||||
- Use the [OpenMoHAA VR issue tracker](https://github.com/bearstonem/openmohaa-vr/issues) for port questions and proposals.
|
||||
@@ -1,24 +1,37 @@
|
||||
# File layout
|
||||
# OpenMoHAA VR repository layout
|
||||
|
||||
| Path | Description |
|
||||
| ---------------------|-------------------------------------------------------------------
|
||||
| code/ | OpenMoHAA source code |
|
||||
| code/cgame | Client game code |
|
||||
| code/client | Client (network stuff, snapshots, etc) |
|
||||
| code/game | Server game code (game logic stuff, etc) |
|
||||
| code/gamespy | Gamespy SDK |
|
||||
| code/null | Null redirector (used by the OpenMoHAA Dedicated Server) |
|
||||
| code/parser | Flex/Bison parser |
|
||||
| code/qcommon | Global shared stuff such as maths |
|
||||
| code/renderer | OpenMoHAA renderer |
|
||||
| code/sdl(12) | SDL code |
|
||||
| code/script | Script engine |
|
||||
| code/server | Server code (network, snapshots, handling clients etc) |
|
||||
| code/skeletor | Skeleton model engine |
|
||||
| code/sys | Main application system |
|
||||
| code/tiki | Complete TIKI engine |
|
||||
| code/uilib | UI library (Ubertools stuff) |
|
||||
| code/{win32,unix} | Platform-specific code |
|
||||
| docker | Docker files, for compiling on various linux versions |
|
||||
| docs | Documentation stuff |
|
||||
| misc/ | Project stuff, definition files and various resources stuff. |
|
||||
This fork contains the OpenMoHAA engine and its VR port. Meta Quest is the current standalone release target; Steam Frame is a development target with no binary available yet. Existing engine names, package IDs, and data paths remain in place for compatibility.
|
||||
|
||||
Start with [VR_PORT_STATUS.md](VR_PORT_STATUS.md) for current support and known issues. The older plan and renderer handoff record design decisions and historical measurements; they are not release support claims.
|
||||
|
||||
| Path | Description |
|
||||
| --- | --- |
|
||||
| [VR_PORT_STATUS.md](VR_PORT_STATUS.md) | Port status, tested behavior, known issues, and build/deployment notes. |
|
||||
| [VR_PORT_PLAN.md](VR_PORT_PLAN.md) | Original Quest port survey and implementation plan. |
|
||||
| [VR_RENDERER_HANDOFF.md](VR_RENDERER_HANDOFF.md) | Renderer migration and gl4es integration history. |
|
||||
| [QUEST_PORTING_GUIDE.md](QUEST_PORTING_GUIDE.md) | Lessons and reference workflows from the Quest port. |
|
||||
| `code/vr/` | OpenXR session, frame submission, tracking, and controller input; `vr_common.h` exposes the engine-facing API. |
|
||||
| `code/cgame/` | Client game presentation, view models, and the grouped VR HUD in `cg_vrhud.cpp`. |
|
||||
| `code/client/` | Client networking, snapshots, UI integration, and VR frame routing. |
|
||||
| `code/fgame/` | Server game logic, including gameplay interactions used by the VR port. |
|
||||
| `code/renderergl1/` | Fixed-function renderer and VR view composition; the default Android renderer uses gl4es. |
|
||||
| `code/renderergl2/` | Shader-based renderer and its VR integration. |
|
||||
| `code/renderercommon/` | Shared renderer interfaces, image handling, and related utilities. |
|
||||
| `code/qcommon/` | Shared engine code, math, filesystem, and configuration handling. |
|
||||
| `code/corepp/` | Shared C++ infrastructure. |
|
||||
| `code/server/` | Server networking, snapshots, and client handling. |
|
||||
| `code/null/` | Platform stubs used by dedicated-server builds. |
|
||||
| `code/sdl/` | SDL input and platform integration. |
|
||||
| `code/sys/` | Platform system code, including Android support under `code/sys/new/`. |
|
||||
| `code/uilib/` | Widget and UI library. |
|
||||
| `code/script/`, `code/parser/` | Script engine and Flex/Bison parser. |
|
||||
| `code/skeletor/`, `code/tiki/` | Skeletal models, animation, and the TIKI model system. |
|
||||
| `code/botlib/`, `code/gamespy/` | Bot support and inherited GameSpy SDK integration. |
|
||||
| `code/thirdparty/` | Bundled third-party dependencies and their notices. |
|
||||
| `misc/android/` | Gradle project, Android manifest/resources, and the external `autoexec.cfg` supplied with Quest releases. |
|
||||
| `misc/linux/`, `misc/macos/`, `misc/windows/` | Desktop packaging and platform resources. |
|
||||
| `cmake/`, `CMakeLists.txt` | Native build configuration and dependency integration. |
|
||||
| `.github/` | Build/release workflows and issue templates for this repository. |
|
||||
| `docs/` | Engine documentation and Android/VR integration notes. |
|
||||
|
||||
Use [CONTRIBUTING.md](CONTRIBUTING.md) for development conventions and report port issues in [bearstonem/openmohaa-vr](https://github.com/bearstonem/openmohaa-vr/issues).
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,109 +1,93 @@
|
||||
# OpenMoHAA
|
||||
# OpenMoHAA VR
|
||||
|
||||
[](https://github.com/openmoh/openmohaa/actions/workflows/branches-build.yml) [](https://github.com/openmoh/openmohaa/releases) [](https://github.com/openmoh/openmohaa/releases)
|
||||
**A standalone VR port of Medal of Honor: Allied Assault targeting Meta Quest and Steam Frame.**
|
||||
|
||||
 
|
||||
[](https://github.com/bearstonem/openmohaa-vr/releases)
|
||||
[](https://github.com/bearstonem/openmohaa-vr/releases)
|
||||
[](COPYING.txt)
|
||||
|
||||

|
||||
OpenMoHAA VR brings the original game into room-scale VR: head tracking, controller-aimed weapons, tracked hands, physical interactions, and menus and HUD adapted for a headset. The game runs on the headset itself. A PC is needed for the documented sideloading workflow, not for streaming gameplay.
|
||||
|
||||

|
||||
This is the VR development fork of [OpenMoHAA](https://github.com/openmoh/openmohaa). You must supply your own original game data; no Medal of Honor game assets are included.
|
||||
|
||||
## What is OpenMoHAA?
|
||||
## Platforms and current status
|
||||
|
||||
OpenMoHAA is an open-source project aimed at preserving and enhancing **Medal of Honor: Allied Assault** (including Spearhead and Breakthrough expansions) by providing more features and bugfixes, across modern platforms and architectures.
|
||||
| Target | Status |
|
||||
| --- | --- |
|
||||
| **Meta Quest** | Standalone Android/OpenXR prerelease available. Quest 3 is the headset tested so far; other Quest models are unverified. |
|
||||
| **Steam Frame** | Standalone VR development target. Platform integration and device validation are still pending; there is no Steam Frame release yet. |
|
||||
|
||||
Powered by [ioquake3](https://github.com/ioquake/ioq3) and the [F.A.K.K SDK](https://code.idtech.space/ritual/fakk2-sdk), OpenMoHAA provides:
|
||||
- Full compatibility with the original game: assets, scripts and multiplayer
|
||||
- Better support for modern systems
|
||||
- Cross-platform support (Linux, Windows, macOS)
|
||||
- Support for both single-player and multiplayer modes
|
||||
- Includes all fixes from Spearhead 2.15 and Breakthrough 2.40b
|
||||
- More fixes and features, such as bots and a ban system
|
||||
The current VR backend uses Android, EGL, and OpenGL ES. The inherited desktop engine build is useful for development, but it does not establish Steam Frame VR support.
|
||||
|
||||
*OpenMoHAA is an independent project and is not affiliated with or endorsed by Electronic Arts.*
|
||||
**This is a port in progress.** The latest code review passed desktop and Android builds and targeted regression checks. Its visual changes and performance impact have not yet been measured in a headset. Upstream campaign, expansion, mod, and multiplayer compatibility should not be read as a claim that each has been validated in this VR port.
|
||||
|
||||
## Getting started
|
||||
## Download and install on Quest
|
||||
|
||||
- 📦 [Installing OpenMoHAA](docs/markdown/01-intro/01-installation.md)
|
||||
- ▶️ [How to play: Launching the game, expansions & file locations](docs/markdown/02-running/01-running.md)
|
||||
- ❓ [FAQ & Troubleshooting](docs/markdown/02-running/03-faq.md)
|
||||
- 🌐 [Setting up a game server](docs/markdown/02-running/02-running-server.md)
|
||||
Get the APK, `autoexec.cfg`, and `SHA256SUMS` from the [VR releases page](https://github.com/bearstonem/openmohaa-vr/releases). The current release is [0.2.0](https://github.com/bearstonem/openmohaa-vr/releases/tag/vr-quest3-0.2.0).
|
||||
|
||||
## Reporting Issues
|
||||
1. Install the APK:
|
||||
|
||||
> [!NOTE]
|
||||
> OpenMoHAA hasn't hit version 1.0.0 yet. Think of it like a beta build from the golden age of LAN parties. Features are being added, bugs are getting squashed, and more things are being tweaked. Things might change, break, or get even better over time.
|
||||
>
|
||||
> If that sounds like your kind of mission, gear up, frag some bots, and help level up OpenMoHAA!
|
||||
```sh
|
||||
adb install -r openmohaa-vr-quest3-0.2.0.apk
|
||||
adb shell am force-stop org.openmoh.openmohaa
|
||||
```
|
||||
|
||||
If you encounter a bug or a problem, you can do one of the following:
|
||||
- Submit an [issue](https://github.com/openmoh/openmohaa/issues) on GitHub (use the template).
|
||||
- Join the [OpenMoHAA Discord](https://discord.gg/NYtH58R) for a quick help.
|
||||
2. On a fresh installation, launch once from inside the headset to initialize the app's files directory, then close it. Supply the retail game's `main/` data under `/sdcard/Android/data/org.openmoh.openmohaa/files/main/`. See the [Quest build and installation guide](docs/markdown/04-coding/03-android.md) for the data-copy steps.
|
||||
3. Copy the release configuration:
|
||||
|
||||
## Additional documentation
|
||||
```sh
|
||||
adb push autoexec.cfg /sdcard/Android/data/org.openmoh.openmohaa/files/main/autoexec.cfg
|
||||
adb shell am force-stop org.openmoh.openmohaa
|
||||
```
|
||||
|
||||
- 📖 [Documentation](https://openmoh.github.io/openmohaa)
|
||||
- ⚙️ [Game settings & configuration](docs/markdown/03-configuration/01-configuration.md)
|
||||
- 📝 [Code & Scripting reference](docs/markdown/04-coding/02-coding.md)
|
||||
- 📜 [Contributing guidelines](CONTRIBUTING.md)
|
||||
4. Launch the game from inside the headset, under Unknown Sources. Release 0.2.0 appears as **OpenMoHAA**; subsequent source builds use **OpenMoHAA VR**. Do not start it with `adb shell am start`.
|
||||
|
||||
## Current state
|
||||
**When upgrading, update the configuration too.** Reinstalling the APK keeps the old `autoexec.cfg`; back up personal changes before replacing it. The supplied configuration starts at the normal menu, disables the development cheats and test map, and includes the current lighting workaround. Let the app create its own `main/save` and `main/configs` directories.
|
||||
|
||||
- 🧰 [List of differences](docs/markdown/01-intro/04-differences.md)
|
||||
The APK is debug-signed with optimized Release native libraries. Version 0.2.0 uses the same signing certificate as 0.1.0 for upgrades. Verify downloaded assets with `sha256sum -c SHA256SUMS`.
|
||||
|
||||
### Single-player
|
||||
## VR features
|
||||
|
||||
The entire single-player campaign should work (Allied Assault, Spearhead and Breakthrough). If you encounter any bug, please create a new [GitHub issue](https://github.com/openmoh/openmohaa/issues) describing them.
|
||||
- Stereo OpenXR rendering and 6DoF head tracking.
|
||||
- Controller-aimed weapons, tracked hands, stick locomotion, and snap turning.
|
||||
- Body holsters, grip-driven ladder climbing, and physical grenade throws.
|
||||
- Menus and mission/death screens displayed on a panel in the room.
|
||||
- An on-demand HUD on the off-hand **X** button: compass, objectives, health, and ammo.
|
||||
- Controller-based use interaction and a weapon-selection wheel.
|
||||
|
||||
### Multiplayer
|
||||
These systems are implemented; controller interactions, weapon alignment, and individual missions still need broader device testing.
|
||||
|
||||
- Almost fully stable
|
||||
- All official game modes are supported, including those from Spearhead and Breakthrough:
|
||||
- Free-For-All
|
||||
- Team-Deathmatch
|
||||
- Round-based match
|
||||
- Objective match
|
||||
- Tug-of-War (Spearhead)
|
||||
- Liberation (Breakthrough)
|
||||
- Popular mods like **Freeze-Tag** are supported
|
||||
- Built-in bots for offline practice and for testing
|
||||
- 🔧 [Setting up bots](docs/markdown/02-running/01-running.md#Playing-with-bots)
|
||||
## Known issues
|
||||
|
||||
You can host your own [OpenMoHAA server](docs/markdown/02-running/02-running-server.md#) or join others using OpenMoHAA.
|
||||
- Some fonts can render as black boxes because of an unresolved font-atlas texture issue.
|
||||
- Dynamic lights can darken the scenery. The supplied `autoexec.cfg` keeps `r_worldDlights 0`; the APK's defaults alone do not apply that workaround.
|
||||
- Per-weapon grip alignment and remaining foliage behavior need further work.
|
||||
- Fresh headset performance measurements are pending. Earlier Quest 3 measurements are historical, not benchmarks of the latest release.
|
||||
- Steam Frame integration and validation remain to be done.
|
||||
|
||||
## Screenshots
|
||||
See [port status](VR_PORT_STATUS.md) for implementation details and the historical Quest measurements.
|
||||
|
||||
| | |
|
||||
|-----------------------------------------------------------------------------------|----------------------------------------------------------------------------|
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
## Development
|
||||
|
||||
*More screenshots [here](docs/assets/images)*
|
||||
The active development branch is `vr-quest3`; the project targets both Quest and Steam Frame.
|
||||
|
||||
## Development & Compiling
|
||||
```sh
|
||||
git clone --branch vr-quest3 https://github.com/bearstonem/openmohaa-vr.git
|
||||
cd openmohaa-vr
|
||||
```
|
||||
|
||||
- 💻 [Building from source](docs/markdown/04-coding/01-compiling.md)
|
||||
- [Quest / Android build and installation](docs/markdown/04-coding/03-android.md)
|
||||
- [Documentation index](docs/README.md)
|
||||
- [Source layout](LAYOUT.md)
|
||||
- [Contributing](CONTRIBUTING.md)
|
||||
- [Port status](VR_PORT_STATUS.md), [original Quest plan](VR_PORT_PLAN.md), and [renderer notes](VR_RENDERER_HANDOFF.md)
|
||||
- [Reusable Quest porting guide](QUEST_PORTING_GUIDE.md)
|
||||
- [Desktop engine build reference](docs/markdown/04-coding/01-compiling.md)
|
||||
|
||||
## Third party librairies
|
||||
## Feedback and credits
|
||||
|
||||
The following third party tools and libraries are used by the project
|
||||
Report port bugs in [this repository's issue tracker](https://github.com/bearstonem/openmohaa-vr/issues). Include your headset, release or commit, map, steps to reproduce, and relevant logs. Steam Frame development reports should include the runtime and build configuration.
|
||||
|
||||
- [Flex](https://github.com/westes/flex)
|
||||
- [Bison](https://savannah.gnu.org/projects/bison/)
|
||||
- [SDL](http://www.libsdl.org/)
|
||||
- [OpenAL](https://www.openal.org/)
|
||||
- [LibMAD](http://www.underbit.com/products/mad/)
|
||||
- [cURL](https://curl.se/)
|
||||
- [Libogg](https://github.com/gcp/libogg)
|
||||
- [Libvorbis](https://xiph.org/vorbis/)
|
||||
- [Libopus](https://opus-codec.org/)
|
||||
Built on the work of the [OpenMoHAA project](https://github.com/openmoh/openmohaa), [ioquake3](https://github.com/ioquake/ioq3), and the F.A.K.K SDK. [Team Beef's RTCWQuest](https://github.com/DrBeef/RTCWQuest) is the reference for much of the VR integration. The original engine documentation, assets, and attribution remain in the tree.
|
||||
|
||||
## Resources
|
||||
|
||||
- 🔗 [GitHub Repository](https://github.com/openmoh/openmohaa/)
|
||||
- 🌐 [MOH-DB](https://www.moh-db.com/)
|
||||
- 🕹️ [333networks](https://333networks.com/)
|
||||
- 📂 [ModDB](https://www.moddb.com/games/medal-of-honor-allied-assault)
|
||||
- 📂 [GameBanana](https://gamebanana.com/games/720)
|
||||
- 💬 [Join us on Discord](https://discord.gg/NYtH58R)
|
||||
Engine source is licensed under [GPL-2.0-or-later](COPYING.txt). Medal of Honor game data is separately owned and is not distributed here. This is an independent community port, not an official Electronic Arts, Meta, or Valve release.
|
||||
+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,419 @@
|
||||
# OpenMoHAA VR — platform status
|
||||
|
||||
This repository develops a standalone Medal of Honor: Allied Assault VR port
|
||||
for **Meta Quest and Steam Frame**. The released build is currently the Android
|
||||
OpenXR port, tested on Quest 3. Other Quest models are unverified. Steam Frame
|
||||
is a development target: platform integration and device validation are pending,
|
||||
and no Steam Frame binary is available yet.
|
||||
|
||||
The notes below document the existing Quest implementation and historical device
|
||||
measurements. They do not establish support or performance on Steam Frame.
|
||||
|
||||
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
+2
-2
@@ -42,7 +42,7 @@ DOXYFILE_ENCODING = UTF-8
|
||||
# title of most generated pages and in a few other places.
|
||||
# The default value is: My Project.
|
||||
|
||||
PROJECT_NAME = "OpenMoHAA"
|
||||
PROJECT_NAME = "OpenMoHAA VR"
|
||||
|
||||
# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
@@ -54,7 +54,7 @@ PROJECT_NUMBER = "$(PROJECT_VERSION_NUMBER)"
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
# quick idea about the purpose of the project. Keep the description short.
|
||||
|
||||
PROJECT_BRIEF =
|
||||
PROJECT_BRIEF = "Standalone Medal of Honor: Allied Assault VR targeting Meta Quest and Steam Frame"
|
||||
|
||||
# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
|
||||
# in the documentation. The maximum height of the logo should not exceed 55
|
||||
|
||||
+31
-18
@@ -1,25 +1,38 @@
|
||||
# Home
|
||||
# OpenMoHAA VR documentation
|
||||
|
||||
Welcome to the [OpenMoHAA](https://www.openmohaa.org/) documentation. Here you'll find resources to help you get started and troubleshoot as needed.
|
||||
OpenMoHAA VR is a standalone **Medal of Honor: Allied Assault** VR port targeting **Meta Quest and Steam Frame**. Quest 3 is the current tested headset. Steam Frame is a development target with no published build yet.
|
||||
|
||||
## 🚀 Getting started
|
||||
## Start here
|
||||
|
||||
- 📦 [Installing OpenMoHAA](markdown/01-intro/01-installation.md)
|
||||
- ▶️ [How to play: Launching the game, expansions & file locations](markdown/02-running/01-running.md)
|
||||
- ⚙️ [Game settings & configuration](markdown/03-configuration/01-configuration.md)
|
||||
- 🌐 [Setting up a game server](markdown/02-running/02-running-server.md)
|
||||
- ❓ [FAQ & Troubleshooting](markdown/02-running/03-faq.md)
|
||||
- [Project overview and platform status](../README.md)
|
||||
- [VR downloads](https://github.com/bearstonem/openmohaa-vr/releases)
|
||||
- [Quest installation and Android builds](markdown/04-coding/03-android.md)
|
||||
- [Port status and known issues](../VR_PORT_STATUS.md)
|
||||
- [Report a VR issue](https://github.com/bearstonem/openmohaa-vr/issues)
|
||||
|
||||
## 🛠️ Coding & Scripting
|
||||
The APK contains only the engine. Supply your own original game data and install the release's `autoexec.cfg`, including when upgrading. That config disables development autostart and applies the current dynamic-lighting workaround.
|
||||
|
||||
- 💻 [Building from source](markdown/04-coding/01-compiling.md)
|
||||
- 📝 [Code & Scripting reference](markdown/04-coding/02-coding.md)
|
||||
- 📜 [Contributing guidelines](https://github.com/openmoh/openmohaa/blob/main/CONTRIBUTING.md)
|
||||
## Working on the port
|
||||
|
||||
## 📚 Resources
|
||||
- [Contributing to OpenMoHAA VR](../CONTRIBUTING.md)
|
||||
- [Source layout](../LAYOUT.md)
|
||||
- [Quest renderer notes](../VR_RENDERER_HANDOFF.md)
|
||||
- [gl4es integration](markdown/04-coding/04-gl4es.md)
|
||||
- [Dynamic-lighting investigation](markdown/04-coding/05-dlights.md)
|
||||
- [Reusable Quest porting guide](../QUEST_PORTING_GUIDE.md)
|
||||
|
||||
- 🌐 [MOH-DB](https://www.moh-db.com/) - A large archive of maps, mods, tools, and tutorials for Medal of Honor: Allied Assault
|
||||
- 📂 [ModDB](https://www.moddb.com/games/medal-of-honor-allied-assault) - Mods, updates, and community content
|
||||
- 📂 [GameBanana](https://gamebanana.com/games/720) - Skins, mods, and custom content
|
||||
- 🕹️ [333networks](https://master.333networks.com/g) - Online server list used by OpenMoHAA
|
||||
- 💬 [OpenMoHAA Discord](https://discord.gg/NYtH58R) - Ask questions, get support, or join the community
|
||||
The VR runtime currently depends on Android, EGL, and OpenGL ES. Steam Frame needs platform integration and testing; compiling the desktop engine alone does not provide a Steam Frame VR build.
|
||||
|
||||
## Inherited engine reference
|
||||
|
||||
These documents cover the underlying OpenMoHAA engine and its desktop tools. Their platform-specific release links and compatibility notes describe upstream capabilities, not validation of the headset port.
|
||||
|
||||
- [Desktop installation](markdown/01-intro/01-installation.md)
|
||||
- [Desktop engine builds](markdown/04-coding/01-compiling.md)
|
||||
- [Game data, expansions, and launch options](markdown/02-running/01-running.md)
|
||||
- [Game settings](markdown/03-configuration/01-configuration.md)
|
||||
- [Server configuration](markdown/02-running/02-running-server.md)
|
||||
- [Scripting reference](markdown/04-coding/02-coding.md)
|
||||
- [Engine FAQ](markdown/02-running/03-faq.md)
|
||||
|
||||
The [upstream OpenMoHAA project](https://github.com/openmoh/openmohaa) maintains the original engine. Use this fork's issue tracker for VR behavior and headset-specific bugs.
|
||||
@@ -1,5 +1,11 @@
|
||||
# Installation guide
|
||||
|
||||
This is the inherited **desktop OpenMoHAA engine** installation reference. For
|
||||
OpenMoHAA VR on Meta Quest, use the [Quest installation guide](../04-coding/03-android.md)
|
||||
and [VR releases](https://github.com/bearstonem/openmohaa-vr/releases). Steam Frame
|
||||
is a development target with no release yet. The desktop downloads below are
|
||||
maintained by upstream and do not include this fork's VR port.
|
||||
|
||||
## Before you start
|
||||
|
||||
The game should be able to run on any modern hardware. You can verify the [System requirements](03-requirements.md).
|
||||
@@ -29,7 +35,6 @@ If you already have MOH:AA from a CD, make sure to [Install official patches](#i
|
||||
2. Then check the "**Start in**" field, that's your game folder.
|
||||
|
||||

|
||||

|
||||
|
||||
Alternatively, try one of these common locations:
|
||||
- C:\GOG Games\Medal of Honor - Allied Assault War Chest
|
||||
@@ -49,7 +54,6 @@ If not there, you'll need to search where you installed it.
|
||||
|
||||
To do that, **right-click each file**, then choose **Send to → Desktop (create shortcut)**.
|
||||
|
||||

|
||||

|
||||
|
||||
Now you can launch any version of the game directly from your desktop using OpenMoHAA.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
This FAQ describes the inherited desktop engine. For headset setup and known
|
||||
VR issues, see the [Quest guide](../04-coding/03-android.md) and
|
||||
[VR port status](../../../VR_PORT_STATUS.md). Report issues with this port in
|
||||
[OpenMoHAA VR](https://github.com/bearstonem/openmohaa-vr/issues).
|
||||
|
||||
<details>
|
||||
<summary>The game won't start or crashes immediately. What should I check?</summary>
|
||||
|
||||
@@ -86,7 +91,7 @@ However, if `grenzuebergang_KE.pk3` is placed in the game installation folder (`
|
||||
`Pak1.pk3` contains the original `scripts/effects.shader` where the impact effect is declared; therefore, the player does not experience any issue, as the original file is "loaded" after the custom file. |
|
||||
|
||||
### 2. Create a new issue:
|
||||
If changing the file precedences has no effect on the glitch you discovered, you have probably found a new bug in the OpenMoHAA engine, so please <a href="https://github.com/openmoh/openmohaa/issues/new/choose">create a new issue</a> for the developers.
|
||||
If changing the file precedences has no effect, [report the issue in OpenMoHAA VR](https://github.com/bearstonem/openmohaa-vr/issues/new/choose). Include the mod, map, headset, release, and reproduction steps. If you also tested the upstream desktop engine, state whether it reproduces there.
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Compiling
|
||||
|
||||
This is the inherited **desktop engine** build reference. For the standalone
|
||||
Quest port, follow the [Android/Quest build guide](03-android.md). Steam Frame
|
||||
integration is still pending; these desktop instructions do not produce a
|
||||
Steam Frame VR build. The architecture notes below describe upstream OpenMoHAA.
|
||||
|
||||
OpenMoHAA supports any architecture, thanks to the CMake build system and cross-platform code/libraries. While the most common little-Endian architectures (arm, aarch64, x86, x86_64, ppc64le) have been tested and confirmed to work, big-Endian architectures should be supported as well; however, they remain untested.
|
||||
|
||||
The following tools are required for all platforms:
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# Building OpenMoHAA VR for Meta Quest
|
||||
|
||||
OpenMoHAA VR runs **Medal of Honor: Allied Assault** in standalone VR through
|
||||
OpenXR. This guide builds the `arm64-v8a` Android APK for the Meta Quest family;
|
||||
Quest 3 is the tested headset. Other Quest models need validation. Steam Frame
|
||||
is a planned target; the current VR runtime implementation is Android-only.
|
||||
|
||||
You need the original retail Allied Assault game data. It is not included in
|
||||
this repository or the APK. Prebuilt packages are available from
|
||||
[OpenMoHAA VR releases](https://github.com/bearstonem/openmohaa-vr/releases).
|
||||
|
||||
> On Android, the fixed function renderer runs through gl4es, which maintains
|
||||
> its own GL state. For rendering problems, read
|
||||
> [the gl4es boundary](04-gl4es.md) and the
|
||||
> [VR port status](../../../VR_PORT_STATUS.md).
|
||||
|
||||
## What you need
|
||||
|
||||
| Tool | Version |
|
||||
|---|---|
|
||||
| Android NDK | r26 or later |
|
||||
| Android SDK | platform 34, build-tools 34, platform-tools (`adb`) |
|
||||
| JDK | 17 |
|
||||
| Gradle | 8.0 or later |
|
||||
| CMake | 3.25 or later |
|
||||
| Ninja, flex, bison | installed on the build host |
|
||||
|
||||
Set `ANDROID_NDK_HOME` and `ANDROID_HOME` to your installed NDK and SDK paths.
|
||||
Enable developer mode and USB debugging on the headset, and authorize the host
|
||||
when prompted. Run the following commands from the repository root unless a
|
||||
step says otherwise.
|
||||
|
||||
## 1. Build the engine
|
||||
|
||||
CMake builds the native libraries; Gradle packages them afterward.
|
||||
|
||||
```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 \
|
||||
-DBUILD_RENDERER_GL1=ON -DBUILD_RENDERER_GL2=OFF -DUSE_GL4ES=ON
|
||||
|
||||
cmake --build build-gl1 -j"$(nproc)"
|
||||
```
|
||||
|
||||
`ANDROID_STL=c++_shared` lets the APK's native libraries share one C++ runtime.
|
||||
Android enables `USE_OPENXR` automatically. The default renderer is
|
||||
`renderergl1` through gl4es; the alternative GLES renderer can be selected with
|
||||
`-DBUILD_RENDERER_GL1=OFF -DBUILD_RENDERER_GL2=ON -DUSE_GL4ES=OFF`.
|
||||
|
||||
SDL2, OpenAL Soft, gl4es, and the Khronos OpenXR loader are downloaded and built
|
||||
from source. Existing source trees can be supplied with `SDL2_SOURCE_PATH`,
|
||||
`OPENAL_SOURCE_PATH`, `GL4ES_SOURCE_PATH`, and `OPENXR_SOURCE_PATH` CMake options.
|
||||
Android links the renderer into the client, disables HTTP downloads and QVM
|
||||
builds, and stages the APK libraries in `build-gl1/apk-libs/arm64-v8a/`.
|
||||
|
||||
## 2. Package the APK
|
||||
|
||||
```sh
|
||||
cd misc/android
|
||||
gradle assembleDebug -PnativeLibsDir=../../build-gl1/apk-libs
|
||||
cd ../..
|
||||
```
|
||||
|
||||
The output is `misc/android/app/build/outputs/apk/debug/app-debug.apk`. Keep the
|
||||
`nativeLibsDir` override: the checked-in Gradle default names a different build
|
||||
directory. For another build location, supply its absolute `apk-libs` path.
|
||||
|
||||
Gradle reads `android-build.properties` beside `apk-libs` to find the SDL Java
|
||||
classes belonging to the SDL library CMake built. Do not mix those classes with
|
||||
a different SDL version. After rebuilding, verify that the APK is newer than the
|
||||
staged native libraries so you install the current code.
|
||||
|
||||
## 3. Install the APK and retail data
|
||||
|
||||
```sh
|
||||
adb install -r misc/android/app/build/outputs/apk/debug/app-debug.apk
|
||||
adb shell am force-stop org.openmoh.openmohaa
|
||||
```
|
||||
|
||||
The Android package ID remains `org.openmoh.openmohaa`, including for upgrades.
|
||||
Launch the app once from inside the headset to initialize its external files
|
||||
directory, then close it. Game data belongs in:
|
||||
|
||||
```text
|
||||
/sdcard/Android/data/org.openmoh.openmohaa/files/main/
|
||||
```
|
||||
|
||||
Copy **all** Allied Assault `.pk3` archives from your retail installation's
|
||||
`main/` directory. Replace the example source path below with your own:
|
||||
|
||||
```sh
|
||||
adb shell mkdir -p /sdcard/Android/data/org.openmoh.openmohaa/files/main
|
||||
adb push /path/to/retail/main/*.pk3 \
|
||||
/sdcard/Android/data/org.openmoh.openmohaa/files/main/
|
||||
```
|
||||
|
||||
Do not pre-create or copy `main/save` or `main/configs` with `adb`. Let the app
|
||||
create these directories so it can write saves and settings. Directories
|
||||
created by the shell can leave the app unable to write; changing their mode
|
||||
does not fix ownership. Keep existing saves when upgrading. A save-directory
|
||||
permission failure can appear as a black screen on New Game because the first
|
||||
autosave fails.
|
||||
|
||||
## 4. Install the VR configuration and launch
|
||||
|
||||
Copy the supplied configuration after installing or upgrading:
|
||||
|
||||
```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 external game configuration, not an APK asset. Reinstalling
|
||||
with `adb install -r` preserves the previous file, so replace it explicitly.
|
||||
Back up any personal edits before replacing it. The supplied configuration
|
||||
starts at the normal menu with cheats, automatic map loading, and diagnostics
|
||||
disabled. Keep its `r_worldDlights 0` setting: it works around the unresolved
|
||||
[dynamic-light darkening defect](05-dlights.md), and the APK alone does not
|
||||
apply that workaround.
|
||||
|
||||
Launch **OpenMoHAA VR** from inside the headset (the published 0.2.0 APK is still
|
||||
named **OpenMoHAA** in the launcher). Do not use `adb shell am start`;
|
||||
it has intermittently left Quest in Meta's loading environment. Force-stop
|
||||
after each APK update because `adb install -r` can leave the old process running.
|
||||
|
||||
## Logging and implementation
|
||||
|
||||
Capture engine output before reproducing a problem:
|
||||
|
||||
```sh
|
||||
adb logcat -s openmohaa:V > openmohaa-vr.log
|
||||
```
|
||||
|
||||
The engine redirects stdout and stderr to the same tag. Its Android crash
|
||||
handler reports the fault address and ARM64 `pc`/`lr` registers as well as an
|
||||
unwind trace. If logcat has no crash report, `adb shell dumpsys activity exit-info`
|
||||
can still identify the termination signal.
|
||||
|
||||
`SDLActivity` loads `libSDL2.so` and `libopenmohaa.so`, then calls the engine's
|
||||
`SDL_main`. The APK also carries OpenAL, gl4es, the OpenXR loader, and
|
||||
`libc++_shared.so`; the engine loads `libgame.so` and `libcgame.so` at runtime.
|
||||
OpenXR uses SDL's EGL context and supplies the eye swapchain images. The
|
||||
[port status](../../../VR_PORT_STATUS.md) records the architecture, known
|
||||
limitations, and which changes still need headset validation.
|
||||
@@ -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/04-coding/03-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">
|
||||
|
||||
<!-- OpenXR uses GLES 3; the default GL1 renderer runs through gl4es. -->
|
||||
<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>
|
||||
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user