# Saints Row (Xbox 360, 2006) - Static Recompilation **Status: 3D Rendering Active -- Cathedral Scene with Correct Sky Rendering** ![Saints Row Logo Screen](logo.png) ![Cathedral Loading Screen](newscreen2.png) Static recompilation of Saints Row for Xbox 360 to native x86-64 PC executable using [XenonRecomp](https://github.com/hedge-dev/XenonRecomp) and [ReXGlue SDK](https://github.com/rexglue/rexglue-sdk). Saints Row (2006) was an Xbox 360 exclusive and has never been ported to PC, making it a compelling preservation target. ## Background This project uses the same toolchain proven across multiple Xbox 360 titles: - [360tools](https://github.com/sp00nznet/360tools) -- Extraction, analysis, and project templates - [XenonRecomp](https://github.com/hedge-dev/XenonRecomp) -- PowerPC to C++ static recompiler - [ReXGlue SDK](https://github.com/rexglue/rexglue-sdk) -- Xbox 360 runtime (kernel, D3D12 GPU, audio, input) ## Project Complexity Saints Row is a large open-world game -- significantly more complex than the arcade ports and smaller titles that have been successfully recompiled so far. Key challenges: - **Scale**: Large PE image with tens of thousands of functions - **Open world**: Streaming world data, LOD systems, AI systems - **Physics**: Havok physics engine integration - **Audio**: Complex multi-channel audio with environmental effects - **Networking**: Xbox Live stubs needed for save/profile systems ## Pipeline ``` Saints Row.iso | v [ extract_iso.py ] -- Extract game files from XDVDFS disc image | v [ extract_pe.py ] -- Decrypt XEX2 + decompress PE image | v [ find_abi_addrs.py ] -- Locate PPC ABI helpers [ extract_switch_tables.py ] -- Map jump tables [ xex_info.py ] -- Analyze XEX2 headers [ parse_xex_imports.py ] -- Identify kernel/XAM imports | v [ XenonRecomp ] -- PowerPC -> C++ static recompilation | v [ ReXGlue SDK ] -- Xbox 360 runtime environment | v Native x86-64 .exe -- Saints Row on PC ``` ## Progress - [x] Acquire game ISO - [x] Extract ISO contents (375 files from XGD2 disc) - [x] Extract and analyze XEX2 binary (33.4 MB PE, 12 sections, Bink middleware) - [x] Find ABI helper addresses (8/10 found, no setjmp/longjmp) - [x] Analyze kernel imports (266 kernel + 204 XAM) - [x] Run XenonRecomp first pass (33,725 functions -> 122 MB C++) - [x] Resolve unrecognized instructions (vandc, mulhdu, frsqrte, dcbst) - [x] Create project scaffold (CMake + ReXGlue SDK v0.7.0) - [x] First compilation (45 MB native x86-64 executable) - [x] First boot -- game entry point executes, loads all packfiles - [x] D3D12 GPU initialization (NVIDIA RTX 5070, ROV, tier 3) - [x] Audio system (XMA decoder + SDL audio) - [x] Game creates 14+ worker threads (physics, streaming, etc.) - [x] Bink video decode -- both splash videos decode and audio plays - [x] Game state machine -- transitions through all loading phases (state 1 -> 2 -> 3) - [x] Content loading system -- all 151 items loaded (~30-60 seconds) - [x] VdSwap/IssueSwap -- frames present to D3D12 swap chain - [x] GPU ring buffer pipeline -- PM4 indirect buffers with circular wrap - [x] GPU command processor -- processes 4500+ kicks, executes draw calls - [x] Shader compilation -- 9 shaders, 5 graphics pipelines (including 3D game rendering) - [x] Render targets created (1280x2048 color + depth at EDRAM base 0/720) - [x] D3D12 presenter connected to window - [x] **3D game content visible** -- Saints Row logo, cathedral, loading screen rendered - [x] Game loop stable at 6500+ frames - [x] Fix presentation flicker -- atomic swap guard, every-frame IssueSwap from GL2_Render - [x] Fix tiling artifacts -- linear fetch constants for D3D12 framebuffer (sky now renders correctly) - [x] Fix PM4 packet overflow -- virtual→physical memcpy for render IBs, zero-gap scanner fix - [x] Fix ring buffer overflow -- zero physical memory on init and wrap-around - [x] Streaming IO pump in all game states (world loading continues in state 3) - [x] Headless static-analysis pipeline (IDA Pro + Ghidra) byte-aligned to the recomp image - [x] Boot/splash sequence confirmed to complete naturally (state 1 -> 2 -> 3, 151-item queue drains) - [x] Root-caused the post-boot crash to a re-entrant IO-init null deref (see below) - [ ] Fix re-entrant IO-subsystem init crash loop (sub_8260CC50) - [ ] Fix remaining color tint on highlights (red cast on bright areas, pink trees) - [ ] Input system (controller navigation past menus) - [ ] In-game rendering - [ ] Gameplay ### Current Architecture The game boots through Bink video playback (THQ/Volition logos), then loads 151 content items via a streaming IO pipeline: StreamCallback -> IOComp -> ResLookup -> BufAlloc -> IORead -> IOWork. Loading uses KeInitializeSemaphore preservation to maintain IO thread synchronization across re-initialization cycles, with multi-pump StreamCallback (8x per GameUpdate) for parallel IO dispatch. After loading, PostLoop5 runs the main game loop calling GL2_Render at ~30fps. The GPU command processor receives PM4 commands via the primary ring buffer (physical memory writes with circular wrap-around), follows INDIRECT_BUFFER_PFD references to secondary buffers containing draw calls, and compiles 5 graphics pipelines including 3 game-specific 3D rendering shader pairs with lighting and texture sampling. ### Key Technical Fixes - **KeInitializeSemaphore override**: Preserves native semaphore objects when game reinitializes IO semaphores, preventing orphaned IO threads - **Audio deadlock fix**: Skip XAudioRegisterRenderDriverClient on re-init (sub_82600A68 acquires global_critical_region_ held by audio callback) - **Ring buffer physical memory**: Write INDIRECT_BUFFER_PFD to physical memory (not virtual) so command processor can read them - **Ring buffer wrap-around**: Circular write position prevents game loop kicks from being silently dropped - **Virtual→physical memcpy**: Game writes PM4 to virtual memory but CP reads physical -- copy render IB data before creating indirect buffer references - **PM4 zero-gap scanner**: Stop at first zero instead of skipping -- CP interprets zeros as Type0 packets causing misalignment and overflow errors - **Linear fetch constants**: D3D12 framebuffer is linear, not Xenos-tiled -- clearing tiled bit fixes crosshatch artifacts and sky color - **Atomic swap guard**: Prevent CallInThread queue buildup with atomic swap_pending flag -- eliminates presentation flashing - **Ring buffer zero-fill**: Zero physical memory on init and wrap to prevent stale loading-phase PM4 data from causing overflow errors - **Null page threshold**: Guest addresses < 256MB treated as null dereferences to prevent demand-page exhaustion - **VEH ultra-fast path**: Handles bad vtable dispatches at fault_addr=0x8000001E without overhead ### Latest Investigation (2026-05-31) A deep static-analysis pass (IDA Pro headless, byte-aligned to the recomp image, cross-checked against the generated PPC) corrected two long-standing assumptions and pinpointed the real post-boot blocker: - **Static-analysis tooling fixed.** IDA had been loading the extracted PE with stale section `PointerToRawData` values, shifting all of `.text` by `0xE00` and producing garbage decompilation. Patching section headers so raw offset == RVA gives a byte-exact, fully-decompiling database (18,842 functions) that matches the recomp 1:1. - **`0x8370F248` is a red herring.** All seven writers set it to `nullsub_2` (a no-op stub) -- it is a hook slot that defaults to doing nothing, not a "physics/world init pointer." Whether it is null or `nullsub_2` is functionally identical. - **The boot/splash load is NOT stuck.** A loading-diagnostic hook had been reading the wrong address (`0x840BAAE8`, which has zero code references and holds uninitialized data) instead of the real loading-slot array at `0x832BAAE8`. With the correct address, the boot state machine (`sub_822827B0`) advances `1 -> 2 -> 3` on its own and the 151-item content queue drains -- the splash sequence completes. - **Real blocker: a re-entrant IO-init crash loop.** After boot, the game crash-loops on a deterministic null-object dereference (read at `obj+0x0`/`+0x8`) reached via `StreamCallback (sub_8265F720) -> IOComplete (sub_82604BE8) -> IOCtxCreate (sub_8260C318) -> IOInit (sub_8260CC50)`. `sub_8260CC50` initializes the IO semaphore (`0x832AD66C`), builds the IO queue list-heads, and spawns the IO worker threads (`sub_8260CBA0`). It is meant to run once but is re-entered every frame, re-initializing the semaphore and queues while the worker threads concurrently use them. The VEH masks each fault, but the corruption eventually turns fatal. Proposed fix: a true once-guard on `sub_8260CC50` so the IO subsystem initializes a single time. ### Known Issues - Re-entrant IO-subsystem init (`sub_8260CC50`) crash-loops on a null deref after boot (current top blocker) - Red color tint on bright areas (highlights, trees) -- likely gamma ramp or render target format mismatch - Scrolling white horizontal bar (screen tearing from unsynchronized IssueSwap) - Second+ Bink videos blocked (garbage allocation crash) ## Related Projects - [StilwaterReclaimed](https://github.com/THE-W0RLD/StilwaterReclaimed) -- Saints Row 2 static recompilation (early stage) - [Halo 3 Recomp](https://github.com/twist84/halo3_cache_debug_recomp) -- Halo 3 cache debug recomp using ReXGlue SDK - [OpenRow2](https://github.com/KairiFey/OpenRow2) -- Decompilation of the Saints Row 2 PC port - [Xenia](https://github.com/xenia-project/xenia) -- Xbox 360 emulator with Saints Row compatibility info ## Prerequisites - Python 3.8+ with `pycryptodome` - CMake 3.25+, Ninja, Clang 18+ (clang-cl on Windows) - MSVC 2022 (for Windows SDK headers) - [360tools](https://github.com/sp00nznet/360tools) ## License Tools and scripts in this repo are provided under the MIT License.