Skip to content
Open
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
31 changes: 31 additions & 0 deletions tools/snesref/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
cmake_minimum_required(VERSION 3.16)
project(snesref LANGUAGES CXX)

add_executable(snesref frontend.cpp)
target_compile_features(snesref PRIVATE cxx_std_17)

# Homebrew, vcpkg, and current SDL2 development packages normally provide the
# imported target. Fall back to pkg-config for distributions that provide only
# sdl2.pc.
find_package(SDL2 CONFIG QUIET)
if(TARGET SDL2::SDL2)
target_link_libraries(snesref PRIVATE SDL2::SDL2)
elseif(TARGET SDL2::SDL2-static)
target_link_libraries(snesref PRIVATE SDL2::SDL2-static)
else()
find_package(PkgConfig REQUIRED)
pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2)
target_link_libraries(snesref PRIVATE PkgConfig::SDL2)
endif()

# The loader uses LoadLibrary on all Windows toolchains. MinGW exposes a
# CMAKE_DL_LIBS value even though it does not ship libdl.
if(CMAKE_DL_LIBS AND NOT WIN32 AND NOT MINGW AND NOT MSYS)
target_link_libraries(snesref PRIVATE ${CMAKE_DL_LIBS})
endif()

if(MSVC)
target_compile_options(snesref PRIVATE /W4)
else()
target_compile_options(snesref PRIVATE -Wall -Wextra -Wpedantic)
endif()
46 changes: 41 additions & 5 deletions tools/snesref/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ for any libretro SNES core.

## Build

### CMake (macOS, Linux, or Windows)

Install the SDL2 development package, then configure and build from the
SNESRecomp repository root:

```sh
cmake -S tools/snesref -B build/snesref -DCMAKE_BUILD_TYPE=Release
cmake --build build/snesref
```

On macOS, Homebrew's `sdl2` formula supplies the required headers, library, and
CMake package. The resulting executable is `build/snesref/snesref`.

### Existing Visual Studio batch build

```bat
:: 1. extract the SDL2 VC dev package here as SDL2-2.30.9\ (libsdl.org)
:: 2. build
Expand All @@ -58,12 +73,19 @@ Produces `snesref.exe`.

## Run

```bat
snesref.exe <core.dll> <rom.sfc>
:: e.g. snesref.exe snes9x_libretro.dll mmx.sfc
```text
snesref <libretro-core> <rom.sfc>

Windows example:
snesref.exe snes9x_libretro.dll game.sfc

macOS example:
./build/snesref/snesref snes9x_libretro.dylib game.sfc
```

Place the libretro core DLL (and its `SDL2.dll`) next to the exe, or pass a path.
Pass a path to a separately supplied libretro core: a DLL on Windows, a dylib
on macOS, or a shared object on Linux. Windows batch-build users should also
place `SDL2.dll` beside `snesref.exe`.

### Deterministic capture

Expand All @@ -72,6 +94,7 @@ Environment variables select non-interactive capture outputs:
| Variable | Purpose |
|---|---|
| `SNESREF_FAST=1` | Hide the window and disable frame pacing. |
| `SNESREF_HEADLESS=1` | Run without SDL video or live keyboard/controller input; scripted input and capture outputs remain available. Headless runs are unpaced. |
| `SNESREF_FRAMES=N` | Exit after exactly `N` emulated frames. |
| `SNESREF_INPUT_FILE=path` | Apply deterministic scripted joypad input. |
| `SNESREF_WRAM_FILL=byte` | Fill exposed system WRAM before frame 1; use `0` to match SNESRecomp's hard-reset state. |
Expand Down Expand Up @@ -111,7 +134,20 @@ The 12-bit mask is:
| 10 | `400` | L |
| 11 | `800` | R |

Example:
POSIX shell example:

```sh
SNESREF_HEADLESS=1 \
SNESREF_FRAMES=1800 \
SNESREF_WRAM_FILL=0 \
SNESREF_INPUT_FILE=menu-input.txt \
SNESREF_TRACE_FILE=wram.jsonl \
SNESREF_WAV=reference.wav \
SNESREF_FRAME_DUMP_DIR=frames \
./build/snesref/snesref /path/to/bsnes_libretro.dylib /path/to/game.sfc
```

PowerShell example:

