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
20 changes: 20 additions & 0 deletions runner/cmake/mmx_state_tests.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Optional real-ROM integration check; the game and host sources are identical
# to the executable. The test entry includes the host to exercise its adapter.
function(snesrecomp_target_mmx_state_tests target game_main)
option(MMX_STATE_TESTS "Build ROM-backed MMX save/rewind checks" OFF)
if(NOT MMX_STATE_TESTS)
return()
endif()
get_target_property(_sources ${target} SOURCES)
list(REMOVE_ITEM _sources src/main.c "${game_main}"
"${SNESRECOMP_RUNNER_ROOT}/src/desktop/host_main.c")
add_executable(mmx_state_tests
"${SNESRECOMP_RUNNER_ROOT}/tests/mmx_state_runtime.c" ${_sources})
foreach(_property INCLUDE_DIRECTORIES COMPILE_DEFINITIONS COMPILE_OPTIONS LINK_LIBRARIES LINK_OPTIONS)
get_target_property(_value ${target} ${_property})
if(_value)
set_property(TARGET mmx_state_tests PROPERTY ${_property} "${_value}")
endif()
endforeach()
target_compile_definitions(mmx_state_tests PRIVATE MMX_GAME_MAIN="${game_main}")
endfunction()
37 changes: 37 additions & 0 deletions runner/src/common_rtl.c
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,11 @@ void rtl_reset_host_pacing(void) {
snes_refresh_state_set(0u, g_cpu.master_cycles);
}

static uint64_t s_state_generation;
uint64_t RtlStateGeneration(void) { return s_state_generation; }

