Skip to content

About

Twisted Metal: Black (PAL, SCES-50360) static recompilation with PS2Recomp: configs, scripts, notes (no game data)

Resources

Stars

4 stars

Watchers

0 watching

Forks

Latest commit

 

History

130 Commits

Folders and files

Repository files navigation

TMB_Recomp

A native Linux and Windows port of Twisted Metal: Black (USA SCUS-97101 and PAL SCES-50360): 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). Still emulated, and being removed: the GS front end, which interprets the GS packets of the menus, HUD and 2D before drawing them with OpenGL (the game's DIRECT packets in a match are drawn natively from their GIF data).

Platform Compiler Based on Status

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 default, the more complete one: the characters' story cinematics after choosing a character; the European disc's are about a tenth of their size), or the European one, SCES-50360. The port is built against their executables:

Release File SHA-1
USA SCUS_971.01 3ff17f4897552e355d0bb8c3abfa5125fe3ff2ef
Europe SCES_503.60 d097dd85167b6a188bf5ec17ecfc53a1f275a581

Status

Picking the work up elsewhere: docs/HANDOFF.md (state, how to measure, next steps).

Playable, at full speed, as a native port: the USA release at NTSC's 60 frames per second, the European one at PAL's 50. The game boots, runs its own IOP modules, shows the PAL language selection (Europe), 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. With the native path (scripts/run_native.sh, at twice the PS2's resolution) a match runs at 58.9 fps on average on the USA release (10th percentile 58.5) and 49.4 on the European one (10th percentile 49.3), on a Ryzen 7 7800X3D with an RTX 5070 Ti. Still being checked: every arena and story level to the endings, and some effects against the PS2 picture (issues found while playing are fixed as they come: docs/RECOMP_NOTES.md).

A match at night: the player's truck with its headlights on, bots ahead The player's car in the rain, the HUD at twice the PS2's resolution The intro movie The game-mode menu

The USA release on the native path (scripts/run_native.sh), 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) alongside the European one (SCES-50360), one runtime for both ✅
Language selection screen (PAL) ✅
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 the disc folder) ✅
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 USA, 50 fps Europe): 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 (still: the GS front end takes the native 3D into its render targets and presents the frame; the effects' register state comes from their GIF packets) ⏳
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 arena and story level to the endings 🔧
Higher internal resolution (PS2X_GS_SCALE, 2x by default) ✅
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 ✅
Sound and IOP on the PC's clock (PS2X_SPU2_WALLCLOCK=1, PS2X_IOP_WALLCLOCK=1) ✅
No paraLLEl-GS: OpenGL on the GPU (the GS native back end) ✅
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 ✅
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) drawn at the game's per-object hook from its own state; today still read from the VIF stream the game sends 🔧
Effects (particles, flares, rain...) drawn from the game's particle lists; today read from the VIF stream ⏳
No GS front end, no copy of GS memory, no VIF stream read: the native renderer draws and presents the whole frame ⏳

✅ 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/), 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); none left interpreted
Sound mixing (SPU2) the SPU2 chip, at the IOP's pace mixed when the PC's audio device asks (PS2X_SPU2_WALLCLOCK=1), with the IOP on the PC's clock (PS2X_IOP_WALLCLOCK=1): the sound no longer slows down with the frame rate
3D (hier renderer: terrain, buildings, cars, characters) VU1 microprograms, then the GS today: read from the VIF stream the game sends (a model of VU1 memory) and drawn by the native renderer with the translated programs (runtime/tmb_native.cpp, TMB_NATIVE=1); VU1 does not run them. Being replaced: the NGP files' prebuilt packets converted offline into a native mesh pack (tools/tmb_mesh_pack.py), drawn at the game's per-object hook (pktAddVu1Obj) from its own state
Skinned characters, sprites, rain, fog, blending VU1, GS translated into the native renderer's terms (bones, fog factor, blend modes)
Effects VU1 programs (beams, chrome and water, particles, flares, streaks, trails, heat haze, lightning) VU1 one native function per program, each derived from VU1 recordings and checked against VU1's own output (TMB_NATIVE_CHECK); a program not translated is not run at all (reported once)
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); 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 still emulated in part: the fork's GS front end takes the remaining packets (the effects' register state) and the native 3D into its render targets and presents the frame (OpenGL, no paraLLEl-GS)
disc files (game_us/, game/) ──► ps2_analyzer ──► recomp/tmb_scus97101.toml, tmb_sces50360.toml (+ recomp/tmb_manual.toml)
                                           │
                     ps2_recomp + the ELF's own symbol table ──► C++ (one file per function, generated/)
   989SND, LIBSD as the IOP loads them ──► iop_recomp.py ──► C++ (generated/iop/)
                                           │
                 CMakeLists.txt ──► PS2Recomp runtime + generated code + runtime/ (TMB's overrides, native adapter)
                                           │
   native renderer · texture cache · SPU2 mixer on the PC clock ──────────────┼─► ps2EntryRunner
   GS native back end (the GS stream as OpenGL draws) · window presentation on the GPU, at 2x
   what is left of the PS2: EE kernel HLE · DMAC · GIF / VIF packet handling · IPU · IOP kernel and other modules
  • Both regions, one runtime. TMB_REGION=us (the default) or pal picks the disc folder (game_us/, game/), the recompiler config, the generated code (generated_us/, generated/) and the build (build/game_us, build/game). Everything hand-written is shared: runtime/ is written against the PAL executable and marks the addresses it uses (TMB_ADDR, TMB_GP); tools/tmb_region_map.py maps them to the region's executable by symbol (a file's statics through the code that uses them) into tmb_region.h, and recomp/tmb_manual.toml holds the recompiler's hand-written settings by symbol name. The two executables differ little (the US one is five months older); NTSC draws a 640 x 224 field instead of 640 x 256, which the native renderer follows.
  • Both executables ship their full symbol table (3,572 named functions, .mdebug), so no Ghidra pass is needed: ps2_analyzer reads 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_native.cpp is TMB's native adapter: it reads every VIF1 stream the game sends and draws it natively, and tells VU1 which programs to skip. 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.cpp holds the other game-specific bindings.

