Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32cc1cb206 | ||
|
|
adf220d344 | ||
|
|
32ad9ed6b0 | ||
|
|
cf19bcfdfa | ||
|
|
3b5796f1a0 | ||
|
|
68135a712d | ||
|
|
1f238ff045 | ||
|
|
88354e5c6c | ||
|
|
8cdce233e4 | ||
|
|
81137e3af3 | ||
|
|
fb670e79b9 | ||
|
|
8022a0b4a1 | ||
|
|
696383f191 | ||
|
|
9a33ecd2d3 | ||
|
|
9447bc2ae4 | ||
|
|
f04c8c4045 | ||
|
|
b026590824 | ||
|
|
2a2b2cdbf3 | ||
|
|
5861e57d99 | ||
|
|
50d685e465 | ||
|
|
0e6b006878 | ||
|
|
ca73ba7909 | ||
|
|
c254d6df05 | ||
|
|
9910c4a9dc | ||
|
|
b1d59e654d | ||
|
|
1f3d1be207 | ||
|
|
d97de6d499 | ||
|
|
fbe48a869f | ||
|
|
206eb0b52b | ||
|
|
1c66715a19 | ||
|
|
87f336df78 | ||
|
|
0a86e406c8 | ||
|
|
c09b40a2ef | ||
|
|
1e2006d06c | ||
|
|
e69e087602 | ||
|
|
87a38e2895 | ||
|
|
788ab74a6b | ||
|
|
6d2dcfaaec | ||
|
|
98e848de39 | ||
|
|
7754532683 | ||
|
|
19b0bcb42e | ||
|
|
f30dd50317 | ||
|
|
07a15190b4 | ||
|
|
f254be4b45 | ||
|
|
a6350d207a | ||
|
|
1929702f5b | ||
|
|
9d6b980ba6 | ||
|
|
9e4f8a2dcb | ||
|
|
53c268b8a6 | ||
|
|
592be6270a | ||
|
|
8817c37470 | ||
|
|
d635d83725 | ||
|
|
2a6dc2b738 | ||
|
|
6112dcfc5a | ||
|
|
62fa7e13ef | ||
|
|
9a12828762 | ||
|
|
7e82e7128e | ||
|
|
94882796b7 | ||
|
|
3dba558e4f | ||
|
|
1dc533fe18 | ||
|
|
8a8e70599f | ||
|
|
b115a41263 | ||
|
|
320464258a | ||
|
|
f5bb75c27c | ||
|
|
5cd1daff77 | ||
|
|
6329be8aa1 | ||
|
|
7a2f4caa50 | ||
|
|
938c37a669 | ||
|
|
fae3fbad69 | ||
|
|
5a35ea2b20 | ||
|
|
2e179ea90f | ||
|
|
9b517e554c | ||
|
|
595bbc5611 | ||
|
|
761210d405 | ||
|
|
7ba780ef59 | ||
|
|
d9c43e3478 | ||
|
|
c04ce7fd00 | ||
|
|
3f06b1b9d8 | ||
|
|
1b2d058a04 | ||
|
|
b5fa53ed95 | ||
|
|
1f2c10a3ca | ||
|
|
12f1155752 | ||
|
|
2b0327bc80 | ||
|
|
58aa352705 | ||
|
|
59c4b2160e | ||
|
|
1cf336f0ff | ||
|
|
d10969dc83 | ||
|
|
fe1d51dd58 | ||
|
|
24e4123882 | ||
|
|
197c199473 | ||
|
|
0acc3b1381 | ||
|
|
1e74e9b03e | ||
|
|
5d108579ed | ||
|
|
7a1ffca28b | ||
|
|
b843156fe8 | ||
|
|
99a31ca82d | ||
|
|
9666575b39 | ||
|
|
8466846d45 | ||
|
|
bc580cf1cf | ||
|
|
8e6b9a211e | ||
|
|
d0016bca89 | ||
|
|
3d575f32ed | ||
|
|
727b9ca80d | ||
|
|
99d7c924db | ||
|
|
57d96e4681 | ||
|
|
177ad4d40d | ||
|
|
bfb668d498 | ||
|
|
fc2b6551ca | ||
|
|
5316a6b145 | ||
|
|
59b1fb422b | ||
|
|
dc383d10da | ||
|
|
a143d9490d | ||
|
|
e55c29a97e | ||
|
|
16361bdef1 | ||
|
|
5ecd3075a0 | ||
|
|
13c14df95c | ||
|
|
b525bf8514 | ||
|
|
087ca8a875 | ||
|
|
24957ff6a0 | ||
|
|
402d4b8351 | ||
|
|
d1825ab08d | ||
|
|
268ec84b74 | ||
|
|
8dc796028e | ||
|
|
8aa381ed14 | ||
|
|
c3f82fa9b2 | ||
|
|
5ff4b6ee8d | ||
|
|
91e838e2f6 | ||
|
|
520b8401db | ||
|
|
2ed4a7b49d | ||
|
|
503992d544 | ||
|
|
ccc7df6611 | ||
|
|
ddcbe79283 | ||
|
|
862d88737e | ||
|
|
8610b28376 | ||
|
|
9e6247e0cb | ||
|
|
dea5fe3b1e | ||
|
|
f0804aa587 | ||
|
|
16ad4cc900 | ||
|
|
a38ede078a | ||
|
|
fa85aa53f5 | ||
|
|
8be9a038a1 | ||
|
|
783b591a6a | ||
|
|
e82d880060 | ||
|
|
7d0e587c51 | ||
|
|
01586cfebf | ||
|
|
de52060644 | ||
|
|
ab7c507a29 | ||
|
|
ba6fe0a826 | ||
|
|
3f6c0c37a4 | ||
|
|
606109551d | ||
|
|
8194c5a00b | ||
|
|
737551fd51 | ||
|
|
7a56c624f5 | ||
|
|
3ae09c3d2c | ||
|
|
3a22816c2f | ||
|
|
2853a42253 | ||
|
|
39ece781ee | ||
|
|
cf17201ec8 | ||
|
|
45de17116b | ||
|
|
7d56f34b83 | ||
|
|
3c7ba95ff5 | ||
|
|
092505224a | ||
|
|
079883d640 | ||
|
|
54faa1dab6 | ||
|
|
273152cc25 | ||
|
|
9bdd7f30c0 | ||
|
|
685bf260a0 | ||
|
|
933ed2b7b8 | ||
|
|
c115f5292d | ||
|
|
0f22c1d592 | ||
|
|
fb1abedd8b | ||
|
|
cd345aac7c | ||
|
|
7e00135d31 | ||
|
|
95416b2cfc | ||
|
|
ffc1512dcb | ||
|
|
88a7c07ec7 | ||
|
|
8e8713681d | ||
|
|
d7099f3e40 | ||
|
|
4ba36d7305 | ||
|
|
e3389991ac | ||
|
|
855f083117 | ||
|
|
6f3786de91 | ||
|
|
493713b0a6 | ||
|
|
bf356fc58a | ||
|
|
6a7b919522 | ||
|
|
20861c06d2 | ||
|
|
fc7271fd52 | ||
|
|
c1305fa94f | ||
|
|
3d2c04d675 | ||
|
|
0fc85d6c67 | ||
|
|
c0a08f395e | ||
|
|
06cb70fb58 | ||
|
|
1709698053 | ||
|
|
cb72dca40f | ||
|
|
bbea2a2bff | ||
|
|
57cdbd8f80 | ||
|
|
c5c5a8eaeb | ||
|
|
a3ee36b5ef | ||
|
|
30a1e0f40e | ||
|
|
327cf96d6f | ||
|
|
867855d388 | ||
|
|
4af570bc08 | ||
|
|
7a1cf79396 | ||
|
|
9e7fd75f57 | ||
|
|
6754403d68 | ||
|
|
aedf5ff9bc | ||
|
|
193cbf59c7 | ||
|
|
fe160e9dbe | ||
|
|
d578f88bd0 | ||
|
|
d3fc997fdc | ||
|
|
a227872ccb | ||
|
|
812ffeea33 | ||
|
|
c05864b3fc | ||
|
|
ce77db8404 | ||
|
|
39350fb81f | ||
|
|
1a15a171ac | ||
|
|
2e3841ff7d | ||
|
|
46edf53657 | ||
|
|
cd747c23c5 | ||
|
|
f148dbd6d9 | ||
|
|
c3adcfe5bf | ||
|
|
3304965653 | ||
|
|
c04765d7f4 | ||
|
|
933aac6175 | ||
|
|
809408c663 | ||
|
|
601a4c1f50 | ||
|
|
133d6a5dc1 | ||
|
|
12c97ffa85 | ||
|
|
61623e68d5 | ||
|
|
23b542601f | ||
|
|
62b630a261 | ||
|
|
0e6327cadf | ||
|
|
5f82df71f0 | ||
|
|
cfdd821b45 | ||
|
|
a9bbce5f7a | ||
|
|
e8e0c2215a | ||
|
|
383355381c | ||
|
|
e94906100c | ||
|
|
57add5ed71 | ||
|
|
2b0103a0ef | ||
|
|
c9ee319ab5 | ||
|
|
770f8881e0 | ||
|
|
38ab192749 | ||
|
|
827eb0fc1d | ||
|
|
a482a5f5bb | ||
|
|
81f321f959 | ||
|
|
f97ff9219f | ||
|
|
85e79a067a | ||
|
|
effeda1b7c | ||
|
|
6299503191 | ||
|
|
c5d86c8d8b | ||
|
|
7242151017 | ||
|
|
1c8e651bb1 | ||
|
|
4b8f8dde8b | ||
|
|
920b1bfea9 | ||
|
|
09f869e44c | ||
|
|
bc45338214 | ||
|
|
a663d1b100 | ||
|
|
d8034d37fd | ||
|
|
f8a39df0cd | ||
|
|
6057146244 | ||
|
|
94b17a7913 | ||
|
|
30f362ff3d | ||
|
|
23d14319a7 | ||
|
|
ff5b91f120 | ||
|
|
f6c2322df9 | ||
|
|
39fbaefaac | ||
|
|
c6397cb515 | ||
|
|
80d30410c8 | ||
|
|
d1c7243cb2 | ||
|
|
fd3d62f960 | ||
|
|
5ba6c13097 | ||
|
|
e44619818b | ||
|
|
b449c43e22 | ||
|
|
66c0d33e34 | ||
|
|
b438cea479 | ||
|
|
634b4197f9 | ||
|
|
ab96597321 | ||
|
|
f0dfb58c94 | ||
|
|
991ab41016 | ||
|
|
5f9748d736 | ||
|
|
56b2a0e8e3 | ||
|
|
5f388f7818 | ||
|
|
9f3e8c92de | ||
|
|
f2ba71d9af | ||
|
|
0182da8172 | ||
|
|
6b77fe428a | ||
|
|
c55e4e2b9d | ||
|
|
7cd1724fe5 | ||
|
|
bd8ec3c798 | ||
|
|
4799980e8a | ||
|
|
f8937c6179 | ||
|
|
557c0b486c |
No files matched your search
@@ -67,7 +67,9 @@ jobs:
|
||||
run: python tools/ci_build.py linux release
|
||||
|
||||
- name: Test
|
||||
run: python -m pytest -q tools/test_linux_port.py
|
||||
run: |
|
||||
python -m pytest -q tools/test_linux_port.py
|
||||
python tools/test_light_storage.py
|
||||
|
||||
- name: Compile cache
|
||||
run: ccache --show-stats
|
||||
@@ -200,6 +202,9 @@ jobs:
|
||||
- name: Release
|
||||
run: python tools/ci_build.py windows release
|
||||
|
||||
- name: Test light storage
|
||||
run: python tools/test_light_storage.py
|
||||
|
||||
- name: Compile cache
|
||||
run: ccache --show-stats
|
||||
|
||||
@@ -242,10 +247,7 @@ jobs:
|
||||
mkdir release
|
||||
for build in artifacts/*/; do
|
||||
name="$(basename "$build")"
|
||||
# stored, not deflated: the updater in builds up to 57 waits for
|
||||
# an end of stream the game's zlib 1.1 does not always report,
|
||||
# and fails on a deflated entry that unpacked correctly
|
||||
(cd "$build" && zip -0 -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
|
||||
(cd "$build" && zip -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
|
||||
done
|
||||
ls -l release
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ xbox/
|
||||
|
||||
# Locally extracted proprietary game data (never commit)
|
||||
assets/
|
||||
# (but the high-res assets, drawn for the port: tools/hud_assets.py)
|
||||
!port/assets/
|
||||
original/
|
||||
|
||||
# CI build outputs (tools/ci_build.py)
|
||||
|
||||
@@ -19,9 +19,9 @@ builds of the latest release:
|
||||
|
||||
| Platform | Release | Debug |
|
||||
| --- | --- | --- |
|
||||
| Linux | [halo-linux-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-debug.zip) |
|
||||
| Windows | [halo-windows-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-debug.zip) |
|
||||
| Android | [halo-android-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-debug.zip) |
|
||||
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
|
||||
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
|
||||
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
|
||||
|
||||
Use the release build to play. The debug build stops at the first failed
|
||||
assertion and writes it to the log. Use the debug build to find and report
|
||||
@@ -32,7 +32,7 @@ if you want to install it. Refer to "Updates" in
|
||||
[port/linux/README.md](port/linux/README.md#updates).
|
||||
|
||||
Each build of the `main` branch that passes on all three platforms is a new
|
||||
release. The [Releases](https://github.com/cybersecurity/halo-ce-universal/releases)
|
||||
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
|
||||
page keeps the last five releases. If the latest build has a problem, get
|
||||
an older build from that page.
|
||||
|
||||
|
||||
@@ -168,7 +168,8 @@ the Xbox:
|
||||
- The HUD stays at the edges of the screen.
|
||||
- The menus, the loading bar and the screens after a game have 640
|
||||
columns, at the center of the screen.
|
||||
- Black bars and fades cover all of the screen.
|
||||
- Black bars and fades cover all of the screen, and so do the menus' dims
|
||||
and backgrounds (the pause menu's dim, dialogs, the menus' gradient).
|
||||
|
||||
## VR
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# Steam Frame
|
||||
|
||||
The same APK as for the Quest runs on the Steam Frame, in Lepton (SteamOS's
|
||||
Android, a Waydroid fork: Android 11, arm64), with SteamVR's OpenXR runtime.
|
||||
What differs is found at run time, not by a build of its own.
|
||||
|
||||
## The device
|
||||
|
||||
- Steam Settings > System > Enable Developer Mode; then start "Lepton
|
||||
Development" from the library. While the headset sleeps Lepton stops, and
|
||||
adb with it.
|
||||
- adb over the network: `adb connect <frame-ip>:5555`; over USB:
|
||||
`adb forward tcp:5555 tcp:5555` then `adb connect localhost:5555`.
|
||||
Always name the device (`adb -s <frame-ip>:5555 ...`): a Quest may be
|
||||
connected as well.
|
||||
- Install with `adb install -r --no-streaming`. It prints "Lepton post
|
||||
installation failed due to APP_PATH being unset", but installs. Start the
|
||||
game with `am start -n com.halo.decomp/.LauncherActivity`.
|
||||
- Lepton empties `/sdcard/Android/data` whenever it starts again: the game's
|
||||
data must live elsewhere (stage 2). `/sdcard/Documents` and
|
||||
`/sdcard/Download` are the SteamOS user's folders and are kept.
|
||||
|
||||
## What the port does for it
|
||||
|
||||
- SDL3's clipboard handler takes a clipboard service for granted, which
|
||||
Lepton has not: `tools/android_build.py` patches it to do without.
|
||||
- SteamVR lists an implicit OpenXR layer for Vulkan apps whose library an
|
||||
app cannot load: the APK carries a pass-through layer under its name
|
||||
(`port/android/xr_layer/fdm_injection_stub.c`).
|
||||
|
||||
## Stages
|
||||
|
||||
| # | Stage | State |
|
||||
| --- | --- | --- |
|
||||
| 1 | Base: the fixes above on the current main | done |
|
||||
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/HaloCE` |
|
||||
| 3 | Menus, HUD and text visible (quad layers) | done: their alpha from their colour |
|
||||
| 4 | The freeze at the cryo bay's calibration | not seen again (2026-10-04) |
|
||||
| 5 | The Frame's controllers | done: their own profile, the Xbox's layout |
|
||||
| 6 | Frame rate, resolution | done: 72 Hz steady, multiview on |
|
||||
| 7 | Multiplayer, voice, cross-play with the Quest | |
|
||||
| 8 | A button in the Steam library | done: `steam_frame_launch.sh` |
|
||||
|
||||
## Starting it on the Frame
|
||||
|
||||
A button in the Steam library ("Halo VR", a non-Steam game) runs
|
||||
`~/Documents/HaloCE/halo-vr.sh` (`port/android/steam_frame_launch.sh`),
|
||||
which starts the game as Steam starts an Android title of its own:
|
||||
`lepton waitforexitandrun -- ~/Documents/HaloCE/HaloVR.apk`, in a container
|
||||
of its own (`steamlaunch-4290000001`) that Lepton installs the APK into
|
||||
anew whenever the file is newer. Updating the game is replacing that APK.
|
||||
|
||||
What the script does that Steam would for an Android title:
|
||||
- a compat data path (`steamapps/compatdata/4290000001/internal`): after an
|
||||
install Lepton links the app's data to it, a dangling link without it;
|
||||
and the shader path, which Lepton reads Steam's shader dump path against;
|
||||
- a container id of its own: with the shortcut's id, SteamVR's client in the
|
||||
container stalled in xrCreateInstance;
|
||||
- a few starts: a new APK's first start only installs it, and a start that
|
||||
ends within 30 s is tried again (up to three times), unless the game was
|
||||
quit (it writes `quit_requested.txt`).
|
||||
|
||||
Steam's "Exit game" does not reach the game (its process is the container's,
|
||||
not Steam's): quit it with the VR options' "Quit Halo" (Y in the main or
|
||||
pause menu, twice). A container ended within 30 s of its start makes
|
||||
Lepton drop the app's install, so the next start installs it again.
|
||||
|
||||
The shortcut is in `userdata/<id>/config/shortcuts.vdf` (Steam reads it when
|
||||
it starts; appid 3151454348 for this name and path), its pictures in
|
||||
`userdata/<id>/config/grid/` (from `port/android/steam_frame_grid/`). From
|
||||
the Mac, over USB the Frame's SteamOS is an adb device of its own, `frame`,
|
||||
beside Lepton's `<frame-ip>:5555`.
|
||||
|
||||
## The game's data
|
||||
|
||||
Lepton uninstalls the app and empties its folders whenever it starts again.
|
||||
The game's data lives in the SteamOS user's Documents instead: when
|
||||
`/home/steamos/Documents/HaloCE` holds `maps/ui.map` and is writable, the
|
||||
launcher and the game take it as their data folder (config.toml, saves and
|
||||
debug.txt go there too). Once:
|
||||
|
||||
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/HaloCE/maps
|
||||
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/HaloCE/maps/
|
||||
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/HaloCE
|
||||
|
||||
After a Lepton restart only the APK is installed again. (Use the path under
|
||||
/home, not /sdcard/Documents: Android 11's scoped storage stands between the
|
||||
app and the latter.)
|
||||
|
||||
## The controllers
|
||||
|
||||
SteamVR offers the Frame's controllers as themselves
|
||||
(`XR_VALVE_frame_controller_interaction`,
|
||||
`/interaction_profiles/valve/frame_controller_valve`; Valve's "Steam Frame
|
||||
Input" page). Without bindings for it, SteamVR maps the Quest's onto them,
|
||||
and the left Touch controller's X and Y land on the D-pad. With them the
|
||||
game gets the Xbox's layout, as the right controller has it: A jump, B
|
||||
flashlight (held: switch grenades), X action and reload, Y switch weapon,
|
||||
menu pause; the left one's View is Back (scores), its D-pad the D-pad, the
|
||||
bumpers switch grenades (left) and the flashlight (right). The VR options'
|
||||
page of buttons shows this layout on the Frame.
|
||||
|
||||
## Findings
|
||||
|
||||
- 2026-10-02: runs flat and in VR (GLES 3.2 by Mesa zink on Turnip, eyes
|
||||
1728x1728, 72 Hz, XR_FB_display_refresh_rate absent). Quad layers
|
||||
(menus, HUD, instructions) are not shown; the projection layer is. Froze
|
||||
at the cryo bay's look calibration; its log was lost to Lepton's wipe.
|
||||
- 2026-10-04: SteamVR draws a quad's texels by their alpha alone; the
|
||||
game's menus and HUD have none in their additive parts, so nothing showed.
|
||||
On SteamVR (by the runtime's name) the overlays' alpha now comes from their
|
||||
colour (host_xr.c; `HALO_XR_QUAD_ALPHA=colour|keep` overrides). With it,
|
||||
menus, HUD and the campaign ran well (the user: "lief 1a"); no freeze.
|
||||
- 2026-10-04: no online games found: the public MQTT brokers had banned the
|
||||
home connection for a while (too many connections from tests that day);
|
||||
broker.emqx.io came back within minutes. Lepton's network is the Frame's
|
||||
own (same IP), DNS works.
|
||||
- 2026-10-04: the refresh rate is SteamOS's setting; the runtime offers
|
||||
only that one (the game asks for the highest at or below
|
||||
`vr.refresh_rate`, and logs the rates offered). At 72 Hz the game holds
|
||||
72 fps (about 11.6 ms a frame with multiview, eyes 7.6 ms; without it
|
||||
eyes about 9 ms). At 90 Hz it cannot (11.1 ms a frame), and SteamVR's
|
||||
making up of the missed frames showed as a doubled picture when turning
|
||||
the head. Keep the Frame at 72 Hz, or lower `vr.resolution_scale` (about
|
||||
0.85) for 90.
|
||||
- 2026-10-05: doubled pictures when turning at 72 Hz too, the game at the
|
||||
edge of its frame (12.5 of 13.9 ms; SteamVR reprojecting 2-8% of the
|
||||
frames). The time was not the drawing's but waits: the layer copies read
|
||||
the GL state back (glGet*) and released each image at once, and with
|
||||
Mesa's GL thread (zink) every read waits for the thread to run all that
|
||||
is queued, every release for the GPU (the left eye's copy took 4.5-7 ms
|
||||
of the frame, the right one's, just after it, 0.7). On SteamVR the copies
|
||||
are now deferred (host_xr.c): no state read back (the game sets its own
|
||||
again), the images released together as the frame ends, and the eyes'
|
||||
work flushed to the GPU as soon as they are copied.
|
||||
`debug.xr_deferred_copies` = 0 brings back the old copies for comparing,
|
||||
1 defers them on any runtime; `vr.log_performance` logs the frame's parts
|
||||
and, with the compositor's counters, each copy's time.
|
||||
@@ -0,0 +1,325 @@
|
||||
# VR roadmap
|
||||
|
||||
What the VR port (Quest 3, Steam Frame) has, and what comes next, in the
|
||||
agreed order. Updated with every step: a step is **done** once the user has
|
||||
tried it in the headset and it is merged into `main`; **built** means in a
|
||||
branch, not yet tried in the headset.
|
||||
|
||||
Several of the next steps follow
|
||||
[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) and the
|
||||
Steam Frame work it grew from,
|
||||
[astromaddie/HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) (both
|
||||
CC0): their ideas, rebuilt into this port's own structure, with the source
|
||||
named where code is taken over.
|
||||
|
||||
## Done (in `main`)
|
||||
|
||||
| Area | What |
|
||||
| --- | --- |
|
||||
| Headsets | Quest 3; Steam Frame (Lepton): its controllers' layout, a Steam library tile that installs, starts, updates and quits the game, data kept in `~/Documents/HaloCE` |
|
||||
| Rendering | Both eyes in one pass (multiview) on the Quest; one by one on SteamVR, where multiview's eyes did not fuse; lens flares at their depth in stereo; HUD, menus, scope, crosshair and names as layers of their own |
|
||||
| Aiming | The weapon in the hand, its shots from the muzzle, laser and crosshair options |
|
||||
| Body | Physical crouch (`vr.crouch_limit`) beside the crouch button, leaning (`vr.lean_limit`), melee by swinging, grenades thrown with the left hand, two-handed weapons |
|
||||
| Vehicles | The Warthog's wheel turned with the hands, right trigger throttle, left brake; first-person seats; turrets with handles |
|
||||
| Comfort | Snap or smooth turning, a comfort vignette in vehicles, VR options in the main and pause menus (Y), a page of every controller button |
|
||||
| Menus | The PC version's menus in VR (`vr.menus`), clicked with a laser; text typed on the on-screen keyboard |
|
||||
| Multiplayer | Up to date with OpenCE (network version 20): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list |
|
||||
| Bots | Bots in multiplayer: on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person |
|
||||
| Controls | Left-handed mode (`vr.handedness`): the weapon in the left hand, the arms and grips mirrored, sticks and buttons mirrored on both controllers (`vr.mirror_controls`), the assault rifle's counter read the right way round (2026-10-06) |
|
||||
| Graphics | Presets low / medium / high / max (`vr.graphics`), refresh rate and resolution in the VR options, all kept over a restart (2026-10-06) |
|
||||
| Arms and hands | Both arms by IK from shoulders that follow the torso, the elbow led by the hand's roll; fingers from trigger, grip and touch; `vr.body` arms / hands / off, `vr.fingers` (2026-10-06) |
|
||||
|
||||
## Next, in order
|
||||
|
||||
| # | Step | State |
|
||||
| --- | --- | --- |
|
||||
| 1 | Steam Frame CPU time: Mesa's trace markers off (after astromaddie/HaloCE-VR), no `glGet` stalls in the visibility counters' copies | **done** (in `main` 2026-10-06): markers quieted, no problems; the frame's work about unchanged (11-12.5 ms of 13.9 in a bot game) |
|
||||
| 3a | Visible arms and hands: the arms by IK to the controllers, fingers by trigger and grip | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good; 0.02 ms a frame) |
|
||||
| 2 | Controls and comfort: left-handed mode, buttons remapped, flashlight by tapping the head, finer turning and vignette choices, graphics presets and refresh rate in the VR options | first part **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good): left-handed mode, graphics presets, refresh rate and resolution in the VR options. Second part **built** (branch `feature/vr-comfort`, headset test pending): buttons remapped, flashlight by tapping the head, finer turning and vignette choices |
|
||||
| 3b | The whole body: torso from head and hands, legs and feet following in the room; full / arms only / hands only | |
|
||||
| 3c | Weapons held physically, holsters at shoulder and hip (the buttons stay) | |
|
||||
| 3d | Other VR players shown with their heads, hands and bodies (today their bodies move as flat players' do: only their aim and throws come from their hands) | |
|
||||
| 4 | Later: disc image import in the launcher, sprint by arm swing (offline) | |
|
||||
|
||||
## Step 2, first part (done)
|
||||
|
||||
Left-handed mode and graphics presets, after HaloCE-Quest-VR (CC0): its
|
||||
mirrored first-person model, its graphics switches.
|
||||
|
||||
Left-handed (`vr.handedness = "left"`):
|
||||
- The weapon in the left hand: aim, shots, muzzle, laser, crosshair,
|
||||
scope disc, turret aim. The first-person model is posed as the right
|
||||
hand's in a mirror image of the room ("model space", `halo_vr.h`) and
|
||||
mirrored back; a model whose node matrices mirror has its triangles'
|
||||
winding turned over (`rasterizer_set_model_skinning`, `d3d8_gl.c`). The
|
||||
arms reach to the other shoulders, the foregrip is gripped with the
|
||||
right hand, the right hand's fingers follow its controller.
|
||||
- Grenades held and thrown with the right trigger and hand; melee by the
|
||||
left grip (a swing of either hand, as before); the motion tracker on the
|
||||
right wrist (`halo_xr_submit.wrist_hand`); the rumble's light motor in
|
||||
the weapon hand.
|
||||
- The left trigger fires, the right one throws; at the Warthog's wheel the
|
||||
left trigger accelerates, the right one brakes.
|
||||
- With `vr.mirror_controls` (default): the right stick moves, the left
|
||||
turns and steers; their clicks, the bumpers and the face buttons change
|
||||
sides (Quest: X jumps, Y light / held grenades, A action and reload, B
|
||||
switch weapon; Steam Frame: the left D-pad does A, B, X, Y as their
|
||||
mirror image, down jump, left light, right action, up switch weapon,
|
||||
and A, B, X, Y do the D-pad's). The menus keep their buttons; the
|
||||
laser pointer works from either hand as before.
|
||||
- Other machines see the shots leave the left hand (the network message
|
||||
is unchanged: aim and muzzle from the camera).
|
||||
- The assault rifle's ammo counter and compass read the right way round
|
||||
on the mirrored model (2026-10-06): their geometry stays mirrored, each
|
||||
digit's bitmap is drawn turned over in u and the digits' order across
|
||||
the display swapped (numeric shaders drawn mirrored,
|
||||
`rasterizer_xbox_transparent_geometry.c`). Checked flat in the emulator
|
||||
with `debug.mirror_first_person` (the first-person weapon mirrored off
|
||||
the headset): 60, 48, 36 ... 06 read correctly. Steam Frame good
|
||||
(2026-10-06).
|
||||
- Kept as they are (accepted 2026-10-06): the small static icons under the
|
||||
counter (round and "x") stay mirror images (a texture atlas, not
|
||||
numeric). The game's prompts still name the Xbox's buttons.
|
||||
- Settings kept: on Android `config.toml` is written beside itself and
|
||||
renamed over (`port_config.c`), so one pushed by adb (the shell's, which
|
||||
the app could not open for writing) no longer loses the VR options'
|
||||
changes at a restart.
|
||||
- Headset tests 2026-10-06: Quest 3 and Steam Frame good (left hand,
|
||||
controls mirrored and not, counter, presets, refresh rates, resolution,
|
||||
settings kept over a restart).
|
||||
|
||||
Graphics (`vr.graphics`, `port/linux/game/vr_graphics.c`), set each frame
|
||||
on the game's own switches:
|
||||
|
||||
| Preset | Leaves out |
|
||||
| --- | --- |
|
||||
| `"high"` (default) | nothing: as before |
|
||||
| `"medium"` | object shadows, environment reflections, lens flares |
|
||||
| `"low"` | also specular light, bump maps, grass and other detail objects, the screen's fog layer, contrails, weather, camouflage's second pass |
|
||||
| `"max"` | nothing; models lit for each pixel, shadows at 512x512 |
|
||||
|
||||
A client in another's game keeps grass, lens flares and the fog layer as
|
||||
the host's rules have them (`cheats.c`). Shadows, specular passes,
|
||||
reflections and detail objects are draws (CPU, which the Frame's GL
|
||||
driver is short of); bump maps, fog, camouflage and per-pixel lighting
|
||||
the GPU's fill.
|
||||
|
||||
VR options (Y): two pages, PLAY (the comfort rows, "Weapon hand",
|
||||
"Mirrored controls") and MORE ("Graphics", "Refresh rate", "Resolution",
|
||||
the controller buttons page, Quit); four since the second part (below). The refresh rate steps through what
|
||||
the runtime offers and changes at once; on the Steam Frame the row shows
|
||||
SteamOS's rate. The resolution (`vr.resolution_scale`, 70-120%) takes a
|
||||
restart. The controller buttons page shows the layout as set.
|
||||
|
||||
| Setting | Values |
|
||||
| --- | --- |
|
||||
| `vr.handedness` | `"right"` (default), `"left"` |
|
||||
| `vr.mirror_controls` | `true` (default): sticks and buttons mirrored when left-handed; `false`: as right-handed (triggers and grips still follow the hands) |
|
||||
| `vr.graphics` | `"low"`, `"medium"`, `"high"` (default), `"max"` |
|
||||
| `vr.refresh_rate` | Hz, as offered (Quest 3: 72, 80, 90, 120) |
|
||||
| `vr.resolution_scale` | 0.7 to 1.2, on restart |
|
||||
|
||||
To try in the headset (Quest 3 and Steam Frame):
|
||||
1. Left-handed: aim, laser, crosshair, scope of sniper and rocket
|
||||
launcher; weapon held right in the left hand, arms from the right
|
||||
shoulders, nothing inside out (faces culled the wrong way would show
|
||||
the inside of the gun or the glove).
|
||||
2. Two-handed grip with the right hand; grenades in the right hand (held
|
||||
and thrown); melee by grip and by swing; motion tracker on the right
|
||||
wrist.
|
||||
3. Buttons and sticks mirrored on the Touch and the Frame controllers
|
||||
(every function reachable), then `Mirrored controls: Off`.
|
||||
4. Vehicles: wheel, throttle and brake, steering stick, a gunner's aim;
|
||||
menus and the laser pointer; a network game (shots from the left hand
|
||||
on the other machine).
|
||||
5. Graphics: each preset's look and smoothness (`vr.log_performance`),
|
||||
in a bot game; Max on the Quest at 90 Hz; refresh rates 72/80/90/120
|
||||
on the Quest; the Frame's row.
|
||||
|
||||
## Step 2, second part (built)
|
||||
|
||||
On branch `feature/vr-comfort`, not yet tried in the headset. The VR
|
||||
options (Y) have four pages: PLAY ("Weapon hand", "Mirrored controls",
|
||||
laser, crosshair, motion tracker, body, fingers, the Warthog's wheel),
|
||||
COMFORT (turning, the vignettes), BUTTONS (the actions' buttons, the
|
||||
flashlight's tap, "Reset buttons to defaults", the controller buttons
|
||||
page) and MORE (graphics, refresh rate, resolution, Quit).
|
||||
|
||||
Buttons remapped (BUTTONS page; `vr.c` `controls_remapped`):
|
||||
- Jump, the flashlight (held: switch grenades), action/reload, switch
|
||||
weapon, crouch, zoom and melee each go on one of seven buttons: A, B,
|
||||
X, Y, the move stick's click, the turning stick's, the weapon hand's
|
||||
grip. Moving an action to a button swaps it with the action there, so
|
||||
none is ever left without one.
|
||||
- The triggers, the free hand's grip, the menu button, the Steam Frame's
|
||||
bumpers, D-pad and View button keep theirs; the menus keep the buttons
|
||||
as they are.
|
||||
- Applied after the left-handed mirroring, on the gamepad's sides: the
|
||||
same on the Touch and the Frame controllers, and a left-handed player's
|
||||
buttons are the mirror image of the right-handed ones (on the Frame,
|
||||
mirrored, the left D-pad's). The defaults are the old layout, the
|
||||
host's bindings and the ABI unchanged.
|
||||
- The melee's aim and jab follow its button; the grips on a Warthog's
|
||||
wheel press nothing, whatever the grip does.
|
||||
- The controller buttons page shows each action where its button is.
|
||||
|
||||
Flashlight by tapping the head (`vr.c` `flashlight_gesture_update`,
|
||||
after HaloCE-Quest-VR, CC0): either hand to the side or the top of the
|
||||
head and away, as a tap, toggles it; the button still does. A zone in
|
||||
the head's axes beside or over the head, never more than 6 cm in front
|
||||
of the eyes (a scope at the eye, a two-handed weapon, a punch and the
|
||||
wrist's tracker are in front of the face); the hand comes from 35 cm
|
||||
away, slower than 3 m/s, stays 40 to 600 ms, alone, and leaves; a second
|
||||
between toggles. None in the menus, with a grenade held or just thrown,
|
||||
just after a melee, with both hands on the weapon, the weapon hand while
|
||||
the scope is drawn, a hand on the wheel. A short rumble in the tapping
|
||||
hand. Its logic was run in a harness off the headset; the feel is the
|
||||
headset's to judge.
|
||||
|
||||
Comfort (COMFORT page): snap turns 10 to 45 degrees by 5, 60, 75, 90;
|
||||
smooth turning 30 to 150 degrees a second by 15, then to 300 by 30; the
|
||||
vehicle vignette Off, 15 to 100%; the on-foot vignette (off by default)
|
||||
while the stick turns smoothly, or also while the move stick moves, as
|
||||
far as the sticks are pushed, with its own strength. The old values are
|
||||
among the steps; defaults unchanged.
|
||||
|
||||
Transparent geometry: room for 2048 groups a window (the Xbox's 384,
|
||||
below).
|
||||
|
||||
| Setting | Values |
|
||||
| --- | --- |
|
||||
| `vr.button_jump` | `"a"` (default), `"b"`, `"x"`, `"y"`, `"move_stick"`, `"turn_stick"`, `"grip"`: as right-handed, mirrored with `vr.mirror_controls` |
|
||||
| `vr.button_light` | the flashlight, held switch grenades; `"b"` (default) |
|
||||
| `vr.button_action` | action, reload; `"x"` (default) |
|
||||
| `vr.button_weapon` | switch weapon; `"y"` (default) |
|
||||
| `vr.button_crouch` | `"move_stick"` (default) |
|
||||
| `vr.button_zoom` | `"turn_stick"` (default) |
|
||||
| `vr.button_melee` | `"grip"` (default) |
|
||||
| `vr.flashlight_gesture` | `true` (default): a tap of the head toggles the flashlight |
|
||||
| `vr.foot_vignette` | `"off"` (default), `"turning"` (smooth turns), `"moving"` (and moving) |
|
||||
| `vr.foot_vignette_strength` | 0.15 to 1, `0.7` (default) |
|
||||
| `debug.vr_options` | `false` (default); `true` shows the VR options off the headset (the emulator) |
|
||||
|
||||
Checked in the emulator (`debug.vr_options`): the four pages without
|
||||
overlaps, each row's values, swaps and reset, the controller buttons page
|
||||
right- and left-handed after swaps, the settings kept over a restart, a
|
||||
15-bot game as before.
|
||||
|
||||
To try in the headset (Quest 3 and Steam Frame):
|
||||
1. Defaults: every button as before (jump, light/held grenades, action,
|
||||
switch weapon, crouch, zoom, melee by grip and by swing), right- and
|
||||
left-handed, mirrored and not; the menus' A and B.
|
||||
2. BUTTONS: swap jump and melee (jump on the grip, melee on A): both work,
|
||||
the melee jabs and aims where the head looks; light on a stick click
|
||||
(tap: light, hold: grenades); the controller buttons page says so.
|
||||
At the Warthog's wheel with jump on the grip: no jump while holding
|
||||
the wheel. Reset: all back.
|
||||
3. The same left-handed with mirrored controls (on the Frame the left
|
||||
D-pad), and with `Mirrored controls: Off`.
|
||||
4. Flashlight tap with either hand, beside the head and over it; the
|
||||
rumble; the flashlight button still works. No toggle while: aiming
|
||||
through the sniper's and the pistol's scope, holding the weapon with
|
||||
both hands, holding and throwing a grenade (overhand past the ear),
|
||||
punching and swinging, putting on or adjusting the headset with both
|
||||
hands or one, looking at the wrist's tracker, at the wheel.
|
||||
5. COMFORT: snap 10 and 90, smooth 30 and 300; vehicle vignette 15% and
|
||||
100%; on-foot vignette "Smooth turns" (nothing with snap turns),
|
||||
"Moving, turning", strength 15% and 100%; no vignette in a vehicle
|
||||
from the on-foot one, none in the menus.
|
||||
6. A game of 30 and more players: effects, glass and water stay; the log
|
||||
(`adb logcat -s halo`) says `rasterizer: N transparent geometry groups
|
||||
in a window` once a window passes 384; frame time as before.
|
||||
|
||||
## Step 3a (done)
|
||||
|
||||
Tried on the Quest 3 and the Steam Frame, in `main`. The arm IK,
|
||||
the forearm's twist and the fingers' poses follow HaloCE-Quest-VR (CC0),
|
||||
in `port/linux/game/vr_arms.c`.
|
||||
|
||||
- The first-person pose (weapon, arms) is built once a frame instead of
|
||||
per eye and scope view (SteamVR: two to three times before).
|
||||
- `vr.log_performance` reports its cost (`VR: first person: ... ms a
|
||||
frame in ... builds (arms ... ms), ... kept`) and, once, how the hands'
|
||||
skin hangs from its bones (none from the root: no triangle filter
|
||||
needed).
|
||||
- Both arms bend from shoulders below the neck, square to the torso. The
|
||||
torso follows the head beyond 15 degrees and turns towards the hands;
|
||||
snap and smooth turns carry it at once.
|
||||
- The free left arm bends at the elbow to the controller (before: carried
|
||||
rigidly). The hand's roll is shared along forearm and upper arm.
|
||||
- The free hand's fingers follow the controller: index from the trigger,
|
||||
the others from the grip, the thumb from where it rests (touch on the
|
||||
Quest's, Index and Steam Frame controllers; half curled where unknown),
|
||||
closed round a held grenade. The weapon hand keeps its grip; its index
|
||||
finger lifts off the trigger when the controller says it is off.
|
||||
- host_xr: touch actions for thumb and index; `suggest()` takes 64
|
||||
bindings (was 32, silently cut) and retries a profile without the
|
||||
touch paths should the runtime refuse them.
|
||||
|
||||
Settings (`[vr]` in config.toml; the first two also in the VR options,
|
||||
Y on the main or pause menu):
|
||||
|
||||
| Setting | Values |
|
||||
| --- | --- |
|
||||
| `vr.body` | `"arms"` (default), `"hands"` (hands alone, the arms shrunk behind the wrists), `"off"` (the weapon alone) |
|
||||
| `vr.fingers` | `true` (default): fingers follow the controller; `false`: as the weapon's animation has them |
|
||||
| `vr.arm_ik` | `true` (default): the free arm bends at the elbow; `false`: rigid as before, should its skin tear |
|
||||
|
||||
After the first Quest tries (2026-10-06):
|
||||
- The shoulders 13 cm behind the eyes and 20 cm below the neck: the open
|
||||
end of the sleeve was in view. Looking hard down to the side one still
|
||||
sees into the arm (the model ends at the shoulder): left as it is.
|
||||
- The elbow bends in the arm's plane (`vr_align_elbow`): it had turned in
|
||||
with the elbow out to the side; the upper arm takes none of the hand's
|
||||
roll.
|
||||
- The free arm is solved first and its hand turned after (as
|
||||
HaloCE-Quest-VR does): turned first, its skin stretched like a rubber
|
||||
band.
|
||||
- The elbow follows the hand's roll (lifted out, the controller rolls in):
|
||||
before, it bent only towards a fixed direction.
|
||||
- Arm length, shoulder width, fingers, grenade grip, body modes, scope,
|
||||
vehicles: fine.
|
||||
|
||||
Not in it (3a.7): the glove fitted to each weapon's grip (gun anchor) and
|
||||
the left hand animated through reloads.
|
||||
|
||||
## Open points outside the order
|
||||
|
||||
- Multiview on the Steam Frame: why the eyes do not fuse there (suspected
|
||||
Mesa zink/Turnip); tools in branch `frame/doubled-images` (86335828),
|
||||
not merged.
|
||||
- Transparent geometry groups: the Xbox's 384 a window overflowed in
|
||||
games of 30 and more players (the last submitted, glass, water,
|
||||
particles, contrails, vanished). Raised to 2048 (320 KB) on
|
||||
`feature/vr-comfort`, to check in a large game (step 2, second part,
|
||||
test 6); upstream still has 384.
|
||||
|
||||
## Headset tests (2026-10-06, all three in `main`)
|
||||
|
||||
Quest 3, 2026-10-06: menus, laser, VR options, head turning, arms and
|
||||
hands, fingers, body modes, scope, vehicles, a bot game (bots spread and
|
||||
leave their bases) and a large online game: all good.
|
||||
Steam Frame, 2026-10-06: buttons, picture (no doubled image), arms, hands,
|
||||
fingers, wheel: all good; SteamVR reprojected 18% of the frames, unseen.
|
||||
What follows is left for the two headsets together.
|
||||
|
||||
Upstream merge (`merge/upstream-2026-10-06`):
|
||||
- Menus, a bot game, head turning (no doubled image), co-op briefly.
|
||||
- A password game hosted on the Frame, joined from the Quest.
|
||||
- A bot making room for a person joining a full lobby.
|
||||
|
||||
Performance (`frame/performance-2`, and the merge's upstream speed-ups),
|
||||
with `vr.log_performance = true`:
|
||||
- Frame: the log says `trace markers: N descriptors to /dev/null`; frame
|
||||
time ("working") against before (about 12.5 ms in a bot game on Blood
|
||||
Gulch); SteamVR's reprojected share (vrcompositor.txt) against before
|
||||
(2 to 17 %); no doubled image.
|
||||
- Lens flares still appear and hide as before (their visibility counters'
|
||||
copies changed).
|
||||
- The pose cache (3a.1): `first person: ... builds` about one a frame;
|
||||
scope, crosshair, laser and the weapon in the hand as before.
|
||||
- Quest: 90 Hz held as before in a bot game and in a large online game;
|
||||
nothing slower than the last build.
|
||||
|
||||
Arms and hands (3a): on the Steam Frame, as on the Quest (fingers on its
|
||||
controllers, every button still working).
|
||||
@@ -35,6 +35,7 @@ import java.nio.channels.FileChannel;
|
||||
*/
|
||||
public class LauncherActivity extends Activity {
|
||||
private static final int PICK_IMAGE = 1;
|
||||
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/HaloCE";
|
||||
|
||||
private File dataRoot;
|
||||
private TextView status;
|
||||
@@ -46,6 +47,12 @@ public class LauncherActivity extends Activity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
dataRoot = getExternalFilesDir(null);
|
||||
// the Steam Frame: Lepton empties the app's own folders whenever it
|
||||
// starts again; the game's data lives in the SteamOS user's
|
||||
// Documents instead, when it is there (port/android/STEAM_FRAME.md;
|
||||
// the same choice as host_main.c's)
|
||||
if (new File(SHARED_DATA_ROOT, "maps/ui.map").isFile() && new File(SHARED_DATA_ROOT).canWrite())
|
||||
dataRoot = new File(SHARED_DATA_ROOT);
|
||||
// created by the app, so that files pushed into it with adb stay
|
||||
// readable (a directory adb creates there belongs to the shell user)
|
||||
if (dataRoot != null)
|
||||
|
||||
@@ -68,6 +68,9 @@ int host_sdl_gl_swap_window(unsigned int window);
|
||||
int host_sdl_poll_event(void *event);
|
||||
int host_sdl_set_clipboard_text(const char *text);
|
||||
void host_sdl_get_clipboard_text(char *buffer, unsigned int size);
|
||||
/* the keyboard's keys by name (the controls' bindings, xinput_sdl.c) */
|
||||
void host_sdl_scancode_name(int scancode, char *buffer, unsigned int size);
|
||||
int host_sdl_scancode_from_name(const char *name);
|
||||
int host_sdl_show_toast(const char *message, int duration, int gravity, int x, int y);
|
||||
int host_sdl_show_simple_message_box(unsigned int flags, const char *title, const char *message);
|
||||
int host_sdl_get_gamepads(unsigned int *ids, int capacity);
|
||||
@@ -116,7 +119,9 @@ headset before the game started, and set HALO_VR); 0 on failure */
|
||||
int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate);
|
||||
/* waits for the next frame and reports the predicted poses */
|
||||
int host_xr_begin_frame(struct halo_xr_frame *frame);
|
||||
/* copies a render target texture (0: clears) into a layer's image */
|
||||
/* copies a render target texture (0: clears) into a layer's image: 0 not
|
||||
copied, 1 copied with the GL state put back, 2 copied with the copy's GL
|
||||
state left (the deferred copies, host_xr.c), which the game sets again */
|
||||
int host_xr_copy_layer(int layer, unsigned int texture, int clear_alpha);
|
||||
/* the same for part of the texture: left, top, right, bottom, 0..1 from its
|
||||
top left */
|
||||
@@ -130,6 +135,11 @@ into a layer's image, opaque */
|
||||
int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer);
|
||||
/* darkens the eye images from their edges, strength 0..1 (0: off) */
|
||||
void host_xr_set_vignette(float strength);
|
||||
/* the refresh rates the headset offers (Hz, at most maximum of them; 0 when
|
||||
it lets none be chosen), and the one it runs at (0: unknown); and asking
|
||||
for the highest offered at or below wanted (0: the highest) */
|
||||
int host_xr_refresh_rates(float *rates, int maximum, float *current);
|
||||
void host_xr_set_refresh_rate(float wanted);
|
||||
|
||||
/* ---------- Android */
|
||||
|
||||
|
||||
@@ -39,6 +39,19 @@ const char *SDL_GetError(void)
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const char *SDL_GetScancodeName(SDL_Scancode scancode)
|
||||
{
|
||||
static __thread char buffer[64];
|
||||
|
||||
host_sdl_scancode_name((int)scancode, buffer, sizeof(buffer));
|
||||
return buffer;
|
||||
}
|
||||
|
||||
SDL_Scancode SDL_GetScancodeFromName(const char *name)
|
||||
{
|
||||
return (SDL_Scancode)host_sdl_scancode_from_name(name);
|
||||
}
|
||||
|
||||
Uint64 SDL_GetTicks(void)
|
||||
{
|
||||
return (Uint64)host_sdl_ticks();
|
||||
|
||||
@@ -92,6 +92,7 @@ void *host_resolve_import(const char *name);
|
||||
/* ---------- SDL / GL (host_sdl.c, host_gl.c) */
|
||||
|
||||
void *host_gl_resolve(const char *name);
|
||||
int host_gl_quiet_trace_markers(void);
|
||||
|
||||
/* ---------- VR (host_xr.c) */
|
||||
|
||||
|
||||
@@ -11,8 +11,52 @@ already have host types by then. Only strings need copying back.
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl32.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Valve's Mesa (the Steam Frame's Lepton) writes a marker to the kernel's
|
||||
trace_marker around every flush and context switch, a couple of hundred a
|
||||
frame, whether or not anything traces. The descriptors it holds open on that
|
||||
file are pointed at /dev/null instead (not closed: a reused descriptor would
|
||||
take the markers elsewhere). After astromaddie/HaloCE-VR (CC0). Returns how
|
||||
many were. */
|
||||
int host_gl_quiet_trace_markers(void)
|
||||
{
|
||||
DIR *directory = opendir("/proc/self/fd");
|
||||
struct dirent *entry;
|
||||
int quieted = 0, null_fd;
|
||||
|
||||
if (!directory)
|
||||
return 0;
|
||||
null_fd = open("/dev/null", O_WRONLY | O_CLOEXEC);
|
||||
while (null_fd >= 0 && (entry = readdir(directory)) != NULL)
|
||||
{
|
||||
char link[64], target[256];
|
||||
ssize_t length;
|
||||
int fd = atoi(entry->d_name);
|
||||
|
||||
if (entry->d_name[0] < '0' || entry->d_name[0] > '9' || fd == null_fd)
|
||||
continue;
|
||||
snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
|
||||
length = readlink(link, target, sizeof(target) - 1);
|
||||
if (length <= 0)
|
||||
continue;
|
||||
target[length] = 0;
|
||||
if (strstr(target, "tracing/trace_marker") && dup3(null_fd, fd, O_CLOEXEC) == fd)
|
||||
quieted++;
|
||||
}
|
||||
if (null_fd >= 0)
|
||||
close(null_fd);
|
||||
closedir(directory);
|
||||
if (quieted)
|
||||
host_logf(HOST_LOG_INFO, "trace markers: %d descriptors to /dev/null", quieted);
|
||||
return quieted;
|
||||
}
|
||||
|
||||
void *host_gl_resolve(const char *name)
|
||||
{
|
||||
@@ -88,14 +132,13 @@ half-drawn targets out of tile memory, for every lens flare. */
|
||||
static GLuint counter_snapshots[COUNTER_SNAPSHOTS];
|
||||
static uint32_t counter_snapshot_sizes[COUNTER_SNAPSHOTS];
|
||||
|
||||
/* (the copy bindings are left unbound after: the renderer binds them itself
|
||||
for each copy. Asking GL what they were would wait for Mesa's GL thread, on
|
||||
the Frame, to run everything queued) */
|
||||
void host_gl_counters_snapshot(uint32_t buffer, uint32_t size, uint32_t slot)
|
||||
{
|
||||
GLint read_binding = 0, write_binding = 0;
|
||||
|
||||
if (slot >= COUNTER_SNAPSHOTS || !buffer || !size)
|
||||
return;
|
||||
glGetIntegerv(GL_COPY_READ_BUFFER_BINDING, &read_binding);
|
||||
glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &write_binding);
|
||||
if (!counter_snapshots[slot])
|
||||
glGenBuffers(1, &counter_snapshots[slot]);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
|
||||
@@ -108,19 +151,17 @@ void host_gl_counters_snapshot(uint32_t buffer, uint32_t size, uint32_t slot)
|
||||
glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, buffer);
|
||||
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, size);
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, (GLuint)read_binding);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, 0);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||
}
|
||||
|
||||
/* copies a snapshot out; its frame must be finished (host_gl_wait_frame) */
|
||||
int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
|
||||
{
|
||||
GLint write_binding = 0;
|
||||
const void *mapping;
|
||||
|
||||
if (slot >= COUNTER_SNAPSHOTS || !counter_snapshots[slot] || size > counter_snapshot_sizes[slot])
|
||||
return 0;
|
||||
glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &write_binding);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
|
||||
mapping = glMapBufferRange(GL_COPY_WRITE_BUFFER, 0, size, GL_MAP_READ_BIT);
|
||||
if (mapping)
|
||||
@@ -128,7 +169,7 @@ int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
|
||||
memcpy(out, mapping, size);
|
||||
glUnmapBuffer(GL_COPY_WRITE_BUFFER);
|
||||
}
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||
return mapping != NULL;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,8 @@ int host_errno(void)
|
||||
|
||||
static char data_root[512];
|
||||
static char save_root[512];
|
||||
/* where a device that empties the app's folders keeps the game's data */
|
||||
#define SHARED_DATA_ROOT "/home/steamos/Documents/HaloCE"
|
||||
|
||||
void host_android_path(int which, char *buffer, uint32_t size)
|
||||
{
|
||||
@@ -269,6 +271,12 @@ static void *game_main(void *unused)
|
||||
external = SDL_GetAndroidExternalStoragePath();
|
||||
if (!external)
|
||||
host_fatal("Android storage is unavailable: %s", SDL_GetError());
|
||||
/* the Steam Frame: Lepton empties the app's own folders whenever it
|
||||
starts again; the game's data lives in the SteamOS user's Documents
|
||||
instead, when it is there (port/android/STEAM_FRAME.md; the same choice
|
||||
as LauncherActivity's) */
|
||||
if (directory_has_maps(SHARED_DATA_ROOT) && access(SHARED_DATA_ROOT, W_OK) == 0)
|
||||
external = SHARED_DATA_ROOT;
|
||||
snprintf(data_root, sizeof(data_root), "%s", external);
|
||||
snprintf(save_root, sizeof(save_root), "%s/save", external);
|
||||
/* readable by adb (the shell user), for managing saves */
|
||||
@@ -293,6 +301,19 @@ static void *game_main(void *unused)
|
||||
environment_set(&environment, "HALO_SAVE_ROOT", save_root);
|
||||
time_zone(zone, sizeof(zone));
|
||||
environment_set(&environment, "TZ", zone);
|
||||
/* internet play's MQTT brokers (network.brokers_file): the APK's list,
|
||||
written beside config.toml at each start, as a desktop update replaces
|
||||
the file beside its game */
|
||||
{
|
||||
size_t brokers_size = 0;
|
||||
void *brokers = SDL_LoadFile("brokers.txt", &brokers_size);
|
||||
|
||||
snprintf(path, sizeof(path), "%s/brokers.txt", data_root);
|
||||
if (!brokers || !SDL_SaveFile(path, brokers, brokers_size))
|
||||
host_logf(HOST_LOG_ERROR, "cannot write %s: %s", path, SDL_GetError());
|
||||
SDL_free(brokers);
|
||||
}
|
||||
snprintf(path, sizeof(path), "%s/config.toml", data_root);
|
||||
|
||||
image = SDL_LoadFile("halo_guest.elf", &image_size);
|
||||
if (!image)
|
||||
@@ -305,6 +326,26 @@ static void *game_main(void *unused)
|
||||
ranges: the headset's runtime, loaded by the probe, maps its libraries
|
||||
wherever there is room, the Xbox window at 0x80000000 included */
|
||||
snprintf(path, sizeof(path), "%s/config.toml", data_root);
|
||||
{
|
||||
/* (debug.gl_thread: Mesa's GL thread on or off, as the driver's
|
||||
mesa_glthread, before the GL context is made; -1 leaves it be) */
|
||||
double gl_thread = config_number(path, "debug.gl_thread", -1.0);
|
||||
|
||||
if (gl_thread >= 0.0)
|
||||
{
|
||||
setenv("mesa_glthread", gl_thread != 0.0 ? "true" : "false", 1);
|
||||
host_logf(HOST_LOG_INFO, "Mesa's GL thread %s (debug.gl_thread)", gl_thread != 0.0 ? "on" : "off");
|
||||
}
|
||||
}
|
||||
{
|
||||
/* (debug.xr_deferred_copies: the headset's layer copies deferred,
|
||||
host_xr.c, or one by one; -1 as the runtime has it, deferred on
|
||||
SteamVR) */
|
||||
double deferred = config_number(path, "debug.xr_deferred_copies", -1.0);
|
||||
|
||||
if (deferred >= 0.0)
|
||||
setenv("HALO_XR_DEFERRED", deferred != 0.0 ? "1" : "0", 1);
|
||||
}
|
||||
{
|
||||
int eye_width, eye_height;
|
||||
|
||||
|
||||
@@ -98,6 +98,16 @@ void host_sdl_get_error(char *buffer, uint32_t size)
|
||||
SDL_strlcpy(buffer, SDL_GetError(), size);
|
||||
}
|
||||
|
||||
void host_sdl_scancode_name(int32_t scancode, char *buffer, uint32_t size)
|
||||
{
|
||||
SDL_strlcpy(buffer, SDL_GetScancodeName((SDL_Scancode)scancode), size);
|
||||
}
|
||||
|
||||
int32_t host_sdl_scancode_from_name(const char *name)
|
||||
{
|
||||
return (int32_t)SDL_GetScancodeFromName(name);
|
||||
}
|
||||
|
||||
int64_t host_sdl_ticks(void)
|
||||
{
|
||||
return (int64_t)SDL_GetTicks();
|
||||
@@ -141,7 +151,11 @@ uint32_t host_sdl_gl_create_context(uint32_t window)
|
||||
{
|
||||
SDL_Window *object = handle_get(window, _handle_window);
|
||||
|
||||
return object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
|
||||
uint32_t context = object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
|
||||
|
||||
/* (the driver opens its trace markers with the context) */
|
||||
host_gl_quiet_trace_markers();
|
||||
return context;
|
||||
}
|
||||
|
||||
int host_sdl_gl_make_current(uint32_t window, uint32_t context)
|
||||
|
||||
@@ -38,6 +38,8 @@ picture; the copy flips them to GL's bottom-up images.
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <sched.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define XR_USE_PLATFORM_ANDROID
|
||||
#define XR_USE_GRAPHICS_API_OPENGL_ES
|
||||
@@ -56,6 +58,7 @@ struct xr_swapchain
|
||||
GLuint framebuffers[MAXIMUM_SWAPCHAIN_IMAGES];
|
||||
uint32_t acquired_index;
|
||||
int filled; /* this frame */
|
||||
int held; /* acquired, released only as the frame ends (xr.deferred) */
|
||||
};
|
||||
|
||||
struct xr_hand_actions
|
||||
@@ -102,15 +105,54 @@ static struct
|
||||
XrAction button_b; /* B on the right controller, Y on the left */
|
||||
XrAction menu;
|
||||
XrAction thumbrest;
|
||||
/* the fingers' touch, for the hands drawn (where the runtime has it):
|
||||
the thumb on the stick, a button or the rest, the index on the trigger */
|
||||
XrAction thumb_touch;
|
||||
XrAction index_touch;
|
||||
/* the Steam Frame's controllers' own (XR_VALVE_frame_controller_interaction) */
|
||||
int has_frame_controller;
|
||||
int frame_layout;
|
||||
XrAction button_x;
|
||||
XrAction button_y;
|
||||
XrAction view;
|
||||
XrAction bumper;
|
||||
XrAction dpad[4];
|
||||
XrAction haptic;
|
||||
struct xr_hand_actions hands[HALO_XR_HAND_COUNT];
|
||||
|
||||
/* where the head and the left hand are this frame (the wrist layer) */
|
||||
/* where the head and the hands are this frame (the wrist layer) */
|
||||
XrPosef head;
|
||||
XrPosef left_grip;
|
||||
int left_grip_valid;
|
||||
XrPosef grips[HALO_XR_HAND_COUNT];
|
||||
int grips_valid[HALO_XR_HAND_COUNT];
|
||||
|
||||
int has_performance_settings;
|
||||
/* (XR_META_performance_metrics: the compositor's counts, in the log,
|
||||
for telling frames shown late from the game's own pace) */
|
||||
int has_metrics;
|
||||
PFN_xrQueryPerformanceMetricsCounterMETA query_metric;
|
||||
XrPath metric_paths[16];
|
||||
uint32_t metric_count;
|
||||
struct timespec metrics_logged;
|
||||
/* (SteamVR: the frames' thread kept on the fast cores, xr_fast_cores) */
|
||||
int steamvr;
|
||||
int cores_pinned;
|
||||
/* (with the metrics: the time the copies into the layers' images and
|
||||
xrEndFrame took, since the last line, and the frames) */
|
||||
double copy_seconds[HALO_XR_LAYER_COUNT];
|
||||
double end_seconds;
|
||||
double acquire_seconds;
|
||||
double release_seconds;
|
||||
int timed_frames;
|
||||
/* The copies without waits in the frame (SteamVR: HALO_XR_DEFERRED,
|
||||
debug.xr_deferred_copies). Each copy otherwise reads the GL state back
|
||||
to restore it (glGet*), and releases its image at once: with Mesa's GL
|
||||
thread (the Steam Frame's zink) each read waits for the thread to run
|
||||
everything queued, and each release for the GPU to finish it, so the
|
||||
game's one thread stood still for the eyes' whole drawing in the middle
|
||||
of every frame. Deferred, a copy leaves its GL state for the game to set
|
||||
again (host_xr_copy_layer's 2) and keeps its image until the frame ends,
|
||||
where all are released at once. */
|
||||
int deferred;
|
||||
int has_refresh_rate;
|
||||
PFN_xrRequestDisplayRefreshRateFB request_refresh_rate;
|
||||
PFN_xrGetDisplayRefreshRateFB get_refresh_rate;
|
||||
@@ -122,6 +164,11 @@ static struct
|
||||
GLint rect_uniform;
|
||||
GLint cut_disc_uniform;
|
||||
GLint vignette_uniform;
|
||||
GLint alpha_from_colour_uniform;
|
||||
/* the runtime draws a quad's texels by their alpha alone, ignoring the
|
||||
colour of those with none (SteamVR on the Steam Frame): the game's
|
||||
overlays, whose additive parts have none, get alpha from their colour */
|
||||
int quad_alpha_from_colour;
|
||||
GLint tangents_uniform;
|
||||
float vignette;
|
||||
/* the same copy from a layer of a texture array (the eyes drawn at
|
||||
@@ -223,6 +270,7 @@ static const char copy_fragment_shader[] =
|
||||
"precision highp float;\n"
|
||||
"uniform sampler2D source;\n"
|
||||
"uniform float cut_disc;\n"
|
||||
"uniform float alpha_from_colour; /* 1: as opaque as it is bright (a quad's, SteamVR) */\n"
|
||||
"uniform float vignette; /* 0..1: the eye images darken from their edges */\n"
|
||||
"uniform vec4 tangents; /* the eye's edges: left, right, down, up */\n"
|
||||
"in vec2 coordinate;\n"
|
||||
@@ -236,6 +284,8 @@ static const char copy_fragment_shader[] =
|
||||
"{\n"
|
||||
" vec4 texel = texture(source, coordinate);\n"
|
||||
" color = vec4(to_linear(texel.rgb), texel.a);\n"
|
||||
" if (alpha_from_colour > 0.5)\n"
|
||||
" color.a = max(color.a, max(color.r, max(color.g, color.b)));\n"
|
||||
" if (cut_disc > 0.5)\n"
|
||||
" {\n"
|
||||
" float inside = 1.0 - smoothstep(0.95, 1.0, length(disc));\n"
|
||||
@@ -326,6 +376,7 @@ static int copy_initialize(void)
|
||||
xr.rect_uniform = glGetUniformLocation(xr.program, "rect");
|
||||
xr.cut_disc_uniform = glGetUniformLocation(xr.program, "cut_disc");
|
||||
xr.vignette_uniform = glGetUniformLocation(xr.program, "vignette");
|
||||
xr.alpha_from_colour_uniform = glGetUniformLocation(xr.program, "alpha_from_colour");
|
||||
xr.tangents_uniform = glGetUniformLocation(xr.program, "tangents");
|
||||
xr.copy_array_layer = -1;
|
||||
{
|
||||
@@ -538,22 +589,51 @@ struct binding
|
||||
const char *path;
|
||||
};
|
||||
|
||||
static void suggest(const char *profile, const struct binding *bindings, int count)
|
||||
{
|
||||
XrActionSuggestedBinding suggested[32];
|
||||
XrInteractionProfileSuggestedBinding information = { XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
|
||||
int index;
|
||||
/* a profile's bindings: those it has to have, and those it may (a path the
|
||||
runtime does not know refuses the whole suggestion, which leaves the
|
||||
profile with none: then the first alone, as before). Nonzero if taken. */
|
||||
#define MAXIMUM_SUGGESTED_BINDINGS 64
|
||||
|
||||
for (index = 0; index < count && index < 32; index++)
|
||||
static int suggest_bindings(const char *profile, const struct binding *bindings, int count,
|
||||
const struct binding *optional, int optional_count)
|
||||
{
|
||||
XrActionSuggestedBinding suggested[MAXIMUM_SUGGESTED_BINDINGS];
|
||||
XrInteractionProfileSuggestedBinding information = { XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
|
||||
XrResult result;
|
||||
int index, total = 0;
|
||||
|
||||
if (count + optional_count > MAXIMUM_SUGGESTED_BINDINGS)
|
||||
{
|
||||
suggested[index].action = *bindings[index].action;
|
||||
suggested[index].binding = path_from_string(bindings[index].path);
|
||||
host_logf(HOST_LOG_ERROR, "xr: %d bindings for %s, room for %d: the rest left out", count + optional_count,
|
||||
profile, MAXIMUM_SUGGESTED_BINDINGS);
|
||||
}
|
||||
for (index = 0; index < count + optional_count && total < MAXIMUM_SUGGESTED_BINDINGS; index++)
|
||||
{
|
||||
const struct binding *binding = index < count ? &bindings[index] : &optional[index - count];
|
||||
|
||||
suggested[total].action = *binding->action;
|
||||
suggested[total].binding = path_from_string(binding->path);
|
||||
total++;
|
||||
}
|
||||
information.interactionProfile = path_from_string(profile);
|
||||
information.suggestedBindings = suggested;
|
||||
information.countSuggestedBindings = (uint32_t)index;
|
||||
if (!XR_SUCCEEDED(xrSuggestInteractionProfileBindings(xr.instance, &information)))
|
||||
host_logf(HOST_LOG_WARN, "xr: no bindings for %s", profile);
|
||||
information.countSuggestedBindings = (uint32_t)total;
|
||||
result = xrSuggestInteractionProfileBindings(xr.instance, &information);
|
||||
if (XR_SUCCEEDED(result))
|
||||
return 1;
|
||||
if (optional_count > 0)
|
||||
{
|
||||
host_logf(HOST_LOG_WARN, "xr: %s refused with its optional bindings (%d); suggested without them", profile,
|
||||
(int)result);
|
||||
return suggest_bindings(profile, bindings, count, NULL, 0);
|
||||
}
|
||||
host_logf(HOST_LOG_WARN, "xr: no bindings for %s (%d)", profile, (int)result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void suggest(const char *profile, const struct binding *bindings, int count)
|
||||
{
|
||||
suggest_bindings(profile, bindings, count, NULL, 0);
|
||||
}
|
||||
|
||||
static int actions_initialize(void)
|
||||
@@ -580,6 +660,16 @@ static int actions_initialize(void)
|
||||
xr.button_b = action_create("button_upper", "B / Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.menu = action_create("menu", "Menu", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.thumbrest = action_create("thumbrest", "Thumb rest", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.thumb_touch = action_create("thumb_touch", "Thumb resting", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.index_touch = action_create("index_touch", "Finger on the trigger", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.button_x = action_create("button_x", "X", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.button_y = action_create("button_y", "Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.view = action_create("view", "View", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.bumper = action_create("bumper", "Bumper", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[0] = action_create("dpad_up", "D-pad up", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[1] = action_create("dpad_down", "D-pad down", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[2] = action_create("dpad_left", "D-pad left", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[3] = action_create("dpad_right", "D-pad right", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.haptic = action_create("haptic", "Rumble", XR_ACTION_TYPE_VIBRATION_OUTPUT);
|
||||
|
||||
{
|
||||
@@ -644,8 +734,95 @@ static int actions_initialize(void)
|
||||
{ &xr.haptic, "/user/hand/right/output/haptic" },
|
||||
};
|
||||
|
||||
suggest("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]));
|
||||
suggest("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[0]));
|
||||
/* the Steam Frame's controllers, as themselves: the right one has
|
||||
A, B, X, Y and a menu button, the left one a D-pad and a View button
|
||||
(port/android/STEAM_FRAME.md) */
|
||||
static const struct binding frame_controller[] =
|
||||
{
|
||||
{ &xr.grip_pose, "/user/hand/left/input/grip/pose" },
|
||||
{ &xr.grip_pose, "/user/hand/right/input/grip/pose" },
|
||||
{ &xr.aim_pose, "/user/hand/left/input/aim/pose" },
|
||||
{ &xr.aim_pose, "/user/hand/right/input/aim/pose" },
|
||||
{ &xr.trigger, "/user/hand/left/input/trigger/value" },
|
||||
{ &xr.trigger, "/user/hand/right/input/trigger/value" },
|
||||
{ &xr.squeeze, "/user/hand/left/input/squeeze/value" },
|
||||
{ &xr.squeeze, "/user/hand/right/input/squeeze/value" },
|
||||
{ &xr.thumbstick, "/user/hand/left/input/thumbstick" },
|
||||
{ &xr.thumbstick, "/user/hand/right/input/thumbstick" },
|
||||
{ &xr.thumbstick_click, "/user/hand/left/input/thumbstick/click" },
|
||||
{ &xr.thumbstick_click, "/user/hand/right/input/thumbstick/click" },
|
||||
{ &xr.button_a, "/user/hand/right/input/a/click" },
|
||||
{ &xr.button_b, "/user/hand/right/input/b/click" },
|
||||
{ &xr.button_x, "/user/hand/right/input/x/click" },
|
||||
{ &xr.button_y, "/user/hand/right/input/y/click" },
|
||||
{ &xr.menu, "/user/hand/right/input/menu/click" },
|
||||
{ &xr.view, "/user/hand/left/input/view/click" },
|
||||
{ &xr.bumper, "/user/hand/left/input/bumper/click" },
|
||||
{ &xr.bumper, "/user/hand/right/input/bumper/click" },
|
||||
{ &xr.dpad[0], "/user/hand/left/input/dpad_up/click" },
|
||||
{ &xr.dpad[1], "/user/hand/left/input/dpad_down/click" },
|
||||
{ &xr.dpad[2], "/user/hand/left/input/dpad_left/click" },
|
||||
{ &xr.dpad[3], "/user/hand/left/input/dpad_right/click" },
|
||||
{ &xr.haptic, "/user/hand/left/output/haptic" },
|
||||
{ &xr.haptic, "/user/hand/right/output/haptic" },
|
||||
};
|
||||
|
||||
/* the fingers' touch, as each profile has it (after HaloCE-Quest-VR,
|
||||
CC0; the Frame's from Valve's "Steam Frame Input" page): optional, a
|
||||
profile refusing them keeps its buttons */
|
||||
static const struct binding touch_fingers[] =
|
||||
{
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/thumbstick/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/thumbstick/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/thumbrest/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/thumbrest/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/x/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/y/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/a/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/b/touch" },
|
||||
{ &xr.index_touch, "/user/hand/left/input/trigger/touch" },
|
||||
{ &xr.index_touch, "/user/hand/right/input/trigger/touch" },
|
||||
};
|
||||
static const struct binding index_fingers[] =
|
||||
{
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/thumbstick/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/thumbstick/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/trackpad/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/trackpad/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/a/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/b/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/a/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/b/touch" },
|
||||
{ &xr.index_touch, "/user/hand/left/input/trigger/touch" },
|
||||
{ &xr.index_touch, "/user/hand/right/input/trigger/touch" },
|
||||
};
|
||||
static const struct binding frame_fingers[] =
|
||||
{
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/thumbstick/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/thumbstick/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/a/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/b/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/x/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/y/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/dpad_up/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/dpad_down/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/dpad_left/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/dpad_right/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/view/touch" },
|
||||
{ &xr.index_touch, "/user/hand/left/input/trigger/touch" },
|
||||
{ &xr.index_touch, "/user/hand/right/input/trigger/touch" },
|
||||
};
|
||||
|
||||
if (xr.has_frame_controller)
|
||||
{
|
||||
suggest_bindings("/interaction_profiles/valve/frame_controller_valve", frame_controller,
|
||||
sizeof(frame_controller) / sizeof(frame_controller[0]), frame_fingers,
|
||||
sizeof(frame_fingers) / sizeof(frame_fingers[0]));
|
||||
}
|
||||
suggest_bindings("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]),
|
||||
touch_fingers, sizeof(touch_fingers) / sizeof(touch_fingers[0]));
|
||||
suggest_bindings("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[0]),
|
||||
index_fingers, sizeof(index_fingers) / sizeof(index_fingers[0]));
|
||||
suggest("/interaction_profiles/khr/simple_controller", simple, sizeof(simple) / sizeof(simple[0]));
|
||||
}
|
||||
|
||||
@@ -688,6 +865,21 @@ static int action_boolean(XrAction action, int hand)
|
||||
return state.currentState != XR_FALSE;
|
||||
}
|
||||
|
||||
/* (and whether the runtime gives the action for the hand at all) */
|
||||
static int action_boolean_active(XrAction action, int hand, int *active)
|
||||
{
|
||||
XrActionStateGetInfo information = { XR_TYPE_ACTION_STATE_GET_INFO };
|
||||
XrActionStateBoolean state = { XR_TYPE_ACTION_STATE_BOOLEAN };
|
||||
|
||||
*active = 0;
|
||||
information.action = action;
|
||||
information.subactionPath = xr.hands[hand].path;
|
||||
if (!XR_SUCCEEDED(xrGetActionStateBoolean(xr.session, &information, &state)) || !state.isActive)
|
||||
return 0;
|
||||
*active = 1;
|
||||
return state.currentState != XR_FALSE;
|
||||
}
|
||||
|
||||
static int action_vector(XrAction action, int hand, float vector[2])
|
||||
{
|
||||
XrActionStateGetInfo information = { XR_TYPE_ACTION_STATE_GET_INFO };
|
||||
@@ -713,6 +905,30 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
sync.activeActionSets = &active;
|
||||
if (!XR_SUCCEEDED(xrSyncActions(xr.session, &sync)))
|
||||
return;
|
||||
{
|
||||
/* (the controllers' profile the runtime chose, in the log as it
|
||||
changes) */
|
||||
static XrPath logged[HALO_XR_HAND_COUNT];
|
||||
|
||||
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
|
||||
{
|
||||
XrInteractionProfileState state = { XR_TYPE_INTERACTION_PROFILE_STATE };
|
||||
|
||||
if (XR_SUCCEEDED(xrGetCurrentInteractionProfile(xr.session, xr.hands[hand].path, &state)) &&
|
||||
state.interactionProfile != logged[hand])
|
||||
{
|
||||
char name[XR_MAX_PATH_LENGTH] = "none";
|
||||
uint32_t length = 0;
|
||||
|
||||
logged[hand] = state.interactionProfile;
|
||||
if (state.interactionProfile != XR_NULL_PATH)
|
||||
xrPathToString(xr.instance, state.interactionProfile, sizeof(name), &length, name);
|
||||
if (hand == HALO_XR_HAND_RIGHT)
|
||||
xr.frame_layout = strstr(name, "frame_controller") != NULL;
|
||||
host_logf(HOST_LOG_INFO, "xr: the %s hand's controller: %s", hand ? "right" : "left", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
|
||||
{
|
||||
struct halo_xr_hand *out = &frame->hands[hand];
|
||||
@@ -730,11 +946,8 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
{
|
||||
out->flags = flags_from_location(location.locationFlags);
|
||||
pose_from_xr(&out->grip, &location.pose);
|
||||
if (hand == HALO_XR_HAND_LEFT)
|
||||
{
|
||||
xr.left_grip = location.pose;
|
||||
xr.left_grip_valid = (out->flags & HALO_XR_POSE_VALID) != 0;
|
||||
}
|
||||
xr.grips[hand] = location.pose;
|
||||
xr.grips_valid[hand] = (out->flags & HALO_XR_POSE_VALID) != 0;
|
||||
}
|
||||
location.locationFlags = 0;
|
||||
if (XR_SUCCEEDED(xrLocateSpace(xr.hands[hand].aim_space, xr.local_space,
|
||||
@@ -757,7 +970,42 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
(action_boolean(xr.menu, hand) ? HALO_XR_BUTTON_MENU : 0) |
|
||||
(action_boolean(xr.thumbrest, hand) ? HALO_XR_BUTTON_THUMBREST : 0) |
|
||||
(out->trigger > 0.5f ? HALO_XR_BUTTON_TRIGGER : 0) |
|
||||
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0);
|
||||
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0) |
|
||||
(action_boolean(xr.view, hand) ? HALO_XR_BUTTON_VIEW : 0) |
|
||||
(action_boolean(xr.bumper, hand) ? HALO_XR_BUTTON_BUMPER : 0) |
|
||||
(action_boolean(xr.dpad[0], hand) ? HALO_XR_BUTTON_DPAD_UP : 0) |
|
||||
(action_boolean(xr.dpad[1], hand) ? HALO_XR_BUTTON_DPAD_DOWN : 0) |
|
||||
(action_boolean(xr.dpad[2], hand) ? HALO_XR_BUTTON_DPAD_LEFT : 0) |
|
||||
(action_boolean(xr.dpad[3], hand) ? HALO_XR_BUTTON_DPAD_RIGHT : 0);
|
||||
{
|
||||
/* the fingers' touch, and whether there is any */
|
||||
int thumb_active = 0, index_active = 0;
|
||||
|
||||
if (action_boolean_active(xr.thumb_touch, hand, &thumb_active))
|
||||
out->buttons |= HALO_XR_BUTTON_THUMB_TOUCH;
|
||||
if (action_boolean_active(xr.index_touch, hand, &index_active))
|
||||
out->buttons |= HALO_XR_BUTTON_INDEX_TOUCH;
|
||||
if (thumb_active)
|
||||
out->buttons |= HALO_XR_BUTTON_TOUCH_KNOWN;
|
||||
}
|
||||
}
|
||||
frame->controller_layout = xr.frame_layout ? HALO_XR_LAYOUT_FRAME : HALO_XR_LAYOUT_TOUCH;
|
||||
if (xr.frame_layout)
|
||||
{
|
||||
/* (the Frame's right X, Y and menu button: the game's X, Y and Start,
|
||||
which the Quest has on the left) */
|
||||
struct halo_xr_hand *left = &frame->hands[HALO_XR_HAND_LEFT];
|
||||
struct halo_xr_hand *right = &frame->hands[HALO_XR_HAND_RIGHT];
|
||||
|
||||
if (action_boolean(xr.button_x, HALO_XR_HAND_RIGHT))
|
||||
left->buttons |= HALO_XR_BUTTON_A;
|
||||
if (action_boolean(xr.button_y, HALO_XR_HAND_RIGHT))
|
||||
left->buttons |= HALO_XR_BUTTON_B;
|
||||
if (right->buttons & HALO_XR_BUTTON_MENU)
|
||||
{
|
||||
right->buttons &= ~HALO_XR_BUTTON_MENU;
|
||||
left->buttons |= HALO_XR_BUTTON_MENU;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -774,6 +1022,13 @@ static void refresh_rate_choose(float wanted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
{
|
||||
char line[128] = "";
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
snprintf(line + strlen(line), sizeof(line) - strlen(line), " %.0f", rates[index]);
|
||||
host_logf(HOST_LOG_INFO, "xr: refresh rates offered:%s Hz", line);
|
||||
}
|
||||
/* the highest rate not above the one wanted (0: the highest) */
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
@@ -790,10 +1045,18 @@ On the back of the left wrist, like a watch: behind the palm, where the
|
||||
grip pose is, and out of the back of the hand. In the left grip pose -Z
|
||||
runs to the fingers, +Y up the handle and -X out of the back of the hand;
|
||||
the layer faces -X with its top towards the elbow, as the wrist turned to
|
||||
the eyes reads it. Shown only while it is. */
|
||||
the eyes reads it. Shown only while it is. Left-handed (wrist_hand), on the
|
||||
right wrist, as the mirror image: there +X runs out of the back of the
|
||||
hand. */
|
||||
|
||||
#define WRIST_IMAGE_SIZE 512
|
||||
#define CROSSHAIR_IMAGE_SIZE 256
|
||||
/* the names layer: HALO_XR_MAXIMUM_NAMES slots of 4:1, one over another */
|
||||
#define NAMES_IMAGE_WIDTH 512
|
||||
#define NAMES_IMAGE_HEIGHT (NAMES_IMAGE_WIDTH / 4 * HALO_XR_MAXIMUM_NAMES)
|
||||
/* a name's slot, as the game draws it: wider than its image's 4:1 (the
|
||||
game's 640x480 drawn square): the height a slot shows at, to its width */
|
||||
#define NAME_SLOT_ASPECT (30.0f / 160.0f)
|
||||
#define WRIST_BEHIND_PALM 0.05f
|
||||
#define WRIST_DOWN_HANDLE 0.02f
|
||||
#define WRIST_ABOVE_HAND 0.04f
|
||||
@@ -824,26 +1087,32 @@ static void quaternion_rotate_vector(const XrQuaternionf *q, const XrVector3f *v
|
||||
|
||||
static int wrist_pose(const struct halo_xr_submit *submit, XrPosef *pose)
|
||||
{
|
||||
/* the layer's axes in the grip's: x = -Y, y = +Z, z = -X, so the
|
||||
layer's right and down, as it is read, are the grip's -Y and -Z */
|
||||
static const XrQuaternionf layer_in_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
|
||||
const XrVector3f offset = { -WRIST_ABOVE_HAND, -WRIST_DOWN_HANDLE - submit->wrist_right,
|
||||
WRIST_BEHIND_PALM - submit->wrist_down };
|
||||
/* the layer's axes in the left grip's: x = -Y, y = +Z, z = -X, so the
|
||||
layer's right and down, as it is read, are the grip's -Y and -Z; in the
|
||||
right grip's x = +Y, y = +Z, z = +X (the mirror image, read the right
|
||||
way round: its x turned back) */
|
||||
static const XrQuaternionf layer_in_left_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
|
||||
static const XrQuaternionf layer_in_right_grip = { 0.5f, 0.5f, 0.5f, 0.5f };
|
||||
int right = submit->wrist_hand == HALO_XR_HAND_RIGHT;
|
||||
const XrPosef *grip = &xr.grips[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT];
|
||||
const XrVector3f offset = { right ? WRIST_ABOVE_HAND : -WRIST_ABOVE_HAND,
|
||||
-WRIST_DOWN_HANDLE - submit->wrist_right, WRIST_BEHIND_PALM - submit->wrist_down };
|
||||
const XrVector3f layer_normal = { 0.0f, 0.0f, 1.0f };
|
||||
/* clockwise as read: about the layer's normal, negatively */
|
||||
float half_turn = -submit->wrist_turn * WRIST_PI / 360.0f;
|
||||
/* clockwise as read: about the layer's normal, negatively (the mirror
|
||||
image's the other way) */
|
||||
float half_turn = (right ? 1.0f : -1.0f) * submit->wrist_turn * WRIST_PI / 360.0f;
|
||||
XrQuaternionf turn = { 0.0f, 0.0f, sinf(half_turn), cosf(half_turn) };
|
||||
XrQuaternionf unturned;
|
||||
XrVector3f moved, normal, to_eyes;
|
||||
float distance;
|
||||
|
||||
if (!xr.left_grip_valid)
|
||||
if (!xr.grips_valid[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT])
|
||||
return 0;
|
||||
quaternion_rotate_vector(&xr.left_grip.orientation, &offset, &moved);
|
||||
pose->position.x = xr.left_grip.position.x + moved.x;
|
||||
pose->position.y = xr.left_grip.position.y + moved.y;
|
||||
pose->position.z = xr.left_grip.position.z + moved.z;
|
||||
quaternion_multiply(&xr.left_grip.orientation, &layer_in_grip, &unturned);
|
||||
quaternion_rotate_vector(&grip->orientation, &offset, &moved);
|
||||
pose->position.x = grip->position.x + moved.x;
|
||||
pose->position.y = grip->position.y + moved.y;
|
||||
pose->position.z = grip->position.z + moved.z;
|
||||
quaternion_multiply(&grip->orientation, right ? &layer_in_right_grip : &layer_in_left_grip, &unturned);
|
||||
quaternion_multiply(&unturned, &turn, &pose->orientation);
|
||||
|
||||
quaternion_rotate_vector(&pose->orientation, &layer_normal, &normal);
|
||||
@@ -889,6 +1158,12 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
/* the loader and the runtime keep the activity */
|
||||
activity = (*environment)->NewGlobalRef(environment, activity);
|
||||
|
||||
/* (The Steam Frame lists an implicit API layer of its own for Vulkan
|
||||
apps, XR_APILAYER_VALVE_fdm_injection, whose library an app cannot load
|
||||
from its namespace, failing xrCreateInstance; the Android loader reads
|
||||
no variable to turn it off, so the app carries a pass-through layer
|
||||
under that library's name: port/android/xr_layer/fdm_injection_stub.c) */
|
||||
|
||||
if (!XR_SUCCEEDED(xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR",
|
||||
(PFN_xrVoidFunction *)&initialize_loader)) || !initialize_loader)
|
||||
{
|
||||
@@ -916,6 +1191,23 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
extensions[index].next = NULL;
|
||||
}
|
||||
xrEnumerateInstanceExtensionProperties(NULL, extension_count, &extension_count, extensions);
|
||||
{
|
||||
/* (the runtime's extensions, in the log: what a headset offers) */
|
||||
char line[512] = "";
|
||||
|
||||
for (index = 0; index < extension_count; index++)
|
||||
{
|
||||
if (strlen(line) + strlen(extensions[index].extensionName) + 2 >= sizeof(line))
|
||||
{
|
||||
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
|
||||
line[0] = 0;
|
||||
}
|
||||
strcat(line, " ");
|
||||
strcat(line, extensions[index].extensionName);
|
||||
}
|
||||
if (line[0])
|
||||
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
|
||||
}
|
||||
if (!has_extension(extensions, extension_count, XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME))
|
||||
{
|
||||
host_logf(HOST_LOG_ERROR, "xr: the runtime has no OpenGL ES support");
|
||||
@@ -927,9 +1219,15 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
xr.has_performance_settings = has_extension(extensions, extension_count, XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME);
|
||||
if (xr.has_performance_settings)
|
||||
enabled[enabled_count++] = XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME;
|
||||
xr.has_frame_controller = has_extension(extensions, extension_count, "XR_VALVE_frame_controller_interaction");
|
||||
if (xr.has_frame_controller)
|
||||
enabled[enabled_count++] = "XR_VALVE_frame_controller_interaction";
|
||||
xr.has_refresh_rate = has_extension(extensions, extension_count, XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME);
|
||||
if (xr.has_refresh_rate)
|
||||
enabled[enabled_count++] = XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME;
|
||||
xr.has_metrics = has_extension(extensions, extension_count, XR_META_PERFORMANCE_METRICS_EXTENSION_NAME);
|
||||
if (xr.has_metrics)
|
||||
enabled[enabled_count++] = XR_META_PERFORMANCE_METRICS_EXTENSION_NAME;
|
||||
|
||||
android.applicationVM = machine;
|
||||
android.applicationActivity = activity;
|
||||
@@ -954,6 +1252,23 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
}
|
||||
xrGetSystemProperties(xr.instance, xr.system, &properties);
|
||||
strncpy(xr.system_name, properties.systemName, sizeof(xr.system_name) - 1);
|
||||
{
|
||||
/* (SteamVR: the quads' alpha from their colour; HALO_XR_QUAD_ALPHA
|
||||
"colour" or "keep" says otherwise, for trying) */
|
||||
XrInstanceProperties instance = { XR_TYPE_INSTANCE_PROPERTIES };
|
||||
const char *choice = getenv("HALO_XR_QUAD_ALPHA");
|
||||
|
||||
if (XR_SUCCEEDED(xrGetInstanceProperties(xr.instance, &instance)))
|
||||
{
|
||||
xr.quad_alpha_from_colour = strstr(instance.runtimeName, "SteamVR") != NULL;
|
||||
xr.steamvr = xr.quad_alpha_from_colour;
|
||||
host_logf(HOST_LOG_INFO, "xr: runtime %s", instance.runtimeName);
|
||||
}
|
||||
if (choice)
|
||||
xr.quad_alpha_from_colour = !strcmp(choice, "colour");
|
||||
if (xr.quad_alpha_from_colour)
|
||||
host_logf(HOST_LOG_INFO, "xr: the overlays' alpha from their colour");
|
||||
}
|
||||
|
||||
views[0].type = views[1].type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
|
||||
views[0].next = views[1].next = NULL;
|
||||
@@ -1032,8 +1347,15 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
|
||||
{
|
||||
int size = layer == HALO_XR_LAYER_WRIST || layer == HALO_XR_LAYER_SCOPE ? WRIST_IMAGE_SIZE :
|
||||
layer == HALO_XR_LAYER_CROSSHAIR ? CROSSHAIR_IMAGE_SIZE : 0;
|
||||
int width = size ? size : xr.eye_width;
|
||||
int height = size ? size : xr.eye_height;
|
||||
|
||||
if (!swapchain_create(&xr.swapchains[layer], size ? size : xr.eye_width, size ? size : xr.eye_height))
|
||||
if (layer == HALO_XR_LAYER_NAMES)
|
||||
{
|
||||
width = NAMES_IMAGE_WIDTH;
|
||||
height = NAMES_IMAGE_HEIGHT;
|
||||
}
|
||||
if (!swapchain_create(&xr.swapchains[layer], width, height))
|
||||
return 0;
|
||||
}
|
||||
if (!copy_initialize() || !actions_initialize())
|
||||
@@ -1053,6 +1375,29 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
|
||||
xr.get_refresh_rate(xr.session, &info->refresh_rate);
|
||||
}
|
||||
|
||||
/* (the runtime's GL binding brings contexts of its own, with their own
|
||||
trace markers: host_gl.c) */
|
||||
host_gl_quiet_trace_markers();
|
||||
|
||||
if (xr.has_metrics)
|
||||
{
|
||||
PFN_xrEnumeratePerformanceMetricsCounterPathsMETA enumerate = NULL;
|
||||
PFN_xrSetPerformanceMetricsStateMETA set_state = NULL;
|
||||
XrPerformanceMetricsStateMETA state = { XR_TYPE_PERFORMANCE_METRICS_STATE_META };
|
||||
|
||||
xrGetInstanceProcAddr(xr.instance, "xrEnumeratePerformanceMetricsCounterPathsMETA",
|
||||
(PFN_xrVoidFunction *)&enumerate);
|
||||
xrGetInstanceProcAddr(xr.instance, "xrSetPerformanceMetricsStateMETA", (PFN_xrVoidFunction *)&set_state);
|
||||
xrGetInstanceProcAddr(xr.instance, "xrQueryPerformanceMetricsCounterMETA",
|
||||
(PFN_xrVoidFunction *)&xr.query_metric);
|
||||
state.enabled = XR_TRUE;
|
||||
xr.has_metrics = enumerate && set_state && xr.query_metric &&
|
||||
XR_SUCCEEDED(enumerate(xr.instance, 16, &xr.metric_count, xr.metric_paths)) &&
|
||||
XR_SUCCEEDED(set_state(xr.session, &state));
|
||||
host_logf(HOST_LOG_INFO, "xr: performance metrics %s (%u counters)", xr.has_metrics ? "on" : "off",
|
||||
xr.has_metrics ? xr.metric_count : 0);
|
||||
}
|
||||
|
||||
if (xr.has_performance_settings)
|
||||
{
|
||||
/* the game's one busy thread and its GPU work both need the clocks
|
||||
@@ -1069,6 +1414,16 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* (deferred on SteamVR; HALO_XR_DEFERRED "1" or "0" says otherwise:
|
||||
debug.xr_deferred_copies, host_main.c) */
|
||||
const char *choice = getenv("HALO_XR_DEFERRED");
|
||||
|
||||
xr.deferred = choice && *choice ? atoi(choice) != 0 : xr.steamvr;
|
||||
host_logf(HOST_LOG_INFO, "xr: layer copies %s", xr.deferred ?
|
||||
"deferred (no waits for the GL thread or the GPU in the frame)" : "restored and released one by one");
|
||||
}
|
||||
|
||||
info->eye_width = xr.eye_width;
|
||||
info->eye_height = xr.eye_height;
|
||||
info->overlay_width = xr.eye_width;
|
||||
@@ -1205,7 +1560,7 @@ int host_xr_begin_frame(struct halo_xr_frame *frame)
|
||||
pose_from_xr(&frame->head, &head.pose);
|
||||
xr.head = head.pose;
|
||||
}
|
||||
xr.left_grip_valid = 0;
|
||||
xr.grips_valid[HALO_XR_HAND_LEFT] = xr.grips_valid[HALO_XR_HAND_RIGHT] = 0;
|
||||
if (xr.focused)
|
||||
hands_read(frame);
|
||||
return 1;
|
||||
@@ -1232,8 +1587,30 @@ int host_xr_copy_layer_array(int layer, uint32_t texture, int array_layer)
|
||||
|
||||
/* copies part of a render target (left, top, right, bottom, 0..1 from its
|
||||
top left) into a layer's image */
|
||||
static double seconds_now(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (double)now.tv_sec + (double)now.tv_nsec * 1e-9;
|
||||
}
|
||||
|
||||
static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
|
||||
float bottom);
|
||||
|
||||
int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
|
||||
float bottom)
|
||||
{
|
||||
double start = xr.has_metrics ? seconds_now() : 0.0;
|
||||
int copied = copy_layer_region(layer, texture, clear_alpha, left, top, right, bottom);
|
||||
|
||||
if (xr.has_metrics && layer >= 0 && layer < HALO_XR_LAYER_COUNT)
|
||||
xr.copy_seconds[layer] += seconds_now() - start;
|
||||
return copied;
|
||||
}
|
||||
|
||||
static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
|
||||
float bottom)
|
||||
{
|
||||
struct xr_swapchain *swapchain;
|
||||
XrSwapchainImageAcquireInfo acquire = { XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO };
|
||||
@@ -1245,17 +1622,27 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
|
||||
return 0;
|
||||
swapchain = &xr.swapchains[layer];
|
||||
if (swapchain->filled)
|
||||
return 1;
|
||||
if (!xr_ok(xrAcquireSwapchainImage(swapchain->handle, &acquire, &swapchain->acquired_index),
|
||||
"xrAcquireSwapchainImage"))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return xr.deferred ? 2 : 1;
|
||||
wait.timeout = XR_INFINITE_DURATION;
|
||||
if (!xr_ok(xrWaitSwapchainImage(swapchain->handle, &wait), "xrWaitSwapchainImage"))
|
||||
return 0;
|
||||
{
|
||||
double start = xr.has_metrics ? seconds_now() : 0.0;
|
||||
|
||||
gl_save(&saved);
|
||||
if (!xr_ok(xrAcquireSwapchainImage(swapchain->handle, &acquire, &swapchain->acquired_index),
|
||||
"xrAcquireSwapchainImage"))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (!xr_ok(xrWaitSwapchainImage(swapchain->handle, &wait), "xrWaitSwapchainImage"))
|
||||
return 0;
|
||||
if (xr.has_metrics)
|
||||
xr.acquire_seconds += seconds_now() - start;
|
||||
}
|
||||
|
||||
/* (deferred: nothing read back; the game sets its state again) */
|
||||
if (xr.deferred)
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
else
|
||||
gl_save(&saved);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, swapchain->framebuffers[swapchain->acquired_index]);
|
||||
glViewport(0, 0, swapchain->width, swapchain->height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
@@ -1303,6 +1690,8 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
|
||||
glUniform1i(xr.source_uniform, 0);
|
||||
glUniform4f(xr.rect_uniform, left, top, right, bottom);
|
||||
glUniform1f(xr.cut_disc_uniform, clear_alpha == HALO_XR_COPY_DISC ? 1.0f : 0.0f);
|
||||
glUniform1f(xr.alpha_from_colour_uniform, xr.quad_alpha_from_colour && clear_alpha == HALO_XR_COPY_ALPHA &&
|
||||
layer != HALO_XR_LAYER_EYE_LEFT && layer != HALO_XR_LAYER_EYE_RIGHT ? 1.0f : 0.0f);
|
||||
if ((layer == HALO_XR_LAYER_EYE_LEFT || layer == HALO_XR_LAYER_EYE_RIGHT) && xr.vignette > 0.001f)
|
||||
{
|
||||
const XrFovf *fov = &xr.views[layer - HALO_XR_LAYER_EYE_LEFT].fov;
|
||||
@@ -1318,13 +1707,22 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
|
||||
glBindVertexArray(xr.vertex_array);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glBindSampler(0, xr.sampler);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, &swizzle_r);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, &swizzle_b);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, swizzle_r);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, swizzle_b);
|
||||
if (xr.deferred)
|
||||
{
|
||||
/* (the game's render targets, all a copy reads, have no
|
||||
swizzle: only its uploaded textures get one, xbox_textures.c) */
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, &swizzle_r);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, &swizzle_b);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, swizzle_r);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, swizzle_b);
|
||||
}
|
||||
if (clear_alpha == HALO_XR_COPY_OPAQUE)
|
||||
{
|
||||
/* an opaque layer: the game's alpha channel holds other things */
|
||||
@@ -1338,13 +1736,95 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
|
||||
glClearColor(0.0f, 0.0f, 0.0f, clear_alpha ? 1.0f : 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
swapchain->filled = 1;
|
||||
if (xr.deferred)
|
||||
{
|
||||
swapchain->held = 1;
|
||||
/* (the eyes, most of the frame's work, on to the GPU while the
|
||||
game draws the rest) */
|
||||
if (layer == HALO_XR_LAYER_EYE_RIGHT)
|
||||
glFlush();
|
||||
return 2;
|
||||
}
|
||||
gl_restore(&saved);
|
||||
|
||||
xrReleaseSwapchainImage(swapchain->handle, &release);
|
||||
swapchain->filled = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* deferred: the images the frame's copies kept, released before it ends */
|
||||
static void release_held(void)
|
||||
{
|
||||
double start = xr.has_metrics ? seconds_now() : 0.0;
|
||||
int layer;
|
||||
|
||||
for (layer = 0; layer < HALO_XR_LAYER_COUNT; layer++)
|
||||
{
|
||||
XrSwapchainImageReleaseInfo release = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
|
||||
struct xr_swapchain *swapchain = &xr.swapchains[layer];
|
||||
|
||||
if (!swapchain->held)
|
||||
continue;
|
||||
swapchain->held = 0;
|
||||
xr_ok(xrReleaseSwapchainImage(swapchain->handle, &release), "xrReleaseSwapchainImage");
|
||||
}
|
||||
if (xr.has_metrics)
|
||||
xr.release_seconds += seconds_now() - start;
|
||||
}
|
||||
|
||||
/* The Steam Frame: Android in a container, without XR_EXT_performance_settings
|
||||
to ask for the clocks, and the scheduler free to put the game's one busy
|
||||
thread, which simulates and draws each frame, on a slow core (its two
|
||||
2.3 GHz ones) now and then: frames late to the compositor, shown twice,
|
||||
the picture doubled as the head turns. The thread that ends the frames is
|
||||
kept on the cores no slower than nine tenths of the fastest. */
|
||||
static void xr_fast_cores(void)
|
||||
{
|
||||
cpu_set_t set;
|
||||
long fastest = 0, speeds[CPU_SETSIZE];
|
||||
int cpu, count = 0;
|
||||
char line[128] = "";
|
||||
|
||||
for (cpu = 0; cpu < CPU_SETSIZE && cpu < 64; cpu++)
|
||||
{
|
||||
char path[96];
|
||||
FILE *file;
|
||||
|
||||
speeds[cpu] = 0;
|
||||
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", cpu);
|
||||
file = fopen(path, "r");
|
||||
if (!file)
|
||||
continue;
|
||||
if (fscanf(file, "%ld", &speeds[cpu]) != 1)
|
||||
speeds[cpu] = 0;
|
||||
fclose(file);
|
||||
if (speeds[cpu] > fastest)
|
||||
fastest = speeds[cpu];
|
||||
}
|
||||
if (!fastest)
|
||||
{
|
||||
host_logf(HOST_LOG_INFO, "xr: the cores' speeds unknown: the frames' thread left where it runs");
|
||||
return;
|
||||
}
|
||||
CPU_ZERO(&set);
|
||||
for (cpu = 0; cpu < CPU_SETSIZE && cpu < 64; cpu++)
|
||||
{
|
||||
if (speeds[cpu] && speeds[cpu] * 10 >= fastest * 9)
|
||||
{
|
||||
char item[8];
|
||||
|
||||
CPU_SET(cpu, &set);
|
||||
count++;
|
||||
snprintf(item, sizeof(item), " %d", cpu);
|
||||
strcat(line, item);
|
||||
}
|
||||
}
|
||||
if (count && sched_setaffinity(0, sizeof(set), &set) == 0)
|
||||
host_logf(HOST_LOG_INFO, "xr: the frames' thread on the fast cores:%s (fastest %ld MHz)", line, fastest / 1000);
|
||||
else
|
||||
host_logf(HOST_LOG_INFO, "xr: the frames' thread could not be kept on the fast cores");
|
||||
}
|
||||
|
||||
int host_xr_end_frame(const struct halo_xr_submit *submit)
|
||||
{
|
||||
XrFrameEndInfo end = { XR_TYPE_FRAME_END_INFO };
|
||||
@@ -1354,13 +1834,15 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
|
||||
XrCompositionLayerQuad wrist = { XR_TYPE_COMPOSITION_LAYER_QUAD };
|
||||
XrCompositionLayerQuad scope = { XR_TYPE_COMPOSITION_LAYER_QUAD };
|
||||
XrCompositionLayerQuad crosshair = { XR_TYPE_COMPOSITION_LAYER_QUAD };
|
||||
const XrCompositionLayerBaseHeader *layers[5];
|
||||
XrCompositionLayerQuad names[HALO_XR_MAXIMUM_NAMES];
|
||||
const XrCompositionLayerBaseHeader *layers[5 + HALO_XR_MAXIMUM_NAMES];
|
||||
uint32_t layer_count = 0;
|
||||
int eye;
|
||||
|
||||
if (!xr.frame_begun)
|
||||
return 0;
|
||||
xr.frame_begun = 0;
|
||||
release_held();
|
||||
|
||||
if (xr.frame_state.shouldRender && submit->eyes && xr.views_valid &&
|
||||
xr.swapchains[HALO_XR_LAYER_EYE_LEFT].filled && xr.swapchains[HALO_XR_LAYER_EYE_RIGHT].filled)
|
||||
@@ -1458,11 +1940,111 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
|
||||
layers[layer_count++] = (const XrCompositionLayerBaseHeader *)&crosshair;
|
||||
}
|
||||
|
||||
if (xr.frame_state.shouldRender && submit->name_count > 0 && xr.swapchains[HALO_XR_LAYER_NAMES].filled)
|
||||
{
|
||||
struct xr_swapchain *swapchain = &xr.swapchains[HALO_XR_LAYER_NAMES];
|
||||
int slot_height = swapchain->height / HALO_XR_MAXIMUM_NAMES;
|
||||
int name;
|
||||
|
||||
for (name = 0; name < submit->name_count && name < HALO_XR_MAXIMUM_NAMES; name++)
|
||||
{
|
||||
XrCompositionLayerQuad *quad_name = &names[name];
|
||||
|
||||
memset(quad_name, 0, sizeof(*quad_name));
|
||||
quad_name->type = XR_TYPE_COMPOSITION_LAYER_QUAD;
|
||||
quad_name->layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
|
||||
quad_name->space = xr.local_space;
|
||||
quad_name->eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
quad_name->subImage.swapchain = swapchain->handle;
|
||||
/* (slot 0 at the image's top: GL's rows run bottom up) */
|
||||
quad_name->subImage.imageRect.offset.x = 0;
|
||||
quad_name->subImage.imageRect.offset.y = swapchain->height - (name + 1) * slot_height;
|
||||
quad_name->subImage.imageRect.extent.width = swapchain->width;
|
||||
quad_name->subImage.imageRect.extent.height = slot_height;
|
||||
quad_name->size.width = submit->name_widths[name];
|
||||
quad_name->size.height = submit->name_widths[name] * NAME_SLOT_ASPECT;
|
||||
pose_to_xr(&quad_name->pose, &submit->name_poses[name]);
|
||||
layers[layer_count++] = (const XrCompositionLayerBaseHeader *)quad_name;
|
||||
}
|
||||
}
|
||||
|
||||
if (xr.steamvr && !xr.cores_pinned)
|
||||
{
|
||||
xr.cores_pinned = 1;
|
||||
if (!getenv("HALO_XR_ANY_CORE"))
|
||||
xr_fast_cores();
|
||||
}
|
||||
/* (every five seconds, the compositor's counters, and the core the
|
||||
frames' thread is on) */
|
||||
if (xr.has_metrics)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
if (now.tv_sec - xr.metrics_logged.tv_sec >= 5)
|
||||
{
|
||||
char line[768] = "";
|
||||
uint32_t counter;
|
||||
|
||||
xr.metrics_logged = now;
|
||||
for (counter = 0; counter < xr.metric_count && counter < 16; counter++)
|
||||
{
|
||||
XrPerformanceMetricsCounterMETA value = { XR_TYPE_PERFORMANCE_METRICS_COUNTER_META };
|
||||
char name[XR_MAX_PATH_LENGTH];
|
||||
uint32_t length = 0;
|
||||
char item[96];
|
||||
|
||||
if (!XR_SUCCEEDED(xr.query_metric(xr.session, xr.metric_paths[counter], &value)) ||
|
||||
!XR_SUCCEEDED(xrPathToString(xr.instance, xr.metric_paths[counter], sizeof(name), &length, name)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
snprintf(item, sizeof(item), " %s=%g", strrchr(name, '/') ? strrchr(name, '/') + 1 : name,
|
||||
(value.counterFlags & XR_PERFORMANCE_METRICS_COUNTER_FLOAT_VALUE_VALID_BIT_META) ?
|
||||
(double)value.floatValue : (double)value.uintValue);
|
||||
if (strlen(line) + strlen(item) < sizeof(line))
|
||||
strcat(line, item);
|
||||
}
|
||||
host_logf(HOST_LOG_INFO, "xr: metrics%s, on core %d", line, sched_getcpu());
|
||||
if (xr.timed_frames)
|
||||
{
|
||||
double frames = (double)xr.timed_frames;
|
||||
char copies[256] = "";
|
||||
int timed;
|
||||
|
||||
for (timed = 0; timed < HALO_XR_LAYER_COUNT; timed++)
|
||||
{
|
||||
char item[24];
|
||||
|
||||
snprintf(item, sizeof(item), " %d:%.2f", timed, xr.copy_seconds[timed] * 1e3 / frames);
|
||||
strcat(copies, item);
|
||||
xr.copy_seconds[timed] = 0.0;
|
||||
}
|
||||
host_logf(HOST_LOG_INFO, "xr: per frame ms: copies (layer:ms)%s; of them acquiring images %.2f; "
|
||||
"releasing them at the end %.2f; xrEndFrame %.2f (copies %s)", copies,
|
||||
xr.acquire_seconds * 1e3 / frames, xr.release_seconds * 1e3 / frames, xr.end_seconds * 1e3 / frames,
|
||||
xr.deferred ? "deferred" : "one by one");
|
||||
xr.acquire_seconds = xr.release_seconds = xr.end_seconds = 0.0;
|
||||
xr.timed_frames = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
end.displayTime = xr.frame_state.predictedDisplayTime;
|
||||
end.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||
end.layerCount = layer_count;
|
||||
end.layers = layers;
|
||||
return xr_ok(xrEndFrame(xr.session, &end), "xrEndFrame");
|
||||
{
|
||||
double start = xr.has_metrics ? seconds_now() : 0.0;
|
||||
int ended = xr_ok(xrEndFrame(xr.session, &end), "xrEndFrame");
|
||||
|
||||
if (xr.has_metrics)
|
||||
{
|
||||
xr.end_seconds += seconds_now() - start;
|
||||
xr.timed_frames++;
|
||||
}
|
||||
return ended;
|
||||
}
|
||||
}
|
||||
|
||||
void host_xr_set_vignette(float strength)
|
||||
@@ -1470,6 +2052,34 @@ void host_xr_set_vignette(float strength)
|
||||
xr.vignette = strength < 0.0f ? 0.0f : strength > 1.0f ? 1.0f : strength;
|
||||
}
|
||||
|
||||
/* the VR options' refresh rates: those the runtime offers (none where it
|
||||
lets none be chosen, as SteamVR, whose rate is SteamOS's), and the one it
|
||||
runs at, by XR_FB_display_refresh_rate or else by its frames' period */
|
||||
int host_xr_refresh_rates(float *rates, int maximum, float *current)
|
||||
{
|
||||
float offered[16];
|
||||
uint32_t count = 0, index;
|
||||
|
||||
*current = 0.0f;
|
||||
if (!xr.initialized)
|
||||
return 0;
|
||||
if (xr.has_refresh_rate)
|
||||
xr.get_refresh_rate(xr.session, current);
|
||||
if (*current <= 0.0f && xr.frame_state.predictedDisplayPeriod > 0)
|
||||
*current = 1e9f / (float)xr.frame_state.predictedDisplayPeriod;
|
||||
if (!xr.has_refresh_rate || !XR_SUCCEEDED(xr.enumerate_refresh_rates(xr.session, 16, &count, offered)))
|
||||
return 0;
|
||||
for (index = 0; index < count && (int)index < maximum; index++)
|
||||
rates[index] = offered[index];
|
||||
return (int)index;
|
||||
}
|
||||
|
||||
void host_xr_set_refresh_rate(float wanted)
|
||||
{
|
||||
if (xr.initialized)
|
||||
refresh_rate_choose(wanted);
|
||||
}
|
||||
|
||||
void host_xr_haptic(int hand, float amplitude, float seconds)
|
||||
{
|
||||
XrHapticActionInfo information = { XR_TYPE_HAPTIC_ACTION_INFO };
|
||||
|
||||
@@ -38,6 +38,8 @@ host_sdl_gamepad_axis
|
||||
host_sdl_gamepad_button
|
||||
host_sdl_gamepad_type
|
||||
host_sdl_rumble_gamepad
|
||||
host_sdl_scancode_name
|
||||
host_sdl_scancode_from_name
|
||||
host_sdl_open_audio_stream
|
||||
host_sdl_put_audio_stream_data
|
||||
host_sdl_resume_audio_stream_device
|
||||
@@ -59,6 +61,8 @@ host_xr_end_frame
|
||||
host_xr_haptic
|
||||
host_xr_set_vignette
|
||||
host_xr_copy_layer_array
|
||||
host_xr_refresh_rates
|
||||
host_xr_set_refresh_rate
|
||||
host_android_path
|
||||
|
||||
host_errno
|
||||
@@ -24,10 +24,13 @@ enum
|
||||
HALO_XR_LAYER_EYE_LEFT = 0,
|
||||
HALO_XR_LAYER_EYE_RIGHT = 1,
|
||||
HALO_XR_LAYER_OVERLAY = 2, /* HUD, menus, cinematics: a flat quad */
|
||||
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the left wrist */
|
||||
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the free hand's wrist */
|
||||
HALO_XR_LAYER_SCOPE = 4, /* the zoomed view, a disc over the weapon */
|
||||
HALO_XR_LAYER_CROSSHAIR = 5, /* the weapon's crosshair, where it aims */
|
||||
HALO_XR_LAYER_COUNT = 6,
|
||||
HALO_XR_LAYER_NAMES = 6, /* players' names, each a quad over a head */
|
||||
HALO_XR_LAYER_COUNT = 7,
|
||||
/* the names layer's slots, one a name, stacked from its top */
|
||||
HALO_XR_MAXIMUM_NAMES = 8,
|
||||
|
||||
/* host_xr_copy_layer's clear_alpha: the source's alpha kept, made
|
||||
opaque, or cut to a disc (premultiplied) */
|
||||
@@ -50,6 +53,29 @@ enum
|
||||
HALO_XR_BUTTON_TRIGGER = 1 << 4, /* trigger past its click point */
|
||||
HALO_XR_BUTTON_SQUEEZE = 1 << 5, /* grip past its click point */
|
||||
HALO_XR_BUTTON_THUMBREST = 1 << 6, /* thumb resting (touch) */
|
||||
/* the Steam Frame's controllers' (HALO_XR_LAYOUT_FRAME): the left one's
|
||||
View button and D-pad, either one's bumper (their right one's X, Y and
|
||||
menu button come as the left hand's A, B and MENU: the game's X, Y and
|
||||
Start, as on the Quest) */
|
||||
HALO_XR_BUTTON_VIEW = 1 << 7,
|
||||
HALO_XR_BUTTON_BUMPER = 1 << 8,
|
||||
HALO_XR_BUTTON_DPAD_UP = 1 << 9,
|
||||
HALO_XR_BUTTON_DPAD_DOWN = 1 << 10,
|
||||
HALO_XR_BUTTON_DPAD_LEFT = 1 << 11,
|
||||
HALO_XR_BUTTON_DPAD_RIGHT = 1 << 12,
|
||||
/* the fingers' touch (the hands drawn: vr.c): the thumb on the stick, a
|
||||
button or the rest; the index finger on the trigger; and whether the
|
||||
controller tells the thumb's at all */
|
||||
HALO_XR_BUTTON_THUMB_TOUCH = 1 << 13,
|
||||
HALO_XR_BUTTON_INDEX_TOUCH = 1 << 14,
|
||||
HALO_XR_BUTTON_TOUCH_KNOWN = 1 << 15,
|
||||
};
|
||||
|
||||
/* the controllers' layout, for what the game tells the player */
|
||||
enum
|
||||
{
|
||||
HALO_XR_LAYOUT_TOUCH = 0, /* the Quest's: X, Y and menu on the left */
|
||||
HALO_XR_LAYOUT_FRAME = 1, /* the Steam Frame's: A, B, X, Y and menu on the right, a D-pad on the left */
|
||||
};
|
||||
|
||||
/* halo_xr_hand.flags, halo_xr_view.flags, halo_xr_frame.head_flags */
|
||||
@@ -109,6 +135,7 @@ struct halo_xr_frame
|
||||
struct halo_xr_pose head;
|
||||
struct halo_xr_view views[HALO_XR_EYE_COUNT];
|
||||
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
|
||||
int controller_layout; /* HALO_XR_LAYOUT_* */
|
||||
};
|
||||
|
||||
/* halo_xr_submit.overlay_mode */
|
||||
@@ -128,15 +155,18 @@ struct halo_xr_submit
|
||||
float overlay_width; /* metres; the height follows from 4:3 */
|
||||
/* the screen's placement in LOCAL space (HALO_XR_OVERLAY_SCREEN) */
|
||||
struct halo_xr_pose screen;
|
||||
/* the wrist layer was filled: shown on the back of the left wrist,
|
||||
this wide (metres), while the wrist is turned to the eyes */
|
||||
/* the wrist layer was filled: shown on the back of the left wrist (of
|
||||
the right one, left-handed: wrist_hand, a HALO_XR_HAND_*), this wide
|
||||
(metres), while the wrist is turned to the eyes */
|
||||
int wrist;
|
||||
float wrist_size;
|
||||
/* its placement as the wrist turned to the eyes reads it: turned this
|
||||
many degrees clockwise, and moved this far right and down (metres) */
|
||||
/* its placement as the left wrist turned to the eyes reads it: turned
|
||||
this many degrees clockwise, and moved this far right and down (metres;
|
||||
on the right wrist, the mirror image) */
|
||||
float wrist_turn;
|
||||
float wrist_right;
|
||||
float wrist_down;
|
||||
int wrist_hand;
|
||||
/* the scope layer was filled: a disc this wide (metres), here in LOCAL
|
||||
space, facing back along the weapon */
|
||||
int scope;
|
||||
@@ -147,6 +177,12 @@ struct halo_xr_submit
|
||||
int crosshair;
|
||||
float crosshair_size;
|
||||
struct halo_xr_pose crosshair_pose;
|
||||
/* the names layer was filled: its first name_count slots, each shown
|
||||
this wide (metres, a slot's height following from its shape), here in
|
||||
LOCAL space, facing the eyes */
|
||||
int name_count;
|
||||
float name_widths[HALO_XR_MAXIMUM_NAMES];
|
||||
struct halo_xr_pose name_poses[HALO_XR_MAXIMUM_NAMES];
|
||||
};
|
||||
|
||||
#endif
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# Halo VR on the Steam Frame: Lepton runs the APK in a container of its own,
|
||||
# and installs it anew whenever the APK is newer than the installed one
|
||||
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/HaloCE.
|
||||
# (a container of its own, not the shortcut's id: with that, SteamVR's
|
||||
# client in the container waited on xrCreateInstance)
|
||||
export SteamAppId="${HALO_LEPTON_APP_ID:-4290000001}"
|
||||
# (what Steam gives an Android title of its own, and a non-Steam game not:
|
||||
# the app's data in the container links to the compat data path's
|
||||
# internal/, a dangling link without it; and the shader paths: Lepton reads
|
||||
# the dump path against the other)
|
||||
export STEAM_COMPAT_DATA_PATH="$HOME/.local/share/Steam/steamapps/compatdata/$SteamAppId"
|
||||
export STEAM_COMPAT_SHADER_PATH="$HOME/.local/share/Steam/steamapps/shadercache/$SteamAppId"
|
||||
mkdir -p "$STEAM_COMPAT_DATA_PATH/internal" "$STEAM_COMPAT_SHADER_PATH"
|
||||
unset STEAM_FOSSILIZE_DUMP_PATH LD_PRELOAD
|
||||
LEPTON="$HOME/.local/share/Steam/steamapps/common/Lepton/lepton"
|
||||
APK="$HOME/Documents/HaloCE/HaloVR.apk"
|
||||
LOG="$HOME/Documents/HaloCE/launcher.log"
|
||||
# (written by the game when the player quits it: no start again)
|
||||
QUIT="$HOME/Documents/HaloCE/quit_requested.txt"
|
||||
rm -f "$QUIT"
|
||||
CONTEXT="steamlaunch-$SteamAppId"
|
||||
|
||||
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
|
||||
log "start (pid $$, parent $PPID)"
|
||||
|
||||
# Steam's "Exit game" ends the game's processes as Steam knows them (this
|
||||
# script, under Steam's reaper), not the container, which podman runs: a
|
||||
# signal here is passed on to Lepton, and a watcher of its own takes the
|
||||
# container down once this script is gone (killed)
|
||||
stopping=0
|
||||
child=0
|
||||
stop()
|
||||
{
|
||||
log "signal $1"
|
||||
stopping=1
|
||||
[ "$child" -gt 0 ] && kill -TERM "$child" 2>/dev/null
|
||||
}
|
||||
trap 'stop TERM' TERM
|
||||
trap 'stop INT' INT
|
||||
trap 'stop HUP' HUP
|
||||
|
||||
script=$$
|
||||
setsid bash -c '
|
||||
while kill -0 '"$script"' 2>/dev/null; do sleep 1; done
|
||||
echo "$(date "+%F %T") watcher: the game was ended, so its container" >> "'"$LOG"'"
|
||||
"'"$LEPTON"'" kill "'"$CONTEXT"'" >/dev/null 2>&1
|
||||
' </dev/null >/dev/null 2>&1 &
|
||||
|
||||
# the game's log while it runs (Lepton's logcat of its container), for
|
||||
# looking into problems: logcat.txt, anew each start
|
||||
LOGCAT="$HOME/Documents/HaloCE/logcat.txt"
|
||||
: > "$LOGCAT"
|
||||
setsid bash -c '
|
||||
while kill -0 '"$script"' 2>/dev/null; do
|
||||
"'"$LEPTON"'" logcat "'"$CONTEXT"'" 2>/dev/null | grep -v "^ERROR: \|^steamlaunch-\|^$" >> "'"$LOGCAT"'"
|
||||
sleep 3
|
||||
done
|
||||
' </dev/null >/dev/null 2>&1 &
|
||||
|
||||
run()
|
||||
{
|
||||
"$LEPTON" waitforexitandrun -- "$APK" &
|
||||
child=$!
|
||||
# (wait returns early when a signal comes: until Lepton is gone)
|
||||
while kill -0 "$child" 2>/dev/null; do
|
||||
wait "$child"
|
||||
done
|
||||
child=0
|
||||
}
|
||||
|
||||
# (a new APK is only installed on its first start, not started; and a start
|
||||
# right after a game that was ended hard can fail in SteamVR, which then
|
||||
# needs a moment: again, up to three times, while it ends within 30 s)
|
||||
for attempt in 1 2 3; do
|
||||
started=$SECONDS
|
||||
run
|
||||
if (( stopping || SECONDS - started >= 30 )) || [ -e "$QUIT" ]; then
|
||||
break
|
||||
fi
|
||||
log "ended within $((SECONDS - started)) s: again"
|
||||
sleep 5
|
||||
done
|
||||
rm -f "$QUIT"
|
||||
log "end"
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
FDM_INJECTION_STUB.C
|
||||
|
||||
An OpenXR API layer that does nothing, named as the Steam Frame's
|
||||
XR_APILAYER_VALVE_fdm_injection names its library
|
||||
(libVkLayer_VALVE_fdm_injection.so). The Frame lists that layer as implicit
|
||||
for every OpenXR app (/vendor/etc/openxr/1/api_layers/implicit.d), but an
|
||||
Android app (in Lepton) cannot load the system's library from its own
|
||||
namespace, and the Khronos loader then fails xrCreateInstance altogether;
|
||||
nor does the Android loader read the variable that would turn the layer
|
||||
off. With this one in the app's libraries the loader finds a layer of that
|
||||
name, which hands every call on to the next. (The real layer adds fragment
|
||||
density maps to Vulkan apps; this game draws with OpenGL ES.) On a Quest no
|
||||
manifest names it and it is never loaded.
|
||||
*/
|
||||
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_loader_negotiation.h>
|
||||
#include <stddef.h>
|
||||
|
||||
static PFN_xrGetInstanceProcAddr next_get_instance_proc_addr;
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL layer_get_instance_proc_addr(XrInstance instance, const char *name,
|
||||
PFN_xrVoidFunction *function)
|
||||
{
|
||||
if (!next_get_instance_proc_addr)
|
||||
return XR_ERROR_HANDLE_INVALID;
|
||||
return next_get_instance_proc_addr(instance, name, function);
|
||||
}
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL layer_create_api_layer_instance(const XrInstanceCreateInfo *info,
|
||||
const XrApiLayerCreateInfo *layer_info, XrInstance *instance)
|
||||
{
|
||||
XrApiLayerCreateInfo next_layer_info;
|
||||
|
||||
if (!layer_info || !layer_info->nextInfo)
|
||||
return XR_ERROR_INITIALIZATION_FAILED;
|
||||
next_get_instance_proc_addr = layer_info->nextInfo->nextGetInstanceProcAddr;
|
||||
next_layer_info = *layer_info;
|
||||
next_layer_info.nextInfo = layer_info->nextInfo->next;
|
||||
return layer_info->nextInfo->nextCreateApiLayerInstance(info, &next_layer_info, instance);
|
||||
}
|
||||
|
||||
__attribute__((visibility("default")))
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrNegotiateLoaderApiLayerInterface(const XrNegotiateLoaderInfo *loader_info,
|
||||
const char *layer_name, XrNegotiateApiLayerRequest *request)
|
||||
{
|
||||
(void)layer_name;
|
||||
if (!loader_info || !request || loader_info->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
|
||||
request->structType != XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST ||
|
||||
loader_info->minInterfaceVersion > XR_CURRENT_LOADER_API_LAYER_VERSION ||
|
||||
loader_info->maxInterfaceVersion < XR_CURRENT_LOADER_API_LAYER_VERSION)
|
||||
{
|
||||
return XR_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
request->layerInterfaceVersion = XR_CURRENT_LOADER_API_LAYER_VERSION;
|
||||
request->layerApiVersion = loader_info->maxApiVersion < XR_CURRENT_API_VERSION ? loader_info->maxApiVersion :
|
||||
XR_CURRENT_API_VERSION;
|
||||
request->getInstanceProcAddr = layer_get_instance_proc_addr;
|
||||
request->createApiLayerInstance = layer_create_api_layer_instance;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
Newtown
|
||||
Copyright (c) Roger White, 1994. Released by its author into the public domain.
|
||||
|
||||
Newtown-Regular.ttf is the TrueType file of Newtown, as Roger White
|
||||
distributed it (NEWTOW__.TTF, version 1.00, SHA-256
|
||||
376d82c5b660c059c182d848d0a96da9dbe7277d7c8820f6e9a2c99b1a256c65), here
|
||||
renamed. OpenCE (OpenCE-Regular.ttf) is made from it by tools/title_font.py.
|
||||
|
||||
The file's copyright notice ("All Rights Reserved - see .txt file for terms
|
||||
for distribution and use") and the readme it was first distributed with
|
||||
(personal use free, commercial use for a fee) date from 1994. Its author
|
||||
later released all of his fonts into the public domain. His website,
|
||||
rogersfonts.org.uk, said so from 2008 until it closed:
|
||||
|
||||
"Despite what is said in the readme files associated with the fonts all
|
||||
fonts on this site are now public domain, whether used for personal use
|
||||
or for commercial purposes. All that I ask is that the source is
|
||||
acknowledged if you are using any for commercial purposes."
|
||||
|
||||
The statement is in the Internet Archive's copies of the site's home page,
|
||||
the first of them from 24 July 2008:
|
||||
|
||||
https://web.archive.org/web/20080724224405/http://www.rogersfonts.org.uk/body_index.html
|
||||
https://web.archive.org/web/20110724011202/http://www.rogersfonts.org.uk/body_index.html
|
||||
|
||||
and Newtown was one of the site's fonts (its samples are archived from 2004,
|
||||
https://web.archive.org/web/20040909032418/http://www.rogersfonts.org.uk/assets/images/Newtown.gif).
|
||||
The site says its fonts were made from the author's own hand-drawn artwork,
|
||||
scanned and cleaned up.
|
||||
|
||||
As he asked, the source is acknowledged: OpenCE is based on Newtown by
|
||||
Roger White.
|
||||
@@ -0,0 +1,95 @@
|
||||
Copyright 2026 The OpenCE Project Authors, for its changes to Newtown's spacing.
|
||||
OpenCE is based on Newtown, by Roger White (1994), which its author released
|
||||
into the public domain (Newtown-LICENSE.txt).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2021 The Overpass Project Authors (https://github.com/RedHatOfficial/Overpass)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Fonts
|
||||
|
||||
The fonts the game's text is drawn with, at the display's resolution, in
|
||||
place of the bitmap fonts of the maps (`port/linux/src/text_hires.c`).
|
||||
`fonts.json` says which font draws each of the maps' font tags.
|
||||
|
||||
| File | Draws | From |
|
||||
| --- | --- | --- |
|
||||
| `Overpass-900.ttf` | `ui\large_ui`, `ui\interstate` | Overpass, weight 900 (Black) |
|
||||
| `Overpass-750.ttf` | `ui\small_ui` | Overpass, weight 750 |
|
||||
| `OpenCE-Regular.ttf` | the menus' titles, as pictures (`port/assets/titles`) | OpenCE: Newtown, respaced for this project (`tools/title_font.py`) |
|
||||
| `Newtown-Regular.ttf` | (OpenCE's source) | Newtown by Roger White, unmodified |
|
||||
|
||||
The maps' fonts are Interstate (Tobias Frere-Jones, The Font Bureau), a
|
||||
commercial typeface that cannot be shipped. Overpass (Red Hat, Delve
|
||||
Withrington) follows the same Highway Gothic road-sign alphabet and is
|
||||
under the SIL Open Font License 1.1 (`Overpass-OFL.txt`), without a
|
||||
Reserved Font Name. Matched against the maps' glyphs, its weights 900 and 750 overlap
|
||||
them as much as Interstate Bold does, at the same letter widths.
|
||||
|
||||
The menus' titles are pictures of text in the maps, set in another
|
||||
commercial typeface. OpenCE (Open Community Edition) stands in for it:
|
||||
Newtown, a typeface in the same style by Roger White (1994), which its author
|
||||
released into the public domain (`Newtown-LICENSE.txt` quotes his statement
|
||||
and where it is archived), respaced for this project. Only its spacing is
|
||||
changed, its outlines are Newtown's: each letter's side spaces and the
|
||||
kerning pairs are measured from where the letters sit in the maps' titles.
|
||||
OpenCE is under the SIL Open Font License 1.1 (`OpenCE-OFL.txt`), without a
|
||||
Reserved Font Name. It is not embedded: `tools/title_assets.py` draws the
|
||||
titles with it, and the builds embed those pictures. To measure its spacing
|
||||
again and rebuild it:
|
||||
|
||||
python3 tools/title_font.py --measure # paste the values into tools/title_font.py
|
||||
python3 tools/title_font.py
|
||||
python3 tools/title_assets.py --map assets/maps/ui.map
|
||||
|
||||
The Overpass files are static instances of Google Fonts' variable Overpass
|
||||
(`ofl/overpass/Overpass[wght].ttf`, google/fonts commit
|
||||
9710da1eacb3be272583c3224dcb70f9da6eadbb), made with fontTools:
|
||||
|
||||
python3 -m fontTools.varLib.instancer 'Overpass[wght].ttf' wght=900 -o Overpass-900.ttf
|
||||
python3 -m fontTools.varLib.instancer 'Overpass[wght].ttf' wght=750 -o Overpass-750.ttf
|
||||
|
||||
The builds embed the Overpass files (`tools/embed_assets.py`), and the
|
||||
release packages carry `Overpass-OFL.txt`, as the license asks.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"comment": "Which font draws each of the maps' font tags (port/linux/src/text_hires.c).",
|
||||
"fonts": [
|
||||
{"tag": "ui\\large_ui", "file": "Overpass-900.ttf"},
|
||||
{"tag": "ui\\interstate", "file": "Overpass-900.ttf"},
|
||||
{"tag": "ui\\small_ui", "file": "Overpass-750.ttf"}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 369 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 701 B |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 846 B |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 502 B |
|
After Width: | Height: | Size: 504 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 275 B |
|
After Width: | Height: | Size: 268 B |
|
After Width: | Height: | Size: 361 B |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |