Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 d4c1fbe9a7 Release: Steam Frame installer, release signing, Frame import kept
- A Steam Frame installer for players, opened in Desktop Mode
  (Install-Halo-VR.desktop, a release asset): it downloads the latest
  release's bundle (tools/steam_frame_bundle.py) and runs
  steam_frame_install.sh, with zenity dialogs and no keyboard: the APK and
  launcher in ~/Documents/HaloCE, the maps copied out of a disc image it
  finds or is given (steam_frame_xiso.py, the app's XisoExtractor in
  Python), and Halo VR in the Steam library with its pictures and OpenVR
  set. Running it again updates the game.
- The release job adds the bundle and the desktop file to each release.
- build.gradle signs with port/android/signing.properties (ignored) when
  the workflow's key is not given, so local builds and releases install over
  each other.
- On the Frame the launcher and host take ~/Documents/HaloCE whenever it can
  be written, not only once it holds maps: a disc image imported in the app
  is no longer emptied with Lepton's folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:06:21 +02:00
Stefan MaierandClaude Opus 5.5 1f238ff045 Merge feature/vr-lefthanded-graphics: left-handed mode, graphics presets, refresh rate and resolution in VR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:21:38 +02:00
Stefan MaierandClaude Opus 5.5 88354e5c6c VR roadmap: left-handed mode and graphics presets done on both headsets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:21:35 +02:00
Stefan MaierandClaude Opus 5.5 8cdce233e4 Android settings: config.toml written beside itself and renamed over
A config.toml pushed by adb belongs to the shell and the app cannot open it
for writing, so the VR options' changes (handedness, graphics, refresh rate,
resolution) lasted only until a restart. Writing a temporary file and
renaming it over the old one replaces it all the same (the folder is the
app's), and a write cut short leaves the old file whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:55:22 +02:00
Stefan MaierandClaude Opus 5.5 81137e3af3 VR roadmap: left-handed mode and graphics presets good on the Steam Frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:40:01 +02:00
Stefan MaierandClaude Opus 5.5 fb670e79b9 VR roadmap: the left hand's ammo counter reads the right way round
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:33:11 +02:00
Stefan MaierandClaude Opus 5.5 8022a0b4a1 VR left-handed: the assault rifle's ammo counter reads the right way round
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>
2026-10-06 20:33:11 +02:00
Stefan MaierandClaude Opus 5.5 696383f191 VR roadmap: the left hand's mirrored ammo display, an open point
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:01:39 +02:00
Stefan Maier 9a33ecd2d3 Revert "VR left-handed: the assault rifle's ammo display reads the right way round"
This reverts commit 530a32b64889c24e64c5e805949e31df1c68fb73.
2026-10-06 20:01:19 +02:00
Stefan Maier 9447bc2ae4 Revert "VR left-handed: the ammo display's subtree found without hanging"
This reverts commit 8e40d6ecacd92f3938e329e528a27c3eddd8dbe1.
2026-10-06 20:01:18 +02:00
Stefan Maier f04c8c4045 Revert "VR left-handed: a part is drawn mirrored only where all its nodes are"
This reverts commit 1a6061b932e7f293cc40351f90f4d0d68edb2bd2.
2026-10-06 20:01:18 +02:00
Stefan MaierandClaude Opus 5.5 b026590824 VR left-handed: a part is drawn mirrored only where all its nodes are
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>
2026-10-06 19:54:01 +02:00
Stefan MaierandClaude Opus 5.5 2a2b2cdbf3 VR left-handed: the ammo display's subtree found without hanging
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>
2026-10-06 19:50:52 +02:00
Stefan MaierandClaude Opus 5.5 5861e57d99 VR left-handed: the assault rifle's ammo display reads the right way round
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>
2026-10-06 19:47:53 +02:00
Stefan MaierandClaude Opus 5.5 50d685e465 VR roadmap: step 2 in part built (left-handed mode, graphics presets, refresh rate)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:33:27 +02:00
Stefan MaierandClaude Opus 5.5 0e6b006878 VR options: graphics, refresh rate and resolution rows
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>
2026-10-06 19:30:32 +02:00
Stefan MaierandClaude Opus 5.5 ca73ba7909 VR: graphics presets (vr.graphics)
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>
2026-10-06 19:28:48 +02:00
Stefan MaierandClaude Opus 5.5 c254d6df05 VR: left-handed, the rumble's motors follow the hands
The game's light (right) motor rumbles the weapon hand, the heavy one the
free hand (halo_vr_weapon_hand), as right-handed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:26:40 +02:00
Stefan MaierandClaude Opus 5.5 9910c4a9dc VR options: weapon hand and mirrored controls, two pages
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>
2026-10-06 19:26:03 +02:00
Stefan MaierandClaude Opus 5.5 b1d59e654d VR: left-handed, the motion tracker on the right wrist
The wrist layer goes on the free hand's wrist: halo_xr_submit.wrist_hand
(new, after wrist_down); host_xr.c keeps both grips and places the layer
on the right wrist as the mirror image of the left one's, read the right
way round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:22:25 +02:00
Stefan MaierandClaude Opus 5.5 1f3d1be207 VR: left-handed controls, mirrored with vr.mirror_controls
Left-handed, in play, vr.c hands xinput_sdl.c the controllers with the
weapon hand's trigger and grip as the right side's (fire, melee, the
wheel's throttle) and the free hand's as the left's (grenade, foregrip,
brake). vr.mirror_controls (default true) mirrors the sticks (move on
the right, turn and steer on the left), their clicks, the bumpers and
the face buttons too: on the Quest X/Y for A/B; on the Steam Frame, whose
A, B, X and Y are all on the right controller, the left D-pad takes them
as their mirror image (down A, left B, right X, up Y) and they take the
D-pad, so every function stays reachable. The menus keep the buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:21:36 +02:00
Stefan MaierandClaude Opus 5.5 d97de6d499 VR: left-handed mode, the weapon in the left hand
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>
2026-10-06 19:20:59 +02:00
Stefan MaierandClaude Opus 5.5 fbe48a869f VR roadmap: steps 1 and 3a done, in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:53:21 +02:00
Stefan MaierandClaude Opus 5.5 206eb0b52b VR roadmap: the Steam Frame's tests done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:46:55 +02:00
Stefan MaierandClaude Opus 5.5 1c66715a19 VR roadmap: the Quest's tests done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:34:15 +02:00
Stefan MaierandClaude Opus 5.5 87f336df78 VR roadmap: 3a tried on the Quest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:30:49 +02:00
Stefan MaierandClaude Opus 5.5 0a86e406c8 VR arms: the hand's roll leads the elbow; the free arm bent by default
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>
2026-10-06 17:56:47 +02:00
Stefan MaierandClaude Opus 5.5 c09b40a2ef VR arms: the free left arm solved first, its hand turned after
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>
2026-10-06 17:54:15 +02:00
Stefan MaierandClaude Opus 5.5 1e2006d06c VR arms: the elbow bends in the arm's plane, the shoulders higher
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>
2026-10-06 16:47:45 +02:00
Stefan MaierandClaude Opus 5.5 e69e087602 VR arms: the free arm rigid by default, the shoulders further back
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>
2026-10-06 15:59:41 +02:00
Stefan MaierandClaude Opus 5.5 87a38e2895 VR roadmap: the pending headset tests, performance among them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:45:44 +02:00
Stefan MaierandClaude Opus 5.5 788ab74a6b VR roadmap: step 3a built in feature/vr-arms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:37:07 +02:00
Stefan MaierandClaude Opus 5.5 6d2dcfaaec VR arms 3a.6: vr.body (arms, hands, off) and vr.fingers in the VR options
- 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>
2026-10-06 11:35:07 +02:00
Stefan MaierandClaude Opus 5.5 98e848de39 VR arms 3a.5: the fingers posed
- 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>
2026-10-06 11:33:12 +02:00
Stefan MaierandClaude Opus 5.5 7754532683 VR arms 3a.4: the IK core from HaloCE-Quest-VR, the free left arm bent
- 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>
2026-10-06 11:31:16 +02:00
Stefan MaierandClaude Opus 5.5 19b0bcb42e VR arms 3a.3: the shoulders follow the torso, not the head
- 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>
2026-10-06 11:26:24 +02:00
Stefan MaierandClaude Opus 5.5 f30dd50317 VR arms 3a.2: the fingers' input (touch, trigger, grip)
- host_xr: two actions, the thumb's touch (stick, buttons, thumb rest,
  the Frame's D-pad and View) and the index finger's (trigger), bound
  for the Touch, Index and Steam Frame profiles (paths after
  HaloCE-Quest-VR, CC0, and Valve's "Steam Frame Input" page) as optional
  bindings: a profile refusing them is suggested again without them.
- ABI: HALO_XR_BUTTON_THUMB_TOUCH, _INDEX_TOUCH and _TOUCH_KNOWN (the
  runtime gives the thumb's touch at all) in the buttons' free bits; the
  structures keep their size (android_abi_check: none unsafe).
- vr.c: each hand's thumb, index, middle and last two fingers, curled
  from those (the thumb half curled where touch is unknown, the hand
  round a held grenade), eased by HaloCE-Quest-VR's critically damped
  spring; halo_vr_finger_curls() hands them to the game; vr.fingers
  (default on) turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:24:58 +02:00
Stefan MaierandClaude Opus 5.5 07a15190b4 host_xr: suggest() takes up to 64 bindings, says when it cannot, and retries
suggest() copied a profile's bindings into an array of 32 and silently
dropped the rest; the Steam Frame's profile already has 26, so a few
more would have lost bindings without a word. It takes 64 now and logs
an error past that, logs the runtime's result when a profile is
refused, and suggest_bindings() takes optional bindings besides: should
the runtime refuse the profile with them (one unknown path refuses the
whole suggestion), it is suggested again without them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:23:11 +02:00
Stefan MaierandClaude Opus 5.5 f254be4b45 VR arms 3a.1: the first-person pose built once a frame, not per eye
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>
2026-10-06 11:22:45 +02:00
Stefan MaierandClaude Opus 5.5 a6350d207a VR arms 3a.0: time the first-person pose, log the hands' skin
- 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>
2026-10-06 11:22:08 +02:00
Stefan MaierandClaude Opus 5.5 1929702f5b Steam Frame CPU: Mesa's trace markers quiet, no glGet in the counters' copies; VR roadmap
- Valve's Mesa in Lepton writes a kernel trace marker around every flush
  and context switch; its descriptors on trace_marker are pointed at
  /dev/null once the GL context and the OpenXR session exist (after
  astromaddie/HaloCE-VR, CC0)
- host_gl_counters_snapshot/_read no longer ask GL for the copy bindings
  (each glGet waits for Mesa's GL thread): they are left unbound
- port/android/VR_ROADMAP.md: what is done and the agreed next steps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:58:04 +02:00
Stefan MaierandClaude Opus 5.5 9d6b980ba6 Merge upstream (OpenCE 9e4f8a2d): performance with many enemies, co-op fixes, anti-aliasing, per-pixel lighting, shadow resolution, reverb, password lobbies, network version 20
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>
2026-10-06 09:29:14 +02:00
MrBruh 9e4f8a2dcb Merge pull request #115 from eltanschauung/fix/attached-light-storage
Fix missing dynamic lights and attached glows on Windows
2026-10-06 19:09:18 +13:00
eltanschauung 53c268b8a6 Preserve existing workflow line endings 2026-10-06 01:18:49 -04:00
eltanschauung 592be6270a Run light storage regressions in Windows and Linux CI 2026-10-06 01:17:37 -04:00
eltanschauung 8817c37470 Test native light storage with ordinary and LTO links 2026-10-06 01:17:23 -04:00
eltanschauung d635d83725 Fix dynamic lighting storage corruption on Windows 2026-10-06 01:16:42 -04:00
MrBruh 2a6dc2b738 Show the server browser's lock icon 2026-10-06 18:08:45 +13:00
MrBruh 6112dcfc5a Password-protected public lobbies 2026-10-06 17:51:54 +13:00
MrBruh 62fa7e13ef Co-op player collisions option 2026-10-06 17:03:38 +13:00
MrBruh 9a12828762 Play MP maps alone 2026-10-06 16:27:27 +13:00
MrBruh 7e82e7128e New settings in the settings menus
Video Setup has Anti-Aliasing (Android's choices its own), Shadow Resolution and Per-Pixel Lighting, its rows closer and the help lower for them to fit; Audio Setup has Reverb.
2026-10-06 15:36:22 +13:00
MrBruh 94882796b7 Anti-aliasing
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.
2026-10-06 15:24:59 +13:00
MrBruh 3dba558e4f Per-pixel model lighting
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.
2026-10-06 15:08:32 +13:00
MrBruh 1dc533fe18 Shadow map resolution setting
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.
2026-10-06 14:59:21 +13:00
MrBruh 8a8e70599f I3DL2 reverb and muffled sounds
Based on #44 and #46 by Tyberious. 3D voices send to an I3DL2 reverb of the camera's sound environment, and obstruction and occlusion muffle their direct path above the HF reference (occlusion their send too). Both use the fixed distance; a maximum distance under the minimum is held at it. A change of environment crossfades the delays and gains over 85 ms, the filters' coefficients ramp over each mix, the diffusers, lines and filters flush denormals, and the reverb stops while it is quiet. audio.reverb (default true) is read again when the settings change.
2026-10-06 14:39:41 +13:00
MrBruh b115a41263 Resample with a table per phase
Fixup for #42: a voice at the output rate or slower blends two rows of weights by its phase instead of working out each tap's (the same low pass, a third of the work), and a voice turned all the way down only moves on.
2026-10-06 13:58:46 +13:00
MrBruh 320464258a Merge pull request #42 from Tyberious/audio-resampling 2026-10-06 13:57:04 +13:00
MrBruh f5bb75c27c Widen the constant checkpoint serial
Fixup for #52: the checkpoint serial main added since is 64-bit too, as the serials it is compared with are.
2026-10-06 13:56:58 +13:00
MrBruh 5cd1daff77 Merge pull request #52 from Tyberious/constant-serial-wrap 2026-10-06 13:56:35 +13:00
MrBruh 6329be8aa1 Merge pull request #53 from Tyberious/interpolation-snaps 2026-10-06 13:56:35 +13:00
MrBruh 7a2f4caa50 Merge pull request #43 from Tyberious/audio-limiter 2026-10-06 13:56:35 +13:00
MrBruh 938c37a669 Merge pull request #45 from Tyberious/audio-distance 2026-10-06 13:56:35 +13:00
MrBruh fae3fbad69 Merge pull request #41 from Tyberious/adpcm-decoding 2026-10-06 13:56:35 +13:00
MrBruh 5a35ea2b20 Network version 18
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.
2026-10-06 13:39:26 +13:00
MrBruh 2e179ea90f Merge pull request #112 from innerhat-dev/networked-break-and-frame-scratch 2026-10-06 13:34:53 +13:00
MrBruh 9b517e554c Merge pull request #99 from xshxdex98/coop-shader-permutation 2026-10-06 13:29:02 +13:00
MrBruh 595bbc5611 Merge pull request #113 from xshxdex98/quiet-garbage-collection 2026-10-06 13:29:02 +13:00
MrBruh 761210d405 Harden map and network input 2026-10-06 13:26:43 +13:00
xshxdex98 7ba780ef59 Garbage collection's notices are chatter: on screen only with console_log = "all"
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.
2026-10-05 18:55:53 -05:00
justinfilip d9c43e3478 Keep a networked glass break from reading the scenario as a damage effect.
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.
2026-10-05 17:19:35 -05:00
Stefan MaierandClaude Opus 5.5 c04ce7fd00 Steam Frame: each eye drawn apart (no multiview on SteamVR)
With both eyes drawn at once (multiview) the eyes' pictures did not fuse
on the Steam Frame, now and then: each eye alone sharp, both together
doubled as the head turned, in the menus too. With vr.multiview = false
the user saw no doubling, at the same frame time (the Frame's time goes
to the GL driver either way). debug.multiview_steamvr brings multiview
back there for finding the cause; the Quest keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:54:53 +02:00
Stefan MaierandClaude Opus 5.5 3f06b1b9d8 Revert "Steam Frame: the layers' images acquired before the frame waits"
This reverts commit 24e583c0. With the images acquired before xrWaitFrame
the frames came fast (about 8 ms of work) but the picture doubled again
on the Steam Frame as the head turned (181 of 3753 frames reprojected);
without it (673b32b1) it did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:40:35 +02:00
Stefan MaierandClaude Opus 5.5 1b2d058a04 VR multiview: lens flares placed by their depth in each eye
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>
2026-10-05 22:34:56 +02:00
Stefan MaierandClaude Opus 5.5 b5fa53ed95 Steam Frame: the layers' images acquired before the frame waits
With the copies deferred the wait only moved: acquiring the images took
8-10 ms a frame, the left eye's 6-8 of it. SteamVR's acquire (or its wait
for the image) waits for the GL work queued before it, the eyes' whole
drawing when the left eye's copy asked, the HUD's for the overlay. The
images the last frame filled are now acquired before xrWaitFrame, when
little is queued and the frame would wait for the headset anyway; one a
frame needs that was not acquired ahead is acquired as before. The
performance log shows the time acquiring ahead apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:34:56 +02:00
Stefan MaierandClaude Opus 5.5 1f2c10a3ca Menus' art: a texture outside the art's range is no menu bitmap
Every texture the renderer binds asks whether it is a menu bitmap
standing for its PNG (menu_art_texture), in a game too, where the PC
menus' bitmaps are loaded for the pause menu's settings: a walk over the
150-odd registered bitmaps for each texture of each draw. Their textures
lie together, so one lowest/highest check answers for the map's own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:11:05 +02:00
Stefan MaierandClaude Opus 5.5 12f1155752 Steam Frame: the headset's layer copies without waits in the frame
The Frame's game thread spent most of each 13.9 ms frame waiting, not
working: the left eye's copy into its swapchain took 4.5-7 ms, the right
one's right after it 0.7, each later copy 0.7-3. Every copy read the GL
state back to restore it (glGet*, glIsEnabled, the source's swizzle) and
released its image at once; with Mesa's GL thread (zink) each read waits
for the thread to run everything queued, each release for the GPU, so the
thread stood still for the eyes' whole drawing mid-frame, and frames came
late enough for SteamVR to reproject 2-8% of them (the doubled picture
when turning the head).

Deferred copies (SteamVR by default; debug.xr_deferred_copies 0 or 1 says
otherwise; the Quest's path is unchanged):
- nothing read back: a copy leaves GL as it drew and returns 2; vr.c has
  the renderer set its state again (d3d8_host_state_changed: the state
  cache invalidated, its vertex arrays bound again)
- no swizzle round trip: copies read render targets, which have none
- the images kept until the frame ends and released together before
  xrEndFrame; the eyes' work flushed to the GPU once they are copied
- the performance log times acquiring and releasing apart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:11:05 +02:00
MrBruh 2b0327bc80 Network version 17
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.
2026-10-06 08:59:22 +13:00
MrBruh 58aa352705 Merge pull request #107 from xshxdex98/sync-glass-and-scenery 2026-10-06 08:40:24 +13:00
MrBruh 59c4b2160e Merge pull request #106 from xshxdex98/coop-scripts-for-everyone 2026-10-06 08:40:24 +13:00
MrBruh 1cf336f0ff Merge pull request #105 from xshxdex98/coop-enemy-spread 2026-10-06 08:40:24 +13:00
MrBruh d10969dc83 Merge pull request #103 from xshxdex98/coop-backtrack-team 2026-10-06 08:40:24 +13:00
MrBruh fe1d51dd58 Merge pull request #102 from xshxdex98/coop-bsp-wait 2026-10-06 08:40:24 +13:00
Stefan MaierandClaude Opus 5.5 24e4123882 VR performance diagnostics: the frame's parts, the layer copies, Mesa's GL thread switch
- 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>
2026-10-05 21:33:00 +02:00
MrBruh 197c199473 Improve performance by ~80% with many enemies
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.
2026-10-06 08:13:44 +13:00
xshxdex98 0acc3b1381 Network damage: one place fills a damage event
damage_event_new fills the fields every damage event has, for scenery's
and units' alike.
2026-10-05 13:11:01 -05:00
xshxdex98 1e74e9b03e Co-op scripts: follower actions named, the skip's revert plainer
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.
2026-10-05 13:10:29 -05:00
xshxdex98 5d108579ed Co-op extra enemies: one function judges a spot
coop_enemies_spot_free makes every check of a spot (way, floor, headroom,
nobody there) in one place, each constant has its own comment, and the
file comment's wrapping is fixed.
2026-10-05 13:09:58 -05:00
xshxdex98 7a1ffca28b Network: glass and destructible scenery break the same for everyone
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.
2026-10-05 13:00:01 -05:00
xshxdex98 b843156fe8 Network: a client never reverts its game on its own
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.
2026-10-05 12:59:00 -05:00
xshxdex98 99a31ca82d Co-op: a cutscene waiting until it's safe waits for one safe player
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.
2026-10-05 12:58:59 -05:00
xshxdex98 9666575b39 Co-op extra enemies: same floor, clear of crates, room to stand
A spot's ground was looked for up to 2.6 m below it, so an extra enemy
could land under a ledge or catwalk, and only walls were in the way, so
it could stand inside a crate or a machine.

A spot now needs a floor within 0.5 m of the starting location's, nothing
in the way from beside the starting location (level, scenery, machines,
vehicles), and 1.6 m of room above its floor.
2026-10-05 12:58:21 -05:00
Stefan MaierandClaude Opus 5.5 8466846d45 Steam Frame: the frames' thread kept on the fast cores
Without XR_EXT_performance_settings (SteamVR in Lepton) the scheduler may
put the game's busy thread on the 2.3 GHz cores; frames then come late to
the compositor and the picture doubles as the head turns, now and then,
at a steady 72 fps. On SteamVR the thread that ends the frames is kept on
the cores no slower than 90% of the fastest (HALO_XR_ANY_CORE: not); the
metrics line says which core it is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:53:47 +02:00
Stefan MaierandClaude Opus 5.5 bc580cf1cf VR: the compositor's performance counters in the log (XR_META_performance_metrics)
Every five seconds, where the runtime offers them (SteamVR on the Steam
Frame): to tell frames the compositor showed late or reprojected from the
game's own pace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:46:54 +02:00
xshxdex98 8e6b9a211e Co-op: triggers, cutscenes and failed missions work for every player
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.
2026-10-05 12:43:48 -05:00
xshxdex98 d0016bca89 Co-op extra enemies: spread onto free ground, never stacked
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.
2026-10-05 12:41:08 -05:00
Stefan MaierandClaude Opus 5.5 3d575f32ed Bots on foot: ramps, rails, door frames and things out of reach
- a way is clear only at the knees and the chest (a rail with room under
  it, a slit in a wall), and for a shortcut along a path at the shoulders
  too (a door frame)
- stuck below a near point (a step, a ramp's lip): a jump on towards it
- a thing to pick up it comes no nearer to for 2.5 s is given up
- stuck three times in five seconds: the goal given up, somewhere away
- at the point it walks to: arrived, not stuck

Emulator, Blood Gulch, 15 bots: CTF stuck events 264 -> 153, mid-field
26% -> 32%, kills 26 -> 33; team slayer stuck events 860 -> 560.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:33:16 +02:00
Stefan MaierandClaude Opus 5.5 727b9ca80d Bots on foot: no crowding at things out of reach, spread over the enemies, hold spots they can reach
- a thing to pick up that it stands by and is not offered (on a ledge,
  out of reach) is given up for 20 s; up to four remembered, in the
  fight too (the crowds jumping at the base's weapons)
- stuck: the side step goes to the open side
- goals: each bot one of the three nearest enemies, and beside them while
  far, so a team spreads out
- holding a spot: stops near it, and one it cannot reach falls back to
  the place's centre

Emulator, Blood Gulch, 15 bots: CTF stuck events 1793 -> 264; team slayer
stuck windows 32% -> 9-15%, time by the bases 85% -> 68-73%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:52:30 +02:00
Stefan MaierandClaude Opus 5.5 99d7c924db Bots: a person joining a lobby the bots fill takes a bot's place
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>
2026-10-05 18:00:31 +02:00
Stefan MaierandClaude Opus 5.5 57d96e4681 VR PC menus: fixes from the first Frame test
- 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>
2026-10-05 17:45:41 +02:00
Stefan MaierandClaude Opus 5.5 177ad4d40d VR: the PC version's menus (vr.menus, on by default)
- 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>
2026-10-05 17:17:16 +02:00
Stefan MaierandClaude Opus 5.5 bfb668d498 Merge branch 'merge/upstream-2026-10-05'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:00:26 +02:00
Stefan MaierandClaude Opus 5.5 fc2b6551ca Merge upstream (OpenCE 13c14df9): online co-op campaign, online split screen, network version 16
Upstream's online co-op (network_actors.c, network_coop.c) ticks beside the
VR port's VR relay and bots; its message types end at 77, below the VR
port's from 240. The look and move flags the action test reads moved into
player_control_action_test_note: the head's turning goes there too. The
vertex shader's screen-space undo is upstream's on every platform now, with
the multiview eye offset after it on Android. The APK carries upstream's
brokers.txt beside the FDM layer stub; the updaters keep the VR port's own
repository (upstream's now points at OpenCommunityEdition/OpenCE).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:22:05 +02:00
Stefan Maier 5316a6b145 Merge branch 'frame/launcher' 2026-10-05 16:19:02 +02:00
Stefan MaierandClaude Opus 5.5 59b1fb422b Steam Frame: a library button that starts, updates and quits the game
The launcher gives Lepton what Steam gives an Android title of its own (a
compat data path, which the app's data links to after an install, and the
shader path), keeps a container id of its own, tries a start that ends at
once again (a new APK's first start only installs it), and stops when the
game was quit. Steam's "Exit game" cannot reach the game in Lepton's
container: the VR options get "Quit Halo" (asked twice), which on Android
now ends the process and leaves quit_requested.txt for the launcher. The
library's pictures are in port/android/steam_frame_grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:17:35 +02:00
xshxdex98 dc383d10da Co-op: clients draw AI units with their variant's shader permutation
An actor variant sets its unit's forced shader permutation after the unit
is made, and only the host runs the AI, so on clients Elite majors and
commanders drew the minor's armor (bitmap 0 of the cube maps instead of 1
and 2). The object creation message carries it in its spare byte, which
builds without this send as 0; a client applies only one that isn't 0, so
the message keeps its size and games hosted on builds without this keep
each tag's own.
2026-10-05 09:04:54 -05:00
xshxdex98 a143d9490d Co-op: tell a player held back at a loading zone what it waits for
Going back to a BSP the team has been in waits for two thirds of the team,
and a player on the trigger was never told so. They now see "Waiting for
your team to go back: 1 of 2 here", at once and every three seconds while
they wait. Each machine works it out for its own players with the count the
host decides by, now one function, so nothing new goes over the network.
2026-10-05 08:51:49 -05:00
xshxdex98 e55c29a97e Co-op: going back counts the team within 45 metres, not 12
Two thirds of the players had to be in the loading zone or within 4 world
units of whoever was in it. A team walking back through a doorway in a line
is longer than that, so going back almost never switched: the first player
walked on past the loaded level, fell and was brought back to the team.
15 world units fits a team moving together and still not a player who ran
back alone.
2026-10-05 08:51:49 -05:00
xshxdex98 16361bdef1 Co-op: a client's player waits on its floor for the host's BSP
The host's switch and its players' new positions travel separately, so a
client could hear where the host moved its player, in the new BSP, before
the switch itself, and drop through the floor until the BSP loaded. A
client now keeps its player on the floor of the BSP it has until the
host's position is somewhere it has loaded. Nothing changes on the wire.
2026-10-05 08:29:25 -05:00
Stefan MaierandClaude Opus 5.5 5ecd3075a0 Steam Frame: a launcher that starts the game as Steam starts Android titles
Lepton runs the APK in a container of its own, kept between starts, and
installs it anew whenever the APK is newer; the script starts it a second
time after a new APK's first start, which only installs it. With the APK in
~/Documents/HaloCE, updating the game is replacing that file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:17:54 +02:00
MrBruh 13c14df95c Merge pull request #101 from xshxdex98/coop-bsp-sync
Co-op: every machine stays on the host's BSP (network version 16)
2026-10-06 02:13:10 +13:00
xshxdex98 b525bf8514 Network version 16
A client's input says which structure BSP it has loaded (co-op).
2026-10-05 08:09:47 -05:00
xshxdex98 087ca8a875 Co-op: every machine stays on the host's BSP
Players in big sessions ended up on different BSPs:

- The host told clients about a switch only every half second, and not
  reliably, so a client could predict its player for a while in a BSP it
  hadn't loaded. A switch is now sent at once, reliably, ahead of the
  players' new positions.
- A client still loading reports so: its input carries the BSP it has
  loaded. Until it has the host's, the host takes none of its movement
  (which falls there with no floor) and none of its loading zones.
- After a switch, by a trigger, a script or a skipped cutscene, no trigger
  can switch again until every machine has loaded the new BSP (at most
  ten seconds), so no client falls two switches behind.
- A player the host can't place beside whoever crossed, when a big team
  runs out of room there, is placed beside another teammate instead of
  being left where they were.
2026-10-05 08:09:46 -05:00
MrBruh 24957ff6a0 Network version 15 2026-10-06 01:28:04 +13:00
MrBruh 402d4b8351 Merge pull request #97 from xshxdex98/coop-dead-vehicles 2026-10-06 01:24:23 +13:00
MrBruh d1825ab08d Merge pull request #96 from xshxdex98/coop-backtrack-gate 2026-10-06 01:24:22 +13:00
MrBruh 268ec84b74 Merge pull request #95 from xshxdex98/coop-allegiances 2026-10-06 01:24:21 +13:00
xshxdex98 8dc796028e Co-op: going back to a visited BSP needs two thirds of the team
One player who stayed behind or went AFK no longer blocks the team.
2026-10-05 07:14:28 -05:00
xshxdex98 8aa381ed14 Co-op: vehicles the host destroys explode on clients
Clients only had a kill path for bipeds, so a Ghost, Banshee or Wraith the
host destroyed stayed intact on clients. A vehicle the host's state says
is dead is now destroyed there too, with its explosion.
2026-10-05 07:01:37 -05:00
xshxdex98 c3f82fa9b2 Co-op: going back to a BSP already visited needs the whole team there
A loading zone brings the whole team to whoever crosses it. Into a new part
of the level that's what you want, but it let one player drag everyone
back. A switch back to a BSP the team has been in now waits until every
living player is in the trigger or near the player in it.
2026-10-05 07:01:32 -05:00
xshxdex98 5ff4b6ee8d Co-op: clients take the host's allegiances
The scripts make allegiances (ai_allegiance player human) and only the host
runs them, so on clients marines stayed enemies of the players: red reticles
and the wrong team. The host sends its ally and friend tables with the
presentation each tick.
2026-10-05 07:01:26 -05:00
MrBruh 91e838e2f6 Network version 14 2026-10-06 00:54:11 +13:00
MrBruh 520b8401db Merge pull request #93 from xshxdex98/test-death-timing-coop 2026-10-06 00:51:37 +13:00
MrBruh 2ed4a7b49d Merge pull request #92 from xshxdex98/coop-flood-dropships 2026-10-06 00:51:36 +13:00
MrBruh 503992d544 Merge pull request #91 from xshxdex98/coop-device-states 2026-10-06 00:51:35 +13:00
MrBruh ccc7df6611 Merge pull request #90 from xshxdex98/coop-loading-zones 2026-10-06 00:51:35 +13:00
xshxdex98 ddcbe79283 test_death_timing.py: stubs for the network co-op paths
The test compiles main.c's and players.c's death handlers against a fake
world; their network co-op branches call functions it had none of, so it
no longer compiled. Stubbed off, it covers the local game it was written
for, and passes.
2026-10-05 06:41:27 -05:00
xshxdex98 862d88737e Co-op: Flood bodies get up on clients; dropship doors open and close there
- A body the host's unit got back up from (a Flood combat form feigning
  death) gets up on the clients too, instead of its ragdoll being dragged
  about. The resurrection is one function both use (unit_port_resurrect).
- The scripts' unit_open and unit_close go to the clients, so dropships'
  doors aren't left open there.
2026-10-05 06:36:27 -05:00
xshxdex98 8610b28376 Co-op: doors and elevators are where the host has them
The host sends each device's position and power when they change (and with
the resent state), and a client puts its device there when the host's is at
rest and they differ, or when they drift apart while it moves. Clients only
had the device groups' values, which aim a device: a door could stop half
open, and an elevator be somewhere else on a client, so its players fell.
2026-10-05 06:36:27 -05:00
xshxdex98 9e6247e0cb Co-op: a loading zone brings the team along; only someone falling is rescued
- Crossing a BSP loading zone moves every other player to whoever crossed,
  as split screen does. Moving only some players left the rest where the
  next switch threw them back to a hallway, or under a floor.
- The rescue for a player outside the loaded BSP waits two seconds and
  only takes someone falling: a player riding an elevator through its
  shaft is left alone.
2026-10-05 06:34:00 -05:00
MrBruh dea5fe3b1e Fix crash past 256 actors 2026-10-06 00:29:42 +13:00
MrBruh f0804aa587 Extra enemies: per player or static multiplier 2026-10-06 00:08:26 +13:00
MrBruh 16ad4cc900 25% extra enemies option 2026-10-05 23:45:31 +13:00
MrBruh a38ede078a Co-op extra enemies per player 2026-10-05 23:25:15 +13:00
MrBruh fa85aa53f5 Kick command; co-op friendly fire on by default 2026-10-05 22:51:05 +13:00
MrBruh 8be9a038a1 Co-op friendly fire setting 2026-10-05 22:40:34 +13:00
MrBruh 783b591a6a Co-op defaults to 16 players and private 2026-10-05 22:27:36 +13:00
MrBruh e82d880060 Singleplayer and multiplayer map choice 2026-10-05 22:17:22 +13:00
MrBruh 7d0e587c51 Co-op review fixes 2026-10-05 21:02:04 +13:00
MrBruh 01586cfebf Merge pull request #89 from xshxdex98/coop-campaign 2026-10-05 20:29:19 +13:00
MrBruh de52060644 Host takes grenade damage only from grenades thrown 2026-10-05 19:54:33 +13:00
MrBruh ab7c507a29 Leaving the lobby after a game goes to the main menu 2026-10-05 19:39:57 +13:00
xshxdex98 ba6fe0a826 AI sync: any team the host's unit has goes with it, not only the campaign's 2026-10-05 00:59:33 -05:00
MrBruh 3f6c0c37a4 Friendly fire stops teammates' instant kills 2026-10-05 18:47:39 +13:00
xshxdex98 606109551d Co-op: friendly reticles, skipped cutscenes' sound, the end of a level, tutorial prompts
- AI units' teams go with their states, so friendlies aren't red on
  clients' reticles.
- A skipped cutscene's music and dialogue stop on the clients.
- Winning a level returns every machine to the lobby with the next level
  set up, instead of leaving them on its last screen.
- Help text carries whether the host shows it, so no prompt is dropped.
2026-10-05 00:46:51 -05:00
xshxdex98 8194c5a00b Online co-op: play the campaign together over LAN and the internet
Hosting a co-op level from the Map screen (CO-OP CAMPAIGN, a level and its
difficulty, then Server Setup) makes a network game on the campaign map
with no game engine, which is what co-op is. Only the host runs the
scripts and the AI; port/linux/game/network_coop.c sends clients what the
scripts do (cutscenes, camera, fades, HUD, sounds, devices, named objects,
animations), and network_actors.c drives the host's AI units on every
machine. Players spawn beside each other, respawn beside a safe teammate,
and spectate while dead or waiting. Skipping a cutscene is a vote.
Players can join a game in progress at any time.
2026-10-05 00:31:30 -05:00
MrBruh 737551fd51 Resolution, scaling and window size settings 2026-10-05 18:28:58 +13:00
xshxdex98 7a56c624f5 Merge upstream main 2026-10-05 00:07:13 -05:00
MrBruh 3ae09c3d2c Fade the radar wedge out fully 2026-10-05 17:23:47 +13:00
MrBruh 3a22816c2f Merge pull request #81 from FDervish/fix/antenna-stepped-per-tick 2026-10-05 17:15:38 +13:00
MrBruh 2853a42253 Clear an antenna's shape record when it is created 2026-10-05 17:13:27 +13:00
Fadi DerouicheandClaude Opus 5 39ece781ee Step the antenna chain in the tick and draw the frames between the ticks
An antenna's chain is a simulation of the Xbox's own step, an update a tick,
and the port was stepping it once a frame with the frame's own time. Its
springs, its carry and the points' physics then acted against the frames'
movement of it, so how far the chain moved per update depended on the frame
rate, and a vehicle at speed swung it far wider than a tick's step does.

- game.c steps the chain in the tick, beside the effects, which are already
  stepped there.
- antenna.c records the shape each tick leaves, relative to the point the
  vehicle's marker carries the base by. A frame drawn between two ticks is the
  part of the way between those two shapes that it is, carried to where the
  marker is now, so the chain stays on the vehicle it hangs from instead of
  lagging a tick behind it. The gate that left a chain that had not been drawn
  for five updates unstepped counted frames; it counts ticks now, which is
  what it counted on the Xbox.
- widgets.c no longer steps the chain from the widget update, which ran once a
  frame.
- game_time.c draws the per-frame effects and widgets that hang off objects
  (game_frame) with the pose between the last two ticks, the pose the renderer
  draws with, so what they place on an object is drawn where they put it.
- objects.c hands markers the matrices the renderer draws with, so an effect,
  a particle system or a contrail attached to a moving object follows it
  between the ticks too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 17:13:27 +13:00
MrBruh cf17201ec8 Split screen divider spans wide screens 2026-10-05 17:13:27 +13:00
MrBruh 45de17116b Merge pull request #82 from addisonbair/gamescope-fullscreen
Count a fullscreen window that gamescope never confirms as fullscreen
2026-10-05 17:01:51 +13:00
xshxdex98 7d56f34b83 Map screen: choose co-op campaign or multiplayer, then vanilla or custom maps
Hosting over LAN or the internet, the Map screen's list first offers CO-OP
CAMPAIGN or MULTIPLAYER. CO-OP CAMPAIGN offers CAMPAIGN or CUSTOM CAMPAIGN,
then a level and its difficulty. MULTIPLAYER offers VANILLA MAPS or CUSTOM
MAPS, then the maps, and goes on to the gametypes as before. B steps back
through the choices and leaves the screen from the first. Split screen
starts at VANILLA MAPS / CUSTOM MAPS.

The custom lists stay empty and a co-op level cannot be hosted yet: network
co-op and Custom Edition maps come in their own pull requests, which fill
them in.
2026-10-04 22:54:24 -05:00
MrBruh 3c7ba95ff5 Merge pull request #70 from startupfoundry/windows-no-console
Release builds of Windows without a console window
2026-10-05 16:51:22 +13:00
MrBruh 092505224a Merge pull request #25 from garfbargle/macos-android-build
Build the Android port on macOS hosts
2026-10-05 16:48:39 +13:00
MrBruh 079883d640 Merge pull request #59 from saulob/fix/startup-color-flash
Fix incorrect Colors on the initial menu frame
2026-10-05 16:39:03 +13:00
MrBruh 54faa1dab6 Merge pull request #87 from eltanschauung/fix/death-camera-timing-pr
Fix death camera and co-op respawn timing at high frame rates
2026-10-05 16:33:14 +13:00
MrBruh 273152cc25 Merge pull request #80 from FDervish/fix/vertex-explosion
Fix vertices exploding to the screen centre near the camera plane
2026-10-05 16:07:31 +13:00
MrBruh 9bdd7f30c0 Merge pull request #86 from thelinkin3000/fix/host-again-after-backing-out
Make a new game each time Create is chosen
2026-10-05 15:53:59 +13:00
MrBruh 685bf260a0 Either co-op player picks the level 2026-10-05 14:47:11 +13:00
MrBruh 933ed2b7b8 Point updates at OpenCommunityEdition/OpenCE 2026-10-05 14:21:03 +13:00
MrBruh c115f5292d Split screen joins of games under way 2026-10-05 14:06:51 +13:00
eltanschauung 0f22c1d592 Add standalone death and co-op regression tests 2026-10-04 20:33:17 -04:00
eltanschauung fb1abedd8b Keep dead camera target when no living player exists 2026-10-04 20:32:59 -04:00
eltanschauung cd345aac7c Count death and co-op respawn delays in game ticks 2026-10-04 20:32:31 -04:00
MrBruh 7e00135d31 Fix menu crash with no network client 2026-10-05 12:59:15 +13:00
thelinkin3000andClaude Opus 5.5 95416b2cfc Make a new game each time Create is chosen
Backing out of a hosted game keeps its server: the lobby's last local
player leaving pauses it (netgame_unjoin_player). Choosing Create again
threw the host's client away, but network_game_start_new_server makes and
joins a server only when there is none, so the new client never joined the
one left over. The lobby then had nobody in it: START added no player, B
could not leave (not in pregame), and the server's map changes failed to
reach the client that was gone.

Create now disposes the client and server first, so it always makes a new
game and joins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:37:36 +00:00
MrBruh ffc1512dcb Read brokers.txt without SDL on Android 2026-10-05 11:27:05 +13:00
MrBruh 88a7c07ec7 Brokers in brokers.txt 2026-10-05 11:15:11 +13:00
Stefan Maier 8e8713681d Merge remote-tracking branch 'origin/fix/multiview-desktop-compile' 2026-10-04 22:59:55 +02:00
Stefan Maier d7099f3e40 Fix desktop link: bot machine check and glClearDepthf were Android-only
network_game_server_is_bot_machine (cea7abfc, Bots as players of the
host) was written into the HALO_ANDROID block of network_server_manager.c
while bots.c calls it on every platform, and so was the array it reads;
both move out of the guard (on the desktop nothing marks a bot machine,
so the check is always false there, the writes stay Android-only).

glClearDepthf (dc21f4b4, Play in VR on OpenXR headsets) is ES-only:
GL_FUNCTIONS declares it for Android's loader and the define at the top
maps glClearDepth to it, but the desktop context links against GL's
glClearDepth. The clear now calls glClearDepth, which the Android define
turns into glClearDepthf as before.
2026-10-04 22:37:17 +02:00
Stefan Maier 4ba36d7305 VR: fix desktop build, targets_multiview was Android-only
6c3f4ce2 (VR: draw both eyes in one pass) changed prepare_draw to pass
targets_multiview to vertex_shader_get, but the variable is declared and
kept inside the HALO_ANDROID guard (L887): Linux and Windows failed to
compile with 'use of undeclared identifier targets_multiview', Android
was fine. On the desktop there are no multiview layers to stand in for
the back buffer, so the desktop call passes FALSE.
2026-10-04 22:25:21 +02:00
MrBruh e3389991ac Online split screen 2026-10-05 09:18:40 +13:00
Stefan Maier 855f083117 Merge branch 'frame/performance' 2026-10-04 22:04:33 +02:00
Stefan Maier 6f3786de91 Merge branch 'fix/vr-shot-origin' 2026-10-04 22:04:33 +02:00
Stefan MaierandClaude Opus 5.5 493713b0a6 Steam Frame: the refresh rates offered in the log; its rate and speed noted
The runtime offers only the rate SteamOS is set to. At 72 Hz the game holds
it with multiview (about 11.6 ms a frame); at 90 Hz it cannot, and the
frames SteamVR makes up showed as a doubled picture when the head turned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:47:56 +02:00
Stefan Maier bf356fc58a Merge branch 'fix/vr-shot-origin' into frame/controllers 2026-10-04 21:42:21 +02:00
Stefan MaierandClaude Opus 5.5 6a7b919522 VR: shots fly along the aim, not above it
A shot fired from the muzzle of the weapon in the hand was kept inside the
level by the game's test of the line from the eyes to it, which in VR runs
down through the player's own body: the test hit it and moved the shot up
towards the eyes, from where it flew parallel to the aim, a few centimetres
above the crosshair and the laser (measured: up to 7 cm at 3 m). In VR the
line is tested against the level alone (a hand through a wall still does
not fire through it); measured after: the impacts on their rays, on
average within a millimetre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:42:07 +02:00
Addison Bair 20861c06d2 Count a fullscreen window that gamescope never confirms as fullscreen
gamescope (the Steam Deck's Game Mode) makes the window the size of the
display but never confirms the fullscreen request, so SDL leaves
SDL_WINDOW_FULLSCREEN unset. platform_screen_mode then reported a window,
the game drew the Xbox's 640x480, and gamescope stretched it to the
screen, soft and 4:3. A window that was asked to be fullscreen and covers
its display now counts as fullscreen too, so the game draws at the
display's resolution and shape (1280x800 on the Deck).
2026-10-04 19:30:07 +00:00
Fadi DerouicheandClaude Opus 5 fc7271fd52 Step the antenna chain in the tick and draw the frames between the ticks
An antenna's chain is a simulation of the Xbox's own step, an update a tick,
and the port was stepping it once a frame with the frame's own time. Its
springs, its carry and the points' physics then acted against the frames'
movement of it, so how far the chain moved per update depended on the frame
rate, and a vehicle at speed swung it far wider than a tick's step does.

- game.c steps the chain in the tick, beside the effects, which are already
  stepped there.
- antenna.c records the shape each tick leaves, relative to the point the
  vehicle's marker carries the base by. A frame drawn between two ticks is the
  part of the way between those two shapes that it is, carried to where the
  marker is now, so the chain stays on the vehicle it hangs from instead of
  lagging a tick behind it. The gate that left a chain that had not been drawn
  for five updates unstepped counted frames; it counts ticks now, which is
  what it counted on the Xbox.
- widgets.c no longer steps the chain from the widget update, which ran once a
  frame.
- game_time.c draws the per-frame effects and widgets that hang off objects
  (game_frame) with the pose between the last two ticks, the pose the renderer
  draws with, so what they place on an object is drawn where they put it.
- objects.c hands markers the matrices the renderer draws with, so an effect,
  a particle system or a contrail attached to a moving object follows it
  between the ticks too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:45:28 +01:00
Stefan MaierandClaude Opus 5.5 c1305fa94f Steam Frame: its controllers as themselves, with the Xbox's layout
SteamVR offers the Frame's controllers by their own profile
(XR_VALVE_frame_controller_interaction); without bindings for it, it mapped
the Quest's onto them, and X and Y landed on the left one's D-pad. Now: the
right one's A, B, X, Y and menu button are the game's A, B, X, Y and Start
(the Xbox's layout), the left one's View is Back and its D-pad the D-pad,
the bumpers switch grenades and the flashlight. The VR options' page of
buttons shows the Frame's layout there. The runtime's extensions and the
controllers' profile are in the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:28:13 +02:00
Fadi DerouicheandClaude Opus 5 3d2c04d675 Fix vertices exploding to the screen centre near the camera plane
The screen-space conversion that the Xbox's vertex shaders do is undone by
multiplying the screen position by w again, which is lossy near the camera
plane: the conversion takes the reciprocal of w (rcc), which clamps there, and
1/w rounds differently on each GPU.

Worse, where w is zero or not a number the reconstruction leaves the vertex at
the clip-space origin. That point is inside the frustum, so nothing clips the
triangle away, and OpenGL's zero divided by zero puts the vertex in the middle
of the screen: the triangle is then drawn from wherever its other vertices are
out to there. On the first-person weapon, whose pose follows the camera and so
reaches the camera plane, that is a streak across the whole view. The Xbox's
own divide sends such a vertex to infinity and its clipper discards the
triangle.

Compute the final position from the clip-space position that the shader keeps
whenever it takes the reciprocal of w, so that nothing is reconstructed by
dividing; and where w is zero or not a number, put the vertex behind the
camera (w = -1), which the clipper also discards, as the Xbox did.

Both paths were behind the Android build's #ifdef, so desktop GL only ever
used the lossy reconstruction. The y flip and depth range that follow remain
per platform.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:27:35 +01:00
Stefan MaierandClaude Opus 5.5 0fc85d6c67 Steam Frame: the overlays' alpha from their colour on SteamVR
SteamVR draws a quad's texels by their alpha alone, and the game's menus,
HUD and text have none in their additive parts: on the Frame nothing showed.
On SteamVR (the runtime's name says) the copy into the quads' images takes
their alpha from their colour where it is higher; HALO_XR_QUAD_ALPHA
(colour, keep) overrides, for trying. The Frame's notes updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:17:43 +02:00
Stefan MaierandClaude Opus 5.5 c0a08f395e Steam Frame: the game's data in the SteamOS user's Documents when it is there
Lepton empties the app's own folders (and uninstalls the app) whenever it
starts again; Documents is the SteamOS user's and kept. When
/home/steamos/Documents/HaloCE holds the maps and is writable, the launcher
and the game take it as the data folder (maps, config.toml, saves, logs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:51:08 +02:00
Stefan MaierandClaude Opus 5.5 06cb70fb58 Steam Frame: notes, and no variable the Android loader never reads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:27:28 +02:00
Stefan MaierandClaude Opus 5.5 1709698053 Start on the Steam Frame (Lepton)
Lepton has no clipboard service, and SDL3's activity died taking one for
granted: the build now patches SDL's clipboard handler to do without.
The Frame lists an implicit OpenXR layer for Vulkan apps whose library an
app cannot load, which failed xrCreateInstance (and the Android loader
reads no variable to turn it off): the app now carries a pass-through
layer under that library's name. With both, the game runs on the Frame
and its OpenXR session starts (SteamVR, 1728x1728 an eye, 72 Hz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:26:39 +02:00
Stefan Maier cb72dca40f Merge branch 'merge/upstream-2026-10-04' 2026-10-04 14:00:26 +02:00
Stefan MaierandClaude Opus 5.5 bbea2a2bff The online panel's game by its row past the fixed rows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:47:56 +02:00
Stefan MaierandClaude Opus 5.5 57cdbd8f80 Merge upstream (cybersecurity/halo-ce-universal 193cbf59): server browser, split-screen co-op
Upstream's server browser of public internet games (p2p_lobby.c) and the VR
port's own public list share the brokers: its topics are a table now, and
the VR port's (join codes' and public games') are entries of it; the VR
port's retained publishes carry MQTT 5's empty properties. The VR port's
public toggle lists the game in the server browser too, and the VR online
panel shows the server browser's games (PC, phones, VR) first, joined by
their invites, then its own list's that it has not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:47:29 +02:00
Stefan Maier c5c5a8eaeb Merge branch 'feature/bots-vehicles' 2026-10-04 13:33:07 +02:00
Stefan Maier a3ee36b5ef Merge branch 'feature/vr-hud-fixes' 2026-10-04 13:33:07 +02:00
Stefan Maier 30a1e0f40e Merge branch 'feature/vr-menu-lists' 2026-10-04 13:33:07 +02:00
Stefan Maier 327cf96d6f Merge branch 'feature/vr-options' 2026-10-04 13:33:07 +02:00
Stefan MaierandClaude Opus 5.5 867855d388 Bot drivers see walls and rocks ahead, back off when stuck, go round teammates
Driving a jeep or a scout, a bot looks ahead (farther the faster) along the
way it turns: a wall or a rock there (not ground it can drive up) is passed
on the most open side nearest its way, slower, and boxed in it backs off.
Stuck is now making no way along its heading for a second and a half, which
spinning wheels no longer hide. A teammate in the way is gone round slowly
on the other side instead of waited for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:25:15 +02:00
Stefan MaierandClaude Opus 5.5 4af570bc08 VR: the online games' hint further left and shorter, clear of Y = Create Game
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:13:27 +02:00
Stefan MaierandClaude Opus 5.5 7a1cf79396 VR: teammates' triangles over their heads, a click joins on the profile screen
The triangle the game draws over each teammate goes into the name's slot
over the head (shown in the world there, not on the HUD before the eyes,
where it hung distorted), under the name, or alone when names are off. On
the screen where controllers join by pressing A, a click is the pad's A. On
the screen of games to join, the online games' hint goes into the key bar's
free left and the join status under it, clear of the map's picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:00:41 +02:00
Stefan MaierandClaude Opus 5.5 9e7fd75f57 Bots: a vehicle left stuck is left alone a while; every third bot follows a person
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:41:40 +02:00
Stefan MaierandClaude Opus 5.5 6754403d68 VR: the menu pointer leaves the stick's choice be; clicks beside a list step it
A choice the stick moved to (a map among the tiles) is no longer taken back
by the pointer resting on another, until the pointer moves well away; and a
click beside a list's slots or a value's box, along the way it steps (where
its arrows are drawn), steps it back or on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:41:09 +02:00
Stefan MaierandClaude Opus 5.5 aedf5ff9bc Bot drivers wait for those coming to get in, and brake for teammates
Just in, a bot driver waits up to six seconds while a bot goes for its
vehicle or a person of its team stands near it with a gun's seat free; it
brakes (backing, if fast) for a teammate on foot in its way. A bot takes a
vehicle only when the nearest enemy (or the objective) is a long way off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:39:24 +02:00
MrBruh 193cbf59c7 Split-screen co-op 2026-10-04 22:27:13 +13:00
Stefan MaierandClaude Opus 5.5 fe160e9dbe Bots man turrets while an enemy is about
A turret near a bot (15 world units) is manned while an enemy is within 35
of it, and left once there has been no one to shoot for ten seconds; at it
the bot aims and fires as at a vehicle's gun. The map's vehicles are named
in the log once a game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:23:49 +02:00
MrBruhandClaude Opus 5.5 d578f88bd0 Validate server browser names as player names
The server browser cleans each game's name as a host cleans the names
of the players and machines that join it, and leaves out a game whose
name has nothing left that names it. A host lists a Latin letter with
a mark as its plain letter, as player names are read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:53:00 +13:00
MrBruhandClaude Opus 5.5 d3fc997fdc Ready walked-over weapons, swap out the selected one
In multiplayer a second weapon is picked up into the empty slot on
walking over it and readied, as in campaign; a client readies the
weapon the host says it picked up once it has it. A swap drops the
weapon the player selected, not the one still in hand mid-switch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:47:06 +13:00
MrBruh a227872ccb Merge pull request #73 from thelinkin3000/fix/unarmed-melee-campaign
Leave an unarmed melee alone where the map has no multiplayer weapons
2026-10-04 20:28:01 +13:00
MrBruh 812ffeea33 Outline the focused server browser row 2026-10-04 20:19:12 +13:00
MrBruh c05864b3fc Leave quit players off the scoreboard 2026-10-04 20:06:28 +13:00
thelinkin3000andClaude Opus 5.5 ce77db8404 Leave an unarmed melee alone where the map has no multiplayer weapons
A player with no weapon, whose biped has no blow of its own, borrows the
melee of the globals' first multiplayer weapon. A campaign map's globals
list no multiplayer weapons, and a10 starts the player unarmed, so a melee
in the cryo tube's tutorial asked for element 0 of an empty block and
halted in tag_groups.c. With no multiplayer weapon there is no blow to
borrow, and the melee does what the game's always did there: nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 06:22:42 +00:00
MrBruh 39350fb81f Enemy names to the motion sensor's range and setting 2026-10-04 18:57:11 +13:00
MrBruh 1a15a171ac No unarmed grenades or melee from vehicle seats 2026-10-04 17:54:34 +13:00
MrBruh 2e3841ff7d Left-align the shotgun's ammo meter 2026-10-04 17:47:55 +13:00
MrBruh 46edf53657 Settings and End Game in the pause menu 2026-10-04 17:20:32 +13:00
Paul Hontz cd747c23c5 Log of the port to halo.log without a console
The release build no longer has a console, and the platform layer's log
(platform_log to stderr: invite links, the data root, crash reports)
went only there. When stderr goes nowhere, halo.log next to halo.exe
gets it instead.
2026-10-03 22:43:14 -04:00
Paul Hontz f148dbd6d9 Release builds of Windows without a console window
The release halo.exe was a console-subsystem PE, so Windows and Wine
open an empty console window next to the game. Under Proton in
gamescope the console can take focus and leave a black screen. Nothing
reads the console (the log is debug.txt), so release builds now link as
GUI with mainCRTStartup; debug builds keep the console.
2026-10-03 22:26:20 -04:00
MrBruh c3adcfe5bf Deflate the release zips again 2026-10-04 14:22:11 +13:00
MrBruh 3304965653 Lobby check test past the compiler launcher 2026-10-04 13:43:13 +13:00
MrBruh c04765d7f4 Server browser of public internet games 2026-10-04 13:34:23 +13:00
MrBruh 933aac6175 No map weapons option, grenades with no weapon 2026-10-04 10:44:37 +13:00
MrBruh 809408c663 Survive kills and joins in departed players' slots 2026-10-04 10:12:32 +13:00
MrBruh 601a4c1f50 Unload the menu map before a client's next game 2026-10-04 09:51:47 +13:00
MrBruh 133d6a5dc1 Free quit players' slots 2026-10-04 09:07:47 +13:00
Stefan MaierandClaude Opus 5.5 12c97ffa85 Bots take vehicles: drive, man the gun, ride along; and race
A bot whose way is long takes an empty vehicle it can drive near it (a
Warthog, a Ghost, a Scorpion), walking to the driver's door and standing
there until the game offers the seat. Driving, it follows a vehicle's path
(from the vehicle's surface, wider, its steps further apart), turning a
Warthog or a Ghost by facing where it goes and pushing a Scorpion there from
where its gun faces; it runs an enemy down, circles one with a gunner
aboard, fires a Ghost's and a Scorpion's guns, backs off what it is stuck
on, roams between the map's spawn points, and gets out at the objective,
overturned, stuck again and again, or with an enemy close while it stands.
In a game of teams a bot mans the gun of a teammate's vehicle, a person's
first (or rides along with a person), aims and fires it, and gets out once
no one drives. Overturned vehicles near it are turned upright.

In a race a bot heads for the flag it is to touch next
(game_engine_race_next_flag), in a vehicle if it can have one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:48:59 +02:00
saulo benigno 61623e68d5 Fix texture bindings after first upload 2026-10-03 11:41:54 -03:00
MrBruh 23b542601f Unarmed melee and infinite grenades for any player count 2026-10-04 03:22:16 +13:00
MrBruh 62b630a261 Remove the flamethrower and gravity rifle 2026-10-04 03:05:59 +13:00
Stefan MaierandClaude Opus 5.5 0e6327cadf The controls page lists switching grenades (B held) on a line of its own
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:41:56 +02:00
Stefan MaierandClaude Opus 5.5 5f82df71f0 VR options: Y on the main menu or the pause menu
A panel of the VR settings one may change while playing (turning, snap
angle, smooth speed, the vehicles' comfort vignette, the Warthog's wheel,
the weapon laser, the crosshair, the motion tracker on the wrist), written
to config.toml, which vr.c reads again as they change; and a page of what
the controllers' buttons do, on foot, in vehicles and in the menus. The
pointer works both, and its hint opens the panel. The menus' screens are
named in the log as they come up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:37:10 +02:00
Stefan Maier cfdd821b45 Merge branch 'merge/upstream-v11' 2026-10-03 15:21:57 +02:00
Stefan MaierandClaude Opus 5.5 a9bbce5f7a Merge upstream (cybersecurity/halo-ce-universal c9ee319a): PC menus, network version 11
Upstream's PC menus are kept for the desktop; in VR the Xbox's stay, which
the VR port's join codes, game list, host options and keyboard pointer are
built into. config_write_integer now writes through upstream's config_write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:11:07 +02:00
MrBruh e8e0c2215a Find the port's headers in Windows game units 2026-10-04 02:08:45 +13:00
MrBruh 383355381c Kills to win up to 500 in the PC menus 2026-10-04 02:04:14 +13:00
Stefan Maier e94906100c Merge branch 'feature/warthog-wheel' 2026-10-03 14:58:32 +02:00
Stefan Maier 57add5ed71 Merge branch 'feature/vr-menu-pointer' 2026-10-03 14:58:32 +02:00
Milenko 2b0103a0ef Slayer's kills to win up to 500, for big games
The game type editor's kills to win (Slayer and Team Slayer) went up to
50, which a game of many players reaches in moments: it now also offers 75,
100, 150, 200, 250 and 500. The spinner's items are its string list's
(ui.map's 5, 10, 15, 25, 50) and then strings of the port's own, each with
its description; the options list's descriptions count only a spinner's
items of the tag's own, so those of the rows after it are where they were.
A spinner with extra items is a little wider, for three digits.
2026-10-04 01:58:26 +13:00
MrBruh c9ee319ab5 Add the PC menus 2026-10-04 01:56:45 +13:00
Stefan MaierandClaude Opus 5.5 770f8881e0 At the wheel, the right trigger is the throttle and the left the brake
Holding a Warthog's wheel, the triggers drive as the left stick pushed up
or down would (backwards once stopped), unless the stick is pushed further;
they neither fire nor hold a grenade then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:50:41 +02:00
Stefan MaierandClaude Opus 5.5 38ab192749 Steer a Warthog by its wheel with the hands
At an open vehicle's wheel, a hand that grips near it (where the driver's
drawn hands hold it) takes hold, and the hands turn it about its column:
both by the line between them, one about the wheel's middle. Turned
vr.vehicle_wheel_degrees (90) either way it steers fully; let go, the right
stick steers as before. Holding the wheel, the right grip is no melee.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:00:26 +02:00
Stefan MaierandClaude Opus 5.5 827eb0fc1d The keyboard gets the menu pointer's clicks; its BACK and ENTER hints click
The menus' mouse handling took the pointer's clicks each frame before the
keyboard asked for them, so the keyboard never saw one: with the keyboard
up the pointer is now the keyboard's alone. Clicking the hints under it
(B = BACK, A = ENTER, in its picture) cancels or finishes as Done does.
The pointer counts as moved past 3 pixels, not a hand's tremor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:47:51 +02:00
Stefan MaierandClaude Opus 5.5 a482a5f5bb The on-screen keyboard follows the menu pointer in VR
The key under the controller's pointer is picked and a click types it.
In the menus the triggers no longer reach the game as the gamepad's (they
click the pointer; on the keyboard they also moved its cursor).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:42:57 +02:00
Stefan MaierandClaude Opus 5.5 81f321f959 Of overlapping screen keys, the one whose icon is nearest on the left
A key's label is a text box often far wider than its text, reaching over
the next key (Back pressed Delete). Of the keys under the pointer, the one
whose icon begins nearest on its left is taken: each key from its icon to
the next one's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:30:20 +02:00
Stefan MaierandClaude Opus 5.5 f97ff9219f Menu pointer: narrower keys, the click where the trigger began to move
The screen keys' margin in VR was wide enough to reach the next key (Back
pressed Delete): now 2 pixels to the sides and 4 above and below, with a
label at most 25 pixels off. A click is taken where the controller
pointed as its trigger began to move (past a tenth), not a fixed time
before, which could still be on the key it just left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:27:28 +02:00
Stefan MaierandClaude Opus 5.5 85e79a067a Steadier menu pointer clicks in VR
A click is taken where the controller pointed a few frames before the
trigger went down (pulling it tips the controller), and in VR a screen
key's button takes a label up to 40 pixels off and a margin of 6 about it,
so the keys under the menus (A, B, Back) are hit as readily as items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:23:28 +02:00
Stefan MaierandClaude Opus 5.5 effeda1b7c Point at the menus with a controller in VR
The desktop builds' mouse in the menus (halo_ui_pointer.h) now comes, on
Android in VR, from a controller: where its aim's line meets the menus'
screen fixed in the room is the pointer, the trigger clicks (the hand
pressed last points; a click ticks its rumble), and a ringed dot shows
where it points. The item under it takes the focus and a click presses A,
as with the mouse; the online games and host options panels take it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:56:05 +02:00
Stefan Maier 6299503191 Merge branch 'feature/bots-team' 2026-10-03 11:10:25 +02:00
Stefan MaierandClaude Opus 5.5 c5d86c8d8b Bots spread out about their objective and keep to it
Each bot has a direction of its own (the golden angle on from the last):
far from a shared goal (the enemy's flag, the ball, the hill) it comes by
a way of its own, out to its side of the straight way, straight only for
the last 10 world units; holding a place it takes a spot of its own: on
the hill, about its flag's stand (every other guard ahead of it towards
the enemy's), about a teammate it escorts. With no way to its spot it
takes the point itself. A bot with an objective no longer leaves it in a
fight for a weapon close by.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:55:28 +02:00
Stefan MaierandClaude Opus 5.5 7242151017 Names over the heads at 60% of their size on the HUD
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:48:01 +02:00
Stefan MaierandClaude Opus 5.5 1c8e651bb1 Bots play with their team
In a game of teams a bot holds its fire while a teammate stands in the
line of it (and steps aside for a clear shot), takes on first the enemies
near its teammates (people above all), escorts a teammate carrying the
enemy's flag or the ball a few world units off on its own side, and, with
nothing to do, every other bot keeps near the nearest person on its team.
A goal it finds no way to three times running (a person up where only a
teleporter goes, a thing out of reach) is given up for a while, and a
roaming bot then goes somewhere near at random.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:41:08 +02:00
Stefan MaierandClaude Opus 5.5 4b8f8dde8b Players' names over their heads in VR, each on a quad of its own
The HUD in VR is a screen at one distance before the eyes, where the names
upstream draws on it hung. Each name is now drawn into a slot of a region
of the back buffer before the HUD (up to eight, one under another), taken
into a names layer, and shown on a quad of its own over its player's head,
facing the eyes, as wide as it would look on the HUD: in depth, for both
eyes. Who is named, and when, is as upstream has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:31:36 +02:00
Stefan Maier 920b1bfea9 Merge branch 'feature/vr-scope' 2026-10-03 10:18:30 +02:00
Stefan Maier 09f869e44c Merge branch 'feature/bots-modes' 2026-10-03 10:18:30 +02:00
Stefan MaierandClaude Opus 5.5 bc45338214 Bots get stuck in corners less
A path the game finds but cannot steer round the objects on it (the other
players among them: obstacle avoidance failed) is walked as smoothed,
instead of none (which had bots walk straight into walls); a step aside
settles a bump. A bot goes on to the next point of its path when it is
in plain view on its own level, walks the last 3 world units straight
only when nothing is in the way, and counts itself stuck when it makes no
way to its point for 6 ticks (only while it walks one): then it steps
aside, finds its way anew, skips a corner's point it cannot reach, jumps
at 12 and goes elsewhere later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:33:58 +02:00
Stefan MaierandClaude Opus 5.5 a663d1b100 Bots take good weapons in a fight too, and value strong ones more
Within 10 world units a bot goes for a weapon or powerup worth at least 4
while it fights. Strong weapons are worth a longer way (worth squared over
distance, falling off over 15 world units), and with two weapons one is
worth what it betters the worse by and 1.5 more, so rocket launchers and
sniper rifles are taken, not only the shotgun close by. The pistol counts
for a little less up close, the assault and plasma rifles more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:27:24 +02:00
Stefan MaierandClaude Opus 5.5 d8034d37fd Raise the rocket launcher's scope disc 3.5 cm more (18 cm)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:03:19 +02:00
Stefan MaierandClaude Opus 5.5 f8a39df0cd No names over the heads in VR for now
In VR the HUD is a screen at a fixed distance before the eyes, so the
names upstream draws on it hung there instead of over the players' heads.
Off until they are drawn into each eye's picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:03:10 +02:00
Stefan Maier 6057146244 Merge remote-tracking branch 'upstream/main' into merge/upstream-2026-10-03
# Conflicts:
#	port/linux/game/network_distributed.c
#	port/linux/game/network_distributed.h
2026-10-03 07:38:52 +02:00
Stefan MaierandClaude Opus 5.5 94b17a7913 Bots play capture the flag, king of the hill and oddball (phase 4)
The flags and balls (weapons that are flags) are gathered each tick with
where they are and who carries them. In CTF a bot carrying the enemy's
flag runs it to its own stand; the rest hunt whoever carries their flag,
touch it home where it lies dropped, or go for the enemy's (every third
guards its own). In king of the hill they go to the hill (the game's goal)
and keep moving in it. In oddball they go for the ball, taken as a weapon
is, hunt its carrier, and carrying it keep away. Carrying, a bot does not
fire, and looks where it runs. The last 3 world units to a goal are
walked straight, over what the path goes round (a flag on its stand).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:22:31 +02:00
Stefan MaierandClaude Opus 5.5 30f362ff3d Bots pick up weapons, powerups and grenades, and throw grenades (phase 2)
Out of a fight a bot looks every second for what lies within 25 world
units worth having: a weapon better than the worse of its two (it changes
to that one and holds the swap while the game offers the weapon), ammo for
an empty weapon it carries, an overshield, camouflage, a health pack when
hurt, grenades when it has few. In a fight it takes the weapon that suits
the distance (shotgun near, sniper rifle far, no rocket up close), at
most every two seconds, changes from an empty one, and now and then lobs
a grenade at an enemy 6 to 20 world units off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:41:48 +02:00
Stefan MaierandClaude Opus 5.5 23d14319a7 Raise the rocket launcher's scope disc 3.5 cm more
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:14:34 +02:00
Stefan MaierandClaude Opus 5.5 ff5b91f120 Raise the VR scope's disc over the rocket launcher's tube
The disc sits 7 cm over the hand, where a rifle's or pistol's scope is;
over the rocket launcher, carried higher on the shoulder, it was inside
the tube. A weapon may raise it (halo_vr_set_scope_raise): the rocket
launcher by 11 cm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:07:26 +02:00
Stefan MaierandClaude Opus 5.5 f6c2322df9 Host options in the lobby: public, bots and their skill
Y in a hosted game's lobby opens a panel to make the game public, choose
how many bots join (none by default, up to 15) and how well they play
(bots.skill: easy, normal, hard; hard is the first bots' play, normal a
little slower to notice, turn and aim). Integer settings can now be
written to config.toml as booleans could.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:05:34 +02:00
Stefan MaierandClaude Opus 5.5 39fbaefaac Keep the new settings out of the network table
The settings file is written in the table's order, so settings of another
section between network.* entries wrote [network] twice, and the whole
file was then refused (defaults used). They now follow the network
settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:52:14 +02:00
Stefan MaierandClaude Opus 5.5 c6397cb515 Bots find their way with the game's pathfinding (phase 1)
A bot walks to its goal along a path from ai/path.c (the maps' pathfinding
surfaces, as the campaign's actors use them), found again every half
second: in a fight towards its target, weaving; out of sight hunting the
nearest enemy, or roaming near someone; looking where it goes. With no
path it walks straight at the goal, and when stuck picks another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:05:12 +02:00
MrBruh 80d30410c8 Flush the GPU every 3 draws on Mesa Intel 2026-10-03 02:56:11 +13:00
MrBruh d1c7243cb2 Rework the multiplayer scoreboard 2026-10-03 01:16:09 +13:00
MrBruh fd3d62f960 Keep player names bannable 2026-10-02 22:12:16 +13:00
Stefan MaierandClaude Opus 5.5 5ba6c13097 Bots as players of the host (phase 0)
bots.count bots are added in the lobby as players of machines of the
host's own that nothing connects to (four a machine, from the top slot
down): new connections skip their slots, and they count as machines for
the countdown, so a game of the host and bots starts. Every machine sees
them as players; each tick the host thinks for them (bots.c) and takes
the result as their input through update_server_handle_distributed_input.
The first brain finds the nearest visible enemy, turns at a player's rate
with some aim error, walks at it weaving and fires on target, and else
wanders, jumping and turning when stuck. Every multiplayer map has
pathfinding surfaces for the next phase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:08:09 +02:00
Jeff ClarkandClaude Opus 5.5 e44619818b Snap poses that changed too much in a tick
Frame interpolation blends each node between its last two tick snapshots.
Blending two snapshots of different poses node by node stretches vertices
across the screen for a tick, then they snap back:
- the first-person pose blended from whatever was drawn last, which after
  being zoomed, in a vehicle, dead or in a cinematic is seconds old, and
  after a weapon switch may be another skeleton with as many nodes;
- an object's pose was only checked by its root node's move (10 units), so
  the game changing a pose at once (an actor waking from dormancy, a model
  swapped) was swept through poses it never had.
Now the first-person pose blends only from the tick just before, and not
when a node moved more than 0.25 units in the camera's frame; and an object
snaps when a node moved more than 0.4 units in its root node's frame (in
that frame, so the whole object turning or moving counts for nothing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:11:46 -04:00
Jeff ClarkandClaude Opus 5.5 b449c43e22 Keep vertex constant serials from wrapping
Each changed vertex constant register takes the next serial, and a program
uploads the registers changed since the serial it last saw. The serials
were 32-bit; a skinned model's draw changes up to 132 registers, so at a
high frame rate they wrapped within minutes to an hour. After a wrap, the
log walk (serial <= constants_serial) and the full scan (register serial >
program's serial) both found nothing changed, and every program's next
draw used the registers it last uploaded: often another object's node
matrices, so its vertices flew across the screen for a frame. The serials
are 64-bit now and never wrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:11:35 -04:00
Stefan MaierandClaude Opus 5.5 66c0d33e34 List public online games and join them from a panel
A host can make its game public (Y in the lobby, or network.public_game):
it then announces name, map, game type, players and join code every 10 s,
retained, on hcvr/1/g<network version>/<id> through the signalling
brokers, and clears it when it stops. On the system link list, X opens an
online games panel listing the public games heard lately (stale ones
dropped after 45 s) and a row to type a code; A joins by the game's code.
Announcements carry no addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:00:57 +02:00
MrBruh b438cea479 Cap enemy name range and shrink names 2026-10-02 20:59:36 +13:00
MrBruh 634b4197f9 Limit enemy names to the weapon's red reticle range 2026-10-02 20:36:46 +13:00
MrBruh ab96597321 Fix the Android build 2026-10-02 20:34:00 +13:00
MrBruh f0dfb58c94 Cap the frame rate with vsync off 2026-10-02 20:26:22 +13:00
Jeff ClarkandClaude Opus 5.5 991ab41016 Fix the distance of 3D sounds
The game gives positions and each sound's minimum and maximum distances in
world units, and sets DirectSound's distance factor to 3.048, the meters in
a unit (10 feet). The mixer multiplied the distance by that factor before
the rolloff, but not the minimum distance, so every 3D sound was heard 3
times as far away as it was: up to 10 dB quieter against the 2D sounds,
and panned as if farther. The distance factor is DirectSound's for Doppler;
the rolloff is in the units the distances are given in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:04:57 -04:00
Jeff ClarkandClaude Opus 5.5 5f9748d736 Resample voices with a windowed sinc
The mixer resampled each voice to 48 kHz by linear interpolation, which
left the images of a 22 kHz voice's band only 3 to 21 dB down (an 8 kHz
tone's image at 14 kHz was 10 dB under it) and dulled the band itself
(-4 dB at 8 kHz): voices sounded gritty and low-fi. It also held a packet's
last frame instead of reading on into the next packet. Now each output
sample is band-limited interpolation of the frames around it (a Kaiser-
windowed sinc over 16 zero crossings, J. O. Smith's method): flat to 8 kHz
at 22 kHz, the images 66 to 98 dB down, the low pass narrowed for voices
played faster than the output rate, and the frames taken from the packets
in turn, so it reads across their boundaries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:50:39 -04:00
Jeff ClarkandClaude Opus 5.5 56b2a0e8e3 Fix Xbox ADPCM decoding
An Xbox ADPCM block's header holds its first sample, and its nibbles code
the 63 after it; the 64th nibble only pads the block. The decoder dropped
the header's sample and decoded the padding in its place, so every 64th
sample was wrong: a buzz at 344 Hz under every 22 kHz sound, voices most
of all. In the maps' sounds the last nibble is always 0, and the next
block's header follows the 63rd nibble's sample by one ordinary step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:47:20 -04:00
Jeff ClarkandClaude Opus 5.5 5f388f7818 Replace the soft clipper with a limiter
The voices sum at their full level, as the Xbox's mix bins did, and the
mixer bent every sample over 0.8 with tanh. When loud voices piled up, as
dialogue over gunfire does, that distorted the sound: a 2x full scale sine
came out with 23% harmonic distortion. A look-ahead limiter now turns the
whole mix down for as long as it would go over -1 dBFS, both channels
alike, and brings it back over 100 ms. The output is 1.3 ms later, so the
gain comes down before each peak; below full scale nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:29:45 -04:00
MrBruh 9f3e8c92de Add high-res postgame carnage report title 2026-10-02 16:24:30 +13:00
MrBruh f2ba71d9af Show player names above heads in multiplayer 2026-10-02 15:24:40 +13:00
MrBruh 0182da8172 Add high-res text and OpenCE titles 2026-10-02 13:39:17 +13:00
Stefan MaierandClaude Opus 5.5 6b77fe428a Show how joining an online game goes on the system link list
p2p_join_state tells the menus whether the host is being reached, was
reached (its game is in the list), did not answer, could not be reached
(both routers too strict: allow UPnP) or was lost; the join screen shows
it under the code's status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:37:25 +02:00
Stefan MaierandClaude Opus 5.5 0badc451cb Join online games with a short code in VR
A hosted game gets an 8-character join code (Crockford base 32) besides
its invite link, shown under the lobby's buttons. On the system link list,
X opens the game's keyboard to type a code in; the code's topic and key
come from a slow PBKDF2 derivation, the host answers an ask on that topic
through the signalling brokers with its invite sealed with the key, and
the joiner joins as with the link. Codes in join_link.txt work too.

Also: the clipboard text last seen is kept from run to run, so a run's
own invite is not joined again at the next start; in menus a held stick
repeats after 350 ms and then every 90 ms, and its threshold is 70%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:32:50 +02:00
MrBruh c55e4e2b9d Widen only flat UI fills on widescreen 2026-10-02 02:21:22 +13:00
bfenty 7cd1724fe5 Widen full-screen UI widget backgrounds to the display width
A widget background bitmap whose bounds cover the whole 640x480 design
space (the pause menu dim, for example) stopped at the edges of the
centered 640 column on widescreen displays, leaving the side bands
undimmed. Widen the bounds and the clip symmetrically to the full
screen width, mirroring the existing fade_to_black special case in
render_ui_widgets().

Tested on a Retroid Pocket Flip2 (854x480 render target).
2026-10-02 02:16:14 +13:00
MrBruh bd8ec3c798 Add high-res sniper ladder and fix meter fringes 2026-10-02 02:09:45 +13:00
MrBruh 4799980e8a Draw the whole HUD in high resolution 2026-10-02 01:24:38 +13:00
MrBruh f8937c6179 Add high-res HUD textures 2026-10-01 22:33:25 +13:00
codi 557c0b486c Build the Android port on macOS hosts
configure.py hardcoded the NDK's linux-x86_64 toolchain folder, so the
Android build could not find the NDK on a Mac. Pick the host's folder
(darwin-x86_64, windows-x86_64 or linux-x86_64), or the only one the NDK
has.

On a case-insensitive file system, the game's <StdDef.h> shim found itself
again through its own <stddef.h> inclusion and recursed. Use include_next,
as the port's other header shims do.
2026-09-29 20:51:24 -06:00
821 changed files with 90822 additions and 1695 deletions

No files matched your search

+18 -6
View File
@@ -67,7 +67,9 @@ jobs:
run: python tools/ci_build.py linux release
- name: Test
run: python -m pytest -q tools/test_linux_port.py
run: |
python -m pytest -q tools/test_linux_port.py
python tools/test_light_storage.py
- name: Compile cache
run: ccache --show-stats
@@ -200,6 +202,9 @@ jobs:
- name: Release
run: python tools/ci_build.py windows release
- name: Test light storage
run: python tools/test_light_storage.py
- name: Compile cache
run: ccache --show-stats
@@ -232,6 +237,13 @@ jobs:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
port/android
port/third_party/extract-xiso
tools
- uses: actions/download-artifact@v4
with:
pattern: halo-*
@@ -242,11 +254,11 @@ jobs:
mkdir release
for build in artifacts/*/; do
name="$(basename "$build")"
# stored, not deflated: the updater in builds up to 57 waits for
# an end of stream the game's zlib 1.1 does not always report,
# and fails on a deflated entry that unpacked correctly
(cd "$build" && zip -0 -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
(cd "$build" && zip -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
done
# the Steam Frame's installer (port/android/STEAM_FRAME.md): its
# bundle with the release APK, and the desktop file that fetches it
python3 tools/steam_frame_bundle.py artifacts/halo-android-release/app-release.apk release "$GH_REPO"
ls -l release
- name: Publish
@@ -260,7 +272,7 @@ jobs:
subject="$(printf '%s' "${MESSAGE:-manual run}" | head -n 1)"
printf '%s\n\nCommit: %s\nBuild: %s\n\nThe release builds are for playing; the debug builds stop at the first failed assertion and log it.\n' \
"$subject" "$COMMIT_URL" "$RUN_URL" > notes.md
gh release create "$tag" release/*.zip --target "$GITHUB_SHA" --latest \
gh release create "$tag" release/* --target "$GITHUB_SHA" --latest \
--title "Build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
echo "tag=$tag" >> "$GITHUB_OUTPUT"
+7
View File
@@ -24,7 +24,14 @@ xbox/
# Locally extracted proprietary game data (never commit)
assets/
# (but the high-res assets, drawn for the port: tools/hud_assets.py)
!port/assets/
original/
# CI build outputs (tools/ci_build.py)
dist/
# The app signing key's local settings and keys (port/android/app/build.gradle; never commit)
port/android/signing.properties
*.jks
*.keystore
+4 -4
View File
@@ -19,9 +19,9 @@ builds of the latest release:
| Platform | Release | Debug |
| --- | --- | --- |
| Linux | [halo-linux-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-debug.zip) |
| Windows | [halo-windows-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-debug.zip) |
| Android | [halo-android-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-debug.zip) |
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
Use the release build to play. The debug build stops at the first failed
assertion and writes it to the log. Use the debug build to find and report
@@ -32,7 +32,7 @@ if you want to install it. Refer to "Updates" in
[port/linux/README.md](port/linux/README.md#updates).
Each build of the `main` branch that passes on all three platforms is a new
release. The [Releases](https://github.com/cybersecurity/halo-ce-universal/releases)
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
page keeps the last five releases. If the latest build has a problem, get
an older build from that page.
+11 -2
View File
@@ -155,7 +155,15 @@ select "Yes":
To install over the previous version, each build must have the same
signature. GitHub Actions signs each build with the key in the
`ANDROID_KEYSTORE_BASE64` and `ANDROID_KEYSTORE_PASSWORD` secrets of the
repository. If you installed a build that has a different signature, remove
repository. A computer that builds for its own devices signs with the same
key when `port/android/signing.properties` (not in the repository) names it:
storeFile=/path/to/halo-vr-release.jks
password=<the keystore's password>
keyAlias=halo
Keep the keystore and its password safe: a lost key means every player must
remove the app before the next version. If you installed a build that has a different signature, remove
that build before you install a new build. Removing the app deletes its data
folder: first make a copy of `maps/` and `save/`.
@@ -168,7 +176,8 @@ the Xbox:
- The HUD stays at the edges of the screen.
- The menus, the loading bar and the screens after a game have 640
columns, at the center of the screen.
- Black bars and fades cover all of the screen.
- Black bars and fades cover all of the screen, and so do the menus' dims
and backgrounds (the pause menu's dim, dialogs, the menus' gradient).
## VR
+169
View File
@@ -0,0 +1,169 @@
# Steam Frame
The same APK as for the Quest runs on the Steam Frame, in Lepton (SteamOS's
Android, a Waydroid fork: Android 11, arm64), with SteamVR's OpenXR runtime.
What differs is found at run time, not by a build of its own.
## Installing it (players)
On the Frame, in Desktop Mode (Steam menu > Power > Switch to Desktop):
1. Put a disc image of Halo: Combat Evolved for the Xbox (`.iso` or
`.xiso`, any version) in Downloads (or Documents, the Desktop, a memory
card).
2. Download `Install-Halo-VR.desktop` from the latest release and open it
(double-click; confirm running it).
3. Answer the dialogs: the installer finds the disc image (or asks for it),
copies the game data out of it, adds "Halo VR" to the Steam library and
switches back to Gaming Mode. Start Halo VR from the library there.
Opening the same file again updates the game (settings, saves and game data
stay).
The desktop file (`steam_frame_installer.desktop`) downloads the latest
release's `halo-vr-steam-frame.zip` (`tools/steam_frame_bundle.py`, made by
the release job of `.github/workflows/build.yml`) and runs its
`install.sh` (`steam_frame_install.sh`), with zenity dialogs:
- `~/Documents/HaloCE`: the launcher (`halo-vr.sh`) and the release APK;
- the maps folder out of the disc image (`steam_frame_xiso.py`, the app's
`XisoExtractor.java` in Python), unless `maps/ui.map` is there;
- the Steam shortcut (`shortcuts.vdf` as below, with `OpenVR` set, and the
library pictures), unless it is there: Steam is closed for that, as it
writes its shortcuts back when it quits.
`HALO_VR_UNATTENDED=1` (and `HALO_VR_IMAGE=<image>`) runs it without
dialogs, for testing over adb; it leaves Steam and its shortcuts alone.
## The device
- Steam Settings > System > Enable Developer Mode; then start "Lepton
Development" from the library. While the headset sleeps Lepton stops, and
adb with it.
- adb over the network: `adb connect <frame-ip>:5555`; over USB:
`adb forward tcp:5555 tcp:5555` then `adb connect localhost:5555`.
Always name the device (`adb -s <frame-ip>:5555 ...`): a Quest may be
connected as well.
- Install with `adb install -r --no-streaming`. It prints "Lepton post
installation failed due to APP_PATH being unset", but installs. Start the
game with `am start -n com.halo.decomp/.LauncherActivity`.
- Lepton empties `/sdcard/Android/data` whenever it starts again: the game's
data must live elsewhere (stage 2). `/sdcard/Documents` and
`/sdcard/Download` are the SteamOS user's folders and are kept.
## What the port does for it
- SDL3's clipboard handler takes a clipboard service for granted, which
Lepton has not: `tools/android_build.py` patches it to do without.
- SteamVR lists an implicit OpenXR layer for Vulkan apps whose library an
app cannot load: the APK carries a pass-through layer under its name
(`port/android/xr_layer/fdm_injection_stub.c`).
## Stages
| # | Stage | State |
| --- | --- | --- |
| 1 | Base: the fixes above on the current main | done |
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/HaloCE` |
| 3 | Menus, HUD and text visible (quad layers) | done: their alpha from their colour |
| 4 | The freeze at the cryo bay's calibration | not seen again (2026-10-04) |
| 5 | The Frame's controllers | done: their own profile, the Xbox's layout |
| 6 | Frame rate, resolution | done: 72 Hz steady, multiview on |
| 7 | Multiplayer, voice, cross-play with the Quest | |
| 8 | A button in the Steam library | done: `steam_frame_launch.sh` |
## Starting it on the Frame
A button in the Steam library ("Halo VR", a non-Steam game) runs
`~/Documents/HaloCE/halo-vr.sh` (`port/android/steam_frame_launch.sh`),
which starts the game as Steam starts an Android title of its own:
`lepton waitforexitandrun -- ~/Documents/HaloCE/HaloVR.apk`, in a container
of its own (`steamlaunch-4290000001`) that Lepton installs the APK into
anew whenever the file is newer. Updating the game is replacing that APK.
What the script does that Steam would for an Android title:
- a compat data path (`steamapps/compatdata/4290000001/internal`): after an
install Lepton links the app's data to it, a dangling link without it;
and the shader path, which Lepton reads Steam's shader dump path against;
- a container id of its own: with the shortcut's id, SteamVR's client in the
container stalled in xrCreateInstance;
- a few starts: a new APK's first start only installs it, and a start that
ends within 30 s is tried again (up to three times), unless the game was
quit (it writes `quit_requested.txt`).
Steam's "Exit game" does not reach the game (its process is the container's,
not Steam's): quit it with the VR options' "Quit Halo" (Y in the main or
pause menu, twice). A container ended within 30 s of its start makes
Lepton drop the app's install, so the next start installs it again.
The shortcut is in `userdata/<id>/config/shortcuts.vdf` (Steam reads it when
it starts; appid 3151454348 for this name and path), its pictures in
`userdata/<id>/config/grid/` (from `port/android/steam_frame_grid/`). From
the Mac, over USB the Frame's SteamOS is an adb device of its own, `frame`,
beside Lepton's `<frame-ip>:5555`.
## The game's data
Lepton uninstalls the app and empties its folders whenever it starts again.
The game's data lives in the SteamOS user's Documents instead: when
`/home/steamos/Documents` can be written, the launcher makes
`/home/steamos/Documents/HaloCE` and the launcher and the game take it as
their data folder (config.toml, saves and debug.txt go there too), so a
disc image imported in the app stays as well. The installer above fills it;
by hand, once:
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/HaloCE/maps
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/HaloCE/maps/
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/HaloCE
After a Lepton restart only the APK is installed again. (Use the path under
/home, not /sdcard/Documents: Android 11's scoped storage stands between the
app and the latter.)
## The controllers
SteamVR offers the Frame's controllers as themselves
(`XR_VALVE_frame_controller_interaction`,
`/interaction_profiles/valve/frame_controller_valve`; Valve's "Steam Frame
Input" page). Without bindings for it, SteamVR maps the Quest's onto them,
and the left Touch controller's X and Y land on the D-pad. With them the
game gets the Xbox's layout, as the right controller has it: A jump, B
flashlight (held: switch grenades), X action and reload, Y switch weapon,
menu pause; the left one's View is Back (scores), its D-pad the D-pad, the
bumpers switch grenades (left) and the flashlight (right). The VR options'
page of buttons shows this layout on the Frame.
## Findings
- 2026-10-02: runs flat and in VR (GLES 3.2 by Mesa zink on Turnip, eyes
1728x1728, 72 Hz, XR_FB_display_refresh_rate absent). Quad layers
(menus, HUD, instructions) are not shown; the projection layer is. Froze
at the cryo bay's look calibration; its log was lost to Lepton's wipe.
- 2026-10-04: SteamVR draws a quad's texels by their alpha alone; the
game's menus and HUD have none in their additive parts, so nothing showed.
On SteamVR (by the runtime's name) the overlays' alpha now comes from their
colour (host_xr.c; `HALO_XR_QUAD_ALPHA=colour|keep` overrides). With it,
menus, HUD and the campaign ran well (the user: "lief 1a"); no freeze.
- 2026-10-04: no online games found: the public MQTT brokers had banned the
home connection for a while (too many connections from tests that day);
broker.emqx.io came back within minutes. Lepton's network is the Frame's
own (same IP), DNS works.
- 2026-10-04: the refresh rate is SteamOS's setting; the runtime offers
only that one (the game asks for the highest at or below
`vr.refresh_rate`, and logs the rates offered). At 72 Hz the game holds
72 fps (about 11.6 ms a frame with multiview, eyes 7.6 ms; without it
eyes about 9 ms). At 90 Hz it cannot (11.1 ms a frame), and SteamVR's
making up of the missed frames showed as a doubled picture when turning
the head. Keep the Frame at 72 Hz, or lower `vr.resolution_scale` (about
0.85) for 90.
- 2026-10-05: doubled pictures when turning at 72 Hz too, the game at the
edge of its frame (12.5 of 13.9 ms; SteamVR reprojecting 2-8% of the
frames). The time was not the drawing's but waits: the layer copies read
the GL state back (glGet*) and released each image at once, and with
Mesa's GL thread (zink) every read waits for the thread to run all that
is queued, every release for the GPU (the left eye's copy took 4.5-7 ms
of the frame, the right one's, just after it, 0.7). On SteamVR the copies
are now deferred (host_xr.c): no state read back (the game sets its own
again), the images released together as the frame ends, and the eyes'
work flushed to the GPU as soon as they are copied.
`debug.xr_deferred_copies` = 0 brings back the old copies for comparing,
1 defers them on any runtime; `vr.log_performance` logs the frame's parts
and, with the compositor's counters, each copy's time.
+230
View File
@@ -0,0 +1,230 @@
# VR roadmap
What the VR port (Quest 3, Steam Frame) has, and what comes next, in the
agreed order. Updated with every step: a step is **done** once the user has
tried it in the headset and it is merged into `main`; **built** means in a
branch, not yet tried in the headset.
Several of the next steps follow
[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) and the
Steam Frame work it grew from,
[astromaddie/HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) (both
CC0): their ideas, rebuilt into this port's own structure, with the source
named where code is taken over.
## Done (in `main`)
| Area | What |
| --- | --- |
| Headsets | Quest 3; Steam Frame (Lepton): its controllers' layout, a Steam library tile that installs, starts, updates and quits the game, data kept in `~/Documents/HaloCE` |
| Rendering | Both eyes in one pass (multiview) on the Quest; one by one on SteamVR, where multiview's eyes did not fuse; lens flares at their depth in stereo; HUD, menus, scope, crosshair and names as layers of their own |
| Aiming | The weapon in the hand, its shots from the muzzle, laser and crosshair options |
| Body | Physical crouch (`vr.crouch_limit`) beside the crouch button, leaning (`vr.lean_limit`), melee by swinging, grenades thrown with the left hand, two-handed weapons |
| Vehicles | The Warthog's wheel turned with the hands, right trigger throttle, left brake; first-person seats; turrets with handles |
| Comfort | Snap or smooth turning, a comfort vignette in vehicles, VR options in the main and pause menus (Y), a page of every controller button |
| Menus | The PC version's menus in VR (`vr.menus`), clicked with a laser; text typed on the on-screen keyboard |
| Multiplayer | Up to date with OpenCE (network version 20): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list |
| Bots | Bots in multiplayer: on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person |
| Controls | Left-handed mode (`vr.handedness`): the weapon in the left hand, the arms and grips mirrored, sticks and buttons mirrored on both controllers (`vr.mirror_controls`), the assault rifle's counter read the right way round (2026-10-06) |
| Graphics | Presets low / medium / high / max (`vr.graphics`), refresh rate and resolution in the VR options, all kept over a restart (2026-10-06) |
| Arms and hands | Both arms by IK from shoulders that follow the torso, the elbow led by the hand's roll; fingers from trigger, grip and touch; `vr.body` arms / hands / off, `vr.fingers` (2026-10-06) |
## Next, in order
| # | Step | State |
| --- | --- | --- |
| 1 | Steam Frame CPU time: Mesa's trace markers off (after astromaddie/HaloCE-VR), no `glGet` stalls in the visibility counters' copies | **done** (in `main` 2026-10-06): markers quieted, no problems; the frame's work about unchanged (11-12.5 ms of 13.9 in a bot game) |
| 3a | Visible arms and hands: the arms by IK to the controllers, fingers by trigger and grip | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good; 0.02 ms a frame) |
| 2 | Controls and comfort: left-handed mode, buttons remapped, flashlight by tapping the head, finer turning and vignette choices, graphics presets and refresh rate in the VR options | first part **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good): left-handed mode, graphics presets, refresh rate and resolution in the VR options. Left: buttons remapped, flashlight by tapping the head, finer turning and vignette choices |
| 3b | The whole body: torso from head and hands, legs and feet following in the room; full / arms only / hands only | |
| 3c | Weapons held physically, holsters at shoulder and hip (the buttons stay) | |
| 3d | Other VR players shown with their heads, hands and bodies (today their bodies move as flat players' do: only their aim and throws come from their hands) | |
| 4 | Later: disc image import in the launcher, sprint by arm swing (offline) | import **built** (branch `feature/release-install`): the app's import (Quest; on the Frame now kept in `~/Documents/HaloCE`), a Steam Frame installer from Desktop Mode (`STEAM_FRAME.md`), a release signing key; test pending |
## Step 2, first part (done)
Left-handed mode and graphics presets, after HaloCE-Quest-VR (CC0): its
mirrored first-person model, its graphics switches.
Left-handed (`vr.handedness = "left"`):
- The weapon in the left hand: aim, shots, muzzle, laser, crosshair,
scope disc, turret aim. The first-person model is posed as the right
hand's in a mirror image of the room ("model space", `halo_vr.h`) and
mirrored back; a model whose node matrices mirror has its triangles'
winding turned over (`rasterizer_set_model_skinning`, `d3d8_gl.c`). The
arms reach to the other shoulders, the foregrip is gripped with the
right hand, the right hand's fingers follow its controller.
- Grenades held and thrown with the right trigger and hand; melee by the
left grip (a swing of either hand, as before); the motion tracker on the
right wrist (`halo_xr_submit.wrist_hand`); the rumble's light motor in
the weapon hand.
- The left trigger fires, the right one throws; at the Warthog's wheel the
left trigger accelerates, the right one brakes.
- With `vr.mirror_controls` (default): the right stick moves, the left
turns and steers; their clicks, the bumpers and the face buttons change
sides (Quest: X jumps, Y light / held grenades, A action and reload, B
switch weapon; Steam Frame: the left D-pad does A, B, X, Y as their
mirror image, down jump, left light, right action, up switch weapon,
and A, B, X, Y do the D-pad's). The menus keep their buttons; the
laser pointer works from either hand as before.
- Other machines see the shots leave the left hand (the network message
is unchanged: aim and muzzle from the camera).
- The assault rifle's ammo counter and compass read the right way round
on the mirrored model (2026-10-06): their geometry stays mirrored, each
digit's bitmap is drawn turned over in u and the digits' order across
the display swapped (numeric shaders drawn mirrored,
`rasterizer_xbox_transparent_geometry.c`). Checked flat in the emulator
with `debug.mirror_first_person` (the first-person weapon mirrored off
the headset): 60, 48, 36 ... 06 read correctly. Steam Frame good
(2026-10-06).
- Kept as they are (accepted 2026-10-06): the small static icons under the
counter (round and "x") stay mirror images (a texture atlas, not
numeric). The game's prompts still name the Xbox's buttons.
- Settings kept: on Android `config.toml` is written beside itself and
renamed over (`port_config.c`), so one pushed by adb (the shell's, which
the app could not open for writing) no longer loses the VR options'
changes at a restart.
- Headset tests 2026-10-06: Quest 3 and Steam Frame good (left hand,
controls mirrored and not, counter, presets, refresh rates, resolution,
settings kept over a restart).
Graphics (`vr.graphics`, `port/linux/game/vr_graphics.c`), set each frame
on the game's own switches:
| Preset | Leaves out |
| --- | --- |
| `"high"` (default) | nothing: as before |
| `"medium"` | object shadows, environment reflections, lens flares |
| `"low"` | also specular light, bump maps, grass and other detail objects, the screen's fog layer, contrails, weather, camouflage's second pass |
| `"max"` | nothing; models lit for each pixel, shadows at 512x512 |
A client in another's game keeps grass, lens flares and the fog layer as
the host's rules have them (`cheats.c`). Shadows, specular passes,
reflections and detail objects are draws (CPU, which the Frame's GL
driver is short of); bump maps, fog, camouflage and per-pixel lighting
the GPU's fill.
VR options (Y): two pages, PLAY (the comfort rows, "Weapon hand",
"Mirrored controls") and MORE ("Graphics", "Refresh rate", "Resolution",
the controller buttons page, Quit). The refresh rate steps through what
the runtime offers and changes at once; on the Steam Frame the row shows
SteamOS's rate. The resolution (`vr.resolution_scale`, 70-120%) takes a
restart. The controller buttons page shows the layout as set.
| Setting | Values |
| --- | --- |
| `vr.handedness` | `"right"` (default), `"left"` |
| `vr.mirror_controls` | `true` (default): sticks and buttons mirrored when left-handed; `false`: as right-handed (triggers and grips still follow the hands) |
| `vr.graphics` | `"low"`, `"medium"`, `"high"` (default), `"max"` |
| `vr.refresh_rate` | Hz, as offered (Quest 3: 72, 80, 90, 120) |
| `vr.resolution_scale` | 0.7 to 1.2, on restart |
To try in the headset (Quest 3 and Steam Frame):
1. Left-handed: aim, laser, crosshair, scope of sniper and rocket
launcher; weapon held right in the left hand, arms from the right
shoulders, nothing inside out (faces culled the wrong way would show
the inside of the gun or the glove).
2. Two-handed grip with the right hand; grenades in the right hand (held
and thrown); melee by grip and by swing; motion tracker on the right
wrist.
3. Buttons and sticks mirrored on the Touch and the Frame controllers
(every function reachable), then `Mirrored controls: Off`.
4. Vehicles: wheel, throttle and brake, steering stick, a gunner's aim;
menus and the laser pointer; a network game (shots from the left hand
on the other machine).
5. Graphics: each preset's look and smoothness (`vr.log_performance`),
in a bot game; Max on the Quest at 90 Hz; refresh rates 72/80/90/120
on the Quest; the Frame's row.
## Step 3a (done)
Tried on the Quest 3 and the Steam Frame, in `main`. The arm IK,
the forearm's twist and the fingers' poses follow HaloCE-Quest-VR (CC0),
in `port/linux/game/vr_arms.c`.
- The first-person pose (weapon, arms) is built once a frame instead of
per eye and scope view (SteamVR: two to three times before).
- `vr.log_performance` reports its cost (`VR: first person: ... ms a
frame in ... builds (arms ... ms), ... kept`) and, once, how the hands'
skin hangs from its bones (none from the root: no triangle filter
needed).
- Both arms bend from shoulders below the neck, square to the torso. The
torso follows the head beyond 15 degrees and turns towards the hands;
snap and smooth turns carry it at once.
- The free left arm bends at the elbow to the controller (before: carried
rigidly). The hand's roll is shared along forearm and upper arm.
- The free hand's fingers follow the controller: index from the trigger,
the others from the grip, the thumb from where it rests (touch on the
Quest's, Index and Steam Frame controllers; half curled where unknown),
closed round a held grenade. The weapon hand keeps its grip; its index
finger lifts off the trigger when the controller says it is off.
- host_xr: touch actions for thumb and index; `suggest()` takes 64
bindings (was 32, silently cut) and retries a profile without the
touch paths should the runtime refuse them.
Settings (`[vr]` in config.toml; the first two also in the VR options,
Y on the main or pause menu):
| Setting | Values |
| --- | --- |
| `vr.body` | `"arms"` (default), `"hands"` (hands alone, the arms shrunk behind the wrists), `"off"` (the weapon alone) |
| `vr.fingers` | `true` (default): fingers follow the controller; `false`: as the weapon's animation has them |
| `vr.arm_ik` | `true` (default): the free arm bends at the elbow; `false`: rigid as before, should its skin tear |
After the first Quest tries (2026-10-06):
- The shoulders 13 cm behind the eyes and 20 cm below the neck: the open
end of the sleeve was in view. Looking hard down to the side one still
sees into the arm (the model ends at the shoulder): left as it is.
- The elbow bends in the arm's plane (`vr_align_elbow`): it had turned in
with the elbow out to the side; the upper arm takes none of the hand's
roll.
- The free arm is solved first and its hand turned after (as
HaloCE-Quest-VR does): turned first, its skin stretched like a rubber
band.
- The elbow follows the hand's roll (lifted out, the controller rolls in):
before, it bent only towards a fixed direction.
- Arm length, shoulder width, fingers, grenade grip, body modes, scope,
vehicles: fine.
Not in it (3a.7): the glove fitted to each weapon's grip (gun anchor) and
the left hand animated through reloads.
## Open points outside the order
- Multiview on the Steam Frame: why the eyes do not fuse there (suspected
Mesa zink/Turnip); tools in branch `frame/doubled-images` (86335828),
not merged.
- Transparent geometry groups: the Xbox's 384 a window overflow in games
of 30 and more players.
- The VR on-screen keyboard shows a password in the clear while it is
typed.
## Headset tests (2026-10-06, all three in `main`)
Quest 3, 2026-10-06: menus, laser, VR options, head turning, arms and
hands, fingers, body modes, scope, vehicles, a bot game (bots spread and
leave their bases) and a large online game: all good.
Steam Frame, 2026-10-06: buttons, picture (no doubled image), arms, hands,
fingers, wheel: all good; SteamVR reprojected 18% of the frames, unseen.
What follows is left for the two headsets together.
Upstream merge (`merge/upstream-2026-10-06`):
- Menus, a bot game, head turning (no doubled image), co-op briefly.
- A password game hosted on the Frame, joined from the Quest.
- A bot making room for a person joining a full lobby.
Performance (`frame/performance-2`, and the merge's upstream speed-ups),
with `vr.log_performance = true`:
- Frame: the log says `trace markers: N descriptors to /dev/null`; frame
time ("working") against before (about 12.5 ms in a bot game on Blood
Gulch); SteamVR's reprojected share (vrcompositor.txt) against before
(2 to 17 %); no doubled image.
- Lens flares still appear and hide as before (their visibility counters'
copies changed).
- The pose cache (3a.1): `first person: ... builds` about one a frame;
scope, crosshair, laser and the weapon in the hand as before.
- Quest: 90 Hz held as before in a bot game and in a large online game;
nothing slower than the last build.
Arms and hands (3a): on the Steam Frame, as on the Quest (fingers on its
controllers, every button still working).
+17 -4
View File
@@ -13,9 +13,22 @@ def buildNumberText = System.getenv("HALO_BUILD_NUMBER") ?: "0"
def buildNumber = buildNumberText.isInteger() ? buildNumberText.toInteger() : 0
// the workflow's signing key (.github/workflows/build.yml): every build of
// main signed with it can install over the one before; without it, the
// main signed with it can install over the one before; on a computer that
// builds for its own devices, the same key from port/android/signing.properties
// (not in the repository: storeFile, password, keyAlias), so that its
// builds and the releases install over each other; without either, the
// debug key of the computer that builds
def signingKey = System.getenv("HALO_ANDROID_KEYSTORE")
def signingPassword = System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
def signingAlias = System.getenv("HALO_ANDROID_KEY_ALIAS") ?: "halo"
def signingProperties = new File(rootDir, "signing.properties")
if (!signingKey && signingProperties.isFile()) {
def properties = new Properties()
signingProperties.withInputStream { properties.load(it) }
signingKey = properties.getProperty("storeFile")
signingPassword = properties.getProperty("password")
signingAlias = properties.getProperty("keyAlias", "halo")
}
android {
namespace "com.halo.decomp"
@@ -60,9 +73,9 @@ android {
if (signingKey) {
halo {
storeFile file(signingKey)
storePassword System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
keyAlias System.getenv("HALO_ANDROID_KEY_ALIAS") ?: "halo"
keyPassword System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
storePassword signingPassword
keyAlias signingAlias
keyPassword signingPassword
}
}
}
@@ -35,6 +35,7 @@ import java.nio.channels.FileChannel;
*/
public class LauncherActivity extends Activity {
private static final int PICK_IMAGE = 1;
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/HaloCE";
private File dataRoot;
private TextView status;
@@ -46,6 +47,17 @@ public class LauncherActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dataRoot = getExternalFilesDir(null);
// the Steam Frame: Lepton empties the app's own folders whenever it
// starts again; the game's data lives in the SteamOS user's
// Documents instead, whenever they can be written, so that a disc
// image imported here stays too (port/android/STEAM_FRAME.md; the
// same choice as host_main.c's)
File shared = new File(SHARED_DATA_ROOT);
if (shared.getParentFile().canWrite() || shared.canWrite()) {
shared.mkdirs();
if (shared.canWrite())
dataRoot = shared;
}
// created by the app, so that files pushed into it with adb stay
// readable (a directory adb creates there belongs to the shell user)
if (dataRoot != null)
+11 -1
View File
@@ -68,6 +68,9 @@ int host_sdl_gl_swap_window(unsigned int window);
int host_sdl_poll_event(void *event);
int host_sdl_set_clipboard_text(const char *text);
void host_sdl_get_clipboard_text(char *buffer, unsigned int size);
/* the keyboard's keys by name (the controls' bindings, xinput_sdl.c) */
void host_sdl_scancode_name(int scancode, char *buffer, unsigned int size);
int host_sdl_scancode_from_name(const char *name);
int host_sdl_show_toast(const char *message, int duration, int gravity, int x, int y);
int host_sdl_show_simple_message_box(unsigned int flags, const char *title, const char *message);
int host_sdl_get_gamepads(unsigned int *ids, int capacity);
@@ -116,7 +119,9 @@ headset before the game started, and set HALO_VR); 0 on failure */
int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate);
/* waits for the next frame and reports the predicted poses */
int host_xr_begin_frame(struct halo_xr_frame *frame);
/* copies a render target texture (0: clears) into a layer's image */
/* copies a render target texture (0: clears) into a layer's image: 0 not
copied, 1 copied with the GL state put back, 2 copied with the copy's GL
state left (the deferred copies, host_xr.c), which the game sets again */
int host_xr_copy_layer(int layer, unsigned int texture, int clear_alpha);
/* the same for part of the texture: left, top, right, bottom, 0..1 from its
top left */
@@ -130,6 +135,11 @@ into a layer's image, opaque */
int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer);
/* darkens the eye images from their edges, strength 0..1 (0: off) */
void host_xr_set_vignette(float strength);
/* the refresh rates the headset offers (Hz, at most maximum of them; 0 when
it lets none be chosen), and the one it runs at (0: unknown); and asking
for the highest offered at or below wanted (0: the highest) */
int host_xr_refresh_rates(float *rates, int maximum, float *current);
void host_xr_set_refresh_rate(float wanted);
/* ---------- Android */
+13
View File
@@ -39,6 +39,19 @@ const char *SDL_GetError(void)
return buffer;
}
const char *SDL_GetScancodeName(SDL_Scancode scancode)
{
static __thread char buffer[64];
host_sdl_scancode_name((int)scancode, buffer, sizeof(buffer));
return buffer;
}
SDL_Scancode SDL_GetScancodeFromName(const char *name)
{
return (SDL_Scancode)host_sdl_scancode_from_name(name);
}
Uint64 SDL_GetTicks(void)
{
return (Uint64)host_sdl_ticks();
+1
View File
@@ -92,6 +92,7 @@ void *host_resolve_import(const char *name);
/* ---------- SDL / GL (host_sdl.c, host_gl.c) */
void *host_gl_resolve(const char *name);
int host_gl_quiet_trace_markers(void);
/* ---------- VR (host_xr.c) */
+50 -9
View File
@@ -11,8 +11,52 @@ already have host types by then. Only strings need copying back.
#include <EGL/egl.h>
#include <GLES3/gl32.h>
#include <GLES2/gl2ext.h>
#include <dirent.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* Valve's Mesa (the Steam Frame's Lepton) writes a marker to the kernel's
trace_marker around every flush and context switch, a couple of hundred a
frame, whether or not anything traces. The descriptors it holds open on that
file are pointed at /dev/null instead (not closed: a reused descriptor would
take the markers elsewhere). After astromaddie/HaloCE-VR (CC0). Returns how
many were. */
int host_gl_quiet_trace_markers(void)
{
DIR *directory = opendir("/proc/self/fd");
struct dirent *entry;
int quieted = 0, null_fd;
if (!directory)
return 0;
null_fd = open("/dev/null", O_WRONLY | O_CLOEXEC);
while (null_fd >= 0 && (entry = readdir(directory)) != NULL)
{
char link[64], target[256];
ssize_t length;
int fd = atoi(entry->d_name);
if (entry->d_name[0] < '0' || entry->d_name[0] > '9' || fd == null_fd)
continue;
snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
length = readlink(link, target, sizeof(target) - 1);
if (length <= 0)
continue;
target[length] = 0;
if (strstr(target, "tracing/trace_marker") && dup3(null_fd, fd, O_CLOEXEC) == fd)
quieted++;
}
if (null_fd >= 0)
close(null_fd);
closedir(directory);
if (quieted)
host_logf(HOST_LOG_INFO, "trace markers: %d descriptors to /dev/null", quieted);
return quieted;
}
void *host_gl_resolve(const char *name)
{
@@ -88,14 +132,13 @@ half-drawn targets out of tile memory, for every lens flare. */
static GLuint counter_snapshots[COUNTER_SNAPSHOTS];
static uint32_t counter_snapshot_sizes[COUNTER_SNAPSHOTS];
/* (the copy bindings are left unbound after: the renderer binds them itself
for each copy. Asking GL what they were would wait for Mesa's GL thread, on
the Frame, to run everything queued) */
void host_gl_counters_snapshot(uint32_t buffer, uint32_t size, uint32_t slot)
{
GLint read_binding = 0, write_binding = 0;
if (slot >= COUNTER_SNAPSHOTS || !buffer || !size)
return;
glGetIntegerv(GL_COPY_READ_BUFFER_BINDING, &read_binding);
glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &write_binding);
if (!counter_snapshots[slot])
glGenBuffers(1, &counter_snapshots[slot]);
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
@@ -108,19 +151,17 @@ void host_gl_counters_snapshot(uint32_t buffer, uint32_t size, uint32_t slot)
glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
glBindBuffer(GL_COPY_READ_BUFFER, buffer);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, size);
glBindBuffer(GL_COPY_READ_BUFFER, (GLuint)read_binding);
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
glBindBuffer(GL_COPY_READ_BUFFER, 0);
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
}
/* copies a snapshot out; its frame must be finished (host_gl_wait_frame) */
int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
{
GLint write_binding = 0;
const void *mapping;
if (slot >= COUNTER_SNAPSHOTS || !counter_snapshots[slot] || size > counter_snapshot_sizes[slot])
return 0;
glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &write_binding);
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
mapping = glMapBufferRange(GL_COPY_WRITE_BUFFER, 0, size, GL_MAP_READ_BIT);
if (mapping)
@@ -128,7 +169,7 @@ int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
memcpy(out, mapping, size);
glUnmapBuffer(GL_COPY_WRITE_BUFFER);
}
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
return mapping != NULL;
}
+41
View File
@@ -90,6 +90,8 @@ int host_errno(void)
static char data_root[512];
static char save_root[512];
/* where a device that empties the app's folders keeps the game's data */
#define SHARED_DATA_ROOT "/home/steamos/Documents/HaloCE"
void host_android_path(int which, char *buffer, uint32_t size)
{
@@ -269,6 +271,12 @@ static void *game_main(void *unused)
external = SDL_GetAndroidExternalStoragePath();
if (!external)
host_fatal("Android storage is unavailable: %s", SDL_GetError());
/* the Steam Frame: Lepton empties the app's own folders whenever it
starts again; the game's data lives in the SteamOS user's Documents
instead, whenever they can be written (port/android/STEAM_FRAME.md; the
same choice as LauncherActivity's, which makes the folder) */
if (access(SHARED_DATA_ROOT, W_OK) == 0)
external = SHARED_DATA_ROOT;
snprintf(data_root, sizeof(data_root), "%s", external);
snprintf(save_root, sizeof(save_root), "%s/save", external);
/* readable by adb (the shell user), for managing saves */
@@ -293,6 +301,19 @@ static void *game_main(void *unused)
environment_set(&environment, "HALO_SAVE_ROOT", save_root);
time_zone(zone, sizeof(zone));
environment_set(&environment, "TZ", zone);
/* internet play's MQTT brokers (network.brokers_file): the APK's list,
written beside config.toml at each start, as a desktop update replaces
the file beside its game */
{
size_t brokers_size = 0;
void *brokers = SDL_LoadFile("brokers.txt", &brokers_size);
snprintf(path, sizeof(path), "%s/brokers.txt", data_root);
if (!brokers || !SDL_SaveFile(path, brokers, brokers_size))
host_logf(HOST_LOG_ERROR, "cannot write %s: %s", path, SDL_GetError());
SDL_free(brokers);
}
snprintf(path, sizeof(path), "%s/config.toml", data_root);
image = SDL_LoadFile("halo_guest.elf", &image_size);
if (!image)
@@ -305,6 +326,26 @@ static void *game_main(void *unused)
ranges: the headset's runtime, loaded by the probe, maps its libraries
wherever there is room, the Xbox window at 0x80000000 included */
snprintf(path, sizeof(path), "%s/config.toml", data_root);
{
/* (debug.gl_thread: Mesa's GL thread on or off, as the driver's
mesa_glthread, before the GL context is made; -1 leaves it be) */
double gl_thread = config_number(path, "debug.gl_thread", -1.0);
if (gl_thread >= 0.0)
{
setenv("mesa_glthread", gl_thread != 0.0 ? "true" : "false", 1);
host_logf(HOST_LOG_INFO, "Mesa's GL thread %s (debug.gl_thread)", gl_thread != 0.0 ? "on" : "off");
}
}
{
/* (debug.xr_deferred_copies: the headset's layer copies deferred,
host_xr.c, or one by one; -1 as the runtime has it, deferred on
SteamVR) */
double deferred = config_number(path, "debug.xr_deferred_copies", -1.0);
if (deferred >= 0.0)
setenv("HALO_XR_DEFERRED", deferred != 0.0 ? "1" : "0", 1);
}
{
int eye_width, eye_height;
+15 -1
View File
@@ -98,6 +98,16 @@ void host_sdl_get_error(char *buffer, uint32_t size)
SDL_strlcpy(buffer, SDL_GetError(), size);
}
void host_sdl_scancode_name(int32_t scancode, char *buffer, uint32_t size)
{
SDL_strlcpy(buffer, SDL_GetScancodeName((SDL_Scancode)scancode), size);
}
int32_t host_sdl_scancode_from_name(const char *name)
{
return (int32_t)SDL_GetScancodeFromName(name);
}
int64_t host_sdl_ticks(void)
{
return (int64_t)SDL_GetTicks();
@@ -141,7 +151,11 @@ uint32_t host_sdl_gl_create_context(uint32_t window)
{
SDL_Window *object = handle_get(window, _handle_window);
return object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
uint32_t context = object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
/* (the driver opens its trace markers with the context) */
host_gl_quiet_trace_markers();
return context;
}
int host_sdl_gl_make_current(uint32_t window, uint32_t context)
+667 -57
View File
@@ -38,6 +38,8 @@ picture; the copy flips them to GL's bottom-up images.
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sched.h>
#include <stdio.h>
#define XR_USE_PLATFORM_ANDROID
#define XR_USE_GRAPHICS_API_OPENGL_ES
@@ -56,6 +58,7 @@ struct xr_swapchain
GLuint framebuffers[MAXIMUM_SWAPCHAIN_IMAGES];
uint32_t acquired_index;
int filled; /* this frame */
int held; /* acquired, released only as the frame ends (xr.deferred) */
};
struct xr_hand_actions
@@ -102,15 +105,54 @@ static struct
XrAction button_b; /* B on the right controller, Y on the left */
XrAction menu;
XrAction thumbrest;
/* the fingers' touch, for the hands drawn (where the runtime has it):
the thumb on the stick, a button or the rest, the index on the trigger */
XrAction thumb_touch;
XrAction index_touch;
/* the Steam Frame's controllers' own (XR_VALVE_frame_controller_interaction) */
int has_frame_controller;
int frame_layout;
XrAction button_x;
XrAction button_y;
XrAction view;
XrAction bumper;
XrAction dpad[4];
XrAction haptic;
struct xr_hand_actions hands[HALO_XR_HAND_COUNT];
/* where the head and the left hand are this frame (the wrist layer) */
/* where the head and the hands are this frame (the wrist layer) */
XrPosef head;
XrPosef left_grip;
int left_grip_valid;
XrPosef grips[HALO_XR_HAND_COUNT];
int grips_valid[HALO_XR_HAND_COUNT];
int has_performance_settings;
/* (XR_META_performance_metrics: the compositor's counts, in the log,
for telling frames shown late from the game's own pace) */
int has_metrics;
PFN_xrQueryPerformanceMetricsCounterMETA query_metric;
XrPath metric_paths[16];
uint32_t metric_count;
struct timespec metrics_logged;
/* (SteamVR: the frames' thread kept on the fast cores, xr_fast_cores) */
int steamvr;
int cores_pinned;
/* (with the metrics: the time the copies into the layers' images and
xrEndFrame took, since the last line, and the frames) */
double copy_seconds[HALO_XR_LAYER_COUNT];
double end_seconds;
double acquire_seconds;
double release_seconds;
int timed_frames;
/* The copies without waits in the frame (SteamVR: HALO_XR_DEFERRED,
debug.xr_deferred_copies). Each copy otherwise reads the GL state back
to restore it (glGet*), and releases its image at once: with Mesa's GL
thread (the Steam Frame's zink) each read waits for the thread to run
everything queued, and each release for the GPU to finish it, so the
game's one thread stood still for the eyes' whole drawing in the middle
of every frame. Deferred, a copy leaves its GL state for the game to set
again (host_xr_copy_layer's 2) and keeps its image until the frame ends,
where all are released at once. */
int deferred;
int has_refresh_rate;
PFN_xrRequestDisplayRefreshRateFB request_refresh_rate;
PFN_xrGetDisplayRefreshRateFB get_refresh_rate;
@@ -122,6 +164,11 @@ static struct
GLint rect_uniform;
GLint cut_disc_uniform;
GLint vignette_uniform;
GLint alpha_from_colour_uniform;
/* the runtime draws a quad's texels by their alpha alone, ignoring the
colour of those with none (SteamVR on the Steam Frame): the game's
overlays, whose additive parts have none, get alpha from their colour */
int quad_alpha_from_colour;
GLint tangents_uniform;
float vignette;
/* the same copy from a layer of a texture array (the eyes drawn at
@@ -223,6 +270,7 @@ static const char copy_fragment_shader[] =
"precision highp float;\n"
"uniform sampler2D source;\n"
"uniform float cut_disc;\n"
"uniform float alpha_from_colour; /* 1: as opaque as it is bright (a quad's, SteamVR) */\n"
"uniform float vignette; /* 0..1: the eye images darken from their edges */\n"
"uniform vec4 tangents; /* the eye's edges: left, right, down, up */\n"
"in vec2 coordinate;\n"
@@ -236,6 +284,8 @@ static const char copy_fragment_shader[] =
"{\n"
" vec4 texel = texture(source, coordinate);\n"
" color = vec4(to_linear(texel.rgb), texel.a);\n"
" if (alpha_from_colour > 0.5)\n"
" color.a = max(color.a, max(color.r, max(color.g, color.b)));\n"
" if (cut_disc > 0.5)\n"
" {\n"
" float inside = 1.0 - smoothstep(0.95, 1.0, length(disc));\n"
@@ -326,6 +376,7 @@ static int copy_initialize(void)
xr.rect_uniform = glGetUniformLocation(xr.program, "rect");
xr.cut_disc_uniform = glGetUniformLocation(xr.program, "cut_disc");
xr.vignette_uniform = glGetUniformLocation(xr.program, "vignette");
xr.alpha_from_colour_uniform = glGetUniformLocation(xr.program, "alpha_from_colour");
xr.tangents_uniform = glGetUniformLocation(xr.program, "tangents");
xr.copy_array_layer = -1;
{
@@ -538,22 +589,51 @@ struct binding
const char *path;
};
static void suggest(const char *profile, const struct binding *bindings, int count)
{
XrActionSuggestedBinding suggested[32];
XrInteractionProfileSuggestedBinding information = { XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
int index;
/* a profile's bindings: those it has to have, and those it may (a path the
runtime does not know refuses the whole suggestion, which leaves the
profile with none: then the first alone, as before). Nonzero if taken. */
#define MAXIMUM_SUGGESTED_BINDINGS 64
for (index = 0; index < count && index < 32; index++)
static int suggest_bindings(const char *profile, const struct binding *bindings, int count,
const struct binding *optional, int optional_count)
{
XrActionSuggestedBinding suggested[MAXIMUM_SUGGESTED_BINDINGS];
XrInteractionProfileSuggestedBinding information = { XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
XrResult result;
int index, total = 0;
if (count + optional_count > MAXIMUM_SUGGESTED_BINDINGS)
{
suggested[index].action = *bindings[index].action;
suggested[index].binding = path_from_string(bindings[index].path);
host_logf(HOST_LOG_ERROR, "xr: %d bindings for %s, room for %d: the rest left out", count + optional_count,
profile, MAXIMUM_SUGGESTED_BINDINGS);
}
for (index = 0; index < count + optional_count && total < MAXIMUM_SUGGESTED_BINDINGS; index++)
{
const struct binding *binding = index < count ? &bindings[index] : &optional[index - count];
suggested[total].action = *binding->action;
suggested[total].binding = path_from_string(binding->path);
total++;
}
information.interactionProfile = path_from_string(profile);
information.suggestedBindings = suggested;
information.countSuggestedBindings = (uint32_t)index;
if (!XR_SUCCEEDED(xrSuggestInteractionProfileBindings(xr.instance, &information)))
host_logf(HOST_LOG_WARN, "xr: no bindings for %s", profile);
information.countSuggestedBindings = (uint32_t)total;
result = xrSuggestInteractionProfileBindings(xr.instance, &information);
if (XR_SUCCEEDED(result))
return 1;
if (optional_count > 0)
{
host_logf(HOST_LOG_WARN, "xr: %s refused with its optional bindings (%d); suggested without them", profile,
(int)result);
return suggest_bindings(profile, bindings, count, NULL, 0);
}
host_logf(HOST_LOG_WARN, "xr: no bindings for %s (%d)", profile, (int)result);
return 0;
}
static void suggest(const char *profile, const struct binding *bindings, int count)
{
suggest_bindings(profile, bindings, count, NULL, 0);
}
static int actions_initialize(void)
@@ -580,6 +660,16 @@ static int actions_initialize(void)
xr.button_b = action_create("button_upper", "B / Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.menu = action_create("menu", "Menu", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.thumbrest = action_create("thumbrest", "Thumb rest", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.thumb_touch = action_create("thumb_touch", "Thumb resting", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.index_touch = action_create("index_touch", "Finger on the trigger", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.button_x = action_create("button_x", "X", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.button_y = action_create("button_y", "Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.view = action_create("view", "View", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.bumper = action_create("bumper", "Bumper", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.dpad[0] = action_create("dpad_up", "D-pad up", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.dpad[1] = action_create("dpad_down", "D-pad down", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.dpad[2] = action_create("dpad_left", "D-pad left", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.dpad[3] = action_create("dpad_right", "D-pad right", XR_ACTION_TYPE_BOOLEAN_INPUT);
xr.haptic = action_create("haptic", "Rumble", XR_ACTION_TYPE_VIBRATION_OUTPUT);
{
@@ -644,8 +734,95 @@ static int actions_initialize(void)
{ &xr.haptic, "/user/hand/right/output/haptic" },
};
suggest("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]));
suggest("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[0]));
/* the Steam Frame's controllers, as themselves: the right one has
A, B, X, Y and a menu button, the left one a D-pad and a View button
(port/android/STEAM_FRAME.md) */
static const struct binding frame_controller[] =
{
{ &xr.grip_pose, "/user/hand/left/input/grip/pose" },
{ &xr.grip_pose, "/user/hand/right/input/grip/pose" },
{ &xr.aim_pose, "/user/hand/left/input/aim/pose" },
{ &xr.aim_pose, "/user/hand/right/input/aim/pose" },
{ &xr.trigger, "/user/hand/left/input/trigger/value" },
{ &xr.trigger, "/user/hand/right/input/trigger/value" },
{ &xr.squeeze, "/user/hand/left/input/squeeze/value" },
{ &xr.squeeze, "/user/hand/right/input/squeeze/value" },
{ &xr.thumbstick, "/user/hand/left/input/thumbstick" },
{ &xr.thumbstick, "/user/hand/right/input/thumbstick" },
{ &xr.thumbstick_click, "/user/hand/left/input/thumbstick/click" },
{ &xr.thumbstick_click, "/user/hand/right/input/thumbstick/click" },
{ &xr.button_a, "/user/hand/right/input/a/click" },
{ &xr.button_b, "/user/hand/right/input/b/click" },
{ &xr.button_x, "/user/hand/right/input/x/click" },
{ &xr.button_y, "/user/hand/right/input/y/click" },
{ &xr.menu, "/user/hand/right/input/menu/click" },
{ &xr.view, "/user/hand/left/input/view/click" },
{ &xr.bumper, "/user/hand/left/input/bumper/click" },
{ &xr.bumper, "/user/hand/right/input/bumper/click" },
{ &xr.dpad[0], "/user/hand/left/input/dpad_up/click" },
{ &xr.dpad[1], "/user/hand/left/input/dpad_down/click" },
{ &xr.dpad[2], "/user/hand/left/input/dpad_left/click" },
{ &xr.dpad[3], "/user/hand/left/input/dpad_right/click" },
{ &xr.haptic, "/user/hand/left/output/haptic" },
{ &xr.haptic, "/user/hand/right/output/haptic" },
};
/* the fingers' touch, as each profile has it (after HaloCE-Quest-VR,
CC0; the Frame's from Valve's "Steam Frame Input" page): optional, a
profile refusing them keeps its buttons */
static const struct binding touch_fingers[] =
{
{ &xr.thumb_touch, "/user/hand/left/input/thumbstick/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/thumbstick/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/thumbrest/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/thumbrest/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/x/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/y/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/a/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/b/touch" },
{ &xr.index_touch, "/user/hand/left/input/trigger/touch" },
{ &xr.index_touch, "/user/hand/right/input/trigger/touch" },
};
static const struct binding index_fingers[] =
{
{ &xr.thumb_touch, "/user/hand/left/input/thumbstick/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/thumbstick/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/trackpad/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/trackpad/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/a/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/b/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/a/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/b/touch" },
{ &xr.index_touch, "/user/hand/left/input/trigger/touch" },
{ &xr.index_touch, "/user/hand/right/input/trigger/touch" },
};
static const struct binding frame_fingers[] =
{
{ &xr.thumb_touch, "/user/hand/left/input/thumbstick/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/thumbstick/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/a/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/b/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/x/touch" },
{ &xr.thumb_touch, "/user/hand/right/input/y/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/dpad_up/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/dpad_down/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/dpad_left/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/dpad_right/touch" },
{ &xr.thumb_touch, "/user/hand/left/input/view/touch" },
{ &xr.index_touch, "/user/hand/left/input/trigger/touch" },
{ &xr.index_touch, "/user/hand/right/input/trigger/touch" },
};
if (xr.has_frame_controller)
{
suggest_bindings("/interaction_profiles/valve/frame_controller_valve", frame_controller,
sizeof(frame_controller) / sizeof(frame_controller[0]), frame_fingers,
sizeof(frame_fingers) / sizeof(frame_fingers[0]));
}
suggest_bindings("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]),
touch_fingers, sizeof(touch_fingers) / sizeof(touch_fingers[0]));
suggest_bindings("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[0]),
index_fingers, sizeof(index_fingers) / sizeof(index_fingers[0]));
suggest("/interaction_profiles/khr/simple_controller", simple, sizeof(simple) / sizeof(simple[0]));
}
@@ -688,6 +865,21 @@ static int action_boolean(XrAction action, int hand)
return state.currentState != XR_FALSE;
}
/* (and whether the runtime gives the action for the hand at all) */
static int action_boolean_active(XrAction action, int hand, int *active)
{
XrActionStateGetInfo information = { XR_TYPE_ACTION_STATE_GET_INFO };
XrActionStateBoolean state = { XR_TYPE_ACTION_STATE_BOOLEAN };
*active = 0;
information.action = action;
information.subactionPath = xr.hands[hand].path;
if (!XR_SUCCEEDED(xrGetActionStateBoolean(xr.session, &information, &state)) || !state.isActive)
return 0;
*active = 1;
return state.currentState != XR_FALSE;
}
static int action_vector(XrAction action, int hand, float vector[2])
{
XrActionStateGetInfo information = { XR_TYPE_ACTION_STATE_GET_INFO };
@@ -713,6 +905,30 @@ static void hands_read(struct halo_xr_frame *frame)
sync.activeActionSets = &active;
if (!XR_SUCCEEDED(xrSyncActions(xr.session, &sync)))
return;
{
/* (the controllers' profile the runtime chose, in the log as it
changes) */
static XrPath logged[HALO_XR_HAND_COUNT];
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
{
XrInteractionProfileState state = { XR_TYPE_INTERACTION_PROFILE_STATE };
if (XR_SUCCEEDED(xrGetCurrentInteractionProfile(xr.session, xr.hands[hand].path, &state)) &&
state.interactionProfile != logged[hand])
{
char name[XR_MAX_PATH_LENGTH] = "none";
uint32_t length = 0;
logged[hand] = state.interactionProfile;
if (state.interactionProfile != XR_NULL_PATH)
xrPathToString(xr.instance, state.interactionProfile, sizeof(name), &length, name);
if (hand == HALO_XR_HAND_RIGHT)
xr.frame_layout = strstr(name, "frame_controller") != NULL;
host_logf(HOST_LOG_INFO, "xr: the %s hand's controller: %s", hand ? "right" : "left", name);
}
}
}
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
{
struct halo_xr_hand *out = &frame->hands[hand];
@@ -730,11 +946,8 @@ static void hands_read(struct halo_xr_frame *frame)
{
out->flags = flags_from_location(location.locationFlags);
pose_from_xr(&out->grip, &location.pose);
if (hand == HALO_XR_HAND_LEFT)
{
xr.left_grip = location.pose;
xr.left_grip_valid = (out->flags & HALO_XR_POSE_VALID) != 0;
}
xr.grips[hand] = location.pose;
xr.grips_valid[hand] = (out->flags & HALO_XR_POSE_VALID) != 0;
}
location.locationFlags = 0;
if (XR_SUCCEEDED(xrLocateSpace(xr.hands[hand].aim_space, xr.local_space,
@@ -757,7 +970,42 @@ static void hands_read(struct halo_xr_frame *frame)
(action_boolean(xr.menu, hand) ? HALO_XR_BUTTON_MENU : 0) |
(action_boolean(xr.thumbrest, hand) ? HALO_XR_BUTTON_THUMBREST : 0) |
(out->trigger > 0.5f ? HALO_XR_BUTTON_TRIGGER : 0) |
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0);
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0) |
(action_boolean(xr.view, hand) ? HALO_XR_BUTTON_VIEW : 0) |
(action_boolean(xr.bumper, hand) ? HALO_XR_BUTTON_BUMPER : 0) |
(action_boolean(xr.dpad[0], hand) ? HALO_XR_BUTTON_DPAD_UP : 0) |
(action_boolean(xr.dpad[1], hand) ? HALO_XR_BUTTON_DPAD_DOWN : 0) |
(action_boolean(xr.dpad[2], hand) ? HALO_XR_BUTTON_DPAD_LEFT : 0) |
(action_boolean(xr.dpad[3], hand) ? HALO_XR_BUTTON_DPAD_RIGHT : 0);
{
/* the fingers' touch, and whether there is any */
int thumb_active = 0, index_active = 0;
if (action_boolean_active(xr.thumb_touch, hand, &thumb_active))
out->buttons |= HALO_XR_BUTTON_THUMB_TOUCH;
if (action_boolean_active(xr.index_touch, hand, &index_active))
out->buttons |= HALO_XR_BUTTON_INDEX_TOUCH;
if (thumb_active)
out->buttons |= HALO_XR_BUTTON_TOUCH_KNOWN;
}
}
frame->controller_layout = xr.frame_layout ? HALO_XR_LAYOUT_FRAME : HALO_XR_LAYOUT_TOUCH;
if (xr.frame_layout)
{
/* (the Frame's right X, Y and menu button: the game's X, Y and Start,
which the Quest has on the left) */
struct halo_xr_hand *left = &frame->hands[HALO_XR_HAND_LEFT];
struct halo_xr_hand *right = &frame->hands[HALO_XR_HAND_RIGHT];
if (action_boolean(xr.button_x, HALO_XR_HAND_RIGHT))
left->buttons |= HALO_XR_BUTTON_A;
if (action_boolean(xr.button_y, HALO_XR_HAND_RIGHT))
left->buttons |= HALO_XR_BUTTON_B;
if (right->buttons & HALO_XR_BUTTON_MENU)
{
right->buttons &= ~HALO_XR_BUTTON_MENU;
left->buttons |= HALO_XR_BUTTON_MENU;
}
}
}
@@ -774,6 +1022,13 @@ static void refresh_rate_choose(float wanted)
{
return;
}
{
char line[128] = "";
for (index = 0; index < count; index++)
snprintf(line + strlen(line), sizeof(line) - strlen(line), " %.0f", rates[index]);
host_logf(HOST_LOG_INFO, "xr: refresh rates offered:%s Hz", line);
}
/* the highest rate not above the one wanted (0: the highest) */
for (index = 0; index < count; index++)
{
@@ -790,10 +1045,18 @@ On the back of the left wrist, like a watch: behind the palm, where the
grip pose is, and out of the back of the hand. In the left grip pose -Z
runs to the fingers, +Y up the handle and -X out of the back of the hand;
the layer faces -X with its top towards the elbow, as the wrist turned to
the eyes reads it. Shown only while it is. */
the eyes reads it. Shown only while it is. Left-handed (wrist_hand), on the
right wrist, as the mirror image: there +X runs out of the back of the
hand. */
#define WRIST_IMAGE_SIZE 512
#define CROSSHAIR_IMAGE_SIZE 256
/* the names layer: HALO_XR_MAXIMUM_NAMES slots of 4:1, one over another */
#define NAMES_IMAGE_WIDTH 512
#define NAMES_IMAGE_HEIGHT (NAMES_IMAGE_WIDTH / 4 * HALO_XR_MAXIMUM_NAMES)
/* a name's slot, as the game draws it: wider than its image's 4:1 (the
game's 640x480 drawn square): the height a slot shows at, to its width */
#define NAME_SLOT_ASPECT (30.0f / 160.0f)
#define WRIST_BEHIND_PALM 0.05f
#define WRIST_DOWN_HANDLE 0.02f
#define WRIST_ABOVE_HAND 0.04f
@@ -824,26 +1087,32 @@ static void quaternion_rotate_vector(const XrQuaternionf *q, const XrVector3f *v
static int wrist_pose(const struct halo_xr_submit *submit, XrPosef *pose)
{
/* the layer's axes in the grip's: x = -Y, y = +Z, z = -X, so the
layer's right and down, as it is read, are the grip's -Y and -Z */
static const XrQuaternionf layer_in_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
const XrVector3f offset = { -WRIST_ABOVE_HAND, -WRIST_DOWN_HANDLE - submit->wrist_right,
WRIST_BEHIND_PALM - submit->wrist_down };
/* the layer's axes in the left grip's: x = -Y, y = +Z, z = -X, so the
layer's right and down, as it is read, are the grip's -Y and -Z; in the
right grip's x = +Y, y = +Z, z = +X (the mirror image, read the right
way round: its x turned back) */
static const XrQuaternionf layer_in_left_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
static const XrQuaternionf layer_in_right_grip = { 0.5f, 0.5f, 0.5f, 0.5f };
int right = submit->wrist_hand == HALO_XR_HAND_RIGHT;
const XrPosef *grip = &xr.grips[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT];
const XrVector3f offset = { right ? WRIST_ABOVE_HAND : -WRIST_ABOVE_HAND,
-WRIST_DOWN_HANDLE - submit->wrist_right, WRIST_BEHIND_PALM - submit->wrist_down };
const XrVector3f layer_normal = { 0.0f, 0.0f, 1.0f };
/* clockwise as read: about the layer's normal, negatively */
float half_turn = -submit->wrist_turn * WRIST_PI / 360.0f;
/* clockwise as read: about the layer's normal, negatively (the mirror
image's the other way) */
float half_turn = (right ? 1.0f : -1.0f) * submit->wrist_turn * WRIST_PI / 360.0f;
XrQuaternionf turn = { 0.0f, 0.0f, sinf(half_turn), cosf(half_turn) };
XrQuaternionf unturned;
XrVector3f moved, normal, to_eyes;
float distance;
if (!xr.left_grip_valid)
if (!xr.grips_valid[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT])
return 0;
quaternion_rotate_vector(&xr.left_grip.orientation, &offset, &moved);
pose->position.x = xr.left_grip.position.x + moved.x;
pose->position.y = xr.left_grip.position.y + moved.y;
pose->position.z = xr.left_grip.position.z + moved.z;
quaternion_multiply(&xr.left_grip.orientation, &layer_in_grip, &unturned);
quaternion_rotate_vector(&grip->orientation, &offset, &moved);
pose->position.x = grip->position.x + moved.x;
pose->position.y = grip->position.y + moved.y;
pose->position.z = grip->position.z + moved.z;
quaternion_multiply(&grip->orientation, right ? &layer_in_right_grip : &layer_in_left_grip, &unturned);
quaternion_multiply(&unturned, &turn, &pose->orientation);
quaternion_rotate_vector(&pose->orientation, &layer_normal, &normal);
@@ -889,6 +1158,12 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
/* the loader and the runtime keep the activity */
activity = (*environment)->NewGlobalRef(environment, activity);
/* (The Steam Frame lists an implicit API layer of its own for Vulkan
apps, XR_APILAYER_VALVE_fdm_injection, whose library an app cannot load
from its namespace, failing xrCreateInstance; the Android loader reads
no variable to turn it off, so the app carries a pass-through layer
under that library's name: port/android/xr_layer/fdm_injection_stub.c) */
if (!XR_SUCCEEDED(xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR",
(PFN_xrVoidFunction *)&initialize_loader)) || !initialize_loader)
{
@@ -916,6 +1191,23 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
extensions[index].next = NULL;
}
xrEnumerateInstanceExtensionProperties(NULL, extension_count, &extension_count, extensions);
{
/* (the runtime's extensions, in the log: what a headset offers) */
char line[512] = "";
for (index = 0; index < extension_count; index++)
{
if (strlen(line) + strlen(extensions[index].extensionName) + 2 >= sizeof(line))
{
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
line[0] = 0;
}
strcat(line, " ");
strcat(line, extensions[index].extensionName);
}
if (line[0])
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
}
if (!has_extension(extensions, extension_count, XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME))
{
host_logf(HOST_LOG_ERROR, "xr: the runtime has no OpenGL ES support");
@@ -927,9 +1219,15 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
xr.has_performance_settings = has_extension(extensions, extension_count, XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME);
if (xr.has_performance_settings)
enabled[enabled_count++] = XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME;
xr.has_frame_controller = has_extension(extensions, extension_count, "XR_VALVE_frame_controller_interaction");
if (xr.has_frame_controller)
enabled[enabled_count++] = "XR_VALVE_frame_controller_interaction";
xr.has_refresh_rate = has_extension(extensions, extension_count, XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME);
if (xr.has_refresh_rate)
enabled[enabled_count++] = XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME;
xr.has_metrics = has_extension(extensions, extension_count, XR_META_PERFORMANCE_METRICS_EXTENSION_NAME);
if (xr.has_metrics)
enabled[enabled_count++] = XR_META_PERFORMANCE_METRICS_EXTENSION_NAME;
android.applicationVM = machine;
android.applicationActivity = activity;
@@ -954,6 +1252,23 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
}
xrGetSystemProperties(xr.instance, xr.system, &properties);
strncpy(xr.system_name, properties.systemName, sizeof(xr.system_name) - 1);
{
/* (SteamVR: the quads' alpha from their colour; HALO_XR_QUAD_ALPHA
"colour" or "keep" says otherwise, for trying) */
XrInstanceProperties instance = { XR_TYPE_INSTANCE_PROPERTIES };
const char *choice = getenv("HALO_XR_QUAD_ALPHA");
if (XR_SUCCEEDED(xrGetInstanceProperties(xr.instance, &instance)))
{
xr.quad_alpha_from_colour = strstr(instance.runtimeName, "SteamVR") != NULL;
xr.steamvr = xr.quad_alpha_from_colour;
host_logf(HOST_LOG_INFO, "xr: runtime %s", instance.runtimeName);
}
if (choice)
xr.quad_alpha_from_colour = !strcmp(choice, "colour");
if (xr.quad_alpha_from_colour)
host_logf(HOST_LOG_INFO, "xr: the overlays' alpha from their colour");
}
views[0].type = views[1].type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
views[0].next = views[1].next = NULL;
@@ -1032,8 +1347,15 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
{
int size = layer == HALO_XR_LAYER_WRIST || layer == HALO_XR_LAYER_SCOPE ? WRIST_IMAGE_SIZE :
layer == HALO_XR_LAYER_CROSSHAIR ? CROSSHAIR_IMAGE_SIZE : 0;
int width = size ? size : xr.eye_width;
int height = size ? size : xr.eye_height;
if (!swapchain_create(&xr.swapchains[layer], size ? size : xr.eye_width, size ? size : xr.eye_height))
if (layer == HALO_XR_LAYER_NAMES)
{
width = NAMES_IMAGE_WIDTH;
height = NAMES_IMAGE_HEIGHT;
}
if (!swapchain_create(&xr.swapchains[layer], width, height))
return 0;
}
if (!copy_initialize() || !actions_initialize())
@@ -1053,6 +1375,29 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
xr.get_refresh_rate(xr.session, &info->refresh_rate);
}
/* (the runtime's GL binding brings contexts of its own, with their own
trace markers: host_gl.c) */
host_gl_quiet_trace_markers();
if (xr.has_metrics)
{
PFN_xrEnumeratePerformanceMetricsCounterPathsMETA enumerate = NULL;
PFN_xrSetPerformanceMetricsStateMETA set_state = NULL;
XrPerformanceMetricsStateMETA state = { XR_TYPE_PERFORMANCE_METRICS_STATE_META };
xrGetInstanceProcAddr(xr.instance, "xrEnumeratePerformanceMetricsCounterPathsMETA",
(PFN_xrVoidFunction *)&enumerate);
xrGetInstanceProcAddr(xr.instance, "xrSetPerformanceMetricsStateMETA", (PFN_xrVoidFunction *)&set_state);
xrGetInstanceProcAddr(xr.instance, "xrQueryPerformanceMetricsCounterMETA",
(PFN_xrVoidFunction *)&xr.query_metric);
state.enabled = XR_TRUE;
xr.has_metrics = enumerate && set_state && xr.query_metric &&
XR_SUCCEEDED(enumerate(xr.instance, 16, &xr.metric_count, xr.metric_paths)) &&
XR_SUCCEEDED(set_state(xr.session, &state));
host_logf(HOST_LOG_INFO, "xr: performance metrics %s (%u counters)", xr.has_metrics ? "on" : "off",
xr.has_metrics ? xr.metric_count : 0);
}
if (xr.has_performance_settings)
{
/* the game's one busy thread and its GPU work both need the clocks
@@ -1069,6 +1414,16 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
}
}
{
/* (deferred on SteamVR; HALO_XR_DEFERRED "1" or "0" says otherwise:
debug.xr_deferred_copies, host_main.c) */
const char *choice = getenv("HALO_XR_DEFERRED");
xr.deferred = choice && *choice ? atoi(choice) != 0 : xr.steamvr;
host_logf(HOST_LOG_INFO, "xr: layer copies %s", xr.deferred ?
"deferred (no waits for the GL thread or the GPU in the frame)" : "restored and released one by one");
}
info->eye_width = xr.eye_width;
info->eye_height = xr.eye_height;
info->overlay_width = xr.eye_width;
@@ -1205,7 +1560,7 @@ int host_xr_begin_frame(struct halo_xr_frame *frame)
pose_from_xr(&frame->head, &head.pose);
xr.head = head.pose;
}
xr.left_grip_valid = 0;
xr.grips_valid[HALO_XR_HAND_LEFT] = xr.grips_valid[HALO_XR_HAND_RIGHT] = 0;
if (xr.focused)
hands_read(frame);
return 1;
@@ -1232,8 +1587,30 @@ int host_xr_copy_layer_array(int layer, uint32_t texture, int array_layer)
/* copies part of a render target (left, top, right, bottom, 0..1 from its
top left) into a layer's image */
static double seconds_now(void)
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (double)now.tv_sec + (double)now.tv_nsec * 1e-9;
}
static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
float bottom);
int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
float bottom)
{
double start = xr.has_metrics ? seconds_now() : 0.0;
int copied = copy_layer_region(layer, texture, clear_alpha, left, top, right, bottom);
if (xr.has_metrics && layer >= 0 && layer < HALO_XR_LAYER_COUNT)
xr.copy_seconds[layer] += seconds_now() - start;
return copied;
}
static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
float bottom)
{
struct xr_swapchain *swapchain;
XrSwapchainImageAcquireInfo acquire = { XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO };
@@ -1245,17 +1622,27 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
return 0;
swapchain = &xr.swapchains[layer];
if (swapchain->filled)
return 1;
if (!xr_ok(xrAcquireSwapchainImage(swapchain->handle, &acquire, &swapchain->acquired_index),
"xrAcquireSwapchainImage"))
{
return 0;
}
return xr.deferred ? 2 : 1;
wait.timeout = XR_INFINITE_DURATION;
if (!xr_ok(xrWaitSwapchainImage(swapchain->handle, &wait), "xrWaitSwapchainImage"))
return 0;
{
double start = xr.has_metrics ? seconds_now() : 0.0;
gl_save(&saved);
if (!xr_ok(xrAcquireSwapchainImage(swapchain->handle, &acquire, &swapchain->acquired_index),
"xrAcquireSwapchainImage"))
{
return 0;
}
if (!xr_ok(xrWaitSwapchainImage(swapchain->handle, &wait), "xrWaitSwapchainImage"))
return 0;
if (xr.has_metrics)
xr.acquire_seconds += seconds_now() - start;
}
/* (deferred: nothing read back; the game sets its state again) */
if (xr.deferred)
glActiveTexture(GL_TEXTURE0);
else
gl_save(&saved);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, swapchain->framebuffers[swapchain->acquired_index]);
glViewport(0, 0, swapchain->width, swapchain->height);
glDisable(GL_SCISSOR_TEST);
@@ -1303,6 +1690,8 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
glUniform1i(xr.source_uniform, 0);
glUniform4f(xr.rect_uniform, left, top, right, bottom);
glUniform1f(xr.cut_disc_uniform, clear_alpha == HALO_XR_COPY_DISC ? 1.0f : 0.0f);
glUniform1f(xr.alpha_from_colour_uniform, xr.quad_alpha_from_colour && clear_alpha == HALO_XR_COPY_ALPHA &&
layer != HALO_XR_LAYER_EYE_LEFT && layer != HALO_XR_LAYER_EYE_RIGHT ? 1.0f : 0.0f);
if ((layer == HALO_XR_LAYER_EYE_LEFT || layer == HALO_XR_LAYER_EYE_RIGHT) && xr.vignette > 0.001f)
{
const XrFovf *fov = &xr.views[layer - HALO_XR_LAYER_EYE_LEFT].fov;
@@ -1318,13 +1707,22 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
glBindVertexArray(xr.vertex_array);
glBindTexture(GL_TEXTURE_2D, texture);
glBindSampler(0, xr.sampler);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, &swizzle_r);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, &swizzle_b);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, swizzle_r);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, swizzle_b);
if (xr.deferred)
{
/* (the game's render targets, all a copy reads, have no
swizzle: only its uploaded textures get one, xbox_textures.c) */
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
else
{
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, &swizzle_r);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, &swizzle_b);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, swizzle_r);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, swizzle_b);
}
if (clear_alpha == HALO_XR_COPY_OPAQUE)
{
/* an opaque layer: the game's alpha channel holds other things */
@@ -1338,13 +1736,95 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
glClearColor(0.0f, 0.0f, 0.0f, clear_alpha ? 1.0f : 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
swapchain->filled = 1;
if (xr.deferred)
{
swapchain->held = 1;
/* (the eyes, most of the frame's work, on to the GPU while the
game draws the rest) */
if (layer == HALO_XR_LAYER_EYE_RIGHT)
glFlush();
return 2;
}
gl_restore(&saved);
xrReleaseSwapchainImage(swapchain->handle, &release);
swapchain->filled = 1;
return 1;
}
/* deferred: the images the frame's copies kept, released before it ends */
static void release_held(void)
{
double start = xr.has_metrics ? seconds_now() : 0.0;
int layer;
for (layer = 0; layer < HALO_XR_LAYER_COUNT; layer++)
{
XrSwapchainImageReleaseInfo release = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
struct xr_swapchain *swapchain = &xr.swapchains[layer];
if (!swapchain->held)
continue;
swapchain->held = 0;
xr_ok(xrReleaseSwapchainImage(swapchain->handle, &release), "xrReleaseSwapchainImage");
}
if (xr.has_metrics)
xr.release_seconds += seconds_now() - start;
}
/* The Steam Frame: Android in a container, without XR_EXT_performance_settings
to ask for the clocks, and the scheduler free to put the game's one busy
thread, which simulates and draws each frame, on a slow core (its two
2.3 GHz ones) now and then: frames late to the compositor, shown twice,
the picture doubled as the head turns. The thread that ends the frames is
kept on the cores no slower than nine tenths of the fastest. */
static void xr_fast_cores(void)
{
cpu_set_t set;
long fastest = 0, speeds[CPU_SETSIZE];
int cpu, count = 0;
char line[128] = "";
for (cpu = 0; cpu < CPU_SETSIZE && cpu < 64; cpu++)
{
char path[96];
FILE *file;
speeds[cpu] = 0;
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", cpu);
file = fopen(path, "r");
if (!file)
continue;
if (fscanf(file, "%ld", &speeds[cpu]) != 1)
speeds[cpu] = 0;
fclose(file);
if (speeds[cpu] > fastest)
fastest = speeds[cpu];
}
if (!fastest)
{
host_logf(HOST_LOG_INFO, "xr: the cores' speeds unknown: the frames' thread left where it runs");
return;
}
CPU_ZERO(&set);
for (cpu = 0; cpu < CPU_SETSIZE && cpu < 64; cpu++)
{
if (speeds[cpu] && speeds[cpu] * 10 >= fastest * 9)
{
char item[8];
CPU_SET(cpu, &set);
count++;
snprintf(item, sizeof(item), " %d", cpu);
strcat(line, item);
}
}
if (count && sched_setaffinity(0, sizeof(set), &set) == 0)
host_logf(HOST_LOG_INFO, "xr: the frames' thread on the fast cores:%s (fastest %ld MHz)", line, fastest / 1000);
else
host_logf(HOST_LOG_INFO, "xr: the frames' thread could not be kept on the fast cores");
}
int host_xr_end_frame(const struct halo_xr_submit *submit)
{
XrFrameEndInfo end = { XR_TYPE_FRAME_END_INFO };
@@ -1354,13 +1834,15 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
XrCompositionLayerQuad wrist = { XR_TYPE_COMPOSITION_LAYER_QUAD };
XrCompositionLayerQuad scope = { XR_TYPE_COMPOSITION_LAYER_QUAD };
XrCompositionLayerQuad crosshair = { XR_TYPE_COMPOSITION_LAYER_QUAD };
const XrCompositionLayerBaseHeader *layers[5];
XrCompositionLayerQuad names[HALO_XR_MAXIMUM_NAMES];
const XrCompositionLayerBaseHeader *layers[5 + HALO_XR_MAXIMUM_NAMES];
uint32_t layer_count = 0;
int eye;
if (!xr.frame_begun)
return 0;
xr.frame_begun = 0;
release_held();
if (xr.frame_state.shouldRender && submit->eyes && xr.views_valid &&
xr.swapchains[HALO_XR_LAYER_EYE_LEFT].filled && xr.swapchains[HALO_XR_LAYER_EYE_RIGHT].filled)
@@ -1458,11 +1940,111 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
layers[layer_count++] = (const XrCompositionLayerBaseHeader *)&crosshair;
}
if (xr.frame_state.shouldRender && submit->name_count > 0 && xr.swapchains[HALO_XR_LAYER_NAMES].filled)
{
struct xr_swapchain *swapchain = &xr.swapchains[HALO_XR_LAYER_NAMES];
int slot_height = swapchain->height / HALO_XR_MAXIMUM_NAMES;
int name;
for (name = 0; name < submit->name_count && name < HALO_XR_MAXIMUM_NAMES; name++)
{
XrCompositionLayerQuad *quad_name = &names[name];
memset(quad_name, 0, sizeof(*quad_name));
quad_name->type = XR_TYPE_COMPOSITION_LAYER_QUAD;
quad_name->layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
quad_name->space = xr.local_space;
quad_name->eyeVisibility = XR_EYE_VISIBILITY_BOTH;
quad_name->subImage.swapchain = swapchain->handle;
/* (slot 0 at the image's top: GL's rows run bottom up) */
quad_name->subImage.imageRect.offset.x = 0;
quad_name->subImage.imageRect.offset.y = swapchain->height - (name + 1) * slot_height;
quad_name->subImage.imageRect.extent.width = swapchain->width;
quad_name->subImage.imageRect.extent.height = slot_height;
quad_name->size.width = submit->name_widths[name];
quad_name->size.height = submit->name_widths[name] * NAME_SLOT_ASPECT;
pose_to_xr(&quad_name->pose, &submit->name_poses[name]);
layers[layer_count++] = (const XrCompositionLayerBaseHeader *)quad_name;
}
}
if (xr.steamvr && !xr.cores_pinned)
{
xr.cores_pinned = 1;
if (!getenv("HALO_XR_ANY_CORE"))
xr_fast_cores();
}
/* (every five seconds, the compositor's counters, and the core the
frames' thread is on) */
if (xr.has_metrics)
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec - xr.metrics_logged.tv_sec >= 5)
{
char line[768] = "";
uint32_t counter;
xr.metrics_logged = now;
for (counter = 0; counter < xr.metric_count && counter < 16; counter++)
{
XrPerformanceMetricsCounterMETA value = { XR_TYPE_PERFORMANCE_METRICS_COUNTER_META };
char name[XR_MAX_PATH_LENGTH];
uint32_t length = 0;
char item[96];
if (!XR_SUCCEEDED(xr.query_metric(xr.session, xr.metric_paths[counter], &value)) ||
!XR_SUCCEEDED(xrPathToString(xr.instance, xr.metric_paths[counter], sizeof(name), &length, name)))
{
continue;
}
snprintf(item, sizeof(item), " %s=%g", strrchr(name, '/') ? strrchr(name, '/') + 1 : name,
(value.counterFlags & XR_PERFORMANCE_METRICS_COUNTER_FLOAT_VALUE_VALID_BIT_META) ?
(double)value.floatValue : (double)value.uintValue);
if (strlen(line) + strlen(item) < sizeof(line))
strcat(line, item);
}
host_logf(HOST_LOG_INFO, "xr: metrics%s, on core %d", line, sched_getcpu());
if (xr.timed_frames)
{
double frames = (double)xr.timed_frames;
char copies[256] = "";
int timed;
for (timed = 0; timed < HALO_XR_LAYER_COUNT; timed++)
{
char item[24];
snprintf(item, sizeof(item), " %d:%.2f", timed, xr.copy_seconds[timed] * 1e3 / frames);
strcat(copies, item);
xr.copy_seconds[timed] = 0.0;
}
host_logf(HOST_LOG_INFO, "xr: per frame ms: copies (layer:ms)%s; of them acquiring images %.2f; "
"releasing them at the end %.2f; xrEndFrame %.2f (copies %s)", copies,
xr.acquire_seconds * 1e3 / frames, xr.release_seconds * 1e3 / frames, xr.end_seconds * 1e3 / frames,
xr.deferred ? "deferred" : "one by one");
xr.acquire_seconds = xr.release_seconds = xr.end_seconds = 0.0;
xr.timed_frames = 0;
}
}
}
end.displayTime = xr.frame_state.predictedDisplayTime;
end.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
end.layerCount = layer_count;
end.layers = layers;
return xr_ok(xrEndFrame(xr.session, &end), "xrEndFrame");
{
double start = xr.has_metrics ? seconds_now() : 0.0;
int ended = xr_ok(xrEndFrame(xr.session, &end), "xrEndFrame");
if (xr.has_metrics)
{
xr.end_seconds += seconds_now() - start;
xr.timed_frames++;
}
return ended;
}
}
void host_xr_set_vignette(float strength)
@@ -1470,6 +2052,34 @@ void host_xr_set_vignette(float strength)
xr.vignette = strength < 0.0f ? 0.0f : strength > 1.0f ? 1.0f : strength;
}
/* the VR options' refresh rates: those the runtime offers (none where it
lets none be chosen, as SteamVR, whose rate is SteamOS's), and the one it
runs at, by XR_FB_display_refresh_rate or else by its frames' period */
int host_xr_refresh_rates(float *rates, int maximum, float *current)
{
float offered[16];
uint32_t count = 0, index;
*current = 0.0f;
if (!xr.initialized)
return 0;
if (xr.has_refresh_rate)
xr.get_refresh_rate(xr.session, current);
if (*current <= 0.0f && xr.frame_state.predictedDisplayPeriod > 0)
*current = 1e9f / (float)xr.frame_state.predictedDisplayPeriod;
if (!xr.has_refresh_rate || !XR_SUCCEEDED(xr.enumerate_refresh_rates(xr.session, 16, &count, offered)))
return 0;
for (index = 0; index < count && (int)index < maximum; index++)
rates[index] = offered[index];
return (int)index;
}
void host_xr_set_refresh_rate(float wanted)
{
if (xr.initialized)
refresh_rate_choose(wanted);
}
void host_xr_haptic(int hand, float amplitude, float seconds)
{
XrHapticActionInfo information = { XR_TYPE_HAPTIC_ACTION_INFO };
+4
View File
@@ -38,6 +38,8 @@ host_sdl_gamepad_axis
host_sdl_gamepad_button
host_sdl_gamepad_type
host_sdl_rumble_gamepad
host_sdl_scancode_name
host_sdl_scancode_from_name
host_sdl_open_audio_stream
host_sdl_put_audio_stream_data
host_sdl_resume_audio_stream_device
@@ -59,6 +61,8 @@ host_xr_end_frame
host_xr_haptic
host_xr_set_vignette
host_xr_copy_layer_array
host_xr_refresh_rates
host_xr_set_refresh_rate
host_android_path
host_errno
+42 -6
View File
@@ -24,10 +24,13 @@ enum
HALO_XR_LAYER_EYE_LEFT = 0,
HALO_XR_LAYER_EYE_RIGHT = 1,
HALO_XR_LAYER_OVERLAY = 2, /* HUD, menus, cinematics: a flat quad */
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the left wrist */
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the free hand's wrist */
HALO_XR_LAYER_SCOPE = 4, /* the zoomed view, a disc over the weapon */
HALO_XR_LAYER_CROSSHAIR = 5, /* the weapon's crosshair, where it aims */
HALO_XR_LAYER_COUNT = 6,
HALO_XR_LAYER_NAMES = 6, /* players' names, each a quad over a head */
HALO_XR_LAYER_COUNT = 7,
/* the names layer's slots, one a name, stacked from its top */
HALO_XR_MAXIMUM_NAMES = 8,
/* host_xr_copy_layer's clear_alpha: the source's alpha kept, made
opaque, or cut to a disc (premultiplied) */
@@ -50,6 +53,29 @@ enum
HALO_XR_BUTTON_TRIGGER = 1 << 4, /* trigger past its click point */
HALO_XR_BUTTON_SQUEEZE = 1 << 5, /* grip past its click point */
HALO_XR_BUTTON_THUMBREST = 1 << 6, /* thumb resting (touch) */
/* the Steam Frame's controllers' (HALO_XR_LAYOUT_FRAME): the left one's
View button and D-pad, either one's bumper (their right one's X, Y and
menu button come as the left hand's A, B and MENU: the game's X, Y and
Start, as on the Quest) */
HALO_XR_BUTTON_VIEW = 1 << 7,
HALO_XR_BUTTON_BUMPER = 1 << 8,
HALO_XR_BUTTON_DPAD_UP = 1 << 9,
HALO_XR_BUTTON_DPAD_DOWN = 1 << 10,
HALO_XR_BUTTON_DPAD_LEFT = 1 << 11,
HALO_XR_BUTTON_DPAD_RIGHT = 1 << 12,
/* the fingers' touch (the hands drawn: vr.c): the thumb on the stick, a
button or the rest; the index finger on the trigger; and whether the
controller tells the thumb's at all */
HALO_XR_BUTTON_THUMB_TOUCH = 1 << 13,
HALO_XR_BUTTON_INDEX_TOUCH = 1 << 14,
HALO_XR_BUTTON_TOUCH_KNOWN = 1 << 15,
};
/* the controllers' layout, for what the game tells the player */
enum
{
HALO_XR_LAYOUT_TOUCH = 0, /* the Quest's: X, Y and menu on the left */
HALO_XR_LAYOUT_FRAME = 1, /* the Steam Frame's: A, B, X, Y and menu on the right, a D-pad on the left */
};
/* halo_xr_hand.flags, halo_xr_view.flags, halo_xr_frame.head_flags */
@@ -109,6 +135,7 @@ struct halo_xr_frame
struct halo_xr_pose head;
struct halo_xr_view views[HALO_XR_EYE_COUNT];
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
int controller_layout; /* HALO_XR_LAYOUT_* */
};
/* halo_xr_submit.overlay_mode */
@@ -128,15 +155,18 @@ struct halo_xr_submit
float overlay_width; /* metres; the height follows from 4:3 */
/* the screen's placement in LOCAL space (HALO_XR_OVERLAY_SCREEN) */
struct halo_xr_pose screen;
/* the wrist layer was filled: shown on the back of the left wrist,
this wide (metres), while the wrist is turned to the eyes */
/* the wrist layer was filled: shown on the back of the left wrist (of
the right one, left-handed: wrist_hand, a HALO_XR_HAND_*), this wide
(metres), while the wrist is turned to the eyes */
int wrist;
float wrist_size;
/* its placement as the wrist turned to the eyes reads it: turned this
many degrees clockwise, and moved this far right and down (metres) */
/* its placement as the left wrist turned to the eyes reads it: turned
this many degrees clockwise, and moved this far right and down (metres;
on the right wrist, the mirror image) */
float wrist_turn;
float wrist_right;
float wrist_down;
int wrist_hand;
/* the scope layer was filled: a disc this wide (metres), here in LOCAL
space, facing back along the weapon */
int scope;
@@ -147,6 +177,12 @@ struct halo_xr_submit
int crosshair;
float crosshair_size;
struct halo_xr_pose crosshair_pose;
/* the names layer was filled: its first name_count slots, each shown
this wide (metres, a slot's height following from its shape), here in
LOCAL space, facing the eyes */
int name_count;
float name_widths[HALO_XR_MAXIMUM_NAMES];
struct halo_xr_pose name_poses[HALO_XR_MAXIMUM_NAMES];
};
#endif
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+266
View File
@@ -0,0 +1,266 @@
#!/bin/bash
# Halo VR's installer for the Steam Frame, run in its Desktop Mode from the
# bundle the release's "Install Halo VR" desktop file downloads
# (steam_frame_installer.desktop; the bundle: this script, HaloVR.apk,
# halo-vr.sh = steam_frame_launch.sh, steam_frame_xiso.py and the library
# pictures in grid/). Everything is answered with dialogs, no keyboard:
# - the game in ~/Documents/HaloCE (port/android/STEAM_FRAME.md), its APK
# replaced (Lepton installs a newer one at the next start), so running the
# installer again updates the game;
# - the game data: the maps folder copied out of a disc image of Halo
# (.iso or .xiso) found in Downloads, Documents, the Desktop or on a
# memory card, or chosen, unless it is there already;
# - "Halo VR" in the Steam library, with its pictures, unless it is there
# already: Steam is closed for that (it keeps its shortcuts in memory and
# writes them back when it quits), and started again in Gaming Mode.
# HALO_VR_UNATTENDED=1 answers every question with its default and prints
# instead of showing dialogs (HALO_VR_IMAGE: the disc image to use), for
# testing over adb; it never closes Steam.
BUNDLE="$(cd "$(dirname "$0")" && pwd)"
HALO_DIR="$HOME/Documents/HaloCE"
STEAM_DIR="$HOME/.local/share/Steam"
LEPTON="$STEAM_DIR/steamapps/common/Lepton/lepton"
TITLE="Halo VR"
NAME="Halo VR"
EXE="\"$HALO_DIR/halo-vr.sh\""
unattended() { [ "${HALO_VR_UNATTENDED:-0}" = 1 ] || ! command -v zenity >/dev/null; }
info()
{
if unattended; then printf "info: %b\n" "$1"; else zenity --info --title="$TITLE" --width=460 --text="$1"; fi
}
fail()
{
if unattended; then printf "error: %b\n" "$1" >&2; else zenity --error --title="$TITLE" --width=460 --text="$1"; fi
exit 1
}
# yes/no; unattended: the default given ($2, "yes" or "no")
ask()
{
if unattended; then [ "$2" = yes ]; return; fi
zenity --question --title="$TITLE" --width=460 --text="$1"
}
# the disc image the player chooses
choose_image()
{
if unattended; then return 1; fi
zenity --file-selection --title="Choose a disc image of Halo: Combat Evolved (.iso or .xiso)" \
--filename="$HOME/Downloads/" --file-filter="Disc images | *.iso *.ISO *.xiso *.XISO" \
--file-filter="All files | *"
}
# the disc images near at hand, largest first (a Halo image is a few GB;
# anything under 500 MB is not one)
find_images()
{
find "$HOME/Downloads" "$HOME/Documents" "$HOME/Desktop" /run/media -maxdepth 3 -type f \
\( -iname '*.iso' -o -iname '*.xiso' \) -size +500M -printf '%s\t%p\n' 2>/dev/null |
sort -rn | cut -f2-
}
extract_maps()
{
local image="$1" log
log="$(mktemp)"
if unattended; then
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$HALO_DIR" 2>"$log" | grep '^#' | tail -n 1
status=${PIPESTATUS[0]}
else
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$HALO_DIR" 2>"$log" |
zenity --progress --title="$TITLE" --width=460 --auto-close --no-cancel \
--text="Copying the game data out of the disc image..."
status=${PIPESTATUS[0]}
fi
if [ "$status" != 0 ]; then
reason="$(sed -n 's/^error: //p' "$log" | head -n 1)"
rm -f "$log"
fail "The game data could not be copied out of\n$image\n\n${reason:-Unknown error.}"
fi
rm -f "$log"
}
# the shortcut's id as Steam makes it for a non-Steam game: its pictures in
# config/grid are named after it
shortcut_id()
{
python3 -c 'import sys, zlib; print(zlib.crc32((sys.argv[1] + sys.argv[2]).encode()) | 0x80000000)' "$EXE" "$NAME"
}
# whether every Steam user's shortcuts.vdf already holds the game
have_shortcut()
{
local found=1 config
for config in "$STEAM_DIR"/userdata/*/config; do
[ -d "$config" ] || continue
case "$config" in */userdata/0/*) continue ;; esac
grep -qaF "$HALO_DIR/halo-vr.sh" "$config/shortcuts.vdf" 2>/dev/null || return 1
found=0
done
return $found
}
# "Halo VR" added to every Steam user's shortcuts.vdf (Steam closed), as a VR
# title, with its pictures
add_shortcut()
{
local id config
id="$(shortcut_id)"
for config in "$STEAM_DIR"/userdata/*/config; do
[ -d "$config" ] || continue
case "$config" in */userdata/0/*) continue ;; esac
python3 - "$config/shortcuts.vdf" "$id" "$NAME" "$EXE" "\"$HALO_DIR/\"" <<'PYTHON' || return 1
import os, struct, sys
path, appid, name, exe, start = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4], sys.argv[5]
# Steam's binary VDF: 0x00 a map, 0x01 a string, 0x02 an int32, 0x08 a map's end
def read_map(data, at):
items = []
while True:
kind = data[at]; at += 1
if kind == 0x08:
return items, at
end = data.index(b"\0", at); key = data[at:end]; at = end + 1
if kind == 0x00:
value, at = read_map(data, at)
elif kind == 0x01:
end = data.index(b"\0", at); value = data[at:end]; at = end + 1
elif kind == 0x02:
value = struct.unpack_from("<I", data, at)[0]; at += 4
else:
raise ValueError("unknown VDF type %d" % kind)
items.append((kind, key, value))
def write_map(items):
out = b""
for kind, key, value in items:
out += bytes([kind]) + key + b"\0"
if kind == 0x00:
out += write_map(value)
elif kind == 0x01:
out += value + b"\0"
else:
out += struct.pack("<I", value)
return out + b"\x08"
shortcuts = []
if os.path.exists(path):
with open(path, "rb") as file:
data = file.read()
if data:
top, _ = read_map(data, 0)
for kind, key, value in top:
if key.lower() == b"shortcuts" and kind == 0x00:
shortcuts = [entry for entry in value if entry[0] == 0x00]
# (an older entry of the game's replaced)
shortcuts = [entry for entry in shortcuts
if not any(k.lower() == b"exe" and v == exe.encode() for _, k, v in entry[2])]
def text(key, value):
return (0x01, key.encode(), value.encode())
def number(key, value):
return (0x02, key.encode(), value)
entry = [
number("appid", appid), text("AppName", name), text("Exe", exe), text("StartDir", start),
text("icon", ""), text("ShortcutPath", ""), text("LaunchOptions", ""), number("IsHidden", 0),
number("AllowDesktopConfig", 1), number("AllowOverlay", 1), number("OpenVR", 1), number("Devkit", 0),
text("DevkitGameID", ""), number("DevkitOverrideAppID", 0), number("LastPlayTime", 0),
text("FlatpakAppID", ""), text("sortas", ""), (0x00, b"tags", []),
]
shortcuts.append((0x00, b"", entry))
shortcuts = [(0x00, str(index).encode(), value) for index, (_, _, value) in enumerate(shortcuts)]
data = write_map([(0x00, b"shortcuts", shortcuts)])
with open(path + ".tmp", "wb") as file:
file.write(data)
os.replace(path + ".tmp", path)
PYTHON
mkdir -p "$config/grid"
cp "$BUNDLE/grid/library.png" "$config/grid/${id}.png"
cp "$BUNDLE/grid/libraryp.png" "$config/grid/${id}p.png"
cp "$BUNDLE/grid/library_hero.png" "$config/grid/${id}_hero.png"
cp "$BUNDLE/grid/library_logo.png" "$config/grid/${id}_logo.png"
done
}
# --- the game
[ -x "$LEPTON" ] || fail "Lepton, which runs Android games on the Steam Frame, was not found.\n\nStart any Android game from the Steam store once, then run this installer again."
for file in HaloVR.apk halo-vr.sh steam_frame_xiso.py grid/library.png; do
[ -f "$BUNDLE/$file" ] || fail "The installer is incomplete ($file is missing). Download it again."
done
if [ -f "$HALO_DIR/HaloVR.apk" ]; then
ask "Halo VR is installed already. Update it to this version?\n\n(Your settings, saves and game data stay.)" yes || exit 0
else
ask "This installs Halo VR on your Steam Frame.\n\nYou need your own disc image of Halo: Combat Evolved for the Xbox (an .iso or .xiso file, any version), for example in Downloads.\n\nContinue?" yes || exit 0
fi
mkdir -p "$HALO_DIR/maps" || fail "Could not create $HALO_DIR."
# (the game runs in Lepton's container as a user of its own)
chmod 777 "$HALO_DIR" "$HALO_DIR/maps"
cp "$BUNDLE/halo-vr.sh" "$HALO_DIR/halo-vr.sh.tmp" && chmod 755 "$HALO_DIR/halo-vr.sh.tmp" &&
mv -f "$HALO_DIR/halo-vr.sh.tmp" "$HALO_DIR/halo-vr.sh" || fail "Could not write $HALO_DIR/halo-vr.sh."
# (newer than the installed one: Lepton installs it at the next start)
cp "$BUNDLE/HaloVR.apk" "$HALO_DIR/HaloVR.apk.tmp" && chmod 666 "$HALO_DIR/HaloVR.apk.tmp" &&
mv -f "$HALO_DIR/HaloVR.apk.tmp" "$HALO_DIR/HaloVR.apk" && touch "$HALO_DIR/HaloVR.apk" ||
fail "Could not write $HALO_DIR/HaloVR.apk (is the storage full?)."
# --- the game data
if [ ! -f "$HALO_DIR/maps/ui.map" ]; then
image="${HALO_VR_IMAGE:-}"
if [ -z "$image" ]; then
mapfile -t images < <(find_images)
for candidate in "${images[@]}"; do
if ask "Use this disc image?\n\n$candidate" yes; then
image="$candidate"
break
fi
done
fi
if [ -z "$image" ]; then
if [ ${#images[@]} = 0 ]; then
info "No disc image was found in Downloads, Documents, on the Desktop or a memory card.\n\nChoose it in the next window."
fi
image="$(choose_image)" || fail "No disc image was chosen. Run the installer again once it is on the Frame (for example in Downloads)."
fi
extract_maps "$image"
chmod -R a+rwX "$HALO_DIR/maps"
image_note="\n\nThe disc image itself is no longer needed:\n$image"
fi
# --- Steam
steam_closed=0
if ! have_shortcut; then
if unattended; then
echo "info: the Steam shortcut is missing (not added unattended)"
else
info "Halo VR is now added to your Steam library. Steam closes for a moment for that."
if pgrep -x steam >/dev/null; then
steam -shutdown >/dev/null 2>&1 &
for _ in $(seq 60); do pgrep -x steam >/dev/null || break; sleep 1; done
pgrep -x steam >/dev/null && fail "Steam did not close. Close it (Steam menu > Exit) and run the installer again."
fi
steam_closed=1
add_shortcut || fail "Could not add Halo VR to the Steam library."
fi
fi
if unattended; then
echo "info: installed in $HALO_DIR"
exit 0
fi
if ask "Halo VR is installed.${image_note:-}\n\nSwitch to Gaming Mode now? Start Halo VR from your library there." yes; then
steamos-session-select gamescope >/dev/null 2>&1 ||
info "Switch to Gaming Mode yourself (the \"Return to Gaming Mode\" icon or the power menu)."
elif [ "$steam_closed" = 1 ]; then
setsid steam >/dev/null 2>&1 < /dev/null &
fi
@@ -0,0 +1,13 @@
[Desktop Entry]
# Halo VR's installer for the Steam Frame (port/android/STEAM_FRAME.md): a
# release asset, "Install-Halo-VR.desktop", opened in Desktop Mode. It
# downloads the latest release's bundle (tools/steam_frame_bundle.py, which
# fills in the repository) and runs its installer, steam_frame_install.sh.
# (no "$", "\" or "%" in Exec: the desktop entry format would need them
# escaped)
Type=Application
Name=Install Halo VR
Comment=Installs Halo VR for the Steam Frame, or updates it
Icon=steam
Terminal=false
Exec=bash -c "if curl -fL --retry 3 -o /tmp/halo-vr-steam-frame.zip https://github.com/@REPOSITORY@/releases/latest/download/halo-vr-steam-frame.zip && rm -rf /tmp/halo-vr-install && python3 -m zipfile -e /tmp/halo-vr-steam-frame.zip /tmp/halo-vr-install; then exec bash /tmp/halo-vr-install/install.sh; else zenity --error --width=460 --title='Halo VR' --text='Halo VR could not be downloaded. Is the Steam Frame online?'; fi"
+85
View File
@@ -0,0 +1,85 @@
#!/bin/bash
# Halo VR on the Steam Frame: Lepton runs the APK in a container of its own,
# and installs it anew whenever the APK is newer than the installed one
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/HaloCE.
# (a container of its own, not the shortcut's id: with that, SteamVR's
# client in the container waited on xrCreateInstance)
export SteamAppId="${HALO_LEPTON_APP_ID:-4290000001}"
# (what Steam gives an Android title of its own, and a non-Steam game not:
# the app's data in the container links to the compat data path's
# internal/, a dangling link without it; and the shader paths: Lepton reads
# the dump path against the other)
export STEAM_COMPAT_DATA_PATH="$HOME/.local/share/Steam/steamapps/compatdata/$SteamAppId"
export STEAM_COMPAT_SHADER_PATH="$HOME/.local/share/Steam/steamapps/shadercache/$SteamAppId"
mkdir -p "$STEAM_COMPAT_DATA_PATH/internal" "$STEAM_COMPAT_SHADER_PATH"
unset STEAM_FOSSILIZE_DUMP_PATH LD_PRELOAD
LEPTON="$HOME/.local/share/Steam/steamapps/common/Lepton/lepton"
APK="$HOME/Documents/HaloCE/HaloVR.apk"
LOG="$HOME/Documents/HaloCE/launcher.log"
# (written by the game when the player quits it: no start again)
QUIT="$HOME/Documents/HaloCE/quit_requested.txt"
rm -f "$QUIT"
CONTEXT="steamlaunch-$SteamAppId"
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
log "start (pid $$, parent $PPID)"
# Steam's "Exit game" ends the game's processes as Steam knows them (this
# script, under Steam's reaper), not the container, which podman runs: a
# signal here is passed on to Lepton, and a watcher of its own takes the
# container down once this script is gone (killed)
stopping=0
child=0
stop()
{
log "signal $1"
stopping=1
[ "$child" -gt 0 ] && kill -TERM "$child" 2>/dev/null
}
trap 'stop TERM' TERM
trap 'stop INT' INT
trap 'stop HUP' HUP
script=$$
setsid bash -c '
while kill -0 '"$script"' 2>/dev/null; do sleep 1; done
echo "$(date "+%F %T") watcher: the game was ended, so its container" >> "'"$LOG"'"
"'"$LEPTON"'" kill "'"$CONTEXT"'" >/dev/null 2>&1
' </dev/null >/dev/null 2>&1 &
# the game's log while it runs (Lepton's logcat of its container), for
# looking into problems: logcat.txt, anew each start
LOGCAT="$HOME/Documents/HaloCE/logcat.txt"
: > "$LOGCAT"
setsid bash -c '
while kill -0 '"$script"' 2>/dev/null; do
"'"$LEPTON"'" logcat "'"$CONTEXT"'" 2>/dev/null | grep -v "^ERROR: \|^steamlaunch-\|^$" >> "'"$LOGCAT"'"
sleep 3
done
' </dev/null >/dev/null 2>&1 &
run()
{
"$LEPTON" waitforexitandrun -- "$APK" &
child=$!
# (wait returns early when a signal comes: until Lepton is gone)
while kill -0 "$child" 2>/dev/null; do
wait "$child"
done
child=0
}
# (a new APK is only installed on its first start, not started; and a start
# right after a game that was ended hard can fail in SteamVR, which then
# needs a moment: again, up to three times, while it ends within 30 s)
for attempt in 1 2 3; do
started=$SECONDS
run
if (( stopping || SECONDS - started >= 30 )) || [ -e "$QUIT" ]; then
break
fi
log "ended within $((SECONDS - started)) s: again"
sleep 5
done
rm -f "$QUIT"
log "end"
+220
View File
@@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""
Copies the maps folder out of an Xbox disc image (an "xiso", or a whole
disc's ".iso") on the Steam Frame's SteamOS side, for its installer
(steam_frame_install.sh). It reads the image as the app's own import does
(app/src/main/java/com/halo/decomp/XisoExtractor.java, which this follows;
the desktop games' is port/linux/src/xiso.c).
steam_frame_xiso.py <image> <destination>
writes <destination>/maps (through maps.partial, so an interrupted copy
never leaves maps/ui.map behind) and prints its progress for zenity
--progress: a percentage on a line of its own, and "# <text>" lines. On
failure it prints "error: <reason the player can act on>" to stderr and
exits 1.
The image's file system is XDVDFS, read as extract-xiso does
(https://github.com/XboxDev/extract-xiso, extract-xiso.c, whose format
handling this follows; its license is below and in
port/third_party/extract-xiso/LICENSE.TXT): 2048-byte sectors; a volume
descriptor at 0x10000 that starts and ends with "MICROSOFT*XBOX*MEDIA" and
gives the root directory's sector and size; and directories whose entries
form a binary tree (each entry: left and right subtree offsets in 4-byte
units, the start sector, the size, attributes, the name's length and the
name). Images made from a whole disc put the game partition further in, at
one of the offsets below.
Some parts of this code are copyright in@fishtank.com. This product
includes software developed by in <in@fishtank.com>.
Copyright (c) 2003 in <in@fishtank.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software
must display the following acknowledgement:
This product includes software developed by in <in@fishtank.com>.
4. Neither the name of "in" nor the email address "in@fishtank.com"
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED `AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
import os
import struct
import sys
SECTOR_SIZE = 2048
VOLUME_DESCRIPTOR_OFFSET = 0x10000
ENTRY_HEADER_SIZE = 14
ATTRIBUTE_DIRECTORY = 0x10
# directory tables are a few sectors; anything much larger is not one
MAXIMUM_DIRECTORY_SIZE = 4 << 20
MAXIMUM_FILES = 256
COPY_BUFFER_SIZE = 1 << 20
VOLUME_MAGIC = b"MICROSOFT*XBOX*MEDIA"
# where the game partition starts: a plain image, and whole-disc images
# (extract-xiso's GLOBAL, XGD3 and XGD1 offsets)
PARTITION_OFFSETS = (0, 0x0FD90000, 0x02080000, 0x18300000)
class ExtractError(Exception):
"""a reason the player can act on"""
def read_at(image, offset, size):
image.seek(offset)
data = image.read(size)
if len(data) != size:
raise ExtractError("Could not read the disc image (is it complete?).")
return data
def find_volume(image):
"""the game partition's offset, and the root directory's sector and size"""
for offset in PARTITION_OFFSETS:
try:
descriptor = read_at(image, offset + VOLUME_DESCRIPTOR_OFFSET, SECTOR_SIZE)
except ExtractError:
continue
if descriptor[:20] != VOLUME_MAGIC or descriptor[0x7EC:0x7EC + 20] != VOLUME_MAGIC:
continue
sector, size = struct.unpack_from("<II", descriptor, 20)
return offset, sector, size
raise ExtractError("This is not an Xbox disc image.")
def read_directory(image, partition, sector, size, what):
if size <= 0 or size > MAXIMUM_DIRECTORY_SIZE:
raise ExtractError(what)
return read_at(image, partition + sector * SECTOR_SIZE, size)
def walk(table, offset, depth, directories, out, visited):
"""collects the entries (name, sector, size) of the subtree at offset
(4-byte units), files or directories as asked"""
at = offset * 4
# a malformed table must not loop or run off its end
visited[0] += 1
if depth > 64 or visited[0] > 4096 or at + ENTRY_HEADER_SIZE > len(table):
return
left, right, sector, size, attributes, name_length = struct.unpack_from("<HHIIBB", table, at)
# 0xFFFF: padding, an empty directory
if left == 0xFFFF:
return
if left:
walk(table, left, depth + 1, directories, out, visited)
is_directory = bool(attributes & ATTRIBUTE_DIRECTORY)
if (at + ENTRY_HEADER_SIZE + name_length <= len(table) and name_length > 0
and is_directory == directories and len(out) < MAXIMUM_FILES):
name = table[at + ENTRY_HEADER_SIZE:at + ENTRY_HEADER_SIZE + name_length].decode("latin-1")
# (as extract-xiso refuses them: no name may leave the folder)
if name not in (".", "..") and "/" not in name and "\\" not in name:
out.append((name, sector, size))
if right:
walk(table, right, depth + 1, directories, out, visited)
def extract(image_path, destination):
with open(image_path, "rb") as image:
partition, root_sector, root_size = find_volume(image)
# the root's maps folder
table = read_directory(image, partition, root_sector, root_size,
"The disc image's file system is damaged.")
directories = []
walk(table, 0, 0, True, directories, [0])
maps = [entry for entry in directories if entry[0].lower() == "maps"]
if not maps:
raise ExtractError("The disc image has no maps folder: it is not a Halo disc.")
# its files
table = read_directory(image, partition, maps[-1][1], maps[-1][2],
"The disc image's maps folder is damaged.")
files = []
walk(table, 0, 0, False, files, [0])
if not any(name.lower() == "ui.map" for name, _, _ in files):
raise ExtractError("The disc image's maps folder has no ui.map: it is not a Halo disc.")
total = sum(size for _, _, size in files) or 1
partial = os.path.join(destination, "maps.partial")
finished = os.path.join(destination, "maps")
try:
os.makedirs(partial, exist_ok=True)
except OSError:
raise ExtractError("Could not create %s." % partial)
done = 0
shown = -1
for name, sector, size in files:
path = os.path.join(partial, name)
offset = partition + sector * SECTOR_SIZE
remaining = size
print("# Copying maps/%s" % name, flush=True)
try:
with open(path, "wb") as out:
while remaining > 0:
count = min(remaining, COPY_BUFFER_SIZE)
out.write(read_at(image, offset, count))
offset += count
remaining -= count
done += count
# (zenity closes at 100: that is left to the caller)
percent = min(99, done * 100 // total)
if percent != shown:
print(percent, flush=True)
shown = percent
except ExtractError:
raise ExtractError("Could not read %s from the disc image (is it complete?)." % name)
except OSError:
raise ExtractError("Could not write %s (is the storage full?)." % path)
# (the maps folder may be there, empty)
try:
os.makedirs(finished, exist_ok=True)
for name, _, _ in files:
os.replace(os.path.join(partial, name), os.path.join(finished, name))
os.rmdir(partial)
except OSError as error:
raise ExtractError("Could not move the maps into %s (%s)." % (finished, error.strerror))
def main():
if len(sys.argv) != 3:
print("usage: steam_frame_xiso.py <image> <destination>", file=sys.stderr)
return 2
try:
extract(sys.argv[1], sys.argv[2])
except ExtractError as error:
print("error: %s" % error, file=sys.stderr)
return 1
except OSError as error:
print("error: Could not open the disc image (%s)." % error.strerror, file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,62 @@
/*
FDM_INJECTION_STUB.C
An OpenXR API layer that does nothing, named as the Steam Frame's
XR_APILAYER_VALVE_fdm_injection names its library
(libVkLayer_VALVE_fdm_injection.so). The Frame lists that layer as implicit
for every OpenXR app (/vendor/etc/openxr/1/api_layers/implicit.d), but an
Android app (in Lepton) cannot load the system's library from its own
namespace, and the Khronos loader then fails xrCreateInstance altogether;
nor does the Android loader read the variable that would turn the layer
off. With this one in the app's libraries the loader finds a layer of that
name, which hands every call on to the next. (The real layer adds fragment
density maps to Vulkan apps; this game draws with OpenGL ES.) On a Quest no
manifest names it and it is never loaded.
*/
#include <openxr/openxr.h>
#include <openxr/openxr_loader_negotiation.h>
#include <stddef.h>
static PFN_xrGetInstanceProcAddr next_get_instance_proc_addr;
static XRAPI_ATTR XrResult XRAPI_CALL layer_get_instance_proc_addr(XrInstance instance, const char *name,
PFN_xrVoidFunction *function)
{
if (!next_get_instance_proc_addr)
return XR_ERROR_HANDLE_INVALID;
return next_get_instance_proc_addr(instance, name, function);
}
static XRAPI_ATTR XrResult XRAPI_CALL layer_create_api_layer_instance(const XrInstanceCreateInfo *info,
const XrApiLayerCreateInfo *layer_info, XrInstance *instance)
{
XrApiLayerCreateInfo next_layer_info;
if (!layer_info || !layer_info->nextInfo)
return XR_ERROR_INITIALIZATION_FAILED;
next_get_instance_proc_addr = layer_info->nextInfo->nextGetInstanceProcAddr;
next_layer_info = *layer_info;
next_layer_info.nextInfo = layer_info->nextInfo->next;
return layer_info->nextInfo->nextCreateApiLayerInstance(info, &next_layer_info, instance);
}
__attribute__((visibility("default")))
XRAPI_ATTR XrResult XRAPI_CALL xrNegotiateLoaderApiLayerInterface(const XrNegotiateLoaderInfo *loader_info,
const char *layer_name, XrNegotiateApiLayerRequest *request)
{
(void)layer_name;
if (!loader_info || !request || loader_info->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
request->structType != XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST ||
loader_info->minInterfaceVersion > XR_CURRENT_LOADER_API_LAYER_VERSION ||
loader_info->maxInterfaceVersion < XR_CURRENT_LOADER_API_LAYER_VERSION)
{
return XR_ERROR_INITIALIZATION_FAILED;
}
request->layerInterfaceVersion = XR_CURRENT_LOADER_API_LAYER_VERSION;
request->layerApiVersion = loader_info->maxApiVersion < XR_CURRENT_API_VERSION ? loader_info->maxApiVersion :
XR_CURRENT_API_VERSION;
request->getInstanceProcAddr = layer_get_instance_proc_addr;
request->createApiLayerInstance = layer_create_api_layer_instance;
return XR_SUCCESS;
}
+32
View File
@@ -0,0 +1,32 @@
Newtown
Copyright (c) Roger White, 1994. Released by its author into the public domain.
Newtown-Regular.ttf is the TrueType file of Newtown, as Roger White
distributed it (NEWTOW__.TTF, version 1.00, SHA-256
376d82c5b660c059c182d848d0a96da9dbe7277d7c8820f6e9a2c99b1a256c65), here
renamed. OpenCE (OpenCE-Regular.ttf) is made from it by tools/title_font.py.
The file's copyright notice ("All Rights Reserved - see .txt file for terms
for distribution and use") and the readme it was first distributed with
(personal use free, commercial use for a fee) date from 1994. Its author
later released all of his fonts into the public domain. His website,
rogersfonts.org.uk, said so from 2008 until it closed:
"Despite what is said in the readme files associated with the fonts all
fonts on this site are now public domain, whether used for personal use
or for commercial purposes. All that I ask is that the source is
acknowledged if you are using any for commercial purposes."
The statement is in the Internet Archive's copies of the site's home page,
the first of them from 24 July 2008:
https://web.archive.org/web/20080724224405/http://www.rogersfonts.org.uk/body_index.html
https://web.archive.org/web/20110724011202/http://www.rogersfonts.org.uk/body_index.html
and Newtown was one of the site's fonts (its samples are archived from 2004,
https://web.archive.org/web/20040909032418/http://www.rogersfonts.org.uk/assets/images/Newtown.gif).
The site says its fonts were made from the author's own hand-drawn artwork,
scanned and cleaned up.
As he asked, the source is acknowledged: OpenCE is based on Newtown by
Roger White.
Binary file not shown.
+95
View File
@@ -0,0 +1,95 @@
Copyright 2026 The OpenCE Project Authors, for its changes to Newtown's spacing.
OpenCE is based on Newtown, by Roger White (1994), which its author released
into the public domain (Newtown-LICENSE.txt).
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2021 The Overpass Project Authors (https://github.com/RedHatOfficial/Overpass)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+45
View File
@@ -0,0 +1,45 @@
# Fonts
The fonts the game's text is drawn with, at the display's resolution, in
place of the bitmap fonts of the maps (`port/linux/src/text_hires.c`).
`fonts.json` says which font draws each of the maps' font tags.
| File | Draws | From |
| --- | --- | --- |
| `Overpass-900.ttf` | `ui\large_ui`, `ui\interstate` | Overpass, weight 900 (Black) |
| `Overpass-750.ttf` | `ui\small_ui` | Overpass, weight 750 |
| `OpenCE-Regular.ttf` | the menus' titles, as pictures (`port/assets/titles`) | OpenCE: Newtown, respaced for this project (`tools/title_font.py`) |
| `Newtown-Regular.ttf` | (OpenCE's source) | Newtown by Roger White, unmodified |
The maps' fonts are Interstate (Tobias Frere-Jones, The Font Bureau), a
commercial typeface that cannot be shipped. Overpass (Red Hat, Delve
Withrington) follows the same Highway Gothic road-sign alphabet and is
under the SIL Open Font License 1.1 (`Overpass-OFL.txt`), without a
Reserved Font Name. Matched against the maps' glyphs, its weights 900 and 750 overlap
them as much as Interstate Bold does, at the same letter widths.
The menus' titles are pictures of text in the maps, set in another
commercial typeface. OpenCE (Open Community Edition) stands in for it:
Newtown, a typeface in the same style by Roger White (1994), which its author
released into the public domain (`Newtown-LICENSE.txt` quotes his statement
and where it is archived), respaced for this project. Only its spacing is
changed, its outlines are Newtown's: each letter's side spaces and the
kerning pairs are measured from where the letters sit in the maps' titles.
OpenCE is under the SIL Open Font License 1.1 (`OpenCE-OFL.txt`), without a
Reserved Font Name. It is not embedded: `tools/title_assets.py` draws the
titles with it, and the builds embed those pictures. To measure its spacing
again and rebuild it:
python3 tools/title_font.py --measure # paste the values into tools/title_font.py
python3 tools/title_font.py
python3 tools/title_assets.py --map assets/maps/ui.map
The Overpass files are static instances of Google Fonts' variable Overpass
(`ofl/overpass/Overpass[wght].ttf`, google/fonts commit
9710da1eacb3be272583c3224dcb70f9da6eadbb), made with fontTools:
python3 -m fontTools.varLib.instancer 'Overpass[wght].ttf' wght=900 -o Overpass-900.ttf
python3 -m fontTools.varLib.instancer 'Overpass[wght].ttf' wght=750 -o Overpass-750.ttf
The builds embed the Overpass files (`tools/embed_assets.py`), and the
release packages carry `Overpass-OFL.txt`, as the license asks.
+8
View File
@@ -0,0 +1,8 @@
{
"comment": "Which font draws each of the maps' font tags (port/linux/src/text_hires.c).",
"fonts": [
{"tag": "ui\\large_ui", "file": "Overpass-900.ttf"},
{"tag": "ui\\interstate", "file": "Overpass-900.ttf"},
{"tag": "ui\\small_ui", "file": "Overpass-750.ttf"}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Loaded 100 of 821 files, more files were not shown because too many files have changed in this diff. Show more