Add checkpointing feature for replay recordings. (#15072)

If cores are not deterministic, or if they only have bounded
determinism, we can obtain less drift if replay files also contain
periodic checkpoint states.  These are configured by the new retroarch
stting replay_checkpoint_interval (measured in seconds).  States are
inserted into the replay file in between frames.

This patch also fixes the settings display for the replay
autoincrement max keep setting.
This commit is contained in:
Joe Osborn 2023-03-09 14:14:02 -08:00 committed by GitHub
parent 297aa1ff72
commit 81b3e128ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 155 additions and 1 deletions

View file

@ -1279,6 +1279,10 @@
* replays will be deleted in this case) */
#define DEFAULT_REPLAY_MAX_KEEP 0
/* Specifies how often checkpoints will be saved to replay files during recording.
* > Setting value to zero disables recording checkpoints. */
#define DEFAULT_REPLAY_CHECKPOINT_INTERVAL 0
/* Automatically saves a savestate at the end of RetroArch's lifetime.
* The path is $SRAM_PATH.auto.
* RetroArch will automatically load any savestate with this path on

View file

@ -2202,6 +2202,7 @@ static struct config_uint_setting *populate_settings_uint(
SETTING_UINT("rewind_buffer_size_step", &settings->uints.rewind_buffer_size_step, true, DEFAULT_REWIND_BUFFER_SIZE_STEP, false);
SETTING_UINT("autosave_interval", &settings->uints.autosave_interval, true, DEFAULT_AUTOSAVE_INTERVAL, false);
SETTING_UINT("replay_max_keep", &settings->uints.replay_max_keep, true, DEFAULT_REPLAY_MAX_KEEP, false);
SETTING_UINT("replay_checkpoint_interval", &settings->uints.replay_checkpoint_interval, true, DEFAULT_REPLAY_CHECKPOINT_INTERVAL, false);
SETTING_UINT("savestate_max_keep", &settings->uints.savestate_max_keep, true, DEFAULT_SAVESTATE_MAX_KEEP, false);
SETTING_UINT("frontend_log_level", &settings->uints.frontend_log_level, true, DEFAULT_FRONTEND_LOG_LEVEL, false);
SETTING_UINT("libretro_log_level", &settings->uints.libretro_log_level, true, DEFAULT_LIBRETRO_LOG_LEVEL, false);

View file

@ -204,6 +204,7 @@ typedef struct settings
unsigned rewind_granularity;
unsigned rewind_buffer_size_step;
unsigned autosave_interval;
unsigned replay_checkpoint_interval;
unsigned replay_max_keep;
unsigned savestate_max_keep;
unsigned network_cmd_port;

View file

@ -4784,7 +4784,8 @@ void bsv_movie_finish_rewind(input_driver_state_t *input_st)
}
void bsv_movie_next_frame(input_driver_state_t *input_st)
{
/* Used for rewinding while playback/record. */
settings_t *settings = config_get_ptr();
unsigned checkpoint_interval = settings->uints.replay_checkpoint_interval;
bsv_movie_t *handle = input_st->bsv_movie_state_handle;
if (!handle)
return;
@ -4799,6 +4800,32 @@ void bsv_movie_next_frame(input_driver_state_t *input_st)
intfstream_write(handle->file, &(handle->key_events[i]), sizeof(bsv_key_data_t));
}
bsv_movie_handle_clear_key_events(handle);
/* maybe record checkpoint */
if (checkpoint_interval != 0 && handle->frame_ptr > 0 && (handle->frame_ptr % (checkpoint_interval*60) == 0))
{
retro_ctx_size_info_t info;
retro_ctx_serialize_info_t serial_info;
uint8_t *st;
uint64_t size;
uint8_t frame_tok = REPLAY_TOKEN_CHECKPOINT_FRAME;
core_serialize_size(&info);
size = info.size;
st = (uint8_t*)malloc(info.size);
serial_info.data = st;
serial_info.size = info.size;
core_serialize(&serial_info);
/* "next frame is a checkpoint" */
intfstream_write(handle->file, (uint8_t *)(&frame_tok), sizeof(uint8_t));
intfstream_write(handle->file, &(swap_if_big64(size)), sizeof(uint64_t));
intfstream_write(handle->file, st, info.size);
free(st);
}
else
{
uint8_t frame_tok = REPLAY_TOKEN_REGULAR_FRAME;
/* write "next frame is not a checkpoint" */
intfstream_write(handle->file, (uint8_t *)(&frame_tok), sizeof(uint8_t));
}
}
if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_PLAYBACK)
{
@ -4816,6 +4843,8 @@ void bsv_movie_next_frame(input_driver_state_t *input_st)
{
/* Unnatural EOF */
RARCH_ERR("[Replay] Keyboard replay ran out of keyboard inputs too early\n");
input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END;
return;
}
}
}
@ -4824,6 +4853,52 @@ void bsv_movie_next_frame(input_driver_state_t *input_st)
RARCH_LOG("[Replay] EOF after buttons\n",handle->key_event_count);
/* Natural(?) EOF */
input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END;
return;
}
{
uint8_t next_frame_type=REPLAY_TOKEN_INVALID;
if (intfstream_read(handle->file, (uint8_t *)(&next_frame_type), sizeof(uint8_t)) != sizeof(uint8_t))
{
/* Unnatural EOF */
RARCH_ERR("[Replay] Replay ran out of frames\n");
input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END;
return;
}
else if(next_frame_type == REPLAY_TOKEN_REGULAR_FRAME)
{
/* do nothing */
}
else if(next_frame_type == REPLAY_TOKEN_CHECKPOINT_FRAME)
{
uint64_t size;
if (intfstream_read(handle->file, &(size), sizeof(uint64_t)) != sizeof(uint64_t))
{
RARCH_ERR("[Replay] Replay ran out of frames\n");
input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END;
return;
}
else
{
retro_ctx_serialize_info_t serial_info;
uint8_t *st;
size = swap_if_big64(size);
st = (uint8_t*)malloc(size);
if(intfstream_read(handle->file, st, size) != size)
{
RARCH_ERR("[Replay] Replay checkpoint truncated\n");
input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END;
free(st);
return;
}
else
{
serial_info.data_const = st;
serial_info.size = size;
core_unserialize(&serial_info);
free(st);
}
}
}
}
}
}