```powershell
$env:SNESREF_FAST = "1"
Expand Down
146 changes: 110 additions & 36 deletions tools/snesref/frontend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
* keyboard input, and logs per-frame WRAM changes (same JSON shape as the
* recomp debug_server's wram_writes_at) to snesref_trace.jsonl.
*
* snesref.exe <core.dll> <rom.sfc>
* snesref <libretro-core> <rom.sfc>
*
* Keys (match the recomp keybinds): arrows=D-pad, Z=B(jump), X=A, A=Y(fire),
* S=X, C=L, V=R, Enter=Start, RShift=Select.
* Shift+F1-F9 = save state slot F1-F9 = load state slot
* Backspace = clear the WRAM trace Esc = quit
*/
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <dlfcn.h>
#endif
#define SDL_MAIN_HANDLED
#include <SDL.h>
#include <cstdio>
Expand All @@ -21,8 +25,53 @@
#include <vector>
#include "libretro.h"

// ---- core function pointers ----
static HMODULE g_core;
// ---- portable dynamic-core loading + core function pointers ----
#ifdef _WIN32
using CoreHandle = HMODULE;
#else
using CoreHandle = void*;
#endif

static CoreHandle g_core;

static CoreHandle core_open(const char* path) {
#ifdef _WIN32
return LoadLibraryA(path);
#else
return dlopen(path, RTLD_NOW | RTLD_LOCAL);
#endif
}

static void* core_symbol(CoreHandle core, const char* name) {
#ifdef _WIN32
return reinterpret_cast<void*>(GetProcAddress(core, name));
#else
dlerror();
return dlsym(core, name);
#endif
}

static const char* core_error() {
#ifdef _WIN32
static char message[64];
snprintf(message, sizeof message, "Windows error %lu",
static_cast<unsigned long>(GetLastError()));
return message;
#else
const char* message = dlerror();
return message ? message : "unknown dynamic-loader error";
#endif
}

static void core_close(CoreHandle core) {
if (!core) return;
#ifdef _WIN32
FreeLibrary(core);
#else
dlclose(core);
#endif
}

#define LR(sym) static decltype(&sym) p_##sym;
LR(retro_init) LR(retro_deinit) LR(retro_api_version)
LR(retro_get_system_info) LR(retro_get_system_av_info)
Expand All @@ -36,8 +85,11 @@ LR(retro_get_memory_data) LR(retro_get_memory_size)
#undef LR

template<class T> static void bind(T& fn, const char* name) {
fn = (T)GetProcAddress(g_core, name);
if (!fn) { fprintf(stderr, "missing core symbol: %s\n", name); exit(2); }
fn = reinterpret_cast<T>(core_symbol(g_core, name));
if (!fn) {
fprintf(stderr, "missing core symbol %s: %s\n", name, core_error());
exit(2);
}
}

// ---- video state ----
Expand All @@ -47,6 +99,7 @@ static SDL_Texture* g_tex;
static int g_tex_w = 0, g_tex_h = 0;
static retro_pixel_format g_fmt = RETRO_PIXEL_FORMAT_0RGB1555;
static SDL_GameController* g_pad = nullptr;
static bool g_headless = false;

static void open_first_pad() {
if (g_pad) return;
Expand Down Expand Up @@ -364,9 +417,11 @@ static void maybe_dump_frame(const void* data, unsigned w, unsigned h, size_t pi
static void cb_video(const void* data, unsigned w, unsigned h, size_t pitch) {
if (data && w && h) {
maybe_dump_frame(data, w, h, pitch);
if (g_headless) return;
ensure_texture(w,h);
SDL_UpdateTexture(g_tex, nullptr, data, (int)pitch);
}
if (g_headless) return;
SDL_RenderClear(g_ren);
if (g_tex) SDL_RenderCopy(g_ren, g_tex, nullptr, nullptr);
SDL_RenderPresent(g_ren);
Expand Down Expand Up @@ -434,6 +489,7 @@ static int16_t cb_input_state(unsigned port, unsigned device, unsigned index, un
}
return (g_scripted_mask & bit) != 0;
}
if (g_headless) return 0;
const Uint8* ks = SDL_GetKeyboardState(nullptr);
SDL_Scancode sc; SDL_GameControllerButton gb;
switch (id) {
Expand Down Expand Up @@ -491,14 +547,21 @@ static void load_state(int slot) {
}

int main(int argc, char** argv) {
if (argc < 3) { fprintf(stderr,"usage: snesref <core.dll> <rom.sfc>\n"); return 1; }
if (argc < 3) {
fprintf(stderr, "usage: snesref <libretro-core> <rom.sfc>\n");
return 1;
}
const char* corePath = argv[1];
const char* romPath = argv[2];
{ const char* input = getenv("SNESREF_INPUT_FILE");
if (input && input[0] && !load_input_file(input)) return 6; }

g_core = LoadLibraryA(corePath);
if (!g_core) { fprintf(stderr,"LoadLibrary failed: %s (err %lu)\n", corePath, GetLastError()); return 2; }
g_core = core_open(corePath);
if (!g_core) {
fprintf(stderr, "cannot load libretro core %s: %s\n",
corePath, core_error());
return 2;
}
bind(p_retro_init,"retro_init"); bind(p_retro_deinit,"retro_deinit");
bind(p_retro_api_version,"retro_api_version");
bind(p_retro_get_system_info,"retro_get_system_info");
Expand Down Expand Up @@ -555,23 +618,32 @@ int main(int argc, char** argv) {
if (qf && qf[0]) quit_frames = atol(qf); }
const char* fast_value = getenv("SNESREF_FAST");
bool fast = fast_value && fast_value[0] && fast_value[0] != '0';
const char* headless_value = getenv("SNESREF_HEADLESS");
bool headless = headless_value && headless_value[0] &&
headless_value[0] != '0';
g_headless = headless;

SDL_SetMainReady();
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK) != 0) { fprintf(stderr,"SDL_Init: %s\n",SDL_GetError()); return 5; }
open_first_pad();
g_win = SDL_CreateWindow("snesref (libretro) — Fn load / Shift+Fn save / Backspace clear-trace",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, vw*2, vh*2,
fast ? SDL_WINDOW_HIDDEN : SDL_WINDOW_RESIZABLE);
g_ren = SDL_CreateRenderer(g_win, -1,
fast ? SDL_RENDERER_ACCELERATED
: SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!g_ren)
g_ren = SDL_CreateRenderer(g_win, -1, SDL_RENDERER_SOFTWARE);
if (!g_win || !g_ren) {
fprintf(stderr, "SDL renderer setup failed: %s\n", SDL_GetError());
return 5;
Uint32 sdl_flags = headless
? SDL_INIT_TIMER
: SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK;
if (SDL_Init(sdl_flags) != 0) { fprintf(stderr,"SDL_Init: %s\n",SDL_GetError()); return 5; }
if (!headless) {
open_first_pad();
g_win = SDL_CreateWindow("snesref (libretro) — Fn load / Shift+Fn save / Backspace clear-trace",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, vw*2, vh*2,
fast ? SDL_WINDOW_HIDDEN : SDL_WINDOW_RESIZABLE);
g_ren = SDL_CreateRenderer(g_win, -1,
fast ? SDL_RENDERER_ACCELERATED
: SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!g_ren)
g_ren = SDL_CreateRenderer(g_win, -1, SDL_RENDERER_SOFTWARE);
if (!g_win || !g_ren) {
fprintf(stderr, "SDL renderer setup failed: %s\n", SDL_GetError());
return 5;
}
SDL_RenderSetLogicalSize(g_ren, vw, vh);
}
SDL_RenderSetLogicalSize(g_ren, vw, vh);

printf("RUN. KB: arrows=DPad Z=B(jump) X=A A=Y(fire) S=X C=L V=R Enter=Start RShift=Select\n");
printf(" Pad: dpad/L-stick, Cross=jump Square=fire Circle=A Triangle=X L1/R1=L/R Start/Select\n");
Expand All @@ -582,18 +654,20 @@ int main(int argc, char** argv) {
Uint64 freq=SDL_GetPerformanceFrequency(), prev=SDL_GetPerformanceCounter();
const double target = (double)freq / 60.098;
while (running) {
SDL_Event e;
while (SDL_PollEvent(&e)) {
if (e.type==SDL_QUIT) running=false;
else if (e.type==SDL_CONTROLLERDEVICEADDED) open_first_pad();
else if (e.type==SDL_CONTROLLERDEVICEREMOVED) { if(g_pad){ SDL_GameControllerClose(g_pad); g_pad=nullptr; printf("[controller removed]\n"); fflush(stdout);} open_first_pad(); }
else if (e.type==SDL_KEYDOWN && e.key.repeat==0) {
SDL_Scancode s = e.key.keysym.scancode;
if (s==SDL_SCANCODE_ESCAPE) running=false;
else if (s==SDL_SCANCODE_BACKSPACE) clear_trace();
else if (s>=SDL_SCANCODE_F1 && s<=SDL_SCANCODE_F9) {
int slot = (int)(s - SDL_SCANCODE_F1) + 1;
if (e.key.keysym.mod & KMOD_SHIFT) save_state(slot); else load_state(slot);
if (!headless) {
SDL_Event e;
while (SDL_PollEvent(&e)) {
if (e.type==SDL_QUIT) running=false;
else if (e.type==SDL_CONTROLLERDEVICEADDED) open_first_pad();
else if (e.type==SDL_CONTROLLERDEVICEREMOVED) { if(g_pad){ SDL_GameControllerClose(g_pad); g_pad=nullptr; printf("[controller removed]\n"); fflush(stdout);} open_first_pad(); }
else if (e.type==SDL_KEYDOWN && e.key.repeat==0) {
SDL_Scancode s = e.key.keysym.scancode;
if (s==SDL_SCANCODE_ESCAPE) running=false;
else if (s==SDL_SCANCODE_BACKSPACE) clear_trace();
else if (s>=SDL_SCANCODE_F1 && s<=SDL_SCANCODE_F9) {
int slot = (int)(s - SDL_SCANCODE_F1) + 1;
if (e.key.keysym.mod & KMOD_SHIFT) save_state(slot); else load_state(slot);
}
}
}
}
Expand All @@ -615,7 +689,7 @@ int main(int argc, char** argv) {
{ static long fr=-2; if(fr==-2){const char*v=getenv("SNESREF_FRAMES"); fr=(v&&v[0])?atol(v):-1;}
if(fr>0 && (long)g_frame>=fr){ if(g_log)fflush(g_log); running=false; } }
// 60fps cap (disabled only for deterministic offline capture).
if (!fast) {
if (!fast && !headless) {
for (;;) {
Uint64 now=SDL_GetPerformanceCounter();
double el=(double)(now-prev);
Expand All @@ -631,6 +705,6 @@ int main(int argc, char** argv) {
}
wav_close();
p_retro_unload_game(); p_retro_deinit();
SDL_Quit(); FreeLibrary(g_core);
SDL_Quit(); core_close(g_core);
return 0;
}