void RtlReset(int mode) {
++s_state_generation;
rtl_reset_host_pacing();
snes_reset(g_snes, true);
g_snes->beamMasterLast = g_cpu.master_cycles;
Expand Down Expand Up @@ -871,6 +875,7 @@ bool RtlLoadSnapshot(const char *filename) {
* game one hook to rebuild it against the freshly restored WRAM. */
if (g_rtl_game_info && g_rtl_game_info->on_state_loaded)
g_rtl_game_info->on_state_loaded(hdr[1]);
++s_state_generation;
return true;
}

Expand Down Expand Up @@ -918,6 +923,7 @@ bool RtlLoadSnapshotFromMemory(const void *data, size_t size) {
PpuResetWidescreenOamHistory(g_snes->ppu);
if (g_rtl_game_info && g_rtl_game_info->on_state_loaded)
g_rtl_game_info->on_state_loaded(hdr[1]);
++s_state_generation;
return true;
}

Expand Down Expand Up @@ -1106,6 +1112,37 @@ static void rtl_rb_residue_apply(const RtlRollbackResidue *r) {
ppu_rb_residue_set(g_snes->ppu, &r->ppu_rb);
}

static RtlRollbackResidue s_loaded_execution;
static bool s_loaded_execution_valid;

void RtlSaveExecutionState(SaveLoadInfo *sli) {
RtlRollbackResidue r;
rtl_rb_residue_capture(&r);
r.cpu.ram = NULL; /* Never persist a process address. */
uint32 size = sizeof(r);
sli->func(sli, &size, sizeof(size));
sli->func(sli, &r, sizeof(r));
cx4_saveload_clock(g_snes->cart->cx4, sli);
}

bool RtlLoadExecutionState(SaveLoadInfo *sli) {
uint32 size = 0;
s_loaded_execution_valid = false;
sli->func(sli, &size, sizeof(size));
if (size != sizeof(s_loaded_execution)) return false;
memset(&s_loaded_execution, 0, sizeof(s_loaded_execution));
sli->func(sli, &s_loaded_execution, sizeof(s_loaded_execution));
cx4_saveload_clock(g_snes->cart->cx4, sli);
return s_loaded_execution_valid =
s_loaded_execution.magic == RTL_RB_RESIDUE_MAGIC &&
s_loaded_execution.version == RTL_RB_RESIDUE_VERSION;
}

void RtlApplyExecutionState(void) {
if (s_loaded_execution_valid) rtl_rb_residue_apply(&s_loaded_execution);
s_loaded_execution_valid = false;
}

size_t RtlRollbackSaveToMemory(void *data, size_t capacity) {
size_t guest;
RtlRollbackResidue residue;
Expand Down
9 changes: 9 additions & 0 deletions runner/src/common_rtl.h
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,15 @@ void RtlSramFilePath(char *buf, size_t buflen);
void RtlMigrateLegacySram(const char *legacy_title);
bool RtlSaveSnapshot(const char *filename);
bool RtlLoadSnapshot(const char *filename);
/* Opt-in native execution extension for title state callbacks. Save/load in
* state_*_extra; apply in on_state_loaded, after generic post-load cleanup.
* Version/size checked, pointer-free; intended for matching runtime builds. */
struct SaveLoadInfo;
void RtlSaveExecutionState(struct SaveLoadInfo *sli);
bool RtlLoadExecutionState(struct SaveLoadInfo *sli);
void RtlApplyExecutionState(void);
/* Host timeline invalidation after a successful load or reset. */
uint64_t RtlStateGeneration(void);
size_t RtlSaveSnapshotToMemory(void *data, size_t capacity);
bool RtlLoadSnapshotFromMemory(const void *data, size_t size);

Expand Down
1 change: 1 addition & 0 deletions runner/src/desktop/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ enum {

extern Config g_config;

void ConfigUseStateMenuDefaults(void);
void ParseConfigFile(const char *filename);
// Re-apply only the [KeyMap] section (launcher hotkey editor wrote it after
// the initial parse). Keyboard command map is rebuilt; gamepad map and all
Expand Down
99 changes: 80 additions & 19 deletions runner/src/desktop/host_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ static struct RendererFuncs g_renderer_funcs;
/* Set by the hotkeys; consumed once in the frame loop. */
static int g_savestate_menu_hotkey;
static int g_rewind_hotkey;
static uint64_t g_state_generation;

/* The last field actually presented, kept so an overlay can freeze the guest
* and still have something to draw behind itself. Sized like g_my_pixels. */
Expand All @@ -342,6 +343,11 @@ static GamepadInfo g_gamepad[2];

extern Snes *g_snes;

void snesrecomp_desktop_set_widescreen(int enabled) {
g_config.widescreen = enabled != 0;
WriteConfigFile(g_active_config_file);
}

/* Presentation frame blending (config.ini [Graphics] FrameBlend, the
* launcher's Display checkbox): each presented frame averaged with the
* previous one, so alternate-frame flicker "transparency" reads as steady
Expand Down Expand Up @@ -474,15 +480,19 @@ static void PreparePpuFrame(void) {
if (fh <= 0 || fh > 240) fh = 224;
g_snes_width = fw;
g_snes_height = fh;
/* The PPU's own widescreen never activates here. */
g_ws_extra = 0;
g_ws_active = false;
/* Native widescreen ports rasterize directly into the widened field. */
g_ws_extra = g_game->native_widescreen ? (fw - 256) / 2 : 0;
g_ws_active = g_ws_extra != 0;
g_new_ppu = (g_ppu_render_flags & kPpuRenderFlags_NewRenderer) != 0;
if (g_config.no_sprite_limits)
g_ppu_render_flags |= kPpuRenderFlags_NoSpriteLimits;
else
g_ppu_render_flags &= ~kPpuRenderFlags_NoSpriteLimits;
PpuBeginDrawing(g_ppu, g_my_pixels, 256 * 4, 0);
uint32 flags = g_game->native_widescreen ? g_ppu_render_flags : 0;
if (g_ws_active) flags |= kPpuRenderFlags_NewRenderer;
PpuBeginDrawing(g_ppu, g_my_pixels,
(g_game->native_widescreen ? fw : 256) * 4, flags);
PpuSetExtraSpace(g_ppu, (uint8)g_ws_extra);
}

// --- Scripted input ---
Expand Down Expand Up @@ -848,6 +858,16 @@ static void CaptureSimulationFrame(unsigned number) {
if (g_game->end_sim_frame) g_game->end_sim_frame(g_my_pixels, number);
}

/* Snapshots and their thumbnails share the same completed raster boundary. */
static void NoteStateFrame(void) {
if (g_ppu && g_ppu->renderBuffer) {
int width = g_game->native_widescreen ? g_snes_width : 256;
snes_savestate_menu_note_frame((const uint32_t *)g_ppu->renderBuffer, width, g_snes_height);
snes_rewind_note_framebuffer((const uint32_t *)g_ppu->renderBuffer, width, g_snes_height);
}
snes_rewind_note_frame();
}

void RtlDrawPpuFrame(uint8 *pixel_buffer, size_t pitch, uint32 render_flags) {
(void)render_flags;
if (!pixel_buffer) return;
Expand Down Expand Up @@ -1082,6 +1102,8 @@ static bool HandleDeviceEvent(const SDL_Event *event) {
* bug as F1 through HandleInput. Controller bits still flow, so the panel
* can be navigated. */
static bool g_overlay_modal;
static void SetAudioPaused(bool paused);
static void ResetAudioTimeline(void);

/* Buttons still held when a panel closed, masked from the guest until each
* is released. The button that closed the panel must not also act in the
Expand Down Expand Up @@ -1113,6 +1135,14 @@ static void PumpOverlayEvents(bool *running, void (*key_down)(int key, int repea
*running = false;
break;
case SDL_KEYDOWN:
/* The browser consumes SNES control bits too. Dispatch only controls;
* slot/reset hotkeys must not change the guest behind a modal panel. */
if (key_down == snes_savestate_menu_handle_key) {
int cmd = FindCmdForSdlKey(SNESRECOMP_SDL_EVENT_KEY(event),
SNESRECOMP_SDL_EVENT_MOD(event));
if (cmd >= kKeys_Controls && cmd <= kKeys_Controls_Last)
HandleCommand(cmd, true);
}
key_down(SNESRECOMP_SDL_EVENT_KEY(event), SNESRECOMP_SDL_EVENT_REPEAT(event));
break;
case SDL_KEYUP:
Expand Down Expand Up @@ -1412,6 +1442,7 @@ static void RunSavestateMenuLoop(bool *running) {
host_report_breadcrumb("save-state browser OPEN - guest frozen until it "
"closes (pad B, or Escape/Backspace on the keyboard)");
g_overlay_modal = true;
SetAudioPaused(true);
while (snes_savestate_menu_is_open() && *running) {
/* Key presses go straight to the overlay, NOT through HandleInput: the
* game's own hotkeys must not fire while a panel owns the screen (F1
Expand All @@ -1427,6 +1458,8 @@ static void RunSavestateMenuLoop(bool *running) {
frames++;
}
g_overlay_modal = false;
ResetAudioTimeline();
SetAudioPaused(g_paused);
OverlayNoteClosed();
host_report_breadcrumb("save-state browser CLOSED after %u pumps - guest resuming",
frames);
Expand Down Expand Up @@ -1456,6 +1489,7 @@ static void RunRewindLoop(bool *running) {
host_report_breadcrumb("rewind filmstrip OPEN - guest frozen until it closes "
"(pad B, or Escape; Left/Right scrub, A or Enter commits)");
g_overlay_modal = true;
SetAudioPaused(true);
while (snes_rewind_is_open() && *running) {
PumpOverlayEvents(running, &RewindKeyDown);
if (!*running)
Expand Down Expand Up @@ -1492,6 +1526,9 @@ static void RunRewindLoop(bool *running) {
frames++;
}
g_overlay_modal = false;
ResetAudioTimeline();
SetAudioPaused(g_paused);
g_state_generation = RtlStateGeneration(); /* keep the trimmed rewind history */
OverlayNoteClosed();
host_report_breadcrumb("rewind filmstrip CLOSED after %u pumps - guest resuming",
frames);
Expand Down Expand Up @@ -1522,6 +1559,18 @@ static uint8 *g_audio_stream_buffer;
static size_t g_audio_stream_buffer_size;
#endif

static void ResetAudioTimeline(void) {
RtlApuLock();
g_audiobuffer_end = g_audiobuffer_cur;
g_audio_primed = false;
RtlApuUnlock();
#if SNESRECOMP_SDL3
/* The stream callback takes the APU mutex: never acquire SDL's stream
* lock while holding that mutex in the opposite order. */
if (g_audio_stream) SDL_ClearAudioStream(g_audio_stream);
#endif
}

void RtlApuLock(void) {
SDL_LockMutex(g_audio_mutex);
++g_apu_lock_depth;
Expand Down Expand Up @@ -1999,6 +2048,7 @@ int snesrecomp_desktop_main(const SnesDesktopHostGame *game, int argc, char **ar
framedump_dir = argv[1];
argc -= 2, argv += 2;
}
if (game->state_menu_hotkeys) ConfigUseStateMenuDefaults();
ParseConfigFile(config_file);
g_active_config_file = config_file;
/* Local overrides (gitignored). Last parser to set a key wins. */
Expand Down Expand Up @@ -2297,8 +2347,8 @@ int snesrecomp_desktop_main(const SnesDesktopHostGame *game, int argc, char **ar
}

g_gamepad[0].joystick_id = g_gamepad[1].joystick_id = -1;
g_ws_extra = 0;
g_ws_active = false;
g_ws_extra = g_game->native_widescreen ? (g_snes_width - 256) / 2 : 0;
g_ws_active = g_ws_extra != 0;
g_ppu_render_flags = g_config.new_renderer * kPpuRenderFlags_NewRenderer |
g_config.no_sprite_limits * kPpuRenderFlags_NoSpriteLimits;

Expand Down Expand Up @@ -2617,10 +2667,17 @@ error_reading:;
/* Rewind ring: reads the env overrides and reserves slot headers; the
* buffer itself is allocated lazily on the first capture. */
snes_rewind_configure();
g_state_generation = RtlStateGeneration();

host_report_breadcrumb("entering main loop");

while (running) {
if (g_state_generation != RtlStateGeneration()) {
ResetAudioTimeline();
snes_rewind_shutdown();
snes_rewind_configure();
g_state_generation = RtlStateGeneration();
}
SDL_Event event;

/* Inert unless SNESRECOMP_CRASH_TEST is set — support drill for the
Expand Down Expand Up @@ -2668,7 +2725,7 @@ error_reading:;
SetAudioPaused(audiopaused);
}

if (g_paused) {
if (g_paused && !g_savestate_menu_hotkey && !g_rewind_hotkey) {
snes_host_clock_reset(&video_clock, MonotonicSeconds(), g_simulation_hz, presentation_hz);
SDL_Delay(16);
continue;
Expand Down Expand Up @@ -2720,8 +2777,8 @@ error_reading:;
frameCtr++;
g_screenshot_frame = frameCtr;
snes_osd_note_frame();
snes_rewind_note_frame();
CaptureSimulationFrame(frameCtr);
NoteStateFrame();
snes_netplay_finish_frame();
if (burst >= snes_host_catchup_budget())
break;
Expand Down Expand Up @@ -2902,6 +2959,7 @@ error_reading:;
GameReset();
continue; /* guest was frozen: no frame to run or present */
}
if (g_paused) continue;
/* The script ticks HERE, after every path that can leave this iteration
* without running a frame. Ticked above the overlay checks, an
* iteration that opened a panel consumed a script frame the guest never
Expand Down Expand Up @@ -2930,16 +2988,6 @@ error_reading:;
}
ApplyScriptForcePokes();
snes_osd_note_frame();
/* One guest frame happened: offer it to the rewind ring, and offer the
* field as the next save's thumbnail. Both are no-ops while a panel is
* open, so a thumbnail is of the game and not of the overlay. */
snes_rewind_note_frame();
if (g_ppu && g_ppu->renderBuffer) {
snes_savestate_menu_note_frame((const uint32_t *)g_ppu->renderBuffer,
256, g_snes_height);
snes_rewind_note_framebuffer((const uint32_t *)g_ppu->renderBuffer,
256, g_snes_height);
}
ProfileEnd(kProfileGuest, profile_start);
frameCtr++;
g_screenshot_frame = frameCtr;
Expand Down Expand Up @@ -2973,6 +3021,7 @@ error_reading:;

profile_start = ProfileStart();
CaptureSimulationFrame(frameCtr);
NoteStateFrame();
g_audio_producer_active = false;
ProfileEnd(kProfileRaster, profile_start);
profile_start = ProfileStart();
Expand Down Expand Up @@ -3028,6 +3077,7 @@ error_reading:;
HandleCommand(kKeys_Save + 0, true);

RtlWriteSram();
snes_rewind_shutdown();
snes_runahead_shutdown();
#if defined(RECOMP_LAUNCHER)
if (g_blend) recomp_frame_blend_destroy(g_blend);
Expand Down Expand Up @@ -3469,7 +3519,18 @@ static const char kDefaultConfigIniContent[] =
"ControlsP2 = DpadUp, DpadDown, DpadLeft, DpadRight, Back, Start, B, A, Y, X, Lb, Rb\n";

static const char *DefaultConfigIni(void) {
return g_game->default_config_ini ? g_game->default_config_ini : kDefaultConfigIniContent;
if (g_game->default_config_ini) return g_game->default_config_ini;
if (g_game->state_menu_hotkeys) {
static char menu_config[sizeof(kDefaultConfigIniContent) + 128];
const char *keys = strstr(kDefaultConfigIniContent, "SaveStateMenu = F11\n");
const char *after_load = strchr(strstr(keys, "Load ="), '\n') + 1;
snprintf(menu_config, sizeof(menu_config), "%.*s%s%s",
(int)(keys - kDefaultConfigIniContent), kDefaultConfigIniContent,
"SaveStateMenu = F7\nRewind = F8\n"
"Load = F1,F2,F3,F4,F5,F6,F11,F12,F9,F10\n", after_load);
return menu_config;
}
return kDefaultConfigIniContent;
}

/* Write the default config.ini next to the executable and chdir there. Silent
Expand Down
5 changes: 5 additions & 0 deletions runner/src/desktop/host_main.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ typedef struct SnesDesktopHostGame {
/* Guest-side polling that decides the default frame width. Left at 0 the
* host presents 256x224. */
int frame_width, frame_height;
/* Use the runner's widened PPU field instead of a game-owned compositor. */
int native_widescreen;
/* F7/F8 menus, with legacy slot 7/8 loads moved to F11/F12. */
int state_menu_hotkeys;

/* ── Hooks. Every one is optional. ────────────────────────────────────── */

Expand Down Expand Up @@ -144,6 +148,7 @@ int snesrecomp_desktop_main(const SnesDesktopHostGame *game, int argc, char **ar
* default). For per-title code that composes against the current frame. */
int snesrecomp_desktop_frame_width(void);
int snesrecomp_desktop_frame_height(void);
void snesrecomp_desktop_set_widescreen(int enabled);

/* Ask the host to reset its pacing clock at the next opportunity (a title
* that just changed its presentation settings). */
Expand Down
Loading