diff --git a/config.def.h b/config.def.h index 4446b93ad6fc..038ab6438813 100644 --- a/config.def.h +++ b/config.def.h @@ -1436,6 +1436,16 @@ #define DEFAULT_SAVESTATE_AUTO_SAVE false #define DEFAULT_SAVESTATE_AUTO_LOAD false +/* Automatically saves a savestate at a regular interval. + * It is measured in seconds. A value of 0 disables automatic savestate saving. */ +#if defined(__i386__) || defined(__i486__) || defined(__i686__) || defined(__x86_64__) || defined(_M_X64) || defined(_WIN32) || defined(OSX) || defined(ANDROID) || defined(IOS) || defined(DINGUX) +/* Disabled by default but can be enabled by user */ +#define DEFAULT_AUTOMATIC_SAVESTATE_INTERVAL 0 +#else +/* Default to disabled on I/O-constrained platforms */ +#define DEFAULT_AUTOMATIC_SAVESTATE_INTERVAL 0 +#endif + /* Take screenshots for save states */ #if defined(__x86_64__) || defined(_M_X64) #define DEFAULT_SAVESTATE_THUMBNAIL_ENABLE true diff --git a/configuration.c b/configuration.c index dd9082bf9343..930c54df7af2 100644 --- a/configuration.c +++ b/configuration.c @@ -2461,6 +2461,7 @@ static struct config_uint_setting *populate_settings_uint( SETTING_UINT("memory_update_interval", &settings->uints.memory_update_interval, true, DEFAULT_MEMORY_UPDATE_INTERVAL, false); SETTING_UINT("core_updater_auto_backup_history_size", &settings->uints.core_updater_auto_backup_history_size, true, DEFAULT_CORE_UPDATER_AUTO_BACKUP_HISTORY_SIZE, false); SETTING_UINT("autosave_interval", &settings->uints.autosave_interval, true, DEFAULT_AUTOSAVE_INTERVAL, false); + SETTING_UINT("automatic_savestate_interval", &settings->uints.automatic_savestate_interval, true, DEFAULT_AUTOMATIC_SAVESTATE_INTERVAL, false); SETTING_UINT("rewind_granularity", &settings->uints.rewind_granularity, true, DEFAULT_REWIND_GRANULARITY, false); SETTING_UINT("rewind_buffer_size_step", &settings->uints.rewind_buffer_size_step, true, DEFAULT_REWIND_BUFFER_SIZE_STEP, false); SETTING_UINT("run_ahead_frames", &settings->uints.run_ahead_frames, true, 1, false); diff --git a/configuration.h b/configuration.h index 71d02a785954..4b98d0d78194 100644 --- a/configuration.h +++ b/configuration.h @@ -242,6 +242,7 @@ typedef struct settings unsigned rewind_granularity; unsigned rewind_buffer_size_step; unsigned autosave_interval; + unsigned automatic_savestate_interval; unsigned replay_checkpoint_interval; unsigned replay_max_keep; unsigned savestate_max_keep; diff --git a/content.h b/content.h index 16ff6f89d0ea..9aa4861573b3 100644 --- a/content.h +++ b/content.h @@ -54,6 +54,9 @@ bool content_load_state(const char* path, bool load_to_backup_buffer, bool autol /* Save a state from memory to disk. */ bool content_save_state(const char *path, bool save_to_disk); +/* Automatically save a state if the interval has elapsed. */ +bool content_save_state_automatic(void); + /* Save an automatic savestate to disk. */ bool content_auto_save_state(const char *path); diff --git a/gfx/drivers_shader/slang_cache.cpp b/gfx/drivers_shader/slang_cache.cpp index 79aa5c257f42..f0036430f80d 100644 --- a/gfx/drivers_shader/slang_cache.cpp +++ b/gfx/drivers_shader/slang_cache.cpp @@ -64,13 +64,18 @@ static bool spirv_cache_get_filename(const char *hash, char *cache_file_out, size_t cache_file_out_len) { char cache_dir[PATH_MAX_LENGTH]; + int ret; if (!spirv_cache_get_dir(cache_dir, sizeof(cache_dir))) return false; - snprintf(cache_file_out, cache_file_out_len, "%s/%s.spirv", + ret = snprintf(cache_file_out, cache_file_out_len, "%s/%s.spirv", cache_dir, hash); + /* Check if snprintf truncated the output */ + if (ret < 0 || (size_t)ret >= cache_file_out_len) + return false; + return true; } diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h index a4419efd1869..2ac2f488aafc 100644 --- a/intl/msg_hash_lbl.h +++ b/intl/msg_hash_lbl.h @@ -392,6 +392,10 @@ MSG_HASH( MENU_ENUM_LABEL_AUTOSAVE_INTERVAL, MENU_ENUM_LABEL_AUTOSAVE_INTERVAL_STR ) +MSG_HASH( + MENU_ENUM_LABEL_AUTOMATIC_SAVESTATE_INTERVAL, + MENU_ENUM_LABEL_AUTOMATIC_SAVESTATE_INTERVAL_STR + ) MSG_HASH( MENU_ENUM_LABEL_REPLAY_CHECKPOINT_INTERVAL, MENU_ENUM_LABEL_REPLAY_CHECKPOINT_INTERVAL_STR diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 239be309ed8c..6cf00e9b066a 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -4779,6 +4779,18 @@ MSG_HASH( MENU_ENUM_LABEL_HELP_AUTOSAVE_INTERVAL, "Autosaves the non-volatile SRAM at a regular interval. This is disabled by default unless set otherwise. The interval is measured in seconds. A value of 0 disables autosave." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_AUTOMATIC_SAVESTATE_INTERVAL, + "Automatic Savestate Interval" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_AUTOMATIC_SAVESTATE_INTERVAL, + "Automatically save a savestate at a regular interval (in seconds). Set to 0 to disable." + ) +MSG_HASH( + MENU_ENUM_LABEL_HELP_AUTOMATIC_SAVESTATE_INTERVAL, + "Automatically saves a savestate at regular intervals. This is useful for creating periodic backups of your game progress. The interval is measured in seconds. A value of 0 disables automatic savestate saving." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_REPLAY_CHECKPOINT_INTERVAL, "Replay: Checkpoint Interval" diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index de8c0eec36b6..05883751cb45 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -825,6 +825,7 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_save_file_compression, MENU_ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_savestate_file_compression, MENU_ENUM_SUBLABEL_SAVESTATE_FILE_COMPRESSION) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_savestate_max_keep, MENU_ENUM_SUBLABEL_SAVESTATE_MAX_KEEP) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_autosave_interval, MENU_ENUM_SUBLABEL_AUTOSAVE_INTERVAL) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_automatic_savestate_interval, MENU_ENUM_SUBLABEL_AUTOMATIC_SAVESTATE_INTERVAL) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_replay_max_keep, MENU_ENUM_SUBLABEL_REPLAY_MAX_KEEP) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_replay_checkpoint_interval, MENU_ENUM_SUBLABEL_REPLAY_CHECKPOINT_INTERVAL) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_replay_checkpoint_deserialize, MENU_ENUM_SUBLABEL_REPLAY_CHECKPOINT_DESERIALIZE) @@ -4107,6 +4108,9 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_AUTOSAVE_INTERVAL: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_autosave_interval); break; + case MENU_ENUM_LABEL_AUTOMATIC_SAVESTATE_INTERVAL: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_automatic_savestate_interval); + break; case MENU_ENUM_LABEL_REPLAY_CHECKPOINT_INTERVAL: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_replay_checkpoint_interval); break; diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index 78c185b58b3e..bf2f682af8d6 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -11138,6 +11138,7 @@ unsigned menu_displaylist_build_list( {MENU_ENUM_LABEL_SORT_SAVEFILES_BY_CONTENT_ENABLE, PARSE_ONLY_BOOL, true}, {MENU_ENUM_LABEL_SAVEFILES_IN_CONTENT_DIR_ENABLE, PARSE_ONLY_BOOL, true}, {MENU_ENUM_LABEL_AUTOSAVE_INTERVAL, PARSE_ONLY_UINT, true}, + {MENU_ENUM_LABEL_AUTOMATIC_SAVESTATE_INTERVAL, PARSE_ONLY_UINT, true}, {MENU_ENUM_LABEL_BLOCK_SRAM_OVERWRITE, PARSE_ONLY_BOOL, true}, #if defined(HAVE_ZLIB) {MENU_ENUM_LABEL_SAVE_FILE_COMPRESSION, PARSE_ONLY_BOOL, true}, diff --git a/menu/menu_setting.c b/menu/menu_setting.c index 1fc6a6401d8e..34f69c037b18 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -12006,6 +12006,26 @@ static bool setting_append_list( (*list)[list_info->index - 1].get_string_representation = &setting_get_string_representation_uint_autosave_interval; #endif + +#ifdef HAVE_THREADS + CONFIG_UINT( + list, list_info, + &settings->uints.automatic_savestate_interval, + MENU_ENUM_LABEL_AUTOMATIC_SAVESTATE_INTERVAL, + MENU_ENUM_LABEL_VALUE_AUTOMATIC_SAVESTATE_INTERVAL, + DEFAULT_AUTOMATIC_SAVESTATE_INTERVAL, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler); + (*list)[list_info->index - 1].action_ok = &setting_action_ok_uint; + menu_settings_list_current_add_range(list, list_info, 0, 0, 1, true, false); + SETTINGS_DATA_LIST_CURRENT_ADD_FLAGS(list, list_info, SD_FLAG_CMD_APPLY_AUTO); + (*list)[list_info->index - 1].get_string_representation = + &setting_get_string_representation_uint_autosave_interval; +#endif + CONFIG_BOOL( list, list_info, &settings->bools.savestate_auto_index, diff --git a/msg_hash.h b/msg_hash.h index 1bfe9e830f03..b5ab3c57f728 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -2546,6 +2546,7 @@ enum msg_hash_enums MENU_LABEL(FRONTEND_LOG_LEVEL), MENU_LBL_H(LIBRETRO_LOG_LEVEL), MENU_LBL_H(AUTOSAVE_INTERVAL), + MENU_LBL_H(AUTOMATIC_SAVESTATE_INTERVAL), MENU_LBL_H(REPLAY_CHECKPOINT_INTERVAL), MENU_LBL_H(REPLAY_CHECKPOINT_DESERIALIZE), MENU_LBL_H(CONFIG_SAVE_ON_EXIT), diff --git a/msg_hash_lbl_str.h b/msg_hash_lbl_str.h index 304770c572b8..11cce45f22ca 100644 --- a/msg_hash_lbl_str.h +++ b/msg_hash_lbl_str.h @@ -107,6 +107,7 @@ #define MENU_ENUM_LABEL_MICROPHONE_WASAPI_SH_BUFFER_LENGTH_STR "microphone_wasapi_sh_buffer_length" #define MENU_ENUM_LABEL_MICROPHONE_RESAMPLER_QUALITY_STR "microphone_resampler_quality" #define MENU_ENUM_LABEL_AUTOSAVE_INTERVAL_STR "autosave_interval" +#define MENU_ENUM_LABEL_AUTOMATIC_SAVESTATE_INTERVAL_STR "automatic_savestate_interval" #define MENU_ENUM_LABEL_REPLAY_CHECKPOINT_INTERVAL_STR "replay_checkpoint_interval" #define MENU_ENUM_LABEL_REPLAY_CHECKPOINT_DESERIALIZE_STR "replay_checkpoint_deserialize" #define MENU_ENUM_LABEL_AUTO_OVERRIDES_ENABLE_STR "auto_overrides_enable" diff --git a/runloop.c b/runloop.c index 034214ee68ad..dc0c07de5fe7 100644 --- a/runloop.c +++ b/runloop.c @@ -7476,6 +7476,9 @@ int runloop_iterate(void) autosave_unlock(); #endif + /* Check if we should save state automatically */ + content_save_state_automatic(); + end: if (vrr_runloop_enable) { diff --git a/tasks/task_save.c b/tasks/task_save.c index 03ff21eb15f1..0a5bb810f5df 100644 --- a/tasks/task_save.c +++ b/tasks/task_save.c @@ -135,6 +135,9 @@ static struct ram_save_state_buf ram_buf; static bool save_state_in_background = false; static bool save_state_disable_undo = false; +/* Time tracking for automatic savestate interval */ +static time_t last_automatic_savestate_time = 0; + typedef struct rastate_size_info { size_t total_size; @@ -1882,3 +1885,43 @@ void set_save_state_disable_undo(bool disable) { save_state_disable_undo = disable; } + +bool content_save_state_automatic(void) +{ + time_t current_time; + char savestate_path[PATH_MAX_LENGTH]; + settings_t *settings = config_get_ptr(); + unsigned automatic_savestate_interval = + settings->uints.automatic_savestate_interval; + + /* Return early if automatic savestate is disabled, + safety checks already happen in content_save_state() */ + if (automatic_savestate_interval == 0) + return false; + + current_time = time(NULL); + + /* Check how long since last autosavestate */ + if ((current_time - last_automatic_savestate_time) < + (time_t)automatic_savestate_interval) + return false; + + /* Generate the savestate path */ + if (!runloop_get_savestate_path(savestate_path, + sizeof(savestate_path), -1)) + { + RARCH_WARN("[State] %s\n", + msg_hash_to_str(MSG_FAILED_TO_SAVE_STATE_TO)); + return false; + } + + /* Trigger the savestate */ + RARCH_LOG("[State] %s (automatic) to \"%s\".\n", + msg_hash_to_str(MSG_SAVING_STATE), + savestate_path); + + /* Update the last savestate time, rinse/repeat */ + last_automatic_savestate_time = current_time; + + return content_auto_save_state(savestate_path); +}