Mortal Kombat Trilogy Recompiled (N64)
Mortal Kombat Trilogy
OrigenNintendo 64
TecnologíaRecompilación Estática (C/C++ nativo)
Disponible paraWindows, Linux
Ver proyecto oficialSobre el proyecto
Mortal Kombat Trilogy brings together characters and stages from the first three Mortal Kombat games.
2 versiones de Mortal Kombat Trilogy
Estás viendo: Mortal Kombat Trilogy Recompiled (N64)
Información oficial del proyecto
Mortal Kombat Trilogy N64 → PC (static recompilation)
Project for decompiling and statically recompiling Mortal Kombat Trilogy (USA) (Rev 2) for the N64 into a native Windows program, using splat (disassembly/decomp), N64Recomp/RSPRecomp (recompilation), and N64ModernRuntime + RT64 (runtime/renderer).
No game data is included. This repository contains no ROM and nothing generated from one. You need your own copy of the game: Mortal Kombat Trilogy (USA) (Rev 2), big-endian
.z64, SHA-111f2488b21f9f7185c25905dd02c2955c35ad5c1. Setup generates everything else from it on your machine.
Quick start (Windows)
- Download or clone this repository.
- Put your ROM in the folder, named exactly
Mortal Kombat Trilogy (USA) (Rev 2).z64. - Double-click
Setup.batand approve the admin prompts. The first run installs the build tools, generates the code from your ROM and builds the game; it takes a while. - Play:
build\MKTRecompiled.exe.
Status
Playable. Builds and runs on Windows (one-click Setup.bat) and Linux.
Verified headless here (Linux, software Vulkan): boot → "1 controller" notice → main menu → story crawl → title → attract-mode demo fights (multiple stages and fighters) → 1-player game: character select, Choose Your Destiny tower, rounds with real player input, CPU opponent, combos, timer, health and Aggressor meters. Full speed (60 fps) with music and sound effects. A 6-minute random-input soak test runs without crashing.
Phase 1: extraction and recompilation setup. Done.
- ROM identified and verified (USA Rev 2, big-endian, CIC-6102, libultra 2.0D). See
docs/ROM_MAP.md. - Full memory map recovered from the game's own overlay tables: the main segment plus 46 overlays (24 contain code, 22 are data-only).
- ROM split with splat: 3,891 functions disassembled into
asm/(with a linker script), ready for matching decompilation intosrc/. - 185 libultra/libc/audio functions named in
symbol_addrs.txt. - CPU code recompiled with N64Recomp: 3,777 functions as C in
RecompiledFuncs/. Of the rest, 105 are libultra functions the runtime replaces and 9 are hand-written replacements (below). - Audio microcode (n_audio aspMain) recompiled with RSPRecomp into
rsp/aspMain.cpp.
Phase 2: playable executable. Done (Linux-verified).
- Host app (
src/main/): SDL2 window, RT64 renderer, SDL audio with resampling, keyboard plus up to 4 gamepads mapped to N64 controllers. It finds the ROM on first run and boots straight into the game. - Graphics: MKT's microcode is an exact hash match for RT64's "F3D SDK 2.0E", so RT64 runs it natively.
- Game-specific engine work (
src/game/), all explained in "How it runs" below. - Two RT64 fixes (
patches/rt64-mkt-fixes.patch, applied automatically by setup).
Phase 3: Windows build. Done. Setup.bat installs everything and builds build\MKTRecompiled.exe with no manual steps. Verified on Windows 11 (clang-cl, Direct3D 12): boots, walks the menus into fights with stage/fighter overlays loading, steady 60 fps with audio, no crashes in a 2-minute scripted run.
Phase 4: settings menu. Done. In-game settings overlay (video, audio, control rebinding, Infinite Health for P1), with the game paused underneath.
Next: a release package (zip with the exe and DLLs that builds the game files from the user's ROM).
Credits: N64Recomp and N64ModernRuntime (Wiseguy and contributors), RT64 (Dario and contributors), splat, SDL2, Dear ImGui; fonts Cinzel and Oswald (SIL OFL 1.1). Mortal Kombat Trilogy is a trademark of its owners; this project is not affiliated with them.
How it runs (what had to be solved)
| Problem | Fix |
|---|---|
| Game threads busy-wait on memory that other threads/interrupts change. The runtime only switches game threads at OS calls, so these loops hung. | Hand-written replacements that yield inside the loop (src/game/patches.c): frame sync func_8008140C, frame-slot claim func_80081230, blocking DMA func_80082EE0, frame waits func_8006DF44 / func_80014A48. tools/find_spinloops.py scans the disassembly for more. |
Midway's cooperative "process" system (about 112 processes, each with its own stack) switches with hand-written assembly that jumps through a saved $ra, which static recompilation can't express. | src/game/midway_procs.cpp: each process runs on its own host fiber (Windows Fibers / POSIX ucontext). The save / swap / restore / exec primitives (func_80080964, func_800809F8, func_8008099C, func_80080A84) are reimplemented natively, with identical semantics, including exec and retargeting a sleeping process. |
| Overlays (stage/mode code) are DMA'd in 0x6000-byte chunks, so the runtime never saw them load. | The patched DMA routine tells the runtime which code sections now live where, and unloads any section it overwrites (mkt_overlay_dma_hook). |
| Two stage overlays (A20, A21) start with data tables, with code at the end. | The splat generator finds each overlay's real code range. |
A few tiny functions (jr $ra; nop stubs called only through pointers) were merged by the disassembler into the function before them, so they were never recompiled ("Failed to find function", seen after ~49 min of attract mode). | Named in symbol_addrs.txt; tools/find_hidden_funcs.py scans the disassembly for any more. |
| Game threads keep polling memory in the yield loops above after the app starts quitting, while the runtime frees game memory (crash on exit). | mkt_yield parks the thread once the runtime is exiting. |
| Audio microcode dispatches through a jump table. | Its 16 command handlers are read from the ROM into aspMain.us.toml. |
| The game submits empty audio tasks, and computes negative buffer lengths if too much audio is queued. | Empty tasks complete immediately; the reported queue depth is capped at one frame. |
| Audio clicked constantly: the game sizes each audio frame from the reported queue depth, which kept only ~16 ms queued, so the device ran dry about once a second. | The depth is reported relative to a 50 ms target (MKT_AUDIO_LATENCY_MS overrides), so ~30–80 ms stays queued; no underruns after startup on Windows or Linux. |
Constant crackle behind all audio: the game calls osAiSetNextBuffer for a buffer whose RSP task hasn't run yet (on the console the AI reads the buffer later, when it starts playing it), so the host copied what the buffer held three frames earlier, cut to the new length. A jump at nearly every frame boundary. | A buffer is held until the audio task that renders it reports done (aspMain_notify → mkt::audio::audio_task_done). Frame-boundary jumps went from ~10× the in-frame level to none. The recompiled ucode was checked bit-exact against the cxd4 RSP interpreter (-DMKT_CXD4_DIR=<mupen64plus-rsp-cxd4>, MKT_AUDIO_RSP=compare). |
| RT64: stale draw state after a frame ends (texrect crash), and clear rectangles larger than small render targets. | patches/rt64-mkt-fixes.patch. |
Running
build\MKTRecompiled.exe (Windows)
./build/MKTRecompiled (Linux)
Options: --rom <path> (first run, if the ROM isn't next to the exe), --fullscreen, --developer (RT64 debug UI). Alt+Enter / F11 toggles fullscreen.
Controls (player 1, defaults). Keyboard: arrows = D-pad, WASD = stick, X = A, C = B, Z = Z, Enter = Start, Q/E = L/R, I/J/K/L = C-up/left/down/right. Gamepad: D-pad, left stick, A→A, X→B, B→C-down, Y→C-left, LB→L, RB→R, LT→Z, RT→C-up, right stick→C-buttons, Start. MKT confirms menus with Start; punches and kicks are on the C-buttons and can be rebound in the game's own Controller Setup. Players 2–4 use further gamepads.
Settings menu
Press Esc or F1 (keyboard), or Back/View (gamepad), at any time. The game pauses while the menu is open.
| Tab | Options |
|---|---|
| Gameplay | Infinite Health – Player 1, Unlock Hidden Characters, One-Button Fatalities (each on/off), Resume, Quit |
| Video | Window mode (windowed/fullscreen), resolution (native 240p, the default / 2x / match window HD), anti-aliasing (off / 2x / 4x MSAA) |
| Audio | Master volume |
| Controls | Rebind every N64 input and the two fatality buttons for keyboard and gamepad (Enter/A, then press the new key/button; Backspace clears a key; bindings swap if already used), reset to defaults |
Navigate with arrows / D-pad / left stick, switch tabs with Q/E or LB/RB, confirm with Enter/A, back with Esc/B. Settings are saved immediately to settings.ini in the app data folder (%APPDATA%\MKTRecompiled on Windows, ~/.local/share/MKTRecompiled on Linux).
Rendering resolution: MKT draws every sprite and background as narrow vertical strips. Above native 240p, real GPUs leave one-pixel seams between strips (thin vertical lines that follow the fighters), so native is the default and the finished frame is scaled up sharply. Settings saved by older builds as resolution=window are moved to native.
Infinite Health refills P1's health every frame, so P1 can't be knocked out. Two side effects: rounds P1 wins count as Flawless Victories, and if neither fighter takes damage the round can still be lost when time runs out.
Unlock Hidden Characters makes Human Smoke and Khameleon selectable on the character select screen (row 2, the two slots between Scorpion and Reptile, normally shown as dragon logos). It sets the same two flags the game's own unlock (Ultimate Kombat Kode 113-840) sets, D_80257658 and D_80257656, and keeps them set because the game clears them on every reset.
One-Button Fatalities: during "Finish Him", Fatality 1 (default V / left-stick click) or Fatality 2 (B / right-stick click) enters Player 1's fighter's fatality input for you. "1" and "2" follow the game's own numbering (its two fatality routines, func_8001A148 / func_8001A1A0), which doesn't always match printed move lists. The game still checks everything it normally does, including the distance (a notice says "get closer" or "step back" when you're outside it). Khameleon has no fatality of her own. How it works: tools/gen_fatalities.py reads each character's fatality inputs from the game's move tables into src/game/fatalities.inc, and a hook N64Recomp inserts into the game's input routine plays the sequence on Player 1's switch word, one input every few game ticks, mirrored for the side you face. It holds Run+Block where a move needs it, presses hold-and-release buttons while blocking so no punch lands, and backdates the game's hold timers.
Debug environment variables: MKT_DEBUG=1 (per-second stats, overlay loads), MKT_TEST_MENU=1 (scripted settings-menu key presses), MKT_RAMDUMP=<dir> (RDRAM snapshot every second), MKT_DEBUG_INPUT=1 (log the game's pad words), MKT_AUTOPRESS=start,none,cu,f1,... (f1/f2 = fatality buttons) with MKT_AUTOPRESS_HOLD=<polls> (scripted input for unattended tests), MKT_DEBUG_P2_WEAK=1 and MKT_DEBUG_FATALITY_PLACE=1 (fatality tests: one hit wins a round; Player 1 is moved into the move's distance window), MKT_FATALITY_TIMING=tap,gap (sequence timing in game ticks), MKT_AUDIO_DUMP=<path> (write the game's audio to <path>.raw + batch sizes to <path>.txt). tools/headless_test.sh runs the game on a virtual display and captures screenshots, video and audio.
Layout
| Path | What |
|---|---|
Mortal Kombat Trilogy (USA) (Rev 2).z64 | your original ROM (never modified) |
baserom.us.z64 | working copy, created by setup |
src/game/fatalities.inc | fatality inputs read from the game's move tables by tools/gen_fatalities.py (generated from the ROM by setup; not in the repo) |
mkt.us.yaml | splat config (generated by tools/gen_splat_yaml.py from the ROM by setup; not in the repo) |
symbol_addrs.txt | hand-maintained function/data names. Add names here as you identify them |
asm/ | splat disassembly (main + overlays) (generated from the ROM by setup; not in the repo) |
mkt.us.ld | linker script for a matching decomp build (generated from the ROM by setup; not in the repo) |
mkt.us.syms.toml | N64Recomp symbol metadata (generated by tools/gen_recomp_syms.py from the ROM by setup; not in the repo) |
mkt.us.toml | N64Recomp config |
aspMain.us.toml | RSPRecomp config (audio microcode) |
RecompiledFuncs/ | N64Recomp output (C) (generated from the ROM by setup; not in the repo) |
rsp/aspMain.cpp | RSPRecomp output (generated from the ROM by setup; not in the repo) |
src/game/ | game glue: registration, overlay tracking, Midway process fibers, spin-loop patches, libultra shims |
src/main/ | host app: window, RT64 renderer, audio, input, settings + in-game settings menu (menu.cpp, Dear ImGui via RT64's present hook) |
assets_ui/fonts/ | menu fonts (Cinzel, Oswald; SIL OFL 1.1), embedded by tools/embed_fonts.py |
patches/rt64-mkt-fixes.patch | RT64 fixes, applied by setup |
CMakeLists.txt | builds MKTRecompiled (the game), MKTGame lib, mkt_linkcheck |
tools/ | generators, setup and rebuild scripts |
docs/ROM_MAP.md | ROM facts, segment and overlay map |
Windows setup (one time)
Double-click Setup.bat. It installs whatever is missing (Git, Python, Visual Studio 2022 Build Tools with the C++/clang/CMake components: approve the admin prompts), loads the VS build environment itself, verifies the ROM, clones N64ModernRuntime, N64Recomp and RT64 at pinned commits (applying the RT64 patch), builds N64Recomp.exe/RSPRecomp.exe, installs splat, generates the code from the ROM (splat → N64Recomp → RSPRecomp; skipped when it is already up to date) and builds build\MKTRecompiled.exe. Progress goes to setup.log; the final result is in setup_status.txt. Re-running is safe. The first build takes a few minutes.
PlayDebug.batruns the game with diagnostics written togame.log.tools\SelfTest.bat [seconds]runs an unattended smoke test (scripted Start presses into a fight) and writesselftest.log/selftest_status.txt.
Regenerating
After editing symbol_addrs.txt, the generators or mkt.us.toml:
powershell -ExecutionPolicy Bypass -File tools\rebuild.ps1 # Windows
tools/rebuild.sh # Linux/WSL
This re-runs splat → symbol generation → N64Recomp → RSPRecomp, then rebuilds with cmake --build build.
Pinned versions
| Tool | Version |
|---|---|
| N64Recomp / RSPRecomp | ffb39cdad1da5de07eaaa48bd1db4a89a7986771 |
| N64ModernRuntime | cdf5abbd5026fef5c364c676e4667c45e42b6863 |
| splat64 | 0.50.1 (spimdisasm 1.42.4, rabbitizer 1.16.2) |
| RT64 | 23cab603c4f9f4a8b369b38e036f1aa484603878 + patches/rt64-mkt-fixes.patch |
| SDL2 (Windows) | 2.30.3 (downloaded by CMake) |
| n64sym (libultra signatures) | ccf4600f3389f1a84bde23339225cf372fdf7712 |
Things to know / risks
- The game starts on a "only 1 controller attached, press Start" notice. That is the original game's behavior, not a hang.
- More spin-waits may exist in rarely used code paths. A hang with the game thread stuck in one function is the symptom;
tools/find_spinloops.pyplus a patch insrc/game/patches.cis the fix pattern. - Process ra token: suspended Midway processes have a marker value in their saved-
raslot. If some game code reads that slot (for example to ask "what is this process running?"), it will see the marker. __osDisableInt/__osRestoreIntexist in three copies; the extra ones run through the runtime's COP0 emulation.- No save hardware: no EEPROM/SRAM/FlashRAM or Controller Pak code is in the ROM.
- Overlay function names include the ROM offset (
func_803F668C_312E3C) because many overlays share vram.
Don't publish
Keep *.z64, baserom.us.z64, assets/, asm/ and RecompiledFuncs/ out of any public repo. They are derived from the ROM, and .gitignore already excludes them. Users regenerate them from their own ROM with the setup and rebuild scripts.
