-
Notifications
You must be signed in to change notification settings - Fork 129
Codegen Pipeline Overview
The rexglue codegen command converts an Xbox 360 XEX binary into portable C++23 source code via a two-phase pipeline:
- Analysis: function discovery and control flow reconstruction
- Recompilation: PPC instruction translation to C++
See rexglue CLI Configuration File for all TOML options that control this pipeline, and Generated Code Structure for the output format.
The pipeline is orchestrated by Analyze() in analyze.cpp, which runs seven sequential stages to build a complete FunctionGraph, followed by Recompile() which walks the sealed graph and emits C++.
XEX Binary
|
v
Phase 1: Analysis (analyze.cpp)
1. Register - import/export resolution, helpers, PDATA, config
2. Scan - segment binary into code/data regions
3. Discover - iterative block discovery (includes VTable scan)
4. GapFill - claim orphaned code regions
5. Discover - block discovery for gap-filled functions
6. Merge - resolve jumps, seal function boundaries
7. Validate - verify all call targets are resolvable
|
v
Phase 2: Recompilation (recompiler.cpp)
- Walk sealed FunctionGraph
- Dispatch each PPC instruction to a builder function
- Emit C++ source files
|
v
Generated C++ (see [[Generated Code Structure]])
registerEntryPoints() populates the FunctionGraph with initial entry points from multiple sources:
-
Import/export resolution: Resolves import thunks against the module's import table via the ExportResolver (see Runtime Architecture Overview). Each resolved import becomes a
FunctionNodewithIMPORTauthority. Unresolved imports log a warning but do not block analysis. -
Helper detection: Runs
SigScanner::helperSignatures()to pattern-match known compiler helpers:-
__savegprlr_14through__savegprlr_31 -
__restgprlr_14through__restgprlr_31 -
__savefpr_14through__savefpr_31 -
__restfpr_14through__restfpr_31 -
__savevmx_64through__savevmx_127 -
__restvmx_64through__restvmx_127
-
-
PDATA parsing: Reads the
.pdataexception table (IMAGE_CE_RUNTIME_FUNCTIONentries, 8 bytes each) to extract function start addresses and sizes. Functions with theExceptionFlagbit set trigger SEH scope table parsing and C++ EH FuncInfo parsing (magic0x19930522), which discovers handler functions and adds them to the graph. -
User config: Loads manual
[functions]entries from the TOML config. These getCONFIGauthority with exact boundaries that cannot be overridden. - Entry point: The XEX entry point is always registered as an immutable entry.
scanBinary() segments each executable section into code and data regions by scanning for null-delimited boundaries. The result is a set of CodeRegion objects that constrain where the Discover phase will look for instructions.
Regions of consecutive null dwords (0x00000000) or padding are classified as data. The data_region_threshold config option controls how many consecutive null dwords trigger a data region split.
discoverAllFunctions() is the core of function discovery. It runs iteratively to a fixed point, since discovering one function's blocks often reveals bl (call) targets that are new function entry points during the first few cycles.
For each registered-but-undiscovered function, discoverBlocks() performs:
-
Linear sweep from the entry point through the containing
CodeRegion -
Block construction: instructions are grouped into basic blocks, terminated by
blr(return),bctr(indirect branch), unconditionalb(branch), or null padding -
Branch following: conditional branches (
beq,bne,blt, etc.) create two paths (the branch target and the fall-through), both added to the work queue -
Call recording:
blinstructions are recorded as unresolved branches for resolution during Merge -
Jump table detection: when
bctris encountered, the scanner backward-scans for themtctr/load pattern and attempts to identify jump table entries (absolutelwzx, byte-offsetlbzx, half-wordlhzx, or computedrlwinm+lbzx). See switch_tables for manual overrides when auto-detection fails.
The discover phase also runs a VTable scan using VTableScanner. This traverses RTTI structures (MSVC-style RTTICompleteObjectLocator at vtable[-1]) to discover vtable slot addresses, which are registered as functions with VTABLE authority.
The iteration continues until no new function entry points are found.
gapFillCodeRegions() examines code regions that are not covered by any discovered function. Uncovered sub-regions are split at natural boundaries (null padding, PDATA markers, exception data patterns) and registered as GAP_FILL authority functions.
GAP_FILL functions are speculative: they represent code that was not reachable from any known entry point but occupies executable memory. During the subsequent Merge phase, GAP_FILL functions can be absorbed into adjacent higher-authority functions via vacancy-based expansion.
A second discovery pass runs block discovery for the newly registered gap-filled functions. After this pass, absorbed GAP_FILL functions (those whose address range was claimed by a neighboring function's expansion) are cleaned up.
mergeAndSeal() resolves all unresolved jumps and seals function boundaries:
-
Jump resolution: For each function with unresolved branches, checks whether the target is (a) a known function entry point, (b) an import thunk, or (c) an internal label within the same function. Resolved jumps become
CallEdgeentries. - Vacancy expansion: Functions can expand into adjacent vacant regions (regions with no null-dword boundary, no chunk claims, and no protected function overlap).
-
Sealing: Functions transition from
kDiscoveredtokSealedstate once all branches are resolved and blocks are finalized. TheFunctionAnalysisis computed (CSR requirements, special register usage).
validateGraph() iterates all sealed functions and verifies that every bl and b target resolves to either a function in the graph or a known import. Unresolved calls are reported as AnalysisErrors::Category::UnresolvedCall. See Unresolved Calls for how these appear in the output.
Note
Validation errors do not prevent code generation if --force is passed. The generated code will contain error comments at unresolved call sites.
The pipeline discovers functions through five complementary mechanisms:
SigScanner performs dword-based pattern matching against executable sections. Each Signature has a pattern (instruction words), a mask (which bits must match), and an entry offset. The built-in helperSignatures() set targets save/restore helper functions generated by the Xbox 360 MSVC compiler:
struct Signature {
std::string name;
std::vector<uint32_t> pattern; // Instruction words to match
std::vector<uint32_t> mask; // 0xFFFFFFFF = exact match
size_t entryOffset = 0; // Offset from pattern start to entry point
std::optional<size_t> size; // Known size, or nullopt
};VTableScanner traverses Xbox 360 MSVC-style RTTI structures in .rdata:
- Find all
RTTICompleteObjectLocatorstructures (signature = 0 for 32-bit) - Locate the vtable that references each COL (COL address appears at vtable[-1])
- Read vtable slots until a non-executable address is encountered
- Register each slot address as a function with
VTABLEauthority
The primary discovery mechanism. From each known entry point, discoverBlocks() performs a worklist-driven linear sweep:
- Instructions are decoded and grouped into basic blocks
- Branch targets create new work items
-
bltargets are recorded as potential new function entries - The outer loop re-discovers when new entries appear
During PDATA parsing, functions with the ExceptionFlag bit trigger exception table analysis:
-
SEH: Scope tables are parsed to extract
__finallyand__excepthandler function addresses -
C++ EH:
FuncInfostructures (identified by magic0x19930522) yield unwind map entries, try block handlers, and catch handler addresses
All discovered handler functions are added to the graph.
Manual [functions] entries in the TOML config provide exact function boundaries with CONFIG authority. These cannot be overridden or absorbed by any other mechanism.
Each PPC instruction maps to a builder function via a static dispatch table in instruction_dispatch.cpp. The table is an unordered_map<int, Builder> where Builder is a function pointer bool (*)(BuilderContext&).
The dispatch table covers approximately 350 entries across these categories:
| Category | Examples |
|---|---|
| Arithmetic |
add, addi, subf, mullw, divw
|
| Logical |
and, or, xor, rlwinm, slw, sraw
|
| Comparison |
cmpw, cmplwi, fcmpu
|
| Control Flow |
b, bl, blr, bctr, beq, bne, bdnz
|
| Floating Point |
fadd, fmul, fmadd, fcfid, fsel
|
| Memory (Load) |
lbz, lhz, lwz, ld, lfs, lfd, lvx
|
| Memory (Store) |
stb, sth, stw, std, stfs, stfd, stvx
|
| System |
nop, sync, eieio, mflr, mtctr, tw, dcbz
|
| Vector (FP) |
vaddfp, vmaddfp, vrefp, vrsqrtefp
|
| Vector (Int) |
vadduwm, vsububs, vmaxsw
|
| Vector (Other) |
vperm, vsel, vsldoi, vspltw, vpkuhum
|
| Conditional Register |
crand, cror, crxor
|
Unimplemented instructions emit a PPC_UNIMPLEMENTED trap macro rather than silently failing, producing a runtime error.
Builder functions access PPC registers through typed unions on PPCContext. The BuilderContext provides accessor functions that return either local variable names or context member expressions, depending on register locality configuration:
BuilderContext::r(); // returns either "rN" (local) or "ctx.rN" (context)
BuilderContext::f(); // returns either "fN" or "ctx.fN"
BuilderContext::v(); // returns either "vN" or "ctx.vN"
BuilderContext::cr(); // returns either "crN" or "ctx.crN"Register values are accessed via typed suffixes on the register union:
-
r3.u64/r3.s64/r3.u32/r3.s32: integer access -
f0.d: double-precision float -
v0: 128-bit vector (backed by SIMDE__m128for portability)
Several TOML config flags control whether registers are stored as local variables (faster, since the compiler can optimize them into machine registers) or accessed through the PPCContext struct:
| Config Flag | Registers Affected |
|---|---|
non_argument_registers_as_local_variables |
r0, r2, r11, r12, f0, v32-v63 |
non_volatile_registers_as_local_variables |
r14-r31, f14-f31, v14-v31, v64-v127 |
cr_registers_as_local_variables |
cr0-cr7 |
ctr_as_local_variable |
ctr |
xer_as_local_variable |
xer |
reserved_register_as_local_variable |
reserved (lwarx/stwcx) |
When non_volatile_registers_as_local_variables is enabled, calls to __savegprlr_N/__restgprlr_N helper functions are elided; the local variable tracking makes them unnecessary.
BuilderContext is the central object passed to every instruction builder. It provides:
- Register accessors (
r(),f(),v(),cr(),ctr(),xer()) - Output helpers (
print(),println()with fmt-style formatting) - MMIO detection (
mmio_check_d_form(),mmio_check_x_form()) - Load/store emission (
emit_load_d_form(),emit_store_x_form(), etc.) - Branch emission (
emit_conditional_branch(),emit_function_call()) - CSR flush mode tracking (
emit_set_flush_mode()) - Mid-ASM hook support (
emit_mid_asm_hook())
Memory-mapped I/O accesses require special treatment because they target hardware registers rather than normal memory. The codegen detects MMIO via two mechanisms:
-
eieio fence: if the instruction following a load/store is
eieio(Enforce In-Order Execution of I/O), the access is MMIO -
Register tracking: when
lisororisloads an upper half matching known MMIO ranges, the destination register is flagged as an MMIO base. Subsequent loads/stores using that register emitPPC_MM_LOAD_*/PPC_MM_STORE_*macros instead of the normal versions.
Known MMIO upper-half ranges:
-
0x7FC8-0x7FCF: GPU registers -
0x7FEA-0x7FEF: XMA/APU registers
The MMIO flag propagates through mr, or, and ori instructions that copy or combine register values.
Xbox 360 PPC uses different denormal handling for scalar FPU vs. vector VMX instructions. The codegen tracks a CSRState (Unknown, FPU, VMX) and emits ctx.fpscr.enableFlushMode() / ctx.fpscr.disableFlushMode() calls at state transitions. After labels (potential jump targets from unknown origins), the state resets to Unknown, causing a conditional mode check on the next FP/VMX instruction.
When the recompiler encounters bctr (branch to count register), it attempts to reconstruct switch/case dispatch:
- Backward scan from
bctrto findmtctr rX - Trace the register chain backward to find the table load instruction
- Identify the table type based on the load instruction:
-
lwzx: absolute 32-bit addresses -
lbzx: byte offsets added to a base address -
lhzx: 16-bit offsets added to a base address -
lbzx+rlwinm: byte values shifted and added to base (computed)
-
- Scan backward for bounds check (
cmpli/cmpiorclrlwimask) - Read table entries and validate targets against the containing code region
If detection succeeds at recompile time (late detection), the jump table and its labels are retroactively added so the generated switch dispatch is correct. If auto-detection fails, use switch_tables in the TOML config to specify tables manually.
Why static recompilation over JIT? Xenia uses a JIT backend that translates PPC instructions at runtime into host machine code. This works well for general-purpose emulation but means the host compiler's optimization passes (register allocation, instruction scheduling, vectorization) must be reimplemented in the JIT. ReXGlue takes the opposite approach: the codegen pipeline runs offline, producing standard C++ that Clang/MSVC can optimize with their full pass pipelines. The output is deterministic, debuggable with standard tools (debuggers, profilers, sanitizers), and can be distributed without shipping the original game code.
Why C++ as the target language? C++ provides direct access to host platform APIs (Win32, POSIX, graphics backends), mature SSE/AVX intrinsic support via SIMDE for PPC vector instruction emulation, and compatibility with existing game engine middleware. No custom intermediate representation is needed; the C++ compiler's own IR handles all optimization. The weak symbol / alias pattern (__attribute__((alias))) allows individual functions to be overridden at link time without modifying generated code.
The FunctionGraph is the central data structure shared between Analysis and Recompilation. Each FunctionNode follows a three-state lifecycle:
kRegistered ──discover()──> kDiscovered ──seal()──> kSealed
- kRegistered: Entry point known, no blocks yet
- kDiscovered: Blocks and instructions assigned, may have unresolved branches
- kSealed: All branches resolved, ready for code generation
Authority levels determine boundary mutability:
| Authority | Source | Mutable? |
|---|---|---|
GAP_FILL |
Unclaimed code regions | Can be absorbed |
DISCOVERED |
bl call targets |
Immutable entry point |
VTABLE |
RTTI vtable slots | Immutable entry point |
HELPER |
Signature-matched helpers | Fixed, overlaps allowed |
PDATA |
Exception table entries | Entry fixed, can extend |
CONFIG |
User rexglue CLI Configuration File | Exact boundaries, immutable |
IMPORT |
Import thunk resolution | External, immutable |
ReXGlue SDK
CLI Reference
Recompilation Pipeline
Runtime Architecture
Technical Reference