Skip to content

The recomp pipeline

Blur’s x86 code is recompiled offline to arm64 C++ and compiled with clang; nothing is emulated at runtime. The pipeline runs on your game files at build time, and its output is never committed or distributed.

Five modules go through it: Blur.exe (1.2 patch), binkw32.dll, msvcr90.dll, d3dx9_42.dll and D3DCompiler_42.dll. from_game.sh modules copies them from the disc, ISO or an install folder and checks each md5 against the version the recompiler, rebase step and loader expect. The app’s installer uses the same sources.

  1. Modules (from_game.sh modules): extract and verify.
  2. P-code (from_game.sh pcode): D3DCompiler_42.dll is rebased to 0x10200000 (tools/recomp/rebase_pe.py) because its preferred base collides with d3dx9_42. Each module then goes through headless Ghidra via tools/recomp/ghidra/discover.sh: RecompDiscover.java finds functions and jump tables, RecompExport.java writes raw P-code per function to <module>/after/pcode.jsonl. Modules run in parallel, ~8 min in total. Entries Ghidra misses but that are proven at run time go in tools/recomp/ghidra/force/<module>.txt.
  3. Generation (from_game.sh gen, tools/recomp/build/gen.sh): concatenates the P-code, runs tools/recomp/emit.py to produce C++, and tools/recomp/build/shard.py splits it into shards (default 128) in work/recomp/m3/gen. Only shards whose content changed are rewritten, so incremental builds stay incremental.
  4. Build: CMake compiles the shards into blurhost when RECOMP_GEN_DIR exists.

emit.py turns P-code into C++ that reads as native code rather than an emulated CPU:

  • registers and flags are function-local variables, spilled to a per-thread context only at calls, returns and indirect jumps;
  • intra-function control flow uses goto; direct calls are C calls; indirect calls go through one dispatch table;
  • a target missing from the dispatch table is a hard error that logs the address. Fix it by adding the address to discovery (a force/ entry) and regenerating;
  • x87 runs as host double; SSE/MMX map to vector types; P-code CALLOTHER ops are implemented in tools/recomp/ops_ext.h;
  • guest memory is one 4 GB space; every access uses acquire/release (TSO-safe) loads and stores.

Constructs the emitter can’t handle fail the whole function loudly; failures are listed in emitted.cpp.failed. Functions the host replaces at run time (the HOOKS set in emit.py) are the only ones that pay a hook lookup per call.

tools/recomp/rt.h and ops_ext.h are the runtime the generated code includes. MSVCR90 is not recompiled for the app’s runtime path; its exports are libc-like and implemented natively in src/host.

CI caches P-code keyed by a hash of the module files, the Ghidra-side tooling in git and the Ghidra version, so emitter changes don’t rerun Ghidra (CI and releases).