Repository navigation
Engine Overrides and Hooks
Most of the original engine runs exactly as translated. A small, explicit set of guest function entries is intercepted by the host instead: some are replaced outright by native code that produces the same result faster or more safely, some are observed for diagnostics without changing anything, and a few have host behaviour attached at an engine boundary (the campaign unlock, the panorama renderer). All of these go through one function, engine_dispatch_override in overrides.c, which the generated dispatcher consults before running a translated function. This page explains that mechanism, why every hooked address must also be listed in engine_hooks.h, and each override and observer: the CRT replacements, native sort and visibility, native geometry leaves and gathers (summarised; details on Geometry Fast Paths), the campaign unlock, the HSC script trace, the a10 diagnostics, game-time telemetry and the menu pointer.
| File | Role |
|---|---|
overrides.c |
engine_dispatch_override, the override table, CRT/FP replacements, native int sort, visible-surface switch, native gather switch and diagnostics, dispatch-interest filter. |
native/EngineReuse/engine_hooks.h |
ENGINE_HOOK_ADDRESSES, ENGINE_TRACE_HOOK_ADDRESSES, engine_hooked(), ENGINE_DIRECT. |
campaign_unlock.h |
halo_unlock_campaign_menu(). |
hsc_trace.inc |
HSC script-thread trace at the scheduler. |
a10_control.inc, a10_gamepad.inc
|
a10 diagnostics and the optional default gamepad preset. |
audio_ownership_trace.inc, model_capture_hooks.inc
|
Default-off observers for sound ownership and model capture. |
panorama_hooks.inc, frame_pacing_hooks.inc
|
Panorama renderer and pacing hooks (see Panorama System, Frame Pacing). |
native_leaves.h, native_gather.h, native_gather_tree.h, visible_surfaces.h, gather_diagnostics.h
|
Native compute replacements (see Geometry Fast Paths). |
core_telemetry.h |
Read-only game-time tick counter. |
pointer.c, pointer.h, pointer_step.inc
|
Menu cursor servo driven by gaze/pinch. |
Tests: test_dispatch_interest.py, test_engine_direct_calls.c, test_game_time_telemetry.c, test_pointer_step.c, test_pointer_input.c
|
See Tests. |
flowchart TB
A["translated call site to fixed entry X"] --> B{"engine_hooked(X)? (compile-time constant switch)"}
B -- no --> C["ENGINE_DIRECT: pc = X, call sub_X(cpu)"]
B -- yes --> D["engine_dispatch(cpu, X)"]
E["indirect call / callback / jump table"] --> D
D --> F{"X >= 0xFE000000"}
F -- yes --> G["engine_dispatch_external (Win32 shim)"]
F -- no --> H["engine_dispatch_override(cpu, X)"]
H --> I{"bit for X set in dispatch_interest?"}
I -- no --> J["return 0"]
I -- yes --> K["ordered checks (table below)"]
K -- "handled: return 1" --> L["caller continues at cpu->pc"]
K -- "not handled: return 0" --> J
J --> M["engine_record(X), binary search, run translated sub_X"]
Originally every translated call went through engine_dispatch: the override filter, the crash call record, a binary search over 8336 entries and an indirect branch. The comment in engine_hooks.h records that on b30 that was about a tenth of the engine thread, and that 25,835 of the 32,373 translated call sites name a fixed function entry. The generator therefore emits those call sites as ENGINE_DIRECT(cpu, address, sub_X), which calls the C function directly unless engine_hooked(address) is true (engine_hooks.h:65-69). Because the test is on a constant, it folds away at each call site.
Consequences:
-
Adding a hook requires adding its address to
ENGINE_HOOK_ADDRESSESand recompiling the translated chunks. Otherwise direct call sites bypassengine_dispatch_overrideand the hook silently never runs for them (it would still run for indirect calls). -
ENGINE_TRACE_HOOK_ADDRESSESlists addresses only theHALO_A10_TRACEdiagnostics compare against (004C8800,00477EA0,005527F0,005528F0,00511F30). In release builds these go direct, so the trace sees only their indirect calls; building the chunks withENGINE_TRACE_HOOKS=1routes them through dispatch too (engine_hooks.h:39-48). - Direct calls skip
engine_record, so the crash dump's call ring contains only indirect calls.
The current hook list (engine_hooks.h:22-37):
| Addresses | Purpose |
|---|---|
004C6E80 |
Main-loop tick: campaign unlock, a10 diagnostics and gamepad preset |
00442550, 00544090, 00544120, 0048A1A0
|
Audio ownership trace; 0048A1A0 is also the HSC scheduler |
004D6FC0, 00533850, 00533730
|
Model capture |
00492430, 0052B050, 00518F40, 004924B0, 005154A0, 005537C0, 0050CC40, 00449780, 00494730, 004984C0, 0050BEA0, 0050BFB0, 0050BA80, 0050F740, 0050BDC0
|
Panorama renderer hooks |
00626BA4, 00631930, 00631260
|
CRT _mbstowcs, _mbtowc, _wctomb
|
00634D8E, 00634D50, 00634F61, 00634DCB
|
MSVC FP intrinsic dispatchers |
00449590 |
int32 sort |
00553920 |
Visible-triangle marking |
004CC0D0, 00554260, 005541B0, 00553380, 00552C20
|
Native leaves |
00552DE0 |
BSP material walk |
005540C0, 00553C40, 00553F10
|
Native light/shadow gather loops |
engine_dispatch_override is called for every dispatched address, so it starts with a conservative membership test (overrides.c:214-239): a 65,536-bit table (dispatch_interest[1024] of uint64_t) indexed by the low 16 bits of the address. A constructor sets the bits for every address in both engine_hooks.h lists and in the override table before any guest thread starts; the table is read-only afterwards. A clear bit returns 0 immediately; a set bit (a real hook or a collision) proceeds to the exact checks. Collisions only cost the slow path.
engine_dispatch_override runs these in order. "Consumes" means it returns 1 and the translated function does not run (or, for wrappers, has already been run by the hook).
| # | Address(es) | Handler | Consumes? |
|---|---|---|---|
| 1 | five leaf entries |
host_native_leaf_dispatch when HALO_NATIVE_LEAVES is not 0
|
if the native version accepted the inputs |
| 2 |
005540C0, 00553C40, 00553F10
|
ov_native_gather when HALO_NATIVE_GATHER=1
|
if the native loop accepted |
| 3 | 004C6E80 |
campaign unlock when HALO_UNLOCK_CAMPAIGN=1
|
no |
| 4 | sound functions |
host_audio_ownership_dispatch (HALO_AUDIO_OWNERSHIP_TRACE=1) |
yes when it wraps the call |
| 5 | model entries |
host_model_capture_dispatch (HALO_MODEL_CAPTURE) |
per its own logic |
| 6 | panorama entries | host_panorama_dispatch |
per Panorama System |
| 7 | 004C6E80 |
host_a10_gamepad_boundary |
no |
| 8 |
004C6E80 and trace addresses |
host_a10_boundary |
no |
| 9 | 0048A1A0 |
host_hsc_trace_boundary |
no |
| 10 | 00449590 |
ov_int_sort |
if the callback is 00552C00
|
| 11 | 00553920 |
ov_visible_surfaces |
unless HALO_NATIVE_VISIBILITY=0
|
| 12 | 00552DE0 |
leaf_bsp_walk_dead when native leaves are on |
if the callback is a bare ret
|
| 13 | override table entries | CRT/FP replacements | yes |
Halo's statically linked MSVC CRT converts between multibyte and wide strings through locale tables. The overrides implement the C-locale behaviour directly, one wide character per byte (overrides.c:35-75). All are cdecl.
| Entry | Function | Behaviour |
|---|---|---|
00626BA4 |
_mbstowcs(dst, src, count) |
Widen bytes until NUL or count; NULL dst returns the length. |
| (absent) | _wcstombs |
Implemented but the table address is 0xFFFFFFFF ("absent in this build"), so it never matches. |
00631930 |
_mbtowc(wc, s, n) |
One byte; returns 1, or 0 for NUL / NULL / n == 0. |
00631260 |
_wctomb(s, wc) |
One byte (? above 255); returns 1. |
__cintrindisp1 (00634D8E), __cintrindisp2 (00634D50), __ctrandisp1 (00634F61) and __ctrandisp2 (00634DCB) are the CRT's dispatchers for x87 transcendental functions. On entry edx points to a descriptor whose first byte is the name length followed by the name. The overrides read the operand(s) from the emulated x87 stack, compute with the host C library, write the result to ST(0) (popping one operand for the two-argument forms) and return to the caller (overrides.c:13-33).
| Arity | Names handled |
|---|---|
| 1 |
exp, log, log10, sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, sqrt
|
| 2 |
pow, fmod, atan2, hypot
|
An unknown name is logged ([fp] unknown 1-arg intrinsic) and the input is returned unchanged. The CRT's own error handling (__trandisp2 and errno/matherr paths) is skipped; results are those of the host libm in double precision.
00449590 sorts an int32 array in place (eax = count, ecx = base, [esp+4] = a "greater than" callback): a quicksort with an insertion sort for eight or fewer elements, every comparison a translated callback call (overrides.c:77-97). The render pass calls it with callback 00552C00 (signed a > b), so the result is ascending signed order, and because equal elements are equal values any correct sort leaves the same bytes. The comment notes that under 00552CF0 this sort was about a tenth of each b30 bearing pass on the Mac. The override handles only that callback and counts up to 0x10000, calls qsort, and returns (eax and flags are dead for every caller). The other caller (00413D0F, callback 004127B0) keeps the translated routine.
00553920 marks the triangles of every visible subcluster by running the frustum test 0050D5B0 for each subcluster of each portal-visible cluster. visible_surfaces.h reimplements it natively (the header documents the exact float operation order it preserves). Selected by HALO_NATIVE_VISIBILITY (overrides.c:99-164):
| Value | Mode |
|---|---|
| unset / anything else | native (default) |
0 |
translated routine |
verify |
run the translated routine, then restore the starting state (CPU, the 0x94-byte stack frames, the triangle bitset and its count) and run the native one; compare registers, flags, x87 state, bitset and stack; log the first 16 mismatches and a progress line at calls 1–4 and every 600th; always keep the translated result |
These are bit-exact native versions of hot compute-only functions; their algorithms and exactness argument are on Geometry Fast Paths.
| Entries | Switch (default) | Notes |
|---|---|---|
004CC0D0 (4x3 matrix multiply), 00554260 (bounds planes), 005541B0 (bounds overlap), 00553380 (BSP node bounds), 00552C20 (surface list) |
HALO_NATIVE_LEAVES (on; 0 disables) |
Each declines (returns 0, translated code runs) if inputs overlap outputs, the guest is not rounding to nearest, the x87 stack lacks free registers, or the direction flag is set for string copies (native_leaves.h:1-30). |
00552DE0 (BSP material walk) |
HALO_NATIVE_LEAVES |
Native only when its callback is a bare ret; handled after the a10 trace so the trace still sees every walk. |
005540C0 (leaf gather), 00553C40 (cluster gather), 00553F10 (recursive collision-BSP gather) |
HALO_NATIVE_GATHER (off; only 1 enables; the visionOS app sets 1) |
Counted in HostGatherDiagnostics (calls, native_calls, fallback_calls, enabled), exposed by host_gather_get_diagnostics (overrides.c:178-203). |
When HALO_UNLOCK_CAMPAIGN=1, every dispatch of 004C6E80 (called at the beginning of each main-loop iteration, outside Present) applies halo_unlock_campaign_menu to the player profile at guest address 0x00712DD8 (overrides.c:242-248):
/* Retail 1.10 profile_unlock_solo_levels, original instructions 00481400..1C.
* Only the menu's ten difficulty masks and unlock flag change. The original
* callback's disk-save and HSC return are deliberately not invoked here. */
static inline void halo_unlock_campaign_menu(uint8_t *profile) {
profile[0x11c] |= 4;
for (unsigned i = 0; i < 10; ++i) profile[0x11e + i] |= 15;
}Because it runs each tick, it applies "after profile selection/replacement on the next main-loop tick". The comment in overrides.c calls it an "in-memory unlock only: existing checkpoints and save files are untouched"; the host itself writes no file. Whether the game later saves the modified profile through its own code is not determined by this function. The environment variable is read with getenv on each call (not cached). The visionOS app sets HALO_UNLOCK_CAMPAIGN=1 by default (Runtime Settings); test_dispatch_interest.py requires 004C6E80 to be in the hook list.
hsc_trace.inc is a read-only, default-off trace of Halo script (HSC) threads, hooked at the entry of 0048A1A0, the per-tick script scheduler called from the main loop (never inside Present). Guarantees stated in its header: it never writes guest memory, bounds-checks every guest pointer with hsc_ok() (above the 64 KiB guard, no wrap, at most 16 MiB), and emits at most a bounded number of events per snapshot.
| Variable | Default | Effect |
|---|---|---|
HALO_HSC_TRACE |
unset (completely inert) | Positive tick interval between snapshots; 0, empty or non-numeric → 120; 1 samples every tick. |
HALO_HSC_TRACE_MAX |
128 | Maximum thread events per snapshot, capped at 512. |
HALO_HSC_TRACE_ALL |
unset | Also emit threads with wake == -1 (otherwise skipped but still counted in total). |
The app sets HALO_HSC_TRACE=120 and HALO_HSC_TRACE_MAX=128 by default. Sampling uses 64-bit tick deltas and resamples immediately when the tick goes backwards (map reload).
Log contract (schema 1), one JSON object per line after [hsc-trace] :
{"schema":1,"event":"config","interval":I,"max":M,"all":0|1}
{"schema":1,"event":"thread","tick":T,"thread":i,"script":s,"kind":k,"wake":w,"state":"runnable|sleeping|dormant|indefinite","node":n|null,"opcode":o|null,"name":"..."|null}
{"schema":1,"event":"scheduler","tick":T,"enabled":0|1,"total":N,"emitted":M,"truncated":0|1}
state is runnable for 0 <= wake <= tick, sleeping for wake > tick, dormant for wake == -2, indefinite for other negative values ("Negative wake values alone do not prove completion or a deadlock"). The guest layouts it reads, each validated against named decompiled functions in the header (hsc_trace.inc:30-46):
| Global | Layout used |
|---|---|
0087A470 script-thread datum array |
+0x2E last index (s16), +0x34 element base; element stride 0x218: +0 salt (0 = free), +4 script index, +8 wake tick, +0x10 current frame pointer, +0x18 stack start |
| frame |
+4 node datum (must lie inside the thread's own element) |
0087A474 node datum array |
stride 0x14; +2 function index reported as opcode
|
006F1D6C game time |
+0x0C current tick |
006B15E8 |
scheduler enabled byte |
00746F8C scenario tag |
+0x49C script count, +0x4A0 script table (stride 0x5C, name at +0, kind at +0x20); a name is emitted only if its 32 bytes are printable ASCII and NUL-terminated |
The header notes that it fixes two errors of an older scanner in d3d9.c (HALO_SCAN_THREADS): wake == -2 was labelled runnable, and the thread type byte was mislabelled as the script kind.
Both run at 004C6E80 and never consume the call.
-
HALO_A10_TRACE(any value) (a10_control.inc): logs[a10],[a10-cinematic]and[a10-view]lines on the first three main-loop iterations and every 60th (tick, game mode, profile count, player/unit/controlled handles, cinematic and input-block flags, camera and unit positions), plus[a10-call],[a10-bsp],[a10-material]and[a10-effects]lines at the trace-only addresses. Campaign mode is game mode 0. -
HALO_A10_GAMEPAD=1(a10_gamepad.inc): on thelevels\a10\a10campaign with a connected controller and a player, assigns DirectInput device 0 to slot 0 if both are unassigned and fills only empty (0x7FFF) axis and button bindings with a session preset (left stick move, right stick look, standard button actions), plus accept/back buttons. "No poses, player commands, script gates or profile files are changed here." WithHALO_A10_TRACEalso set it logs[a10-pad]state. See Input and Controllers.
| Hook | Switch | What it does |
|---|---|---|
host_audio_ownership_dispatch (audio_ownership_trace.inc) |
HALO_AUDIO_OWNERSHIP_TRACE=1 |
Logs [audio-ownership] snapshots of the sound tag table around 00442550, 00544090, 00544120 (each wrapped call executes exactly once with its own ABI) and table changes at 0048A1A0; at most 512 reports. See Audio System. |
host_model_capture_dispatch (model_capture_hooks.inc) |
HALO_MODEL_CAPTURE |
Records the normal-world and deferred model draw boundaries (004D6FC0, 00533850, 00533730) for the one-frame model vertex capture; "No original function is skipped". See Diagnostics and Telemetry. |
host_panorama_dispatch (panorama_hooks.inc) |
HALO_PANORAMA and related |
Renders several directions at one simulation time by driving the original renderer. See Panorama System; pacing is in Frame Pacing. |
The core telemetry measures how many game ticks each presented frame ran, without any hook. core_telemetry.h documents, verified against the generated sub_00470BF0.c (the tick driver): the pointer at 006F1D6C names game_time_globals; after each returned call to 0045B780 the driver increments the dword at +0x0C; it stores the last call's tick count (a word) at +0x10, which the inactive-game branch zeroes. "Reading this counter introduces no intercepted address or guest write."
-
halo_game_time_read(flat, sample)(core_telemetry.h:83-97) reads the counter and last-call word if the globals pointer is in[0x10000, 0xFFFFFF00](zero until the game allocates them; the first 64 KiB is the null guard). -
halo_frame_ticks(before, now, &resync)returns the counter delta, or 0 withresyncset when either sample is invalid, the globals moved, the counter went backwards, or it jumped by more thanHALO_TICK_JUMP(120) ticks.+0x10is only a comparison ("Never guess using last_call"). - The engine thread samples once per Present in
engine_thread_sample(EngineVisionRuntime.m); frames are bucketed by 0, 1, 2, 3 and 4+ ticks. See Diagnostics and Telemetry.
tests/test_game_time_telemetry.c compiles the real generated sub_00470BF0.c (it prints SKIP if the translation is not on the include path), stubs its callees, and for 0–120 requested ticks in all three game modes plus inactive and forced-single-tick cases asserts that the counter delta equals the number of game_tick calls, that +0x10 matches, and that reading the counters changes neither guest memory nor any CPU/x87 state.
On the headset there is no mouse, but Halo's front-end and pause menus follow a DirectInput mouse cursor. pointer.c makes the engine's own cursor converge on where the player is looking and turns a pinch into a click, without patching the menu code (pointer.h:4-15).
| Constant | Value | Source |
|---|---|---|
| Cursor X / Y globals |
0x00718F84, 0x00718F88 (int32) |
located by the desktop probe moving a mouse and watching guest memory |
| UI extent | 640 x 480 | the clamps the probe read |
| Tap queue capacity | 16 | |
| Arrival tolerance | Manhattan distance ≤ 6 px |
Producer (app threads): host_pointer_set(u, v, on_panel, action) (pointer.c:38-57), reached through enginevision_pointer from EngineImmersive.swift. u, v are normalized (0,0 top-left) and mapped to UI pixels; NaN/infinite or off-panel targets are invalid. Actions:
| Action | Value | Effect |
|---|---|---|
HOST_POINTER_HOVER |
0 | Replace the hover target (the pinch's gaze preview, or the head ray when the "Head pointer" setting is on). |
HOST_POINTER_TAP |
2 | Also queue a click at this target (once per successful pinch); a queued tap keeps its own target until the cursor arrives, so later hover samples cannot redirect it. |
HOST_POINTER_CANCEL |
-1 | Drop pending taps and hover; release on the next servo. Sent on session start/end. |
Consumer (engine thread): host_pointer_servo() runs once per Present, before DirectInput is read (d3d9.c:543, pointer.c:59-92):
- If no menu is active (
host_dinput_menu_active()), clear all pending input and release a held button, then return. "Never carry pending menu input into gameplay or the next menu"; the controller keeps owning aim and fire in play. - Read the engine's cursor from guest memory (published for the report via
host_pointer_stats). - If the button was pressed last frame, release it this frame and do nothing else: each click is exactly one engine frame down and one up.
- Target the oldest queued tap, else the hover target. If a tap's target is reached, press button 0 (
host_mouse_buttons[0] = 0x80) and dequeue; otherwise add a servo step tohost_mouse_dx/dy.
Servo step (pointer_step.inc): the engine accelerates mouse input; the desktop probe measured that d counts move the cursor d pixels up to about 5, and d + 0.047 d^2 beyond (8 → 11, 16 → 28, 32 → 83, 64 → 268, 96 → 520). A proportional step in counts would therefore overshoot and ring. host_pointer_step plans 0.7 of the remaining distance in pixels, caps it at 300 px per axis, and converts pixels to counts through the inverse curve (-1 + sqrt(1 + 4kp)) / 2k with k = 0.047 (identity up to 5 px). "A glance across the panel lands in three frames, a small correction at once."
host_pointer_enabled() reports the live "Head pointer" setting (halo_settings_gaze_pointer, seeded from HALO_GAZE_POINTER, default off); system taps work regardless. See Runtime Settings and Input and Controllers.
| Test | Asserts |
|---|---|
test_dispatch_interest.py |
Parses engine_hooks.h, the hook fragments and overrides.c: every address any hook compares against (plus 004C6E80) and every override-table entry is in ENGINE_HOOK_ADDRESSES; only a10_control.inc addresses may be trace-only; behavioural hooks are never in the trace list; overrides.c feeds both lists and the table into dispatch_interest. |
test_engine_direct_calls.c |
ENGINE_DIRECT sets pc and calls the function for unhooked entries; hooked entries (0050BEA0, 00449590, 00553920, 00552DE0) go through engine_dispatch; 00511F30 goes direct unless ENGINE_TRACE_HOOKS; every listed address is hooked and its neighbour is not. |
test_game_time_telemetry.c |
Described above. |
test_pointer_step.c |
Against the measured curve: a glance across the panel settles within 6 frames with ≤ 6 px overshoot in both directions; a small correction within 3; still converges within 10 frames on a steeper curve (k = 0.060, overshoot ≤ 40) and a gentler one (k = 0.035); no motion on target; a full jump is at most 75 counts. |
test_pointer_input.c |
Includes pointer.c with a simulated engine cursor: a fast tap survives later hover and clicks at its own target; three rapid taps produce three separate down/up pairs at their targets; cancel drops pending taps and releases a delivered click; leaving the menu clears input and does not overwrite the controller's button state; invalid targets never click; the queue is bounded at 16. |
Other native replacements have their own differential tests (test_native_leaves.c, test_visible_surfaces.c, test_native_gather.c); see Geometry Fast Paths.
Documents master-chef at commit 9f915af (v1.0.3). Unofficial project, not affiliated with Microsoft, Bungie, Gearbox or Apple. Original code is MIT licensed; game content is not included.
Overview
- Architecture Overview
- Repository Layout
- Glossary
- Environment Variables
- Contributing Guide
- Open Questions
Translation
- Static Translation Pipeline
- XWA Decoder and Lifter
- Function Address Lists
- EngineReuse Runtime
- x87 Floating Point
Host runtime
- EngineHost Overview
- Win32 Compatibility Layer
- Threading and Synchronization
- Guest Memory and Heap
- Engine Overrides and Hooks
- Runtime Settings
Graphics
- Direct3D9 Bridge
- Metal Renderer
- Shader Translation
- Textures and Texture Packs
- Geometry Fast Paths
- Radial Fog
Panorama and presentation
- Panorama System
- Panorama Budget and LOD
- Frame Pacing
- visionOS App
- Immersive Presenter
- Layer Alignment
Audio and input
Tooling and process