SteamVR's GL binding makes its own context current for every swapchain
acquire, wait and release (and the game's again after), each a flush,
fences and trace markers in Zink: on the device they were a fifth of the
game thread's cycles. The eyes and the quad (HUD or flat screen) are now
parts of one atlas swapchain, acquired by whichever part comes first and
released as the frame ends; the layers take their parts' rectangles
(halo_xr_info.x, .y). Every layer shown is still drawn the frame it is
shown, as a new image of the atlas is each frame's.
On the device at 90 Hz and 1.0x: a30 play from 10.3 to 7.8-9.9 ms of CPU
a frame, 84-90 to 87-90 fps; a10's opening cutscene at 89-90 fps. Dumps of
a stereo frame (eyes, HUD) and a cutscene frame (the 3D screen's eyes)
show each part where it belongs.
View (the left controller's) keeps its short press as back and its 1 s
hold as a recentre with a buzz in both hands, but that recentre now takes
the head's yaw and horizontal position and keeps the standing height
(host_xr_recenter takes HALO_XR_RECENTRE_HEIGHT). A new 3 s hold takes the
height too, which is what crouching is measured from, with a second
buzz. The hold works in the Xbox-pad layout as well, whose back becomes
the short press.
The both-stick-clicks recentre is gone (it also fired crouch and melee).
A recentre brings the flat and cutscene screens back ahead of the head,
and the host handles XrEventDataReferenceSpaceChangePending as a recentre
with height, once the runtime has moved the space.
While a hand-aimed weapon is zoomed, a pass between the eyes and the HUD
renders the view along the gun into a corner of the back buffer, sized
to the scope's 768x768 image. It goes through a sight mask (round, or
the sniper rifle's wide one) onto a layer held at the gun, at the PC
mod HaloCEVR's per-weapon offsets. The eyes never zoom: the first-person
gun stays in the hand and the HUD layer drops the zoom mask.
vr.scope and vr.scope_size are the settings; vr.diag_zoom_seconds zooms
unattended for dumps (vr-scope.bmp). The docs now cover the VR controls,
gestures, weapon feel and scope, and credit LivingFray's HaloCEVR for
their designs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the designs of LivingFray's PC mod HaloCEVR, re-implemented here.
vr.controls "vr" (the default) lays the headset's controllers out for VR
over the Xbox pad the game reads: right trigger fire, left trigger zoom,
A jump, B reload and action, X switch grenades, Y switch weapons, right
bumper grenade, left bumper flashlight, right stick click melee, left
stick click crouch, menu pause, view back, held a second to recentre.
"pad" keeps the Xbox layout. The host now reports each controller's own
buttons (hand_buttons) beside the pad it built.
Gestures, taken into the game's own control flags in
handle_one_player_input (before a cutscene's inhibition clears them):
- a hand swung up or down faster than vr.melee_speed (2.5 m/s) melees;
- the off hand brought within vr.flashlight_distance (0.2 m) of the
head's middle toggles the flashlight;
- ducking vr.crouch_height (0.15 m) below the recentred height crouches,
standing again within 5 cm;
- the weapon hand's grip at either shoulder (vr.holsters) switches
weapons, with a tick as the hand enters a holster;
- the off hand's grip holds the gun in both hands (vr.two_handed "grip",
within 80 cm; "auto" keeps the earlier proximity test);
- palms together and the off hand's grip moves the weapon to that hand.
vr.left_handed (or that swap) puts the weapon in the left hand: the
weapon hand is a variable throughout, its trigger fires and its bumper
throws grenades, and the first-person model is mirrored across the
weapon camera's upright plane with its winding turned over; the IK then
reaches the gun arm from the left shoulder. vr.move_relative moves
relative to the head or a hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cutscenes play on a 3D screen: each eye's view from the cutscene's camera
moved half the eye separation aside (vr.cinema_separation), its frustum
turned in to converge at vr.cinema_convergence, the console window's
letterbox and titles drawn over it, shown as a quad layer each eye sees
its own of (vr.cinema_distance, vr.cinema_width). A screen (menus,
loading, cutscenes) comes up where the head faces when it appears, and
every change of what is shown comes in from black (a quarter second
black, then a 0.35 s fade), which also hides the few frames of play a
level shows before its opening cutscene.
The headset asks for vr.refresh_rate, now 90 Hz (XR_FB_display_refresh_rate),
and falls back to 72 if the game does not hold 90% of it for 20 seconds
while worn. The game thread measured busy 59% of a 72 Hz frame (8.3 ms).
The renderer's stream and index buffers are persistently mapped where the
driver has GL_EXT_buffer_storage, so an upload is a copy with no GL call
(on Zink each map synchronized the threaded context): each eye's pass went
from 3.2 to 2.2 ms. A full buffer within a frame waits for the GPU
instead of orphaning, which immutable storage cannot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With vr.aim "hand", on foot the right controller's aim sets the
player's facing, so shots, grenades and melee follow it; in a vehicle's
or a turret's seat the head still aims. The left stick is turned to move
relative to the head rather than the aim. In a local game the shots
start at the hand (unit_adjust_projectile_ray) unless a wall stands
between it and the unit's eye.
The first-person weapon is posed from a camera placed so that its model
sits in the hand (vr.weapon_offset_*, to tune in the headset). The HUD
crosshair gives way to a reticle on its own OpenXR layer where the
hand's aim meets the world, about a degree across.
Head aiming stays the default until this is checked in the headset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the VR build the game draws the Xbox's 640x480 at the eyes' resolution
(vr.resolution_scale of the runtime's recommendation) and D3DDevice_Present
copies each frame into a quad floating ahead (vr.screen_distance,
vr.screen_width) instead of swapping the window, so OpenXR's frame timing
paces the game. The sRGB swapchain is written without conversion
(GL_EXT_sRGB_write_control). The headset's controllers merge into the
first pad.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
configure.py --vr builds com.halo.decomp.vr: the host gains host_xr.c,
which owns the Khronos loader (1.1.63, fetched from Maven Central and
checked by SHA-256), the instance, a GLES session on the game's EGL
context, two eye swapchains and a quad, and controller actions for the
Steam Frame, Touch and Index profiles. The guest drives it through seven
imports (host_imports_vr.list) with fixed-layout structures in
halo_android_abi.h, checked on both ABIs.
vr.probe_seconds shows dim colours in each eye before the game starts.
On the Steam Frame (Lepton) it reaches SYNCHRONIZED and holds 72 frames
a second; the Frame controller profile binds.
SDL3's Java side assumed a clipboard service, which Lepton lacks; a
patch (port/android/patches) applied at configure time checks for it.
The update provider's authority follows the application id, so the
flat and VR apps install side by side.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>