The mirrored first-person model leaves its geometry mirrored; a numeric
display drawn mirrored (rasterizer_set_model_skinning's winding flag) now
shows each digit's bitmap turned over in u (1-u on the first map's texture
transform) and swaps the digits' order across the display (the quad of the
n-th digit from the right shows the n-th from the left; digit count from
the counter limit, the compass's single digit unchanged). Only for 2d,
non-screen-space first maps of numeric chicago/generic shaders, only when
mirrored: right-handed and flat output unchanged.
debug.mirror_first_person (Android, default false): off the headset the
first-person weapon is mirrored across the camera's forward/up plane as
the left hand's is in VR, for checking such things in the emulator.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The assault rifle's ammo display, mirrored back, vanished: its part's
winding was judged by its first node alone, which may be the mirrored gun,
so its triangles were turned over and culled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Walking up from a node to the display through parent_node_index never
ended where the root names itself as its parent: the game hung loading a
level left-handed (Steam Frame). Each step now goes only to a parent
before its child, as vr_skeleton_build does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its node ("... display") and what hangs from it are mirrored back about
the display's own centre after the pose is mirrored: it stays where the
mirrored gun has it, reads as in the right hand, and its triangles keep
their winding (rasterizer_xbox.c decides per skinned part). After
HaloCE-Quest-VR (CC0).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel's second page (MORE) gets "Graphics" (vr.graphics: Low,
Medium, High, Max), "Refresh rate" and "Resolution" rows, with a note
of what they do. The refresh rate steps through the rates the runtime
offers (XR_FB_display_refresh_rate: host_xr_refresh_rates, new host
imports with host_xr_set_refresh_rate) and changes at once (vr.c asks
for it when vr.refresh_rate changes); where the runtime offers none
(SteamVR on the Steam Frame, whose rate is SteamOS's) the row shows the
rate it runs at, from its frames' period, and stays as it is. The
resolution (vr.resolution_scale, 70 to 120%) is read as the app starts:
the row says when a restart is needed. vr.log_performance's late frames
follow the rate as it changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
port/linux/game/vr_graphics.c (after HaloCE-Quest-VR's, CC0) sets the
game's own effect switches each frame before drawing: "high" (the
default) draws everything as before; "medium" no object shadows,
environment reflections or lens flares; "low" nor specular light, bump
maps, detail objects (grass), the screen's fog layer, contrails, weather
or camouflage's second pass; "max" is "high" with per-pixel model
lighting and 512x512 shadow maps (d3d8_gl.c asks halo_vr_graphics). A
switch the game changes (scripts, console) is taken as its wish and
combined with the preset's. A client in another's game keeps grass, lens
flares and the fog layer as the host's rules have them; cheats.c leaves
the environment parts the preset may keep off there (shadows, specular,
reflections).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The VR options panel gets a "Weapon hand" row (vr.handedness) and a
"Mirrored controls" row (vr.mirror_controls, dim when right-handed). Its
rows now go on two pages with tabs beside the title (PLAY, MORE; up and
down run on across them, a tab clicked shows its page). The controller
buttons page is built from the hands' jobs: the weapon hand's trigger
and grip, the free hand's, the move and turn sticks and the face
buttons (Quest X/Y for A/B; Steam Frame D-pad for A, B, X, Y) on the
sides the settings put them; the wrist, wheel and aiming notes name the
hands. network_vr.c's notes name the weapon and the free hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vr.handedness = "left" puts the weapon in the left hand: aim, muzzle,
laser, crosshair, scope and turret aim follow the left controller; the
grenade, the foregrip and the fingers' closing round a grenade move to
the right hand, the grip's melee to the left. The first-person model is
posed as the right hand's in a mirror image of the room ("model space":
vr.c mirrors the controllers into it, first_person_weapons.c the body)
and the posed skeleton mirrored back, after HaloCE-Quest-VR (CC0); a
model whose node matrices mirror has its triangles' winding turned over
(rasterizer_set_model_skinning -> d3d8_gl.c, ended with the model and
each transparent group). halo_vr_left_hand is now halo_vr_free_hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The elbow could not be lifted out to the side: it bent only towards the
fixed pole (down, out, back). An elbow lifted out rolls the controller's
top in, so the pole now leans towards the hand's underside (the weapon's
for the right arm, the free left controller's for the left; the hand on
the foregrip keeps the fixed pole). With the free arm solved before its
hand is turned it looked right on the Quest: vr.arm_ik defaults to true.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As HaloCE-Quest-VR does it: the free arm is bent to the controller's grip
from the animated pose (the hand free to swing with it), and only then is
the hand turned to the controller and the roll shared along the forearm.
Turning the hand first left the solve to swing it on, which stretched the
arm's skin like a rubber band (Quest, 2026-10-06). vr.arm_ik stays off by
default until it is seen right.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second Quest test: with the elbow out to the side the right elbow's skin
turned inwards: the shortest turn of the solve left the upper arm's hinge
where it was. vr_align_elbow now rolls the upper arm about its length so
the animation's bend lies in the plane the arm bends in, and the upper
arm takes no share of the hand's roll any more. The open sleeve end could
still be seen: the shoulders are 5 cm higher (20 cm below the neck).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
First Quest test: the free left arm's skin stretched like a rubber band
with the new elbow IK, so vr.arm_ik now defaults to false (the arm's shape
as before, pointed at the shoulder); and the open end of the right
sleeve came into view looking down to the side: the shoulders now stand
4 cm behind the neck instead of 3 cm in front of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vr.body = "arms" (default): arms and hands as before; "hands": the arms'
bones shrunk to a point 3.5 cm behind each wrist along the hand's line
so the cuff closes behind the hand (vr_hide_arm, after HaloCE-Quest-VR's
vr_hide_forearms, CC0), no arm IK solved; "off": the hands model is not
drawn (one model draw less an eye) and nothing of the arms posed; the
weapon, its aim with both hands, barrel, laser and crosshair stay.
- vr.c reads vr.body with the live settings (halo_vr_body_mode).
- The VR options panel gets "Body" (Arms and hands / Hands only / Weapon
only) and "Fingers" (Tracked / Off) rows; its twelve rows are 22
pixels apart (were 26) so the panel keeps its box.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vr_arms.c: vr_finger_nodes, vr_pose_finger (after HaloCE-Quest-VR, CC0,
nearly verbatim): each finger's three joints found by name under the
hand, posed by absolute directions in the palm's frame (a little bent
at rest, a fist at full curl; the thumb up the handle or across the
palm), each segment's subtree turned with its length kept. Its touch
of the world (rays per finger) is left out for the Frame's processor.
- The free left hand takes its controller's curls (halo_vr_finger_curls:
index from the trigger, the others from the grip, the thumb from where
it rests; round a held grenade). On the foregrip the hand keeps the
weapon's pose.
- The hand on the weapon keeps its grip; only its index finger is lifted
part way off the trigger while the controller says it is (touch known
and the finger off it), never where touch is unknown.
- Joints found once per skeleton; the cost is a few dozen node turns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- port/linux/game/vr_arms.c: vr_solve_arm and vr_distribute_arm_twist,
after HaloCE-Quest-VR (CC0), nearly verbatim: the shoulder reaches 8 cm
and the arm stretches up to 18% out of reach (further, the shoulder
follows the held hand, as this port had it, so the arm stays whole),
bends no further than 145 degrees, the elbow's swing eased per arm
(vr_limb_history), the hand's roll shared by forearm (85%) and upper
arm (25%) within 110 degrees, continuous across half a turn. The
skeleton is read through the graph's index; the nodes under each node
come from one pass a frame (vr_skeleton_build) rather than walking
ancestors per node; time is the headset's frame's (halo_vr_pose_time).
- Both arms use it; the right hand and a left hand on the foregrip stay
where the weapon puts them (hand_stays), so shots, barrel and laser do
not move.
- The free left hand: its wrist and fingers are set on the controller as
before, and the arm now bends to it from the shoulder instead of being
carried rigidly. The ribbons of e377e4dc did not come from skin on the
root (3a.0: none hangs there), so no triangle filter is needed; the
rigid arm stays behind vr.arm_ik = false, read live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vr.c keeps the torso's heading in LOCAL space (torso_update, after
HaloCE-Quest-VR's vr_body_heading, CC0): the head's heading, steady at
any pitch, drawn halfway towards the hands when both are ahead (at most
45 degrees), followed beyond a 15 degree cone, eased at 10 a second;
reset on a recentre or a gap. In LOCAL space the stick's snap and
smooth turns carry it at once (HaloCE-Quest-VR's "arm IK lags as the
body turns").
- halo_vr_body_frame gives the torso's heading and the neck the head
turns about (9 cm behind the eyes); the shoulders hang from the neck,
square to the torso, so turning the head no longer swings them (as the
head faces the torso, where they were before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Posed from the camera at the right controller, the first-person weapon
and the arms are the same for both eyes and the scope's view, yet were
built (animation, interpolation, arm IK) for each: twice a frame on
SteamVR, three times zoomed in. The pose is kept for the rest of the
headset's frame (halo_vr_frame_serial) once built from the controller,
for the same weapon; posed from an eye's own camera (the hand not
tracked), each eye still builds its own. vr.log_performance counts the
builds and the poses kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vr.log_performance reports how long the first-person pose takes a frame
(its animation and the arms), how often it is built, and the arms' part
of it (halo_vr_timing_start/_end).
- Once per hands model it logs how many triangles hang from each node and
how many of those from the skeleton's root ('frame bone24'), read from
the part's own buffers (rasterizer_model_buffer_data, after
HaloCE-Quest-VR, CC0). In the emulator the cyborg's hands have none on
the root: the ribbons of e377e4dc did not come from root skin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: d3d8_gl.c (multiview variants kept beside the per-pixel lit
shaders: shader[4] and lit_shader[4], vertex_shader_get takes both;
multiview targets before multisampling; the picture read resolves both),
nv2a_vsh.c/xgpu.h (lighting and multiview parameters), render.c (no
anti-aliasing pass on VR eye passes), dsound_sdl.c, p2p.h.
VR: anti-aliasing off (the eyes go to the headset from their textures);
the VR online list skips password games (the server browser asks).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
display.anti_aliasing, off by default: "fxaa" or "smaa" (a pass over the
3D view before the HUD and menus; SMAA vendored from port/third_party/smaa,
MIT), "ssaa2x" (the screen's targets at twice the resolution, at most the
GPU's largest target) and "msaa2x" to "msaa8x". Multisampled framebuffers
have every attachment multisampled, so the mirror's view with the back
buffer's depth buffer is too, and a target's texture and renderbuffer stay
coherent. The setting is read again between frames. Android has FXAA for
SMAA and no supersampling.
Based on #38 and #39 by Tyberious.
display.per_pixel_lighting (off by default) lights the models for each
pixel by the same ambient, distant and point lights the model lighting
programs (9, 10, 17 and 27) sum for each vertex. The normal and position
are found in each program's instructions and the program is checked to
light as the pixel shader does; the lights come in a small uniform of
their own, and a shader that fails falls back to per-vertex lighting.
Based on #48 by Tyberious.
display.shadow_resolution (128 by default, as on the Xbox; 256, 512 or
1024) draws the two shadow maps larger. The blur's taps move to half a
texel of the larger map and two passes more for each doubling widen it
to the Xbox's tent, so the shadows keep their softness.
Based on #47 by Tyberious.
Fixups for #99 and #112: the version raised for the object message's
shader permutation, NETCODE.md brought up to date, a networked glass
break's sound placed where it broke, a frame scratch pool that runs out
logged once, and the new comments marked as the port's.
With many enemies (co-op's extra enemies) object memory runs low and the
garbage collector prints "garbage collection critical" and "removing
objects" at every pass, filling the screen for the host and every client.
They are the game's own chatter, so they show as game.console_log says, as
the sound and texture caches' already do; the log keeps every line.
The host's notice of a broken surface carries no damage effect. The client filled a damage record with zeros and then read tag 0, which is the scenario, and halted. Shards still spawn. They skip the kick from an effect this machine never received.
The frame scratch pool was still the Xbox size after the particle pool grew eight times, so a busy level spent it every frame. It now grows with that particle pool, and a frame that still runs out is logged once.
A console line longer than the buffer halted inside the error printer, before the reason was written to the log. Long lines are cut to fit, and the reason is written first.
A lens flare is projected on the CPU (w 1, its depth in z) and drawn
without the depth test, which multiview took for something at infinity,
as the sun's flares are. A weapon's own lights have flares too, at its
markers half a metre from the eyes (the plasma pistol's at its prongs,
the plasma rifle's at its muzzle): each eye saw its flare where infinity
puts it, a green ball at the barrel the eyes could not fuse. While flares
are drawn (rasterizer_lens_flares_draw), each eye's is now placed by the
flare's depth, as its occlusion test already was; the sun's are far
enough for infinity either way. Whether a flare shows at all depends on
its occlusion test, which is why the balls came and went, and showed on
the Frame more than on the Quest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixups for #102, #103 and #105-#107: the version raised for the glass,
scenery, cutscene cluster and revert messages, NETCODE.md brought up to
date, co-op's followers' object lists freed as soon as they are used, and
extra enemies that find no free ground placed on the rings they used before
rather than left out (at 32x a quarter of them were), with the places found
taken not tried again for the rest of the squad, and the way to a place
looked along level.
- vr.log_performance: frames with game ticks and without, how many late,
and their time by part (input, ticks, camera, draw, present)
- with XR_META_performance_metrics: per frame, each layer copy's time, the
waits for swapchain images and xrEndFrame
- debug.gl_thread: Mesa's GL thread on or off (mesa_glthread) for trying
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Silent Cartographer with 32x co-op enemies (484 actors) went from 53
to about 97 fps on the Linux PC, with the same game and the same picture:
cluster list removals start where the reference was put, full data arrays
are not walked again, object lighting keeps each cluster's drawn lights
and passes over far ones without their distance, the per-texture profile
timer only runs while something reads it, and the renderer binds streams
once per draw, caches its texture, target and shader lookups, and no
longer makes Mesa's GL thread wait on large uploads or visibility tests.
What the followers of player0 do (teleport, suspend, get out, board) is a
function each in coop_scripts.c, so every engine hook is one call. The
cutscene skip reverts only in a local game or on a co-op host, written as
two plain branches; a non-co-op network host no longer reverts alone.
Breakable surfaces (glass) are broken by each machine's own projectiles
with its own random damage, so a pane could shatter on the host and stay
whole, still blocking, on a client. In co-op the host now sends each break
(and a few again every refresh, for late joiners), and a client breaks the
same surface with its shards.
Destructible scenery and devices (lights, panels) are the map's, at the
same index on every machine, but only damage to units was synced: a
client's shots never damaged them, and what the host broke didn't break
or play its effects on the clients. A client now reports its hits on them
like any other, the host checks and deals them, and every client deals the
host's damage to its own copy, which breaks, plays its effects and is
destroyed as the host's was.
Both are new message kinds that a client without them ignores, so the
network version is unchanged.
The pause menu's revert (main_revert_map) reverted a network client's own
game state, leaving it out of step with the host. A client now ignores it,
and the cutscene skip's local revert, which a client only reaches outside
the co-op vote, is the host's or a local game's alone.
The scripts check game_safe_to_save before some cutscenes (the Maw's
bridge). The game's test fails while any enemy sees any player, so with
players spread out a fight elsewhere held up the cutscene a player had
reached. For the scripts in co-op, one player alive, on the ground and
unseen is enough; checkpoints keep the game's own test.
The campaign scripts only run on the host, against every player's unit,
but some of their habits assumed split-screen:
- volume_test_objects_all on the players waited for every player in the
volume at once: the Maw's bridge cutscene, a30's Pelican pickups. In
co-op one player arriving is enough now.
- Scripts name players as player0 and player1, so everyone past the
second was left out of what they did: Pelican loads (a30's pickups,
insertions), getting out, cutscene teleports and freezes, starting
profiles. Those players now follow player0 (coop_scripts.c).
- A cutscene filmed far from every player (b30's sword Elite at the
shaft door) wasn't active on clients: the cluster the scripts force
active (object_pvs_activate) now rides in the presentation's spare
bytes, and what the host's camera films is sent to clients as often as
what is next to their players.
- A mission the scripts fail (d40's timer, a50's Keyes) only respawned the
players, leaving them in the failure cutscene for good. It now reverts
like the cutscene skip does; everyone dying still respawns at the last
checkpoint. The scripts' game_revert reverts the same way.
The network version is unchanged: the presentation keeps its size, and a
host without the active cluster sends zeroes, which read as none.
An extra enemy was placed on a ring around its starting location, trying
one spot per ring, and checked only against walls. When the 8 tries failed
(tight corridors, the Maw's interiors) it went on the starting location
itself, on top of the squad's own actor, and nothing stopped it landing on
another starting location's actor a metre away.
Now every spot of each ring is tried (a ring holds more spots the further
out it is), and a spot needs ground under it, no wall in the way and no
biped within 0.6 m or vehicle around it. A starting location with no room
passes its enemy to the squad's next one, and with no room anywhere the
enemy isn't placed. Dropship riders released beside the ones who jumped
out are spread the same way.
The bots fill the game up to its maximum; a machine joining the lobby
keeps its place (no bot added for it) and its player takes the last
bot's; the game lists as open while a bot could leave.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- bots: no more than the game's MAXIMUM PLAYERS has room for beside the
people (instead of raising it); the host options show and change the
bots there are
- VR options hint on the PC main menu drawn whole (left-aligned)
- the online and host panels: a click beside them or on their key's B
closes them, on the online panel's X enters a code
- spinners' arrows (drawn just outside them) clickable
- Android flat (the emulator): the mouse drives the menus' pointer, for
testing the menus' clicks with adb shell input mouse
- Frame launcher: Lepton's logcat of the game's container in logcat.txt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- menu_tags.c builds the PC menus in VR too unless vr.menus = "xbox"
- their text fields (Server Setup's game name) typed on the Xbox's
on-screen keyboard in VR
- join code / online panel on the PC Server Browser (X or click its hint),
host options (Y or click) and join code on the PC lobby, VR options on
the PC main menu
- bots: room made for them beside the people (Server Setup's maximum
players), none in co-op
- player_name_valid accepts trailing spaces (Xbox menus pad new profile
names, "New001 ", which the PC lobby refused)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>