From 98c79b3f146938a44205418b83793e85af5202b8 Mon Sep 17 00:00:00 2001 From: zoltanvb <101990835+zoltanvb@users.noreply.github.com> Date: Wed, 4 Sep 2024 07:01:41 +0200 Subject: [PATCH] Add savestate wraparound. (#16947) When save state auto indexing is enabled, and maximum kept states are limited, wrap around after reaching the configured maximum. A gap in the indexing is used to keep track of most recent state. If e.g. maximum kept amount is 5, then indexes 0..5 will be used, if 3 is empty, most recent state is 2. --- command.c | 322 ++++++++++++++++++++++++++++++++++++---------------- command.h | 6 +- retroarch.c | 6 +- 3 files changed, 226 insertions(+), 108 deletions(-) diff --git a/command.c b/command.c index 47c24fe52f..45319cdb6c 100644 --- a/command.c +++ b/command.c @@ -1419,27 +1419,43 @@ void command_event_load_auto_state(void) savestate_name_auto, "failed"); } -void command_event_set_savestate_auto_index(settings_t *settings) +/** + * Scans existing states to determine which one should be loaded + * and which one can be deleted, using savestate wraparound if + * enabled. + * + * @param settings The usual RetroArch settings ptr. + * @param last_index Return value for load slot. + * @param file_to_delete Return value for file name that should be removed. + */ +static void scan_states(settings_t *settings, + unsigned *last_index, char *file_to_delete) { - size_t i; - char state_base[128]; + + runloop_state_t *runloop_st = runloop_state_get_ptr(); + bool show_hidden_files = settings->bools.show_hidden_files; + unsigned savestate_max_keep = settings->uints.savestate_max_keep; + int curr_state_slot = settings->ints.state_slot; + + unsigned max_idx = 0; + unsigned loa_idx = 0; + unsigned gap_idx = UINT_MAX; + unsigned del_idx = UINT_MAX; + retro_bits_512_t slot_mapping_low = {0}; + retro_bits_512_t slot_mapping_high = {0}; + + struct string_list *dir_list = NULL; + const char *savefile_root = NULL; + size_t savefile_root_length = 0; + + size_t i, cnt = 0; + size_t cnt_in_range = 0; char state_dir[PATH_MAX_LENGTH]; - - struct string_list *dir_list = NULL; - unsigned max_idx = 0; - runloop_state_t *runloop_st = runloop_state_get_ptr(); - bool savestate_auto_index = settings->bools.savestate_auto_index; - bool show_hidden_files = settings->bools.show_hidden_files; - - if (!savestate_auto_index) - return; - - /* Find the file in the same directory as runloop_st->savestate_name - * with the largest numeral suffix. - * - * E.g. /foo/path/content.state, will try to find - * /foo/path/content.state%d, where %d is the largest number available. - */ + /* Base name of 128 may be too short for some (<<1%) of the + tosec-based file names, but in practice truncating will not + lead to mismatch */ + char state_base[128]; + fill_pathname_basedir(state_dir, runloop_st->name.savestate, sizeof(state_dir)); @@ -1455,68 +1471,10 @@ void command_event_set_savestate_auto_index(settings_t *settings) for (i = 0; i < dir_list->size; i++) { unsigned idx; - char elem_base[128] = {0}; - const char *end = NULL; - const char *dir_elem = dir_list->elems[i].data; - - fill_pathname_base(elem_base, dir_elem, sizeof(elem_base)); - - if (strstr(elem_base, state_base) != elem_base) - continue; - - end = dir_elem + strlen(dir_elem); - while ((end > dir_elem) && ISDIGIT((int)end[-1])) - end--; - - idx = (unsigned)strtoul(end, NULL, 0); - if (idx > max_idx) - max_idx = idx; - } - - dir_list_free(dir_list); - - configuration_set_int(settings, settings->ints.state_slot, max_idx); - - RARCH_LOG("[State]: %s: #%d\n", - msg_hash_to_str(MSG_FOUND_LAST_STATE_SLOT), - max_idx); -} - -void command_event_set_savestate_garbage_collect( - unsigned max_to_keep, - bool show_hidden_files - ) -{ - size_t i, cnt = 0; - char state_dir[PATH_MAX_LENGTH]; - char state_base[128]; - runloop_state_t *runloop_st = runloop_state_get_ptr(); - - struct string_list *dir_list = NULL; - unsigned min_idx = UINT_MAX; - const char *oldest_save = NULL; - - /* Similar to command_event_set_savestate_auto_index(), - * this will find the lowest numbered save-state */ - fill_pathname_basedir(state_dir, runloop_st->name.savestate, - sizeof(state_dir)); - - dir_list = dir_list_new_special(state_dir, DIR_LIST_PLAIN, NULL, - show_hidden_files); - - if (!dir_list) - return; - - fill_pathname_base(state_base, runloop_st->name.savestate, - sizeof(state_base)); - - for (i = 0; i < dir_list->size; i++) - { - unsigned idx; - char elem_base[128]; - const char *ext = NULL; - const char *end = NULL; - const char *dir_elem = dir_list->elems[i].data; + char elem_base[128] = {0}; + const char *ext = NULL; + const char *end = NULL; + const char *dir_elem = dir_list->elems[i].data; if (string_is_empty(dir_elem)) continue; @@ -1535,39 +1493,206 @@ void command_event_set_savestate_garbage_collect( if (!string_starts_with(elem_base, state_base)) continue; - /* This looks like a valid save */ - cnt++; + /* This looks like a valid savestate */ + /* Save filename root and length (once) */ + if (savefile_root_length == 0) + { + savefile_root = dir_elem; + savefile_root_length = strlen(dir_elem); + } - /* > Get index */ + /* Decode the savestate index */ end = dir_elem + strlen(dir_elem); while ((end > dir_elem) && ISDIGIT((int)end[-1])) + { end--; - + if (savefile_root == dir_elem) + savefile_root_length--; + } idx = string_to_unsigned(end); - /* > Check if this is the lowest index so far */ - if (idx < min_idx) + /* Simple administration: max, total. */ + if (idx > max_idx) + max_idx = idx; + cnt++; + if (idx <= savestate_max_keep) + cnt_in_range++; + + /* Maintain a 2x512 bit map of occupied save states */ + if (idx<512) + BIT512_SET(slot_mapping_low,idx); + else if (idx<1024) + BIT512_SET(slot_mapping_high,idx-512); + } + + /* Next loop on the bitmap, since the file system may have presented the files in any order above */ + for(i=0 ; i <= savestate_max_keep ; i++) + { + /* Unoccupied save slots */ + if ((i < 512 && !BIT512_GET(slot_mapping_low, i)) || + (i > 511 && !BIT512_GET(slot_mapping_high, i-512)) ) { - min_idx = idx; - oldest_save = dir_elem; + /* Gap index: lowest free slot in the wraparound range */ + if (gap_idx == UINT_MAX) + gap_idx = i; + } + /* Occupied save slots */ + else + { + /* Del index: first occupied slot in the wraparound range, + after gap index */ + if (gap_idx < UINT_MAX && + del_idx == UINT_MAX) + del_idx = i; } } + /* Special cases of wraparound */ + + /* No previous savestate - set to end, so that first save + goes to 0 */ + if (cnt_in_range == 0) + { + if (cnt == 0) + loa_idx = savestate_max_keep; + /* Transient: nothing in current range, but something is present + * higher up -> load that */ + else + loa_idx = max_idx; + gap_idx = savestate_max_keep; + del_idx = savestate_max_keep; + } + /* No gap was found - deduct from current index or default + and set (missing) gap index to be deleted */ + else if (gap_idx == UINT_MAX) + { + /* Transient: no gap, and max is higher than currently + * allowed -> load that, but wrap around so that next + * time gap will be present */ + if (max_idx > savestate_max_keep) + { + loa_idx = max_idx; + gap_idx = 1; + } + /* Current index is in range, so let's assume it is correct */ + else if ( (unsigned)curr_state_slot < savestate_max_keep) + { + loa_idx = curr_state_slot; + gap_idx = curr_state_slot + 1; + } + else + { + loa_idx = savestate_max_keep; + gap_idx = 0; + } + del_idx = gap_idx; + } + /* Gap was found */ + else + { + /* No candidate to delete */ + if (del_idx == UINT_MAX) + { + /* Either gap is at the end of the range: wraparound. + or there is no better idea than the lowest index */ + del_idx = 0; + } + /* Adjust load index */ + if (gap_idx == 0) + loa_idx = savestate_max_keep; + else + loa_idx = gap_idx - 1; + } + + RARCH_DBG("[State]: savestate scanning finished, used slots (in range): " + "%d (%d), max:%d, load index %d, gap index %d, delete index %d\n", + cnt, cnt_in_range, max_idx, loa_idx, gap_idx, del_idx); + + if (last_index != NULL) + { + *last_index = loa_idx; + } + if (file_to_delete != NULL && cnt_in_range >= savestate_max_keep) + { + strlcpy(file_to_delete, savefile_root, savefile_root_length + 1); + /* ".state0" is just ".state" instead, so don't print that. */ + if (del_idx > 0) + snprintf(file_to_delete+savefile_root_length, 5, "%d", del_idx); + } + + dir_list_free(dir_list); +} + +/** + * Determines next savestate slot in case of auto-increment, + * i.e. save state scanning was done already earlier. + * Logic moved here so that all save state wraparound code is + * in this file. + * + * @param settings The usual RetroArch settings ptr. + * @return \c The next savestate slot. + */ +int command_event_get_next_savestate_auto_index(settings_t *settings) +{ + unsigned savestate_max_keep = settings->uints.savestate_max_keep; + int new_state_slot = settings->ints.state_slot + 1; + + /* If previous save was above the wraparound range, or it overflows, + return to the start of the range. */ + if( savestate_max_keep > 0 && (unsigned)new_state_slot > savestate_max_keep) + new_state_slot = 0; + + return new_state_slot; +} + +/** + * Determines most recent savestate slot in case of content load. + * + * @param settings The usual RetroArch settings ptr. + * @return \c The most recent savestate slot. + */ +void command_event_set_savestate_auto_index(settings_t *settings) +{ + unsigned max_idx = 0; + bool savestate_auto_index = settings->bools.savestate_auto_index; + + if (!savestate_auto_index) + return; + + scan_states(settings, &max_idx, NULL); + configuration_set_int(settings, settings->ints.state_slot, max_idx); + + RARCH_LOG("[State]: %s: #%d\n", + msg_hash_to_str(MSG_FOUND_LAST_STATE_SLOT), + max_idx); +} + +/** + * Deletes the oldest save state and its thumbnail, if needed. + * + * @param settings The usual RetroArch settings ptr. + */ +static void command_event_set_savestate_garbage_collect(settings_t *settings) +{ + char state_to_delete[PATH_MAX_LENGTH] = {0}; + size_t i; + + scan_states(settings, NULL, state_to_delete); /* Only delete one save state per save action * > Conservative behaviour, designed to minimise * the risk of deleting multiple incorrect files * in case of accident */ - if (!string_is_empty(oldest_save) && (cnt > max_to_keep)) + if (!string_is_empty(state_to_delete)) { - filestream_delete(oldest_save); + filestream_delete(state_to_delete); + RARCH_DBG("[State]: garbage collect, deleting \"%s\" \n",state_to_delete); /* Construct the save state thumbnail name * and delete that one as well. */ - i = strlcpy(state_dir,oldest_save,PATH_MAX_LENGTH); - strlcpy(state_dir + i,".png",STRLEN_CONST(".png")+1); - filestream_delete(state_dir); + i = strlen(state_to_delete); + strlcpy(state_to_delete + i,".png",STRLEN_CONST(".png")+1); + filestream_delete(state_to_delete); + RARCH_DBG("[State]: garbage collect, deleting \"%s\" \n",state_to_delete); } - - dir_list_free(dir_list); } void command_event_set_replay_auto_index(settings_t *settings) @@ -1998,10 +2123,7 @@ bool command_event_main_state(unsigned cmd) /* Clean up excess savestates if necessary */ if (savestate_auto_index && (savestate_max_keep > 0)) - command_event_set_savestate_garbage_collect( - settings->uints.savestate_max_keep, - settings->bools.show_hidden_files - ); + command_event_set_savestate_garbage_collect(settings); if (frame_time_counter_reset_after_save_state) video_st->frame_time_count = 0; diff --git a/command.h b/command.h index 677ae6dee9..290537aea4 100644 --- a/command.h +++ b/command.h @@ -381,10 +381,8 @@ void command_event_load_auto_state(void); void command_event_set_savestate_auto_index( settings_t *settings); -void command_event_set_savestate_garbage_collect( - unsigned max_to_keep, - bool show_hidden_files - ); +int command_event_get_next_savestate_auto_index( + settings_t *settings); void command_event_set_replay_auto_index( settings_t *settings); diff --git a/retroarch.c b/retroarch.c index 45c9512d12..7a23c61aa0 100644 --- a/retroarch.c +++ b/retroarch.c @@ -3558,12 +3558,10 @@ bool command_event(enum event_command cmd, void *data) case CMD_EVENT_SAVE_STATE: case CMD_EVENT_SAVE_STATE_TO_RAM: { - int state_slot = settings->ints.state_slot; - if (settings->bools.savestate_auto_index) { - int new_state_slot = state_slot + 1; - configuration_set_int(settings, settings->ints.state_slot, new_state_slot); + configuration_set_int(settings, settings->ints.state_slot, + command_event_get_next_savestate_auto_index(settings)); } } if (!command_event_main_state(cmd))