Repository navigation
Function Address Lists
The translator does not try to discover Halo's functions heuristically from the raw code section. It starts from two checked-in lists of function entry addresses in decompilation/c9acf0c46954/, follows the call graph from them, and uses them to decide where one function ends and the next begins. These lists are the only Halo-specific data in the public source tree: they are plain numbers, contain no code or game data, and are valid only for the one supported halo.exe. This page documents their naming, format, contents, and how the Static Translation Pipeline consumes them.
| File | Role |
|---|---|
| decompilation/c9acf0c46954/function-addresses.txt | Primary function entry index (6,495 addresses) |
| decompilation/c9acf0c46954/extra-function-entries.txt | Additional entries (1,627 addresses) |
| tools/generate_engine_reuse.py | Reads the lists (@file arguments, known-function set) |
| tools/setup_halo.py | Passes both lists; fingerprints them to decide when to regenerate |
| native/EngineHost/Makefile | Generation rule using both lists |
| docs/BUILDING.md | User instructions; notes the lists are executable-specific |
| SOURCE_MANIFEST.json | Pins both files by size and SHA-256 for the source release |
c9acf0c46954 is the first 12 hexadecimal digits of the SHA-256 of the supported executable:
c9acf0c469543283cfed6d7dc04ade976dbdfc7cb4532cf070386de169c19545 halo.exe (Halo PC 1.10 retail)
The full digest is the PE_SHA256 constant in tools/engine_reuse/upstream.py:22, which the generator checks before reading anything else. The same 12-digit prefix is stamped into the comment line of the generated engine_imports.c (Generated from halo.exe c9acf0c46954 ...) and appears in host comments that cite this build (for example directsound_engine_state.inc). Keying the directory by digest makes it explicit that the numbers mean nothing for any other build: BUILDING.md says "Address lists are specific to that executable; Halo Custom Edition, Anniversary, and other builds are not interchangeable."
Both files share one format:
- one address per line, exactly eight lower-case hexadecimal digits, no
0xprefix (00401000); - sorted in ascending order;
- no duplicates within a file, and no address appears in both files;
- no comments or blank lines.
The generator parses each file with (VISION/path).read_text().split() and int(x, 16) (line 85). That parser is more permissive than the files: any whitespace separates tokens, upper-case digits and a 0x prefix are accepted, and order does not matter. It does not accept comments; a # token raises ValueError. The path after @ is resolved relative to the repository root, not the shell's working directory.
function-addresses.txt |
extra-function-entries.txt |
Union | |
|---|---|---|---|
| Entries | 6,495 | 1,627 | 8,122 |
| Lowest | 00401000 |
004010e0 |
00401000 |
| Highest | 0063957e |
00639563 |
0063957e |
| 16-byte aligned | 4,571 | 1,296 | |
| Not 16-byte aligned | 1,924 | 331 |
(Counts measured on the tree at commit 9f915af.)
The generator calls this file the index and treats it as a fallback for a richer source (lines 81-82):
index = VISION/'decompilation/c9acf0c46954/function-addresses.txt'
known = {int(p.stem,16) for p in (VISION/'decompilation/c9acf0c46954/functions').glob('*.c')} \
or {int(line,16) for line in index.read_text().split()}If a directory decompilation/c9acf0c46954/functions/ of per-function C files exists, the known set is taken from their file names. Host comments cite such files by name, for example "validated against decompilation/c9acf0c46954/functions/" with references like 0048a1a0.c:12-18 in hsc_trace.inc. That directory is not part of the public tree, so a public checkout always uses function-addresses.txt, whose naming (lower-case, eight digits) matches those file stems. The repository does not document how the per-function decompilation was produced.
Additional entry points that are not in the index but that the translation needs as separate functions. The repository does not document how each was found. From the code, the categories that need explicit listing are:
- functions reached only through pointers stored in data (vtables, callback tables), which neither the call-graph walk nor the
--discoverimmediate-operand scan can see; - alternate entries into the middle of another function's body. The generator's comment names one: "Alternate entries into this body are listed as functions too (the script interpreter 424C20 has 424C4E)"; both
00424c20and00424c4eare in this file; - addresses the host hooks. Every address in
ENGINE_HOOK_ADDRESSESandENGINE_TRACE_HOOK_ADDRESSES(engine_hooks.h) is in one of the two lists;00511f30(a per-draw render call watched by theHALO_A10_TRACEdiagnostics) is in this file and the rest are in the index.
flowchart TD
F1["function-addresses.txt"] -->|"@file"| E["entries (BFS queue seed)"]
F2["extra-function-entries.txt"] -->|"@file"| E
F1 -->|"index fallback"| K["known (function boundaries)"]
E -->|"known.update(entries)"| K
K --> D["EngineDisassembler: branch or fallthrough into a<br/>known entry = tail call, not part of this body"]
K --> J["jump-table plausibility bound:<br/>next known entry after the decoded body"]
E --> W["closure walk: decode, lift, enqueue callees"]
W --> DISC["--discover: immediates added to known and queue"]
W --> T["engine_entries[] (sorted) in engine_bundle.c"]
DISC --> T
-
Seeds. Every listed address is queued for translation. With
--max-functions 10000and 8,122 seeds, the cap leaves room for functions found only as callees or through--discover. The dispatch-table size quoted in engine_hooks.h for one build ("a binary search over 8336 entries") is larger than the seed count for that reason; the exact number for a given run isENGINE_FN_COUNTinengine_bundle.candfunctionsGeneratedingeneration.json. -
Boundaries. The
knownset tellsEngineDisassemblerwhere a body stops. A jump, or a fallthrough after a conditional jump or a plain instruction, into another known entry is recorded as a tail call and lowered toENGINE_DIRECT(...); return;orengine_dispatch(...); return;instead of being decoded as part of the current function. This is how CRT routines that fall through into a shared epilogue are handled. -
Jump-table bounds. While harvesting
jmp [idx*4 + table]targets, the generator accepts pointers up to the next known entry after the decoded body; pointers to other known entries are skipped (Static Translation Pipeline). -
Hand-checked switches.
005590a0,005061c0and005067b0, all in the index, get their jump-table targets from asserted byte patterns rather than from the generic harvester.
Because the lists define function boundaries, they change the generated code, not just its coverage:
- Adding an address that lies inside an existing function's body makes it a separate function. Branches from the old body to it become tail calls, and it gets its own
sub_XXXXXXXXand dispatch-table entry. This is required when the host needs to hook that address, becauseengine_dispatchandENGINE_DIRECToperate on function entries. - Removing an address can merge it back into a neighbor, or drop it from translation if nothing else reaches it, in which case a dispatch to it lands in the preceding function's
L_ENTRYswitch and fails withdispatch to non-leader address.
test_decode.py demonstrates both behaviors on the same bytes: with 0x4D05CA in the known set, function 0x4D0580 ends with a tail call to it; without it (as in the shipped lists, which do not contain 004d05ca), the epilogue is an internal block.
- The setup wizard includes the bytes of every
decompilation/**/*.txtin its generation fingerprint (setup_halo.py:363-370), so editing a list triggers regeneration on the next setup run. The macOS Makefile does not track the lists; deletenative/build/engine-reuse/whole-exe/engine_bundle.c(or rerun the generator) after editing them. -
SOURCE_MANIFEST.jsonrecords each list's size and SHA-256 (553496156c...for the index,fa60af4788...for the extras) as part of the source release inventory; RELEASING.md describes how that manifest is used. A change to a list must be reflected there for a release. - The lists are inputs to every generated file, so
generation.jsonrecordssourceExecutableSHA256and per-functiongeneratedSHA256values that change when boundaries change.
When an entry needs to change (for example to hook a new function):
- Confirm the address is an instruction boundary in the supported executable and that it is genuinely entered from elsewhere (call, pointer, or jump from another function).
- Add it to
extra-function-entries.txt, keeping eight lower-case digits and ascending order. - Regenerate with the canonical command and check
generation.jsonfor a newundecodableorunsupportedrecord at that address, and for changedtailCallsin its neighbor. - If the address is hooked, list it in
ENGINE_HOOK_ADDRESSESin engine_hooks.h and runnative/EngineHost/tests/test_dispatch_interest.py(see EngineReuse Runtime). - Update
SOURCE_MANIFEST.jsonfor the release.
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