Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,26 @@

# NESRecomp

A static 6502 recompiler framework for NES games. Translates NES ROM machine code to C, which is then compiled to native x64 for direct execution on modern PCs.
A static 6502 recompiler framework for NES games. Translates NES ROM machine code to C, which is then compiled to native machine code for direct execution on modern PCs.

**This is NOT an emulator.** Each 6502 instruction is translated to equivalent C code at build time. JSR becomes a direct C function call, branches become gotos, and the NES hardware (PPU, APU, mapper) is simulated by the runner library.

## 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)**](https://github.com/nbudin) in
[#10](https://github.com/mstan/nesrecomp/pull/10) — thank you! 🙏

## Game Projects

| Game | Mapper | Status | Repository |
Expand Down Expand Up @@ -172,7 +188,10 @@ executed.

## Building

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

### Windows (Visual Studio 2022)

```bash
# Build the recompiler
Expand All @@ -183,6 +202,32 @@ cmake --build build --config Release
build/Release/NESRecomp.exe <rom.nes> --game <path/to/game.toml>
```

### macOS / Linux

```bash
# 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:

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

(`ENABLE_NESTOPIA_ORACLE` is a developer verify-mode feature and is off here for
a plain playable build.)

## Adding a New Game

See [CLAUDE.md](CLAUDE.md) for detailed instructions. In short:
Expand All @@ -192,3 +237,10 @@ See [CLAUDE.md](CLAUDE.md) for detailed instructions. In short:
3. Create `extras.c` implementing the `game_extras.h` hook interface
4. Run `NESRecomp.exe <rom.nes> --game game.toml` to generate C code
5. Build with CMake, linking against the runner library and SDL2

## Acknowledgements

- [**Nat Budin (@nbudin)**](https://github.com/nbudin) — macOS / POSIX build
support ([#10](https://github.com/mstan/nesrecomp/pull/10)): POSIX `ucontext`
coroutine backend, cross-platform directory creation, and the compiler-flag
groundwork that made native macOS builds possible.
9 changes: 9 additions & 0 deletions recompiler/src/code_generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -2098,6 +2098,15 @@ static void emit_dispatch(FILE *f, const EmittedWrapper *wrappers, int wrapper_c
"/* AUTO-GENERATED dispatch table. DO NOT EDIT. */\n"
"#include \"nes_runtime.h\"\n"
"extern int g_current_bank;\n\n"
);

/* Forward-declare every dispatch target. The switch below calls these
* func_* wrappers, which are defined in the _full.c translation unit.
* Without these declarations the dispatch TU relies on implicit function
* declarations, which MSVC tolerates but clang/gcc reject under C99+. */
emit_forward_decls(f, wrappers, wrapper_count, rom);

fprintf(f,
"int call_by_address(uint16_t addr) {\n"
);

Expand Down
10 changes: 8 additions & 2 deletions recompiler/src/main_nes.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <direct.h>
#ifdef _WIN32
# include <direct.h>
# define make_dir(p) _mkdir(p)
#else
# include <sys/stat.h>
# define make_dir(p) mkdir((p), 0755)
#endif
#include "rom_parser.h"
#include "cpu6502_decoder.h"
#include "function_finder.h"
Expand All @@ -28,7 +34,7 @@ static void delete_if_exists(const char *path) {
}

static void ensure_output_dir_exists(void) {
if (_mkdir("generated") == 0) {
if (make_dir("generated") == 0) {
printf("[NESRecomp] Created output directory: generated\n");
}
}
Expand Down
5 changes: 0 additions & 5 deletions runner/include/debug_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,6 @@ int debug_server_get_input_override(void);
void debug_server_set_verify_result(int passed, int diff_count,
const FrameDiffEntry *diffs, int n_diffs);

/* Return a historical frame record from the ring buffer, or NULL if the
* frame is not currently available. The returned pointer is owned by the
* debug server and is only valid until the ring slot is overwritten. */
const NESFrameRecord *debug_server_get_frame_record(uint32_t frame);

/* ---- Public send helpers (for game command handlers) ---- */

/* Send a complete JSON line to the connected client. */
Expand Down
7 changes: 6 additions & 1 deletion runner/include/nes_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ void ppu_render_oam_debug(uint32_t *buf);

/* ---- Mapper Interface ---- */
void mapper_write(uint16_t addr, uint8_t val);
void mapper_init(const uint8_t *prg_data, int prg_banks);
void mapper_init(const uint8_t *prg_data, int prg_banks,
int mapper_type, int initial_mirroring);

/* ---- Runtime Init ---- */
void runtime_init(void);
Expand Down Expand Up @@ -251,6 +252,10 @@ uint8_t runtime_get_ppudata_buf(void);
void runtime_set_ppudata_buf(uint8_t val);
uint16_t runtime_get_ppuaddr(void);
void runtime_set_ppuaddr(uint16_t addr);
uint16_t runtime_get_ppu_t(void);
int runtime_scroll_from_t_valid(void);
void runtime_get_latch_state(uint8_t *ppuaddr_latch, uint8_t *scroll_latch);
void runtime_set_latch_state(uint8_t ppuaddr_latch, uint8_t scroll_latch);
extern uint8_t g_oamaddr;

/* ---- Dispatch miss monitor ---- */
Expand Down
2 changes: 2 additions & 0 deletions runner/nestopia_cmake.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,6 @@ target_compile_definitions(nestopia_core PRIVATE _CRT_SECURE_NO_WARNINGS NST_NO_

if(MSVC)
target_compile_options(nestopia_core PRIVATE /W2 /WX- /EHsc /wd4244 /wd4267 /wd4018 /wd4996 /wd4305)
else()
target_compile_options(nestopia_core PRIVATE -Wno-narrowing -Wno-c++11-narrowing)
endif()
212 changes: 210 additions & 2 deletions runner/src/coroutine.c
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,214 @@ int coroutine_get_current_channel(void) {
}

#else
/* Non-Windows stub — TODO: implement with ucontext */
#error "Coroutine support requires Windows Fibers (or ucontext on POSIX)"
/* POSIX implementation using ucontext_t */
#define _XOPEN_SOURCE 600
#include <ucontext.h>
#include <stdlib.h>
#include <string.h>

#define COROUTINE_STACK_SIZE (1024 * 1024)

static ucontext_t s_scheduler_ctx;
static ucontext_t s_coroutine_ctx[COROUTINE_MAX_CHANNELS];
static void *s_coroutine_stack[COROUTINE_MAX_CHANNELS];
static int s_coroutine_active[COROUTINE_MAX_CHANNELS];
static int s_active = 0;
static int s_current_channel = -1;
static int s_in_coroutine = 0;
static uint16_t s_start_addr = 0;
static int s_fiber_finished_channel = -1;
static int s_restart_requested = 0;

static int s_yield_count = 0;
static int s_resume_count = 0;
static int s_start_count = 0;
static uint8_t s_last_yield_sp = 0;
static uint8_t s_last_resume_sp = 0;

static SchedTraceEntry s_sched_trace[SCHED_TRACE_SIZE];
static int s_sched_trace_count = 0;
static int s_sched_trace_idx = 0;

static void sched_trace_record(SchedEventType evt, int channel, uint16_t addr) {
SchedTraceEntry *e = &s_sched_trace[s_sched_trace_idx];
e->frame = g_frame_count;
e->channel = channel;
e->addr = addr;
e->sp_before = g_cpu.S;
e->event_type = (uint8_t)evt;
s_sched_trace_idx = (s_sched_trace_idx + 1) % SCHED_TRACE_SIZE;
if (s_sched_trace_count < SCHED_TRACE_SIZE) s_sched_trace_count++;
}

extern int call_by_address(uint16_t addr);

static void coroutine_entry(void)
{
int ch = s_current_channel;
call_by_address(s_start_addr);
s_fiber_finished_channel = ch;
s_in_coroutine = 0;
swapcontext(&s_coroutine_ctx[ch], &s_scheduler_ctx);
for (;;) {
s_fiber_finished_channel = ch;
swapcontext(&s_coroutine_ctx[ch], &s_scheduler_ctx);
}
}

int coroutine_scheduler_setjmp(void)
{
if (!s_active)
s_active = 1;

if (s_in_coroutine) {
int ch = s_current_channel;
s_restart_requested = 1;
sched_trace_record(SCHED_EVT_YIELD, ch, 0xFEAA);
s_in_coroutine = 0;
swapcontext(&s_coroutine_ctx[ch], &s_scheduler_ctx);
return 0;
}

if (s_restart_requested) {
s_restart_requested = 0;
for (int i = 0; i < COROUTINE_MAX_CHANNELS; i++) {
if (s_coroutine_active[i]) {
free(s_coroutine_stack[i]);
s_coroutine_stack[i] = NULL;
s_coroutine_active[i] = 0;
}
}
s_current_channel = -1;
extern void runtime_reset_vblank_depth(void);
runtime_reset_vblank_depth();
}
return 0;
}

void coroutine_yield(void)
{
if (!s_active) {
fprintf(stderr, "[coroutine] yield: no active scheduler\n");
return;
}
int ch = s_current_channel;
s_yield_count++;
s_last_yield_sp = g_cpu.S;
sched_trace_record(SCHED_EVT_YIELD, ch, 0);
uint8_t s_before = g_cpu.S;
s_in_coroutine = 0;
swapcontext(&s_coroutine_ctx[ch], &s_scheduler_ctx);
s_in_coroutine = 1;
if (g_cpu.S != (uint8_t)(s_before + 4)) {
printf("[coroutine] YIELD S MISMATCH: before=$%02X after=$%02X expected=$%02X f=%llu\n",
s_before, g_cpu.S, (uint8_t)(s_before + 4),
(unsigned long long)g_frame_count);
fflush(stdout);
}
}

void coroutine_resume(int channel)
{
if (channel < 0 || channel >= COROUTINE_MAX_CHANNELS) {
fprintf(stderr, "[coroutine] resume: bad channel %d\n", channel);
return;
}
if (!s_coroutine_active[channel])
return;
s_current_channel = channel;
s_resume_count++;
s_last_resume_sp = g_cpu.S;
sched_trace_record(SCHED_EVT_RESUME, channel, 0);
s_fiber_finished_channel = -1;
s_in_coroutine = 1;
swapcontext(&s_scheduler_ctx, &s_coroutine_ctx[channel]);
s_in_coroutine = 0;
if (s_fiber_finished_channel == channel) {
free(s_coroutine_stack[channel]);
s_coroutine_stack[channel] = NULL;
s_coroutine_active[channel] = 0;
s_fiber_finished_channel = -1;
}
}

void coroutine_start(int channel, uint16_t addr)
{
if (channel < 0 || channel >= COROUTINE_MAX_CHANNELS) {
fprintf(stderr, "[coroutine] start: bad channel %d\n", channel);
return;
}
if (s_coroutine_active[channel]) {
free(s_coroutine_stack[channel]);
s_coroutine_stack[channel] = NULL;
s_coroutine_active[channel] = 0;
}
s_start_count++;
s_start_addr = addr;
s_current_channel = channel;
sched_trace_record(SCHED_EVT_START, channel, addr);

s_coroutine_stack[channel] = malloc(COROUTINE_STACK_SIZE);
getcontext(&s_coroutine_ctx[channel]);
s_coroutine_ctx[channel].uc_stack.ss_sp = s_coroutine_stack[channel];
s_coroutine_ctx[channel].uc_stack.ss_size = COROUTINE_STACK_SIZE;
s_coroutine_ctx[channel].uc_link = NULL;
makecontext(&s_coroutine_ctx[channel], coroutine_entry, 0);
s_coroutine_active[channel] = 1;

s_fiber_finished_channel = -1;
s_in_coroutine = 1;
swapcontext(&s_scheduler_ctx, &s_coroutine_ctx[channel]);
s_in_coroutine = 0;
if (s_fiber_finished_channel == channel) {
free(s_coroutine_stack[channel]);
s_coroutine_stack[channel] = NULL;
s_coroutine_active[channel] = 0;
s_fiber_finished_channel = -1;
}
}

void coroutine_set_channel(int channel)
{
s_current_channel = channel;
}

int coroutine_is_active(void)
{
return s_active;
}

int coroutine_has_context(int channel)
{
if (channel < 0 || channel >= COROUTINE_MAX_CHANNELS) return 0;
return s_coroutine_active[channel];
}

void coroutine_get_debug_counters(int *yields, int *resumes, int *starts,
uint8_t *yield_sp, uint8_t *resume_sp)
{
if (yields) *yields = s_yield_count;
if (resumes) *resumes = s_resume_count;
if (starts) *starts = s_start_count;
if (yield_sp) *yield_sp = s_last_yield_sp;
if (resume_sp) *resume_sp = s_last_resume_sp;
}

void coroutine_get_sched_trace(int *out_count, int *out_idx) {
*out_count = s_sched_trace_count;
*out_idx = s_sched_trace_idx;
}

const SchedTraceEntry *coroutine_get_sched_trace_buf(void) {
return s_sched_trace;
}

int coroutine_get_current_channel(void) {
return s_current_channel;
}

int coroutine_restart_requested(void) {
return s_restart_requested;
}

#endif