TMB_Recomp
TMB_Recomp is a native PC port of Twisted Metal: Black that recompiles the game to C++ for Windows and Linux.
- Type: PC port
- Game: Twisted Metal: Black
- Runs on Windows
- By The Bozos
- Latest release v0.3.2, 2026-11-02
- Source: https://github.com/TheBozos/TMB_Recomp
README
TMB_Recomp
A native Linux and Windows port of Twisted Metal: Black (USA SCUS-97101): the whole game recompiled to C++ and run on the PC itself.
Not an emulator, and getting rid of what is left of one. The game's MIPS code (EE) and its sound driver (IOP) run as
native code; its 3D is drawn by a PC renderer from the game's own meshes, matrices and textures; its frames are paced and
its sound mixed on the PC's clock. Every VU1 program the game runs is native C++ (no VU1 interpreter, no sandbox).
No GS front end either: by default no GS object, no GS memory, no GS thread and no GS register state exist; the native
renderer draws and presents every frame, and what the game still asks of the display (the field a vblank ended, its
display mode) comes from native code on the PC's clock.
[!IMPORTANT] No game files are included: no ELF, IRX modules, data archives, or code generated from them. You need your own copy of the game: the USA release, PS2 ID
SCUS-97101. The port is built against its executable:
File SHA-1 SCUS_971.013ff17f4897552e355d0bb8c3abfa5125fe3ff2ef
For players (Windows)
You build the game yourself, on your PC, from your own disc: nothing of the game is in the download.
- Download the release zip from the Releases page. Each release
publishes the zip's SHA-256: check it before running anything
(
Get-FileHash .\TMB_Recomp-<version>.zipin PowerShell, orcertutil -hashfile <zip> SHA256). - Unzip it anywhere with about 10 GB free (into
D:\Games, say: the zip holds aTMB_Recompfolder), but not inProgram Files(the launcher writes the game next to itself). Keep its folders together:TMB Launcher.exe,source\(our code),kit\(the compiler and tools that build the game),README.txt(these steps, English and French),LICENSES\. - Double-click TMB Launcher.exe. The launcher is not signed, so Windows may say "Windows protected your PC": click More info, then Run anyway.
- Click Select your disc image… and pick your image of the USA disc (
SCUS-97101): a.iso, or a.binwith its.cue. The launcher checks it at once (the European disc and other games are refused with a message saying so). - Click Prepare the game. It takes a few minutes, once (about 3.5 minutes on an 8-core PC); you can stop it and it continues where it left off next time.
- Click Play. In the game, F2 opens the settings (controls, video, cheats). Your memory cards, settings and
logs are in
%APPDATA%\TMB_Recomp(the launcher's Maintenance tab opens it).
The launcher says when a newer release is out (it asks GitHub for the latest version number at start; Settings
turns that off; nothing is downloaded by itself). Its Update button downloads the new release's zip, checks it against the
release's SHA-256, installs it over this one and restarts (tools/apply_update.py); unzipping the new release where the old
one was (replacing the files) does the same by hand. The launcher then sees the code changed since the game was built and offers Rebuild now (also Rebuild in the Maintenance
tab); only what changed is built again, the files the new release no longer has are removed, and the saves stay. To uninstall, delete the folder (and %APPDATA%\TMB_Recomp for the saves
and settings). Releases are Windows only for now; on Linux the port builds from source (below), but has not been
tested there since the native work of November 2026.
Status
Picking the work up elsewhere: docs/HANDOFF.md (state, how to measure, next steps).
Playable, at full speed, as a native port, at NTSC's 60 frames per second. The game boots, runs its own IOP modules, plays the movies with their sound, goes through the menus and level loading, and plays matches and story levels: driving, weapons, bots, HUD, music and effects, deaths, respawns, saving and loading. Natively (the default, at twice the PS2's resolution) a match holds 57.7 to 60.4 fps (4 players split screen: 55.8 to 60.4) on a Ryzen 7 7800X3D with an RTX 5070 Ti. Every Challenge arena has been played; still being checked: story levels 2 to 8 to the endings, and some effects against the PS2 picture (issues found while playing are fixed as they come: docs/RECOMP_NOTES.md).
The USA release, native (the default), drawn at twice the PS2's resolution.
| Milestone | |
|---|---|
| Map and recompile the main ELF (3,572 functions, symbols intact) | ✅ |
| Link the generated code with the PS2Recomp runtime on Linux and Windows | ✅ |
| Boot: EE kernel, the game's own IOP modules, CD reads from the disc files | ✅ |
| USA release (SCUS-97101, NTSC 60 Hz) | ✅ |
Controller input; every button and stick direction rebindable to keys, controllers and the mouse (F2, input.ini) | ✅ |
| Movies (PSS, MPEG-2 decoded with FFmpeg) | ✅ |
| Title screen and main menu | ✅ |
Memory card: save, load, overwrite (mc0, mc1 in %APPDATA%\TMB_Recomp) | ✅ |
| Story mode: level 1 of 8 cleared (Junkyard, Midtown Suburbs), progress saved and loaded | ✅ |
| Menus, level select, level loading | ✅ |
| First match: HUD, cars, textured 3D, bots | ✅ |
| Deaths, respawns, game over and replay | ✅ |
| Full speed (60 fps): native renderer, VU1 microcode as native code | ✅ |
| Frames paced on the PC's clock, not on the PS2's vblanks | ✅ |
| Sound and music (989SND, movie audio) | ✅ |
| Every IOP module recompiled (sound driver, pad, memory card...), sound mixed on the PC's clock (not tied to the frame rate) | ✅ |
| The match's 3D drawn from the game's own data (translated VU1 programs) | ✅ |
| Every VU1 program translated (menus, 3D, beams, chrome and water, particles, flares, trails, heat haze, lightning): no VU1 sandbox | ✅ |
No GS emulation: no GS front end, no GS memory, no GS packet read; the native renderer draws and presents the frame (runtime/native/native_display.h for the display's field and mode) | ✅ |
Multi-player split screen, 2 to 4 players (a multitap on port 0: players 1-4 on their own controllers; PS2X_PAD2_SCRIPT to PS2X_PAD4_SCRIPT for scripted players) | ✅ |
| Every Challenge arena played (all of them; the ones a fresh save keeps locked through the Cheats tab's Unlock everything) | ✅ |
| Settings window (F2): controls, video (resolution, windowed / fullscreen / borderless, render scale, frame rate) and cheats, all switched while playing | ✅ |
| Frame rates above 60 for high-refresh monitors (the Video tab's frame rate: the frames between the game's 60 drawn interpolated, one game frame of latency; experimental: rain, snow, lens flares and some trails still move at 60) | 🧪 |
| Every story level to the endings (level 1 of 8 checked) | 🔧 |
Higher internal resolution (2x by default; the Video tab's render scale, PS2X_RENDER_SCALE) | ✅ |
| Widescreen, frame rates above the PS2's | ⏳ |
Native port (no PS2 hardware emulation left, not even a small part: the shared goal of the fork's ports). Native means the game's draws built straight from its own data (vertices, matrices, materials), with no GIF packet, no GS state machine and no copy of GS memory in between.
| Layer | |
|---|---|
IOP modules (989SND, LIBSD, PADMAN, SIO2MAN, MTAPMAN, MCMAN, MCSERV, USBD, LGDEV) recompiled to C++, none interpreted (0 interpreted instructions after boot in [iop:hw]) | ✅ |
No IOP hardware: no SIO2, no IOP interrupt controller, DMA or timer registers modelled; the modules reach the IOP through their imports only ([iop:hw] hardware absent) | ✅ |
Sound and IOP on the PC's clock (PS2X_SPU2_WALLCLOCK=1, PS2X_IOP_WALLCLOCK=1) | ✅ |
No SPU2 emulation: 989SND's libsd calls played by the fork's native mixer (PS2X_NATIVE_AUDIO=1), the SPU2 absent | ✅ |
| No paraLLEl-GS and no GS back end: the native renderer draws with OpenGL on the GPU | ✅ |
Frame loop on the PC's clock (FramePacer; TMB_NATIVE_FRAMES=0: the vblanks) | ✅ |
| Every VU1 program translated to C++ (3D, skinning, effects), no VU interpreter and no sandbox | ✅ |
Every VU0 program translated to C++ (the scene traversal's, bit-exact with VU0, and the 2D engine's), no VIF0 packet read, no VU0 interpreter (runtime/tmb_vu0.cpp) | ✅ |
| Textures from an offline native pack, looked up by the game's own texture ids | ✅ |
| Font, HUD, loading bar, overlays' darkening and movies drawn natively from the game's variables (no GIF packet built or read) | ✅ |
3D meshes from an offline native pack (tools/tmb_mesh_pack.py, through the fork's generic pack writer and runtime: PS2Recomp/docs/native_packs.md) drawn at the game's per-object hook from its own state, in the game's own order, clipped to its microprograms' clip volume; no VIF stream read | ✅ |
Effects (particles, lens flares, rain...) drawn from the game's particle lists (runtime/tmb_particles.cpp); no VIF stream read | ✅ |
| IOP kernel layer (threads, semaphores, interrupts, timers, SIF RPC, cdvdman) answered natively at its API, under the recompiled modules (the software layer, not hardware) | ✅ |
No GS front end, no copy of GS memory, no VIF stream read: the native renderer draws and presents the whole frame (TMB_GS_FRONTEND=1 and the diagnostic switches bring the GS front end back, headless) | ✅ |
✅ works · 🔧 in progress · ⏳ not reached yet
docs/RECOMP_NOTES.md records how the port is brought up, every fix and how it was found, and what remains open.
Goal
A fully playable Twisted Metal: Black as a true PC program, from the intro to the endings: no PS2 hardware simulated where the PC can do the job itself. Accuracy first; then widescreen and frame rates above the PS2's.
How it works
The port started as a static recompilation on a reimplementation of the PS2 hardware, and is becoming a native PC program piece by piece. Each piece below replaces a part of the PS2 that was simulated:
| Part of the game | On the PS2 | In the port |
|---|---|---|
| Frame timing | the vblank interrupt; the game counts the fields a frame took from a hardware timer | each frame paced on the PC's clock (the fork's FramePacer) and shown as soon as it ends, its fields counted from the real time it took (TMB_NATIVE_FRAMES=0: the vblanks) |
| Game code (EE, MIPS R5900) | the EE CPU | 3,572 functions recompiled to C++ (generated_us/), with the ELF's own symbols |
| IOP modules (989SND, LIBSD, PADMAN, SIO2MAN, MTAPMAN, MCMAN, MCSERV, USBD, LGDEV) | R3000A code on the IOP | all recompiled to C++ by the fork's tools/iop_recomp.py (scripts/iop_recomp.sh), switch cases and frameless leaf functions included; none left interpreted ([iop:hw]: interpreted instructions 0) |
| IOP hardware (interrupt controller, DMA channels, root counters, SIO2, CDVD and SIF registers) | devices behind the IOP's registers | none: the IOP kernel (threads, semaphores, events, intrman, timrman, vblank), SIF (sifman, sifcmd, RPC) and cdvdman are answered natively at their API; pad and memory card are the EE's libpad / libmc HLE; the registers a module's start code still touches (LIBSD's bus setup, USBD's probe, SIO2MAN's first write: about 1000 accesses at boot, none after) are plain storage without effect (PS2X_IOP_HARDWARE=0, TMB's default; TMB_IOP_HARDWARE=1 brings the models back) |
| Sound mixing (SPU2) | the SPU2 chip, at the IOP's pace | no SPU2: libsd's exports (the recompiled 989SND's calls: voices, banks, effects, the movie stream) taken over by the fork's native mixer (ps2xIOP/src/native_audio, PS2X_NATIVE_AUDIO=1, TMB's default): 48 voices playing the game's ADPCM banks from a native sound memory, ADSR, reverb and the looping sound data input (movie audio), mixed at 48 kHz when the PC's audio device asks; the SPU2 emulation is absent (no sound RAM, no registers, no DMA, never scheduled; TMB_NATIVE_AUDIO=0 brings it back) |
| 3D (hier renderer: terrain, buildings, cars, characters) | VU1 microprograms, then the GS | every object kind (rigid, pool-less, sprites, ribbons, skinned, environment-mapped and morphing objects) drawn from a native mesh pack the NGP files' prebuilt packets are converted into offline (tools/tmb_mesh_pack.py, which only parses the NGP files and writes through the fork's tools/native_pack; the pack's loader, object store, view order, GS state and clipping are the fork's runtime/native/native_mesh_pack.h / native_mesh_draw.h, shared with the other ports), at the game's per-object hook (pktAddVu1ObjAsm) from its own state (runtime/tmb_meshes.cpp), submitted in the game's own order (opaque in traversal order, alpha by pktLinkAlphaObjAsm's depth buckets) at each view's kick, each triangle clipped to the strip programs' own clip volume (VU 13-16 x VU 0-3, as their CLIP test does); no VIF stream is read and VU1 runs nothing (TMB_NATIVE_VIF=1: the old stream reading, for comparison) |
| Skinned characters, sprites, rain, fog, blending | VU1, GS | translated into the native renderer's terms (bones, fog factor, blend modes) |
| Effects (particles, lens flares, streaks, trails, heat haze, lightning, beams, chrome and water) | VU1 | drawn from the game's own particle lists and flare list (runtime/tmb_particles.cpp), the effects programs' work done natively; the per-program translations of the stream (runtime/tmb_native.cpp, checked against VU1's own output with TMB_NATIVE_CHECK) remain for the comparison modes |
| Textures | GS memory | converted once, offline, from the disc's .TEX / .RTX files into a native pack in the build tree (tools/tmb_texture_pack.py, run by scripts/recomp.sh; the pack format, loader and PS2 texel decoders are the fork's native_texture_pack.h); at runtime looked up by the game's own keys, caught in its texture code (each texture's id as texmSetupTexture sets it up, each palette's VRAM block as texmLoadResTexture loads it): nothing read from texture records, GS packets or GS memory while the game runs (runtime/tmb_textures.cpp) |
| Font, HUD, loading bar, overlays, movies | GS, IPU | drawn natively from the game's variables: each packet builder rewritten in C++ (runtime/tmb_2d.cpp), nothing built or read as a GS packet; movies decoded with FFmpeg and shown as the native renderer's backdrop |
| Everything else sent to the GS | GS | nothing: no GS front end is constructed (ps2x::native::setGsFrontEndAbsent), no GS memory allocated; the DMA kicks to the GIF, VIF1 and VIF0 complete unread, the libgraph calls send nothing, the game's display registers writes are dropped and its reads of CSR get the field from the native display (runtime/native/native_display.h: vblanks on the frame pacer's clock); the native renderer presents its own frame (circuit-2 picture, movie backdrop, 3D, 2D) |
| Display (vblank, CSR.FIELD, display mode) | GS CRTC | native: the field from the EE scheduler's vblank count, the mode from sceGsResetGraph / SetGsCrt, GsPutIMR kept for GsGetIMR (no GS interrupt exists) |
disc files (game_us/) ──► ps2_analyzer ──► recomp/tmb_scus97101.toml (+ recomp/tmb_manual.toml)
│
ps2_recomp + the ELF's own symbol table ──► C++ (one file per function, generated_us/)
the disc's IOP modules, loaded offline ──► iop_recomp.py ──► C++ (generated_us/iop/)
│
CMakeLists.txt ──► PS2Recomp runtime + generated code + runtime/ (TMB's overrides, native adapter)
│
native renderer · texture cache · native sound mixer on the PC clock ──────┼─► ps2EntryRunner
native display (field, mode) · window presentation on the GPU, at 2x (no GS front end; diagnostics: the GS native back end)
what is left of the PS2: EE kernel HLE · DMAC (kicks to the GIF / VIF complete unread) · IPU · IOP kernel layer
- The USA executable's addresses. The build is
game_us/->generated_us/->build/game_us;runtime/'s hooks use the USA executable's addresses and its generated function names directly, andrecomp/tmb_manual.tomlholds the recompiler's hand-written settings by symbol name. NTSC draws a 640 x 224 field, which the native renderer follows. - The executable ships its full symbol table (3,572 named functions,
.mdebug), so no Ghidra pass is needed:ps2_analyzerreads the symbols, recognises the Sony SDK functions that have runtime handlers and writes the config. - PS2Recomp turns every function into C++ and emits the address-indexed function table.
runtime/tmb_meshes.cpp(3D at the game's hooks),runtime/tmb_particles.cpp(effects from the particle lists),runtime/tmb_2d.cpp(2D builders),runtime/tmb_textures.cpp,runtime/tmb_vu0.cppdraw the game natively;runtime/tmb_native.cppsets the native switches, publishes the frame, and keeps the old VIF1-stream adapter for the comparison modes (TMB_NATIVE_VIF=1). The generic parts live in the fork (ps2xRuntime/src/lib/native/: renderer, texture cache, GIF translation;docs/native.md), shared with the other ports.runtime/tmb_overrides.cppholds the other game-specific bindings.
The PS2Recomp fork lives in PS2Recomp/ (git submodule, TheBozos/PS2Recomp),
shared with the other recompilations built on it (Valkyrie Profile 2, Drakengard, The Lord of the Rings: The Third
Age, Jade Cocoon 2, Soulcalibur III and others). Fixes made for this port land there.
Requirements
Players need none of this: the release's launcher and build kit bring everything ("For players" above). To build from the repository:
- Windows 10/11 with MSYS2 (UCRT64, see below), or Linux (developed on Arch at first; not tested since November 2026)
- CMake 3.21+, Ninja, GCC 13+ or Clang with C++20
- FFmpeg development libraries (
libavcodec,libavutil,libswscale) andpkg-config(or a static FFmpeg folder:PS2X_FFMPEG_ROOT) - X11/Wayland and OpenGL development headers (raylib is fetched and built by the fork)
- A CPU with AVX2 (the runtime is built for
x86-64-v3) - Your own copy of
SCUS-97101(check the hash above)
Build and run
One command, from your disc (Windows)
tools/tmb_build.py goes from your own disc image to a game folder that runs, with nothing derived from the game
ever downloaded: it checks the disc (the USA release, SCUS-97101: SCUS_971.01's SHA-1; the European disc and other
images are refused with a message saying so), extracts it with its own ISO9660 reader (.iso, .bin / .cue; no
bsdtar), recompiles the game's code and VU1 microcode, converts the textures and models into their packs, recompiles
the IOP modules st