The PS2Recomp fork lives in PS2Recomp/ (git submodule, boubou666/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

  • Linux (developed on Arch) or Windows 10/11 with MSYS2 (UCRT64, see below)
  • CMake 3.21+, Ninja, GCC 13+ or Clang with C++20
  • FFmpeg development libraries (libavcodec, libavformat, libavutil, libswresample, libswscale) and pkg-config
  • X11/Wayland and OpenGL development headers (raylib is fetched and built by the fork)
  • bsdtar (libarchive) to extract the disc
  • A CPU with AVX2 (the runtime is built for x86-64-v3)
  • Your own copy of SCUS-97101 or SCES-50360 (check the hash above)

Build and run

git clone --recurse-submodules https://github.com/boubou666/TMB_Recomp.git
cd TMB_Recomp
scripts/extract_iso.sh "/path/to/Twisted Metal - Black (USA).iso"      # disc files -> game_us/ (TMB_REGION=pal: the European disc -> game/)
scripts/build_tools.sh                                                  # ps2_recomp, ps2_analyzer, ps2_vu_aot -> build/tools
scripts/recomp.sh                                                       # ELF + recomp/tmb_scus97101.toml -> generated_us/ (VU1 microcode -> generated_us/vu1/)
scripts/build_runtime.sh                                                # runtime + generated code -> build/game_us
scripts/iop_recomp.sh                                                   # optional: boots once to recompile the IOP modules -> generated_us/iop/, then rebuild
scripts/run_native.sh                                                   # the native port (native renderer, recompiled sound driver, sound on the PC clock)
scripts/run.sh                                                          # the PS2 path only (VU1 and GS simulated), for comparison
scripts/pgo.sh                                                          # optional, experimental: profile-guided rebuild (~200 s of scripted play)

Windows

The same scripts run in an MSYS2 UCRT64 shell, with MSYS2's GCC (MSVC is not supported for this port).

pacman -S --needed mingw-w64-ucrt-x86_64-{gcc,cmake,ninja,pkgconf,ffmpeg,libarchive,python} git rsync
git clone --recurse-submodules https://github.com/boubou666/TMB_Recomp.git

Then follow the steps above. scripts/stage_dlls.sh copies the DLLs the runner needs next to ps2EntryRunner.exe, so it can also be started outside the MSYS2 shell. The native port draws with OpenGL 3.3; the PS2 path (scripts/run.sh) draws the GS with paraLLEl-GS, which needs a Vulkan driver (any recent NVIDIA, AMD or Intel driver ships one).

scripts/regress.sh <build-dir> <out-dir> is the fork's port regression run for TMB: boot movies, menus and a Skyscrapers match under the bench lock, with a summary.txt (route reached, exit, fps mean and p10, frames over 25 ms, errors) and the last frame.

scripts/analyze.sh regenerates the TOML from scratch; it discards manual edits, so diff before committing. The runner build compiles about 70 MB of generated C++ and takes a while the first time.

Run the game with the extracted disc folder as cdrom0: (that is what scripts/run.sh does):

build/game_us/PS2Recomp/ps2xRuntime/ps2EntryRunner game_us/SCUS_971.01

Player 1 plays with the keyboard and the first controller, player 2 with the second controller. The default keys (US key positions):

PS2 button Keyboard
D-pad Arrow keys or W A S D
Cross / Circle / Square / Triangle X or Space / C or Esc / Z or Numpad 0 / V or Numpad 1
L1 / R1 / L2 / R2 Q / E / Left Shift / Right Shift
Start / Select Enter / Tab
L3 / R3 F / G
Left stick / right stick the movement keys (with the D-pad) / I J K L
Key What it does
F1 Runtime debugger
F2 Controls: rebind every PS2 button and stick direction, per player, to keys, controller buttons, sticks and triggers, mouse buttons, the wheel or mouse movement (sensitivity, invert Y, cursor capture). Saved to input.ini in the disc folder (game_us/input.ini; PS2X_INPUT_CONFIG=<file> for another place).
Runtime and diagnostic options
Variable Effect
TMB_NATIVE=1 The native port: the native renderer, the GS native back end at 2x, the recompiled IOP modules, the SPU2 and IOP on the PC's clock (scripts/run_native.sh sets it; each part has its own switch below)
TMB_NATIVE_FRAMES=0 Frames on the emulated vblanks instead of the PC clock
PS2X_GS_SCALE=<n> With TMB_NATIVE=1: the internal resolution, n times the PS2's each way (1 to 4, default 2)
PS2X_WINDOW_SIZE=<w>x<h> The window's size when it opens
TMB_NATIVE_VU1=1 VU1 runs the translated programs too (the GS frame then has everything, to compare)
TMB_NATIVE_VU1_PCS=<pc,...> Diagnostic: leave those setups on VU1 (a PS2X_VU1_RECORD reference for a translation)
TMB_NATIVE_CHECK=<n> Diagnostic: run VU1's own program beside each translation (an interpreter, off in play) and log where they differ
PS2X_NATIVE_VIEW=only Show the native frame alone, without the GS one under it
PS2X_NATIVE_INTERPOLATE=1, PS2X_PRESENT_FPS=<n> Draw frames between the game's 50 (objects interpolated), presented at up to n per second
PS2X_IOP_NATIVE=1 Run the recompiled sound driver (scripts/iop_recomp.sh generates it)
PS2X_SPU2_WALLCLOCK=1, PS2X_IOP_WALLCLOCK=1 Mix the sound when the audio device asks, and run the IOP on the PC's clock
PS2X_CAPTURE_DIR=<dir>, PS2X_CAPTURE_INTERVAL=<s> Save the presented frame every few seconds (works minimized)
PS2X_PAD_SCRIPT=t:BUTTON[/ms],… Scripted button presses (seconds from start), for unattended runs
PS2X_VU1_RECORD=<file> Record VU1 microprogram runs for ps2_vu1_replay / build/game/tmb_vu1_replay (with the compiled microcode; the default fast VU1 path differs in cycle counts only, TMB_VU1_EXACT=1 scripts/recomp.sh for the exact one)
PS2X_VU_AOT=0 Interpret the VU1 microcode instead of running the compiled one
TMB_VBLANK_HOLD=<ms> With TMB_NATIVE_FRAMES=0: hold the vblank up to while a match frame finishes (default 6; 0 off); TMB_VBLANK_HOLD_AB=1 compares on/off in one run
TMB_ENEMY_HEALTH_CAP=<x> Testing: hold the opponents' health at most x (a scripted run can win its matches)
TMB_KILL_ENEMIES=<s> Testing: every seconds every opponent alive takes a killing hit through the game's own damage function (the kill counts; scripted story runs)
TMB_AB=<hold|slice|lag|holdlimit|fill> In-run A/B of an experiment, on and off every 100 vblanks, real-time fps and game speed per mode
TMB_PLAYER_HEALTH_CAP=<x> Testing: hold the player car's health at most x (game over within minutes of play)
TMB_PLAYER_GOD=1 Testing: the player car's health goes back to its highest value every frame (story walkthroughs)
PS2X_VU_LIFTED=0 Run the compiled microcode instead of the loops lifted to native code (the rain, runtime/vu1)
PS2X_VU1_RECORD_PC=<pc> With PS2X_VU1_RECORD: only the microprogram runs starting at that pc
TMB_MAX_FRAME_MS=<ms> Longest frame weapons and debris move over (default 40; slow motion below 25 fps)
TMB_SUBSTEPS=<n> Run the game update once per vblank field, up to n times a frame, so cars do not go through the ground below 50 fps (default 4, 1 disables)
TMB_TRACE_FRAMES=1 Every 100 frames: frame time split, EE idle and VIF1/GS waits, CPU ms per frame of each host thread
TMB_TRACE_TIMELINE=<file> 4 s of the match as a timeline (game events, game/vif1/gs thread CPU every 0.2 ms) for tools/timeline.py
PS2X_IOP_THREAD=1 Run the IOP on its own thread (experimental, slower in TMB today)
PS2X_SHOW_FPS=0 Hide the frame rate counter at the top right (on by default; F3 toggles it while playing)
PS2X_GS_RENDERER=parallel / cpu The emulated GS instead of the GS native back end: paraLLEl-GS (Vulkan; the default without TMB_NATIVE=1) or the CPU rasterizer
TMB_TRACE_POS=<file> Every vehicle's position at each collision pass (scripts/bots.sh and tools/falls.py count the falls)
TMB_TRACE_BLACKBOX=<file> Dump the player vehicle's last 64 frames when its position goes wild
TMB_GS_SERIAL=1 Rasterize on the GS thread alone (default: 4 rasterization workers, PS2X_GS_RASTER_THREADS=<n>)
TMB_VIF1_SERIAL=1 Run VIF1 and the VU1 microcode on the game thread (default: their own thread)
TMB_GS_LAG=1 Let the GS and VU1 finish a frame while the game runs the next (one frame of extra input latency, no measurable gain; off by default)
PS2X_GS_RECORD=<file>, PS2X_GS_RECORD_AT=<s>, PS2X_GS_RECORD_SECONDS=<s> Record the GS command stream for ps2_gs_replay (rasterizer timing, VRAM hash)
PS2X_GS_DUMP=<dir>, PS2X_GS_DUMP_AFTER=<s>, PS2X_GS_DUMP_COUNT=<n> Log the next GS primitives with their state, decoded textures and VRAM
PS2X_GS_SYNC=1 Rasterize on the game thread instead of the GS worker threads
PS2X_GS_LOG_LIMIT=<n> Raise the cap on the [gs:gif]/[gs:reg] diagnostic logs
TMB_ISO=<path> Disc image for raw sceCdRead calls (default: the .iso above the disc folder that boots the executable)
TMB_TRACE_MOVIE=1 Log the movie player's thread teardown, audio state and MPEG HLE summary
TMB_TRACE_THREADS=<s> Every few seconds: guest threads, MPEG HLE state, GS display registers (TMB_TRACE_IOP=1 adds IOP threads)
PS2X_IOP_TRACE_RPC=1 Log IOP RPC dispatch and blocking calls

Disc facts

Boot ELF SCUS_971.01 (USA, May 2001) / SCES_503.60 (Europe, October 2001), entry 0x100008, .text at 0x100000 (1.2 MB), not stripped
Video mode NTSC (USA), PAL (Europe)
IOP modules MOD/*.IRX: 989SND, LGDEV, LIBSD, MCMAN, MCSERV, MTAPMAN, PADMAN, SIO2MAN, SNDBGET, USBD, USBKEYBD, USBMLOAD
IOP image IOPRP213.IMG
Data LVL/, PCAR/, ACAR/, PTS/, RST/, SND/, VAG/, SHELL/ (per-language UI), MOV/*.PSS

Repository layout

Path Contents
recomp/ Recompiler config (analyzer output plus manual additions)
runtime/ Game override module (game-specific bindings)
scripts/ Extract, build tools, analyze, recomp, build runner, run
tools/ Analysis helpers (iso_lbn.py: disc file of a sector; gs_fb_decode.py: framebuffer out of a VRAM dump; irx_disasm.py: IOP module code)
docs/ Bring-up notes
PS2Recomp/ PS2Recomp fork (submodule)

Everything derived from the game (game_us/, game/, generated_us/, generated/, build/, disc images, captures) is git-ignored. Never commit or redistribute it.

Credits

  • PS2Recomp by ran-j and contributors (GPL), the recompiler and runtime this port is built on.
  • The Valkyrie Profile 2: Silmeria and Drakengard recompilations, developed on the same PS2Recomp fork; runtime fixes are shared.
  • Twisted Metal: Black © Sony Interactive Entertainment, developed by Incognito Entertainment. This is an unofficial fan project, not affiliated with or endorsed by them.

About

Twisted Metal: Black (PAL, SCES-50360) static recompilation with PS2Recomp: configs, scripts, notes (no game data)

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages