From 1bfaacee22662a8cd7be71ea55d0c55e19551bf8 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:59:16 +0100 Subject: [PATCH 01/29] feat(rewind): implement rewind functionality with configurable options --- llm/RetroArch/RetroArch | 1 + workspace/all/common/config.c | 76 ++++++++ workspace/all/common/config.h | 17 ++ workspace/all/minarch/makefile | 6 +- workspace/all/minarch/minarch.c | 327 +++++++++++++++++++++++++++++--- 5 files changed, 399 insertions(+), 28 deletions(-) create mode 160000 llm/RetroArch/RetroArch diff --git a/llm/RetroArch/RetroArch b/llm/RetroArch/RetroArch new file mode 160000 index 000000000..d98800357 --- /dev/null +++ b/llm/RetroArch/RetroArch @@ -0,0 +1 @@ +Subproject commit d98800357a0ea9fd34bdd6389e3790d13fbf29c4 diff --git a/workspace/all/common/config.c b/workspace/all/common/config.c index 9f25fbe2a..a874158c2 100644 --- a/workspace/all/common/config.c +++ b/workspace/all/common/config.c @@ -60,6 +60,10 @@ void CFG_defaults(NextUISettings *cfg) .saveFormat = CFG_DEFAULT_SAVEFORMAT, .stateFormat = CFG_DEFAULT_STATEFORMAT, .useExtractedFileName = CFG_DEFAULT_EXTRACTEDFILENAME, + .rewindEnable = CFG_DEFAULT_REWIND_ENABLE, + .rewindBufferMB = CFG_DEFAULT_REWIND_BUFFER_MB, + .rewindGranularity = CFG_DEFAULT_REWIND_GRANULARITY, + .rewindMuteAudio = CFG_DEFAULT_REWIND_MUTE_AUDIO, .wifi = CFG_DEFAULT_WIFI, .wifiDiagnostics = CFG_DEFAULT_WIFI_DIAG, @@ -230,6 +234,26 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb) CFG_setUseExtractedFileName((bool)temp_value); continue; } + if (sscanf(line, "rewindEnable=%i", &temp_value) == 1) + { + CFG_setRewindEnable((bool)temp_value); + continue; + } + if (sscanf(line, "rewindBufferMB=%i", &temp_value) == 1) + { + CFG_setRewindBufferMB(temp_value); + continue; + } + if (sscanf(line, "rewindGranularity=%i", &temp_value) == 1) + { + CFG_setRewindGranularity(temp_value); + continue; + } + if (sscanf(line, "rewindMuteAudio=%i", &temp_value) == 1) + { + CFG_setRewindMuteAudio((bool)temp_value); + continue; + } if (sscanf(line, "muteLeds=%i", &temp_value) == 1) { CFG_setMuteLEDs(temp_value); @@ -583,6 +607,50 @@ void CFG_setUseExtractedFileName(bool use) CFG_sync(); } +bool CFG_getRewindEnable(void) +{ + return settings.rewindEnable; +} + +void CFG_setRewindEnable(bool enable) +{ + settings.rewindEnable = enable; + CFG_sync(); +} + +int CFG_getRewindBufferMB(void) +{ + return settings.rewindBufferMB; +} + +void CFG_setRewindBufferMB(int mb) +{ + settings.rewindBufferMB = clamp(mb, 1, 256); + CFG_sync(); +} + +int CFG_getRewindGranularity(void) +{ + return settings.rewindGranularity; +} + +void CFG_setRewindGranularity(int granularity) +{ + settings.rewindGranularity = clamp(granularity, 1, 60); + CFG_sync(); +} + +bool CFG_getRewindMuteAudio(void) +{ + return settings.rewindMuteAudio; +} + +void CFG_setRewindMuteAudio(bool enable) +{ + settings.rewindMuteAudio = enable; + CFG_sync(); +} + bool CFG_getMuteLEDs(void) { return settings.muteLeds; @@ -885,6 +953,10 @@ void CFG_sync(void) fprintf(file, "saveFormat=%i\n", settings.saveFormat); fprintf(file, "stateFormat=%i\n", settings.stateFormat); fprintf(file, "useExtractedFileName=%i\n", settings.useExtractedFileName); + fprintf(file, "rewindEnable=%i\n", settings.rewindEnable); + fprintf(file, "rewindBufferMB=%i\n", settings.rewindBufferMB); + fprintf(file, "rewindGranularity=%i\n", settings.rewindGranularity); + fprintf(file, "rewindMuteAudio=%i\n", settings.rewindMuteAudio); fprintf(file, "muteLeds=%i\n", settings.muteLeds); fprintf(file, "artWidth=%i\n", (int)(settings.gameArtWidth * 100)); fprintf(file, "wifi=%i\n", settings.wifi); @@ -928,6 +1000,10 @@ void CFG_print(void) printf("\t\"saveFormat\": %i,\n", settings.saveFormat); printf("\t\"stateFormat\": %i,\n", settings.stateFormat); printf("\t\"useExtractedFileName\": %i,\n", settings.useExtractedFileName); + printf("\t\"rewindEnable\": %i,\n", settings.rewindEnable); + printf("\t\"rewindBufferMB\": %i,\n", settings.rewindBufferMB); + printf("\t\"rewindGranularity\": %i,\n", settings.rewindGranularity); + printf("\t\"rewindMuteAudio\": %i,\n", settings.rewindMuteAudio); printf("\t\"muteLeds\": %i,\n", settings.muteLeds); printf("\t\"artWidth\": %i,\n", (int)(settings.gameArtWidth * 100)); printf("\t\"wifi\": %i,\n", settings.wifi); diff --git a/workspace/all/common/config.h b/workspace/all/common/config.h index b331f1be6..9db34bece 100644 --- a/workspace/all/common/config.h +++ b/workspace/all/common/config.h @@ -102,6 +102,10 @@ typedef struct int saveFormat; int stateFormat; bool useExtractedFileName; + bool rewindEnable; + int rewindBufferMB; + int rewindGranularity; + bool rewindMuteAudio; // Haptic bool haptics; @@ -141,6 +145,10 @@ typedef struct #define CFG_DEFAULT_SAVEFORMAT SAVE_FORMAT_SAV #define CFG_DEFAULT_STATEFORMAT STATE_FORMAT_SAV #define CFG_DEFAULT_EXTRACTEDFILENAME false +#define CFG_DEFAULT_REWIND_ENABLE false +#define CFG_DEFAULT_REWIND_BUFFER_MB 16 +#define CFG_DEFAULT_REWIND_GRANULARITY 1 +#define CFG_DEFAULT_REWIND_MUTE_AUDIO true #define CFG_DEFAULT_MUTELEDS false #define CFG_DEFAULT_GAMEARTWIDTH 0.45 #define CFG_DEFAULT_WIFI false @@ -226,6 +234,15 @@ void CFG_setStateFormat(int); // use extracted file name instead of archive name (for cores that do not support archives natively) bool CFG_getUseExtractedFileName(void); void CFG_setUseExtractedFileName(bool); +// Rewind controls +bool CFG_getRewindEnable(void); +void CFG_setRewindEnable(bool enable); +int CFG_getRewindBufferMB(void); +void CFG_setRewindBufferMB(int mb); +int CFG_getRewindGranularity(void); +void CFG_setRewindGranularity(int granularity); +bool CFG_getRewindMuteAudio(void); +void CFG_setRewindMuteAudio(bool enable); // Enable/disable mute also shutting off LEDs. bool CFG_getMuteLEDs(void); void CFG_setMuteLEDs(bool); diff --git a/workspace/all/minarch/makefile b/workspace/all/minarch/makefile index 2fc9b2fb2..c2dd5fc40 100644 --- a/workspace/all/minarch/makefile +++ b/workspace/all/minarch/makefile @@ -31,12 +31,12 @@ LDFLAGS += -lmsettings -lsamplerate ifeq ($(PLATFORM), desktop) ifeq ($(UNAME_S),Linux) CFLAGS += `pkg-config --cflags libzip` -LDFLAGS += `pkg-config --libs libzip` +LDFLAGS += `pkg-config --libs libzip` -lz else -LDFLAGS += -lzip +LDFLAGS += -lzip -lz endif else -LDFLAGS += -Llibretro-common -lsrm -lzip +LDFLAGS += -Llibretro-common -lsrm -lzip -lz CFLAGS += -DHAS_SRM endif ifeq ($(PLATFORM), tg5040) diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 3daee1b6c..e24eb1615 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -13,6 +13,7 @@ #include #include #include +#include // libretro-common #include "libretro.h" @@ -70,6 +71,8 @@ static int show_debug = 0; static int max_ff_speed = 3; // 4x static int ff_audio = 0; static int fast_forward = 0; +static int rewind_pressed = 0; +static int rewinding = 0; static int overclock = 3; // auto static int has_custom_controllers = 0; static int gamepad_type = 0; // index in gamepad_labels/gamepad_values @@ -1082,6 +1085,7 @@ static void State_autosave(void) { State_write(); state_slot = last_state_slot; } +static void Rewind_on_state_change(void); static void State_resume(void) { if (!exists(RESUME_SLOT_PATH)) return; @@ -1090,9 +1094,251 @@ static void State_resume(void) { unlink(RESUME_SLOT_PATH); State_read(); state_slot = last_state_slot; + Rewind_on_state_change(); } /////////////////////////////// +// Rewind buffer (in-memory, compressed) + +typedef struct { + size_t offset; + size_t size; +} RewindEntry; + +typedef struct { + uint8_t *buffer; + size_t capacity; + size_t head; + size_t tail; + + RewindEntry *entries; + int entry_capacity; + int entry_head; + int entry_tail; + int entry_count; + + uint8_t *state_buf; + size_t state_size; + uint8_t *scratch; + size_t scratch_size; + + int granularity; + int frame_counter; + int enabled; + int mute_audio; +} RewindContext; + +static RewindContext rewind_ctx = {0}; +static int rewind_warn_empty = 0; +static int last_rewind_pressed = 0; + +static void Rewind_free(void) { + if (rewind_ctx.buffer) free(rewind_ctx.buffer); + if (rewind_ctx.entries) free(rewind_ctx.entries); + if (rewind_ctx.state_buf) free(rewind_ctx.state_buf); + if (rewind_ctx.scratch) free(rewind_ctx.scratch); + memset(&rewind_ctx, 0, sizeof(rewind_ctx)); + rewinding = 0; +} + +static void Rewind_reset(void) { + if (!rewind_ctx.enabled) return; + rewind_ctx.head = rewind_ctx.tail = 0; + rewind_ctx.entry_head = rewind_ctx.entry_tail = rewind_ctx.entry_count = 0; + rewind_ctx.frame_counter = 0; + rewinding = 0; + rewind_warn_empty = 0; +} + +static size_t Rewind_free_space(void) { + if (rewind_ctx.entry_count>0 && rewind_ctx.head==rewind_ctx.tail) return 0; + if (rewind_ctx.head >= rewind_ctx.tail) + return rewind_ctx.capacity - (rewind_ctx.head - rewind_ctx.tail); + else + return rewind_ctx.tail - rewind_ctx.head; +} + +static void Rewind_drop_oldest(void) { + if (!rewind_ctx.entry_count) return; + RewindEntry *e = &rewind_ctx.entries[rewind_ctx.entry_tail]; + rewind_ctx.tail = (e->offset + e->size) % rewind_ctx.capacity; + rewind_ctx.entry_tail = (rewind_ctx.entry_tail + 1) % rewind_ctx.entry_capacity; + rewind_ctx.entry_count -= 1; + if (rewind_ctx.entry_count==0) { + rewind_ctx.head = rewind_ctx.tail = 0; + } +} + +static int Rewind_init(size_t state_size) { + Rewind_free(); + int enable = CFG_getRewindEnable(); + int buf_mb = CFG_getRewindBufferMB(); + int gran = CFG_getRewindGranularity(); + int mute = CFG_getRewindMuteAudio(); + const char *force_path = SHARED_USERDATA_PATH "/rewind.force"; + if (exists((char*)force_path)) { + enable = 1; + LOG_info("Rewind: force enable via %s\n", force_path); + } + LOG_info("Rewind: config enable=%i bufferMB=%i granularity=%i mute=%i\n", enable, buf_mb, gran, mute); + if (!enable) { + LOG_info("Rewind: disabled via config\n"); + return 0; + } + if (!state_size) { + LOG_info("Rewind: core reported zero serialize size, disabling\n"); + return 0; + } + + size_t buffer_mb = CFG_getRewindBufferMB(); + if (buffer_mb < 1) buffer_mb = 1; + if (buffer_mb > 256) buffer_mb = 256; + + rewind_ctx.capacity = buffer_mb * 1024 * 1024; + rewind_ctx.buffer = calloc(1, rewind_ctx.capacity); + if (!rewind_ctx.buffer) { + LOG_error("Rewind: failed to allocate buffer\n"); + return 0; + } + + rewind_ctx.state_size = state_size; + rewind_ctx.state_buf = calloc(1, state_size); + if (!rewind_ctx.state_buf) { + LOG_error("Rewind: failed to allocate state buffer\n"); + Rewind_free(); + return 0; + } + + rewind_ctx.scratch_size = compressBound(state_size); + rewind_ctx.scratch = calloc(1, rewind_ctx.scratch_size); + if (!rewind_ctx.scratch) { + LOG_error("Rewind: failed to allocate scratch buffer\n"); + Rewind_free(); + return 0; + } + + int entry_cap = rewind_ctx.capacity / 4096; + if (entry_cap < 8) entry_cap = 8; + rewind_ctx.entry_capacity = entry_cap; + rewind_ctx.entries = calloc(entry_cap, sizeof(RewindEntry)); + if (!rewind_ctx.entries) { + LOG_error("Rewind: failed to allocate entry table\n"); + Rewind_free(); + return 0; + } + + rewind_ctx.granularity = CFG_getRewindGranularity(); + if (rewind_ctx.granularity < 1) rewind_ctx.granularity = 1; + rewind_ctx.mute_audio = CFG_getRewindMuteAudio(); + rewind_ctx.enabled = 1; + + LOG_info("Rewind: enabled (%zu bytes buffer, granularity %i)\n", rewind_ctx.capacity, rewind_ctx.granularity); + return 1; +} + +static void Rewind_push(int force) { + if (!rewind_ctx.enabled) return; + if (!rewind_ctx.buffer || !rewind_ctx.state_buf) return; + + if (!force) { + rewind_ctx.frame_counter += 1; + if (rewind_ctx.frame_counter < rewind_ctx.granularity) return; + rewind_ctx.frame_counter = 0; + } else { + rewind_ctx.frame_counter = 0; + } + + if (!core.serialize || !core.serialize_size) return; + + if (!core.serialize(rewind_ctx.state_buf, rewind_ctx.state_size)) { + LOG_error("Rewind: serialize failed\n"); + return; + } + + uLongf dest_len = rewind_ctx.scratch_size; + int res = compress2(rewind_ctx.scratch, &dest_len, rewind_ctx.state_buf, rewind_ctx.state_size, Z_BEST_SPEED); + if (res != Z_OK) { + LOG_error("Rewind: compression failed (%i)\n", res); + return; + } + + if (dest_len >= rewind_ctx.capacity) { + LOG_error("Rewind: state does not fit in buffer\n"); + return; + } + + // wrap write position if needed + if (rewind_ctx.head + dest_len > rewind_ctx.capacity) { + rewind_ctx.head = 0; + if (rewind_ctx.entry_count==0) rewind_ctx.tail = 0; + } + + // make room + while (Rewind_free_space() <= dest_len) { + Rewind_drop_oldest(); + } + + memcpy(rewind_ctx.buffer + rewind_ctx.head, rewind_ctx.scratch, dest_len); + + RewindEntry *e = &rewind_ctx.entries[rewind_ctx.entry_head]; + e->offset = rewind_ctx.head; + e->size = dest_len; + + rewind_ctx.head += dest_len; + if (rewind_ctx.head >= rewind_ctx.capacity) rewind_ctx.head = 0; + + rewind_ctx.entry_head = (rewind_ctx.entry_head + 1) % rewind_ctx.entry_capacity; + if (rewind_ctx.entry_count < rewind_ctx.entry_capacity) rewind_ctx.entry_count += 1; + else Rewind_drop_oldest(); + rewind_warn_empty = 0; +} + +static bool Rewind_step_back(void) { + if (!rewind_ctx.enabled) return false; + if (!rewind_ctx.entry_count) { + if (!rewind_warn_empty) { + LOG_info("Rewind: no buffered states yet\n"); + rewind_warn_empty = 1; + } + return false; + } + + int idx = rewind_ctx.entry_head - 1; + if (idx < 0) idx += rewind_ctx.entry_capacity; + RewindEntry *e = &rewind_ctx.entries[idx]; + + uLongf dest_len = rewind_ctx.state_size; + int res = uncompress(rewind_ctx.state_buf, &dest_len, rewind_ctx.buffer + e->offset, e->size); + if (res != Z_OK || dest_len < rewind_ctx.state_size) { + LOG_error("Rewind: decompress failed (%i)\n", res); + Rewind_drop_oldest(); + return false; + } + + if (!core.unserialize(rewind_ctx.state_buf, rewind_ctx.state_size)) { + LOG_error("Rewind: unserialize failed\n"); + Rewind_drop_oldest(); + return false; + } + + // pop newest + rewind_ctx.entry_head = idx; + rewind_ctx.entry_count -= 1; + if (rewind_ctx.entry_count==0) { + rewind_ctx.head = rewind_ctx.tail = 0; + } + rewinding = 1; + LOG_info("Rewind: stepped back, entries remaining %i\n", rewind_ctx.entry_count); + return true; +} + +static void Rewind_on_state_change(void) { + Rewind_reset(); + Rewind_push(1); + LOG_info("Rewind: state changed, buffer re-seeded\n"); +} + + /////////////////////////////// typedef struct Option { char* key; @@ -1391,6 +1637,7 @@ enum { SHORTCUT_CYCLE_EFFECT, SHORTCUT_TOGGLE_FF, SHORTCUT_HOLD_FF, + SHORTCUT_HOLD_REWIND, SHORTCUT_GAMESWITCHER, SHORTCUT_SCREENSHOT, // Trimui only @@ -1951,10 +2198,11 @@ static struct Config { [SHORTCUT_SAVE_QUIT] = {"Save & Quit", -1, BTN_ID_NONE, 0}, [SHORTCUT_CYCLE_SCALE] = {"Cycle Scaling", -1, BTN_ID_NONE, 0}, [SHORTCUT_CYCLE_EFFECT] = {"Cycle Effect", -1, BTN_ID_NONE, 0}, - [SHORTCUT_TOGGLE_FF] = {"Toggle FF", -1, BTN_ID_NONE, 0}, - [SHORTCUT_HOLD_FF] = {"Hold FF", -1, BTN_ID_NONE, 0}, - [SHORTCUT_GAMESWITCHER] = {"Game Switcher", -1, BTN_ID_NONE, 0}, - [SHORTCUT_SCREENSHOT] = {"Screenshot", -1, BTN_ID_NONE, 0}, + [SHORTCUT_TOGGLE_FF] = {"Toggle FF", -1, BTN_ID_NONE, 0}, + [SHORTCUT_HOLD_FF] = {"Hold FF", -1, BTN_ID_NONE, 0}, + [SHORTCUT_HOLD_REWIND] = {"Hold Rewind", -1, BTN_ID_NONE, 0}, + [SHORTCUT_GAMESWITCHER] = {"Game Switcher", -1, BTN_ID_NONE, 0}, + [SHORTCUT_SCREENSHOT] = {"Screenshot", -1, BTN_ID_NONE, 0}, // Trimui only [SHORTCUT_TOGGLE_TURBO_A] = {"Toggle Turbo A", -1, BTN_ID_NONE, 0}, [SHORTCUT_TOGGLE_TURBO_B] = {"Toggle Turbo B", -1, BTN_ID_NONE, 0}, @@ -3173,7 +3421,7 @@ static void input_poll_callback(void) { GFX_clear(screen); } - + if (PAD_justPressed(BTN_POWER)) { } @@ -3182,6 +3430,7 @@ static void input_poll_callback(void) { } static int toggled_ff_on = 0; // this logic only works because TOGGLE_FF is before HOLD_FF in the menu... + rewind_pressed = 0; for (int i=0; ilocal; @@ -3198,7 +3447,7 @@ static void input_poll_callback(void) { break; } } - else if (i==SHORTCUT_HOLD_FF) { + else if (i==SHORTCUT_HOLD_FF) { // don't allow turn off fast_forward with a release of the hold button // if it was initially turned on with the toggle button if (PAD_justPressed(btn) || (!toggled_ff_on && PAD_justReleased(btn))) { @@ -3206,9 +3455,17 @@ static void input_poll_callback(void) { if (mapping->mod) ignore_menu = 1; // very unlikely but just in case } } + else if (i==SHORTCUT_HOLD_REWIND) { + rewind_pressed = PAD_isPressed(btn); + if (rewind_pressed != last_rewind_pressed) { + LOG_info("Rewind hotkey %s\n", rewind_pressed ? "pressed" : "released"); + last_rewind_pressed = rewind_pressed; + } + if (mapping->mod && rewind_pressed) ignore_menu = 1; + } // Trimui only else if (PLAT_canTurbo() && i>=SHORTCUT_TOGGLE_TURBO_A && i<=SHORTCUT_TOGGLE_TURBO_R2) { - if (PAD_justPressed(btn)) { + if (PAD_justPressed(btn)) { switch(i) { case SHORTCUT_TOGGLE_TURBO_A: PLAT_toggleTurbo(BTN_ID_A); break; case SHORTCUT_TOGGLE_TURBO_B: PLAT_toggleTurbo(BTN_ID_B); break; @@ -4760,6 +5017,7 @@ static void video_refresh_callback(const void* data, unsigned width, unsigned he /////////////////////////////// static void audio_sample_callback(int16_t left, int16_t right) { + if (rewinding && rewind_ctx.mute_audio) return; if (!fast_forward || ff_audio) { if (use_core_fps || fast_forward) { SND_batchSamples_fixed_rate(&(const SND_Frame){left,right}, 1); @@ -4770,6 +5028,7 @@ static void audio_sample_callback(int16_t left, int16_t right) { } } static size_t audio_sample_batch_callback(const int16_t *data, size_t frames) { + if (rewinding && rewind_ctx.mute_audio) return frames; if (!fast_forward || ff_audio) { if (use_core_fps || fast_forward) { return SND_batchSamples_fixed_rate((const SND_Frame*)data, frames); @@ -4920,6 +5179,7 @@ void Core_load(void) { } void Core_reset(void) { core.reset(); + Rewind_on_state_change(); } void Core_unload(void) { // Disabling this is a dumb hack for bluetooth, we should really be using @@ -6533,11 +6793,11 @@ static void Menu_saveState(void) { } static void Menu_loadState(void) { Menu_updateState(); - - if (menu.save_exists) { - if (menu.total_discs) { - char slot_disc_name[256]; - getFile(menu.txt_path, slot_disc_name, 256); + + if (menu.save_exists) { + if (menu.total_discs) { + char slot_disc_name[256]; + getFile(menu.txt_path, slot_disc_name, 256); char slot_disc_path[256]; if (slot_disc_name[0]=='/') strcpy(slot_disc_path, slot_disc_name); @@ -6548,12 +6808,13 @@ static void Menu_loadState(void) { Game_changeDisc(slot_disc_path); } } - - state_slot = menu.slot; - putInt(menu.slot_path, menu.slot); - State_read(); + + state_slot = menu.slot; + putInt(menu.slot_path, menu.slot); + State_read(); + Rewind_on_state_change(); + } } -} static void Menu_loop(void) { @@ -7120,6 +7381,8 @@ int main(int argc , char* argv[]) { Menu_init(); State_resume(); Menu_initState(); // make ready for state shortcuts + Rewind_init(core.serialize_size()); + Rewind_on_state_change(); PWR_warn(1); PWR_disableAutosleep(); @@ -7154,14 +7417,27 @@ int main(int argc , char* argv[]) { // release config when all is loaded Config_free(); - LOG_info("total startup time %ims\n\n",SDL_GetTicks()); - while (!quit) { - GFX_startFrame(); - - core.run(); - limitFF(); - trackFPS(); + LOG_info("total startup time %ims\n\n",SDL_GetTicks()); + while (!quit) { + GFX_startFrame(); + if (rewind_pressed) { + bool did_rewind = Rewind_step_back(); + rewinding = did_rewind; + if (did_rewind) fast_forward = 0; + core.run(); // render from the restored state + if (!did_rewind) { + Rewind_push(0); + } + } + else { + rewinding = 0; + core.run(); + Rewind_push(0); + } + limitFF(); + trackFPS(); + if (has_pending_opt_change) { has_pending_opt_change = 0; @@ -7211,10 +7487,11 @@ int main(int argc , char* argv[]) { Menu_quit(); QuitSettings(); - + finish: Game_close(); + Rewind_free(); Core_unload(); Core_quit(); Core_close(); From 6aa6600404c74d9771ff1fd1c4dfa86821988fd9 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Tue, 9 Dec 2025 21:43:07 +0100 Subject: [PATCH 02/29] feat(rewind): enhance rewind functionality with configurable options --- workspace/all/minarch/minarch.c | 159 +++++++++++++++++++++++++++----- 1 file changed, 137 insertions(+), 22 deletions(-) diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index e24eb1615..92dac3e69 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -72,7 +72,12 @@ static int max_ff_speed = 3; // 4x static int ff_audio = 0; static int fast_forward = 0; static int rewind_pressed = 0; +static int rewind_toggle = 0; static int rewinding = 0; +static int rewind_cfg_enable = CFG_DEFAULT_REWIND_ENABLE; +static int rewind_cfg_buffer_mb = CFG_DEFAULT_REWIND_BUFFER_MB; +static int rewind_cfg_granularity = CFG_DEFAULT_REWIND_GRANULARITY; +static int rewind_cfg_mute_audio = CFG_DEFAULT_REWIND_MUTE_AUDIO; static int overclock = 3; // auto static int has_custom_controllers = 0; static int gamepad_type = 0; // index in gamepad_labels/gamepad_values @@ -1171,10 +1176,10 @@ static void Rewind_drop_oldest(void) { static int Rewind_init(size_t state_size) { Rewind_free(); - int enable = CFG_getRewindEnable(); - int buf_mb = CFG_getRewindBufferMB(); - int gran = CFG_getRewindGranularity(); - int mute = CFG_getRewindMuteAudio(); + int enable = rewind_cfg_enable; + int buf_mb = rewind_cfg_buffer_mb; + int gran = rewind_cfg_granularity; + int mute = rewind_cfg_mute_audio; const char *force_path = SHARED_USERDATA_PATH "/rewind.force"; if (exists((char*)force_path)) { enable = 1; @@ -1190,7 +1195,7 @@ static int Rewind_init(size_t state_size) { return 0; } - size_t buffer_mb = CFG_getRewindBufferMB(); + size_t buffer_mb = buf_mb; if (buffer_mb < 1) buffer_mb = 1; if (buffer_mb > 256) buffer_mb = 256; @@ -1227,9 +1232,9 @@ static int Rewind_init(size_t state_size) { return 0; } - rewind_ctx.granularity = CFG_getRewindGranularity(); + rewind_ctx.granularity = gran; if (rewind_ctx.granularity < 1) rewind_ctx.granularity = 1; - rewind_ctx.mute_audio = CFG_getRewindMuteAudio(); + rewind_ctx.mute_audio = mute; rewind_ctx.enabled = 1; LOG_info("Rewind: enabled (%zu bytes buffer, granularity %i)\n", rewind_ctx.capacity, rewind_ctx.granularity); @@ -1392,6 +1397,26 @@ static char* resample_labels[] = { "Max", NULL }; +static char* rewind_enable_labels[] = { + "Off", + "On", + NULL +}; +static char* rewind_buffer_labels[] = { + "8", + "16", + "32", + "64", + NULL +}; +static char* rewind_granularity_labels[] = { + "1", + "2", + "3", + "4", + "5", + NULL +}; static char* ambient_labels[] = { "Off", "All", @@ -1625,6 +1650,10 @@ enum { FE_OPT_DEBUG, FE_OPT_MAXFF, FE_OPT_FF_AUDIO, + FE_OPT_REWIND_ENABLE, + FE_OPT_REWIND_BUFFER, + FE_OPT_REWIND_GRANULARITY, + FE_OPT_REWIND_MUTE, FE_OPT_COUNT, }; @@ -1638,6 +1667,7 @@ enum { SHORTCUT_TOGGLE_FF, SHORTCUT_HOLD_FF, SHORTCUT_HOLD_REWIND, + SHORTCUT_TOGGLE_REWIND, SHORTCUT_GAMESWITCHER, SHORTCUT_SCREENSHOT, // Trimui only @@ -1988,6 +2018,46 @@ static struct Config { .values = onoff_labels, .labels = onoff_labels, }, + [FE_OPT_REWIND_ENABLE] = { + .key = "minarch_rewind_enable", + .name = "Rewind", + .desc = "Enable in-memory rewind buffer.", + .default_value = CFG_DEFAULT_REWIND_ENABLE ? 1 : 0, + .value = CFG_DEFAULT_REWIND_ENABLE ? 1 : 0, + .count = 2, + .values = rewind_enable_labels, + .labels = rewind_enable_labels, + }, + [FE_OPT_REWIND_BUFFER] = { + .key = "minarch_rewind_buffer_mb", + .name = "Rewind Buffer (MB)", + .desc = "Memory reserved for rewind snapshots.", + .default_value = 1, // 16MB + .value = 1, + .count = 4, + .values = rewind_buffer_labels, + .labels = rewind_buffer_labels, + }, + [FE_OPT_REWIND_GRANULARITY] = { + .key = "minarch_rewind_granularity", + .name = "Rewind Granularity", + .desc = "Frames between rewind snapshots.", + .default_value = 0, // 1 frame + .value = 0, + .count = 5, + .values = rewind_granularity_labels, + .labels = rewind_granularity_labels, + }, + [FE_OPT_REWIND_MUTE] = { + .key = "minarch_rewind_mute_audio", + .name = "Rewind Mute Audio", + .desc = "Mute audio while rewinding.", + .default_value = CFG_DEFAULT_REWIND_MUTE_AUDIO ? 1 : 0, + .value = CFG_DEFAULT_REWIND_MUTE_AUDIO ? 1 : 0, + .count = 2, + .values = onoff_labels, + .labels = onoff_labels, + }, [FE_OPT_COUNT] = {NULL} } }, @@ -2201,6 +2271,7 @@ static struct Config { [SHORTCUT_TOGGLE_FF] = {"Toggle FF", -1, BTN_ID_NONE, 0}, [SHORTCUT_HOLD_FF] = {"Hold FF", -1, BTN_ID_NONE, 0}, [SHORTCUT_HOLD_REWIND] = {"Hold Rewind", -1, BTN_ID_NONE, 0}, + [SHORTCUT_TOGGLE_REWIND] = {"Toggle Rewind", -1, BTN_ID_NONE, 0}, [SHORTCUT_GAMESWITCHER] = {"Game Switcher", -1, BTN_ID_NONE, 0}, [SHORTCUT_SCREENSHOT] = {"Screenshot", -1, BTN_ID_NONE, 0}, // Trimui only @@ -2347,9 +2418,40 @@ static void Config_syncFrontend(char* key, int value) { ff_audio = value; i = FE_OPT_FF_AUDIO; } + else if (exactMatch(key,config.frontend.options[FE_OPT_REWIND_ENABLE].key)) { + i = FE_OPT_REWIND_ENABLE; + } + else if (exactMatch(key,config.frontend.options[FE_OPT_REWIND_BUFFER].key)) { + i = FE_OPT_REWIND_BUFFER; + } + else if (exactMatch(key,config.frontend.options[FE_OPT_REWIND_GRANULARITY].key)) { + i = FE_OPT_REWIND_GRANULARITY; + } + else if (exactMatch(key,config.frontend.options[FE_OPT_REWIND_MUTE].key)) { + i = FE_OPT_REWIND_MUTE; + } if (i==-1) return; Option* option = &config.frontend.options[i]; option->value = value; + if (i==FE_OPT_REWIND_ENABLE || i==FE_OPT_REWIND_BUFFER || i==FE_OPT_REWIND_GRANULARITY || i==FE_OPT_REWIND_MUTE) { + const char* sval = option->values && option->values[value] ? option->values[value] : "0"; + int parsed = 0; + if (i==FE_OPT_REWIND_ENABLE || i==FE_OPT_REWIND_MUTE) { + // use option index (Off/On) + parsed = value; + } + else { + parsed = strtol(sval, NULL, 10); + } + switch (i) { + case FE_OPT_REWIND_ENABLE: rewind_cfg_enable = parsed; break; + case FE_OPT_REWIND_BUFFER: rewind_cfg_buffer_mb = parsed; break; + case FE_OPT_REWIND_GRANULARITY: rewind_cfg_granularity = parsed; break; + case FE_OPT_REWIND_MUTE: rewind_cfg_mute_audio = parsed; break; + } + Rewind_init(core.serialize_size ? core.serialize_size() : 0); + if (core.initialized) Rewind_on_state_change(); + } } char** list_files_in_folder(const char* folderPath, int* fileCount, const char* extensionFilter) { @@ -3455,16 +3557,28 @@ static void input_poll_callback(void) { if (mapping->mod) ignore_menu = 1; // very unlikely but just in case } } - else if (i==SHORTCUT_HOLD_REWIND) { - rewind_pressed = PAD_isPressed(btn); - if (rewind_pressed != last_rewind_pressed) { - LOG_info("Rewind hotkey %s\n", rewind_pressed ? "pressed" : "released"); - last_rewind_pressed = rewind_pressed; + else if (i==SHORTCUT_HOLD_REWIND) { + rewind_pressed = PAD_isPressed(btn); + if (rewind_pressed != last_rewind_pressed) { + LOG_info("Rewind hotkey %s\n", rewind_pressed ? "pressed" : "released"); + last_rewind_pressed = rewind_pressed; + } + if (mapping->mod && rewind_pressed) ignore_menu = 1; } - if (mapping->mod && rewind_pressed) ignore_menu = 1; - } - // Trimui only - else if (PLAT_canTurbo() && i>=SHORTCUT_TOGGLE_TURBO_A && i<=SHORTCUT_TOGGLE_TURBO_R2) { + else if (i==SHORTCUT_TOGGLE_REWIND) { + if (PAD_justPressed(btn)) { + rewind_toggle = !rewind_toggle; + LOG_info("Rewind toggle %s\n", rewind_toggle ? "on" : "off"); + if (mapping->mod) ignore_menu = 1; + break; + } + else if (PAD_justReleased(btn)) { + if (mapping->mod) ignore_menu = 1; + break; + } + } + // Trimui only + else if (PLAT_canTurbo() && i>=SHORTCUT_TOGGLE_TURBO_A && i<=SHORTCUT_TOGGLE_TURBO_R2) { if (PAD_justPressed(btn)) { switch(i) { case SHORTCUT_TOGGLE_TURBO_A: PLAT_toggleTurbo(BTN_ID_A); break; @@ -7421,12 +7535,13 @@ int main(int argc , char* argv[]) { while (!quit) { GFX_startFrame(); - if (rewind_pressed) { - bool did_rewind = Rewind_step_back(); - rewinding = did_rewind; - if (did_rewind) fast_forward = 0; - core.run(); // render from the restored state - if (!did_rewind) { + int do_rewind = rewind_pressed || rewind_toggle; + if (do_rewind) { + bool did_rewind = Rewind_step_back(); + rewinding = did_rewind; + if (did_rewind) fast_forward = 0; + core.run(); // render from the restored state + if (!did_rewind) { Rewind_push(0); } } From 688c8b4429f10af968d50b8a66f04f8d276b50b1 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Tue, 9 Dec 2025 22:49:37 +0100 Subject: [PATCH 03/29] feat(rewind): enhance fast forward and rewind interaction handling --- workspace/all/minarch/minarch.c | 143 +++++++++++++++++++++++--------- 1 file changed, 106 insertions(+), 37 deletions(-) diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 92dac3e69..8cfe286af 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -73,6 +73,9 @@ static int ff_audio = 0; static int fast_forward = 0; static int rewind_pressed = 0; static int rewind_toggle = 0; +static int ff_toggled = 0; +static int ff_hold_active = 0; +static int ff_paused_by_rewind_hold = 0; static int rewinding = 0; static int rewind_cfg_enable = CFG_DEFAULT_REWIND_ENABLE; static int rewind_cfg_buffer_mb = CFG_DEFAULT_REWIND_BUFFER_MB; @@ -1176,6 +1179,7 @@ static void Rewind_drop_oldest(void) { static int Rewind_init(size_t state_size) { Rewind_free(); + // pull current option values directly int enable = rewind_cfg_enable; int buf_mb = rewind_cfg_buffer_mb; int gran = rewind_cfg_granularity; @@ -2450,6 +2454,13 @@ static void Config_syncFrontend(char* key, int value) { case FE_OPT_REWIND_MUTE: rewind_cfg_mute_audio = parsed; break; } Rewind_init(core.serialize_size ? core.serialize_size() : 0); + if (i==FE_OPT_REWIND_ENABLE) { + // ensure runtime toggles don't linger when enabling/disabling feature + rewind_toggle = 0; + rewind_pressed = 0; + rewinding = 0; + ff_paused_by_rewind_hold = 0; + } if (core.initialized) Rewind_on_state_change(); } } @@ -3495,8 +3506,12 @@ static void Menu_saveState(void); static void Menu_loadState(void); static int setFastForward(int enable) { - fast_forward = enable; - return enable; + int val = enable ? 1 : 0; + if (fast_forward != val) { + LOG_info("FF state -> %i\n", val); + } + fast_forward = val; + return val; } static uint32_t buttons = 0; // RETRO_DEVICE_ID_JOYPAD_* buttons @@ -3541,6 +3556,14 @@ static void input_poll_callback(void) { if (i==SHORTCUT_TOGGLE_FF) { if (PAD_justPressed(btn)) { toggled_ff_on = setFastForward(!fast_forward); + ff_toggled = toggled_ff_on; + ff_hold_active = 0; + if (ff_toggled && rewind_toggle) { + // last toggle wins: disable rewind toggle when FF toggle is enabled + rewind_toggle = 0; + rewind_pressed = 0; + rewinding = 0; + } if (mapping->mod) ignore_menu = 1; break; } @@ -3549,31 +3572,50 @@ static void input_poll_callback(void) { break; } } - else if (i==SHORTCUT_HOLD_FF) { - // don't allow turn off fast_forward with a release of the hold button - // if it was initially turned on with the toggle button - if (PAD_justPressed(btn) || (!toggled_ff_on && PAD_justReleased(btn))) { - fast_forward = setFastForward(PAD_isPressed(btn)); - if (mapping->mod) ignore_menu = 1; // very unlikely but just in case + else if (i==SHORTCUT_HOLD_FF) { + // don't allow turn off fast_forward with a release of the hold button + // if it was initially turned on with the toggle button + if (PAD_justPressed(btn) || (!toggled_ff_on && PAD_justReleased(btn))) { + int pressed = PAD_isPressed(btn); + fast_forward = setFastForward(pressed); + ff_hold_active = pressed ? 1 : 0; + if (mapping->mod) ignore_menu = 1; // very unlikely but just in case + } + if (PAD_justReleased(btn) && toggled_ff_on) { + ff_hold_active = 0; + } + } + else if (i==SHORTCUT_HOLD_REWIND) { + rewind_pressed = PAD_isPressed(btn) ? 1 : 0; + if (rewind_pressed != last_rewind_pressed) { + LOG_info("Rewind hotkey %s\n", rewind_pressed ? "pressed" : "released"); + last_rewind_pressed = rewind_pressed; + } + if (rewind_pressed && ff_toggled && !ff_paused_by_rewind_hold) { + ff_paused_by_rewind_hold = 1; + fast_forward = setFastForward(0); } + else if (!rewind_pressed && ff_paused_by_rewind_hold) { + ff_paused_by_rewind_hold = 0; + if (ff_toggled) fast_forward = setFastForward(1); + } + if (mapping->mod && rewind_pressed) ignore_menu = 1; } - else if (i==SHORTCUT_HOLD_REWIND) { - rewind_pressed = PAD_isPressed(btn); - if (rewind_pressed != last_rewind_pressed) { - LOG_info("Rewind hotkey %s\n", rewind_pressed ? "pressed" : "released"); - last_rewind_pressed = rewind_pressed; + else if (i==SHORTCUT_TOGGLE_REWIND) { + if (PAD_justPressed(btn)) { + rewind_toggle = !rewind_toggle; + LOG_info("Rewind toggle %s\n", rewind_toggle ? "on" : "off"); + if (rewind_toggle && ff_toggled) { + // disable fast forward toggle when rewinding is toggled on + ff_toggled = 0; + fast_forward = setFastForward(0); + ff_paused_by_rewind_hold = 0; } - if (mapping->mod && rewind_pressed) ignore_menu = 1; + if (mapping->mod) ignore_menu = 1; + break; } - else if (i==SHORTCUT_TOGGLE_REWIND) { - if (PAD_justPressed(btn)) { - rewind_toggle = !rewind_toggle; - LOG_info("Rewind toggle %s\n", rewind_toggle ? "on" : "off"); - if (mapping->mod) ignore_menu = 1; - break; - } - else if (PAD_justReleased(btn)) { - if (mapping->mod) ignore_menu = 1; + else if (PAD_justReleased(btn)) { + if (mapping->mod) ignore_menu = 1; break; } } @@ -7495,8 +7537,6 @@ int main(int argc , char* argv[]) { Menu_init(); State_resume(); Menu_initState(); // make ready for state shortcuts - Rewind_init(core.serialize_size()); - Rewind_on_state_change(); PWR_warn(1); PWR_disableAutosleep(); @@ -7528,6 +7568,8 @@ int main(int argc , char* argv[]) { initShaders(); Config_readOptions(); applyShaderSettings(); + Rewind_init(core.serialize_size()); + Rewind_on_state_change(); // release config when all is loaded Config_free(); @@ -7535,21 +7577,48 @@ int main(int argc , char* argv[]) { while (!quit) { GFX_startFrame(); - int do_rewind = rewind_pressed || rewind_toggle; - if (do_rewind) { - bool did_rewind = Rewind_step_back(); - rewinding = did_rewind; - if (did_rewind) fast_forward = 0; + // if rewind is toggled, fast-forward toggle must stay off; fast-forward hold pauses rewind + int do_rewind = (rewind_pressed || rewind_toggle) && !(rewind_toggle && ff_hold_active); + if (do_rewind) { + bool did_rewind = Rewind_step_back(); + rewinding = did_rewind; + if (did_rewind) { + fast_forward = 0; + } + else { + // buffer empty: auto untoggle rewind, resume FF if it was paused for a hold + if (rewind_toggle) rewind_toggle = 0; + if (ff_paused_by_rewind_hold && ff_toggled) { + ff_paused_by_rewind_hold = 0; + fast_forward = setFastForward(1); + } + } core.run(); // render from the restored state if (!did_rewind) { - Rewind_push(0); - } - } - else { - rewinding = 0; - core.run(); - Rewind_push(0); + Rewind_push(0); + } } + else { + rewinding = 0; + if (ff_paused_by_rewind_hold && !rewind_pressed) { + // resume fast forward after hold rewind ends + if (ff_toggled) fast_forward = setFastForward(1); + ff_paused_by_rewind_hold = 0; + } + + int ff_runs = 1; + if (fast_forward) { + // when "None" is selected, assume a modest 2x instead of unbounded spam + ff_runs = max_ff_speed ? max_ff_speed + 1 : 2; + } + + for (int ff_step = 0; ff_step < ff_runs; ff_step++) { + core.run(); + if (!fast_forward) { + Rewind_push(0); + } + } + } limitFF(); trackFPS(); From df6ec12e4dfd018dbd914dec7f43241b556e1152 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Wed, 10 Dec 2025 20:19:06 +0100 Subject: [PATCH 04/29] feat(rewind): first phase of the threading implementation --- workspace/all/common/config.c | 4 +- workspace/all/common/config.h | 2 +- workspace/all/minarch/minarch.c | 406 +++++++++++++++++++++++++++----- 3 files changed, 356 insertions(+), 56 deletions(-) diff --git a/workspace/all/common/config.c b/workspace/all/common/config.c index a874158c2..cde71daea 100644 --- a/workspace/all/common/config.c +++ b/workspace/all/common/config.c @@ -636,7 +636,9 @@ int CFG_getRewindGranularity(void) void CFG_setRewindGranularity(int granularity) { - settings.rewindGranularity = clamp(granularity, 1, 60); + // Granularity is interpreted as milliseconds once it exceeds the legacy + // frame-based range, so allow a wider range to cover slower cadences. + settings.rewindGranularity = clamp(granularity, 1, 2000); CFG_sync(); } diff --git a/workspace/all/common/config.h b/workspace/all/common/config.h index 9db34bece..2d88e9b0b 100644 --- a/workspace/all/common/config.h +++ b/workspace/all/common/config.h @@ -147,7 +147,7 @@ typedef struct #define CFG_DEFAULT_EXTRACTEDFILENAME false #define CFG_DEFAULT_REWIND_ENABLE false #define CFG_DEFAULT_REWIND_BUFFER_MB 16 -#define CFG_DEFAULT_REWIND_GRANULARITY 1 +#define CFG_DEFAULT_REWIND_GRANULARITY 300 #define CFG_DEFAULT_REWIND_MUTE_AUDIO true #define CFG_DEFAULT_MUTELEDS false #define CFG_DEFAULT_GAMEARTWIDTH 0.45 diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 8cfe286af..5118bcd1e 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -1130,43 +1130,130 @@ typedef struct { uint8_t *scratch; size_t scratch_size; - int granularity; + int granularity_frames; + int interval_ms; + uint32_t last_push_ms; + int use_time_cadence; int frame_counter; + unsigned int generation; int enabled; int mute_audio; + + // async capture/compression + pthread_t worker; + pthread_mutex_t lock; + pthread_mutex_t queue_mx; + pthread_cond_t queue_cv; + int worker_stop; + int worker_running; + int drop_warned; + int locks_ready; + + uint8_t **capture_pool; + unsigned int *capture_gen; + uint8_t *capture_busy; + int pool_size; + int free_count; + int *free_stack; + + int queue_capacity; + int queue_head; + int queue_tail; + int queue_count; + int *queue; + + z_stream zstream; + int zstream_ready; } RewindContext; static RewindContext rewind_ctx = {0}; static int rewind_warn_empty = 0; static int last_rewind_pressed = 0; +static void* Rewind_worker_thread(void *arg); +static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len); +static int Rewind_compress_state(const uint8_t *src, uLongf *dest_len); + static void Rewind_free(void) { + if (rewind_ctx.worker_running) { + pthread_mutex_lock(&rewind_ctx.queue_mx); + rewind_ctx.worker_stop = 1; + pthread_cond_signal(&rewind_ctx.queue_cv); + pthread_mutex_unlock(&rewind_ctx.queue_mx); + pthread_join(rewind_ctx.worker, NULL); + rewind_ctx.worker_running = 0; + } + + if (rewind_ctx.zstream_ready) { + deflateEnd(&rewind_ctx.zstream); + rewind_ctx.zstream_ready = 0; + } + + if (rewind_ctx.capture_pool) { + for (int i = 0; i < rewind_ctx.pool_size; i++) { + if (rewind_ctx.capture_pool[i]) free(rewind_ctx.capture_pool[i]); + } + free(rewind_ctx.capture_pool); + } + if (rewind_ctx.capture_gen) free(rewind_ctx.capture_gen); + if (rewind_ctx.capture_busy) free(rewind_ctx.capture_busy); + if (rewind_ctx.free_stack) free(rewind_ctx.free_stack); + if (rewind_ctx.queue) free(rewind_ctx.queue); if (rewind_ctx.buffer) free(rewind_ctx.buffer); if (rewind_ctx.entries) free(rewind_ctx.entries); if (rewind_ctx.state_buf) free(rewind_ctx.state_buf); if (rewind_ctx.scratch) free(rewind_ctx.scratch); + if (rewind_ctx.locks_ready) { + pthread_mutex_destroy(&rewind_ctx.lock); + pthread_mutex_destroy(&rewind_ctx.queue_mx); + pthread_cond_destroy(&rewind_ctx.queue_cv); + } memset(&rewind_ctx, 0, sizeof(rewind_ctx)); rewinding = 0; } static void Rewind_reset(void) { if (!rewind_ctx.enabled) return; + pthread_mutex_lock(&rewind_ctx.lock); rewind_ctx.head = rewind_ctx.tail = 0; rewind_ctx.entry_head = rewind_ctx.entry_tail = rewind_ctx.entry_count = 0; + pthread_mutex_unlock(&rewind_ctx.lock); rewind_ctx.frame_counter = 0; + rewind_ctx.last_push_ms = 0; + rewind_ctx.generation += 1; + rewind_ctx.drop_warned = 0; + rewind_ctx.worker_stop = 0; + if (!rewind_ctx.generation) rewind_ctx.generation = 1; // avoid zero if it wrapped + // clear pending async work so new snapshots don't mix with stale ones + if (rewind_ctx.pool_size) { + pthread_mutex_lock(&rewind_ctx.queue_mx); + while (rewind_ctx.queue_count > 0) { + int slot = rewind_ctx.queue[rewind_ctx.queue_head]; + rewind_ctx.queue_head = (rewind_ctx.queue_head + 1) % rewind_ctx.queue_capacity; + rewind_ctx.queue_count -= 1; + rewind_ctx.capture_busy[slot] = 0; + } + rewind_ctx.queue_head = rewind_ctx.queue_tail = 0; + rewind_ctx.free_count = 0; + for (int i = 0; i < rewind_ctx.pool_size; i++) { + if (!rewind_ctx.capture_busy[i] && rewind_ctx.free_count < rewind_ctx.pool_size) { + rewind_ctx.free_stack[rewind_ctx.free_count++] = i; + } + } + pthread_mutex_unlock(&rewind_ctx.queue_mx); + } rewinding = 0; rewind_warn_empty = 0; } -static size_t Rewind_free_space(void) { +static size_t Rewind_free_space_locked(void) { if (rewind_ctx.entry_count>0 && rewind_ctx.head==rewind_ctx.tail) return 0; if (rewind_ctx.head >= rewind_ctx.tail) return rewind_ctx.capacity - (rewind_ctx.head - rewind_ctx.tail); else return rewind_ctx.tail - rewind_ctx.head; } - -static void Rewind_drop_oldest(void) { +static void Rewind_drop_oldest_locked(void) { if (!rewind_ctx.entry_count) return; RewindEntry *e = &rewind_ctx.entries[rewind_ctx.entry_tail]; rewind_ctx.tail = (e->offset + e->size) % rewind_ctx.capacity; @@ -1176,6 +1263,76 @@ static void Rewind_drop_oldest(void) { rewind_ctx.head = rewind_ctx.tail = 0; } } +static void Rewind_drop_oldest(void) { + pthread_mutex_lock(&rewind_ctx.lock); + Rewind_drop_oldest_locked(); + pthread_mutex_unlock(&rewind_ctx.lock); +} + +static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len) { + if (dest_len >= rewind_ctx.capacity) { + LOG_error("Rewind: state does not fit in buffer\n"); + return 0; + } + + // wrap write position if needed + if (rewind_ctx.head + dest_len > rewind_ctx.capacity) { + rewind_ctx.head = 0; + if (rewind_ctx.entry_count==0) rewind_ctx.tail = 0; + } + + // make room + while (Rewind_free_space_locked() <= dest_len) { + Rewind_drop_oldest_locked(); + } + + memcpy(rewind_ctx.buffer + rewind_ctx.head, compressed, dest_len); + + RewindEntry *e = &rewind_ctx.entries[rewind_ctx.entry_head]; + e->offset = rewind_ctx.head; + e->size = dest_len; + + rewind_ctx.head += dest_len; + if (rewind_ctx.head >= rewind_ctx.capacity) rewind_ctx.head = 0; + + rewind_ctx.entry_head = (rewind_ctx.entry_head + 1) % rewind_ctx.entry_capacity; + if (rewind_ctx.entry_count < rewind_ctx.entry_capacity) rewind_ctx.entry_count += 1; + else Rewind_drop_oldest_locked(); + rewind_warn_empty = 0; + return 1; +} + +static int Rewind_compress_state(const uint8_t *src, uLongf *dest_len) { + int res = Z_OK; + if (!rewind_ctx.scratch || !dest_len) return Z_MEM_ERROR; + + if (!rewind_ctx.zstream_ready) { + memset(&rewind_ctx.zstream, 0, sizeof(rewind_ctx.zstream)); + res = deflateInit2(&rewind_ctx.zstream, Z_BEST_SPEED, Z_DEFLATED, 15, 8, Z_RLE); + if (res == Z_OK) rewind_ctx.zstream_ready = 1; + } + + if (rewind_ctx.zstream_ready) { + rewind_ctx.zstream.next_in = (Bytef *)src; + rewind_ctx.zstream.avail_in = rewind_ctx.state_size; + rewind_ctx.zstream.next_out = rewind_ctx.scratch; + rewind_ctx.zstream.avail_out = rewind_ctx.scratch_size; + + res = deflateReset(&rewind_ctx.zstream); + if (res == Z_OK) { + res = deflate(&rewind_ctx.zstream, Z_FINISH); + } + if (res == Z_STREAM_END) { + *dest_len = rewind_ctx.scratch_size - rewind_ctx.zstream.avail_out; + return Z_OK; + } + } + + // fallback to plain compress2 if the streaming path fails + *dest_len = rewind_ctx.scratch_size; + res = compress2(rewind_ctx.scratch, dest_len, src, rewind_ctx.state_size, Z_BEST_SPEED); + return res; +} static int Rewind_init(size_t state_size) { Rewind_free(); @@ -1189,7 +1346,8 @@ static int Rewind_init(size_t state_size) { enable = 1; LOG_info("Rewind: force enable via %s\n", force_path); } - LOG_info("Rewind: config enable=%i bufferMB=%i granularity=%i mute=%i\n", enable, buf_mb, gran, mute); + LOG_info("Rewind: config enable=%i bufferMB=%i interval=%i%s mute=%i\n", + enable, buf_mb, gran, gran > 60 ? "ms" : " frames", mute); if (!enable) { LOG_info("Rewind: disabled via config\n"); return 0; @@ -1236,75 +1394,202 @@ static int Rewind_init(size_t state_size) { return 0; } - rewind_ctx.granularity = gran; - if (rewind_ctx.granularity < 1) rewind_ctx.granularity = 1; + rewind_ctx.granularity_frames = gran; + rewind_ctx.interval_ms = 0; + rewind_ctx.use_time_cadence = gran > 60; + if (gran < 1) gran = 1; + if (rewind_ctx.use_time_cadence) { + rewind_ctx.interval_ms = gran; + rewind_ctx.granularity_frames = 1; + } + if (rewind_ctx.granularity_frames < 1) rewind_ctx.granularity_frames = 1; rewind_ctx.mute_audio = mute; rewind_ctx.enabled = 1; + rewind_ctx.generation = 1; + rewind_ctx.worker_stop = 0; + rewind_ctx.queue_head = rewind_ctx.queue_tail = rewind_ctx.queue_count = 0; + rewind_ctx.drop_warned = 0; + + pthread_mutex_init(&rewind_ctx.lock, NULL); + pthread_mutex_init(&rewind_ctx.queue_mx, NULL); + pthread_cond_init(&rewind_ctx.queue_cv, NULL); + rewind_ctx.locks_ready = 1; + + // set up async capture buffers + rewind_ctx.pool_size = 3; + if (state_size > 2 * 1024 * 1024) rewind_ctx.pool_size = 2; + if (rewind_ctx.pool_size < 1) rewind_ctx.pool_size = 1; + rewind_ctx.capture_pool = calloc(rewind_ctx.pool_size, sizeof(uint8_t*)); + rewind_ctx.capture_gen = calloc(rewind_ctx.pool_size, sizeof(unsigned int)); + rewind_ctx.capture_busy = calloc(rewind_ctx.pool_size, sizeof(uint8_t)); + rewind_ctx.free_stack = calloc(rewind_ctx.pool_size, sizeof(int)); + rewind_ctx.queue = calloc(rewind_ctx.pool_size, sizeof(int)); + if (!rewind_ctx.capture_pool || !rewind_ctx.capture_gen || !rewind_ctx.capture_busy || !rewind_ctx.free_stack || !rewind_ctx.queue) { + LOG_error("Rewind: failed to allocate async capture buffers\n"); + Rewind_free(); + return 0; + } + for (int i = 0; i < rewind_ctx.pool_size; i++) { + rewind_ctx.capture_pool[i] = calloc(1, state_size); + if (!rewind_ctx.capture_pool[i]) { + LOG_error("Rewind: failed to allocate capture slot %i\n", i); + Rewind_free(); + return 0; + } + rewind_ctx.free_stack[i] = i; + } + rewind_ctx.queue_capacity = rewind_ctx.pool_size; + rewind_ctx.free_count = rewind_ctx.pool_size; + + if (pthread_create(&rewind_ctx.worker, NULL, Rewind_worker_thread, NULL) != 0) { + // fallback to synchronous path + LOG_error("Rewind: failed to start worker thread, falling back to synchronous capture\n"); + rewind_ctx.pool_size = 0; + rewind_ctx.queue_capacity = 0; + rewind_ctx.free_count = 0; + } + else { + rewind_ctx.worker_running = 1; + } - LOG_info("Rewind: enabled (%zu bytes buffer, granularity %i)\n", rewind_ctx.capacity, rewind_ctx.granularity); + LOG_info("Rewind: enabled (%zu bytes buffer, cadence %i %s)\n", rewind_ctx.capacity, + rewind_ctx.use_time_cadence ? rewind_ctx.interval_ms : rewind_ctx.granularity_frames, + rewind_ctx.use_time_cadence ? "ms" : "frames"); return 1; } +static void* Rewind_worker_thread(void *arg) { + (void)arg; + + while (1) { + pthread_mutex_lock(&rewind_ctx.queue_mx); + while (!rewind_ctx.worker_stop && rewind_ctx.queue_count == 0) { + pthread_cond_wait(&rewind_ctx.queue_cv, &rewind_ctx.queue_mx); + } + if (rewind_ctx.worker_stop && rewind_ctx.queue_count == 0) { + pthread_mutex_unlock(&rewind_ctx.queue_mx); + break; + } + + int slot = rewind_ctx.queue[rewind_ctx.queue_head]; + rewind_ctx.queue_head = (rewind_ctx.queue_head + 1) % rewind_ctx.queue_capacity; + rewind_ctx.queue_count -= 1; + unsigned int gen = rewind_ctx.capture_gen[slot]; + pthread_mutex_unlock(&rewind_ctx.queue_mx); + + if (gen != rewind_ctx.generation) { + // stale snapshot, drop quietly + pthread_mutex_lock(&rewind_ctx.queue_mx); + rewind_ctx.capture_busy[slot] = 0; + rewind_ctx.free_stack[rewind_ctx.free_count++] = slot; + pthread_mutex_unlock(&rewind_ctx.queue_mx); + continue; + } + + uLongf dest_len = rewind_ctx.scratch_size; + int res = Rewind_compress_state(rewind_ctx.capture_pool[slot], &dest_len); + if (res == Z_OK) { + pthread_mutex_lock(&rewind_ctx.lock); + Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); + pthread_mutex_unlock(&rewind_ctx.lock); + } + else { + LOG_error("Rewind: compression failed (%i)\n", res); + } + + pthread_mutex_lock(&rewind_ctx.queue_mx); + rewind_ctx.capture_busy[slot] = 0; + rewind_ctx.free_stack[rewind_ctx.free_count++] = slot; + pthread_mutex_unlock(&rewind_ctx.queue_mx); + } + + return NULL; +} + static void Rewind_push(int force) { if (!rewind_ctx.enabled) return; if (!rewind_ctx.buffer || !rewind_ctx.state_buf) return; + uint32_t now_ms = SDL_GetTicks(); if (!force) { - rewind_ctx.frame_counter += 1; - if (rewind_ctx.frame_counter < rewind_ctx.granularity) return; - rewind_ctx.frame_counter = 0; + if (rewind_ctx.use_time_cadence) { + if (rewind_ctx.last_push_ms && (int)(now_ms - rewind_ctx.last_push_ms) < rewind_ctx.interval_ms) return; + rewind_ctx.last_push_ms = now_ms; + } + else { + rewind_ctx.frame_counter += 1; + if (rewind_ctx.frame_counter < rewind_ctx.granularity_frames) return; + rewind_ctx.frame_counter = 0; + } } else { rewind_ctx.frame_counter = 0; + rewind_ctx.last_push_ms = now_ms; } if (!core.serialize || !core.serialize_size) return; + if (rewind_ctx.worker_running && rewind_ctx.pool_size) { + int slot = -1; + pthread_mutex_lock(&rewind_ctx.queue_mx); + if (rewind_ctx.free_count && rewind_ctx.queue_count < rewind_ctx.queue_capacity) { + slot = rewind_ctx.free_stack[--rewind_ctx.free_count]; + rewind_ctx.capture_busy[slot] = 1; + } + pthread_mutex_unlock(&rewind_ctx.queue_mx); + + if (slot < 0) { + if (!rewind_ctx.drop_warned) { + LOG_info("Rewind: skipping snapshot (worker busy)\n"); + rewind_ctx.drop_warned = 1; + } + return; + } + + uint8_t *buf = rewind_ctx.capture_pool[slot]; + if (!core.serialize(buf, rewind_ctx.state_size)) { + LOG_error("Rewind: serialize failed\n"); + pthread_mutex_lock(&rewind_ctx.queue_mx); + rewind_ctx.capture_busy[slot] = 0; + rewind_ctx.free_stack[rewind_ctx.free_count++] = slot; + pthread_mutex_unlock(&rewind_ctx.queue_mx); + return; + } + + rewind_ctx.capture_gen[slot] = rewind_ctx.generation; + pthread_mutex_lock(&rewind_ctx.queue_mx); + rewind_ctx.queue[rewind_ctx.queue_tail] = slot; + rewind_ctx.queue_tail = (rewind_ctx.queue_tail + 1) % rewind_ctx.queue_capacity; + rewind_ctx.queue_count += 1; + pthread_cond_signal(&rewind_ctx.queue_cv); + pthread_mutex_unlock(&rewind_ctx.queue_mx); + rewind_ctx.drop_warned = 0; + return; + } + + // synchronous fallback (thread not available) if (!core.serialize(rewind_ctx.state_buf, rewind_ctx.state_size)) { LOG_error("Rewind: serialize failed\n"); return; } uLongf dest_len = rewind_ctx.scratch_size; - int res = compress2(rewind_ctx.scratch, &dest_len, rewind_ctx.state_buf, rewind_ctx.state_size, Z_BEST_SPEED); + int res = Rewind_compress_state(rewind_ctx.state_buf, &dest_len); if (res != Z_OK) { LOG_error("Rewind: compression failed (%i)\n", res); return; } - if (dest_len >= rewind_ctx.capacity) { - LOG_error("Rewind: state does not fit in buffer\n"); - return; - } - - // wrap write position if needed - if (rewind_ctx.head + dest_len > rewind_ctx.capacity) { - rewind_ctx.head = 0; - if (rewind_ctx.entry_count==0) rewind_ctx.tail = 0; - } - - // make room - while (Rewind_free_space() <= dest_len) { - Rewind_drop_oldest(); - } - - memcpy(rewind_ctx.buffer + rewind_ctx.head, rewind_ctx.scratch, dest_len); - - RewindEntry *e = &rewind_ctx.entries[rewind_ctx.entry_head]; - e->offset = rewind_ctx.head; - e->size = dest_len; - - rewind_ctx.head += dest_len; - if (rewind_ctx.head >= rewind_ctx.capacity) rewind_ctx.head = 0; - - rewind_ctx.entry_head = (rewind_ctx.entry_head + 1) % rewind_ctx.entry_capacity; - if (rewind_ctx.entry_count < rewind_ctx.entry_capacity) rewind_ctx.entry_count += 1; - else Rewind_drop_oldest(); - rewind_warn_empty = 0; + pthread_mutex_lock(&rewind_ctx.lock); + Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); + pthread_mutex_unlock(&rewind_ctx.lock); + rewind_ctx.drop_warned = 0; } static bool Rewind_step_back(void) { if (!rewind_ctx.enabled) return false; + pthread_mutex_lock(&rewind_ctx.lock); if (!rewind_ctx.entry_count) { + pthread_mutex_unlock(&rewind_ctx.lock); if (!rewind_warn_empty) { LOG_info("Rewind: no buffered states yet\n"); rewind_warn_empty = 1; @@ -1320,13 +1605,15 @@ static bool Rewind_step_back(void) { int res = uncompress(rewind_ctx.state_buf, &dest_len, rewind_ctx.buffer + e->offset, e->size); if (res != Z_OK || dest_len < rewind_ctx.state_size) { LOG_error("Rewind: decompress failed (%i)\n", res); - Rewind_drop_oldest(); + Rewind_drop_oldest_locked(); + pthread_mutex_unlock(&rewind_ctx.lock); return false; } if (!core.unserialize(rewind_ctx.state_buf, rewind_ctx.state_size)) { LOG_error("Rewind: unserialize failed\n"); - Rewind_drop_oldest(); + Rewind_drop_oldest_locked(); + pthread_mutex_unlock(&rewind_ctx.lock); return false; } @@ -1336,8 +1623,11 @@ static bool Rewind_step_back(void) { if (rewind_ctx.entry_count==0) { rewind_ctx.head = rewind_ctx.tail = 0; } + int remaining = rewind_ctx.entry_count; + pthread_mutex_unlock(&rewind_ctx.lock); + rewinding = 1; - LOG_info("Rewind: stepped back, entries remaining %i\n", rewind_ctx.entry_count); + LOG_info("Rewind: stepped back, entries remaining %i\n", remaining); return true; } @@ -1413,12 +1703,20 @@ static char* rewind_buffer_labels[] = { "64", NULL }; +static char* rewind_granularity_values[] = { + "150", + "300", + "450", + "600", + "900", + NULL +}; static char* rewind_granularity_labels[] = { - "1", - "2", - "3", - "4", - "5", + "150 ms", + "300 ms", + "450 ms", + "600 ms", + "900 ms", NULL }; static char* ambient_labels[] = { @@ -2044,12 +2342,12 @@ static struct Config { }, [FE_OPT_REWIND_GRANULARITY] = { .key = "minarch_rewind_granularity", - .name = "Rewind Granularity", - .desc = "Frames between rewind snapshots.", - .default_value = 0, // 1 frame - .value = 0, + .name = "Rewind Interval", + .desc = "Milliseconds between rewind snapshots.", + .default_value = 1, // 300ms + .value = 1, .count = 5, - .values = rewind_granularity_labels, + .values = rewind_granularity_values, .labels = rewind_granularity_labels, }, [FE_OPT_REWIND_MUTE] = { From fd692c267d66556eb6c4ad4d485d517a844e5b49 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:08:44 +0100 Subject: [PATCH 05/29] feat(rewind): integrate LZ4 compression for rewind functionality --- makefile.toolchain | 3 +- workspace/all/minarch/makefile | 2 + workspace/all/minarch/minarch.c | 92 ++++++++++++++++----------------- 3 files changed, 48 insertions(+), 49 deletions(-) diff --git a/makefile.toolchain b/makefile.toolchain index f2d5a2ba1..2fa75e12d 100644 --- a/makefile.toolchain +++ b/makefile.toolchain @@ -11,7 +11,8 @@ GUEST_WORKSPACE=/root/workspace GIT_IF_NECESSARY=toolchains/$(PLATFORM)-toolchain INIT_IF_NECESSARY=toolchains/$(PLATFORM)-toolchain/.build -IMAGE_NAME=ghcr.io/loveretro/$(PLATFORM)-toolchain:modernize +# Use local image if it exists, otherwise use remote +IMAGE_NAME=$(shell docker images -q $(PLATFORM)-toolchain:latest 2>/dev/null | head -1 | grep -q . && echo "$(PLATFORM)-toolchain:latest" || echo "ghcr.io/loveretro/$(PLATFORM)-toolchain:modernize") all: $(INIT_IF_NECESSARY) docker run -it --rm -v $(HOST_WORKSPACE):$(GUEST_WORKSPACE) $(IMAGE_NAME) /bin/bash diff --git a/workspace/all/minarch/makefile b/workspace/all/minarch/makefile index c2dd5fc40..8342565bc 100644 --- a/workspace/all/minarch/makefile +++ b/workspace/all/minarch/makefile @@ -28,6 +28,7 @@ CC = $(CROSS_COMPILE)gcc CFLAGS += $(OPT) -fomit-frame-pointer CFLAGS += $(INCDIR) -DPLATFORM=\"$(PLATFORM)\" -std=gnu99 LDFLAGS += -lmsettings -lsamplerate +LDFLAGS += -llz4 ifeq ($(PLATFORM), desktop) ifeq ($(UNAME_S),Linux) CFLAGS += `pkg-config --cflags libzip` @@ -68,6 +69,7 @@ all: clean libretro-common libsrm.a $(PREFIX_LOCAL)/include/msettings.h cp $(PREFIX)/lib/libbz2.so.1.0 build/$(PLATFORM) cp $(PREFIX)/lib/liblzma.so.5 build/$(PLATFORM) cp $(PREFIX)/lib/libzstd.so.1 build/$(PLATFORM) + cp $(PREFIX)/lib/liblz4.so.1 build/$(PLATFORM) $(CC) $(SOURCE) -o $(PRODUCT) $(CFLAGS) $(LDFLAGS) endif diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 5118bcd1e..12c1f0761 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -13,7 +13,11 @@ #include #include #include -#include + +// minimal LZ4 API forward declarations (linked via -llz4) +int LZ4_compress_default(const char* src, char* dst, int srcSize, int dstCapacity); +int LZ4_decompress_safe(const char* src, char* dst, int compressedSize, int dstCapacity); +int LZ4_compressBound(int inputSize); // libretro-common #include "libretro.h" @@ -1133,6 +1137,8 @@ typedef struct { int granularity_frames; int interval_ms; uint32_t last_push_ms; + uint32_t last_step_ms; + int playback_interval_ms; int use_time_cadence; int frame_counter; unsigned int generation; @@ -1161,9 +1167,6 @@ typedef struct { int queue_tail; int queue_count; int *queue; - - z_stream zstream; - int zstream_ready; } RewindContext; static RewindContext rewind_ctx = {0}; @@ -1172,7 +1175,7 @@ static int last_rewind_pressed = 0; static void* Rewind_worker_thread(void *arg); static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len); -static int Rewind_compress_state(const uint8_t *src, uLongf *dest_len); +static int Rewind_compress_state(const uint8_t *src, size_t *dest_len); static void Rewind_free(void) { if (rewind_ctx.worker_running) { @@ -1184,11 +1187,6 @@ static void Rewind_free(void) { rewind_ctx.worker_running = 0; } - if (rewind_ctx.zstream_ready) { - deflateEnd(&rewind_ctx.zstream); - rewind_ctx.zstream_ready = 0; - } - if (rewind_ctx.capture_pool) { for (int i = 0; i < rewind_ctx.pool_size; i++) { if (rewind_ctx.capture_pool[i]) free(rewind_ctx.capture_pool[i]); @@ -1220,6 +1218,7 @@ static void Rewind_reset(void) { pthread_mutex_unlock(&rewind_ctx.lock); rewind_ctx.frame_counter = 0; rewind_ctx.last_push_ms = 0; + rewind_ctx.last_step_ms = 0; rewind_ctx.generation += 1; rewind_ctx.drop_warned = 0; rewind_ctx.worker_stop = 0; @@ -1302,36 +1301,13 @@ static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len) return 1; } -static int Rewind_compress_state(const uint8_t *src, uLongf *dest_len) { - int res = Z_OK; - if (!rewind_ctx.scratch || !dest_len) return Z_MEM_ERROR; - - if (!rewind_ctx.zstream_ready) { - memset(&rewind_ctx.zstream, 0, sizeof(rewind_ctx.zstream)); - res = deflateInit2(&rewind_ctx.zstream, Z_BEST_SPEED, Z_DEFLATED, 15, 8, Z_RLE); - if (res == Z_OK) rewind_ctx.zstream_ready = 1; - } - - if (rewind_ctx.zstream_ready) { - rewind_ctx.zstream.next_in = (Bytef *)src; - rewind_ctx.zstream.avail_in = rewind_ctx.state_size; - rewind_ctx.zstream.next_out = rewind_ctx.scratch; - rewind_ctx.zstream.avail_out = rewind_ctx.scratch_size; - - res = deflateReset(&rewind_ctx.zstream); - if (res == Z_OK) { - res = deflate(&rewind_ctx.zstream, Z_FINISH); - } - if (res == Z_STREAM_END) { - *dest_len = rewind_ctx.scratch_size - rewind_ctx.zstream.avail_out; - return Z_OK; - } - } - - // fallback to plain compress2 if the streaming path fails - *dest_len = rewind_ctx.scratch_size; - res = compress2(rewind_ctx.scratch, dest_len, src, rewind_ctx.state_size, Z_BEST_SPEED); - return res; +static int Rewind_compress_state(const uint8_t *src, size_t *dest_len) { + if (!rewind_ctx.scratch || !dest_len) return -1; + int max_dst = (int)rewind_ctx.scratch_size; + int res = LZ4_compress_default((const char*)src, (char*)rewind_ctx.scratch, (int)rewind_ctx.state_size, max_dst); + if (res <= 0) return -1; + *dest_len = (size_t)res; + return 0; } static int Rewind_init(size_t state_size) { @@ -1376,7 +1352,7 @@ static int Rewind_init(size_t state_size) { return 0; } - rewind_ctx.scratch_size = compressBound(state_size); + rewind_ctx.scratch_size = LZ4_compressBound((int)state_size); rewind_ctx.scratch = calloc(1, rewind_ctx.scratch_size); if (!rewind_ctx.scratch) { LOG_error("Rewind: failed to allocate scratch buffer\n"); @@ -1403,6 +1379,18 @@ static int Rewind_init(size_t state_size) { rewind_ctx.granularity_frames = 1; } if (rewind_ctx.granularity_frames < 1) rewind_ctx.granularity_frames = 1; + double fps = core.fps > 1.0 ? core.fps : 60.0; + int frame_ms = (int)(1000.0 / fps); + if (frame_ms < 1) frame_ms = 1; + // Try to play back at roughly the cadence snapshots were captured. + int capture_ms = rewind_ctx.use_time_cadence + ? rewind_ctx.interval_ms + : rewind_ctx.granularity_frames * frame_ms; + if (capture_ms < frame_ms) capture_ms = frame_ms; + // Play back faster than capture to smooth motion while avoiding runaway speed. + int playback_ms = capture_ms / 4; + if (playback_ms < frame_ms) playback_ms = frame_ms; + rewind_ctx.playback_interval_ms = playback_ms; rewind_ctx.mute_audio = mute; rewind_ctx.enabled = 1; rewind_ctx.generation = 1; @@ -1486,9 +1474,9 @@ static void* Rewind_worker_thread(void *arg) { continue; } - uLongf dest_len = rewind_ctx.scratch_size; + size_t dest_len = rewind_ctx.scratch_size; int res = Rewind_compress_state(rewind_ctx.capture_pool[slot], &dest_len); - if (res == Z_OK) { + if (res == 0) { pthread_mutex_lock(&rewind_ctx.lock); Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); pthread_mutex_unlock(&rewind_ctx.lock); @@ -1572,9 +1560,9 @@ static void Rewind_push(int force) { return; } - uLongf dest_len = rewind_ctx.scratch_size; + size_t dest_len = rewind_ctx.scratch_size; int res = Rewind_compress_state(rewind_ctx.state_buf, &dest_len); - if (res != Z_OK) { + if (res != 0) { LOG_error("Rewind: compression failed (%i)\n", res); return; } @@ -1587,6 +1575,13 @@ static void Rewind_push(int force) { static bool Rewind_step_back(void) { if (!rewind_ctx.enabled) return false; + uint32_t now_ms = SDL_GetTicks(); + if (rewind_ctx.playback_interval_ms > 0 && rewind_ctx.last_step_ms && + (int)(now_ms - rewind_ctx.last_step_ms) < rewind_ctx.playback_interval_ms) { + // still rewinding, just waiting for cadence; do not push new snapshots + return true; + } + pthread_mutex_lock(&rewind_ctx.lock); if (!rewind_ctx.entry_count) { pthread_mutex_unlock(&rewind_ctx.lock); @@ -1601,9 +1596,9 @@ static bool Rewind_step_back(void) { if (idx < 0) idx += rewind_ctx.entry_capacity; RewindEntry *e = &rewind_ctx.entries[idx]; - uLongf dest_len = rewind_ctx.state_size; - int res = uncompress(rewind_ctx.state_buf, &dest_len, rewind_ctx.buffer + e->offset, e->size); - if (res != Z_OK || dest_len < rewind_ctx.state_size) { + int res = LZ4_decompress_safe((const char*)rewind_ctx.buffer + e->offset, + (char*)rewind_ctx.state_buf, (int)e->size, (int)rewind_ctx.state_size); + if (res < (int)rewind_ctx.state_size) { LOG_error("Rewind: decompress failed (%i)\n", res); Rewind_drop_oldest_locked(); pthread_mutex_unlock(&rewind_ctx.lock); @@ -1627,6 +1622,7 @@ static bool Rewind_step_back(void) { pthread_mutex_unlock(&rewind_ctx.lock); rewinding = 1; + rewind_ctx.last_step_ms = now_ms; LOG_info("Rewind: stepped back, entries remaining %i\n", remaining); return true; } From 4d4de3b9daa2bff101606576250703e818682e14 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Wed, 10 Dec 2025 22:06:42 +0100 Subject: [PATCH 06/29] feat(rewind): rewind performance improvements and robustness --- workspace/all/common/config.h | 2 +- workspace/all/minarch/minarch.c | 138 ++++++++++++++++++++++---------- 2 files changed, 98 insertions(+), 42 deletions(-) diff --git a/workspace/all/common/config.h b/workspace/all/common/config.h index 2d88e9b0b..97f8389fb 100644 --- a/workspace/all/common/config.h +++ b/workspace/all/common/config.h @@ -147,7 +147,7 @@ typedef struct #define CFG_DEFAULT_EXTRACTEDFILENAME false #define CFG_DEFAULT_REWIND_ENABLE false #define CFG_DEFAULT_REWIND_BUFFER_MB 16 -#define CFG_DEFAULT_REWIND_GRANULARITY 300 +#define CFG_DEFAULT_REWIND_GRANULARITY 66 #define CFG_DEFAULT_REWIND_MUTE_AUDIO true #define CFG_DEFAULT_MUTELEDS false #define CFG_DEFAULT_GAMEARTWIDTH 0.45 diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 12c1f0761..11993d846 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -1268,35 +1268,70 @@ static void Rewind_drop_oldest(void) { pthread_mutex_unlock(&rewind_ctx.lock); } +// Check if an entry overlaps with range [range_start, range_end) in a non-wrapping buffer region +static int Rewind_entry_overlaps_range(int entry_idx, size_t range_start, size_t range_end) { + RewindEntry *e = &rewind_ctx.entries[entry_idx]; + size_t e_start = e->offset; + size_t e_end = e->offset + e->size; + // Check for overlap: ranges overlap if start < other_end AND other_start < end + return (e_start < range_end) && (range_start < e_end); +} + static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len) { if (dest_len >= rewind_ctx.capacity) { LOG_error("Rewind: state does not fit in buffer\n"); return 0; } - // wrap write position if needed - if (rewind_ctx.head + dest_len > rewind_ctx.capacity) { + size_t write_offset = rewind_ctx.head; + + // If this write would go past the end of the buffer, wrap to 0 + if (write_offset + dest_len > rewind_ctx.capacity) { + write_offset = 0; rewind_ctx.head = 0; - if (rewind_ctx.entry_count==0) rewind_ctx.tail = 0; + if (rewind_ctx.entry_count == 0) { + rewind_ctx.tail = 0; + } + } + + // Drop any entries that overlap with the region we're about to write: [write_offset, write_offset + dest_len) + // We need to check all entries from tail to head and drop any that overlap. + // Since entries are stored oldest-to-newest, we drop from oldest while they overlap. + while (rewind_ctx.entry_count > 0) { + int oldest_idx = rewind_ctx.entry_tail; + if (Rewind_entry_overlaps_range(oldest_idx, write_offset, write_offset + dest_len)) { + Rewind_drop_oldest_locked(); + } else { + break; + } } - // make room - while (Rewind_free_space_locked() <= dest_len) { + // Still need to make room based on free space calculation + while (rewind_ctx.entry_count > 0 && Rewind_free_space_locked() <= dest_len) { Rewind_drop_oldest_locked(); } - memcpy(rewind_ctx.buffer + rewind_ctx.head, compressed, dest_len); + // Safety check: if we still can't fit, there's a logic error + if (Rewind_free_space_locked() <= dest_len && rewind_ctx.entry_count > 0) { + LOG_error("Rewind: unable to make room for entry (need %zu, have %zu)\n", dest_len, Rewind_free_space_locked()); + return 0; + } + + memcpy(rewind_ctx.buffer + write_offset, compressed, dest_len); RewindEntry *e = &rewind_ctx.entries[rewind_ctx.entry_head]; - e->offset = rewind_ctx.head; + e->offset = write_offset; e->size = dest_len; - rewind_ctx.head += dest_len; + rewind_ctx.head = write_offset + dest_len; if (rewind_ctx.head >= rewind_ctx.capacity) rewind_ctx.head = 0; rewind_ctx.entry_head = (rewind_ctx.entry_head + 1) % rewind_ctx.entry_capacity; - if (rewind_ctx.entry_count < rewind_ctx.entry_capacity) rewind_ctx.entry_count += 1; - else Rewind_drop_oldest_locked(); + if (rewind_ctx.entry_count < rewind_ctx.entry_capacity) { + rewind_ctx.entry_count += 1; + } else { + Rewind_drop_oldest_locked(); + } rewind_warn_empty = 0; return 1; } @@ -1322,8 +1357,8 @@ static int Rewind_init(size_t state_size) { enable = 1; LOG_info("Rewind: force enable via %s\n", force_path); } - LOG_info("Rewind: config enable=%i bufferMB=%i interval=%i%s mute=%i\n", - enable, buf_mb, gran, gran > 60 ? "ms" : " frames", mute); + LOG_info("Rewind: config enable=%i bufferMB=%i interval=%ims mute=%i\n", + enable, buf_mb, gran, mute); if (!enable) { LOG_info("Rewind: disabled via config\n"); return 0; @@ -1370,27 +1405,21 @@ static int Rewind_init(size_t state_size) { return 0; } - rewind_ctx.granularity_frames = gran; - rewind_ctx.interval_ms = 0; - rewind_ctx.use_time_cadence = gran > 60; - if (gran < 1) gran = 1; - if (rewind_ctx.use_time_cadence) { - rewind_ctx.interval_ms = gran; - rewind_ctx.granularity_frames = 1; - } - if (rewind_ctx.granularity_frames < 1) rewind_ctx.granularity_frames = 1; + rewind_ctx.granularity_frames = 1; + rewind_ctx.interval_ms = gran < 1 ? 1 : gran; // treat granularity as milliseconds always + rewind_ctx.use_time_cadence = 1; double fps = core.fps > 1.0 ? core.fps : 60.0; int frame_ms = (int)(1000.0 / fps); if (frame_ms < 1) frame_ms = 1; - // Try to play back at roughly the cadence snapshots were captured. - int capture_ms = rewind_ctx.use_time_cadence - ? rewind_ctx.interval_ms - : rewind_ctx.granularity_frames * frame_ms; + // Capture interval in milliseconds (time-based only) + int capture_ms = rewind_ctx.interval_ms; if (capture_ms < frame_ms) capture_ms = frame_ms; - // Play back faster than capture to smooth motion while avoiding runaway speed. - int playback_ms = capture_ms / 4; + // Play back at the capture cadence (match recorded speed) but never faster than native frame time + int playback_ms = capture_ms; if (playback_ms < frame_ms) playback_ms = frame_ms; rewind_ctx.playback_interval_ms = playback_ms; + LOG_info("Rewind: capture_ms=%d, playback_ms=%d (state size=%zu bytes, buffer=%zu bytes, entries=%d)\n", + capture_ms, playback_ms, state_size, rewind_ctx.capacity, rewind_ctx.entry_capacity); rewind_ctx.mute_audio = mute; rewind_ctx.enabled = 1; rewind_ctx.generation = 1; @@ -1404,8 +1433,8 @@ static int Rewind_init(size_t state_size) { rewind_ctx.locks_ready = 1; // set up async capture buffers - rewind_ctx.pool_size = 3; - if (state_size > 2 * 1024 * 1024) rewind_ctx.pool_size = 2; + // Larger states need a deeper pool to avoid drops; cap to a modest size to limit RAM + rewind_ctx.pool_size = (state_size > 2 * 1024 * 1024) ? 4 : 3; if (rewind_ctx.pool_size < 1) rewind_ctx.pool_size = 1; rewind_ctx.capture_pool = calloc(rewind_ctx.pool_size, sizeof(uint8_t*)); rewind_ctx.capture_gen = calloc(rewind_ctx.pool_size, sizeof(unsigned int)); @@ -1475,15 +1504,15 @@ static void* Rewind_worker_thread(void *arg) { } size_t dest_len = rewind_ctx.scratch_size; + pthread_mutex_lock(&rewind_ctx.lock); int res = Rewind_compress_state(rewind_ctx.capture_pool[slot], &dest_len); if (res == 0) { - pthread_mutex_lock(&rewind_ctx.lock); Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); - pthread_mutex_unlock(&rewind_ctx.lock); } else { LOG_error("Rewind: compression failed (%i)\n", res); } + pthread_mutex_unlock(&rewind_ctx.lock); pthread_mutex_lock(&rewind_ctx.queue_mx); rewind_ctx.capture_busy[slot] = 0; @@ -1526,10 +1555,23 @@ static void Rewind_push(int force) { pthread_mutex_unlock(&rewind_ctx.queue_mx); if (slot < 0) { - if (!rewind_ctx.drop_warned) { - LOG_info("Rewind: skipping snapshot (worker busy)\n"); - rewind_ctx.drop_warned = 1; + // worker is busy; fall back to synchronous capture so we don't miss cadence + if (!core.serialize(rewind_ctx.state_buf, rewind_ctx.state_size)) { + LOG_error("Rewind: serialize failed (sync fallback)\n"); + return; } + + size_t dest_len = rewind_ctx.scratch_size; + pthread_mutex_lock(&rewind_ctx.lock); + int res = Rewind_compress_state(rewind_ctx.state_buf, &dest_len); + if (res != 0) { + pthread_mutex_unlock(&rewind_ctx.lock); + LOG_error("Rewind: compression failed (sync fallback) (%i)\n", res); + return; + } + + Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); + pthread_mutex_unlock(&rewind_ctx.lock); return; } @@ -1561,13 +1603,14 @@ static void Rewind_push(int force) { } size_t dest_len = rewind_ctx.scratch_size; + pthread_mutex_lock(&rewind_ctx.lock); int res = Rewind_compress_state(rewind_ctx.state_buf, &dest_len); if (res != 0) { + pthread_mutex_unlock(&rewind_ctx.lock); LOG_error("Rewind: compression failed (%i)\n", res); return; } - pthread_mutex_lock(&rewind_ctx.lock); Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); pthread_mutex_unlock(&rewind_ctx.lock); rewind_ctx.drop_warned = 0; @@ -1599,8 +1642,14 @@ static bool Rewind_step_back(void) { int res = LZ4_decompress_safe((const char*)rewind_ctx.buffer + e->offset, (char*)rewind_ctx.state_buf, (int)e->size, (int)rewind_ctx.state_size); if (res < (int)rewind_ctx.state_size) { - LOG_error("Rewind: decompress failed (%i)\n", res); - Rewind_drop_oldest_locked(); + LOG_error("Rewind: decompress failed (res=%i, want=%zu, compressed=%zu, offset=%zu, idx=%d head=%d tail=%d count=%d buf_head=%zu buf_tail=%zu)\n", + res, rewind_ctx.state_size, e->size, e->offset, idx, rewind_ctx.entry_head, rewind_ctx.entry_tail, rewind_ctx.entry_count, rewind_ctx.head, rewind_ctx.tail); + // On decompression failure, drop the corrupted newest entry instead of oldest + rewind_ctx.entry_head = idx; + rewind_ctx.entry_count -= 1; + if (rewind_ctx.entry_count == 0) { + rewind_ctx.head = rewind_ctx.tail = 0; + } pthread_mutex_unlock(&rewind_ctx.lock); return false; } @@ -1623,7 +1672,6 @@ static bool Rewind_step_back(void) { rewinding = 1; rewind_ctx.last_step_ms = now_ms; - LOG_info("Rewind: stepped back, entries remaining %i\n", remaining); return true; } @@ -1700,19 +1748,27 @@ static char* rewind_buffer_labels[] = { NULL }; static char* rewind_granularity_values[] = { + "33", + "50", + "66", + "100", "150", + "200", "300", "450", "600", - "900", NULL }; static char* rewind_granularity_labels[] = { - "150 ms", + "33 ms (~30 fps)", + "50 ms (~20 fps)", + "66 ms (~15 fps)", + "100 ms (~10 fps)", + "150 ms (~7 fps)", + "200 ms (~5 fps)", "300 ms", "450 ms", "600 ms", - "900 ms", NULL }; static char* ambient_labels[] = { From a3bb725db37a48c56eb467bf85e5550fafec40b5 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Wed, 10 Dec 2025 22:48:12 +0100 Subject: [PATCH 07/29] feat(rewind): playback choppyness improvements --- workspace/all/minarch/minarch.c | 42 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 11993d846..daa7d5142 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -1616,13 +1616,14 @@ static void Rewind_push(int force) { rewind_ctx.drop_warned = 0; } -static bool Rewind_step_back(void) { - if (!rewind_ctx.enabled) return false; +// Returns: 0 = buffer empty/disabled, 1 = stepped back successfully, 2 = waiting for cadence (don't run core) +static int Rewind_step_back(void) { + if (!rewind_ctx.enabled) return 0; uint32_t now_ms = SDL_GetTicks(); if (rewind_ctx.playback_interval_ms > 0 && rewind_ctx.last_step_ms && (int)(now_ms - rewind_ctx.last_step_ms) < rewind_ctx.playback_interval_ms) { - // still rewinding, just waiting for cadence; do not push new snapshots - return true; + // still rewinding, just waiting for cadence; don't run core, just re-render + return 2; } pthread_mutex_lock(&rewind_ctx.lock); @@ -1632,7 +1633,7 @@ static bool Rewind_step_back(void) { LOG_info("Rewind: no buffered states yet\n"); rewind_warn_empty = 1; } - return false; + return 0; } int idx = rewind_ctx.entry_head - 1; @@ -1651,14 +1652,14 @@ static bool Rewind_step_back(void) { rewind_ctx.head = rewind_ctx.tail = 0; } pthread_mutex_unlock(&rewind_ctx.lock); - return false; + return 0; } if (!core.unserialize(rewind_ctx.state_buf, rewind_ctx.state_size)) { LOG_error("Rewind: unserialize failed\n"); Rewind_drop_oldest_locked(); pthread_mutex_unlock(&rewind_ctx.lock); - return false; + return 0; } // pop newest @@ -1672,7 +1673,7 @@ static bool Rewind_step_back(void) { rewinding = 1; rewind_ctx.last_step_ms = now_ms; - return true; + return 1; } static void Rewind_on_state_change(void) { @@ -1745,6 +1746,7 @@ static char* rewind_buffer_labels[] = { "16", "32", "64", + "128", NULL }; static char* rewind_granularity_values[] = { @@ -2388,7 +2390,7 @@ static struct Config { .desc = "Memory reserved for rewind snapshots.", .default_value = 1, // 16MB .value = 1, - .count = 4, + .count = 5, .values = rewind_buffer_labels, .labels = rewind_buffer_labels, }, @@ -7930,23 +7932,29 @@ int main(int argc , char* argv[]) { // if rewind is toggled, fast-forward toggle must stay off; fast-forward hold pauses rewind int do_rewind = (rewind_pressed || rewind_toggle) && !(rewind_toggle && ff_hold_active); if (do_rewind) { - bool did_rewind = Rewind_step_back(); - rewinding = did_rewind; - if (did_rewind) { + // Rewind_step_back returns: 0=buffer empty, 1=stepped back, 2=waiting for cadence + int rewind_result = Rewind_step_back(); + rewinding = (rewind_result != 0); + if (rewind_result == 1) { + // Actually stepped back - run one frame to render the restored state + fast_forward = 0; + core.run(); + } + else if (rewind_result == 2) { + // Waiting for cadence - don't run core, just re-render current frame fast_forward = 0; + // Skip core.run() entirely to avoid advancing the game } else { - // buffer empty: auto untoggle rewind, resume FF if it was paused for a hold + // Buffer empty: auto untoggle rewind, resume FF if it was paused for a hold if (rewind_toggle) rewind_toggle = 0; if (ff_paused_by_rewind_hold && ff_toggled) { ff_paused_by_rewind_hold = 0; fast_forward = setFastForward(1); } + core.run(); + Rewind_push(0); } - core.run(); // render from the restored state - if (!did_rewind) { - Rewind_push(0); - } } else { rewinding = 0; From 9c0a6605edd5c4db2c0e6049e545dcc2a92b2a27 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:45:29 +0100 Subject: [PATCH 08/29] feat(rewind): remove deprecated rewind settings and introduce new defaults --- workspace/all/common/config.c | 78 ---------------- workspace/all/common/config.h | 17 ---- workspace/all/minarch/minarch.c | 158 ++++++++++++++++++++++---------- 3 files changed, 109 insertions(+), 144 deletions(-) diff --git a/workspace/all/common/config.c b/workspace/all/common/config.c index cde71daea..9f25fbe2a 100644 --- a/workspace/all/common/config.c +++ b/workspace/all/common/config.c @@ -60,10 +60,6 @@ void CFG_defaults(NextUISettings *cfg) .saveFormat = CFG_DEFAULT_SAVEFORMAT, .stateFormat = CFG_DEFAULT_STATEFORMAT, .useExtractedFileName = CFG_DEFAULT_EXTRACTEDFILENAME, - .rewindEnable = CFG_DEFAULT_REWIND_ENABLE, - .rewindBufferMB = CFG_DEFAULT_REWIND_BUFFER_MB, - .rewindGranularity = CFG_DEFAULT_REWIND_GRANULARITY, - .rewindMuteAudio = CFG_DEFAULT_REWIND_MUTE_AUDIO, .wifi = CFG_DEFAULT_WIFI, .wifiDiagnostics = CFG_DEFAULT_WIFI_DIAG, @@ -234,26 +230,6 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb) CFG_setUseExtractedFileName((bool)temp_value); continue; } - if (sscanf(line, "rewindEnable=%i", &temp_value) == 1) - { - CFG_setRewindEnable((bool)temp_value); - continue; - } - if (sscanf(line, "rewindBufferMB=%i", &temp_value) == 1) - { - CFG_setRewindBufferMB(temp_value); - continue; - } - if (sscanf(line, "rewindGranularity=%i", &temp_value) == 1) - { - CFG_setRewindGranularity(temp_value); - continue; - } - if (sscanf(line, "rewindMuteAudio=%i", &temp_value) == 1) - { - CFG_setRewindMuteAudio((bool)temp_value); - continue; - } if (sscanf(line, "muteLeds=%i", &temp_value) == 1) { CFG_setMuteLEDs(temp_value); @@ -607,52 +583,6 @@ void CFG_setUseExtractedFileName(bool use) CFG_sync(); } -bool CFG_getRewindEnable(void) -{ - return settings.rewindEnable; -} - -void CFG_setRewindEnable(bool enable) -{ - settings.rewindEnable = enable; - CFG_sync(); -} - -int CFG_getRewindBufferMB(void) -{ - return settings.rewindBufferMB; -} - -void CFG_setRewindBufferMB(int mb) -{ - settings.rewindBufferMB = clamp(mb, 1, 256); - CFG_sync(); -} - -int CFG_getRewindGranularity(void) -{ - return settings.rewindGranularity; -} - -void CFG_setRewindGranularity(int granularity) -{ - // Granularity is interpreted as milliseconds once it exceeds the legacy - // frame-based range, so allow a wider range to cover slower cadences. - settings.rewindGranularity = clamp(granularity, 1, 2000); - CFG_sync(); -} - -bool CFG_getRewindMuteAudio(void) -{ - return settings.rewindMuteAudio; -} - -void CFG_setRewindMuteAudio(bool enable) -{ - settings.rewindMuteAudio = enable; - CFG_sync(); -} - bool CFG_getMuteLEDs(void) { return settings.muteLeds; @@ -955,10 +885,6 @@ void CFG_sync(void) fprintf(file, "saveFormat=%i\n", settings.saveFormat); fprintf(file, "stateFormat=%i\n", settings.stateFormat); fprintf(file, "useExtractedFileName=%i\n", settings.useExtractedFileName); - fprintf(file, "rewindEnable=%i\n", settings.rewindEnable); - fprintf(file, "rewindBufferMB=%i\n", settings.rewindBufferMB); - fprintf(file, "rewindGranularity=%i\n", settings.rewindGranularity); - fprintf(file, "rewindMuteAudio=%i\n", settings.rewindMuteAudio); fprintf(file, "muteLeds=%i\n", settings.muteLeds); fprintf(file, "artWidth=%i\n", (int)(settings.gameArtWidth * 100)); fprintf(file, "wifi=%i\n", settings.wifi); @@ -1002,10 +928,6 @@ void CFG_print(void) printf("\t\"saveFormat\": %i,\n", settings.saveFormat); printf("\t\"stateFormat\": %i,\n", settings.stateFormat); printf("\t\"useExtractedFileName\": %i,\n", settings.useExtractedFileName); - printf("\t\"rewindEnable\": %i,\n", settings.rewindEnable); - printf("\t\"rewindBufferMB\": %i,\n", settings.rewindBufferMB); - printf("\t\"rewindGranularity\": %i,\n", settings.rewindGranularity); - printf("\t\"rewindMuteAudio\": %i,\n", settings.rewindMuteAudio); printf("\t\"muteLeds\": %i,\n", settings.muteLeds); printf("\t\"artWidth\": %i,\n", (int)(settings.gameArtWidth * 100)); printf("\t\"wifi\": %i,\n", settings.wifi); diff --git a/workspace/all/common/config.h b/workspace/all/common/config.h index 97f8389fb..b331f1be6 100644 --- a/workspace/all/common/config.h +++ b/workspace/all/common/config.h @@ -102,10 +102,6 @@ typedef struct int saveFormat; int stateFormat; bool useExtractedFileName; - bool rewindEnable; - int rewindBufferMB; - int rewindGranularity; - bool rewindMuteAudio; // Haptic bool haptics; @@ -145,10 +141,6 @@ typedef struct #define CFG_DEFAULT_SAVEFORMAT SAVE_FORMAT_SAV #define CFG_DEFAULT_STATEFORMAT STATE_FORMAT_SAV #define CFG_DEFAULT_EXTRACTEDFILENAME false -#define CFG_DEFAULT_REWIND_ENABLE false -#define CFG_DEFAULT_REWIND_BUFFER_MB 16 -#define CFG_DEFAULT_REWIND_GRANULARITY 66 -#define CFG_DEFAULT_REWIND_MUTE_AUDIO true #define CFG_DEFAULT_MUTELEDS false #define CFG_DEFAULT_GAMEARTWIDTH 0.45 #define CFG_DEFAULT_WIFI false @@ -234,15 +226,6 @@ void CFG_setStateFormat(int); // use extracted file name instead of archive name (for cores that do not support archives natively) bool CFG_getUseExtractedFileName(void); void CFG_setUseExtractedFileName(bool); -// Rewind controls -bool CFG_getRewindEnable(void); -void CFG_setRewindEnable(bool enable); -int CFG_getRewindBufferMB(void); -void CFG_setRewindBufferMB(int mb); -int CFG_getRewindGranularity(void); -void CFG_setRewindGranularity(int granularity); -bool CFG_getRewindMuteAudio(void); -void CFG_setRewindMuteAudio(bool enable); // Enable/disable mute also shutting off LEDs. bool CFG_getMuteLEDs(void); void CFG_setMuteLEDs(bool); diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index daa7d5142..8802d2572 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -59,6 +59,12 @@ enum { SCALE_COUNT, }; +// defaults for rewind UI options (frontend-only) +#define MINARCH_DEFAULT_REWIND_ENABLE 0 +#define MINARCH_DEFAULT_REWIND_BUFFER_MB 16 +#define MINARCH_DEFAULT_REWIND_GRANULARITY 66 +#define MINARCH_DEFAULT_REWIND_AUDIO 0 + // default frontend options static int screen_scaling = SCALE_ASPECT; static int resampling_quality = 2; @@ -81,10 +87,11 @@ static int ff_toggled = 0; static int ff_hold_active = 0; static int ff_paused_by_rewind_hold = 0; static int rewinding = 0; -static int rewind_cfg_enable = CFG_DEFAULT_REWIND_ENABLE; -static int rewind_cfg_buffer_mb = CFG_DEFAULT_REWIND_BUFFER_MB; -static int rewind_cfg_granularity = CFG_DEFAULT_REWIND_GRANULARITY; -static int rewind_cfg_mute_audio = CFG_DEFAULT_REWIND_MUTE_AUDIO; +static int rewind_cfg_enable = MINARCH_DEFAULT_REWIND_ENABLE; +static int rewind_cfg_buffer_mb = MINARCH_DEFAULT_REWIND_BUFFER_MB; +static int rewind_cfg_granularity = MINARCH_DEFAULT_REWIND_GRANULARITY; +static int rewind_cfg_audio = MINARCH_DEFAULT_REWIND_AUDIO; +static int rewind_cfg_skip_compress = 0; static int overclock = 3; // auto static int has_custom_controllers = 0; static int gamepad_type = 0; // index in gamepad_labels/gamepad_values @@ -1143,7 +1150,9 @@ typedef struct { int frame_counter; unsigned int generation; int enabled; - int mute_audio; + int audio; + int compress; + int logged_first; // async capture/compression pthread_t worker; @@ -1338,10 +1347,24 @@ static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len) static int Rewind_compress_state(const uint8_t *src, size_t *dest_len) { if (!rewind_ctx.scratch || !dest_len) return -1; + if (!rewind_ctx.compress) { + *dest_len = rewind_ctx.state_size; + memcpy(rewind_ctx.scratch, src, rewind_ctx.state_size); + if (!rewind_ctx.logged_first) { + rewind_ctx.logged_first = 1; + LOG_info("Rewind: compression disabled, storing %zu bytes per snapshot\n", rewind_ctx.state_size); + } + return 0; + } int max_dst = (int)rewind_ctx.scratch_size; int res = LZ4_compress_default((const char*)src, (char*)rewind_ctx.scratch, (int)rewind_ctx.state_size, max_dst); if (res <= 0) return -1; *dest_len = (size_t)res; + if (!rewind_ctx.logged_first) { + rewind_ctx.logged_first = 1; + LOG_info("Rewind: state size before compression=%zu bytes, after=%zu bytes (%.1f%%)\n", + rewind_ctx.state_size, *dest_len, 100.0 * (double)*dest_len / (double)rewind_ctx.state_size); + } return 0; } @@ -1351,14 +1374,8 @@ static int Rewind_init(size_t state_size) { int enable = rewind_cfg_enable; int buf_mb = rewind_cfg_buffer_mb; int gran = rewind_cfg_granularity; - int mute = rewind_cfg_mute_audio; - const char *force_path = SHARED_USERDATA_PATH "/rewind.force"; - if (exists((char*)force_path)) { - enable = 1; - LOG_info("Rewind: force enable via %s\n", force_path); - } - LOG_info("Rewind: config enable=%i bufferMB=%i interval=%ims mute=%i\n", - enable, buf_mb, gran, mute); + int audio = rewind_cfg_audio; + int skip_compress = rewind_cfg_skip_compress; if (!enable) { LOG_info("Rewind: disabled via config\n"); return 0; @@ -1373,6 +1390,15 @@ static int Rewind_init(size_t state_size) { if (buffer_mb > 256) buffer_mb = 256; rewind_ctx.capacity = buffer_mb * 1024 * 1024; + rewind_ctx.compress = skip_compress ? 0 : 1; + if (!rewind_ctx.compress && rewind_ctx.capacity <= state_size) { + LOG_warn("Rewind: raw snapshots (%zu bytes) do not fit in %zu-byte buffer; falling back to compression\n", + state_size, rewind_ctx.capacity); + rewind_ctx.compress = 1; + } + rewind_ctx.logged_first = 0; + LOG_info("Rewind: config enable=%i bufferMB=%i interval=%ims audio=%i compression=%s\n", + enable, buf_mb, gran, audio, rewind_ctx.compress ? "lz4" : "raw"); rewind_ctx.buffer = calloc(1, rewind_ctx.capacity); if (!rewind_ctx.buffer) { LOG_error("Rewind: failed to allocate buffer\n"); @@ -1388,6 +1414,7 @@ static int Rewind_init(size_t state_size) { } rewind_ctx.scratch_size = LZ4_compressBound((int)state_size); + if (!rewind_ctx.compress) rewind_ctx.scratch_size = state_size; rewind_ctx.scratch = calloc(1, rewind_ctx.scratch_size); if (!rewind_ctx.scratch) { LOG_error("Rewind: failed to allocate scratch buffer\n"); @@ -1420,7 +1447,7 @@ static int Rewind_init(size_t state_size) { rewind_ctx.playback_interval_ms = playback_ms; LOG_info("Rewind: capture_ms=%d, playback_ms=%d (state size=%zu bytes, buffer=%zu bytes, entries=%d)\n", capture_ms, playback_ms, state_size, rewind_ctx.capacity, rewind_ctx.entry_capacity); - rewind_ctx.mute_audio = mute; + rewind_ctx.audio = audio; rewind_ctx.enabled = 1; rewind_ctx.generation = 1; rewind_ctx.worker_stop = 0; @@ -1640,12 +1667,26 @@ static int Rewind_step_back(void) { if (idx < 0) idx += rewind_ctx.entry_capacity; RewindEntry *e = &rewind_ctx.entries[idx]; - int res = LZ4_decompress_safe((const char*)rewind_ctx.buffer + e->offset, - (char*)rewind_ctx.state_buf, (int)e->size, (int)rewind_ctx.state_size); - if (res < (int)rewind_ctx.state_size) { - LOG_error("Rewind: decompress failed (res=%i, want=%zu, compressed=%zu, offset=%zu, idx=%d head=%d tail=%d count=%d buf_head=%zu buf_tail=%zu)\n", - res, rewind_ctx.state_size, e->size, e->offset, idx, rewind_ctx.entry_head, rewind_ctx.entry_tail, rewind_ctx.entry_count, rewind_ctx.head, rewind_ctx.tail); - // On decompression failure, drop the corrupted newest entry instead of oldest + int decode_ok = 1; + if (rewind_ctx.compress) { + int res = LZ4_decompress_safe((const char*)rewind_ctx.buffer + e->offset, + (char*)rewind_ctx.state_buf, (int)e->size, (int)rewind_ctx.state_size); + if (res < (int)rewind_ctx.state_size) { + LOG_error("Rewind: decompress failed (res=%i, want=%zu, compressed=%zu, offset=%zu, idx=%d head=%d tail=%d count=%d buf_head=%zu buf_tail=%zu)\n", + res, rewind_ctx.state_size, e->size, e->offset, idx, rewind_ctx.entry_head, rewind_ctx.entry_tail, rewind_ctx.entry_count, rewind_ctx.head, rewind_ctx.tail); + decode_ok = 0; + } + } else { + if (e->size != rewind_ctx.state_size) { + LOG_error("Rewind: raw snapshot size mismatch (got=%zu, want=%zu, offset=%zu)\n", + e->size, rewind_ctx.state_size, e->offset); + decode_ok = 0; + } else { + memcpy(rewind_ctx.state_buf, rewind_ctx.buffer + e->offset, rewind_ctx.state_size); + } + } + if (!decode_ok) { + // On decode failure, drop the corrupted newest entry instead of oldest rewind_ctx.entry_head = idx; rewind_ctx.entry_count -= 1; if (rewind_ctx.entry_count == 0) { @@ -1750,6 +1791,9 @@ static char* rewind_buffer_labels[] = { NULL }; static char* rewind_granularity_values[] = { + "16", + "22", + "25", "33", "50", "66", @@ -1762,6 +1806,9 @@ static char* rewind_granularity_values[] = { NULL }; static char* rewind_granularity_labels[] = { + "16 ms (~60 fps)", + "22 ms (~45 fps)", + "25 ms (~40 fps)", "33 ms (~30 fps)", "50 ms (~20 fps)", "66 ms (~15 fps)", @@ -2009,7 +2056,8 @@ enum { FE_OPT_REWIND_ENABLE, FE_OPT_REWIND_BUFFER, FE_OPT_REWIND_GRANULARITY, - FE_OPT_REWIND_MUTE, + FE_OPT_REWIND_AUDIO, + FE_OPT_REWIND_SKIP_COMPRESSION, FE_OPT_COUNT, }; @@ -2022,8 +2070,8 @@ enum { SHORTCUT_CYCLE_EFFECT, SHORTCUT_TOGGLE_FF, SHORTCUT_HOLD_FF, - SHORTCUT_HOLD_REWIND, SHORTCUT_TOGGLE_REWIND, + SHORTCUT_HOLD_REWIND, SHORTCUT_GAMESWITCHER, SHORTCUT_SCREENSHOT, // Trimui only @@ -2378,8 +2426,8 @@ static struct Config { .key = "minarch_rewind_enable", .name = "Rewind", .desc = "Enable in-memory rewind buffer.", - .default_value = CFG_DEFAULT_REWIND_ENABLE ? 1 : 0, - .value = CFG_DEFAULT_REWIND_ENABLE ? 1 : 0, + .default_value = MINARCH_DEFAULT_REWIND_ENABLE ? 1 : 0, + .value = MINARCH_DEFAULT_REWIND_ENABLE ? 1 : 0, .count = 2, .values = rewind_enable_labels, .labels = rewind_enable_labels, @@ -2398,18 +2446,28 @@ static struct Config { .key = "minarch_rewind_granularity", .name = "Rewind Interval", .desc = "Milliseconds between rewind snapshots.", - .default_value = 1, // 300ms - .value = 1, - .count = 5, + .default_value = 5, // 66ms + .value = 5, + .count = 12, .values = rewind_granularity_values, .labels = rewind_granularity_labels, }, - [FE_OPT_REWIND_MUTE] = { - .key = "minarch_rewind_mute_audio", - .name = "Rewind Mute Audio", - .desc = "Mute audio while rewinding.", - .default_value = CFG_DEFAULT_REWIND_MUTE_AUDIO ? 1 : 0, - .value = CFG_DEFAULT_REWIND_MUTE_AUDIO ? 1 : 0, + [FE_OPT_REWIND_AUDIO] = { + .key = "minarch_rewind_audio", + .name = "Rewind audio", + .desc = "Play or mute audio when rewinding.", + .default_value = MINARCH_DEFAULT_REWIND_AUDIO ? 1 : 0, + .value = MINARCH_DEFAULT_REWIND_AUDIO ? 1 : 0, + .count = 2, + .values = onoff_labels, + .labels = onoff_labels, + }, + [FE_OPT_REWIND_SKIP_COMPRESSION] = { + .key = "minarch_rewind_skip_compression", + .name = "Skip Rewind Compression", + .desc = "Store raw rewind snapshots instead of compressing them. Uses more memory but less CPU.", + .default_value = 0, + .value = 0, .count = 2, .values = onoff_labels, .labels = onoff_labels, @@ -2624,12 +2682,12 @@ static struct Config { [SHORTCUT_SAVE_QUIT] = {"Save & Quit", -1, BTN_ID_NONE, 0}, [SHORTCUT_CYCLE_SCALE] = {"Cycle Scaling", -1, BTN_ID_NONE, 0}, [SHORTCUT_CYCLE_EFFECT] = {"Cycle Effect", -1, BTN_ID_NONE, 0}, - [SHORTCUT_TOGGLE_FF] = {"Toggle FF", -1, BTN_ID_NONE, 0}, - [SHORTCUT_HOLD_FF] = {"Hold FF", -1, BTN_ID_NONE, 0}, - [SHORTCUT_HOLD_REWIND] = {"Hold Rewind", -1, BTN_ID_NONE, 0}, - [SHORTCUT_TOGGLE_REWIND] = {"Toggle Rewind", -1, BTN_ID_NONE, 0}, - [SHORTCUT_GAMESWITCHER] = {"Game Switcher", -1, BTN_ID_NONE, 0}, - [SHORTCUT_SCREENSHOT] = {"Screenshot", -1, BTN_ID_NONE, 0}, + [SHORTCUT_TOGGLE_FF] = {"Toggle FF", -1, BTN_ID_NONE, 0}, + [SHORTCUT_HOLD_FF] = {"Hold FF", -1, BTN_ID_NONE, 0}, + [SHORTCUT_TOGGLE_REWIND] = {"Toggle Rewind", -1, BTN_ID_NONE, 0}, + [SHORTCUT_HOLD_REWIND] = {"Hold Rewind", -1, BTN_ID_NONE, 0}, + [SHORTCUT_GAMESWITCHER] = {"Game Switcher", -1, BTN_ID_NONE, 0}, + [SHORTCUT_SCREENSHOT] = {"Screenshot", -1, BTN_ID_NONE, 0}, // Trimui only [SHORTCUT_TOGGLE_TURBO_A] = {"Toggle Turbo A", -1, BTN_ID_NONE, 0}, [SHORTCUT_TOGGLE_TURBO_B] = {"Toggle Turbo B", -1, BTN_ID_NONE, 0}, @@ -2783,16 +2841,19 @@ static void Config_syncFrontend(char* key, int value) { else if (exactMatch(key,config.frontend.options[FE_OPT_REWIND_GRANULARITY].key)) { i = FE_OPT_REWIND_GRANULARITY; } - else if (exactMatch(key,config.frontend.options[FE_OPT_REWIND_MUTE].key)) { - i = FE_OPT_REWIND_MUTE; + else if (exactMatch(key,config.frontend.options[FE_OPT_REWIND_AUDIO].key)) { + i = FE_OPT_REWIND_AUDIO; + } + else if (exactMatch(key,config.frontend.options[FE_OPT_REWIND_SKIP_COMPRESSION].key)) { + i = FE_OPT_REWIND_SKIP_COMPRESSION; } if (i==-1) return; Option* option = &config.frontend.options[i]; option->value = value; - if (i==FE_OPT_REWIND_ENABLE || i==FE_OPT_REWIND_BUFFER || i==FE_OPT_REWIND_GRANULARITY || i==FE_OPT_REWIND_MUTE) { + if (i==FE_OPT_REWIND_ENABLE || i==FE_OPT_REWIND_BUFFER || i==FE_OPT_REWIND_GRANULARITY || i==FE_OPT_REWIND_AUDIO || i==FE_OPT_REWIND_SKIP_COMPRESSION) { const char* sval = option->values && option->values[value] ? option->values[value] : "0"; int parsed = 0; - if (i==FE_OPT_REWIND_ENABLE || i==FE_OPT_REWIND_MUTE) { + if (i==FE_OPT_REWIND_ENABLE || i==FE_OPT_REWIND_AUDIO || i==FE_OPT_REWIND_SKIP_COMPRESSION) { // use option index (Off/On) parsed = value; } @@ -2803,7 +2864,8 @@ static void Config_syncFrontend(char* key, int value) { case FE_OPT_REWIND_ENABLE: rewind_cfg_enable = parsed; break; case FE_OPT_REWIND_BUFFER: rewind_cfg_buffer_mb = parsed; break; case FE_OPT_REWIND_GRANULARITY: rewind_cfg_granularity = parsed; break; - case FE_OPT_REWIND_MUTE: rewind_cfg_mute_audio = parsed; break; + case FE_OPT_REWIND_AUDIO: rewind_cfg_audio = parsed; break; + case FE_OPT_REWIND_SKIP_COMPRESSION: rewind_cfg_skip_compress = parsed; break; } Rewind_init(core.serialize_size ? core.serialize_size() : 0); if (i==FE_OPT_REWIND_ENABLE) { @@ -5525,7 +5587,7 @@ static void video_refresh_callback(const void* data, unsigned width, unsigned he /////////////////////////////// static void audio_sample_callback(int16_t left, int16_t right) { - if (rewinding && rewind_ctx.mute_audio) return; + if (rewinding && !rewind_ctx.audio) return; if (!fast_forward || ff_audio) { if (use_core_fps || fast_forward) { SND_batchSamples_fixed_rate(&(const SND_Frame){left,right}, 1); @@ -5536,7 +5598,7 @@ static void audio_sample_callback(int16_t left, int16_t right) { } } static size_t audio_sample_batch_callback(const int16_t *data, size_t frames) { - if (rewinding && rewind_ctx.mute_audio) return frames; + if (rewinding && !rewind_ctx.audio) return frames; if (!fast_forward || ff_audio) { if (use_core_fps || fast_forward) { return SND_batchSamples_fixed_rate((const SND_Frame*)data, frames); @@ -7972,9 +8034,7 @@ int main(int argc , char* argv[]) { for (int ff_step = 0; ff_step < ff_runs; ff_step++) { core.run(); - if (!fast_forward) { - Rewind_push(0); - } + Rewind_push(0); } } limitFF(); From 81d85a138ba28ad59d87cc1be7d91c86a066d456 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:18:42 +0100 Subject: [PATCH 09/29] feat(rewind): conditionally initialize rewind only if core is ready --- workspace/all/minarch/minarch.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 8802d2572..e261fb64e 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -2867,7 +2867,11 @@ static void Config_syncFrontend(char* key, int value) { case FE_OPT_REWIND_AUDIO: rewind_cfg_audio = parsed; break; case FE_OPT_REWIND_SKIP_COMPRESSION: rewind_cfg_skip_compress = parsed; break; } - Rewind_init(core.serialize_size ? core.serialize_size() : 0); + // Only call Rewind_init if core is initialized; early config reads happen before + // the core is ready and will be followed by an explicit Rewind_init later + if (core.initialized) { + Rewind_init(core.serialize_size ? core.serialize_size() : 0); + } if (i==FE_OPT_REWIND_ENABLE) { // ensure runtime toggles don't linger when enabling/disabling feature rewind_toggle = 0; From dd46677dab973d4fb2fe2839a4e8872c16b23fbd Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:49:57 +0100 Subject: [PATCH 10/29] feat(rewind): sensible defaults for rewind options --- llm/RetroArch/RetroArch | 1 - .../Emus/tg5040/MD.pak/default-brick.cfg | 4 ++++ .../EXTRAS/Emus/tg5040/MD.pak/default.cfg | 3 +++ .../Emus/tg5040/PS.pak/default-brick.cfg | 4 ++++ .../EXTRAS/Emus/tg5040/PS.pak/default.cfg | 3 +++ .../Emus/tg5040/SFC.pak/default-brick.cfg | 4 ++++ .../EXTRAS/Emus/tg5040/SFC.pak/default.cfg | 3 +++ .../tg5040/paks/Emus/FC.pak/default-brick.cfg | 4 ++++ .../tg5040/paks/Emus/FC.pak/default.cfg | 4 ++++ .../tg5040/paks/Emus/GB.pak/default-brick.cfg | 22 +++++++++++++++++++ .../tg5040/paks/Emus/GB.pak/default.cfg | 4 ++++ .../paks/Emus/GBA.pak/default-brick.cfg | 18 +++++++++++++++ .../tg5040/paks/Emus/GBA.pak/default.cfg | 4 ++++ .../paks/Emus/GBC.pak/default-brick.cfg | 19 ++++++++++++++++ .../tg5040/paks/Emus/GBC.pak/default.cfg | 4 ++++ .../tg5040/paks/Emus/MD.pak/default-brick.cfg | 22 +++++++++++++++++++ .../tg5040/paks/Emus/MD.pak/default.cfg | 4 ++++ .../tg5040/paks/Emus/PS.pak/default-brick.cfg | 4 ++++ .../tg5040/paks/Emus/PS.pak/default.cfg | 4 ++++ .../paks/Emus/SFC.pak/default-brick.cfg | 4 ++++ .../tg5040/paks/Emus/SFC.pak/default.cfg | 4 ++++ 21 files changed, 142 insertions(+), 1 deletion(-) delete mode 160000 llm/RetroArch/RetroArch create mode 100644 skeleton/EXTRAS/Emus/tg5040/MD.pak/default-brick.cfg create mode 100644 skeleton/EXTRAS/Emus/tg5040/MD.pak/default.cfg create mode 100644 skeleton/EXTRAS/Emus/tg5040/PS.pak/default-brick.cfg create mode 100644 skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg create mode 100644 skeleton/EXTRAS/Emus/tg5040/SFC.pak/default-brick.cfg create mode 100644 skeleton/EXTRAS/Emus/tg5040/SFC.pak/default.cfg create mode 100644 skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default-brick.cfg create mode 100644 skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default-brick.cfg create mode 100644 skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default-brick.cfg create mode 100644 skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default-brick.cfg diff --git a/llm/RetroArch/RetroArch b/llm/RetroArch/RetroArch deleted file mode 160000 index d98800357..000000000 --- a/llm/RetroArch/RetroArch +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d98800357a0ea9fd34bdd6389e3790d13fbf29c4 diff --git a/skeleton/EXTRAS/Emus/tg5040/MD.pak/default-brick.cfg b/skeleton/EXTRAS/Emus/tg5040/MD.pak/default-brick.cfg new file mode 100644 index 000000000..73e87c532 --- /dev/null +++ b/skeleton/EXTRAS/Emus/tg5040/MD.pak/default-brick.cfg @@ -0,0 +1,4 @@ +minarch_screen_scaling = Fullscreen +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = on +minarch_rewind_buffer_mb = 64 diff --git a/skeleton/EXTRAS/Emus/tg5040/MD.pak/default.cfg b/skeleton/EXTRAS/Emus/tg5040/MD.pak/default.cfg new file mode 100644 index 000000000..b023d0a2e --- /dev/null +++ b/skeleton/EXTRAS/Emus/tg5040/MD.pak/default.cfg @@ -0,0 +1,3 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = on +minarch_rewind_buffer_mb = 64 diff --git a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default-brick.cfg b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default-brick.cfg new file mode 100644 index 000000000..3fb868094 --- /dev/null +++ b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default-brick.cfg @@ -0,0 +1,4 @@ +minarch_screen_scaling = Fullscreen +minarch_rewind_granularity = 33 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 128 diff --git a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg new file mode 100644 index 000000000..0f2d11f82 --- /dev/null +++ b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg @@ -0,0 +1,3 @@ +minarch_rewind_granularity = 33 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 128 diff --git a/skeleton/EXTRAS/Emus/tg5040/SFC.pak/default-brick.cfg b/skeleton/EXTRAS/Emus/tg5040/SFC.pak/default-brick.cfg new file mode 100644 index 000000000..aeead9a81 --- /dev/null +++ b/skeleton/EXTRAS/Emus/tg5040/SFC.pak/default-brick.cfg @@ -0,0 +1,4 @@ +minarch_screen_scaling = Fullscreen +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 64 diff --git a/skeleton/EXTRAS/Emus/tg5040/SFC.pak/default.cfg b/skeleton/EXTRAS/Emus/tg5040/SFC.pak/default.cfg new file mode 100644 index 000000000..1a84a2d71 --- /dev/null +++ b/skeleton/EXTRAS/Emus/tg5040/SFC.pak/default.cfg @@ -0,0 +1,3 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 64 diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default-brick.cfg index 90365a9d1..f63e1359e 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default-brick.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default-brick.cfg @@ -1,3 +1,7 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 64 + fceumm_sndquality = High fceumm_sndvolume = 10 -fceumm_aspect = 8:7 PAR diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default.cfg index 90365a9d1..f63e1359e 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default.cfg @@ -1,3 +1,7 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 64 + fceumm_sndquality = High fceumm_sndvolume = 10 -fceumm_aspect = 8:7 PAR diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default-brick.cfg new file mode 100644 index 000000000..2ebae818a --- /dev/null +++ b/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default-brick.cfg @@ -0,0 +1,22 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 64 + +gambatte_gb_colorization = internal +gambatte_gb_internal_palette = TWB64 - Pack 1 +gambatte_gb_palette_twb64_1 = TWB64 038 - Pokemon mini Ver. +gambatte_gb_bootloader = disabled +-gambatte_audio_resampler = sinc + +bind Up = UP +bind Down = DOWN +bind Left = LEFT +bind Right = RIGHT +bind Select = SELECT +bind Start = START +bind A Button = A +bind B Button = B +bind A Turbo = NONE:X +bind B Turbo = NONE:Y +bind Prev. Palette = NONE:L1 +bind Next Palette = NONE:R1 diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default.cfg index d3e80e7b6..2ebae818a 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default.cfg @@ -1,3 +1,7 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 64 + gambatte_gb_colorization = internal gambatte_gb_internal_palette = TWB64 - Pack 1 gambatte_gb_palette_twb64_1 = TWB64 038 - Pokemon mini Ver. diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default-brick.cfg new file mode 100644 index 000000000..50c99dea9 --- /dev/null +++ b/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default-brick.cfg @@ -0,0 +1,18 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 64 + +-gpsp_save_method = libretro + +bind Up = UP +bind Down = DOWN +bind Left = LEFT +bind Right = RIGHT +bind Select = SELECT +bind Start = START +bind A Button = A +bind B Button = B +bind A Turbo = NONE:X +bind B Turbo = NONE:Y +bind L Button = L1 +bind R Button = R1 diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default.cfg index ea76e21f6..50c99dea9 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default.cfg @@ -1,3 +1,7 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 64 + -gpsp_save_method = libretro bind Up = UP diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default-brick.cfg new file mode 100644 index 000000000..969c7667f --- /dev/null +++ b/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default-brick.cfg @@ -0,0 +1,19 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 64 + +gambatte_gb_bootloader = disabled +-gambatte_audio_resampler = sinc + +bind Up = UP +bind Down = DOWN +bind Left = LEFT +bind Right = RIGHT +bind Select = SELECT +bind Start = START +bind A Button = A +bind B Button = B +bind A Turbo = NONE:X +bind B Turbo = NONE:Y +bind Prev. Palette = NONE:L1 +bind Next Palette = NONE:R1 diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default.cfg index 606e7b74c..969c7667f 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default.cfg @@ -1,3 +1,7 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 64 + gambatte_gb_bootloader = disabled -gambatte_audio_resampler = sinc diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default-brick.cfg new file mode 100644 index 000000000..a4e7ba96d --- /dev/null +++ b/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default-brick.cfg @@ -0,0 +1,22 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = on +minarch_rewind_buffer_mb = 64 + +-picodrive_sound_rate = 44100 +-picodrive_smstype = Auto +-picodrive_smsfm = off +-picodrive_smsmapper = Auto +-picodrive_ggghost = off + +bind Up = UP +bind Down = DOWN +bind Left = LEFT +bind Right = RIGHT +bind Mode = SELECT +bind Start = START +bind A Button = Y +bind B Button = X:B +bind C Button = A +bind X Button = B:L1 +bind Y Button = L1:X +bind Z Button = R1 diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default.cfg index dac408932..a4e7ba96d 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default.cfg @@ -1,3 +1,7 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = on +minarch_rewind_buffer_mb = 64 + -picodrive_sound_rate = 44100 -picodrive_smstype = Auto -picodrive_smsfm = off diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg index aecdba77e..3aab24600 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg @@ -1,3 +1,7 @@ +minarch_rewind_granularity = 33 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 128 + -minarch_prevent_tearing = Strict -pcsx_rearmed_display_internal_fps = disabled -pcsx_rearmed_show_input_settings = disabled diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg index 67f23b5b7..6c37d5a06 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg @@ -1,3 +1,7 @@ +minarch_rewind_granularity = 33 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 128 + -minarch_prevent_tearing = Strict -pcsx_rearmed_display_internal_fps = disabled -pcsx_rearmed_show_input_settings = disabled diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default-brick.cfg index 8100f20a1..03959bf06 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default-brick.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default-brick.cfg @@ -1,3 +1,7 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 64 + bind Up = UP bind Down = DOWN bind Left = LEFT diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default.cfg index 8100f20a1..03959bf06 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default.cfg @@ -1,3 +1,7 @@ +minarch_rewind_granularity = 16 +minarch_rewind_skip_compression = off +minarch_rewind_buffer_mb = 64 + bind Up = UP bind Down = DOWN bind Left = LEFT From 98498ac118d151bdbcfd7eef270639c832a3aab0 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:49:57 +0100 Subject: [PATCH 11/29] feat(rewind): sensible defaults for rewind options --- workspace/all/minarch/minarch.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index e261fb64e..c87cc96fd 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -61,8 +61,8 @@ enum { // defaults for rewind UI options (frontend-only) #define MINARCH_DEFAULT_REWIND_ENABLE 0 -#define MINARCH_DEFAULT_REWIND_BUFFER_MB 16 -#define MINARCH_DEFAULT_REWIND_GRANULARITY 66 +#define MINARCH_DEFAULT_REWIND_BUFFER_MB 64 +#define MINARCH_DEFAULT_REWIND_GRANULARITY 16 #define MINARCH_DEFAULT_REWIND_AUDIO 0 // default frontend options @@ -2436,8 +2436,8 @@ static struct Config { .key = "minarch_rewind_buffer_mb", .name = "Rewind Buffer (MB)", .desc = "Memory reserved for rewind snapshots.", - .default_value = 1, // 16MB - .value = 1, + .default_value = 3, // 64MB + .value = 3, .count = 5, .values = rewind_buffer_labels, .labels = rewind_buffer_labels, @@ -2446,8 +2446,8 @@ static struct Config { .key = "minarch_rewind_granularity", .name = "Rewind Interval", .desc = "Milliseconds between rewind snapshots.", - .default_value = 5, // 66ms - .value = 5, + .default_value = 0, // 16ms + .value = 0, .count = 12, .values = rewind_granularity_values, .labels = rewind_granularity_labels, From b43e11fa042bc6fec6fd2b3128e277932a30aedb Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Mon, 15 Dec 2025 21:41:09 +0100 Subject: [PATCH 12/29] feat(rewind): implement delta compression for rewind states --- workspace/all/minarch/minarch.c | 146 ++++++++++++++++++++++++++++++-- 1 file changed, 140 insertions(+), 6 deletions(-) diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index c87cc96fd..c5426de80 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -16,6 +16,7 @@ // minimal LZ4 API forward declarations (linked via -llz4) int LZ4_compress_default(const char* src, char* dst, int srcSize, int dstCapacity); +int LZ4_compress_fast(const char* src, char* dst, int srcSize, int dstCapacity, int acceleration); int LZ4_decompress_safe(const char* src, char* dst, int compressedSize, int dstCapacity); int LZ4_compressBound(int inputSize); @@ -1141,6 +1142,13 @@ typedef struct { uint8_t *scratch; size_t scratch_size; + // Delta compression: store XOR of current vs previous state + uint8_t *prev_state_enc; // previous state for delta encoding (compression) + uint8_t *prev_state_dec; // previous state for delta decoding (decompression) + uint8_t *delta_buf; // scratch buffer for XOR result + int has_prev_enc; // 1 if prev_state_enc is valid + int has_prev_dec; // 1 if prev_state_dec is valid + int granularity_frames; int interval_ms; uint32_t last_push_ms; @@ -1210,6 +1218,9 @@ static void Rewind_free(void) { if (rewind_ctx.entries) free(rewind_ctx.entries); if (rewind_ctx.state_buf) free(rewind_ctx.state_buf); if (rewind_ctx.scratch) free(rewind_ctx.scratch); + if (rewind_ctx.prev_state_enc) free(rewind_ctx.prev_state_enc); + if (rewind_ctx.prev_state_dec) free(rewind_ctx.prev_state_dec); + if (rewind_ctx.delta_buf) free(rewind_ctx.delta_buf); if (rewind_ctx.locks_ready) { pthread_mutex_destroy(&rewind_ctx.lock); pthread_mutex_destroy(&rewind_ctx.queue_mx); @@ -1224,6 +1235,8 @@ static void Rewind_reset(void) { pthread_mutex_lock(&rewind_ctx.lock); rewind_ctx.head = rewind_ctx.tail = 0; rewind_ctx.entry_head = rewind_ctx.entry_tail = rewind_ctx.entry_count = 0; + rewind_ctx.has_prev_enc = 0; + rewind_ctx.has_prev_dec = 0; pthread_mutex_unlock(&rewind_ctx.lock); rewind_ctx.frame_counter = 0; rewind_ctx.last_push_ms = 0; @@ -1356,15 +1369,42 @@ static int Rewind_compress_state(const uint8_t *src, size_t *dest_len) { } return 0; } + + // Delta compression: XOR current state with previous state + // The result is mostly zeros for similar consecutive states, which compresses much faster + const uint8_t *compress_src = src; + int used_delta = 0; + if (rewind_ctx.has_prev_enc && rewind_ctx.prev_state_enc && rewind_ctx.delta_buf) { + size_t i = 0; + size_t state_size = rewind_ctx.state_size; + uint8_t *delta = rewind_ctx.delta_buf; + const uint8_t *prev = rewind_ctx.prev_state_enc; + // Process 8 bytes at a time for better performance + for (; i + 8 <= state_size; i += 8) { + *(uint64_t*)(delta + i) = *(const uint64_t*)(src + i) ^ *(const uint64_t*)(prev + i); + } + // Handle remaining bytes + for (; i < state_size; i++) { + delta[i] = src[i] ^ prev[i]; + } + compress_src = delta; + used_delta = 1; + } + int max_dst = (int)rewind_ctx.scratch_size; - int res = LZ4_compress_default((const char*)src, (char*)rewind_ctx.scratch, (int)rewind_ctx.state_size, max_dst); + // Use LZ4_compress_fast with acceleration=2 for faster compression + // acceleration: 1=default speed, higher=faster but slightly lower ratio + // For rewind states that compress extremely well (often <5%), speed is more valuable + int res = LZ4_compress_fast((const char*)compress_src, (char*)rewind_ctx.scratch, (int)rewind_ctx.state_size, max_dst, 2); if (res <= 0) return -1; *dest_len = (size_t)res; - if (!rewind_ctx.logged_first) { - rewind_ctx.logged_first = 1; - LOG_info("Rewind: state size before compression=%zu bytes, after=%zu bytes (%.1f%%)\n", - rewind_ctx.state_size, *dest_len, 100.0 * (double)*dest_len / (double)rewind_ctx.state_size); + + // Update prev_state_enc with the current state for next delta + if (rewind_ctx.prev_state_enc) { + memcpy(rewind_ctx.prev_state_enc, src, rewind_ctx.state_size); + rewind_ctx.has_prev_enc = 1; } + return 0; } @@ -1422,6 +1462,18 @@ static int Rewind_init(size_t state_size) { return 0; } + // Allocate delta compression buffers (separate for encode/decode to avoid race conditions) + rewind_ctx.prev_state_enc = calloc(1, state_size); + rewind_ctx.prev_state_dec = calloc(1, state_size); + rewind_ctx.delta_buf = calloc(1, state_size); + if (!rewind_ctx.prev_state_enc || !rewind_ctx.prev_state_dec || !rewind_ctx.delta_buf) { + LOG_error("Rewind: failed to allocate delta buffers\n"); + Rewind_free(); + return 0; + } + rewind_ctx.has_prev_enc = 0; + rewind_ctx.has_prev_dec = 0; + int entry_cap = rewind_ctx.capacity / 4096; if (entry_cap < 8) entry_cap = 8; rewind_ctx.entry_capacity = entry_cap; @@ -1653,6 +1705,34 @@ static int Rewind_step_back(void) { return 2; } + // On first rewind step, we need to: + // 1. Wait for any pending compression to finish (so entry indices are stable) + // 2. Copy the last compressed state as our delta reference + if (!rewinding && rewind_ctx.compress && rewind_ctx.prev_state_dec) { + // Wait for worker to finish all pending compressions + if (rewind_ctx.worker_running && rewind_ctx.locks_ready) { + pthread_mutex_lock(&rewind_ctx.queue_mx); + while (rewind_ctx.queue_count > 0) { + pthread_mutex_unlock(&rewind_ctx.queue_mx); + struct timespec ts = {0, 1000000}; // 1ms + nanosleep(&ts, NULL); + pthread_mutex_lock(&rewind_ctx.queue_mx); + } + pthread_mutex_unlock(&rewind_ctx.queue_mx); + } + + // Copy the encoder's prev_state (which is the last state that was compressed) + // This is the state we need to XOR against to decode the most recent entry + pthread_mutex_lock(&rewind_ctx.lock); + if (rewind_ctx.has_prev_enc && rewind_ctx.prev_state_enc) { + memcpy(rewind_ctx.prev_state_dec, rewind_ctx.prev_state_enc, rewind_ctx.state_size); + rewind_ctx.has_prev_dec = 1; + } else { + rewind_ctx.has_prev_dec = 0; + } + pthread_mutex_unlock(&rewind_ctx.lock); + } + pthread_mutex_lock(&rewind_ctx.lock); if (!rewind_ctx.entry_count) { pthread_mutex_unlock(&rewind_ctx.lock); @@ -1669,12 +1749,40 @@ static int Rewind_step_back(void) { int decode_ok = 1; if (rewind_ctx.compress) { + // Decompress into delta_buf first (it contains the XOR delta) int res = LZ4_decompress_safe((const char*)rewind_ctx.buffer + e->offset, - (char*)rewind_ctx.state_buf, (int)e->size, (int)rewind_ctx.state_size); + (char*)rewind_ctx.delta_buf, (int)e->size, (int)rewind_ctx.state_size); if (res < (int)rewind_ctx.state_size) { LOG_error("Rewind: decompress failed (res=%i, want=%zu, compressed=%zu, offset=%zu, idx=%d head=%d tail=%d count=%d buf_head=%zu buf_tail=%zu)\n", res, rewind_ctx.state_size, e->size, e->offset, idx, rewind_ctx.entry_head, rewind_ctx.entry_tail, rewind_ctx.entry_count, rewind_ctx.head, rewind_ctx.tail); decode_ok = 0; + } else if (rewind_ctx.has_prev_dec && rewind_ctx.prev_state_dec) { + // Delta decompression: XOR the delta with prev_state_dec to recover the actual state + // prev_state_dec holds the current state (state N), delta = state_N XOR state_(N-1) + // So: state_(N-1) = delta XOR state_N = delta XOR prev_state_dec + size_t i = 0; + size_t state_size = rewind_ctx.state_size; + uint8_t *result = rewind_ctx.state_buf; + const uint8_t *delta = rewind_ctx.delta_buf; + const uint8_t *prev = rewind_ctx.prev_state_dec; + // Process 8 bytes at a time for better performance + for (; i + 8 <= state_size; i += 8) { + *(uint64_t*)(result + i) = *(const uint64_t*)(delta + i) ^ *(const uint64_t*)(prev + i); + } + // Handle remaining bytes + for (; i < state_size; i++) { + result[i] = delta[i] ^ prev[i]; + } + // Update prev_state_dec to the state we just recovered (for next rewind step) + memcpy(rewind_ctx.prev_state_dec, result, state_size); + } else { + // No previous state for delta - this is the first frame or after reset + // The compressed data is the full state, just copy it + memcpy(rewind_ctx.state_buf, rewind_ctx.delta_buf, rewind_ctx.state_size); + if (rewind_ctx.prev_state_dec) { + memcpy(rewind_ctx.prev_state_dec, rewind_ctx.state_buf, rewind_ctx.state_size); + rewind_ctx.has_prev_dec = 1; + } } } else { if (e->size != rewind_ctx.state_size) { @@ -1717,6 +1825,29 @@ static int Rewind_step_back(void) { return 1; } +// Call this when rewind ends to sync the encode buffer with the last decoded state +// Also clears old entries that were compressed with a different delta chain +static void Rewind_sync_encode_state(void) { + if (!rewind_ctx.enabled || !rewind_ctx.compress) return; + if (!rewinding) return; // Only sync if we were actually rewinding + + pthread_mutex_lock(&rewind_ctx.lock); + + // Clear all existing entries - they were compressed against a different delta chain + // and cannot be decompressed correctly after we resume with a new chain + rewind_ctx.head = rewind_ctx.tail = 0; + rewind_ctx.entry_head = rewind_ctx.entry_tail = rewind_ctx.entry_count = 0; + + // The decoder's prev_state_dec contains the state we rewound to + // This becomes the new reference for future compressions + if (rewind_ctx.has_prev_dec && rewind_ctx.prev_state_dec && rewind_ctx.prev_state_enc) { + memcpy(rewind_ctx.prev_state_enc, rewind_ctx.prev_state_dec, rewind_ctx.state_size); + rewind_ctx.has_prev_enc = 1; + } + + pthread_mutex_unlock(&rewind_ctx.lock); +} + static void Rewind_on_state_change(void) { Rewind_reset(); Rewind_push(1); @@ -2876,6 +3007,7 @@ static void Config_syncFrontend(char* key, int value) { // ensure runtime toggles don't linger when enabling/disabling feature rewind_toggle = 0; rewind_pressed = 0; + Rewind_sync_encode_state(); rewinding = 0; ff_paused_by_rewind_hold = 0; } @@ -3980,6 +4112,7 @@ static void input_poll_callback(void) { // last toggle wins: disable rewind toggle when FF toggle is enabled rewind_toggle = 0; rewind_pressed = 0; + Rewind_sync_encode_state(); rewinding = 0; } if (mapping->mod) ignore_menu = 1; @@ -8023,6 +8156,7 @@ int main(int argc , char* argv[]) { } } else { + Rewind_sync_encode_state(); rewinding = 0; if (ff_paused_by_rewind_hold && !rewind_pressed) { // resume fast forward after hold rewind ends From d9f429ad4803b8831a9043af93b18c3239375c73 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:00:16 +0100 Subject: [PATCH 13/29] feat(rewind): enhance descriptions for rewind configuration options --- workspace/all/minarch/minarch.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index c5426de80..1f8fa1051 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -15,7 +15,6 @@ #include // minimal LZ4 API forward declarations (linked via -llz4) -int LZ4_compress_default(const char* src, char* dst, int srcSize, int dstCapacity); int LZ4_compress_fast(const char* src, char* dst, int srcSize, int dstCapacity, int acceleration); int LZ4_decompress_safe(const char* src, char* dst, int compressedSize, int dstCapacity); int LZ4_compressBound(int inputSize); @@ -2556,7 +2555,7 @@ static struct Config { [FE_OPT_REWIND_ENABLE] = { .key = "minarch_rewind_enable", .name = "Rewind", - .desc = "Enable in-memory rewind buffer.", + .desc = "Enable in-memory rewind buffer.\nMust set a shortcut to access rewind during gameplay.\nUses extra CPU and memory.", .default_value = MINARCH_DEFAULT_REWIND_ENABLE ? 1 : 0, .value = MINARCH_DEFAULT_REWIND_ENABLE ? 1 : 0, .count = 2, @@ -2566,7 +2565,7 @@ static struct Config { [FE_OPT_REWIND_BUFFER] = { .key = "minarch_rewind_buffer_mb", .name = "Rewind Buffer (MB)", - .desc = "Memory reserved for rewind snapshots.", + .desc = "Memory reserved for rewind snapshots.\nIncrease for longer rewind times.", .default_value = 3, // 64MB .value = 3, .count = 5, @@ -2576,7 +2575,7 @@ static struct Config { [FE_OPT_REWIND_GRANULARITY] = { .key = "minarch_rewind_granularity", .name = "Rewind Interval", - .desc = "Milliseconds between rewind snapshots.", + .desc = "Interval between rewind snapshots.\nShorter intervals improve smoothness during rewind,\nbut increase CPU and memory usage.", .default_value = 0, // 16ms .value = 0, .count = 12, @@ -2596,7 +2595,7 @@ static struct Config { [FE_OPT_REWIND_SKIP_COMPRESSION] = { .key = "minarch_rewind_skip_compression", .name = "Skip Rewind Compression", - .desc = "Store raw rewind snapshots instead of compressing them. Uses more memory but less CPU.", + .desc = "Store raw rewind snapshots instead of compressing them.\nUses more memory but less CPU.", .default_value = 0, .value = 0, .count = 2, From 40dc7b0180730b34743ff344330f277b616eb3a3 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:35:46 +0100 Subject: [PATCH 14/29] feat(build): update library paths for shared libraries in makefile --- workspace/all/minarch/makefile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/workspace/all/minarch/makefile b/workspace/all/minarch/makefile index 8342565bc..7f3d12602 100644 --- a/workspace/all/minarch/makefile +++ b/workspace/all/minarch/makefile @@ -63,13 +63,13 @@ all: clean libretro-common $(PREFIX_LOCAL)/include/msettings.h else all: clean libretro-common libsrm.a $(PREFIX_LOCAL)/include/msettings.h mkdir -p build/$(PLATFORM) - cp $(PREFIX)/lib/libsamplerate.so.0 build/$(PLATFORM) - # This is a bandaid fix, needs to be cleaned up if/when we expand to other platforms. - cp $(PREFIX)/lib/libzip.so.5 build/$(PLATFORM) - cp $(PREFIX)/lib/libbz2.so.1.0 build/$(PLATFORM) - cp $(PREFIX)/lib/liblzma.so.5 build/$(PLATFORM) - cp $(PREFIX)/lib/libzstd.so.1 build/$(PLATFORM) - cp $(PREFIX)/lib/liblz4.so.1 build/$(PLATFORM) + # Copy required shared libraries - paths vary by where they're installed in the toolchain + cp /usr/lib/aarch64-linux-gnu/libsamplerate.so.0 build/$(PLATFORM) + cp /usr/local/lib/libzip.so.5 build/$(PLATFORM) + cp /lib/aarch64-linux-gnu/libbz2.so.1.0 build/$(PLATFORM) + cp /lib/aarch64-linux-gnu/liblzma.so.5 build/$(PLATFORM) + cp /usr/lib/aarch64-linux-gnu/libzstd.so.1 build/$(PLATFORM) + cp /usr/lib/aarch64-linux-gnu/liblz4.so.1 build/$(PLATFORM) $(CC) $(SOURCE) -o $(PRODUCT) $(CFLAGS) $(LDFLAGS) endif From 46afceb1014508490d2dcabb49dafa0d34d695e7 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:13:28 +0100 Subject: [PATCH 15/29] feat(build): update image paths and library flags in makefiles --- makefile.toolchain | 4 ++-- workspace/all/minarch/makefile | 14 ++++++++------ workspace/tg5040/poweroff_next/makefile | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/makefile.toolchain b/makefile.toolchain index 2fa75e12d..b6e3f0d31 100644 --- a/makefile.toolchain +++ b/makefile.toolchain @@ -11,8 +11,8 @@ GUEST_WORKSPACE=/root/workspace GIT_IF_NECESSARY=toolchains/$(PLATFORM)-toolchain INIT_IF_NECESSARY=toolchains/$(PLATFORM)-toolchain/.build -# Use local image if it exists, otherwise use remote -IMAGE_NAME=$(shell docker images -q $(PLATFORM)-toolchain:latest 2>/dev/null | head -1 | grep -q . && echo "$(PLATFORM)-toolchain:latest" || echo "ghcr.io/loveretro/$(PLATFORM)-toolchain:modernize") +# Use local image if it exists, otherwise use the specified remote image +IMAGE_NAME=$(shell docker images -q $(PLATFORM)-toolchain:latest 2>/dev/null | head -1 | grep -q . && echo "$(PLATFORM)-toolchain:latest" || echo "ghcr.io/helaas/tg5040-toolchain:modernize-lz4") all: $(INIT_IF_NECESSARY) docker run -it --rm -v $(HOST_WORKSPACE):$(GUEST_WORKSPACE) $(IMAGE_NAME) /bin/bash diff --git a/workspace/all/minarch/makefile b/workspace/all/minarch/makefile index 7f3d12602..664862948 100644 --- a/workspace/all/minarch/makefile +++ b/workspace/all/minarch/makefile @@ -29,6 +29,8 @@ CFLAGS += $(OPT) -fomit-frame-pointer CFLAGS += $(INCDIR) -DPLATFORM=\"$(PLATFORM)\" -std=gnu99 LDFLAGS += -lmsettings -lsamplerate LDFLAGS += -llz4 +# In the modernized toolchain image, liblz4 is available under /usr/lib/aarch64-linux-gnu +LDFLAGS += -L/usr/lib/aarch64-linux-gnu ifeq ($(PLATFORM), desktop) ifeq ($(UNAME_S),Linux) CFLAGS += `pkg-config --cflags libzip` @@ -63,12 +65,12 @@ all: clean libretro-common $(PREFIX_LOCAL)/include/msettings.h else all: clean libretro-common libsrm.a $(PREFIX_LOCAL)/include/msettings.h mkdir -p build/$(PLATFORM) - # Copy required shared libraries - paths vary by where they're installed in the toolchain - cp /usr/lib/aarch64-linux-gnu/libsamplerate.so.0 build/$(PLATFORM) - cp /usr/local/lib/libzip.so.5 build/$(PLATFORM) - cp /lib/aarch64-linux-gnu/libbz2.so.1.0 build/$(PLATFORM) - cp /lib/aarch64-linux-gnu/liblzma.so.5 build/$(PLATFORM) - cp /usr/lib/aarch64-linux-gnu/libzstd.so.1 build/$(PLATFORM) + cp $(PREFIX)/lib/libsamplerate.so.0 build/$(PLATFORM) + # This is a bandaid fix, needs to be cleaned up if/when we expand to other platforms. + cp $(PREFIX)/lib/libzip.so.5 build/$(PLATFORM) + cp $(PREFIX)/lib/libbz2.so.1.0 build/$(PLATFORM) + cp $(PREFIX)/lib/liblzma.so.5 build/$(PLATFORM) + cp $(PREFIX)/lib/libzstd.so.1 build/$(PLATFORM) cp /usr/lib/aarch64-linux-gnu/liblz4.so.1 build/$(PLATFORM) $(CC) $(SOURCE) -o $(PRODUCT) $(CFLAGS) $(LDFLAGS) endif diff --git a/workspace/tg5040/poweroff_next/makefile b/workspace/tg5040/poweroff_next/makefile index e57bf234c..52d3e63ac 100644 --- a/workspace/tg5040/poweroff_next/makefile +++ b/workspace/tg5040/poweroff_next/makefile @@ -24,7 +24,7 @@ SOURCE = $(TARGET).c ../../all/common/utils.c ../../all/common/config.c CC = $(CROSS_COMPILE)gcc # Override CFLAGS and LDFLAGS to not include SDL2 dependencies -CFLAGS = $(ARCH) -g -std=gnu11 -Wall -Wextra +CFLAGS = $(OPT) -g -std=gnu11 -Wall -Wextra CFLAGS += $(INCDIR) -DPLATFORM=\"$(PLATFORM)\" LDFLAGS = -lpthread -ldl From f427d82c262590c82a55a85dc639f25f7cfab4b8 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:31:31 +0100 Subject: [PATCH 16/29] feat(build): update lz4 library path in makefile to use PREFIX --- workspace/all/minarch/makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace/all/minarch/makefile b/workspace/all/minarch/makefile index 664862948..83ef5d699 100644 --- a/workspace/all/minarch/makefile +++ b/workspace/all/minarch/makefile @@ -71,7 +71,7 @@ all: clean libretro-common libsrm.a $(PREFIX_LOCAL)/include/msettings.h cp $(PREFIX)/lib/libbz2.so.1.0 build/$(PLATFORM) cp $(PREFIX)/lib/liblzma.so.5 build/$(PLATFORM) cp $(PREFIX)/lib/libzstd.so.1 build/$(PLATFORM) - cp /usr/lib/aarch64-linux-gnu/liblz4.so.1 build/$(PLATFORM) + cp $(PREFIX)/lib/liblz4.so.1 build/$(PLATFORM) $(CC) $(SOURCE) -o $(PRODUCT) $(CFLAGS) $(LDFLAGS) endif From 37d371e34ce1b299b6abfd25ed6f2e444886da5c Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:50:04 +0100 Subject: [PATCH 17/29] feat(rewind): remove deprecated rewind configuration files and add LZ4 acceleration option --- .../Emus/tg5040/MD.pak/default-brick.cfg | 4 - .../EXTRAS/Emus/tg5040/MD.pak/default.cfg | 3 - .../Emus/tg5040/PS.pak/default-brick.cfg | 5 +- .../EXTRAS/Emus/tg5040/PS.pak/default.cfg | 5 +- .../Emus/tg5040/SFC.pak/default-brick.cfg | 4 - .../EXTRAS/Emus/tg5040/SFC.pak/default.cfg | 3 - workspace/all/minarch/minarch.c | 77 +++++++++++++++---- 7 files changed, 66 insertions(+), 35 deletions(-) delete mode 100644 skeleton/EXTRAS/Emus/tg5040/MD.pak/default-brick.cfg delete mode 100644 skeleton/EXTRAS/Emus/tg5040/MD.pak/default.cfg delete mode 100644 skeleton/EXTRAS/Emus/tg5040/SFC.pak/default-brick.cfg delete mode 100644 skeleton/EXTRAS/Emus/tg5040/SFC.pak/default.cfg diff --git a/skeleton/EXTRAS/Emus/tg5040/MD.pak/default-brick.cfg b/skeleton/EXTRAS/Emus/tg5040/MD.pak/default-brick.cfg deleted file mode 100644 index 73e87c532..000000000 --- a/skeleton/EXTRAS/Emus/tg5040/MD.pak/default-brick.cfg +++ /dev/null @@ -1,4 +0,0 @@ -minarch_screen_scaling = Fullscreen -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = on -minarch_rewind_buffer_mb = 64 diff --git a/skeleton/EXTRAS/Emus/tg5040/MD.pak/default.cfg b/skeleton/EXTRAS/Emus/tg5040/MD.pak/default.cfg deleted file mode 100644 index b023d0a2e..000000000 --- a/skeleton/EXTRAS/Emus/tg5040/MD.pak/default.cfg +++ /dev/null @@ -1,3 +0,0 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = on -minarch_rewind_buffer_mb = 64 diff --git a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default-brick.cfg b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default-brick.cfg index 3fb868094..ef7c2f830 100644 --- a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default-brick.cfg +++ b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default-brick.cfg @@ -1,4 +1,5 @@ minarch_screen_scaling = Fullscreen -minarch_rewind_granularity = 33 -minarch_rewind_skip_compression = off minarch_rewind_buffer_mb = 128 +minarch_rewind_granularity = 33 +minarch_rewind_compression_speed = 12 +minarch_rewind_skip_compression = on \ No newline at end of file diff --git a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg index 0f2d11f82..4dcf9bfc6 100644 --- a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg +++ b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg @@ -1,3 +1,4 @@ -minarch_rewind_granularity = 33 -minarch_rewind_skip_compression = off minarch_rewind_buffer_mb = 128 +minarch_rewind_granularity = 33 +minarch_rewind_compression_speed = 12 +minarch_rewind_skip_compression = on \ No newline at end of file diff --git a/skeleton/EXTRAS/Emus/tg5040/SFC.pak/default-brick.cfg b/skeleton/EXTRAS/Emus/tg5040/SFC.pak/default-brick.cfg deleted file mode 100644 index aeead9a81..000000000 --- a/skeleton/EXTRAS/Emus/tg5040/SFC.pak/default-brick.cfg +++ /dev/null @@ -1,4 +0,0 @@ -minarch_screen_scaling = Fullscreen -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 64 diff --git a/skeleton/EXTRAS/Emus/tg5040/SFC.pak/default.cfg b/skeleton/EXTRAS/Emus/tg5040/SFC.pak/default.cfg deleted file mode 100644 index 1a84a2d71..000000000 --- a/skeleton/EXTRAS/Emus/tg5040/SFC.pak/default.cfg +++ /dev/null @@ -1,3 +0,0 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 64 diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 1f8fa1051..833d2a38b 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -64,6 +64,7 @@ enum { #define MINARCH_DEFAULT_REWIND_BUFFER_MB 64 #define MINARCH_DEFAULT_REWIND_GRANULARITY 16 #define MINARCH_DEFAULT_REWIND_AUDIO 0 +#define MINARCH_DEFAULT_REWIND_LZ4_ACCELERATION 2 // default frontend options static int screen_scaling = SCALE_ASPECT; @@ -92,6 +93,7 @@ static int rewind_cfg_buffer_mb = MINARCH_DEFAULT_REWIND_BUFFER_MB; static int rewind_cfg_granularity = MINARCH_DEFAULT_REWIND_GRANULARITY; static int rewind_cfg_audio = MINARCH_DEFAULT_REWIND_AUDIO; static int rewind_cfg_skip_compress = 0; +static int rewind_cfg_lz4_acceleration = MINARCH_DEFAULT_REWIND_LZ4_ACCELERATION; static int overclock = 3; // auto static int has_custom_controllers = 0; static int gamepad_type = 0; // index in gamepad_labels/gamepad_values @@ -1159,6 +1161,7 @@ typedef struct { int enabled; int audio; int compress; + int lz4_acceleration; int logged_first; // async capture/compression @@ -1391,10 +1394,9 @@ static int Rewind_compress_state(const uint8_t *src, size_t *dest_len) { } int max_dst = (int)rewind_ctx.scratch_size; - // Use LZ4_compress_fast with acceleration=2 for faster compression // acceleration: 1=default speed, higher=faster but slightly lower ratio - // For rewind states that compress extremely well (often <5%), speed is more valuable - int res = LZ4_compress_fast((const char*)compress_src, (char*)rewind_ctx.scratch, (int)rewind_ctx.state_size, max_dst, 2); + int accel = rewind_ctx.lz4_acceleration > 0 ? rewind_ctx.lz4_acceleration : MINARCH_DEFAULT_REWIND_LZ4_ACCELERATION; + int res = LZ4_compress_fast((const char*)compress_src, (char*)rewind_ctx.scratch, (int)rewind_ctx.state_size, max_dst, accel); if (res <= 0) return -1; *dest_len = (size_t)res; @@ -1435,9 +1437,19 @@ static int Rewind_init(size_t state_size) { state_size, rewind_ctx.capacity); rewind_ctx.compress = 1; } + int accel = rewind_cfg_lz4_acceleration; + if (accel < 1) accel = 1; + if (accel > 64) accel = 64; + rewind_ctx.lz4_acceleration = accel; rewind_ctx.logged_first = 0; - LOG_info("Rewind: config enable=%i bufferMB=%i interval=%ims audio=%i compression=%s\n", - enable, buf_mb, gran, audio, rewind_ctx.compress ? "lz4" : "raw"); + if (rewind_ctx.compress) { + LOG_info("Rewind: config enable=%i bufferMB=%i interval=%ims audio=%i compression=lz4 (accel=%i)\n", + enable, buf_mb, gran, audio, rewind_ctx.lz4_acceleration); + } + else { + LOG_info("Rewind: config enable=%i bufferMB=%i interval=%ims audio=%i compression=raw\n", + enable, buf_mb, gran, audio); + } rewind_ctx.buffer = calloc(1, rewind_ctx.capacity); if (!rewind_ctx.buffer) { LOG_error("Rewind: failed to allocate buffer\n"); @@ -1950,6 +1962,22 @@ static char* rewind_granularity_labels[] = { "600 ms", NULL }; +static char* rewind_compression_accel_values[] = { + "1", + "2", + "4", + "8", + "12", + NULL +}; +static char* rewind_compression_accel_labels[] = { + "1 (best ratio)", + "2 (default)", + "4 (fast)", + "8 (faster)", + "12 (fastest)", + NULL +}; static char* ambient_labels[] = { "Off", "All", @@ -2186,8 +2214,9 @@ enum { FE_OPT_REWIND_ENABLE, FE_OPT_REWIND_BUFFER, FE_OPT_REWIND_GRANULARITY, - FE_OPT_REWIND_AUDIO, FE_OPT_REWIND_SKIP_COMPRESSION, + FE_OPT_REWIND_COMPRESSION_ACCEL, + FE_OPT_REWIND_AUDIO, FE_OPT_COUNT, }; @@ -2582,16 +2611,6 @@ static struct Config { .values = rewind_granularity_values, .labels = rewind_granularity_labels, }, - [FE_OPT_REWIND_AUDIO] = { - .key = "minarch_rewind_audio", - .name = "Rewind audio", - .desc = "Play or mute audio when rewinding.", - .default_value = MINARCH_DEFAULT_REWIND_AUDIO ? 1 : 0, - .value = MINARCH_DEFAULT_REWIND_AUDIO ? 1 : 0, - .count = 2, - .values = onoff_labels, - .labels = onoff_labels, - }, [FE_OPT_REWIND_SKIP_COMPRESSION] = { .key = "minarch_rewind_skip_compression", .name = "Skip Rewind Compression", @@ -2602,6 +2621,26 @@ static struct Config { .values = onoff_labels, .labels = onoff_labels, }, + [FE_OPT_REWIND_COMPRESSION_ACCEL] = { + .key = "minarch_rewind_compression_speed", + .name = "Rewind Compression Speed", + .desc = "LZ4 acceleration used for rewind snapshots.\nLower values compress more but use more CPU.", + .default_value = 1, // value 2 + .value = 1, + .count = 5, + .values = rewind_compression_accel_values, + .labels = rewind_compression_accel_labels, + }, + [FE_OPT_REWIND_AUDIO] = { + .key = "minarch_rewind_audio", + .name = "Rewind audio", + .desc = "Play or mute audio when rewinding.", + .default_value = MINARCH_DEFAULT_REWIND_AUDIO ? 1 : 0, + .value = MINARCH_DEFAULT_REWIND_AUDIO ? 1 : 0, + .count = 2, + .values = onoff_labels, + .labels = onoff_labels, + }, [FE_OPT_COUNT] = {NULL} } }, @@ -2977,10 +3016,13 @@ static void Config_syncFrontend(char* key, int value) { else if (exactMatch(key,config.frontend.options[FE_OPT_REWIND_SKIP_COMPRESSION].key)) { i = FE_OPT_REWIND_SKIP_COMPRESSION; } + else if (exactMatch(key,config.frontend.options[FE_OPT_REWIND_COMPRESSION_ACCEL].key)) { + i = FE_OPT_REWIND_COMPRESSION_ACCEL; + } if (i==-1) return; Option* option = &config.frontend.options[i]; option->value = value; - if (i==FE_OPT_REWIND_ENABLE || i==FE_OPT_REWIND_BUFFER || i==FE_OPT_REWIND_GRANULARITY || i==FE_OPT_REWIND_AUDIO || i==FE_OPT_REWIND_SKIP_COMPRESSION) { + if (i==FE_OPT_REWIND_ENABLE || i==FE_OPT_REWIND_BUFFER || i==FE_OPT_REWIND_GRANULARITY || i==FE_OPT_REWIND_AUDIO || i==FE_OPT_REWIND_SKIP_COMPRESSION || i==FE_OPT_REWIND_COMPRESSION_ACCEL) { const char* sval = option->values && option->values[value] ? option->values[value] : "0"; int parsed = 0; if (i==FE_OPT_REWIND_ENABLE || i==FE_OPT_REWIND_AUDIO || i==FE_OPT_REWIND_SKIP_COMPRESSION) { @@ -2996,6 +3038,7 @@ static void Config_syncFrontend(char* key, int value) { case FE_OPT_REWIND_GRANULARITY: rewind_cfg_granularity = parsed; break; case FE_OPT_REWIND_AUDIO: rewind_cfg_audio = parsed; break; case FE_OPT_REWIND_SKIP_COMPRESSION: rewind_cfg_skip_compress = parsed; break; + case FE_OPT_REWIND_COMPRESSION_ACCEL: rewind_cfg_lz4_acceleration = parsed; break; } // Only call Rewind_init if core is initialized; early config reads happen before // the core is ready and will be followed by an explicit Rewind_init later From 51b6b4f3ef247b40bfb2b9ddd7c1732b355c0647 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:57:41 +0100 Subject: [PATCH 18/29] feat(rewind): remove deprecated rewind configuration options from various emulator config files --- .../EXTRAS/Emus/tg5040/PS.pak/default.cfg | 4 ---- .../tg5040/paks/Emus/FC.pak/default-brick.cfg | 4 ---- .../tg5040/paks/Emus/FC.pak/default.cfg | 4 ---- .../tg5040/paks/Emus/GB.pak/default-brick.cfg | 22 ------------------- .../tg5040/paks/Emus/GB.pak/default.cfg | 4 ---- .../paks/Emus/GBA.pak/default-brick.cfg | 18 --------------- .../tg5040/paks/Emus/GBA.pak/default.cfg | 4 ---- .../paks/Emus/GBC.pak/default-brick.cfg | 19 ---------------- .../tg5040/paks/Emus/GBC.pak/default.cfg | 4 ---- .../tg5040/paks/Emus/MD.pak/default-brick.cfg | 22 ------------------- .../tg5040/paks/Emus/MD.pak/default.cfg | 4 ---- .../tg5040/paks/Emus/PS.pak/default-brick.cfg | 4 ---- .../tg5040/paks/Emus/PS.pak/default.cfg | 4 ---- .../paks/Emus/SFC.pak/default-brick.cfg | 4 ---- .../tg5040/paks/Emus/SFC.pak/default.cfg | 4 ---- 15 files changed, 125 deletions(-) delete mode 100644 skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg delete mode 100644 skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default-brick.cfg delete mode 100644 skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default-brick.cfg delete mode 100644 skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default-brick.cfg delete mode 100644 skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default-brick.cfg diff --git a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg deleted file mode 100644 index 4dcf9bfc6..000000000 --- a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg +++ /dev/null @@ -1,4 +0,0 @@ -minarch_rewind_buffer_mb = 128 -minarch_rewind_granularity = 33 -minarch_rewind_compression_speed = 12 -minarch_rewind_skip_compression = on \ No newline at end of file diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default-brick.cfg index f63e1359e..90365a9d1 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default-brick.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default-brick.cfg @@ -1,7 +1,3 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 64 - fceumm_sndquality = High fceumm_sndvolume = 10 -fceumm_aspect = 8:7 PAR diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default.cfg index f63e1359e..90365a9d1 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/FC.pak/default.cfg @@ -1,7 +1,3 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 64 - fceumm_sndquality = High fceumm_sndvolume = 10 -fceumm_aspect = 8:7 PAR diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default-brick.cfg deleted file mode 100644 index 2ebae818a..000000000 --- a/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default-brick.cfg +++ /dev/null @@ -1,22 +0,0 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 64 - -gambatte_gb_colorization = internal -gambatte_gb_internal_palette = TWB64 - Pack 1 -gambatte_gb_palette_twb64_1 = TWB64 038 - Pokemon mini Ver. -gambatte_gb_bootloader = disabled --gambatte_audio_resampler = sinc - -bind Up = UP -bind Down = DOWN -bind Left = LEFT -bind Right = RIGHT -bind Select = SELECT -bind Start = START -bind A Button = A -bind B Button = B -bind A Turbo = NONE:X -bind B Turbo = NONE:Y -bind Prev. Palette = NONE:L1 -bind Next Palette = NONE:R1 diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default.cfg index 2ebae818a..d3e80e7b6 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/GB.pak/default.cfg @@ -1,7 +1,3 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 64 - gambatte_gb_colorization = internal gambatte_gb_internal_palette = TWB64 - Pack 1 gambatte_gb_palette_twb64_1 = TWB64 038 - Pokemon mini Ver. diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default-brick.cfg deleted file mode 100644 index 50c99dea9..000000000 --- a/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default-brick.cfg +++ /dev/null @@ -1,18 +0,0 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 64 - --gpsp_save_method = libretro - -bind Up = UP -bind Down = DOWN -bind Left = LEFT -bind Right = RIGHT -bind Select = SELECT -bind Start = START -bind A Button = A -bind B Button = B -bind A Turbo = NONE:X -bind B Turbo = NONE:Y -bind L Button = L1 -bind R Button = R1 diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default.cfg index 50c99dea9..ea76e21f6 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/GBA.pak/default.cfg @@ -1,7 +1,3 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 64 - -gpsp_save_method = libretro bind Up = UP diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default-brick.cfg deleted file mode 100644 index 969c7667f..000000000 --- a/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default-brick.cfg +++ /dev/null @@ -1,19 +0,0 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 64 - -gambatte_gb_bootloader = disabled --gambatte_audio_resampler = sinc - -bind Up = UP -bind Down = DOWN -bind Left = LEFT -bind Right = RIGHT -bind Select = SELECT -bind Start = START -bind A Button = A -bind B Button = B -bind A Turbo = NONE:X -bind B Turbo = NONE:Y -bind Prev. Palette = NONE:L1 -bind Next Palette = NONE:R1 diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default.cfg index 969c7667f..606e7b74c 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/GBC.pak/default.cfg @@ -1,7 +1,3 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 64 - gambatte_gb_bootloader = disabled -gambatte_audio_resampler = sinc diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default-brick.cfg deleted file mode 100644 index a4e7ba96d..000000000 --- a/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default-brick.cfg +++ /dev/null @@ -1,22 +0,0 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = on -minarch_rewind_buffer_mb = 64 - --picodrive_sound_rate = 44100 --picodrive_smstype = Auto --picodrive_smsfm = off --picodrive_smsmapper = Auto --picodrive_ggghost = off - -bind Up = UP -bind Down = DOWN -bind Left = LEFT -bind Right = RIGHT -bind Mode = SELECT -bind Start = START -bind A Button = Y -bind B Button = X:B -bind C Button = A -bind X Button = B:L1 -bind Y Button = L1:X -bind Z Button = R1 diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default.cfg index a4e7ba96d..dac408932 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/MD.pak/default.cfg @@ -1,7 +1,3 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = on -minarch_rewind_buffer_mb = 64 - -picodrive_sound_rate = 44100 -picodrive_smstype = Auto -picodrive_smsfm = off diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg index 3aab24600..aecdba77e 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg @@ -1,7 +1,3 @@ -minarch_rewind_granularity = 33 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 128 - -minarch_prevent_tearing = Strict -pcsx_rearmed_display_internal_fps = disabled -pcsx_rearmed_show_input_settings = disabled diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg index 6c37d5a06..67f23b5b7 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg @@ -1,7 +1,3 @@ -minarch_rewind_granularity = 33 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 128 - -minarch_prevent_tearing = Strict -pcsx_rearmed_display_internal_fps = disabled -pcsx_rearmed_show_input_settings = disabled diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default-brick.cfg index 03959bf06..8100f20a1 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default-brick.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default-brick.cfg @@ -1,7 +1,3 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 64 - bind Up = UP bind Down = DOWN bind Left = LEFT diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default.cfg index 03959bf06..8100f20a1 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/SFC.pak/default.cfg @@ -1,7 +1,3 @@ -minarch_rewind_granularity = 16 -minarch_rewind_skip_compression = off -minarch_rewind_buffer_mb = 64 - bind Up = UP bind Down = DOWN bind Left = LEFT From 59322bc0c5d8c0c35db426ff8ab85b885c1376c7 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Tue, 23 Dec 2025 22:07:14 +0100 Subject: [PATCH 19/29] feat(rewind): remove deprecated default-brick configuration file and restore default configuration --- .../Emus/tg5040/PS.pak/{default-brick.cfg => default.cfg} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename skeleton/EXTRAS/Emus/tg5040/PS.pak/{default-brick.cfg => default.cfg} (58%) diff --git a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default-brick.cfg b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg similarity index 58% rename from skeleton/EXTRAS/Emus/tg5040/PS.pak/default-brick.cfg rename to skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg index ef7c2f830..15d5fc581 100644 --- a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default-brick.cfg +++ b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg @@ -1,5 +1,4 @@ -minarch_screen_scaling = Fullscreen minarch_rewind_buffer_mb = 128 minarch_rewind_granularity = 33 minarch_rewind_compression_speed = 12 -minarch_rewind_skip_compression = on \ No newline at end of file +minarch_rewind_skip_compression = on From b8da1018c2f02f4c7306e55a1ae9a741c39c6513 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Tue, 23 Dec 2025 23:19:29 +0100 Subject: [PATCH 20/29] feat(rewind): simplify git clone command and improve thread safety in rewind logic --- makefile.toolchain | 3 +- workspace/all/minarch/minarch.c | 194 ++++++++++++++++---------------- 2 files changed, 100 insertions(+), 97 deletions(-) diff --git a/makefile.toolchain b/makefile.toolchain index b6e3f0d31..232c5cf35 100644 --- a/makefile.toolchain +++ b/makefile.toolchain @@ -14,6 +14,7 @@ INIT_IF_NECESSARY=toolchains/$(PLATFORM)-toolchain/.build # Use local image if it exists, otherwise use the specified remote image IMAGE_NAME=$(shell docker images -q $(PLATFORM)-toolchain:latest 2>/dev/null | head -1 | grep -q . && echo "$(PLATFORM)-toolchain:latest" || echo "ghcr.io/helaas/tg5040-toolchain:modernize-lz4") + all: $(INIT_IF_NECESSARY) docker run -it --rm -v $(HOST_WORKSPACE):$(GUEST_WORKSPACE) $(IMAGE_NAME) /bin/bash @@ -22,7 +23,7 @@ $(INIT_IF_NECESSARY): $(GIT_IF_NECESSARY) $(GIT_IF_NECESSARY): mkdir -p toolchains - git clone -b modernize https://github.com/LoveRetro/$(PLATFORM)-toolchain/ toolchains/$(PLATFORM)-toolchain + git clone https://github.com/LoveRetro/$(PLATFORM)-toolchain/ toolchains/$(PLATFORM)-toolchain docker pull $(IMAGE_NAME) && touch toolchains/$(PLATFORM)-toolchain/.build clean: diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 833d2a38b..16de109d2 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -13,11 +13,7 @@ #include #include #include - -// minimal LZ4 API forward declarations (linked via -llz4) -int LZ4_compress_fast(const char* src, char* dst, int srcSize, int dstCapacity, int acceleration); -int LZ4_decompress_safe(const char* src, char* dst, int compressedSize, int dstCapacity); -int LZ4_compressBound(int inputSize); +#include // libretro-common #include "libretro.h" @@ -1729,19 +1725,27 @@ static int Rewind_step_back(void) { nanosleep(&ts, NULL); pthread_mutex_lock(&rewind_ctx.queue_mx); } + // Hold queue_mx while copying prev_state to prevent new work from racing + pthread_mutex_lock(&rewind_ctx.lock); + if (rewind_ctx.has_prev_enc && rewind_ctx.prev_state_enc) { + memcpy(rewind_ctx.prev_state_dec, rewind_ctx.prev_state_enc, rewind_ctx.state_size); + rewind_ctx.has_prev_dec = 1; + } else { + rewind_ctx.has_prev_dec = 0; + } + pthread_mutex_unlock(&rewind_ctx.lock); pthread_mutex_unlock(&rewind_ctx.queue_mx); - } - - // Copy the encoder's prev_state (which is the last state that was compressed) - // This is the state we need to XOR against to decode the most recent entry - pthread_mutex_lock(&rewind_ctx.lock); - if (rewind_ctx.has_prev_enc && rewind_ctx.prev_state_enc) { - memcpy(rewind_ctx.prev_state_dec, rewind_ctx.prev_state_enc, rewind_ctx.state_size); - rewind_ctx.has_prev_dec = 1; } else { - rewind_ctx.has_prev_dec = 0; + // No worker thread, just copy under lock + pthread_mutex_lock(&rewind_ctx.lock); + if (rewind_ctx.has_prev_enc && rewind_ctx.prev_state_enc) { + memcpy(rewind_ctx.prev_state_dec, rewind_ctx.prev_state_enc, rewind_ctx.state_size); + rewind_ctx.has_prev_dec = 1; + } else { + rewind_ctx.has_prev_dec = 0; + } + pthread_mutex_unlock(&rewind_ctx.lock); } - pthread_mutex_unlock(&rewind_ctx.lock); } pthread_mutex_lock(&rewind_ctx.lock); @@ -4166,17 +4170,17 @@ static void input_poll_callback(void) { } } else if (i==SHORTCUT_HOLD_FF) { - // don't allow turn off fast_forward with a release of the hold button - // if it was initially turned on with the toggle button - if (PAD_justPressed(btn) || (!toggled_ff_on && PAD_justReleased(btn))) { - int pressed = PAD_isPressed(btn); - fast_forward = setFastForward(pressed); - ff_hold_active = pressed ? 1 : 0; - if (mapping->mod) ignore_menu = 1; // very unlikely but just in case - } - if (PAD_justReleased(btn) && toggled_ff_on) { - ff_hold_active = 0; - } + // don't allow turn off fast_forward with a release of the hold button + // if it was initially turned on with the toggle button + if (PAD_justPressed(btn) || (!toggled_ff_on && PAD_justReleased(btn))) { + int pressed = PAD_isPressed(btn); + fast_forward = setFastForward(pressed); + ff_hold_active = pressed ? 1 : 0; + if (mapping->mod) ignore_menu = 1; // very unlikely but just in case + } + if (PAD_justReleased(btn) && toggled_ff_on) { + ff_hold_active = 0; + } } else if (i==SHORTCUT_HOLD_REWIND) { rewind_pressed = PAD_isPressed(btn) ? 1 : 0; @@ -4209,12 +4213,12 @@ static void input_poll_callback(void) { } else if (PAD_justReleased(btn)) { if (mapping->mod) ignore_menu = 1; - break; - } + break; } - // Trimui only - else if (PLAT_canTurbo() && i>=SHORTCUT_TOGGLE_TURBO_A && i<=SHORTCUT_TOGGLE_TURBO_R2) { - if (PAD_justPressed(btn)) { + } + // Trimui only + else if (PLAT_canTurbo() && i>=SHORTCUT_TOGGLE_TURBO_A && i<=SHORTCUT_TOGGLE_TURBO_R2) { + if (PAD_justPressed(btn)) { switch(i) { case SHORTCUT_TOGGLE_TURBO_A: PLAT_toggleTurbo(BTN_ID_A); break; case SHORTCUT_TOGGLE_TURBO_B: PLAT_toggleTurbo(BTN_ID_B); break; @@ -7542,28 +7546,28 @@ static void Menu_saveState(void) { } static void Menu_loadState(void) { Menu_updateState(); - - if (menu.save_exists) { - if (menu.total_discs) { - char slot_disc_name[256]; - getFile(menu.txt_path, slot_disc_name, 256); - + + if (menu.save_exists) { + if (menu.total_discs) { + char slot_disc_name[256]; + getFile(menu.txt_path, slot_disc_name, 256); + char slot_disc_path[256]; if (slot_disc_name[0]=='/') strcpy(slot_disc_path, slot_disc_name); else sprintf(slot_disc_path, "%s%s", menu.base_path, slot_disc_name); - + char* disc_path = menu.disc_paths[menu.disc]; if (!exactMatch(slot_disc_path, disc_path)) { Game_changeDisc(slot_disc_path); } } - - state_slot = menu.slot; - putInt(menu.slot_path, menu.slot); - State_read(); - Rewind_on_state_change(); - } + + state_slot = menu.slot; + putInt(menu.slot_path, menu.slot); + State_read(); + Rewind_on_state_change(); } +} static void Menu_loop(void) { @@ -8166,60 +8170,59 @@ int main(int argc , char* argv[]) { // release config when all is loaded Config_free(); - LOG_info("total startup time %ims\n\n",SDL_GetTicks()); - while (!quit) { - GFX_startFrame(); - - // if rewind is toggled, fast-forward toggle must stay off; fast-forward hold pauses rewind - int do_rewind = (rewind_pressed || rewind_toggle) && !(rewind_toggle && ff_hold_active); - if (do_rewind) { - // Rewind_step_back returns: 0=buffer empty, 1=stepped back, 2=waiting for cadence - int rewind_result = Rewind_step_back(); - rewinding = (rewind_result != 0); - if (rewind_result == 1) { - // Actually stepped back - run one frame to render the restored state - fast_forward = 0; - core.run(); - } - else if (rewind_result == 2) { - // Waiting for cadence - don't run core, just re-render current frame - fast_forward = 0; - // Skip core.run() entirely to avoid advancing the game - } - else { - // Buffer empty: auto untoggle rewind, resume FF if it was paused for a hold - if (rewind_toggle) rewind_toggle = 0; - if (ff_paused_by_rewind_hold && ff_toggled) { - ff_paused_by_rewind_hold = 0; - fast_forward = setFastForward(1); - } - core.run(); - Rewind_push(0); - } + LOG_info("total startup time %ims\n\n",SDL_GetTicks()); + while (!quit) { + GFX_startFrame(); + + // if rewind is toggled, fast-forward toggle must stay off; fast-forward hold pauses rewind + int do_rewind = (rewind_pressed || rewind_toggle) && !(rewind_toggle && ff_hold_active); + if (do_rewind) { + // Rewind_step_back returns: 0=buffer empty, 1=stepped back, 2=waiting for cadence + int rewind_result = Rewind_step_back(); + rewinding = (rewind_result != 0); + if (rewind_result == 1) { + // Actually stepped back - run one frame to render the restored state + fast_forward = 0; + core.run(); + } + else if (rewind_result == 2) { + // Waiting for cadence - don't run core, just re-render current frame + fast_forward = 0; + // Skip core.run() entirely to avoid advancing the game + } + else { + // Buffer empty: auto untoggle rewind, resume FF if it was paused for a hold + if (rewind_toggle) rewind_toggle = 0; + if (ff_paused_by_rewind_hold && ff_toggled) { + ff_paused_by_rewind_hold = 0; + fast_forward = setFastForward(1); + } + core.run(); + Rewind_push(0); + } + } + else { + Rewind_sync_encode_state(); + rewinding = 0; + if (ff_paused_by_rewind_hold && !rewind_pressed) { + // resume fast forward after hold rewind ends + if (ff_toggled) fast_forward = setFastForward(1); + ff_paused_by_rewind_hold = 0; } - else { - Rewind_sync_encode_state(); - rewinding = 0; - if (ff_paused_by_rewind_hold && !rewind_pressed) { - // resume fast forward after hold rewind ends - if (ff_toggled) fast_forward = setFastForward(1); - ff_paused_by_rewind_hold = 0; - } - int ff_runs = 1; - if (fast_forward) { - // when "None" is selected, assume a modest 2x instead of unbounded spam - ff_runs = max_ff_speed ? max_ff_speed + 1 : 2; - } + int ff_runs = 1; + if (fast_forward) { + // when "None" is selected, assume a modest 2x instead of unbounded spam + ff_runs = max_ff_speed ? max_ff_speed + 1 : 2; + } - for (int ff_step = 0; ff_step < ff_runs; ff_step++) { - core.run(); - Rewind_push(0); - } - } - limitFF(); - trackFPS(); - + for (int ff_step = 0; ff_step < ff_runs; ff_step++) { + core.run(); + Rewind_push(0); + } + } + limitFF(); + trackFPS(); if (has_pending_opt_change) { has_pending_opt_change = 0; @@ -8231,7 +8234,6 @@ int main(int argc , char* argv[]) { chooseSyncRef(); } - if (show_menu) { PWR_updateFrequency(PWR_UPDATE_FREQ,1); Menu_loop(); From cc2b7efd37e3f7589b88378221b4a8affd02ae34 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Tue, 23 Dec 2025 23:42:56 +0100 Subject: [PATCH 21/29] feat(rewind): implement worker idle wait mechanism for improved thread synchronization --- workspace/all/minarch/minarch.c | 107 ++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 38 deletions(-) diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 16de109d2..0f80eef72 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -1191,6 +1191,7 @@ static int last_rewind_pressed = 0; static void* Rewind_worker_thread(void *arg); static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len); static int Rewind_compress_state(const uint8_t *src, size_t *dest_len); +static void Rewind_wait_for_worker_idle(void); static void Rewind_free(void) { if (rewind_ctx.worker_running) { @@ -1230,6 +1231,7 @@ static void Rewind_free(void) { static void Rewind_reset(void) { if (!rewind_ctx.enabled) return; + Rewind_wait_for_worker_idle(); pthread_mutex_lock(&rewind_ctx.lock); rewind_ctx.head = rewind_ctx.tail = 0; rewind_ctx.entry_head = rewind_ctx.entry_tail = rewind_ctx.entry_count = 0; @@ -1288,6 +1290,19 @@ static void Rewind_drop_oldest(void) { pthread_mutex_unlock(&rewind_ctx.lock); } +// Block until the worker has drained its queue and is not holding any slots +static void Rewind_wait_for_worker_idle(void) { + if (!rewind_ctx.worker_running || !rewind_ctx.pool_size) return; + pthread_mutex_lock(&rewind_ctx.queue_mx); + while (rewind_ctx.queue_count > 0 || rewind_ctx.free_count < rewind_ctx.pool_size) { + pthread_mutex_unlock(&rewind_ctx.queue_mx); + struct timespec ts = {0, 1000000}; // 1ms + nanosleep(&ts, NULL); + pthread_mutex_lock(&rewind_ctx.queue_mx); + } + pthread_mutex_unlock(&rewind_ctx.queue_mx); +} + // Check if an entry overlaps with range [range_start, range_end) in a non-wrapping buffer region static int Rewind_entry_overlaps_range(int entry_idx, size_t range_start, size_t range_end) { RewindEntry *e = &rewind_ctx.entries[entry_idx]; @@ -1591,13 +1606,16 @@ static void* Rewind_worker_thread(void *arg) { size_t dest_len = rewind_ctx.scratch_size; pthread_mutex_lock(&rewind_ctx.lock); - int res = Rewind_compress_state(rewind_ctx.capture_pool[slot], &dest_len); - if (res == 0) { - Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); - } - else { - LOG_error("Rewind: compression failed (%i)\n", res); + if (gen == rewind_ctx.generation) { + int res = Rewind_compress_state(rewind_ctx.capture_pool[slot], &dest_len); + if (res == 0) { + Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); + } + else { + LOG_error("Rewind: compression failed (%i)\n", res); + } } + // If generation changed mid-flight, drop silently after releasing the slot pthread_mutex_unlock(&rewind_ctx.lock); pthread_mutex_lock(&rewind_ctx.queue_mx); @@ -1633,12 +1651,45 @@ static void Rewind_push(int force) { if (rewind_ctx.worker_running && rewind_ctx.pool_size) { int slot = -1; - pthread_mutex_lock(&rewind_ctx.queue_mx); - if (rewind_ctx.free_count && rewind_ctx.queue_count < rewind_ctx.queue_capacity) { - slot = rewind_ctx.free_stack[--rewind_ctx.free_count]; - rewind_ctx.capture_busy[slot] = 1; + while (1) { + pthread_mutex_lock(&rewind_ctx.queue_mx); + if (rewind_ctx.free_count && rewind_ctx.queue_count < rewind_ctx.queue_capacity) { + slot = rewind_ctx.free_stack[--rewind_ctx.free_count]; + rewind_ctx.capture_busy[slot] = 1; + pthread_mutex_unlock(&rewind_ctx.queue_mx); + break; + } + // No free slot: synchronously process the oldest queued capture to preserve ordering + if (rewind_ctx.queue_count > 0) { + int queued_slot = rewind_ctx.queue[rewind_ctx.queue_head]; + unsigned int gen = rewind_ctx.capture_gen[queued_slot]; + rewind_ctx.queue_head = (rewind_ctx.queue_head + 1) % rewind_ctx.queue_capacity; + rewind_ctx.queue_count -= 1; + pthread_mutex_unlock(&rewind_ctx.queue_mx); + + size_t dest_len = rewind_ctx.scratch_size; + pthread_mutex_lock(&rewind_ctx.lock); + if (gen == rewind_ctx.generation) { + int res = Rewind_compress_state(rewind_ctx.capture_pool[queued_slot], &dest_len); + if (res == 0) { + Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); + } + else { + LOG_error("Rewind: compression failed (%i)\n", res); + } + } + pthread_mutex_unlock(&rewind_ctx.lock); + + pthread_mutex_lock(&rewind_ctx.queue_mx); + rewind_ctx.capture_busy[queued_slot] = 0; + rewind_ctx.free_stack[rewind_ctx.free_count++] = queued_slot; + pthread_mutex_unlock(&rewind_ctx.queue_mx); + // loop again to try to grab a free slot for the current frame + continue; + } + pthread_mutex_unlock(&rewind_ctx.queue_mx); + break; } - pthread_mutex_unlock(&rewind_ctx.queue_mx); if (slot < 0) { // worker is busy; fall back to synchronous capture so we don't miss cadence @@ -1717,35 +1768,15 @@ static int Rewind_step_back(void) { // 2. Copy the last compressed state as our delta reference if (!rewinding && rewind_ctx.compress && rewind_ctx.prev_state_dec) { // Wait for worker to finish all pending compressions - if (rewind_ctx.worker_running && rewind_ctx.locks_ready) { - pthread_mutex_lock(&rewind_ctx.queue_mx); - while (rewind_ctx.queue_count > 0) { - pthread_mutex_unlock(&rewind_ctx.queue_mx); - struct timespec ts = {0, 1000000}; // 1ms - nanosleep(&ts, NULL); - pthread_mutex_lock(&rewind_ctx.queue_mx); - } - // Hold queue_mx while copying prev_state to prevent new work from racing - pthread_mutex_lock(&rewind_ctx.lock); - if (rewind_ctx.has_prev_enc && rewind_ctx.prev_state_enc) { - memcpy(rewind_ctx.prev_state_dec, rewind_ctx.prev_state_enc, rewind_ctx.state_size); - rewind_ctx.has_prev_dec = 1; - } else { - rewind_ctx.has_prev_dec = 0; - } - pthread_mutex_unlock(&rewind_ctx.lock); - pthread_mutex_unlock(&rewind_ctx.queue_mx); + Rewind_wait_for_worker_idle(); + pthread_mutex_lock(&rewind_ctx.lock); + if (rewind_ctx.has_prev_enc && rewind_ctx.prev_state_enc) { + memcpy(rewind_ctx.prev_state_dec, rewind_ctx.prev_state_enc, rewind_ctx.state_size); + rewind_ctx.has_prev_dec = 1; } else { - // No worker thread, just copy under lock - pthread_mutex_lock(&rewind_ctx.lock); - if (rewind_ctx.has_prev_enc && rewind_ctx.prev_state_enc) { - memcpy(rewind_ctx.prev_state_dec, rewind_ctx.prev_state_enc, rewind_ctx.state_size); - rewind_ctx.has_prev_dec = 1; - } else { - rewind_ctx.has_prev_dec = 0; - } - pthread_mutex_unlock(&rewind_ctx.lock); + rewind_ctx.has_prev_dec = 0; } + pthread_mutex_unlock(&rewind_ctx.lock); } pthread_mutex_lock(&rewind_ctx.lock); From 18fa433ecf32ef89466a6c36a2d5e7742eb258a0 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Wed, 24 Dec 2025 00:05:37 +0100 Subject: [PATCH 22/29] feat(rewind): enhance rewind functionality with keyframe support and buffer size adjustments --- workspace/all/minarch/minarch.c | 68 +++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 0f80eef72..c99071f23 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -62,6 +62,15 @@ enum { #define MINARCH_DEFAULT_REWIND_AUDIO 0 #define MINARCH_DEFAULT_REWIND_LZ4_ACCELERATION 2 +// rewind implementation constants +#define REWIND_ENTRY_SIZE_HINT 4096 // assumed avg entry size for capacity calc +#define REWIND_MIN_ENTRIES 8 // minimum entry table size +#define REWIND_POOL_SIZE_SMALL 3 // capture pool size for small states +#define REWIND_POOL_SIZE_LARGE 4 // capture pool size for large states +#define REWIND_LARGE_STATE_THRESHOLD (2*1024*1024) // 2MB threshold for pool sizing +#define REWIND_MAX_BUFFER_MB 256 // max rewind buffer size +#define REWIND_MAX_LZ4_ACCELERATION 64 // max LZ4 acceleration value + // default frontend options static int screen_scaling = SCALE_ASPECT; static int resampling_quality = 2; @@ -1120,6 +1129,7 @@ static void State_resume(void) { typedef struct { size_t offset; size_t size; + uint8_t is_keyframe; // 1 if this entry is a full state, 0 if delta-encoded } RewindEntry; typedef struct { @@ -1189,8 +1199,8 @@ static int rewind_warn_empty = 0; static int last_rewind_pressed = 0; static void* Rewind_worker_thread(void *arg); -static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len); -static int Rewind_compress_state(const uint8_t *src, size_t *dest_len); +static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len, int is_keyframe); +static int Rewind_compress_state(const uint8_t *src, size_t *dest_len, int *is_keyframe_out); static void Rewind_wait_for_worker_idle(void); static void Rewind_free(void) { @@ -1312,7 +1322,7 @@ static int Rewind_entry_overlaps_range(int entry_idx, size_t range_start, size_t return (e_start < range_end) && (range_start < e_end); } -static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len) { +static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len, int is_keyframe) { if (dest_len >= rewind_ctx.capacity) { LOG_error("Rewind: state does not fit in buffer\n"); return 0; @@ -1357,6 +1367,7 @@ static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len) RewindEntry *e = &rewind_ctx.entries[rewind_ctx.entry_head]; e->offset = write_offset; e->size = dest_len; + e->is_keyframe = is_keyframe ? 1 : 0; rewind_ctx.head = write_offset + dest_len; if (rewind_ctx.head >= rewind_ctx.capacity) rewind_ctx.head = 0; @@ -1371,11 +1382,13 @@ static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len) return 1; } -static int Rewind_compress_state(const uint8_t *src, size_t *dest_len) { +static int Rewind_compress_state(const uint8_t *src, size_t *dest_len, int *is_keyframe_out) { if (!rewind_ctx.scratch || !dest_len) return -1; + if (is_keyframe_out) *is_keyframe_out = 1; // default to keyframe if (!rewind_ctx.compress) { *dest_len = rewind_ctx.state_size; memcpy(rewind_ctx.scratch, src, rewind_ctx.state_size); + if (is_keyframe_out) *is_keyframe_out = 1; // raw snapshots are always keyframes if (!rewind_ctx.logged_first) { rewind_ctx.logged_first = 1; LOG_info("Rewind: compression disabled, storing %zu bytes per snapshot\n", rewind_ctx.state_size); @@ -1411,6 +1424,9 @@ static int Rewind_compress_state(const uint8_t *src, size_t *dest_len) { if (res <= 0) return -1; *dest_len = (size_t)res; + // Report whether this was a keyframe (full state) or delta + if (is_keyframe_out) *is_keyframe_out = used_delta ? 0 : 1; + // Update prev_state_enc with the current state for next delta if (rewind_ctx.prev_state_enc) { memcpy(rewind_ctx.prev_state_enc, src, rewind_ctx.state_size); @@ -1439,7 +1455,7 @@ static int Rewind_init(size_t state_size) { size_t buffer_mb = buf_mb; if (buffer_mb < 1) buffer_mb = 1; - if (buffer_mb > 256) buffer_mb = 256; + if (buffer_mb > REWIND_MAX_BUFFER_MB) buffer_mb = REWIND_MAX_BUFFER_MB; rewind_ctx.capacity = buffer_mb * 1024 * 1024; rewind_ctx.compress = skip_compress ? 0 : 1; @@ -1450,7 +1466,7 @@ static int Rewind_init(size_t state_size) { } int accel = rewind_cfg_lz4_acceleration; if (accel < 1) accel = 1; - if (accel > 64) accel = 64; + if (accel > REWIND_MAX_LZ4_ACCELERATION) accel = REWIND_MAX_LZ4_ACCELERATION; rewind_ctx.lz4_acceleration = accel; rewind_ctx.logged_first = 0; if (rewind_ctx.compress) { @@ -1496,8 +1512,8 @@ static int Rewind_init(size_t state_size) { rewind_ctx.has_prev_enc = 0; rewind_ctx.has_prev_dec = 0; - int entry_cap = rewind_ctx.capacity / 4096; - if (entry_cap < 8) entry_cap = 8; + int entry_cap = rewind_ctx.capacity / REWIND_ENTRY_SIZE_HINT; + if (entry_cap < REWIND_MIN_ENTRIES) entry_cap = REWIND_MIN_ENTRIES; rewind_ctx.entry_capacity = entry_cap; rewind_ctx.entries = calloc(entry_cap, sizeof(RewindEntry)); if (!rewind_ctx.entries) { @@ -1535,7 +1551,7 @@ static int Rewind_init(size_t state_size) { // set up async capture buffers // Larger states need a deeper pool to avoid drops; cap to a modest size to limit RAM - rewind_ctx.pool_size = (state_size > 2 * 1024 * 1024) ? 4 : 3; + rewind_ctx.pool_size = (state_size > REWIND_LARGE_STATE_THRESHOLD) ? REWIND_POOL_SIZE_LARGE : REWIND_POOL_SIZE_SMALL; if (rewind_ctx.pool_size < 1) rewind_ctx.pool_size = 1; rewind_ctx.capture_pool = calloc(rewind_ctx.pool_size, sizeof(uint8_t*)); rewind_ctx.capture_gen = calloc(rewind_ctx.pool_size, sizeof(unsigned int)); @@ -1605,11 +1621,12 @@ static void* Rewind_worker_thread(void *arg) { } size_t dest_len = rewind_ctx.scratch_size; + int is_keyframe = 1; pthread_mutex_lock(&rewind_ctx.lock); if (gen == rewind_ctx.generation) { - int res = Rewind_compress_state(rewind_ctx.capture_pool[slot], &dest_len); + int res = Rewind_compress_state(rewind_ctx.capture_pool[slot], &dest_len, &is_keyframe); if (res == 0) { - Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); + Rewind_write_entry_locked(rewind_ctx.scratch, dest_len, is_keyframe); } else { LOG_error("Rewind: compression failed (%i)\n", res); @@ -1668,11 +1685,12 @@ static void Rewind_push(int force) { pthread_mutex_unlock(&rewind_ctx.queue_mx); size_t dest_len = rewind_ctx.scratch_size; + int is_keyframe = 1; pthread_mutex_lock(&rewind_ctx.lock); if (gen == rewind_ctx.generation) { - int res = Rewind_compress_state(rewind_ctx.capture_pool[queued_slot], &dest_len); + int res = Rewind_compress_state(rewind_ctx.capture_pool[queued_slot], &dest_len, &is_keyframe); if (res == 0) { - Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); + Rewind_write_entry_locked(rewind_ctx.scratch, dest_len, is_keyframe); } else { LOG_error("Rewind: compression failed (%i)\n", res); @@ -1699,15 +1717,16 @@ static void Rewind_push(int force) { } size_t dest_len = rewind_ctx.scratch_size; + int is_keyframe = 1; pthread_mutex_lock(&rewind_ctx.lock); - int res = Rewind_compress_state(rewind_ctx.state_buf, &dest_len); + int res = Rewind_compress_state(rewind_ctx.state_buf, &dest_len, &is_keyframe); if (res != 0) { pthread_mutex_unlock(&rewind_ctx.lock); LOG_error("Rewind: compression failed (sync fallback) (%i)\n", res); return; } - Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); + Rewind_write_entry_locked(rewind_ctx.scratch, dest_len, is_keyframe); pthread_mutex_unlock(&rewind_ctx.lock); return; } @@ -1740,15 +1759,16 @@ static void Rewind_push(int force) { } size_t dest_len = rewind_ctx.scratch_size; + int is_keyframe = 1; pthread_mutex_lock(&rewind_ctx.lock); - int res = Rewind_compress_state(rewind_ctx.state_buf, &dest_len); + int res = Rewind_compress_state(rewind_ctx.state_buf, &dest_len, &is_keyframe); if (res != 0) { pthread_mutex_unlock(&rewind_ctx.lock); LOG_error("Rewind: compression failed (%i)\n", res); return; } - Rewind_write_entry_locked(rewind_ctx.scratch, dest_len); + Rewind_write_entry_locked(rewind_ctx.scratch, dest_len, is_keyframe); pthread_mutex_unlock(&rewind_ctx.lock); rewind_ctx.drop_warned = 0; } @@ -1795,13 +1815,20 @@ static int Rewind_step_back(void) { int decode_ok = 1; if (rewind_ctx.compress) { - // Decompress into delta_buf first (it contains the XOR delta) + // Decompress into delta_buf first (it may contain XOR delta or full state) int res = LZ4_decompress_safe((const char*)rewind_ctx.buffer + e->offset, (char*)rewind_ctx.delta_buf, (int)e->size, (int)rewind_ctx.state_size); if (res < (int)rewind_ctx.state_size) { LOG_error("Rewind: decompress failed (res=%i, want=%zu, compressed=%zu, offset=%zu, idx=%d head=%d tail=%d count=%d buf_head=%zu buf_tail=%zu)\n", res, rewind_ctx.state_size, e->size, e->offset, idx, rewind_ctx.entry_head, rewind_ctx.entry_tail, rewind_ctx.entry_count, rewind_ctx.head, rewind_ctx.tail); decode_ok = 0; + } else if (e->is_keyframe) { + // This is a keyframe (full state), just copy it directly + memcpy(rewind_ctx.state_buf, rewind_ctx.delta_buf, rewind_ctx.state_size); + if (rewind_ctx.prev_state_dec) { + memcpy(rewind_ctx.prev_state_dec, rewind_ctx.state_buf, rewind_ctx.state_size); + rewind_ctx.has_prev_dec = 1; + } } else if (rewind_ctx.has_prev_dec && rewind_ctx.prev_state_dec) { // Delta decompression: XOR the delta with prev_state_dec to recover the actual state // prev_state_dec holds the current state (state N), delta = state_N XOR state_(N-1) @@ -1822,8 +1849,9 @@ static int Rewind_step_back(void) { // Update prev_state_dec to the state we just recovered (for next rewind step) memcpy(rewind_ctx.prev_state_dec, result, state_size); } else { - // No previous state for delta - this is the first frame or after reset - // The compressed data is the full state, just copy it + // Delta frame but no previous state - this shouldn't happen with proper keyframe tracking + // Fall back to treating it as a full state (may produce incorrect results) + LOG_warn("Rewind: delta frame without previous state, results may be incorrect\n"); memcpy(rewind_ctx.state_buf, rewind_ctx.delta_buf, rewind_ctx.state_size); if (rewind_ctx.prev_state_dec) { memcpy(rewind_ctx.prev_state_dec, rewind_ctx.state_buf, rewind_ctx.state_size); From ade9c548e669fa1ed992ef5f5fc44800b6ad9391 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Wed, 24 Dec 2025 23:00:29 +0100 Subject: [PATCH 23/29] feat(rewind): maintain rewind history integrity after resuming after rewind --- workspace/all/minarch/minarch.c | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index c99071f23..55a027a12 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -1328,6 +1328,12 @@ static int Rewind_write_entry_locked(const uint8_t *compressed, size_t dest_len, return 0; } + // If the entry table is full, drop the oldest entry *before* writing so we don't + // overwrite its metadata (entry_head == entry_tail when full). + if (rewind_ctx.entry_count == rewind_ctx.entry_capacity) { + Rewind_drop_oldest_locked(); + } + size_t write_offset = rewind_ctx.head; // If this write would go past the end of the buffer, wrap to 0 @@ -1906,17 +1912,15 @@ static void Rewind_sync_encode_state(void) { if (!rewinding) return; // Only sync if we were actually rewinding pthread_mutex_lock(&rewind_ctx.lock); - - // Clear all existing entries - they were compressed against a different delta chain - // and cannot be decompressed correctly after we resume with a new chain - rewind_ctx.head = rewind_ctx.tail = 0; - rewind_ctx.entry_head = rewind_ctx.entry_tail = rewind_ctx.entry_count = 0; - - // The decoder's prev_state_dec contains the state we rewound to - // This becomes the new reference for future compressions + + // The decoder's prev_state_dec contains the state we rewound to. + // Use it as the new reference for future compressions so the existing + // rewind history remains valid and we can continue rewinding further back. if (rewind_ctx.has_prev_dec && rewind_ctx.prev_state_dec && rewind_ctx.prev_state_enc) { memcpy(rewind_ctx.prev_state_enc, rewind_ctx.prev_state_dec, rewind_ctx.state_size); rewind_ctx.has_prev_enc = 1; + } else { + rewind_ctx.has_prev_enc = 0; } pthread_mutex_unlock(&rewind_ctx.lock); From e2aa2906b7c400a9590fb27010ad2ef57f37713f Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Wed, 24 Dec 2025 23:23:11 +0100 Subject: [PATCH 24/29] fix(config): correct casing for minarch_rewind_skip_compression in default.cfg refactor(minarch): streamline conditional statements in Rewind_init and Rewind_push functions --- skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg | 2 +- workspace/all/minarch/minarch.c | 17 +++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg index 15d5fc581..83de67fd5 100644 --- a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg +++ b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg @@ -1,4 +1,4 @@ minarch_rewind_buffer_mb = 128 minarch_rewind_granularity = 33 minarch_rewind_compression_speed = 12 -minarch_rewind_skip_compression = on +minarch_rewind_skip_compression = On diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 55a027a12..a43e40d35 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -1478,8 +1478,7 @@ static int Rewind_init(size_t state_size) { if (rewind_ctx.compress) { LOG_info("Rewind: config enable=%i bufferMB=%i interval=%ims audio=%i compression=lz4 (accel=%i)\n", enable, buf_mb, gran, audio, rewind_ctx.lz4_acceleration); - } - else { + } else { LOG_info("Rewind: config enable=%i bufferMB=%i interval=%ims audio=%i compression=raw\n", enable, buf_mb, gran, audio); } @@ -1587,8 +1586,7 @@ static int Rewind_init(size_t state_size) { rewind_ctx.pool_size = 0; rewind_ctx.queue_capacity = 0; rewind_ctx.free_count = 0; - } - else { + } else { rewind_ctx.worker_running = 1; } @@ -1633,8 +1631,7 @@ static void* Rewind_worker_thread(void *arg) { int res = Rewind_compress_state(rewind_ctx.capture_pool[slot], &dest_len, &is_keyframe); if (res == 0) { Rewind_write_entry_locked(rewind_ctx.scratch, dest_len, is_keyframe); - } - else { + } else { LOG_error("Rewind: compression failed (%i)\n", res); } } @@ -1659,8 +1656,7 @@ static void Rewind_push(int force) { if (rewind_ctx.use_time_cadence) { if (rewind_ctx.last_push_ms && (int)(now_ms - rewind_ctx.last_push_ms) < rewind_ctx.interval_ms) return; rewind_ctx.last_push_ms = now_ms; - } - else { + } else { rewind_ctx.frame_counter += 1; if (rewind_ctx.frame_counter < rewind_ctx.granularity_frames) return; rewind_ctx.frame_counter = 0; @@ -1697,8 +1693,7 @@ static void Rewind_push(int force) { int res = Rewind_compress_state(rewind_ctx.capture_pool[queued_slot], &dest_len, &is_keyframe); if (res == 0) { Rewind_write_entry_locked(rewind_ctx.scratch, dest_len, is_keyframe); - } - else { + } else { LOG_error("Rewind: compression failed (%i)\n", res); } } @@ -1897,7 +1892,6 @@ static int Rewind_step_back(void) { if (rewind_ctx.entry_count==0) { rewind_ctx.head = rewind_ctx.tail = 0; } - int remaining = rewind_ctx.entry_count; pthread_mutex_unlock(&rewind_ctx.lock); rewinding = 1; @@ -3120,7 +3114,6 @@ static void Config_syncFrontend(char* key, int value) { rewinding = 0; ff_paused_by_rewind_hold = 0; } - if (core.initialized) Rewind_on_state_change(); } } From 995d7c264e370091e85342e211e48075ae3e4b78 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Thu, 25 Dec 2025 20:20:46 +0100 Subject: [PATCH 25/29] refactor(rewind): improve code readability and maintainability by standardizing spacing and simplifying logic --- workspace/all/minarch/minarch.c | 42 +++++++++++---------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index a43e40d35..8b7a43bf6 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -1278,7 +1278,7 @@ static void Rewind_reset(void) { } static size_t Rewind_free_space_locked(void) { - if (rewind_ctx.entry_count>0 && rewind_ctx.head==rewind_ctx.tail) return 0; + if (rewind_ctx.entry_count > 0 && rewind_ctx.head == rewind_ctx.tail) return 0; if (rewind_ctx.head >= rewind_ctx.tail) return rewind_ctx.capacity - (rewind_ctx.head - rewind_ctx.tail); else @@ -1290,15 +1290,10 @@ static void Rewind_drop_oldest_locked(void) { rewind_ctx.tail = (e->offset + e->size) % rewind_ctx.capacity; rewind_ctx.entry_tail = (rewind_ctx.entry_tail + 1) % rewind_ctx.entry_capacity; rewind_ctx.entry_count -= 1; - if (rewind_ctx.entry_count==0) { + if (rewind_ctx.entry_count == 0) { rewind_ctx.head = rewind_ctx.tail = 0; } } -static void Rewind_drop_oldest(void) { - pthread_mutex_lock(&rewind_ctx.lock); - Rewind_drop_oldest_locked(); - pthread_mutex_unlock(&rewind_ctx.lock); -} // Block until the worker has drained its queue and is not holding any slots static void Rewind_wait_for_worker_idle(void) { @@ -1407,16 +1402,11 @@ static int Rewind_compress_state(const uint8_t *src, size_t *dest_len, int *is_k const uint8_t *compress_src = src; int used_delta = 0; if (rewind_ctx.has_prev_enc && rewind_ctx.prev_state_enc && rewind_ctx.delta_buf) { - size_t i = 0; size_t state_size = rewind_ctx.state_size; uint8_t *delta = rewind_ctx.delta_buf; const uint8_t *prev = rewind_ctx.prev_state_enc; - // Process 8 bytes at a time for better performance - for (; i + 8 <= state_size; i += 8) { - *(uint64_t*)(delta + i) = *(const uint64_t*)(src + i) ^ *(const uint64_t*)(prev + i); - } - // Handle remaining bytes - for (; i < state_size; i++) { + // Byte-by-byte XOR to avoid unaligned memory access issues + for (size_t i = 0; i < state_size; i++) { delta[i] = src[i] ^ prev[i]; } compress_src = delta; @@ -1459,9 +1449,10 @@ static int Rewind_init(size_t state_size) { return 0; } - size_t buffer_mb = buf_mb; - if (buffer_mb < 1) buffer_mb = 1; - if (buffer_mb > REWIND_MAX_BUFFER_MB) buffer_mb = REWIND_MAX_BUFFER_MB; + // Bounds check before size_t conversion to avoid negative int issues + if (buf_mb < 1) buf_mb = 1; + if (buf_mb > REWIND_MAX_BUFFER_MB) buf_mb = REWIND_MAX_BUFFER_MB; + size_t buffer_mb = (size_t)buf_mb; rewind_ctx.capacity = buffer_mb * 1024 * 1024; rewind_ctx.compress = skip_compress ? 0 : 1; @@ -1834,17 +1825,12 @@ static int Rewind_step_back(void) { // Delta decompression: XOR the delta with prev_state_dec to recover the actual state // prev_state_dec holds the current state (state N), delta = state_N XOR state_(N-1) // So: state_(N-1) = delta XOR state_N = delta XOR prev_state_dec - size_t i = 0; size_t state_size = rewind_ctx.state_size; uint8_t *result = rewind_ctx.state_buf; const uint8_t *delta = rewind_ctx.delta_buf; const uint8_t *prev = rewind_ctx.prev_state_dec; - // Process 8 bytes at a time for better performance - for (; i + 8 <= state_size; i += 8) { - *(uint64_t*)(result + i) = *(const uint64_t*)(delta + i) ^ *(const uint64_t*)(prev + i); - } - // Handle remaining bytes - for (; i < state_size; i++) { + // Byte-by-byte XOR to avoid unaligned memory access issues + for (size_t i = 0; i < state_size; i++) { result[i] = delta[i] ^ prev[i]; } // Update prev_state_dec to the state we just recovered (for next rewind step) @@ -1889,7 +1875,7 @@ static int Rewind_step_back(void) { // pop newest rewind_ctx.entry_head = idx; rewind_ctx.entry_count -= 1; - if (rewind_ctx.entry_count==0) { + if (rewind_ctx.entry_count == 0) { rewind_ctx.head = rewind_ctx.tail = 0; } pthread_mutex_unlock(&rewind_ctx.lock); @@ -1926,7 +1912,7 @@ static void Rewind_on_state_change(void) { LOG_info("Rewind: state changed, buffer re-seeded\n"); } - /////////////////////////////// +/////////////////////////////// typedef struct Option { char* key; @@ -8221,8 +8207,8 @@ int main(int argc , char* argv[]) { initShaders(); Config_readOptions(); applyShaderSettings(); - Rewind_init(core.serialize_size()); - Rewind_on_state_change(); + Rewind_init(core.serialize_size ? core.serialize_size() : 0); + if (core.serialize_size) Rewind_on_state_change(); // release config when all is loaded Config_free(); From 940c696dbde4d1a0feec10bf970b8a6052f9ef5b Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Thu, 25 Dec 2025 21:24:07 +0100 Subject: [PATCH 26/29] fix(makefile): remove outdated LDFLAGS for liblz4 in minarch makefile --- workspace/all/minarch/makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/workspace/all/minarch/makefile b/workspace/all/minarch/makefile index 83ef5d699..8342565bc 100644 --- a/workspace/all/minarch/makefile +++ b/workspace/all/minarch/makefile @@ -29,8 +29,6 @@ CFLAGS += $(OPT) -fomit-frame-pointer CFLAGS += $(INCDIR) -DPLATFORM=\"$(PLATFORM)\" -std=gnu99 LDFLAGS += -lmsettings -lsamplerate LDFLAGS += -llz4 -# In the modernized toolchain image, liblz4 is available under /usr/lib/aarch64-linux-gnu -LDFLAGS += -L/usr/lib/aarch64-linux-gnu ifeq ($(PLATFORM), desktop) ifeq ($(UNAME_S),Linux) CFLAGS += `pkg-config --cflags libzip` From 7600706fd3009eaa7cc7c80fcedeccab8c4870c1 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Thu, 25 Dec 2025 21:40:28 +0100 Subject: [PATCH 27/29] fix(makefile): revert IMAGE_NAME to use the remote toolchain image --- makefile.toolchain | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/makefile.toolchain b/makefile.toolchain index 232c5cf35..eb4f83142 100644 --- a/makefile.toolchain +++ b/makefile.toolchain @@ -11,8 +11,7 @@ GUEST_WORKSPACE=/root/workspace GIT_IF_NECESSARY=toolchains/$(PLATFORM)-toolchain INIT_IF_NECESSARY=toolchains/$(PLATFORM)-toolchain/.build -# Use local image if it exists, otherwise use the specified remote image -IMAGE_NAME=$(shell docker images -q $(PLATFORM)-toolchain:latest 2>/dev/null | head -1 | grep -q . && echo "$(PLATFORM)-toolchain:latest" || echo "ghcr.io/helaas/tg5040-toolchain:modernize-lz4") +IMAGE_NAME=ghcr.io/loveretro/$(PLATFORM)-toolchain:modernize all: $(INIT_IF_NECESSARY) From 7c4bb8e7cf6213ad50cae7cd85773191b9ce0a5b Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Thu, 25 Dec 2025 23:32:38 +0100 Subject: [PATCH 28/29] feat(config): migrate rewind settings from default.cfg to default-brick.cfg --- skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg | 4 ---- skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg | 4 ++++ skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) delete mode 100644 skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg diff --git a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg b/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg deleted file mode 100644 index 83de67fd5..000000000 --- a/skeleton/EXTRAS/Emus/tg5040/PS.pak/default.cfg +++ /dev/null @@ -1,4 +0,0 @@ -minarch_rewind_buffer_mb = 128 -minarch_rewind_granularity = 33 -minarch_rewind_compression_speed = 12 -minarch_rewind_skip_compression = On diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg index aecdba77e..8531b6331 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg @@ -2,6 +2,10 @@ -pcsx_rearmed_display_internal_fps = disabled -pcsx_rearmed_show_input_settings = disabled pcsx_rearmed_dithering = enabled +minarch_rewind_buffer_mb = 128 +minarch_rewind_granularity = 33 +minarch_rewind_compression_speed = 12 +minarch_rewind_skip_compression = On minarch_gamepad_type = 1 bind Up = UP diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg index 67f23b5b7..6d72a4755 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg @@ -1,6 +1,10 @@ -minarch_prevent_tearing = Strict -pcsx_rearmed_display_internal_fps = disabled -pcsx_rearmed_show_input_settings = disabled +minarch_rewind_buffer_mb = 128 +minarch_rewind_granularity = 33 +minarch_rewind_compression_speed = 12 +minarch_rewind_skip_compression = On pcsx_rearmed_dithering = enabled @@ -18,4 +22,4 @@ bind Square = Y bind L1 Button = L1 bind R1 Button = R1 bind L2 Button = L2 -bind R2 Button = R2 \ No newline at end of file +bind R2 Button = R2 From d8d3fd9796d8033f5795281093124fb2b5b53c37 Mon Sep 17 00:00:00 2001 From: Kevin Vranken <29517264+Helaas@users.noreply.github.com> Date: Thu, 25 Dec 2025 23:59:52 +0100 Subject: [PATCH 29/29] fix(config): PS: set minarch_rewind_skip_compression to Off in default.cfg and default-brick.cfg feat(minarch): add 256MB option to rewind buffer size and update count in config --- skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg | 2 +- skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg | 2 +- workspace/all/minarch/minarch.c | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg index 8531b6331..bacd9fcd3 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default-brick.cfg @@ -5,7 +5,7 @@ pcsx_rearmed_dithering = enabled minarch_rewind_buffer_mb = 128 minarch_rewind_granularity = 33 minarch_rewind_compression_speed = 12 -minarch_rewind_skip_compression = On +minarch_rewind_skip_compression = Off minarch_gamepad_type = 1 bind Up = UP diff --git a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg index 6d72a4755..b940102c1 100755 --- a/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg +++ b/skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/default.cfg @@ -4,7 +4,7 @@ minarch_rewind_buffer_mb = 128 minarch_rewind_granularity = 33 minarch_rewind_compression_speed = 12 -minarch_rewind_skip_compression = On +minarch_rewind_skip_compression = Off pcsx_rearmed_dithering = enabled diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 8b7a43bf6..7e9db17bd 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -1977,6 +1977,7 @@ static char* rewind_buffer_labels[] = { "32", "64", "128", + "256", NULL }; static char* rewind_granularity_values[] = { @@ -2644,7 +2645,7 @@ static struct Config { .desc = "Memory reserved for rewind snapshots.\nIncrease for longer rewind times.", .default_value = 3, // 64MB .value = 3, - .count = 5, + .count = 6, .values = rewind_buffer_labels, .labels = rewind_buffer_labels, },