Skip to content

About

Turn any Xbox game binary into a native Windows executable. No emulation. No interpreter. Just raw, recompiled C.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

xboxrecomp

 #   #  ####    ###   #   #         #####   ###    ###   #       ###
 #   #  #   #  #   #  #   #           #    #   #  #   #  #      #
  # #   ####   #   #   # #            #    #   #  #   #  #       ##
  # #   #   #  #   #   # #            #    #   #  #   #  #         #
 #   #  #   #  #   #  #   #           #    #   #  #   #  #         #
 #   #  ####    ###   #   #           #     ###    ###   #####   ###

 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.

Recent Changes

Current version: v0.9.0 — "Quietly Wrong" (September 2026). See the Changelog for what landed and when.


What Is This?

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.

Why Not Just Use an Emulator?

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

The Pipeline

         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!
    +-------------------+

Runtime Libraries

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

Building the Libraries

cd xboxrecomp
cmake -S . -B build
cmake --build build --config Release

This 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.

Integration Pattern

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 overrides

The recompiler output (tools/recomp) generates these automatically. The xboxrecomp libraries handle everything else — memory layout, kernel calls, graphics, audio, and input.

Architecture

┌─────────────────────────────────────────────────┐
│              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   │
└──────────────────────────────────────────────────┘

Quick Start

Prerequisites

  • 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_symbols uses 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.

Step-by-Step

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.

Running it

The executable runs the game by itself — double-click it, or make a shortcut anywhere. It looks for the game in this order:

  1. a folder called game next to the executable, which is what a build you hand to somebody else should look like;
  2. games/<title>/ in this repository, relative to the executable, which is where step 2 put it;
  3. whatever RECOMP_GAME_DIR points 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.

The launcher

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.

Stage by stage

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 1000

Two 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.

What To Expect

The first time you run a recompiled game, it will crash. That's normal. The process is iterative:

  1. Boot — get past the entry point (usually straightforward)
  2. Stub — identify and stub out functions that touch hardware you haven't implemented yet
  3. Fix ICALLs — indirect calls (vtable dispatches, function pointers) are the hardest 10%
  4. Add runtime — implement kernel functions, D3D calls, and input as the game needs them
  5. Debug — use the ICALL trace ring buffer, memory access logging, and your debugger
  6. 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.

Repository Structure

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

Documentation

Start Here

  • Getting Started Guide — End-to-end walkthrough from XBE to running game
  • Decompilation Guide — Using this as a function splitter instead: one byte-exact .s per 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

Per-Module API Reference

  • 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)

Pipeline Guides

Technical Deep Dives

Xbox Formats

How It Works

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 why VirtualAlloc cannot 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_epilog are detected per title and bridged.

Games That Work Well As Targets

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.

Projects Using This Toolkit

  • 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.xip read. 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.

How You Can Help

This is an emerging field. Here's how you can contribute:

  1. 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.
  2. 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.
  3. Document Xbox formats — Every game has its own asset formats. Document what you discover.
  4. Build runtime components — Better D3D8 emulation, audio, networking — the runtime layer is where most per-game work happens.
  5. 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.

Dependencies

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.)

Running the tests

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.sh

The 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

FAQ

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.

License

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.

Contributors

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.

Credits

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.

Changelog

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.

