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.

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+.

  1. Put the disc image (.bin + .cue, raw 2352-byte sectors) in data/ (rom/ also works).
  2. Run build.bat. It extracts the program modules from the disc, recompiles them into gen/, 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\:

FileWhat it is
MutantRampageBodyslam-<version>-win64-portable.zipThe game folder: unzip anywhere and run. The game asks for the disc image on first start.
MutantRampageBodyslam-<version>-win64-setup.exeA 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

InputKeyboardGamepad (XInput)
Move / menuArrow keys (Shift = faster)D-pad or left stick
Button 1Z, Space, AA
Button 2X, Ctrl, SB, Y
Both buttonsC, EnterX, Start
Fast-forwardTab (hold)
Quick save / next slot / quick loadF2 / F3 / F4
Invincibility on/offF5
Sprite interpolation on/off (HD)F6
Run-ahead: off, 1, 2 fieldsF7
Aspect / filter / fullscreenF9 / F10 / F11 (or Alt+Enter)
QuitEsc

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:

ModuleBaseNotes
cdi_loader0x100000Boot program (from the disc label). Loads the game and F$Chains to it.
cdi_bodyslam0x104000The game (MPW build, A5 jump table, MacsBug symbols).
cdi_bumper0xE0000Philips 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): /mv and /ma drivers, 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 in runtime/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.json lists instruction addresses per module before which the generated code calls game_hook() (invincibility and interpolation capture). runtime/frontend.c handles fields, run-ahead, save slots and messages; runtime/motion.c is the interpolator.
  • --frames N runs also write saves/ramdump.bin; tools/analyze.py listings 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_test passes all 12 600 vectors. insn_test passes 754 of 755; the failure is a random test that writes a word to the odd address 0x7FFFFFFF (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 / --trace3 log syscalls with callers, signals, disc and FMV activity.
    • --headless --frames N --dump K runs fast without a window and writes every Kth frame as a BMP (and ramdump.bin at the end).
    • --script "field:key:dur,..." scripts input (keys 1 2 L R U D).
    • --fuzz SEED drives the game with random input.
    • --wav out.wav records the audio.
    • Script keys Q N W I M A trigger F2, F3, F4, F5, F6 and F7.
    • --turbo runs a windowed session unpaced; --gpushot N:file.bmp saves the presented image (GPU output, at window size) at field N.
    • --statetest saves and restores the machine every field; --statehash logs 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.py and python test/gen_alu_vectors.py generate test sources. Then build the insn_test and alu_test targets:

    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 to build/listing_<module>.asm.

Layout

PathContents
tools/cdi_extract.pyGreen Book filesystem reader. Writes program files and _app.txt (--all also dumps the real-time streams).
tools/m68k.pyMC68000 decoder
tools/os9mod.pyOS-9 module header, idata and irefs
tools/analyze.pyCode discovery (recursive descent plus common compiler patterns)
tools/gen.py68000→C generator, flag liveness, dispatch tables, gen/game_config.h
tools/config.pygame.json loader
tools/gen_tests.pyPer-instruction differential tests
tools/bmp2png.pyConverts frame dumps to PNG
runtime/cpu.cDispatcher and run loop
runtime/interp.c68000 interpreter, for RAM code and the --interp reference mode
runtime/alu.cShifts, division and BCD with exact flags
runtime/os9.cOS-9 kernel: memory, processes (fork/wait/chain/exit), signals, alarms, paths, NVRAM, CSD
runtime/cdfm.cCD file manager: reads, and real-time SS_Play with PCB/PCL/CIL
runtime/video.cUCM display control (FCT/LCT), MCD212-style renderer, cursor, pointer
runtime/audio.cADPCM levels A/B/C, sound maps with gapless SM_Out, attenuation, final mix
runtime/fmv.cDigital Video cartridge: MPEG video and audio drivers
runtime/state.cMachine snapshots (run-ahead, quicksave)
runtime/frontend.cField boundary: presentation, run-ahead, save slots, hotkeys, messages, game hooks
runtime/motion.cSprite interpolation (HD build)
runtime/render_d3d11.cGPU presenter: aspect modes; sharp, smooth, CRT, nearest and (HD) Cartoon HD/HD+ filters
runtime/host_win32.cWindow, input (keyboard and XInput), waveOut, pacing, scripted and fuzz input
test/Instruction differential tester and ALU reference vectors
docs/CDI_NOTES.mdCD-i facts and API codes worked out during the ports
package.bat, packaging/windows/package.ps1Builds the portable zip and the installer into dist\
packaging/windows/bodyslam.issInno Setup installer script (disc image page, shortcuts, uninstall)
packaging/README.txt, packaging/THIRD_PARTY_NOTICES.txtPlayer 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.