Skip to content
mstanPublic

About

NES game static recompiler ecosystem · Part of the R.A.I.D. community

Resources

Stars

124 stars

Watchers

6 watching

Forks

Latest commit

 

History

730 Commits

Folders and files

Repository files navigation

NESRecomp

NESRecomp

A beta static-recompilation framework for NES and Famicom games. NESRecomp translates 6502 code into native C and supplies the PPU, APU, mapper, input and save systems around it. Each port integrates and validates its own cartridge or disk revision.

The ecosystem includes four-player Mario co-op with rollback netplay, adaptive ultrawide views, optional voxel presentation, runtime translations and modern controller support. Features are title-specific, and the framework's beta status does not make every game or mod complete.

Cycle-backend desktop builds with recomp-ui include a save-state browser (F7) and rewind (F8). The browser has twelve thumbnail slots; rewind lets you preview earlier moments and resume from one. Controller shortcuts are Back + L3 and Back + R3. History starts enabled and can be adjusted in the launcher. Local state changes are unavailable during online play.

Play a game · Start a new port without AI

Super Mario Bros.: four players, one adventure

Mario, Luigi, Wario and Waluigi share the same level in the optional Simultaneous Co-op mod. Play with two to four people locally or through rollback netplay; the room settles the shared player count and rules before boot. Team lives, scarce pickups and a shared camera keep it one adventure.

Mario, Luigi, Wario and Waluigi together in World 1-1
Super Mario Bros. — four-player co-op. Local capture of the same game mode available over netplay.

Co-op uses a shared 4:3 camera and excludes widescreen, voxel display and character-replacement mods. See the co-op rules and netplay setup and validation limits.

Ultra-wide NES views

Separately, Mario's experimental renderer offers Fit window, 16:9, 21:9 and 32:9. It extends level terrain, authored enemy loading and independent platforms, with title-specific limits for special spawners and linked objects. Metroid has its own adaptive renderer, showing adjacent room scenery and optional viewport actors. These views preserve the original pixel artwork.

Super Mario Bros. World 1-1 in 32:9, revealing pipes, terrain and enemies beyond the original viewport
Super Mario Bros. — 32:9 single-player view, separate from co-op.
Metroid in 32:9 with Samus, enemies and an item visible across the expanded room
Metroid — experimental 32:9 room view.

Screenshot provenance and feature qualifications are recorded in the showcase sources.

How to use NESRecomp

Use the Windows CLI

  1. Open the Releases page.
  2. Download nesrecomp-cli-windows-x86_64.zip. Do not download GitHub's "Source code" archives.
  3. Extract the zip to its own folder.
  4. Open PowerShell in that folder.
  5. Run:
.\nesrecomp.exe build `
  --rom "C:\Games\MyGame.nes" `
  --output "C:\Projects\MyGameRecomp"

Cartridge projects need no BIOS. Famicom Disk System projects require their own supported disk-system BIOS. The output folder contains generated C source, a proposed game.toml, CMake files, and build scripts.

To check that the generated source compiles:

cd "C:\Projects\MyGameRecomp"
.\build.ps1

This builds a static library. It does not create a playable game by itself. Each game still needs game-specific configuration and runner integration. Use one of the Game Projects as a starting point for a full port. Cycle output uses generated/<name>_cyc*.c. To generate the retained older backend, add --legacy (or --backend legacy). An explicit [game] cycle_accurate = false also selects legacy unless overridden with --backend cycle.

If you already have a game.toml, pass it to the build command:

