Mutant Rampage: Bodyslam static recomp
Mutant Rampage: Bodyslam static recompilation is a native Windows port of the Philips CD-i game Mutant Rampage: Bodyslam with HD upscalers and 60 fps sprite interpolation.
- Type: PC port
- Game: Mutant Rampage: Bodyslam
- Runs on Windows
- By tools81
- Latest release v1.0.0, 2026-10-21
- Source: https://github.com/tools81/mutant-rampage-bodyslam-static-recomp
README
Mutant Rampage: Bodyslam — CD-i static recompilation
A native Windows port of Mutant Rampage: Bodyslam (Philips CD-i, 1995). The game's 68000 code is translated ahead of time into C. A runtime provides the parts of the CD-i the code expects: the CD-RTOS (OS-9) kernel, the CD file manager, the video decoder (with a Direct3D 11 presenter at any resolution), the ADPCM audio processor, and the Digital Video (FMV) cartridge, whose MPEG decoder plays the game's music and animated cutscenes.
It is built from the CD-i static recompilation template (extracted from the Link: The Faces of Evil port).
Building
Requirements: Windows 10+, Visual Studio 2022 (C compiler), CMake 3.19+ and Python 3.8+.
- Put the disc image (
.bin+.cue, raw 2352-byte sectors) indata/(rom/also works). - Run
build.bat. It extracts the program modules from the disc, recompiles them intogen/, builds with MSVC and copies two executables to the project root:bodyslam.exe: the original presentation;bodyslam_hd.exe: the same game with the Cartoon HD / HD+ upscalers and 60 fps sprite interpolation.
Both run the same translated code and share save states. The executables find the image in data/ or rom/ next to itself or the
working directory (up to four levels up), or takes the .cue/.bin path as an
argument. Saves (NVRAM) and the log go to saves/ next to the executable.
Packaging for players (Windows)
package.bat (it runs packaging\windows\package.ps1) builds the game if
needed and writes two packages to dist\:
| File | What it is |
|---|---|
MutantRampageBodyslam-<version>-win64-portable.zip | The game folder: unzip anywhere and run. The game asks for the disc image on first start. |
MutantRampageBodyslam-<version>-win64-setup.exe | A single-file installer (Inno Setup 6). It asks for the disc image, checks it, and can copy it into the game folder. |
Options: -Build (rebuild first), -Version 1.2.3 (default: version in
game.json), -NoZip, -NoInstaller, -Iscc <path to ISCC.exe>. The
installer needs Inno Setup 6 (winget install JRSoftware.InnoSetup); without
it only the zip is made.
The package contains bodyslam.exe, bodyslam_hd.exe, a player README.txt
(packaging\README.txt), THIRD_PARTY_NOTICES.txt and an empty data\
folder. The packer refuses to include disc images, and it checks that the
programs do not need the Visual C++ runtime: they link the C runtime
statically and otherwise use only DLLs that ship with Windows 10/11
(Direct3D 11, the D3D shader compiler, winmm, comdlg32).
Portable by design. The programs write only to saves\ next to
themselves: NVRAM, settings, quicksaves and the log. Nothing goes to AppData
or the registry, so no --portable switch is needed. The disc image path is
read from bodyslam.ini next to the programs ([paths] / disc=). A path
inside the game folder is stored relative to it, so the folder can be moved
or copied to another PC. If the folder is not writable the game says so at
start-up.
The installer installs per user without administrator rights, by default
to %USERPROFILE%\Games\Mutant Rampage Bodyslam, and refuses Program Files
(the game must be able to write next to itself). Its wizard has a disc image
page: the file must be a raw 2352-byte-sector CD-i image whose disc label
boots cdi_loader; a .cue is resolved to its .bin. Leaving it empty is
allowed after a confirmation, and the game then asks on first start. The
installer writes bodyslam.ini and creates Start menu shortcuts (original,
HD, Change disc image (--choose-disc), read me, game folder, uninstall)
and, optionally, desktop shortcuts. Uninstalling removes the programs and any
disc copy made by Setup, and asks before deleting saves\. Unattended:
MutantRampageBodyslam-1.0.0-win64-setup.exe /VERYSILENT /DIR="D:\Games\Bodyslam" /DISC="D:\CD-i\Bodyslam.cue" [/COPYDISC=1]
Finding the disc image, in order: a path on the command line, disc= in
bodyslam.ini, a rom\ or data\ folder next to the program or the working
directory, then a file dialog (whose choice is saved to bodyslam.ini).
--choose-disc shows the dialog even when an image is configured.
Playing
| Input | Keyboard | Gamepad (XInput) |
|---|---|---|
| Move / menu | Arrow keys (Shift = faster) | D-pad or left stick |
| Button 1 | Z, Space, A | A |
| Button 2 | X, Ctrl, S | B, Y |
| Both buttons | C, Enter | X, Start |
| Fast-forward | Tab (hold) | |
| Quick save / next slot / quick load | F2 / F3 / F4 | |
| Invincibility on/off | F5 | |
| Sprite interpolation on/off (HD) | F6 | |
| Run-ahead: off, 1, 2 fields | F7 | |
| Aspect / filter / fullscreen | F9 / F10 / F11 (or Alt+Enter) | |
| Quit | Esc |
Changes are confirmed with a short message in the top-left corner of the
picture. Options can also be given on the command line: --aspect N,
--filter N, --runahead N, --smooth 0|1, --size WxH, --fullscreen,
--windowed, --gdi. The choices made with the keys are remembered in
saves/settings.ini (saves/settings_hd.ini for the HD build). Invincibility
always starts off.
Enhancements
Aspect modes (F9): 4:3 with blurred sides (the picture fills the height; a blurred, dimmed copy fills the rest of a wide screen), 4:3 with black bars, stretch to fill, and zoom to fill width (crops the top and bottom on a wide screen).
Filters (F10): sharp (crisp pixels without shimmer at any scale), smooth (bicubic), CRT (scanlines and aperture grille), nearest pixel. The HD build adds:
- Cartoon HD: edge-directed reconstruction in the style of xBR (level 2), computed per output pixel on the game's real 384-pixel grid. Stair-stepped outlines of the hand-drawn art become smooth lines while detail inside them stays sharp.
- Cartoon HD+ (the HD default): the same after a de-dithering pass that replaces checkerboard and stripe dithers, and lone speckle pixels, with the blend they stand for (smoke, shadows, gradients).
Quicksave (F2/F3/F4): ten slots. F2 saves the whole machine to the
current slot, F3 selects the next slot, F4 loads it. Slots are also written to
saves/quicksave_NN.state and can be loaded in a later session. Save states are
not available during the Philips logo at start-up (it runs as a separate
process).
Invincibility (F5): the hero's strength never drops. It is a hook on the
game's damage routine (set_current_strength), so strength bars and
everything else behave normally.
Run-ahead (F7, default 1 field): after each field the machine is saved, the next field is emulated with the same input and shown, and the save is restored. Your input is visible one field (17 ms) sooner with no other change: the game's timeline is identical with run-ahead on or off. Two fields remove more lag at twice the CPU cost.
Sprite interpolation (HD build, F6): the game redraws at about 16 frames per second. Between its frames the HD build moves every sprite, shadow and the scrolling playfield along their motion, so movement is shown at the full 60 fields per second. Changes of pose still happen at the game's rate. The picture is shown up to one game frame (about 3 fields) later; run-ahead offsets part of that.
None of these change the game itself: the original and HD executables, with run-ahead and interpolation on or off, produce byte-identical game audio for the same input.
What is recompiled
game.json lists the three program modules on the disc:
| Module | Base | Notes |
|---|---|---|
cdi_loader | 0x100000 | Boot program (from the disc label). Loads the game and F$Chains to it. |
cdi_bodyslam | 0x104000 | The game (MPW build, A5 jump table, MacsBug symbols). |
cdi_bumper | 0xE0000 | Philips Interactive Media logo, run as a child process. |
All three are fully translated; the analyzer finds every reachable function
without extra_entries. The only code that runs in the built-in interpreter
is what the game loads into RAM at run time and calls through function tables
(first seen from setup_new_city_menu). That is expected, and the log notes
the first time it happens.
Changes to the template runtime for this game
- F$Chain (process replacement), with per-module A5 tracking.
- Digital Video cartridge (
runtime/fmv.c):/mvand/madrivers, MPEG sectors fed from CDFM PCLs, MPEG-1 video shown as external video behind the UCM planes (ICM bit 18), MPEG-1 Layer II audio mixed with the ADPCM output, and the decoder event signals the game waits on. Decoding uses pl_mpeg (MIT), vendored inruntime/third_party/. - Soundmaps decode from the SM_Info descriptor, which the game repoints at its sound effects.
- The disc file table holds 1024 files (this disc has 82).
- Run loop: video fields end at the top of the run loop with no translated
code on the native stack, and F$Sleep unwinds to the run loop instead of
waiting inside the system call. That makes the whole machine snapshottable at
every field (
runtime/state.c, a serializer per subsystem, including the MPEG decoder state). - NVRAM is kept in memory and mirrored to
saves/nvr_*; only real (non-run-ahead) execution writes the files. - Hooks:
game.jsonlists instruction addresses per module before which the generated code callsgame_hook()(invincibility and interpolation capture).runtime/frontend.chandles fields, run-ahead, save slots and messages;runtime/motion.cis the interpolator. --frames Nruns also writesaves/ramdump.bin;tools/analyze.pylistings annotate call targets (including A5 jump-table calls) with function names.
The function codes and behaviour worked out along the way are in
docs/CDI_NOTES.md.
Verification
For the enhancements:
- Run-ahead off, 1 and 2, the interpreter, and the HD build with interpolation all give byte-identical audio over the same fuzzed sessions.
- With run-ahead 1, every presented frame equals the next field of a run without run-ahead (679 of 679 compared).
- Quickload, in the same session and from disk in a new session, continues exactly as the game did after the save (99 of 99 frames identical).
- Invincibility: over the same 20 000 fuzzed fields the heroes ended at strengths 63, 0, 0 without it and 100, 100, 100 with it (57 hits blocked).
- In a fight the HD build changes the picture on almost every field instead of every 3–4 fields, and it keeps 60 fps at 1920x1080 with Cartoon HD+, interpolation and run-ahead on.
For the port:
- A recompiled run and a pure-interpreter run (
--interp) of the same 4000 fields of fuzzed play produce identical frames and identical audio. - Three 30 000-field (about 8 minutes each) fuzz runs reach several stages, menus, cutscenes and music tracks without errors or unimplemented calls.
alu_testpasses all 12 600 vectors.insn_testpasses 754 of 755; the failure is a random test that writes a word to the odd address0x7FFFFFFF(an address error on real hardware, never done by the game).
Debugging workflow
-
Find what fails. Unimplemented OS calls and SetStat/GetStat codes are logged to
saves/recomp.log. Fatal errors also show a 68k register dump and a stack backtrace with function names. -
Debug options:
--trace/--trace3log syscalls with callers, signals, disc and FMV activity.--headless --frames N --dump Kruns fast without a window and writes every Kth frame as a BMP (andramdump.binat the end).--script "field:key:dur,..."scripts input (keys1 2 L R U D).--fuzz SEEDdrives the game with random input.--wav out.wavrecords the audio.- Script keys
Q N W I M Atrigger F2, F3, F4, F5, F6 and F7. --turboruns a windowed session unpaced;--gpushot N:file.bmpsaves the presented image (GPU output, at window size) at field N.--statetestsaves and restores the machine every field;--statehashlogs a hash of each snapshot section per field (compare two runs to find state that diverges).
-
Check the translation. Run the same headless session twice, once with
--interp, and compare the frame dumps and WAVs. Headless runs are deterministic: emulated time is counted in instructions, and the clock uses a fixed epoch. Any difference points to a translation bug. -
Unit-level checks.
python tools/gen_tests.pyandpython test/gen_alu_vectors.pygenerate test sources. Then build theinsn_testandalu_testtargets:cmake --build build\cmake --config Release --target insn_test alu_test build\cmake\Release\insn_test.exe "data\Mutant Rampage - Bodyslam (USA).bin" 200 build\cmake\Release\alu_test.exe -
Read the code.
python tools/analyze.py [module index]writes a disassembly with function names tobuild/listing_<module>.asm.
Layout
| Path | Contents |
|---|---|
tools/cdi_extract.py | Green Book filesystem reader. Writes program files and _app.txt (--all also dumps the real-time streams). |
tools/m68k.py | MC68000 decoder |
tools/os9mod.py | OS-9 module header, idata and irefs |
tools/analyze.py | Code discovery (recursive descent plus common compiler patterns) |
tools/gen.py | 68000→C generator, flag liveness, dispatch tables, gen/game_config.h |
tools/config.py | game.json loader |
tools/gen_tests.py | Per-instruction differential tests |
tools/bmp2png.py | Converts frame dumps to PNG |
runtime/cpu.c | Dispatcher and run loop |
runtime/interp.c | 68000 interpreter, for RAM code and the --interp reference mode |
runtime/alu.c | Shifts, division and BCD with exact flags |
runtime/os9.c | OS-9 kernel: memory, processes (fork/wait/chain/exit), signals, alarms, paths, NVRAM, CSD |
runtime/cdfm.c | CD file manager: reads, and real-time SS_Play with PCB/PCL/CIL |
runtime/video.c | UCM display control (FCT/LCT), MCD212-style renderer, cursor, pointer |
runtime/audio.c | ADPCM levels A/B/C, sound maps with gapless SM_Out, attenuation, final mix |
runtime/fmv.c | Digital Video cartridge: MPEG video and audio drivers |
runtime/state.c | Machine snapshots (run-ahead, quicksave) |
runtime/frontend.c | Field boundary: presentation, run-ahead, save slots, hotkeys, messages, game hooks |
runtime/motion.c | Sprite interpolation (HD build) |
runtime/render_d3d11.c | GPU presenter: aspect modes; sharp, smooth, CRT, nearest and (HD) Cartoon HD/HD+ filters |
runtime/host_win32.c | Window, input (keyboard and XInput), waveOut, pacing, scripted and fuzz input |
test/ | Instruction differential tester and ALU reference vectors |
docs/CDI_NOTES.md | CD-i facts and API codes worked out during the ports |
package.bat, packaging/windows/package.ps1 | Builds the portable zip and the installer into dist\ |
packaging/windows/bodyslam.iss | Inno Setup installer script (disc image page, shortcuts, uninstall) |
packaging/README.txt, packaging/THIRD_PARTY_NOTICES.txt | Player read-me and licence notices shipped with the game |
Known limitations
-
Sprite interpolation moves whole sprites; it cannot invent in-between poses, and objects that jump more than 64 pixels in one game frame (cuts) are not interpolated. Screen shakes are shown as the game draws them.
-
Run-ahead roughly halves the speed headroom (about 2x real time at 1 field on the test PC).
-
The MPEG audio event
$04(enabled by the game, purpose unknown) is never generated; the game only switches it off when it arrives. -
MPEG picture placement follows MV_Pos/MV_Window as inferred from this game; it may be off by a line or two compared with real hardware.
-
Only NTSC timing (525 lines, 60 Hz) is emulated, which is what the US disc selects.