diff --git a/README.md b/README.md index 65fb83e..34be344 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -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 @@ -183,6 +202,32 @@ cmake --build build --config Release build/Release/NESRecomp.exe --game ``` +### 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 --game +``` + +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: @@ -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 --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. diff --git a/recompiler/src/code_generator.c b/recompiler/src/code_generator.c index 685adda..70e7d41 100644 --- a/recompiler/src/code_generator.c +++ b/recompiler/src/code_generator.c @@ -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" ); diff --git a/recompiler/src/main_nes.c b/recompiler/src/main_nes.c index 27b9c7d..5f966e8 100644 --- a/recompiler/src/main_nes.c +++ b/recompiler/src/main_nes.c @@ -6,7 +6,13 @@ #include #include #include -#include +#ifdef _WIN32 +# include +# define make_dir(p) _mkdir(p) +#else +# include +# define make_dir(p) mkdir((p), 0755) +#endif #include "rom_parser.h" #include "cpu6502_decoder.h" #include "function_finder.h" @@ -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"); } } diff --git a/runner/include/debug_server.h b/runner/include/debug_server.h index f73b112..d81a363 100644 --- a/runner/include/debug_server.h +++ b/runner/include/debug_server.h @@ -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. */ diff --git a/runner/include/nes_runtime.h b/runner/include/nes_runtime.h index 565e402..a35f6ee 100644 --- a/runner/include/nes_runtime.h +++ b/runner/include/nes_runtime.h @@ -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); @@ -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 ---- */ diff --git a/runner/nestopia_cmake.cmake b/runner/nestopia_cmake.cmake index 64a7e9f..f77b908 100644 --- a/runner/nestopia_cmake.cmake +++ b/runner/nestopia_cmake.cmake @@ -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() diff --git a/runner/src/coroutine.c b/runner/src/coroutine.c index c16ed4c..008a05c 100644 --- a/runner/src/coroutine.c +++ b/runner/src/coroutine.c @@ -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 +#include +#include + +#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