Commit Graph
45 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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
MadisonandClaude Opus 5.5 b70ff4cfca Show a zoom in a scope held at the gun (untested on device)
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>
2026-10-02 16:31:34 +09:00
MadisonandClaude Opus 5.5 5a733a26c3 VR controls, gestures and either hand (untested on device)
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>
2026-10-02 16:15:16 +09:00
MadisonandClaude Opus 5.5 c87cf4413c 3D cutscenes on a screen, fades between views, 90 Hz, mapped stream buffers
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>
2026-10-02 11:29:08 +09:00
MadisonandClaude Opus 5.5 619ff88241 Let the CPU and GPU overlap: asynchronous visibility results, quiet trace markers
Reading the visibility counters still waited, once a frame, for the GPU
to finish all queued work, so a frame's time was the CPU's plus the
GPU's: raising the headset's resolution to 1.25 dropped the first level
to 37 frames a second. Each frame's counters are now copied, behind the
frame fence the stream ring already uses, into a buffer of that ring
slot, and read once the GPU has passed it (host_gl_frame_done, without
waiting); a test's result is the latest the GPU has finished, a frame or
two old, which lens flares do not show. At 1.25 (2160x2160 an eye) the
second level now runs stereo at the headset's 72 frames a second.

Valve's Mesa in Lepton writes a kernel trace marker around every flush
and context switch, a couple of hundred a frame; the descriptors it
opens on trace_marker are pointed at /dev/null once the GL context and
the OpenXR session exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:39:09 +09:00
MadisonandClaude Opus 5.5 12c74e43f2 Read the visibility counters once a frame; fix the HUD pass's fog
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>
2026-10-02 10:28:08 +09:00
MadisonandClaude Opus 5.5 ff7365b5d5 Package the OpenXR loader with the VR build only
It is staged in build/android/jniLibs-vr, which only the vr flavour
packages, instead of beside the flat build's libraries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:35:43 +09:00
MadisonandClaude Opus 5.5 06a0e85c18 Aim with the right hand in the headset (vr.aim = "hand"; untested)
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>
2026-10-02 09:34:10 +09:00
MadisonandClaude Opus 5.5 b27d74ad97 Keep the VR build's data in the headset's Documents, and deploy tools
Lepton's app storage goes with a container reset; the headset's
Documents folder (Android's /sdcard/Documents) stays. The VR build
prefers game data in Documents/HaloCE, with its settings and saves
beside it.

tools/steam_frame/extract_maps.py copies maps out of a disc image on the
Mac (as port/linux/src/xiso.c reads it); deploy.sh copies them to the
headset over SSH, edits config.toml settings, installs and starts the
app over Lepton's adb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:28:58 +09:00
MadisonandClaude Opus 5.5 283a62f15b Show the game on a screen in the headset
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>
2026-10-02 09:19:03 +09:00
MadisonandClaude Opus 5.5 0b489d5e93 Add an OpenXR session for the Android VR build
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>
2026-10-02 09:16:14 +09:00
MrBruh c55e4e2b9d Widen only flat UI fills on widescreen 2026-10-02 02:21:22 +13:00
MrBruh c5fcfbd46a Kick speed hacks and add bans 2026-10-01 13:12:54 +13:00
MrBruh 0d548798e5 Fix second netcode review 2026-10-01 03:54:02 +13:00
MrBruh 4adc3a87a5 Remove matching tooling and HALO_LINUX ifdefs 2026-09-30 21:49:46 +13:00
MrBruh 6feea5dbaa Only reclaim ART's idle large object space 2026-09-30 16:31:05 +13:00
Brandon Fenty 114e4a7635 Android: reclaim idle ART reservations squatting on the Xbox memory window
On the Retroid Pocket Flip2 (kona, kernel 4.19) ART reserves its free-list large object space across 0x754d0000-0x954d0000 at zygote start, covering the fixed Xbox window at 0x80000000, so MAP_FIXED_NOREPLACE fails with EEXIST and the game image never loads.

When the fixed reservation hits EEXIST, munmap exactly the intersection of the requested range with anonymous [anon:dalvik-*] space reservations (idle: those spaces commit lazily from their bottom and this app allocates no large objects) and retry. Nothing changes when the window is already free.
2026-09-30 16:29:26 +13:00
MrBruh caa249457a Install Android maps from a disc image 2026-09-29 15:01:08 +13:00
thelinkin3000 96a48d9daa Stop invalidating streamed buffer ranges on Android
The Android port writes each streamed vertex and index range with a
glMapBufferRange call that set both GL_MAP_UNSYNCHRONIZED_BIT and
GL_MAP_INVALIDATE_RANGE_BIT. The two contradict each other: the first
promises that no queued draw is reading the range, the second tells the
driver to discard whatever the range held, which is the work the first exists
to avoid.

Adreno charges for that discard, around 0.85 ms per call whatever the range
written, and the renderer makes one call per draw. On a Galaxy Z Flip 4
(Snapdragon 8 Gen 1) a few hundred of those a frame came to 96% of a 550 ms
frame and left the game running at 1.8 fps with the GPU idle the whole time.
Dropping the invalidation takes the same call to well under a microsecond and
the same game to the display's refresh rate.

Mali is unaffected either way, and its memory use is flat before and after.
2026-09-28 23:37:19 +00:00
MrBruh 087c1f5685 Improve netcode, add network versions 2026-09-29 11:32:57 +13:00
MrBruh cd47170f39 Fix Android audio deadlock 2026-09-29 03:19:11 +13:00
MrBruh 3c9cd59c1b Add Android app icon 2026-09-29 03:09:03 +13:00
MrBruh c4425807c5 Mark Android app as a game 2026-09-29 03:05:24 +13:00
MrBruh 8994176637 Add auto-updater 2026-09-29 02:51:49 +13:00
MrBruh ed28cb2fa1 Rewrite READMEs, add rolling releases 2026-09-29 01:59:09 +13:00
MrBruh 16514a13d1 Add internet play 2026-09-28 17:42:22 +13:00
MrBruh d9b12fd42e Replace the XDK with clean headers 2026-09-28 13:50:57 +13:00
MrBruh f7ee09b8e3 Move settings to config.toml 2026-09-27 23:56:37 +13:00
MrBruh 50eae19bac Native resolution fullscreen with F11 toggle 2026-09-27 20:42:09 +13:00
MrBruh 658211a88f Improve performance by 2x to 3x 2026-09-27 16:40:58 +13:00
MrBruh 95881a5f91 Replace port assembly with C 2026-09-26 23:56:21 +12:00
MrBruh 1206619936 improve android icon 2026-09-26 23:08:41 +12:00
MrBruh a06ef74eec Detach framerate from gameplay simulation 2026-09-26 21:49:07 +12:00
MrBruh e29125b50e Port decomp to Android 2026-09-26 16:16:00 +12:00