Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 f13ccc98cb Test: our join state enum renamed beside upstream's join progress
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:11:10 +01:00
Stefan MaierandClaude Opus 5.5 499c548ac5 Test: merge upstream/main (network version 39) and OpenCE#486, the VR hand sent as fire origin
For the VR + flat network test of OpenCE#486: upstream's main (network
version 39, client builds as distributed message 92) and the PR's branch
(fire origins, distributed message 93). network_vr.c registers the
weapon hand as the fire origin source; our own host-side use of the VR
poses for the line of sight is dropped in favour of the PR's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:09:43 +01:00
Stefan MaierandClaude Opus 5.5 04cfd1a12c Host line of sight: a VR player's weapon hand is a muzzle too
Upstream's host now refuses a reported hit when the shooter had no line
of sight to the target, from the unit's camera (or a vehicle gun's
muzzle). A VR player fires from the weapon in their hand, and their view
follows their head up to vr.lean_limit from the body: a shot past a
corner or over an edge that the unit's camera is behind was refused.

The host takes the hand their machine sends (network_vr.c) as a muzzle
in the trail points, as a vehicle's gun: the line from the eye to it
must be clear (no hand through a wall), and it must be within 0.4 world
units (1.2 m) of the camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:15:43 +01:00
Stefan MaierandClaude Opus 5.5 2b40b42f75 Merge upstream/main (2026-10-25): host line of sight check, early inventories
Network version stays 38. The conflict: upstream's unbinding of the read
framebuffer before the swap (for OBS) goes into the window's branch,
where the swap is (VR presents through vr.c).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:11:13 +01:00
Stefan MaierandClaude Opus 5.5 9ef32d4304 Merge upstream/main (2026-10-24): OpenCE network version 38
OpenCE gametypes named in the server browser (network version 37), and
the host may leave the shield effects to each player (SHIELD EFFECTS'
PLAYERS' OWN, network version 38). The conflict: our Android listing
declarations stay beside upstream's p2p_set_game_listing with its new
kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:00:39 +02:00
Stefan MaierandClaude Opus 5.5 24ddaecd78 Merge upstream/main (2026-10-23): OpenCE network version 36
New gametypes (SWAT, Infection, Gun Game, Hardcore), shield effects the
host's choice in network games, co-op ally friendly fire, network fixes
(scenery shields, tripmines, co-op teleports), Custom Edition HUD and
texture fixes, ASTC for Android GPUs without S3TC.

Left out on purpose: upstream's two Android workarounds for Mali phones,
the GL shadow state refreshed before every draw (prepare_draw) and the
geometry cache bypassed (mirror_range). Both would cost every draw of
both eyes on the Quest, which never showed the corruption they work
around; their tests are skipped. Conflicts: the GL capabilities log
keeps multiview and adds ASTC; xbox_textures.c keeps vr.c's texture work
counters; README.md stays the player's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:58:11 +02:00
Stefan MaierandClaude Opus 5.5 4e9de9a11c Merge upstream/main (2026-10-22): co-op script fixes, cached high_res_hud, menu cursor
Network version stays 32. The conflict in D3DDevice_Present: the swap
stays in the window's branch (VR presents through vr.c); upstream's
visibility_read_batch, after the swap, is desktop GL only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:23:25 +02:00
Stefan MaierandClaude Opus 5.5 35554cd243 Rename the mod to Mjolnir VR
"Ringfall" is another game's name. Every name players see is now
"Mjolnir VR": the app, the Steam Frame installer and shortcut, the logo
(tools/mjolnir_art.py, was ringfall_art.py), the release's title and
assets (MjolnirVR.apk, mjolnir-vr-android-*.zip,
mjolnir-vr-steam-frame.zip, Install-Mjolnir-VR.desktop), the docs. The
updaters look for Cesarus85/mjolnir-vr, the repository's new name
(GitHub forwards the old one).

Builds 302 to 313 keep working: every release also carries its assets
under their old names, which those builds' updaters and the links
already shared ask for. Kept as they were: the app id, the signing key's
alias, the Steam Frame's ~/Documents/RingfallMjolnirVR and the file
names inside its bundle (an installed game's Steam shortcut starts the
launcher there). The Frame installer now also refreshes the library
pictures of a shortcut that is there already, and says how to rename a
shortcut still called "Ringfall: Mjolnir VR".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:01:25 +02:00
Stefan MaierandClaude Opus 5.5 d4687b24e2 Merge feature/bots-ladders: OpenCE network version 32 (HDR off in VR, Ringfall's host bots kept), bots climb ladders and take partial paths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 22:05:49 +02:00
Stefan MaierandClaude Opus 5.5 9d65e7e63f Roadmap: network version 32, bots' ladders and paths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 22:05:48 +02:00
Stefan MaierandClaude Opus 5.5 f3ce8c3bf0 Bots: paths to as near the goal as the search reaches; the goal's floor looked for deeper; ladders for goals above
On Damnation two of three of the bots' path searches failed (1078 against
527 in 80 s), and the goal's floor was not found 357 times: a search had
to reach the goal's surface itself, and the floor under the goal was
looked for 3 world units down. As OpenCE's bots do (bots.c), a search now
goes to as near the goal as it reaches (the bot searches again from
there), and the floor is looked for 6 units down. A path that ends short
of a goal above the bot takes a ladder near it (now within 8 units).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:44:14 +02:00
Stefan MaierandClaude Opus 5.5 bce5c2b560 Bots climb ladders (Damnation's), which the game's pathfinding does not take
A bot whose way is not found twice running, or that is stuck, with the
foot of a ladder going up within 5 world units, walks to the foot, turns
to the ladder and walks into it (as a biped climbs a climbable surface,
bipeds.c), and on past its top a moment; then it finds its way anew. Ten
seconds without getting up, it gives that up for fifteen. The ladders are
the level's climbable upright surfaces, near ones facing alike merged,
found the first time one is wanted in a game. Reported: bots stuck in a
gap of Damnation's that only a ladder leads out of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:29:10 +02:00
Stefan MaierandClaude Opus 5.5 8097ac23b0 Merge OpenCE upstream: network version 32, HDR, model detail, texture filtering, co-op options, aim assist policy, its local bots, 5.1 sound
Ringfall's side of it:
- The bots: OpenCE's own (bots.c, a local split screen game's) are not
  used in VR builds (their lobby hook would take Ringfall's host bots
  off, and their bots.count and bots.skill differ); Ringfall's are now
  host_bots.c, so that OpenCE's bots.c merges as it changes. Each
  platform's bots.count and bots.skill are its own (config_setting_index
  takes this platform's of two of a name).
- HDR is never on in VR (d3d8_gl.c); the flat window's present is
  OpenCE's, inside the VR port's branch for it.
- The VR frame begins where the events are pumped (sdl_platform.c), as
  before, now that exit_after_check is apart.
- The message numbers: OpenCE's aim assist policy (90) before the VR
  port's poses (240), which stay last.
- The visibility counters keep the VR port's snapshot ring (both eyes'
  counts halved); OpenCE's tone mapping goes before the antialiasing, off
  the VR eyes' pass.
- Join codes, the online list and OpenCE's probes all in the signalling
  topics; the HUD's PNG helpers both ways; the disc import keeps the
  language file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:10:30 +02:00
Stefan MaierandClaude Opus 5.5 c8dfd3773f Merge merge/upstream-agpl: OpenCE upstream under the AGPL 3.0 or later, Ringfall with it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:29:39 +02:00
Stefan MaierandClaude Opus 5.5 856ca06a5d Merge OpenCE upstream under its new licence, the AGPL 3.0 or later
OpenCE moved from CC0 1.0 to AGPL-3.0-or-later (4b09168e7), and Ringfall:
Mjolnir VR moves with it: README's Legal section and THIRD_PARTY_NOTICES
say so (releases before this one stay CC0 1.0, LICENSES/CC0-1.0.txt).

Its disc image readers were written again from a description of the
format, extract-xiso's code and notice gone. The app's (XisoExtractor.java)
is OpenCE's new one with Ringfall's language folders (maps_es, maps_it)
added again; the Steam Frame's (steam_frame_xiso.py) is written anew the
same way, the language folders kept; the bundle and the release job no
longer carry extract-xiso's licence. Also in: bnunu/halo-1's latest
matches, the web build's WebRTC and loading fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:17:41 +02:00
Stefan MaierandClaude Opus 5.5 307d2a953c Merge fix/lan-join-bots: a lobby the bots fill is open to a person on the LAN too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:09:46 +02:00
Stefan MaierandClaude Opus 5.5 ce8eba8088 Roadmap: bots make room on the LAN too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:09:45 +02:00
Stefan MaierandClaude Opus 5.5 62dee62eb6 A lobby the bots fill is open to a person on the LAN too
The bots make room for a person who joins, and the server browser's
listing counted their slots as free, but the LAN advertisement and the
join's check still asked for a free slot: with 15 bots and the host a
lobby of 16 was advertised closed, and a Quest on the LAN was refused
("attempted to join a closed game"). Both now count a bot's slot in a
lobby as a person's, as the listing does (network_game_server_room_for_person).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:59:49 +02:00
Stefan MaierandClaude Opus 5.5 cf11cc86ce Merge merge/upstream-chat: OpenCE's text chat and streamer mode, the scores on the menu button held
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:13 +02:00
Stefan MaierandClaude Opus 5.5 40e4ff2cf8 Roadmap: the scores on the Quest, the text chat in VR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:12 +02:00
Stefan MaierandClaude Opus 5.5 e901c92b41 VR: the menu button held shows the scores (the Quest's controllers had no way to them)
A press pauses as before, now as it is let go; held 0.4 s, the scores
show as long as it is, as the Xbox's Back. In the menus it is Start at
once. The Quest's controls page says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:42:41 +02:00
Stefan MaierandClaude Opus 5.5 dc52b7a592 Merge fixes: a brace the p2p.c merge lost; in VR the chat lines where the desktop has them
The chat's lines go at the top left on Android, where the touch controls
keep the bottom left free; VR has none, so it lays them out as the desktop
does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:34:36 +02:00
Stefan MaierandClaude Opus 5.5 93b67eb7d7 Merge OpenCE upstream: text chat in network games, streamer mode, faster map sharing, the web launcher
The touch controls stay off in VR alongside the chat's on-screen
keyboard (xinput_sdl.c); the join codes and the online list beside the
server browser's listed hosts (p2p.c).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:33:00 +02:00
Stefan MaierandClaude Opus 5.5 5530d19769 Merge fix/vr-weapon-arm: each weapon held so that its hand lands on the real one, the scope's disc with it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:00:14 +02:00
Stefan MaierandClaude Opus 5.5 603ad6b543 Roadmap: each weapon held so that its hand lands on the real one
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:00:13 +02:00
Stefan MaierandClaude Opus 5.5 322146f3cb Scope: the disc moves with the weapon from its old fixed place in the hand
The disc was placed for the weapons at the old fixed place (7 cm above
the hand, 22 cm ahead of it): with the pistol now held 20 cm further
back it sat at the muzzle. It moves as the weapon did (forward and up;
sideways the barrel places it, as before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:54:43 +02:00
Stefan MaierandClaude Opus 5.5 a31c482d5a Hold each weapon so that its hand lands on the real one
The first-person model hung from a camera at one fixed place from the
weapon hand's controller (18 cm behind, 10 cm left, 14 cm above), but
each weapon's animations hold the hand elsewhere from that camera: the
pistol's wrist came out 15 cm before the controller (22 cm before where
the free hand's wrist is, 7 cm behind its grip), the plasma pistol's 11,
the assault rifle's 8 cm aside and 10 cm low; the weapon arm looked
longer, reaching forward. Measured on the Quest 3 (2026-10-17).

Now the wrist as the weapon is held ready is kept for each weapon
(eased, the last eight weapons), and the camera put so that it lands
where the free hand's does: 7 cm behind the grip, along the aim. The
model, its barrel, the laser and the shots move with the camera, as
before. A weapon not yet seen is held at the old place for its first
frame. vr.weapon_hand_forward/_right/_down (0 by default) move the hand
on the weapon; vr.weapon_forward/_right/_down, the old fixed place, are
gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:41:48 +02:00
Stefan MaierandClaude Opus 5.5 e543fe53e0 Merge merge/upstream-oct18: OpenCE's high-resolution Xbox HUD, pause menu artwork, client-side mods, resampler, unsteered vehicles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:31:03 +02:00
Stefan MaierandClaude Opus 5.5 76fbf67334 Merge OpenCE upstream: the high-resolution Xbox HUD completed, pause menu artwork, client-side mods, the resampler setting, unsteered vehicles' netcode
README.md keeps Ringfall's; OpenCE's README is copied to
docs/OPENCE_README.md as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:54:00 +02:00
Stefan MaierandClaude Opus 5.5 7da699b035 Merge feature/disc-languages: the European disc in Spanish and Italian imported, in the headset's language
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:39:31 +02:00
Stefan MaierandClaude Opus 5.5 c5c604bb95 Import the European disc in Spanish and Italian, in the headset's language
That disc has no maps folder but one for each language, maps_es and
maps_it, and the disc image setup stopped: "The disc image has no maps
folder: it is not a Halo disc." The Quest's (XisoExtractor.java) and
the Steam Frame's (steam_frame_xiso.py, RINGFALL_LANGUAGE or the
system's) now take the folder of the device's language, else English's,
else the first, and write its language into maps/language.txt. The game
takes that as the Xbox's language (XGetLanguage) while game.language is
unset, so the menus speak it too. (Reported for ChupathingyCE, another
OpenCE fork, in its issue 145; written anew here.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:39:30 +02:00
Stefan MaierandClaude Opus 5.5 8662777f11 Merge feature/frame-updater: the Steam Frame's launcher updates the game
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:53:11 +02:00
Stefan MaierandClaude Opus 5.5 e24df8ead9 The Steam Frame's launcher updates the game
The game's own updater (the Quest's) cannot on the Steam Frame: Lepton
installs the APK in the game's folder there at every start. So the
launcher does it: while the game runs it asks GitHub for the latest
release, fetches a newer one's Steam Frame bundle into update/, checks
it (its build.txt is the release's number), and puts the APK and the
launcher in place as the next start begins, before Lepton runs the APK.
update.auto = false in config.toml stops it, as on the Quest; a build
with no number (build.txt, which the bundle now carries and the
installer copies; 0 for local builds) never looks; without the internet,
or when anything fails, nothing changes. The in-game updater no longer
starts there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:40:26 +02:00
Stefan MaierandClaude Opus 5.5 7a10a413c8 Merge feature/vr-refresh-auto: the eyes drawn one by one (multiview's doubled pictures online), the refresh rate stepped down while frames come late
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:57:18 +02:00
Stefan MaierandClaude Opus 5.5 5ce39c27a7 Roadmap: multiview to look into later; refresh rate steps in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:57:17 +02:00
Stefan MaierandClaude Opus 5.5 67d738451c Draw the eyes one by one by default; multiview only with debug.multiview
On the Quest 3, in big online games (26-35 players), the picture was
doubled and shifted from the start as the head turned, nauseating; with
48 bots it was not, and with the eyes drawn one by one it was not online
either. Not the frame rate: one by one, online at 72 Hz, the compositor
repeated as many frames and nothing was seen. The Steam Frame had the
same doubling with multiview (2026-10-05) and draws them one by one
since. So both now do by default; debug.multiview (was vr.multiview, and
debug.multiview_steamvr) draws them at once, for finding what the shared
pass draws wrongly. The refresh rate's steps take up the extra cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:48:27 +02:00
Stefan MaierandClaude Opus 5.5 91bdb7e67c Refresh rate: step down at one late frame in 32, show the rate it runs at
The first try stepped 90 to 80 Hz a few seconds into a 30-player game,
and no doubled picture was seen, but 2-5 stale frames a second were left
at 80: one late frame in 32 (was 16) takes it on to 72. The VR options
show the rate chosen and the one it runs at while lowered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:44:18 +02:00
Stefan MaierandClaude Opus 5.5 bf778b6d60 Step the refresh rate down while frames come late (vr.refresh_rate_auto)
In big online games a client's ticks cost 3 to 5 ms (twice what bots or
smaller games do), and the frames with a tick missed 90 Hz's 11.1 ms:
the headset showed them twice, a doubled picture when turning, with the
GPU at 60-80 %. While more than one frame in sixteen comes late, two
two-second windows running, the rate now steps down to the next the
headset offers (90 to 72 Hz); after ten windows in which hardly any
would be late at the rate above, it steps back up, never above
vr.refresh_rate. A load's long frames count for neither. On by default,
"Lower it when frames are late" on the VR options' MORE page; nothing
where the headset offers one rate (SteamVR).

Also the BUTTONS page's notes below its ten rows (the push to talk row
pushed the last one onto them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:38:59 +02:00
Stefan MaierandClaude Opus 5.5 d8a786f5a1 Merge merge/upstream-oct16: OpenCE's string conversion fix, voice packets checked, the postgame host screen's menu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:34:03 +02:00
Stefan MaierandClaude Opus 5.5 7c294c9c39 Merge OpenCE upstream: its fix of the string conversion (taken over ours), voice packets checked, the postgame host screen's menu
Upstream fixed the Android asm conversion's string bug independently
(d9669f6ee, also from a Steam Frame crash); its converter and tests
replace ours (c85cf8ece), which they cover and extend (quoted names
honour escapes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:05:24 +02:00
Stefan MaierandClaude Opus 5.5 b5cdd9268c Merge merge/upstream-net29: OpenCE network version 29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:26:16 +02:00
Stefan MaierandClaude Opus 5.5 34b61fba31 Roadmap: OpenCE network version 29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:06:41 +02:00
Stefan MaierandClaude Opus 5.5 87fe9fc1ad Merge OpenCE upstream: network version 29, Custom Edition maps keep their names, map sharing hardened
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:04:54 +02:00
Stefan MaierandClaude Opus 5.5 238f3e2391 Merge fix/frame-voice-crash: the Android asm conversion keeps string data (the Steam Frame's voice chat crash), push to talk on any button
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:45:46 +02:00
Stefan MaierandClaude Opus 5.5 e18c2b37d6 Push to talk on any button, never sent by a grip reaching for the weapon
Push to talk becomes an action of the BUTTONS page (vr.button_talk),
on an eighth button there, the free hand's grip (its default): it can
trade places with any other action (the move stick's click, say, so
that a player holding a weapon in both hands can talk).

The free hand's grip as a button is pressed once held 0.15 s, at the
threshold halo_vr_two_hand takes the foregrip at, and not at all while
it holds the weapon's foregrip or the Warthog's wheel: taken in that
time, the press is theirs until the grip opens. Before, the runtime's
grip button (which closes before the foregrip's threshold) could open
the microphone for a frame or two as the hand took the weapon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:45:40 +02:00
Stefan MaierandClaude Opus 5.5 c85cf8ece1 Keep string data as it is in the Android asm conversion
android_asm_convert.py renamed identifiers in every directive's operands,
string data included: a string starting with "_" was taken for a quoted
symbol and lost the "_", and one with \" in it was split wrongly, so a
"_" starting a word in it was dropped too. 26 of the guest's objects had
strings or tables cut short. Among them Opus's pulse cache (cache_bits50,
one byte short from entry 338): its encoder picked more pulses than its
tables hold for some bands and crashed (the Steam Frame's crashes in
online games, with push to talk). Also musl's wide character tables,
map sharing's hashed names ("_%s"), the "_lan_" menu check, libtiff's
and the sound code's tables.

.ascii, .asciz and .string are now passed on unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:06:59 +02:00
Stefan MaierandClaude Opus 5.5 01076509dd Merge fix/vr-frame-hitches: OpenCE network version 28, a shader worker and cache, graphics "medium" by default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:04:19 +02:00
Stefan MaierandClaude Opus 5.5 0f6aa62eda VR: graphics "medium" by default; the PTC Quest's doubled pictures noted
On the developer's Quest 3 the picture doubles when turning with every
build back to 2026-10-06's, at 90 and 72 Hz, even when the compositor
reports no stale frames; that Quest runs Horizon OS from the Public Test
Channel. "medium" is smooth there, so it is the default until a Quest
outside PTC has tried "high" (roadmap). An unknown vr.graphics is
"medium" too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:04:03 +02:00
Stefan MaierandClaude Opus 5.5 5696244b1c VR roadmap: graphics "auto", what the effects cost, and its headset test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:18:04 +02:00
Stefan MaierandClaude Opus 5.5 1c7adac6e5 VR: graphics "auto", effects left out while the GPU falls behind and brought back after
On the Quest 3 the shader worker took away the long frames, but a 15-bot
game on Blood Gulch still had seconds at a time of 9-13 ms of GPU a frame
(GPU% 0.91-0.94, 4-37 stale frames a second, the picture doubled), between
spells of 5-7 ms; "medium" held 90 Hz.

- vr.graphics "auto", the new default (the VR options' Graphics row has
  Auto): "high", less what the GPU's time asks to leave out. host_xr.c
  reads XR_META_performance_metrics' app/gpu_frametime each frame (the
  metrics are now on wherever the runtime has them; logged only with
  vr.log_performance), handed to the guest by host_xr_gpu_time.
- vr_graphics.c: past four fifths of the frame (8.9 ms at 90 Hz, scaled
  with the rate) for three quarters of a quarter second's frames, the next
  steps are left out, as many as their known costs say the excess takes:
  the shield's glow, object shadows, reflections, specular light, lens
  flares, bump maps, the rest of "low". Steps that leave out nothing
  drawn (the glow without display.shield_glow) are passed over. A step's
  cost is learnt from the GPU time before it and the second after it
  settled. An effect comes back once the GPU time plus its cost stays
  under seven tenths of the frame for 3 s; an effect that had to go again
  within 10 s of coming back makes the wait twice as long, up to a minute,
  back to 3 s after a minute without leaving anything out. Each change is
  logged with the GPU time.
- Where the headset does not tell the GPU's time (SteamVR: the Steam
  Frame) "auto" draws everything, as "high" did.
- shield_glow.c draws no glow while "auto" leaves it out.
- debug.vr_graphics_measure leaves each effect out in turn (1.5 s on, 1.5 s
  off) and logs its cost by the GPU time, with means every round.

Checked: the APK builds, android_abi_check finds nothing, the desktop and
web paths compile; a host simulation of vr_graphics.c with a lagging
synthetic GPU time (heavy spells of 12 ms, effects of 0.3-2 ms) left out
two or three steps within 0.6 s of a spell and brought them back 3 s
after it, longer after a second spell; the measure mode recovers the
simulated costs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:17:41 +02:00
Stefan MaierandClaude Opus 5.5 2df0c9bd12 VR roadmap: the Quest's measurements, the shader worker and its headset test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:02:52 +02:00
Stefan MaierandClaude Opus 5.5 482a5ecf3a Android menus: pictures decoded on a thread of their own
A menu screen's first frame decoded its PNG pictures on the game's thread,
about 15 ms each ("7 menu pictures in 108 ms"): in VR a frame shown late.
Now a picture not yet decoded is asked of a decoding thread and drawn
transparent for the frames that takes; the game's thread only uploads the
decoded texels. Forgotten art's results are dropped by generation.
Decoding them all ahead was not done: 165 pictures, about 350 MB decoded
with their mipmaps.

Checked: the APK builds; desktop and web keep decoding as before (the
thread is the app's only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:58:33 +02:00
Stefan MaierandClaude Opus 5.5 d396eedb61 Android: shader programs made on a worker thread; every map's programs asked for as a map loads
The shader cache took most programs from their binaries the second time a
map was played, but the user still saw doubled pictures in VR: 29 new
programs in one frame 22 s into a match (794 ms), single ones at 18-64 ms,
and draws of 254 ms and 74 ms with nothing compiled at all (most likely
the driver making a program's variants at its first draw).

- port/android/host/host_gl_worker.c: a thread with an EGL context that
  shares the game's (surfaceless, or on a 1x1 pbuffer) makes the programs
  asked for: from the file's binary, or compiled and linked from the
  sources (its binary handed back for the file). It sets the samplers'
  units, draws once with each program into a small target of the eyes'
  kinds (RGBA8 with depth and stencil, plain and two-layer multiview) and
  puts a fence after it. The game uses a program only once the fence has
  passed (glClientWaitSync, never waiting). Urgent requests (a draw needs
  the program) go before the rest and can hurry a queued one along.
- d3d8_gl.c: a draw whose program is being made is left out for the frames
  it takes (draws_skipped_waiting), instead of the frame waiting; a lit
  program's wait no longer marks the lighting failed. A program's uniform
  setup is its own function, run when the program arrives.
- xgpu_program_cache.c: jobs by key in place of the ready table. As a map
  loads, its own list is asked of the worker, then every map's programs
  (shader_cache/all.txt, a union kept as any map draws), eight a frame, so
  a map first played or a weapon first fired mostly finds its programs
  made. The worker's compiled programs' binaries are written two a frame.
  Without the worker (display.shader_worker false, or no shared context)
  the map's programs are made on the game's thread as before.
- debug.shader_warm_up: "worker" (default), "load" (the map's programs
  waited for, up to 3 s, and drawn with on the game's thread as the map
  loads, for comparing) or "off".
- vr.log_performance: draws are timed (the CPU's counter); a long frame's
  line gives the draw calls' time, the slowest draw with its vertex
  program and program key, and the draws left out waiting; the 5 s report
  counts the draws left out.

Checked: the APK builds, android_abi_check finds nothing, the desktop and
web paths compile; a host harness with a mock worker (first run, second,
a first-played map, driver change, no worker, "load"); on the emulator
(SwiftShader, which refuses kept binaries, so all compiled there) the
worker runs on a pbuffer, the menus' load asked the other maps' 63
programs, Blood Gulch found 69 of 73 made, and 4-bot games ran with the
worker, with "load" and without the worker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:58:33 +02:00
Stefan MaierandClaude Opus 5.5 d46e3fcb38 VR: foveation off by default, its "high" level taken out
On the Quest 3 (graphics "high", 90 Hz, multiview, a 15-bot game on Blood
Gulch) the app's GPU time was 5.5-6.4 ms with foveation off, low, medium
and high alike: the GPU is not what makes the frames late. "high" also
drew a grey rectangle below the player. vr.foveation is now "off" by
default with "low" and "medium" left to try; an unknown value is off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:34:57 +02:00
Stefan MaierandClaude Opus 5.5 8b7857c070 Android shader cache: no format check of its own, and a refused binary logged
The emulator's GL (ANGLE under gfxstream) lists ANGLE's binary format but
hands out binaries of format 1, so every kept binary was passed over before
the driver saw it. The driver checks a binary itself (it made it, the same
driver by the hash); the first three it refuses are logged with the format,
size, GL error and info log, and the cache's start names the driver's
format.

Checked on the emulator with SwiftShader: programs kept (4 MB for the menus
and Blood Gulch), the binaries refused there (gfxstream's do not outlive
the process), so the map's programs were compiled from the kept sources as
it loaded, up to the two seconds, the rest as drawn; a 4-bot game ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:52:48 +02:00
Stefan MaierandClaude Opus 5.5 0989fe8b91 VR roadmap: frame hitches and GPU time (foveation, metrics, shader cache), with a headset test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:35:32 +02:00
Stefan MaierandClaude Opus 5.5 a79dab53d1 Android: a program binary cache, and a map's programs made ready as it loads
The Quest's driver compiles and links a shader program the first time it
is drawn with, 10 to 30 ms each, which the frame waits for: a long frame
at a map's start (14 programs, 256 ms) and near its end (61 in 780 ms),
seen in VR as stale frames.

- port/linux/src/xgpu_program_cache.c: a linked program is written to
  shader_cache in the data folder, named by its sources' hash, with its
  binary (glGetProgramBinary), the driver that made it (GL_RENDERER and
  GL_VERSION) and both sources. Asked for again it is loaded with
  glProgramBinary when the driver, the sizes, the checksum and the sources
  agree; otherwise compiled as before and written again. A refused binary
  only falls back to compiling.
- d3d8_gl.c makes its shaders with their sources and compiles them only
  when a program of them must be linked (shader_make, program_make), so a
  program from the cache costs no compile. Each program entry keeps its
  cache key and lists itself once per map.
- Each map keeps the programs it drew with (shader_cache/maps/<map>.txt,
  written every 20 s while it grows and as the map goes). As a map loads
  (game_initialize_for_new_map, a port: hook) its programs are loaded from
  their binaries; after a driver update compiled again from the kept
  sources, in the background with GL_KHR_parallel_shader_compile where the
  driver has it (polled each frame, binaries written two a frame), else
  for at most two seconds of the load.
- display.shader_cache (true) turns it off; the folder is emptied past
  three quarters of 192 MB or 12000 files at the start, and may be deleted.
- VR's long frame line now also says how many programs came from the cache
  and what textures were uploaded (and decoded from DXT: the Quest has no
  S3TC), menu pictures decoded and render targets made, and in how long,
  for the menus' long frames that compile nothing.

Checked: the APK builds; the desktop and web paths compile; a host harness
with a mock driver went through the first run, a second run (all loaded),
a driver change (compiled from the kept sources, with and without
background compiling), a corrupt and a truncated file (compiled again) and
a missing list. On the emulator (no binary formats) the cache logs itself
off and a 4-bot game on Blood Gulch runs as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:34:37 +02:00
Stefan MaierandClaude Opus 5.5 52ca15dcf0 VR: fixed foveated rendering of the eyes on the Quest (vr.foveation)
The Quest 3 at 90 Hz with graphics "high" spends 5-9 ms of GPU a frame
on the eyes and shows stale frames (ASW, doubled pictures as the head
turns); "medium" holds, so the frame is short of GPU time. The eyes are
now drawn at full detail about each lens's centre and coarser towards
their edges with GL_QCOM_texture_foveated (the Quest's Adreno 740), where
the lenses blur them anyway.

- vr.foveation "off", "low", "medium" (the default) or "high", a row
  on the VR options' MORE page (dim where the GPU has no foveation, as the
  Steam Frame's zink); read as it changes.
- The focal point of each eye is its lens centre, tangent 0, in the
  picture's coordinates: off the middle, the Quest's views being
  asymmetric. Full detail within sqrt(1 + area) / gain of it (0.69, 0.58,
  0.43 of the half width), down to a least density of 1/2, 1/4, 1/8.
- Only the eyes' targets: the multiview layers, which nothing else draws
  into; one eye at a time, the back buffer only from the eye's camera to
  its copy into the headset's image, so the HUD, menus, scope, crosshair
  and names drawn into it after stay at full detail. A texture is first
  foveated when the setting is not off (it cannot be undone); "off"
  afterwards sets the gain to 0, full detail everywhere.
- The GL ES log line names foveation, GL_KHR_parallel_shader_compile and
  the program binary formats the driver has; the first foveated target
  logs the extension's features and focal points, vr.c the level and the
  lens centres.

Checked: the APK builds; the desktop and web paths compile (no foveation
code outside HALO_ANDROID). Not tried on a headset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:34:22 +02:00
Stefan MaierandClaude Opus 5.5 aa7a490d1f VR: the compositor's performance counters logged with vr.log_performance
XR_META_performance_metrics was asked for its counters with room for 16,
and the Quest 3 has more, so the call failed and the metrics stayed off
("xr: performance metrics off (0 counters)"). The counters are now
enumerated with the two-call idiom (up to 48 kept), sampled every third
frame and logged every five seconds as mean/most, named by their last two
path parts (app/gpu_frametime, compositor/dropped_frame_count), so the
app's GPU time and the stale frames show beside the game's own report.
They are enabled only with vr.log_performance (host_main.c passes it on as
HALO_XR_LOG_PERFORMANCE), as is the copies' timing that came with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:26:59 +02:00
Stefan MaierandClaude Opus 5.5 57beb4c2a7 VR: log long frames with their phases and shader compile time
A frame two of the headset's frames long logs its input, ticks, camera,
draw and present times and the shaders compiled or linked in it, so the
hitches seen as stale frames can be told apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:03:09 +02:00
Stefan MaierandClaude Opus 5.5 9e1eef683a VR roadmap: up to date with OpenCE, network version 28
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:23:08 +02:00
Stefan MaierandClaude Opus 5.5 7c6fa07cb5 Merge upstream 2026-10-12: network version 28, map sharing, pregame lobby, body and BSP fixes
Conflicts:
- rasterizer.h: upstream raised the transparent geometry groups to 2048
  as we had (halo_port_capacity.h); theirs.
- d3d8_gl.c: the ES capabilities' log with upstream's base vertex and our
  multiview.
- p2p.c, p2p_internal.h, p2p_signal.c: upstream's several joins and
  reconnect, with our join codes' state and the codes' and public list's
  updates each pass.
- network_server_manager.c: a machine downloading the map is not refused,
  and the bots' room logic stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:23:08 +02:00
Stefan MaierandClaude Opus 5.5 3c0d860c10 VR roadmap: arm swinging (online too) and its sprint (offline), the 2048 groups in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:30:52 +02:00
Stefan MaierandClaude Opus 5.5 9f084155d0 CI: the web pages job off without a second condition
The merge left the pages job two if: keys, which made the workflow
invalid; it is off (if: false) as the web job is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:13:47 +02:00
Stefan MaierandClaude Opus 5.5 6fdee4fabb Merge merge/upstream-2026-10-11: OpenCE network version 26, its first-person legs off in VR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:12:19 +02:00
Stefan MaierandClaude Opus 5.5 74bdffb957 Merge upstream 2026-10-11: network version 26, first-person legs and shield flare, web port, fixes
Conflicts:
- render_objects.c: upstream's first-person legs and our VR body share one
  node_matrices (ours no longer shadowed it); upstream's legs never in VR
  (first_person_legs_wanted: set back behind a flat view's eye, they would
  stand behind the player; the VR body is vr.body "full").
- AndroidManifest.xml: upstream's ':game' process left out (the Steam
  Frame's Lepton waits for the app's process to end; the headsets have no
  Java heap over the Xbox window); HaloApplication kept, idle without it.
- xinput_sdl.c: the touch controls and the web port's, never in VR; the
  controllers' rumble in VR before the phone's.
- d3d8_gl.c: multiview's uniforms and upstream's blend constant, both.
- build.yml: Android only; upstream's web and pages jobs off.
- README.md: ours; upstream's in docs/OPENCE_README.md.
- menu_tags.c: our VR pause-list fit dropped: upstream now spaces the rows
  30 apart and moves the button hints below them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:04:08 +02:00
Stefan MaierandClaude Opus 5.5 7566f49f5e CI: builds for main only
Every pushed branch built (and mailed its failures); the branches are tried
on the headsets from local builds, and only main makes releases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:50:56 +02:00
Stefan MaierandClaude Opus 5.5 e09af359f3 CI: Android only, the Linux and Windows jobs off
Ringfall: Mjolnir VR ships the Android app (Quest, Steam Frame) only. The
desktop jobs stay in the workflow, off (if: false), so that upstream's
changes to them still merge; the release and its notice wait on Android
alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:03:06 +02:00
Stefan MaierandClaude Opus 5.5 7d3533e7b9 Merge feature/vr-voice-and-body: the whole body in VR, push to talk on the free hand's grip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:41:48 +02:00
Stefan MaierandClaude Opus 5.5 bb2f2b19a1 VR roadmap: step 3b, the whole body, done on the Quest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:41:45 +02:00
Stefan MaierandClaude Opus 5.5 486f182fec VR body: the legs walk the way the player moves, the feet on the ground
- Moving, the hips and legs are no longer turned to the torso's heading:
  they face as the game faces the body, so the game's own choice of
  animation (forward, back, aside, as for any player, by the way it moves
  against the way it faces) runs along the way the player moves; the spine
  turns the torso. Standing, the hips turn to the torso's heading again,
  eased (0.12 s). Walking off, the feet ease from where they stood into the
  walk (0.2 s); stopping, they stay and step into place.
- The feet rest on the ground: as they are put down a ray down under the
  body and one under each foot (one more as a foot steps; none walking)
  find the ground, read only; each foot keeps the animation's height above
  it, tilts with the slope, and a foot's ground below lowers the hips, up
  to 45 cm, eased in and out. Nothing found: level.

Checked flat (debug.vr_body): strafing, backwards and diagonally with the
touch stick, the game's sideways and backwards walk; on a Blood Gulch base's
ramp the feet 15 cm up and 10 cm down, both on it; a two-minute bot game
without errors. 0.02 to 0.1 ms a pose in the emulator, a few rays a second
at most. Roadmap: the new behaviour and its headset tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:07:47 +02:00
Stefan MaierandClaude Opus 5.5 313668e0da VR roadmap: step 3b, the whole body, built on the branch
What was built, its settings (vr.body "full", debug.vr_body), what the
emulator showed, and the headset test list for the Quest 3 and the Steam
Frame, with the branch's push to talk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:34:55 +02:00
Stefan MaierandClaude Opus 5.5 22f9d03742 VR body: the neck at its height, the stance reset after a jump, mirrors
- The body is moved down for its neck to be where the head's pivot puts
  it (its model has the neck 4 cm higher standing, 9 cm crouched by the
  button: its shoulders came up by the eyes); the physical crouch still
  sends the hips back.
- Feet more than 80 cm from their planted place (a teleporter) are put
  down at once rather than stepping across.
- A mirror's view no longer decides the frame's pose.
- debug.vr_body moves the flat eyes round the neck as the head pitches,
  as a real head's go.

Checked flat: standing and crouched by the button (touch Crouch), looking
down: shoulders below the eyes, torso upright standing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:34:07 +02:00
Stefan MaierandClaude Opus 5.5 de509fb1a2 VR body: the neck shrunk with the head, the collar closed below the eyes
The neck's own skin stayed around its root, 20 cm below and 14 cm behind
the eyes: its open collar could be seen into looking down. Shrunk onto its
root with the head, the collar closes (checked flat, debug.vr_body back and
down: no stretched skin).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:25:34 +02:00
Stefan MaierandClaude Opus 5.5 378f8bf897 VR: the whole body, vr.body "full": torso, hips and legs under the arms
The player's own biped is drawn in first person among the world's objects
(it sorts against the world by depth; the first-person weapon and arms,
drawn first into the stencil, stay over it), posed from a copy of its node
matrices once a frame (port/linux/game/vr_body.c): turned to the torso's
heading, the neck below and behind the eyes as the head turns about it, the
hips down with the physical crouch and part of the way with a lean, the
spine upright to the neck, the feet planted while standing and stepping
after the body as it turns, the knees bent by the arms' two-bone IK. The
head and the third-person arms are shrunk to nothing, the weapon and what
the biped carries left out; the first-person arms hang from the body's
shoulders (mirrored with the weapon left-handed). A passenger in an open
vehicle sits as the seat's animation has it; at a wheel or a gun the body
is drawn as before, in a closed seat none. Nothing of the game's own pose,
hit boxes or network messages changes.

The neck pivot, the planted feet and the leg's clamped reach follow
HaloCE-Quest-VR (CC0), rebuilt for this port's skeleton and caches.

vr.body gets "full" (VR options, Body: "Full body"); "arms" stays the
default. vr.log_performance reports the body's pose. debug.vr_body shows
the body flat (the emulator): from the eyes, looking down, or from the
front, side or back, the head turning, crouching or leaning on its own.

Checked in the emulator: the cyborg's 19 nodes found, the body posed
standing, crouched, leaning and turning (feet stepping), 0.006 ms a pose;
two-minute bot games on Blood Gulch with it on and off, no errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:22:55 +02:00
Stefan MaierandClaude Opus 5.5 53e13fc69d VR: voice chat's push to talk on the free hand's grip, a Voice chat row
Upstream's voice chat talks while controls.push_to_talk is held, a key. In
VR the free hand's grip is push to talk while it holds neither the weapon's
foregrip nor the Warthog's wheel (it is no button of the game's there).
The VR options' PLAY page gets Voice chat: push to talk (grip), open mic or
off (audio.voice_chat); the controller page names the grip's talking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:29:32 +02:00
Stefan MaierandClaude Opus 5.5 697a85f1cb Merge merge/upstream-2026-10-10: OpenCE network version 25, in-game settings, touch controls (off in VR), FOV options (off in VR)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:11:50 +02:00
Stefan MaierandClaude Opus 5.5 6f43e4f915 VR: the campaign's pause menu keeps its list's height with SETTINGS
Upstream puts SETTINGS into the single-player pause menu's list; a list
with room grows longer and is centred, which put its last row (SAVE AND
QUIT) on the line of the button hints below it (B Cancel, A Select). In VR
its rows now go closer within the span they had, as a list without room's
do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:17:28 +02:00
Stefan MaierandClaude Opus 5.5 2548dd4a54 Desktop updater: the repository's new name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:10:48 +02:00
Stefan MaierandClaude Opus 5.5 e005b4ef3f VR roadmap: up to date with OpenCE, network version 25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 08:51:53 +02:00
Stefan MaierandClaude Opus 5.5 b6a943f1a6 Merge upstream 2026-10-10: network version 25, touch controls, in-game settings, FOV options, voice chat, votekick
248 OpenCE commits (network version 25): touch controls and touch menus for
phones, single-player in-game Settings, opt-in FOV/viewmodel settings, voice
chat and votes to kick, lens flare culling, memory leak fixes (render
targets, gamepads, the Android audio stream), profiling builds, the
HALO_ANDROID split into HALO_ARM64_GUEST / HALO_GLES / HALO_ANDROID.

Conflicts and how they were resolved:
- README.md: ours kept whole. Upstream's README changes (portable build,
  profiling builds) went into docs/OPENCE_README.md: upstream's new text
  with our header note, links rewritten to ../ and our "made from your own
  copy" wording.
- port/android/README.md: both merged; upstream's touch, WSL and save
  notes taken, with our application id in every adb path; our VR section,
  signing.properties and wording kept; the stale "How it works" list
  dropped as upstream dropped it.
- Launcher icons (mipmap-*/ic_launcher_foreground.png, art/android-icon.png):
  ours (the Ringfall art). Upstream's helmet monochrome layers,
  drawable/ic_launcher_background.xml and its ic_launcher.xml were left out;
  our ic_launcher.xml, values/ic_launcher_background.xml and
  tools/android_icon.py stay. (port/assets/icon and tools/app_icon.py come
  in for the desktop builds' window icon only; do not run app_icon.py, it
  would write the helmet into the Android resources.)
- port/android/host/host_main.c: both config helpers kept, and upstream's
  emulator write tracking (native_bridge_active); upstream's display width
  block dropped in favour of ours (640 in VR) and its display density for
  the touch controls moved into our flat (non-VR) branch.
- port/linux/game/menu_functions.c: platform_text_field(typing, password)
  from upstream; the VR keyboard launch for text fields kept after it.
- port/linux/game/network_distributed.c: upstream's profile_scope wrappers,
  with network_vr_tick and bots_tick kept in their places.
- port/linux/src/dsound_sdl.c: both (our debug.audio_log, upstream's
  audio.output_device).
- port/linux/src/port_config.c: our temp-and-rename config write kept,
  under upstream's HALO_ARM64_GUEST (it is guest file I/O).
- port/linux/src/sdl_platform.c/.h: upstream's (Android menu pointer from
  the touchscreen, Steam keyboard, scoreboard pointer); our quit marker for
  the Steam Frame launcher, clipboard_seen and VR frame hooks kept. Our
  flat-mouse menu pointer (an emulator aid) is replaced by upstream's touch
  pointer, which also drives the menus on the emulator.
- port/linux/src/d3d8_gl.c: halo_ui_pointer_update is upstream's, with the
  VR controller ray taking over while VR is active (the touchscreen then
  is not the pointer); our persistent-buffer writes kept for stream and
  index uploads, now writing upstream's exact length and under HALO_GLES
  like the code around them (stream_reserve, index_upload, mirror_refresh
  too); multiview uniforms stay under HALO_ANDROID, the counter binding
  under HALO_GLES. Visibility counters: ours kept (the snapshot ring with
  multiview views, host_gl_counters_snapshot) over upstream's per-frame
  glCopyBufferSubData into counter_snapshots: ours reads finished frames
  without stalling and counts both eyes' samples once; upstream's
  desktop query batches come in unchanged.
- port/linux/src/xinput_sdl.c: both (VR controllers and touch controls).
  In VR the touch controls neither show nor act (touch_input_gamepad,
  touch_input_controls, touch move/look skipped); rumble goes to the
  controllers in VR, to the phone otherwise.
- source/interface/first_person_weapons.c: both (VR scope pass check and
  display.viewmodel_visible).
- source/interface/ui_widget.c: upstream's reworked menu targets taken
  (ui_mouse_fit_button_targets, merged rows, widened values), which do
  what our icon_x0 key picking and the 12-unit value arrows did; our VR
  label distance (25) kept, and a VR ray counts as a coarse pointer, so it
  gets the taller legend areas and the widened values (not the merged
  rows: a row's label still presses A). Our join code and VR options
  panels kept; their early return now forgets the frame's targets too.
  The on-screen keyboard: upstream's virtual_keyboard_click for taps and
  the mouse, ours (hover highlights the key) for the VR ray only.
- source/main/main.c: both (VR phases around upstream's profile scopes).

VR guards for new upstream features:
- view_fov.c: display.fov, display.viewmodel_fov and
  display.viewmodel_visible do nothing while VR is active (the headset
  gives each eye's frustum; the weapon is in the hand).
- Touch controls: off in VR (xinput_sdl.c), the menus' touch pointer too
  (d3d8_gl.c); the overlay stays hidden as the game never reports a scene.
- Lens flare culling (744040c8, b1a71709) uses global_window_parameters'
  frustum, which in VR is each eye's (per-eye passes) or the union frustum
  of the multiview centre camera: no change needed.
- In-game Settings: VR options still open with Y on the main and pause
  menus (checked flat with debug.vr_options).
- The back button change (enableOnBackInvokedCallback=false) keeps the
  legacy KEYCODE_BACK path, which targetSdk 35 already had: no change on
  Quest or Frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 08:51:46 +02:00
Stefan MaierandClaude Opus 5.5 d18339be66 VR roadmap: up to date with OpenCE, network version 24
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:29:34 +02:00
Stefan MaierandClaude Opus 5.5 e28767b5e9 Merge merge/upstream-2026-10-08: OpenCE network version 24, Custom Edition maps, positional stereo sound
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:29:34 +02:00
Stefan MaierandClaude Opus 5.5 a167de3873 Merge upstream 2026-10-08: network version 24, Custom Edition maps, positional stereo sound, fixes
Conflicts:
- d3d8_gl.c, xgpu.h: upstream's own snapshot ring for the visibility
  counters left out for ours (host_gl_counters_snapshot, the same idea,
  tried on both headsets, which also halves multiview's counts; its copy
  already has the memory barrier upstream added); both declarations kept.
- rasterizer_xbox.c: the mirrored skinning's winding and upstream's
  many-node matrices, both.
- main.c, network_server_manager.c: both (bots' machines and a lone host
  may count down).
- android_build.py: the OpenXR loader and upstream's SDL mouse patch
  (another SDL file than ours), both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:01:08 +02:00
Stefan MaierandClaude Opus 5.5 ff9cb2b55a VR roadmap: up to date with OpenCE, network version 21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:55:36 +02:00
Stefan MaierandClaude Opus 5.5 1109660bbe Merge merge/upstream-2026-10-07: OpenCE network version 21, co-op and netcode fixes, a VR crash fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:55:26 +02:00
Stefan MaierandClaude Opus 5.5 c1b2d34fce VR: no first-person lookups of a weapon or grenade deleted since the tick
On the Steam Frame the game stopped right after a melee: the VR log of the
first-person skeleton's nodes looked up the player's weapon, which had been
deleted since the tick (object #93 unused), before the game's own check
forgets such a weapon ("deleted unexpectedly"). It now skips a weapon
that is gone, and the grenade drawn in the free hand checks its unit and
grenade the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:50:47 +02:00
Stefan MaierandClaude Opus 5.5 448a5d5353 Merge upstream 2026-10-07: network version 21, co-op and netcode fixes, renderer speed-ups, hardening
Conflicts: port_config.c (both sides' debug settings), hud_weapon.c (the
VR crosshair filter before upstream's state index check),
first_person_weapons.c (the VR arms kept; upstream's animation node check
added, and its node count limit applied to the VR arms too).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:42:13 +02:00
Stefan MaierandClaude Opus 5.5 c3e8220792 README: what can be played (campaign, co-op, LAN, online, bots, maps, cross-play)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:22:12 +02:00
Stefan MaierandClaude Opus 5.5 2670b3589f Merge feature/release-ready: README for players, third-party notices, release assets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:06:46 +02:00
Stefan MaierandClaude Opus 5.5 a950de4b2b Release-ready: README for players, third-party notices, issue template
- README.md: what the mod is (a VR mod for Halo: Combat Evolved), what is
  needed (a disc image of one's own, purchased copy), simple installation
  for the Quest 3 / 3S (SideQuest) and the Steam Frame (Desktop Mode, the
  installer), playing, reporting problems, network use, credits (OpenCE,
  the decompilation, HaloCE-Quest-VR, HaloCE-VR, extract-xiso) and the
  legal notes (unofficial, not affiliated with Microsoft and others).
- THIRD_PARTY_NOTICES.md: the libraries and fonts and their licenses.
- Releases: the APK itself (RingfallMjolnirVR.apk) for sideloading, notes
  that say which file is for which headset, the project's name as title.
- An issue template (headset, build, log; no game files).
- docs/OPENCE_README.md marked as the base port's; the disc images'
  wording everywhere: made from one's own copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:05:29 +02:00
Stefan MaierandClaude Opus 5.5 1984996d04 VR roadmap: comfort, buttons, import, installer and the new name done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:55:46 +02:00
Stefan MaierandClaude Opus 5.5 93b96cf5b8 Merge feature/rename-ringfall: Ringfall: Mjolnir VR, comfort and buttons, Frame installer, release signing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:55:25 +02:00
Stefan MaierandClaude Opus 5.5 f68db3b03e VR: the flashlight by tapping the head taken out again
Revert of c2aceb64: on the Quest 3 the tap did not toggle the flashlight.
The flashlight's button stays; the on-foot vignette that came after it is
kept. The roadmap and README no longer name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:53:48 +02:00
Stefan MaierandClaude Opus 5.5 bd08baacac Steam Frame installer: the shortcut added by Steam itself
On the Frame Desktop Mode runs inside Gaming Mode's session: closing Steam
to write shortcuts.vdf ended the session, and the installer with it, before
it wrote the shortcut (the library kept "Halo VR"). Steam now adds the
game itself (steam://addnonsteamgame, from a desktop file beside the game,
as SteamOS's "Add to Steam" does) and picks its id, which the installer
reads back for the library pictures. An old "Halo VR" shortcut is named
for the player to remove.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:32:49 +02:00
Stefan MaierandClaude Opus 5.5 f39126e019 Steam Frame installer: Steam found by its pid file, an install log
Steam's client on SteamOS is not always a process named steam: the
installer may have written the shortcut while Steam ran, and Steam wrote its
own list back when it quit (the library kept "Halo VR"). Steam is now
found by ~/.steam/steam.pid or its ubuntu12_32/steam process, given time
after it ends, and the shortcut checked once written; what the installer
does goes to install.log beside the game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:28:50 +02:00
Stefan MaierandClaude Opus 5.5 f649bd1a3e Release as Ringfall: Mjolnir VR
The product no longer carries Halo in its name; the game it plays is named
only to say which disc image it needs.

- The app: "Ringfall: Mjolnir VR", application id
  io.github.cesarus85.ringfall (the Java classes keep the decompilation's
  package), its update provider's authority from the id; the launcher's,
  updater's, host's and VR options' texts; the OpenXR application name.
- New icon and Steam library pictures, a tilted, broken ring with a falling
  streak (tools/ringfall_art.py, from shapes and Overpass; the helmet and
  the HALO logo gone).
- The Steam Frame: ~/Documents/RingfallMjolnirVR, RingfallMjolnirVR.apk,
  ringfall-mjolnir-vr.sh, Install-Ringfall-Mjolnir-VR.desktop and
  ringfall-mjolnir-vr-steam-frame.zip; the installer renames the folder of
  the builds before (~/Documents/HaloCE) and replaces their "Halo VR"
  shortcut and pictures.
- Releases: only the Android builds, as ringfall-mjolnir-vr-android-*.zip
  (the updater's names), and the Frame's installer; the repository
  Cesarus85/ringfall-mjolnir-vr.
- The signing key's default alias ringfall.
- README.md is the project's own; the port's moved to docs/OPENCE_README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:16:31 +02:00
Stefan Maier 719563a48c Merge branch 'feature/release-install' into test/comfort-release 2026-10-06 22:08:41 +02:00
Stefan MaierandClaude Opus 5.5 32cc1cb206 VR roadmap: step 2's second part built on feature/vr-comfort
Buttons remapped, the flashlight by tapping the head, finer turning and
vignette steps, the transparent geometry groups raised: what they do, the
settings, what the emulator showed and the headset tests for the Quest 3
and the Steam Frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:07:28 +02:00
Stefan MaierandClaude Opus 5.5 d4c1fbe9a7 Release: Steam Frame installer, release signing, Frame import kept
- A Steam Frame installer for players, opened in Desktop Mode
  (Install-Halo-VR.desktop, a release asset): it downloads the latest
  release's bundle (tools/steam_frame_bundle.py) and runs
  steam_frame_install.sh, with zenity dialogs and no keyboard: the APK and
  launcher in ~/Documents/HaloCE, the maps copied out of a disc image it
  finds or is given (steam_frame_xiso.py, the app's XisoExtractor in
  Python), and Halo VR in the Steam library with its pictures and OpenVR
  set. Running it again updates the game.
- The release job adds the bundle and the desktop file to each release.
- build.gradle signs with port/android/signing.properties (ignored) when
  the workflow's key is not given, so local builds and releases install over
  each other.
- On the Frame the launcher and host take ~/Documents/HaloCE whenever it can
  be written, not only once it holds maps: a disc image imported in the app
  is no longer emptied with Lepton's folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:06:21 +02:00
Stefan MaierandClaude Opus 5.5 adf220d344 VR: finer turning steps, an on-foot vignette, a COMFORT page
The VR options get a fourth page, COMFORT (PLAY, COMFORT, BUTTONS, MORE;
the tabs beside the title share its width): Turning, Snap turn, Smooth
turn speed and the vehicle vignette move there from PLAY, joined by the
on-foot vignette and its strength. PLAY keeps the hands, laser,
crosshair, motion tracker, body, fingers and the Warthog's wheel.

- Snap turns step through 10 to 45 degrees by 5, then 60, 75 and 90 (were
  30, 45, 60, 90); smooth turning through 30 to 150 degrees a second by
  15, then to 300 by 30 (were 60 to 180 by 30); the vehicle vignette
  through Off, 15, 25, 35, 50, 60, 70, 85 and 100% (were Off, Light 35%,
  Medium 70%, Strong). The old values are among the steps; the defaults
  (snap 30, 120 a second, vignette 70%) are unchanged.
- vr.foot_vignette ("off" default, "turning", "moving") and
  vr.foot_vignette_strength (0.7): on foot the eye images darken from
  their edges while the turning stick turns smoothly and, with "moving",
  while the move stick moves, as far as the sticks are pushed beyond
  their dead zone (fully at three quarters), eased in and out as the
  vehicle vignette is. Not in a vehicle's seat (whether or not its view
  is first person), not in the menus; snap turns have none. The stronger
  of the two vignettes is shown. Off by default: nothing changes.

Checked in the emulator (debug.vr_options): the four tabs and the rows
without overlaps, each row's steps, the values kept over a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:05:10 +02:00
Stefan MaierandClaude Opus 5.5 32ad9ed6b0 VR: the flashlight by tapping the side or top of the head
Either hand brought to the side or the top of the head and away again, as
a tap, toggles the flashlight (vr.flashlight_gesture, default true; a row
on the VR options' BUTTONS page); the flashlight's button keeps working.
After HaloCE-Quest-VR (CC0), whose off hand near the head's middle
toggles it, made stricter against false toggles:

- The zone is in the head's axes: beside the head (7 to 24 cm out, from
  10 cm below the eyes to 18 cm above) or over it (within 12 cm of its
  middle line, 8 to 30 cm up), never more than 6 cm in front of the eyes,
  so a scope held to the eye, a two-handed weapon, a punch and the wrist's
  motion tracker, all in front of the face, are outside it.
- A tap: the hand comes from 35 cm or more from the head's middle, enters
  slower than 3 m/s (a throw past the ear is faster), stays 40 to 600 ms
  (adjusting the headset takes longer), alone (both hands at the head is
  the headset adjusted) and leaves 4 cm past the zone's edges; toggles are
  at least a second apart.
- None while the menus are up, while recentring, while a grenade is held
  or for 0.6 s after one is thrown, within 0.7 s of a melee swing, with
  both hands on the weapon, with the weapon hand while the scope is drawn,
  or with a hand on a Warthog's wheel; such a hand must leave the head
  before it taps.
- A toggle presses the flashlight's button (white) for 100 ms, as B's tap
  does, and gives the tapping hand a short rumble.

The tap's state machine was run off the headset in a small harness over
taps beside and over the head, a turned head, a second tap within the
cooldown, a long dwell, both hands, a scope at the eye, the scope flag, a
fast throw past the ear, a held grenade and a two-handed grip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:00:53 +02:00
Stefan MaierandClaude Opus 5.5 cf19bcfdfa VR: the game's actions moved between the controllers' buttons
A BUTTONS page in the VR options (between PLAY and MORE) puts jump, the
flashlight (held: switch grenades), action/reload, switch weapon, crouch,
zoom and melee on any of seven buttons: A, B, X, Y, the move and turning
sticks' clicks and the weapon hand's grip. Moving an action to a button
swaps it with the action there, so every action always has a button;
"Reset buttons to defaults" puts them back. The triggers, the free hand's
grip (both hands on the weapon), the menu button, the Steam Frame's
bumpers, D-pad and View button keep what they do, and the menus keep
their buttons.

Settings vr.button_jump, _light, _action, _weapon, _crouch, _zoom and
_melee ("a", "b", "x", "y", "move_stick", "turn_stick", "grip"; the
defaults are today's layout), named as right-handed. vr.c reads them with
the live settings and, after the left-handed mirroring
(controllers_mirrored), reads each action's button and hands it on as the
button that action has by default (controls_remapped), so xinput_sdl.c's
table is unchanged and the defaults change nothing. Because the mapping
sits after the mirroring it works the same on the Quest's Touch and the
Steam Frame's controllers (mirrored on the Frame the left D-pad is A, B,
X, Y), and a mirrored player's swaps are the mirror image of the
right-handed ones. The guest/host ABI is unchanged.

- The melee's aim and the weapon's jab (melee_update) follow the melee's
  button, not the grip as such; the grips holding a Warthog's wheel press
  nothing whatever action the grip has (moved from xinput_sdl.c).
- The controller buttons page (now on the BUTTONS page) shows each action
  under the controller and name of its button, for both layouts and the
  mirrored hands; its ON FOOT tab no longer draws the MENUS tab's notes
  over its own.
- debug.vr_options (Android, false): the VR options panel off the
  headset, for checking its pages in the emulator.

Checked in the emulator (debug.vr_options): the rows, swaps, reset, the
controls page right- and left-handed, the settings kept over a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:57:15 +02:00
Stefan MaierandClaude Opus 5.5 3b5796f1a0 VR roadmap: the keyboard's password in the clear is no open point
Nobody else sees the headset's view (decided 2026-10-06): no masking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:47:26 +02:00
Stefan MaierandClaude Opus 5.5 68135a712d Transparent geometry: room for 2048 groups a window (was the Xbox's 384)
Every transparent thing a window draws is a group of its own: a model's
transparent part (shields, plasma, glass), each particle system, contrail
and light volume, a window's or water's surface. The Xbox had room for 384
(rasterizer_transparent_geometry_new_group); past that new_group returns
NULL and the submitter drops the geometry, logging "### ERROR too many
transparent geometry groups" once (rasterizer_xbox_models.c,
rasterizer_xbox_widgets.c; environment and dynavobgeom drop it silently).
render_window submits the objects first, then the structure's transparent
geometry, weather, particles, particle systems and contrails, so in games
of 30 and more players the late ones vanish: water and glass surfaces,
explosions, muzzle flashes, smoke, plasma glows, contrails.

The groups are a heap array (0xA0 each), a short array of sorted indices
and a bit vector of pending flags, all sized by the constant; indices are
shorts (sorted_index, presorted indices, the draw loop), good to 32767.
Nothing else depends on the number (the debug view's "all groups" index
1000 only colours groups in a debug mode). Upstream (halo-ce-universal)
still has 384.

2048 groups take 320 KB (61 KB before). A one-line log tells when a window
passes 384 and every 128 beyond, for checking big games in the headset.
In the emulator, a 36-player bot game on Blood Gulch had up to 134 groups
a window from a standing player, using under 8 KB of the rasterizer's
768 KB memory pool (the models' queued matrices and lighting), so the
pool is not the next limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:47:22 +02:00
Stefan MaierandClaude Opus 5.5 1f238ff045 Merge feature/vr-lefthanded-graphics: left-handed mode, graphics presets, refresh rate and resolution in VR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:21:38 +02:00
Stefan MaierandClaude Opus 5.5 88354e5c6c VR roadmap: left-handed mode and graphics presets done on both headsets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:21:35 +02:00
Stefan MaierandClaude Opus 5.5 8cdce233e4 Android settings: config.toml written beside itself and renamed over
A config.toml pushed by adb belongs to the shell and the app cannot open it
for writing, so the VR options' changes (handedness, graphics, refresh rate,
resolution) lasted only until a restart. Writing a temporary file and
renaming it over the old one replaces it all the same (the folder is the
app's), and a write cut short leaves the old file whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:55:22 +02:00
Stefan MaierandClaude Opus 5.5 81137e3af3 VR roadmap: left-handed mode and graphics presets good on the Steam Frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:40:01 +02:00
Stefan MaierandClaude Opus 5.5 fb670e79b9 VR roadmap: the left hand's ammo counter reads the right way round
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:33:11 +02:00
Stefan MaierandClaude Opus 5.5 8022a0b4a1 VR left-handed: the assault rifle's ammo counter reads the right way round
The mirrored first-person model leaves its geometry mirrored; a numeric
display drawn mirrored (rasterizer_set_model_skinning's winding flag) now
shows each digit's bitmap turned over in u (1-u on the first map's texture
transform) and swaps the digits' order across the display (the quad of the
n-th digit from the right shows the n-th from the left; digit count from
the counter limit, the compass's single digit unchanged). Only for 2d,
non-screen-space first maps of numeric chicago/generic shaders, only when
mirrored: right-handed and flat output unchanged.

debug.mirror_first_person (Android, default false): off the headset the
first-person weapon is mirrored across the camera's forward/up plane as
the left hand's is in VR, for checking such things in the emulator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:33:11 +02:00
Stefan MaierandClaude Opus 5.5 696383f191 VR roadmap: the left hand's mirrored ammo display, an open point
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:01:39 +02:00
Stefan Maier 9a33ecd2d3 Revert "VR left-handed: the assault rifle's ammo display reads the right way round"
This reverts commit 530a32b64889c24e64c5e805949e31df1c68fb73.
2026-10-06 20:01:19 +02:00
Stefan Maier 9447bc2ae4 Revert "VR left-handed: the ammo display's subtree found without hanging"
This reverts commit 8e40d6ecacd92f3938e329e528a27c3eddd8dbe1.
2026-10-06 20:01:18 +02:00
Stefan Maier f04c8c4045 Revert "VR left-handed: a part is drawn mirrored only where all its nodes are"
This reverts commit 1a6061b932e7f293cc40351f90f4d0d68edb2bd2.
2026-10-06 20:01:18 +02:00
Stefan MaierandClaude Opus 5.5 b026590824 VR left-handed: a part is drawn mirrored only where all its nodes are
The assault rifle's ammo display, mirrored back, vanished: its part's
winding was judged by its first node alone, which may be the mirrored gun,
so its triangles were turned over and culled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:54:01 +02:00
Stefan MaierandClaude Opus 5.5 2a2b2cdbf3 VR left-handed: the ammo display's subtree found without hanging
Walking up from a node to the display through parent_node_index never
ended where the root names itself as its parent: the game hung loading a
level left-handed (Steam Frame). Each step now goes only to a parent
before its child, as vr_skeleton_build does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:50:52 +02:00
Stefan MaierandClaude Opus 5.5 5861e57d99 VR left-handed: the assault rifle's ammo display reads the right way round
Its node ("... display") and what hangs from it are mirrored back about
the display's own centre after the pose is mirrored: it stays where the
mirrored gun has it, reads as in the right hand, and its triangles keep
their winding (rasterizer_xbox.c decides per skinned part). After
HaloCE-Quest-VR (CC0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:47:53 +02:00
Stefan MaierandClaude Opus 5.5 50d685e465 VR roadmap: step 2 in part built (left-handed mode, graphics presets, refresh rate)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:33:27 +02:00
Stefan MaierandClaude Opus 5.5 0e6b006878 VR options: graphics, refresh rate and resolution rows
The panel's second page (MORE) gets "Graphics" (vr.graphics: Low,
Medium, High, Max), "Refresh rate" and "Resolution" rows, with a note
of what they do. The refresh rate steps through the rates the runtime
offers (XR_FB_display_refresh_rate: host_xr_refresh_rates, new host
imports with host_xr_set_refresh_rate) and changes at once (vr.c asks
for it when vr.refresh_rate changes); where the runtime offers none
(SteamVR on the Steam Frame, whose rate is SteamOS's) the row shows the
rate it runs at, from its frames' period, and stays as it is. The
resolution (vr.resolution_scale, 70 to 120%) is read as the app starts:
the row says when a restart is needed. vr.log_performance's late frames
follow the rate as it changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:30:32 +02:00
Stefan MaierandClaude Opus 5.5 ca73ba7909 VR: graphics presets (vr.graphics)
port/linux/game/vr_graphics.c (after HaloCE-Quest-VR's, CC0) sets the
game's own effect switches each frame before drawing: "high" (the
default) draws everything as before; "medium" no object shadows,
environment reflections or lens flares; "low" nor specular light, bump
maps, detail objects (grass), the screen's fog layer, contrails, weather
or camouflage's second pass; "max" is "high" with per-pixel model
lighting and 512x512 shadow maps (d3d8_gl.c asks halo_vr_graphics). A
switch the game changes (scripts, console) is taken as its wish and
combined with the preset's. A client in another's game keeps grass, lens
flares and the fog layer as the host's rules have them; cheats.c leaves
the environment parts the preset may keep off there (shadows, specular,
reflections).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:28:48 +02:00
Stefan MaierandClaude Opus 5.5 c254d6df05 VR: left-handed, the rumble's motors follow the hands
The game's light (right) motor rumbles the weapon hand, the heavy one the
free hand (halo_vr_weapon_hand), as right-handed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:26:40 +02:00
Stefan MaierandClaude Opus 5.5 9910c4a9dc VR options: weapon hand and mirrored controls, two pages
The VR options panel gets a "Weapon hand" row (vr.handedness) and a
"Mirrored controls" row (vr.mirror_controls, dim when right-handed). Its
rows now go on two pages with tabs beside the title (PLAY, MORE; up and
down run on across them, a tab clicked shows its page). The controller
buttons page is built from the hands' jobs: the weapon hand's trigger
and grip, the free hand's, the move and turn sticks and the face
buttons (Quest X/Y for A/B; Steam Frame D-pad for A, B, X, Y) on the
sides the settings put them; the wrist, wheel and aiming notes name the
hands. network_vr.c's notes name the weapon and the free hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:26:03 +02:00
Stefan MaierandClaude Opus 5.5 b1d59e654d VR: left-handed, the motion tracker on the right wrist
The wrist layer goes on the free hand's wrist: halo_xr_submit.wrist_hand
(new, after wrist_down); host_xr.c keeps both grips and places the layer
on the right wrist as the mirror image of the left one's, read the right
way round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:22:25 +02:00
Stefan MaierandClaude Opus 5.5 1f3d1be207 VR: left-handed controls, mirrored with vr.mirror_controls
Left-handed, in play, vr.c hands xinput_sdl.c the controllers with the
weapon hand's trigger and grip as the right side's (fire, melee, the
wheel's throttle) and the free hand's as the left's (grenade, foregrip,
brake). vr.mirror_controls (default true) mirrors the sticks (move on
the right, turn and steer on the left), their clicks, the bumpers and
the face buttons too: on the Quest X/Y for A/B; on the Steam Frame, whose
A, B, X and Y are all on the right controller, the left D-pad takes them
as their mirror image (down A, left B, right X, up Y) and they take the
D-pad, so every function stays reachable. The menus keep the buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:21:36 +02:00
Stefan MaierandClaude Opus 5.5 d97de6d499 VR: left-handed mode, the weapon in the left hand
vr.handedness = "left" puts the weapon in the left hand: aim, muzzle,
laser, crosshair, scope and turret aim follow the left controller; the
grenade, the foregrip and the fingers' closing round a grenade move to
the right hand, the grip's melee to the left. The first-person model is
posed as the right hand's in a mirror image of the room ("model space":
vr.c mirrors the controllers into it, first_person_weapons.c the body)
and the posed skeleton mirrored back, after HaloCE-Quest-VR (CC0); a
model whose node matrices mirror has its triangles' winding turned over
(rasterizer_set_model_skinning -> d3d8_gl.c, ended with the model and
each transparent group). halo_vr_left_hand is now halo_vr_free_hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:20:59 +02:00
Stefan MaierandClaude Opus 5.5 fbe48a869f VR roadmap: steps 1 and 3a done, in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:53:21 +02:00
Stefan MaierandClaude Opus 5.5 206eb0b52b VR roadmap: the Steam Frame's tests done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:46:55 +02:00
Stefan MaierandClaude Opus 5.5 1c66715a19 VR roadmap: the Quest's tests done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:34:15 +02:00
Stefan MaierandClaude Opus 5.5 87f336df78 VR roadmap: 3a tried on the Quest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:30:49 +02:00
Stefan MaierandClaude Opus 5.5 0a86e406c8 VR arms: the hand's roll leads the elbow; the free arm bent by default
The elbow could not be lifted out to the side: it bent only towards the
fixed pole (down, out, back). An elbow lifted out rolls the controller's
top in, so the pole now leans towards the hand's underside (the weapon's
for the right arm, the free left controller's for the left; the hand on
the foregrip keeps the fixed pole). With the free arm solved before its
hand is turned it looked right on the Quest: vr.arm_ik defaults to true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:56:47 +02:00
Stefan MaierandClaude Opus 5.5 c09b40a2ef VR arms: the free left arm solved first, its hand turned after
As HaloCE-Quest-VR does it: the free arm is bent to the controller's grip
from the animated pose (the hand free to swing with it), and only then is
the hand turned to the controller and the roll shared along the forearm.
Turning the hand first left the solve to swing it on, which stretched the
arm's skin like a rubber band (Quest, 2026-10-06). vr.arm_ik stays off by
default until it is seen right.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:54:15 +02:00
Stefan MaierandClaude Opus 5.5 1e2006d06c VR arms: the elbow bends in the arm's plane, the shoulders higher
Second Quest test: with the elbow out to the side the right elbow's skin
turned inwards: the shortest turn of the solve left the upper arm's hinge
where it was. vr_align_elbow now rolls the upper arm about its length so
the animation's bend lies in the plane the arm bends in, and the upper
arm takes no share of the hand's roll any more. The open sleeve end could
still be seen: the shoulders are 5 cm higher (20 cm below the neck).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:47:45 +02:00
Stefan MaierandClaude Opus 5.5 e69e087602 VR arms: the free arm rigid by default, the shoulders further back
First Quest test: the free left arm's skin stretched like a rubber band
with the new elbow IK, so vr.arm_ik now defaults to false (the arm's shape
as before, pointed at the shoulder); and the open end of the right
sleeve came into view looking down to the side: the shoulders now stand
4 cm behind the neck instead of 3 cm in front of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:59:41 +02:00
Stefan MaierandClaude Opus 5.5 87a38e2895 VR roadmap: the pending headset tests, performance among them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:45:44 +02:00
Stefan MaierandClaude Opus 5.5 788ab74a6b VR roadmap: step 3a built in feature/vr-arms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:37:07 +02:00
Stefan MaierandClaude Opus 5.5 6d2dcfaaec VR arms 3a.6: vr.body (arms, hands, off) and vr.fingers in the VR options
- vr.body = "arms" (default): arms and hands as before; "hands": the arms'
  bones shrunk to a point 3.5 cm behind each wrist along the hand's line
  so the cuff closes behind the hand (vr_hide_arm, after HaloCE-Quest-VR's
  vr_hide_forearms, CC0), no arm IK solved; "off": the hands model is not
  drawn (one model draw less an eye) and nothing of the arms posed; the
  weapon, its aim with both hands, barrel, laser and crosshair stay.
- vr.c reads vr.body with the live settings (halo_vr_body_mode).
- The VR options panel gets "Body" (Arms and hands / Hands only / Weapon
  only) and "Fingers" (Tracked / Off) rows; its twelve rows are 22
  pixels apart (were 26) so the panel keeps its box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:35:07 +02:00
Stefan MaierandClaude Opus 5.5 98e848de39 VR arms 3a.5: the fingers posed
- vr_arms.c: vr_finger_nodes, vr_pose_finger (after HaloCE-Quest-VR, CC0,
  nearly verbatim): each finger's three joints found by name under the
  hand, posed by absolute directions in the palm's frame (a little bent
  at rest, a fist at full curl; the thumb up the handle or across the
  palm), each segment's subtree turned with its length kept. Its touch
  of the world (rays per finger) is left out for the Frame's processor.
- The free left hand takes its controller's curls (halo_vr_finger_curls:
  index from the trigger, the others from the grip, the thumb from where
  it rests; round a held grenade). On the foregrip the hand keeps the
  weapon's pose.
- The hand on the weapon keeps its grip; only its index finger is lifted
  part way off the trigger while the controller says it is (touch known
  and the finger off it), never where touch is unknown.
- Joints found once per skeleton; the cost is a few dozen node turns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:33:12 +02:00
Stefan MaierandClaude Opus 5.5 7754532683 VR arms 3a.4: the IK core from HaloCE-Quest-VR, the free left arm bent
- port/linux/game/vr_arms.c: vr_solve_arm and vr_distribute_arm_twist,
  after HaloCE-Quest-VR (CC0), nearly verbatim: the shoulder reaches 8 cm
  and the arm stretches up to 18% out of reach (further, the shoulder
  follows the held hand, as this port had it, so the arm stays whole),
  bends no further than 145 degrees, the elbow's swing eased per arm
  (vr_limb_history), the hand's roll shared by forearm (85%) and upper
  arm (25%) within 110 degrees, continuous across half a turn. The
  skeleton is read through the graph's index; the nodes under each node
  come from one pass a frame (vr_skeleton_build) rather than walking
  ancestors per node; time is the headset's frame's (halo_vr_pose_time).
- Both arms use it; the right hand and a left hand on the foregrip stay
  where the weapon puts them (hand_stays), so shots, barrel and laser do
  not move.
- The free left hand: its wrist and fingers are set on the controller as
  before, and the arm now bends to it from the shoulder instead of being
  carried rigidly. The ribbons of e377e4dc did not come from skin on the
  root (3a.0: none hangs there), so no triangle filter is needed; the
  rigid arm stays behind vr.arm_ik = false, read live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:31:16 +02:00
Stefan MaierandClaude Opus 5.5 19b0bcb42e VR arms 3a.3: the shoulders follow the torso, not the head
- vr.c keeps the torso's heading in LOCAL space (torso_update, after
  HaloCE-Quest-VR's vr_body_heading, CC0): the head's heading, steady at
  any pitch, drawn halfway towards the hands when both are ahead (at most
  45 degrees), followed beyond a 15 degree cone, eased at 10 a second;
  reset on a recentre or a gap. In LOCAL space the stick's snap and
  smooth turns carry it at once (HaloCE-Quest-VR's "arm IK lags as the
  body turns").
- halo_vr_body_frame gives the torso's heading and the neck the head
  turns about (9 cm behind the eyes); the shoulders hang from the neck,
  square to the torso, so turning the head no longer swings them (as the
  head faces the torso, where they were before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:26:24 +02:00
Stefan MaierandClaude Opus 5.5 f30dd50317 VR arms 3a.2: the fingers' input (touch, trigger, grip)
- host_xr: two actions, the thumb's touch (stick, buttons, thumb rest,
  the Frame's D-pad and View) and the index finger's (trigger), bound
  for the Touch, Index and Steam Frame profiles (paths after
  HaloCE-Quest-VR, CC0, and Valve's "Steam Frame Input" page) as optional
  bindings: a profile refusing them is suggested again without them.
- ABI: HALO_XR_BUTTON_THUMB_TOUCH, _INDEX_TOUCH and _TOUCH_KNOWN (the
  runtime gives the thumb's touch at all) in the buttons' free bits; the
  structures keep their size (android_abi_check: none unsafe).
- vr.c: each hand's thumb, index, middle and last two fingers, curled
  from those (the thumb half curled where touch is unknown, the hand
  round a held grenade), eased by HaloCE-Quest-VR's critically damped
  spring; halo_vr_finger_curls() hands them to the game; vr.fingers
  (default on) turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:24:58 +02:00
Stefan MaierandClaude Opus 5.5 07a15190b4 host_xr: suggest() takes up to 64 bindings, says when it cannot, and retries
suggest() copied a profile's bindings into an array of 32 and silently
dropped the rest; the Steam Frame's profile already has 26, so a few
more would have lost bindings without a word. It takes 64 now and logs
an error past that, logs the runtime's result when a profile is
refused, and suggest_bindings() takes optional bindings besides: should
the runtime refuse the profile with them (one unknown path refuses the
whole suggestion), it is suggested again without them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:23:11 +02:00
Stefan MaierandClaude Opus 5.5 f254be4b45 VR arms 3a.1: the first-person pose built once a frame, not per eye
Posed from the camera at the right controller, the first-person weapon
and the arms are the same for both eyes and the scope's view, yet were
built (animation, interpolation, arm IK) for each: twice a frame on
SteamVR, three times zoomed in. The pose is kept for the rest of the
headset's frame (halo_vr_frame_serial) once built from the controller,
for the same weapon; posed from an eye's own camera (the hand not
tracked), each eye still builds its own. vr.log_performance counts the
builds and the poses kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:22:45 +02:00
Stefan MaierandClaude Opus 5.5 a6350d207a VR arms 3a.0: time the first-person pose, log the hands' skin
- vr.log_performance reports how long the first-person pose takes a frame
  (its animation and the arms), how often it is built, and the arms' part
  of it (halo_vr_timing_start/_end).
- Once per hands model it logs how many triangles hang from each node and
  how many of those from the skeleton's root ('frame bone24'), read from
  the part's own buffers (rasterizer_model_buffer_data, after
  HaloCE-Quest-VR, CC0). In the emulator the cyborg's hands have none on
  the root: the ribbons of e377e4dc did not come from root skin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:22:08 +02:00
Stefan MaierandClaude Opus 5.5 1929702f5b Steam Frame CPU: Mesa's trace markers quiet, no glGet in the counters' copies; VR roadmap
- Valve's Mesa in Lepton writes a kernel trace marker around every flush
  and context switch; its descriptors on trace_marker are pointed at
  /dev/null once the GL context and the OpenXR session exist (after
  astromaddie/HaloCE-VR, CC0)
- host_gl_counters_snapshot/_read no longer ask GL for the copy bindings
  (each glGet waits for Mesa's GL thread): they are left unbound
- port/android/VR_ROADMAP.md: what is done and the agreed next steps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:58:04 +02:00
Stefan MaierandClaude Opus 5.5 9d6b980ba6 Merge upstream (OpenCE 9e4f8a2d): performance with many enemies, co-op fixes, anti-aliasing, per-pixel lighting, shadow resolution, reverb, password lobbies, network version 20
Conflicts: d3d8_gl.c (multiview variants kept beside the per-pixel lit
shaders: shader[4] and lit_shader[4], vertex_shader_get takes both;
multiview targets before multisampling; the picture read resolves both),
nv2a_vsh.c/xgpu.h (lighting and multiview parameters), render.c (no
anti-aliasing pass on VR eye passes), dsound_sdl.c, p2p.h.
VR: anti-aliasing off (the eyes go to the headset from their textures);
the VR online list skips password games (the server browser asks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:29:14 +02:00
Stefan MaierandClaude Opus 5.5 c04ce7fd00 Steam Frame: each eye drawn apart (no multiview on SteamVR)
With both eyes drawn at once (multiview) the eyes' pictures did not fuse
on the Steam Frame, now and then: each eye alone sharp, both together
doubled as the head turned, in the menus too. With vr.multiview = false
the user saw no doubling, at the same frame time (the Frame's time goes
to the GL driver either way). debug.multiview_steamvr brings multiview
back there for finding the cause; the Quest keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:54:53 +02:00
Stefan MaierandClaude Opus 5.5 3f06b1b9d8 Revert "Steam Frame: the layers' images acquired before the frame waits"
This reverts commit 24e583c0. With the images acquired before xrWaitFrame
the frames came fast (about 8 ms of work) but the picture doubled again
on the Steam Frame as the head turned (181 of 3753 frames reprojected);
without it (673b32b1) it did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:40:35 +02:00
Stefan MaierandClaude Opus 5.5 1b2d058a04 VR multiview: lens flares placed by their depth in each eye
A lens flare is projected on the CPU (w 1, its depth in z) and drawn
without the depth test, which multiview took for something at infinity,
as the sun's flares are. A weapon's own lights have flares too, at its
markers half a metre from the eyes (the plasma pistol's at its prongs,
the plasma rifle's at its muzzle): each eye saw its flare where infinity
puts it, a green ball at the barrel the eyes could not fuse. While flares
are drawn (rasterizer_lens_flares_draw), each eye's is now placed by the
flare's depth, as its occlusion test already was; the sun's are far
enough for infinity either way. Whether a flare shows at all depends on
its occlusion test, which is why the balls came and went, and showed on
the Frame more than on the Quest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:34:56 +02:00
Stefan MaierandClaude Opus 5.5 b5fa53ed95 Steam Frame: the layers' images acquired before the frame waits
With the copies deferred the wait only moved: acquiring the images took
8-10 ms a frame, the left eye's 6-8 of it. SteamVR's acquire (or its wait
for the image) waits for the GL work queued before it, the eyes' whole
drawing when the left eye's copy asked, the HUD's for the overlay. The
images the last frame filled are now acquired before xrWaitFrame, when
little is queued and the frame would wait for the headset anyway; one a
frame needs that was not acquired ahead is acquired as before. The
performance log shows the time acquiring ahead apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:34:56 +02:00
Stefan MaierandClaude Opus 5.5 1f2c10a3ca Menus' art: a texture outside the art's range is no menu bitmap
Every texture the renderer binds asks whether it is a menu bitmap
standing for its PNG (menu_art_texture), in a game too, where the PC
menus' bitmaps are loaded for the pause menu's settings: a walk over the
150-odd registered bitmaps for each texture of each draw. Their textures
lie together, so one lowest/highest check answers for the map's own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:11:05 +02:00
Stefan MaierandClaude Opus 5.5 12f1155752 Steam Frame: the headset's layer copies without waits in the frame
The Frame's game thread spent most of each 13.9 ms frame waiting, not
working: the left eye's copy into its swapchain took 4.5-7 ms, the right
one's right after it 0.7, each later copy 0.7-3. Every copy read the GL
state back to restore it (glGet*, glIsEnabled, the source's swizzle) and
released its image at once; with Mesa's GL thread (zink) each read waits
for the thread to run everything queued, each release for the GPU, so the
thread stood still for the eyes' whole drawing mid-frame, and frames came
late enough for SteamVR to reproject 2-8% of them (the doubled picture
when turning the head).

Deferred copies (SteamVR by default; debug.xr_deferred_copies 0 or 1 says
otherwise; the Quest's path is unchanged):
- nothing read back: a copy leaves GL as it drew and returns 2; vr.c has
  the renderer set its state again (d3d8_host_state_changed: the state
  cache invalidated, its vertex arrays bound again)
- no swizzle round trip: copies read render targets, which have none
- the images kept until the frame ends and released together before
  xrEndFrame; the eyes' work flushed to the GPU once they are copied
- the performance log times acquiring and releasing apart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:11:05 +02:00
Stefan MaierandClaude Opus 5.5 24e4123882 VR performance diagnostics: the frame's parts, the layer copies, Mesa's GL thread switch
- vr.log_performance: frames with game ticks and without, how many late,
  and their time by part (input, ticks, camera, draw, present)
- with XR_META_performance_metrics: per frame, each layer copy's time, the
  waits for swapchain images and xrEndFrame
- debug.gl_thread: Mesa's GL thread on or off (mesa_glthread) for trying

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:33:00 +02:00
Stefan MaierandClaude Opus 5.5 8466846d45 Steam Frame: the frames' thread kept on the fast cores
Without XR_EXT_performance_settings (SteamVR in Lepton) the scheduler may
put the game's busy thread on the 2.3 GHz cores; frames then come late to
the compositor and the picture doubles as the head turns, now and then,
at a steady 72 fps. On SteamVR the thread that ends the frames is kept on
the cores no slower than 90% of the fastest (HALO_XR_ANY_CORE: not); the
metrics line says which core it is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:53:47 +02:00
Stefan MaierandClaude Opus 5.5 bc580cf1cf VR: the compositor's performance counters in the log (XR_META_performance_metrics)
Every five seconds, where the runtime offers them (SteamVR on the Steam
Frame): to tell frames the compositor showed late or reprojected from the
game's own pace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:46:54 +02:00
Stefan MaierandClaude Opus 5.5 3d575f32ed Bots on foot: ramps, rails, door frames and things out of reach
- a way is clear only at the knees and the chest (a rail with room under
  it, a slit in a wall), and for a shortcut along a path at the shoulders
  too (a door frame)
- stuck below a near point (a step, a ramp's lip): a jump on towards it
- a thing to pick up it comes no nearer to for 2.5 s is given up
- stuck three times in five seconds: the goal given up, somewhere away
- at the point it walks to: arrived, not stuck

Emulator, Blood Gulch, 15 bots: CTF stuck events 264 -> 153, mid-field
26% -> 32%, kills 26 -> 33; team slayer stuck events 860 -> 560.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:33:16 +02:00
Stefan MaierandClaude Opus 5.5 727b9ca80d Bots on foot: no crowding at things out of reach, spread over the enemies, hold spots they can reach
- a thing to pick up that it stands by and is not offered (on a ledge,
  out of reach) is given up for 20 s; up to four remembered, in the
  fight too (the crowds jumping at the base's weapons)
- stuck: the side step goes to the open side
- goals: each bot one of the three nearest enemies, and beside them while
  far, so a team spreads out
- holding a spot: stops near it, and one it cannot reach falls back to
  the place's centre

Emulator, Blood Gulch, 15 bots: CTF stuck events 1793 -> 264; team slayer
stuck windows 32% -> 9-15%, time by the bases 85% -> 68-73%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:52:30 +02:00
Stefan MaierandClaude Opus 5.5 99d7c924db Bots: a person joining a lobby the bots fill takes a bot's place
The bots fill the game up to its maximum; a machine joining the lobby
keeps its place (no bot added for it) and its player takes the last
bot's; the game lists as open while a bot could leave.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:00:31 +02:00
Stefan MaierandClaude Opus 5.5 57d96e4681 VR PC menus: fixes from the first Frame test
- bots: no more than the game's MAXIMUM PLAYERS has room for beside the
  people (instead of raising it); the host options show and change the
  bots there are
- VR options hint on the PC main menu drawn whole (left-aligned)
- the online and host panels: a click beside them or on their key's B
  closes them, on the online panel's X enters a code
- spinners' arrows (drawn just outside them) clickable
- Android flat (the emulator): the mouse drives the menus' pointer, for
  testing the menus' clicks with adb shell input mouse
- Frame launcher: Lepton's logcat of the game's container in logcat.txt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:45:41 +02:00
Stefan MaierandClaude Opus 5.5 177ad4d40d VR: the PC version's menus (vr.menus, on by default)
- menu_tags.c builds the PC menus in VR too unless vr.menus = "xbox"
- their text fields (Server Setup's game name) typed on the Xbox's
  on-screen keyboard in VR
- join code / online panel on the PC Server Browser (X or click its hint),
  host options (Y or click) and join code on the PC lobby, VR options on
  the PC main menu
- bots: room made for them beside the people (Server Setup's maximum
  players), none in co-op
- player_name_valid accepts trailing spaces (Xbox menus pad new profile
  names, "New001     ", which the PC lobby refused)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:17:16 +02:00
Stefan MaierandClaude Opus 5.5 bfb668d498 Merge branch 'merge/upstream-2026-10-05'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:00:26 +02:00
Stefan MaierandClaude Opus 5.5 fc2b6551ca Merge upstream (OpenCE 13c14df9): online co-op campaign, online split screen, network version 16
Upstream's online co-op (network_actors.c, network_coop.c) ticks beside the
VR port's VR relay and bots; its message types end at 77, below the VR
port's from 240. The look and move flags the action test reads moved into
player_control_action_test_note: the head's turning goes there too. The
vertex shader's screen-space undo is upstream's on every platform now, with
the multiview eye offset after it on Android. The APK carries upstream's
brokers.txt beside the FDM layer stub; the updaters keep the VR port's own
repository (upstream's now points at OpenCommunityEdition/OpenCE).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:22:05 +02:00
Stefan Maier 5316a6b145 Merge branch 'frame/launcher' 2026-10-05 16:19:02 +02:00
Stefan MaierandClaude Opus 5.5 59b1fb422b Steam Frame: a library button that starts, updates and quits the game
The launcher gives Lepton what Steam gives an Android title of its own (a
compat data path, which the app's data links to after an install, and the
shader path), keeps a container id of its own, tries a start that ends at
once again (a new APK's first start only installs it), and stops when the
game was quit. Steam's "Exit game" cannot reach the game in Lepton's
container: the VR options get "Quit Halo" (asked twice), which on Android
now ends the process and leaves quit_requested.txt for the launcher. The
library's pictures are in port/android/steam_frame_grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:17:35 +02:00
Stefan MaierandClaude Opus 5.5 5ecd3075a0 Steam Frame: a launcher that starts the game as Steam starts Android titles
Lepton runs the APK in a container of its own, kept between starts, and
installs it anew whenever the APK is newer; the script starts it a second
time after a new APK's first start, which only installs it. With the APK in
~/Documents/HaloCE, updating the game is replacing that file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:17:54 +02:00
Stefan Maier 8e8713681d Merge remote-tracking branch 'origin/fix/multiview-desktop-compile' 2026-10-04 22:59:55 +02:00
Stefan Maier d7099f3e40 Fix desktop link: bot machine check and glClearDepthf were Android-only
network_game_server_is_bot_machine (cea7abfc, Bots as players of the
host) was written into the HALO_ANDROID block of network_server_manager.c
while bots.c calls it on every platform, and so was the array it reads;
both move out of the guard (on the desktop nothing marks a bot machine,
so the check is always false there, the writes stay Android-only).

glClearDepthf (dc21f4b4, Play in VR on OpenXR headsets) is ES-only:
GL_FUNCTIONS declares it for Android's loader and the define at the top
maps glClearDepth to it, but the desktop context links against GL's
glClearDepth. The clear now calls glClearDepth, which the Android define
turns into glClearDepthf as before.
2026-10-04 22:37:17 +02:00
Stefan Maier 4ba36d7305 VR: fix desktop build, targets_multiview was Android-only
6c3f4ce2 (VR: draw both eyes in one pass) changed prepare_draw to pass
targets_multiview to vertex_shader_get, but the variable is declared and
kept inside the HALO_ANDROID guard (L887): Linux and Windows failed to
compile with 'use of undeclared identifier targets_multiview', Android
was fine. On the desktop there are no multiview layers to stand in for
the back buffer, so the desktop call passes FALSE.
2026-10-04 22:25:21 +02:00
Stefan Maier 855f083117 Merge branch 'frame/performance' 2026-10-04 22:04:33 +02:00
Stefan Maier 6f3786de91 Merge branch 'fix/vr-shot-origin' 2026-10-04 22:04:33 +02:00
Stefan MaierandClaude Opus 5.5 493713b0a6 Steam Frame: the refresh rates offered in the log; its rate and speed noted
The runtime offers only the rate SteamOS is set to. At 72 Hz the game holds
it with multiview (about 11.6 ms a frame); at 90 Hz it cannot, and the
frames SteamVR makes up showed as a doubled picture when the head turned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:47:56 +02:00
Stefan Maier bf356fc58a Merge branch 'fix/vr-shot-origin' into frame/controllers 2026-10-04 21:42:21 +02:00
Stefan MaierandClaude Opus 5.5 6a7b919522 VR: shots fly along the aim, not above it
A shot fired from the muzzle of the weapon in the hand was kept inside the
level by the game's test of the line from the eyes to it, which in VR runs
down through the player's own body: the test hit it and moved the shot up
towards the eyes, from where it flew parallel to the aim, a few centimetres
above the crosshair and the laser (measured: up to 7 cm at 3 m). In VR the
line is tested against the level alone (a hand through a wall still does
not fire through it); measured after: the impacts on their rays, on
average within a millimetre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:42:07 +02:00
Stefan MaierandClaude Opus 5.5 c1305fa94f Steam Frame: its controllers as themselves, with the Xbox's layout
SteamVR offers the Frame's controllers by their own profile
(XR_VALVE_frame_controller_interaction); without bindings for it, it mapped
the Quest's onto them, and X and Y landed on the left one's D-pad. Now: the
right one's A, B, X, Y and menu button are the game's A, B, X, Y and Start
(the Xbox's layout), the left one's View is Back and its D-pad the D-pad,
the bumpers switch grenades and the flashlight. The VR options' page of
buttons shows the Frame's layout there. The runtime's extensions and the
controllers' profile are in the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:28:13 +02:00
Stefan MaierandClaude Opus 5.5 0fc85d6c67 Steam Frame: the overlays' alpha from their colour on SteamVR
SteamVR draws a quad's texels by their alpha alone, and the game's menus,
HUD and text have none in their additive parts: on the Frame nothing showed.
On SteamVR (the runtime's name says) the copy into the quads' images takes
their alpha from their colour where it is higher; HALO_XR_QUAD_ALPHA
(colour, keep) overrides, for trying. The Frame's notes updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:17:43 +02:00
Stefan MaierandClaude Opus 5.5 c0a08f395e Steam Frame: the game's data in the SteamOS user's Documents when it is there
Lepton empties the app's own folders (and uninstalls the app) whenever it
starts again; Documents is the SteamOS user's and kept. When
/home/steamos/Documents/HaloCE holds the maps and is writable, the launcher
and the game take it as the data folder (maps, config.toml, saves, logs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:51:08 +02:00
Stefan MaierandClaude Opus 5.5 06cb70fb58 Steam Frame: notes, and no variable the Android loader never reads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:27:28 +02:00
Stefan MaierandClaude Opus 5.5 1709698053 Start on the Steam Frame (Lepton)
Lepton has no clipboard service, and SDL3's activity died taking one for
granted: the build now patches SDL's clipboard handler to do without.
The Frame lists an implicit OpenXR layer for Vulkan apps whose library an
app cannot load, which failed xrCreateInstance (and the Android loader
reads no variable to turn it off): the app now carries a pass-through
layer under that library's name. With both, the game runs on the Frame
and its OpenXR session starts (SteamVR, 1728x1728 an eye, 72 Hz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:26:39 +02:00
Stefan Maier cb72dca40f Merge branch 'merge/upstream-2026-10-04' 2026-10-04 14:00:26 +02:00
Stefan MaierandClaude Opus 5.5 bbea2a2bff The online panel's game by its row past the fixed rows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:47:56 +02:00
Stefan MaierandClaude Opus 5.5 57cdbd8f80 Merge upstream (cybersecurity/halo-ce-universal 193cbf59): server browser, split-screen co-op
Upstream's server browser of public internet games (p2p_lobby.c) and the VR
port's own public list share the brokers: its topics are a table now, and
the VR port's (join codes' and public games') are entries of it; the VR
port's retained publishes carry MQTT 5's empty properties. The VR port's
public toggle lists the game in the server browser too, and the VR online
panel shows the server browser's games (PC, phones, VR) first, joined by
their invites, then its own list's that it has not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:47:29 +02:00
Stefan Maier c5c5a8eaeb Merge branch 'feature/bots-vehicles' 2026-10-04 13:33:07 +02:00
Stefan Maier a3ee36b5ef Merge branch 'feature/vr-hud-fixes' 2026-10-04 13:33:07 +02:00
Stefan Maier 30a1e0f40e Merge branch 'feature/vr-menu-lists' 2026-10-04 13:33:07 +02:00
Stefan Maier 327cf96d6f Merge branch 'feature/vr-options' 2026-10-04 13:33:07 +02:00
Stefan MaierandClaude Opus 5.5 867855d388 Bot drivers see walls and rocks ahead, back off when stuck, go round teammates
Driving a jeep or a scout, a bot looks ahead (farther the faster) along the
way it turns: a wall or a rock there (not ground it can drive up) is passed
on the most open side nearest its way, slower, and boxed in it backs off.
Stuck is now making no way along its heading for a second and a half, which
spinning wheels no longer hide. A teammate in the way is gone round slowly
on the other side instead of waited for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:25:15 +02:00
Stefan MaierandClaude Opus 5.5 4af570bc08 VR: the online games' hint further left and shorter, clear of Y = Create Game
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:13:27 +02:00
Stefan MaierandClaude Opus 5.5 7a1cf79396 VR: teammates' triangles over their heads, a click joins on the profile screen
The triangle the game draws over each teammate goes into the name's slot
over the head (shown in the world there, not on the HUD before the eyes,
where it hung distorted), under the name, or alone when names are off. On
the screen where controllers join by pressing A, a click is the pad's A. On
the screen of games to join, the online games' hint goes into the key bar's
free left and the join status under it, clear of the map's picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:00:41 +02:00
Stefan MaierandClaude Opus 5.5 9e7fd75f57 Bots: a vehicle left stuck is left alone a while; every third bot follows a person
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:41:40 +02:00
Stefan MaierandClaude Opus 5.5 6754403d68 VR: the menu pointer leaves the stick's choice be; clicks beside a list step it
A choice the stick moved to (a map among the tiles) is no longer taken back
by the pointer resting on another, until the pointer moves well away; and a
click beside a list's slots or a value's box, along the way it steps (where
its arrows are drawn), steps it back or on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:41:09 +02:00
Stefan MaierandClaude Opus 5.5 aedf5ff9bc Bot drivers wait for those coming to get in, and brake for teammates
Just in, a bot driver waits up to six seconds while a bot goes for its
vehicle or a person of its team stands near it with a gun's seat free; it
brakes (backing, if fast) for a teammate on foot in its way. A bot takes a
vehicle only when the nearest enemy (or the objective) is a long way off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:39:24 +02:00
Stefan MaierandClaude Opus 5.5 fe160e9dbe Bots man turrets while an enemy is about
A turret near a bot (15 world units) is manned while an enemy is within 35
of it, and left once there has been no one to shoot for ten seconds; at it
the bot aims and fires as at a vehicle's gun. The map's vehicles are named
in the log once a game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:23:49 +02:00
Stefan MaierandClaude Opus 5.5 12c97ffa85 Bots take vehicles: drive, man the gun, ride along; and race
A bot whose way is long takes an empty vehicle it can drive near it (a
Warthog, a Ghost, a Scorpion), walking to the driver's door and standing
there until the game offers the seat. Driving, it follows a vehicle's path
(from the vehicle's surface, wider, its steps further apart), turning a
Warthog or a Ghost by facing where it goes and pushing a Scorpion there from
where its gun faces; it runs an enemy down, circles one with a gunner
aboard, fires a Ghost's and a Scorpion's guns, backs off what it is stuck
on, roams between the map's spawn points, and gets out at the objective,
overturned, stuck again and again, or with an enemy close while it stands.
In a game of teams a bot mans the gun of a teammate's vehicle, a person's
first (or rides along with a person), aims and fires it, and gets out once
no one drives. Overturned vehicles near it are turned upright.

In a race a bot heads for the flag it is to touch next
(game_engine_race_next_flag), in a vehicle if it can have one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:48:59 +02:00
Stefan MaierandClaude Opus 5.5 0e6327cadf The controls page lists switching grenades (B held) on a line of its own
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:41:56 +02:00
Stefan MaierandClaude Opus 5.5 5f82df71f0 VR options: Y on the main menu or the pause menu
A panel of the VR settings one may change while playing (turning, snap
angle, smooth speed, the vehicles' comfort vignette, the Warthog's wheel,
the weapon laser, the crosshair, the motion tracker on the wrist), written
to config.toml, which vr.c reads again as they change; and a page of what
the controllers' buttons do, on foot, in vehicles and in the menus. The
pointer works both, and its hint opens the panel. The menus' screens are
named in the log as they come up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:37:10 +02:00
Stefan Maier cfdd821b45 Merge branch 'merge/upstream-v11' 2026-10-03 15:21:57 +02:00
Stefan MaierandClaude Opus 5.5 a9bbce5f7a Merge upstream (cybersecurity/halo-ce-universal c9ee319a): PC menus, network version 11
Upstream's PC menus are kept for the desktop; in VR the Xbox's stay, which
the VR port's join codes, game list, host options and keyboard pointer are
built into. config_write_integer now writes through upstream's config_write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:11:07 +02:00
Stefan Maier e94906100c Merge branch 'feature/warthog-wheel' 2026-10-03 14:58:32 +02:00
Stefan Maier 57add5ed71 Merge branch 'feature/vr-menu-pointer' 2026-10-03 14:58:32 +02:00
Stefan MaierandClaude Opus 5.5 770f8881e0 At the wheel, the right trigger is the throttle and the left the brake
Holding a Warthog's wheel, the triggers drive as the left stick pushed up
or down would (backwards once stopped), unless the stick is pushed further;
they neither fire nor hold a grenade then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:50:41 +02:00
Stefan MaierandClaude Opus 5.5 38ab192749 Steer a Warthog by its wheel with the hands
At an open vehicle's wheel, a hand that grips near it (where the driver's
drawn hands hold it) takes hold, and the hands turn it about its column:
both by the line between them, one about the wheel's middle. Turned
vr.vehicle_wheel_degrees (90) either way it steers fully; let go, the right
stick steers as before. Holding the wheel, the right grip is no melee.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:00:26 +02:00
Stefan MaierandClaude Opus 5.5 827eb0fc1d The keyboard gets the menu pointer's clicks; its BACK and ENTER hints click
The menus' mouse handling took the pointer's clicks each frame before the
keyboard asked for them, so the keyboard never saw one: with the keyboard
up the pointer is now the keyboard's alone. Clicking the hints under it
(B = BACK, A = ENTER, in its picture) cancels or finishes as Done does.
The pointer counts as moved past 3 pixels, not a hand's tremor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:47:51 +02:00
Stefan MaierandClaude Opus 5.5 a482a5f5bb The on-screen keyboard follows the menu pointer in VR
The key under the controller's pointer is picked and a click types it.
In the menus the triggers no longer reach the game as the gamepad's (they
click the pointer; on the keyboard they also moved its cursor).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:42:57 +02:00
Stefan MaierandClaude Opus 5.5 81f321f959 Of overlapping screen keys, the one whose icon is nearest on the left
A key's label is a text box often far wider than its text, reaching over
the next key (Back pressed Delete). Of the keys under the pointer, the one
whose icon begins nearest on its left is taken: each key from its icon to
the next one's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:30:20 +02:00
Stefan MaierandClaude Opus 5.5 f97ff9219f Menu pointer: narrower keys, the click where the trigger began to move
The screen keys' margin in VR was wide enough to reach the next key (Back
pressed Delete): now 2 pixels to the sides and 4 above and below, with a
label at most 25 pixels off. A click is taken where the controller
pointed as its trigger began to move (past a tenth), not a fixed time
before, which could still be on the key it just left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:27:28 +02:00
Stefan MaierandClaude Opus 5.5 85e79a067a Steadier menu pointer clicks in VR
A click is taken where the controller pointed a few frames before the
trigger went down (pulling it tips the controller), and in VR a screen
key's button takes a label up to 40 pixels off and a margin of 6 about it,
so the keys under the menus (A, B, Back) are hit as readily as items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:23:28 +02:00
Stefan MaierandClaude Opus 5.5 effeda1b7c Point at the menus with a controller in VR
The desktop builds' mouse in the menus (halo_ui_pointer.h) now comes, on
Android in VR, from a controller: where its aim's line meets the menus'
screen fixed in the room is the pointer, the trigger clicks (the hand
pressed last points; a click ticks its rumble), and a ringed dot shows
where it points. The item under it takes the focus and a click presses A,
as with the mouse; the online games and host options panels take it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:56:05 +02:00
Stefan Maier 6299503191 Merge branch 'feature/bots-team' 2026-10-03 11:10:25 +02:00
Stefan MaierandClaude Opus 5.5 c5d86c8d8b Bots spread out about their objective and keep to it
Each bot has a direction of its own (the golden angle on from the last):
far from a shared goal (the enemy's flag, the ball, the hill) it comes by
a way of its own, out to its side of the straight way, straight only for
the last 10 world units; holding a place it takes a spot of its own: on
the hill, about its flag's stand (every other guard ahead of it towards
the enemy's), about a teammate it escorts. With no way to its spot it
takes the point itself. A bot with an objective no longer leaves it in a
fight for a weapon close by.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:55:28 +02:00
Stefan MaierandClaude Opus 5.5 7242151017 Names over the heads at 60% of their size on the HUD
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:48:01 +02:00
Stefan MaierandClaude Opus 5.5 1c8e651bb1 Bots play with their team
In a game of teams a bot holds its fire while a teammate stands in the
line of it (and steps aside for a clear shot), takes on first the enemies
near its teammates (people above all), escorts a teammate carrying the
enemy's flag or the ball a few world units off on its own side, and, with
nothing to do, every other bot keeps near the nearest person on its team.
A goal it finds no way to three times running (a person up where only a
teleporter goes, a thing out of reach) is given up for a while, and a
roaming bot then goes somewhere near at random.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:41:08 +02:00
Stefan MaierandClaude Opus 5.5 4b8f8dde8b Players' names over their heads in VR, each on a quad of its own
The HUD in VR is a screen at one distance before the eyes, where the names
upstream draws on it hung. Each name is now drawn into a slot of a region
of the back buffer before the HUD (up to eight, one under another), taken
into a names layer, and shown on a quad of its own over its player's head,
facing the eyes, as wide as it would look on the HUD: in depth, for both
eyes. Who is named, and when, is as upstream has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:31:36 +02:00
Stefan Maier 920b1bfea9 Merge branch 'feature/vr-scope' 2026-10-03 10:18:30 +02:00
Stefan Maier 09f869e44c Merge branch 'feature/bots-modes' 2026-10-03 10:18:30 +02:00
Stefan MaierandClaude Opus 5.5 bc45338214 Bots get stuck in corners less
A path the game finds but cannot steer round the objects on it (the other
players among them: obstacle avoidance failed) is walked as smoothed,
instead of none (which had bots walk straight into walls); a step aside
settles a bump. A bot goes on to the next point of its path when it is
in plain view on its own level, walks the last 3 world units straight
only when nothing is in the way, and counts itself stuck when it makes no
way to its point for 6 ticks (only while it walks one): then it steps
aside, finds its way anew, skips a corner's point it cannot reach, jumps
at 12 and goes elsewhere later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:33:58 +02:00
Stefan MaierandClaude Opus 5.5 a663d1b100 Bots take good weapons in a fight too, and value strong ones more
Within 10 world units a bot goes for a weapon or powerup worth at least 4
while it fights. Strong weapons are worth a longer way (worth squared over
distance, falling off over 15 world units), and with two weapons one is
worth what it betters the worse by and 1.5 more, so rocket launchers and
sniper rifles are taken, not only the shotgun close by. The pistol counts
for a little less up close, the assault and plasma rifles more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:27:24 +02:00
Stefan MaierandClaude Opus 5.5 d8034d37fd Raise the rocket launcher's scope disc 3.5 cm more (18 cm)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:03:19 +02:00
Stefan MaierandClaude Opus 5.5 f8a39df0cd No names over the heads in VR for now
In VR the HUD is a screen at a fixed distance before the eyes, so the
names upstream draws on it hung there instead of over the players' heads.
Off until they are drawn into each eye's picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:03:10 +02:00
Stefan Maier 6057146244 Merge remote-tracking branch 'upstream/main' into merge/upstream-2026-10-03
# Conflicts:
#	port/linux/game/network_distributed.c
#	port/linux/game/network_distributed.h
2026-10-03 07:38:52 +02:00
Stefan MaierandClaude Opus 5.5 94b17a7913 Bots play capture the flag, king of the hill and oddball (phase 4)
The flags and balls (weapons that are flags) are gathered each tick with
where they are and who carries them. In CTF a bot carrying the enemy's
flag runs it to its own stand; the rest hunt whoever carries their flag,
touch it home where it lies dropped, or go for the enemy's (every third
guards its own). In king of the hill they go to the hill (the game's goal)
and keep moving in it. In oddball they go for the ball, taken as a weapon
is, hunt its carrier, and carrying it keep away. Carrying, a bot does not
fire, and looks where it runs. The last 3 world units to a goal are
walked straight, over what the path goes round (a flag on its stand).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:22:31 +02:00
Stefan MaierandClaude Opus 5.5 30f362ff3d Bots pick up weapons, powerups and grenades, and throw grenades (phase 2)
Out of a fight a bot looks every second for what lies within 25 world
units worth having: a weapon better than the worse of its two (it changes
to that one and holds the swap while the game offers the weapon), ammo for
an empty weapon it carries, an overshield, camouflage, a health pack when
hurt, grenades when it has few. In a fight it takes the weapon that suits
the distance (shotgun near, sniper rifle far, no rocket up close), at
most every two seconds, changes from an empty one, and now and then lobs
a grenade at an enemy 6 to 20 world units off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:41:48 +02:00
Stefan MaierandClaude Opus 5.5 23d14319a7 Raise the rocket launcher's scope disc 3.5 cm more
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:14:34 +02:00
Stefan MaierandClaude Opus 5.5 ff5b91f120 Raise the VR scope's disc over the rocket launcher's tube
The disc sits 7 cm over the hand, where a rifle's or pistol's scope is;
over the rocket launcher, carried higher on the shoulder, it was inside
the tube. A weapon may raise it (halo_vr_set_scope_raise): the rocket
launcher by 11 cm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:07:26 +02:00
Stefan MaierandClaude Opus 5.5 f6c2322df9 Host options in the lobby: public, bots and their skill
Y in a hosted game's lobby opens a panel to make the game public, choose
how many bots join (none by default, up to 15) and how well they play
(bots.skill: easy, normal, hard; hard is the first bots' play, normal a
little slower to notice, turn and aim). Integer settings can now be
written to config.toml as booleans could.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:05:34 +02:00
Stefan MaierandClaude Opus 5.5 39fbaefaac Keep the new settings out of the network table
The settings file is written in the table's order, so settings of another
section between network.* entries wrote [network] twice, and the whole
file was then refused (defaults used). They now follow the network
settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:52:14 +02:00
Stefan MaierandClaude Opus 5.5 c6397cb515 Bots find their way with the game's pathfinding (phase 1)
A bot walks to its goal along a path from ai/path.c (the maps' pathfinding
surfaces, as the campaign's actors use them), found again every half
second: in a fight towards its target, weaving; out of sight hunting the
nearest enemy, or roaming near someone; looking where it goes. With no
path it walks straight at the goal, and when stuck picks another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:05:12 +02:00
Stefan MaierandClaude Opus 5.5 5ba6c13097 Bots as players of the host (phase 0)
bots.count bots are added in the lobby as players of machines of the
host's own that nothing connects to (four a machine, from the top slot
down): new connections skip their slots, and they count as machines for
the countdown, so a game of the host and bots starts. Every machine sees
them as players; each tick the host thinks for them (bots.c) and takes
the result as their input through update_server_handle_distributed_input.
The first brain finds the nearest visible enemy, turns at a player's rate
with some aim error, walks at it weaving and fires on target, and else
wanders, jumping and turning when stuck. Every multiplayer map has
pathfinding surfaces for the next phase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:08:09 +02:00
Stefan MaierandClaude Opus 5.5 66c0d33e34 List public online games and join them from a panel
A host can make its game public (Y in the lobby, or network.public_game):
it then announces name, map, game type, players and join code every 10 s,
retained, on hcvr/1/g<network version>/<id> through the signalling
brokers, and clears it when it stops. On the system link list, X opens an
online games panel listing the public games heard lately (stale ones
dropped after 45 s) and a row to type a code; A joins by the game's code.
Announcements carry no addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:00:57 +02:00
Stefan MaierandClaude Opus 5.5 6b77fe428a Show how joining an online game goes on the system link list
p2p_join_state tells the menus whether the host is being reached, was
reached (its game is in the list), did not answer, could not be reached
(both routers too strict: allow UPnP) or was lost; the join screen shows
it under the code's status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:37:25 +02:00
Stefan MaierandClaude Opus 5.5 0badc451cb Join online games with a short code in VR
A hosted game gets an 8-character join code (Crockford base 32) besides
its invite link, shown under the lobby's buttons. On the system link list,
X opens the game's keyboard to type a code in; the code's topic and key
come from a slow PBKDF2 derivation, the host answers an ask on that topic
through the signalling brokers with its invite sealed with the key, and
the joiner joins as with the link. Codes in join_link.txt work too.

Also: the clipboard text last seen is kept from run to run, so a run's
own invite is not joined again at the next start; in menus a held stick
repeats after 350 ms and then every 90 ms, and its threshold is 70%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:32:50 +02:00
Stefan MaierandClaude Opus 5.5 39a6cc9794 Hold a VR player's relayed aim for a second
Where another machine's VR player aims and fires from is kept for 30
ticks after their hands last came (was 10), over networks that deliver
in bursts; a held grenade still lets go after 10, and a throw is taken
within its own 30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:36:34 +02:00
Stefan MaierandClaude Opus 5.5 35b90f26fc Send VR players' hands to the other machines
A new distributed message (_distributed_message_vr_poses, last in the
list so builds without it drop it) carries a VR player's controller aim,
muzzle offset, held grenade and last hand throw every tick: clients send
their own to the host, the host relays them with its own. Other machines
aim that player's weapon where the controller points, fire from the hand,
hold the grenade while the hand does and throw it as the hand threw it.
Damage stays the shooter's checked report; network version unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:58:52 +02:00
Stefan MaierandClaude Opus 5.5 02316c7f35 Merge upstream (92 commits) into the VR port
Upstream's own fixes replace two of ours: reclaiming ART's idle large
object space for the guest's fixed ranges (host_memory.c), and the audio
callback's gathering (host_sdl.c). Its consolidated headers take the VR
declarations (model_animation_definitions.h, render/render.h). The
updaters look at this repository's releases: the original's builds, made
without VR, must never replace this one (they share the package name).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:30:05 +02:00
Stefan MaierandClaude Opus 5.5 4e6b3f69f6 Merge feature/multiview: both eyes in one draw
GL_OVR_multiview2 (vr.multiview, on by default): the scene drawn once from
between the eyes into a two-layer target, each view's positions moved to
its eye in the vertex programs; the sky as though infinitely far, what the
game projects itself by its depth, screen reads from a composite of both
eyes. At one spot in 'Halo': both eyes 6.3 ms -> 4.0 ms, the frame's work
7.9 -> 5.8 ms. Also: no VR facing during cinematics, hands held still under
the system menu. Merged with reservations; one-time diagnostics log what
may still be off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:50:35 +02:00
Stefan MaierandClaude Opus 5.5 792531577c VR: no facing from the headset or controller during a cinematic
Seated in a level's opening cinematic (a passenger), the right controller
turned the game's camera the flat screen shows. The scripted-camera rule
went back out: the main menu is a scripted camera and was immersive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:47:12 +02:00
Stefan MaierandClaude Opus 5.5 ee35524e65 VR: scripted cameras on the flat screen; hands held still under the system menu
A scripted camera outside a cinematic (a level's intro) was drawn in
stereo from the head, with the weapon at the controllers in it; it now
goes to the screen like a cinematic. Under the system menu the headset
reports no hands and the weapon fell back to the head; the hands last
seen in focus stand in meanwhile, their buttons let go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:40:58 +02:00
Stefan MaierandClaude Opus 5.5 1713082296 VR multiview: log what may still be off (screen-space depth-tested draws, picture reads)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:31:41 +02:00
Stefan MaierandClaude Opus 5.5 ba3997e2f4 VR multiview: what the game projects itself gets its depth's parallax
Sprites, light volumes (a plasma bolt's glow) and lens flare tests are
projected on the CPU into screen pixels with w 1 and their depth in z, and
the widgets' vertex program takes them to clip space: taken for a point
1 world unit away, a bolt 30 away stood 3 m off in stereo and seemed to be
swallowed half way. Where w is 1 and depth is tested, the depth comes from
z by the shared camera's projection (mv_depth), and the eye's offset is by
it. (Also logs, once each, the other targets the pass draws into.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:24:49 +02:00
Stefan MaierandClaude Opus 5.5 19681e60d5 VR multiview: drop the test logging; performance log off again
Measured at one spot in 'Halo': both eyes 6.3 ms one by one, 4.0 ms at
once; the frame's work 7.9 ms against 5.8 ms (90 Hz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:31:02 +02:00
Stefan MaierandClaude Opus 5.5 353bd19d94 VR multiview: the back buffer read gets both eyes, the left over the right
The shared view reaches further right than the left eye: the sun there
was measured on a stale picture and its glow shrank as the head turned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:23:59 +02:00
Stefan MaierandClaude Opus 5.5 2104fb230e VR multiview: the back buffer read gets the left eye where it lies
The sun's glow measures the picture about the sun where the shared camera
has it; the left eye's picture, copied as it is, put the sun elsewhere
(a black patch on the rock, the sun through a gap as the head turned). It
is now laid where it lies in the shared camera's picture. Logs, once each,
the render targets drawing into both eyes reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:21:32 +02:00
Stefan MaierandClaude Opus 5.5 1302c8613c VR multiview: screen-space draws to each view as though far
A lens flare is drawn in screen space, at where the sun is in the shared
view, which is wider than an eye's: drawn alike in both layers, the sun
and its occlusion test landed elsewhere in each eye (the sky seemed to tear
open onto the sun). Screen-space positions now take each view's mapping
without the eye's offset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:13:47 +02:00
Stefan MaierandClaude Opus 5.5 df45daed55 VR multiview: the sky as though infinitely far
The sky is drawn about the camera (its view has no position), so no eye
stands aside of it; the eye's offset made it a dome a few metres away,
flat and swimming (the sea in 'Halo' is the sky's). Its draws take rows
without the offset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:52:40 +02:00
Stefan MaierandClaude Opus 5.5 d462f80fa9 VR: draw both eyes in one pass (multiview, first cut)
With GL_OVR_multiview2 (vr.multiview), the scene is drawn once, from a
camera between the eyes whose view takes in both (plus a margin), into a
two-layer back buffer: every NV2A vertex program gets a two-view variant
that moves each view's clip position to its eye (x' = a x + b w + c, from
the shared view's and the eye's edges and the eye's place; nv2a_vsh.c),
screen-space geometry excepted. Sampling the back buffer meanwhile reads
the left eye's picture; visibility counts are halved. The host copies each
layer to its eye's image (host_xr_copy_layer_array).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:37:28 +02:00
Stefan MaierandClaude Opus 5.5 c97a49050b Merge feature/vehicles: vehicles in first person
Seated first person that turns with the vehicle, right-stick steering,
controller-aimed guns (as by handles), own body at wheel and gun, eyes
raised over enclosing hulls, vehicle guns converging on the player's aim,
comfort vignette, X as the action button, and a vehicle-spawn test aid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:22:29 +02:00
Stefan MaierandClaude Opus 5.5 e820135027 VR: a Ghost's or Banshee's steering swings its aim less (40 degrees)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:16:21 +02:00
Stefan MaierandClaude Opus 5.5 0e3b634e77 VR: steering along a curve and eased in, for finer aim
The right stick's deflection past the dead zone is squared
(vr.vehicle_steer_curve) and eased in over ~0.12 s
(vr.vehicle_steer_smoothing): a Ghost's or Banshee's crosshair moves
finely near the middle and no longer leaps; full deflection still steers
the whole vr.vehicle_steer_degrees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:10:30 +02:00
Stefan MaierandClaude Opus 5.5 88f3412568 VR: a Banshee's pilot's eyes higher still
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:05:42 +02:00
Stefan MaierandClaude Opus 5.5 428d5f113a VR: vehicle guns converge on the player's aim again, crosshair there
The game steers a vehicle gun's shots (within its deviation cone) at what
the camera aims at; VR had replaced that camera with the barrel's own
line, right on foot but not seated, where a Banshee's nose rises as it
hovers and a tank's turret lags: the Banshee's crosshair rode high. Seated,
the camera is now the player's eyes and the unit's aim (the stick's steer,
the controller at a gun), and the crosshair floats on that line; the
barrel-line crosshair is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:02:39 +02:00
Stefan MaierandClaude Opus 5.5 40306f5f8c VR: a Banshee's pilot's eyes higher, over its back
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:58:24 +02:00
Stefan MaierandClaude Opus 5.5 73e9bed243 VR test aid: both sticks held in spawn the map's next vehicle
debug.vehicle_spawn: holding both thumbsticks in for 1.5 s puts the next
vehicle the map has loaded (not the Pelican, dropship or lifepod) a few
metres ahead of the player, cycling through them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:55:25 +02:00
Stefan MaierandClaude Opus 5.5 167ec9a573 VR: a manned vehicle gun's crosshair on the line it fires along
A vehicle's gun, once manned, fires along its own barrel (not the aiming
vector) from its primary trigger, moved onto the vehicle's camera line if
it fires from its camera. The crosshair ran from the right hand along the
controller instead, and the Scorpion's shells landed above and behind it.
It now floats on the gun's own line (weapon_vr_manned_gun_line), as the
turret actually points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:47:23 +02:00
Stefan MaierandClaude Opus 5.5 8756b1ba22 VR: raise the eyes over a hull by set heights only
The collision model's top lifted the Scorpion's driver onto the turret as
it swung over him (screenshots); it never caught anything the set heights
did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:33:14 +02:00
Stefan MaierandClaude Opus 5.5 043f2c6db6 VR: a Warthog's driver sits nearer the windscreen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:28:49 +02:00
Stefan MaierandClaude Opus 5.5 bcc40772f8 VR: the Scorpion's driver sees out over its hull
Like the Ghost's: the driver sits down in the hull (screenshot: the
compartment's inside). The raise applies to a vehicle's driver and gunner
only; riders on a tank's back sit on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:27:33 +02:00
Stefan MaierandClaude Opus 5.5 f761b082a9 VR: the aim passes through the player's own vehicle; gun eyes higher
The crosshair floated where the aim met the Shade's own neck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:12:04 +02:00
Stefan MaierandClaude Opus 5.5 f1b9dbeea8 VR: apply the hull's eye raise in the seated camera
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:06:19 +02:00
Stefan MaierandClaude Opus 5.5 75e7036b61 VR: eyes raised over a Ghost's hood, a Banshee, a Covenant gun's shield
Their collision models leave out what hid the view (screenshots: the
Ghost's hood arches over the rider's eyes, the Shade's shield sits at
them). The eyes now rise by a set height per kind of hull
(vr.ghost_eye_raise, vr.banshee_eye_raise, vr.turret_eye_raise), and at
least over the collision model's top.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:05:54 +02:00
Stefan MaierandClaude Opus 5.5 034842dc4d VR: no first-person weapon at any vehicle's controls
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:02:53 +02:00
Stefan MaierandClaude Opus 5.5 57ecc722c3 VR: no own body in a Ghost, a Banshee or a Covenant gun
Their rider bends down into the vehicle, and the eyes at his head looked
into his own shoulders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:59:55 +02:00
Stefan MaierandClaude Opus 5.5 4ab1661ac5 VR: see out over a Ghost's, a Banshee's or a Covenant gun's hull
Their riders sit down in the hull, drawn from behind in the game; from the
rider's head the view met its inside. The eyes now rise to above the hull's
top over the head and up to 50 cm ahead of it (a downward ray against the
vehicle's own collision model), plus vr.enclosed_seat_clearance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:52:35 +02:00
Stefan MaierandClaude Opus 5.5 4577b96fca VR: a vehicle's gun works as by its handles: tipped down, it rises
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:11:16 +02:00
Stefan MaierandClaude Opus 5.5 6146294562 VR: no crosshair at a wheel with no gun to aim
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:05:37 +02:00
Stefan MaierandClaude Opus 5.5 a6710f68ee VR: no first-person weapon at a wheel or a gun
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:55:29 +02:00
Stefan MaierandClaude Opus 5.5 2c32a7032c VR: own body at the wheel and the gun, gunner view back, clearer glass
Seated as a driver or gunner, the player's body is drawn (its hands on
the wheel or the gun's handles, as the seat animates them), its head
shrunk to nothing out of the eyes' way. A gunner's view is moved back from
the gun's sights (vr.gunner_camera_back). Glass tints and reflects half as
much (vr.glass_strength), which up close hid the view through the
Warthog's windscreen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:50:55 +02:00
Stefan MaierandClaude Opus 5.5 59b76697d2 VR: seated camera at the rider's head; X is the action button
Seated, the first-person camera stood where the seat's chase camera turns
about (the vehicle's middle, low); it is now at the rider's head marker.
X now does what the game's prompts say it does (action, reload); B takes
the flashlight (tap) and grenade switch (hold).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:29:08 +02:00
Stefan MaierandClaude Opus 5.5 e9e5984e11 VR: vehicles in first person, steered with the right stick
Seated, the view stays behind the player's own eyes (vr.vehicle_first_person)
and turns with the vehicle, smoothly between ticks (its interpolated
heading); the head looks around freely, and recentring faces the vehicle's
forward. A driver who steers by facing (jeep, boat, plane, scout, fighter)
steers with the right stick: aside up to vr.vehicle_steer_degrees off the
heading, up and down for pitch (vr.vehicle_pitch_degrees). Gunners, the
tank's driver and passengers aim with the right controller, and turn in
the seat with the stick. The crosshair and its distance follow the
vehicle's aim. While the vehicle turns the player, the eye images darken
from their edges (vr.comfort_vignette, host_xr_set_vignette).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:41:49 +02:00
Stefan MaierandClaude Opus 5.5 5430863919 Merge feature/grenade-throw: hold and throw grenades by hand
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:25:02 +02:00
Stefan MaierandClaude Opus 5.5 8ed3558fc7 VR: drop the grenade test logging
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:25:01 +02:00
Stefan MaierandClaude Opus 5.5 7f3d7ea804 VR: held grenade up the knuckles, to the middle of the palm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:23:08 +02:00
Stefan MaierandClaude Opus 5.5 ccf0a24e4b VR: held grenade towards the wrist along the drawn fingers
The grip's forward runs through the fist (towards the index finger), not
along the fingers; the step towards the wrist is taken along the drawn
hand's fingers (vr.grenade_back) instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:20:25 +02:00
Stefan MaierandClaude Opus 5.5 914de29d4c VR: held grenade further towards the wrist
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:17:08 +02:00
Stefan MaierandClaude Opus 5.5 66f16b4a07 VR: held grenade out of the palm a little, towards the wrist
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:14:00 +02:00
Stefan MaierandClaude Opus 5.5 4796df2b53 VR: held grenade back in the palm, moved down the knuckles instead
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:11:38 +02:00
Stefan MaierandClaude Opus 5.5 cf3e45050e VR: held grenade lower in the palm and out of it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:08:30 +02:00
Stefan MaierandClaude Opus 5.5 b97330e719 VR: draw the held grenade with the first-person hands
The hands are drawn over the world, so they hid the grenade wherever they
overlapped it, and turning the palm up showed it behind the hand. It is now
drawn in the first-person pass with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:02:48 +02:00
Stefan MaierandClaude Opus 5.5 0656429c11 VR: the held grenade in the palm at hand size; throws keep their speed
The held grenade is drawn in front of the left palm (vr.grenade_right,
_forward, _up from the grip) at 0.55 of its size (vr.grenade_scale).
A hand-thrown grenade is no longer brought up to the projectile's final
velocity, which made every throw as far; strength now runs from 0.25 to
full at 5 m/s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:58:02 +02:00
Stefan MaierandClaude Opus 5.5 2b988e10a9 VR: draw the held grenade in the left hand
The first-person view leaves out the player's body and what hangs from it,
the grenade in hand too. While the left trigger holds it, it is drawn at
the left controller each frame. Logs the grenade's hold and release for
testing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:49:12 +02:00
Stefan MaierandClaude Opus 5.5 dc4a724f8a VR: throw grenades by hand
The left trigger holds the grenade in the hand (the game keeps it there
past the throw animation); letting go throws it from the left hand, along
the fastest the hand moved over its last 150 ms, as hard as that was (full
at 4 m/s). Without a throw (vr.grenade_throw_speed, 1.5 m/s) the game
throws as before. The left hand's swing is no melee while throwing, and
the arms keep their pose through the throw animation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:37:43 +02:00
Stefan MaierandClaude Opus 5.5 506f95a8dd Merge feature/scope-zoom: scope zoom, floating crosshair, shots from the muzzle
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:58:44 +02:00
Stefan MaierandClaude Opus 5.5 aa1bc76916 VR: crosshair messages (reload, low ammo) stay in the HUD
Halo draws its centre prompts as crosshair elements of the weapon HUD.
Only the reticle states now go to the crosshair layer; the message states
are drawn in the head-locked HUD, also while zoomed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:52:06 +02:00
Stefan MaierandClaude Opus 5.5 74ad349bbf VR: draw the crosshair in its own pass, keep HUD messages whole
The crosshair used to be cut out of the HUD image (an 80x80 region around
the centre) and moved to where the weapon aims, taking with it whatever
else lay there, such as part of the reload prompt. Now it is drawn alone
first (hud_draw_crosshair), captured into its layer, and the HUD is drawn
afterwards without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:48:12 +02:00
Stefan MaierandClaude Opus 5.5 c8e64f3b2b VR: shots start from the crosshair's own point
Shots started at an offset from the unit's eye point (its sight
position), the crosshair at the same offset from the drawn camera; the two
need not agree, and the shots ran parallel to the crosshair's line, a few
centimetres higher (plain on the pistol's small crosshair). Both now start
at the same point in the world, the drawn weapon's muzzle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:37:35 +02:00
Stefan MaierandClaude Opus 5.5 69f4164f29 VR: aim assist traces along the weapon, so shots meet the crosshair
The projectile aim assist traces from the camera along the aim and turns
the shot, fired from the weapon, towards what that ray hits; in VR the
eyes' line runs parallel to the weapon's but higher, and shots landed
above the crosshair. For a VR player it traces and seeks targets from the
muzzle along the weapon's own line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:29:00 +02:00
Stefan MaierandClaude Opus 5.5 467c62b497 VR: one frame for the weapon's aim, model, crosshair and scope
- The weapon's frames are derived in one place: the controller's, the
  model's (turned so its barrel points where the controller does), and,
  with both hands on it, both turned alike by the turn that lines the
  model's grips up with the hands, measured in the model's frame (it was
  applied in the controller's, which tipped the aim up when gripping).
- The crosshair runs along that aim from the muzzle, as the shots do.
- The scope keeps its place above and ahead of the hand, moved only
  sideways over the barrel as drawn, facing back along the aim.
- Each weapon's barrel angle and gun node offset go to debug.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:22:26 +02:00
Stefan MaierandClaude Opus 5.5 e2954e6e71 VR: turn each weapon so its barrel points where it aims; scope on it
First-person models point their barrels in towards the screen's centre,
so the model pointed a few degrees left of where the controller aims and
shots, the crosshair and the scope went. The weapon camera is now turned
by the barrel's direction at the ready (the gun node's axis nearest the
camera's forward), which is logged per weapon, and the scope's disc sits
over the barrel as drawn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:10:57 +02:00
Stefan MaierandClaude Opus 5.5 c3b72b5848 VR: shots and the crosshair start from the weapon as drawn
Each weapon's first-person model sits differently in the hand, and the
pistol's barrel ran a few centimetres left of the controller's aim, so its
crosshair sat right of it. Shots, the crosshair and the laser now start at
the gun node as drawn at the ready, along the controller's aim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:11:25 +02:00
Stefan MaierandClaude Opus 5.5 917a2757ac VR: the crosshair where the weapon aims; the weapon stays zoomed in
- The weapon's own crosshair (red on an enemy, as in the HUD) is taken
  from the HUD's centre and floats where the weapon aims, as far away as
  what it aims at, facing the eyes, as large as the HUD showed it
  (vr.crosshair). The laser is off by default (vr.laser) and never shows
  over the menus.
- Zoomed in, the first-person weapon stays in the hand (the game hides
  it), the HUD's crosshair and zoom overlays give way to the scope, which
  has a fine cross of its own, and the scope's disc is 10 cm across.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:01:00 +02:00
Stefan MaierandClaude Opus 5.5 2581d7ee9c VR: a scope for the zoom, on a disc over the weapon
Zoomed in, the player's view is drawn once more, from the weapon along
its aim through the zoom's field of view, into a small square of the
back buffer (the first-person weapon left out), and shown on a disc over
the barrel that magnifies what it covers of the view by the zoom's
magnification (vr.scope_size). The weapon's zoom mask and filters no
longer darken the HUD before the eyes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:16:30 +02:00
Stefan MaierandClaude Opus 5.5 37abf500b8 Merge feature/motion-tracker: the motion tracker on the left wrist
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:53:41 +02:00
Stefan MaierandClaude Opus 5.5 e903665fa4 VR: the wrist's motion tracker placement, as tuned in the headset
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:53:41 +02:00
Stefan MaierandClaude Opus 5.5 2bce077c68 VR: the wrist's motion tracker turns and moves by settings
vr.motion_tracker_turn, _right and _down place it as the wrist turned to
the eyes reads it; by default 22 degrees clockwise, 1.5 cm right and 3 cm
down from before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:49:19 +02:00
Stefan MaierandClaude Opus 5.5 0159deeb0e VR: turn the wrist's motion tracker to read the right way up
Its top now runs towards the elbow, as the wrist turned to the eyes reads
it (the V at the top), and it sits 3 cm nearer the fingers and 2 cm down
the handle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:45:23 +02:00
Stefan MaierandClaude Opus 5.5 07dbf079ef VR: the wrist takes just the motion tracker; no log lines in the HUD
- The part of the HUD moved to the wrist is now just around the motion
  tracker's disc and its range (measured in a 640-wide HUD), larger on the
  wrist and without what else the corner holds.
- The terminal's log lines, which the beta prints over the bottom left
  of the screen, are left out in VR; the console, when open, still shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:07:54 +02:00
Stefan MaierandClaude Opus 5.5 abdd0ce2e5 VR: the motion tracker on the left wrist
The HUD's motion tracker, its one element at the bottom left, is copied
from the HUD overlay into a layer of its own and cleared from the HUD. The
host shows that layer on the back of the left wrist, like a watch, posed
from the left controller each frame, while the wrist is turned towards the
eyes (vr.motion_tracker_on_wrist, vr.motion_tracker_size).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:59:54 +02:00
Stefan MaierandClaude Opus 5.5 87f552ea5e VR: the laser follows the barrel while a reload turns the weapon
While an animation turns the weapon away from where it aims (reloads,
drawing and putting it away, posing, venting), the laser follows the gun
node's turn from its ready pose. The ready pose is now kept only while
the weapon is held ready or firing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:49:04 +02:00
Stefan MaierandClaude Opus 5.5 8fe34719f2 VR: the right grip's melee attack jabs the weapon
With the game's melee animation held off, an attack on the right grip
showed no movement at all; the weapon now jabs forward and back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:42:10 +02:00
Stefan MaierandClaude Opus 5.5 e0b71215c7 VR: through a melee attack, keep the whole pose in the hand
The melee animation moves both hands about the gun (the right one to the
stock, the left one onto the barrel) and the left arm with them; holding
only the gun still showed that. The whole skeleton now keeps its pose
from before the attack, in the hand. The melee diagnostics are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:31:29 +02:00
Stefan MaierandClaude Opus 5.5 1ae451d3cb VR: melee damage from the swing's speed
The game scales a player's melee damage by the forward run (the lunge),
and a VR player punches standing: blows hit but did next to nothing. The
swing's speed now weighs the blow (half at 2 m/s, full at 4; three
quarters on the right grip), unless the run gives more. Melee attacks and
what they hit are logged in debug.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:21:47 +02:00
Stefan MaierandClaude Opus 5.5 2661e26066 VR: melee attacks aim where the head looks until they land
A melee attack's damage is dealt some ticks into it, along the unit's
aim, which was back on the right controller by then. The head aims it for
the whole attack, whether swung or on the right grip; the swing threshold
is 2 m/s again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:20:34 +02:00
Stefan MaierandClaude Opus 5.5 8de21c2ea0 VR: the free left upper arm points at the shoulder
The free left arm keeps its shape on the foregrip, which left its upper
arm pointing anywhere (at the belly, looking down); it now turns about the
elbow to point back at the shoulder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:15:12 +02:00
Stefan MaierandClaude Opus 5.5 b4f3afdc9b VR: the free left arm keeps its shape; punches land where you look
- Bent to the shoulder, the free left arm's skin, bound partly to the
  skeleton's root at the other hand, tore into ribbons wherever the hand
  was far from where the animation has it. The whole arm now follows the
  hand as it is shaped on the foregrip; the shoulder IK is kept for the
  right arm and the left one on the weapon.
- A melee attack aims where the head looks (the right controller aims
  the weapon), and each one is logged; vr.melee_swing_speed is 1.5 m/s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:10:03 +02:00
Stefan MaierandClaude Opus 5.5 a24f615b4e VR: twist the forearm with the hand
The forearm, turned only to point at the wrist, kept the animation's
twist while the free hand could roll half a turn from it, and the skin
between them collapsed into a ribbon. It now takes most of the hand's roll
about its bone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:49:44 +02:00
Stefan MaierandClaude Opus 5.5 62f451d8dc VR: a natural free left hand, melee by swinging
- The free left hand lines up with the controller as a hand holds it:
  fingers along it, the knuckles (little finger to index) up its handle,
  the wrist behind its grip.
- Thrusting a hand forward (vr.melee_swing_speed) is a melee attack; the
  right grip still is too.
- Through a melee attack the gun stays in the hand; its animation would
  swing it before the camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:42:05 +02:00
Stefan MaierandClaude Opus 5.5 1882ae06bf VR: arms that follow the hands, the left hand free or on the weapon
- The first-person arms' upper arms and forearms are turned (two-bone
  IK) to run from the player's shoulders, below the head, to the wrists,
  instead of floating with the weapon.
- The left wrist follows the left controller, its fingers with it,
  unless the grip closes at the weapon's foregrip (vr.two_hand_reach):
  then the hand holds the weapon, which aims along both hands.
- Left controller: grip holds the weapon, Y switches weapons, X is the
  flashlight, and held switches grenades.
- The first-person skeleton's nodes are logged once per weapon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:25:07 +02:00
Stefan MaierandClaude Opus 5.5 fff0df8d86 Android: fix audio, which never played; VR: wider look-at tests
- SDL 3.4 holds an audio stream's lock through its callback, and the
  game's data was put into the stream from the thread that runs the
  game's callback, which then waited for that lock for ever: the first
  callback never returned and nothing was heard. The data is now
  gathered and put in by SDL's own thread once the game is done.
- debug.audio_log logs the level of the mixed sound.
- In VR a player's unit sees what its head points at within 15 degrees,
  or what the weapon's laser points at, since the head only points
  roughly where the eyes look (the training's flashing lights ask for
  5 degrees).
- vr.log_performance also logs the unit's looking vector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:43:16 +02:00
Stefan MaierandClaude Opus 5.5 bdd6ad3fc8 VR: the unit looks where the head looks
Scripts test where the player looks (unit_can_see_point, the unit's
looking vector), and the head only set it while the game let the player
turn; the training's "target each of the flashing lights" never passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:24:43 +02:00
Stefan MaierandClaude Opus 5.5 a4f4bbb6f0 VR: turning the head counts as looking around
The training at the start of the campaign waits for the look input
(player_control.c's action tests, from facing_delta), which the head
never produced. The view's turning since the last test counts too, once
past a few degrees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:14:03 +02:00
Stefan MaierandClaude Opus 5.5 282991fc07 VR: aim with the right controller, the weapon in the hand
With vr.aim_with = "controller" (the default), on foot:
- the unit's aiming vector comes from the right controller, while its
  facing (the body, the view and the movement) keeps following the head;
- shots start at the muzzle of the weapon in the hand, kept inside the
  level from the camera as before;
- the first-person weapon and hands are posed from the controller
  (vr.weapon_forward/right/down place the model in the hand);
- the HUD's crosshair, which marks where the head looks, is left out, and
  a laser runs from the weapon to what it aims at (vr.laser).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:01:53 +02:00
Stefan MaierandClaude Opus 5.5 a7bc454563 VR: recentre on entering play, limit the head's offset, log performance
- The view recentres (the head's position becomes the game camera's, its
  heading the player's facing) whenever play starts after menus, loading
  or a cinematic, and when the headset is recentred.
- The head's offset from the body is limited (vr.lean_limit,
  vr.crouch_limit); vr.positional_tracking turns it off.
- vr.log_performance logs the frame rate and the time spent drawing the
  eyes and waiting for the headset every five seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:51:27 +02:00
Stefan MaierandClaude Opus 5.5 d331212729 Stop stalling on the GPU on Adreno (Quest): 2 fps to 90 fps
Adreno's driver waits for the GPU in every glMapBufferRange, unsynchronized
or not, and the renderer mapped a buffer for every streamed draw (several
hundred a frame), every visibility counter reset and every mirrored page
run, and read every lens flare's count back mid-frame.

- Give the stream and index rings, the visibility counters and the mirror
  segments persistent, coherent storage (GL_EXT_buffer_storage) where the
  driver has it; uploads into them are memcpys. The frame fences already
  keep writes away from what the GPU still reads.
- Read the visibility counts from a snapshot copied at the end of each
  frame and read once the GPU has finished it, a few frames late.
- Ask the OpenXR runtime for sustained-high CPU and GPU levels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:30:28 +02:00
Stefan MaierandClaude Opus 5.5 13c0ff9368 Fix VR startup on Quest 3
- Enumerate the swapchain formats with the two-call idiom: Meta's runtime
  lists more than 64.
- Probe for the headset after loading the guest image, so the runtime's
  libraries cannot land on the guest's fixed ranges.
- On Horizon OS, ART's free-list large object space (512 MB, from
  dalvik.vm.heapsize) covers 0x80000000; take the Xbox window and the image
  range from its top, which ART fills only from the bottom.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:10:55 +02:00
Stefan MaierandClaude Opus 5.5 ce59138a2b Play in VR on OpenXR headsets (Quest 3/3S, Steam Frame)
The Android port gets an OpenXR session in the host (Khronos loader, core
OpenXR plus XR_KHR_opengl_es_enable): on a headset the game keeps its
640x480 layout drawn at the eye images' resolution, renders the player's
view once per eye from the tracked eye poses through asymmetric frusta,
and shows the HUD and menus as a head-locked or room-fixed quad layer.
The head aims the player; the right stick turns; the Touch controllers
map onto the Xbox pad; rumble goes to the controllers. Frames without a
player view (menus, loading, cinematics) go onto a screen in the room.

A marker file, xr_test, runs a self-test of the session, layers, colour
path and controllers without game data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 09:17:19 +02:00
Stefan MaierandClaude Opus 5.5 07de435468 Build the Android port on macOS hosts
Use the NDK's prebuilt toolchain for the build host instead of assuming
linux-x86_64, and make the StdDef.h wrapper use #include_next so that it
does not include itself on case-insensitive file systems.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:57:47 +02:00
139 changed files with 27836 additions and 710 deletions

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
name: Problem report
description: Something does not work as it should
body:
- type: markdown
attributes:
value: |
Thanks for reporting! Please do **not** ask for, link to or attach game files or disc images; such issues are closed and deleted.
- type: dropdown
id: headset
attributes:
label: Headset
options:
- Meta Quest 3
- Meta Quest 3S
- Steam Frame
- Other
validations:
required: true
- type: input
id: build
attributes:
label: Build number
description: The release you installed, e.g. "build 180".
validations:
required: true
- type: textarea
id: what
attributes:
label: What happened
description: What you did, what you expected and what happened instead.
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: |
Attach `debug.txt`. Quest: `Android/data/io.github.cesarus85.ringfall/files/debug.txt` (SideQuest's file manager). Steam Frame: `Documents/RingfallMjolnirVR/debug.txt`, and `logcat.txt` and `install.log` beside it.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: How to install
url: https://github.com/Cesarus85/mjolnir-vr#install-on-meta-quest-3--3s
about: Step-by-step installation for the Quest 3 / 3S and the Steam Frame.
+54 -44
View File
@@ -13,6 +13,9 @@ name: Build
# - a newer push to the same branch cancels a run still going;
# - pushes that only change documentation do not build;
# - Windows, whose minutes cost twice Linux's, builds only Windows;
# - Mjolnir VR builds only Android (the VR app): the Linux and
# Windows jobs are kept, off (if: false), so that upstream's changes to
# them still merge;
# - artifacts are kept for three days.
#
# A successful build of main is also published as a GitHub release, the
@@ -21,7 +24,10 @@ name: Build
# gives the latest build; the last five are kept. Its web build is then the
# site on GitHub Pages (https://halocombatevolved.com/, port/web/README.md).
on:
# (Mjolnir VR: main only; its branches are tried on the headsets
# from local builds)
push:
branches: [main]
paths-ignore:
- "**.md"
workflow_dispatch:
@@ -46,6 +52,8 @@ env:
jobs:
linux:
# (off: Mjolnir VR ships the Android app only)
if: ${{ false }}
# Arch Linux has what the build needs as packages: clang 22 (for the
# profile-guided optimisation in pgo/), and CMake, pkg-config and
# wayland-scanner for the SDL 3 that the portable build brings. The build
@@ -91,10 +99,7 @@ jobs:
- name: Test
run: |
python -m pytest -q tools/test_linux_port.py tools/test_android_asm_convert.py tools/test_android_mirror_uploads.py tools/test_android_ce_texture_cache.py tools/test_host_watch_hash.py tools/test_touch_menu.py tools/harness tools/test_profile.py tools/stagekit/tests tools/test_net_compare.py tools/test_server_sim.py tools/test_egg.py tools/test_deploy_gametypes.py
# the dedicated server: --version, and --server without maps refused (exit code 2); its cases with
# clients need the retail maps, and are skipped
python -m pytest -q tools/test_dedicated_server.py
python -m pytest -q tools/test_linux_port.py tools/test_android_asm_convert.py tools/test_android_mirror_uploads.py tools/test_android_ce_texture_cache.py tools/test_host_watch_hash.py tools/test_touch_menu.py tools/harness tools/test_profile.py tools/stagekit/tests tools/test_net_compare.py
python tools/test_light_storage.py
- name: Compile cache
@@ -198,6 +203,8 @@ jobs:
retention-days: 3
windows:
# (off: Mjolnir VR ships the Android app only)
if: ${{ false }}
# The runner image has LLVM, Python and Visual Studio's x86 libraries and
# Windows SDK.
runs-on: windows-2025
@@ -272,6 +279,8 @@ jobs:
retention-days: 3
web:
# (off: Mjolnir VR ships the Android app only)
if: ${{ false }}
# Emscripten (emsdk, the version below, cached) builds the web port's
# site (port/web/README.md); the tests check its graph, the page's
# importer and service worker, its sockets, that it starts in a
@@ -320,8 +329,6 @@ jobs:
run: |
source ~/emsdk/emsdk_env.sh
node --test "port/web/tests/*.test.js"
# (the dedicated server's admin page, port/assets/server_ui, against tools/server_sim.py)
node --test tools/server_ui_tests/
python3 -m pytest -q tools/test_web_build.py
- name: Test in Chromium and Firefox
@@ -354,12 +361,9 @@ jobs:
# The eight builds (and the Windows builds' PDBs) as a release, tagged
# build-<run number> and marked latest
# (every file keeps its name from release to release, for the README's
# latest/download links), with their SHA256SUMS and, when the
# RELEASE_SIGNING_KEY secret is set, its Ed25519 signature by the release
# key (SHA256SUMS.sig; the public key is deploy/release-key.pub); then all
# but the newest five build-* releases go, tags and all. Only for main,
# and only when every platform built.
needs: [linux, android, windows, web]
# latest/download links); then all but the newest five build-* releases
# go, tags and all. Only for main, and only when every platform built.
needs: [android]
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-24.04
timeout-minutes: 15
@@ -371,14 +375,11 @@ jobs:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
# (tools/ci_build.py and tools/release_sign.sh, and the public key)
- uses: actions/checkout@v4
with:
sparse-checkout: |
tools/ci_build.py
tools/release_sign.sh
deploy/release-key.pub
sparse-checkout-cone-mode: false
port/android
tools
- uses: actions/download-artifact@v4
with:
@@ -388,28 +389,28 @@ jobs:
- name: Package
run: |
mkdir release
for build in artifacts/*/; do
name="$(basename "$build")"
(cd "$build" && zip -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
# (Mjolnir VR publishes its Android builds only, under
# its own name: Updater.java looks for them; the desktop ports are
# built to keep them building)
for config in debug release; do
(cd "artifacts/halo-android-$config" &&
zip -qr "$GITHUB_WORKSPACE/release/mjolnir-vr-android-$config.zip" .)
done
python3 tools/ci_build.py sums release
# the Steam Frame's installer (port/android/STEAM_FRAME.md): its
# bundle with the release APK, and the desktop file that fetches it
python3 tools/steam_frame_bundle.py artifacts/halo-android-release/app-release.apk release "$GH_REPO"
# the APK itself, for sideloading on the Quest (SideQuest, adb)
cp artifacts/halo-android-release/app-release.apk release/MjolnirVR.apk
# the same under the names before "Mjolnir VR" (Ringfall: Mjolnir
# VR, builds 302 to 313): their updaters (Updater.java, the Frame's
# launcher) and the links already shared ask for them
cp release/mjolnir-vr-android-debug.zip release/ringfall-mjolnir-vr-android-debug.zip
cp release/mjolnir-vr-android-release.zip release/ringfall-mjolnir-vr-android-release.zip
cp release/mjolnir-vr-steam-frame.zip release/ringfall-mjolnir-vr-steam-frame.zip
cp release/Install-Mjolnir-VR.desktop release/Install-Ringfall-Mjolnir-VR.desktop
cp release/MjolnirVR.apk release/RingfallMjolnirVR.apk
ls -l release
# SHA256SUMS signed with the release key's seed (the RELEASE_SIGNING_KEY
# secret, 64 hex digits), and the signature checked against
# deploy/release-key.pub before anything is published: a secret that is
# not the key of the public key in the repository stops the release
- name: Sign the sums
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
run: |
if [ -z "$RELEASE_SIGNING_KEY" ]; then
echo "::warning::no RELEASE_SIGNING_KEY secret: the release's SHA256SUMS is not signed"
exit 0
fi
sh tools/release_sign.sh sign release/SHA256SUMS
sh tools/release_sign.sh verify release/SHA256SUMS
- name: Publish
id: publish
env:
@@ -419,10 +420,18 @@ jobs:
run: |
tag="build-$GITHUB_RUN_NUMBER"
subject="$(printf '%s' "${MESSAGE:-manual run}" | head -n 1)"
printf '%s\n\nCommit: %s\nBuild: %s\n\nThe release builds are for playing; the debug builds stop at the first failed assertion and log it.\n' \
"$subject" "$COMMIT_URL" "$RUN_URL" > notes.md
readme="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
{
printf '%s\n\n' "$subject"
printf '**Mjolnir VR** is an unofficial VR mod for Halo: Combat Evolved. It needs a disc image of your own, purchased Xbox copy of the game; no game files are included.\n\n'
printf '| Headset | Download | How to install |\n| --- | --- | --- |\n'
printf '| Meta Quest 3 / 3S | `MjolnirVR.apk` | [Install on Quest](%s#install-on-meta-quest-3--3s) |\n' "$readme"
printf '| Steam Frame | `Install-Mjolnir-VR.desktop` (open it in Desktop Mode) | [Install on Steam Frame](%s#install-on-steam-frame) |\n\n' "$readme"
printf 'The other files are for the built-in updater and the Steam Frame installer (those named `ringfall-…` / `Ringfall…`: the same, for the builds before the name Mjolnir VR).\n\n'
printf 'Commit: %s\nBuild: %s\n' "$COMMIT_URL" "$RUN_URL"
} > notes.md
gh release create "$tag" release/* --target "$GITHUB_SHA" --latest \
--title "Build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
--title "Mjolnir VR, build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
echo "tag=$tag" >> "$GITHUB_OUTPUT"
# The Windows builds' executables and PDBs to Sentry, which turns the
@@ -464,13 +473,14 @@ jobs:
done
pages:
# (off: Mjolnir VR ships the Android app only)
if: ${{ false }}
# The release's web build to GitHub Pages, which serves it at
# https://halocombatevolved.com/ (port/web/README.md, "Hosting"). Only
# main, only once every platform built and the release is out (the site
# and the release are one build), and a deployment is never cancelled
# halfway. The repository's Pages source must be GitHub Actions.
needs: [release]
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
@@ -541,8 +551,8 @@ jobs:
# A Discord embed for the run, to the DISCORD_WEBHOOK secret (nothing
# without it): green when every platform built, red when one did not,
# with each platform's result, this run's release (main only), and links
# to the latest release's builds and the site.
needs: [linux, android, windows, web, release, pages]
# to the latest release's builds.
needs: [android, release]
if: ${{ !cancelled() }}
runs-on: ubuntu-24.04
timeout-minutes: 5
@@ -578,13 +588,13 @@ jobs:
*) echo "➖ Skipped" ;;
esac
}
if [ "$LINUX" = success ] && [ "$ANDROID" = success ] && [ "$WINDOWS" = success ] && [ "$WEB" = success ]; then
if [ "$ANDROID" = success ]; then
title="Build succeeded"; color=5763719
else
title="Build failed"; color=15548997
fi
latest="$SERVER/$REPOSITORY/releases/latest"
downloads="[Linux]($latest/download/halo-linux-release.zip) · [Windows]($latest/download/halo-windows-release.zip) · [Android]($latest/download/halo-android-release.zip) · [Web]($SITE_URL) ([all builds]($latest))"
downloads="[Quest]($latest/download/mjolnir-vr-android-release.zip) · [Steam Frame]($latest/download/Install-Mjolnir-VR.desktop) ([all builds]($latest))"
if [ -n "$RELEASE_TAG" ]; then
release="[$RELEASE_TAG]($SERVER/$REPOSITORY/releases/tag/$RELEASE_TAG)"
if [ "$PAGES" = success ]; then
+5
View File
@@ -30,3 +30,8 @@ original/
# CI build outputs (tools/ci_build.py)
dist/
# The app signing key's local settings and keys (port/android/app/build.gradle; never commit)
port/android/signing.properties
*.jks
*.keystore
+217 -317
View File
@@ -1,327 +1,227 @@
# OpenCE - Halo: Combat Evolved for Linux, Windows, Android and the web
<p align="center">
<img src="port/assets/icon/opence-icon.png" alt="OpenCE" width="128">
<img src="port/android/steam_frame_grid/library.png" width="640" alt="Mjolnir VR">
</p>
<p align="center">
<a href="https://github.com/OpenCommunityEdition/OpenCE/releases/latest"><img src="https://img.shields.io/github/v/release/OpenCommunityEdition/OpenCE?label=Download&style=for-the-badge" alt="Download"></a>
<a href="https://halocombatevolved.com/"><img src="https://img.shields.io/badge/Play-in_browser-blue?style=for-the-badge" alt="Play in browser"></a>
<a href="https://discord.gg/9gqcHyr5km"><img src="https://img.shields.io/badge/Discord-join-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord"></a>
</p>
**OpenCE** (Open Community Edition) is an open source port of the Halo: Combat Evolved decompilation (Halo CE) based on an early Xbox development build (2342) and forked from
[bnunu/halo-1](https://github.com/bnunu/halo-1). The project aims to enhance the existing game and adds many features, such as 128-player multiplayer, online campaign co-op, mod packs, field-of-view settings, a server browser, peer-to-peer hosting, automatic map downloads, voice chat, support for Custom Edition maps and much more!
The game runs natively on **Windows, Linux, Android** and in the **web browser** with nothing to install. A macOS port is in progress. Worth noting that it is not an emulator, **the game's own code is compiled for each platform** with a new layer in place of the Xbox's hardware. Runs on OpenGL (Android is on OpenGL ES and WebGL 2 in the browser). Sound, input, gamepads are all handled through SDL3. The game currently draws a frame every refresh of your display with Smooth Motion enabled, while the simulation runs at 30 ticks just like the original Xbox.
<img width="1284" height="1007" alt="The game running on Windows" src="https://github.com/user-attachments/assets/0a4412e1-f6d3-48ed-8e17-0a374c2122f1" />
<p align="center"><em>OpenCE running on Windows (release build)</em></p>
## A copy of Halo: Combat Evolved is required
OpenCE does not include the game data. You **must provide your own Xbox disc image** (`.xiso` or `.iso`) of Halo: Combat Evolved. Every version of the game works. The maps of the European (PAL) version were made for a slower console - the game changes them to play as the North American (NTSC) maps do, so players of the two versions can play together.
1. Start the game (`halo.exe` on Windows and `halo` on Linux).
2. At the first start you will get a pop up message asking for the disc image. Select it in the
file picker. On Linux and Windows, a disc image (`.iso` or `.xiso`) next
to the executable is used without the file picker. Where no file picker
can be shown (on Linux, without the desktop portal or `zenity`), the game
says to put the disc image next to the executable, and stops.
3. The game extracts the `maps` folder. Then the game starts.
The `maps` folder holds the Xbox maps only. If it holds Halo PC's maps
instead (a Halo PC or Custom Edition install's `maps` folder), the game says
so and stops: Custom Edition maps go in `custom_maps` folder, beside `maps` folder.
On Linux and Windows, the game puts `maps` next to the executable. On
Android, copy the disc image to the phone first. The app puts `maps/` in its
data folder. Refer to [port/android/README.md](port/android/README.md). On
the web, the page copies `maps/` out of the disc image into the browser's
storage for the site - nothing is uploaded ([port/web/README.md](port/web/README.md)).
## Download
GitHub Actions builds the game for each commit.
Builds for latest release:
| Platform | Release | Debug | Guide |
| --- | --- | --- | --- |
| 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) | [Linux](port/linux/README.md) |
| 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) | [Windows](port/windows/README.md) |
| 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) | [Android](port/android/README.md) |
| Web | [halocombatevolved.com](https://halocombatevolved.com/) (or [halo-web-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-web-release.zip) for your own server) | [halo-web-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-web-debug.zip) | [Web](port/web/README.md) |
Release build is recommended for regular play.
The debug build stops at the first failed check (assertion) and writes it to the log, so use it when you want to find and report problems.
The game updates itself. At start-up it looks for a newer release, and asks
if you want to install it. Refer to "Updates" in
[port/linux/README.md](port/linux/README.md#updates). The web site offers
each new build when it is out.
Each build of the `main` branch that passes on all four platforms is a new
release, and the web site then plays it. 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.
Each release also has `SHA256SUMS`, the SHA-256 of each of its files, and
`SHA256SUMS.sig`, that file's Ed25519 signature by the project's release
key, whose public key is [deploy/release-key.pub](deploy/release-key.pub).
`sha256sum -c --ignore-missing SHA256SUMS` checks the downloads, and
`tools/release_sign.sh verify SHA256SUMS` (OpenSSL 3) the signature.
The Linux README also covers the controls, every setting and the multiplayer features. These are almost the same on all platforms. The settings are in `config.toml` next to the game, and many are in the menus too.
The Arch-based distribution release is available on the [AUR](https://aur.archlinux.org/packages/opence-bin).
## Multiplayer
- **Supports up to 128 players** on up to 128 machines and up to 4 players on each device in split screen.
- **Every platform plays together:** Windows, Linux, Android and the browser all support cross-play! On a local network, the native builds can play LAN. Browser players can play each other in rooms over WebRTC ([port/web/README.md](port/web/README.md)).
- **Peer-to-peer internet hosting:** join through the server browser or an invite link. A host lists a game as public in the server browser, or keeps it private for players with the invite link. With the Discord integration, the host can also send the invite through Discord.
- **Online co-op campaign:** with settings for friendly fire, player collisions and the number of enemies as a multiplier based on per player or static.
- **New gametypes:** SWAT, Infection, Gun Game and Hardcore (Slayer, CTF, Oddball and King), in Select Gametype's OPENCE bank; Edit Gametypes' OPENCE OPTIONS gives any gametype their rules ([port/linux/README.md](port/linux/README.md), "OpenCE gametypes").
- **Voice Chat:** push to talk (V) or an open microphone (can be changed in the menu's audio settings)
- **Map sharing:** a player who does not have the game's Custom Edition map downloads it from the other players.
- **Moderation:** the host can kick and ban players from the scoreboard menu or the `kick` and `ban` console commands. Players can vote to kick.
- **Scrollable Scoreboard:** the mouse wheel or PageUp / PageDown scrolls through list of players in large multiplayer lobbies.
- **A new netcode:** each machine moves its own player at once, and the host makes the decision for the game. Refer to [port/linux/NETCODE.md](port/linux/NETCODE.md).
## Custom Edition Maps
Halo Custom Edition maps are placed in `custom_maps` folder, not in the `maps` folder itself. It requires `bitmaps.map`, `sounds.map`, and `loc.map` in the `custom_maps` folder, which you must copy over from your existing installed copy of Halo Custom Edition. The maps folder is only for Xbox maps that's extracted from your disc image, not Halo Custom Edition.
Halo PC Gearbox exclusive maps (Ice Fields, Death Island, Danger Canyon, Infinity, Timberland and Gephyrophobia) **are not included by default**. They would need to be copied over from your existing Halo Custom Edition install's `maps` folder into `custom_maps` folder too.
Once that's setup, the maps will appear on CUSTOM SINGLEPLAYER and CUSTOM MULTIPLAYER in the map list in Campaign's New Game, and on the Map screen of Create Game (Internet or LAN). A CUSTOM SINGLEPLAYER map plays as a campaign level, alone or as
online co-op.
Maps that require OpenSauce are not compatible.
Both the server browser and the LAN browser support automatic map downloads - when you join a game it will download the custom maps from other players, with any of `bitmaps.map`, `sounds.map` and `loc.map` you are missing. This works on a LAN and on the
Internet, when the host has map sharing enabled (on by default).
Refer to [docs/custom_edition_caches.md](docs/custom_edition_caches.md).
## Mods
Mods go in a `mods` folder next to `maps`. Each mod is a folder of Halo PC tag files, as the Halo Editing Kit and Invader make them, with a `mod.toml` describing it. Every mod in the folder is on by default. Mods in the main menu turns them on and off and sets their order. Think of it like a resource pack list, the mod at the top plays over the ones below it. The game also refuses mods that say they cannot be played together.
Refer to [docs/mods.md](docs/mods.md) to make your own.
## Graphics
The game retains the original look and feel as it did on the Xbox by default. The video settings menus have additional options that go beyond it.
**Display**
| Option | What it does | Default |
| --- | --- | --- |
| DISPLAY MODE, RESOLUTION, WINDOW SIZE | Fullscreen, borderless or windowed, at any resolution (F11 switches) | Borderless, at the display's resolution |
| RESOLUTION SCALING | Draw at the window's resolution, or at the Xbox's 640x480 and scale it up | Native resolution |
| V-SYNC, FRAME RATE LIMIT | Wait for the display, or run free up to a frame cap; works with G-Sync and FreeSync | V-sync on |
| SMOOTH MOTION | A frame for every refresh of the display (60, 120, 240 Hz and more), blended between the game's 30 ticks a second | On |
| INSTANT AIM | In first person, the view turns the frame the mouse moves, not up to two ticks later | On |
| FOV, VIEWMODEL FOV | Field of view of the view and of the weapon and hands, from 80 to 150 degrees | Stock |
**Image quality**
| Option | What it does | Default |
| --- | --- | --- |
| ANTI-ALIASING | FXAA, SMAA, MSAA or SSAA | Off, as on the Xbox |
| TEXTURE FILTERING | Anisotropic filtering, from 2 to 16 - provides sharper floors, walls and terrain at an angle | Off, as on the Xbox |
| SHADOW RESOLUTION | Object shadows at 128 (the Xbox's), 256, 512 or 1024 pixels | 128 |
| PER-PIXEL LIGHTING | Lights models for each pixel, without the facets of the Xbox's per-vertex lighting | Off |
| MODEL DETAIL | Keep more model detail at a distance at high resolutions, or always the most detailed | Stock |
| HDR, HDR DISPLAY | Keeps explosions, plasma and the sun's glare past white and tone maps them; on an HDR monitor, shows them brighter than white | Off |
| HIGH-RES HUD, HIGH-RES TEXT | The HUD from hand-drawn redraws at 8x the size, and text in the Overpass font, sharp at any resolution | On |
**Extras**
| Option | What it does | Default |
| --- | --- | --- |
| LEGS | See your own legs when you look down in first person | Off |
| ARM SHIELDS | The energy shield's flare on your first-person arms too | Off |
| SHIELD GLOW, GLOW INTENSITY | Energy shields light up what is around them as they flare | Off |
| SHIELD COLOR | Your own shield in a multiplayer armor color, as your machine draws it | Default |
| OVERSHIELD | The overshield's own color, rising up the body as it charges | Off |
| PASSENGER THIRD PERSON | Third-person view for Warthog and Scorpion passengers (Campaign-only) | Off |
## How to build the game
You do not need the Xbox SDK. The port supplies the SDK declarations that
the game uses. Refer to [port/include/xdk](port/include/xdk/README.md).
To build the game:
1. Install Python and [ninja](https://ninja-build.org/).
2. Install the tools for your platform. Refer to the README for the
platform.
3. In the root folder of the repository, enter `python configure.py`.
4. Enter `ninja` with the target for the platform:
| Target | Result |
| --- | --- |
| `ninja linux` | `build/linux/halo` |
| `ninja windows` (on Windows, or on Linux after `configure.py --windows-cross`) | `build/windows/halo.exe` and `SDL3.dll` |
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
| `ninja web` | `build/web/site/`, the web site (`python tools/web_serve.py` serves it) |
If you enter `ninja` without a target, ninja builds the game for the
computer that you use.
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
enter `python tools/ci_build.py linux release`.
### Build options
Give these options to `configure.py`:
| Option | Result |
| --- | --- |
| (none) | A debug build. A failed assertion stops the game. |
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. The Linux build also operates on older distributions and on SteamOS: refer to "Portable build" in [port/linux/README.md](port/linux/README.md#portable-build). Use this option for builds that you give to other persons. |
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
| `--pgo=off` | No profile-guided optimization. |
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
| `--profile` | A profiling build. It records CPU times. Refer to "Profiling builds". |
| `--web-emcc PATH` | The Emscripten `emcc` for `ninja web` (else the one on the PATH, or `~/emsdk`'s). |
Without `--portable`, the Linux and Windows builds use all the instructions
of the processor that builds them (`-march=native`). Such a build does not
always start on a different computer.
## For developers
### Testing
The tests are listed in [AGENTS.md](AGENTS.md) ("Testing"). Most run
without the game data, in GitHub Actions too.
One check needs the retail maps, so it runs on your own computer (or on a
self-hosted runner), not in GitHub Actions: `tools/net_compare.py` plays
two builds together in a network game on one computer and checks that they
stay in step. Run it when a change can alter how a build calculates the
game, as a change to the simulation, the compiler or the maths does,
before it is merged. For example, to check a build with such a change
against a build without it:
```sh
python tools/net_compare.py --a build/linux/halo --b <other checkout>/build/linux/halo \
--data <folder with maps/> --maps bloodgulch,beavercreek,hangemhigh --swap
```
Each game prints PASS or FAIL. A FAIL names the first player, tick and
value that differ. Refer to "Comparing two builds" in
[port/linux/NETCODE.md](port/linux/NETCODE.md).
### Optimization profiles
The builds use profiles of the game to optimize the code:
- `pgo/halo_linux.profdata` for Linux and Android.
- `pgo/halo_windows.profdata` for Windows.
The profiles need clang 22 or later. With an older clang, the builds do not
use the profiles.
To record a new profile:
1. Delete the profile.
2. Enter `python configure.py --pgo=train`.
3. Enter `ninja linux` or `ninja windows`.
The build then plays the main menu and the first minute of each campaign
level. This procedure continues for approximately 15 minutes. The game
data must be in `assets/`.
### Profiling builds
A profiling build records where the game spends its time.
1. Enter `python configure.py --profile`, then build as usual.
2. Start a recording. Enter `profile_record` in the console (or the telnet
console), or set `debug.profile_record = true` in `config.toml`. To
record each game of a session, also set
`debug.profile_record_when = "game"`.
3. Stop the recording. Enter `profile_stop`, load another map, or quit.
The game writes numbered `.part<n>.json` files under `profiles/` in its data root
and logs their paths. Parts are retained after the recording stops. Logs go to
standard error; a Windows release build without standard error writes them to
`halo.log` next to `halo.exe`.
`tools/net_report.py` is a helper for developers. Enter
`python tools/net_report.py <recording.part1.json>` to read a recording
as tables. The command also writes `<recording>.summary.txt`, a short text
file.
| Command | Result |
| --- | --- |
| `profile_record [seconds]` | Starts a recording. With a number of seconds, the recording stops after that time. |
| `profile_stop` | Stops the recording. |
`debug.profile_memory` sets the memory of a recording in MB (default 256,
from 4 to 1024). A recording has no length limit:
the game writes a part each time the memory is full. Refer to "Settings" in
[port/linux/README.md](port/linux/README.md).
On Android, the files are in `/sdcard/Android/data/com.halo.decomp/files/profiles/`.
Enter `adb pull` to copy the recording parts to a computer.
A profiling build plays with normal builds. It does not use
`--pgo=train`.
# Mjolnir VR
**A VR mod for Halo: Combat Evolved** (the original Xbox game) on the
**Meta Quest 3 / 3S** and the **Steam Frame**. Play the campaign and
multiplayer in full 6DoF virtual reality, with your hands in the game.
Mjolnir VR runs on your headset by itself, without a PC. It
brings no game files: it uses a disc image of **your own, purchased copy**
of the game.
> **Unofficial fan project.** Mjolnir VR is a free,
> non-commercial fan project. It is not affiliated with, endorsed,
> sponsored or approved by Microsoft, Xbox Game Studios, Halo Studios or
> Bungie. See [Legal](#legal).
Until build 313 it was called *Ringfall: Mjolnir VR*. Installed builds
update to the new name by themselves, and the old links lead here.
## Contents
- [What you get](#what-you-get)
- [What you can play](#what-you-can-play)
- [What you need](#what-you-need)
- [Install on Meta Quest 3 / 3S](#install-on-meta-quest-3--3s)
- [Install on Steam Frame](#install-on-steam-frame)
- [Playing](#playing)
- [Problems and questions](#problems-and-questions)
- [Credits](#credits)
- [Legal](#legal)
## What you get
- **Full VR**: stereo 6DoF at your headset's refresh rate (72 to 120 Hz);
the HUD, menus, scope and crosshair float as layers of their own.
- **Your hands in the game**: the weapon sits in your hand and fires
where you point it; arms and fingers follow your controllers;
two-handed weapons, melee by swinging, grenades thrown with your hand,
crouching and leaning with your body.
- **Vehicles**: steer the Warthog by turning its wheel with both hands;
turrets with handles.
- **Comfort**: snap or smooth turning in fine steps, comfort vignettes,
left-handed mode, buttons remapped freely, graphics presets, refresh
rate and resolution, all in the in-game VR options (**Y**).
- **The PC version's menus**, clicked with a laser pointer, and an
on-screen keyboard for names, codes and passwords.
## What you can play
- **The whole campaign**: all ten missions, on every difficulty, saved as
you go.
- **Co-op campaign** with friends, online or on your local network, with
settings for friendly fire and extra enemies per player.
- **LAN multiplayer** (system link) with everyone on your network.
- **Online multiplayer**, no server of your own needed: host a game and
share its join code, find public games in the server browser, or protect
a lobby with a password. Up to 128 players.
- **Bots** to fill a game, on foot, in vehicles and at turrets; they make
room when a person joins.
- **All 13 multiplayer maps** and the Xbox game's game types: Slayer,
Capture the Flag, King of the Hill, Oddball and Race.
- **Cross-play**: VR players on the Quest and the Steam Frame, together with
players of the PC and phone builds of
[OpenCE](https://github.com/OpenCommunityEdition/OpenCE).
## What you need
- A **Meta Quest 3 / 3S** or a **Steam Frame**.
- A **disc image** (`.iso` or `.xiso`) of **Halo: Combat Evolved for the
original Xbox**, made from **a disc you own**. Any region and version
works. The game plays in the disc's language; on the European disc in
Spanish and Italian it takes your headset's language (else the first).
This project does not include, link to or help find game files. Please
do not ask for or share disc images in issues or discussions; such posts
are deleted.
## Install on Meta Quest 3 / 3S
You install the game once with a computer (Windows, macOS or Linux); after
that it updates itself.
1. **Turn on Developer Mode** for your Quest, once: in the Meta Horizon
app on your phone, open your headset's settings, then *Developer
Mode* (Meta asks you to create a free developer account first).
2. **Install [SideQuest](https://sidequestvr.com/setup-howto)** (the
"Advanced Installer") on your computer and connect the Quest with a
USB cable. In the headset, allow USB debugging for this computer.
3. **Download `MjolnirVR.apk`** from the
[latest release](../../releases/latest) and drag it onto the SideQuest
window. It installs the game.
4. **Copy your disc image** to the Quest's `Download` folder: in
SideQuest, open the file manager (the folder icon), go to `Download`
and drag the `.iso` file in.
5. **In the headset**, open your *Library*, choose *Unknown Sources* and
start **Mjolnir VR**. Select **Choose disc image** and pick
your file in `Download`. The game copies what it needs (about 1.8 GB,
a minute or two) and starts.
You can delete the disc image from `Download` afterwards. When a new
version comes out, the game asks you at start whether to update.
## Install on Steam Frame
No computer and no keyboard needed.
1. **Switch to Desktop Mode**: press the Steam button, then *Power* >
*Switch to Desktop*.
2. **Put your disc image** in the `Downloads` folder (for example from a
USB stick or memory card with the file manager, Dolphin).
3. **Download `Install-Mjolnir-VR.desktop`** from the
[latest release](../../releases/latest) with the web browser.
4. **Open it**: in Dolphin, open `Downloads` and double-click the file.
If you are asked whether to run it, choose to continue.
5. **Answer the dialogs.** The installer finds your disc image, copies
what the game needs, adds **Mjolnir VR** to your Steam
library and offers to switch back to Gaming Mode.
6. Start **Mjolnir VR** from your library (under *Non-Steam*).
The game updates itself: when a new version is out, it is downloaded
while you play and used from the next start on. Your settings, saves and
game data stay. More about the Steam Frame:
[port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md).
## Playing
- **Y** opens the VR options in the main and pause menus: turning,
comfort, weapon hand, buttons, graphics, refresh rate and resolution.
*Quit the game* is on its last page.
- In the menus, point with a controller and pull the trigger to click.
- The controller layout is on the VR options' *Controller buttons* page.
- **Voice chat** in network games: hold the grip of the hand without the
weapon to talk (not while it holds the weapon or the Warthog's wheel).
The *BUTTONS* page can put push to talk on another button.
## Problems and questions
Please open an [issue](../../issues) and include:
- your headset and the build number of the release you installed;
- what you did and what happened;
- the game's log, `debug.txt`:
- Quest: `Android/data/io.github.cesarus85.ringfall/files/debug.txt`
(SideQuest's file manager);
- Steam Frame: `Documents/RingfallMjolnirVR/debug.txt` (and
`logcat.txt` and `install.log` beside it).
What is done and what comes next: [port/android/VR_ROADMAP.md](port/android/VR_ROADMAP.md).
**Network use.** The game connects to the internet only for what you
use: the update check (GitHub), online games (to other players
directly), and join codes and lobby lists through public MQTT brokers
(listed in [brokers.txt](port/assets/network/brokers.txt)). There is no
telemetry.
## Credits
The decompilation is of the Xbox build 2342 (`cachebeta.exe`, SHA-256
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
OpenCE starts from the decompilation of
[bnunu/halo-1](https://github.com/bnunu/halo-1), which is a fork of
[punpckhdq/halo](https://github.com/punpckhdq/halo). The artists of the
high-res redraws are in [AUTHORS.md](AUTHORS.md). The main contributors are Jonas Volman (@bnunu) and MrBruh (@cybersecurity). Everyone else who has contributed is on the
[contributors page](https://github.com/OpenCommunityEdition/OpenCE/graphs/contributors).
Mjolnir VR stands on the work of others:
## Licence
- **[OpenCE](https://github.com/OpenCommunityEdition/OpenCE)** (Open
Community Edition): the port of the decompiled game to Linux, Windows
and Android, with its renderer, netcode, PC menus and multiplayer, that
this project builds on and follows.
- **The decompilation**: [bnunu/halo-1](https://github.com/bnunu/halo-1),
a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
- **[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR)**
by moistman42069 (CC0): the arm IK, the forearm's twist and the
fingers' poses are taken from it (`port/linux/game/vr_arms.c`); the
left-handed mode and the graphics presets follow its ideas.
- **[HaloCE-VR](https://github.com/astromaddie/HaloCE-VR)** by astromaddie
(CC0): the Steam Frame work, whose findings (such as quieting Mesa's
trace markers) are used here.
- **[xboxdevwiki](https://xboxdevwiki.net/XDVDFS)**: its description of
the Xbox's disc file system, which the disc image readers are written
from.
- The libraries and fonts in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
OpenCE is free software: you can give it to others and change it under the
terms of the GNU Affero General Public License as published by the Free
Software Foundation, version 3 of the License or (at your option) any later
version (`AGPL-3.0-or-later`). It comes with no warranty. The full text is in
[LICENSE](LICENSE).
Thank you all.
- **From the change.** The AGPL applies from the commit that made the change,
and to each release built from that commit or a later one. Earlier
releases, and the source at every earlier commit, were released under
[CC0 1.0](LICENSES/CC0-1.0.txt) and stay that way.
- **What OpenCE builds on.** The decompilation of
[punpckhdq/halo](https://github.com/punpckhdq/halo) is under the AGPL 3.0
from 17 October 2026, and under CC0 1.0 before that date.
[bnunu/halo-1](https://github.com/bnunu/halo-1) is under CC0 1.0.
- **Contributions** are under `AGPL-3.0-or-later`. A change taken from a
project under GPL 3.0 keeps that licence and its notices; the GPL 3.0 and
the AGPL 3.0 let the two be combined (section 13 of each).
## Building it
`python3 configure.py --release`, then `ninja android_apk` (Android SDK
and NDK needed). See [port/android/README.md](port/android/README.md) for
the details and [docs/OPENCE_README.md](docs/OPENCE_README.md) for the
port it is built on.
## Legal
Mjolnir VR is an unofficial, free, non-commercial fan project.
It is not affiliated with, endorsed, sponsored or approved by Microsoft
Corporation, Xbox Game Studios, Halo Studios or Bungie. Halo, Halo: Combat
Evolved and Xbox are trademarks of Microsoft Corporation; they are named
here only to say which game this mod works with. Meta Quest is a trademark
of Meta Platforms, Inc.; Steam and Steam Frame are trademarks of Valve
Corporation.
No game data, art, music or other content of the game is included or
distributed. You need your own, legally purchased copy of the game. If
you enjoy this mod, please support the creators of the original by buying
their games.
### Licence
Mjolnir VR is free software: you can give it to others and change
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, version 3 of the License or (at your option)
any later version (`AGPL-3.0-or-later`), as OpenCE, which it builds on, is.
It comes with no warranty. The full text is in [LICENSE](LICENSE).
- **From build 312 on.** The AGPL applies from the commit that took in
OpenCE's change of licence (19 October 2026), and to each release built
from it or a later one. Earlier releases, and the source at every earlier
commit, were released under [CC0 1.0](LICENSES/CC0-1.0.txt) and stay
that way.
- **Hosts and servers.** If you change the game and let other persons play
with your version over a network (for example, as the host of an internet
game, or on a web site), the AGPL asks you to offer them the source of
your version (section 13). The game as released has its source here, and
each release's source is its tag (`build-<n>`).
- **Third-party code, fonts and art** keep their own licences, which are
next to them (`port/third_party`, `port/assets/fonts`,
`port/assets/icons`, `port/assets/text_filter`). The release downloads and
the web site's `licenses.txt` carry their notices.
- **Halo is not covered.** The licence covers the work of OpenCE's authors.
It gives no rights in Halo, its maps or anything else that Microsoft owns.
## Disclaimer
OpenCE (Open Community Edition) is an unofficial fan project. It is not endorsed by or affiliated with Microsoft, Xbox Game Studios, Halo Studios, Activision or Bungie. Halo and Halo: Combat Evolved are trademarks of Microsoft Corporation; Halo © Microsoft Corporation.
No game data is included. You must provide your own copy of the game.
with your version over a network (for example, as the host of an
internet game), the AGPL asks you to offer them the source of your
version. The game as released has its source here, and each release's
source is its tag (`build-<n>`).
- **Third-party code and fonts** keep their own licences: see
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
- **Halo is not covered.** The licence covers the work of this project's
and OpenCE's authors. It gives no rights in Halo, its maps or anything
else that Microsoft owns.
+48
View File
@@ -0,0 +1,48 @@
# Third-party notices
Mjolnir VR is released under the GNU Affero General Public
License, version 3 or later ([LICENSE](LICENSE)); releases before build 312
were released under [CC0 1.0](LICENSES/CC0-1.0.txt). It includes or builds
on the following, under their own licenses; each license's full text is in
the file named.
## Code
| Component | Used for | License | Text |
| --- | --- | --- | --- |
| [OpenCE](https://github.com/OpenCommunityEdition/OpenCE) and the decompilation ([bnunu/halo-1](https://github.com/bnunu/halo-1), [punpckhdq/halo](https://github.com/punpckhdq/halo)) | the game's code, the port | AGPL 3.0 or later (OpenCE from 19 October 2026, punpckhdq/halo from 17 October 2026; CC0 1.0 before); bnunu/halo-1: CC0 1.0 | [LICENSE](LICENSE), [LICENSES/CC0-1.0.txt](LICENSES/CC0-1.0.txt) |
| [HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) | the arm IK, forearm twist and finger poses (`port/linux/game/vr_arms.c`) | CC0 1.0 | (its repository) |
| [HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) | Steam Frame findings | CC0 1.0 | (its repository) |
| [SDL 3](https://libsdl.org) | windows, input, sound | zlib | (in SDL's source, `LICENSE.txt`) |
| [Khronos OpenXR loader](https://github.com/KhronosGroup/OpenXR-SDK) | VR runtime access | Apache 2.0 | (in the loader's package) |
| [musl](https://musl.libc.org) | the C library of the game's code on Android, its maths | MIT | [port/third_party/musl-math](port/third_party/musl-math/README.md) |
| [Expat](https://libexpat.github.io) | the PC menus' definitions | MIT | [port/third_party/expat/COPYING](port/third_party/expat/COPYING) |
| [Mbed TLS](https://www.trustedfirmware.org/projects/mbed-tls/) | HTTPS for the desktop builds' updater (not in the Android app) | Apache 2.0 | [port/third_party/mbedtls/LICENSE](port/third_party/mbedtls/LICENSE) |
| [miniupnpc](https://miniupnp.tuxfamily.org) | router port forwarding for online games | BSD 3-Clause | [port/third_party/miniupnpc/LICENSE](port/third_party/miniupnpc/LICENSE) |
| [KCP](https://github.com/skywind3000/kcp) | reliable transport for online games | MIT | [port/third_party/kcp/LICENSE](port/third_party/kcp/LICENSE) |
| [Monocypher](https://monocypher.org) | signing public games' listings (Ed25519); online play's other cryptography is the port's own | CC0 1.0 or BSD 2-Clause | [port/third_party/monocypher/LICENCE.md](port/third_party/monocypher/LICENCE.md) |
| [tomlc17](https://github.com/cktan/tomlc17) | reading `config.toml` | MIT | [port/third_party/tomlc17/LICENSE](port/third_party/tomlc17/LICENSE) |
| [stb](https://github.com/nothings/stb) | drawing the game's text sharply (stb_truetype) | MIT or public domain | [port/third_party/stb/LICENSE](port/third_party/stb/LICENSE) |
| [SMAA](https://www.iryoku.com/smaa/) | anti-aliasing (flat screens; off in VR) | MIT | [port/third_party/smaa/LICENSE](port/third_party/smaa/LICENSE) |
The disc image readers (`XisoExtractor.java`, `steam_frame_xiso.py`,
`xiso.c`) are written from a description of the Xbox's disc file system
([xboxdevwiki's XDVDFS page](https://xboxdevwiki.net/XDVDFS)), not from
extract-xiso, which earlier releases followed.
## Fonts
| Font | Used for | License | Text |
| --- | --- | --- | --- |
| Overpass (Red Hat, Delve Withrington) | the game's text, the library pictures | SIL Open Font License 1.1 | [port/assets/fonts/Overpass-OFL.txt](port/assets/fonts/Overpass-OFL.txt) |
| OpenCE (after Newtown) | the menus' titles | SIL Open Font License 1.1 | [port/assets/fonts/OpenCE-OFL.txt](port/assets/fonts/OpenCE-OFL.txt) |
| Newtown (Roger White) | OpenCE's source | public domain | [port/assets/fonts/Newtown-LICENSE.txt](port/assets/fonts/Newtown-LICENSE.txt) |
See [port/assets/fonts/README.md](port/assets/fonts/README.md) for where
each font is used.
## Not included
No file of Halo: Combat Evolved (maps, sounds, art, music, fonts or
executables) is part of this repository or its releases. The game reads
them from the player's own disc image.
+240
View File
@@ -0,0 +1,240 @@
> This is the README of [OpenCE](https://github.com/OpenCommunityEdition/OpenCE),
> the port Mjolnir VR is built on, kept for its build and
> development notes. Its download links are OpenCE's. For the VR mod, see
> the [main README](../README.md).
# Halo: Combat Evolved for Linux, Windows, Android and the web
[![Join our Discord](https://invidget.switchblade.xyz/9gqcHyr5km)](https://discord.gg/9gqcHyr5km)
This project is a port of the Halo: Combat Evolved decompilation to Linux,
Windows, Android and web browsers. The decompilation is of the Xbox build 2342
(`cachebeta.exe`, SHA-256
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
The port starts from the decompilation of [bnunu/halo-1](https://github.com/bnunu/halo-1).
That project is a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
## Download
GitHub Actions builds the game for each commit. These links download the
builds of the latest release:
| Platform | Release | Debug |
| --- | --- | --- |
| 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) |
| Web | [halocombatevolved.com](https://halocombatevolved.com/) (or [halo-web-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-web-release.zip) for your own server) | [halo-web-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-web-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
problems.
The game updates itself. At start-up it looks for a newer release, and asks
if you want to install it. Refer to "Updates" in
[port/linux/README.md](port/linux/README.md#updates). The web site offers
each new build when it is out.
Each build of the `main` branch that passes on all four platforms is a new
release, and the web site then plays it. 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.
### Arch-based Linux Distributions
The above Linux binary release is available on the [AUR](https://aur.archlinux.org/packages/opence-bin).
## Game data
The port does not include the game data. Download an Xbox disc image
(`.xiso` or `.iso`) of Halo: Combat Evolved. All versions of the game
operate. The maps of the European (PAL) version were made for a slower
console. The port changes them to play as the North American (NTSC) maps do,
so players of the two versions can play together.
1. Start the game.
2. At the first start, the game asks for the disc image. Select it.
3. The game extracts the `maps/` folder. Then the game starts.
On Linux and Windows, the game puts `maps/` next to the executable. On
Android, copy the disc image to the phone first. The app puts `maps/` in its
data folder. Refer to [port/android/README.md](port/android/README.md). On
the web, the page copies `maps/` out of the disc image into the browser's
storage for the site; nothing is uploaded. Refer to
[port/web/README.md](port/web/README.md).
## Mods
Mods are folders of Halo PC tag files, as the Halo Editing Kit and Invader
write them, in `mods/` next to `maps/`: texture, sound, text and model packs
that play over the maps' on your machine only, in any game. Each has a
`mod.toml`. Every mod in the folder is on; the main menu's Mods turns them
off and on and sets the order they load in.
Refer to [docs/mods.md](docs/mods.md).
## Platforms
Each platform has its own instructions:
| Platform | Instructions |
| --- | --- |
| Linux (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/linux/README.md](port/linux/README.md) |
| Windows (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/windows/README.md](port/windows/README.md) |
| Android (arm64 app, OpenGL ES 3, SDL3) | [port/android/README.md](port/android/README.md) |
| Web (WebAssembly, WebGL 2, SDL3; desktop Chrome, Edge and Firefox) | [port/web/README.md](port/web/README.md) |
The Linux README also gives the controls, the settings and the multiplayer
functions. These are almost the same on all platforms.
## Multiplayer
The game can play system link games on a local network and on the internet:
- A system link game can have up to 128 players on up to 128 machines.
- Linux, Windows and Android machines and browsers can play in the same
game. On a local network, the native builds play system link; browsers
play each other in rooms, over WebRTC
([port/web/README.md](port/web/README.md)).
- An invite link, or the server browser, lets a machine join a game on the
internet, browsers too: a native build takes a browser's WebRTC
([port/linux/README.md](port/linux/README.md), "Browsers"). No server of
this project is necessary.
- The netcode is new. Each machine moves its own player at once,
and the host makes the decisions for the game. Refer to
[port/linux/NETCODE.md](port/linux/NETCODE.md).
## Build the game
You do not need the Xbox SDK. The port supplies the SDK declarations that
the game uses. Refer to [port/include/xdk](port/include/xdk/README.md).
To build the game:
1. Install Python and [ninja](https://ninja-build.org/).
2. Install the tools for your platform. Refer to the README for the
platform.
3. In the root folder of the repository, enter `python configure.py`.
4. Enter `ninja` with the target for the platform:
| Target | Result |
| --- | --- |
| `ninja linux` | `build/linux/halo` |
| `ninja windows` (on Windows) | `build/windows/halo.exe` and `SDL3.dll` |
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
| `ninja web` | `build/web/site/`, the web site (`python tools/web_serve.py` serves it) |
If you enter `ninja` without a target, ninja builds the game for the
computer that you use.
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
enter `python tools/ci_build.py linux release`.
### Build options
Give these options to `configure.py`:
| Option | Result |
| --- | --- |
| (none) | A debug build. A failed assertion stops the game. |
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. The Linux build also operates on older distributions and on SteamOS: refer to "Portable build" in [port/linux/README.md](port/linux/README.md#portable-build). Use this option for builds that you give to other persons. |
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
| `--pgo=off` | No profile-guided optimization. |
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
| `--profile` | A profiling build. It records CPU times. Refer to "Profiling builds". |
| `--web-emcc PATH` | The Emscripten `emcc` for `ninja web` (else the one on the PATH, or `~/emsdk`'s). |
Without `--portable`, the Linux and Windows builds use all the instructions
of the processor that builds them (`-march=native`). Such a build does not
always start on a different computer.
### Optimization profiles
The builds use profiles of the game to optimize the code:
- `pgo/halo_linux.profdata` for Linux and Android.
- `pgo/halo_windows.profdata` for Windows.
The profiles need clang 22 or later. With an older clang, the builds do not
use the profiles.
To record a new profile:
1. Delete the profile.
2. Enter `python configure.py --pgo=train`.
3. Enter `ninja linux` or `ninja windows`.
The build then plays the main menu and the first minute of each campaign
level. This procedure continues for approximately 15 minutes. The game
data must be in `assets/`.
### Profiling builds
A profiling build records where the game spends its time.
1. Enter `python configure.py --profile`, then build as usual.
2. Start a recording. Enter `profile_record` in the console (or the telnet
console), or set `debug.profile_record = true` in `config.toml`. To
record each game of a session, also set
`debug.profile_record_when = "game"`.
3. Stop the recording. Enter `profile_stop`, load another map, or quit.
The game writes numbered `.part<n>.json` files under `profiles/` in its data root
and logs their paths. Parts are retained after the recording stops. Logs go to
standard error; a Windows release build without standard error writes them to
`halo.log` next to `halo.exe`.
`tools/net_report.py` is a helper for developers. Enter
`python tools/net_report.py <recording.part1.json>` to read a recording
as tables. The command also writes `<recording>.summary.txt`, a short text
file.
| Command | Result |
| --- | --- |
| `profile_record [seconds]` | Starts a recording. With a number of seconds, the recording stops after that time. |
| `profile_stop` | Stops the recording. |
`debug.profile_memory` sets the memory of a recording in MB (default 256,
from 4 to 1024). A recording has no length limit:
the game writes a part each time the memory is full. Refer to "Settings" in
[port/linux/README.md](port/linux/README.md).
On Android, the files are in `/sdcard/Android/data/com.halo.decomp/files/profiles/`.
Enter `adb pull` to copy the recording parts to a computer.
A profiling build plays with normal builds. It does not use
`--pgo=train`.
## Licence
OpenCE is free software: you can give it to others and change it under the
terms of the GNU Affero General Public License as published by the Free
Software Foundation, version 3 of the License or (at your option) any later
version (`AGPL-3.0-or-later`). It comes with no warranty. The full text is in
[LICENSE](LICENSE).
- **From the change.** The AGPL applies from the commit that made the change,
and to each release built from that commit or a later one. Earlier
releases, and the source at every earlier commit, were released under
[CC0 1.0](LICENSES/CC0-1.0.txt) and stay that way.
- **What OpenCE builds on.** The decompilation of
[punpckhdq/halo](https://github.com/punpckhdq/halo) is under the AGPL 3.0
from 17 October 2026, and under CC0 1.0 before that date.
[bnunu/halo-1](https://github.com/bnunu/halo-1) is under CC0 1.0.
- **Contributions** are under `AGPL-3.0-or-later`. A change taken from a
project under GPL 3.0 keeps that licence and its notices; the GPL 3.0 and
the AGPL 3.0 let the two be combined (section 13 of each).
- **Hosts and servers.** If you change the game and let other persons play
with your version over a network (for example, as the host of an internet
game, or on a web site), the AGPL asks you to offer them the source of
your version (section 13). The game as released has its source here, and
each release's source is its tag (`build-<n>`).
- **Third-party code, fonts and art** keep their own licences, which are
next to them (`port/third_party`, `port/assets/fonts`,
`port/assets/icons`, `port/assets/text_filter`). The release downloads and
the web site's `licenses.txt` carry their notices.
- **Halo is not covered.** The licence covers the work of OpenCE's authors.
It gives no rights in Halo, its maps or anything else that Microsoft owns.
+93 -11
View File
@@ -1,5 +1,8 @@
# Android
The app of Mjolnir VR (application id `io.github.cesarus85.ringfall`, from
the name before, "Ringfall: Mjolnir VR", kept so that installed apps update;
its Java classes keep the decompilation's package, `com.halo.decomp`).
`ninja android` builds the game for 64-bit ARM Android (arm64-v8a).
`ninja android_apk` makes an app from it:
`port/android/app/build/outputs/apk/debug/app-debug.apk`.
@@ -54,9 +57,9 @@ Your own builds all have the same signature (the debug key of WSL,
(`adb install -r`).
To keep the saved games before you remove the app, enter
`adb pull /sdcard/Android/data/com.halo.decomp/files/save/u` (the profiles).
`adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/save/u` (the profiles).
After the new install, before the first start of the app, enter
`adb push u /sdcard/Android/data/com.halo.decomp/files/save/u`. The app reads
`adb push u /sdcard/Android/data/io.github.cesarus85.ringfall/files/save/u`. The app reads
and overwrites what adb pushes. adb cannot add files to `save/u` or `save/z` once the app made them, or
change files that the app wrote, so push before the first start. adb cannot read
some files that the app writes (the `blam.lst` files of `save/z`), so `adb pull`
@@ -75,8 +78,8 @@ of the whole `save` folder stops there.
## Game data
The game needs the `maps/` folder from an Xbox disc image (`.xiso` or
`.iso`) of any version of the game. The app extracts `maps/` from the disc
image. The app keeps the data in `/sdcard/Android/data/com.halo.decomp/files`.
`.iso`) made from your own copy of the game, any version. The app extracts `maps/` from the disc
image. The app keeps the data in `/sdcard/Android/data/io.github.cesarus85.ringfall/files`.
To install the data with the app:
@@ -91,9 +94,9 @@ To install the data with the app:
To install the data from a computer:
1. Start the app one time. The app makes its folders.
2. Enter `adb push <folder>/. /sdcard/Android/data/com.halo.decomp/files/`.
2. Enter `adb push <folder>/. /sdcard/Android/data/io.github.cesarus85.ringfall/files/`.
| Item | Location in `/sdcard/Android/data/com.halo.decomp/files` |
| Item | Location in `/sdcard/Android/data/io.github.cesarus85.ringfall/files` |
| --- | --- |
| Saved games (`z:\` and `u:\`) | `save` |
| Mods (`docs/mods.md`) | `mods`: `adb push <mod folder> /sdcard/Android/data/com.halo.decomp/files/mods/`; the main menu's Mods orders them and turns them off |
@@ -101,7 +104,7 @@ To install the data from a computer:
| Settings | `config.toml` |
To make a copy of the saved games, enter
`adb pull /sdcard/Android/data/com.halo.decomp/files/save/u` (the profiles).
`adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/save/u` (the profiles).
An `adb pull` of the whole `save` folder stops at the `blam.lst` files of
`save/z`, which adb cannot read.
@@ -211,9 +214,9 @@ Removing the app's data or the app removes the layout.
The settings are in `config.toml` in the data folder of the app. To change
them:
1. Enter `adb pull /sdcard/Android/data/com.halo.decomp/files/config.toml`.
1. Enter `adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/config.toml`.
2. Change the file.
3. Enter `adb push config.toml /sdcard/Android/data/com.halo.decomp/files/`.
3. Enter `adb push config.toml /sdcard/Android/data/io.github.cesarus85.ringfall/files/`.
At the first start, the game writes the file with the default values. To
get the default values again, delete the file.
@@ -227,6 +230,8 @@ These settings are only for Android:
| `input.touch_aim_assist` | `true` (the default): the touch controls' swipe aiming gets the aim assist of a controller (the aim slows over a target and follows a moving one). `false`: none, as with a mouse; the bullets' own autoaim stays. |
| `input.touch_controls` | The touch controls in a game. `"auto"` (the default): shown on a touchscreen while no controller is connected. `"on"`: also shown with a controller. `"off"`: never shown. A device without a touchscreen never shows them. The menus take taps in any case. |
| `display.screen_width` | The number of columns of the 480-line picture. `0` (the default): the shape of the display (1068 on a 20:9 phone). `640`: the 4:3 shape of the Xbox. |
| `display.shader_cache` | `true` (the default): the linked shader programs are kept in `shader_cache` in the data folder (their binaries, the driver that made them and their sources), and each map's list of the programs it drew with; as a map loads, the programs on its list are made ready, so that its first frames do not wait for the driver to compile them. A driver update compiles them again from the kept sources. `false`: each program is compiled the first time it is drawn. The folder may be deleted at any time. |
| `display.shader_worker` | `true` (the default): the shader programs are made on a thread of their own, whose GL context shares the game's (loaded from the shader cache, or compiled), and drawn with once there (`debug.shader_warm_up`: `"worker"`, `"load"` on the game's thread as a map loads, or `"off"`); a draw whose program is not made yet is left out for the frames it takes, and the frame does not wait. As a map loads, its own programs and then every map's (`shader_cache/all.txt`) are asked for. `false`: each is made on the game's thread when first drawn with. |
| `debug.sample_seconds` | Refer to "Find problems". |
| `debug.memory_watch` | `true` (the default): the app notices the game's writes to cached textures by page protection. `false`: it compares page contents once a frame instead, which is slower. Geometry uses the streaming path in either mode. Refer to "Limits". |
@@ -262,14 +267,22 @@ select "Yes":
1. The app downloads the new version.
2. The package installer of Android opens. At the first update, Android asks
you to let Halo install apps. Allow it.
you to let the game install apps. Allow it.
3. Select "Update". Android replaces the app.
4. Select "Open" to start the new version.
To install over the previous version, each build must have the same
signature. GitHub Actions signs each build with the key in the
`ANDROID_KEYSTORE_BASE64` and `ANDROID_KEYSTORE_PASSWORD` secrets of the
repository. If you installed a build that has a different signature, remove
repository. A computer that builds for its own devices signs with the same
key when `port/android/signing.properties` (not in the repository) names it:
storeFile=/path/to/ringfall-release.jks
password=<the keystore's password>
keyAlias=ringfall
Keep the keystore and its password safe: a lost key means every player must
remove the app before the next version. If you installed a build that has a different signature, remove
that build before you install a new build. Removing the app deletes its data
folder: first make a copy of `maps/` and `save/u`.
@@ -287,6 +300,75 @@ the Xbox:
All the ports draw this way.
## VR
On a VR headset with an OpenXR runtime (Meta Quest 3 and 3S; the Steam
Frame runs Android apps in Lepton) the same APK plays in VR, with 6DoF head
tracking and the Touch controllers. The launcher, a flat panel on the
headset, imports the game data as on a phone and then starts the game
immersive. `vr.enabled = false` in `config.toml` shows it on a flat panel
instead.
Controls in play (in the menus the buttons are the Xbox's by position):
| Controller | Game |
| --- | --- |
| right trigger / left trigger | fire / throw grenade |
| right grip / left grip | melee / switch weapon |
| A / B | jump / action, reload |
| X / Y | switch grenade / flashlight |
| left stick, click | move, crouch |
| right stick, click | turn (snap or smooth), zoom |
| menu | pause |
You aim with your head; turning your head turns the player, and the right
stick turns you further. Recentre with the headset's own gesture (holding
the Meta button). The `[vr]` settings in `config.toml` choose snap or
smooth turning, the resolution, the refresh rate, and where the HUD and the
screen for menus and cinematics float.
### How VR works
`host_main.c` asks the Khronos OpenXR loader for a headset before the game
starts. If there is one, the game keeps the Xbox's 640x480 layout but draws
it at the eye images' resolution (`d3d8_gl.c`, `screen_mode_choose`), and
`port/linux/src/vr.c` drives the session the host keeps
(`host/host_xr.c`, which the guest calls through the import stubs;
`include/halo_xr_abi.h` has the structures both sides share):
- the frame starts when the last one has been presented
(`platform_pump_events`), before the game reads its input: the host waits
for the headset's frame and reports where the eyes, the head and the
controllers will be when it is shown;
- the player's facing follows the head (`halo_vr_aim` in
`player_control.c`); a "body" yaw that the right stick turns maps the
headset's space onto the game's;
- `render.c` draws the player's view once per eye, from the eye's pose
placed at the game's camera, through the eye's asymmetric frustum
(`frustum_bounds`, as the tiled screenshots use), leaving out the HUD;
each eye is copied to its swapchain;
- the back buffer is then cleared to transparent black, and the HUD and the
menus drawn over nothing; Present copies it to a quad layer that follows
the head (premultiplied alpha, so the HUD's additive parts add to the
scene), or, while a menu is up, stands in the room;
- frames without a player's view (the main menu, loading, cinematics,
unless `vr.cinematics_in_3d`) go whole onto that screen in the room.
The game draws in gamma space; the swapchains are sRGB, so the host copies
through a shader that decodes to linear, and the stored bytes are the
game's. Camera shake is off in VR.
A file named `xr_test` in the data folder
(`adb shell touch /sdcard/Android/data/io.github.cesarus85.ringfall/files/xr_test`)
starts a self-test instead of the game, which needs no game data: each eye
in its own colour (red while the right trigger is held), a checkerboard
screen in the room (its top-left square red), and the controllers' buttons
in the log (`adb logcat -s halo`). Delete the file to play.
Not done yet: aiming with the controller and the weapon in the hand, the
HUD's parts on the controllers, zoom through the scope, a first-person view
in vehicles, and a separate depth for the HUD's reticle.
## How the port operates
### ILP32 code
+182
View File
@@ -0,0 +1,182 @@
# 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.
## Installing it (players)
On the Frame, in Desktop Mode (Steam menu > Power > Switch to Desktop):
1. Put your own disc image of Halo: Combat Evolved for the original Xbox (`.iso` or
`.xiso`, any version) in Downloads (or Documents, the Desktop, a memory
card).
2. Download `Install-Mjolnir-VR.desktop` from the latest release and open it
(double-click; confirm running it).
3. Answer the dialogs: the installer finds the disc image (or asks for it),
copies the game data out of it, adds "Mjolnir VR" to the Steam library
and switches back to Gaming Mode. Start it from the library there.
The game updates itself: while it runs, its launcher looks for a newer
release, fetches it and puts it in place as the next start begins
(`steam_frame_launch.sh`; `update.auto = false` in `config.toml` stops it;
`launcher.log` says what it did). Opening the same file again updates it
too (settings, saves and game data stay).
The desktop file (`steam_frame_installer.desktop`) downloads the latest
release's `mjolnir-vr-steam-frame.zip` (`tools/steam_frame_bundle.py`, made by
the release job of `.github/workflows/build.yml`) and runs its
`install.sh` (`steam_frame_install.sh`), with zenity dialogs:
- `~/Documents/RingfallMjolnirVR`: the launcher (`ringfall-mjolnir-vr.sh`)
and the release APK (`RingfallMjolnirVR.apk`); the folder of the builds
before the name, `~/Documents/HaloCE`, renamed to it, and their "Halo VR"
shortcut replaced. (These keep the name of builds 302 to 313, "Ringfall:
Mjolnir VR": the Steam shortcut of an installed game starts the launcher
there, and the launcher's update looks for them in the bundle.);
- the maps folder out of the disc image (`steam_frame_xiso.py`, the app's
`XisoExtractor.java` in Python), unless `maps/ui.map` is there;
- the Steam shortcut, unless it is there: added by Steam itself
(`steam://addnonsteamgame`, as SteamOS's "Add to Steam" does) from
`Mjolnir VR.desktop` beside the game, then the library pictures
under the id Steam gave it. Steam is not closed: on the Frame, Desktop
Mode runs inside Gaming Mode's session, and closing Steam ends it (and the
installer with it). An old "Halo VR" shortcut is left for the player to
remove (Steam would write it back);
- what it did in `install.log` beside the game.
`HALO_VR_UNATTENDED=1` (and `HALO_VR_IMAGE=<image>`) runs it without
dialogs, for testing over adb; it leaves Steam and its shortcuts alone.
## 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 io.github.cesarus85.ringfall/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/RingfallMjolnirVR` |
| 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 ("Mjolnir VR", a non-Steam game)
runs `~/Documents/RingfallMjolnirVR/ringfall-mjolnir-vr.sh` (`port/android/steam_frame_launch.sh`),
which starts the game as Steam starts an Android title of its own:
`lepton waitforexitandrun -- ~/Documents/RingfallMjolnirVR/RingfallMjolnirVR.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 the game" (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` can be written, the launcher makes
`/home/steamos/Documents/RingfallMjolnirVR` and the launcher and the game take it as
their data folder (config.toml, saves and debug.txt go there too), so a
disc image imported in the app stays as well. The installer above fills it;
by hand, once:
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/RingfallMjolnirVR/maps
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/RingfallMjolnirVR/maps/
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/RingfallMjolnirVR
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.
+600
View File
@@ -0,0 +1,600 @@
# VR roadmap
What Mjolnir VR (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/RingfallMjolnirVR` |
| Rendering | The eyes drawn one by one (both at once, multiview, only with `debug.multiview`: its eyes did not always fuse, see the open points); 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; each weapon held so that its model's hand lands on the real one, as the free hand's (it learns where each weapon's animations hold the hand; the pistol's had been 15 cm before the real hand, the assault rifle's 8 cm aside), the scope's disc moved with it; `vr.weapon_hand_*` to adjust (2026-10-17) |
| 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 |
| Scores | The menu button held shows the scores on the Quest (a press pauses, as it is let go); the Steam Frame's View button as before; the text chat's lines where the desktop has them (2026-10-18) |
| 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 32, 2026-10-20; HDR kept off in VR): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list |
| Bots | Bots in multiplayer (`host_bots.c`; OpenCE's own, `bots.c`, are local split screen games' and off in VR): on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person, on the LAN too (2026-10-18); they climb ladders (Damnation's) and take paths as near their goal as a search reaches, as OpenCE's do: on Damnation 0 searches of 1747 failed, 1078 of 1605 before (2026-10-20). Later: OpenCE's bots' aiming (lead, bursts, taps for the pistol and sniper rifle, field of view), learnt obstacles and map fixes into ours |
| Voice chat | Push to talk on any button (`vr.button_talk`, BUTTONS page; the free hand's grip by default, sending nothing while it reaches for the foregrip or the wheel), open mic, off (2026-10-13) |
| Builds | The Android build's assembly conversion (`tools/android_asm_convert.py`) keeps string data as it is: strings and tables starting with "_" or holding \" lost a "_" (Opus's pulse cache among them: the Steam Frame's crashes in online games while talking), 2026-10-13 |
| Languages | The European disc in Spanish and Italian (a maps folder for each, `maps_es` and `maps_it`, none called `maps`) imported on the Quest and the Steam Frame: the headset's language's, its language the game's and the menus' (`maps/language.txt`, unless `game.language` is set), 2026-10-17; not yet tried with that disc |
| 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 **done** (in `main` 2026-10-06; Quest 3 good): buttons remapped, finer turning and vignette choices; the flashlight by tapping the head taken out (did not work) |
| 3b | The whole body: torso from head and hands, legs and feet following in the room; full / arms only / hands only | **done** (in `main` 2026-10-10; Quest 3 good): `vr.body` "full" ("Full body" in the VR options; "arms" still the default), legs walking the way the player moves, feet on the ground. Also in: voice chat's push to talk on the free hand's grip. Steam Frame: its cost still to measure |
| 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; moving by swinging the arms (in place of the stick, never faster than it: online too), and a sprint by swinging them harder (faster than the game allows: offline only) | import **done** (in `main` 2026-10-06): the app's import (tried on the Quest 3: all 24 maps; on the Frame kept in `~/Documents/RingfallMjolnirVR`), a Steam Frame installer from Desktop Mode (`STEAM_FRAME.md`; tried on the Frame: the old folder renamed, the shortcut added by Steam), a release signing key (CI and local builds the same). Arm swinging and its offline sprint still open |
| 5 | Later: multiview again (both eyes in one pass, half the drawing's CPU cost): find why its eyes do not fuse in big online games on the Quest 3 (and now and then on the Steam Frame), then turn it back on | open: off by default since 2026-10-16 (`debug.multiview`); findings in the open points |
| R | Release as **Ringfall: Mjolnir VR** (no "Halo" in the product's name): app name, id `io.github.cesarus85.ringfall`, icon and library pictures (`tools/ringfall_art.py`), Frame folder and installer, release assets, README; the repository renamed | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good) |
| R2 | Renamed **Mjolnir VR** ("Ringfall" is another game's name): app and Steam names, logo (`tools/mjolnir_art.py`), release assets (also under the old names for the updaters of builds 302 to 313), updaters, README; the repository renamed `Cesarus85/mjolnir-vr` (GitHub forwards the old links). Kept: the app id, the signing key's alias, the Frame's `~/Documents/RingfallMjolnirVR` and the names inside its bundle (installed games and their shortcuts) | in `feature/rename-mjolnir-vr` (2026-10-21) |
## 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` | `"auto"` (default, branch `fix/vr-frame-hitches`), `"low"`, `"medium"`, `"high"`, `"max"` |
| `vr.foveation` | `"off"` (default), `"low"`, `"medium"` (the Quest only; branch `fix/vr-frame-hitches`) |
| `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 (done)
In `main` 2026-10-06; tried on the Quest 3 (with the rename): good. The flashlight by tapping the head (after HaloCE-Quest-VR)
did not work in the headset and was taken out again; the button stays.
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,
"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, melee and push to talk each go on one of eight
buttons: A, B, X, Y, the move stick's click, the turning stick's, the
weapon hand's grip, the free hand's grip. Moving an action to a button
swaps it with the action there, so none is ever left without one.
- The free hand's grip as a button (`free_grip_down`): pressed once held
0.15 s at the foregrip's threshold, and not at all while it holds the
weapon's foregrip or the Warthog's wheel (taken in that time, the press
is theirs until the grip opens).
- The triggers, 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.
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.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. 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.
5. 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.
## Step 3b (done)
On `feature/vr-voice-and-body`, not yet tried in the headset. The neck
pivot, the planted feet with their steps and the leg's clamped reach follow
HaloCE-Quest-VR (CC0), its full body, rebuilt in
`port/linux/game/vr_body.c` on this port's skeleton (`vr_arms.c`).
- `vr.body = "full"` (VR options, Body: "Full body") draws the player's own
biped under the first-person arms and weapon: its torso, hips and legs, in
the map's own model and colours. "arms" stays the default until the
headsets have tried it.
- Drawn among the world's objects, so it sorts against the world by depth;
the first-person weapon and arms, drawn before the world into the
stencil, stay over it, as before. In both eyes (multiview on the Quest,
each eye on SteamVR), posed once a frame for every eye and the scope.
- The torso: the torso's heading (the head's beyond 15 degrees, drawn
towards the hands, the stick's turns at once), upright from the hips to a
neck 14 cm behind and 20 cm below the eyes as the head turns about it
(looking down, the eyes go forward and the chest stays behind them). The
aiming's twist and bend of the third-person torso are taken out.
- The hips: down with the physical crouch (and back a little, the torso
leaning forward), a third of the way with a lean (the spine tilts the
rest). The body's model has its neck higher than the eyes' pivot: it is
moved down to it, so the shoulders stay below the eyes, crouched by the
button too.
- The legs: the game's own animation (walking, running, jumping, crouched
by the button). Moving, the hips and legs stay as the game faces them, so
its own choice of animation (forward, back, aside, as for any player,
by the way it moves against the way it faces) runs along the way the
player moves, and the spine turns the torso; standing, the hips turn to
the torso's heading (eased, 0.12 s). Walking off, the feet ease from
where they stood into the walk (0.2 s); stopping, they stay where the
walk left them and step into place. Standing still, the feet stay
planted and step, one at a time (0.22 s, 6 cm lifted), when the body has
turned 35 degrees from them or their place moved 18 cm; the knees bend by
IK to them as the hips go down. Moved 80 cm at once (a teleporter), they
are put down there.
- The ground: put down, each foot rests on the ground under it (a slope, a
stair), its height above the ground as the animation has it, tilted with
the slope (no steeper than 45 degrees), up to 45 cm from the ground under
the body; a foot's ground below it lowers the hips as far. Rays straight
down, read only, cast only as the feet are put down (three: under the
body and each foot) and as one steps (one), none while walking, where
the animation's feet are kept; eased (0.08 s) in and out. Nothing found
(a ledge): level.
- Hidden: the head and neck (shrunk onto the neck's root: no head or
collar seen from inside), the third-person arms (shrunk onto their
shoulders), the third-person weapon and all the biped carries.
- The first-person arms hang from the body's shoulders: beside its neck,
as wide as its shoulder joints within 14 to 24 cm (the aiming's shrug
left out). Left-handed, the shoulders go into the mirror with the arms
(the room's left one is the model's right): the body itself is the
room's, not mirrored.
- Seats: a passenger in an open vehicle (the Warthog's back) sits as the
seat's animation has it, the head and arms shrunk alike; at a wheel or a
gun the body is drawn whole as before (its hands on them, no
first-person weapon); in a closed seat (Ghost, Banshee, tank) none.
- Not drawn when dead, in cutscenes, in third person, in a mirror's view;
camouflage as the game has it.
- Only drawn, on this machine: the game's own pose, hit boxes, markers and
network messages are unchanged.
- Cost: `vr.log_performance` adds `body ... ms a frame in ... poses` to the
`first person:` line. In the emulator 0.006 to 0.025 ms a pose standing
(as busy as the emulator was), up to 0.1 ms averaged while turning with
the feet's rays (at most a few a second); the drawing is one more biped
model (as one more player close by).
| Setting | Values |
| --- | --- |
| `vr.body` | `"full"` (the whole body), `"arms"` (default), `"hands"`, `"off"` |
| `debug.vr_body` | `"off"` (default); off the headset (the emulator) the body posed from the flat camera: `"eye"`, `"down"` (looking down 60 degrees, 90 degrees wide), `"front"`, `"side"`, `"back"` (the camera 2.6 m away); after a colon the head's motion: `"cycle"` (turning, crouching, leaning, 5 s each), `"still"`, `"turn"`, `"crouch"`, `"lean"`, `"raw"` (the game's pose, only shrunk). Logs its pose once a second |
Checked in the emulator (`debug.vr_body`): the cyborg's 19 nodes (bip01
pelvis, spine, spine1, neck, head, clavicles, arms, thighs, calves, feet);
the body standing, crouched physically and by the button, leaning,
turning with the feet stepping; looking down, the shoulders below the eyes
and no head or collar; two-minute bot games on Blood Gulch with it on and
off, deaths and respawns, nothing in the log. Then the legs by the way of
moving (strafing, backwards, diagonally with the touch stick: the game's
sideways and backwards walk) and the feet on the ground: on the ramp of a
Blood Gulch base, the upper foot 15 cm up, the lower 10 cm down, both on
it; stairs not reached in the emulator (Prisoner's spawn was level). Three
rays when the feet are put down, one a step, none walking. Screenshots in
`build/body-shots/` (local).
Push to talk is built on the same branch: voice chat's talking on the free
hand's grip, while it holds neither the weapon's foregrip nor the Warthog's
wheel (`audio.voice_chat`, VR options PLAY page "Voice chat": push to talk
(grip), open mic, off).
To try in the headset (Quest 3 and Steam Frame), with `vr.body = "full"`
(VR options, Body: Full body) and `vr.log_performance = true`:
1. Standing, looking down: torso, hips, legs and feet, the shoulders below
the eyes, no head, collar or helmet in view, the chest not in the way;
the arms from the shoulders, lined up with the body, the weapon as
before.
2. Turning the head alone: the body stays within about 15 degrees, then
follows; turning further, the feet step after it, one at a time, and
settle. Snap and smooth turns: the body turns, the feet step.
3. Physical crouch: the knees bend, the feet stay; kneeling deep, nothing
of the body in the eyes. Leaning forward and aside: the torso tilts, the
feet stay. Crouch button: the game's crouch, shoulders below the eyes.
4. Walking, running, jumping by the stick: the legs walk with the game's
animation, no sliding when starting or stopping. Strafing left and
right, walking backwards and diagonally, also with the head turned away
from the way of walking: the legs step the way the body moves (sideways,
backwards), the torso facing as the head and hands have it; stopping,
the hips turn back under the torso and the feet step into place.
4a. Ground: standing still on a slope (Blood Gulch's hills and the bases'
ramps), across it and along it: both feet on the ground, tilted with
it, the lower knee straighter, the hips down with the lower foot; on
stairs (Prisoner, Chill Out, Damnation), one foot a step higher than
the other, turning in place there: each step lands on its stair. At a
ledge, one foot over it: level, nothing odd.
5. Left-handed (`vr.handedness = left`, mirrored or not): the arms from
the body's shoulders, the weapon in the left hand, the body as
right-handed.
6. Vehicles: Warthog driver and gunner as before (body with its hands on
the wheel or gun); Warthog passenger: seated legs and torso, the
first-person arms from its shoulders; Ghost, Banshee, Scorpion: no body.
7. Dying and respawning, a teleporter, a cutscene (campaign), the menus
and pause menu, the scope, camouflage, a game with 15 bots: nothing
broken, no body where it should not be.
8. Body "Arms and hands", "Hands only", "Weapon only": as before.
9. Performance: `first person: ... body ... ms` in `adb logcat -s halo`;
the Quest holds 90 Hz in a bot game; the Steam Frame's working time
against "arms" (about 12 ms of 13.9 before).
10. Push to talk (Voice chat: push to talk), in a network game with
another player: holding the free grip away from the weapon talks;
nothing while the free hand holds the foregrip or the Warthog's wheel;
"Open mic" talks without the grip; "Off" sends nothing (others still
heard).
## Frame hitches and GPU time (built: branch `fix/vr-frame-hitches`)
Measured on the Quest 3 (2026-10-12; 90 Hz, multiview, graphics "high",
full body, a 15-bot game on Blood Gulch): the game's CPU work 5-6 ms a
frame; the app's GPU time 5.5-6.4 ms (`xr: metrics`), the same with
foveation off, low, medium and high: **the GPU is not the bottleneck**.
The doubled pictures come from single long frames: shader programs compiled
in a frame (29 new ones in a 794 ms frame 22 s into a match, single ones
18-64 ms), draws of 254 ms (the first frame in a map) and 74 ms with
nothing compiled (most likely the driver making a program's variants at
its first draw), and the menus' PNG pictures (7 in 108 ms).
What the branch has:
| Part | What |
| --- | --- |
| Long frames | `VR: long frame N ms: input, ticks, camera, draw, present; shaders compiled or linked, programs from the cache; textures uploaded, menu pictures, render targets; draw calls N ms, the slowest N ms (vertex program, program key); N draws left out waiting for their programs`. The 5 s report counts the draws left out |
| GPU time | XR_META_performance_metrics on with `vr.log_performance`: `xr: metrics (mean/most) app/gpu_frametime=... compositor/dropped_frame_count=...` |
| Foveation | `vr.foveation` "off" (default) / "low" / "medium" on the MORE page (GL_QCOM_texture_foveated, the eyes only, lens centres); "high" taken out (it drew a grey rectangle below the player). Kept for later, no gain measured |
| Shader cache | `display.shader_cache`: programs kept in `shader_cache/` (binary, driver, sources); each map's list (`maps/<map>.txt`) and every map's (`all.txt`) |
| Worker | `display.shader_worker` (default true): programs made on a thread with a GL context sharing the game's (`gl worker: running`), from the cache's binary or compiled; a draw whose program is not made yet is left out for those frames, the frame never waits. As a map loads, its programs and then every map's are asked for |
| Warm-up | `debug.shader_warm_up`: "worker" (default: each program drawn with once on the worker into a small target of the eyes' kinds), "load" (the map's programs drawn with on the game's thread as it loads), "off" |
| Menus | The PC menus' pictures decoded on a thread of their own, drawn transparent for the frames that takes |
Second Quest test (latest build): the worker works, no long frame after the
map's first frames (245 ms and 72 ms there, each one draw of 67-75 ms with
vertex program 4). But doubled pictures in spells of seconds with the GPU
at 9-13 ms a frame (GPU% 0.91-0.94, 4-37 stale frames a second), 5-7 ms in
between: "high" is too much for 90 Hz in heavy scenes, "medium" holds.
Hence:
| Part | What |
| --- | --- |
| Graphics "auto" | `vr.graphics = "auto"`, the new default (Graphics row: Auto): "high" while the GPU keeps up. Past 4/5 of the frame (8.9 ms at 90 Hz) for a quarter second it leaves out the next steps (shield glow, shadows, reflections, specular light, lens flares, bump maps, the rest of "low"), as many as their learnt costs say; brings one back once GPU time + its cost stays under 7/10 of the frame for 3 s (longer if it had to go again within 10 s). Logs `VR graphics auto: shadows left out (step 2): the GPU 10.2 ms a frame of 11.1`, `... saved 1.9 ms`, `... back`. On the Steam Frame (no GPU time from SteamVR) "auto" is "high" |
| Measuring | `debug.vr_graphics_measure = true`: each effect left out in turn (1.5 s on, 1.5 s off), `VR graphics measure: shadows 2.10 ms (...)` and means each round |
What the effects cost, from the code (to be measured with the option above):
object shadows are drawn once a frame with multiview (the scene is drawn
once), twice one eye at a time; each shadow-casting object (every bot, its
weapon, vehicles) renders its map, blurs it and projects it onto the level
beneath: a pass of fill on both eyes per object. Reflections and specular
light are passes over the level's geometry. The shield's glow
(`display.shield_glow`, off by default) is a dynamic light per flaring unit
(at most 8), each lighting the level's surfaces in its reach once more, not
a pass per object; upstream's shield colour on the arms only changes a
colour. The whole body adds the player's own model and its shadow.
Not done, and why:
- Drawing the eyes straight into the swapchain: moot now (GPU not the
bottleneck); it needs GL_EXT_sRGB_write_control, the vignette as a pass
of its own and array swapchains acquired mid-frame.
- Decoding all menu pictures ahead: 165 pictures, about 350 MB decoded with
their mipmaps.
- Later: XR_KHR_visibility_mask.
### Headset test, graphics "auto" (Quest 3)
1. VR options, MORE: Graphics **Auto** (a `config.toml` written before
still says "high"), Foveation Off; `vr.log_performance = true`.
2. Log at start: `xr: performance metrics on (... the app's GPU time read)`,
`VR: graphics auto`.
3. 15-bot match on Blood Gulch, a few minutes, also into the busy middle:
- `VR graphics auto: ... left out (step N): the GPU X ms` within a
quarter to half a second of a heavy spell, `... saved Y ms`, and
`... back` a few seconds after it.
- VrApi: `Stale=` 0 nearly every second (at most a few in the second a
spell begins), no `ASW=1` spells; App GPU under about 9 ms.
- Look for effects that pop: shadows going and coming back. If they
change more than every ten seconds or so, note the log lines.
4. Costs: a second match with `debug.vr_graphics_measure = true` (Graphics
setting ignored meanwhile; stand somewhere busy, a few minutes):
`VR graphics measure: means (ms of GPU a frame): shadows ...;
reflections ...`. The steps' order should follow the largest costs.
5. The first frames of a map (245/72 ms, vertex program 4): try
`debug.shader_warm_up = "load"` once and compare those lines.
### Headset test, shader worker (Quest 3)
Before: `adb logcat -G 16M` (again after any reboot); in `config.toml`
`vr.log_performance = true`, 90 Hz, graphics "high", multiview, full body;
Foveation Off (VR options, MORE: the config may still say "medium"). After
each test: `adb logcat -d -s halo VrApi > test.txt`. The shader cache from
the earlier tests stays.
1. Start: `gl worker: running (ES 3, surfaceless, programs warmed up)` and
`shader cache: ... N programs kept (... N listed) ...; programs made on
the worker, warmed up there`. As the menus load: `shader cache:
levels_ui_ui: N programs listed; N asked of the worker ...; N of other
maps to ask for`.
2. Bot match on Blood Gulch: as it loads, `... bloodgulch: N listed; ...
N there already` (made while in the menus). In play no `VR: long frame`
with shaders compiled; at most a few `draws left out` (objects appearing
a frame or two late), and VrApi `Stale=0` most seconds, no `ASW=1`.
Any long frame left: its `draw calls ... the slowest N ms (vertex program
N, program K)` says whether a draw (a driver variant) is the cause.
3. A map not played yet (Damnation): as it loads, `N programs listed` is 0
but `N there already` should not matter: its programs mostly come from
every map's list (made in the menus). New ones are compiled on the worker
(`draws left out` for a moment, no long frame).
4. If draws of 70-250 ms remain with nothing compiled: `debug.shader_warm_up
= "load"` (the log: `shader cache: N programs drawn with once as the map
loaded, in N ms`), then compare; and `"off"` for the baseline.
5. Menus: open new screens; pictures may appear a frame late, no long
frames with `menu pictures`.
6. If anything breaks (missing objects for good, a crash):
`display.shader_worker = false` brings the game's-thread compiling back.
Steam Frame: the worker also runs there if its EGL shares contexts (the log
says `gl worker: running` or why not); no foveation. A short bot game to
see that nothing is missing or slower.
## Open points outside the order
- Multiview's doubled pictures (2026-10-16): on the developer's Quest 3,
doubled and shifted pictures from the start of big online games (26-35
players) as the head turned, nauseating; none in a local game with 48
bots; none online either with the eyes drawn one by one. Not the frame
rate: one by one, online at 72 Hz, the compositor repeated as many
frames (about 4 a second) and nothing was seen. Not the facing jumps
(respawns only) nor the camera (it moves every frame). Before: the
Steam Frame's (2026-10-05), each eye alone sharp. So the eyes are drawn
one by one by default now (`debug.multiview` for both at once). To do:
find what the shared pass draws wrongly online, e.g. by saving both
eyes' pictures in a big online game and comparing what is doubled
(sprites and effects projected by the game itself, `mv_depth`; programs
made on the shader worker; the sky rows), then turn it back on: it
halves the drawing's CPU cost.
- Big online games (2026-10-16): doubled pictures on a 32-player server,
none in a local game with 48 bots. Not the GPU (60–80 %): the ticks cost
3.3–4.6 ms as a client there (1.3–1.9 ms with bots or 16 players), so
47–70 of 150 frames with a tick missed 90 Hz's 11.1 ms (the doubling
itself was multiview's, above). In `main` 2026-10-16 (Quest 3 good):
`vr.refresh_rate_auto`
(VR options MORE, "Lower it when frames are late", on by default) steps
the rate down a step (90, 80, 72 Hz) while more than one frame in 32
comes late, and back up once they would keep up; "VR: refresh rate" in
the log. First try: 90 to 80 Hz a few seconds into a 30-player game, no
doubled picture seen; 2-5 stale frames a second left at 80, hence one
in 32 (was 16). Later:
why a client's ticks cost that much per remote player.
- Doubled pictures when turning on the developer's Quest 3 (2026-10-12):
with every build tried, back to 2026-10-06's, at 90 and at 72 Hz, even
in seconds the compositor reported no stale frame. That Quest runs
Horizon OS from the Public Test Channel (PTC; its build 2026-10-08),
the likeliest cause. Released with `vr.graphics` "medium" as the
default (smooth there). To do: try this and later builds with "high" on
a Quest 3 outside PTC (the developer's wife's), and set the default
back to "high" or "auto" if it holds.
Resolved 2026-10-16: multiview (above), not the Horizon OS PTC.
- 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), in `main`;
still to check in a game of 30 and more players; upstream raised them
to 2048 too (2026-10-12).
## 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).
+27 -5
View File
@@ -13,16 +13,30 @@ def buildNumberText = System.getenv("HALO_BUILD_NUMBER") ?: "0"
def buildNumber = buildNumberText.isInteger() ? buildNumberText.toInteger() : 0
// the workflow's signing key (.github/workflows/build.yml): every build of
// main signed with it can install over the one before; without it, the
// main signed with it can install over the one before; on a computer that
// builds for its own devices, the same key from port/android/signing.properties
// (not in the repository: storeFile, password, keyAlias), so that its
// builds and the releases install over each other; without either, the
// debug key of the computer that builds
def signingKey = System.getenv("HALO_ANDROID_KEYSTORE")
def signingPassword = System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
def signingAlias = System.getenv("HALO_ANDROID_KEY_ALIAS") ?: "ringfall"
def signingProperties = new File(rootDir, "signing.properties")
if (!signingKey && signingProperties.isFile()) {
def properties = new Properties()
signingProperties.withInputStream { properties.load(it) }
signingKey = properties.getProperty("storeFile")
signingPassword = properties.getProperty("password")
signingAlias = properties.getProperty("keyAlias", "ringfall")
}
android {
namespace "com.halo.decomp"
compileSdk 35
defaultConfig {
applicationId "com.halo.decomp"
// (the app's own id; the Java classes keep the decompilation's package)
applicationId "io.github.cesarus85.ringfall"
minSdk 28
targetSdk 35
versionCode Math.max(1, buildNumber)
@@ -60,9 +74,9 @@ android {
if (signingKey) {
halo {
storeFile file(signingKey)
storePassword System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
keyAlias System.getenv("HALO_ANDROID_KEY_ALIAS") ?: "halo"
keyPassword System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
storePassword signingPassword
keyAlias signingAlias
keyPassword signingPassword
}
}
}
@@ -84,3 +98,11 @@ android {
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
// the Khronos OpenXR loader (VR, port/android/host/host_xr.c): its
// library, and the manifest entries through which it finds the
// headset's runtime. The version matches OPENXR_VERSION in
// tools/android_build.py, whose headers libmain.so is built against.
implementation "org.khronos.openxr:openxr_loader_for_android:1.1.63"
}
+22 -5
View File
@@ -7,6 +7,10 @@
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
<!-- VR (port/android/README.md, "VR"): on a headset the game runs as an
immersive OpenXR app; elsewhere as before. Not required, so the one
APK still installs on phones. -->
<uses-feature android:name="android.hardware.vr.headtracking" android:version="1" android:required="false" />
<!-- controller rumble -->
<uses-permission android:name="android.permission.VIBRATE" />
@@ -68,26 +72,39 @@
</intent-filter>
</activity>
<!-- in a process of its own, with a fresh Java heap (HaloApplication) -->
<!-- The VR categories make headsets start this activity immersive
(without them Horizon OS shows it as a flat panel, and the OpenXR
session never gets focus); phones ignore them. The launcher,
a flat panel on a headset, starts it once the game data is
there. (port: upstream runs it in a process of its own,
":game", for handhelds whose Java heap covers the Xbox window;
not here: on the Steam Frame Lepton waits for the app's process
to end, and the headsets have no such overlap) -->
<activity
android:name=".HaloActivity"
android:process=":game"
android:exported="false"
android:exported="true"
android:alwaysRetainTaskState="true"
android:launchMode="singleInstance"
android:screenOrientation="sensorLandscape"
android:screenOrientation="landscape"
android:resizeableActivity="false"
android:preferMinimalPostProcessing="true"
android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="com.oculus.intent.category.VR" />
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
</intent-filter>
<meta-data
android:name="android.app.category"
android:value="game" />
</activity>
<meta-data android:name="com.oculus.supportedDevices" android:value="quest2|questpro|quest3|quest3s" />
<!-- the downloaded new version, for the package installer (Updater) -->
<provider
android:name=".UpdateProvider"
android:authorities="com.halo.decomp.update"
android:authorities="${applicationId}.update"
android:exported="false"
android:grantUriPermissions="true" />
@@ -27,7 +27,7 @@ import java.nio.channels.FileChannel;
* Starts the game once its data is in place.
*
* The game reads the Xbox game data (the folder holding maps/) from the
* app's external files directory, /sdcard/Android/data/com.halo.decomp/files.
* app's external files directory, /sdcard/Android/data/<application id>/files.
* If it is missing, this screen lets the player pick an Xbox disc image of
* the game (.xiso or .iso, any version) with the system file picker, and
* copies its maps folder there (XisoExtractor), as the desktop games do; or
@@ -35,6 +35,7 @@ import java.nio.channels.FileChannel;
*/
public class LauncherActivity extends Activity {
private static final int PICK_IMAGE = 1;
static final String SHARED_DATA_ROOT = "/home/steamos/Documents/RingfallMjolnirVR";
private File dataRoot;
private TextView status;
@@ -46,6 +47,17 @@ 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, whenever they can be written, so that a disc
// image imported here stays too (port/android/STEAM_FRAME.md; the
// same choice as host_main.c's)
File shared = new File(SHARED_DATA_ROOT);
if (shared.getParentFile().canWrite() || shared.canWrite()) {
shared.mkdirs();
if (shared.canWrite())
dataRoot = shared;
}
// 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)
@@ -111,8 +123,10 @@ public class LauncherActivity extends Activity {
partial.delete();
}
/** The game data, or the marker file of the VR self-test (host_xr.c). */
private boolean haveData() {
return dataRoot != null && new File(dataRoot, "maps/ui.map").isFile();
return dataRoot != null && (new File(dataRoot, "maps/ui.map").isFile() ||
new File(dataRoot, "xr_test").isFile());
}
private void startGame() {
@@ -133,15 +147,15 @@ public class LauncherActivity extends Activity {
layout.setBackgroundColor(Color.rgb(12, 16, 20));
TextView title = new TextView(this);
title.setText("Halo needs its game data");
title.setText("Mjolnir VR needs game data");
title.setTextColor(Color.WHITE);
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24);
title.setGravity(Gravity.CENTER);
layout.addView(title);
TextView message = new TextView(this);
message.setText("Choose an Xbox disc image of Halo: Combat Evolved (an .iso or .xiso file, any "
+ "version) on this device. Its maps folder is copied into the app's storage (about 1.8 GB), "
message.setText("Choose your own disc image of Halo: Combat Evolved for the original Xbox (an .iso or "
+ ".xiso file, any version) on this device. Its maps folder is copied into the app's storage (about 1.8 GB), "
+ "and you can delete the image afterwards.\n\n"
+ "You can also copy a maps folder from a computer:\n"
+ "adb push <folder with maps>/. " + (dataRoot != null ? dataRoot.getAbsolutePath() : "") + "/");
@@ -13,13 +13,13 @@ import java.io.FileNotFoundException;
/**
* Hands Android's package installer the downloaded new version of the app
* (Updater), read-only, the one file content://com.halo.decomp.update/halo.apk.
* (Updater), read-only, the one file content://<application id>.update/update.apk.
* (The app has no AndroidX, whose FileProvider does the same.)
*/
public class UpdateProvider extends ContentProvider {
static final String AUTHORITY = "com.halo.decomp.update";
static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".update";
static final String DIRECTORY = "update";
static final String APK = "halo.apk";
static final String APK = "update.apk";
private File apk() {
return new File(new File(getContext().getCacheDir(), DIRECTORY), APK);
@@ -36,7 +36,8 @@ import java.util.zip.ZipInputStream;
* the latest release when it starts, on a thread of its own, and if it is
* newer asks the player whether to update:
*
* - Yes: the release's app (halo-android-release.zip or -debug.zip) is
* - Yes: the release's app (mjolnir-vr-android-release.zip or
* -debug.zip) is
* downloaded and handed to Android's package installer, which replaces the
* game (closing it) and offers to open the new version.
* - No: nothing, until the next start.
@@ -45,9 +46,15 @@ import java.util.zip.ZipInputStream;
*
* Every build of main is signed with the same key (the workflow's), which an
* app must keep for Android to install a new version over it.
*
* Not on the Steam Frame: Lepton installs the APK in the game's folder there
* at every start, so its launcher updates that (steam_frame_launch.sh).
*/
final class Updater {
private static final String REPOSITORY = "OpenCommunityEdition/OpenCE";
/* the VR port's own releases: the original's, made without VR, must
never replace it (they share the package name) */
private static final String APP_NAME = "Mjolnir VR";
private static final String REPOSITORY = "Cesarus85/mjolnir-vr";
private static final String USER_AGENT = "halo-ce-universal-updater";
private static final int TIMEOUT_MILLISECONDS = 20000;
/** the most a download (a release's zip, about 35 MB) or the app in it may be */
@@ -60,7 +67,8 @@ final class Updater {
static void start(Activity activity) {
File config = configFile(activity);
if (BuildConfig.HALO_BUILD_NUMBER <= 0 || config == null || !autoUpdate(config))
if (BuildConfig.HALO_BUILD_NUMBER <= 0 || config == null || !autoUpdate(config) ||
new File(LauncherActivity.SHARED_DATA_ROOT).isDirectory())
return;
new Thread(() -> {
int latest = latestRelease();
@@ -202,8 +210,8 @@ final class Updater {
if (activity.isFinishing())
return;
new AlertDialog.Builder(activity)
.setTitle("Halo: new version")
.setMessage("A new version of Halo was detected (build " + latest + "; this is build "
.setTitle(APP_NAME + ": new version")
.setMessage("A new version of " + APP_NAME + " was detected (build " + latest + "; this is build "
+ BuildConfig.HALO_BUILD_NUMBER + ").\n\nDo you want to update? The game will close and start "
+ "the new version.")
.setCancelable(false)
@@ -215,7 +223,7 @@ final class Updater {
private static void confirmNever(Activity activity) {
new AlertDialog.Builder(activity)
.setTitle("Halo: new version")
.setTitle(APP_NAME + ": new version")
.setMessage("Stop asking about new versions?\n\nTo ask again, set auto = true in the [update] section "
+ "of config.toml.")
.setCancelable(false)
@@ -232,7 +240,7 @@ final class Updater {
/* ---------- updating */
private static void update(Activity activity, int latest) {
String asset = "halo-android-" + (BuildConfig.DEBUG ? "debug" : "release") + ".zip";
String asset = "mjolnir-vr-android-" + (BuildConfig.DEBUG ? "debug" : "release") + ".zip";
File directory = new File(activity.getCacheDir(), UpdateProvider.DIRECTORY);
LinearLayout layout = new LinearLayout(activity);
TextView status = new TextView(activity);
@@ -247,7 +255,7 @@ final class Updater {
layout.addView(status);
layout.addView(bar);
AlertDialog progress = new AlertDialog.Builder(activity)
.setTitle("Halo: new version")
.setTitle(APP_NAME + ": new version")
.setView(layout)
.setCancelable(false)
.show();
@@ -275,7 +283,7 @@ final class Updater {
activity.runOnUiThread(() -> {
progress.dismiss();
new AlertDialog.Builder(activity)
.setTitle("Halo: new version")
.setTitle(APP_NAME + ": new version")
.setMessage("The update failed:\n\n" + e.getMessage())
.setPositiveButton("OK", null)
.show();
@@ -153,26 +153,32 @@ final class XisoExtractor {
break;
}
}
/* a multi-language European disc has no "maps" but a "maps_xx" per
language: the one for the device's language, else English, else the
first (as port/linux/src/xiso.c) */
// (Mjolnir VR: a European disc in two languages, Spanish and Italian,
// has no maps folder but one for each, maps_es and maps_it: the
// device's language's is taken, else English's, else the first, and
// its language written to maps/language.txt, which the game asks the
// Xbox for while game.language is unset: port/linux/src/xbox_xapi.c)
String language = null;
if (maps == null) {
String wanted = "maps_" + Locale.getDefault().getLanguage();
String wanted = Locale.getDefault().getLanguage();
Entry english = null, first = null;
for (Entry entry : directories) {
if (entry.name.length() != 7 || !entry.name.regionMatches(true, 0, "maps_", 0, 5))
for (Entry entry : walkTable(root, true)) {
String code = mapsLanguage(entry.name);
if (code == null)
continue;
if (entry.name.equalsIgnoreCase(wanted)) {
if (code.equals(wanted) && maps == null)
maps = entry;
break;
}
if (english == null && entry.name.equalsIgnoreCase("maps_en"))
if (code.equals("en") && english == null)
english = entry;
if (first == null)
first = entry;
}
if (maps == null)
maps = english != null ? english : first;
if (maps != null)
language = mapsLanguage(maps.name);
}
if (maps == null)
throw new ExtractException("The disc image has no maps folder: it is not a Halo disc.");
@@ -223,6 +229,16 @@ final class XisoExtractor {
File mapsFolder = new File(destination, "maps");
if (!mapsFolder.isDirectory() && !mapsFolder.mkdirs() && !mapsFolder.isDirectory())
throw new ExtractException("Could not create " + mapsFolder.getPath() + ".");
// (the language of a language's maps folder: before ui.map is there)
File languageFile = new File(mapsFolder, "language.txt");
languageFile.delete();
if (language != null) {
try (FileOutputStream out = new FileOutputStream(languageFile)) {
out.write((language + "\n").getBytes(StandardCharsets.US_ASCII));
} catch (IOException e) {
throw new ExtractException("Could not write " + languageFile.getPath() + " (is the storage full?).");
}
}
List<Entry> order = new ArrayList<>(files);
order.remove(userInterface);
order.add(userInterface);
@@ -235,6 +251,14 @@ final class XisoExtractor {
partial.delete();
}
/** a language's maps folder's language ("maps_it": "it"), else null */
static String mapsLanguage(String name) {
if (name.length() != 7 || !name.regionMatches(true, 0, "maps_", 0, 5))
return null;
String code = name.substring(5).toLowerCase(Locale.ROOT);
return Character.isLetter(code.charAt(0)) && Character.isLetter(code.charAt(1)) ? code : null;
}
/** No path, no . or .., and only printable ASCII that every file system takes as a plain name. */
static boolean isPlainName(String name) {
if (name == null || name.isEmpty() || name.equals(".") || name.equals(".."))
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- the launcher icon's background (tools/app_icon.py) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient android:angle="270" android:startColor="#0A2B54" android:endColor="#030610" />
</shape>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- the launcher icon (tools/app_icon.py, from port/assets/icon) -->
<!-- the launcher icon (tools/android_icon.py, from port/android/art/android-icon.png) -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- the launcher icon's background (tools/android_icon.py) -->
<resources>
<color name="ic_launcher_background">#1E538A</color>
</resources>
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Halo</string>
<string name="app_name">Mjolnir VR</string>
</resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+60
View File
@@ -108,10 +108,70 @@ the GPU's writes to it */
void host_gl_read_buffer(unsigned int buffer, unsigned int offset, unsigned int size, void *data);
/* unsynchronized write into the buffer bound to target */
void host_gl_buffer_write(unsigned int target, unsigned int offset, unsigned int size, const void *data);
/* persistent, coherent storage for the buffer bound to target (0: the driver
cannot); writes into such a buffer (0: it has none) */
int host_gl_buffer_storage(unsigned int target, unsigned int size);
int host_gl_persistent_write(unsigned int buffer, unsigned int offset, unsigned int size, const void *data);
/* fences the GPU work queued so far as that of ring slot `slot`; waits for
the GPU to finish the work last fenced for a slot */
void host_gl_fence_frame(unsigned int slot);
void host_gl_wait_frame(unsigned int slot);
/* copies the visibility counters into snapshot `slot` at the end of a frame;
reads a snapshot once its frame is finished */
void host_gl_counters_snapshot(unsigned int buffer, unsigned int size, unsigned int slot);
int host_gl_counters_read(unsigned int slot, void *out, unsigned int size);
/* the shader programs' worker (port/android/host/host_gl_worker.c), for
port/linux/src/xgpu_program_cache.c: started on the game's thread with its
context current (1: running); a program asked for (its binary tried first,
else its sources compiled; urgent: a draw waits for it), a job's number or
0; hurried along; polled (1 done: *program, its kept binary's size, the
time it took; 0 not yet; -1 failed); its kept binary copied out; the job
forgotten; how many jobs are not done; and a warm-up draw with a program on
the calling thread's context */
int host_gl_worker_start(int warm_up);
unsigned int host_gl_program_request(int urgent, const char *vertex_source, const char *fragment_source,
unsigned int binary_format, const void *binary, unsigned int binary_size, int keep_binary);
void host_gl_program_hurry(unsigned int id);
int host_gl_program_poll(unsigned int id, unsigned int *program, unsigned int *binary_size, unsigned int *microseconds);
unsigned int host_gl_program_binary(unsigned int id, void *buffer, unsigned int size, unsigned int *format);
void host_gl_program_release(unsigned int id);
unsigned int host_gl_worker_waiting(void);
void host_gl_program_warm_up(unsigned int program, int multiview);
/* ---------- OpenXR (port/android/host/host_xr.c; the structures are in
halo_xr_abi.h) */
struct halo_xr_info;
struct halo_xr_frame;
struct halo_xr_submit;
/* creates the session on the current GL context (the host found the
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: 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 */
int host_xr_copy_layer_region(int layer, unsigned int texture, int clear_alpha, float left, float top, float right,
float bottom);
/* submits the frame begun by host_xr_begin_frame */
int host_xr_end_frame(const struct halo_xr_submit *submit);
void host_xr_haptic(int hand, float amplitude, float seconds);
/* copies a layer of a texture array (the eyes drawn at once, multiview)
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);
/* the app's GPU time of a recent frame (ms; 0: the runtime does not tell
it), for vr.graphics "auto" */
float host_xr_gpu_time(void);
/* ---------- Android */
+21
View File
@@ -106,5 +106,26 @@ 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);
/* the shader programs' worker (host_gl_worker.c; the guest's imports) */
int host_gl_worker_start(int warm_up);
uint32_t host_gl_program_request(int urgent, const char *vertex_source, const char *fragment_source,
uint32_t binary_format, const void *binary, uint32_t binary_size, int keep_binary);
void host_gl_program_hurry(uint32_t id);
int host_gl_program_poll(uint32_t id, uint32_t *program, uint32_t *binary_size, uint32_t *microseconds);
uint32_t host_gl_program_binary(uint32_t id, void *buffer, uint32_t size, uint32_t *format);
void host_gl_program_release(uint32_t id);
uint32_t host_gl_worker_waiting(void);
void host_gl_program_warm_up(uint32_t program, int multiview);
/* ---------- VR (host_xr.c) */
/* whether there is an OpenXR headset, and the size of its eye images;
before the game starts */
int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height);
int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
float bottom);
/* the VR self-test, instead of the game; does not return */
void host_xr_test(void) __attribute__((noreturn));
#endif
+182
View File
@@ -10,8 +10,53 @@ 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)
{
@@ -78,6 +123,57 @@ void host_gl_read_buffer(uint32_t buffer, uint32_t offset, uint32_t size, void *
glBindBuffer(GL_COPY_READ_BUFFER, 0);
}
/* The visibility tests' counters (d3d8_gl.c), copied at the end of each
frame into a ring of buffers that are read once the GPU has finished that
frame: reading the counters themselves (host_gl_read_buffer_word) waits for
the GPU in the middle of a frame, which on a tiler (Adreno) also writes the
half-drawn targets out of tile memory, for every lens flare. */
#define COUNTER_SNAPSHOTS 8
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)
{
if (slot >= COUNTER_SNAPSHOTS || !buffer || !size)
return;
if (!counter_snapshots[slot])
glGenBuffers(1, &counter_snapshots[slot]);
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
if (counter_snapshot_sizes[slot] != size)
{
glBufferData(GL_COPY_WRITE_BUFFER, size, NULL, GL_STREAM_READ);
counter_snapshot_sizes[slot] = size;
}
/* the counts come from shaders' atomic writes */
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, 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)
{
const void *mapping;
if (slot >= COUNTER_SNAPSHOTS || !counter_snapshots[slot] || size > counter_snapshot_sizes[slot])
return 0;
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
mapping = glMapBufferRange(GL_COPY_WRITE_BUFFER, 0, size, GL_MAP_READ_BIT);
if (mapping)
{
memcpy(out, mapping, size);
glUnmapBuffer(GL_COPY_WRITE_BUFFER);
}
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
return mapping != NULL;
}
/* The renderer streams each frame's vertices and indices into the next of
a ring of buffers (d3d8_gl.c). A fence marks the end of each frame's work,
and a buffer is written again only once the GPU has passed the fence of the
@@ -106,6 +202,92 @@ void host_gl_wait_frame(uint32_t slot)
frame_fences[slot] = NULL;
}
/* Buffers the renderer writes into for every draw (d3d8_gl.c: the stream
and index rings, the visibility counters), mapped once for good where the
driver has GL_EXT_buffer_storage. Adreno (Quest) waits for the GPU in every
glMapBufferRange, unsynchronized or not, and the game streams several
hundred small uploads a frame; into a persistent mapping each is a memcpy.
The renderer's frame fences keep it from writing what the GPU still reads,
as with the unsynchronized mappings. */
#define PERSISTENT_BUFFERS 48 /* the rings, the counters and the mirror's 32 segments */
static struct persistent_buffer
{
GLuint name;
uint32_t size;
uint8_t *mapping;
} persistent_buffers[PERSISTENT_BUFFERS];
static int persistent_buffer_count;
static GLenum binding_of(uint32_t target)
{
switch (target)
{
case GL_ARRAY_BUFFER: return GL_ARRAY_BUFFER_BINDING;
case GL_ELEMENT_ARRAY_BUFFER: return GL_ELEMENT_ARRAY_BUFFER_BINDING;
case GL_ATOMIC_COUNTER_BUFFER: return GL_ATOMIC_COUNTER_BUFFER_BINDING;
case GL_COPY_WRITE_BUFFER: return GL_COPY_WRITE_BUFFER_BINDING;
default: return 0;
}
}
/* gives the buffer bound to target persistent, coherent storage of size
bytes, instead of glBufferData; 0 if the driver cannot */
int host_gl_buffer_storage(uint32_t target, uint32_t size)
{
static int checked;
static PFNGLBUFFERSTORAGEEXTPROC buffer_storage;
const GLbitfield flags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT_EXT | GL_MAP_COHERENT_BIT_EXT;
GLint name = 0;
void *mapping;
if (!checked)
{
checked = 1;
if (host_gl_has_extension("GL_EXT_buffer_storage"))
buffer_storage = (PFNGLBUFFERSTORAGEEXTPROC)eglGetProcAddress("glBufferStorageEXT");
host_logf(HOST_LOG_INFO, "persistent buffers: %s", buffer_storage ? "yes" : "no");
}
if (!buffer_storage || !binding_of(target) || persistent_buffer_count == PERSISTENT_BUFFERS)
return 0;
glGetIntegerv(binding_of(target), &name);
if (!name)
return 0;
/* dynamic storage keeps glBufferSubData working on it too */
buffer_storage(target, size, NULL, flags | GL_DYNAMIC_STORAGE_BIT_EXT);
mapping = glMapBufferRange(target, 0, size, flags);
if (!mapping)
{
host_logf(HOST_LOG_WARN, "persistent buffer %d cannot be mapped", name);
return 0;
}
persistent_buffers[persistent_buffer_count].name = (GLuint)name;
persistent_buffers[persistent_buffer_count].size = size;
persistent_buffers[persistent_buffer_count].mapping = mapping;
persistent_buffer_count++;
return 1;
}
/* writes into a buffer given persistent storage; 0 if it has none */
int host_gl_persistent_write(uint32_t buffer, uint32_t offset, uint32_t size, const void *data)
{
int index;
for (index = 0; index < persistent_buffer_count; index++)
{
struct persistent_buffer *entry = &persistent_buffers[index];
if (entry->name == buffer)
{
if (offset > entry->size || size > entry->size - offset)
return 0;
memcpy(entry->mapping + offset, data, size);
return 1;
}
}
return 0;
}
/* writes data into the buffer bound to target. The renderer streams a
range per draw, so a frame makes hundreds of these, and the cost per call
rather than per byte is what a frame is made of.
+671
View File
@@ -0,0 +1,671 @@
/*
HOST_GL_WORKER.C
The shader programs' worker (port/linux/src/xgpu_program_cache.c): a thread
with a GL context of its own, sharing the game's objects, that makes the
programs the game asks for while the game goes on drawing. The drivers
compile a program the first time it is drawn with, 10 to 70 ms each on the
Quest's, and the frame waited for it: in VR a frame shown late, the picture
doubled as the head turns.
- host_gl_worker_start, on the game's thread with its context current:
makes a context that shares the game's (surfaceless, or on a 1x1
pbuffer) and the thread that keeps it current.
- host_gl_program_request: a program of these sources, from their binary
where one is given and the driver takes it, else compiled and linked
(its binary kept for the cache's file). The sources and the binary are
copied: the caller's may go at once. Urgent requests (a draw waits for
them) go before the others (a map's programs made ready as it loads).
- On the worker each program is linked, its samplers set to their units
(as d3d8_gl.c sets them), drawn with once into a small target of its own
(a degenerate triangle: the driver makes the variants it makes at a
first draw now, not in the game's frame; "warm up"), and a fence put
after it all.
- host_gl_program_poll, on the game's thread: done once the fence has
passed (GL's rule for an object made in one context and used in another:
the work finished before the other binds it); then the program is the
game's (a program name is the same in both contexts).
host_gl_program_warm_up draws once with a program on the calling thread's
context instead (debug.shader_warm_up "load": on the game's, as a map
loads), for comparing the two.
*/
#include "host.h"
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl32.h>
#include <GLES2/gl2ext.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define JOB_BUCKETS 1024
/* the warm-up's target, each way */
#define WARM_UP_SIZE 16
enum
{
JOB_QUEUED,
JOB_WORKING,
JOB_DONE,
JOB_FAILED,
};
struct job
{
struct job *next_in_bucket;
struct job *next_in_queue;
uint32_t id;
int urgent;
char *sources[2];
void *binary;
uint32_t binary_size;
uint32_t binary_format;
int keep_binary;
int state;
/* the results */
GLuint program;
GLsync sync;
int from_binary;
void *kept_binary;
uint32_t kept_size;
uint32_t kept_format;
uint32_t microseconds;
};
/* a context's warm-up target: the eyes' kinds, one layer and two
(multiview), RGBA8 with a depth and stencil buffer */
struct warm_up_targets
{
int made;
/* each kind's framebuffer complete */
int complete[2];
GLuint framebuffers[2];
GLuint textures[4];
GLuint vertex_array;
GLuint counters;
};
static struct
{
int started;
int running;
int warm_up;
EGLDisplay display;
EGLContext context;
EGLSurface surface;
pthread_t thread;
pthread_mutex_t lock;
pthread_cond_t wake;
pthread_cond_t ready;
struct job *buckets[JOB_BUCKETS];
struct job *urgent_first, *urgent_last;
struct job *later_first, *later_last;
uint32_t next_id;
uint32_t waiting;
int failures_logged;
PFNGLFRAMEBUFFERTEXTUREMULTIVIEWOVRPROC framebuffer_texture_multiview;
struct warm_up_targets targets[2];
} worker;
static uint64_t now_us(void)
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (uint64_t)now.tv_sec * 1000000u + (uint64_t)now.tv_nsec / 1000u;
}
/* ---------- the warm-up draw */
static int targets_make(struct warm_up_targets *targets)
{
GLint major = 0, minor = 0;
int kind;
if (targets->made)
return targets->made > 0;
targets->made = -1;
glGenTextures(4, targets->textures);
glGenFramebuffers(2, targets->framebuffers);
for (kind = 0; kind < 2; kind++)
{
GLenum target = kind ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_2D;
glBindTexture(target, targets->textures[kind * 2]);
if (kind)
glTexStorage3D(target, 1, GL_RGBA8, WARM_UP_SIZE, WARM_UP_SIZE, 2);
else
glTexStorage2D(target, 1, GL_RGBA8, WARM_UP_SIZE, WARM_UP_SIZE);
glBindTexture(target, targets->textures[kind * 2 + 1]);
if (kind)
glTexStorage3D(target, 1, GL_DEPTH24_STENCIL8, WARM_UP_SIZE, WARM_UP_SIZE, 2);
else
glTexStorage2D(target, 1, GL_DEPTH24_STENCIL8, WARM_UP_SIZE, WARM_UP_SIZE);
glBindTexture(target, 0);
glBindFramebuffer(GL_FRAMEBUFFER, targets->framebuffers[kind]);
if (kind)
{
if (!worker.framebuffer_texture_multiview)
continue;
worker.framebuffer_texture_multiview(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, targets->textures[2], 0, 0, 2);
worker.framebuffer_texture_multiview(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, targets->textures[3], 0,
0, 2);
}
else
{
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, targets->textures[0], 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, targets->textures[1], 0);
}
targets->complete[kind] = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
if (!targets->complete[kind])
host_logf(HOST_LOG_WARN, "gl worker: the %s warm-up target is incomplete; no warm-up there",
kind ? "multiview" : "plain");
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glGenVertexArrays(1, &targets->vertex_array);
/* (the visibility tests' programs count samples into binding 0) */
glGetIntegerv(GL_MAJOR_VERSION, &major);
glGetIntegerv(GL_MINOR_VERSION, &minor);
if (major > 3 || (major == 3 && minor >= 1))
{
glGenBuffers(1, &targets->counters);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, targets->counters);
glBufferData(GL_ATOMIC_COUNTER_BUFFER, 64, NULL, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
}
targets->made = 1;
return 1;
}
/* one degenerate triangle with the program into the target of its kind,
depth tested and blended as most of the game's draws are; the GL state it
changes is left changed (the worker's context has no other use, and the
game's sets it again: d3d8_host_state_changed) */
static void warm_up_draw(struct warm_up_targets *targets, GLuint program, int multiview)
{
if (!targets_make(targets) || !targets->complete[multiview ? 1 : 0])
return;
glBindFramebuffer(GL_FRAMEBUFFER, targets->framebuffers[multiview ? 1 : 0]);
glViewport(0, 0, WARM_UP_SIZE, WARM_UP_SIZE);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_STENCIL_TEST);
glDisable(GL_CULL_FACE);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glDepthMask(GL_TRUE);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBindVertexArray(targets->vertex_array);
if (targets->counters)
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, targets->counters);
glUseProgram(program);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void host_gl_program_warm_up(uint32_t program, int multiview)
{
if (!worker.running || !program)
return;
warm_up_draw(&worker.targets[0], program, multiview);
}
/* ---------- the worker's jobs */
static struct job *job_find(uint32_t id)
{
struct job *job;
for (job = worker.buckets[id % JOB_BUCKETS]; job; job = job->next_in_bucket)
{
if (job->id == id)
return job;
}
return NULL;
}
static void queue_append(struct job *job)
{
struct job **first = job->urgent ? &worker.urgent_first : &worker.later_first;
struct job **last = job->urgent ? &worker.urgent_last : &worker.later_last;
job->next_in_queue = NULL;
if (*last)
(*last)->next_in_queue = job;
else
*first = job;
*last = job;
}
static struct job *queue_take(void)
{
struct job *job = worker.urgent_first;
if (job)
{
worker.urgent_first = job->next_in_queue;
if (!worker.urgent_first)
worker.urgent_last = NULL;
return job;
}
job = worker.later_first;
if (job)
{
worker.later_first = job->next_in_queue;
if (!worker.later_first)
worker.later_last = NULL;
}
return job;
}
static GLuint shader_compile(GLenum type, const char *source)
{
GLuint shader = glCreateShader(type);
GLint status = 0;
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (!status)
{
char log[1024];
log[0] = 0;
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
if (worker.failures_logged++ < 4)
host_logf(HOST_LOG_WARN, "gl worker: a %s shader does not compile: %s",
type == GL_VERTEX_SHADER ? "vertex" : "pixel", log);
glDeleteShader(shader);
return 0;
}
return shader;
}
/* the job's program (outside the lock: only the worker touches a job
being worked on) */
static void job_work(struct job *job)
{
uint64_t start = now_us();
GLuint program = 0;
GLint status = 0;
if (job->binary && job->binary_size)
{
program = glCreateProgram();
glProgramBinary(program, (GLenum)job->binary_format, job->binary, (GLsizei)job->binary_size);
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status)
job->from_binary = 1;
else
{
glDeleteProgram(program);
program = 0;
while (glGetError() != GL_NO_ERROR)
{
}
}
}
if (!program && job->sources[0] && job->sources[1])
{
GLuint vertex = shader_compile(GL_VERTEX_SHADER, job->sources[0]);
GLuint fragment = vertex ? shader_compile(GL_FRAGMENT_SHADER, job->sources[1]) : 0;
if (vertex && fragment)
{
program = glCreateProgram();
glProgramParameteri(program, GL_PROGRAM_BINARY_RETRIEVABLE_HINT, GL_TRUE);
glAttachShader(program, vertex);
glAttachShader(program, fragment);
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (!status)
{
char log[1024];
log[0] = 0;
glGetProgramInfoLog(program, sizeof(log), NULL, log);
if (worker.failures_logged++ < 4)
host_logf(HOST_LOG_WARN, "gl worker: a program does not link: %s", log);
glDeleteProgram(program);
program = 0;
}
}
/* (attached, they go with the program) */
if (vertex)
glDeleteShader(vertex);
if (fragment)
glDeleteShader(fragment);
if (program && job->keep_binary)
{
GLint length = 0;
GLsizei written = 0;
GLenum format = 0;
glGetProgramiv(program, GL_PROGRAM_BINARY_LENGTH, &length);
if (length > 0 && (job->kept_binary = malloc((size_t)length)) != NULL)
{
glGetProgramBinary(program, length, &written, &format, job->kept_binary);
if (written > 0 && written <= length)
{
job->kept_size = (uint32_t)written;
job->kept_format = (uint32_t)format;
}
else
{
free(job->kept_binary);
job->kept_binary = NULL;
}
}
}
}
if (program)
{
int unit;
/* the samplers' units, as d3d8_gl.c's program_get sets them (a
first draw with two kinds of sampler on one unit draws nothing) */
glUseProgram(program);
for (unit = 0; unit < 4; unit++)
{
char name[8];
snprintf(name, sizeof(name), "tex%d", unit);
glUniform1i(glGetUniformLocation(program, name), unit);
}
glUseProgram(0);
if (worker.warm_up)
warm_up_draw(&worker.targets[1], program,
job->sources[0] && strstr(job->sources[0], "GL_OVR_multiview") != NULL);
job->sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}
glFlush();
job->program = program;
job->microseconds = (uint32_t)(now_us() - start);
}
static void *worker_main(void *unused)
{
(void)unused;
if (!eglMakeCurrent(worker.display, worker.surface, worker.surface, worker.context))
{
host_logf(HOST_LOG_ERROR, "gl worker: its context cannot be made current (0x%x)", eglGetError());
pthread_mutex_lock(&worker.lock);
worker.started = -1;
pthread_cond_broadcast(&worker.ready);
pthread_mutex_unlock(&worker.lock);
return NULL;
}
pthread_mutex_lock(&worker.lock);
worker.started = 1;
pthread_cond_broadcast(&worker.ready);
for (;;)
{
struct job *job;
while ((job = queue_take()) == NULL)
pthread_cond_wait(&worker.wake, &worker.lock);
job->state = JOB_WORKING;
pthread_mutex_unlock(&worker.lock);
job_work(job);
pthread_mutex_lock(&worker.lock);
free(job->binary);
job->binary = NULL;
free(job->sources[0]);
free(job->sources[1]);
job->sources[0] = job->sources[1] = NULL;
job->state = job->program ? JOB_DONE : JOB_FAILED;
worker.waiting--;
}
return NULL;
}
/* ---------- the game's */
int host_gl_worker_start(int warm_up)
{
EGLDisplay display = eglGetCurrentDisplay();
EGLContext shared = eglGetCurrentContext();
EGLint config_id = 0, count = 0, version = 3;
EGLint config_attributes[] = { EGL_CONFIG_ID, 0, EGL_NONE };
EGLint context_attributes[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };
EGLConfig config;
const char *extensions;
int started;
if (worker.running)
return 1;
if (worker.started)
return 0;
worker.warm_up = warm_up;
if (display == EGL_NO_DISPLAY || shared == EGL_NO_CONTEXT)
{
host_logf(HOST_LOG_WARN, "gl worker: no current context to share");
worker.started = -1;
return 0;
}
eglQueryContext(display, shared, EGL_CONFIG_ID, &config_id);
eglQueryContext(display, shared, EGL_CONTEXT_CLIENT_VERSION, &version);
config_attributes[1] = config_id;
if (!eglChooseConfig(display, config_attributes, &config, 1, &count) || count != 1)
{
host_logf(HOST_LOG_WARN, "gl worker: the game's context's configuration is not found");
worker.started = -1;
return 0;
}
context_attributes[1] = version;
extensions = eglQueryString(display, EGL_EXTENSIONS);
worker.surface = EGL_NO_SURFACE;
if (!extensions || !strstr(extensions, "EGL_KHR_surfaceless_context"))
{
/* (a 1x1 pbuffer to be current on, of a configuration that has one) */
EGLint pbuffer_config[] = { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR, EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_NONE };
EGLint pbuffer_size[] = { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE };
if (!eglChooseConfig(display, pbuffer_config, &config, 1, &count) || count != 1 ||
(worker.surface = eglCreatePbufferSurface(display, config, pbuffer_size)) == EGL_NO_SURFACE)
{
host_logf(HOST_LOG_WARN, "gl worker: neither surfaceless contexts nor a pbuffer");
worker.started = -1;
return 0;
}
}
worker.context = eglCreateContext(display, config, shared, context_attributes);
if (worker.context == EGL_NO_CONTEXT)
{
host_logf(HOST_LOG_WARN, "gl worker: no context sharing the game's (0x%x)", eglGetError());
worker.started = -1;
return 0;
}
worker.display = display;
worker.next_id = 1;
worker.framebuffer_texture_multiview = (PFNGLFRAMEBUFFERTEXTUREMULTIVIEWOVRPROC)eglGetProcAddress(
"glFramebufferTextureMultiviewOVR");
pthread_mutex_init(&worker.lock, NULL);
pthread_cond_init(&worker.wake, NULL);
pthread_cond_init(&worker.ready, NULL);
if (pthread_create(&worker.thread, NULL, worker_main, NULL) != 0)
{
eglDestroyContext(display, worker.context);
worker.started = -1;
return 0;
}
pthread_mutex_lock(&worker.lock);
while (!worker.started)
pthread_cond_wait(&worker.ready, &worker.lock);
started = worker.started > 0;
pthread_mutex_unlock(&worker.lock);
worker.running = started;
/* (Mesa's trace markers, for the new context's too: host_gl.c) */
if (started)
host_gl_quiet_trace_markers();
host_logf(HOST_LOG_INFO, "gl worker: %s (ES %d, %s%s)", started ? "running" : "failed", (int)version,
worker.surface == EGL_NO_SURFACE ? "surfaceless" : "on a pbuffer", warm_up ? ", programs warmed up" : "");
return started;
}
uint32_t host_gl_program_request(int urgent, const char *vertex_source, const char *fragment_source,
uint32_t binary_format, const void *binary, uint32_t binary_size, int keep_binary)
{
struct job *job;
uint32_t id;
if (!worker.running)
return 0;
job = calloc(1, sizeof(*job));
if (!job)
return 0;
job->urgent = urgent != 0;
job->keep_binary = keep_binary != 0;
job->sources[0] = vertex_source ? strdup(vertex_source) : NULL;
job->sources[1] = fragment_source ? strdup(fragment_source) : NULL;
if (binary && binary_size && (job->binary = malloc(binary_size)) != NULL)
{
memcpy(job->binary, binary, binary_size);
job->binary_size = binary_size;
job->binary_format = binary_format;
}
pthread_mutex_lock(&worker.lock);
id = job->id = worker.next_id++;
if (!worker.next_id)
worker.next_id = 1;
job->state = JOB_QUEUED;
job->next_in_bucket = worker.buckets[id % JOB_BUCKETS];
worker.buckets[id % JOB_BUCKETS] = job;
queue_append(job);
worker.waiting++;
pthread_cond_signal(&worker.wake);
pthread_mutex_unlock(&worker.lock);
return id;
}
/* a queued job a draw now waits for: before the others */
void host_gl_program_hurry(uint32_t id)
{
struct job *job, **link;
if (!worker.running)
return;
pthread_mutex_lock(&worker.lock);
job = job_find(id);
if (job && job->state == JOB_QUEUED && !job->urgent)
{
for (link = &worker.later_first; *link; link = &(*link)->next_in_queue)
{
if (*link == job)
{
struct job *previous = NULL, *walk;
*link = job->next_in_queue;
/* (the last of the later queue found again) */
for (walk = worker.later_first; walk; walk = walk->next_in_queue)
previous = walk;
worker.later_last = previous;
job->urgent = 1;
queue_append(job);
break;
}
}
}
pthread_mutex_unlock(&worker.lock);
}
int host_gl_program_poll(uint32_t id, uint32_t *program, uint32_t *binary_size, uint32_t *microseconds)
{
struct job *job;
int result = 0;
if (!worker.running)
return -1;
pthread_mutex_lock(&worker.lock);
job = job_find(id);
if (!job || job->state == JOB_FAILED)
result = -1;
else if (job->state == JOB_DONE)
{
/* (the fence passed: everything the worker did with the program is
done, and this context may use it) */
if (job->sync)
{
GLenum waited = glClientWaitSync(job->sync, 0, 0);
if (waited == GL_ALREADY_SIGNALED || waited == GL_CONDITION_SATISFIED || waited == GL_WAIT_FAILED)
{
glDeleteSync(job->sync);
job->sync = NULL;
}
}
if (!job->sync)
{
*program = job->program;
*binary_size = job->kept_size;
*microseconds = job->microseconds;
result = 1;
}
}
pthread_mutex_unlock(&worker.lock);
return result;
}
uint32_t host_gl_program_binary(uint32_t id, void *buffer, uint32_t size, uint32_t *format)
{
struct job *job;
uint32_t copied = 0;
if (!worker.running)
return 0;
pthread_mutex_lock(&worker.lock);
job = job_find(id);
if (job && job->state == JOB_DONE && job->kept_binary && size >= job->kept_size)
{
memcpy(buffer, job->kept_binary, job->kept_size);
*format = job->kept_format;
copied = job->kept_size;
}
pthread_mutex_unlock(&worker.lock);
return copied;
}
/* forgets a job (its program stays: the game's); a job still queued or
being worked on is let go when it is done */
void host_gl_program_release(uint32_t id)
{
struct job *job, **link;
if (!worker.running)
return;
pthread_mutex_lock(&worker.lock);
for (link = &worker.buckets[id % JOB_BUCKETS]; (job = *link) != NULL; link = &job->next_in_bucket)
{
if (job->id != id)
continue;
if (job->state == JOB_QUEUED || job->state == JOB_WORKING)
break;
*link = job->next_in_bucket;
if (job->sync)
glDeleteSync(job->sync);
free(job->kept_binary);
free(job);
break;
}
pthread_mutex_unlock(&worker.lock);
}
uint32_t host_gl_worker_waiting(void)
{
uint32_t waiting;
if (!worker.running)
return 0;
pthread_mutex_lock(&worker.lock);
waiting = worker.waiting;
pthread_mutex_unlock(&worker.lock);
return waiting;
}
+120 -34
View File
@@ -65,7 +65,7 @@ void host_fatal(const char *format, ...)
vsnprintf(message, sizeof(message), format, arguments);
va_end(arguments);
__android_log_write(ANDROID_LOG_FATAL, "halo", message);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Halo", message, NULL);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Mjolnir VR", message, NULL);
_exit(1);
}
@@ -92,6 +92,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/RingfallMjolnirVR"
void host_android_path(int which, char *buffer, uint32_t size)
{
@@ -229,6 +231,28 @@ static int config_sample_seconds(const char *path, char *text, size_t size)
return found;
}
/* a number from config.toml, or fallback when it is not set */
static double config_number(const char *path, const char *key, double fallback)
{
toml_result_t result = toml_parse_file_ex(path);
double value = fallback;
if (!result.ok)
return fallback;
{
toml_datum_t datum = toml_seek(result.toptab, key);
if (datum.type == TOML_FP64)
value = datum.u.fp64;
else if (datum.type == TOML_INT64)
value = (double)datum.u.int64;
else if (datum.type == TOML_BOOLEAN)
value = datum.u.boolean ? 1.0 : 0.0;
}
toml_free(result);
return value;
}
/* a boolean setting of config.toml (a dotted name), or otherwise when the
file, the setting or a boolean is missing */
static int config_boolean_or(const char *path, const char *name, int otherwise)
@@ -331,51 +355,34 @@ 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, whenever they can be written (port/android/STEAM_FRAME.md; the
same choice as LauncherActivity's, which makes the folder) */
if (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 */
mkdir(save_root, 0775);
share_save_tree(save_root);
{
struct stat information;
/* the VR self-test (host_xr.c), which needs no game data */
snprintf(path, sizeof(path), "%s/xr_test", data_root);
if (stat(path, &information) == 0)
host_xr_test();
}
if (!directory_has_maps(data_root))
{
host_fatal("The Halo game data was not found.\n\nCopy the PAL game data (build 01.01.14.2342), "
host_fatal("The game data was not found.\n\nCopy the PAL game data (build 01.01.14.2342), "
"the folder that contains maps, into\n%s\nor import it from the launcher screen.", data_root);
}
environment_set(&environment, "HOME", save_root);
environment_set(&environment, "HALO_DATA_ROOT", data_root);
environment_set(&environment, "HALO_SAVE_ROOT", save_root);
{
/* the game renders 480 lines at the display's aspect ratio
(landscape) unless display.screen_width says otherwise (d3d8_gl.c) */
const SDL_DisplayMode *mode;
char width[16];
SDL_InitSubSystem(SDL_INIT_VIDEO);
mode = SDL_GetDesktopDisplayMode(SDL_GetPrimaryDisplay());
if (mode && mode->w > 0 && mode->h > 0)
{
int longer = mode->w > mode->h ? mode->w : mode->h;
int shorter = mode->w > mode->h ? mode->h : mode->w;
snprintf(width, sizeof(width), "%d", (480 * longer / shorter) & ~1);
environment_set(&environment, "HALO_DISPLAY_WIDTH", width);
host_logf(HOST_LOG_INFO, "display %dx%d: rendering %sx480", mode->w, mode->h, width);
}
{
/* dp for the touch controls (port/linux/src/touch_input.c): SDL
gives Android's densityDpi / 160 */
float density = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
char text[16];
if (density > 0.0f)
{
snprintf(text, sizeof(text), "%g", density);
environment_set(&environment, "HALO_DISPLAY_DENSITY", text);
host_logf(HOST_LOG_INFO, "display density %s", text);
}
}
}
time_zone(zone, sizeof(zone));
environment_set(&environment, "TZ", zone);
/* internet play's MQTT brokers (network.brokers_file): the APK's list,
@@ -413,6 +420,85 @@ static void *game_main(void *unused)
}
SDL_free(image);
/* after the image, whose loading reserves the guest's fixed address
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);
}
/* (vr.log_performance: the compositor's counters too, host_xr.c) */
if (config_boolean_or(path, "vr.log_performance", 0))
setenv("HALO_XR_LOG_PERFORMANCE", "1", 1);
{
int eye_width, eye_height;
/* on a headset the game runs in VR (port/linux/src/vr.c): it
keeps the Xbox's 640x480 layout and draws it at the eye images'
resolution (d3d8_gl.c) */
if (config_number(path, "vr.enabled", 1.0) != 0.0 &&
host_xr_probe((float)config_number(path, "vr.resolution_scale", 1.0), &eye_width, &eye_height))
{
char text[16];
environment_set(&environment, "HALO_VR", "1");
snprintf(text, sizeof(text), "%d", eye_width);
environment_set(&environment, "HALO_VR_EYE_WIDTH", text);
snprintf(text, sizeof(text), "%d", eye_height);
environment_set(&environment, "HALO_VR_EYE_HEIGHT", text);
environment_set(&environment, "HALO_DISPLAY_WIDTH", "640");
}
else
{
/* the game renders 480 lines at the display's aspect ratio
(landscape) unless display.screen_width says otherwise (d3d8_gl.c) */
const SDL_DisplayMode *mode;
char width[16];
char text_density[16];
SDL_InitSubSystem(SDL_INIT_VIDEO);
mode = SDL_GetDesktopDisplayMode(SDL_GetPrimaryDisplay());
if (mode && mode->w > 0 && mode->h > 0)
{
int longer = mode->w > mode->h ? mode->w : mode->h;
int shorter = mode->w > mode->h ? mode->h : mode->w;
snprintf(width, sizeof(width), "%d", (480 * longer / shorter) & ~1);
environment_set(&environment, "HALO_DISPLAY_WIDTH", width);
host_logf(HOST_LOG_INFO, "display %dx%d: rendering %sx480", mode->w, mode->h, width);
}
{
/* dp for the touch controls (port/linux/src/touch_input.c): SDL
gives Android's densityDpi / 160 */
float density = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
if (density > 0.0f)
{
snprintf(text_density, sizeof(text_density), "%g", density);
environment_set(&environment, "HALO_DISPLAY_DENSITY", text_density);
host_logf(HOST_LOG_INFO, "display density %s", text_density);
}
}
}
}
{
char seconds[32];
@@ -454,7 +540,7 @@ int main(int argc, char *argv[])
{
(void)argc;
(void)argv;
host_logf(HOST_LOG_INFO, "Halo for Android starting");
host_logf(HOST_LOG_INFO, "Mjolnir VR starting");
host_install_signal_handlers();
if (host_native_thread_create(game_main, NULL, MAIN_STACK_SIZE) != 0)
host_fatal("cannot start the game thread");
+5 -1
View File
@@ -178,7 +178,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)
File diff suppressed because it is too large. Load diff
+23
View File
@@ -53,8 +53,31 @@ host_gl_get_string
host_gl_has_extension
host_gl_read_buffer
host_gl_buffer_write
host_gl_buffer_storage
host_gl_persistent_write
host_gl_fence_frame
host_gl_wait_frame
host_gl_counters_snapshot
host_gl_counters_read
host_gl_worker_start
host_gl_program_request
host_gl_program_hurry
host_gl_program_poll
host_gl_program_binary
host_gl_program_release
host_gl_worker_waiting
host_gl_program_warm_up
host_xr_initialize
host_xr_begin_frame
host_xr_copy_layer
host_xr_copy_layer_region
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_xr_gpu_time
host_android_path
host_gesture_insets
host_touch_read
+188
View File
@@ -0,0 +1,188 @@
/*
HALO_XR_ABI.H
The structures the game (the ILP32 guest, port/linux/src/vr.c) and the
host's OpenXR session (port/android/host/host_xr.c) exchange. Both sides
compile this header, so every member is a 32-bit int or float: the layout
is the same under ILP32 and LP64.
Poses are in OpenXR's convention: metres, right-handed, +x right, +y up,
-z forward, relative to the LOCAL reference space (the head's position and
heading when the user last recentred). Quaternions are x, y, z, w.
*/
#ifndef __HALO_XR_ABI_H
#define __HALO_XR_ABI_H
enum
{
HALO_XR_EYE_LEFT = 0,
HALO_XR_EYE_RIGHT = 1,
HALO_XR_EYE_COUNT = 2,
/* the images the game fills each frame (host_xr_copy_layer) */
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 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_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) */
HALO_XR_COPY_ALPHA = 0,
HALO_XR_COPY_OPAQUE = 1,
HALO_XR_COPY_DISC = 2,
HALO_XR_HAND_LEFT = 0,
HALO_XR_HAND_RIGHT = 1,
HALO_XR_HAND_COUNT = 2,
};
/* halo_xr_hand.buttons */
enum
{
HALO_XR_BUTTON_A = 1 << 0, /* right hand: A; left hand: X */
HALO_XR_BUTTON_B = 1 << 1, /* right hand: B; left hand: Y */
HALO_XR_BUTTON_THUMBSTICK = 1 << 2, /* stick click */
HALO_XR_BUTTON_MENU = 1 << 3, /* left hand's menu button */
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 */
enum
{
HALO_XR_POSE_VALID = 1 << 0,
HALO_XR_POSE_TRACKED = 1 << 1,
};
struct halo_xr_pose
{
float orientation[4];
float position[3];
};
struct halo_xr_view
{
int flags;
struct halo_xr_pose pose;
/* tangents of the view's edges (left and down negative) */
float tangent_left;
float tangent_right;
float tangent_up;
float tangent_down;
};
struct halo_xr_hand
{
int flags;
int active; /* a controller of this hand is connected */
struct halo_xr_pose grip;
struct halo_xr_pose aim;
float trigger; /* 0..1 */
float squeeze; /* 0..1 */
float thumbstick[2]; /* x right, y up; -1..1 */
int buttons; /* HALO_XR_BUTTON_* */
};
struct halo_xr_info
{
int eye_width; /* the eye images' size in pixels */
int eye_height;
int overlay_width;
int overlay_height;
float refresh_rate; /* Hz; 0 if unknown */
char system_name[64];
};
struct halo_xr_frame
{
int session_running; /* frames are being submitted to the headset */
int should_render; /* the eye images will be shown this frame */
int focused; /* input goes to the app (no system menu over it) */
int recentred; /* the LOCAL space moved since the last frame */
float predicted_display_seconds;
int head_flags;
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 */
enum
{
HALO_XR_OVERLAY_NONE = 0,
HALO_XR_OVERLAY_HEAD_LOCKED = 1, /* the HUD: follows the head */
HALO_XR_OVERLAY_SCREEN = 2, /* menus and cinematics: a screen fixed in the room */
};
struct halo_xr_submit
{
int eyes; /* the eye layers were filled this frame */
int overlay_mode; /* HALO_XR_OVERLAY_* */
int overlay_blend; /* the overlay has alpha (premultiplied): it is drawn over the eyes */
float overlay_distance; /* metres */
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 (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 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;
float scope_size;
struct halo_xr_pose scope_pose;
/* the crosshair layer was filled: shown this wide (metres), here in
LOCAL space, facing the eyes */
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

+308
View File
@@ -0,0 +1,308 @@
#!/bin/bash
# Mjolnir VR's installer for the Steam Frame, run in its Desktop
# Mode from the bundle the release's "Install Mjolnir VR" desktop
# file downloads (steam_frame_installer.desktop; the bundle: this script,
# RingfallMjolnirVR.apk, ringfall-mjolnir-vr.sh = steam_frame_launch.sh,
# steam_frame_xiso.py and the library pictures in grid/). Everything is
# answered with dialogs, no keyboard:
# - the game in ~/Documents/RingfallMjolnirVR (port/android/STEAM_FRAME.md),
# its APK replaced (Lepton installs a newer one at the next start), so
# running the installer again updates the game; the folder of the builds
# before the name, ~/Documents/HaloCE, becomes it;
# - the game data: the maps folder copied out of a disc image of the
# original Xbox game (.iso or .xiso) found in Downloads, Documents, the
# Desktop or on a memory card, or chosen, unless it is there already;
# - the game in the Steam library, with its pictures, unless it is there
# already: added by Steam itself (steam://addnonsteamgame, as SteamOS's
# "Add to Steam" does), from a desktop file beside the game. Steam is not
# closed: it keeps its shortcuts in memory and writes them back, and on the
# Steam Frame Desktop Mode runs inside Gaming Mode's session, which ends
# with Steam. The shortcut of the builds before the name ("Halo VR") is
# left for the player to remove (Steam would write it back).
# MJOLNIR_UNATTENDED=1 answers every question with its default and prints
# instead of showing dialogs (MJOLNIR_IMAGE: the disc image to use), for
# testing over adb; it never closes Steam.
BUNDLE="$(cd "$(dirname "$0")" && pwd)"
GAME_DIR="$HOME/Documents/RingfallMjolnirVR"
STEAM_DIR="$HOME/.local/share/Steam"
LEPTON="$STEAM_DIR/steamapps/common/Lepton/lepton"
TITLE="Mjolnir VR"
NAME="Mjolnir VR"
APK_NAME="RingfallMjolnirVR.apk"
LAUNCHER="ringfall-mjolnir-vr.sh"
EXE="\"$GAME_DIR/$LAUNCHER\""
# (the desktop file Steam adds the game from)
NAME_FILE="Mjolnir VR"
# the builds before the name: their folder and Steam shortcut
OLD_DIR="$HOME/Documents/HaloCE"
OLD_NAME="Halo VR"
# the name of builds 302 to 313, which their shortcut keeps (Steam writes
# its shortcuts back from memory: the player renames it)
FORMER_NAME="Ringfall: Mjolnir VR"
# what the installer did, for looking into problems: written to /tmp while
# it runs, then kept beside the game as install.log
LOG="/tmp/mjolnir-vr-install.log"
: > "$LOG"
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
keep_log() { [ -d "$GAME_DIR" ] && cp -f "$LOG" "$GAME_DIR/install.log" 2>/dev/null; }
trap keep_log EXIT
log "installer from $BUNDLE"
# whether Steam runs: its pid file (~/.steam/steam.pid), or its client's
# process (ubuntu12_32/steam: the process is not always named "steam")
steam_running()
{
local pid
pid="$(cat "$HOME/.steam/steam.pid" 2>/dev/null)"
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
return 0
fi
pgrep -f 'ubuntu12_32/steam( |$)' >/dev/null || pgrep -x steam >/dev/null
}
unattended() { [ "${MJOLNIR_UNATTENDED:-0}" = 1 ] || ! command -v zenity >/dev/null; }
info()
{
if unattended; then printf "info: %b\n" "$1"; else zenity --info --title="$TITLE" --width=460 --text="$1"; fi
}
fail()
{
log "error: $1"
if unattended; then printf "error: %b\n" "$1" >&2; else zenity --error --title="$TITLE" --width=460 --text="$1"; fi
exit 1
}
# yes/no; unattended: the default given ($2, "yes" or "no")
ask()
{
if unattended; then [ "$2" = yes ]; return; fi
zenity --question --title="$TITLE" --width=460 --text="$1"
}
# the disc image the player chooses
choose_image()
{
if unattended; then return 1; fi
zenity --file-selection --title="Choose a disc image of Halo: Combat Evolved for the Xbox (.iso or .xiso)" \
--filename="$HOME/Downloads/" --file-filter="Disc images | *.iso *.ISO *.xiso *.XISO" \
--file-filter="All files | *"
}
# the disc images near at hand, largest first (the game's image is a few GB;
# anything under 500 MB is not one)
find_images()
{
find "$HOME/Downloads" "$HOME/Documents" "$HOME/Desktop" /run/media -maxdepth 3 -type f \
\( -iname '*.iso' -o -iname '*.xiso' \) -size +500M -printf '%s\t%p\n' 2>/dev/null |
sort -rn | cut -f2-
}
extract_maps()
{
local image="$1" log
log="$(mktemp)"
if unattended; then
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$GAME_DIR" 2>"$log" | grep '^#' | tail -n 1
status=${PIPESTATUS[0]}
else
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$GAME_DIR" 2>"$log" |
zenity --progress --title="$TITLE" --width=460 --auto-close --no-cancel \
--text="Copying the game data out of the disc image..."
status=${PIPESTATUS[0]}
fi
if [ "$status" != 0 ]; then
reason="$(sed -n 's/^error: //p' "$log" | head -n 1)"
rm -f "$log"
fail "The game data could not be copied out of\n$image\n\n${reason:-Unknown error.}"
fi
rm -f "$log"
}
# the Steam users' shortcut files (userdata/0 is no user)
shortcut_files()
{
local config
for config in "$STEAM_DIR"/userdata/*/config; do
case "$config" in */userdata/0/*) continue ;; esac
[ -f "$config/shortcuts.vdf" ] && echo "$config/shortcuts.vdf"
done
}
# whether a Steam user's library holds the launcher given
have_shortcut_to()
{
local file
while read -r file; do
grep -qaF "$1" "$file" && return 0
done < <(shortcut_files)
return 1
}
have_shortcut() { have_shortcut_to "$GAME_DIR/$LAUNCHER"; }
# the id Steam gave the shortcut whose Exe is $2, in shortcuts.vdf $1 (Steam
# picks it; its library pictures in config/grid are named after it)
shortcut_id()
{
python3 - "$1" "$2" <<'PYTHON'
import struct, sys
path, exe = sys.argv[1], sys.argv[2].encode()
# Steam's binary VDF: 0x00 a map, 0x01 a string, 0x02 an int32, 0x08 a map's end
def read_map(data, at):
items = []
while True:
kind = data[at]; at += 1
if kind == 0x08:
return items, at
end = data.index(b"\0", at); key = data[at:end].lower(); at = end + 1
if kind == 0x00:
value, at = read_map(data, at)
elif kind == 0x01:
end = data.index(b"\0", at); value = data[at:end]; at = end + 1
elif kind == 0x02:
value = struct.unpack_from("<I", data, at)[0]; at += 4
else:
raise ValueError("unknown VDF type %d" % kind)
items.append((key, value))
with open(path, "rb") as file:
top, _ = read_map(file.read(), 0)
for key, shortcuts in top:
if key == b"shortcuts":
for _, entry in shortcuts:
fields = dict(entry)
if fields.get(b"exe") == exe and b"appid" in fields:
print(fields[b"appid"])
sys.exit(0)
sys.exit(1)
PYTHON
}
# the game added to the library of the Steam user logged in, by Steam (it
# must run), from a desktop file beside the game; then its pictures
add_shortcut()
{
local desktop="$GAME_DIR/$NAME_FILE.desktop" url
printf '[Desktop Entry]\nType=Application\nName=%s\nExec=%s\nPath=%s\nIcon=steam\nTerminal=false\n' \
"$NAME" "$GAME_DIR/$LAUNCHER" "$GAME_DIR" > "$desktop" && chmod 755 "$desktop" || return 1
url="steam://addnonsteamgame/$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$desktop")"
log "asking Steam: $url"
# (Steam's own "Add to Steam" touches this first: steamos-add-to-steam)
touch /tmp/addnonsteamgamefile
steam "$url" >/dev/null 2>&1 &
for _ in $(seq 30); do have_shortcut && break; sleep 1; done
have_shortcut || return 1
add_pictures
return 0
}
# the library pictures of the game's shortcut, for every Steam user who has
# it (again on an update: the builds before the name "Mjolnir VR" had
# Ringfall's)
add_pictures()
{
local file id
while read -r file; do
id="$(shortcut_id "$file" "$EXE")" || continue
log "shortcut $id in $file"
mkdir -p "$(dirname "$file")/grid"
cp "$BUNDLE/grid/library.png" "$(dirname "$file")/grid/${id}.png"
cp "$BUNDLE/grid/libraryp.png" "$(dirname "$file")/grid/${id}p.png"
cp "$BUNDLE/grid/library_hero.png" "$(dirname "$file")/grid/${id}_hero.png"
cp "$BUNDLE/grid/library_logo.png" "$(dirname "$file")/grid/${id}_logo.png"
done < <(shortcut_files)
}
# --- the game
[ -x "$LEPTON" ] || fail "Lepton, which runs Android games on the Steam Frame, was not found.\n\nStart any Android game from the Steam store once, then run this installer again."
for file in "$APK_NAME" "$LAUNCHER" steam_frame_xiso.py grid/library.png; do
[ -f "$BUNDLE/$file" ] || fail "The installer is incomplete ($file is missing). Download it again."
done
# the builds before the name: their folder becomes the game's (settings,
# saves and game data kept), without their APK and launcher
if [ -d "$OLD_DIR" ] && [ ! -e "$GAME_DIR" ]; then
log "renaming $OLD_DIR to $GAME_DIR"
mv "$OLD_DIR" "$GAME_DIR" || fail "Could not rename $OLD_DIR to $GAME_DIR."
rm -f "$GAME_DIR/HaloVR.apk" "$GAME_DIR/halo-vr.sh"
fi
if [ -f "$GAME_DIR/$APK_NAME" ] || [ -f "$GAME_DIR/maps/ui.map" ]; then
ask "$NAME is installed already. Update it to this version?\n\n(Your settings, saves and game data stay.)" yes || exit 0
else
ask "This installs $NAME on your Steam Frame.\n\nYou need your own disc image of Halo: Combat Evolved for the original Xbox (an .iso or .xiso file, any version), for example in Downloads.\n\nContinue?" yes || exit 0
fi
mkdir -p "$GAME_DIR/maps" || fail "Could not create $GAME_DIR."
# (the game runs in Lepton's container as a user of its own)
chmod 777 "$GAME_DIR" "$GAME_DIR/maps"
cp "$BUNDLE/$LAUNCHER" "$GAME_DIR/$LAUNCHER.tmp" && chmod 755 "$GAME_DIR/$LAUNCHER.tmp" &&
mv -f "$GAME_DIR/$LAUNCHER.tmp" "$GAME_DIR/$LAUNCHER" || fail "Could not write $GAME_DIR/$LAUNCHER."
# (newer than the installed one: Lepton installs it at the next start)
cp "$BUNDLE/$APK_NAME" "$GAME_DIR/$APK_NAME.tmp" && chmod 666 "$GAME_DIR/$APK_NAME.tmp" &&
mv -f "$GAME_DIR/$APK_NAME.tmp" "$GAME_DIR/$APK_NAME" && touch "$GAME_DIR/$APK_NAME" ||
fail "Could not write $GAME_DIR/$APK_NAME (is the storage full?)."
# (the build's number: the launcher updates from it, steam_frame_launch.sh;
# an update fetched for an older build is dropped)
cp "$BUNDLE/build.txt" "$GAME_DIR/build.txt" 2>/dev/null || rm -f "$GAME_DIR/build.txt"
rm -rf "$GAME_DIR/update"
# --- the game data
if [ ! -f "$GAME_DIR/maps/ui.map" ]; then
image="${MJOLNIR_IMAGE:-}"
if [ -z "$image" ]; then
mapfile -t images < <(find_images)
for candidate in "${images[@]}"; do
if ask "Use this disc image?\n\n$candidate" yes; then
image="$candidate"
break
fi
done
fi
if [ -z "$image" ]; then
if [ ${#images[@]} = 0 ]; then
info "No disc image was found in Downloads, Documents, on the Desktop or a memory card.\n\nChoose it in the next window."
fi
image="$(choose_image)" || fail "No disc image was chosen. Run the installer again once it is on the Frame (for example in Downloads)."
fi
log "extracting $image"
extract_maps "$image"
chmod -R a+rwX "$GAME_DIR/maps"
image_note="\n\nThe disc image itself is no longer needed:\n$image"
fi
# --- Steam
if have_shortcut; then
log "the Steam shortcut is there"
add_pictures
elif unattended; then
echo "info: the Steam shortcut is missing (not added unattended)"
elif ! steam_running; then
info "Steam is not running, so $NAME could not be added to your library.\n\nStart Steam and run the installer again."
else
add_shortcut || fail "Steam did not add $NAME to your library. Run the installer again; if it happens again, add\n$GAME_DIR/$NAME_FILE.desktop\nwith Steam's \"Add a Non-Steam Game\"."
fi
if unattended; then
echo "info: installed in $GAME_DIR"
exit 0
fi
if have_shortcut_to "$OLD_DIR/halo-vr.sh"; then
info "The library still shows the old \"$OLD_NAME\" from before the game's new name. It no longer starts: remove it in Steam (its gear or Manage > Remove non-Steam game)."
fi
if have_shortcut_to "$FORMER_NAME"; then
info "The library shows the game by its former name, \"$FORMER_NAME\". To call it $NAME there: its gear > Properties, then its name at the top."
fi
if ask "$NAME is installed.${image_note:-}\n\nSwitch to Gaming Mode now? Start $NAME from your library there (its pictures may show once Steam starts again)." yes; then
steamos-session-select gamescope >/dev/null 2>&1 ||
info "Switch to Gaming Mode yourself (the \"Return to Gaming Mode\" icon or the power menu)."
fi
@@ -0,0 +1,14 @@
[Desktop Entry]
# Mjolnir VR's installer for the Steam Frame
# (port/android/STEAM_FRAME.md): a release asset,
# "Install-Mjolnir-VR.desktop", opened in Desktop Mode. It downloads
# the latest release's bundle (tools/steam_frame_bundle.py, which fills in
# the repository) and runs its installer, steam_frame_install.sh.
# (no "$", "\" or "%" in Exec: the desktop entry format would need them
# escaped)
Type=Application
Name=Install Mjolnir VR
Comment=Installs Mjolnir VR for the Steam Frame, or updates it
Icon=steam
Terminal=false
Exec=bash -c "if curl -fL --retry 3 -o /tmp/mjolnir-vr-steam-frame.zip https://github.com/@REPOSITORY@/releases/latest/download/mjolnir-vr-steam-frame.zip && rm -rf /tmp/mjolnir-vr-install && python3 -m zipfile -e /tmp/mjolnir-vr-steam-frame.zip /tmp/mjolnir-vr-install; then exec bash /tmp/mjolnir-vr-install/install.sh; else zenity --error --width=460 --title='Mjolnir VR' --text='Mjolnir VR could not be downloaded. Is the Steam Frame online?'; fi"
+158
View File
@@ -0,0 +1,158 @@
#!/bin/bash
# Mjolnir 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/RingfallMjolnirVR.
# (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"
DATA="$HOME/Documents/RingfallMjolnirVR"
APK="$DATA/RingfallMjolnirVR.apk"
LOG="$DATA/launcher.log"
# (written by the game when the player quits it: no start again)
QUIT="$DATA/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="$DATA/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
}
# Updates (update.auto in config.toml; the game's own updater, the Quest's,
# does nothing here: Lepton installs the APK in this folder each start).
# While the game runs, the latest release is looked for; a newer one's
# bundle is fetched into update/, checked, and put in place as the next
# start begins, before Lepton runs the APK (it installs the newer one).
# A build that knows no number of its own (build.txt: a local one) never
# looks. Nothing changes without the internet, or when anything fails.
REPOSITORY="@REPOSITORY@"
UPDATE="$DATA/update"
BUILD_FILE="$DATA/build.txt"
SELF="$DATA/ringfall-mjolnir-vr.sh"
update_apply()
{
local new="$UPDATE/bundle" build
[ -f "$UPDATE/ready" ] || { rm -rf "$UPDATE"; return 0; }
build=$(cat "$new/build.txt" 2>/dev/null)
# (the launcher renamed over this one: bash goes on reading its own copy)
if cp "$new/ringfall-mjolnir-vr.sh" "$SELF.tmp" && chmod 755 "$SELF.tmp" &&
cp "$new/RingfallMjolnirVR.apk" "$APK.tmp" && chmod 666 "$APK.tmp" &&
mv -f "$SELF.tmp" "$SELF" && mv -f "$APK.tmp" "$APK" && touch "$APK" &&
cp "$new/build.txt" "$BUILD_FILE"
then
log "update: build $build put in place"
else
log "update: build $build could not be put in place"
rm -f "$SELF.tmp" "$APK.tmp"
fi
rm -rf "$UPDATE"
}
update_wanted()
{
# (update.auto = false in config.toml's [update]: never)
! awk '/^\[/ { section = $0 } section == "[update]" && /^[ \t]*auto[ \t]*=/ { print $3 }' \
"$DATA/config.toml" 2>/dev/null | grep -q false
}
update_check()
{
local installed latest
installed=$(cat "$BUILD_FILE" 2>/dev/null)
[[ "$installed" =~ ^[0-9]+$ ]] && (( installed > 0 )) || return 0
update_wanted || return 0
latest=$(curl -fsSL --max-time 20 -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$REPOSITORY/releases/latest" |
python3 -I -c 'import json, sys; print(json.load(sys.stdin)["tag_name"].rpartition("-")[2])' 2>/dev/null)
[[ "$latest" =~ ^[0-9]+$ ]] && (( latest > installed )) || return 0
log "update: build $latest found (this is $installed), fetching it"
rm -rf "$UPDATE" && mkdir -p "$UPDATE" || return 0
if curl -fsSL --retry 2 --max-time 900 -o "$UPDATE/bundle.zip" \
"https://github.com/$REPOSITORY/releases/download/build-$latest/mjolnir-vr-steam-frame.zip" &&
python3 -I -m zipfile -e "$UPDATE/bundle.zip" "$UPDATE/bundle" &&
[ "$(cat "$UPDATE/bundle/build.txt" 2>/dev/null)" = "$latest" ] &&
[ -s "$UPDATE/bundle/RingfallMjolnirVR.apk" ] && [ -s "$UPDATE/bundle/ringfall-mjolnir-vr.sh" ]
then
rm -f "$UPDATE/bundle.zip"
touch "$UPDATE/ready"
log "update: build $latest fetched, put in place at the next start"
else
log "update: build $latest could not be fetched"
rm -rf "$UPDATE"
fi
}
update_apply
update_check </dev/null >/dev/null 2>&1 &
updater=$!
# (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"
# (a fetch still under way is given up: the next start looks again)
pkill -P "$updater" 2>/dev/null
kill "$updater" 2>/dev/null
log "end"
+264
View File
@@ -0,0 +1,264 @@
#!/usr/bin/env python3
"""
Copies the maps folder out of the player's Xbox disc image of Halo (an
"xiso", or a whole disc's ".iso") on the Steam Frame's SteamOS side, for
its installer (steam_frame_install.sh), as the app's own import does
(app/src/main/java/com/halo/decomp/XisoExtractor.java, which this follows;
the desktop games' is port/linux/src/xiso.c).
steam_frame_xiso.py <image> <destination>
writes <destination>/maps (through maps.partial, ui.map moved in last, so
an interrupted copy never leaves maps/ui.map behind) and prints its
progress for zenity --progress: a percentage on a line of its own, and
"# <text>" lines. On failure it prints "error: <reason the player can act
on>" to stderr and exits 1.
Written from a description of the Xbox's disc file system, XDVDFS
(xboxdevwiki's XDVDFS page, https://xboxdevwiki.net/XDVDFS), as
XisoExtractor.java is:
- Sectors are 2048 bytes and integers little-endian. Sector numbers count
from the start of the game partition: an image of the partition alone has
it at 0, an image of a whole disc further in (PARTITION_STARTS, tried in
that order).
- The volume descriptor is the partition's sector 32 (at 0x10000). It holds
"MICROSOFT*XBOX*MEDIA" at its start and again at 0x7EC, and the root
directory table's start sector (u32 at 0x14) and size in bytes (u32 at
0x18).
- A directory table is a binary tree of entries whose root is the entry at
offset 0. An entry is a left and a right subtree link (u16 each, in 4-byte
units from the table's start, 0 for none), the start sector and the size
of the file or of the subdirectory's table (u32 each), the attributes (u8,
0x10 for a directory), the name's length (u8) and the name. Unused bytes
are 0xFF, so a left link of 0xFFFF is padding, not an entry.
- A file is one contiguous run of bytes from the start of its sector.
The image is the player's file and nothing read from it is trusted: only
the root's table and the maps folder's are read, each whole and at most
4 MB; a table's walk visits each offset once, goes at most 64 links deep
and looks at most 4096 entries; and a file is written only under a plain
name that no file before it had (ignoring case).
A European disc in two languages (Spanish and Italian) has no maps folder
but one for each, maps_es and maps_it: the one for the language asked for
(MJOLNIR_LANGUAGE, else the system's) is taken, else English's, else the
first, and its language written to maps/language.txt, which the game takes
as the Xbox's language while game.language is unset.
"""
import os
import struct
import sys
SECTOR_SIZE = 2048
DESCRIPTOR_OFFSET = 0x10000
MAGIC = b"MICROSOFT*XBOX*MEDIA"
SECOND_MAGIC_OFFSET = 0x7EC
ENTRY_FIXED_SIZE = 14
ATTRIBUTE_DIRECTORY = 0x10
PADDING_LINK = 0xFFFF
# where the game partition may start: an image of the partition alone, a
# whole XGD2, XGD3 and XGD1 (the original Xbox's) disc
PARTITION_STARTS = (0x00000000, 0x0FD90000, 0x02080000, 0x18300000)
# directory tables are a few sectors; anything much larger is not one
MAXIMUM_TABLE_SIZE = 4 << 20
MAXIMUM_DEPTH = 64
MAXIMUM_VISITS = 4096
MAXIMUM_ENTRIES = 256
COPY_CHUNK = 1 << 20
PARTIAL_DIRECTORY = "maps.partial"
class ExtractError(Exception):
"""a reason the player can act on"""
def read_exactly(image, position, size):
"""size bytes of the image from position; None if it ends first"""
image.seek(position)
data = image.read(size)
return data if len(data) == size else None
def find_partition(image, image_size):
"""the game partition's start, and the root table's sector and size"""
for start in PARTITION_STARTS:
if start + DESCRIPTOR_OFFSET + SECTOR_SIZE > image_size:
continue
descriptor = read_exactly(image, start + DESCRIPTOR_OFFSET, SECTOR_SIZE)
if (descriptor is None or descriptor[:len(MAGIC)] != MAGIC or
descriptor[SECOND_MAGIC_OFFSET:SECOND_MAGIC_OFFSET + len(MAGIC)] != MAGIC):
continue
sector, size = struct.unpack_from("<II", descriptor, 0x14)
return start, sector, size
raise ExtractError("This is not an Xbox disc image.")
def read_table(image, partition, sector, size, damaged):
if size == 0 or size > MAXIMUM_TABLE_SIZE:
raise ExtractError(damaged)
table = read_exactly(image, partition + sector * SECTOR_SIZE, size)
if table is None:
raise ExtractError("Could not read the disc image (is it complete?).")
return table
def is_plain_name(name):
"""no path, no . or .., and only printable ASCII that every file system
takes as a plain name"""
if not name or name in (".", ".."):
return False
return all(0x20 <= ord(character) <= 0x7E and character not in '/\\:*?"<>|' for character in name)
def walk_table(table, directories):
"""the folders (or the files) of a table with plain names, as (name,
sector, size), in the tree's order: each entry's left subtree, the entry,
its right subtree; a link to padding, past the table or back to an entry
already visited ends that branch"""
entries, visited = [], set()
visits = [0]
def walk(offset, depth):
if depth > MAXIMUM_DEPTH or visits[0] >= MAXIMUM_VISITS:
return
if offset >= len(table) or len(table) - offset < ENTRY_FIXED_SIZE:
return
left, right = struct.unpack_from("<HH", table, offset)
if left == PADDING_LINK or offset // 4 in visited:
return
visited.add(offset // 4)
visits[0] += 1
if left:
walk(left * 4, depth + 1)
name_length = table[offset + 13]
if name_length and name_length <= len(table) - offset - ENTRY_FIXED_SIZE:
is_directory = bool(table[offset + 12] & ATTRIBUTE_DIRECTORY)
name = table[offset + ENTRY_FIXED_SIZE:offset + ENTRY_FIXED_SIZE + name_length].decode("latin-1")
# (the destination's file system may ignore case)
if (is_directory == directories and len(entries) < MAXIMUM_ENTRIES and is_plain_name(name) and
all(entry[0].lower() != name.lower() for entry in entries)):
sector, size = struct.unpack_from("<II", table, offset + 4)
entries.append((name, sector, size))
if right:
walk(right * 4, depth + 1)
walk(0, 0)
return entries
def maps_language(name):
"""a language's maps folder's language ("maps_it": "it"), else None"""
if len(name) == 7 and name[:5].lower() == "maps_" and name[5:].isalpha():
return name[5:].lower()
return None
def choose_maps(folders):
"""the maps folder, and its language (None for a plain "maps")"""
for folder in folders:
if folder[0].lower() == "maps":
return folder, None
languages = [(maps_language(folder[0]), folder) for folder in folders if maps_language(folder[0])]
wanted = (os.environ.get("MJOLNIR_LANGUAGE") or os.environ.get("LC_ALL") or
os.environ.get("LC_MESSAGES") or os.environ.get("LANG") or "")[:2].lower()
for preferred in (wanted, "en"):
for language, folder in languages:
if language == preferred:
return folder, language
if languages:
return languages[0][1], languages[0][0]
raise ExtractError("The disc image has no maps folder: it is not a Halo disc.")
def extract(image_path, destination):
with open(image_path, "rb") as image:
image_size = os.fstat(image.fileno()).st_size
partition, root_sector, root_size = find_partition(image, image_size)
# the root's maps folder
root = read_table(image, partition, root_sector, root_size, "The disc image's file system is damaged.")
(_, maps_sector, maps_size), language = choose_maps(walk_table(root, True))
# its files
table = read_table(image, partition, maps_sector, maps_size, "The disc image's maps folder is damaged.")
files = walk_table(table, False)
user_interface = next((file for file in files if file[0].lower() == "ui.map"), None)
if user_interface is None:
raise ExtractError("The disc image's maps folder has no ui.map: it is not a Halo disc.")
# (an image that ends before a file does is refused before anything
# is written; an empty file's sector need not be anywhere)
for name, sector, size in files:
if size and partition + sector * SECTOR_SIZE + size > image_size:
raise ExtractError("Could not read %s from the disc image (is it complete?)." % name)
total = sum(size for _, _, size in files) or 1
# the copy, to maps.partial (emptied of what an earlier attempt left)
partial = os.path.join(destination, PARTIAL_DIRECTORY)
try:
os.makedirs(partial, exist_ok=True)
for leftover in os.listdir(partial):
os.remove(os.path.join(partial, leftover))
except OSError:
raise ExtractError("Could not create %s." % partial)
done, shown = 0, -1
for name, sector, size in files:
path = os.path.join(partial, name)
position, remaining = partition + sector * SECTOR_SIZE, size
print("# Copying maps/%s" % name, flush=True)
try:
with open(path, "wb") as out:
while remaining > 0:
count = min(remaining, COPY_CHUNK)
data = read_exactly(image, position, count)
if data is None:
raise ExtractError("Could not read %s from the disc image (is it complete?)." % name)
out.write(data)
position += count
remaining -= count
done += count
# (zenity closes at 100: that is left to the caller)
percent = min(99, done * 100 // total)
if percent != shown:
print(percent, flush=True)
shown = percent
except OSError:
raise ExtractError("Could not write %s (is the storage full?)." % path)
# then into maps (which may be there, empty), the language before and
# ui.map (the sign that the data is there) last
finished = os.path.join(destination, "maps")
try:
os.makedirs(finished, exist_ok=True)
language_file = os.path.join(finished, "language.txt")
if os.path.exists(language_file):
os.remove(language_file)
if language:
with open(language_file, "w") as out:
out.write(language + "\n")
order = [file for file in files if file is not user_interface] + [user_interface]
for name, _, _ in order:
os.replace(os.path.join(partial, name), os.path.join(finished, name))
os.rmdir(partial)
except OSError as error:
raise ExtractError("Could not move the maps into %s (%s)." % (finished, error.strerror))
def main():
if len(sys.argv) != 3:
print("usage: steam_frame_xiso.py <image> <destination>", file=sys.stderr)
return 2
try:
extract(sys.argv[1], sys.argv[2])
except ExtractError as error:
print("error: %s" % error, file=sys.stderr)
return 1
except OSError as error:
print("error: Could not open the disc image (%s)." % error.strerror, file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -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;
}
+8
View File
@@ -25,6 +25,7 @@ keep their place and size at any field of view.
#include "rasterizer/xbox/rasterizer_xbox.h"
#include "object_mesh.h"
#include "halo_vr.h"
#include <string.h>
@@ -74,6 +75,13 @@ boolean first_person_legs_wanted(
{
struct object_datum *object = object_try_and_get(object_index);
#ifdef HALO_ANDROID
/* port: not in VR, where the eye is the headset's and the whole body is
vr_body.c's (vr.body "full"); set back behind a flat view's eye, these
legs would stand behind the player there */
if (halo_vr_active())
return FALSE;
#endif
return first_person_legs_enabled() && object && object->object.type == _object_type_biped &&
object->object.parent_object_index == NONE &&
!TEST_FLAG(object->object.damage_flags, _object_dead_bit) &&
File diff suppressed because it is too large. Load diff
+66 -1
View File
@@ -2860,6 +2860,14 @@ int p2p_peer_address(unsigned char const *identifier, unsigned long *address);
int platform_clipboard_get(char *text, int size);
void platform_clipboard_set(char const *text);
void platform_text_field(int typing, int password);
#ifdef HALO_ANDROID
/* (VR: the Xbox's on-screen keyboard, virtual_keyboard.c) */
int halo_vr_active(void);
boolean virtual_keyboard_launch(wchar_t *text_buffer, word buffer_size, short caption_index);
boolean virtual_keyboard_active(void);
boolean virtual_keyboard_last_exit_saved_text(void);
void virtual_keyboard_set_caption(wchar_t const *caption);
#endif
int config_boolean(char const *name);
void ui_widget_port_post_button(short controller_index, short button_index);
@@ -2922,6 +2930,30 @@ static struct
of: its screen has gone) */
static unsigned long text_field_shown_time;
#ifdef HALO_ANDROID
/* VR: no keyboard to type on; the Xbox's on-screen one types the field,
which is done when it closes (its ENTER) or as it was (its BACK) */
static struct
{
boolean open;
wchar_t text[TEXT_FIELD_LENGTH];
} text_field_keyboard;
static void text_field_keyboard_launch(void)
{
short index;
for (index = 0; index < TEXT_FIELD_LENGTH - 1 && text_field.text[index]; index++)
text_field_keyboard.text[index] = (wchar_t)(unsigned char)text_field.text[index];
text_field_keyboard.text[index] = 0;
virtual_keyboard_set_caption(L"Type, then ENTER");
text_field_keyboard.open = virtual_keyboard_launch(text_field_keyboard.text,
(word)(MIN(text_field.maximum + 1, TEXT_FIELD_LENGTH) * sizeof(wchar_t)), 8);
if (!text_field_keyboard.open)
virtual_keyboard_set_caption(NULL);
}
#endif
static boolean text_field_editing(struct widget_instance *row)
{
return text_field.row && (!row || text_field.row == row);
@@ -2942,6 +2974,10 @@ static void text_field_open(struct widget_instance *row, char const *text, short
while (input_get_key(&key))
;
platform_text_field(TRUE, masked);
#ifdef HALO_ANDROID
if (halo_vr_active())
text_field_keyboard_launch();
#endif
}
static void text_field_begin(struct widget_instance *row, char const *text, short maximum,
@@ -2990,6 +3026,30 @@ static void text_field_show(struct widget_instance *value, char const *text, boo
wchar_t shown[TEXT_FIELD_LENGTH + 2];
short index;
#ifdef HALO_ANDROID
/* (VR: the on-screen keyboard closed, the field done with what it typed) */
if (editing && text_field_keyboard.open && !virtual_keyboard_active())
{
boolean typed = virtual_keyboard_last_exit_saved_text();
text_field_keyboard.open = FALSE;
virtual_keyboard_set_caption(NULL);
if (typed)
{
short length;
for (length = 0; length < text_field.maximum && text_field_keyboard.text[length]; length++)
{
wchar_t character = text_field_keyboard.text[length];
text_field.text[length] = character >= ' ' && character <= '~' ? (char)character : '?';
}
text_field.text[length] = 0;
}
text_field_end(typed);
editing = FALSE;
}
#endif
if (editing)
{
struct key_stroke key;
@@ -7411,7 +7471,12 @@ void pc_menu_game_data_function_invoke(
if (!name)
return;
/* (a text field whose screen has gone: let go of) */
/* (a text field whose screen has gone: let go of; not while VR's
on-screen keyboard types it) */
#ifdef HALO_ANDROID
if (text_field_keyboard.open)
text_field_shown_time = system_milliseconds();
#endif
if (text_field.row && system_milliseconds() - text_field_shown_time > 500)
text_field_end(FALSE);
if (!strcmp(name, "solo map list update"))
+4
View File
@@ -42,6 +42,7 @@ menu gets FRIENDLY FIRE too.
#include "rasterizer/xbox/rasterizer_xbox_hardware_bitmaps.h"
#include "halo_menus.h"
#include "halo_vr.h"
#include "custom_edition_cache.h"
#include <stdlib.h>
@@ -2198,6 +2199,9 @@ void menu_tags_loaded(
{
return;
}
/* (in VR, the Xbox's unless vr.menus asks for the PC version's) */
if (halo_vr_active() && strcmp(config_string("vr.menus"), "pc"))
return;
menus = halo_menus_load();
if (!menus)
return;
+20 -1
View File
@@ -3825,6 +3825,8 @@ void network_distributed_new_game(
update_queues_distributed_reset();
network_objects_new_game();
network_damage_new_game();
network_vr_new_game();
bots_new_game();
network_actors_new_game();
network_coop_new_game();
network_aim_assist_reset();
@@ -3883,6 +3885,8 @@ void network_distributed_tick(
profile_scope(distributed_tick_send_structure_bsp_section, distributed_send_structure_bsp();)
}
profile_scope(distributed_tick_send_players_section, distributed_host_send_players();)
network_vr_tick();
bots_tick();
profile_scope(distributed_tick_actors_host_tick_section, network_actors_host_tick();)
profile_scope(distributed_tick_coop_host_tick_section, network_coop_host_tick();)
profile_scope(distributed_tick_send_pickups_section, distributed_send_pickups();)
@@ -3896,6 +3900,7 @@ void network_distributed_tick(
if (game_time_get() % PING_REFRESH_TICKS == 0 && latency_meter_enabled())
distributed_refresh_pings();
profile_scope(distributed_tick_send_inputs_section, distributed_client_send_inputs();)
network_vr_tick();
profile_scope(distributed_tick_send_predictions_section, distributed_client_send_predictions();)
profile_scope(distributed_tick_objects_client_tick_section, network_objects_client_tick();)
profile_scope(distributed_tick_damage_client_tick_section, network_damage_client_tick();)
@@ -3985,6 +3990,7 @@ static boolean distributed_message_stale(
case _distributed_message_player_inputs:
case _distributed_message_relayed_actions:
case _distributed_message_damage_events:
case _distributed_message_vr_poses:
case _distributed_message_pings:
case _distributed_message_actor_states:
case _distributed_message_structure_bsp:
@@ -4748,6 +4754,7 @@ void network_distributed_handle_message(
case _distributed_message_notice:
case _distributed_message_client_ready: entry_size = 0; break;
case _distributed_message_client_identity: entry_size = sizeof(struct distributed_client_identity); break;
case _distributed_message_vr_poses: entry_size = network_vr_entry_size(); break;
case _distributed_message_client_build: entry_size = sizeof(struct distributed_client_build); break;
case _distributed_message_damage_events:
case _distributed_message_hit_reports:
@@ -4789,7 +4796,19 @@ void network_distributed_handle_message(
{
return;
}
/* (each kind from the host, or from a client) */
/* (each kind from the host, or from a client; a VR player's hands
both ways) */
if (header.type == _distributed_message_vr_poses)
{
if (machine_index == NONE ? game_connection() != _game_connection_network_client :
game_connection() != _game_connection_network_server)
{
return;
}
if (!distributed_message_stale(machine_index, &header))
network_vr_handle_poses(machine_index, entries, header.count);
return;
}
switch (header.type)
{
case _distributed_message_player_prediction:
+17
View File
@@ -150,6 +150,13 @@ enum
while they do (network_damage.c): its line of sight check's */
_distributed_message_fire_origins = 93,
/* The VR port's own messages, numbered far past the game's (240 on),
so that messages upstream adds never take their numbers: a build
without them drops them, as it drops any type past its own. */
/* a VR player's hands, every tick: a client's own to the host, the
host's of the others to each client (unreliable; network_vr.c) */
_distributed_message_vr_poses = 240,
NUMBER_OF_DISTRIBUTED_MESSAGES
};
@@ -487,4 +494,14 @@ the camera; NULL (the default) for a build whose shots always do */
typedef boolean (*network_damage_fire_origin_source)(short local_player_index, real_vector3d *offset);
void network_damage_set_fire_origin_source(network_damage_fire_origin_source source);
/* VR players' hands (network_vr.c) */
void network_vr_new_game(void);
void network_vr_tick(void);
word network_vr_entry_size(void);
void network_vr_handle_poses(long machine_index, void const *entries, short count);
/* bots (bots.c) */
void bots_new_game(void);
void bots_tick(void);
#endif // __NETWORK_DISTRIBUTED_H
+535
View File
@@ -0,0 +1,535 @@
/*
NETWORK_VR.C
VR players' hands for the other machines: an extension of the distributed
netcode (port/linux/NETCODE.md, network_distributed.c). A VR player's
weapon aims where the weapon hand's controller points and fires from the
hand, and the free hand holds a grenade and throws it its own way
(port/linux/src/vr.c; the right hand and the left, or left-handed the other
way round); the input the netcode relays has only where the head looks, so
the other machines would show that player aiming and throwing where they
look.
Every tick, a machine with a VR player sends the host that player's hands
(_distributed_message_vr_poses, unreliable): where the weapon aims, where
its muzzle is from the unit's camera (whichever hand holds it: the others
see shots leave the left hand of a left-handed player), whether the free
hand holds a grenade, and the last throw (from where, which way, how hard)
for a second after it. The host keeps them, its own VR players' too, and
sends each client those of the players of other machines. A machine drives
a VR player of another (players.c, weapons.c, units.c) as long as their
hands keep coming, else as before.
What the hands decide is only how the game looks: damage is the shooter's
report, which the host checks as any other (network_damage.c); a VR
player's weapon hand also goes to the host as their fire origin, which its
line of sight check looks from too.
A machine that does not know the message (a build without this, of the same
network version) drops it, as it drops any type past its own
(network_distributed_handle_message): VR and other builds still play
together, the others seeing a VR player aim where the head looks.
*/
#include "cseries.h"
#include "game/game.h"
#include "game/players.h"
#include "units/units.h"
#include "network_distributed.h"
#include "halo_vr.h"
#include <math.h>
void platform_log(char const *format, ...);
/* ---------- constants */
enum
{
_vr_pose_aim_bit,
_vr_pose_weapon_offset_bit,
_vr_pose_grenade_held_bit,
_vr_pose_throw_bit,
};
/* how long a player's hands drive them after the last came (ticks): longer
and they are driven as before (the hands stopped coming: the player left
VR, or the network). Where the weapon aims and fires from is held a
second, over the gaps of a network that delivers in bursts (as the input
the netcode relays is); a grenade held, only a third of one, so that it
does not hang in the hand after the hand let go. The host relays only
hands as fresh as that. */
#define VR_POSE_FRESH_TICKS 10
#define VR_AIM_HOLD_TICKS 30
/* how long a throw is sent after it (ticks), and taken after it came */
#define VR_THROW_TICKS 30
/* offsets from the unit's camera: in 1/4096 of a world unit, no further
than a hand reaches */
#define VR_OFFSET_SCALE 4096.0f
#define VR_OFFSET_MAXIMUM 1.0f
/* how often what was sent and heard is logged (ticks) */
#define VR_LOG_TICKS 150
/* ---------- structures */
struct distributed_vr_pose
{
byte player_index;
byte flags;
byte throw_sequence;
byte throw_strength;
struct distributed_vector aim;
struct distributed_vector weapon_offset;
struct distributed_vector throw_offset;
struct distributed_vector throw_direction;
};
typedef char distributed_vr_pose_size_assert[sizeof(struct distributed_vr_pose) == 28 ? 1 : -1];
/* a player's hands, as last heard of (this machine's own VR players on the
host too, to send) */
struct network_vr_player
{
boolean valid;
long received_time;
struct distributed_vr_pose pose;
/* the throw this machine has taken for the player's grenade */
boolean throw_taken;
byte throw_taken_sequence;
long throw_received_time;
byte throw_received_sequence;
};
/* a local VR player's last throw */
struct network_vr_throw
{
boolean valid;
long time;
byte sequence;
real_vector3d offset;
real_vector3d direction;
real strength;
};
/* ---------- globals */
static struct network_vr_player network_vr_players[MAXIMUM_TRACKED_PLAYERS];
static struct network_vr_throw network_vr_throws[MAXIMUM_LOCAL_PLAYERS];
static byte network_vr_throw_sequences[MAXIMUM_LOCAL_PLAYERS];
/* what was sent and heard since last logged */
static struct
{
long sent;
long heard;
long aimed;
} network_vr_counts;
/* ---------- private code */
static boolean network_vr_in_network_game(
void)
{
short connection = game_connection();
return connection == _game_connection_network_server || connection == _game_connection_network_client;
}
static void network_vr_offset_pack(
real_vector3d const *offset,
struct distributed_vector *result)
{
distributed_vector_pack(offset, VR_OFFSET_SCALE, result);
}
/* (FALSE: not a hand's offset) */
static boolean network_vr_offset_unpack(
struct distributed_vector const *packed,
real_vector3d *offset)
{
distributed_vector_unpack(packed, VR_OFFSET_SCALE, offset);
return fabsf(offset->i) <= VR_OFFSET_MAXIMUM && fabsf(offset->j) <= VR_OFFSET_MAXIMUM &&
fabsf(offset->k) <= VR_OFFSET_MAXIMUM;
}
/* (FALSE: no direction) */
static boolean network_vr_direction_unpack(
struct distributed_vector const *packed,
real_vector3d *direction)
{
distributed_unit_vector_unpack(packed, direction);
return direction->i * direction->i + direction->j * direction->j + direction->k * direction->k > 0.5f;
}
/* a player of another machine whose hands came lately, or NULL */
static struct network_vr_player *network_vr_remote_player(
long player_index,
long fresh_ticks)
{
struct network_vr_player *vr_player;
long absolute_index;
if (player_index == NONE || !network_vr_in_network_game() || distributed_player_is_local(player_index))
return NULL;
absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index);
if (absolute_index < 0 || absolute_index >= MAXIMUM_TRACKED_PLAYERS)
return NULL;
vr_player = &network_vr_players[absolute_index];
if (!vr_player->valid || game_time_get() - vr_player->received_time > fresh_ticks ||
game_time_get() < vr_player->received_time)
{
return NULL;
}
return vr_player;
}
/* what is said of a player's hands, kept (checked: a client's of its own) */
static void network_vr_take_pose(
struct distributed_vr_pose const *pose,
boolean heard)
{
struct network_vr_player *vr_player;
real_vector3d vector;
if (pose->player_index >= MAXIMUM_TRACKED_PLAYERS)
return;
vr_player = &network_vr_players[pose->player_index];
vr_player->pose = *pose;
if (TEST_FLAG(pose->flags, _vr_pose_aim_bit) && !network_vr_direction_unpack(&pose->aim, &vector))
SET_FLAG(vr_player->pose.flags, _vr_pose_aim_bit, FALSE);
if (TEST_FLAG(pose->flags, _vr_pose_weapon_offset_bit) && !network_vr_offset_unpack(&pose->weapon_offset, &vector))
SET_FLAG(vr_player->pose.flags, _vr_pose_weapon_offset_bit, FALSE);
if (TEST_FLAG(pose->flags, _vr_pose_throw_bit) &&
(!network_vr_offset_unpack(&pose->throw_offset, &vector) ||
!network_vr_direction_unpack(&pose->throw_direction, &vector)))
{
SET_FLAG(vr_player->pose.flags, _vr_pose_throw_bit, FALSE);
}
/* (a throw new to this machine: when it came) */
if (TEST_FLAG(vr_player->pose.flags, _vr_pose_throw_bit) &&
(!vr_player->valid || vr_player->throw_received_sequence != pose->throw_sequence ||
vr_player->throw_received_time == NONE))
{
vr_player->throw_received_sequence = pose->throw_sequence;
vr_player->throw_received_time = game_time_get();
}
vr_player->valid = TRUE;
vr_player->received_time = game_time_get();
if (heard)
network_vr_counts.heard++;
}
/* this machine's VR player's hands, to send; FALSE when it has none */
static boolean network_vr_local_pose(
short local_player_index,
struct distributed_vr_pose *pose)
{
long player_index = local_player_get_player_index(local_player_index);
struct player_datum *player = player_index != NONE ? player_try_and_get(player_index) : NULL;
struct unit_datum *unit = player && player->unit_index != NONE ? unit_try_and_get(player->unit_index) : NULL;
struct network_vr_throw *last_throw = &network_vr_throws[local_player_index];
float vector[3];
if (!unit || !halo_vr_active())
return FALSE;
csmemset(pose, 0, sizeof(*pose));
pose->player_index = distributed_player_to_byte(player_index);
/* (on foot: seated, the seat aims, and the input says where) */
if (unit->object.parent_object_index == NONE)
{
if (halo_vr_aim_vector(local_player_index, vector))
{
real_vector3d aim;
aim.i = vector[0];
aim.j = vector[1];
aim.k = vector[2];
distributed_vector_pack(&aim, DISTRIBUTED_UNIT_SCALE, &pose->aim);
SET_FLAG(pose->flags, _vr_pose_aim_bit, TRUE);
}
if (halo_vr_weapon_origin_offset(local_player_index, vector))
{
real_vector3d offset;
offset.i = vector[0];
offset.j = vector[1];
offset.k = vector[2];
if (fabsf(offset.i) <= VR_OFFSET_MAXIMUM && fabsf(offset.j) <= VR_OFFSET_MAXIMUM &&
fabsf(offset.k) <= VR_OFFSET_MAXIMUM)
{
network_vr_offset_pack(&offset, &pose->weapon_offset);
SET_FLAG(pose->flags, _vr_pose_weapon_offset_bit, TRUE);
}
}
}
if (halo_vr_grenade_held(local_player_index))
SET_FLAG(pose->flags, _vr_pose_grenade_held_bit, TRUE);
if (last_throw->valid && game_time_get() - last_throw->time <= VR_THROW_TICKS &&
game_time_get() >= last_throw->time)
{
network_vr_offset_pack(&last_throw->offset, &pose->throw_offset);
distributed_vector_pack(&last_throw->direction, DISTRIBUTED_UNIT_SCALE, &pose->throw_direction);
pose->throw_sequence = last_throw->sequence;
pose->throw_strength = (byte)(PIN(last_throw->strength, 0.0f, 1.0f) * 255.0f + 0.5f);
SET_FLAG(pose->flags, _vr_pose_throw_bit, TRUE);
}
return TRUE;
}
/* where a local VR player's shots leave from, from the unit's camera: the
weapon hand, on foot (network_damage.c's fire origins, which the host's
line of sight check also looks from) */
static boolean network_vr_fire_origin(
short local_player_index,
real_vector3d *offset)
{
long player_index = local_player_get_player_index(local_player_index);
struct player_datum *player = player_index != NONE ? player_try_and_get(player_index) : NULL;
struct unit_datum *unit = player && player->unit_index != NONE ? unit_try_and_get(player->unit_index) : NULL;
float vector[3];
if (!unit || !halo_vr_active() || unit->object.parent_object_index != NONE ||
!halo_vr_weapon_origin_offset(local_player_index, vector))
{
return FALSE;
}
offset->i = vector[0];
offset->j = vector[1];
offset->k = vector[2];
return TRUE;
}
/* ---------- public code */
void network_vr_new_game(
void)
{
network_damage_set_fire_origin_source(network_vr_fire_origin);
short index;
csmemset(network_vr_players, 0, sizeof(network_vr_players));
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
network_vr_players[index].throw_received_time = NONE;
csmemset(network_vr_throws, 0, sizeof(network_vr_throws));
csmemset(&network_vr_counts, 0, sizeof(network_vr_counts));
}
word network_vr_entry_size(
void)
{
return sizeof(struct distributed_vr_pose);
}
/* after each tick (network_distributed_tick): a client its own VR players'
hands to the host; the host its own and its clients' to each client */
void network_vr_tick(
void)
{
short connection = game_connection();
short local_player_index;
struct
{
struct distributed_message_header header;
struct distributed_vr_pose poses[MAXIMUM_TRACKED_PLAYERS];
} message;
short count = 0;
if (game_time_get() % VR_LOG_TICKS == 0 &&
(network_vr_counts.sent || network_vr_counts.heard || network_vr_counts.aimed))
{
platform_log("VR network: hands sent %ld, heard %ld, other players aimed by them %ld (in %d ticks)",
network_vr_counts.sent, network_vr_counts.heard, network_vr_counts.aimed, VR_LOG_TICKS);
csmemset(&network_vr_counts, 0, sizeof(network_vr_counts));
}
if (connection == _game_connection_network_client)
{
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
if (network_vr_local_pose(local_player_index, &message.poses[count]))
count++;
}
network_vr_counts.sent += count;
if (count)
{
distributed_send(&message, _distributed_message_vr_poses, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_vr_pose)), _distributed_to_host);
}
}
else if (connection == _game_connection_network_server)
{
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
short machine_count;
short machine_number;
short player_index;
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
{
struct distributed_vr_pose pose;
if (network_vr_local_pose(local_player_index, &pose))
network_vr_take_pose(&pose, FALSE);
}
machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
for (machine_number = 0; machine_number < machine_count; machine_number++)
{
long machine_index = machine_indices[machine_number];
count = 0;
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS && count < 255; player_index++)
{
struct network_vr_player const *vr_player = &network_vr_players[player_index];
if (vr_player->valid && game_time_get() - vr_player->received_time <= VR_POSE_FRESH_TICKS &&
game_time_get() >= vr_player->received_time &&
distributed_player(player_index) &&
!distributed_machine_has_player(machine_index, player_index))
{
message.poses[count++] = vr_player->pose;
}
}
network_vr_counts.sent += count;
if (count)
{
distributed_send_to_machine(machine_index, &message, _distributed_message_vr_poses, count,
(word)(sizeof(message.header) + count * sizeof(struct distributed_vr_pose)));
}
}
}
}
/* the hands a machine sent: the host's of a client's players, a client's
of the others */
void network_vr_handle_poses(
long machine_index,
void const *entries,
short count)
{
short index;
for (index = 0; index < count; index++)
{
struct distributed_vr_pose pose;
csmemcpy(&pose, (byte const *)entries + index * sizeof(pose), sizeof(pose));
if (pose.player_index >= MAXIMUM_TRACKED_PLAYERS || !distributed_player(pose.player_index))
continue;
if (machine_index != NONE)
{
/* (the host: only of that client's own players) */
if (!distributed_machine_has_player(machine_index, pose.player_index))
continue;
}
else if (distributed_player_is_local(distributed_player_from_byte(pose.player_index)))
{
continue;
}
network_vr_take_pose(&pose, TRUE);
}
}
/* ---------- the game's side (halo_vr.h) */
int halo_vr_network_aim(long player_index, float vector[3])
{
struct network_vr_player *vr_player = network_vr_remote_player(player_index, VR_AIM_HOLD_TICKS);
real_vector3d aim;
if (!vr_player || !TEST_FLAG(vr_player->pose.flags, _vr_pose_aim_bit) ||
!network_vr_direction_unpack(&vr_player->pose.aim, &aim))
{
return 0;
}
vector[0] = aim.i;
vector[1] = aim.j;
vector[2] = aim.k;
network_vr_counts.aimed++;
return 1;
}
int halo_vr_network_weapon_origin(long player_index, long unit_index, float origin[3])
{
struct network_vr_player *vr_player = network_vr_remote_player(player_index, VR_AIM_HOLD_TICKS);
real_vector3d offset;
real_point3d camera;
if (!vr_player || unit_index == NONE || !TEST_FLAG(vr_player->pose.flags, _vr_pose_weapon_offset_bit) ||
!network_vr_offset_unpack(&vr_player->pose.weapon_offset, &offset))
{
return 0;
}
unit_get_camera_position(unit_index, &camera);
origin[0] = camera.x + offset.i;
origin[1] = camera.y + offset.j;
origin[2] = camera.z + offset.k;
return 1;
}
int halo_vr_network_grenade_held(long player_index)
{
struct network_vr_player *vr_player = network_vr_remote_player(player_index, VR_POSE_FRESH_TICKS);
return vr_player && TEST_FLAG(vr_player->pose.flags, _vr_pose_grenade_held_bit);
}
int halo_vr_network_grenade_throw(long player_index, long unit_index, float origin[3], float direction[3],
float *strength)
{
struct network_vr_player *vr_player = network_vr_remote_player(player_index, VR_THROW_TICKS);
real_vector3d offset;
real_vector3d way;
real_point3d camera;
if (!vr_player || unit_index == NONE || !TEST_FLAG(vr_player->pose.flags, _vr_pose_throw_bit) ||
vr_player->throw_received_time == NONE || game_time_get() - vr_player->throw_received_time > VR_THROW_TICKS ||
(vr_player->throw_taken && vr_player->throw_taken_sequence == vr_player->pose.throw_sequence) ||
!network_vr_offset_unpack(&vr_player->pose.throw_offset, &offset) ||
!network_vr_direction_unpack(&vr_player->pose.throw_direction, &way))
{
return 0;
}
vr_player->throw_taken = TRUE;
vr_player->throw_taken_sequence = vr_player->pose.throw_sequence;
unit_get_camera_position(unit_index, &camera);
origin[0] = camera.x + offset.i;
origin[1] = camera.y + offset.j;
origin[2] = camera.z + offset.k;
direction[0] = way.i;
direction[1] = way.j;
direction[2] = way.k;
*strength = (float)vr_player->pose.throw_strength / 255.0f;
platform_log("VR network: player %ld's grenade thrown as their hand threw it (%.2f)",
DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index), *strength);
return 1;
}
void halo_vr_network_grenade_thrown(short local_player_index, long unit_index, const float origin[3],
const float direction[3], float strength)
{
struct network_vr_throw *last_throw;
real_point3d camera;
if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS || unit_index == NONE ||
!network_vr_in_network_game())
{
return;
}
last_throw = &network_vr_throws[local_player_index];
unit_get_camera_position(unit_index, &camera);
last_throw->offset.i = origin[0] - camera.x;
last_throw->offset.j = origin[1] - camera.y;
last_throw->offset.k = origin[2] - camera.z;
/* (a hand further off than it reaches: none sent) */
if (fabsf(last_throw->offset.i) > VR_OFFSET_MAXIMUM || fabsf(last_throw->offset.j) > VR_OFFSET_MAXIMUM ||
fabsf(last_throw->offset.k) > VR_OFFSET_MAXIMUM)
{
last_throw->valid = FALSE;
return;
}
last_throw->direction.i = direction[0];
last_throw->direction.j = direction[1];
last_throw->direction.k = direction[2];
last_throw->strength = strength;
last_throw->time = game_time_get();
last_throw->sequence = ++network_vr_throw_sequences[local_player_index];
last_throw->valid = TRUE;
platform_log("VR network: grenade throw #%d sent (%.2f)", last_throw->sequence, strength);
}
+10
View File
@@ -45,6 +45,11 @@ int config_boolean(const char *name);
const char *config_string(const char *name);
unsigned long config_changes(void);
#ifdef HALO_ANDROID
/* port/linux/game/vr_graphics.c */
boolean vr_graphics_allows_shield_glow(void);
#endif
/* object_lights.c (port) */
long light_port_glow_definition_get(void);
boolean light_port_glow_set(long light_index, long object_index, real_rgb_color const *color, real radius);
@@ -117,6 +122,11 @@ static boolean shield_glow_enabled(
enabled = config_boolean("display.shield_glow") != 0;
shield_glow_scale = intensity && !strcmp(intensity, "light_show") ? 3.f : 1.f;
}
#ifdef HALO_ANDROID
/* (VR's "auto" graphics leaves it out first: vr_graphics.c) */
if (enabled && !vr_graphics_allows_shield_glow())
return FALSE;
#endif
return enabled;
}
+19 -2
View File
@@ -25,6 +25,9 @@ that draw the first-person weapon (rasterizer and render sources).
#include "render/render_cameras_internal.h"
#include "units/unit_definitions.h"
#include "units/units.h"
#ifdef HALO_ANDROID
#include "halo_vr.h"
#endif
#include <math.h>
@@ -38,6 +41,18 @@ double config_real(const char *name);
int config_boolean(const char *name);
unsigned long config_changes(void);
/* port: in VR the headset gives each eye's view (vr.c) and the weapon is
held in the hand: display.fov, display.viewmodel_fov and
display.viewmodel_visible do nothing */
static boolean view_fov_vr(void)
{
#ifdef HALO_ANDROID
return halo_vr_active() != 0;
#else
return FALSE;
#endif
}
/* ---------- the world's field of view */
static real reticle_scales[MAXIMUM_LOCAL_PLAYERS];
@@ -65,7 +80,7 @@ static real render_fov_adjust(short local_player_index, real native_vertical_fie
requested_tangent = degrees >= 20.0 && degrees <= 150.0 ?
(real)(tan(degrees * _pi / 360.0) * (9.0 / 16.0)) : 0.0f;
}
if (!requested_tangent || local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS ||
if (!requested_tangent || view_fov_vr() || local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS ||
cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) ||
director_get_perspective(local_player_index) != _director_perspective_first_person ||
director_inhibited_facing(local_player_index))
@@ -201,6 +216,8 @@ void viewmodel_projection_begin(void)
if (projection_depth++ != 0)
return;
projection_applied = FALSE;
if (view_fov_vr())
return;
/* (display.viewmodel_fov's angle; by default the view's own, from before
display.fov widened it: arms and a gun right against the camera stretch
at a wide angle) */
@@ -260,7 +277,7 @@ boolean viewmodel_is_visible(void)
initialized = TRUE;
read_at = changes;
}
return visible;
return visible || view_fov_vr();
}
/* ---------- whether the energy shield's flare is drawn on the arms */
+875
View File
@@ -0,0 +1,875 @@
/*
VR_ARMS.C
The VR arms' bones (vr_arms.h): the first-person hands' arms bent from the
shoulders to the hands (interface/first_person_weapons.c poses them).
The solver, the forearm's twist and their helpers are after
moistman42069/HaloCE-Quest-VR (CC0), port/linux/game/vr_render.c, nearly
as they are there: the skeleton is read through its animation graph's
index (the nodes under each node found once a frame, not per node), and
the time is the headset's frame's (halo_vr_pose_time).
*/
#include "cseries.h"
#include "math/real_math.h"
#include "models/model_animation_definitions.h"
#include "halo_vr.h"
#include "vr_arms.h"
#include <math.h>
#include <string.h>
#ifdef HALO_ANDROID
#define VR_UNITS_PER_METRE (1.0f / 3.048f)
/* ---------- the skeleton */
boolean vr_skeleton_build(
struct vr_skeleton *skeleton,
long graph_index,
short node_count)
{
short node_index;
skeleton->graph_index = NONE;
if (graph_index == NONE || node_count <= 0 || node_count > MAXIMUM_NODES_PER_ANIMATION ||
node_count != animation_graph_node_count(graph_index))
{
return FALSE;
}
for (node_index = 0; node_index < node_count; node_index++)
{
short parent = animation_graph_node_parent(graph_index, node_index);
/* (parents before their children: else no parent) */
skeleton->parents[node_index] = parent >= 0 && parent < node_index ? parent : NONE;
skeleton->under[node_index] = 0;
}
/* each node under itself and all its ancestors */
for (node_index = 0; node_index < node_count; node_index++)
{
short ancestor = node_index;
while (ancestor != NONE)
{
skeleton->under[ancestor] |= 1ULL << node_index;
ancestor = skeleton->parents[ancestor];
}
}
skeleton->graph_index = graph_index;
skeleton->node_count = node_count;
return TRUE;
}
boolean vr_skeleton_under(
struct vr_skeleton const *skeleton,
short node_index,
short root)
{
return root >= 0 && root < skeleton->node_count && node_index >= 0 && node_index < skeleton->node_count &&
(skeleton->under[root] & (1ULL << node_index)) != 0;
}
/* ---------- rotations (column vectors) */
/* v += a * scale */
static void vr_vector_scale_add_local(
real_vector3d *v,
real_vector3d const *a,
real scale)
{
v->i += a->i * scale;
v->j += a->j * scale;
v->k += a->k * scale;
}
static real vr_length(
real_vector3d const *v)
{
return (real)sqrt(v->i * v->i + v->j * v->j + v->k * v->k);
}
static void vr_point_minus(
real_point3d const *a,
real_point3d const *b,
real_vector3d *out)
{
out->i = a->x - b->x;
out->j = a->y - b->y;
out->k = a->z - b->z;
}
static boolean vr_unit_vector(
real_vector3d *v)
{
real length = vr_length(v);
if (!(length > 1e-5f && length < 1e8f))
return FALSE;
v->i /= length;
v->j /= length;
v->k /= length;
return TRUE;
}
/* the rotation taking direction a to direction b, the shortest way */
void vr_rotation_between(
real_vector3d const *a,
real_vector3d const *b,
real rotation[3][3])
{
real_vector3d u = *a, v = *b, axis;
real c, s, t;
normalize3d(&u);
normalize3d(&v);
cross_product3d(&u, &v, &axis);
s = vr_length(&axis);
c = u.i * v.i + u.j * v.j + u.k * v.k;
if (s < 1e-6f)
{
memset(rotation, 0, sizeof(real) * 9);
if (c >= 0.0f)
{
rotation[0][0] = rotation[1][1] = rotation[2][2] = 1.0f;
}
else
{
/* half a turn about an axis across */
real_vector3d basis;
int row, column;
basis.i = 1.0f;
basis.j = basis.k = 0.0f;
if (fabs(u.i) > 0.8f)
{
basis.i = 0.0f;
basis.j = 1.0f;
}
cross_product3d(&u, &basis, &axis);
normalize3d(&axis);
for (row = 0; row < 3; row++)
{
for (column = 0; column < 3; column++)
rotation[row][column] = 2.0f * axis.n[row] * axis.n[column] - (row == column ? 1.0f : 0.0f);
}
}
return;
}
axis.i /= s;
axis.j /= s;
axis.k /= s;
t = 1.0f - c;
rotation[0][0] = c + axis.i * axis.i * t;
rotation[0][1] = axis.i * axis.j * t - axis.k * s;
rotation[0][2] = axis.i * axis.k * t + axis.j * s;
rotation[1][0] = axis.j * axis.i * t + axis.k * s;
rotation[1][1] = c + axis.j * axis.j * t;
rotation[1][2] = axis.j * axis.k * t - axis.i * s;
rotation[2][0] = axis.k * axis.i * t - axis.j * s;
rotation[2][1] = axis.k * axis.j * t + axis.i * s;
rotation[2][2] = c + axis.k * axis.k * t;
}
/* a rotation of angle radians about the unit axis */
static void vr_axis_rotation(
real_vector3d const *axis,
real angle,
real rotation[3][3])
{
real c = (real)cos(angle), s = (real)sin(angle), t = 1.0f - c;
real x = axis->i, y = axis->j, z = axis->k;
rotation[0][0] = c + x * x * t;
rotation[0][1] = x * y * t - z * s;
rotation[0][2] = x * z * t + y * s;
rotation[1][0] = y * x * t + z * s;
rotation[1][1] = c + y * y * t;
rotation[1][2] = y * z * t - x * s;
rotation[2][0] = z * x * t - y * s;
rotation[2][1] = z * y * t + x * s;
rotation[2][2] = c + z * z * t;
}
static void vr_rotate_vector(
real rotation[3][3],
real_vector3d const *in,
real_vector3d *out)
{
real_vector3d v = *in;
out->i = rotation[0][0] * v.i + rotation[0][1] * v.j + rotation[0][2] * v.k;
out->j = rotation[1][0] * v.i + rotation[1][1] * v.j + rotation[1][2] * v.k;
out->k = rotation[2][0] * v.i + rotation[2][1] * v.j + rotation[2][2] * v.k;
}
static void vr_multiply_rotations(
real a[3][3],
real b[3][3],
real out[3][3])
{
real r[3][3];
int row, column;
for (row = 0; row < 3; row++)
{
for (column = 0; column < 3; column++)
r[row][column] = a[row][0] * b[0][column] + a[row][1] * b[1][column] + a[row][2] * b[2][column];
}
memcpy(out, r, sizeof(r));
}
/* a node moved from `from` to `to` and turned by rotation about itself: its
matrix, or that of a node it carries (whose old matrix is node) */
static void vr_carry(
real_matrix4x3 *node,
real_point3d const *from,
real_point3d const *to,
real rotation[3][3])
{
real_vector3d offset;
vr_point_minus(&node->position, from, &offset);
vr_rotate_vector(rotation, &offset, &offset);
node->position.x = to->x + offset.i;
node->position.y = to->y + offset.j;
node->position.z = to->z + offset.k;
vr_rotate_vector(rotation, &node->forward, &node->forward);
vr_rotate_vector(rotation, &node->left, &node->left);
vr_rotate_vector(rotation, &node->up, &node->up);
}
/* ---------- the arm */
void vr_solve_arm(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES],
real_point3d shoulder,
real_point3d const *target,
real_vector3d const *pole,
boolean hand_stays,
struct vr_limb_history *history)
{
real_matrix4x3 old[NUMBER_OF_VR_ARM_BONES];
real rotation[NUMBER_OF_VR_ARM_BONES][3][3];
real_point3d moved_to[NUMBER_OF_VR_ARM_BONES];
real_vector3d upper, fore, reach, direction, bend, old_direction;
real upper_length, fore_length, distance, along, across;
real_point3d solved_target = *target;
short moved[MAXIMUM_NODES_PER_ANIMATION];
short index, bone;
for (bone = 0; bone < NUMBER_OF_VR_ARM_BONES; bone++)
{
if (bones[bone] < 0 || bones[bone] >= skeleton->node_count)
return;
old[bone] = matrices[bones[bone]];
}
vr_point_minus(&old[_vr_arm_fore].position, &old[_vr_arm_upper].position, &upper);
vr_point_minus(&old[_vr_arm_hand].position, &old[_vr_arm_fore].position, &fore);
upper_length = vr_length(&upper);
fore_length = vr_length(&fore);
if (!isfinite(upper_length) || !isfinite(fore_length) || upper_length < 1e-4f || fore_length < 1e-4f)
return;
vr_point_minus(target, &shoulder, &reach);
distance = vr_length(&reach);
if (!isfinite(distance) || distance < 1e-4f)
return;
direction = reach;
normalize3d(&direction);
/* out of reach: the shoulder reaches a little (8 cm), then the arm
stretches a little (18%); the hand stays where it is held */
if (distance > (upper_length + fore_length) * (history ? 0.996f : 0.999f))
{
real maximum = (upper_length + fore_length) * (history ? 0.996f : 0.999f);
real travel = distance - maximum;
travel = MIN(travel, 0.08f * VR_UNITS_PER_METRE);
shoulder.x += direction.i * travel;
shoulder.y += direction.j * travel;
shoulder.z += direction.k * travel;
distance -= travel;
if (history && distance > maximum)
{
real stretch = MIN(distance / maximum, 1.18f);
upper_length *= stretch;
fore_length *= stretch;
maximum *= stretch;
}
if (hand_stays && distance > maximum)
{
/* (further still, as this port had it: the shoulder follows the
hand, and the arm stays whole) */
shoulder.x += direction.i * (distance - maximum);
shoulder.y += direction.j * (distance - maximum);
shoulder.z += direction.k * (distance - maximum);
}
distance = MIN(distance, maximum);
}
if (history)
{
/* bent no further than 145 degrees: nearer, the bones shorten (to
65% at most) */
real minimum = (real)sqrt(upper_length * upper_length + fore_length * fore_length -
1.638304f * upper_length * fore_length);
if (distance < minimum)
{
real compress = MAX(0.65f, distance / minimum);
upper_length *= compress;
fore_length *= compress;
}
}
if (distance < (real)fabs(upper_length - fore_length) + 1e-3f)
distance = (real)fabs(upper_length - fore_length) + 1e-3f;
solved_target.x = shoulder.x + direction.i * distance;
solved_target.y = shoulder.y + direction.j * distance;
solved_target.z = shoulder.z + direction.k * distance;
/* the elbow: in the plane of the reach and the pole */
{
real dot = pole->i * direction.i + pole->j * direction.j + pole->k * direction.k;
bend.i = pole->i - direction.i * dot;
bend.j = pole->j - direction.j * dot;
bend.k = pole->k - direction.k * dot;
if (vr_length(&bend) < 1e-4f)
{
/* reaching along the pole: the old bend, else a steady side */
dot = dot_product3d(&upper, &direction);
bend = upper;
bend.i -= dot * direction.i;
bend.j -= dot * direction.j;
bend.k -= dot * direction.k;
if (vr_length(&bend) < 1e-4f)
{
real_vector3d basis;
basis.i = basis.j = 0.0f;
basis.k = 1.0f;
if (fabs(direction.k) > 0.9f)
{
basis.j = 1.0f;
basis.k = 0.0f;
}
cross_product3d(&direction, &basis, &bend);
}
}
if (history)
{
/* the elbow's swing about the reach, eased (18 a second, at most 8
radians a second) unless the arm was away or jumped */
double now = halo_vr_pose_time();
double dt = now - history->time;
real_vector3d previous = history->bend, travel;
real confidence = PIN(vr_length(&bend) / 0.35f, 0.0f, 1.0f);
vr_point_minus(&shoulder, &history->shoulder, &travel);
dot = dot_product3d(&previous, &direction);
previous.i -= dot * direction.i;
previous.j -= dot * direction.j;
previous.k -= dot * direction.k;
if (history->valid && history->graph_index == skeleton->graph_index && dt >= 0.0 && dt < 0.25 &&
vr_length(&travel) < 0.5f * VR_UNITS_PER_METRE && vr_unit_vector(&previous) && vr_unit_vector(&bend))
{
real_vector3d tangent;
real angle, change;
cross_product3d(&previous, &bend, &tangent);
angle = (real)atan2(dot_product3d(&tangent, &direction), dot_product3d(&previous, &bend));
change = angle * (1.0f - (real)exp(-18.0 * MIN(dt, 0.05))) * confidence;
change = PIN(change, (real)(-8.0 * dt), (real)(8.0 * dt));
cross_product3d(&direction, &previous, &tangent);
bend.i = previous.i * (real)cos(change) + tangent.i * (real)sin(change);
bend.j = previous.j * (real)cos(change) + tangent.j * (real)sin(change);
bend.k = previous.k * (real)cos(change) + tangent.k * (real)sin(change);
}
else
{
history->roll_valid = FALSE;
}
normalize3d(&bend);
history->valid = TRUE;
history->graph_index = skeleton->graph_index;
history->time = now;
history->shoulder = shoulder;
history->bend = bend;
}
else
{
normalize3d(&bend);
}
}
along = (upper_length * upper_length + distance * distance - fore_length * fore_length) / (2.0f * distance);
across = upper_length * upper_length - along * along;
across = across > 0.0f ? (real)sqrt(across) : 0.0f;
moved_to[_vr_arm_upper] = shoulder;
moved_to[_vr_arm_fore].x = shoulder.x + direction.i * along + bend.i * across;
moved_to[_vr_arm_fore].y = shoulder.y + direction.j * along + bend.j * across;
moved_to[_vr_arm_fore].z = shoulder.z + direction.k * along + bend.k * across;
moved_to[_vr_arm_hand] = solved_target;
/* each bone turned to point at the next joint, and the hand carried by
the forearm's turn */
vr_point_minus(&moved_to[_vr_arm_fore], &moved_to[_vr_arm_upper], &reach);
vr_rotation_between(&upper, &reach, rotation[_vr_arm_upper]);
vr_rotate_vector(rotation[_vr_arm_upper], &fore, &old_direction);
vr_point_minus(&moved_to[_vr_arm_hand], &moved_to[_vr_arm_fore], &reach);
vr_rotation_between(&old_direction, &reach, rotation[_vr_arm_fore]);
vr_multiply_rotations(rotation[_vr_arm_fore], rotation[_vr_arm_upper], rotation[_vr_arm_fore]);
if (hand_stays)
{
/* the hand stays as it was put, and what it holds */
memset(rotation[_vr_arm_hand], 0, sizeof(rotation[_vr_arm_hand]));
rotation[_vr_arm_hand][0][0] = rotation[_vr_arm_hand][1][1] = rotation[_vr_arm_hand][2][2] = 1.0f;
moved_to[_vr_arm_hand] = old[_vr_arm_hand].position;
}
else
{
memcpy(rotation[_vr_arm_hand], rotation[_vr_arm_fore], sizeof(rotation[_vr_arm_hand]));
}
/* every node under a moved bone goes with the nearest one above it
(parents come first) */
for (index = 0; index < skeleton->node_count; index++)
{
short parent = skeleton->parents[index];
moved[index] = NONE;
for (bone = 0; bone < NUMBER_OF_VR_ARM_BONES; bone++)
{
if (bones[bone] == index)
moved[index] = bone;
}
if (moved[index] == NONE && parent != NONE)
moved[index] = moved[parent];
if (moved[index] != NONE)
{
bone = moved[index];
vr_carry(&matrices[index], &old[bone].position, &moved_to[bone], rotation[bone]);
}
}
}
/* The controller's roll shared along the arm rather than all of it at the
wrist's skin: the hand's turn about the forearm from where the animation
has it against its forearm (old_fore, old_hand), 85% of it on the forearm
(none on the upper arm: vr_align_elbow rolls it), within 110 degrees, running on across half a
turn (history). The bones' places and the hand stay. */
void vr_distribute_arm_twist(
real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES],
real_matrix4x3 const *old_fore,
real_matrix4x3 const *old_hand,
struct vr_limb_history *history)
{
real_matrix4x3 *fore = &matrices[bones[_vr_arm_fore]];
real_matrix4x3 *hand = &matrices[bones[_vr_arm_hand]];
real_matrix3x3 delta;
real_vector3d expected[3], desired[3], axis;
real_quaternion q;
real angle, rotation[3][3];
int a, b, row, column;
vr_point_minus(&hand->position, &fore->position, &axis);
if (!vr_unit_vector(&axis))
return;
for (a = 0; a < 3; a++)
{
expected[a].i = expected[a].j = expected[a].k = 0.0f;
desired[a].i = hand->n[a][0];
desired[a].j = hand->n[a][1];
desired[a].k = hand->n[a][2];
for (b = 0; b < 3; b++)
{
real amount = old_fore->n[b][0] * old_hand->n[a][0] + old_fore->n[b][1] * old_hand->n[a][1] +
old_fore->n[b][2] * old_hand->n[a][2];
for (row = 0; row < 3; row++)
expected[a].n[row] += fore->n[b][row] * amount;
}
if (!vr_unit_vector(&expected[a]) || !vr_unit_vector(&desired[a]))
return;
}
for (row = 0; row < 3; row++)
{
for (column = 0; column < 3; column++)
{
delta.n[row][column] = 0.0f;
for (a = 0; a < 3; a++)
delta.n[row][column] += desired[a].n[row] * expected[a].n[column];
}
}
matrix3x3_rotation_to_quaternion(&delta, &q);
angle = 2.0f * (real)atan2(dot_product3d(&q.v, &axis), q.w);
if (!isfinite(angle))
return;
angle = (real)atan2(sin(angle), cos(angle));
/* (continuous across half a turn before the limit) */
if (history && history->roll_valid)
angle = history->roll + (real)atan2(sin(angle - history->roll), cos(angle - history->roll));
angle = PIN(angle, -1.92f, 1.92f);
if (history)
{
history->roll = angle;
history->roll_valid = TRUE;
}
vr_axis_rotation(&axis, angle * 0.85f, rotation);
vr_rotate_vector(rotation, &fore->forward, &fore->forward);
vr_rotate_vector(rotation, &fore->left, &fore->left);
vr_rotate_vector(rotation, &fore->up, &fore->up);
}
/* The upper arm rolled about its length so that the elbow bends in the
plane the arm now bends in, as the animation has it bend: the shortest
turn that took the upper arm to its new direction leaves its hinge where
it was, and with the elbow out to the side the elbow's skin turned in
(Quest, 2026-10-06). The hinge is the animation's bend (upper arm by
forearm, old_upper, old_fore, old_hand) in the upper arm's own axes. A
straight arm, then or now, has no bend: left as it is. */
void vr_align_elbow(
real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES],
real_matrix4x3 const *old_upper,
real_matrix4x3 const *old_fore,
real_matrix4x3 const *old_hand)
{
real_matrix4x3 *upper = &matrices[bones[_vr_arm_upper]];
real_matrix4x3 const *fore = &matrices[bones[_vr_arm_fore]];
real_matrix4x3 const *hand = &matrices[bones[_vr_arm_hand]];
real_vector3d old_along, old_below, old_bend, along, below, bend, hinge;
real c_forward, c_left, c_up, angle, rotation[3][3];
vr_point_minus(&old_fore->position, &old_upper->position, &old_along);
vr_point_minus(&old_hand->position, &old_fore->position, &old_below);
cross_product3d(&old_along, &old_below, &old_bend);
vr_point_minus(&fore->position, &upper->position, &along);
vr_point_minus(&hand->position, &fore->position, &below);
cross_product3d(&along, &below, &bend);
if (vr_length(&old_bend) < 1e-4f * vr_length(&old_along) * vr_length(&old_below) ||
vr_length(&bend) < 0.05f * vr_length(&along) * vr_length(&below) ||
!vr_unit_vector(&old_bend) || !vr_unit_vector(&bend) || !vr_unit_vector(&along))
{
return;
}
/* (the hinge in the upper arm's axes, then where those axes are now) */
c_forward = dot_product3d(&old_bend, &old_upper->forward);
c_left = dot_product3d(&old_bend, &old_upper->left);
c_up = dot_product3d(&old_bend, &old_upper->up);
hinge.i = upper->forward.i * c_forward + upper->left.i * c_left + upper->up.i * c_up;
hinge.j = upper->forward.j * c_forward + upper->left.j * c_left + upper->up.j * c_up;
hinge.k = upper->forward.k * c_forward + upper->left.k * c_left + upper->up.k * c_up;
/* (both across the arm: the turn about it between them) */
vr_vector_scale_add_local(&hinge, &along, -dot_product3d(&hinge, &along));
vr_vector_scale_add_local(&bend, &along, -dot_product3d(&bend, &along));
if (!vr_unit_vector(&hinge) || !vr_unit_vector(&bend))
return;
{
real_vector3d cross;
cross_product3d(&hinge, &bend, &cross);
angle = (real)atan2(dot_product3d(&cross, &along), dot_product3d(&hinge, &bend));
}
if (!isfinite(angle))
return;
vr_axis_rotation(&along, angle, rotation);
vr_rotate_vector(rotation, &upper->forward, &upper->forward);
vr_rotate_vector(rotation, &upper->left, &upper->left);
vr_rotate_vector(rotation, &upper->up, &upper->up);
}
/* ---------- the fingers
Each finger's joints are found by name: "finger" and its number (0 the
thumb to 4 the little finger), or "thumb", "index", "middle", "ring",
"pinky", on the hand's side ("l ", "r "). They are posed by absolute
directions in the controller's palm frame, each segment's subtree turned,
its length kept (after HaloCE-Quest-VR, CC0, nearly verbatim; its touch of
the world, a few rays a finger, is left out: it costs the Steam Frame's
processor too much). */
/* the finger (0 the thumb .. 4 the little finger) a node's name says it is
a joint of, on that side; NONE when not a finger's */
static short vr_finger_of(
char const *name,
char const *side)
{
static char const *const names[] = { "thumb", "index", "middle", "ring", "pinky" };
char const *finger;
short index;
if (!name || !strstr(name, side))
return NONE;
finger = strstr(name, "finger");
if (finger && finger[6] >= '0' && finger[6] <= '4')
return (short)(finger[6] - '0');
for (index = 0; index < 5; index++)
{
if (strstr(name, names[index]))
return index;
}
return NONE;
}
void vr_finger_nodes(
struct vr_skeleton const *skeleton,
short hand,
char const *side,
short joints[5][3])
{
short node_index, finger, depth;
for (finger = 0; finger < 5; finger++)
{
for (depth = 0; depth < 3; depth++)
joints[finger][depth] = NONE;
}
for (node_index = 0; node_index < skeleton->node_count; node_index++)
{
short at;
finger = vr_finger_of(animation_graph_node_name(skeleton->graph_index, node_index), side);
if (finger == NONE || !vr_skeleton_under(skeleton, node_index, hand))
continue;
/* its depth: how many of the same finger's joints above it */
depth = 0;
for (at = skeleton->parents[node_index]; at != NONE; at = skeleton->parents[at])
{
if (vr_finger_of(animation_graph_node_name(skeleton->graph_index, at), side) == finger)
depth++;
}
if (depth < 3)
joints[finger][depth] = node_index;
}
}
/* a node and all under it turned about the node */
void vr_turn_subtree(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short root,
real rotation[3][3])
{
real_point3d pivot = matrices[root].position;
unsigned long long under = skeleton->under[root];
short node_index;
for (node_index = 0; node_index < skeleton->node_count; node_index++)
{
if (under & (1ULL << node_index))
vr_carry(&matrices[node_index], &pivot, &pivot, rotation);
}
}
/* a finger's joints (low, middle, tip) curled 0 (straight, a little bent
at rest) to 1 (a fist), in the palm's frame: forward along the fingers,
up the knuckles towards the index, palm out of the palm */
static boolean vr_pose_finger(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short hand,
short const joints[3],
int finger,
real curl,
real_vector3d const *palm,
real_vector3d const *forward,
real_vector3d const *up)
{
static real const rest[3] = { 0.12f, 0.15f, 0.10f };
static real const bend[3] = { 1.25f, 1.6f, 1.1f };
real_vector3d base, axis, direction, wanted;
real angle = 0.0f, dot, rotation[3][3];
int depth;
if (joints[0] == NONE || joints[1] == NONE || joints[2] == NONE)
return FALSE;
vr_point_minus(&matrices[joints[0]].position, &matrices[hand].position, &base);
dot = dot_product3d(&base, palm);
base.i -= dot * palm->i;
base.j -= dot * palm->j;
base.k -= dot * palm->k;
if (finger == 0)
{
/* the thumb: up the handle, or across the palm */
base.i = (up->i * 0.9f + forward->i * 0.3f) * (1.0f - curl) +
(forward->i * 0.75f + palm->i * 0.55f + up->i * 0.1f) * curl;
base.j = (up->j * 0.9f + forward->j * 0.3f) * (1.0f - curl) +
(forward->j * 0.75f + palm->j * 0.55f + up->j * 0.1f) * curl;
base.k = (up->k * 0.9f + forward->k * 0.3f) * (1.0f - curl) +
(forward->k * 0.75f + palm->k * 0.55f + up->k * 0.1f) * curl;
}
if (!vr_unit_vector(&base))
return FALSE;
cross_product3d(&base, palm, &axis);
if (!vr_unit_vector(&axis))
return FALSE;
for (depth = 0; depth < 3; depth++)
{
short joint = joints[depth];
if (depth < 2)
{
vr_point_minus(&matrices[joints[depth + 1]].position, &matrices[joint].position, &direction);
}
else
{
/* the tip: its own axis that the middle joint's bone runs along */
real_vector3d previous;
real_vector3d *axes[3];
real_vector3d *last[3];
real best = 0.8f, sign = 1.0f;
int a, found = -1;
axes[0] = &matrices[joints[1]].forward;
axes[1] = &matrices[joints[1]].left;
axes[2] = &matrices[joints[1]].up;
last[0] = &matrices[joint].forward;
last[1] = &matrices[joint].left;
last[2] = &matrices[joint].up;
vr_point_minus(&matrices[joint].position, &matrices[joints[1]].position, &previous);
if (!vr_unit_vector(&previous))
return FALSE;
for (a = 0; a < 3; a++)
{
real_vector3d unit = *axes[a];
if (!vr_unit_vector(&unit))
continue;
dot = dot_product3d(&unit, &previous);
if (fabs(dot) > best)
{
best = (real)fabs(dot);
found = a;
sign = dot < 0.0f ? -1.0f : 1.0f;
}
}
if (found < 0)
return TRUE;
direction.i = last[found]->i * sign;
direction.j = last[found]->j * sign;
direction.k = last[found]->k * sign;
}
if (!vr_unit_vector(&direction))
return FALSE;
angle += finger == 0 ? (depth == 0 ? 0.0f : (depth == 1 ? 0.35f : 0.3f) * curl) :
rest[depth] + bend[depth] * curl;
vr_axis_rotation(&axis, angle, rotation);
vr_rotate_vector(rotation, &base, &wanted);
vr_rotation_between(&direction, &wanted, rotation);
vr_turn_subtree(skeleton, matrices, joint, rotation);
}
return TRUE;
}
void vr_pose_fingers(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short hand,
short const joints[5][3],
float const curls[4],
real_vector3d const *palm,
real_vector3d const *forward,
real_vector3d const *up)
{
int finger;
for (finger = 0; finger < 5; finger++)
vr_pose_finger(skeleton, matrices, hand, joints[finger], finger, curls[finger < 3 ? finger : 3], palm, forward, up);
}
/* the finger's middle and tip turned this much (0..1) towards the line of
its low joint: a finger lifted off the trigger (the weapon hand's index,
whose low joint, and the rest of the hand, keep the weapon's grip) */
void vr_straighten_finger(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short const joints[3],
real amount)
{
real_vector3d low, middle, axis;
real rotation[3][3];
real angle;
if (amount <= 0.0f || joints[0] == NONE || joints[1] == NONE || joints[2] == NONE)
return;
vr_point_minus(&matrices[joints[1]].position, &matrices[joints[0]].position, &low);
vr_point_minus(&matrices[joints[2]].position, &matrices[joints[1]].position, &middle);
if (!vr_unit_vector(&low) || !vr_unit_vector(&middle))
return;
cross_product3d(&middle, &low, &axis);
if (!vr_unit_vector(&axis))
return;
angle = (real)acos(PIN(dot_product3d(&middle, &low), -1.0f, 1.0f)) * PIN(amount, 0.0f, 1.0f);
vr_axis_rotation(&axis, angle, rotation);
vr_turn_subtree(skeleton, matrices, joints[1], rotation);
}
/* ---------- hands alone (vr.body "hands")
The arm's bones (not the hand's, its fingers' or the weapon's) shrunk to
nothing at a point 3.5 cm back from the wrist along the hand's line,
knuckles to wrist: the cuff closes just behind the hand. Shrunk onto their
own places, the forearm drew the wrist's skin to the elbow and the hand
looked cut off (after HaloCE-Quest-VR, CC0, its vr_hide_forearms and
vr_hand_back_axis). */
void vr_hide_arm(
struct vr_skeleton const *skeleton,
real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES],
short const fingers[5][3],
short gun)
{
real_point3d centre = matrices[bones[_vr_arm_hand]].position;
real_point3d knuckles;
real_vector3d back;
boolean along = FALSE;
short finger, node_index, found = 0;
knuckles.x = knuckles.y = knuckles.z = 0.0f;
/* (the four fingers' bases: the thumb's sits off the line) */
for (finger = 1; finger < 5; finger++)
{
if (fingers[finger][0] == NONE)
continue;
knuckles.x += matrices[fingers[finger][0]].position.x;
knuckles.y += matrices[fingers[finger][0]].position.y;
knuckles.z += matrices[fingers[finger][0]].position.z;
found++;
}
if (found)
{
knuckles.x /= found;
knuckles.y /= found;
knuckles.z /= found;
vr_point_minus(&centre, &knuckles, &back);
along = vr_unit_vector(&back);
}
if (!along)
{
vr_point_minus(&matrices[bones[_vr_arm_fore]].position, &centre, &back);
along = vr_unit_vector(&back);
}
if (along)
{
centre.x += back.i * 0.035f * VR_UNITS_PER_METRE;
centre.y += back.j * 0.035f * VR_UNITS_PER_METRE;
centre.z += back.k * 0.035f * VR_UNITS_PER_METRE;
}
for (node_index = 0; node_index < skeleton->node_count; node_index++)
{
if (!vr_skeleton_under(skeleton, node_index, bones[_vr_arm_upper]) ||
vr_skeleton_under(skeleton, node_index, bones[_vr_arm_hand]) ||
(gun != NONE && vr_skeleton_under(skeleton, node_index, gun)))
{
continue;
}
matrices[node_index].scale = 0.0f;
matrices[node_index].position = centre;
}
}
#endif
+89
View File
@@ -0,0 +1,89 @@
/*
VR_ARMS.H
The VR arms' bones (vr_arms.c), for the first-person hands
(interface/first_person_weapons.c): the arms bent from the shoulders to the
hands. Include after the game's math and animation headers.
*/
#ifndef __VR_ARMS_H
#define __VR_ARMS_H
#ifdef HALO_ANDROID
enum
{
_vr_arm_upper,
_vr_arm_fore,
_vr_arm_hand,
NUMBER_OF_VR_ARM_BONES
};
/* a skeleton's parents, and the nodes under each node (it among them), from
its animation graph: parents come before their children */
struct vr_skeleton
{
long graph_index;
short node_count;
short parents[MAXIMUM_NODES_PER_ANIMATION];
unsigned long long under[MAXIMUM_NODES_PER_ANIMATION];
};
/* an arm's last frame, for its elbow's swing and its forearm's twist to run
on smoothly (one each arm) */
struct vr_limb_history
{
long graph_index;
double time;
real_point3d shoulder;
real_vector3d bend;
boolean valid;
boolean roll_valid;
real roll;
};
boolean vr_skeleton_build(struct vr_skeleton *skeleton, long graph_index, short node_count);
boolean vr_skeleton_under(struct vr_skeleton const *skeleton, short node_index, short root);
/* the rotation (column vectors) taking direction a to direction b, the
shortest way; a node and all under it turned by one about the node (the
whole body, vr_body.c) */
void vr_rotation_between(real_vector3d const *a, real_vector3d const *b, real rotation[3][3]);
void vr_turn_subtree(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices, short root,
real rotation[3][3]);
/* one arm (bones: upper arm, forearm, hand) bent from the shoulder for the
hand to reach target, its elbow towards the pole; the hand (and what it
holds) stays where it is, or turns with the forearm */
void vr_solve_arm(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES], real_point3d shoulder, real_point3d const *target,
real_vector3d const *pole, boolean hand_stays, struct vr_limb_history *history);
/* the hand's roll from the animation (old_fore, old_hand: the arm as
animated) shared by the forearm and the upper arm */
void vr_align_elbow(real_matrix4x3 *matrices, short const bones[NUMBER_OF_VR_ARM_BONES],
real_matrix4x3 const *old_upper, real_matrix4x3 const *old_fore, real_matrix4x3 const *old_hand);
void vr_distribute_arm_twist(real_matrix4x3 *matrices, short const bones[NUMBER_OF_VR_ARM_BONES],
real_matrix4x3 const *old_fore, real_matrix4x3 const *old_hand, struct vr_limb_history *history);
/* a hand's fingers' joints (low, middle, tip; NONE where not found), by
their names on that side ("l ", "r "): the thumb, index, middle, ring and
little finger */
void vr_finger_nodes(struct vr_skeleton const *skeleton, short hand, char const *side, short joints[5][3]);
/* the fingers curled (halo_vr_finger_curls: the thumb, index, middle, the
last two) in the palm's frame: forward along the fingers, up the knuckles
towards the index, palm out of the palm */
void vr_pose_fingers(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices, short hand,
short const joints[5][3], float const curls[4], real_vector3d const *palm, real_vector3d const *forward,
real_vector3d const *up);
/* a finger's middle and tip turned this much (0..1) into line with its low
joint */
void vr_straighten_finger(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices, short const joints[3],
real amount);
/* the arm's bones shrunk to nothing just behind the wrist, the hand (with
its fingers) and the weapon left: the hand alone */
void vr_hide_arm(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices,
short const bones[NUMBER_OF_VR_ARM_BONES], short const fingers[5][3], short gun);
#endif
#endif
File diff suppressed because it is too large. Load diff
+36
View File
@@ -0,0 +1,36 @@
/*
VR_BODY.H
The whole body in VR (vr.body "full", vr_body.c): the player's own biped
drawn under the first-person arms and weapon. Include after the game's
math, object and render headers.
*/
#ifndef __VR_BODY_H
#define __VR_BODY_H
#ifdef HALO_ANDROID
struct render_camera;
/* render_objects.c: whether the object is the local player's own unit, drawn
whole in first person this frame (its weapon and what it carries left
out), and its node matrices as posed */
boolean vr_body_drawn(short local_player_index, long object_index);
real_matrix4x3 *vr_body_node_matrices(long object_index);
/* first_person_weapons.c: the drawn body's shoulders this frame (world
units; the model's left, then right), from which the first-person arms hang;
FALSE when no body is drawn */
boolean vr_body_shoulders(short local_player_index, real_point3d shoulders[2]);
/* debug.vr_body, flat (the emulator): the camera of a player's view moved to
look at the body (render.c), and whether the first-person weapon is left out
of it (first_person_weapons.c) */
boolean vr_body_debug_camera(short local_player_index, struct render_camera *render_camera,
struct render_camera *rasterizer_camera);
boolean vr_body_debug_hides_first_person(void);
#endif
#endif
+612
View File
@@ -0,0 +1,612 @@
/*
VR_GRAPHICS.C
The VR graphics presets (vr.graphics, the VR options' "Graphics" row): which
of the game's costlier effects are drawn, for headsets with little time to
spare.
- "auto": "high", less what the GPU's time asks to leave out (below);
- "high": everything;
- "medium", the default (2026-10-12): no object shadows, environment
reflections or lens flares;
- "low": nor specular light, bump maps, grass and other detail objects,
the screen's fog layer, contrails, weather or camouflage's second pass;
- "max": "high", the models lit for each pixel and the shadows drawn at
512x512 (d3d8_gl.c asks halo_vr_graphics).
The Quest 3 held 90 Hz with everything in the autumn's first tests; by
2026-10-12 (the whole body, upstream's shield colors and glow, 15 bots) a
bot game on Blood Gulch had seconds at a time of 9-13 ms of GPU a frame,
more than the 11.1 ms a frame less the compositor's share: stale frames,
ASW, the picture doubled as the head turns. "medium" held. So "auto" reads
the app's GPU time (XR_META_performance_metrics, host_xr.c) each frame: past
four fifths of the frame for a quarter of a second, it leaves out the next
effect of its steps (vr_graphics_auto_steps, the costliest for the least
seen first); an effect comes back once the GPU time, plus what leaving it
out saved when it went, stays under about seven tenths of the frame for
three seconds (longer each time it had to go again soon after). Where the
headset does not tell the GPU's time (SteamVR: the Steam Frame, whose time
goes to its driver on the CPU) "auto" is "high".
debug.vr_graphics_measure leaves each effect out in turn (a second and a
half on, as long off) and logs what the GPU time says it costs.
Each effect is a switch the game has already: the rasterizer's console
variables (rasterizer_debug_options) and render.c's effect switches, after
HaloCE-Quest-VR's vr_graphics.c (CC0); and the shield's glow
(shield_glow.c, display.shield_glow: a light per flaring unit, each lighting
the level once more). Scripts and the console set some of them
(render_effects turns particles off in some cutscenes): a switch is drawn
only while both the game and the preset want it. A change the game made
since the last frame is taken as its wish, kept and combined with the
preset's. Playing in another's game, a client draws grass, lens flares and
the screen's fog as the host's rules have it (cheats.c), whatever the
preset; the reductions it does keep there, cheats.c leaves alone
(vr_graphics_reduces).
What each costs: shadows (each object's map, blur and projection onto the
level: draws and fill; drawn once with both eyes at once, multiview, twice
one eye at a time), specular light and reflections (passes of the level's
geometry: draws and fill), detail objects (many small draws), lens flares
(their visibility tests), contrails and weather (particles), bump maps, the
fog layer and camouflage's pass (the GPU's fill).
*/
#ifdef HALO_ANDROID
#include "cseries.h"
#include "rasterizer/rasterizer_console_vars.h"
#include "networking/network_game_globals.h"
#include "halo_vr.h"
#include <stdio.h>
#include <string.h>
/* source/render/render.c, render_objects.c; source/effects/
weather_particle_systems.c */
extern boolean render_shadows;
extern boolean render_contrails_enabled;
extern boolean render_weather_particle_systems_enabled;
extern boolean weather;
void platform_log(char const *format, ...);
/* port/linux/src/port_config.c */
int config_boolean(char const *name);
/* an effect: whether each preset draws it (low, medium; high and max draw
everything), whether a client in another's game draws it anyway, and the
game's switches it governs */
struct vr_graphics_effect
{
char const *name;
boolean preset[2];
boolean host_rules;
boolean *switches[2];
};
static struct vr_graphics_effect vr_graphics_effects[] =
{
{ "shadows", { FALSE, FALSE }, FALSE,
{ &render_shadows, &rasterizer_debug_options.draw_environment_shadows } },
{ "specular", { FALSE, TRUE }, FALSE,
{ &rasterizer_debug_options.draw_environment_specular_lights,
&rasterizer_debug_options.draw_environment_specular_lightmaps } },
{ "reflections", { FALSE, FALSE }, FALSE,
{ &rasterizer_debug_options.draw_environment_reflections, NULL } },
{ "bump maps", { FALSE, TRUE }, FALSE,
{ &rasterizer_debug_options.bump_mapping_enabled, NULL } },
{ "detail objects", { FALSE, TRUE }, TRUE,
{ &rasterizer_debug_options.draw_detail_objects, NULL } },
{ "lens flares", { FALSE, FALSE }, TRUE,
{ &rasterizer_debug_options.draw_lens_flares, NULL } },
{ "screen fog", { FALSE, TRUE }, TRUE,
{ &rasterizer_debug_options.draw_environment_fog_screen, NULL } },
{ "contrails", { FALSE, TRUE }, FALSE,
{ &render_contrails_enabled, NULL } },
{ "weather", { FALSE, TRUE }, FALSE,
{ &weather, &render_weather_particle_systems_enabled } },
{ "camouflage pass", { FALSE, TRUE }, FALSE,
{ &rasterizer_debug_options.active_camouflage_multipass_enabled, NULL } },
};
enum
{
/* vr_graphics_effects' order */
_vr_effect_shadows,
_vr_effect_specular,
_vr_effect_reflections,
_vr_effect_bump_maps,
_vr_effect_detail_objects,
_vr_effect_lens_flares,
_vr_effect_screen_fog,
_vr_effect_contrails,
_vr_effect_weather,
_vr_effect_camouflage_pass,
/* the shield's glow (shield_glow.c), no switch of the table's */
_vr_effect_shield_glow,
NUMBER_OF_VR_EFFECTS
};
#define NUMBER_OF_VR_GRAPHICS_EFFECTS (sizeof(vr_graphics_effects) / sizeof(vr_graphics_effects[0]))
static char const *const vr_effect_names[NUMBER_OF_VR_EFFECTS] =
{
"shadows", "specular", "reflections", "bump maps", "detail objects", "lens flares", "screen fog", "contrails",
"weather", "camouflage pass", "shield glow"
};
/* "auto"'s steps, each leaving out more: the shield's glow (a light per
flaring unit, a pass of the level each), object shadows, reflections,
specular light, lens flares (with the first three, "medium"), bump maps,
then the rest of "low" */
static unsigned long const vr_graphics_auto_steps[] =
{
1ul << _vr_effect_shield_glow,
1ul << _vr_effect_shadows,
1ul << _vr_effect_reflections,
1ul << _vr_effect_specular,
1ul << _vr_effect_lens_flares,
1ul << _vr_effect_bump_maps,
(1ul << _vr_effect_detail_objects) | (1ul << _vr_effect_screen_fog) | (1ul << _vr_effect_contrails) |
(1ul << _vr_effect_weather) | (1ul << _vr_effect_camouflage_pass),
};
#define NUMBER_OF_VR_AUTO_STEPS (sizeof(vr_graphics_auto_steps) / sizeof(vr_graphics_auto_steps[0]))
/* of the frame's time: the GPU's past this leaves an effect out; under this
with an effect's cost brings it back */
#define VR_AUTO_HIGH 0.80f
#define VR_AUTO_LOW 0.70f
/* seconds: past the high mark this long (a share of the frames: the
metric's frames vary) leaves out; settled after a change; under the low
mark this long brings back (doubled, to a minute, while an effect brought
back had to go again within ten seconds) */
#define VR_AUTO_OVER_SECONDS 0.25f
#define VR_AUTO_OVER_SHARE 0.75f
#define VR_AUTO_SETTLE_SECONDS 0.4f
#define VR_AUTO_UNDER_SECONDS 3.0f
#define VR_AUTO_UNDER_MOST 60.0f
#define VR_AUTO_AGAIN_SECONDS 10.0f
/* an effect's cost (ms) until measured, and the least kept */
#define VR_AUTO_COST_GUESS 1.5f
#define VR_AUTO_COST_LEAST 0.3f
/* debug.vr_graphics_measure: each half (on, off), seconds, of which the
last part is measured */
#define VR_MEASURE_HALF_SECONDS 1.5f
#define VR_MEASURE_SKIP_SECONDS 0.5f
#define VR_GRAPHICS_SWITCHES (sizeof(vr_graphics_effects[0].switches) / sizeof(vr_graphics_effects[0].switches[0]))
static struct
{
boolean started;
int preset;
boolean client;
/* whether the preset allows each effect; what the game wants of each
switch, and what was written last frame */
boolean allowed[NUMBER_OF_VR_GRAPHICS_EFFECTS];
boolean wanted[NUMBER_OF_VR_GRAPHICS_EFFECTS][VR_GRAPHICS_SWITCHES];
boolean written[NUMBER_OF_VR_GRAPHICS_EFFECTS][VR_GRAPHICS_SWITCHES];
/* the effects left out now ("auto", debug.vr_graphics_measure), as
bits of _vr_effect_*, and the shield's glow allowed */
unsigned long left_out;
boolean shield_glow_allowed;
/* "auto": the steps taken (0: everything drawn); the frames since the
last change, over the high mark lately and under the low mark in a
row; the GPU time's sums for the step's cost (before the last step, and
after it settled); how long under the mark brings back (frames' seconds),
and when (frames) an effect came back */
boolean automatic, no_gpu_time_logged;
unsigned long timed_frame;
unsigned long steps;
unsigned long frame, changed_frame, restored_frame, dropped_frame;
float over_window[64];
unsigned long over_count;
unsigned long under_frames;
float before_sum;
unsigned long before_count;
float after_sum;
unsigned long after_count;
unsigned long measured_step;
float step_costs[NUMBER_OF_VR_AUTO_STEPS];
float under_seconds;
/* debug.vr_graphics_measure: the effect measured, the frame its turn
began, its sums on and off, and each effect's costs measured */
boolean measuring;
unsigned long measure_effect, measure_start;
float measure_on_sum, measure_off_sum;
unsigned long measure_on_count, measure_off_count;
float measure_costs[NUMBER_OF_VR_EFFECTS];
unsigned long measure_counts[NUMBER_OF_VR_EFFECTS];
} vr_graphics;
static void vr_graphics_allow(
void)
{
unsigned int index;
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
{
struct vr_graphics_effect const *effect = &vr_graphics_effects[index];
int preset = vr_graphics.preset;
vr_graphics.allowed[index] = (vr_graphics.client && effect->host_rules) ||
((preset == HALO_VR_GRAPHICS_AUTO || preset >= HALO_VR_GRAPHICS_HIGH || effect->preset[preset]) &&
!(vr_graphics.left_out & (1ul << index)));
}
vr_graphics.shield_glow_allowed = !(vr_graphics.left_out & (1ul << _vr_effect_shield_glow));
}
static void vr_graphics_choose(
int preset,
boolean client)
{
static char const *const names[] = { "low", "medium", "high", "max", "auto" };
vr_graphics.preset = preset;
vr_graphics.client = client;
vr_graphics.automatic = preset == HALO_VR_GRAPHICS_AUTO;
if (!vr_graphics.automatic && !vr_graphics.measuring)
vr_graphics.left_out = 0;
vr_graphics_allow();
platform_log("VR: graphics %s%s", names[preset], client ? " (in another's game)" : "");
}
/* the steps' effects left out, as bits */
static unsigned long vr_auto_left_out(
unsigned long steps)
{
unsigned long left_out = 0, step;
for (step = 0; step < steps && step < NUMBER_OF_VR_AUTO_STEPS; step++)
left_out |= vr_graphics_auto_steps[step];
return left_out;
}
static void vr_auto_effects_names(
unsigned long effects,
char *text,
size_t size)
{
unsigned long effect;
text[0] = 0;
for (effect = 0; effect < NUMBER_OF_VR_EFFECTS; effect++)
{
if (effects & (1ul << effect))
{
size_t length = strlen(text);
snprintf(text + length, size - length, "%s%s", length ? ", " : "", vr_effect_names[effect]);
}
}
}
/* whether a step leaves out anything drawn now: the shield's glow only
with display.shield_glow, an effect only while the game wants it */
static boolean vr_auto_step_matters(
unsigned long step)
{
unsigned long effects = vr_graphics_auto_steps[step];
unsigned long effect;
if ((effects & (1ul << _vr_effect_shield_glow)) && config_boolean("display.shield_glow"))
return TRUE;
for (effect = 0; effect < NUMBER_OF_VR_GRAPHICS_EFFECTS; effect++)
{
if ((effects & (1ul << effect)) && (vr_graphics.wanted[effect][0] || vr_graphics.wanted[effect][1]))
return TRUE;
}
return FALSE;
}
/* "auto": steps more left out, enough of them for what is over the mark by
their costs as known (more: TRUE), or one brought back */
static void vr_auto_step(
boolean more,
float gpu_ms,
float frame_ms,
float excess_ms)
{
char names[160];
unsigned long first = vr_graphics.steps;
if (more)
{
float saved = 0.f;
while (vr_graphics.steps < NUMBER_OF_VR_AUTO_STEPS && (saved < excess_ms || saved == 0.f))
{
if (vr_auto_step_matters(vr_graphics.steps))
saved += vr_graphics.step_costs[vr_graphics.steps];
vr_graphics.steps++;
}
{
unsigned long step, effects = 0;
for (step = first; step < vr_graphics.steps; step++)
effects |= vr_auto_step_matters(step) ? vr_graphics_auto_steps[step] : 0;
vr_auto_effects_names(effects, names, sizeof(names));
}
platform_log("VR graphics auto: %s left out (step %lu): the GPU %.1f ms a frame of %.1f", names,
vr_graphics.steps, gpu_ms, frame_ms);
/* (their cost: the GPU's time before, against after it settles; one
step's alone) */
vr_graphics.measured_step = vr_graphics.steps - first == 1 ? vr_graphics.steps : 0;
vr_graphics.after_sum = 0.f;
vr_graphics.after_count = 0;
/* (it had to go again soon after coming back: wait longer, once for
each coming back) */
if (vr_graphics.restored_frame && vr_graphics.frame - vr_graphics.restored_frame <
(unsigned long)(VR_AUTO_AGAIN_SECONDS * (1000.f / frame_ms)))
{
vr_graphics.under_seconds = MIN(vr_graphics.under_seconds * 2.f, VR_AUTO_UNDER_MOST);
}
vr_graphics.restored_frame = 0;
vr_graphics.dropped_frame = vr_graphics.frame;
}
else
{
/* (one that matters back, and those that do not below it with it) */
vr_graphics.steps--;
while (vr_graphics.steps && !vr_auto_step_matters(vr_graphics.steps - 1))
vr_graphics.steps--;
vr_auto_effects_names(vr_auto_left_out(first) & ~vr_auto_left_out(vr_graphics.steps), names, sizeof(names));
platform_log("VR graphics auto: %s back (step %lu): the GPU %.1f ms a frame of %.1f, %.1f ms for it",
names, vr_graphics.steps, gpu_ms, frame_ms, vr_graphics.step_costs[vr_graphics.steps]);
vr_graphics.restored_frame = vr_graphics.frame;
vr_graphics.measured_step = 0;
}
vr_graphics.changed_frame = vr_graphics.frame;
vr_graphics.over_count = 0;
vr_graphics.under_frames = 0;
vr_graphics.left_out = vr_auto_left_out(vr_graphics.steps);
vr_graphics_allow();
}
/* "auto", each frame: the GPU's time against the frame's */
static void vr_auto_update(
float gpu_ms,
float frame_ms,
float frames_a_second)
{
unsigned long settle = (unsigned long)(VR_AUTO_SETTLE_SECONDS * frames_a_second);
unsigned long over_frames = (unsigned long)(VR_AUTO_OVER_SECONDS * frames_a_second);
unsigned long since = vr_graphics.frame - vr_graphics.changed_frame;
unsigned long index, over = 0;
if (over_frames < 4)
over_frames = 4;
if (over_frames > NUMBEROF(vr_graphics.over_window))
over_frames = NUMBEROF(vr_graphics.over_window);
/* (what the last step saved: the GPU time before it, against the next
second's after it settled) */
if (vr_graphics.measured_step && since > settle && since <= settle + (unsigned long)frames_a_second)
{
vr_graphics.after_sum += gpu_ms;
vr_graphics.after_count++;
if (since == settle + (unsigned long)frames_a_second && vr_graphics.before_count && vr_graphics.after_count)
{
float cost = vr_graphics.before_sum / vr_graphics.before_count - vr_graphics.after_sum / vr_graphics.after_count;
unsigned long step = vr_graphics.measured_step - 1;
vr_graphics.step_costs[step] = MAX(VR_AUTO_COST_LEAST, 0.5f * (vr_graphics.step_costs[step] + cost));
platform_log("VR graphics auto: leaving out step %lu saved %.1f ms (taken as %.1f)", step + 1, cost,
vr_graphics.step_costs[step]);
vr_graphics.measured_step = 0;
}
}
if (since < settle)
return;
vr_graphics.over_window[vr_graphics.over_count++ % over_frames] = gpu_ms;
for (index = 0; index < over_frames && index < vr_graphics.over_count; index++)
over += vr_graphics.over_window[index] > VR_AUTO_HIGH * frame_ms;
if (vr_graphics.over_count >= over_frames && over >= (unsigned long)(VR_AUTO_OVER_SHARE * over_frames) &&
vr_graphics.steps < NUMBER_OF_VR_AUTO_STEPS)
{
float sum = 0.f;
for (index = 0; index < over_frames; index++)
sum += vr_graphics.over_window[index];
vr_graphics.before_sum = sum;
vr_graphics.before_count = over_frames;
/* (far over: as many steps as its costs say it takes, at once) */
vr_auto_step(TRUE, sum / over_frames, frame_ms, sum / over_frames - VR_AUTO_HIGH * frame_ms);
return;
}
/* (back: the time, with the effect's own, under the low mark) */
if (vr_graphics.steps && gpu_ms + vr_graphics.step_costs[vr_graphics.steps - 1] < VR_AUTO_LOW * frame_ms)
{
vr_graphics.under_frames++;
}
else
{
vr_graphics.under_frames = 0;
}
if (vr_graphics.steps && vr_graphics.under_frames > (unsigned long)(vr_graphics.under_seconds * frames_a_second))
vr_auto_step(FALSE, gpu_ms, frame_ms, 0.f);
/* (a minute without having to leave anything out: as quick as at first) */
if (vr_graphics.frame - vr_graphics.dropped_frame > (unsigned long)(60.f * frames_a_second))
vr_graphics.under_seconds = VR_AUTO_UNDER_SECONDS;
}
/* debug.vr_graphics_measure, each frame: each effect left out in turn */
static void vr_measure_update(
float gpu_ms,
float frames_a_second)
{
unsigned long half = (unsigned long)(VR_MEASURE_HALF_SECONDS * frames_a_second);
unsigned long skip = (unsigned long)(VR_MEASURE_SKIP_SECONDS * frames_a_second);
unsigned long since = vr_graphics.frame - vr_graphics.measure_start;
if (since >= skip && since < half)
{
vr_graphics.measure_on_sum += gpu_ms;
vr_graphics.measure_on_count++;
}
else if (since >= half + skip && since < 2 * half)
{
vr_graphics.measure_off_sum += gpu_ms;
vr_graphics.measure_off_count++;
}
if (since == half)
{
vr_graphics.left_out = 1ul << vr_graphics.measure_effect;
vr_graphics_allow();
}
if (since < 2 * half)
return;
if (vr_graphics.measure_on_count && vr_graphics.measure_off_count)
{
unsigned long effect = vr_graphics.measure_effect;
float on = vr_graphics.measure_on_sum / vr_graphics.measure_on_count;
float off = vr_graphics.measure_off_sum / vr_graphics.measure_off_count;
vr_graphics.measure_costs[effect] += on - off;
vr_graphics.measure_counts[effect]++;
platform_log("VR graphics measure: %s %.2f ms (on %.2f, off %.2f; mean of %lu: %.2f)", vr_effect_names[effect],
on - off, on, off, vr_graphics.measure_counts[effect],
vr_graphics.measure_costs[effect] / vr_graphics.measure_counts[effect]);
}
vr_graphics.measure_effect = (vr_graphics.measure_effect + 1) % NUMBER_OF_VR_EFFECTS;
vr_graphics.measure_start = vr_graphics.frame;
vr_graphics.measure_on_sum = vr_graphics.measure_off_sum = 0.f;
vr_graphics.measure_on_count = vr_graphics.measure_off_count = 0;
vr_graphics.left_out = 0;
vr_graphics_allow();
if (!vr_graphics.measure_effect)
{
char line[512] = "";
unsigned long effect;
for (effect = 0; effect < NUMBER_OF_VR_EFFECTS; effect++)
{
size_t length = strlen(line);
if (vr_graphics.measure_counts[effect])
snprintf(line + length, sizeof(line) - length, " %s %.2f;", vr_effect_names[effect],
vr_graphics.measure_costs[effect] / vr_graphics.measure_counts[effect]);
}
platform_log("VR graphics measure: means (ms of GPU a frame):%s", line);
}
}
/* the GPU's time against the frame's, each frame: "auto" and
debug.vr_graphics_measure */
static void vr_graphics_govern(
void)
{
float gpu_ms = halo_vr_gpu_milliseconds();
float rate = halo_vr_refresh_rate();
float frames_a_second = rate > 1.f ? rate : 90.f;
vr_graphics.frame++;
if (!vr_graphics.automatic && !vr_graphics.measuring)
return;
/* (none for five seconds: the headset does not tell it) */
if (gpu_ms > 0.f)
vr_graphics.timed_frame = vr_graphics.frame;
if (gpu_ms <= 0.f && vr_graphics.frame - vr_graphics.timed_frame < (unsigned long)(5.f * frames_a_second))
return;
if (gpu_ms <= 0.f)
{
if (!vr_graphics.no_gpu_time_logged)
platform_log("VR graphics %s: the headset does not tell the GPU's time: everything drawn (\"high\")",
vr_graphics.measuring ? "measure" : "auto");
vr_graphics.no_gpu_time_logged = TRUE;
if (vr_graphics.left_out)
{
vr_graphics.left_out = 0;
vr_graphics.steps = 0;
vr_graphics_allow();
}
return;
}
if (vr_graphics.measuring)
vr_measure_update(gpu_ms, frames_a_second);
else
vr_auto_update(gpu_ms, 1000.f / frames_a_second, frames_a_second);
}
/* each frame, before it is drawn (render.c) */
void vr_graphics_update(
void)
{
int preset = halo_vr_graphics();
boolean client = network_game_distributed_client();
unsigned int index, which;
if (!vr_graphics.started)
{
/* the game's switches as it starts are its wishes */
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
{
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
{
vr_graphics.wanted[index][which] = *vr_graphics_effects[index].switches[which];
vr_graphics.written[index][which] = *vr_graphics_effects[index].switches[which];
}
}
vr_graphics.started = TRUE;
vr_graphics.measuring = config_boolean("debug.vr_graphics_measure") != 0;
vr_graphics.under_seconds = VR_AUTO_UNDER_SECONDS;
for (index = 0; index < NUMBER_OF_VR_AUTO_STEPS; index++)
vr_graphics.step_costs[index] = VR_AUTO_COST_GUESS;
vr_graphics.shield_glow_allowed = TRUE;
vr_graphics_choose(preset, client);
if (vr_graphics.measuring)
platform_log("VR graphics measure: each effect left out in turn, %.1f s on and %.1f s off",
VR_MEASURE_HALF_SECONDS, VR_MEASURE_HALF_SECONDS);
}
else if (preset != vr_graphics.preset || client != vr_graphics.client)
{
vr_graphics.steps = 0;
vr_graphics_choose(preset, client);
}
vr_graphics_govern();
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
{
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
{
boolean *game_switch = vr_graphics_effects[index].switches[which];
boolean value;
/* (the game, a script, the console changed it since: its wish) */
if (*game_switch != vr_graphics.written[index][which])
vr_graphics.wanted[index][which] = *game_switch;
value = vr_graphics.wanted[index][which] && vr_graphics.allowed[index];
*game_switch = value;
vr_graphics.written[index][which] = value;
}
}
}
/* whether the shield's glow may be drawn (shield_glow.c): "auto" leaves it
out first */
boolean vr_graphics_allows_shield_glow(
void)
{
return !vr_graphics.started || vr_graphics.shield_glow_allowed;
}
/* whether the preset keeps this switch of the game's off now (cheats.c, a
client's rules: those it may keep off) */
boolean vr_graphics_reduces(
boolean const *game_switch)
{
unsigned int index, which;
if (!vr_graphics.started)
return FALSE;
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
{
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
{
if (vr_graphics_effects[index].switches[which] == game_switch)
return !vr_graphics.allowed[index];
}
}
return FALSE;
}
#endif
@@ -85,6 +85,11 @@ void halo_vertex_shader_lighting(unsigned long handle);
/* display.anti_aliasing's pass over a window's 3D view, before the HUD and
menus (source/render/render.c): the window's bounds on the screen */
void halo_screen_anti_alias(short x0, short y0, short x1, short y1);
/* a map loaded (game_initialize_for_new_map): the shader programs it drew
with last time made ready; and as it goes, its list of them kept
(port/linux/src/xgpu_program_cache.c, the Android app's) */
void halo_shader_programs_map_loaded(char const *map_name);
void halo_shader_programs_map_disposed(void);
/* display.hdr's tone mapping of a window's 3D view, before its
anti-aliasing (source/render/render.c) */
void halo_screen_tone_map(short x0, short y0, short x1, short y1);
+418
View File
@@ -0,0 +1,418 @@
/*
HALO_VR.H
VR for the native ports (port/linux/src/vr.c; implemented on Android, where
the host runs an OpenXR session, port/android/host/host_xr.c). The game's
sources call these under #ifdef HALO_ANDROID; the types are plain C so that
both the game (MSVC-era C) and the platform layer can include this.
Coordinates here are the game's: world units (10 feet, 3.048 metres), +z up,
yaw counter-clockwise from +x.
A frame in VR:
- halo_vr_frame_begin (the platform layer, before input is read) waits for
the headset's next frame and takes its predicted poses;
- the player's facing follows the head (halo_vr_aim, player_control.c);
- render.c draws the player's view once per eye (halo_vr_eye_camera), each
copied to its eye image (halo_vr_capture_eye), then clears the back buffer
(halo_vr_begin_overlay) and draws the HUD, menus and overlays over nothing;
- Present (d3d8_gl.c) copies the back buffer to the overlay layer and
submits the frame (halo_vr_present): the overlay follows the head over the
eyes' images, or, when no eyes were drawn (menus, loading, cinematics),
becomes a screen fixed in the room.
*/
#ifndef __HALO_VR_H
#define __HALO_VR_H
#ifdef __cplusplus
extern "C" {
#endif
/* the game runs in VR (a headset was found and its session created) */
int halo_vr_active(void);
/* this frame can draw the eyes: the session is shown and the views known */
int halo_vr_stereo_frame(void);
/* whether the given local player's view is drawn once per eye this frame:
cinematics and the like are shown on the flat screen instead */
int halo_vr_stereo_player(short local_player_index, int cinematic);
/* the camera of an eye, from the game's camera position: the eye's position
and orientation, the vertical field of view (radians) and the frustum
bounds (x0, x1, y0, y1; render_camera_build_frustum) for the game's
640x480 viewport */
void halo_vr_eye_camera(int eye, const float game_position[3],
float position[3], float forward[3], float up[3],
float *vertical_field_of_view, float frustum_bounds[4]);
/* copies the back buffer, just drawn for an eye, to that eye's image */
void halo_vr_capture_eye(int eye);
/* Both eyes in one draw (debug.multiview, where GL_OVR_multiview2 is): whether
this frame draws so; the camera between the eyes whose view takes in both
(as halo_vr_eye_camera), which also starts drawing into both eyes' layers;
and after the draw, the layers taken for the eyes' images. */
int halo_vr_multiview(void);
void halo_vr_center_camera(const float game_position[3], float position[3], float forward[3], float up[3],
float *vertical_field_of_view, float frustum_bounds[4]);
void halo_vr_capture_multiview(void);
/* the sky, drawn about the camera, is being drawn: infinitely far */
void halo_vr_multiview_sky(int sky);
/* the shared camera's projection's [2][2] and [3][2], for what the game
projects itself (sprites, lens flare tests) */
void halo_vr_multiview_projection(float projection_22, float projection_32);
/* lens flares are being drawn: placed by their depth, though not depth
tested */
void halo_vr_multiview_lens_flares(int drawing);
/* clears the back buffer to transparent black for the overlay, after the
eyes; menus_active chooses where the overlay is shown */
void halo_vr_begin_overlay(int menus_active);
/* the player's facing from the headset. Given the game's current desired
facing, returns nonzero with the facing to use instead. */
int halo_vr_aim(short local_player_index, float current_yaw, float current_pitch,
float *yaw, float *pitch);
/* where the head looks, in the game's axes; 0 when not in VR play */
int halo_vr_look_vector(short local_player_index, float vector[3]);
/* vr.log_performance: logs the unit's looking vector against the head's
every second (players.c) */
void halo_vr_debug_look(short local_player_index, const float unit_looking[3], int in_vehicle);
/* how far the view has turned since this was last asked, once past a few
degrees on an axis (the other axis 0); nonzero if it has. The game counts
it as looking around (player_control.c's action tests). */
int halo_vr_look_delta(float *yaw, float *pitch);
/* The weapon in the right hand, or the left one (vr.handedness; vr.aim_with
= "controller"; each returns 0, and the game does as before, when the hand
is not tracked):
- the direction the player's weapon aims, where the controller points (the
body and the view keep facing where the head looks);
- where its shots start, as an offset from the unit's camera (world units);
- the camera the first-person weapon model is posed from, so that it lies
in the hand (in model space, below);
- the aiming laser's start and direction; */
int halo_vr_aim_vector(short local_player_index, float vector[3]);
int halo_vr_weapon_origin_offset(short local_player_index, float offset[3]);
/* - and the same as a point in the world, where the crosshair's line starts
(as the drawn frame had it): shots start there too, not from an offset to
the unit's own eye point, which need not be the drawn camera's */
int halo_vr_weapon_origin(short local_player_index, float origin[3]);
/* The first-person model's camera: where the model's weapon hand lands on
the weapon hand's controller, as the free hand does (graph_index: the
weapon's first-person animations, whose hand halo_vr_weapon_wrist says
where it is, in the camera's axes: forward, right, up, world units, from
the weapon held ready; any other, the weapon as one held at the old fixed
place) */
int halo_vr_weapon_camera(short local_player_index, long graph_index, float position[3], float forward[3],
float up[3]);
void halo_vr_weapon_wrist(long graph_index, const float wrist[3]);
int halo_vr_laser(short local_player_index, float start[3], float direction[3]);
/* Model space: the first-person model (first_person_weapons.c) is always
posed as the right hand's. Left-handed, it is posed in a mirror image of
the room and drawn mirrored back (its triangles' winding turned over); the
mirror: a point on it (world units) and its normal, or 0 when it is not
mirrored. What first_person_weapons.c and vr.c exchange about the model
is in model space: the weapon camera, the free hand, the foregrip, the
fingers (0 the model's left hand, the free one), the barrel and the laser
along it. */
int halo_vr_first_person_mirror(short local_player_index, float point[3], float normal[3]);
/* the laser for this frame, along the barrel (first_person_weapons.c: while
an animation turns the weapon, as in a reload; model space), and the laser
at the ready, where the controller aims (in the room) */
void halo_vr_set_laser(short local_player_index, const float start[3], const float direction[3]);
int halo_vr_ready_laser(short local_player_index, float start[3], float direction[3]);
/* The arms (interface/first_person_weapons.c):
- the body: the eyes' position (world units), the neck they turn about,
and the torso's flat heading (which follows the head beyond a few
degrees, and the hands; the stick's turns turn it at once), from which
the shoulders hang;
- the free hand's pose (the left one, left-handed the right; model space);
- the free hand on the weapon: given where the model's left hand holds it
(world units) and the line from its right hand to its left in the
weapon's forward, right and up, whether the free hand grips it; then the
weapon aims along both hands; */
int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], float forward[3]);
int halo_vr_free_hand(short local_player_index, float position[3], float forward[3], float up[3]);
/* - for the whole body: the eyes (world units), the head's forward and up,
and the game's camera: where the eyes are standing straight (lower
crouched by the button), from which the physical crouch is measured; */
int halo_vr_head_pose(short local_player_index, float eye[3], float forward[3], float up[3], float standing[3]);
int halo_vr_two_hand(short local_player_index, const float foregrip[3], const float reference[3]);
/* - the fingers of a hand of the model (0 the left, the free hand; 1 the
right, the weapon's), from the controller's trigger, grip and touch: the thumb, index, middle finger and the last two, each
curled 0 (straight) to 1; 0 when the hand is not tracked or vr.fingers
is off, else HALO_VR_FINGERS_CURLED, and HALO_VR_FINGERS_TOUCH too when
the controller tells where the fingers rest (else a finger off the
trigger reads as one on it, unpressed); */
enum
{
HALO_VR_FINGERS_CURLED = 1,
HALO_VR_FINGERS_TOUCH = 2,
};
int halo_vr_finger_curls(short local_player_index, int hand, float curls[4]);
/* - whether the free hand's arm bends at the elbow to reach it from the
shoulder (vr.arm_ik); else it keeps the shape it has on the foregrip,
pointed at the shoulder;
- the time of the headset's frame (seconds, steady through the frame), by
which the arms' bending is eased */
int halo_vr_free_arm_bends(void);
double halo_vr_pose_time(void);
/* - what of the body is drawn (vr.body): the arms and hands, the hands
alone, or neither (the weapon alone); or the whole body, its torso, hips
and legs the player's own biped's (port/linux/game/vr_body.c), the arms
and hands as with "arms" */
enum
{
HALO_VR_BODY_ARMS = 0,
HALO_VR_BODY_HANDS,
HALO_VR_BODY_OFF,
HALO_VR_BODY_FULL,
};
int halo_vr_body_mode(void);
/* a hand has just been thrust forward: a melee attack (xinput_sdl.c) */
int halo_vr_melee(void);
/* voice chat's push to talk held: its button (vr.button_talk; the free
hand's grip by default) */
int halo_vr_push_to_talk_held(void);
/* the weight of the melee attack under way, from the swing's speed (0..1;
0 with none): the game scales melee damage by the player's run instead,
and in VR a player punches standing (units.c) */
float halo_vr_melee_strength(void);
/* The grenade (units.c): whether the free hand's trigger holds it in the
hand; and
as it let go, whether the hand threw it: from where (world units), which
way (the game's axes) and how hard (0..1 of the game's throw) */
int halo_vr_grenade_held(short local_player_index);
int halo_vr_grenade_throw(short local_player_index, float origin[3], float direction[3], float *strength);
/* where the held grenade is drawn (first_person_weapons.c): in the free
hand's palm, and at what size */
int halo_vr_grenade_in_hand(short local_player_index, float position[3], float forward[3], float up[3],
float *scale);
/* The scope (render.c): zoomed in by magnification, the camera for the
view on a disc over the weapon, in the game's axes; then that view, drawn
into the given part of the back buffer (left, top, right, bottom, 0..1),
is taken for the disc. Between the two, the first-person weapon is left
out (halo_vr_scope_pass). */
int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3],
float *vertical_field_of_view);
int halo_vr_scope_pass(void);
/* The menus' pointer (d3d8_gl.c's halo_ui_pointer_update): where a
controller points on the menus' screen, in their 640x480, whether that
moved since asked, and the trigger's clicks since (nonzero while it points
at the screen); and where it points this frame, to draw it (render.c) */
int halo_vr_menu_pointer(int *x, int *y, int *click_x, int *click_y, int *moved, int *clicks);
int halo_vr_menu_pointer_at(int *x, int *y);
/* Players' names over their heads (hud.c, render.c): whether they are drawn
apart this frame; the slot a name is drawn into (left, top, right, bottom
of the game's 640x480); a name's place (world units, over the head),
giving its slot (-1: no more); and the slots, drawn on the cleared back
buffer, taken (it is cleared again) */
int halo_vr_names_apart(void);
void halo_vr_name_slot(int slot, short bounds[4]);
int halo_vr_add_name(const float point[3]);
void halo_vr_capture_names(void);
/* how much higher than a rifle's the weapon in hand has its scope's disc
(metres; render.c, before halo_vr_scope) */
void halo_vr_set_scope_raise(float metres);
void halo_vr_capture_scope(float left, float top, float right, float bottom);
/* where the weapon's gun node is as drawn, at the ready (world units;
first_person_weapons.c, each frame; model space): shots, the crosshair and
the laser start there; and the direction its barrel points in the weapon
camera's frame (forward, left, up), which the camera is turned to aim */
void halo_vr_set_barrel(short local_player_index, const float world[3], const float direction[3]);
/* how far, in world units, the weapon's aim runs before it hits something
(render.c, each frame); the crosshair floats there */
void halo_vr_set_aim_distance(float world_units);
/* - and whether the HUD's crosshair, which marks where the head looks, is
left out */
int halo_vr_hide_crosshair(void);
/* whether the crosshair is drawn apart this frame (render.c), and taking
it, drawn alone on the cleared back buffer, which is cleared again */
int halo_vr_crosshair_apart(void);
void halo_vr_capture_crosshair(void);
/* Vehicles (player_control.c, each tick): the seat the player sits in and
its vehicle's heading (yaw, radians). Seated, the body turns with the
vehicle; a driver who steers by facing (HALO_VR_SEAT_STEER) steers with the
turning stick, else the weapon hand's controller aims (HALO_VR_SEAT_AIM).
And each
frame (render.c) the heading as drawn, between ticks. */
enum
{
HALO_VR_SEAT_NONE = 0,
HALO_VR_SEAT_STEER = 1,
HALO_VR_SEAT_AIM = 2,
};
/* (armed: HALO_VR_SEAT_ARMED if the seat fires the vehicle's gun, as a
Ghost's driver does, and HALO_VR_SEAT_TURRET too if all it does is man the
gun, as the Warthog's gunner) */
enum
{
HALO_VR_SEAT_ARMED = 1,
HALO_VR_SEAT_TURRET = 2,
};
void halo_vr_set_seat(short local_player_index, int seat, int armed, float vehicle_yaw);
void halo_vr_set_seat_frame(short local_player_index, float vehicle_yaw);
/* At an open vehicle's wheel (a Warthog's), each frame (render.c): the
driver's hands on it as drawn (world units, from the game's camera), or
NULL; the player's own hands turn it. And whether they hold it. */
void halo_vr_set_wheel(short local_player_index, const float left[3], const float right[3]);
int halo_vr_wheel_held(void);
/* the controllers' layout (HALO_XR_LAYOUT_*: 0 the Quest's, 1 the Steam
Frame's), for what the game tells the player */
int halo_vr_controller_layout(void);
/* the hand that holds the weapon (HALO_XR_HAND_*: 1 the right, 0 the left,
vr.handedness) */
int halo_vr_weapon_hand(void);
/* The game's actions on the controllers' buttons (vr.button_*; the VR
options' BUTTONS page): each action on one of eight buttons, named as
right-handed (A and B on the right controller, X and Y, the Quest's, on
the left; the move stick's click, the turning stick's, the weapon hand's
grip, the free hand's); left-handed with vr.mirror_controls, their mirror
images. The actions are numbered by the button each has by default. The
triggers, the menu button and the Steam Frame's bumpers, D-pad and View
button keep theirs; the menus keep the buttons as they are. */
enum
{
HALO_VR_INPUT_A = 0,
HALO_VR_INPUT_B,
HALO_VR_INPUT_X,
HALO_VR_INPUT_Y,
HALO_VR_INPUT_MOVE_STICK,
HALO_VR_INPUT_TURN_STICK,
HALO_VR_INPUT_GRIP,
/* (pressed held a moment, if it took neither the weapon's foregrip nor
the Warthog's wheel: vr.c's free_grip_down) */
HALO_VR_INPUT_FREE_GRIP,
HALO_VR_INPUT_COUNT,
HALO_VR_ACTION_JUMP = HALO_VR_INPUT_A,
HALO_VR_ACTION_LIGHT = HALO_VR_INPUT_B, /* a tap the flashlight; held, switch grenades */
HALO_VR_ACTION_ACTION = HALO_VR_INPUT_X, /* action, reload */
HALO_VR_ACTION_WEAPON = HALO_VR_INPUT_Y, /* switch weapon */
HALO_VR_ACTION_CROUCH = HALO_VR_INPUT_MOVE_STICK,
HALO_VR_ACTION_ZOOM = HALO_VR_INPUT_TURN_STICK,
HALO_VR_ACTION_MELEE = HALO_VR_INPUT_GRIP,
HALO_VR_ACTION_TALK = HALO_VR_INPUT_FREE_GRIP, /* voice chat's push to talk */
HALO_VR_ACTION_COUNT = HALO_VR_INPUT_COUNT,
};
/* an action's setting ("vr.button_jump"), a button's value in it ("a"),
and the button an action is on, as set */
const char *halo_vr_action_setting(int action);
const char *halo_vr_input_value(int input);
int halo_vr_action_input(int action);
/* the graphics preset (vr.graphics; HALO_VR_GRAPHICS_HIGH outside VR): which
effects are drawn (port/linux/game/vr_graphics.c), and with "max" the
models lit for each pixel and larger shadows (d3d8_gl.c); "auto" is "high"
less what the GPU's time asks to leave out */
enum
{
HALO_VR_GRAPHICS_LOW = 0,
HALO_VR_GRAPHICS_MEDIUM,
HALO_VR_GRAPHICS_HIGH,
HALO_VR_GRAPHICS_MAX,
HALO_VR_GRAPHICS_AUTO,
};
int halo_vr_graphics(void);
/* the app's GPU time of a recent frame (ms; 0: the headset does not tell
it, as SteamVR), and the refresh rate (Hz; 0: unknown) */
float halo_vr_gpu_milliseconds(void);
float halo_vr_refresh_rate(void);
/* whether vr.foveation has an effect: the headset's GPU foveates (the
Quest's; vr.c, d3d8_gl.c) */
int halo_vr_foveation_supported(void);
/* the VR options: the refresh rates the headset offers (Hz, at most maximum;
0 when it lets none be chosen, as SteamVR), and the one it runs at (0:
unknown; vr.refresh_rate asks for one); and vr.resolution_scale as the app
started (it takes a restart) */
int halo_vr_refresh_rates(float *rates, int maximum, float *current);
float halo_vr_started_resolution_scale(void);
/* whether a seated player's view stays in first person (director.c) */
int halo_vr_vehicle_first_person(void);
/* VR test aid (debug.vehicle_spawn): both sticks held in for a moment asks
for a vehicle; nonzero once per ask (player_control.c spawns it) */
int halo_vr_take_vehicle_spawn(short local_player_index);
/* how far a gunner's view is moved back from the gun (world units) */
float halo_vr_gunner_camera_back(void);
/* how far forward an open vehicle's driver's view is moved (world units) */
float halo_vr_driver_camera_forward(void);
/* how far the eyes of a rider who sits down in the hull rise above his
head (world units), by the kind of hull */
enum
{
HALO_VR_HULL_OPEN = 0,
HALO_VR_HULL_SCOUT, /* a Ghost: its hood arches over the rider */
HALO_VR_HULL_FIGHTER, /* a Banshee: the pilot lies in it */
HALO_VR_HULL_TURRET, /* a Shade: its shield is at the eyes */
HALO_VR_HULL_TANK, /* a Scorpion: its driver sits in the hull */
};
float halo_vr_enclosed_seat_raise(int hull);
/* how strongly glass tints and reflects (1 as the game has it) */
float halo_vr_glass_strength(void);
/* VR players of other machines in a network game (port/linux/game/
network_vr.c): their hands, as their machines send them, while they keep
coming (each returns 0 otherwise, and for this machine's own players, and
the game does as before):
- where the weapon aims, on foot;
- where its shots start (world units), from the unit's camera;
- whether the free hand holds the grenade;
- the hand's throw, once, as the grenade goes (as halo_vr_grenade_throw);
and this machine's own VR player's throw, to send (units.c). */
int halo_vr_network_aim(long player_index, float vector[3]);
int halo_vr_network_weapon_origin(long player_index, long unit_index, float origin[3]);
int halo_vr_network_grenade_held(long player_index);
int halo_vr_network_grenade_throw(long player_index, long unit_index, float origin[3], float direction[3],
float *strength);
void halo_vr_network_grenade_thrown(short local_player_index, long unit_index, const float origin[3],
const float direction[3], float strength);
/* the platform layer's side (vr.c) */
void halo_vr_initialize(void);
void halo_vr_frame_begin(void);
/* vr.log_performance: the end of a part of the frame, for the report: the
input and network, the game's ticks (how many), the camera and director,
the drawing; the rest is presenting */
enum
{
HALO_VR_PHASE_INPUT,
HALO_VR_PHASE_TICKS,
HALO_VR_PHASE_CAMERA,
HALO_VR_PHASE_DRAW,
HALO_VR_PHASE_COUNT
};
void halo_vr_phase(int phase, int ticks);
/* vr.log_performance: time taken by a part of the drawing, for the report
(first_person_weapons.c): its start (0 when not logged), and its end. The
first-person pose (its animation and the arms), the arms alone, the
pose taken as built already this frame (counted), and the whole body's pose
(vr_body.c; flat with debug.vr_body too, reported alone) */
enum
{
HALO_VR_TIMING_FIRST_PERSON,
HALO_VR_TIMING_ARMS,
HALO_VR_TIMING_FIRST_PERSON_KEPT,
HALO_VR_TIMING_BODY,
HALO_VR_TIMING_COUNT
};
unsigned long long halo_vr_timing_start(void);
void halo_vr_timing_end(int part, unsigned long long start);
/* the headset's frames, counted (0 before the first): what is drawn the same
for every eye, as the first-person pose posed from the controller, is built
once a frame */
unsigned long halo_vr_frame_serial(void);
void halo_vr_present(unsigned int back_buffer_texture);
/* the eye image size: the back buffer's size in pixels in VR */
int halo_vr_eye_size(int *width, int *height);
#ifdef __cplusplus
}
#endif
#endif
+1127 -169
View File
File diff suppressed because it is too large. Load diff
+33
View File
@@ -1540,6 +1540,34 @@ static void mix(float *output, unsigned long frames)
static SDL_AudioStream *audio_stream;
static BOOL audio_started = FALSE;
static BOOL audio_log = FALSE;
/* debug.audio_log: the level of what is mixed, every five seconds */
static void audio_log_level(const float *samples, unsigned long count)
{
static double sum;
static float peak;
static unsigned long total;
unsigned long index;
for (index = 0; index < count; index++)
{
float value = samples[index] < 0.0f ? -samples[index] : samples[index];
sum += (double)value * value;
if (value > peak)
peak = value;
}
total += count;
if (total >= (unsigned long)OUTPUT_RATE * OUTPUT_CHANNELS * 5)
{
platform_log("audio: mixed peak %.4f, rms %.5f over %lu samples", peak, sqrt(sum / total), total);
sum = 0.0;
peak = 0.0f;
total = 0;
}
}
/* the device it plays on (audio.output_device), and when it was looked at */
static char audio_device_name[PLATFORM_AUDIO_DEVICE_NAME_SIZE];
static unsigned long audio_device_read_at = (unsigned long)-1;
@@ -1567,6 +1595,8 @@ static void SDLCALL audio_callback(void *userdata, SDL_AudioStream *stream, int
if (!frames)
frames = 1;
mix(buffer, frames);
if (audio_log)
audio_log_level(buffer, frames * output_channels);
SDL_PutAudioStreamData(stream, buffer, (int)(frames * output_channels * sizeof(float)));
additional_amount -= (int)(frames * output_channels * sizeof(float));
}
@@ -1628,6 +1658,7 @@ static void audio_start(void)
return;
audio_started = TRUE;
master_volume = (float)config_real("audio.volume");
audio_log = config_boolean("debug.audio_log");
reverb_enabled = config_boolean("audio.reverb");
resampler_mode = resampler_mode_read();
resampler_initialize();
@@ -1672,6 +1703,8 @@ static void audio_start(void)
if (audio_stream)
{
SDL_ResumeAudioStreamDevice(audio_stream);
platform_log("audio: device stream open, %d Hz, %d channels, volume %.2f", OUTPUT_RATE, OUTPUT_CHANNELS,
master_volume);
return;
}
platform_log("cannot open an audio device (%s); sound is silent", SDL_GetError());
+34
View File
@@ -135,6 +135,10 @@ this list to generate the guest's entry points */
X(glGenQueries) \
X(glBeginQuery) \
X(glEndQuery) \
X(glTexStorage3D) \
X(glFramebufferTextureLayer) \
X(glFramebufferTextureMultiviewOVR) \
X(glIsEnabled) \
X(glGetQueryObjectuiv) \
X(glGetInternalformativ)
/* entry points of extensions, which a driver may lack: loaded where the
@@ -143,6 +147,21 @@ context has the extension, never required (gl_functions.c). On ES 3.0 and
#define GL_OPTIONAL_FUNCTIONS(X) \
X(glDrawElementsBaseVertexEXT) \
X(glDrawElementsBaseVertexOES)
#ifdef HALO_ANDROID
/* the app's alone (not the web's, whose WebGL 2 has none of them): the
program binary cache (xgpu_program_cache.c) and the eyes' foveation
(d3d8_gl.c); the optional ones, of GL_QCOM_texture_foveated and
GL_KHR_parallel_shader_compile, as above */
#define GL_ANDROID_FUNCTIONS(X) \
X(glDeleteProgram) \
X(glProgramParameteri) \
X(glGetProgramBinary) \
X(glProgramBinary) \
X(glGetTexParameteriv)
#define GL_ANDROID_OPTIONAL_FUNCTIONS(X) \
X(glTextureFoveationParametersQCOM) \
X(glMaxShaderCompilerThreadsKHR)
#endif
/* ANDROID_GL_FUNCTIONS_END */
#else
#define GL_FUNCTIONS(X) \
@@ -274,6 +293,10 @@ GL_FUNCTIONS(GL_DECLARE_FUNCTION)
#ifdef HALO_GLES
GL_OPTIONAL_FUNCTIONS(GL_DECLARE_FUNCTION)
#endif
#ifdef HALO_ANDROID
GL_ANDROID_FUNCTIONS(GL_DECLARE_FUNCTION)
GL_ANDROID_OPTIONAL_FUNCTIONS(GL_DECLARE_FUNCTION)
#endif
#undef GL_DECLARE_FUNCTION
/* call sites use the ordinary names; gl_functions.c, which defines the
@@ -385,7 +408,18 @@ pointers, sees the declarations without these aliases */
#define glGenQueries halo_glGenQueries
#define glBeginQuery halo_glBeginQuery
#define glEndQuery halo_glEndQuery
#define glTexStorage3D halo_glTexStorage3D
#define glFramebufferTextureLayer halo_glFramebufferTextureLayer
#define glFramebufferTextureMultiviewOVR halo_glFramebufferTextureMultiviewOVR
#define glIsEnabled halo_glIsEnabled
#define glGetQueryObjectuiv halo_glGetQueryObjectuiv
#ifdef HALO_ANDROID
#define glDeleteProgram halo_glDeleteProgram
#define glProgramParameteri halo_glProgramParameteri
#define glGetProgramBinary halo_glGetProgramBinary
#define glProgramBinary halo_glProgramBinary
#define glGetTexParameteriv halo_glGetTexParameteriv
#endif
#define glGetInternalformativ halo_glGetInternalformativ
#else
#define glGetString halo_glGetString
+10
View File
@@ -15,6 +15,10 @@ GL_FUNCTIONS(GL_DEFINE_FUNCTION)
#ifdef HALO_GLES
GL_OPTIONAL_FUNCTIONS(GL_DEFINE_FUNCTION)
#endif
#ifdef HALO_ANDROID
GL_ANDROID_FUNCTIONS(GL_DEFINE_FUNCTION)
GL_ANDROID_OPTIONAL_FUNCTIONS(GL_DEFINE_FUNCTION)
#endif
int gl_functions_load(void)
{
@@ -38,6 +42,9 @@ has them (xgpu_capabilities, d3d8_gl.c) */
success = success && !GL_MISSING_FAILS; \
}
GL_FUNCTIONS(GL_LOAD_FUNCTION)
#ifdef HALO_ANDROID
GL_ANDROID_FUNCTIONS(GL_LOAD_FUNCTION)
#endif
#undef GL_LOAD_FUNCTION
#if defined(HALO_GLES) && !defined(HALO_WEB)
/* a missing one is no failure: the renderer uses each only where the
@@ -46,6 +53,9 @@ has them (xgpu_capabilities, d3d8_gl.c) */
#define GL_LOAD_OPTIONAL_FUNCTION(name) \
halo_##name = (__typeof__(halo_##name))SDL_GL_GetProcAddress(#name);
GL_OPTIONAL_FUNCTIONS(GL_LOAD_OPTIONAL_FUNCTION)
#ifdef HALO_ANDROID
GL_ANDROID_OPTIONAL_FUNCTIONS(GL_LOAD_OPTIONAL_FUNCTION)
#endif
#undef GL_LOAD_OPTIONAL_FUNCTION
#endif
return success;
+13
View File
@@ -616,6 +616,19 @@ unsigned int hud_hires_png_texture(const void *png, unsigned long size, unsigned
return png_pixels_texture(pixels, width, height, levels);
}
/* (port, Mjolnir VR: the same in two steps, the decode on any thread:
menu_files.c's menu pictures) */
unsigned char *hud_hires_png_pixels(const void *png, unsigned long size, unsigned long *width, unsigned long *height)
{
return png_decode(png, size, width, height);
}
unsigned int hud_hires_pixels_texture(unsigned char *pixels, unsigned long width, unsigned long height,
unsigned long *levels)
{
return pixels ? png_pixels_texture(pixels, width, height, levels) : 0;
}
unsigned int hud_hires_override_texture(long asset, unsigned long *levels)
{
const struct hud_hires_embedded *embedded;
+5
View File
@@ -85,6 +85,11 @@ unsigned int hud_hires_override_texture(long asset, unsigned long *levels);
/* a GL texture drawn from an 8-bit RGBA PNG (as the tools write them), with
all its mip levels, and their number; 0 if it could not be */
unsigned int hud_hires_png_texture(const void *png, unsigned long size, unsigned long *levels);
/* the same in two steps: the PNG's texels (RGBA, rows top first; any thread
may decode), and a texture of them (which frees them) */
unsigned char *hud_hires_png_pixels(const void *png, unsigned long size, unsigned long *width, unsigned long *height);
unsigned int hud_hires_pixels_texture(unsigned char *pixels, unsigned long width, unsigned long height,
unsigned long *levels);
/* the HUD's textures for a map's bitmaps (port/linux/game/hud_hires_tags.c),
decoded from their PNGs as it loads, on a thread of their own, so that they
are only uploaded when first drawn; and, as it is unloaded, that stopped and
+214 -3
View File
@@ -1034,9 +1034,190 @@ static struct
unsigned int texture;
unsigned long levels;
int failed;
#ifdef HALO_ANDROID
/* being decoded on the pictures' thread (art_decode) */
int decoding;
#endif
} art[MAXIMUM_ART];
static long art_count;
#ifdef HALO_ANDROID
/* The menus' pictures decoded on a thread of their own: a screen's first
frame decoded its PNGs, 15 ms each, 100 ms and more for a screen (in VR a
frame shown late, the picture doubled). A picture not decoded yet is drawn
transparent for the frames it takes; the game's thread only uploads it. The
requests and results carry the art's generation: forgotten art's results
are dropped. */
#include <pthread.h>
#define ART_DECODE_QUEUE 64
static struct
{
int started;
pthread_mutex_t lock;
pthread_cond_t wake;
/* asked (the art's index, its PNG and generation), and decoded */
struct
{
long index;
unsigned long generation;
const unsigned char *png;
unsigned long size;
unsigned char *pixels;
unsigned long width, height;
int state; /* 0 free, 1 asked, 2 decoding, 3 decoded */
} slots[ART_DECODE_QUEUE];
unsigned long generation;
/* drawn while a picture is decoded */
unsigned int transparent;
} art_decode;
static void *art_decode_main(void *unused)
{
(void)unused;
pthread_mutex_lock(&art_decode.lock);
for (;;)
{
int slot;
for (slot = 0; slot < ART_DECODE_QUEUE && art_decode.slots[slot].state != 1; slot++)
{
}
if (slot == ART_DECODE_QUEUE)
{
pthread_cond_wait(&art_decode.wake, &art_decode.lock);
continue;
}
art_decode.slots[slot].state = 2;
pthread_mutex_unlock(&art_decode.lock);
{
unsigned long width = 0, height = 0;
unsigned char *pixels = hud_hires_png_pixels(art_decode.slots[slot].png, art_decode.slots[slot].size,
&width, &height);
pthread_mutex_lock(&art_decode.lock);
art_decode.slots[slot].pixels = pixels;
art_decode.slots[slot].width = width;
art_decode.slots[slot].height = height;
art_decode.slots[slot].state = 3;
}
}
return NULL;
}
/* the art's texture if its picture is decoded (uploaded now), else asked
for and 0 (*waiting: draw it transparent for now) */
static unsigned int art_decoded_texture(long index, int *waiting)
{
unsigned int texture = 0;
int slot, free_slot = -1;
*waiting = 0;
if (!art_decode.started)
{
pthread_t thread;
art_decode.started = -1;
pthread_mutex_init(&art_decode.lock, NULL);
pthread_cond_init(&art_decode.wake, NULL);
if (pthread_create(&thread, NULL, art_decode_main, NULL) == 0)
{
pthread_detach(thread);
art_decode.started = 1;
}
}
if (art_decode.started < 0)
return 0;
pthread_mutex_lock(&art_decode.lock);
for (slot = 0; slot < ART_DECODE_QUEUE; slot++)
{
if (art_decode.slots[slot].state == 3 && art_decode.slots[slot].generation != art_decode.generation)
{
/* (forgotten art's) */
free(art_decode.slots[slot].pixels);
art_decode.slots[slot].pixels = NULL;
art_decode.slots[slot].state = 0;
}
if (art_decode.slots[slot].state == 0 && free_slot < 0)
free_slot = slot;
if (art_decode.slots[slot].state && art_decode.slots[slot].index == index &&
art_decode.slots[slot].generation == art_decode.generation)
{
break;
}
}
if (slot < ART_DECODE_QUEUE)
{
if (art_decode.slots[slot].state == 3)
{
unsigned char *pixels = art_decode.slots[slot].pixels;
unsigned long width = art_decode.slots[slot].width, height = art_decode.slots[slot].height;
art_decode.slots[slot].pixels = NULL;
art_decode.slots[slot].state = 0;
pthread_mutex_unlock(&art_decode.lock);
art[index].decoding = 0;
if (!pixels)
return 0;
{
unsigned long long start = xgpu_shader_work_now_us();
texture = hud_hires_pixels_texture(pixels, width, height, &art[index].levels);
xgpu_texture_work_add(XGPU_TEXTURE_WORK_PICTURE, width * height, start);
}
return texture;
}
pthread_mutex_unlock(&art_decode.lock);
*waiting = 1;
return 0;
}
if (free_slot >= 0 && !art[index].decoding)
{
unsigned long size = 0;
const unsigned char *png;
pthread_mutex_unlock(&art_decode.lock);
png = file_data(art[index].png, &size);
if (!png)
return 0;
pthread_mutex_lock(&art_decode.lock);
art_decode.slots[free_slot].index = index;
art_decode.slots[free_slot].generation = art_decode.generation;
art_decode.slots[free_slot].png = png;
art_decode.slots[free_slot].size = size;
art_decode.slots[free_slot].state = 1;
art[index].decoding = 1;
pthread_cond_signal(&art_decode.wake);
}
pthread_mutex_unlock(&art_decode.lock);
/* (the queue full: asked again at its next draw) */
*waiting = 1;
return 0;
}
static unsigned int art_transparent_texture(void)
{
if (!art_decode.transparent)
{
static const unsigned char clear[4] = { 0, 0, 0, 0 };
glGenTextures(1, &art_decode.transparent);
glBindTexture(GL_TEXTURE_2D, art_decode.transparent);
xgpu_gl_state_invalidate();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, clear);
xgpu_gl_state_invalidate();
}
return art_decode.transparent;
}
#endif
/* the lowest and highest texture the art stands for: every texture the
renderer binds asks (xbox_textures.c), every frame of a game too, and the
menus' lie together, made as the map loaded, mostly apart from the map's
(in its texture cache) */
static unsigned long art_lowest, art_highest;
void halo_menus_art_register(void const *texture, char const *png)
{
unsigned long data = ((const unsigned long *)texture)[1];
@@ -1072,6 +1253,10 @@ void halo_menus_art_register(void const *texture, char const *png)
memset(&art[index], 0, sizeof(art[index]));
art[index].data = data;
art[index].png = strdup(png);
if (art_count == 1 || data < art_lowest)
art_lowest = data;
if (art_count == 1 || data > art_highest)
art_highest = data;
}
void halo_menus_art_forget(void)
@@ -1088,12 +1273,23 @@ void halo_menus_art_forget(void)
}
}
art_count = 0;
art_lowest = art_highest = 0;
#ifdef HALO_ANDROID
if (art_decode.started > 0)
{
pthread_mutex_lock(&art_decode.lock);
art_decode.generation++;
pthread_mutex_unlock(&art_decode.lock);
}
#endif
}
unsigned int menu_art_texture(unsigned long data, unsigned long *levels)
{
long index;
if (!art_count || data < art_lowest || data > art_highest)
return 0;
for (index = 0; index < art_count; index++)
{
if (art[index].data == data)
@@ -1104,10 +1300,25 @@ unsigned int menu_art_texture(unsigned long data, unsigned long *levels)
return 0;
if (!art[index].texture)
{
unsigned long size = 0;
const unsigned char *png = file_data(art[index].png, &size);
#ifdef HALO_ANDROID
int waiting = 0;
art[index].texture = png ? hud_hires_png_texture(png, size, &art[index].levels) : 0;
art[index].texture = art_decoded_texture(index, &waiting);
if (waiting)
{
*levels = 1;
return art_transparent_texture();
}
if (!art[index].texture && art_decode.started < 0)
#endif
{
unsigned long size = 0;
unsigned long long start = xgpu_shader_work_now_us();
const unsigned char *png = file_data(art[index].png, &size);
art[index].texture = png ? hud_hires_png_texture(png, size, &art[index].levels) : 0;
xgpu_texture_work_add(XGPU_TEXTURE_WORK_PICTURE, 0, start);
}
if (!art[index].texture)
{
platform_log("menus: could not draw %s", art[index].png);
+42 -1
View File
@@ -387,13 +387,30 @@ static const char shader_prologue[] =
"}\n";
char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instruction_count,
unsigned long packed_attribute_mask, const struct nv2a_vertex_lighting *lighting)
unsigned long packed_attribute_mask, const struct nv2a_vertex_lighting *lighting, int multiview)
{
struct xgpu_text text = { 0 };
unsigned long index;
#ifdef HALO_GLES
xgpu_text_append(&text, "#version %s\n", xgpu_capabilities.shading_language);
if (multiview)
{
/* VR: one draw into both eyes' layers. The scene is posed for a
camera between the eyes whose view covers both; each view moves the
position to its eye (half the eyes' distance aside) and onto its
own view's edges. As x' = a x + b w + c and y' likewise, with w the
depth along the view: mv_rows[2 * view] and [2 * view + 1];
what is drawn in screen space goes without c, as though far. */
xgpu_text_append(&text,
"#extension GL_OVR_multiview2 : require\n"
"layout(num_views = 2) in;\n"
"uniform vec4 mv_rows[4];\n"
/* the projection's depth: z = x + y / depth; z: depth tested */
"uniform vec4 mv_depth;\n");
}
#else
(void)multiview;
#endif
xgpu_text_append(&text, "%s", shader_prologue);
/* (the pixel shader's model_lighting; the normal's length in w) */
@@ -580,6 +597,7 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
GL: rows from the top, depth 0..1 */
"\tgl_Position.y = -gl_Position.y;\n"
"\tgl_Position.z = 2.0 * gl_Position.z - gl_Position.w;\n"
"%s"
#endif
"\tgl_PointSize = oPts.x;\n"
"\txD0 = clamp(oD0, 0.0, 1.0);\n"
@@ -593,6 +611,29 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
"\txFog = oFog.x;\n"
"}\n",
XGPU_VERTEX_CONSTANT_BIAS - 38, XGPU_VERTEX_CONSTANT_BIAS - 37
#ifdef HALO_ANDROID
/* (what is drawn in screen space, which kept no clip position, is
taken to each view as though infinitely far: a lens flare's sun
lands where the sky's does) */
/* (what the game projects itself, a sprite or a lens flare's
test, comes as a screen position with w 1 and its depth in z: its
eye's offset is by that depth, where it is depth tested) */
, multiview ?
"\t{\n"
"\t\tint row = int(gl_ViewID_OVR) * 2;\n"
"\t\tfloat near = 0.0;\n"
"\t\tif (clip_captured && abs(clip_position.w - 1.0) > 1.0e-5)\n"
"\t\t\tnear = 1.0;\n"
"\t\telse if (clip_captured && mv_depth.z > 0.5)\n"
"\t\t{\n"
"\t\t\tfloat depth = mv_depth.y / (clip_position.z - mv_depth.x);\n"
"\t\t\tnear = depth > 0.0 ? 1.0 / depth : 0.0;\n"
"\t\t}\n"
"\t\tgl_Position.xy = vec2(dot(vec3(gl_Position.xw, near), mv_rows[row].xyz),\n"
"\t\t\tdot(vec3(gl_Position.yw, near), mv_rows[row + 1].xyz));\n"
"\t}\n" :
""
#endif
);
return text.buffer;
}
+146 -2
View File
@@ -421,6 +421,8 @@ static struct
unsigned char join_host_hash[P2P_KEY_HASH_SIZE];
unsigned char join_token[P2P_TOKEN_SIZE];
unsigned long join_time;
/* how the latest join goes, for the menus (p2p_join_state) */
int join_state;
/* the join's progress (p2p_join_progress): one was asked for this run;
its host answered; it was given up (JOIN_TIMEOUT); the host's game
refused it (its rejection code, -1 if not) */
@@ -976,6 +978,12 @@ static void drop_peer(struct peer *peer, const char *reason)
{
int ask_again = p2p.joining && peer->is_host && !memcmp(peer->identifier, p2p.join_host, P2P_IDENTIFIER_SIZE);
if (peer->is_host && !memcmp(peer->identifier, p2p.join_host, P2P_IDENTIFIER_SIZE) &&
p2p.join_state != P2P_JOIN_STATE_NONE)
{
p2p.join_state = peer->connected ? P2P_JOIN_STATE_LOST : P2P_JOIN_STATE_BLOCKED;
}
if (!peer->probe)
{
platform_log("Internet play: %s %s: %s", peer->is_host && !peer->is_player ? "host" : "player", peer->name,
@@ -1225,6 +1233,8 @@ static void peer_heard(struct peer *peer, unsigned long address, unsigned short
peer->name, log_address_ipv4(address, port, text, sizeof(text)));
if (peer->is_host && !peer->is_player)
{
if (!memcmp(p2p.join_host, peer->identifier, P2P_IDENTIFIER_SIZE) && p2p.join_state != P2P_JOIN_STATE_NONE)
p2p.join_state = P2P_JOIN_STATE_CONNECTED;
if (p2p.joining && !memcmp(p2p.join_host, peer->identifier, P2P_IDENTIFIER_SIZE))
{
p2p.joining = 0;
@@ -2839,6 +2849,7 @@ static int join_invite(const char *text)
memcpy(p2p.join_host_hash, hash, sizeof(hash));
memcpy(p2p.join_token, token, sizeof(token));
p2p.join_requested = 1;
p2p.join_state = P2P_JOIN_STATE_REACHING;
join_progress_begin(0);
return 1;
}
@@ -2883,6 +2894,130 @@ int p2p_join_invite(const char *text)
return result > 0;
}
int p2p_join_code(char *text, int size)
{
int result;
pthread_mutex_lock(&p2p_lock);
result = p2p.running && p2p_code_hosted(text, size);
pthread_mutex_unlock(&p2p_lock);
return result;
}
int p2p_join_with_code(const char *typed)
{
int result;
pthread_mutex_lock(&p2p_lock);
result = p2p.running && p2p_code_look_up(typed);
pthread_mutex_unlock(&p2p_lock);
return result;
}
int p2p_join_code_status(void)
{
int status;
pthread_mutex_lock(&p2p_lock);
status = p2p.running ? p2p_code_status() : P2P_CODE_OFFLINE;
pthread_mutex_unlock(&p2p_lock);
return status;
}
void p2p_set_public_game(int public_game)
{
pthread_mutex_lock(&p2p_lock);
p2p_list_set_public(public_game);
pthread_mutex_unlock(&p2p_lock);
/* (and in the server browser of every build: p2p_lobby.c) */
p2p_set_hosting_public(public_game);
}
int p2p_public_game(void)
{
int result;
pthread_mutex_lock(&p2p_lock);
result = p2p_list_public();
pthread_mutex_unlock(&p2p_lock);
return result;
}
void p2p_set_game_description(const char *name, const char *map, const char *mode, int players,
int maximum_players)
{
pthread_mutex_lock(&p2p_lock);
p2p_list_set_description(name, map, mode, players, maximum_players);
pthread_mutex_unlock(&p2p_lock);
}
/* the server browser's games first (every build lists its public games
there: PC, phones, VR), then the VR port's own list's that it has not */
int p2p_public_games(struct p2p_public_game *games, int maximum)
{
struct p2p_listing listings[32];
struct p2p_public_game own[32];
int listing_count;
int own_count;
int count = 0;
int index;
p2p_lobby_browse(1);
listing_count = p2p_lobby_games(listings, 32);
for (index = 0; index < listing_count && count < maximum; index++)
{
struct p2p_public_game *game;
/* (one with a password: joined from the server browser, which asks
for it, not from this list, which has no way to) */
if (listings[index].locked)
continue;
game = &games[count++];
memset(game, 0, sizeof(*game));
snprintf(game->name, sizeof(game->name), "%s", listings[index].name);
snprintf(game->map, sizeof(game->map), "%s", listings[index].map);
snprintf(game->mode, sizeof(game->mode), "%s", listings[index].gametype);
snprintf(game->invite, sizeof(game->invite), "%s", listings[index].invite);
game->players = listings[index].player_count;
game->maximum_players = listings[index].maximum_player_count;
}
pthread_mutex_lock(&p2p_lock);
own_count = p2p.running ? p2p_list_games(own, 32) : 0;
pthread_mutex_unlock(&p2p_lock);
for (index = 0; index < own_count && count < maximum; index++)
{
int other;
int listed = 0;
for (other = 0; other < listing_count; other++)
{
if (!strcmp(own[index].name, listings[other].name) && !strcmp(own[index].map, listings[other].map))
listed = 1;
}
if (!listed)
games[count++] = own[index];
}
return count;
}
int p2p_join_state(void)
{
int state;
pthread_mutex_lock(&p2p_lock);
state = p2p.running ? p2p.join_state : P2P_JOIN_STATE_NONE;
pthread_mutex_unlock(&p2p_lock);
return state;
}
void p2p_join_code_clear_status(void)
{
pthread_mutex_lock(&p2p_lock);
p2p_code_clear_status();
pthread_mutex_unlock(&p2p_lock);
}
int p2p_join_listing(const struct p2p_listing *listing)
{
unsigned char hash[P2P_KEY_HASH_SIZE], token[P2P_TOKEN_SIZE];
@@ -2970,8 +3105,9 @@ void p2p_join_refused(int rejection_code)
void p2p_invite_received(const char *text)
{
/* (an older version's is logged as such) */
if (!join_invite(text))
/* (an older version's is logged as such; a join code is looked up,
p2p_code.c) */
if (!join_invite(text) && !p2p_code_look_up(text))
platform_log("Internet play: that is not an invite");
}
@@ -3001,6 +3137,8 @@ static void update_joining(void)
p2p.joining = 0;
p2p.join_gave_up = 1;
p2p_signal_stop_joining();
if (p2p.join_state == P2P_JOIN_STATE_REACHING)
p2p.join_state = P2P_JOIN_STATE_NO_ANSWER;
platform_log("Internet play: no answer from the invite's host; it may have stopped hosting or quit");
}
}
@@ -3144,8 +3282,13 @@ static void update_hosting(void)
p2p_signal_start();
p2p_signal_host(p2p.token);
{
/* (and a join code for it, p2p_code.c) */
unsigned char bytes[P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE];
char shown[128];
p2p_key_hash(p2p_public_key(), bytes);
memcpy(bytes + P2P_KEY_HASH_SIZE, p2p.token, P2P_TOKEN_SIZE);
p2p_code_host(bytes);
/* (whole on the clipboard and in the menus) */
platform_log("Internet play: hosting. Invite players with this link (it only works while this "
"copy of the game runs; it is on the clipboard): %s",
@@ -3163,6 +3306,7 @@ static void update_hosting(void)
{
p2p.hosting = 0;
p2p_signal_stop_hosting();
p2p_code_stop_hosting();
p2p_discord_set_hosting(NULL, 0, 0);
}
if (p2p.hosting)
+58
View File
@@ -26,6 +26,63 @@ int p2p_hand_off_invite(void);
words around it. Returns nonzero if it held an invite */
int p2p_join_invite(const char *text);
/* Join codes (p2p_code.c): the hosted game's code as XXXX-XXXX (0 if
none); looking a typed one up, which then joins as its invite does (0 if
it is not a code, or internet play is off); and how the look up goes */
enum
{
P2P_CODE_IDLE,
P2P_CODE_LOOKING,
P2P_CODE_FOUND,
P2P_CODE_NOT_FOUND,
P2P_CODE_INVALID,
P2P_CODE_OWN,
P2P_CODE_OFFLINE,
};
int p2p_join_code(char *text, int size);
int p2p_join_with_code(const char *typed);
int p2p_join_code_status(void);
void p2p_join_code_clear_status(void);
/* how the latest join of an invite or code goes (the VR port's, for the
menus): reaching the host, connected (its game is in the system link list),
or why not */
enum
{
P2P_JOIN_STATE_NONE,
P2P_JOIN_STATE_REACHING,
P2P_JOIN_STATE_CONNECTED,
P2P_JOIN_STATE_NO_ANSWER,
P2P_JOIN_STATE_BLOCKED,
P2P_JOIN_STATE_LOST,
};
int p2p_join_state(void);
/* Public games (p2p_list.c): the hosted game listed for anyone or not, and
what it is (the game's server, as it changes); and the games others list,
joined by their codes (p2p_join_with_code) */
enum
{
P2P_PUBLIC_NAME_SIZE = 16,
P2P_PUBLIC_MAP_SIZE = 32,
P2P_PUBLIC_MODE_SIZE = 24,
};
struct p2p_public_game
{
char name[P2P_PUBLIC_NAME_SIZE];
char map[P2P_PUBLIC_MAP_SIZE];
char mode[P2P_PUBLIC_MODE_SIZE];
char code[12];
int players;
int maximum_players;
/* (a game of the server browser's: no code, its invite) */
char invite[77];
};
void p2p_set_public_game(int public_game);
int p2p_public_game(void);
void p2p_set_game_description(const char *name, const char *map, const char *mode, int players,
int maximum_players);
int p2p_public_games(struct p2p_public_game *games, int maximum);
/* this machine's identifier, which its XNADDR carries (6 bytes) */
const unsigned char *p2p_identifier(void);
/* the address the game reaches the machine with this identifier at, if it
@@ -126,6 +183,7 @@ enum
P2P_LISTING_GAMETYPE_SIZE = 24,
/* an invite link's text (P2P_LINK_SIZE) */
P2P_LISTING_INVITE_SIZE = 77,
/* (struct p2p_public_game's invite is as large) */
/* a password's game's: its host's key and its key's hash, and its token
sealed (P2P_KEY_SIZE, P2P_KEY_HASH_SIZE, P2P_SEALED_TOKEN_SIZE) */
P2P_LISTING_SIGNING_KEY_SIZE = 32,
+317
View File
@@ -0,0 +1,317 @@
/*
P2P_CODE.C
Join codes (the VR port's): a short code that stands for a hosted game's
invite (p2p.c), for a player who cannot pass a link on, as in a headset,
where a link is not copied out and 64 hex digits are not typed in.
- A host makes a code when it starts hosting: 8 characters of Crockford's
base 32 (40 bits), shown as XXXX-XXXX (ui_join_code.c draws it in the
lobby). It lasts while the host hosts.
- From the code come a topic and a key (PBKDF2-HMAC-SHA256, slow on
purpose: CODE_KDF_ITERATIONS). The host listens on the topic through the
signalling brokers (p2p_signal.c). A player who types the code asks
there (ASK: a nonce), and the host answers with its invite, sealed with
the key, with the nonce (INVITE); the asker opens it and joins as an
invite link would.
- The brokers, and anyone who watches them, see the topic, but cannot tell
the code from it, or open the answer, short of working the slow
derivation out for codes until one fits; a code is guessed at one in 2^40
of them. An asker who has the code has the invite, as one given the link
has: it joins as anyone invited does.
A build without this (another of the same network version) has no codes,
and its players join with the link (which the host still logs and copies).
*/
#include "platform.h"
#include "posix.h"
#include "p2p_internal.h"
#include <stdio.h>
#include <string.h>
enum
{
CODE_SYMBOLS = 8,
CODE_KDF_ITERATIONS = 20000,
CODE_NONCE_SIZE = 8,
CODE_INVITE_SIZE = P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE,
CODE_MESSAGE_VERSION = 1,
/* milliseconds */
ASK_INTERVAL = 1000,
LOOK_TIMEOUT = 20000,
/* the host's answers: this many at once, and one more each this often */
MAXIMUM_ANSWERS = 8,
ANSWER_INTERVAL = 250,
};
enum
{
_code_message_ask = 'Q',
_code_message_invite = 'I',
};
/* Crockford's base 32: no I, L, O or U */
static const char code_alphabet[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
static struct
{
/* hosting: the code, its topic and key, and the invite it stands for */
int hosting;
char host_code[CODE_SYMBOLS + 1];
char host_topic[P2P_CODE_TOPIC_SIZE];
unsigned char host_key[P2P_SHA256_SIZE];
unsigned char invite[CODE_INVITE_SIZE];
int answers_left;
unsigned long answers_time;
/* looking a code up */
int status;
char look_topic[P2P_CODE_TOPIC_SIZE];
unsigned char look_key[P2P_SHA256_SIZE];
unsigned char look_nonce[CODE_NONCE_SIZE];
unsigned long look_started_time;
unsigned long asked_time;
int asked;
} code;
static int code_elapsed(unsigned long since, unsigned long time)
{
return (unsigned int)(p2p_now() - since) >= (unsigned int)time;
}
/* the code's symbols from what was typed: letters and digits, read as
Crockford reads them (O as 0, I and L as 1), the rest (dashes, spaces)
left out; 0 unless they are exactly a code's */
static int code_normalize(const char *text, char *symbols)
{
int count = 0;
for (; *text; text++)
{
char character = *text;
if (character >= 'a' && character <= 'z')
character = (char)(character - 'a' + 'A');
if (character == 'O')
character = '0';
else if (character == 'I' || character == 'L')
character = '1';
if (!strchr(code_alphabet, character) || !character)
{
if (character == '-' || character == ' ' || character == '\t' || character == '\r' ||
character == '\n')
{
continue;
}
return 0;
}
if (count == CODE_SYMBOLS)
return 0;
symbols[count++] = character;
}
symbols[count] = 0;
return count == CODE_SYMBOLS;
}
/* the topic and key of a code (PBKDF2-HMAC-SHA256, one block) */
static void code_derive(const char *symbols, char *topic, unsigned char *key)
{
static const char salt[] = "halo-ce-vr join code 1";
unsigned char block[sizeof(salt) - 1 + 4];
unsigned char u[P2P_SHA256_SIZE];
unsigned char derived[P2P_SHA256_SIZE];
unsigned char digest[P2P_SHA256_SIZE];
char hex[2 * P2P_SHA256_SIZE + 1];
int iteration;
int index;
memcpy(block, salt, sizeof(salt) - 1);
block[sizeof(salt) - 1] = 0;
block[sizeof(salt)] = 0;
block[sizeof(salt) + 1] = 0;
block[sizeof(salt) + 2] = 1;
p2p_hmac_sha256((const unsigned char *)symbols, CODE_SYMBOLS, block, sizeof(block), u);
memcpy(derived, u, sizeof(derived));
for (iteration = 1; iteration < CODE_KDF_ITERATIONS; iteration++)
{
p2p_hmac_sha256((const unsigned char *)symbols, CODE_SYMBOLS, u, sizeof(u), u);
for (index = 0; index < P2P_SHA256_SIZE; index++)
derived[index] ^= u[index];
}
p2p_hmac_sha256(derived, sizeof(derived), "topic", 5, digest);
p2p_hex(digest, 16, hex);
snprintf(topic, P2P_CODE_TOPIC_SIZE, "hcvr/1/%s", hex);
p2p_hmac_sha256(derived, sizeof(derived), "seal", 4, key);
}
/* ---------- p2p.c's side (under p2p_lock) */
void p2p_code_host(const unsigned char *invite)
{
unsigned char random[CODE_SYMBOLS];
int index;
posix_random_bytes(random, sizeof(random));
for (index = 0; index < CODE_SYMBOLS; index++)
code.host_code[index] = code_alphabet[random[index] & 31];
code.host_code[CODE_SYMBOLS] = 0;
memcpy(code.invite, invite, CODE_INVITE_SIZE);
code_derive(code.host_code, code.host_topic, code.host_key);
code.answers_left = MAXIMUM_ANSWERS;
code.answers_time = p2p_now();
code.hosting = 1;
platform_log("Internet play: join code %.4s-%.4s", code.host_code, code.host_code + 4);
p2p_signal_sync_topics();
}
void p2p_code_stop_hosting(void)
{
code.hosting = 0;
code.host_code[0] = 0;
p2p_signal_sync_topics();
}
int p2p_code_hosted(char *text, int size)
{
if (!code.hosting || size < CODE_SYMBOLS + 2)
return 0;
snprintf(text, (size_t)size, "%.4s-%.4s", code.host_code, code.host_code + 4);
return 1;
}
int p2p_code_look_up(const char *typed)
{
char symbols[CODE_SYMBOLS + 1];
if (!code_normalize(typed, symbols))
{
code.status = P2P_CODE_INVALID;
return 0;
}
if (code.hosting && !strcmp(symbols, code.host_code))
{
code.status = P2P_CODE_OWN;
return 0;
}
code_derive(symbols, code.look_topic, code.look_key);
posix_random_bytes(code.look_nonce, sizeof(code.look_nonce));
code.look_started_time = p2p_now();
code.asked = 0;
code.status = P2P_CODE_LOOKING;
p2p_signal_sync_topics();
return 1;
}
int p2p_code_status(void)
{
return code.status;
}
void p2p_code_clear_status(void)
{
if (code.status != P2P_CODE_LOOKING)
code.status = P2P_CODE_IDLE;
}
/* ---------- p2p_signal.c's side (under p2p_lock) */
void p2p_code_topics(const char **host_topic, const char **look_topic)
{
*host_topic = code.hosting ? code.host_topic : "";
*look_topic = code.status == P2P_CODE_LOOKING ? code.look_topic : "";
}
static void code_answer(const unsigned char *nonce)
{
unsigned char message[2 + CODE_NONCE_SIZE + CODE_INVITE_SIZE];
unsigned char sealed[2 + sizeof(message) + P2P_SEAL_OVERHEAD];
int sealed_size;
/* (anyone watching the brokers can ask, as often as they like) */
while (code.answers_left < MAXIMUM_ANSWERS && code_elapsed(code.answers_time, ANSWER_INTERVAL))
{
code.answers_left++;
code.answers_time += ANSWER_INTERVAL;
}
if (code.answers_left == MAXIMUM_ANSWERS)
code.answers_time = p2p_now();
if (code.answers_left <= 0)
return;
code.answers_left--;
message[0] = _code_message_invite;
message[1] = CODE_MESSAGE_VERSION;
memcpy(message + 2, nonce, CODE_NONCE_SIZE);
memcpy(message + 2 + CODE_NONCE_SIZE, code.invite, CODE_INVITE_SIZE);
sealed[0] = _code_message_invite;
sealed[1] = CODE_MESSAGE_VERSION;
sealed_size = p2p_seal(code.host_key, message, sizeof(message), sealed + 2);
p2p_signal_publish(code.host_topic, sealed, 2 + sealed_size);
}
static void code_invite_received(const unsigned char *payload, int size)
{
unsigned char message[2 + CODE_NONCE_SIZE + CODE_INVITE_SIZE + 16];
int message_size;
char hex[2 * CODE_INVITE_SIZE + 1];
char link[P2P_LINK_SIZE];
if (size > (int)sizeof(message) + P2P_SEAL_OVERHEAD)
return;
message_size = p2p_open(code.look_key, payload, size, message);
if (message_size != 2 + CODE_NONCE_SIZE + CODE_INVITE_SIZE || message[0] != _code_message_invite ||
message[1] != CODE_MESSAGE_VERSION || !p2p_equal(message + 2, code.look_nonce, CODE_NONCE_SIZE))
{
return;
}
code.status = P2P_CODE_FOUND;
p2p_signal_sync_topics();
p2p_hex(message + 2 + CODE_NONCE_SIZE, CODE_INVITE_SIZE, hex);
snprintf(link, sizeof(link), "halo://join/%s", hex);
platform_log("Internet play: the join code's host answered");
p2p_invite_received(link);
}
int p2p_code_received(const char *topic, const unsigned char *payload, int size)
{
if (size < 2 || payload[1] != CODE_MESSAGE_VERSION)
return 0;
if (code.hosting && !strcmp(topic, code.host_topic))
{
if (payload[0] == _code_message_ask && size == 2 + CODE_NONCE_SIZE)
code_answer(payload + 2);
return 1;
}
if (code.status == P2P_CODE_LOOKING && !strcmp(topic, code.look_topic))
{
if (payload[0] == _code_message_invite)
code_invite_received(payload + 2, size - 2);
return 1;
}
return 0;
}
void p2p_code_update(void)
{
unsigned char message[2 + CODE_NONCE_SIZE];
if (code.status != P2P_CODE_LOOKING)
return;
p2p_signal_start();
if (code_elapsed(code.look_started_time, LOOK_TIMEOUT))
{
code.status = P2P_CODE_NOT_FOUND;
p2p_signal_sync_topics();
platform_log("Internet play: no game answered the join code");
return;
}
if (!p2p_signal_connected() || (code.asked && !code_elapsed(code.asked_time, ASK_INTERVAL)))
return;
message[0] = _code_message_ask;
message[1] = CODE_MESSAGE_VERSION;
memcpy(message + 2, code.look_nonce, CODE_NONCE_SIZE);
p2p_signal_publish(code.look_topic, message, sizeof(message));
code.asked = 1;
code.asked_time = p2p_now();
}
+47
View File
@@ -169,6 +169,53 @@ int p2p_signal_probe(const unsigned char *host_hash, const unsigned char *token)
void p2p_signal_stop_probe(const unsigned char *identifier);
/* whether any broker is connected */
int p2p_signal_connected(void);
/* (for p2p_code.c) the topics every broker listens on, as they are now; and
a message to a topic through every broker */
void p2p_signal_sync_topics(void);
void p2p_signal_publish(const char *topic, const unsigned char *payload, int size);
/* the same, kept by the brokers for whoever listens later (empty: no
longer) */
void p2p_signal_publish_retained(const char *topic, const unsigned char *payload, int size);
/* ---------- p2p_code.c: join codes (under p2p_lock) */
enum
{
P2P_CODE_TOPIC_SIZE = 7 + 32 + 1,
};
/* hosting: a new code for the invite (its key hash and token), and none */
void p2p_code_host(const unsigned char *invite);
void p2p_code_stop_hosting(void);
/* the code hosted, as XXXX-XXXX; 0 if none */
int p2p_code_hosted(char *text, int size);
/* starts looking up a typed code (0: not a code, or this machine's own;
p2p_code_status says which) */
int p2p_code_look_up(const char *typed);
int p2p_code_status(void);
/* forgets a finished look up's result */
void p2p_code_clear_status(void);
/* (p2p_signal.c) the topics to listen on (empty: none); a message on a
topic, nonzero if it was a code's; and each pass of the p2p thread */
void p2p_code_topics(const char **host_topic, const char **look_topic);
int p2p_code_received(const char *topic, const unsigned char *payload, int size);
void p2p_code_update(void);
/* ---------- p2p_list.c: public games (under p2p_lock) */
/* hosting: whether the game is listed, and what it is */
void p2p_list_set_public(int public_game);
int p2p_list_public(void);
void p2p_list_set_description(const char *name, const char *map, const char *mode, int players,
int maximum_players);
/* the games listed lately; asking keeps this machine listening a while */
int p2p_list_games(struct p2p_public_game *games, int maximum);
/* (p2p_signal.c) the topic to listen on (empty: none); a message on one
(nonzero if it was a listing's); and each pass, whether hosting */
const char *p2p_list_topic(void);
int p2p_list_received(const char *topic, const unsigned char *payload, int size);
void p2p_list_update(int hosting);
/* every broker's connection made anew at once: after the p2p thread was
stopped (the process was suspended, or the machine slept), the old ones
may carry nothing, and would be noticed only at their silence timeout */
+335
View File
@@ -0,0 +1,335 @@
/*
P2P_LIST.C
Public games (the VR port's): a host may list its game for anyone, who
finds it in a list of online games and joins it by its join code
(p2p_code.c), without a server of this project's.
- A public game's host announces it through the signalling brokers
(p2p_signal.c) every ANNOUNCE_INTERVAL, retained, on a topic of its own
under hcvr/1/g<network version>/: its name, map, game type, players, its
join code, and the time. Retained, a machine that starts listening gets
every game's latest at once. A host that stops (it closes its game, or
makes it private) clears its topic; one that quits without that leaves
its last announcement, which listeners drop once it is older than
STALE_SECONDS (by the machines' clocks).
- A machine that shows the list listens on the topics of its network
version (a wildcard), and keeps the games heard of lately.
- An announcement carries no address: a machine reaches a public game's
host only by its code, as anyone given the code does. Anyone can announce
anything, so what is shown is checked to be printable and in bounds, and
a game that is not what it says is only a code that does not answer.
*/
#include "platform.h"
#include "posix.h"
#include "port_config.h"
#include "p2p_internal.h"
#include "halo_port_limits.h"
#include <stdio.h>
#include <string.h>
#include <time.h>
enum
{
ANNOUNCEMENT_VERSION = 1,
/* milliseconds */
ANNOUNCE_INTERVAL = 10000,
/* a listener's interest lapses this long after the menu last asked */
WATCH_TIME = 3000,
STALE_SECONDS = 45,
MAXIMUM_LISTED_GAMES = 32,
};
/* what goes to the brokers (byte for byte) */
struct announcement
{
char magic[4];
unsigned char version;
unsigned char players;
unsigned char maximum_players;
unsigned char flags;
unsigned char time[8];
char code[8];
char name[P2P_PUBLIC_NAME_SIZE];
char map[P2P_PUBLIC_MAP_SIZE];
char mode[P2P_PUBLIC_MODE_SIZE];
};
struct listed_game
{
int used;
char topic[P2P_CODE_TOPIC_SIZE];
unsigned long heard_time;
long long announced_time;
struct p2p_public_game game;
};
static struct
{
/* hosting (whether public: network.public_game, until Y changes it) */
int initialized;
int public_game;
char topic[P2P_CODE_TOPIC_SIZE];
int announced;
unsigned long announced_time;
char name[P2P_PUBLIC_NAME_SIZE];
char map[P2P_PUBLIC_MAP_SIZE];
char mode[P2P_PUBLIC_MODE_SIZE];
int players;
int maximum_players;
/* listening */
unsigned long watched_time;
int watching;
char watch_topic[P2P_CODE_TOPIC_SIZE];
struct listed_game games[MAXIMUM_LISTED_GAMES];
} list;
static int list_elapsed(unsigned long since, unsigned long time)
{
return (unsigned int)(p2p_now() - since) >= (unsigned int)time;
}
/* text as shown: printable ASCII only, ended */
static void list_text(char *destination, int size, const char *source, int source_size)
{
int index;
for (index = 0; index < size - 1 && index < source_size && source[index]; index++)
destination[index] = source[index] >= 32 && source[index] < 127 ? source[index] : '?';
destination[index] = 0;
}
static long long list_time_now(void)
{
return (long long)time(NULL);
}
static void list_topics_make(void)
{
if (!list.topic[0])
{
unsigned char random[8];
char hex[17];
posix_random_bytes(random, sizeof(random));
p2p_hex(random, sizeof(random), hex);
snprintf(list.topic, sizeof(list.topic), "hcvr/1/g%d/%s", HALO_PORT_NETWORK_VERSION, hex);
}
if (!list.watch_topic[0])
snprintf(list.watch_topic, sizeof(list.watch_topic), "hcvr/1/g%d/+", HALO_PORT_NETWORK_VERSION);
}
static void list_announce(void)
{
struct announcement announcement;
char code[16];
long long now = list_time_now();
int index;
if (!p2p_code_hosted(code, sizeof(code)))
return;
memset(&announcement, 0, sizeof(announcement));
memcpy(announcement.magic, "HCVR", 4);
announcement.version = ANNOUNCEMENT_VERSION;
announcement.players = (unsigned char)(list.players < 0 ? 0 : list.players > 255 ? 255 : list.players);
announcement.maximum_players =
(unsigned char)(list.maximum_players < 0 ? 0 : list.maximum_players > 255 ? 255 : list.maximum_players);
for (index = 0; index < 8; index++)
announcement.time[index] = (unsigned char)(now >> (8 * index));
/* (the code without its dash) */
memcpy(announcement.code, code, 4);
memcpy(announcement.code + 4, code + 5, 4);
memcpy(announcement.name, list.name, sizeof(announcement.name));
memcpy(announcement.map, list.map, sizeof(announcement.map));
memcpy(announcement.mode, list.mode, sizeof(announcement.mode));
p2p_signal_publish_retained(list.topic, (const unsigned char *)&announcement, sizeof(announcement));
if (!list.announced)
platform_log("Internet play: the game is listed as public (%s, %s, %s)", list.name, list.map, list.mode);
list.announced = 1;
list.announced_time = p2p_now();
}
static void list_withdraw(void)
{
if (!list.announced)
return;
p2p_signal_publish_retained(list.topic, NULL, 0);
platform_log("Internet play: the game is no longer listed as public");
list.announced = 0;
}
/* ---------- p2p.c's side (under p2p_lock) */
void p2p_list_set_public(int public_game)
{
if (public_game == list.public_game)
return;
list.public_game = public_game;
list_topics_make();
if (!public_game)
list_withdraw();
else
list.announced_time = p2p_now() - ANNOUNCE_INTERVAL;
}
int p2p_list_public(void)
{
return list.public_game;
}
void p2p_list_set_description(const char *name, const char *map, const char *mode, int players,
int maximum_players)
{
int changed = strncmp(list.name, name, sizeof(list.name) - 1) || strncmp(list.map, map, sizeof(list.map) - 1) ||
strncmp(list.mode, mode, sizeof(list.mode) - 1) || players != list.players ||
maximum_players != list.maximum_players;
list_text(list.name, sizeof(list.name), name, P2P_PUBLIC_NAME_SIZE);
list_text(list.map, sizeof(list.map), map, P2P_PUBLIC_MAP_SIZE);
list_text(list.mode, sizeof(list.mode), mode, P2P_PUBLIC_MODE_SIZE);
list.players = players;
list.maximum_players = maximum_players;
/* (a change goes out soon, not at the next interval) */
if (changed && list.announced && list_elapsed(list.announced_time, 1000))
list.announced_time = p2p_now() - ANNOUNCE_INTERVAL;
}
int p2p_list_games(struct p2p_public_game *games, int maximum)
{
int count = 0;
int index;
list.watched_time = p2p_now();
list.watching = 1;
for (index = 0; index < MAXIMUM_LISTED_GAMES && count < maximum; index++)
{
if (list.games[index].used)
games[count++] = list.games[index].game;
}
return count;
}
/* ---------- p2p_signal.c's side (under p2p_lock) */
const char *p2p_list_topic(void)
{
return list.watching ? list.watch_topic : "";
}
int p2p_list_received(const char *topic, const unsigned char *payload, int size)
{
struct announcement announcement;
struct listed_game *slot = NULL;
long long announced = 0;
char code[16];
int index;
if (!list.watch_topic[0] || strncmp(topic, list.watch_topic, strlen(list.watch_topic) - 1))
return 0;
/* (this machine's own, as the broker echoes it) */
if (!strcmp(topic, list.topic))
return 1;
for (index = 0; index < MAXIMUM_LISTED_GAMES; index++)
{
if (list.games[index].used && !strcmp(list.games[index].topic, topic))
slot = &list.games[index];
}
if (size != (int)sizeof(announcement))
{
/* (empty: withdrawn) */
if (slot && size == 0)
slot->used = 0;
return 1;
}
memcpy(&announcement, payload, sizeof(announcement));
if (memcmp(announcement.magic, "HCVR", 4) || announcement.version != ANNOUNCEMENT_VERSION ||
!announcement.maximum_players || announcement.players > announcement.maximum_players)
{
return 1;
}
for (index = 0; index < 8; index++)
announced |= (long long)announcement.time[index] << (8 * index);
if (announced < list_time_now() - STALE_SECONDS || announced > list_time_now() + STALE_SECONDS)
{
if (slot)
slot->used = 0;
return 1;
}
snprintf(code, sizeof(code), "%.4s-%.4s", announcement.code, announcement.code + 4);
for (index = 0; code[index]; index++)
{
if (index != 4 && !((code[index] >= '0' && code[index] <= '9') || (code[index] >= 'A' && code[index] <= 'Z')))
return 1;
}
if (!slot)
{
int oldest = -1;
for (index = 0; index < MAXIMUM_LISTED_GAMES && !slot; index++)
{
if (!list.games[index].used)
slot = &list.games[index];
else if (oldest < 0 || list.games[index].announced_time < list.games[oldest].announced_time)
oldest = index;
}
if (!slot)
slot = &list.games[oldest];
memset(slot, 0, sizeof(*slot));
snprintf(slot->topic, sizeof(slot->topic), "%s", topic);
}
slot->used = 1;
slot->heard_time = p2p_now();
slot->announced_time = announced;
list_text(slot->game.name, sizeof(slot->game.name), announcement.name, sizeof(announcement.name));
list_text(slot->game.map, sizeof(slot->game.map), announcement.map, sizeof(announcement.map));
list_text(slot->game.mode, sizeof(slot->game.mode), announcement.mode, sizeof(announcement.mode));
snprintf(slot->game.code, sizeof(slot->game.code), "%s", code);
slot->game.players = announcement.players;
slot->game.maximum_players = announcement.maximum_players;
return 1;
}
void p2p_list_update(int hosting)
{
int index;
if (!list.initialized)
{
list.initialized = 1;
if (!list.public_game)
list.public_game = config_boolean("network.public_game");
}
list_topics_make();
/* hosting a public game: announced every so often; withdrawn when it
is not */
if (hosting && list.public_game)
{
p2p_signal_start();
if (p2p_signal_connected() && list_elapsed(list.announced_time, ANNOUNCE_INTERVAL))
list_announce();
}
else if (list.announced)
{
list_withdraw();
}
/* listening while the menu asks */
if (list.watching && list_elapsed(list.watched_time, WATCH_TIME))
{
list.watching = 0;
memset(list.games, 0, sizeof(list.games));
p2p_signal_sync_topics();
}
else if (list.watching)
{
p2p_signal_start();
p2p_signal_sync_topics();
}
for (index = 0; index < MAXIMUM_LISTED_GAMES; index++)
{
if (list.games[index].used && list.games[index].announced_time < list_time_now() - STALE_SECONDS)
list.games[index].used = 0;
}
}
+48
View File
@@ -177,6 +177,11 @@ enum
_topic_own_slot,
_topic_query,
_topic_slots,
/* the VR port's: a hosted join code's, a looked up one's, the public
games' (p2p_code.c, p2p_list.c) */
_topic_code_host,
_topic_code_look,
_topic_list,
_topic_probe,
/* the topics a broker is subscribed to */
NUMBER_OF_TOPICS = _topic_probe + P2P_MAXIMUM_PROBES,
@@ -700,6 +705,9 @@ static void broker_sync_topics(struct broker *broker)
wanted[_topic_own_slot] = lobby && signalling.lobby_listed ? signalling.own_slot : "";
wanted[_topic_query] = lobby && signalling.lobby_listed ? P2P_LOBBY_QUERY_TOPIC : "";
wanted[_topic_slots] = lobby && signalling.lobby_browsing ? P2P_LOBBY_SLOT_PREFIX "+" : "";
/* (the VR port's join codes and public games: p2p_code.c, p2p_list.c) */
p2p_code_topics(&wanted[_topic_code_host], &wanted[_topic_code_look]);
wanted[_topic_list] = p2p_list_topic();
for (index = 0; index < P2P_MAXIMUM_PROBES; index++)
wanted[_topic_probe + index] = signalling.probes[index].join.active ? signalling.probes[index].topic : "";
for (index = 0; index < NUMBER_OF_TOPICS; index++)
@@ -1468,6 +1476,8 @@ static void publish_received(struct broker *broker, const char *topic, const uns
if (size > MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD)
return;
if (p2p_code_received(topic, payload, size) || p2p_list_received(topic, payload, size))
return;
if (!strncmp(topic, P2P_LOBBY_SLOT_PREFIX, sizeof(P2P_LOBBY_SLOT_PREFIX) - 1))
{
p2p_lobby_slot_heard(topic + sizeof(P2P_LOBBY_SLOT_PREFIX) - 1, payload, size, retained);
@@ -1969,6 +1979,8 @@ void p2p_signal_update(const int *read, int read_count, const int *write, int wr
if (elapsed(join->sent_time, JOIN_INTERVAL))
send_join(join);
}
p2p_code_update();
p2p_list_update(signalling.hosting);
}
int p2p_signal_connected(void)
@@ -2008,6 +2020,42 @@ static void sync_all_topics(void)
broker_sync_topics(&signalling.brokers[index]);
}
void p2p_signal_sync_topics(void)
{
sync_all_topics();
}
void p2p_signal_publish(const char *topic, const unsigned char *payload, int size)
{
if (size <= MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD)
publish_everywhere(topic, payload, size);
}
void p2p_signal_publish_retained(const char *topic, const unsigned char *payload, int size)
{
unsigned char body[3 + TOPIC_SIZE + MAXIMUM_MESSAGE_SIZE];
int index;
if (size > MAXIMUM_MESSAGE_SIZE)
return;
for (index = 0; index < signalling.broker_count; index++)
{
struct broker *broker = &signalling.brokers[index];
int body_size;
if (broker->state != _broker_ready)
continue;
body_size = put_string(body, topic);
/* (MQTT 5: no properties) */
if (broker->protocol == 5)
body[body_size++] = 0;
if (size)
memcpy(body + body_size, payload, (size_t)size);
/* (PUBLISH, at most once, retained: an empty one clears it) */
broker_send(broker, 0x31, body, body_size + size);
}
}
void p2p_signal_host(const unsigned char *token)
{
if (!signalling.has_nonce_key)
+345 -7
View File
@@ -21,6 +21,7 @@ it exists, so that the player's edits and comments stay.
#include <SDL3/SDL.h>
#include <ctype.h>
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
@@ -215,6 +216,258 @@ static const struct config_setting config_settings[] =
"pixel by the lights the game gives them, without the facets the light\n"
"of each vertex shows across curved surfaces; false lights each vertex,\n"
"as the Xbox does." },
{ "display.shader_cache", _config_boolean, "true", "HALO_SHADER_CACHE", _environment_value, _platform_android,
"Keep the compiled shader programs in the shader_cache folder beside this\n"
"file, and make ready those a map drew with while it loads: fewer\n"
"stalls the first time something is drawn. false compiles each the first\n"
"time it is drawn. The folder may be deleted at any time." },
{ "display.shader_worker", _config_boolean, "true", "HALO_SHADER_WORKER", _environment_value, _platform_android,
"Make the shader programs on a thread of their own (with a GL context\n"
"sharing the game's), from the shader cache or compiled: a draw whose\n"
"program is not made yet is left out for the frames it takes, and the\n"
"frame does not wait. false makes each on the game's thread when it is\n"
"first drawn with. Read when the app starts." },
{ "vr.enabled", _config_boolean, "true", "HALO_VR_ENABLED", _environment_value, _platform_android,
"On a VR headset (Quest, Steam Frame), play in VR; false shows the game\n"
"on a flat panel instead. Read when the app starts." },
{ "vr.menus", _config_string, "\"pc\"", "HALO_VR_MENUS", _environment_value, _platform_android,
"The menus in VR: \"pc\" for the PC version's (as display.menus), \"xbox\"\n"
"for the Xbox's. Read when the menus' map loads." },
{ "vr.resolution_scale", _config_real, "1.0", "HALO_VR_RESOLUTION_SCALE", _environment_value, _platform_android,
"The eye images' size as a multiple of what the headset recommends;\n"
"lower is faster. Read when the app starts." },
{ "vr.refresh_rate", _config_real, "90.0", "HALO_VR_REFRESH_RATE", _environment_value, _platform_android,
"The display refresh rate to ask the headset for, in Hz (72, 90, 120);\n"
"it picks the nearest it has at or below. 0 for the highest." },
{ "vr.refresh_rate_auto", _config_boolean, "true", "HALO_VR_REFRESH_RATE_AUTO", _environment_value,
_platform_android,
"While frames come late at that rate (big online games), step down to the\n"
"next the headset has (90 to 72 Hz), and back up once they would keep up\n"
"again; the headset shows a late frame twice, a doubled picture when\n"
"turning. Only where the headset lets the game choose (not SteamVR)." },
{ "vr.graphics", _config_string, "\"medium\"", "HALO_VR_GRAPHICS", _environment_value, _platform_android,
"Which of the costlier effects are drawn (\"medium\" by default): \"auto\" all of them while the\n"
"GPU keeps up, leaving out one after another (the shield's glow, object\n"
"shadows, reflections, specular light, lens flares, bump maps, the rest\n"
"of \"low\") while it does not, and bringing them back when it does again\n"
"(the Quest; on the Steam Frame, which does not tell the GPU's time, as\n"
"\"high\"); \"high\" all of them; \"medium\"\n"
"no object shadows, environment reflections or lens flares; \"low\" nor\n"
"specular light, bump maps, grass, the screen's fog, contrails, weather\n"
"or camouflage's second pass (a client in another's game keeps grass,\n"
"lens flares and fog); \"max\" all of them, the models lit for each\n"
"pixel and shadows at 512x512." },
{ "vr.foveation", _config_string, "\"off\"", "HALO_VR_FOVEATION", _environment_value, _platform_android,
"Foveated rendering on the Quest (GL_QCOM_texture_foveated): the eyes'\n"
"pictures drawn at full detail around the lenses' centres and coarser\n"
"towards their edges, for less GPU time. \"off\", \"low\" or \"medium\"\n"
"(coarser edges, nearer the centre). The HUD and menus are not\n"
"foveated; a headset without it ignores it." },
{ "vr.positional_tracking", _config_boolean, "true", "HALO_VR_POSITIONAL", _environment_value, _platform_android,
"Moving your head moves the view (leaning, ducking); false only turns it.\n"
"The view recentres when play starts or resumes after a cinematic, and\n"
"when you recentre the headset (hold the Meta button)." },
{ "vr.lean_limit", _config_real, "0.35", "HALO_VR_LEAN_LIMIT", _environment_value, _platform_android,
"How far, in metres, the view follows your head sideways or forwards\n"
"away from the player's body (walls are not checked)." },
{ "vr.crouch_limit", _config_real, "0.9", "HALO_VR_CROUCH_LIMIT", _environment_value, _platform_android,
"How far, in metres, the view follows your head down." },
{ "vr.log_performance", _config_boolean, "false", "HALO_VR_LOG_PERFORMANCE", _environment_value, _platform_android,
"Log the frame rate and where the frame time goes every five seconds\n"
"(adb logcat -s halo)." },
{ "vr.aim_with", _config_string, "\"controller\"", "HALO_VR_AIM_WITH", _environment_value, _platform_android,
"\"controller\": the weapon is in your hand (vr.handedness) and aims where\n"
"it points, while you move and look with your head; \"head\": you aim\n"
"where you look." },
{ "vr.handedness", _config_string, "\"right\"", "HALO_VR_HANDEDNESS", _environment_value, _platform_android,
"The hand that holds the weapon: \"right\", or \"left\", which mirrors the\n"
"weapon and the arms, throws grenades and grips the foregrip with the\n"
"right hand, and fires with the left trigger." },
{ "vr.mirror_controls", _config_boolean, "true", "HALO_VR_MIRROR_CONTROLS", _environment_value,
_platform_android,
"Left-handed, the sticks and buttons swap sides too: the right stick moves\n"
"and the left one turns, X jumps and A reloads (on the Steam Frame the\n"
"D-pad takes A, B, X and Y as their mirror image); false keeps them as\n"
"right-handed. The menus keep their buttons either way." },
{ "vr.button_jump", _config_string, "\"a\"", "HALO_VR_BUTTON_JUMP", _environment_value, _platform_android,
"The button each of the game's actions is on (the VR options' BUTTONS\n"
"page swaps them): \"a\", \"b\", \"x\", \"y\" (A and B on the right\n"
"controller, X and Y the Quest's on the left; the Steam Frame has all\n"
"four on the right), \"move_stick\" and \"turn_stick\" (their clicks),\n"
"\"grip\" (the weapon hand's), \"free_grip\" (the free hand's, held a\n"
"moment, when it took neither the weapon's foregrip nor the Warthog's\n"
"wheel), named as right-handed: left-handed with\n"
"mirror_controls, their mirror images. Jump," },
{ "vr.button_light", _config_string, "\"b\"", "HALO_VR_BUTTON_LIGHT", _environment_value, _platform_android,
"the flashlight (held: switch grenades)," },
{ "vr.button_action", _config_string, "\"x\"", "HALO_VR_BUTTON_ACTION", _environment_value, _platform_android,
"action and reload," },
{ "vr.button_weapon", _config_string, "\"y\"", "HALO_VR_BUTTON_WEAPON", _environment_value, _platform_android,
"switch weapon," },
{ "vr.button_crouch", _config_string, "\"move_stick\"", "HALO_VR_BUTTON_CROUCH", _environment_value,
_platform_android,
"crouch," },
{ "vr.button_zoom", _config_string, "\"turn_stick\"", "HALO_VR_BUTTON_ZOOM", _environment_value,
_platform_android,
"zoom" },
{ "vr.button_melee", _config_string, "\"grip\"", "HALO_VR_BUTTON_MELEE", _environment_value, _platform_android,
"melee" },
{ "vr.button_talk", _config_string, "\"free_grip\"", "HALO_VR_BUTTON_TALK", _environment_value,
_platform_android,
"and voice chat's push to talk (audio.voice_chat). The triggers and the\n"
"menu button keep theirs, as do the menus." },
{ "vr.laser", _config_boolean, "false", "HALO_VR_LASER", _environment_value, _platform_android,
"A laser from the weapon to what it aims at (with aim_with = \"controller\")." },
{ "vr.crosshair", _config_boolean, "true", "HALO_VR_CROSSHAIR", _environment_value, _platform_android,
"The weapon's crosshair where it aims, at the distance of what it aims at\n"
"(with aim_with = \"controller\"); false leaves it out." },
{ "vr.weapon_hand_forward", _config_real, "0.0", "HALO_VR_WEAPON_HAND_FORWARD", _environment_value,
_platform_android,
"The weapon in your hand: each one's model is held so that its hand is\n"
"where yours is, as the free hand's; this moves your hand on it, in\n"
"metres (weapon_hand_right: to its right, weapon_hand_down: down)." },
{ "vr.weapon_hand_right", _config_real, "0.0", "HALO_VR_WEAPON_HAND_RIGHT", _environment_value,
_platform_android,
"See weapon_hand_forward." },
{ "vr.weapon_hand_down", _config_real, "0.0", "HALO_VR_WEAPON_HAND_DOWN", _environment_value,
_platform_android,
"See weapon_hand_forward." },
{ "vr.two_hand_reach", _config_real, "0.2", "HALO_VR_TWO_HAND_REACH", _environment_value, _platform_android,
"How near, in metres, the free hand must be to the weapon's foregrip for\n"
"its grip button to take hold of it with both hands." },
{ "vr.body", _config_string, "\"arms\"", "HALO_VR_BODY", _environment_value, _platform_android,
"What of your body is drawn with the weapon: \"arms\", the arms and hands\n"
"reaching from the shoulders to the controllers; \"full\", the whole\n"
"body, its torso, hips and legs too, under the head and following the\n"
"physical crouch and lean, the feet stepping after it; \"hands\", the\n"
"hands alone; \"off\", neither (the weapon alone)." },
{ "vr.fingers", _config_boolean, "true", "HALO_VR_FINGERS", _environment_value, _platform_android,
"The free hand's fingers follow the controller: the index finger the\n"
"trigger, the others the grip, the thumb where it rests (on the Quest's\n"
"and the Steam Frame's controllers); false leaves the hand as the\n"
"weapon's animation has it." },
{ "vr.arm_ik", _config_boolean, "true", "HALO_VR_ARM_IK", _environment_value, _platform_android,
"The free hand's arm bends at the elbow to reach it from the shoulder;\n"
"false keeps the arm's shape as on the weapon, pointed at the shoulder." },
{ "vr.melee_swing_speed", _config_real, "2.0", "HALO_VR_MELEE_SWING", _environment_value, _platform_android,
"Thrusting a hand forward faster than this, in metres per second, is a\n"
"melee attack (the weapon hand's grip is too); 0 turns it off." },
{ "vr.grenade_throw_speed", _config_real, "1.5", "HALO_VR_GRENADE_THROW", _environment_value, _platform_android,
"A grenade is held while the free hand's trigger is, and thrown as it\n"
"lets go: along the hand's throw, when the hand moved faster than this\n"
"(metres per second; full strength at 4), else as the game throws it. 0\n"
"turns the hand's throw off." },
{ "vr.grenade_scale", _config_real, "0.55", "HALO_VR_GRENADE_SCALE", _environment_value, _platform_android,
"The size the grenade held in the free hand is drawn at (1 as in the game)." },
{ "vr.grenade_right", _config_real, "0.055", "HALO_VR_GRENADE_RIGHT", _environment_value, _platform_android,
"Where the held grenade is drawn, in metres from the free controller's\n"
"grip: towards the palm (the left hand's right),", },
{ "vr.grenade_forward", _config_real, "0.01", "HALO_VR_GRENADE_FORWARD", _environment_value, _platform_android,
"the grip's forward (through the fist, towards the index finger),", },
{ "vr.grenade_down", _config_real, "0.01", "HALO_VR_GRENADE_DOWN", _environment_value, _platform_android,
"down the drawn hand's knuckles, from the index finger to the little one,", },
{ "vr.grenade_back", _config_real, "0.02", "HALO_VR_GRENADE_BACK", _environment_value, _platform_android,
"and back along its fingers, towards the wrist." },
{ "vr.vehicle_first_person", _config_boolean, "true", "HALO_VR_VEHICLE_FIRST_PERSON", _environment_value,
_platform_android,
"In a vehicle, the view stays behind the player's own eyes, and turns\n"
"with the vehicle (false: the game's camera behind it, as on a screen)." },
{ "vr.turret_handles", _config_boolean, "true", "HALO_VR_TURRET_HANDLES", _environment_value,
_platform_android,
"At a vehicle's gun, the right controller works it as by its handles: tipped\n"
"down, the barrel rises (false: the barrel points where the controller does)." },
{ "vr.ghost_eye_raise", _config_real, "0.45", "HALO_VR_GHOST_EYE_RAISE", _environment_value, _platform_android,
"In a Ghost, whose hood arches over its rider, the eyes this many metres\n"
"above his head," },
{ "vr.banshee_eye_raise", _config_real, "1.0", "HALO_VR_BANSHEE_EYE_RAISE", _environment_value,
_platform_android,
"in a Banshee, whose pilot lies in it, this many," },
{ "vr.turret_eye_raise", _config_real, "0.5", "HALO_VR_TURRET_EYE_RAISE", _environment_value,
_platform_android,
"at a Covenant gun, whose shield is at the eyes, this many," },
{ "vr.tank_eye_raise", _config_real, "0.5", "HALO_VR_TANK_EYE_RAISE", _environment_value, _platform_android,
"and driving a Scorpion, whose driver sits in the hull, this many." },
{ "vr.driver_camera_forward", _config_real, "0.15", "HALO_VR_DRIVER_CAMERA_FORWARD", _environment_value,
_platform_android,
"At an open vehicle's wheel (a Warthog's), the view this many metres forward\n"
"of the driver's head, nearer the windscreen." },
{ "vr.gunner_camera_back", _config_real, "0.15", "HALO_VR_GUNNER_CAMERA_BACK", _environment_value,
_platform_android,
"At a vehicle's gun, the view this many metres back from where the\n"
"gunner's head leans into its sights." },
{ "vr.glass_strength", _config_real, "0.5", "HALO_VR_GLASS_STRENGTH", _environment_value, _platform_android,
"How strongly glass tints and reflects (1 as the game has it): up close, as\n"
"through a vehicle's windscreen, it hides the view." },
{ "vr.vehicle_steer_degrees", _config_real, "60", "HALO_VR_VEHICLE_STEER", _environment_value, _platform_android,
"Driving, the right stick pushed fully aside steers this many degrees off\n"
"the vehicle's heading (the game turns the vehicle towards it)." },
{ "vr.vehicle_gun_steer_degrees", _config_real, "40", "HALO_VR_VEHICLE_GUN_STEER", _environment_value,
_platform_android,
"The same for a vehicle whose steering aims its guns too (a Ghost, a Banshee)." },
{ "vr.vehicle_steer_curve", _config_real, "2.0", "HALO_VR_VEHICLE_STEER_CURVE", _environment_value,
_platform_android,
"Driving, the right stick's deflection to this power steers (1 evenly;\n"
"above it, finely near the middle, fully at the end)," },
{ "vr.vehicle_steer_smoothing", _config_real, "0.12", "HALO_VR_VEHICLE_STEER_SMOOTHING", _environment_value,
_platform_android,
"eased in over about this many seconds (0: at once)." },
{ "vr.vehicle_pitch_degrees", _config_real, "45", "HALO_VR_VEHICLE_PITCH", _environment_value, _platform_android,
"Driving, the right stick pushed fully up or down aims this many degrees\n"
"above or below the horizon (a Banshee climbs or dives)." },
{ "vr.vehicle_wheel", _config_boolean, "true", "HALO_VR_VEHICLE_WHEEL", _environment_value, _platform_android,
"At an open vehicle's wheel (a Warthog's), grip it with one hand or both and\n"
"turn it to steer, the right trigger the throttle and the left the brake;\n"
"let go, and the right stick steers." },
{ "vr.vehicle_wheel_degrees", _config_real, "90", "HALO_VR_VEHICLE_WHEEL_DEGREES", _environment_value,
_platform_android,
"The wheel turned this many degrees either way steers fully." },
{ "vr.comfort_vignette", _config_real, "0.7", "HALO_VR_COMFORT_VIGNETTE", _environment_value, _platform_android,
"How far the view darkens from its edges while a vehicle turns you\n"
"(0 never, 1 to a small window)." },
{ "vr.foot_vignette", _config_string, "\"off\"", "HALO_VR_FOOT_VIGNETTE", _environment_value, _platform_android,
"On foot, the view darkens from its edges too: \"turning\" while the stick\n"
"turns you smoothly (smooth_turn), \"moving\" while a stick turns or moves\n"
"you, as far as it is pushed; \"off\" never." },
{ "vr.foot_vignette_strength", _config_real, "0.7", "HALO_VR_FOOT_VIGNETTE_STRENGTH", _environment_value,
_platform_android,
"How far it darkens then (as comfort_vignette)." },
{ "vr.motion_tracker_on_wrist", _config_boolean, "true", "HALO_VR_MOTION_TRACKER_WRIST", _environment_value,
_platform_android,
"Show the motion tracker on the back of the left wrist, like a watch (turn\n"
"it towards you to read it), instead of in the HUD." },
{ "vr.motion_tracker_size", _config_real, "0.11", "HALO_VR_MOTION_TRACKER_SIZE", _environment_value, _platform_android,
"How wide the motion tracker on the wrist is, in metres." },
{ "vr.motion_tracker_turn", _config_real, "32.0", "HALO_VR_MOTION_TRACKER_TURN", _environment_value, _platform_android,
"Turns the motion tracker on the wrist, in degrees clockwise as you read it." },
{ "vr.motion_tracker_right", _config_real, "0.015", "HALO_VR_MOTION_TRACKER_RIGHT", _environment_value,
_platform_android,
"Moves the motion tracker on the wrist right as you read it, in metres." },
{ "vr.motion_tracker_down", _config_real, "0.055", "HALO_VR_MOTION_TRACKER_DOWN", _environment_value,
_platform_android,
"Moves the motion tracker on the wrist down as you read it, in metres." },
{ "vr.scope_size", _config_real, "0.10", "HALO_VR_SCOPE_SIZE", _environment_value, _platform_android,
"Zoomed in, the scope's view shows on a disc over the weapon this wide, in\n"
"metres; 0 turns it off." },
{ "vr.smooth_turn", _config_boolean, "false", "HALO_VR_SMOOTH_TURN", _environment_value, _platform_android,
"The right stick turns smoothly; false turns in steps (snap_turn_degrees)." },
{ "vr.snap_turn_degrees", _config_real, "30.0", "HALO_VR_SNAP_TURN", _environment_value, _platform_android,
"How far a flick of the right stick turns, in degrees (the VR options step\n"
"through 10 to 45 by 5, 60, 75 and 90)." },
{ "vr.smooth_turn_speed", _config_real, "120.0", "HALO_VR_SMOOTH_TURN_SPEED", _environment_value, _platform_android,
"Degrees per second the right stick turns at, fully pushed, with smooth_turn\n"
"(the VR options step through 30 to 150 by 15, then to 300 by 30)." },
{ "vr.cinematics_in_3d", _config_boolean, "false", "HALO_VR_CINEMATICS_3D", _environment_value, _platform_android,
"Show cinematics around you, from the cinematic camera; false shows them\n"
"on the screen in front of you (easier on the stomach)." },
{ "vr.hud_distance", _config_real, "1.5", "HALO_VR_HUD_DISTANCE", _environment_value, _platform_android,
"How far in front of the eyes the HUD floats, in metres." },
{ "vr.hud_field_of_view", _config_real, "62.0", "HALO_VR_HUD_FOV", _environment_value, _platform_android,
"How wide the HUD is, in degrees across." },
{ "vr.screen_distance", _config_real, "2.5", "HALO_VR_SCREEN_DISTANCE", _environment_value, _platform_android,
"How far away the screen for menus, loading and cinematics stands, in metres." },
{ "vr.screen_width", _config_real, "3.2", "HALO_VR_SCREEN_WIDTH", _environment_value, _platform_android,
"That screen's width in metres (its height is three quarters of it)." },
{ "display.structure_culling", _config_integer, "31", "HALO_STRUCTURE_CULLING", _environment_value,
_platform_all,
"Which tests skip the level's triangles the camera cannot see, as bits\n"
@@ -420,14 +673,16 @@ static const struct config_setting config_settings[] =
"as CUSTOM SINGLEPLAYER and CUSTOM MULTIPLAYER. Their tags are checked\n"
"as the game's own maps' are before they run; false refuses them\n"
"(docs/custom_edition_caches.md)." },
{ "bots.count", _config_integer, "0", "HALO_BOTS", _environment_value, _platform_all,
/* (the desktop's local bots, bots.c; Mjolnir VR's host bots have their own
bots.count and bots.skill, Android's, below: host_bots.c) */
{ "bots.count", _config_integer, "0", "HALO_BOTS", _environment_value, _platform_desktop,
"Computer players (bots) in a local (split screen) Slayer game: 0 to 15\n"
"join its lobby with the players, as far as the game has room. Not in\n"
"system link or internet games. 0: none, as in the stock game." },
{ "bots.skill", _config_string, "\"normal\"", "HALO_BOT_SKILL", _environment_value, _platform_all,
{ "bots.skill", _config_string, "\"normal\"", "HALO_BOT_SKILL", _environment_value, _platform_desktop,
"How well the bots play: \"easy\", \"normal\", \"heroic\" or \"legendary\"\n"
"(how soon they react, how well they aim, how far they see)." },
{ "bots.teams", _config_string, "\"even\"", "HALO_BOT_TEAMS", _environment_value, _platform_all,
{ "bots.teams", _config_string, "\"even\"", "HALO_BOT_TEAMS", _environment_value, _platform_desktop,
"In team games: \"even\" puts the bots on both teams, evening them with\n"
"the players; \"against\" puts them all on the team the players are not on." },
{ "display.viewmodel_shield", _config_boolean, "false", "HALO_VIEWMODEL_SHIELD", _environment_value, _platform_all,
@@ -505,6 +760,9 @@ static const struct config_setting config_settings[] =
"clipboard) that lets whoever has it join over the internet; opening a\n"
"link (or copying one before switching to the game) joins. Only people\n"
"with the invite can join. Off keeps system link to the local network." },
{ "network.public_game", _config_boolean, "false", "HALO_NET_PUBLIC_GAME", _environment_value, _platform_all,
"Host games as public: listed for anyone among the online games (the\n"
"system link screen's X), as Y in a hosted game's lobby makes them." },
{ "network.join_from_clipboard", _config_boolean, "true", "HALO_NET_JOIN_FROM_CLIPBOARD", _environment_value,
_platform_all,
"Join the game of an invite link found on the clipboard when the game\n"
@@ -705,6 +963,12 @@ static const struct config_setting config_settings[] =
"The Discord application internet play invites go through while the\n"
"Discord desktop client runs; empty for none." },
{ "bots.count", _config_integer, "0", "HALO_BOTS_COUNT", _environment_value, _platform_android,
"Bots in the multiplayer games this machine hosts (0 to 15; 0: none):\n"
"players it drives itself, which every machine sees as players. Set in\n"
"the lobby's host options (Y)." },
{ "bots.skill", _config_integer, "1", "HALO_BOTS_SKILL", _environment_value, _platform_android,
"How well the bots play: 0 easy, 1 normal, 2 hard." },
{ "update.auto", _config_boolean, "true", "HALO_UPDATE_AUTO", _environment_value, _platform_all,
"Look for a new version when the game starts, and offer to update to it;\n"
"false never looks (the game's \"Do not ask again\" writes false here)." },
@@ -967,6 +1231,47 @@ static const struct config_setting config_settings[] =
"machines of twice it), to test the netcode as over the internet; 0 none." },
{ "debug.network_loss", _config_real, "0.0", "HALO_NETWORK_LOSS", _environment_value, _platform_all,
"Percent of datagrams received that are dropped, for the same; 0 none." },
{ "debug.multiview", _config_boolean, "false", "HALO_MULTIVIEW", _environment_value, _platform_android,
"Draw both eyes in one pass (GL_OVR_multiview2), from between them: about\n"
"half the drawing's cost on the processor, but the eyes' pictures did not\n"
"always fuse (doubled as the head turned: on the Steam Frame now and then,\n"
"on the Quest 3 in big online games). Off, one eye after the other; on,\n"
"for finding why." },
{ "debug.gl_thread", _config_integer, "-1", "HALO_GL_THREAD", _environment_value, _platform_android,
"Mesa's GL thread (its drivers: the Steam Frame's): 1 on, 0 off, -1 as the\n"
"driver chooses. Read when the app starts." },
{ "debug.xr_deferred_copies", _config_integer, "-1", "HALO_XR_DEFERRED", _environment_value, _platform_android,
"The headset's layer copies: 1 deferred (no waits for the GL thread or the\n"
"GPU in the middle of the frame), 0 each one restored and released at once,\n"
"-1 as the runtime needs (deferred on SteamVR). Read when the app starts." },
{ "debug.vr_graphics_measure", _config_boolean, "false", "HALO_VR_GRAPHICS_MEASURE", _environment_value,
_platform_android,
"VR: leave each costlier effect out in turn (a second and a half drawn,\n"
"as long left out) and log what the GPU's time says it costs\n"
"(\"VR graphics measure\"); vr.graphics is ignored meanwhile." },
{ "debug.shader_warm_up", _config_string, "\"worker\"", "HALO_SHADER_WARM_UP", _environment_value,
_platform_android,
"Each shader program made on the worker (display.shader_worker) is drawn\n"
"with once there, into a small target of its own, so that the driver\n"
"makes what it makes at a first draw then and not in a frame: \"worker\";\n"
"\"load\" draws with a map's programs on the game's thread as the map\n"
"loads instead; \"off\" draws with none. Read when the app starts." },
{ "debug.vr_options", _config_boolean, "false", "HALO_DEBUG_VR_OPTIONS", _environment_value, _platform_android,
"Off the headset (the emulator), show the VR options panel on the main\n"
"and pause menus all the same, for checking its pages." },
{ "debug.mirror_first_person", _config_boolean, "false", "HALO_MIRROR_FIRST_PERSON", _environment_value,
_platform_android,
"Off the headset (flat), the first-person weapon drawn mirrored, as the\n"
"left hand's is in VR: a test aid for what reads mirrored there. Read once." },
{ "debug.vr_body", _config_string, "\"off\"", "HALO_DEBUG_VR_BODY", _environment_value, _platform_android,
"Off the headset (flat), the player's own body drawn as vr.body \"full\"\n"
"draws it, posed from the flat camera, a test aid: \"eye\" (seen from the\n"
"eyes), \"down\" (looking down, as in the headset), \"front\", \"side\" or\n"
"\"back\" (the camera moved to look at it); after a colon how the head\n"
"moves: \"cycle\" (turning, crouching, leaning, five seconds each; the\n"
"default), \"still\", \"turn\", \"crouch\", \"lean\", or \"raw\" (the game's\n"
"own pose, only the head and arms shrunk). \"off\" (default): none. Read\n"
"once." },
{ "debug.network_corrupt", _config_real, "0.0", "HALO_NETWORK_CORRUPT", _environment_value, _platform_all,
"Percent of the datagrams received that are damaged at random, to test\n"
"that nothing a machine sends can crash the game; 0 none." },
@@ -1039,6 +1344,11 @@ static const struct config_setting config_settings[] =
"Log texture uploads." },
{ "debug.texture_no_cache", _config_boolean, "false", "HALO_TEXTURE_NO_CACHE", _environment_set_is_true, _platform_all,
"Upload textures again every time they are used." },
{ "debug.vehicle_spawn", _config_boolean, "true", "HALO_VEHICLE_SPAWN", _environment_value, _platform_android,
"A test aid in VR: both thumbsticks held in for a second and a half put\n"
"the map's next vehicle a few metres ahead." },
{ "debug.audio_log", _config_boolean, "false", "HALO_AUDIO_LOG", _environment_set_is_true, _platform_all,
"Log the level of the mixed sound every five seconds." },
{ "debug.sample_seconds", _config_real, "0.0", "HALO_SAMPLE", _environment_value, _platform_android,
"Log where every game thread is this often, in seconds (read by the\n"
"app, port/android/host/host_debug.c); 0 never." },
@@ -1198,13 +1508,24 @@ static char *config_read_file(const char *path, size_t *size)
static int config_write_file(const char *path, const char *text)
{
#ifdef HALO_ARM64_GUEST
FILE *file = fopen(path, "wb");
/* written beside it and renamed over it: a file the app may not write
(one pushed by adb belongs to the shell) is replaced all the same, as the
folder is the app's, and a write cut short leaves the old file whole */
char temporary[1100];
FILE *file;
int written;
snprintf(temporary, sizeof(temporary), "%s.tmp", path);
file = fopen(temporary, "wb");
if (!file)
return 0;
written = fwrite(text, 1, strlen(text), file) == strlen(text);
return fclose(file) == 0 && written;
written = fclose(file) == 0 && written;
if (written && rename(temporary, path) == 0)
return 1;
platform_log("settings: cannot replace %s (%s)", path, strerror(errno));
remove(temporary);
return 0;
#elif !defined(HALO_ANDROID) && !defined(HALO_WEB)
/* (a dedicated server's holds its passwords: its user's to read alone) */
if (!SDL_SaveFile(path, text, strlen(text)))
@@ -1611,13 +1932,21 @@ static volatile unsigned long config_change_count;
static long config_setting_index(const char *name)
{
size_t index;
long other = -1;
/* (port: this platform's, where two of a name are each another's: the
bots') */
for (index = 0; index < NUMBER_OF_CONFIG_SETTINGS; index++)
{
if (!strcmp(config_settings[index].name, name))
return (long)index;
{
if (config_settings[index].platforms & CONFIG_PLATFORM)
return (long)index;
if (other < 0)
other = (long)index;
}
}
return -1;
return other;
}
/* a key in the file that is no setting, likely misspelt or in the wrong
@@ -2461,6 +2790,15 @@ void config_folder(char *path, size_t size)
snprintf(path, size, "%s", file);
}
int config_write_integer(const char *name, long value)
{
long index = config_setting_index(name);
char text[32];
snprintf(text, sizeof(text), "%ld", value);
return index >= 0 && config_settings[index].type == _config_integer && config_write(name, text);
}
/* ---------- public code */
char *config_file_read(const char *path, size_t *size)
+1
View File
@@ -24,6 +24,7 @@ const char *config_string(const char *name);
writes it into config.toml (only its line changes); 1 on success */
int config_write(const char *name, const char *value);
int config_write_boolean(const char *name, int value);
int config_write_integer(const char *name, long value);
/* a setting's value as text ("true", "60", "1.5", "all"); 0 if there is no
such setting */
int config_text(const char *name, char *text, size_t size);
+68 -5
View File
@@ -11,6 +11,7 @@ and the debug keyboard that the game's console reads.
#include "platform.h"
#include "sdl_platform.h"
#include "halo_vr.h"
#include "gl.h"
#include "port_config.h"
#include "p2p.h"
@@ -1287,6 +1288,11 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height)
(void)version;
platform_event_thread = SDL_GetCurrentThreadID();
platform_log("OpenGL %s on %s", (const char *)glGetString(GL_VERSION), (const char *)glGetString(GL_RENDERER));
/* on a headset, its session on this context (vr.c); the headset then
paces the frames, and the window is not shown */
halo_vr_initialize();
if (halo_vr_active())
SDL_GL_SetSwapInterval(0);
#if defined(__linux__) && !defined(HALO_ANDROID)
{
const char *renderer = (const char *)glGetString(GL_RENDERER);
@@ -1659,6 +1665,37 @@ static BOOL platform_text_has_invite_link(const char *text)
return FALSE;
}
/* the last clipboard text looked at, kept beside the settings from run to
run (the VR port's): a run's own invite, still on the clipboard when the
game starts again, is not joined then (its host, the run before, is gone,
and the attempt held up a join for a minute and a half) */
static void platform_clipboard_seen(char *seen, int size, BOOL save)
{
char path[512];
FILE *file;
snprintf(path, sizeof(path), "%s/clipboard_seen.txt", platform_data_root());
if (save)
{
file = fopen(path, "wb");
if (file)
{
fputs(seen, file);
fclose(file);
}
return;
}
file = fopen(path, "rb");
if (!file)
return;
{
size_t length = fread(seen, 1, (size_t)size - 1, file);
seen[length] = 0;
}
fclose(file);
}
/* the clipboard's text (the menus' text fields' Ctrl+V), and text put on it
(the server settings' invite link); 0 if there is none. The main thread's */
int platform_clipboard_get(char *text, int size)
@@ -1682,8 +1719,14 @@ static void platform_invite_clipboard(BOOL look)
{
/* the last clipboard text looked at, so each invite is joined once */
static char seen[256];
static BOOL seen_loaded;
const char *invite = p2p_take_clipboard_text();
if (!seen_loaded)
{
seen_loaded = TRUE;
platform_clipboard_seen(seen, sizeof(seen), FALSE);
}
if (invite)
{
#ifdef HALO_WEB
@@ -1693,6 +1736,7 @@ static void platform_invite_clipboard(BOOL look)
#else
SDL_SetClipboardText(invite);
snprintf(seen, sizeof(seen), "%s", invite);
platform_clipboard_seen(seen, sizeof(seen), TRUE);
platform_log("Internet play: the invite link is on the clipboard");
#endif
#ifdef HALO_ANDROID
@@ -1706,6 +1750,7 @@ static void platform_invite_clipboard(BOOL look)
if (text && strcmp(text, seen) && strlen(text) < sizeof(seen))
{
snprintf(seen, sizeof(seen), "%s", text);
platform_clipboard_seen(seen, sizeof(seen), TRUE);
/* (a link, not a bare code: 64 hex digits alone are as often a
checksum copied for something else) */
if (platform_text_has_invite_link(text) && p2p_join_invite(text))
@@ -1852,18 +1897,33 @@ static void platform_show_pending_message(void)
/* ---------- events */
/* quits as closing the window does, when the events are next read (the
menus' Quit: port/linux/game/menu_functions.c); on Android at once */
menus' Quit: port/linux/game/menu_functions.c); on Android at once, as
the system closes its apps: the VR options' Quit (ui_widget.c) too, as on
the Steam Frame Steam cannot end the game, which runs in Lepton's container
(port/android/STEAM_FRAME.md) */
void platform_request_quit(void)
{
#ifdef HALO_ANDROID
/* (the guest has no SDL_PushEvent: exit ends the process, host_exit) */
exit(EXIT_SUCCESS);
#else
#ifndef HALO_ANDROID
SDL_Event event;
memset(&event, 0, sizeof(event));
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
#else
/* (the guest has no SDL_PushEvent: exit ends the process, host_exit) */
{
/* (and says so beside the game's data: the Steam Frame's launcher
does not then take it for a start that failed and start it again) */
char path[512];
FILE *file;
snprintf(path, sizeof(path), "%s/quit_requested.txt", platform_data_root());
file = fopen(path, "wb");
if (file)
fclose(file);
}
platform_log("quit: the player asked");
exit(EXIT_SUCCESS);
#endif
}
@@ -2028,6 +2088,9 @@ void platform_pump_events(void)
}
if (SDL_GetCurrentThreadID() != platform_event_thread)
return;
/* VR: the next frame starts here, after the last was presented and
before the game reads its input (vr.c) */
halo_vr_frame_begin();
exit_after_check();
platform_show_pending_message();
#if !defined(HALO_ANDROID) && !defined(HALO_GLES)
+2 -1
View File
@@ -46,7 +46,8 @@ update.h's: posix_update.c on Linux, win32_update.c on Windows.
#define HALO_BUILD_FLAVOR "release"
#endif
#define UPDATE_REPOSITORY "OpenCommunityEdition/OpenCE"
/* the VR port's own releases (the original's must not replace it) */
#define UPDATE_REPOSITORY "Cesarus85/mjolnir-vr"
#ifdef _WIN32
#define UPDATE_PLATFORM "windows"
#define PATH_SEPARATOR "\\"
+3878
View File
File diff suppressed because it is too large. Load diff
+48
View File
@@ -927,9 +927,57 @@ void halo_custom_edition_texels_forget(void)
custom_edition_texel_capacity = 0;
}
/* the textures uploaded, the menus' pictures decoded and uploaded and the
render targets made since xgpu_texture_work_take last asked, and in how
long (vr.c's long frames: a menu screen's first frame uploads its pictures) */
static struct
{
unsigned long counts[XGPU_TEXTURE_WORK_KINDS];
unsigned long texels;
unsigned long long microseconds;
} texture_work;
void xgpu_texture_work_add(int kind, unsigned long texels, unsigned long long start_us)
{
if (kind < 0 || kind >= XGPU_TEXTURE_WORK_KINDS)
return;
texture_work.counts[kind]++;
texture_work.texels += texels;
texture_work.microseconds += xgpu_shader_work_now_us() - start_us;
}
void xgpu_texture_work_take(unsigned long *uploads, unsigned long *kilotexels, unsigned long *decoded,
unsigned long *pictures, unsigned long *targets, float *milliseconds)
{
*uploads = texture_work.counts[XGPU_TEXTURE_WORK_UPLOAD] + texture_work.counts[XGPU_TEXTURE_WORK_DECODED];
*kilotexels = texture_work.texels / 1000;
*decoded = texture_work.counts[XGPU_TEXTURE_WORK_DECODED];
*pictures = texture_work.counts[XGPU_TEXTURE_WORK_PICTURE];
*targets = texture_work.counts[XGPU_TEXTURE_WORK_TARGET];
*milliseconds = texture_work.microseconds * 1e-3f;
memset(&texture_work, 0, sizeof(texture_work));
}
static void upload_texels(GLuint texture, GLenum target, const struct xgpu_texture_description *description,
const unsigned char *base, const D3DCOLOR *palette, unsigned char channel_order);
static void upload(GLuint texture, GLenum target, const struct xgpu_texture_description *description,
const unsigned char *base, const D3DCOLOR *palette, unsigned char channel_order)
{
unsigned long long start = xgpu_shader_work_now_us();
int kind = XGPU_TEXTURE_WORK_UPLOAD;
#ifdef HALO_GLES
if (description->compressed && !xgpu_capabilities.s3tc)
kind = XGPU_TEXTURE_WORK_DECODED;
#endif
upload_texels(texture, target, description, base, palette, channel_order);
xgpu_texture_work_add(kind, description->width * description->height * description->depth *
(description->cube_map ? 6 : 1), start);
}
static void upload_texels(GLuint texture, GLenum target, const struct xgpu_texture_description *description,
const unsigned char *base, const D3DCOLOR *palette, unsigned char channel_order)
{
struct format_information information = format_information(description->format);
unsigned long face_count = description->cube_map ? 6 : 1;
+30
View File
@@ -42,10 +42,40 @@ DWORD WINAPI XLaunchNewImageA(LPCSTR image_path, PLAUNCH_DATA launch_data)
exit(EXIT_SUCCESS);
}
/* port: a European disc in two languages has a maps folder for each, and
the Android and Steam Frame disc image setups (XisoExtractor.java,
steam_frame_xiso.py) write the language of the one they took into
maps/language.txt: the Xbox's language, unless game.language says another */
static const char *maps_language(void)
{
static char language[4];
static BOOL read;
if (!read)
{
char path[MAX_PATH];
FILE *file;
read = TRUE;
snprintf(path, sizeof(path), "%s/maps/language.txt", platform_data_root());
if ((file = fopen(path, "r")) != NULL)
{
if (!fgets(language, sizeof(language), file))
language[0] = '\0';
language[2] = '\0';
fclose(file);
}
}
return language;
}
DWORD WINAPI XGetLanguage(void)
{
const char *language = config_string("game.language");
if (!*language)
language = maps_language();
if (*language)
{
if (!strncmp(language, "ja", 2)) return XC_LANGUAGE_JAPANESE;
+71 -1
View File
@@ -33,14 +33,27 @@ struct xgpu_capabilities
BOOL color_buffer_half_float;
/* "300 es" or "310 es" */
const char *shading_language;
/* GL_OVR_multiview2: both eyes in one draw (VR, d3d8_multiview_begin) */
BOOL multiview;
/* the app's (HALO_ANDROID): GL_QCOM_texture_foveated, for the eyes'
targets (d3d8_foveation_parameters); GL_KHR_parallel_shader_compile,
and the program binary formats the driver has (xgpu_program_cache.c) */
BOOL foveation;
BOOL parallel_compile;
int program_binary_formats;
};
extern struct xgpu_capabilities xgpu_capabilities;
/* port/android/guest/runtime/guest_host.h */
int host_gl_has_extension(const char *name);
unsigned int host_gl_read_buffer_word(unsigned int buffer, unsigned int offset);
void host_gl_counters_snapshot(unsigned int buffer, unsigned int size, unsigned int slot);
int host_gl_counters_read(unsigned int slot, void *out, unsigned int size);
void host_gl_read_buffer(unsigned int buffer, unsigned int offset, unsigned int size, void *data);
void host_gl_buffer_write(unsigned int target, unsigned int offset, unsigned int size, const void *data);
int host_gl_buffer_storage(unsigned int target, unsigned int size);
int host_gl_persistent_write(unsigned int buffer, unsigned int offset, unsigned int size, const void *data);
void host_gl_fence_frame(unsigned int slot);
void host_gl_wait_frame(unsigned int slot);
#endif
@@ -63,6 +76,61 @@ void xgpu_gl_texture_deleted(unsigned int texture);
written */
GLuint xgpu_compile_shader(GLenum type, const char *code, const char *what);
GLuint xgpu_link_program(GLuint vertex_shader, GLuint fragment_shader, const char *what);
/* the time a shader's compile, a program's link or its load from the
cache took, added to the frame's (xgpu_shader_work_take: vr.c's long
frames) */
void xgpu_shader_work_add(BOOL loaded, unsigned long long start_us);
unsigned long long xgpu_shader_work_now_us(void);
/* the same for a texture's upload (with its texels), one decoded from DXT
first (GPUs without S3TC), a menu picture's (a PNG decoded) or a render
target's making (xbox_textures.c: xgpu_texture_work_take) */
enum
{
XGPU_TEXTURE_WORK_UPLOAD,
XGPU_TEXTURE_WORK_DECODED,
XGPU_TEXTURE_WORK_PICTURE,
XGPU_TEXTURE_WORK_TARGET,
XGPU_TEXTURE_WORK_KINDS
};
void xgpu_texture_work_add(int kind, unsigned long texels, unsigned long long start_us);
#ifdef HALO_ANDROID
/* ---------- the program binary cache (xgpu_program_cache.c)
Linked programs kept in the data folder's shader_cache, by their sources'
hash (xgpu_program_key), with the driver they were linked by; and for each
map the programs it used, loaded or compiled again as it loads. */
void xgpu_program_cache_initialize(void);
/* whether programs go through the cache (display.shader_cache, and a driver
with binary formats) */
BOOL xgpu_program_cache_enabled(void);
unsigned long long xgpu_program_key(unsigned long long vertex_hash, unsigned long long fragment_hash);
unsigned long long xgpu_source_hash(const char *source);
/* the program of these sources: made (*program), being made (a draw that
needs it is left out for now), or to make here (XGPU_PROGRAM_NONE) */
enum
{
XGPU_PROGRAM_NONE,
XGPU_PROGRAM_READY,
XGPU_PROGRAM_WAITING,
};
int xgpu_program_cache_take(unsigned long long key, const char *vertex_source, const char *fragment_source,
GLuint *program);
/* a program just linked from these sources, written to the cache */
void xgpu_program_cache_store(unsigned long long key, GLuint program, const char *vertex_source,
const char *fragment_source);
/* a program the map drew with (the map's list); asked again of each
program as xgpu_program_map_serial changes (a new map) */
void xgpu_program_cache_used(unsigned long long key);
extern unsigned long xgpu_program_map_serial;
/* each frame: other maps' programs asked for, the worker's results, the
lists */
void xgpu_program_cache_frame(void);
#endif
/* the headset's copy, or anything else, left GL as it drew: all set again
(d3d8_gl.c) */
void d3d8_host_state_changed(void);
/* ---------- generated source text */
@@ -106,8 +174,10 @@ NORMPACKED3 32-bit integers and unpacked in the shader. With lighting (else
NULL), the normal and world position go to the pixel shader too, which
lights the diffuse color for each pixel (nv2a_pixel_shader_key
per_pixel_lighting). Returns a malloc'd string. */
/* multiview: for a two-view (GL_OVR_multiview2) target, each view's
position corrected from the shared one by mv_rows (d3d8_gl.c, VR) */
char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instruction_count,
unsigned long packed_attribute_mask, const struct nv2a_vertex_lighting *lighting);
unsigned long packed_attribute_mask, const struct nv2a_vertex_lighting *lighting, int multiview);
/* the fixed-function pipeline's vertex shader (the loading screen's draws):
positions already in clip space. Returns a malloc'd string. */
char *nv2a_fixed_function_glsl(void);
File diff suppressed because it is too large. Load diff
+226 -6
View File
@@ -40,6 +40,7 @@ drive the controller.
#include "platform.h"
#include "sdl_platform.h"
#include "port_config.h"
#include "halo_vr.h"
#include "touch_input.h"
/* port/linux/game/network_aim_assist.c */
extern unsigned char network_aim_assist_block_mouse(void);
@@ -219,7 +220,8 @@ int halo_chat_taps(float *x, float *y)
int halo_chat_touch_layout(void)
{
#if defined(HALO_ANDROID)
return TRUE;
/* (port: not in VR, which has no touch controls in the way) */
return !halo_vr_active();
#elif defined(HALO_WEB)
int used;
@@ -279,7 +281,8 @@ int halo_linux_touch_move(short controller_index, float *forward, float *strafe)
if (controller_index != 0)
return FALSE;
#if defined(HALO_ANDROID) || defined(HALO_WEB)
if (!touch_input_move(forward, strafe) || network_chat_keyboard_open())
/* (port: not in VR, where the touch controls are off) */
if (halo_vr_active() || !touch_input_move(forward, strafe) || network_chat_keyboard_open())
{
*forward = 0.0f;
*strafe = 0.0f;
@@ -306,6 +309,9 @@ int halo_linux_touch_look(short gamepad_index, float *yaw, float *pitch, float *
if (gamepad_index != 0)
return FALSE;
#if defined(HALO_ANDROID) || defined(HALO_WEB)
/* (port: not in VR, where the touch controls are off) */
if (halo_vr_active())
return FALSE;
touch_input_look(0.0022f, yaw, pitch, gyro_yaw, gyro_pitch);
/* (none while the chat's on-screen keyboard is up: what was swiped is
dropped, not saved for after) */
@@ -772,6 +778,11 @@ int halo_push_to_talk_held(void)
struct platform_input_state input;
int slot;
#ifdef HALO_ANDROID
/* (port: in VR its button too, vr.button_talk: halo_vr_push_to_talk_held) */
if (halo_vr_active() && halo_vr_push_to_talk_held())
return 1;
#endif
/* (the window losing the focus lets every key go: sdl_platform.c) */
bindings_read();
platform_input_read(&input, FALSE);
@@ -1251,6 +1262,192 @@ static int controller_port(HANDLE device)
return -1;
}
#ifdef HALO_ANDROID
/* ---------- VR controllers (vr.c)
In play, laid out for VR shooters rather than by position:
right trigger fire left trigger grenade: held in the hand, thrown as it lets go (vr.c)
right grip melee (B) left grip hold the weapon with both hands (vr.c)
(a hand thrust forward, vr.melee_swing_speed, is a melee attack too)
A jump X action, reload (X), as the game's prompts say
B flashlight (white); held, switch grenade (black)
Y switch weapon (Y)
right stick turns (vr.c); click zooms left stick move; click crouches
menu pause (start)
Holding a Warthog's wheel (vr.c), the right trigger is the throttle and the
left one the brake (backwards, once stopped): the left stick pushed up or
down as far, unless it is pushed further.
Left-handed (vr.handedness), vr.c hands the controllers over with the sides
swapped where the hands' jobs are (halo_vr_controllers): the left trigger
fires and the left grip strikes, the right ones hold and throw the grenade
and grip the foregrip; and with vr.mirror_controls the sticks, bumpers and
face buttons too (the right stick moves, X jumps, A reloads; on the Steam
Frame the left D-pad takes A, B, X and Y). Here "right" and "left" are the
sides so handed over.
The VR options' BUTTONS page (vr.button_*) moves jump, the flashlight,
action, switch weapon, crouch, zoom and melee between A, B, X, Y, the
stick clicks and the weapon hand's grip: vr.c hands each action's button
over as the one above, so "A" here is the jump wherever it is.
In the menus the buttons are the Xbox's by position: A accepts, B goes back. */
#include "guest_host.h"
#include "halo_xr_abi.h"
int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menus);
int halo_vr_wheel_held(void);
/* a click that stands for the pad's A (ui_widget.c: the screen where
controllers join by pressing it), held a moment */
static Uint64 vr_pad_a_until;
void halo_vr_pulse_pad_a(void)
{
vr_pad_a_until = SDL_GetTicks() + 120;
}
static SHORT vr_stick(float value)
{
if (value > 1.0f)
value = 1.0f;
if (value < -1.0f)
value = -1.0f;
return (SHORT)(value * 32767.0f);
}
static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
{
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
const struct halo_xr_hand *left = &hands[HALO_XR_HAND_LEFT];
const struct halo_xr_hand *right = &hands[HALO_XR_HAND_RIGHT];
int menus = 0;
SHORT value;
if (!halo_vr_controllers(hands, &menus))
return;
value = vr_stick(left->thumbstick[0]);
if (abs(value) > abs(pad->sThumbLX)) pad->sThumbLX = value;
value = vr_stick(left->thumbstick[1]);
if (abs(value) > abs(pad->sThumbLY)) pad->sThumbLY = value;
{
/* the menu button: a press pauses, as Start; held, the scores show
while it is, as Back (the Quest's controllers have no button of
their own for it). In the menus it is Start at once */
static Uint64 menu_pressed_at, menu_pulse_until;
static int menu_held_handled;
Uint64 now = SDL_GetTicks();
int down = (left->buttons & HALO_XR_BUTTON_MENU) != 0;
if (menus)
{
if (down)
pad->wButtons |= XINPUT_GAMEPAD_START;
menu_pressed_at = 0;
}
else if (down)
{
if (!menu_pressed_at)
{
menu_pressed_at = now;
menu_held_handled = 0;
}
else if (now - menu_pressed_at >= 400)
{
pad->wButtons |= XINPUT_GAMEPAD_BACK;
menu_held_handled = 1;
}
}
else if (menu_pressed_at)
{
if (!menu_held_handled)
menu_pulse_until = now + 100;
menu_pressed_at = 0;
}
if (now < menu_pulse_until)
pad->wButtons |= XINPUT_GAMEPAD_START;
}
/* (the Steam Frame's controllers: the left one's View button and D-pad) */
if (left->buttons & HALO_XR_BUTTON_VIEW) pad->wButtons |= XINPUT_GAMEPAD_BACK;
if (left->buttons & HALO_XR_BUTTON_DPAD_UP) pad->wButtons |= XINPUT_GAMEPAD_DPAD_UP;
if (left->buttons & HALO_XR_BUTTON_DPAD_DOWN) pad->wButtons |= XINPUT_GAMEPAD_DPAD_DOWN;
if (left->buttons & HALO_XR_BUTTON_DPAD_LEFT) pad->wButtons |= XINPUT_GAMEPAD_DPAD_LEFT;
if (left->buttons & HALO_XR_BUTTON_DPAD_RIGHT) pad->wButtons |= XINPUT_GAMEPAD_DPAD_RIGHT;
if (left->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_LEFT_THUMB;
if (right->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_RIGHT_THUMB;
/* (in the menus the triggers click the pointer: halo_vr_menu_pointer) */
if (!menus && (BYTE)(right->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER])
pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = (BYTE)(right->trigger * 255.0f);
if (!menus && (BYTE)(left->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER])
pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER] = (BYTE)(left->trigger * 255.0f);
if (!menus && halo_vr_wheel_held())
{
/* at the wheel: the triggers drive, and neither fire nor throw */
pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = 0;
pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER] = 0;
value = vr_stick(right->trigger - left->trigger);
if (abs(value) > abs(pad->sThumbLY)) pad->sThumbLY = value;
}
if (menus)
{
/* the right stick scrolls too; turning is off in the menus */
value = vr_stick(right->thumbstick[0]);
if (abs(value) > abs(pad->sThumbRX)) pad->sThumbRX = value;
value = vr_stick(right->thumbstick[1]);
if (abs(value) > abs(pad->sThumbRY)) pad->sThumbRY = value;
merge_button(pad, XINPUT_GAMEPAD_A, (right->buttons & HALO_XR_BUTTON_A) != 0 ||
SDL_GetTicks() < vr_pad_a_until);
merge_button(pad, XINPUT_GAMEPAD_B, (right->buttons & HALO_XR_BUTTON_B) != 0);
merge_button(pad, XINPUT_GAMEPAD_X, (left->buttons & HALO_XR_BUTTON_A) != 0);
merge_button(pad, XINPUT_GAMEPAD_Y, (left->buttons & HALO_XR_BUTTON_B) != 0);
return;
}
merge_button(pad, XINPUT_GAMEPAD_A, (right->buttons & HALO_XR_BUTTON_A) != 0);
merge_button(pad, XINPUT_GAMEPAD_X, (left->buttons & HALO_XR_BUTTON_A) != 0);
/* (the grips holding a Warthog's wheel are not a blow: vr.c) */
merge_button(pad, XINPUT_GAMEPAD_B, (right->buttons & HALO_XR_BUTTON_SQUEEZE) != 0 || halo_vr_melee());
merge_button(pad, XINPUT_GAMEPAD_Y, (left->buttons & HALO_XR_BUTTON_B) != 0);
/* (the Steam Frame's bumpers: the left switches grenades, the right is
the flashlight) */
merge_button(pad, XINPUT_GAMEPAD_BLACK, (left->buttons & HALO_XR_BUTTON_BUMPER) != 0);
merge_button(pad, XINPUT_GAMEPAD_WHITE, (right->buttons & HALO_XR_BUTTON_BUMPER) != 0);
{
/* B: a tap is the flashlight, holding it switches grenades; the
game sees a short press of either */
static Uint64 pressed_at, pulse_until;
static int pulse_button = -1, held_handled;
Uint64 now = SDL_GetTicks();
int down = (right->buttons & HALO_XR_BUTTON_B) != 0;
if (down && !pressed_at)
{
pressed_at = now;
held_handled = 0;
}
else if (down && !held_handled && now - pressed_at >= 500)
{
pulse_button = XINPUT_GAMEPAD_BLACK;
pulse_until = now + 100;
held_handled = 1;
}
else if (!down && pressed_at)
{
if (!held_handled)
{
pulse_button = XINPUT_GAMEPAD_WHITE;
pulse_until = now + 100;
}
pressed_at = 0;
}
if (pulse_button >= 0 && now < pulse_until)
merge_button(pad, pulse_button, TRUE);
}
}
#endif
/* reads a controller's state; for port 0 the keyboard, mouse, debug input
and touchscreen are merged into the first gamepad's; runs on the game's main
thread (touch_input_gamepad relies on it); returns ERROR_SUCCESS or
@@ -1290,11 +1487,21 @@ DWORD WINAPI XInputGetState(HANDLE device, PXINPUT_STATE state)
}
if (port_gamepad(gamepads, count, 0))
sdl_gamepad_state(gamepads[0], &state->Gamepad);
test_input_gamepad(&state->Gamepad);
touch_input_gamepad(&state->Gamepad);
#if defined(HALO_ANDROID) || defined(HALO_WEB)
touch_input_controls(&state->Gamepad, input.menus);
#ifdef HALO_ANDROID
vr_gamepad_state(&state->Gamepad);
#endif
test_input_gamepad(&state->Gamepad);
#ifdef HALO_ANDROID
/* port: in VR the touchscreen's controls neither show (the overlay
stays hidden until touch_input_controls reports the scene) nor act */
if (!halo_vr_active())
#endif
{
touch_input_gamepad(&state->Gamepad);
#if defined(HALO_ANDROID) || defined(HALO_WEB)
touch_input_controls(&state->Gamepad, input.menus);
#endif
}
/* (the text chat's on-screen keyboard reads them, and takes them
while it is up) */
chat_pad_record(&state->Gamepad);
@@ -1333,6 +1540,19 @@ DWORD WINAPI XInputSetState(HANDLE device, PXINPUT_FEEDBACK feedback)
if (port < 0)
return ERROR_DEVICE_NOT_CONNECTED;
#if defined(HALO_ANDROID) || defined(HALO_WEB)
#ifdef HALO_ANDROID
/* VR: the heavy (left) motor in the free hand, the light one in the
weapon's (left-handed, the other way round), as the game refreshes them
every frame */
if (port == 0 && halo_vr_active())
{
int weapon = halo_vr_weapon_hand();
host_xr_haptic(HALO_XR_HAND_COUNT - 1 - weapon, feedback->Rumble.wLeftMotorSpeed / 65535.0f, 0.1f);
host_xr_haptic(weapon, feedback->Rumble.wRightMotorSpeed / 65535.0f, 0.1f);
}
else
#endif
/* the phone vibrates for the touch controls' player (in a browser, where
it can) */
if (port == 0)
+40
View File
@@ -681,6 +681,46 @@ unsigned long tag_groups_checksum(
return cache_file_globals.tag_header->checksum;
}
#ifdef HALO_ANDROID
/* VR test aid: the loaded tag of the group after the given one (NONE: the
first), wrapping round; NONE if the map has none. Its name too. */
long tag_vr_next_loaded(
long group_tag,
long after_tag_index,
char const **name)
{
short absolute_index;
short start = 0;
short count;
if (!cache_file_globals.tags_loaded)
return NONE;
count = (short)cache_file_globals.tag_header->tag_count;
if (after_tag_index != NONE)
{
for (absolute_index = 0; absolute_index < count; absolute_index++)
{
if (global_tag_instances[absolute_index].tag_index == after_tag_index)
{
start = absolute_index + 1;
break;
}
}
}
for (absolute_index = 0; absolute_index < count; absolute_index++)
{
struct cache_file_tag_instance *instance = &global_tag_instances[(start + absolute_index) % count];
if (instance->group_tag == group_tag)
{
*name = instance->name;
return instance->tag_index;
}
}
return NONE;
}
#endif
long tag_loaded(
long group_tag,
char const *name)
+8
View File
@@ -139,6 +139,9 @@ symbols in this file:
#include "units/unit_definitions.h"
#include "units/units.h"
#ifdef HALO_ANDROID
#include "halo_vr.h"
#endif
/* network_game_globals.c's */
boolean network_game_distributed_client(void);
@@ -373,6 +376,11 @@ short director_desired_perspective(
if (*perspective == 1 || *perspective == 3)
following = TRUE;
}
#ifdef HALO_ANDROID
/* VR: seated, the player stays behind his own eyes (vr.vehicle_first_person) */
if (halo_vr_vehicle_first_person())
following = FALSE;
#endif
return following;
}
+70
View File
@@ -33,6 +33,10 @@ symbols in this file:
#include "units/vehicle_definitions.h"
#include "units/vehicles.h"
#ifdef HALO_ANDROID
#include "halo_vr.h"
#endif
/* ---------- constants */
/* ---------- macros */
@@ -156,6 +160,20 @@ void first_person_camera_update(
/* ---------- private code */
#ifdef HALO_ANDROID
/* VR: a rider who sits down in the hull (a Ghost, a Banshee, a Covenant
gun, a Scorpion's driver) sees out over it: the eyes rise by a height set
for the kind of hull (vr.ghost_eye_raise and the like). Not by the hull's
collision model: it leaves out the Ghost's hood and the Shade's shield, and
has the Scorpion's turret, which swings over the driver. */
static void first_person_camera_vr_over_hull(
long vehicle_index,
real_point3d *eyes)
{
eyes->z += halo_vr_enclosed_seat_raise(vehicle_vr_hull(vehicle_index));
}
#endif
static void first_person_camera_for_unit_and_vector(
long unit_index,
real_vector3d const *forward,
@@ -226,6 +244,58 @@ static void first_person_camera_for_unit_and_vector(
matrix4x3_transform_normal(&matrix, &result->up, &result->up);
}
}
#ifdef HALO_ANDROID
if (halo_vr_vehicle_first_person())
{
/* VR: seated, behind the rider's own eyes, not at the point
the seat's camera turns about (the vehicle's middle) */
struct object_marker marker;
struct unit_datum *seated_in = vehicle_try_and_get(unit->object.parent_object_index);
unsigned long seat_flags = seated_in ?
TAG_BLOCK_GET_ELEMENT(
&vehicle_definition_get(seated_in->definition_index)->unit.seats,
unit->unit.parent_seat_index,
struct unit_seat)->flags :
0;
if (object_get_marker_by_name(unit_index, "head", &marker, 1))
{
result->position = marker.matrix.position;
if (TEST_FLAG(seat_flags, _unit_seat_gunner_bit))
{
/* a gunner leans into the gun's sights: back from them
(vr.gunner_camera_back) */
real_vector3d back = unit->unit.aiming_vector;
back.k = 0.f;
if (normalize3d(&back) > 0.f)
point_from_line3d(&result->position, &back, -halo_vr_gunner_camera_back(), &result->position);
}
if (seated_in && TEST_FLAG(seat_flags, _unit_seat_driver_bit) &&
!vehicle_vr_encloses_riders(unit->object.parent_object_index))
{
/* at an open wheel (a Warthog's), nearer the windscreen
(vr.driver_camera_forward) */
real_vector3d vehicle_forward;
real_vector3d vehicle_up;
object_get_orientation(unit->object.parent_object_index, &vehicle_forward, &vehicle_up);
vehicle_forward.k = 0.f;
if (normalize3d(&vehicle_forward) > 0.f)
{
point_from_line3d(&result->position, &vehicle_forward, halo_vr_driver_camera_forward(),
&result->position);
}
}
/* (the riders on a tank's back sit on it, not in it) */
if (seated_in && vehicle_vr_encloses_riders(unit->object.parent_object_index) &&
(TEST_FLAG(seat_flags, _unit_seat_driver_bit) || TEST_FLAG(seat_flags, _unit_seat_gunner_bit)))
{
first_person_camera_vr_over_hull(unit->object.parent_object_index, &result->position);
}
}
}
#endif
}
result->flags = FLAG(0);
+33
View File
@@ -69,6 +69,10 @@ symbols in this file:
#include "units/unit_definitions.h"
#include "units/units.h"
#ifdef HALO_ANDROID
#include "halo_vr.h"
#endif
/* ---------- constants */
enum
@@ -577,6 +581,35 @@ long player_aim_projectile_preview(
real autoaim_level;
director_camera_deterministic(player->unit_index, &camera_position, &camera_direction);
#ifdef HALO_ANDROID
if (halo_vr_active() && player->local_player_index==0)
{
struct unit_datum *vr_unit= unit_get(player->unit_index);
float vr_eyes[3], vr_neck[3], vr_heading[3];
if (vr_unit->object.parent_object_index!=NONE &&
halo_vr_body_frame(player->local_player_index, vr_eyes, vr_neck, vr_heading))
{
/* VR, seated: as the game aims a vehicle's gun, at what lies
along the player's aim, from the eyes (vr.c draws the crosshair
there); its barrel alone points elsewhere (a Banshee's nose
rises as it hovers, a tank's turret lags) */
camera_position.x= vr_eyes[0];
camera_position.y= vr_eyes[1];
camera_position.z= vr_eyes[2];
camera_direction= vr_unit->unit.aiming_vector;
}
else
{
/* VR: the weapon aims from the hand, not from the eyes: trace
and seek targets along its own line, where its crosshair is
(from the camera the shots would meet the head's parallel
line, above) */
camera_position= *position;
camera_direction= *direction;
}
}
#endif
target_direction= *direction;
autoaim_level= 0.f;
+10
View File
@@ -118,6 +118,10 @@ void cheats_dispose_from_old_map(
/* network_game_globals.c's */
boolean network_game_distributed_client(void);
#ifdef HALO_ANDROID
/* port/linux/game/vr_graphics.c */
boolean vr_graphics_reduces(boolean const *game_switch);
#endif
/* bipeds.c's, player_control.c's */
extern boolean rider_ejection;
extern boolean player_autoaim_flag;
@@ -155,6 +159,12 @@ static boolean cheats_network_client_rasterizer_enforce(
environment_part <= &rasterizer_debug_options.draw_environment_fog_screen;
environment_part++)
{
#ifdef HALO_ANDROID
/* (VR: a part the graphics preset keeps off, as it may in another's
game: shadows, specular light, reflections; vr_graphics.c) */
if (vr_graphics_reduces(environment_part))
continue;
#endif
if (!*environment_part)
changed = TRUE;
*environment_part = TRUE;
+6
View File
@@ -662,6 +662,10 @@ void game_initialize_for_new_map(
game_engine_initialize(&game_variant_global);
real_math_reset_precision();
rasterizer_initialize_for_new_map();
/* port: the shader programs the map drew with last time compiled now,
while it loads, not as its first frames draw (port/linux/src/
xgpu_program_cache.c) */
halo_shader_programs_map_loaded(game_globals->options.map_name);
game_state_initialize_for_new_map();
game_time_initialize_for_new_map();
interface_initialize_for_new_map();
@@ -911,6 +915,8 @@ void game_precache_new_map(
void game_dispose_from_old_map(
void)
{
/* port: the map's list of the shader programs it drew with kept (port/linux/src/xgpu_program_cache.c) */
halo_shader_programs_map_disposed();
rasterizer_dispose_from_old_map();
game_state_dispose_from_old_map();
cheats_dispose_from_old_map();
+37
View File
@@ -1685,3 +1685,40 @@ boolean game_engine_race_read_network_state(
csmemcpy(race_events.best_laps, state.best_laps, sizeof(race_events.best_laps));
return TRUE;
}
boolean game_engine_race_next_flag(
long player_index,
real_point3d const *from,
real_point3d *position)
{
struct scenario *scenario = global_scenario_get();
real best_distance = REAL_MAX;
boolean found = FALSE;
long itr;
if (!scenario || DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) >= MULTIPLAYER_MAXIMUM_PLAYERS)
return FALSE;
for (itr = 0; itr < scenario->netgame_flags.count; itr++)
{
struct scenario_netgame_flag *flag = TAG_BLOCK_GET_ELEMENT(
&scenario->netgame_flags,
itr,
struct scenario_netgame_flag);
real distance;
if (flag->type != _netgame_flag_race_track ||
!TEST_FLAG(race_globals.lap_completed_value, flag->team_index) ||
!can_touch_team(player_index, flag->team_index))
{
continue;
}
distance = distance3d(from, &flag->position);
if (distance < best_distance)
{
best_distance = distance;
*position = flag->position;
found = TRUE;
}
}
return found;
}
+7
View File
@@ -18,6 +18,13 @@ struct race_globals;
void race_flags_make_unique(
void);
/* port: the race flag a player is to touch next, the one of those it may
touch nearest the point (the VR port's bots, port/linux/game/bots.c);
FALSE: none */
boolean game_engine_race_next_flag(
long player_index,
real_point3d const *from,
real_point3d *position);
/* ---------- globals */
+128
View File
@@ -392,6 +392,13 @@ real player_look_pitch_rate[MAXIMUM_NUMBER_OF_LOCAL_PLAYERS] = {0};
boolean controls_enable_crouch = FALSE;
boolean controls_enable_doubled_spin = FALSE;
boolean controls_swap_doubled_spin_state = FALSE;
#ifdef HALO_ANDROID
#include "halo_vr.h"
#include "tag_files/tag_groups.h"
void platform_log(const char *format, ...);
/* how far up or down the head can aim the player */
#define VR_PITCH_LIMIT (85.f * _pi / 180.f)
#endif
/* The native builds read input once a frame and draw several frames per
30 Hz tick (port/linux/game/render_interpolation.c). The pitch autolevel and
limits were stepped once per input update, a tick on the Xbox, so they step
@@ -399,6 +406,49 @@ by the ticks the frame lasted; an impulse applied from a tick steps by one. */
static real player_control_angle_step_ticks = 1.f;
static real player_control_autolevel_time[MAXIMUM_NUMBER_OF_LOCAL_PLAYERS];
#ifdef HALO_ANDROID
/* VR test aid (debug.vehicle_spawn, both sticks held in): the map's next
vehicle, a few metres ahead of the player; not the flying transports */
static void player_control_vr_spawn_vehicle(
long unit_index)
{
static long last_definition_index = NONE;
struct object_placement_data data;
struct unit_datum *unit = unit_get(unit_index);
real_vector3d forward = unit->unit.aiming_vector;
real_point3d position;
char const *name = NULL;
long definition_index = last_definition_index;
short tries;
for (tries = 0; tries < 64; tries++)
{
definition_index = tag_vr_next_loaded(VEHICLE_DEFINITION_TAG, definition_index, &name);
if (definition_index == NONE || definition_index == last_definition_index && tries > 0)
break;
if (!strstr(name, "pelican") && !strstr(name, "dropship") && !strstr(name, "lifepod"))
break;
}
if (definition_index == NONE)
{
platform_log("VR: no vehicle in this map");
return;
}
last_definition_index = definition_index;
forward.k = 0.f;
if (normalize3d(&forward) == 0.f)
forward = *global_forward3d;
object_get_origin(unit_index, &position);
point_from_line3d(&position, &forward, 2.f, &position);
position.z += 0.3f;
object_placement_data_new(&data, definition_index, NONE);
data.position = position;
data.forward = forward;
data.up = *global_up3d;
platform_log("VR: spawning %s: %s", name, object_new(&data) == NONE ? "failed" : "done");
}
#endif
/* ---------- public code */
short unit_get_local_player_index(
@@ -758,6 +808,44 @@ static void handle_one_player_input(
player->zoom_level);
}
#ifdef HALO_ANDROID
{
/* VR: the seat the player sits in, and its vehicle's heading
(vr.c: the body turns with it, and the right stick steers or the
right controller aims) */
int vr_seat = HALO_VR_SEAT_NONE;
int vr_armed = FALSE;
real vr_vehicle_yaw = 0.f;
struct unit_datum *vr_vehicle = unit->object.parent_object_index == NONE ?
NULL :
vehicle_try_and_get(unit->object.parent_object_index);
if (vr_vehicle)
{
struct unit_seat *seat = TAG_BLOCK_GET_ELEMENT(
&vehicle_definition_get(vr_vehicle->definition_index)->unit.seats,
unit->unit.parent_seat_index,
struct unit_seat);
real_vector3d vehicle_forward;
real_vector3d vehicle_up;
object_get_orientation(unit->object.parent_object_index, &vehicle_forward, &vehicle_up);
vr_vehicle_yaw = (real)atan2(vehicle_forward.j, vehicle_forward.i);
if (TEST_FLAG(seat->flags, _unit_seat_gunner_bit))
{
vr_armed = HALO_VR_SEAT_ARMED |
(TEST_FLAG(seat->flags, _unit_seat_driver_bit) ? 0 : HALO_VR_SEAT_TURRET);
}
vr_seat = TEST_FLAG(seat->flags, _unit_seat_driver_bit) &&
vehicle_vr_steered_by_facing(unit->object.parent_object_index) ?
HALO_VR_SEAT_STEER :
HALO_VR_SEAT_AIM;
}
halo_vr_set_seat(local_player_index, vr_seat, vr_armed, vr_vehicle_yaw);
if (halo_vr_take_vehicle_spawn(local_player_index) && !vr_vehicle)
player_control_vr_spawn_vehicle(player->unit_index);
}
#endif
if (!director_inhibited_facing(local_player_index))
{
player_control_angle_step_ticks = time_delta_sec * TICKS_PER_SECOND;
@@ -766,6 +854,26 @@ static void handle_one_player_input(
input.facing_delta.yaw,
input.facing_delta.pitch);
player_control_angle_step_ticks = 1.f;
#ifdef HALO_ANDROID
{
/* VR: the player faces where the head does (vr.c); not in a
cinematic, whose camera the game has (seated, the controller
would turn it on the screen) */
real vr_yaw;
real vr_pitch;
if (!cinematic_in_progress() && halo_vr_aim(
local_player_index,
player->desired_angles.yaw,
player->desired_angles.pitch,
&vr_yaw,
&vr_pitch))
{
player->desired_angles.yaw = vr_yaw;
player->desired_angles.pitch = PIN(vr_pitch, -VR_PITCH_LIMIT, VR_PITCH_LIMIT);
}
}
#endif
}
if (unit->object.parent_object_index == NONE)
@@ -1787,6 +1895,26 @@ void player_control_action_test_note(
{
globals->action_flags |= FLAG(_player_control_look_relative_right_bit);
}
#ifdef HALO_ANDROID
{
/* VR: turning the head is looking around too (the training at the
start of the campaign waits for it) */
real vr_yaw;
real vr_pitch;
if (halo_vr_look_delta(&vr_yaw, &vr_pitch))
{
if (vr_pitch > 0.f)
globals->action_flags |= FLAG(_player_control_look_relative_up_bit);
else if (vr_pitch < 0.f)
globals->action_flags |= FLAG(_player_control_look_relative_down_bit);
if (vr_yaw > 0.f)
globals->action_flags |= FLAG(_player_control_look_relative_left_bit);
else if (vr_yaw < 0.f)
globals->action_flags |= FLAG(_player_control_look_relative_right_bit);
}
}
#endif
if (throttle->i > 0.f)
{
globals->action_flags |= FLAG(_player_control_move_relative_forward_bit);
+63 -2
View File
@@ -281,6 +281,9 @@ symbols in this file:
#include "units/units.h"
#include "units/vehicle_definitions.h"
#include "units/vehicles.h"
#ifdef HALO_ANDROID
#include "halo_vr.h"
#endif
/* network_game_globals.c's */
boolean network_game_distributed_client(void);
@@ -609,8 +612,11 @@ void players_initialize_for_new_map(
machine_to_player_table,
NONE,
sizeof(machine_to_player_table));
/* port: the bots of local games (port/linux/game/bots.c) */
/* port: the bots of local games (port/linux/game/bots.c; not in the VR
port, whose own are host_bots.c's) */
#ifndef HALO_ANDROID
bots_initialize_for_new_map();
#endif
return;
}
@@ -618,8 +624,11 @@ void players_initialize_for_new_map(
void players_dispose_from_old_map(
void)
{
/* port: the bots of local games (port/linux/game/bots.c) */
/* port: the bots of local games (port/linux/game/bots.c; not in the VR
port) */
#ifndef HALO_ANDROID
bots_dispose_from_old_map();
#endif
data_make_invalid(player_data);
data_make_invalid(team_data);
@@ -4793,7 +4802,9 @@ void players_update_before_game(
{
/* port: the controls of a local game's bots this tick
(port/linux/game/bots.c), in place of what their queues held */
#ifndef HALO_ANDROID
bots_update_actions(actions);
#endif
data_iterator_new(&iterator, player_data);
while (player = data_iterator_next(&iterator))
{
@@ -4952,6 +4963,45 @@ void players_update_before_game(
&action->desired_facing);
control_data.facing_vector =
control_data.looking_vector = control_data.aiming_vector;
#ifdef HALO_ANDROID
if (unit->object.parent_object_index == NONE)
{
/* VR: on foot, the weapon aims where the right controller
points, while the body and the view keep facing where the
head looks (port/linux/src/vr.c) */
real vr_aim[3];
real vr_look[3];
/* and it looks where the head looks, whether or not the
game lets the player turn now (scripts test it:
unit_can_see_point) */
if (halo_vr_look_vector(player->local_player_index, vr_look))
{
control_data.looking_vector.i = vr_look[0];
control_data.looking_vector.j = vr_look[1];
control_data.looking_vector.k = vr_look[2];
}
/* (a VR player of another machine: where their
machine says their weapon aims, network_vr.c) */
if (player->local_player_index != NONE ?
halo_vr_aim_vector(player->local_player_index, vr_aim) :
halo_vr_network_aim(iterator.datum_index, vr_aim))
{
control_data.aiming_vector.i = vr_aim[0];
control_data.aiming_vector.j = vr_aim[1];
control_data.aiming_vector.k = vr_aim[2];
}
}
{
real unit_look[3];
unit_look[0] = unit->unit.looking_vector.i;
unit_look[1] = unit->unit.looking_vector.j;
unit_look[2] = unit->unit.looking_vector.k;
halo_vr_debug_look(player->local_player_index, unit_look,
unit->object.parent_object_index != NONE);
}
#endif
control_data.throttle.i = action->throttle.i;
control_data.throttle.j = action->throttle.j;
control_data.throttle.k = 0.f;
@@ -5280,6 +5330,17 @@ boolean player_name_valid(
return FALSE;
for (index = 0; index < count && (cleaned[index] || name[index]); index++)
{
/* (but the spaces it trims from the end: the Xbox's menus pad a new
profile's name, "New001 ") */
if (!cleaned[index])
{
for (; index < count && name[index]; index++)
{
if (name[index] != ' ')
return FALSE;
}
break;
}
if (cleaned[index] != name[index])
return FALSE;
}
+58 -2
View File
@@ -42,7 +42,13 @@ enum
_event_type_right_stick,
_event_type_button,
#ifdef HALO_ANDROID
/* (the VR port's: a headset's controllers' thumbsticks are pushed as
far less readily than a gamepad's) */
STICK_EVENT_THRESHOLD = 22937,
#else
STICK_EVENT_THRESHOLD = 29490,
#endif
STICK_EVENT_REPEAT_MILLISECONDS = 250
};
@@ -71,6 +77,24 @@ typedef char verify_event_manager_state_size[
typedef char verify_event_manager_globals_size[
sizeof(struct event_manager_globals) == 0x168 ? 1 : -1];
#ifdef HALO_ANDROID
/* The VR port's: a stick held moves on once after a pause, then quickly,
as menus do now (the Xbox's moved on every quarter second, which in a
headset, with the keyboard's letters to cross, is slow going). */
enum
{
STICK_REPEAT_FIRST_MILLISECONDS = 350,
STICK_REPEAT_MILLISECONDS = 90,
};
static short stick_repeats[NUMBER_OF_GAMEPAD_STICKS][MAXIMUM_GAMEPADS];
#define STICK_REPEAT_INTERVAL(stick, controller_index) \
(stick_repeats[stick][controller_index] ? STICK_REPEAT_MILLISECONDS : STICK_REPEAT_FIRST_MILLISECONDS)
#else
#define STICK_REPEAT_INTERVAL(stick, controller_index) STICK_EVENT_REPEAT_MILLISECONDS
#endif
/* ---------- prototypes */
static void queue_event(
@@ -83,6 +107,30 @@ struct event_manager_globals event_manager_globals = { 0 };
/* ---------- public code */
#ifdef HALO_ANDROID
/* an event the stick posts: a fresh push (it was within the threshold on
that axis) starts the count of repeats again */
static void stick_note_event(
short stick,
short controller_index,
long x,
long y,
unsigned long time)
{
boolean fresh =
(ABS(x) >= STICK_EVENT_THRESHOLD &&
ABS(event_manager_globals.previous_stick_axes[stick][0][controller_index]) < STICK_EVENT_THRESHOLD) ||
(ABS(y) >= STICK_EVENT_THRESHOLD &&
ABS(event_manager_globals.previous_stick_axes[stick][1][controller_index]) < STICK_EVENT_THRESHOLD);
(void)time;
if (fresh)
stick_repeats[stick][controller_index] = 0;
else if (stick_repeats[stick][controller_index] < SHORT_MAX)
stick_repeats[stick][controller_index]++;
}
#endif
void event_manager_initialize(
void)
{
@@ -209,12 +257,16 @@ static void queue_event(
ABS(event_manager_globals.previous_stick_axes[_gamepad_stick_left][0][controller_index]) < STICK_EVENT_THRESHOLD) ||
(ABS(y) >= STICK_EVENT_THRESHOLD &&
ABS(event_manager_globals.previous_stick_axes[_gamepad_stick_left][1][controller_index]) < STICK_EVENT_THRESHOLD) ||
time - event_manager_globals.stick_event_times[_gamepad_stick_left][controller_index] >= STICK_EVENT_REPEAT_MILLISECONDS))
time - event_manager_globals.stick_event_times[_gamepad_stick_left][controller_index] >=
STICK_REPEAT_INTERVAL(_gamepad_stick_left, controller_index)))
{
post = FALSE;
}
else
{
#ifdef HALO_ANDROID
stick_note_event(_gamepad_stick_left, controller_index, x, y, time);
#endif
event_manager_globals.stick_event_times[_gamepad_stick_left][controller_index] = time;
post = TRUE;
@@ -260,12 +312,16 @@ static void queue_event(
ABS(event_manager_globals.previous_stick_axes[_gamepad_stick_right][0][controller_index]) < STICK_EVENT_THRESHOLD) ||
(ABS(y) >= STICK_EVENT_THRESHOLD &&
ABS(event_manager_globals.previous_stick_axes[_gamepad_stick_right][1][controller_index]) < STICK_EVENT_THRESHOLD) ||
time - event_manager_globals.stick_event_times[_gamepad_stick_right][controller_index] >= STICK_EVENT_REPEAT_MILLISECONDS))
time - event_manager_globals.stick_event_times[_gamepad_stick_right][controller_index] >=
STICK_REPEAT_INTERVAL(_gamepad_stick_right, controller_index)))
{
post = FALSE;
}
else
{
#ifdef HALO_ANDROID
stick_note_event(_gamepad_stick_right, controller_index, x, y, time);
#endif
event_manager_globals.stick_event_times[_gamepad_stick_right][controller_index] = time;
post = TRUE;
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff. Show more