v0.9.0 — "Quietly Wrong" (September 2026)

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 al returns 1 on the CPU and returned 0 lifted — @NoRain211 (#34)
  • PADDUSW/PSUBUSW became TODO comments while the MOVQ loads 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_MAX that 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 SRCCOLOR or DESTCOLOR failed CreateBlendState with E_INVALIDARG and left the previous state bound — a wrong blend rather than a missing one — @NoRain211 (#36)
  • D3DFVF_XYZRHW threw 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.

v0.8.0 — "Snapshot" (September 2026)

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. comiss lifted to a comment and the comparison was reconstructed at the consuming jcc from 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 by lea eax, [esi + eax*4] means the address register becomes a pointer before the branch reads it. The generated C evaluated the operand with eax already holding 0x1438C348, which wraps to guest 0x651BCD20. 19 of Half-Life 2's 12,617 float compares have that shape; rare, and silently fatal in each.
  • xor reg, reg cleared the register but not the carry flag, so a later adc/sbb borrowed 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 cmpxchg declares 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_XYZRHW is a bit test against an encoded field, so D3DFVF_XYZB1 tested 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 SetCurrentPosition did not seek and Play discarded 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 RtlInitUnicodeString and ObReferenceObjectByName, 64-bit returns split across g_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.h for the memory queries and honest TODOs where Darwin has no equivalent — macOS has no MAP_FIXED_NOREPLACE, and plain MAP_FIXED would unmap whatever is already there — plus xbox_wcslen for the 16-bit Xbox WCHAR — @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.

v0.7.1 — "Non-Local" (September 2026)

Contributed work, plus what a system application asks for that a game does not.

Contributed.

  • ReleaseMutex reported success for a release it never performed — the POSIX shim returned TRUE unconditionally, so a thread releasing a mutex it did not own got success and NtReleaseMutant handed STATUS_SUCCESS back to the guest. The guest then ran on believing a still-held mutex was free. Also adds the missing ERROR_NOT_OWNER and sets ERROR_INVALID_HANDLE on 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 a Texture2DArray with per-face unswizzle, volume textures as Texture3D with 3D Z-order unswizzle, and software channel conversion for the formats with no direct DXGI equivalent. Ships tests/d3d8_smoke, which builds the real d3d8_resources.c against 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 PsCreateSystemThreadEx began 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.
  • 0xFF000000 was 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_PUT holds a physical address and nv2a_pb_scan takes 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.

v0.7.0 — "Non-Local" (August 2026)

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.

v0.6.0 — "Credit Where Due" (August 2026)

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 — jcc to 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 scasb folded their flags to a literal 1, so every memcmp/strcmp-shaped loop in the CRT reported "equal" regardless of input — @NoRain211 (#8)
  • NEG carry was dropped before a non-adjacent SBB/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/movups moved 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/FNSTSW were comments too, so every fcom-derived parity test read a hardcoded true (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_analysis now exists. tools.recomp had always looked for abi_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 the ret immediate, return-type hints and frame shape — @DarthSidious666 (#6)
  • The SSE runtime. The lift in #9/#10 emitted 28 XMM_* helpers that nothing defined. Added RecompXmm plus lane-wise implementations, verified by compiling real lifter output under MSVC and checking the cases where x86 disagrees with naive C — MINPS returning its second operand on a tie, ANDNPS being ~dst & src, CMPNEQPS being 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 NOTICE and 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/symbols aborted pytest collection for the whole tree, so pytest 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 reports fxch with both operands, (st(0), st(i)), and it is the only x87 form that does, so the handler picked up the implicit st(0) and swapped st0 with itself.
    • stc/clc/cmc were unimplemented, so the carry a following adc/sbb read kept whatever the last arithmetic left in it.
    • fnstsw did not model TOP (status bits 11–13, AH bits 3–5) and the ax form 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 full FunctionTranslator, 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 jcc consuming a cmp from a non-adjacent block inherited the flags of whatever sat above it in memory — usually an add, which clobbers them. State now propagates along predecessor edges, and only when every predecessor agrees.
    • js/jns evaluated the sign at 32 bits, so after an 8- or 16-bit test every 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/btc were 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 uses btr on the x87 control word.

    The corpus lifts from a linked image, so jump tables, .rdata float constants and calls to CRT helpers all work — __allmul is lifted and verified alongside the corpus itself.

  • 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, plain ret, 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 fnstsw did not model C2, the unordered bit. An x87 compare against a NaN sets C3, C2 and C0 together, and fucompp; fnstsw ax; test ah,44h; jp is 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).

v0.5.0 — "Fall-Through" (July 2026)

  • 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_epilog addresses were hardcoded to one game's CRT, so on every other title the ebp read-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.

v0.4.0 — "Portable" (May 2026)

  • 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.

v0.3.0 — "Fixed Function" (March 2026)

  • Full multi-texture fixed-function pipeline — 4-stage blending with all D3D8 operations and full D3DTA argument 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-functions for iterative disassembly on stripped binaries.

v0.2.0 — "Programmable" (March 2026)

  • 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.

v0.1.0 — "First Light" (March 2026)

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.

References

About

Turn any Xbox game binary into a native Windows executable. No emulation. No interpreter. Just raw, recompiled C.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages