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.
This commit is contained in:
zoltanvb 2024-09-04 07:01:41 +02:00 committed by GitHub
parent cbfe2a7279
commit 98c79b3f14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 226 additions and 108 deletions

322
command.c
View File

@ -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;

View File

@ -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);

View File

@ -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))