Static recompilation of Super Mario Bros. from the ROM into C and
WebAssembly. The repository contains no game code or data: the recompiler
reads your own copy of the ROM, and maps/smb.map gives its routines, tables
and variables this project's own names. The page recompiles the ROM in the
browser and shows the generated C next to the game.
- D-Pad: WASD
- B: K
- A: L
- start: Enter
- select: Space
- z: Save state
- x: Load state
- Static translation of the disassembly to low-level C
- PPU & APU emulation layers
- Convert subroutines to C functions
- Convert most gotos to if statements
- Remove unused flag updates
- Replace PPU with direct draw calls (display state written directly, GPU renderer)
- Emit instructions as C instead of instruction-emulation helpers
- Infer routine interfaces (registers and flags read and produced)
- Recompile from the ROM with our own names (no disassembly in the repository)
- An IR shared by a C and a WebAssembly backend, with optimisation passes
- Recompile in the browser, with a code viewer
- Synthesise sound on the audio thread, band-limited, from per-frame register writes
- Registers as C locals and parameters
- Manually rewrite portions of the code to higher level C
See toward a modern C program for how these steps are done and checked.
- Fetch the submodules:
$ git submodule update --init --recursive-
Build raylib, follow the instructions here
-
Install MoonBit (see Codegen), place a legally obtained dump of SMB called
smb.nesin the root folder, then recompile it and build:
$ make codegen
$ make build- Run
./smbfrom the root folder (it loadsweb/video.frag, the GPU renderer's shader;./smb --softwareuses the software renderer;--debug-chrdraws with generated placeholder tiles)
The page recompiles your ROM in the browser: the recompiler (MoonBit compiled to JavaScript) turns it into a WebAssembly module and C, which a code viewer shows next to the game (SHOW CODE; the map tab is editable and recompiles).
- Install a recent
clangwith thewasm32target and MoonBit (see Codegen) - Run
make web - Serve the
web/folder over HTTP and openindex.html - Load a legally obtained dump of SMB, an iNES file or a raw dump of the cartridge; it stays in your browser. An unrecognised dump (by SHA-256) still loads, with a warning
DEBUG TILES draws with generated placeholder tiles instead of the ROM's, and
?software in the URL selects the software renderer instead of WebGL2.
- Install Moonbit:
$ curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash -s '0.10.9+6e6c44045'- Place your
smb.nesin the root folder and runmake codegen; the C goes tocodegen/game/(not in the repository). A raw dump of the cartridge (no iNES header) works too:make codegen ROM=smb.bin
The game is recompiled from the ROM image and maps/smb.map, which gives the
routines, tables and variables this project's own names (see
recompiling from the ROM).
The generator recovers reachable instructions and callable regions from the
vectors, builds an IR, optimises it and emits structured C and, with
--wasm, WebAssembly. The hand-written display routines
(codegen/native/display.c) use codegen/native/smb.h, the map's names as
a C header (make header, no ROM needed). See the
analysis and migration notes and
toward a modern C program.
Run python3 tests/check_codegen.py for the ROM-free suite (as CI does):
generator tests, and the independent NROM fixtures (from assembly and from an
iNES image) through both backends, natively and in WebAssembly. With your
smb.nes, append --rom to also recompile the game, replay the recorded
7,987 frames natively against tests/golden/warpless.trace (debug and ROM
tiles), run the differential test of every generated routine
(tests/check_routines.py) and replay the WebAssembly build
(tests/web/replay_split.mjs). These checks do not require Raylib.
The GPU renderers can be compared with the software renderer pixel for pixel:
make gpu-check && xvfb-run ./gpu_check --debug-chr (desktop) and
NODE_PATH=$(npm root -g) node tests/web/gpu_check.cjs (WebGL2 in headless
Chromium, needs Playwright).
Calls are plain C calls. Return addresses are not kept on the emulated stack, except by the inline-table dispatchers, which read their table through it.
To compile another compatible assembly input into a separate directory:
moon run src/main -- --input tests/nrom128.asm --output /tmp/nrom-codegen- doppelganger's disassembly, which this project was originally built from
- SuperMarioBros-C by MitchellSternke
- Nesdev Wiki
- nessy
- An Overview of NES Rendering by Austin Morlan
