Split HALO_ANDROID into HALO_ARM64_GUEST, HALO_GLES and HALO_ANDROID
Conflict resolved: main's render-target eviction and mip composites are kept, with the PR's HALO_GLES guard on the ES blit fallback after them.
Their attachment's movement was truncated to whole units before the
test, so it took two units of movement in one update, not one, to move
the vertices along with it. Visual only.
(cherry picked from commit 99c2db2d4da3d6d66b6ec28d459c756aeab55a64)
(cherry picked from commit eb0667bcc81ed1d970422c7856162e8aadff6fd7)
HALO_ANDROID meant three things at once. The port's code now names each:
HALO_ARM64_GUEST for the ILP32 AArch64 guest's code paths (the inline
assembly's C, the bss pragmas, the CRT's), HALO_GLES for the OpenGL ES
renderer (its GL header and function list, shaders and capabilities), and
HALO_ANDROID for the app alone (its display, input, files and lifecycle).
The Android build defines all three, so nothing changes for it, nor for
Linux and Windows, which define none. A build for another platform can
then take one part without the others: the ES renderer, or the guest's
ABI, without the Android app.
port/android/README.md also no longer says that the widescreen changes are
in #ifdef HALO_ANDROID: none have been since 50eae19b.
The scenario's tag name was copied with strcpy into the game state
header's 256-byte map_name, a tag's name was formatted with sprintf into
the out of objects message's 512 bytes, and csprintf bounded its output
at MAXIMUM_STRING_SIZE even when it formats into temporary, which is 256
bytes and where most callers put tag names. All three are bounded by
their buffers' sizes now.
The powerup bounds of #117 move from source/models/models.c to port/linux/game/powerup_render_bounds.c, called from scenario_tags_load beside the other tags-loaded hooks, so the game's models.c no longer takes <xtl.h> and D3D buffer locks. It uses square_root and a relative margin in place of sqrtf and nextafterf, which strict gnu89 does not declare, and the objects.h scale change is marked as the port's. tools/test_powerup_bounds.py is dropped: it redeclares the engine's types by hand and CI does not run it.
On bloodgulch the active camouflage's render radius grows from 0.1 to 0.23 and the overshield's from 0.1 to 0.21; no other equipment changes.
The cache is forgotten before every game state load (beside cluster_partitions_port_forget) and at each new map, after the declarations, rather than at the top of four functions.
objects_port_set_activating_cluster takes the cluster a co-op host sends
in its presentation (network_coop.c), and refused only NONE and indices
past the BSP's clusters. Any other negative value (activating_cluster is
sent plus one, so a host sending -5 or 0x8001) was stored, and the client
then read the cluster PVS before the start of its array. Any negative
index now means none, as NONE already did.
A collision query (collision_get_features_in_sphere) reads the bounding
sphere of every collideable object in every cluster it touches, to pass
over the far ones. Every biped queries each tick, so a crowded cluster
(co-op's extra enemies, a carrier burst) costs the square of the crowd in
scattered object reads.
object_bounds_cache.c keeps the spheres packed by absolute index, written
by their only writer (object_compute_node_matrices). The loop runs the same
point_in_sphere on the copy, so the same objects are passed over and the
same features gathered, in order. game_state.c invalidates the copy when
the game state is replaced (revert, loaded core or game, new map).
Deterministic offline crowd (c20, legendary, extra enemies x8, 24 carriers
burst, seed pinned): game state identical tick for tick; collision queries
4.2 -> 3.0 us (-28%), object updates -15%.
A plasma bolt is drawn by its light volume, a widget, and both pools were
still the Xbox's: 64 widgets and 256 light volumes. Every assault rifle
holds a light volume for its flashlight beam from when it is made, held or
dropped. In network co-op the rifles left lying about are kept as garbage
in proportion to the players, and garbage that any player can see is never
collected, so a large game fills the widget pool and keeps it full; a client
that joins a game in progress makes all the rifles at once and takes every
slot. From then on widgets_new finds the pool full and skips the widget, so
every bolt flies and hits unseen.
- widgets 64 -> 2048 (HALO_PORT_MAXIMUM_WIDGETS)
- light volumes 256 -> 2048 (HALO_PORT_MAXIMUM_LIGHT_VOLUMES); with only the
widgets grown, the light volumes run out next
At 128 players a game can hold about 1,000: the rifles in hand, the ones on
the ground (garbage collection starts at 400 and keeps those in view) and the
bolts and needles in the air. The CPU part of the game state grows by 38,144
bytes, to 18,242,828 (2,466,548 free). As with any pool size, saved games of
earlier builds no longer load.
Garbage (bodies, dropped weapons) is kept while a player may see it: in any player's potentially visible clusters, near them or in a 45 degree cone of where they aim, however far. With many players spread over a level and many bodies (co-op's extra enemies), every body was seen by someone, nothing could go, and the whole collection (each garbage object's visibility, the object memory pool compacted) ran again every tick on the host, which also wrote 'garbage collection critical' to debug.txt each tick while short of memory. A player now sees garbage no further than 30 world units off, a collection of active garbage that left too much is tried again only after a second, nothing collected compacts nothing, and the critical line is logged as often as the warning. Running short of memory or objects still collects at once.
With many enemies (co-op's extra enemies) object memory runs low and the
garbage collector prints "garbage collection critical" and "removing
objects" at every pass, filling the screen for the host and every client.
They are the game's own chatter, so they show as game.console_log says, as
the sound and texture caches' already do; the log keeps every line.
The 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.
Breakable surfaces (glass) are broken by each machine's own projectiles
with its own random damage, so a pane could shatter on the host and stay
whole, still blocking, on a client. In co-op the host now sends each break
(and a few again every refresh, for late joiners), and a client breaks the
same surface with its shards.
Destructible scenery and devices (lights, panels) are the map's, at the
same index on every machine, but only damage to units was synced: a
client's shots never damaged them, and what the host broke didn't break
or play its effects on the clients. A client now reports its hits on them
like any other, the host checks and deals them, and every client deals the
host's damage to its own copy, which breaks, plays its effects and is
destroyed as the host's was.
Both are new message kinds that a client without them ignores, so the
network version is unchanged.
The 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.
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.
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>
Recover transparent geometry submit (+1,285 meaningful code, zero exact losses); reconcile COMMON ownership and Q10 85+EXT1 credit, actions/main data, and separately disclosed denominator corrections. Preserve canonical-only repairs and all held research. C09 is ordinary disclosed cleanup, not a claim of proven scaffolding intent. Clean rebuild, 1,663 tests, parks, audits, provider links and independent EXT1 check pass. Local reconciliation only.
Import only independently reviewed production changes. Preserve canonical physics and virtual-keyboard repairs; keep B3 and all unresolved holds out.
Record exactly four approved canary debits (6987 meaningful bytes), six Halo admissions and two revocations. Keep vendor ioinit/storage work separate. Full build, 8252-row sweep, 1318 tests, park/admission/fake audits and 621-object binding pass within approved debit bounds.
Do not publish donor research dumps or private assets. README and unrelated user research remain untouched. See frontier_publication_reconciliation_20260927.md for complete evidence limits and accounting.
Independently gate storage + K, OQ-4 P-a/P-b1, four static-local packets and comment-only disclosures. Preserve all 7641 exact owner rows, 71 valid parks and 1161 passing tests; code/data credit unchanged. G and game_engine admission remain held because current canonical loses populate_statistic_buffer. Existing collision static-local repair is not duplicated. No donor tree, held candidate, scorer or private asset imported.