Static recompilation of Super Mario Bros., Metroid, Super Mario Bros. 3,
Bomberman, Tetris, The Legend of Zelda, The Chessmaster and Kirby's
Adventure 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 the
maps in maps/ (smb.map, metroid.map, smb3.map, ...) give 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
- A second game: Metroid (MMC1 banks, waits for the interrupt, return-address tricks)
- A third game: Super Mario Bros. 3 (MMC3: two bank windows, CHR banks, frames split by the scanline IRQ)
- Five more: Bomberman, Tetris, Zelda, The Chessmaster and Kirby's Adventure (restarts, code in RAM, coroutines, preemption)
- 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, recompiling Metroid for the second game, recompiling Super Mario Bros. 3 for the third and five more games for the others.
- 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)
For Metroid, place a legally obtained dump called metroid.nes in the root
folder and add GAME=metroid: make codegen GAME=metroid recompiles it into
codegen/metroid/ and make build GAME=metroid builds ./metroid (it saves
states to metroid.sav). Likewise for Super Mario Bros. 3 with smb3.nes
and GAME=smb3 (codegen/smb3/, ./smb3), and for bomberman.nes,
tetris.nes, zelda.nes, chessmaster.nes and kirby.nes with
GAME=bomberman, tetris, zelda, chessmaster and kirby.
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 one of the games, an iNES file (or for SMB a raw dump of the cartridge); it stays in your browser. The page recognises the game by the SHA-256 of its PRG and CHR; another NROM dump is recompiled as SMB, another MMC1 dump as Metroid and another MMC3 dump as SMB3, with a warning. Loading another ROM switches games
DEBUG TILES draws SMB with generated placeholder tiles instead of the ROM's,
and ?software in the URL selects the software renderer instead of WebGL2.
THEME draws the frames differently (WebGL2; ORIGINAL is the game's own picture), in every game:
- 2D GLOBAL ILLUMINATION: fire, lava, coins and question blocks light the scene, with soft shadows and a bounce, by radiance cascades; daylight stays as in the game;
- 2.5D RAY TRACING: every pixel becomes voxels, seen from slightly above, with sun and fire shadows traced through them;
- 3D: the level and its characters as shapes made from the ROM's own tiles and level data (textured cubes, pipes as cylinders, hills, clouds and sprites inflated from their silhouettes), through a perspective camera. No model is drawn by hand: nothing of the game is in the repository.
In SMB the themes know what is solid from the game's own level data (its block buffer, by the map's names), not from the picture. In Metroid and SMB3 the background is solid, the sprites stand in front of it, and the light comes from open space. See themes.
- Install Moonbit:
$ curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash -s '0.10.17+d971670cd'- 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). With your metroid.nes, --metroid
recompiles Metroid and replays rec/metroid.rec (4,601 frames) natively and
in WebAssembly against tests/golden/metroid.trace, which an independent
6502 interpreter (tests/interpreter.c) wrote and is checked to reproduce;
with your smb3.nes, --smb3 does the same for SMB3 over rec/smb3.rec
(3,201 frames, tests/golden/smb3.trace), and --bomberman, --tetris,
--zelda, --chessmaster and --kirby for the others (rec/NAME.rec,
tests/golden/NAME.trace). Each also compares the page's WebGL renderer
with the software renderer and draws the themes
(tests/web/cartridge_gpu.cjs, Playwright). These checks do not require
Raylib, except SMB3's desktop GPU check.
The GPU renderers can be compared with the software renderer pixel for pixel:
make gpu-check && xvfb-run ./gpu_check --debug-chr (desktop; for Metroid
make gpu-check GAME=metroid && xvfb-run ./gpu_check --every 25 --variations,
for SMB3 GAME=smb3) and
NODE_PATH=$(npm root -g) node tests/web/gpu_check.cjs (WebGL2 in headless
Chromium, needs Playwright). tests/web/themes.cjs draws frames of the
replay with every theme and fails on WebGL errors; with a query and a
directory it saves them, e.g. 'frames=7800&rom=1' /tmp/themes.
In SMB, 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. Metroid reads and drops return addresses, so its calls push them as the 6502 does (see return addresses).
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

