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>
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>
A game's level name is the map's own (custom_maps\hugeass), so no screen or
server browser listing shows a hash, and the game record's map version is
the map's hash's first 32 bits, which a client finds its file by. Maps are
no longer renamed; a download beside another version of its name is named
<name>_<hash>.map and listed as "(2)"; maps the last build renamed get
their names back. Network version 29: the level name and the map version
mean something else.
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>
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>
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>
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>
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>
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>
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>
Network version 28: the host now holds a downloading machine's players out
of the game, and keeps a multiplayer map's first scripted BSP. Dynamic lights
and the flashlight stay on past the Xbox limits, and a second weapon picked
up while firing is readied. Machines of version 27 would disagree on these.
- A large co-op game's respawn fallback no longer picks a teammate who is in
the air, outside the BSP, or riding an AI vehicle.
- A machine still downloading the map times out when it goes silent.
- Held players join through the late-join queue, so they get the same checks.
A machine with no held players is started instead of being refused.
- The co-op teleport uses world positions, which are right for seated players.
- Overshield drain no longer cuts off a hit's flash, and hits over 0.02 flash.
- The menu XML is the generator's (Custom Edition's layout) again.
- port: markers on two new declarations.
Checked: ninja linux, android and web build; tools/harness,
test_touch_menu and test_web_build pass.
rasterizer_debug_model_vertices declares 2048 debug vertices of 64 bytes
on the stack. render_model calls it for each part it draws, and the
release build inlines it there, so render_model's frame was 136,304
bytes, and the Windows build probed it a page at a time (_chkstk, 33
pages) for every model drawn, although the debug display is off. Its
vertices are static now: only the vertices below debug_vertex_count are
read, each written when it is added, and the function runs on the
render thread only, so nothing it draws changes.
Measured on Windows (7800X3D, RTX 4080, release build, vsync off), b30
co-op with 32 times the enemies and a bot, 75 s a run, five runs of each
build alternated: render_model's frame is 5,360 bytes (the PDB's
S_FRAMEPROC); _chkstk fell from 2.0% to 0.5% of the game thread's
samples; fps over windows with 2000 draws a frame or more, origin/main
128, 141, 139, 134, 107 (median 134), this change 140, 139, 144, 141,
132 (median 140). Linux, Android and the web build do not probe the
stack, so the change only shortens their frame. They were not built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every transparent thing a frame draws takes a group: each transparent
part of a model, each widget (light volumes, antennas, flags, glows),
each batch of dynamic geometry (particles, contrails), each transparent
surface of the structure. The Xbox's 384 were a limit the native builds'
larger pools of objects, widgets, effects and particles could pass: a
frame past it was drawn without the rest, and debug.txt said "too many
transparent geometry groups". A part refused a group has no sort record
(models.c checks the group's index), so nothing else changes.
Checked on Windows in b30 co-op with eight times the enemies and a bot,
75 s a run: origin/main overflowed in 3 runs of 4; a build counting the
groups asked for showed up to 464 in a frame, while the dynamic triangle
and vertex buffers peaked at 123 and 328 of their 1024 and the dynamic
triangles at 10272 of 49152, so they are left as they are. With this
change, the same count showed 391 in a frame and no overflow; two more
runs had none. The pool is 2048 * 160 bytes, malloc'd; the model order
pass keeps 14 bytes a group on the stack (28 KB). Harness: the
transparent_plane_order tests pass at the new size (natively on Windows;
the other 44 failures there are the same on origin/main, POSIX headers).
Android and web builds not built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wait only counted teammates the map called grounded, so jumping, riding, or standing on a beach left it with nobody to use. A late joiner after a checkpoint uses the same wait when nobody is safe to spawn beside.
A scenario's titles (cutscene titles, their text from the scenario's
ingame_help_text) are drawn with the screen's overlays, which come after
the players' windows and so after their menus. Some Custom Edition maps
show their name with one for the whole game: Extinction's "Extinction",
at the bottom right, was drawn over the pause menu and over the profile
screen's CANCEL button. cinematic_render already leaves out the letterbox
while a menu is up; it now leaves out the titles too. Their time goes on
as before, so a title still fades out when it would have.
Checked: the Windows build (clang 23.1.3, full LTO) without warnings;
Extinction from the Italian Custom Edition in a hidden window, START
pressed by a local test input after 14 seconds: the title is drawn in
the game and not over the pause menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The high-res text sizes Overpass by the font tag's capitals and centres
each glyph on the tag's character width, and Overpass was chosen for the
Xbox maps' widths. A Custom Edition loc.map's fonts are narrower: the
Italian one's large_ui and small_ui are the same font, its capitals 10
units tall and its letters 87% as wide as Overpass's at that height
(against 95 to 98% for the Xbox maps' interstate, large_ui and small_ui),
so the menus' and HUD's letters ran into each other. Each font is now
narrowed until its letters A to Z and a to z are, summed, as wide as the
tag's; the Xbox maps' fonts are narrowed by 2 to 5 hundredths.
The fonts were also remembered by the tag's name, and the Xbox maps and a
loc.map have tags of the same names with other measures, and by the font
tag's address, which the next map's tags may reuse: they are remembered
by their measures too, and forgotten with the map's tags.
Checked: the fonts' widths measured from the Italian loc.map and the Xbox
ui.map against Overpass's advances; the Windows build (clang 23.1.3, full
LTO) without warnings; Extinction from the Italian Custom Edition, whose
large_ui is now drawn 87% wide, its letters apart, and the main menu's
small_ui 98% wide.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hosts send players who join without the game's Custom Edition map the map
and the resource maps it needs; the other players help, torrent-style.
Network version 27: new map sharing messages, and players of an internet
game now reach each other.
Fast setup and the network test's join opened ui.map's pregame screen by name, where ui_widget.c opens the port's lobby; a ui.map of only the menus' tags hosts and joins with both.
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>
msvc_comdat.c defines __inline as weak on Windows, which a static function cannot be; the accessor is __inline__. Checked by compiling msvc_comdat.c for i686-pc-windows-msvc.