# # #### ### # # ##### ### ### # ###
# # # # # # # # # # # # # # #
# # #### # # # # # # # # # # ##
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # #### ### # # # ### ### ##### ###
Static Recompilation Toolkit for Original Xbox Games
Turn any Xbox game binary into a native Windows executable. No emulation. No interpreter. Just raw, recompiled C.
→ INSTRUCTIONS.md — start here. The whole journey in one page: picking a target from your ISOs, running the pipeline, building, the boot loop, and what "playable" actually means.
Join the sp00nznet recomp Discord — the community hub for sp00nznet's recomp projects, where ps3recomp development happens in the open. Good place to ask questions, show a port you are working on, or find out what people are stuck on before you duplicate the effort.
Title-agnostic. The runtime, kernel layer, D3D8 abstraction, NV2A translator, and the Python pipeline (parser → disasm → func_id → abi_analysis → recomp) all derive per-title layout and behavior from the XBE itself. Burnout 3: Takedown was the reference title the toolkit was built against, so many docs use its metrics as examples — see docs/technical/candidate-games.md for ports in progress.
Current version: v0.9.0 — "Quietly Wrong" (September 2026). See the Changelog for what landed and when.
This is a complete toolkit for statically recompiling original Xbox (2001-2005) games from their retail XBE executables into native Windows programs.
Static recompilation takes the raw x86 machine code from an Xbox binary and translates every function — every mov, every jmp, every call — into equivalent C source code. That C code compiles with MSVC into a native x86-64 .exe that runs on modern Windows. The game's original logic executes directly on your CPU, not through an interpreter or JIT compiler.
This is the first public static recompilation toolkit for the original Xbox. Microsoft got here first: their internal Ficl/Fission recompiler shipped Xbox back-compat on the 360. We have since studied it — see Microsoft's Own Recompiler.
The technique has been proven on other platforms — N64Recomp showed MIPS-to-C was viable, XenonRecomp brought it to Xbox 360's PowerPC — but nobody had tackled the OG Xbox until now. Its x86 architecture makes it both easier (same instruction set family as the host) and harder (variable-length instructions, complex addressing modes, x87 FPU stack) than MIPS or PPC targets.
Emulators are great. Cxbx-Reloaded and xemu do incredible work. But static recomp offers some unique advantages:
- Native performance — recompiled code runs at full speed, no interpretation overhead
- Moddability — the output is human-readable C code; you can patch, extend, and improve the game
- Portability — the C output can target any platform with a C compiler (ARM, RISC-V, WebAssembly...)
- Preservation — a self-contained native binary is the ultimate form of game preservation
- Understanding — the process forces you to deeply understand the game at the machine code level
YOUR XBOX DISC
|
v
+-------------------+
| 1. Extract XBE | Extract default.xbe from the disc image
+-------------------+
|
v
+-------------------+
| 2. Parse XBE | Read headers, sections, kernel imports
+-------------------+ tools/xbe_parser/
|
v
+-------------------+
| 3. Disassemble | Find functions, build control flow graphs
+-------------------+ tools/disasm/
|
v
+-------------------+
| 4. Identify | Classify: CRT, RenderWare, D3D, game code
+-------------------+ tools/func_id/
|
v
+-------------------+
| 5. Lift to C | Translate x86 instructions to C statements
+-------------------+ tools/recomp/
|
v
+-------------------+
| 6. Build Runtime | Kernel shim, D3D translation, memory layout
+-------------------+ templates/runtime/
|
v
+-------------------+
| 7. Compile & Run | MSVC builds native .exe — game runs!
+-------------------+
Following the RexGlueSDK pattern (which does the same for Xbox 360 via Xenia), xboxrecomp provides link-time libraries extracted from xemu and purpose-built compatibility layers. Your recompiled game links against these — no emulator needed at runtime.
| Library | Source | What It Does |
|---|---|---|
| xbox_kernel | Custom | Xbox kernel → Win32 (170 of the kernel's 371 ordinals routed, 169 with dedicated bridge functions: memory, file I/O, threading, sync, crypto, HAL, EEPROM, SMBus) |
| xbox_d3d8 | Custom | D3D8 → D3D11 graphics: 4-stage multi-texture FFP pipeline, NV2A register combiner pixel shaders, programmable vertex shaders (NV2A microcode → HLSL), hardware T&L lighting (8 lights), vertex fog, DrawPrimitiveUP ring buffer, texture unswizzling, 20+ format conversions |
| xbox_dsound | Custom | DirectSound → software mixer (IDirectSound8/IDirectSoundBuffer8) |
| xbox_apu | xemu (LGPL-2.1+) | MCPX APU audio (256-voice processor, ADPCM/PCM, envelopes, HRTF, waveOut output) |
| xbox_nv2a | xemu (regs, LGPL-2.1+) + Custom | NV2A GPU (register handlers, MMIO interception, push buffer parsing, PGRAPH → D3D11 translation) |
| xbox_input | Custom | Xbox gamepad → XInput |
| xbox_video | Custom | FMV playback: Media Foundation decode onto a D3D8 texture, plus a window on the guest framebuffer. For titles whose video is a container Windows already decodes, the emulated decoder does not have to work for the video to be watchable — and the title still decides when it plays |
cd xboxrecomp
cmake -S . -B build
cmake --build build --config ReleaseThis produces 6 static libraries in build/src/*/Release/. Link your game project against xboxrecomp (umbrella target) or individual libraries.
This repo builds libraries only — there is no game .exe here, and building it will never produce one. The executable is built by your game project, which lives in its own directory and links these libraries. Start it by copying templates/new-game/: it has the CMakeLists.txt that produces the .exe and the main.c that boots the guest. See Getting Started, Step 6.
Your recompiled game provides two callback functions that the kernel bridge calls to resolve function addresses:
typedef void (*recomp_func_t)(void);
recomp_func_t recomp_lookup(uint32_t xbox_va); // Auto-generated dispatch table
recomp_func_t recomp_lookup_manual(uint32_t xbox_va); // Hand-written overridesThe recompiler output (tools/recomp) generates these automatically. The xboxrecomp libraries handle everything else — memory layout, kernel calls, graphics, audio, and input.
┌─────────────────────────────────────────────────┐
│ Your Game (.exe) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ recomp/ │ │ manual │ │ game-specific │ │
│ │ gen/*.c │ │ overrides│ │ loaders/formats │ │
│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └──────┬──────┘────────────────┘ │
│ │ recomp_lookup() / ICALL dispatch │
├──────────────┼────────────────────────────────────┤
│ │ xboxrecomp libraries │
│ ┌───────────┴──────────┐ │
│ │ xbox_kernel │ Memory layout, file │
│ │ (kernel_bridge.c) │ I/O, threading, sync │
│ └───────────┬──────────┘ │
│ │ │
│ ┌───────┐ ┌┴──────┐ ┌────────┐ ┌──────┐ ┌─────┐│
│ │xbox_ │ │xbox_ │ │xbox_ │ │xbox_ │ │xbox_││
│ │d3d8 │ │dsound │ │apu │ │nv2a │ │input││
│ │D3D8→ │ │DSound→│ │MCPX APU│ │NV2A │ │XPP→ ││
│ │D3D11 │ │mixer │ │(xemu) │ │(xemu)│ │XInput│
│ └───────┘ └───────┘ └────────┘ └──────┘ └─────┘│
├──────────────────────────────────────────────────┤
│ Windows 11: D3D11, XInput, waveOut, Win32 API │
└──────────────────────────────────────────────────┘
- Windows 11/10 (D3D11 backend, Vulkan optional) — or Linux / macOS (Vulkan backend;
MoltenVK on macOS). The Vulkan backend needs the vendored submodules and, to build, DXC's headers:
a Vulkan SDK provides those, the loader and
libdxcompiler - Python 3.10+ with
capstone(pip install capstone) - Visual Studio 2022, or the 2019 Build Tools (either MSVC works; the 2019 Build Tools ship a CMake of their own, so you may not need to install one)
- CMake 3.20+
- XbSymbolDatabase (MIT), as the submodule
third_party/XbSymbolDatabase, pinned to a known commit.tools.xdk_symbolsuses its CLI to name the XDK functions (D3D8, DirectSound, ...) linked into a title; step 1 builds it. - An original Xbox game disc image (you must own the game)
py -3 below is the Windows Python Launcher — on Linux and macOS use
python3, and on a Microsoft Store install that has no py, use python.
Four stages turn a disc into C, and one script runs all four. The long version is docs/GETTING_STARTED.md, which explains why each flag is there — read that when your title behaves differently from the example, not before.
# 1. Clone with the submodule, and build the XDK symbol tool once.
# tools.xdk_symbols finds the CLI in third_party/XbSymbolDatabase/build by
# itself (or pass --cli, or set XBSDB_CLI).
git clone --recurse-submodules https://github.com/sp00nznet/xboxrecomp.git
cd xboxrecomp
# Already cloned without it: git submodule update --init
cmake -S third_party/XbSymbolDatabase -B third_party/XbSymbolDatabase/build
cmake --build third_party/XbSymbolDatabase/build --config Release
# 2. Put the game's own files where the toolkit will look for them.
# Extract your disc image (xdvdfs, extract-xiso, tools/xiso) into
# games/<title>/, so that games/<title>/default.xbe exists alongside the
# rest of the disc. The game is never distributed with this toolkit: you
# supply it, from a copy you own.
# 3. Make the project the executable is built from. It starts as a copy of
# the template, and the recompiler writes its generated C into it.
cp -r templates/new-game titles/<name> # Windows: xcopy /E /I templates\new-game titles\<name>
# Then set project(<name>_recomp C) in titles/<name>/CMakeLists.txt, and
# point XBOXRECOMP_DIR in the same file at this repository.
# 4. Recompile. This runs all four stages -- parse, disassemble, identify,
# lift -- and writes the generated C into the project.
py -3 scripts/recompile.py "games/<title>/default.xbe" \
--work-dir games/_pipeline/<name>/out --project titles/<name>
# 5. Tell the entry point where to start. The parse in step 4 reports it, and
# leaves it in games/<title>/default_analysis.json as "entry_point".
py -3 scripts/regen_title_main.py <name> "Nice Name" 0x001CF3C9 "<title>"
# 6. Build it.
cmake -S titles/<name> -B titles/<name>/build -G "Visual Studio 16 2019" -A x64
cmake --build titles/<name>/build --config Release
# 7. Run it.
titles\<name>\build\Release\<name>_recomp.exe<name> is whatever you want to call the project; <title> is the folder your
disc was extracted into. They can differ — games/Time Splitters 2/ builds
titles/timesplitters2/.
What to keep. titles/<name>/ is a normal CMake project, and the part
worth committing is small: CMakeLists.txt, src/main.c and
src/recomp_manual.c, which is where hand-written replacements for functions
the lifter could not translate go. The generated C lands in src/recomp/gen/
and the intermediate stage output in the --work-dir; neither belongs in
version control. Give every title its own --work-dir and --project, or a
second one silently overwrites the first.
scripts/regen_title_main.py in step 5 rewrites src/main.c from the
template rather than patching it, so run it again after changing the template
and your edits are not lost — which is why anything title-specific belongs in
recomp_manual.c instead.
It lifts only the game's own code by default (--game-only). CRT and XDK
library code is replaced at the boundary rather than translated, which is both
faster to build and easier to debug. --all takes everything, when you need it.
The executable runs the game by itself — double-click it, or make a shortcut anywhere. It looks for the game in this order:
- a folder called
gamenext to the executable, which is what a build you hand to somebody else should look like; games/<title>/in this repository, relative to the executable, which is where step 2 put it;- whatever
RECOMP_GAME_DIRpoints at, which overrides both.
It is a windowed program, so there is no console. Diagnostics go to whatever
redirected them, else to the terminal you started it from, else to
<executable>.log beside it. If it cannot find the game it says so in a
message box, naming every path it tried.
While it runs: F9 shows the frame rate, F10 steps the frame cap
(adaptive, 60, 30, off), F11 saves the frame on screen as a picture and as
a replayable capture — which is how to report a rendering bug. A keyboard works
out of the box and an XInput controller works as itself;
py -3 -m tools.input_ui rebinds either, for up to four players.
A title can ship a second, small program beside its executable: a launcher that shows the settings worth choosing, saves them, and starts the game. It has a Video tab (resolution scale, widescreen, wide camera, texture sharpness, frame cap, frame-rate counter), an Input tab that rebinds the keyboard and pad for all four ports, and an About tab. It can be driven entirely with a controller — D-pad or stick to move, A to play, B to quit, the shoulder buttons to change tab — because the front ends a recompiled game ends up inside may never see a keyboard.
A title does not get one unless its project asks for it. The code is in
src/launcher/, and the top-level CMakeLists.txt defines a
function for it, but no project calls that function by default, the template
included. To build one, add this to titles/<name>/CMakeLists.txt, after the
add_subdirectory(${XBOXRECOMP_DIR} ...) line (the function does not exist
before it):
if(COMMAND recomp_add_launcher) # Windows only; the guard keeps other hosts building
recomp_add_launcher(${PROJECT_NAME}_launcher ${PROJECT_NAME}.exe)
endif()The next build then produces <name>_recomp_launcher.exe beside
<name>_recomp.exe in build/Release. The second argument is the file name the
launcher starts from its own folder, so it has to match the game's executable
exactly; ${PROJECT_NAME}.exe always does.
The launcher is optional. It writes a settings file and starts the game; the game reads that file whether or not a launcher wrote it, and runs without either. The file is per title, named after the title id in the XBE certificate:
| Host | Settings file |
|---|---|
| Windows | %APPDATA%\xboxrecomp\titles\<title id>.conf |
| Linux | $XDG_CONFIG_HOME/xboxrecomp/titles/<title id>.conf (else ~/.config/...) |
RECOMP_DISPLAY_CONFIG=<path> names a different file for both. Environment
variables override the file, so a .bat that sets RECOMP_RES_SCALE, or
any of the other switches, still behaves exactly as it did. Input bindings are
not in this file: they belong to the player, not the title, and live in
input_bindings.json (docs/technical/input-binding.md).
The launcher needs the game's XBE to know the title id, and so which file
to write. It looks exactly where the game does, in the same order:
RECOMP_GAME_DIR, then game/ beside the executable, then the title's
YOUR_GAME_DIR, which recomp_add_launcher reads out of src/main.c. Its
About tab shows the XBE it found, or says that none was found, and in that case
settings go to titles\default.conf, which no game reads. On the game's side,
the first line starting [CONFIG] in its log names the file it read.
scripts/recompile.py is a driver over four tools you can also run yourself,
which is what you want when a stage needs an argument the driver does not pass
or you want to inspect its output:
# Parse. --json is not optional: the disassembler reads the section layout
# back out of it, and looks for <xbe stem>_analysis.json beside the XBE.
py -3 -m tools.xbe_parser "games/<title>/default.xbe" --json "games/<title>/default_analysis.json"
# Disassemble. --text-only means only .text; a title with code in its XDK
# library sections needs them named, e.g. --extra-sections XIPS,DOLBY.
py -3 -m tools.disasm "games/<title>/default.xbe" --text-only -v
# Identify CRT, RenderWare and library functions, and recover vtables.
py -3 -m tools.func_id "games/<title>/default.xbe" -v
# Lift to C.
py -3 -m tools.recomp "games/<title>/default.xbe" --game-only --split 1000Two optional stages the driver does not run. tools.abi_analysis recovers
calling conventions and parameter counts; without it every signature falls back
to cdecl with no parameters, which still builds but makes the generated C
harder to read. And if you have Ghidra, tools/ghidra_naming/ recognises a few
hundred statically linked CRT and XDK helpers by signature and names them —
worth doing before lifting, since the names reach the generated C, the crash
traces and the ABI reports. Both are covered in
docs/GETTING_STARTED.md.
The first time you run a recompiled game, it will crash. That's normal. The process is iterative:
- Boot — get past the entry point (usually straightforward)
- Stub — identify and stub out functions that touch hardware you haven't implemented yet
- Fix ICALLs — indirect calls (vtable dispatches, function pointers) are the hardest 10%
- Add runtime — implement kernel functions, D3D calls, and input as the game needs them
- Debug — use the ICALL trace ring buffer, memory access logging, and your debugger
- Iterate — each crash teaches you something about the game. Fix it and move on.
With Burnout 3 (the first game recompiled with this toolkit), the process from "empty repo" to "game boots and renders textured 3D tracks" took about two weeks of iterative development.
xboxrecomp/
├── README.md # You are here
├── CMakeLists.txt # Top-level build (builds all runtime libs)
├── tools/ # The recompilation toolchain (Python)
│ ├── xbe_parser/ # XBE file format parser
│ ├── disasm/ # x86 disassembler + function detector
│ ├── func_id/ # Library function identifier
│ ├── abi_analysis/ # Calling convention / param recovery
│ ├── recomp/ # x86 -> C static recompiler
│ ├── debug_symbols/ # Debug-build symbol recovery
│ ├── symbols/ ghidra_naming/ # Optional symbol-name recovery (Ghidra)
│ ├── ida_naming/ # ... or the same thing through IDA
│ ├── xiso/ xmv/ # Disc image and video container tools
│ └── fusion/ # MS Ficl/Fission study tooling
├── src/ # Runtime libraries (C, link-time)
│ ├── kernel/ # xbox_kernel - Xbox kernel → Win32
│ ├── d3d/ # xbox_d3d8 - D3D8 → D3D11 graphics
│ ├── audio/ # xbox_dsound - DirectSound compat
│ ├── apu/ # xbox_apu - MCPX APU emulation (xemu)
│ ├── nv2a/ # xbox_nv2a - NV2A GPU emulation (xemu)
│ ├── input/ # xbox_input - Gamepad → XInput
│ ├── video/ # xbox_video - FMV playback + framebuffer window
│ ├── config/ # xbox_config - per-title settings file
│ └── launcher/ # Settings launcher a title can build beside its .exe
├── include/xbox/ # Public umbrella header (xboxrecomp.h)
├── templates/ # Starter templates for new projects
│ ├── new-game/ # ** Copy this to start a game project **
│ │ ├── CMakeLists.txt # Builds the game .exe, links xboxrecomp
│ │ └── src/main.c # Host entry point: loads XBE, boots guest
│ └── runtime/ # Runtime shim templates
│ ├── recomp_types.h # Register model + ICALL macros
│ ├── xbox_memory.h # Memory layout helpers
│ └── kernel_stubs.h # Kernel function stub templates
└── docs/ # Documentation
├── pipeline/ # Step-by-step pipeline guides
├── technical/ # Deep technical documentation
├── formats/ # Xbox file format references
└── runtime/ # Runtime implementation guides
- Getting Started Guide — End-to-end walkthrough from XBE to running game
- Decompilation Guide — Using this as a function splitter instead: one byte-exact
.sper function, with signatures and the call graph. You never run the recompiler - Tools Reference — Detailed usage for every pipeline tool
- Runtime Libraries — Architecture, build instructions, integration guide
- xbox_kernel — Memory layout, file I/O, threading, sync, crypto, EEPROM, SMBus (11,128 LOC)
- xbox_d3d8 — D3D8 interface, register combiners, vertex shaders, texture unswizzle (8,838 LOC)
- xbox_dsound — DirectSound buffers, 3D audio, mixbins (573 LOC)
- xbox_apu — MCPX APU voice processor, mixer, MMIO (4,168 LOC)
- xbox_nv2a — NV2A GPU registers, push buffer, PGRAPH→D3D11 (4,892 LOC)
- xbox_input — Gamepad state, vibration, button mapping (360 LOC)
- Extracting and Parsing XBE Files
- Disassembly and Function Detection
- Function Identification
- x86 to C Lifting
- Building the Runtime
- Iterative Debugging
- The Register Model — Why global registers work and how the stack is simulated
- Memory Layout Reproduction — CreateFileMapping, mirror views, and address space tricks
- Indirect Call Dispatch — The RECOMP_ICALL problem and how to solve it
- D3D8 to D3D11 Translation — Bridging Xbox's graphics API to modern DirectX
- NV2A Shader Translation — Register combiners and vertex microcode to HLSL
- D3D8LTCG Device Context — Device field map, PB ring management, stub calling conventions
- Xbox Kernel Replacement — Mapping Xbox kernel ordinals to Win32
- SEH and Exception Handling — Structured exception handling in recompiled code
- Lessons Learned — What worked, what didn't, mistakes to avoid
- Gap Analysis vs xemu — What's implemented, what's missing, prioritized roadmap
- Microsoft's Own Recompiler — White-room analysis of Ficl/Fission: pipeline, address map, HLE boundary
- Ficl/Fission Codegen Teardown — IDA/Hex-Rays teardown of both their translators, and how it reframes our roadmap
- Burnout 3 Reunification — bringing the origin title back onto the extracted toolkit: what's done, and the threading gate that makes the runtime a merge not a swap
- XBE File Format — Xbox executable format reference
- Xbox Kernel Exports — All 366 kernel functions documented
The interesting parts each have their own document rather than a summary here, so there is one place to keep correct:
- The Register Model — why the guest
registers are globals (and thread-local), how the guest stack is simulated,
and why every recompiled function is
void f(void). - Memory Layout — reproducing the Xbox
address space with
CreateFileMapping+ 28 mirror views, and whyVirtualAlloccannot do it (mirrors must alias the same physical pages, not copy them). - Indirect Call Dispatch —
call [eax+0x10]with no compile-time target. The hardest part of any bring-up. - NV2A Shader Translation — register combiners and vertex microcode to HLSL, both translated at runtime and cached.
- SEH and Exception Handling — how
__SEH_prolog/__SEH_epilogare detected per title and bridged.
Based on our experience with Burnout 3, the best candidates for Xbox static recomp share these traits:
| Factor | Easier | Harder |
|---|---|---|
| Engine | RenderWare (shared patterns) | Custom engine (unique quirks) |
| Threading | Single-threaded | Multi-threaded with sync |
| GPU usage | Standard D3D8 calls | NV2A push buffer microcode |
| Code size | Small .text section | Large with LTCG |
| Online | Offline only | Xbox Live dependent |
| PC port | No PC version (worth the effort!) | Good PC port exists |
See docs/technical/candidate-games.md for a detailed list of promising targets.
- Burnout 3: Takedown — The origin title and most mature target. 22,097 functions lifted. An earlier build was playable to the main menu at 60fps, but leaned on hand-written menu and render scaffolding; that is being replaced with genuinely recompiled code, and the honest bring-up currently reaches engine/RenderWare init. Treat the old "playable" claim as retired until the recompiled path gets back there.
- Xbox Dashboard — The original Xbox system shell (build 3944); the toolkit on system software rather than a game. Nothing renders yet: the earlier "green orb at 60fps" was the project's own scaffolding drawing a disc, and has been retired along with the fake scene root and hand-rolled asset loader around it. What runs is the dashboard's own code — full init chain, its own D3D8 sizing and allocating its own 640x480 surfaces, its own NV2A pushbuffer, its own
default.xipread. Its UI is driven by a VRML97 + JavaScript scene engine (text→bytecode compiler + stack-machine VM + node-class reflection registry), which is the piece still to come online. - Wreckless: The Yakuza Missions — Xbox launch title (2002). Custom engine, 3,407 functions, boots through CRT init into game main. Debugging early gameplay crash.
- Blood Wake — First-party Microsoft naval combat (2001). Stormfront Studios custom engine. 4,608 functions, 367K lines of C generated (99.1% success). Project scaffolded, working toward first build.
This is an emerging field. Here's how you can contribute:
- Try it on a new game — Pick an Xbox exclusive, follow the pipeline, and see how far you get. Even partial results teach us about the toolchain's gaps.
- Improve the lifter — Coverage is good but unquantified; the honest signal is that an unhandled instruction lifts to a bare
/* mnemonic */comment, so grepping generated output for those finds the gaps. Segment prefixes and the rarer x87/SSE forms are where they cluster. - Document Xbox formats — Every game has its own asset formats. Document what you discover.
- Build runtime components — Better D3D8 emulation, audio, networking — the runtime layer is where most per-game work happens.
- Share your findings — Write up what you learn. The Xbox modding/preservation community benefits from every discovery.
Not sure where to start, or want to sanity-check an idea first? Ask in the Discord — several of the people working on ports and on the lifter are there.
The toolchain is intentionally lightweight:
Python 3.10+
capstone # x86 disassembly (pip install capstone)
pytest # test suite only (pip install pytest)
That's it for the core pipeline — no IDA, no Ghidra, no proprietary tools. Just the standard library + Capstone. (Optional tools/ghidra_naming and tools/ida_naming helpers use headless Ghidra or IDA purely to recover symbol names; neither is ever required to produce a working build.)
py -3 -m pytest tools/ # unit tests
py -3 -m tools.conformance # differential: lifted C vs the real CPU
Run unit tests on MacOS
bash tools/macos/run_tests.shThe unit tests are fast and need no game files. The conformance suite goes further: it assembles each snippet with MSVC, lifts the resulting bytes, then runs the lifted C and the original instructions over the same inputs and requires them to agree. Because we target x86 and run on x86, the host CPU is the oracle — no model to be wrong. See Conformance Testing. It needs a 32-bit MSVC, and is skipped rather than failed where there isn't one.
If you fix a lift, add the case.
The runtime libraries (C) use:
- MSVC (Visual Studio 2022) or MinGW-w64
- Windows SDK (D3D11, DXGI, XInput, waveOut)
- CMake 3.20+
- No external dependencies — all hardware emulation code is self-contained
Q: Is this legal? A: This project provides tools and documentation. You must own a legitimate copy of any game you recompile. No copyrighted game code or assets are included in this repository.
Q: How is this different from an emulator?
A: Emulators interpret or JIT-compile code at runtime. Static recompilation translates the entire binary ahead of time into native C code that compiles to a regular .exe. There's no CPU emulation at runtime — the recompiled functions execute directly.
Q: Can I use this on Xbox 360 games? A: No. Xbox 360 uses PowerPC (big-endian, different ISA). See XenonRecomp for Xbox 360 static recompilation. This toolkit is specifically for the original Xbox's x86 code.
Q: How long does it take to get a game running? A: It depends on the game's complexity. Burnout 3 went from zero to "boots and renders 3D tracks" in about two weeks. Simple games might be faster; complex ones with custom engines could take longer. The toolchain handles the mechanical translation — the real work is building the runtime shims and debugging indirect calls.
Q: Why C output instead of direct x86-64 binary translation? A: C is portable, debuggable, and the compiler optimizes it for you. You can read the output, set breakpoints in it, and modify individual functions. Direct binary translation would be faster to run but impossible to debug or modify.
GPL-3.0 — see LICENSE. This fork became GPL-3.0 so it can reuse code from doaxbv-re, which is GPL-3.0. The upstream code it builds on was released under MIT and keeps that notice (LICENSE.upstream-MIT); MIT and LGPL-2.1-or-later are both compatible with GPL-3.0, so the combined work is distributed under GPL-3.0. Third-party components keep their original licence:
| Component | Licence | Copyright |
|---|---|---|
the DirectSound replacement in src/hle/ (hle_dsound.c, dsound_buffer_model.*, xbox_adpcm.*, audio_output*) |
GPL-3.0 | adapted from doaxbv-re (NoRain211 and contributors) |
the MCPX APU sources in src/apu/ |
LGPL-2.1-or-later | espes; Jannik Vogel; Matt Borgerson |
src/nv2a/nv2a_regs.h |
LGPL-2.1-or-later | espes; Jannik Vogel |
| upstream xboxrecomp code | MIT | sp00nz and contributors |
The APU and the NV2A register definitions were extracted from xemu and are that project's work, not ours. LGPL-2.1 expressly permits linking them from MIT or proprietary code, so a recompiled game is unaffected; what it asks is that the notices stay, the source stays available, and users can relink against a modified library. LICENSES/LGPL-2.1.txt is the verbatim licence text — shipping it alongside those files is a requirement, not a courtesy.
Not every file under src/apu/ and src/nv2a/ is xemu-derived. See
NOTICE for the exact list, each with the copyright it actually
carries — including algorithms we implemented ourselves but learned from xemu,
credited there even where no licence obligation attaches.
xboxrecomp is built by more than one person. See CONTRIBUTORS.md for who did what — including the people who never sent a patch and still moved the project further than a patch would have, by finding the wall everyone else was about to hit.
Thank you, all of you.
Built with Claude Code (Anthropic) — proving that AI-assisted systems programming can tackle problems previously considered impractical.
Human contributors are credited in CONTRIBUTORS.md; the third-party code we build on is credited in NOTICE.
Versions start at v0.1.0 with the initial public release; earlier entries were reconstructed from the commit history, so they are dated by when the work actually landed rather than by any tag that existed at the time.
A release of contributed fixes, and nearly all of them share a shape: the code ran, returned, and was wrong, with no error anywhere. A stub that answers 0. A flag that was dropped instead of preserved. A value rounded the wrong way. A blend state that failed to create and left the previous one bound. None of them look like a bug from where you find them.
Every kernel ordinal is routed. All 371 Xbox kernel exports — 347 function
ordinals plus 24 data exports — now have either a real bridge, a documented
stub, or a data entry. The ~136 that were unrouted fell through to a silent
return-0, which is worse than a crash: the title carries on with a plausible
answer it never asked for. The structural piece is a guest-VA to host-HANDLE
shadow table — a KEVENT/KSEMAPHORE/KMUTANT created through
KeInitializeEvent lives in guest memory and is not a handle, and
KeSetEvent and the KeWaitFor* pair had been treating the VA as one. The
audit that was supposed to catch all this was itself broken and passing: its
regexes anchored on a function's name, the file gained a comment mentioning
that name, the comment matched first, and the check was skipped entirely —
@DarthSidious666 (#32)
Two generator bugs that stop the build. cmovcc reads CF exactly as a
jcc does, but the carry-declaration scan looked only at jcc and setcc, so
a cmovb after an add emitted if (_cf) with _cf never declared. And a
guest function whose recovered name is a reserved C identifier or a Win32
export collides at compile or link time — Black has a function literally named
onexit (C2373 against UCRT's), Nightfire re-exports shims named exactly like
the APIs they wrap (LNK2005 against kernel32.lib). Both take the _<addr>
suffix func_id already gives duplicate names —
@DarthSidious666 (#28)
MMX was losing comparisons and rounding by hand.
- Fifteen implemented MMX forms were missing from the EFLAGS-preserve set,
so the lifter dropped the live comparison before them and recomputed.
cmp eax, 0; pavgb mm0, mm1; sete alreturns 1 on the CPU and returned 0 lifted — @NoRain211 (#34) PADDUSW/PSUBUSWbecame TODO comments while theMOVQloads and stores around them still ran, so a store published the unchanged value — @NoRain211 (#33)- Float-to-MMX conversion added 0.5 and cast, rounding halfway away from
zero regardless of MXCSR, and range-checked against a float
INT32_MAXthat rounds up to 2147483648 and admits an out-of-range cast. Uses the SSE scalar conversions on x86 now — @NoRain211 (#35)
Two D3D8 states that were wrong in the invisible direction.
- Colour blend factors were copied into the alpha fields, which D3D11
rejects, so a guest
SRCCOLORorDESTCOLORfailedCreateBlendStatewithE_INVALIDARGand left the previous state bound — a wrong blend rather than a missing one — @NoRain211 (#36) D3DFVF_XYZRHWthrew RHW away and emitted clip W = 1, so pretransformed geometry landed in the right place with its texture coordinates interpolated affinely across it — @NoRain211 (#37)
The POSIX build works again. Missing includes that C99 turned from warnings
into errors, strtok_s where POSIX wants strtok_r, and no implementation at
all for GetFileSizeEx or the Slim reader/writer locks. An SRWLOCK is usable
straight from SRWLOCK_INIT and is by definition taken from several threads
with nothing else held, so unlike the condition variables its first use
genuinely races, and it is serialised accordingly. Also caught the FATX
geometry constants being defined inside the _WIN32 half and referenced from
the POSIX half — @dplewis (#27)
A real flip drives the frame counter. FLIP_STALL now advances every
registered swap counter, and while those arrive the 62 Hz fallback timer stands
down. That timer exists for a title nothing presents for; once the pushbuffer
executor is actually running flips it is the wrong clock and an actively
harmful one. Half-Life 2's loader paces its intro video on this count, so a
62 Hz timer against an executor managing a fraction of a frame per second ran
the video forward in virtual time far faster than it could be drawn — which
looks exactly like a stalling, blocky video rather than a clock running away.
The rasteriser's per-pixel surface check moved to once per batch alongside it:
dma_resolve walked the arena high-water mark twice per pixel to guard a
rasterisation cheaper than the guard, and neither answer can change mid-batch.
An IDA path for name recovery, alongside the Ghidra one. Not a port of
pcrecomp's four IDA scripts — one exporter that writes the same
functions.json/symbols.json merge_names.py already reads, so the merge,
the placeholder filter, the sanitising and --apply stay where they are.
IDA's FLIRT and Ghidra's FidDb are the same idea with different coverage and
neither is a superset, so running both and taking the union names more than
either alone. merge_names learned IDA's autonames while it was there — loc_
is IDA's LAB_, and jpt_/algn_/asc_/stru_ have no Ghidra equivalent,
so without them an IDA export merges thousands of addresses-in-disguise into
the recompiler.
Also. write_if_changed in the translator — a regen rewrites all 54 chunks
of generated C, and an mtime bump on identical bytes costs a full /O2 rebuild
of 365 MB for nothing.
Half-Life 2 loads a level and draws its own loading screen. Most of what stood in the way was one mistake wearing different clothes: a value read at the wrong moment.
Read where it is set, not where it is used.
- An SSE compare was rebuilt at the branch, not recorded at the compare.
comisslifted to a comment and the comparison was reconstructed at the consumingjccfrom the operands as they read there — which is the same comparison only if nothing in between writes them. MSVC writes them constantly:comiss xmm5, [esi + eax*4]followed bylea eax, [esi + eax*4]means the address register becomes a pointer before the branch reads it. The generated C evaluated the operand witheaxalready holding0x1438C348, which wraps to guest0x651BCD20. 19 of Half-Life 2's 12,617 float compares have that shape; rare, and silently fatal in each. xor reg, regcleared the register but not the carry flag, so a lateradc/sbbborrowed a carry the hardware had cleared — with conformance cases — @NoRain211 (#22)- Carry conditions are lowered from the snapshot rather than reconstructed
after the write, and
cmpxchgdeclares the snapshot it needs.
A missed function boundary skips an epilogue, and an epilogue is where locks
are released. The orphan-recovery pass accepted a recovered block only if it
reached a ret, so a block ending in jmp stayed a stub that pops a return
address and returns. Half-Life 2's CRT _lock helper exits its scan loop
through exactly that shape, and the stub skipped the __finally that calls
_unlock. Traced by address, every CRT lock balanced except _OSFHND_LOCK:
15 takes, 0 drops. Critical sections are recursive, so the holder kept running
and only the second thread blocked — which is why it read as an AB-BA
deadlock between two locks rather than one lock leaking. With that fixed, a
level load goes from 7.8 MB and a deadlock to 15.3 MB with real locks. Four
more boundary shapes recovered alongside it: tail calls, vcall thunks,
__SEH_prolog frames, and constant accessors with no frame at all.
A DMA-object offset is physical. SET_SURFACE_COLOR_OFFSET and
SET_VERTEX_DATA_ARRAY_OFFSET are offsets into a DMA object, not guest VAs,
and the pushbuffer executor only corrected for that when the offset would have
hit the loaded image. Whether it does is an accident of where the image ends —
Half-Life 2's colour surface clears it by 700 KB — so the executor cleared
1.2 MB of black through the guest heap while the real framebuffer sat untouched
in the contiguous window. The test is now the contiguous arena's high-water
mark, which is an answer rather than a guess.
Vertex colours arrive as D3DCOLOR. fetch_attr had no case for NV2A format
0 — a DWORD 0xAARRGGBB whose little-endian bytes run B,G,R,A, the reverse of
every other format it handled — so the fetch failed and the caller's white
fallback took over, which is indistinguishable from a title asking for white.
The colour is also found by format now rather than by slot: slot 3 is diffuse
by convention and HL2 puts it in slot 5.
Contributed.
- The FVF position field was tested as bits —
fvf & D3DFVF_XYZRHWis a bit test against an encoded field, soD3DFVF_XYZB1tested as transformed, and the attribute offset stepped over blend weights and normals as if they were absent — @NoRain211 (#23) - DirectSound cursors and the mixer disagreed, so
SetCurrentPositiondid not seek andPlaydiscarded the position it was given; the fixed-point source position also overflowed past 65,535 frames. Its regression compiles the real mixer against the real device rather than a copy of either — @NoRain211 (#24) - 20 more kernel ordinals routed (SMBus, PCI config space, IRQL, EEPROM
save, semaphores, FP-state save/restore) and the memory-model corrections
behind them: allocator bridges answering from the guest heap instead of
returning a host pointer the title truncates to four bytes, guest-width
writes in
RtlInitUnicodeStringandObReferenceObjectByName, 64-bit returns split acrossg_eax/g_edx. 170 of 371 ordinals routed, and every ordinal Half-Life 2 was hitting unbridged now answers — @DarthSidious666 (#25) - The macOS build path, with
mach/mach.hfor the memory queries and honestTODOs where Darwin has no equivalent — macOS has noMAP_FIXED_NOREPLACE, and plainMAP_FIXEDwould unmap whatever is already there — plusxbox_wcslenfor the 16-bit XboxWCHAR— @dplewis (#20)
Diagnostics, because each of the above cost a day of looking in the wrong
place first: per-lock acquire/release tracing by address (RECOMP_CS_TRACE_CRT),
a watch on one lock with a guest backtrace (RECOMP_CS_WATCH), the guest call
site of a contended lock's holder, RECOMP_WORKERS=inline to answer whether a
bug needs two threads, and a failed file open that names its Win32 error rather
than only its NTSTATUS.
Also: MmAllocateSystemMemory bridged (page-aligned and zeroed, as the
console's page allocator returns), a TIB per guest thread, lock-prefixed
atomics, and the guest's own critical sections actually doing something —
they had been a no-op, which no title had noticed until one ran two threads
through a CRT that cares.
Contributed work, plus what a system application asks for that a game does not.
Contributed.
ReleaseMutexreported success for a release it never performed — the POSIX shim returnedTRUEunconditionally, so a thread releasing a mutex it did not own got success andNtReleaseMutanthandedSTATUS_SUCCESSback to the guest. The guest then ran on believing a still-held mutex was free. Also adds the missingERROR_NOT_OWNERand setsERROR_INVALID_HANDLEon the bad-handle path — @dplewis (#18)- D3D8 texture translation, 4,096 lines and the largest single contribution
to that layer. All 66 Xbox
D3DFMT_*formats mapped to DXGI, cube textures as aTexture2DArraywith per-face unswizzle, volume textures asTexture3Dwith 3D Z-order unswizzle, and software channel conversion for the formats with no direct DXGI equivalent. Shipstests/d3d8_smoke, which builds the reald3d8_resources.cagainst stub device accessors so the format tables are checkable without a D3D11 device. The same PR took hardcoded Burnout 3 strings out of the tools and the Linux default paths — @DarthSidious666 (#17)
Generated-code banners now prefer the title read from the XBE header, with
--game-name as an explicit override — the two mechanisms arrived from
different directions in the same release and both are worth having.
The Xbox Dashboard reached its frame loop, which meant finding four things between a title and a first visible frame, none of them in the title:
- Worker thread stacks were never reclaimed. The pool counted threads ever
created rather than threads alive, so a title that cycles workers exhausted it
and
PsCreateSystemThreadExbegan running them inline — which deadlocks rather than slows, because the worker finishes before its caller reaches the wait it was going to be signalled from. 0xFF000000was not mapped. The MCPX span stops one page short of the flash ROM, so an access that is ordinary on hardware was a hard fault. Backed as plain memory like the NV2A and MCPX apertures.- The pushbuffer survey read the wrong memory.
DMA_PUTholds a physical address andnv2a_pb_scantakes guest VAs, so it walked low memory and reported a confident inventory of nothing while the title was submitting methods all along. - The framebuffer window only ever opened from
AvSetDisplayMode, so a title that draws before setting a display mode got no window however much it rendered. The pushbuffer executor opens it now, when a clear has just proved a surface address is real.
RECOMP_WATCHDOG_SECS also did nothing in any project copied from the template,
because xbox_WatchdogStart() is the host's to call and the template never
called it — the one diagnostic that separates a hang from slowness, silently
inert while appearing to be set.
tools.split — one byte-exact .s per function, for decompilation rather
than recompilation. The bytes are db directives and the disassembly is the
comment beside them, because x86 has multiple encodings per mnemonic and
reassembling a listing produces code that runs identically and does not match.
Verified against the binary: 2,254 of 2,254 functions in the Xbox Dashboard's
.text are byte-identical, including the ones with MSVC switch tables parked
mid-body. See docs/DECOMP.md.
Fixed for new users, all three from people reporting where they got stuck:
recomp_types.h is now written into --gen-dir by the pipeline instead of
living only in templates/runtime/; tools.disasm names the analysis JSON it
wants and the command that writes it; the README's own quick start ran
tools.xbe_parser with no --json, which is why the next step could not find
it. The project template also could not link, defining three ICALL globals the
runtime already owns.
Control flow that leaves a function without returning from it, and the three places the toolkit got that wrong.
Non-local jumps. A recompiled function is a real C function, so restoring
the guest's esp is only half of a longjmp: the abandoned frames are still on
the native stack, and control returns into them once the resume point finishes.
Each guest jmp_buf is now paired with a native one taken at the setjmp call
site — the only place a native setjmp is valid — and the guest longjmp
becomes a native one, so the frames actually unwind. The CRT's pair is found by
the "VC20" cookie MSVC stamps into every jmp_buf. On the title tested this
turned a correctly caught image-loader exception, which had been re-entering the
decoder on a dead frame and looping forever, into a clean unwind.
Frameless callees inherited a dead frame. A function with no prologue of its
own reads ebp through g_seh_ebp, but only tail jumps and the SEH helpers
ever wrote it — so one reached by an ordinary call got whatever frame the last
tail jump left behind. It is now published wherever g_ebp is. setjmp was
saving that stale frame into the buffer, so the longjmp that should have
resumed a catch restored a frame two calls dead.
The fs: segment prefix was dropped, putting the TIB at guest address 0 —
the same address a null pointer dereferences. Two things went wrong there and
both were silent: a null check written as cmp byte [ecx], 0 read the exception
chain head's 0xFF and decided the pointer was fine, and a store through a null
pointer overwrote that head instead of faulting. Segment overrides are now
recorded and based at XBOX_FS_BASE, which leaves page zero free —
RECOMP_TRAP_NULL=1 then makes a null dereference fault where it happens
instead of surfacing hundreds of steps later as a NaN.
Kernel exports that existed but were never dispatched. RtlUnwind,
XeLoadSection/XeUnloadSection and NtSuspendThread all had implementations
and no entry in the bridge table, which is worse than an outright stub: each
returned success without doing anything. NtSuspendThread was the costly one —
a worker that parked itself never stopped, and spun through 289 million kernel
calls while the title believed it was idle. After bridging: 9,789.
MCPX APU never started. The frame thread idles on pause_requested, which
init sets and only the test tone ever cleared, so a title that enabled the APU
through NV_PAPU_SECTL/FECTL got an APU that stayed asleep. Writing those
registers now resumes it.
Instructions. cvtps2pi / cvttps2pi implemented — 36 of them sat inside
one title's WMV decoder as no-op comments.
Diagnostics, because a recompiled title offers no debugger and no printf:
tools/stackwalk.py— guest backtraces from a stack dump. The native stack shows only whichever translated function is spinning; the guest stack still carries a return site for every guest frame.RECOMP_WATCHDOG_SECS— dumps the guest call stack when a title stops making progress, which is otherwise indistinguishable from working.RECOMP_TRACE_ARGS/RECOMP_TRACE_DEREF— stack arguments and one level of pointer dereference at each traced entry. Registers alone will not tell you which argument arrived null.RECOMP_PEEK/RECOMP_PEEK_CHAIN— read guest dwords, or walk a pointer chain, without a run per level.RECOMP_WATCH_VA— hardware watchpoint on a guest address, generalised from a single hardcoded one.RECOMP_PB_SCAN/RECOMP_PB_EXEC— survey a title's NV2A pushbuffer and execute its surface and clear methods. The survey ranks what is not implemented, so the remaining work is a list rather than a guess.RECOMP_FB_WINDOW— a window on the guest framebuffer. Nothing else scans it out, so however much of the GPU works, none of it is observable without this.
Fixed: duplicate trace symbols broke the link for any title defining its own
recomp_trace_*; they now live once in the kernel.
The first release with contributors other than the maintainer, and the housekeeping that should have been in place before there were any.
Correctness — the silent kind. Every fix here produced C that compiled, linked, ran, and was wrong, with no lifter warning anywhere.
- Conditional tail calls skipped the frame bridge —
jccto a known function entry is a tail call, but only the unconditional form emitted the bridge, so the taken edge ran with the caller's frame still live. 8,263 call sites across 5,426 functions on the title tested — @NoRain211 (#7) - Indirect calls read their target after the return-address push, so
call [esp+X]resolved from the wrong slot — @NoRain211 (#7) repe cmpsb/repne scasbfolded their flags to a literal 1, so everymemcmp/strcmp-shaped loop in the CRT reported "equal" regardless of input — @NoRain211 (#8)NEGcarry was dropped before a non-adjacentSBB/ADC, which is the standard 64-bit subtract and sign-extend idiom — @NoRain211 (#8)- Signed compares evaluated at 32 bits regardless of operand width, so the sign bit of an 8- or 16-bit operand was never in the right place — @NoRain211 (#8)
- Packed SSE was lifted as a scalar
float—movaps/movupsmoved 4 of 16 bytes and dropped the upper three lanes (18,439 moves), and packed arithmetic had no pattern at all (561 operations dropped) — @NoRain211 (#9) - 904 x87 instructions across 28 mnemonics lifted to comments, desynchro-
nising the FPU stack from that point on;
FNSTCW/FNSTSWwere comments too, so everyfcom-derived parity test read a hardcodedtrue(1,326 sites) — @NoRain211 (#9) - XMM was a function-local, so a value written in one lifted block and read in the next was lost — @NoRain211 (#10)
Pipeline
tools/abi_analysisnow exists.tools.recomphad always looked forabi_functions.json, warned when it was missing, and then fallen back to cdecl / 0 params / int-or-void for every function — because the tool meant to produce that file was never written. Recovers calling convention (including thiscall), parameter count from theretimmediate, return-type hints and frame shape — @DarthSidious666 (#6)- The SSE runtime. The lift in #9/#10 emitted 28
XMM_*helpers that nothing defined. AddedRecompXmmplus lane-wise implementations, verified by compiling real lifter output under MSVC and checking the cases where x86 disagrees with naive C —MINPSreturning its second operand on a tie,ANDNPSbeing~dst & src,CMPNEQPSbeing the unordered form. - The research branch merged back: per-title SEH detection, the function-boundary fix, operand-aware x87, the MS Ficl/Fission study, XISO redump support, and indirect-call feedback.
Project
-
CONTRIBUTORS.md — including the people who only ever filed an issue. @Tiptup300 (#1) found that every documented getting-started step was broken, on Linux; that report is why the pipeline was fixed and why this repository has a LICENSE file at all. @M0RSM4LLEO (#2) reproduced it with the detail that made it actionable.
-
LGPL compliance. The xemu-derived APU and NV2A sources always carried their notices, but the repository shipped no
NOTICEand no copy of the licence. Both now present, with every affected file listed against the copyright it actually carries. -
The test suite actually runs. A bare import in
tools/symbolsaborted pytest collection for the whole tree, sopytest tools/executed nothing. Now 141 tests. -
Differential conformance testing (
tools/conformance) — assembles each snippet with MSVC, lifts the bytes, and runs the lifted C against the original instructions on the real CPU over 2,043 input vectors covering integer results, the x87 stack (values and depth) and all four SSE lanes. Adapted from ps3recomp's methodology, but stronger here: we target x86 and run on x86, so the oracle is the hardware rather than a model of it. It found three live bugs, all of which the existing string-comparison tests passed:fxch st(i)was a silent no-op — Capstone reportsfxchwith both operands,(st(0), st(i)), and it is the only x87 form that does, so the handler picked up the implicitst(0)and swapped st0 with itself.stc/clc/cmcwere unimplemented, so the carry a followingadc/sbbread kept whatever the last arithmetic left in it.fnstswdid not model TOP (status bits 11–13, AH bits 3–5) and theaxform wrote only AH rather than all of AX.
-
Whole-function conformance — a second phase compiles a C corpus with
/O2 /arch:IA32(Pentium III: SSE1, no SSE2, like the real hardware), lifts the machine code back through the fullFunctionTranslator, and runs it against the original. Testing what the optimiser emits rather than what someone thought to write down found two more:- Flag state followed address order, not control flow. A
jccconsuming acmpfrom a non-adjacent block inherited the flags of whatever sat above it in memory — usually anadd, which clobbers them. State now propagates along predecessor edges, and only when every predecessor agrees. js/jnsevaluated the sign at 32 bits, so after an 8- or 16-bittestevery value with the top bit set looked positive. The same width bug the signed compares had; these two were missed at the time.bt/btr/bts/btcwere unhandled — 386 instructions, lifted to a comment, so the bit was silently left alone. Surfaced once the corpus began lifting the CRT's float-to-int helper, which usesbtron the x87 control word.
The corpus lifts from a linked image, so jump tables,
.rdatafloat constants and calls to CRT helpers all work —__allmulis lifted and verified alongside the corpus itself. - Flag state followed address order, not control flow. A
-
Conformance against a real title (
--xbe path/to/default.xbe) — Xbox code is 32-bit x86 and the harness is a 32-bit x86 process, so a game's own machine code can be executed as the oracle: map the XBE where it was linked for, call one of its functions, run the lifted C over the same arguments, and compare. Candidates are picked mechanically (no calls, no invented pointers, plainret, nothing lifting to a comment), so what gets compared is provably safe to run. Verified clean on Burnout 3, Conker, Crimson Skies and Blood Wake. No game files are included or needed for the rest of the suite.It found that
fnstswdid not model C2, the unordered bit. An x87 compare against a NaN sets C3, C2 and C0 together, andfucompp; fnstsw ax; test ah,44h; jpis how this era's CRT asks "is this a NaN" — reporting "equal" answered no every time, sending every float classification in a title down the wrong branch. Found by running Crimson Skies' own float classification against itself.Totals: 2,599 snippet vectors, 211 whole-function vectors, plus per-title runs (Burnout 3: 37 functions / 161 vectors clean).
- Fall-through into the next function was dropped. When the disassembler splits a straight-line run of code at an internal branch target, the earlier function often ends by falling through into the next — which x86 executes. The lifter emitted nothing, so the body ended and skipped the next function's shared epilogue: an esp leak that corrupted callee-saved registers. 4,587 of 35,286 functions in Burnout 3 had this shape.
- Per-title SEH detection.
__SEH_prolog/__SEH_epilogaddresses were hardcoded to one game's CRT, so on every other title theebpread-back was never emitted. Found by signature now. - Halo bring-up: debug-build symbol recovery, per-target memory map, x87 correctness, and seven misrouted kernel ordinals.
- Cross-platform layer with an OpenGL D3D8 backend beside the Windows D3D11 path, POSIX path handling, and Linux build deps. Builds with GCC/Clang.
ghidra_naming(optional) — headless Ghidra FidDb pass recovers real CRT/XDK symbol names from a stripped XBE. The core pipeline still needs no disassembler.
- Full multi-texture fixed-function pipeline — 4-stage blending with all
D3D8 operations and full
D3DTAargument resolution, 4 samplers per draw. - Hardware T&L lighting — up to 8 lights with materials, global ambient, specular, and world-space normal transform; Blinn-Phong with attenuation and spotlight cones.
- Vertex fog (linear/exp/exp2) and a 4MB DrawPrimitiveUP ring buffer that removes per-call buffer create/destroy.
--seed-functionsfor iterative disassembly on stripped binaries.
- NV2A register combiner pixel shaders — full 8-stage plus final combiner translated to HLSL at runtime, with a 128-entry cache.
- NV2A programmable vertex shaders — 128-bit microcode parser and HLSL generator covering all 14 MAC and 8 ILU operations, 192 constant registers, and relative addressing.
- Texture unswizzling — Xbox Z-order (Morton) to linear.
- NV2A PGRAPH → D3D11 translator, push buffer method interception.
- EEPROM / AV pack / SMBus so games can query region, language, video standard and hardware info.
Initial public release: XBE parser, x86 disassembler and function detector, library-function identifier, the x86 → C recompiler, and the runtime libraries (kernel, D3D8, DirectSound, APU, NV2A, input), extracted from the Burnout 3 bring-up that started it.
- XBE File Format — Xbox Dev Wiki
- Xbox Kernel Exports — Xbox Dev Wiki
- NV2A GPU — Xbox GPU documentation
- Xbox Architecture — Copetti's deep dive
- N64Recomp — Static recomp for N64 (MIPS→C)
- XenonRecomp — Static recomp for Xbox 360 (PPC→C)
- RexGlueSDK — Xbox 360 recomp runtime (Xenia as link-time library)
- Cxbx-Reloaded — Xbox emulator (dynamic recomp)
- xemu — Xbox emulator (LLE)