.\nesrecomp.exe build `
  --rom "C:\Games\MyGame.nes" `
  --game "C:\Projects\MyGamePort\game.toml" `
  --output "C:\Projects\MyGameRecomp"

NESRecomp does not include game ROMs.

Platform Support

Platform Status
Windows (x64, MSVC) Primary / mature
macOS (Apple Silicon + Intel) Experimental — newly added
Other UNIX (Linux) Likely works via the same POSIX path; less tested

macOS support is recent and should be considered experimental. The toolchain (recompiler + runner) builds cleanly with Apple Clang and games run natively — The Legend of Zelda, for example, plays well. Individual titles may show minor behavioral quirks that don't appear on Windows (Super Mario Bros. has some known timing/demo differences); please file an issue if you hit one. The macOS/POSIX port was contributed by Nat Budin (@nbudin) in #10 — thank you! 🙏

Game Projects

These summaries were checked against public documentation and release notes on 21 October 2026. Game/mapper coverage and enabled features vary by build; a title/menu check does not establish a complete playthrough.

Game Project Features and scope
Super Mario Bros. SuperMarioBrosNESRecomp Two-to-four-player local/online co-op; separate adaptive/ultrawide, voxel and donor-ROM character mods.
Duck Hunt DuckHuntNESRecomp Mouse-as-Zapper input and native game presentation.
Dr. Mario DrMarioNesRecomp Playable puzzle port; consult current one/two-player validation notes.
The Legend of Zelda LegendOfZeldaNESRecomp Overworld/dungeon gameplay, battery saves and optional voxel diorama; current cycle build still needs a fresh campaign audit.
Metroid MetroidNESRecomp USA/Europe builds, password saves, adaptive/32:9 room renderer and optional viewport actors/voxel view.
Faxanadu FaxanaduRecomp Action-RPG port and runtime text/localization override showcase.
Yoshi YoshiNESRecomp Native puzzle port; see its release notes for current testing scope.
Yoshi’s Cookie YoshisCookieRecomp MMC3 puzzle port with desktop controls and saves.
Mega Man 3 Megaman3NESRecomp Development preview with title/menu and early-stage gameplay.
Gumshoe GumshoeNESRecomp Mouse-as-Zapper action; end-to-end play reported, with title-specific cosmetic limits.
Super Mario Bros. 2 (Japan / The Lost Levels) SuperMarioBros2JapanFDSRecomp Famicom Disk System development preview; exact disk and BIOS required.
Mother / EarthBound Beginnings Mother1NESRecomp Early standalone bring-up, largely untested beyond initial boot/basic play.

Public game downloads are linked from each project's README. Other local development targets include Super Mario Bros. 2/3 and Kirby's Adventure; they are not advertised here as public releases.

Mapper Support

Public examples exercise NROM (Mario and Duck Hunt), MMC1 (Zelda, Metroid, Dr. Mario, Faxanadu and Yoshi), MMC3 (Yoshi's Cookie and Mega Man 3), GxROM (Gumshoe), and the Famicom Disk System path. A mapper implementation does not automatically qualify every board or game using it; follow the selected port's ROM and region requirements.

Voxel 3D presentation

The Legend of Zelda overworld rendered as an optional voxel diorama
The Legend of Zelda — optional voxel presentation.

NESRecomp includes an opt-in, presentation-only Voxel 3D renderer for game projects that can describe their live tile map and sprites. It reconstructs solid tile geometry, camera-facing sprite cards, depth sorting, contact shadows, HUD preservation, and adjustable pitch, yaw, roll, zoom, and sprite scale. The software scene compositor consumes the normal NES frame and game metadata; it does not patch the player's ROM or change save data.

The Legend of Zelda uses the renderer as an orbitable tabletop diorama, while Super Mario Bros. uses it as a forward-facing first-person experiment. Games opt in through trusted mod packages, so ordinary builds and launches remain on the stock rendering path unless the player enables Voxel 3D. See docs/VOXEL_RENDERER.md for the host API and docs/MOD_PACKAGES.md for package integration.

Text Override System

NESRecomp includes a runtime text replacement system that allows modifying in-game text without editing the ROM. The runner exposes writable PRG ROM bank accessors (runner_get_prg_bank_rw()) that game plugins use to patch string data at load time.

FaxanaduRecomp is the first game to exercise this feature, implementing a JSON-driven override system with:

  • Multiple encoding registries (ASCII, tile-based dialogue fonts)
  • PRG ROM patching for all rendering paths (direct PPU writes, DMA queues)
  • Hot-reload — edit text_overrides.json while the game is running and changes apply within ~1 second

This enables localization, retranslation, and accessibility improvements without ROM hacking. See override_text.h in FaxanaduRecomp for the full API.

Architecture

The cycle backend is the default. It emits CPU-cycle execution against the cycle hardware runtime; see the current host integration. The function-level pipeline and runner files below remain available for explicit legacy builds.

NES ROM (.nes)
    |
    v
nesrecomp.exe build + optional game.toml
    |
    v
generated/<game>_cyc.c       (compiled ROM-view dispatch)
generated/<game>_cyc_b*.c    (6502 instructions expressed as CPU cycles)
    |
    v
Game executable (linked with runner library + SDL2)

Key Components

Component Purpose
recompiler/src/cyc_codegen.c Default cycle-code emitter and generated fallback interpreter
runner/cyc/ Cycle CPU, PPU, APU, mappers, host and enhancement adapters
runner/cyc/project.cmake Default game integration and build-time generation
recompiler/src/code_generator.c 6502-to-C emitter
recompiler/src/function_finder.c Static analysis: discovers functions via BFS from vectors
runner/src/runtime.c NES memory map, PPU register stubs, mapper
runner/src/ppu_renderer.c Background + sprite rendering (8x8 and 8x16)
runner/src/main_runner.c SDL2 window, NMI loop, frame timing
runner/src/debug_server.c TCP debug server with ring buffer

Configuration Format

Game configuration is TOML (game.toml). The legacy plain-text .cfg format has been removed — passing a .cfg path now prints a migration message and exits. All in-tree game projects already use game.toml; see Dr. Mario's game.toml or Faxanadu's for working examples.

Configurable Controls

A keybinds.ini file is auto-generated next to the game executable on first run. Player 1 keyboard bindings and both players' gamepad bindings are configurable. Edit the INI file and restart the game to apply changes.

Keyboard — the [player1] section maps each NES button to an SDL key name. Player 2 uses an explicitly assigned gamepad or netplay peer rather than sharing Player 1's keyboard. The default Player 1 Select key is \; Tab and F1–F12 are reserved runtime hotkeys.

Gamepad — game controllers are supported cross-platform via SDL's SDL_GameController API (Xbox, PlayStation/DualSense, Switch Pro, and generic pads; on Windows this uses XInput under the hood). The first connected pad is NES port 1, the second is port 2, and hotplug is handled — plug in or unplug at any time. Keyboard and gamepad input are merged, so both work simultaneously and no configuration is required to start playing.

[gamepad1] / [gamepad2] sections make the mapping fully editable. Each NES button takes a comma-separated list of SDL controller button names (so multiple physical buttons can drive one NES button). Defaults:

[gamepad1]
a = a,b          ; both right-hand face buttons act as NES A
b = x,y          ; both left-hand face buttons act as NES B
select = back
start = start
up = dpup
down = dpdown
left = dpleft
right = dpright
deadzone = 16000 ; left-stick threshold (0-32767)
analog = true    ; left analog stick also drives the d-pad

Valid button names: a b x y back start guide leftshoulder rightshoulder leftstick rightstick dpup dpdown dpleft dpright (use none to unbind).

Display

Hotkey Action
Tab Toggle turbo (fast-forward)
F1–F12 Load state slot 1–12
Shift+F1–Shift+F12 Save state slot 1–12
Alt+Enter Toggle fullscreen (borderless desktop)

State slots are stored in the savestates directory next to the game executable.

The picture preserves the NES aspect ratio with integer (whole-pixel) scaling and nearest-neighbor filtering, so it letterboxes rather than stretching and stays crisp at any window or screen size.

Fallback Discovery Logs

Interpreter fallback discovery is durable by default. The runner appends two files next to the game executable:

  • dispatch_misses.log contains one manifest-ready extra_func line for each first-seen bank/address pair.
  • fallback_telemetry.jsonl contains an immediately flushed dispatch_discovery record for each first sighting, plus approximately once-per-second samples of interpreter instructions, runs, native handoffs, per-target call counts, and per-entry instruction hotspots (including save-state continuation entries). A final session_end record is appended on a normal exit. Discovery records and completed samples survive a later crash.

Both logs append across play sessions. Set NESRECOMP_FALLBACK_LOG to a custom JSONL path, or to off, to override the telemetry file. Per-dispatch disk writes are intentionally limited to first sightings; repeated hot targets are aggregated into periodic samples so observation does not create gameplay slowdown.

game.toml Directives

The full schema lives in recompiler/src/game_config.c; the most commonly used sections are summarized below.

Section Purpose
[game] Output prefix, symbol file path, and recompiler flags (push_all_jsr, disable_ptr_scan, deduplicate_functions, ...)
[mapper] bank_switch = [...] Addresses of MMC1/mapper bank-switch routines
[[inline_dispatch]] addr Indexed dispatch via inline address table after JSR
[[inline_pointer]] addr, zp = [lo, hi], call JSR reads 2 inline bytes into zero page; call also invokes the resulting target
[[extra_func]] bank, addr Force-create a function entry at this address
[[extra_label]] bank, addr Secondary entry point within an existing function
[[sram_map]] sram_start, rom_start, bank, size SRAM-to-ROM code mapping
[[nop_jsr]] addr Skip this JSR entirely (stack-manipulation routines incompatible with recompilation)
[[data_region]] bank, start, end Exclude byte range from pointer scanner (known data, not code)
[functions] fixed = [...], bankN = [...] Bulk extra_func lists keyed by bank
[force_interp] fixed = [...], bankN = [...] Seed entries that always execute as interpreter islands

Numeric values may be given in hex (0xC000) or decimal. Address-only directives are TOML arrays of tables; per-bank lists under [functions] and [force_interp] accept plain integer arrays.

Optional Cross-Bank Function Deduplication

Banked games can opt into a conservative post-processing pass that shares identical generated function bodies:

[game]
deduplicate_functions = true

The pass is disabled by default because its benefit depends on the game. It merges functions only when their generated C is byte-for-byte equal after normalizing the function's own symbol; bank constants, callees, mapper state, fallback behavior, and all other emitted code must still match. The lowest bank/address is retained as the shared implementation, while every original public function symbol remains available through a thin forwarding wrapper.

Each run writes generated/<prefix>_function_groups.json with the proof hash, representative, group members, and estimated duplicate body bytes. Review this manifest and compare generated-source or build sizes before enabling the option for a game: some titles save substantially, while titles without proven groups may see no reduction. [[dedup_exclude]] can keep a member independent, and [[replace_func]] scope = "group" can replace an entire proven group. See modding guide for those directives and external replacement examples.

Function Discovery: Table-Run Scanner

The recompiler discovers functions in two phases:

  1. BFS from vectors: Walk RESET/NMI/IRQ, follow JSR/JMP, discover all statically reachable functions.
  2. Pointer table scanner: Find dynamically-dispatched functions (enemy AI handlers, state machine callbacks) that are only reachable via indirect jumps at runtime.

The table-run scanner finds pointer tables by looking for runs of 4+ consecutive 16-bit LE values in $8000-$BFFF where each target passes a deep-decode validation (7+ valid 6502 instructions without hitting an illegal opcode, or clean termination via RTS/JMP).

It scans both:

  • Switchable banks (tables embedded in the same bank as handlers)
  • Fixed bank → switchable (dispatch tables in the fixed bank pointing to switchable bank handlers)

This catches dispatch patterns that use indirect addressing ((ZP),Y, stack-based) where no direct abs,X/abs,Y code reference to the table exists.

Set NESRECOMP_LEGACY_FUNCTION_FINDER=1 to disable the newer heuristic discovery passes and fall back to the older BFS-first finder behavior for comparison runs.

Harmful vs harmless false positives: The deep-decode check eliminates "harmful" false positives (data misidentified as code, which would generate invalid C). "Harmless" false positives (valid code in another bank's context) are accepted — they generate unused but compilable functions. Adding data_region entries for known data areas eliminates remaining edge cases where structured data happens to decode as 7+ valid instructions.

Expected results (validated against Zelda with exhaustive disassembly ground truth):

  • ~80-93% recall of dispatch table targets, 0 harmful false positives
  • Remaining targets (small inline tables, direct-call-only functions) require extra_func entries

extra_func vs extra_label

  • extra_func: Creates a standalone function. Use for addresses NOT inside any existing function.
  • extra_label: Creates a secondary entry point within the parent function that contains this address. The parent emits a _body(int _entry) function with switch/goto dispatch. Use for addresses that fall inside existing functions (e.g., SRAM-mapped subroutines within a larger code block).

This distinction is critical when importing complete disassembly function lists. Adding an address as extra_func when it's inside an existing function splits that function, breaking internal gotos and causing freezes. extra_label preserves the parent's control flow while making the address callable from the dispatch table.

Undocumented 6502 Opcodes

The NMOS 6502 has 256 possible opcodes but only 151 are officially documented. The remaining 105 have deterministic behavior on the physical chip because the internal decode logic combines signals from multiple "official" instruction paths. NESRecomp's decoder (cpu6502_decoder.c) recognizes every undocumented opcode with the correct size and cycle count; the code generator emits real semantics for the ones that NES games actually use, and treats the rest as sized NOPs (the instruction stream stays byte-aligned, but side effects are lost).

Emitted with full semantics:

Opcode Name Behavior
LAX Load A+X A = X = mem, sets N/Z. Used by Elite, Super Cars, etc.
SAX Store A&X mem = A & X. No flags, no register changes.
DOP / TOP (NOP*) 2-/3-byte NOP-with-read Performs the operand read (so $2002 PPUSTATUS latch clears correctly) and discards the result. Immediate variants skip the operand byte without a read.

Recognized but currently emitted as sized NOPs (skipped, side effects lost):

DCP, ISC, SLO, RLA, SRE, RRA, ANC, ALR, ARR, AXS, KIL, plus the rarely-seen SHX/SHY/SHA/TAS/LAS family. A game that depends on, for example, DCP zp to decrement-and-compare in one step will misbehave. None of the games currently in this project rely on these — when one does, fill in the missing emit logic in code_generator.c alongside LAX/SAX (the decoder already provides the addressing mode and operand size).

Dispatch safety: the function finder rejects candidate function entry points whose first byte decodes as MN_ILLEGAL, so byte values in the illegal range that appear inside data tables don't get misidentified as code and executed.

Build NESRecomp from source

CMake 3.20+ is required. The recompiler itself is pure C11 with no external dependencies; game runners additionally need SDL2.

Windows (Visual Studio 2022)

# Build the recompiler
cmake -S recompiler -B build/recompiler -G "Visual Studio 17 2022" -A x64
cmake --build build/recompiler --config Release

# Recompile a game ROM
build/recompiler/Release/NESRecomp.exe <rom.nes> --game <path/to/game.toml>

To build the same self-contained Windows zip published on the Releases page, install Python 3.12 and PyInstaller, then run:

py -3.12 -m pip install pyinstaller
py -3.12 tools/build_cli.py

The zip is written to build/cli-release/.

macOS / Linux

# Install prerequisites (macOS / Homebrew shown; use your distro's packages on Linux)
brew install cmake sdl2 ninja

# Build the recompiler
cmake -S recompiler -B build/recompiler -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build/recompiler

# Recompile a game ROM (note: no .exe suffix)
build/recompiler/NESRecomp <rom.nes> --game <path/to/game.toml>

Game projects build the same way on macOS/Linux — from the game directory, run its setup.sh to fetch the pinned nesrecomp, then:

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
      -DCMAKE_PREFIX_PATH="$(brew --prefix)"
cmake --build build

Adding a New Game

Follow Your first NESRecomp port. It uses the default cycle backend, a separate game configuration and a baseline host. The cycle-project reference documents desktop/UI, hook and netplay integration. Keep title-specific addresses and behavior in the game repository; generated code stays in build output.

Developer references

Guide Contents
Cycle host integration Current game project and runtime integration
Generated C Translation units, shared declarations and game build integration
Bank extraction / dispatch patterns ROM analysis and verified 6502 control-flow idioms
Modding / mod packages Cross-bank sharing, overrides and trusted plugins
Debug protocol Runtime inspection and control
Legacy interpreter bridge Dispatch fallback, continuations and stack boundaries
Legacy differential co-simulation Oracle comparisons and measurement limits
Historical validation Dated audits and bring-up evidence

Cycle-Accurate Mode

Cycle-accurate generation is the default. --cycle-accurate explicitly selects it; --legacy selects the retained function/frame backend. An explicit [game] cycle_accurate = false also selects legacy, and a command-line backend selection takes precedence over the file. A missing key uses cycle mode. It emits one block per ROM instruction with every CPU cycle's bus activity spelled out, against NESRecomp's own cycle-accurate 6502; the fallback interpreter is generated from the same templates. The CPU, PPU, APU, DMAs and mappers are NESRecomp's own (runner/cyc/hw_*.c), and TriCNES's complete machine is kept only as a test oracle. The runner is runner/cyc.

See the mapper catalog for supported cartridge boards. A block is generated per (PRG bank, CPU address) pair and entered only while the cartridge has that bank mapped there, since the bytes a block folded to constants depend on it. A recompiled AccuracyCoin passes 144/144, and scripted playthroughs of Super Mario Bros. 3 (MMC3), Mega Man 2 (MMC1), Mega Man (UxROM) and Donkey Kong Original Edition (CNROM) run at 100% native; all of them match the oracle on every bus access at all four CPU/PPU alignments. AxROM and GxROM have not been run with a game in the original qualification. See runner/cyc/README.md; AccuracyCoin is set up as a test in runner/cyc/tests/accuracycoin.

Acknowledgements

  • TriCNES by Chris Siebert (MIT) — the PPU/APU model behind cycle-accurate mode and its reference oracle (runner/cyc/tric_core.cpp).

  • Nat Budin (@nbudin) — macOS / POSIX build support (#10): POSIX ucontext coroutine backend, cross-platform directory creation, and the compiler-flag groundwork that made native macOS builds possible.


R.A.I.D. — Retro AI Development · a Discord for AI-assisted retro reverse-engineering, decomp & recomp

Join the Retro AI Development (R.A.I.D.) Discord

About

NES game static recompiler ecosystem · Part of the R.A.I.D. community

Resources

Stars

124 stars

Watchers

6 watching

Forks

Releases

Packages

Contributors

Languages