View file

@ -163,6 +163,10 @@ struct bsv_movie
};
typedef struct bsv_movie bsv_movie_t;
#define REPLAY_TOKEN_INVALID '\0'
#define REPLAY_TOKEN_REGULAR_FRAME 'f'
#define REPLAY_TOKEN_CHECKPOINT_FRAME 'c'
#endif
/**

View file

@ -337,6 +337,10 @@ MSG_HASH(
MENU_ENUM_LABEL_AUTOSAVE_INTERVAL,
"autosave_interval"
)
MSG_HASH(
MENU_ENUM_LABEL_REPLAY_CHECKPOINT_INTERVAL,
"replay_checkpoint_interval"
)
MSG_HASH(
MENU_ENUM_LABEL_AUTO_OVERRIDES_ENABLE,
"auto_overrides_enable"
@ -3269,6 +3273,10 @@ MSG_HASH(
MENU_ENUM_LABEL_SAVESTATE_MAX_KEEP,
"savestate_max_keep"
)
MSG_HASH(
MENU_ENUM_LABEL_REPLAY_MAX_KEEP,
"replay_max_keep"
)
MSG_HASH(
MENU_ENUM_LABEL_SAVESTATE_DIRECTORY,
"savestate_directory"

View file

@ -265,6 +265,9 @@ int msg_hash_get_help_us_enum(enum msg_hash_enums msg, char *s, size_t len)
case MENU_ENUM_LABEL_AUTOSAVE_INTERVAL:
strlcpy(s, msg_hash_to_str(MENU_ENUM_LABEL_HELP_AUTOSAVE_INTERVAL), len);
break;
case MENU_ENUM_LABEL_REPLAY_CHECKPOINT_INTERVAL:
strlcpy(s, msg_hash_to_str(MENU_ENUM_LABEL_HELP_REPLAY_CHECKPOINT_INTERVAL), len);
break;
case MENU_ENUM_LABEL_VALUE_INPUT_ADC_TYPE:
strlcpy(s, msg_hash_to_str(MENU_ENUM_LABEL_HELP_INPUT_ADC_TYPE), len);
break;

View file

@ -4170,6 +4170,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_REPLAY_CHECKPOINT_INTERVAL,
"Replay Checkpoint Interval"
)
MSG_HASH(
MENU_ENUM_SUBLABEL_REPLAY_CHECKPOINT_INTERVAL,
"Automatically bookmark the game state during replay recording at a regular interval (in seconds)."
)
MSG_HASH(
MENU_ENUM_LABEL_HELP_REPLAY_CHECKPOINT_INTERVAL,
"Autosaves the game state during replay recording at a regular interval. This is disabled by default unless set otherwise. The interval is measured in seconds. A value of 0 disables checkpoint recording."
)
MSG_HASH(
MENU_ENUM_LABEL_VALUE_SAVESTATE_AUTO_INDEX,
"Increment Save State Index Automatically"

View file

@ -731,6 +731,8 @@ 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_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_input_remap_binds_enable, MENU_ENUM_SUBLABEL_INPUT_REMAP_BINDS_ENABLE)
DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_autodetect_enable, MENU_ENUM_SUBLABEL_INPUT_AUTODETECT_ENABLE)
#if defined(HAVE_DINPUT) || defined(HAVE_WINRAWINPUT)
@ -3699,9 +3701,15 @@ 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_REPLAY_CHECKPOINT_INTERVAL:
BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_replay_checkpoint_interval);
break;
case MENU_ENUM_LABEL_SAVESTATE_MAX_KEEP:
BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_savestate_max_keep);
break;
case MENU_ENUM_LABEL_REPLAY_MAX_KEEP:
BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_replay_max_keep);
break;
case MENU_ENUM_LABEL_SAVESTATE_THUMBNAIL_ENABLE:
BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_savestate_thumbnail_enable);
break;

View file

@ -9736,6 +9736,7 @@ unsigned menu_displaylist_build_list(
case DISPLAYLIST_SAVING_SETTINGS_LIST:
{
bool savestate_auto_index = settings->bools.savestate_auto_index;
bool replay_auto_index = settings->bools.replay_auto_index;
menu_displaylist_build_info_selective_t build_list[] = {
{MENU_ENUM_LABEL_SORT_SAVEFILES_ENABLE, PARSE_ONLY_BOOL, true},
@ -9751,6 +9752,7 @@ unsigned menu_displaylist_build_list(
{MENU_ENUM_LABEL_SAVESTATE_THUMBNAIL_ENABLE, PARSE_ONLY_BOOL, true},
{MENU_ENUM_LABEL_REPLAY_AUTO_INDEX, PARSE_ONLY_BOOL, true},
{MENU_ENUM_LABEL_REPLAY_MAX_KEEP, PARSE_ONLY_UINT, false},
{MENU_ENUM_LABEL_REPLAY_CHECKPOINT_INTERVAL, PARSE_ONLY_UINT, true},
{MENU_ENUM_LABEL_SAVE_FILE_COMPRESSION, PARSE_ONLY_BOOL, true},
{MENU_ENUM_LABEL_SAVESTATE_FILE_COMPRESSION, PARSE_ONLY_BOOL, true},
{MENU_ENUM_LABEL_SORT_SCREENSHOTS_BY_CONTENT_ENABLE, PARSE_ONLY_BOOL, true},
@ -9769,6 +9771,9 @@ unsigned menu_displaylist_build_list(
case MENU_ENUM_LABEL_SAVESTATE_MAX_KEEP:
build_list[i].checked = savestate_auto_index;
break;
case MENU_ENUM_LABEL_REPLAY_MAX_KEEP:
build_list[i].checked = replay_auto_index;
break;
default:
break;
}

View file

@ -6700,6 +6700,22 @@ static void setting_get_string_representation_uint_autosave_interval(
}
#endif
static void setting_get_string_representation_uint_replay_checkpoint_interval(
rarch_setting_t *setting,
char *s, size_t len)
{
if (!setting)
return;
if (*setting->value.target.unsigned_integer)
{
snprintf(s, len, "%u ", *setting->value.target.unsigned_integer);
strlcat(s, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_SECONDS), len);
}
else
strlcpy(s, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_OFF), len);
}
#if defined(HAVE_NETWORKING)
static void setting_get_string_representation_netplay_mitm_server(
rarch_setting_t *setting,
@ -11160,6 +11176,22 @@ static bool setting_append_list(
general_read_handler);
(*list)[list_info->index - 1].action_ok = &setting_action_ok_uint;
menu_settings_list_current_add_range(list, list_info, 0, 999, 1, true, true);
CONFIG_UINT(
list, list_info,
&settings->uints.replay_checkpoint_interval,
MENU_ENUM_LABEL_REPLAY_CHECKPOINT_INTERVAL,
MENU_ENUM_LABEL_VALUE_REPLAY_CHECKPOINT_INTERVAL,
DEFAULT_REPLAY_CHECKPOINT_INTERVAL,
&group_info,
&subgroup_info,
parent_group,
general_write_handler,
general_read_handler);
(*list)[list_info->index - 1].action_ok = &setting_action_ok_uint;
(*list)[list_info->index - 1].get_string_representation =
&setting_get_string_representation_uint_replay_checkpoint_interval;
menu_settings_list_current_add_range(list, list_info, 0, 3600, 60, true, false);
#endif
CONFIG_BOOL(

View file

@ -2358,6 +2358,7 @@ enum msg_hash_enums
MENU_LABEL(FRONTEND_LOG_LEVEL),
MENU_LBL_H(LIBRETRO_LOG_LEVEL),
MENU_LBL_H(AUTOSAVE_INTERVAL),
MENU_LBL_H(REPLAY_CHECKPOINT_INTERVAL),
MENU_LBL_H(CONFIG_SAVE_ON_EXIT),
MENU_LABEL(REMAP_SAVE_ON_EXIT),
MENU_LABEL(CONFIGURATION_LIST),