Asked for, worn: no seeing into the body through the neck, and the
shoulders and torso following the head in a seat.
In a seat the neck now stays and only the head goes, shrunk into the neck's
base: the neck closes like a dimple instead of opening into the body, and
the seated body is drawn two-sided, its insides dark, so the shoulders and
collar near the eyes show no hole. (On foot the neck still goes, and the
body stays one-sided: there, folded, the collar and shoulders leave thin
sheets over the chest that drawn both ways hid it.)
The seated chest now turns with the head, 0.6 of the head's turn from the
body's way up to 35 degrees, and leans toward where the head has moved
from its place at rest, up to 25 degrees, about the spine's base, before
it goes back out of the near plane; the arms then reach from the moved
shoulders to the controls as before.
Dumps (b30, a Warthog's driver): looking down, a closed dark collar on the
chest plate; the head turned 40 degrees left, the left shoulder solid in
view and the hands still on the wheel. On foot (a30) as before.
At 1.5x the GPU needed 11.5 ms a frame (timestamps, vr.timing_gpu_stamps),
over 90 Hz's 11.1, almost all of it the eyes' pixels. The runtime's
visibility mask (XR_KHR_visibility_mask, 15.6% of each eye's image on the
Frame) is now drawn first in each eye's pass at the near plane, black, so
the scene is not shaded there (vr.visibility_mask, on). Two things it took:
the device binds its target and viewport lazily, so the mask binds the
eye's first (halo_vr_bind_current_target); and its depth goes through the
position, not gl_FragDepth, which turned Adreno's early depth culling off
for the rest of the pass (it saved 0.3 ms that way, 1.5 ms this way).
On the device in a30 at 1.5x: the GPU's frame from 11.5 to 10.0 ms (the
eyes 4.4/5.1 to 3.6/4.1), 85.5 fps with 45 misses in 10 s to 89.4 with 2
to 5. Cutscenes (the 3D screen, not masked) hold 78 to 89.
Also: vr.timing_gpu_passes (each pass waited for; Zink's glFinish costs
too much for it to mean much), glQueryCounterEXT and
glGetQueryObjectui64vEXT for the guest, the mask's share logged.
Halo draws no body of the player's own in first person. With vr.body
"legs" (off by default) the eyes' passes draw the player's unit, on foot
and outside cutscenes, from a copy of its bones' matrices: the head, neck
and upper arms (and all under them) scaled away, the body turned to the
head's yaw (as the first-person shoulders now are) and moved under the
head, 12 cm behind the eyes. What the unit carries (its third-person
weapon) is not drawn with it, nor is its shadow. The body's bone names are
logged once, with which are hidden. The head-yaw direction is now shared
with the shoulders (vr_head_yaw_forward). Built only; not yet run.
profile_texture_start and _end read the clock around every texture set,
thousands of times a frame (4.7 of the 5.3 million clock reads in 10 s of
a30), for a timer only the profile graph reads. On the device the game
thread's CPU time a frame in a30 went from 6.2 to 5.9 ms.
In stereo the game's screen flash (a white flash into a scene, a fade to
black) was drawn in the HUD pass, so it covered the HUD's panel: a box in
the middle of the view (reported worn). It is now drawn in each eye's pass,
with the game's own blend over the eye's image. The HUD pass keeps only a
flash that darkens (the darken type, or a lighten toward black), so a fade
to black still takes the HUD with it; one that lightens would light the
panel up as a box again.
The frames between ticks are placed by the game clock, which the main loop
stepped by its own measure (the performance counter) of each pass. That
pass also holds the game's tick and the drawing, which wander by
milliseconds from frame to frame (game 6 to 9 ms on the device), so what
moves steadily was drawn at uneven places between the ticks: cutscene pans
and flying banshees juddered (reported worn). The runtime's frame is now
begun at the top of the loop (vr_frame_seconds) and the clock steps from
the last frame's predicted display time to this one's, whole periods; the
loop's own measure stays for when there is no runtime frame.
The first-person arms model ends partway up the upper arm, open, where
the Xbox's camera never looked. With the arms reaching for the hands
(vr.arms "ik") that end comes into view and, its inside faces culled, the
arms look hollow: one sees through them (reported worn). While they reach,
the hands model (only it, not the gun) is drawn with culling off and its
back faces shaded to 30% (a pixel shader key bit, back_faces_dark, so no
other draw's program changes): the sleeve reads as a closed, shadowed
inside. The user chose this over folding the upper arms away.
cache_file_add_tag freed its last copy of the tag table before making one,
and on the first map with a pause menu (the first level; the main menu has
none) that copy is NULL. The game's free (match_free) halts on a null
pointer, so loading any level from the main menu stopped at the blue
error screen once vr_menu_tags_loaded added the VR settings. On the device
a10 now loads, the VR settings join the pause menu and the opening
cutscene plays (not yet worn past it).
When a level's tags load, the VR build adds widget tags cloned from the
solo pause menu's (same font, colours, buttons and boxes): a fifth item,
VR SETTINGS, in the pause list (its button hints move under the mission
objectives to make room), and a screen of nine settings in two columns -
controls, aim, gun hand, turning, room-scale, scope, vehicle view,
steering and cutscenes. A or right steps a setting on, left back, B
returns to the pause menu; the laser pointer clicks them too.
Changes are written into config.toml (config_write_real/_string join
config_write_boolean) and the VR layer takes them up at once
(vr_reload_settings). cache_file_add_tag adds tags made in memory after
a map's own; the buttons' text and changes come through a new game data
input function and two event handler functions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vr.roomscale (off by default, after the PC mod HaloCEVR's) moves the
player's collision pill each tick by how far the head walked from where
they stand, through collision_move_pill so walls stop or slide it as
walking does. The eyes lean from that standing place blended between its
last two ticks, as the game's camera is, so walking adds no latency and
no judder at the 30 Hz ticks. Vehicles, cutscenes, scripted holds,
death and network games pause it; a jump of over 0.5 m in a tick is
taken up without moving. vr.diag_walk_speed walks the synthetic head for
unattended checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The HUD panel defaults to 15 m away and 10 m wide (about 37 degrees, as
the PC mod HaloCEVR's), so the eyes need not refocus between it and the
world. Its pass is drawn from the head's camera with frustum bounds
matching the panel, so nav points and friends' names sit over what they
mark rather than following the hand's aim. The motion sensor turns with
the head too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Zoomed in, the aim eases toward the hand's (the PC mod HaloCEVR's
half-life formula: about 15 ms at zoom 1, 25 ms at zoom 2), steadying a
scope; unzoomed it is the hand's as it is. Smoothing in the headset's
recentred space, it is untouched by snap turns.
The local player's shots buzz the weapon hand (and, held in both, the
other more softly), by weapon from its tag's name: pistol, shotgun,
sniper, rocket, fuel rod, flamethrower, needler, plasma rifle, assault
rifle, plasma pistol; a shotgun's pellets are one buzz. vr.haptics
scales every buzz, the gestures' too.
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>
vr.vehicle_view "first_person" (the default) has the director treat every
seat as first person, which hides the player's body, and puts the eyes at
the player's head in the seat (a seat's camera marker is where the chase
camera sits). The view turns with the vehicle's yaw only, the horizon
kept level; a passenger seat facing aside keeps its turn from the
vehicle. The player's own gun is put away in driver and gunner seats.
vr.vehicle_steering "stick" (the default) leaves a driver's facing to the
game, so the right stick steers (and pitches the Banshee) as on a flat
screen while the head only looks; "head" and "hand" steer where you look
or point. Gunners aim with the head relative to the vehicle, so a turret
follows the head without turning the view.
vr.diag_drive_seconds seats the player as the nearest vehicle's driver,
for unattended checks (after cheat_all_vehicles). The headset went
offline before the first vehicle test ran; the build is warning-clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the hand aiming, the first-person weapon's camera puts the gun in
the right hand and used to carry both arms along with it. vr.arms "ik"
(the default) gives the arms shoulders where the body is (below and
either side of the eyes, turned with the heading) and solves each arm's
upper arm and forearm, elbows down and out, to reach its hand: the right
the gun's grip as the controller holds it (the wrist and the gun, its
child, stay as they are), the left the left controller, or the gun's
grip for it while the left controller is within 15 cm of it. Out of
reach, the shoulder comes forward rather than the hand letting go.
Fingers and other bones go with the bone above them. "hidden" shows the
gun alone, "animated" the arms as the animation moves them.
Halo's first-person graphs name the hands' bones "wriste"; the gun is the
right one's child.
Cutscene frames have their own dump trigger (vr.dump_cinema_frame).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Android renderer answered each visibility test (lens-flare
occlusion) by mapping the counter buffer for its one word, and each map
waits for the GPU's queue to drain; on the Steam Frame's Zink it also
flushes and logs a performance warning. In stereo, with the flare tests
of two eyes, that took 45% of the game thread: 13-20 frames a second in
the first level's cryo bay. All counters are now read together into a
copy the first time a result is asked for after a test ends (one wait a
frame), and the same scene runs at the headset's 72.
The headset's HUD pass took the planar fog, whose animation offset only
a structure pass consumes, and the next frame's assertion stopped the
game in fogged clusters; the HUD pass leaves the fog alone.
In VR the head's turns count as looking for the scripts that wait for
the player to look (the first level's look test).
The host's thread sampler takes fractions of a second.
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>
While gameplay is in stereo, the player's facing follows the head: the
runtime's frame is begun in player_control_update, so aim and eyes use
the same predicted pose, and the facing becomes the head's direction
turned by a heading of the headset's own. The right stick turns that
heading (vr.snap_turn, 30 degrees, or vr.smooth_turn_speed) instead of
the game; both sticks pressed together recentre. When the game turns
the player itself (a script, a respawn, another pad), the heading takes
it up. View magnetism is off while the head aims, so the view never
turns under it; pitch stops at the game's 85.5 degrees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A frame with one player and no cinematic renders the player's window
three times in render_frame's window loop: the left and right eyes, each
from the game's camera turned by its heading and moved by the tracked
eye (vr.world_scale, head reach limited to 0.35 m), with OpenXR's
off-centre field as render_camera_build_frustum's bounds, and each
copied into its swapchain image as it finishes; then the HUD alone on a
transparent ground, shown head-locked (vr.hud_distance, vr.hud_width).
What moves on as it draws does so once a frame: the sky's animation
phase, weather and glow particles run for the left eye only, and the
first-person weapon is posed once, from the head between the eyes. The
fog's screen layers, which assume a symmetric frustum, are left out of
the eyes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A widget background bitmap whose bounds cover the whole 640x480 design
space (the pause menu dim, for example) stopped at the edges of the
centered 640 column on widescreen displays, leaving the side bands
undimmed. Widen the bounds and the clip symmetrically to the full
screen width, mirroring the existing fade_to_black special case in
render_ui_widgets().
Tested on a Retroid Pocket Flip2 (854x480 render target).
Only the view's magnetism is turned off for the mouse: the bullets still bend toward the target, as with a controller, so shots land as they did.
Co-authored-by: Cursor <cursoragent@cursor.com>
When the mouse moved after the right stick, the controller's magnetism and autoaim leave the player be (input.mouse_aim_assist, off by default). In lockstep games the autoaim stays, since every machine simulates every shot alike.
Co-authored-by: Cursor <cursoragent@cursor.com>