Commit Graph
3661 Commits
Author SHA1 Message Date
Madison 0da170ec7a Fix the seated chest's lean, the wrist, and the body in third-person views
From a review of the body code (headset-unverified, as the body is):

- The chest's turn and lean were composed the wrong way round (yaw * tilt
  tilts first, then turns the tilt by the turn), so a head turned 35
  degrees and moved forward leaned the chest 35 degrees aside; now
  tilt * yaw (checked on the host: straight ahead, as asked).
- A hand the moved shoulder could not reach was still put back where the
  animation had it, off the end of the forearm; it now goes with where the
  forearm reached, keeping its animated pose.
- The lean was capped each way, letting a diagonal one reach 33 degrees;
  it is capped as a whole at 25.
- The chest's turn jumped from one side to the other as the head looked
  straight behind; it fades out from 120 degrees on.
- On foot the body was drawn whenever the hand aims, also from a scripted
  or transition camera elsewhere, where the game draws the whole unit; it
  now needs the game's first-person view too.
- vr_rotation_between turned exactly opposite vectors with the negated
  identity, a reflection that would draw a limb inside out; it is now a
  half turn about an axis across them (also used by the first-person arms).
2026-10-04 00:40:36 +09:00
Madison 41567ad82c Add vr.diag_sweep: several head poses and dumps in one unattended run
Each frame dump of a pose took a launch of its own (config, restart,
level, a minute and a half), and a run on the shared headset has 20
minutes. With vr.diag_sweep "yaw:pitch;yaw:pitch;..." the synthetic head
takes each pose in turn and a dump is written for each, from vr.dump_frame
on and vr.diag_sweep_frames apart (180 by default), as vr-eye0-s<n>.bmp and
the like; each is logged with its pose.
2026-10-04 00:36:35 +09:00
Madison 6be73ed41a Don't start the VR build when it is reinstalled
The updater's receiver starts the game on MY_PACKAGE_REPLACED. On a
shared headset every install of the VR APK then launched Halo in a new
task, and SteamVR quit whichever scene app was running for it. The VR
flavor's manifest removes the receiver.
2026-10-04 00:29:54 +09:00
Madison a5586f7bc8 Close the seated body's neck, and turn and lean its chest after the head (headset-unverified)
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.
2026-10-04 00:21:55 +09:00
Madison 0b636ed5b6 Show the body in vehicles, its hands on the controls (headset-unverified)
In a seat seen from it (vr.vehicle_view "first_person"), vr.body now draws
the player's body as the game seats it, with only the head and neck taken
away: its own animation has the hands on the Warthog's wheel (turning with
the steering), on the turret's handles, on the Banshee's controls. The
view rides at its head, so its shoulders sat inside the near plane, cut
into flat shards across the view: the chest and all on it go back 12 cm
and down 4 cm (along the way the body faces, from its shoulders, so a
gunner's follows the turret), each arm reaches from its moved shoulder to
where the animation had its hand, elbow bent as before, and the hands are
put back as animated.

Dumps on the device, driver and gunner of a Warthog (b30) and a Banshee's
pilot (b40), looking down: the arms reach the wheel, handles and controls,
the chest and legs below, the shoulder shards gone but for a small flap at
the view's lower corner.
2026-10-04 00:06:47 +09:00
Madison acbed8539c Seat the player unattended in a chosen vehicle and seat
vr.diag_drive_seconds took the nearest vehicle's driver seat, and seated
from afar the player did not stay in it. It now logs the map's vehicles,
takes the nearest whose tag name has vr.diag_vehicle in it, as the driver
or (vr.diag_seat "gunner") the gunner, and brings a vehicle more than 2
units off beside the player first. Dumps on the device: a Warthog's
driver (b30), its gunner, a Banshee's pilot (b40), each seen from the seat.
2026-10-03 23:50:53 +09:00
Madison 75cdea4834 Draw the headset's screens (cutscenes, menus) at the quad's resolution
A cutscene's 3D screen and the flat screen of the menus and loading were
drawn at the eyes' resolution (2592 square at vr.resolution_scale 1.5),
though each covers some 60 degrees of view that the display shows in about
1280 pixels; unmasked, the cutscene's two views cost it its frame rate.
They are now drawn at 1280x960, the quad's size, taken between frames from
the mode last shown. On the device, a10's opening cutscene at 1.5x went
from 78-89 fps (up to 15 misses in 10 s) to 88-90 (1 or 2); a dump of it
shows the 3D screen whole, letterboxed.
2026-10-03 23:33:24 +09:00
Madison 6eaf8e92e5 Mask each eye's hidden area, and time the GPU's frame from timestamps
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.
2026-10-03 21:49:13 +09:00
Madison ce3f25d13b Log which GL extensions could make the eyes cheaper
On the device (Zink on Turnip): no fragment shading rate and no Qualcomm
foveation (so no foveation through GL), but OVR_multiview2,
EXT_multisampled_render_to_texture and EXT_clip_control.
2026-10-03 21:26:15 +09:00
Madison f269efc6e6 Relax the free hand from the forearm's line, and fold the hidden body in (headset-unverified)
Worn, the free hand looked like it held a controller and its wrist hung
limp: it pointed along the grip's forward while the forearm came in at
its own angle. It now continues the forearm's line, turned a quarter of
the way toward the grip's forward and rolled as the controller is, and
its fingers are set to a relaxed bend (14 to 30 degrees a joint, from the
bone before) rather than bent further from the weapon animation's half
closed grip; the grip still closes them into a fist.

The body (vr.body) goes back as the head looks down, up to 25 cm more
looking straight down, so the eyes meet the chest rather than the collar
(asked for, worn). Its hidden bones (head, neck, clavicles, upper arms)
now shrink to the nearest kept bone above them, inside the chest, rather
than to their own origins: the collar and shoulders fold in instead of
stretching into spikes across the view.

Dumps: the free hand open and loosely bent at rest, a fist at full grip;
the torso looking down 45 degrees without the spikes, 75 degrees showing
the chest plate.
2026-10-03 20:53:32 +09:00
Madison a93462a29e Offer the body as legs or torso, chosen in the VR settings (headset-unverified)
vr.body is now "off", "legs" (the hips down: all above the spine scaled
away, set 5 cm behind the eyes) or "torso" (the chest down, 10 cm behind
and 15 cm below, as before), and BODY on the VR settings' second page sets
it, taking effect at once. Looking down, the legs alone have nothing near
the eyes (no shards, no collar), though from above the hips' top hides
much of the legs; the torso shows the chest armour and a dark collar.
Dumps only: which reads better worn is for the player.
2026-10-03 20:33:01 +09:00
Madison c31baa18e2 Set the body lower and only a little behind the eyes (headset-unverified)
Dumps of vr.body looking down: 12 cm behind the eyes the collar and
shoulder pads sat inside Halo's near plane (some 19 cm out at this scale)
and were cut into shards; 25 cm behind, looking down showed the floor ahead
of the feet. The body now sits 10 cm behind and 15 cm below the eyes:
looking down 75 degrees shows the chest armour whole, though the inside of
its collar shows dark and the legs stay under the chest. Placed 1.5 m
ahead (a temporary build) the headless, armless body draws clean. Still
off by default.
2026-10-03 20:27:53 +09:00
Madison aad22adf51 Add vr.diag_pitch: tilt the synthetic head up or down
For dumps looking down at the body (vr.body) or the hands; the synthetic
hands keep to the head's yaw.
2026-10-03 20:04:42 +09:00
Madison abad4d7352 Draw the player's own legs and torso in VR, behind vr.body (headset-unverified)
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.
2026-10-03 20:04:06 +09:00
Madison d779210040 Hold the free hand as the controller is, and tuck the thumb in a fist (headset-unverified)
Off the gun the other hand turned with its forearm, so it pointed where the
arm did (straight up in the free-hand dumps). It now turns about the wrist
so that its length runs along the grip's forward and its palm faces as the
player's does (OpenXR's grip +x is the player's right for either hand), and
the wrist is aimed 7 cm behind the grip pose (which sits in the palm). A
closed grip now tucks the thumb further (50 and 55 degrees). The rotation
(the hand's frame onto the grip's) was checked on the host over 10,000
random poses; which way the model's palm faces, and SteamVR's grip axes,
are for a dump on the device.
2026-10-03 20:01:09 +09:00
Madison 445e8cb43b Relax the free hand's fingers, and close them with the grip (headset-unverified)
Off the gun the other hand kept the weapon animation's pose for it. Its
fingers (each a low, mid and tip node in the first-person hands) are now
bent toward the palm at their two joints: a little at rest, into a fist as
the controller's grip closes, the index with the trigger. The palm's side
comes from the knuckles' line and the wrist, flipped for a mirrored hand.
vr.diag_free_hand holds the synthetic other hand up ahead of the eyes at
a given grip; dumps at grip 0 and 1 show loosely bent fingers, then the
fingers folded over the palm (the thumb only a little).
2026-10-03 19:58:08 +09:00
Madison d88c72e34b Add vr.diag_buttons: log the hands' and the pad's buttons as they change
Used to check the input change (d17ed23f) worn: every button of the Frame
controllers reached the game as each layout maps it, in the VR layout and
the Xbox pad's.
2026-10-03 19:41:18 +09:00
Madison c2bcb7fbf2 Turn the shoulders with the head, and set them behind the eyes (headset-unverified)
The first-person arms model ends partway up the upper arm, open. With the
shoulders placed by the body's heading, a head turned toward one looked
along the upper arm into its open end (reported worn). The shoulders now
take the head's yaw (its up where looking straight down leaves the forward
none) and sit 12 cm behind the eyes instead of 6, so that end stays out of
view; the hands stay on the controllers. An unattended dump with the head
turned 70 degrees left and the hands ahead shows the off arm reaching in
from the lower left and the gun ahead, no open sleeve.
2026-10-03 19:30:11 +09:00
Madison d17ed23f7d Derive the Xbox pad from the hands' buttons, without actions of its own (headset-unverified)
xrSyncActions looks up every bound input of every action each frame,
through SteamVR's property maps: 9.7% of the game thread in a30. The ten
Xbox-layout button actions (A, B, X, Y, white, black, start, back, the
stick clicks) duplicated what the per-hand actions already read, with 25
bindings over the three profiles; they are gone, and vr.controls "pad"
takes its buttons from each hand's (the Frame's right four face buttons,
or on Touch and Index the right's two and the left's two; bumpers or their
stand-ins; Menu; the Frame's View; the stick clicks). The d-pad is asked
once a frame, not twice, and hand buttons no profile binds on a hand are
not asked for.
2026-10-03 14:25:23 +09:00
Madison e92d54fa4f Wait a minute for the runtime, and never fall back to a flat window in Lepton (untested on device)
Twice today SteamVR refused the instance at launch (XR_ERROR_RUNTIME_FAILURE,
the headset asleep), the game fell back to drawing on its Android window,
SurfaceFlinger refused those frames (1920x984 against a 1920x1080 window),
and about 20 s later the display composer aborted, then SurfaceFlinger,
and the whole Lepton container went down. The host now retries the
instance once a second for a minute; if there is still no runtime, the
Android build logs why and quits rather than drawing flat.
2026-10-03 14:24:11 +09:00
Madison 821a8c9e7c Time textures for the profiler only while its graph is shown
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.
2026-10-03 14:17:38 +09:00
Madison 15e08b8a7f Read the guest's clock through the vDSO, not a system call
host_syscall served the guest's clock_gettime with a raw syscall(),
bypassing the vDSO: every QueryPerformanceCounter and system_milliseconds
went through the kernel and Lepton's seccomp filter. The game read the
clock about 535,000 times a second in a30 (Halo's profiler, mostly); on
the device host_syscall fell from 12.6% to 9.8% of the game thread's
cycles with this, to 1.3% with the profiler's texture timer gated too.
2026-10-03 14:17:38 +09:00
Madison 01b962ad67 vr.diag_motion: follow a moving object and count blended draws
Each logged frame now also has how many objects were drawn blended
between ticks and how many fell back to the latest tick, and the blended
root of the first object seen moving; the blend is read from the game
clock (render_interpolation_fraction is 1 outside the drawing).

On the device: a10's opening cutscene and a30's play step their blends by
exactly a third a frame at 90 Hz (0.224, 0.557, 0.891), the cutscene
camera's steps stay within about 1% of their neighbours with no pattern
by the tick, and in a30 175 objects a frame are blended, none not; a
followed object's steps are even except where a frame was dropped.
2026-10-03 14:01:01 +09:00
Madison fc3f472a84 Add vr.diag_motion: log each frame's display step, blend and camera
For judging judder unattended: a line a frame while a view is drawn, with
the step between display times, the blend between the ticks, the tick and
the game camera's interpolated position.
2026-10-03 13:50:45 +09:00
Madison 3821984dcf Draw screen flashes and fades over each eye, not on the HUD panel (headset-unverified)
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.
2026-10-03 13:50:45 +09:00
Madison 3cfa297bde Put both eyes and the quad in one swapchain, acquired once a frame
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.
2026-10-03 13:46:09 +09:00
Madison 1d4c18a586 Draw the reticle's image once, not every frame
A layer shows its swapchain's last released image, and each acquire,
wait and release costs SteamVR's GL binding an eglMakeCurrent (a flush,
fences and trace markers in Zink): about a fifth of the game thread's
cycles went to those switches. The reticle (and the menus' pointer, which
shares it) is a fixed dot. On the device, a30 at 90 Hz and 1.0x: the
frame's CPU time from about 11.3 to 10.3 ms, 78-81 fps to 84-90.
2026-10-03 13:40:01 +09:00
Madison cc9f773e5b Time a cutscene's eye passes as the eyes in [vr-perf]
The 3D screen's two passes (3 and 4) were timed in the scope's slot and
not at all, so their cost showed as the game's own work.
2026-10-03 13:18:59 +09:00
Madison 0288752115 Step the game clock by the headset's display times (headset-unverified)
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.
2026-10-03 13:09:19 +09:00
Madison 23dcfe4fcd Draw the reaching arms two-sided, their insides dark (headset-unverified)
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.
2026-10-03 12:59:04 +09:00
Madison 8362927f65 Keep the gun's angle when the second hand takes hold (headset-unverified)
Two-handed aim pointed the gun straight along the line from the weapon
hand's grip to the other's. That line is not the barrel's (the hands sit at
different heights, the grip poses in the palms), so pressing grip jumped
the gun to a new angle and the hand on its grip with it (reported worn:
"moves into a weird position and makes aiming odd"). The gun now keeps the
turn from that line it had as the hand took hold, and turns as the line
does. A host check over 10,000 random poses: no jump on taking hold
(6e-7), and the gun follows the line's turn (3e-6).
2026-10-03 12:56:55 +09:00
Madison 1a7d3b7b9b Keep the off hand's grip on the gun as animated (headset-unverified)
With the other controller near the gun's grip for it, the arm reached the
animation's hand position but turned the hand by the forearm's turn, which
twisted its fingers off the gun (reported worn: the left hand grips the gun
but is misaligned). A hand on the gun now keeps the animation's orientation,
as the gun hand does (vr_solve_arm's hand_stays); on its controller it still
follows the forearm.
2026-10-03 12:54:20 +09:00
Madison 19c433c982 Allocate the VR menu's tags with the game's malloc, as they are freed
cseries.h sends malloc and free to the game's checked allocator but not
calloc, so the VR settings' cloned widgets came from libc and were let go
through debug_free, which halted on their missing header ("Signature is
incorrect", vr_menu.c:447) at the first map change after a level: Save and
Quit, or any level to the next. On the device a10, a30 and the main menu now
load one after another through the console (not yet worn).
2026-10-03 12:52:01 +09:00
Madison 7d96cc32ec Don't free a null tag table on the first level load
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).
2026-10-03 12:48:03 +09:00
Madison 137ffff809 Give the Xbox layout's Menu action its own name, so controllers bind again
OpenXR wants each action's localized name unique in its set. button_start
and hand_menu shared "Menu (pause; Index: a firm press of a trackpad)", so
creating hand_menu failed (XR_ERROR_LOCALIZED_NAME_DUPLICATED), no actions
were attached and no controller reached the game: the menus took no input.
On the device the actions are now made and all three profiles' bindings
suggested (seen in the log; not yet worn).
2026-10-03 12:41:25 +09:00
Madison e3a9ce6248 Document sfvr, the turn settings, the settings pages and the perf line (headset-unverified) 2026-10-02 18:17:14 +09:00
Madison 5bd3f5dd08 Put the HUD mode in the VR settings, on a second page (headset-unverified)
The VR settings page held ten items in its two columns of five. Its last
item on the first page is now MORE..., which opens a second page (B goes
back a page) with HUD: FAR, NEAR or LEASH, and last of all QUIT TO STEAM.
Each page is built by one function from the items marked with it, so
another page is a table row away.
2026-10-02 18:16:14 +09:00
Madison 4ddcb79eb1 Pause on a firm trackpad press on Index (headset-unverified)
Menu was bound to the Index controller's system button, which the runtime
keeps, so Index had no way to pause. It is now either trackpad's force
(a float on a boolean action, read as pressed past the runtime's
threshold), as a chord of existing inputs would have conflicted with the
standard's rule against stick-click chords and with every free button.
2026-10-02 18:14:41 +09:00
Madison f54d386ae5 Run View, turning, haptics and [vr-perf] on sfvr (headset-unverified)
system_layer takes its timing from sfvr_view_update (the same 1 s and 3 s
holds and short press, now on the real clock, and a press cut by losing
focus is dropped). Buzzes are submitted to an sfvr_haptic_queue (vr_haptic
keeps its signature) and flushed once a frame in vr_present at vr.haptics'
strength, overlapping ones coalescing into the strongest; the system buzz
is SFVR_HAPTIC_SYSTEM. The right stick's turn is sfvr_turn_update, from
vr.turn_mode, vr.snap_angle and vr.smooth_turn_speed (snap 30 and smooth
120 stay the defaults; smooth now has sfvr's 0.2 dead zone and rescaling,
its sign converted to the heading's).

The guest logs the standard [vr-perf] line every 10 s (2 s with vr.timing)
from an sfvr_perf, with the stages game, render, copy and end, each eye's
pass, the GPU wait with vr.timing_gpu, and frames missed counted from the
display times; it replaces the old [vr-frame] line. The host's own
statistics line (which drives the 72 Hz fallback) is now an [openxr] line.
2026-10-02 18:14:19 +09:00
Madison a4f43161f5 Name the turn settings by the standard: vr.turn_mode and vr.snap_angle (headset-unverified)
vr.turn_mode (snap, smooth, off) and vr.snap_angle (15 to 90) replace
vr.snap_turn, which stays as a read-only legacy setting: a file at
settings_version 0 or 1 that holds it is migrated on load (smooth if it
was 0, else snap at the nearest standard angle), the new keys written, the
old line commented out and settings_version set to 2. The migration edits
the file in place and the file is read again before what is missing is
added; the old HALO_VR_SNAP_TURN still works for one run when the new
variables are unset.

Numeric vr.* values take sfvr's range where its key registry has the key
(hud_distance 0.8 to 20, refresh_rate 72 to 144, probe_seconds 0 to 60,
diag_walk_speed 0 to 5 and so on), and the limits chosen here elsewhere.
The pause menu's TURNING item writes the two new keys (snap 30, snap 45,
smooth, off); config_write_integer is new.
2026-10-02 18:13:59 +09:00
Madison eabb888079 Vendor sfvr into the guest build (headset-unverified)
port/third_party/sfvr is a copy of ~/code/sfvr's include/ and src/ (VERSION
holds its commit, ad2f545), never edited here. The VR guest build compiles
its six .c files for the ILP32 guest with the project's ABI flags (and
-Wall -Wextra, which they pass clean); nothing uses them yet.
2026-10-02 18:10:10 +09:00
Madison 6c031d3024 Document the system layer, quit, settings version and the 72 Hz fallback (headset-unverified)
docs/VR_ARCHITECTURE.md: the View system layer (short press, 1 s, 3 s) and
what a recentre does, the Touch and Index fallbacks, QUIT TO STEAM, the
settings file's version, migration and ranges, the display rate request and
its automatic fall back to 72 Hz (which the doc did not mention), the nine
host imports, and status rows for each of these marked not yet run on the
device, with the standard's items still to do.
2026-10-02 18:07:10 +09:00
Madison e370e0b28e Reach grenade and flashlight on the Touch and Index fallbacks (headset-unverified)
Touch and Index have no bumpers, so the VR layout's grenade (weapon hand's
bumper) and flashlight (the other's) had no input. The host now watches
which controller each hand holds (the interaction profile changed event)
and, for one without a bumper, reports a grip held with a stick click as
that hand's bumper; the stick click alone still melees and crouches. The
Frame's own bumpers are untouched.

The action labels, which SteamVR shows in its bindings, no longer name the
Xbox pad's roles as if they were the VR layout's: the pad-layout actions
say so, and the per-hand ones name their VR roles, View included.
2026-10-02 18:06:20 +09:00
Madison 09144f7456 Version and range-check the VR settings (headset-unverified)
vr.settings_version = 1 is written to config.toml. A VR file without it
is migrated: vr.hud_distance and vr.hud_width, always written at the old
15 and 10, become 0 (the HUD mode's own) so they stop overriding the near
and leash modes; the lines are edited in place and the player's other
edits stay (config_edit_text, which config_write now shares).

Every numeric vr.* value is clamped to a range on load, from the file or
the environment, with a log line naming the setting, the value and the
range: the standard's where it gives one (resolution_scale 0.75 to 1.5,
snap_turn 15 to 90 or 0, smooth_turn_speed 30 to 360, haptics 0 to 1) and
otherwise one the setting can sensibly take. Defaults are unchanged. The
header comment about the file never being rewritten is corrected.
2026-10-02 18:05:24 +09:00
Madison 30e61dffca Add QUIT TO STEAM to the VR settings menu, with a confirm (headset-unverified)
The tenth item of the VR settings page (the right box's last row). A
asks "SURE? A TO QUIT"; A again within five seconds asks the runtime to
end the session (a new host import, host_xr_request_exit, calling
xrRequestExitSession) and exits when its STOPPING and EXITING states have
come, which the frame loop already follows, or after three seconds if
they never do. Left, another item or the time puts it back. It adds no
structure to the ABI, so there is no layout to assert.
2026-10-02 18:03:34 +09:00
Madison 7bbb9ccdc1 Add vr.hud_mode: far, near and leash HUD panels (headset-unverified)
far keeps the HUD as it was (15 m, 10 m wide, head-locked, drawn from the
head). near brings it to 1.5 m, 1.1 m wide (40 degrees), in LOCAL space at
the head's place turned by its yaw and pitch but not its roll. leash holds
that panel level in the room, following the head's position, and eases it
(about 0.3 s) to the head's yaw only once the head is 25 degrees off it;
a recentre or a return from a flat screen puts it ahead again.

vr.hud_distance and vr.hud_width now default to 0, the mode's own, and
override it when above 0. The HUD pass is drawn from the head's place
turned as the panel is (vr_hud_view), with bounds from the same distance
and width in every mode, and the menus' pointer meets the panel where it
is. Documented in docs/VR_ARCHITECTURE.md.
2026-10-02 18:02:18 +09:00
Madison 5172ba65a9 Make View the system layer: hold 1 s recentres, 3 s sets height (headset-unverified)
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.
2026-10-02 17:59:00 +09:00
Madison cf05a9639f Fix haptics recursing into themselves (headset-unverified)
vr_haptic called itself after scaling, so the first buzz (default
vr.haptics 1.0) overflowed the stack. It now calls the host import
host_xr_haptic.
2026-10-02 17:53:53 +09:00
MadisonandClaude Opus 5.5 cff675537d VR settings in the pause menu (untested on device)
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>
2026-10-02 17:03:30 +09:00
MadisonandClaude Opus 5.5 b5c6ae2a9b Walk the player by walking the room (untested on device)
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>
2026-10-02 16:48:28 +09:00