From f7e6af11c6a90fcdf627c76a1388a5131e6cedcb Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Fri, 29 Aug 2025 15:31:09 -0700 Subject: [PATCH 01/12] Add save checkpoint, prev/next checkpoint commands and hotkeys --- Makefile.common | 12 ++++++------ command.h | 6 ++++++ config.def.keybinds.h | 21 +++++++++++++++++++++ configuration.c | 3 +++ input/bsv/bsvmovie.c | 13 +++++++++++++ input/input_defines.h | 3 +++ input/input_driver.h | 3 +++ intl/msg_hash_us.h | 24 ++++++++++++++++++++++++ menu/cbs/menu_cbs_sublabel.c | 12 ++++++++++++ msg_hash.h | 6 ++++++ retroarch.c | 15 +++++++++++++++ runloop.c | 3 +++ 12 files changed, 115 insertions(+), 6 deletions(-) diff --git a/Makefile.common b/Makefile.common index 3a6dc9685ce2..411f959f392e 100644 --- a/Makefile.common +++ b/Makefile.common @@ -25,10 +25,6 @@ ifeq ($(HAVE_SAPI), 1) LIBS += sapi.dll endif -ifeq ($(HAVE_STATESTREAM), 1) - DEF_FLAGS += -DHAVE_STATESTREAM -endif - ifeq ($(HAVE_GL_CONTEXT),) HAVE_GL_CONTEXT = 0 HAVE_GL_MODERN = 0 @@ -455,8 +451,12 @@ endif ifeq ($(HAVE_BSV_MOVIE), 1) DEFINES += -DHAVE_BSV_MOVIE - OBJ += input/bsv/bsvmovie.o \ - input/bsv/uint32s_index.o + OBJ += input/bsv/bsvmovie.o +endif + +ifeq ($(HAVE_STATESTREAM), 1) + DEFINES += -DHAVE_STATESTREAM + OBJ += input/bsv/uint32s_index.o endif ifeq ($(HAVE_RUNAHEAD), 1) diff --git a/command.h b/command.h index 6f4564acb4b6..57f4a0cdc61f 100644 --- a/command.h +++ b/command.h @@ -69,6 +69,9 @@ enum event_command CMD_EVENT_PLAY_REPLAY, CMD_EVENT_RECORD_REPLAY, CMD_EVENT_HALT_REPLAY, + CMD_EVENT_SAVE_REPLAY_CHECKPOINT, + CMD_EVENT_PREV_REPLAY_CHECKPOINT, + CMD_EVENT_NEXT_REPLAY_CHECKPOINT, CMD_EVENT_REPLAY_DECREMENT, CMD_EVENT_REPLAY_INCREMENT, /* Save state actions. */ @@ -480,6 +483,9 @@ static const struct cmd_map map[] = { { "PLAY_REPLAY", RARCH_PLAY_REPLAY_KEY }, { "RECORD_REPLAY", RARCH_RECORD_REPLAY_KEY }, { "HALT_REPLAY", RARCH_HALT_REPLAY_KEY }, + { "SAVE_REPLAY_CHECKPOINT", RARCH_SAVE_REPLAY_CHECKPOINT_KEY }, + { "PREV_REPLAY_CHECKPOINT", RARCH_PREV_REPLAY_CHECKPOINT_KEY }, + { "NEXT_REPLAY_CHECKPOINT", RARCH_NEXT_REPLAY_CHECKPOINT_KEY }, { "REPLAY_SLOT_PLUS", RARCH_REPLAY_SLOT_PLUS }, { "REPLAY_SLOT_MINUS", RARCH_REPLAY_SLOT_MINUS }, diff --git a/config.def.keybinds.h b/config.def.keybinds.h index e6190e0db893..e16abc03d658 100644 --- a/config.def.keybinds.h +++ b/config.def.keybinds.h @@ -430,6 +430,27 @@ static const struct retro_keybind retro_keybinds_1[] = { RARCH_HALT_REPLAY_KEY, NO_BTN, NO_BTN, 0, true }, + { + NULL, NULL, + AXIS_NONE, AXIS_NONE, + MENU_ENUM_LABEL_VALUE_INPUT_META_SAVE_REPLAY_CHECKPOINT_KEY, RETROK_UNKNOWN, + RARCH_SAVE_REPLAY_CHECKPOINT_KEY, NO_BTN, NO_BTN, 0, + true + }, + { + NULL, NULL, + AXIS_NONE, AXIS_NONE, + MENU_ENUM_LABEL_VALUE_INPUT_META_PREV_REPLAY_CHECKPOINT_KEY, RETROK_UNKNOWN, + RARCH_PREV_REPLAY_CHECKPOINT_KEY, NO_BTN, NO_BTN, 0, + true + }, + { + NULL, NULL, + AXIS_NONE, AXIS_NONE, + MENU_ENUM_LABEL_VALUE_INPUT_META_NEXT_REPLAY_CHECKPOINT_KEY, RETROK_UNKNOWN, + RARCH_NEXT_REPLAY_CHECKPOINT_KEY, NO_BTN, NO_BTN, 0, + true + }, { NULL, NULL, AXIS_NONE, AXIS_NONE, diff --git a/configuration.c b/configuration.c index abe9d63d45b8..e804ecd2b731 100644 --- a/configuration.c +++ b/configuration.c @@ -366,6 +366,9 @@ const struct input_bind_map input_config_bind_map[RARCH_BIND_LIST_END_NULL] = { DECLARE_META_BIND(1, play_replay, RARCH_PLAY_REPLAY_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_PLAY_REPLAY_KEY), DECLARE_META_BIND(1, record_replay, RARCH_RECORD_REPLAY_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_RECORD_REPLAY_KEY), DECLARE_META_BIND(1, halt_replay, RARCH_HALT_REPLAY_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_HALT_REPLAY_KEY), + DECLARE_META_BIND(1, save_replay_checkpoint,RARCH_SAVE_REPLAY_CHECKPOINT_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_SAVE_REPLAY_CHECKPOINT_KEY), + DECLARE_META_BIND(1, save_replay_checkpoint,RARCH_PREV_REPLAY_CHECKPOINT_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_PREV_REPLAY_CHECKPOINT_KEY), + DECLARE_META_BIND(1, save_replay_checkpoint,RARCH_NEXT_REPLAY_CHECKPOINT_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_NEXT_REPLAY_CHECKPOINT_KEY), DECLARE_META_BIND(2, replay_slot_increase, RARCH_REPLAY_SLOT_PLUS, MENU_ENUM_LABEL_VALUE_INPUT_META_REPLAY_SLOT_PLUS), DECLARE_META_BIND(2, replay_slot_decrease, RARCH_REPLAY_SLOT_MINUS, MENU_ENUM_LABEL_VALUE_INPUT_META_REPLAY_SLOT_MINUS), diff --git a/input/bsv/bsvmovie.c b/input/bsv/bsvmovie.c index 6add8d69ff84..f4d4d281881f 100644 --- a/input/bsv/bsvmovie.c +++ b/input/bsv/bsvmovie.c @@ -1446,3 +1446,16 @@ bool bsv_movie_read_deduped_state(bsv_movie_t *movie, return ret; } #endif + +bool movie_commit_checkpoint(input_driver_state_t *input_st) +{ + return false; +} +bool movie_skip_to_next_checkpoint(input_driver_state_t *input_st) +{ + return false; +} +bool movie_skip_to_prev_checkpoint(input_driver_state_t *input_st) +{ + return false; +} diff --git a/input/input_defines.h b/input/input_defines.h index ea18b5e11a9e..ff9ec45c0f88 100644 --- a/input/input_defines.h +++ b/input/input_defines.h @@ -147,6 +147,9 @@ enum RARCH_PLAY_REPLAY_KEY, RARCH_RECORD_REPLAY_KEY, RARCH_HALT_REPLAY_KEY, + RARCH_SAVE_REPLAY_CHECKPOINT_KEY, + RARCH_PREV_REPLAY_CHECKPOINT_KEY, + RARCH_NEXT_REPLAY_CHECKPOINT_KEY, RARCH_REPLAY_SLOT_PLUS, RARCH_REPLAY_SLOT_MINUS, diff --git a/input/input_driver.h b/input/input_driver.h index 271f9af14141..b58cc97d5802 100644 --- a/input/input_driver.h +++ b/input/input_driver.h @@ -1095,6 +1095,9 @@ void bsv_movie_deinit(input_driver_state_t *input_st); void bsv_movie_deinit_full(input_driver_state_t *input_st); void bsv_movie_enqueue(input_driver_state_t *input_st, bsv_movie_t *state, enum bsv_flags flags); +bool movie_commit_checkpoint(input_driver_state_t *input_st); +bool movie_skip_to_prev_checkpoint(input_driver_state_t *input_st); +bool movie_skip_to_next_checkpoint(input_driver_state_t *input_st); bool movie_start_playback(input_driver_state_t *input_st, char *path); bool movie_start_record(input_driver_state_t *input_st, char *path); bool movie_stop_playback(input_driver_state_t *input_st); diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 8036cfa06236..1423c7c48cad 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -4085,6 +4085,30 @@ MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_META_HALT_REPLAY_KEY, "Stops recording/playback of current replay." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_META_SAVE_REPLAY_CHECKPOINT_KEY, + "Save Replay Checkpoint" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_INPUT_META_SAVE_REPLAY_CHECKPOINT_KEY, + "Commits a checkpoint to the currently playing replay." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_META_PREV_REPLAY_CHECKPOINT_KEY, + "Prev Replay Checkpoint" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_INPUT_META_PREV_REPLAY_CHECKPOINT_KEY, + "Rewinds the replay to the previous automatically or manually saved checkpoint." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_META_NEXT_REPLAY_CHECKPOINT_KEY, + "Next Replay Checkpoint" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_INPUT_META_NEXT_REPLAY_CHECKPOINT_KEY, + "Fast-forwards the replay to the next automatically or manually saved checkpoint." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_REPLAY_SLOT_PLUS, "Next Replay Slot" diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index b768665dbae1..376bfb6b8ea6 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -447,6 +447,9 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_state_slot_minus, ME DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_play_replay_key, MENU_ENUM_SUBLABEL_INPUT_META_PLAY_REPLAY_KEY) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_record_replay_key, MENU_ENUM_SUBLABEL_INPUT_META_RECORD_REPLAY_KEY) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_halt_replay_key, MENU_ENUM_SUBLABEL_INPUT_META_HALT_REPLAY_KEY) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_save_replay_checkpoint_key, MENU_ENUM_SUBLABEL_INPUT_META_SAVE_REPLAY_CHECKPOINT_KEY) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_prev_replay_checkpoint_key, MENU_ENUM_SUBLABEL_INPUT_META_PREV_REPLAY_CHECKPOINT_KEY) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_next_replay_checkpoint_key, MENU_ENUM_SUBLABEL_INPUT_META_NEXT_REPLAY_CHECKPOINT_KEY) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_replay_slot_plus, MENU_ENUM_SUBLABEL_INPUT_META_REPLAY_SLOT_PLUS) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_replay_slot_minus, MENU_ENUM_SUBLABEL_INPUT_META_REPLAY_SLOT_MINUS) @@ -2383,6 +2386,15 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case RARCH_HALT_REPLAY_KEY: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_meta_halt_replay_key); return 0; + case RARCH_SAVE_REPLAY_CHECKPOINT_KEY: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_meta_save_replay_checkpoint_key); + return 0; + case RARCH_PREV_REPLAY_CHECKPOINT_KEY: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_meta_prev_replay_checkpoint_key); + return 0; + case RARCH_NEXT_REPLAY_CHECKPOINT_KEY: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_meta_next_replay_checkpoint_key); + return 0; case RARCH_REPLAY_SLOT_PLUS: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_meta_replay_slot_plus); return 0; diff --git a/msg_hash.h b/msg_hash.h index 8d7c8e04671b..2f50eccab392 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -1138,6 +1138,9 @@ enum msg_hash_enums MENU_ENUM_LABEL_VALUE_INPUT_META_PLAY_REPLAY_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_RECORD_REPLAY_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_HALT_REPLAY_KEY, + MENU_ENUM_LABEL_VALUE_INPUT_META_SAVE_REPLAY_CHECKPOINT_KEY, + MENU_ENUM_LABEL_VALUE_INPUT_META_PREV_REPLAY_CHECKPOINT_KEY, + MENU_ENUM_LABEL_VALUE_INPUT_META_NEXT_REPLAY_CHECKPOINT_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_REPLAY_SLOT_PLUS, MENU_ENUM_LABEL_VALUE_INPUT_META_REPLAY_SLOT_MINUS, @@ -1238,6 +1241,9 @@ enum msg_hash_enums MENU_ENUM_SUBLABEL_INPUT_META_PLAY_REPLAY_KEY, MENU_ENUM_SUBLABEL_INPUT_META_RECORD_REPLAY_KEY, MENU_ENUM_SUBLABEL_INPUT_META_HALT_REPLAY_KEY, + MENU_ENUM_SUBLABEL_INPUT_META_SAVE_REPLAY_CHECKPOINT_KEY, + MENU_ENUM_SUBLABEL_INPUT_META_PREV_REPLAY_CHECKPOINT_KEY, + MENU_ENUM_SUBLABEL_INPUT_META_NEXT_REPLAY_CHECKPOINT_KEY, MENU_ENUM_SUBLABEL_INPUT_META_REPLAY_SLOT_PLUS, MENU_ENUM_SUBLABEL_INPUT_META_REPLAY_SLOT_MINUS, diff --git a/retroarch.c b/retroarch.c index cdad61558f89..53636f0044b5 100644 --- a/retroarch.c +++ b/retroarch.c @@ -3553,6 +3553,21 @@ bool command_event(enum event_command cmd, void *data) configuration_set_int(settings, settings->ints.state_slot, new_state_slot); } break; + case CMD_EVENT_SAVE_REPLAY_CHECKPOINT: +#ifdef HAVE_BSV_MOVIE + movie_commit_checkpoint(input_state_get_ptr()); +#endif + break; + case CMD_EVENT_PREV_REPLAY_CHECKPOINT: +#ifdef HAVE_BSV_MOVIE + movie_skip_to_prev_checkpoint(input_state_get_ptr()); +#endif + break; + case CMD_EVENT_NEXT_REPLAY_CHECKPOINT: +#ifdef HAVE_BSV_MOVIE + movie_skip_to_next_checkpoint(input_state_get_ptr()); +#endif + break; case CMD_EVENT_REPLAY_DECREMENT: #ifdef HAVE_BSV_MOVIE { diff --git a/runloop.c b/runloop.c index 8861a06a5548..e18c6ff63aac 100644 --- a/runloop.c +++ b/runloop.c @@ -7009,6 +7009,9 @@ static enum runloop_state_enum runloop_check_state( HOTKEY_CHECK(RARCH_PLAY_REPLAY_KEY, CMD_EVENT_PLAY_REPLAY, true, NULL); HOTKEY_CHECK(RARCH_RECORD_REPLAY_KEY, CMD_EVENT_RECORD_REPLAY, true, NULL); HOTKEY_CHECK(RARCH_HALT_REPLAY_KEY, CMD_EVENT_HALT_REPLAY, true, NULL); + HOTKEY_CHECK(RARCH_SAVE_REPLAY_CHECKPOINT_KEY, CMD_EVENT_SAVE_REPLAY_CHECKPOINT, true, NULL); + HOTKEY_CHECK(RARCH_PREV_REPLAY_CHECKPOINT_KEY, CMD_EVENT_PREV_REPLAY_CHECKPOINT, true, NULL); + HOTKEY_CHECK(RARCH_NEXT_REPLAY_CHECKPOINT_KEY, CMD_EVENT_NEXT_REPLAY_CHECKPOINT, true, NULL); /* Check Disc Control hotkeys */ HOTKEY_CHECK3( From 4811b33fd64d3f0185c62003946443661036f294 Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Fri, 29 Aug 2025 15:49:41 -0700 Subject: [PATCH 02/12] Fix copy paste error in meta binds --- configuration.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration.c b/configuration.c index e804ecd2b731..b32156cd4536 100644 --- a/configuration.c +++ b/configuration.c @@ -367,8 +367,8 @@ const struct input_bind_map input_config_bind_map[RARCH_BIND_LIST_END_NULL] = { DECLARE_META_BIND(1, record_replay, RARCH_RECORD_REPLAY_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_RECORD_REPLAY_KEY), DECLARE_META_BIND(1, halt_replay, RARCH_HALT_REPLAY_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_HALT_REPLAY_KEY), DECLARE_META_BIND(1, save_replay_checkpoint,RARCH_SAVE_REPLAY_CHECKPOINT_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_SAVE_REPLAY_CHECKPOINT_KEY), - DECLARE_META_BIND(1, save_replay_checkpoint,RARCH_PREV_REPLAY_CHECKPOINT_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_PREV_REPLAY_CHECKPOINT_KEY), - DECLARE_META_BIND(1, save_replay_checkpoint,RARCH_NEXT_REPLAY_CHECKPOINT_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_NEXT_REPLAY_CHECKPOINT_KEY), + DECLARE_META_BIND(1, prev_replay_checkpoint,RARCH_PREV_REPLAY_CHECKPOINT_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_PREV_REPLAY_CHECKPOINT_KEY), + DECLARE_META_BIND(1, next_replay_checkpoint,RARCH_NEXT_REPLAY_CHECKPOINT_KEY, MENU_ENUM_LABEL_VALUE_INPUT_META_NEXT_REPLAY_CHECKPOINT_KEY), DECLARE_META_BIND(2, replay_slot_increase, RARCH_REPLAY_SLOT_PLUS, MENU_ENUM_LABEL_VALUE_INPUT_META_REPLAY_SLOT_PLUS), DECLARE_META_BIND(2, replay_slot_decrease, RARCH_REPLAY_SLOT_MINUS, MENU_ENUM_LABEL_VALUE_INPUT_META_REPLAY_SLOT_MINUS), From 56c9501f1f22db7ea5d482c0547e3104335f228a Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Fri, 29 Aug 2025 15:49:55 -0700 Subject: [PATCH 03/12] Implement save_replay_checkpoint command --- input/bsv/bsvmovie.c | 13 +++++++++---- input/input_driver.h | 3 ++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/input/bsv/bsvmovie.c b/input/bsv/bsvmovie.c index f4d4d281881f..70598851ba2e 100644 --- a/input/bsv/bsvmovie.c +++ b/input/bsv/bsvmovie.c @@ -713,9 +713,10 @@ void bsv_movie_next_frame(input_driver_state_t *input_st) handle->input_event_count = 0; /* Maybe record checkpoint */ - if ( (checkpoint_interval != 0) - && (handle->frame_counter > 0) - && (handle->frame_counter % (checkpoint_interval*60) == 0)) + if ((input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_FORCE_CHECKPOINT) || + ((checkpoint_interval != 0) + && (handle->frame_counter > 0) + && (handle->frame_counter % (checkpoint_interval*60) == 0))) { uint8_t frame_tok = REPLAY_TOKEN_CHECKPOINT2_FRAME; uint8_t compression = handle->checkpoint_compression; @@ -724,6 +725,7 @@ void bsv_movie_next_frame(input_driver_state_t *input_st) #else uint8_t encoding = REPLAY_CHECKPOINT2_ENCODING_RAW; #endif + input_st->bsv_movie_state.flags &= ~BSV_FLAG_MOVIE_FORCE_CHECKPOINT; /* "next frame is a checkpoint" */ intfstream_write(handle->file, (uint8_t *)(&frame_tok), sizeof(uint8_t)); /* compression and encoding schemes */ @@ -1449,7 +1451,10 @@ bool bsv_movie_read_deduped_state(bsv_movie_t *movie, bool movie_commit_checkpoint(input_driver_state_t *input_st) { - return false; + if (!input_st->bsv_movie_state_handle) + return false; + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_FORCE_CHECKPOINT; + return true; } bool movie_skip_to_next_checkpoint(input_driver_state_t *input_st) { diff --git a/input/input_driver.h b/input/input_driver.h index b58cc97d5802..f70fd8e20176 100644 --- a/input/input_driver.h +++ b/input/input_driver.h @@ -202,7 +202,8 @@ enum bsv_flags BSV_FLAG_MOVIE_PLAYBACK = (1 << 2), BSV_FLAG_MOVIE_RECORDING = (1 << 3), BSV_FLAG_MOVIE_END = (1 << 4), - BSV_FLAG_MOVIE_EOF_EXIT = (1 << 5) + BSV_FLAG_MOVIE_EOF_EXIT = (1 << 5), + BSV_FLAG_MOVIE_FORCE_CHECKPOINT = (1 << 6) }; struct bsv_state From 9c6c68881915e8874b3f63bee75b8bd6d12330d7 Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Fri, 5 Sep 2025 15:36:16 -0700 Subject: [PATCH 04/12] Implement prev/next checkpoint commands --- input/bsv/bsvmovie.c | 359 ++++++++++++++++++++++++++++++-------- input/bsv/bsvmovie.h | 2 +- input/bsv/uint32s_index.c | 8 + input/input_driver.h | 19 +- runloop.c | 11 ++ tasks/task_movie.c | 79 ++------- 6 files changed, 331 insertions(+), 147 deletions(-) diff --git a/input/bsv/bsvmovie.c b/input/bsv/bsvmovie.c index 70598851ba2e..3e1505b1cfe3 100644 --- a/input/bsv/bsvmovie.c +++ b/input/bsv/bsvmovie.c @@ -53,10 +53,83 @@ void bsv_movie_free(bsv_movie_t*); #ifdef HAVE_STATESTREAM int64_t bsv_movie_write_deduped_state(bsv_movie_t *movie, uint8_t *state, size_t state_size, uint8_t *output, size_t output_capacity); -bool bsv_movie_read_deduped_state(bsv_movie_t *movie, - uint8_t *encoded, size_t encoded_size, bool output); +bool bsv_movie_read_deduped_state(bsv_movie_t *movie, uint8_t *encoded, size_t encoded_size); #endif +bool bsv_movie_skip_to_next_checkpoint_impl(bsv_movie_t *movie); +bool bsv_movie_skip_to_prev_checkpoint_impl(bsv_movie_t *movie); + +bool bsv_movie_reset_playback(bsv_movie_t *handle) +{ + uint32_t state_size = 0; + uint32_t header[REPLAY_HEADER_LEN] = {0}; + uint32_t vsn; + if (!handle) + return false; + vsn = handle->version; + intfstream_rewind(handle->file); + if (intfstream_read(handle->file, header, REPLAY_HEADER_LEN_BYTES) < REPLAY_HEADER_LEN_BYTES) + return false; + handle->frame_counter = 0; + handle->cur_save_valid = false; + + state_size = swap_if_big32(header[REPLAY_HEADER_STATE_SIZE_INDEX]); + if (state_size && vsn <= 1) + { + size_t info_size; + retro_ctx_serialize_info_t serial_info; + uint8_t *buf = (uint8_t*)malloc(state_size); + + if (!buf) + return false; + + /* The header used to be six ints long */ + intfstream_seek(handle->file, REPLAY_HEADER_V0V1_LEN_BYTES, SEEK_SET); + + if (intfstream_read(handle->file, buf, state_size) != state_size) + { + RARCH_ERR("[Replay] %s\n", msg_hash_to_str(MSG_COULD_NOT_READ_STATE_FROM_MOVIE)); + return false; + } + info_size = core_serialize_size(); + /* For cores like dosbox, the reported size is not always + correct. So we just give a warning if they don't match up. */ + serial_info.data_const = buf; + serial_info.size = state_size; + core_unserialize(&serial_info); + free(buf); + if (info_size != state_size) + RARCH_WARN("[Replay] %s\n", + msg_hash_to_str(MSG_MOVIE_FORMAT_DIFFERENT_SERIALIZER_VERSION)); + } + else if (vsn >= 2) + { + uint8_t compression, encoding; +#ifdef HAVE_STATESTREAM + uint32_t commit_settings = header[REPLAY_HEADER_CHECKPOINT_CONFIG_INDEX]; + uint32_t superblock_size = swap_if_big32(header[REPLAY_HEADER_SUPERBLOCK_SIZE_INDEX]); + uint32_t block_size = swap_if_big32(header[REPLAY_HEADER_BLOCK_SIZE_INDEX]); + handle->commit_interval = commit_settings >> 24; + handle->commit_threshold = (commit_settings >> 16) & 0x000000FF; + handle->checkpoint_compression = (commit_settings >> 8) & 0x000000FF; + if (handle->superblocks) + uint32s_index_free(handle->superblocks); + handle->superblocks = uint32s_index_new(superblock_size,handle->commit_interval,handle->commit_threshold); + if (handle->blocks) + uint32s_index_free(handle->blocks); + handle->blocks = uint32s_index_new(block_size/4,handle->commit_interval,handle->commit_threshold); +#endif + if (intfstream_read(handle->file, &(compression), sizeof(uint8_t)) != sizeof(uint8_t) || + intfstream_read(handle->file, &(encoding), sizeof(uint8_t)) != sizeof(uint8_t)) + return false; + if (!bsv_movie_load_checkpoint(handle, compression, encoding, REPLAY_CPBEHAVIOR_DESERIALIZE)) + return false; + } + if(vsn == 0) + return true; + return bsv_movie_read_next_events(handle, REPLAY_CPBEHAVIOR_DESERIALIZE, true); +} + bool bsv_movie_reset_recording(bsv_movie_t *handle) { size_t state_size, state_size_; @@ -65,11 +138,14 @@ bool bsv_movie_reset_recording(bsv_movie_t *handle) uint8_t encoding = REPLAY_CHECKPOINT2_ENCODING_STATESTREAM; /* If recording, we simply reset * the starting point. Nice and easy. */ - uint32s_index_clear(handle->superblocks); - uint32s_index_clear(handle->blocks); + if (handle->superblocks) + uint32s_index_clear(handle->superblocks); + if (handle->blocks) + uint32s_index_clear(handle->blocks); #else uint8_t encoding = REPLAY_CHECKPOINT2_ENCODING_RAW; #endif + handle->cur_save_valid = false; intfstream_seek(handle->file, REPLAY_HEADER_LEN_BYTES, SEEK_SET); intfstream_truncate(handle->file, REPLAY_HEADER_LEN_BYTES); @@ -132,13 +208,15 @@ void bsv_movie_frame_rewind() intfstream_seek(handle->file, (int)handle->min_file_pos, SEEK_SET); /* clear incremental checkpoint table data. We do this both on recording and playback for simplicity. */ #ifdef HAVE_STATESTREAM - uint32s_index_remove_after(handle->superblocks, 0); - uint32s_index_remove_after(handle->blocks, 0); + if (handle->superblocks) + uint32s_index_remove_after(handle->superblocks, 0); + if (handle->blocks) + uint32s_index_remove_after(handle->blocks, 0); #endif if (recording) intfstream_truncate(handle->file, (int)handle->min_file_pos); else - bsv_movie_read_next_events(handle, false); + bsv_movie_read_next_events(handle, REPLAY_CPBEHAVIOR_DESERIALIZE, true); } else { @@ -154,15 +232,17 @@ void bsv_movie_frame_rewind() else handle->frame_counter = 0; #ifdef HAVE_STATESTREAM - uint32s_index_remove_after(handle->superblocks, handle->frame_counter); - uint32s_index_remove_after(handle->blocks, handle->frame_counter); + if (handle->superblocks) + uint32s_index_remove_after(handle->superblocks, handle->frame_counter); + if (handle->blocks) + uint32s_index_remove_after(handle->blocks, handle->frame_counter); #endif RARCH_LOG("[REPLAY] rewound to %d\n", handle->frame_counter); intfstream_seek(handle->file, (int)handle->frame_pos[handle->frame_counter & handle->frame_mask], SEEK_SET); if (recording) intfstream_truncate(handle->file, (int)handle->frame_pos[handle->frame_counter & handle->frame_mask]); else - bsv_movie_read_next_events(handle, false); + bsv_movie_read_next_events(handle, REPLAY_CPBEHAVIOR_DESERIALIZE, true); } if (intfstream_tell(handle->file) <= (long)handle->min_file_pos) @@ -173,10 +253,12 @@ void bsv_movie_frame_rewind() { intfstream_seek(handle->file, (int)handle->min_file_pos, SEEK_SET); #ifdef HAVE_STATESTREAM + if (handle->superblocks) uint32s_index_remove_after(handle->superblocks, 0); + if (handle->blocks) uint32s_index_remove_after(handle->blocks, 0); #endif - bsv_movie_read_next_events(handle, false); + bsv_movie_read_next_events(handle, REPLAY_CPBEHAVIOR_DESERIALIZE, true); } else { @@ -248,14 +330,13 @@ void bsv_movie_finish_rewind(input_driver_state_t *input_st) handle->did_rewind = false; } -bool bsv_movie_load_checkpoint(bsv_movie_t *handle, uint8_t compression, uint8_t encoding, bool just_update_structures) +bool bsv_movie_load_checkpoint(bsv_movie_t *handle, uint8_t compression, uint8_t encoding, replay_checkpoint_behavior checkpoint_behavior) { input_driver_state_t *input_st = input_state_get_ptr(); uint32_t compressed_encoded_size, encoded_size, size; uint8_t *compressed_data = NULL, *encoded_data = NULL; retro_ctx_serialize_info_t serial_info; bool ret = true; - if (intfstream_read(handle->file, &(size), sizeof(uint32_t)) != sizeof(uint32_t)) { @@ -280,7 +361,9 @@ bool bsv_movie_load_checkpoint(bsv_movie_t *handle, uint8_t compression, uint8_t size = swap_if_big32(size); encoded_size = swap_if_big32(encoded_size); compressed_encoded_size = swap_if_big32(compressed_encoded_size); - if (just_update_structures && encoding == REPLAY_CHECKPOINT2_ENCODING_RAW) + if (checkpoint_behavior == REPLAY_CPBEHAVIOR_SKIP || + ((checkpoint_behavior == REPLAY_CPBEHAVIOR_UPDATE) && + encoding == REPLAY_CHECKPOINT2_ENCODING_RAW)) { intfstream_seek(handle->file, compressed_encoded_size, SEEK_CUR); goto exit; @@ -370,7 +453,7 @@ bool bsv_movie_load_checkpoint(bsv_movie_t *handle, uint8_t compression, uint8_t break; #ifdef HAVE_STATESTREAM case REPLAY_CHECKPOINT2_ENCODING_STATESTREAM: - if(!bsv_movie_read_deduped_state(handle, encoded_data, encoded_size, !just_update_structures)) + if(!bsv_movie_read_deduped_state(handle, encoded_data, encoded_size)) { RARCH_ERR("[STATESTREAM] Couldn't load incremental checkpoint"); ret = false; @@ -383,14 +466,13 @@ bool bsv_movie_load_checkpoint(bsv_movie_t *handle, uint8_t compression, uint8_t ret = false; goto exit; } - if (just_update_structures) + if (checkpoint_behavior != REPLAY_CPBEHAVIOR_DESERIALIZE) goto exit; serial_info.data_const = handle->cur_save; serial_info.size = size; /* TODO: should this happen at the end of the current frame, or at the beginning before inputs have been polled/etc? FCEUMM and PPSSPP have some jankiness here */ if (!core_unserialize(&serial_info)) { - abort(); ret = false; goto exit; } @@ -532,7 +614,7 @@ int64_t bsv_movie_write_checkpoint(bsv_movie_t *handle, uint8_t compression, uin return ret; } -bool bsv_movie_read_next_events(bsv_movie_t *handle, bool skip_checkpoints) +bool bsv_movie_read_next_events(bsv_movie_t *handle, replay_checkpoint_behavior checkpoint_behavior, bool end_movie) { input_driver_state_t *input_st = input_state_get_ptr(); /* Skip over backref */ @@ -549,7 +631,8 @@ bool bsv_movie_read_next_events(bsv_movie_t *handle, bool skip_checkpoints) { /* 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; + if (end_movie) + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; return false; } } @@ -558,7 +641,8 @@ bool bsv_movie_read_next_events(bsv_movie_t *handle, bool skip_checkpoints) { RARCH_LOG("[Replay] EOF after buttons\n"); /* Natural(?) EOF */ - input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; + if (end_movie) + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; return false; } if (handle->version > 0) @@ -574,7 +658,8 @@ bool bsv_movie_read_next_events(bsv_movie_t *handle, bool skip_checkpoints) { /* Unnatural EOF */ RARCH_ERR("[Replay] Input replay ran out of inputs too early\n"); - input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; + if (end_movie) + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; return false; } } @@ -583,7 +668,8 @@ bool bsv_movie_read_next_events(bsv_movie_t *handle, bool skip_checkpoints) { RARCH_LOG("[Replay] EOF after inputs\n"); /* Natural(?) EOF */ - input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; + if (end_movie) + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; return false; } } @@ -595,7 +681,8 @@ bool bsv_movie_read_next_events(bsv_movie_t *handle, bool skip_checkpoints) { /* Unnatural EOF */ RARCH_ERR("[Replay] Replay ran out of frames\n"); - input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; + if (end_movie) + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; return false; } else if (next_frame_type == REPLAY_TOKEN_CHECKPOINT_FRAME) @@ -606,11 +693,12 @@ bool bsv_movie_read_next_events(bsv_movie_t *handle, bool skip_checkpoints) 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; + if (end_movie) + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; return false; } size = swap_if_big64(size); - if(skip_checkpoints) + if(checkpoint_behavior != REPLAY_CPBEHAVIOR_DESERIALIZE) intfstream_seek(handle->file, size, SEEK_CUR); else { @@ -618,7 +706,8 @@ bool bsv_movie_read_next_events(bsv_movie_t *handle, bool skip_checkpoints) if (intfstream_read(handle->file, state, size) != (int64_t)size) { RARCH_ERR("[Replay] Replay checkpoint truncated\n"); - input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; + if (end_movie) + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; free(state); return false; } @@ -627,7 +716,10 @@ bool bsv_movie_read_next_events(bsv_movie_t *handle, bool skip_checkpoints) if (!core_unserialize(&serial_info)) { RARCH_ERR("[Replay] Failed to load movie checkpoint, failing\n"); - input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; + if (end_movie) + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; + free(state); + return false; } free(state); } @@ -640,25 +732,26 @@ bool bsv_movie_read_next_events(bsv_movie_t *handle, bool skip_checkpoints) { /* Unexpected EOF */ RARCH_ERR("[Replay] Replay checkpoint truncated.\n"); - input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; + if (end_movie) + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_END; return false; } - if (!bsv_movie_load_checkpoint(handle, compression, encoding, skip_checkpoints)) + if (!bsv_movie_load_checkpoint(handle, compression, encoding, checkpoint_behavior)) RARCH_WARN("[Replay] Failed to load movie checkpoint\n"); } } return true; } -void bsv_movie_scan_from_start(input_driver_state_t *input_st, int32_t len) +void bsv_movie_scan_from_start(bsv_movie_t *movie, int32_t len) { - bsv_movie_t *movie = input_st->bsv_movie_state_handle; if (movie->version == 0) return; /* Old movies don't store enough information to fixup the frame counters. */ intfstream_seek(movie->file, movie->min_file_pos, SEEK_SET); movie->frame_counter = 0; movie->frame_pos[0] = intfstream_tell(movie->file); - while(intfstream_tell(movie->file) < len && bsv_movie_read_next_events(movie, true)) + movie->cur_save_valid = false; + while(intfstream_tell(movie->file) < len && bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_UPDATE, false)) { movie->frame_counter += 1; movie->frame_pos[movie->frame_counter & movie->frame_mask] = intfstream_tell(movie->file); @@ -743,11 +836,23 @@ void bsv_movie_next_frame(input_driver_state_t *input_st) /* write "next frame is not a checkpoint" */ intfstream_write(handle->file, (uint8_t *)(&frame_tok), sizeof(uint8_t)); } + intfstream_truncate(handle->file, intfstream_tell(handle->file)); } if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_PLAYBACK) - bsv_movie_read_next_events(handle, !checkpoint_deserialize); + bsv_movie_read_next_events(handle, checkpoint_deserialize ? REPLAY_CPBEHAVIOR_DESERIALIZE : REPLAY_CPBEHAVIOR_UPDATE, true); handle->frame_pos[handle->frame_counter & handle->frame_mask] = intfstream_tell(handle->file); + + if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_PREV_CHECKPOINT) + { + bsv_movie_skip_to_prev_checkpoint_impl(handle); + input_st->bsv_movie_state.flags &= ~BSV_FLAG_MOVIE_PREV_CHECKPOINT; + } + else if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_NEXT_CHECKPOINT) + { + bsv_movie_skip_to_next_checkpoint_impl(handle); + input_st->bsv_movie_state.flags &= ~BSV_FLAG_MOVIE_NEXT_CHECKPOINT; + } } size_t replay_get_serialize_size(void) @@ -1030,8 +1135,10 @@ bool replay_set_serialized_data(void* buf) RARCH_WARN("[Replay] %s.\n", _msg); } #ifdef HAVE_STATESTREAM - uint32s_index_remove_after(handle->superblocks, 0); - uint32s_index_remove_after(handle->blocks, 0); + if (handle->superblocks) + uint32s_index_remove_after(handle->superblocks, 0); + if (handle->blocks) + uint32s_index_remove_after(handle->blocks, 0); #endif intfstream_rewind(handle->file); intfstream_write(handle->file, header, loaded_len); @@ -1040,20 +1147,22 @@ bool replay_set_serialized_data(void* buf) /* TODO: in the future, if same_timeline, don't clear indices above and only scan forward from handle_idx. */ /* TODO use backrefs to help here */ - bsv_movie_scan_from_start(input_st, loaded_len); + bsv_movie_scan_from_start(handle, loaded_len); } else { #ifdef HAVE_STATESTREAM - uint32s_index_remove_after(handle->superblocks, 0); - uint32s_index_remove_after(handle->blocks, 0); + if (handle->superblocks) + uint32s_index_remove_after(handle->superblocks, 0); + if (handle->blocks) + uint32s_index_remove_after(handle->blocks, 0); #endif intfstream_seek(handle->file, loaded_len, SEEK_SET); /* TODO: in the future, don't clear indices above and only update frame counter and remove index entries after the loaded movie's frame counter */ /* TODO use backrefs to help here */ - bsv_movie_scan_from_start(input_st, loaded_len); + bsv_movie_scan_from_start(handle, loaded_len); if (recording) intfstream_truncate(handle->file, loaded_len); } @@ -1258,8 +1367,7 @@ int64_t bsv_movie_write_deduped_state(bsv_movie_t *movie, uint8_t *state, size_t return encoded_size; } -bool bsv_movie_read_deduped_state(bsv_movie_t *movie, - uint8_t *encoded, size_t encoded_size, bool output) +bool bsv_movie_read_deduped_state(bsv_movie_t *movie, uint8_t *encoded, size_t encoded_size) { static retro_perf_tick_t total_decode_micros = 0; static retro_perf_tick_t total_decode_count = 0; @@ -1385,44 +1493,40 @@ bool bsv_movie_read_deduped_state(bsv_movie_t *movie, goto exit; } len = item.val.array.len; - if (output) + if (!movie->superblock_seq) + movie->superblock_seq = calloc(len,sizeof(uint32_t)); + for(i = 0; i < len; i++) { - if (!movie->superblock_seq) - movie->superblock_seq = calloc(len,sizeof(uint32_t)); - for(i = 0; i < len; i++) + struct rmsgpack_dom_value inner_item = item.val.array.items[i]; + /* assert(inner_item.type == RDT_INT); */ + uint32_t superblock_idx = inner_item.val.int_; + uint32_t *superblock; + size_t j; + /* if this superblock is the same as last time, no need to scan the blocks. */ + if (movie->cur_save_valid && movie->cur_save && superblock_idx == movie->superblock_seq[i]) { - struct rmsgpack_dom_value inner_item = item.val.array.items[i]; - /* assert(inner_item.type == RDT_INT); */ - uint32_t superblock_idx = inner_item.val.int_; - uint32_t *superblock; - size_t j; - /* if this superblock is the same as last time, no need to scan the blocks. */ - if (movie->cur_save_valid && movie->cur_save && superblock_idx == movie->superblock_seq[i]) - { - superblock = uint32s_index_get(movie->superblocks, movie->superblock_seq[i]); - uint32s_index_bump_count(movie->superblocks, movie->superblock_seq[i]); - /* We do need to increment all the involved block counts though */ - for (j = 0; j < movie->superblocks->object_size; j++) - uint32s_index_bump_count(movie->blocks, superblock[j]); - continue; - } - movie->superblock_seq[i] = superblock_idx; - superblock = uint32s_index_get(movie->superblocks, superblock_idx); - uint32s_index_bump_count(movie->superblocks, superblock_idx); - for(j = 0; j < movie->superblocks->object_size; j++) - { - uint32_t block_idx = superblock[j]; - size_t block_start = MIN(i*superblock_byte_size+j*block_byte_size, state_size); - size_t block_end = MIN(block_start+block_byte_size, state_size); - uint8_t *block; - /* This (==) can only happen in the last superblock, if it was padded with extra blocks. */ - if(block_end <= block_start) { break; } - block = (uint8_t *)uint32s_index_get(movie->blocks, block_idx); - uint32s_index_bump_count(movie->blocks, block_idx); - memcpy(movie->cur_save+block_start, (uint8_t*)block, block_end-block_start); - } + superblock = uint32s_index_get(movie->superblocks, movie->superblock_seq[i]); + uint32s_index_bump_count(movie->superblocks, movie->superblock_seq[i]); + /* We do need to increment all the involved block counts though */ + for (j = 0; j < movie->superblocks->object_size; j++) + uint32s_index_bump_count(movie->blocks, superblock[j]); + continue; + } + movie->superblock_seq[i] = superblock_idx; + superblock = uint32s_index_get(movie->superblocks, superblock_idx); + uint32s_index_bump_count(movie->superblocks, superblock_idx); + for(j = 0; j < movie->superblocks->object_size; j++) + { + uint32_t block_idx = superblock[j]; + size_t block_start = MIN(i*superblock_byte_size+j*block_byte_size, state_size); + size_t block_end = MIN(block_start+block_byte_size, state_size); + uint8_t *block; + /* This (==) can only happen in the last superblock, if it was padded with extra blocks. */ + if(block_end <= block_start) { break; } + block = (uint8_t *)uint32s_index_get(movie->blocks, block_idx); + uint32s_index_bump_count(movie->blocks, block_idx); + memcpy(movie->cur_save+block_start, (uint8_t*)block, block_end-block_start); } - } rmsgpack_dom_value_free(&item); ret = true; @@ -1456,11 +1560,112 @@ bool movie_commit_checkpoint(input_driver_state_t *input_st) input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_FORCE_CHECKPOINT; return true; } +uint8_t bsv_movie_peek_frame_token(bsv_movie_t *movie) +{ + uint8_t keycount, tok = REPLAY_TOKEN_INVALID; + uint16_t event_count; + int64_t pos; + if (!movie || movie->version == 0) + return REPLAY_TOKEN_INVALID; + pos = intfstream_tell(movie->file); + if (movie->version > 1 && + intfstream_seek(movie->file, sizeof(uint32_t), SEEK_CUR) < 0) + goto end; + if (intfstream_read(movie->file, &keycount, 1) != 1) + goto end; + if (intfstream_seek(movie->file, sizeof(bsv_key_data_t)*keycount, SEEK_CUR) < 0) + goto end; + if (intfstream_read(movie->file, &event_count, 2) != 2) + goto end; + event_count = swap_if_big16(event_count); + if (intfstream_seek(movie->file, sizeof(bsv_input_data_t)*event_count, SEEK_CUR) < 0) + goto end; + if (intfstream_read(movie->file, &tok, 1) != 1) + goto end; + end: + if (intfstream_seek(movie->file, pos, SEEK_SET) < 0) + return REPLAY_TOKEN_INVALID; + return tok; +} bool movie_skip_to_next_checkpoint(input_driver_state_t *input_st) { + /* For now, can't skip forward in a recording replay */ + if (!input_st->bsv_movie_state_handle || !input_st->bsv_movie_state_handle->playback) + return false; + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_NEXT_CHECKPOINT; + return true; +} +bool bsv_movie_skip_to_next_checkpoint_impl(bsv_movie_t *movie) +{ + uint8_t tok = REPLAY_TOKEN_INVALID; + int64_t start_pos, start_frame; + if (!movie || movie->version == 0) + return false; + /* scan forward until peek shows a checkpoint or checkpoint2 */ + /* if we get to the end, come back to here instead */ + start_pos = intfstream_tell(movie->file); + start_frame = movie->frame_counter; + do { + tok = REPLAY_TOKEN_INVALID; + if (!bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_UPDATE, false)) + break; + movie->frame_counter += 1; + tok = bsv_movie_peek_frame_token(movie); + } while (tok != REPLAY_TOKEN_CHECKPOINT_FRAME && + tok != REPLAY_TOKEN_CHECKPOINT2_FRAME && + tok != REPLAY_TOKEN_INVALID); + if (tok != REPLAY_TOKEN_INVALID) + return bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_DESERIALIZE, true); + /* There was no next savepoint, move the file cursor back to here */ + intfstream_seek(movie->file, start_pos, SEEK_SET); + movie->frame_counter = start_frame; + bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_UPDATE, true); return false; } bool movie_skip_to_prev_checkpoint(input_driver_state_t *input_st) { - return false; + if (!input_st->bsv_movie_state_handle) + return false; + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_PREV_CHECKPOINT; + return true; +} +bool bsv_movie_skip_to_prev_checkpoint_impl(bsv_movie_t *movie) +{ + /* skip to prev needs to go back at least 60 frames if rewinding when not paused */ + runloop_state_t *runloop_st = runloop_state_get_ptr(); + bool paused = !!(runloop_st->flags & RUNLOOP_FLAG_PAUSED); + const int64_t prev_skip_min_distance = 60; + int64_t cp_pos = -1; + int64_t target_frame = (int64_t)movie->frame_counter; + bool ret = false; + if (!movie || movie->version == 0) + return false; + /* Find the right checkpoint to jump to. + In the future, backrefs could be used to make this faster */ + movie->frame_counter = 0; + intfstream_seek(movie->file, movie->min_file_pos, SEEK_SET); + while ((int64_t)movie->frame_counter < target_frame) + { + uint8_t tok = bsv_movie_peek_frame_token(movie); + uint8_t keycount; + uint16_t evtcount; + if (tok == REPLAY_TOKEN_INVALID) + break; + if (tok == REPLAY_TOKEN_CHECKPOINT_FRAME || tok == REPLAY_TOKEN_CHECKPOINT2_FRAME) + { + if (target_frame - (int64_t)movie->frame_counter < prev_skip_min_distance && !paused) + break; + cp_pos = intfstream_tell(movie->file); + } + movie->frame_counter += 1; + bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_SKIP, false); + } + /* Scan from the start up to the target checkpoint pos. If + statestream blocks stored their file position we wouldn't need + to do this and could just use backrefs */ + bsv_movie_reset_playback(movie); + if (cp_pos >= 0) + bsv_movie_scan_from_start(movie, cp_pos); + ret = bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_DESERIALIZE, true); + return ret; } diff --git a/input/bsv/bsvmovie.h b/input/bsv/bsvmovie.h index c7dc086beaab..b1fe1c7985eb 100644 --- a/input/bsv/bsvmovie.h +++ b/input/bsv/bsvmovie.h @@ -38,7 +38,7 @@ void bsv_movie_push_input_event(bsv_movie_t *movie, bool bsv_movie_load_checkpoint(bsv_movie_t *movie, uint8_t compression, uint8_t encoding, - bool just_update_structures); + replay_checkpoint_behavior cpbehavior); int64_t bsv_movie_write_checkpoint(bsv_movie_t *movie, uint8_t compression, uint8_t encoding); diff --git a/input/bsv/uint32s_index.c b/input/bsv/uint32s_index.c index 2191a1141e93..5cfad0784ef0 100644 --- a/input/bsv/uint32s_index.c +++ b/input/bsv/uint32s_index.c @@ -252,7 +252,15 @@ uint32_t *uint32s_index_get(uint32s_index_t *index, uint32_t which) return NULL; if (!index->objects[which]) { + int i; RARCH_LOG("[STATESTREAM] accessed garbage collected block %d\n", which); + for (i = RBUF_LEN(index->additions); i != 0; i--) + { + if (which >= index->additions[i].first_index) + { + RARCH_LOG("[STATESTREAM] originally allocated on frame %ld\n", index->additions[i].frame_counter); + } + } return NULL; } return index->objects[which]; diff --git a/input/input_driver.h b/input/input_driver.h index f70fd8e20176..abd21c6ffac1 100644 --- a/input/input_driver.h +++ b/input/input_driver.h @@ -203,12 +203,14 @@ enum bsv_flags BSV_FLAG_MOVIE_RECORDING = (1 << 3), BSV_FLAG_MOVIE_END = (1 << 4), BSV_FLAG_MOVIE_EOF_EXIT = (1 << 5), - BSV_FLAG_MOVIE_FORCE_CHECKPOINT = (1 << 6) + BSV_FLAG_MOVIE_FORCE_CHECKPOINT = (1 << 6), + BSV_FLAG_MOVIE_PREV_CHECKPOINT = (1 << 7), + BSV_FLAG_MOVIE_NEXT_CHECKPOINT = (1 << 8) }; struct bsv_state { - uint8_t flags; + uint16_t flags; /* Movie playback/recording support. */ char movie_auto_path[PATH_MAX_LENGTH]; /* Immediate playback/recording. */ @@ -279,6 +281,15 @@ struct bsv_movie }; typedef struct bsv_movie bsv_movie_t; + +enum replay_checkpoint_behavior_ { + REPLAY_CPBEHAVIOR_SKIP, + REPLAY_CPBEHAVIOR_UPDATE, + REPLAY_CPBEHAVIOR_DESERIALIZE +}; + +typedef enum replay_checkpoint_behavior_ replay_checkpoint_behavior; + #endif /** @@ -1087,9 +1098,11 @@ void input_overlay_check_mouse_cursor(void); #endif #ifdef HAVE_BSV_MOVIE +void replay_maybe_skip_prev_next(input_driver_state_t *input_st); void bsv_movie_frame_rewind(void); void bsv_movie_next_frame(input_driver_state_t *input_st); -bool bsv_movie_read_next_events(bsv_movie_t *handle, bool skip_checkpoints); +bool bsv_movie_read_next_events(bsv_movie_t *handle, replay_checkpoint_behavior checkpoint_behavior, bool end_movie_on_eof); +bool bsv_movie_reset_playback(bsv_movie_t *handle); bool bsv_movie_reset_recording(bsv_movie_t *handle); void bsv_movie_finish_rewind(input_driver_state_t *input_st); void bsv_movie_deinit(input_driver_state_t *input_st); diff --git a/runloop.c b/runloop.c index e18c6ff63aac..ed543254996c 100644 --- a/runloop.c +++ b/runloop.c @@ -19,6 +19,7 @@ * If not, see . */ +#include "input/input_driver.h" #ifdef _WIN32 #ifdef _XBOX #include @@ -7286,6 +7287,16 @@ int runloop_iterate(void) #ifdef HAVE_NETWORKING /* FIXME: This is an ugly way to tell Netplay this... */ netplay_driver_ctl(RARCH_NETPLAY_CTL_PAUSE, NULL); +#endif +#ifdef HAVE_BSV_MOVIE + if (input_st->bsv_movie_state.flags & + (BSV_FLAG_MOVIE_FORCE_CHECKPOINT | + BSV_FLAG_MOVIE_PREV_CHECKPOINT | + BSV_FLAG_MOVIE_NEXT_CHECKPOINT)) + { + runloop_st->flags &= ~RUNLOOP_FLAG_PAUSED; + runloop_st->run_frames_and_pause = 2; + } #endif video_driver_cached_frame(); goto end; diff --git a/tasks/task_movie.c b/tasks/task_movie.c index dd8cc923d616..6e6861459ef2 100644 --- a/tasks/task_movie.c +++ b/tasks/task_movie.c @@ -73,11 +73,8 @@ static bool bsv_movie_init_playback(bsv_movie_t *handle, const char *path) int64_t *identifier_loc; uint32_t state_size = 0; uint32_t header[REPLAY_HEADER_LEN] = {0}; + uint64_t header_size = REPLAY_HEADER_LEN_BYTES; uint32_t vsn = 0; -#ifdef HAVE_STATESTREAM - uint32_t superblock_size = DEFAULT_SUPERBLOCK_SIZE, - block_size = DEFAULT_BLOCK_SIZE/4; -#endif intfstream_t *file = intfstream_open_file(path, RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); @@ -103,67 +100,16 @@ static bool bsv_movie_init_playback(bsv_movie_t *handle, const char *path) RARCH_ERR("[Replay] %s : vsn %d vs %d\n", msg_hash_to_str(MSG_MOVIE_FILE_IS_NOT_A_VALID_REPLAY_FILE), vsn, REPLAY_FORMAT_VERSION); return false; } + if (vsn < 2) + header_size = REPLAY_HEADER_V0V1_LEN_BYTES; handle->version = vsn; state_size = swap_if_big32(header[REPLAY_HEADER_STATE_SIZE_INDEX]); identifier_loc = (int64_t *)(header+REPLAY_HEADER_IDENTIFIER_INDEX); handle->identifier = swap_if_big64(*identifier_loc); - handle->min_file_pos = sizeof(header) + state_size; - if (state_size && vsn <= 1) - { - size_t info_size; - retro_ctx_serialize_info_t serial_info; - uint8_t *buf = (uint8_t*)malloc(state_size); - - if (!buf) - return false; - - /* The header used to be six ints long */ - intfstream_seek(handle->file, REPLAY_HEADER_V0V1_LEN_BYTES, SEEK_SET); - - if (intfstream_read(handle->file, buf, state_size) != state_size) - { - RARCH_ERR("[Replay] %s\n", msg_hash_to_str(MSG_COULD_NOT_READ_STATE_FROM_MOVIE)); - return false; - } - info_size = core_serialize_size(); - /* For cores like dosbox, the reported size is not always - correct. So we just give a warning if they don't match up. */ - serial_info.data_const = buf; - serial_info.size = state_size; - core_unserialize(&serial_info); - free(buf); - if (info_size != state_size) - RARCH_WARN("[Replay] %s\n", - msg_hash_to_str(MSG_MOVIE_FORMAT_DIFFERENT_SERIALIZER_VERSION)); - } - else if (vsn >= 2) - { - uint8_t compression, encoding; -#ifdef HAVE_STATESTREAM - uint32_t commit_settings = header[REPLAY_HEADER_CHECKPOINT_CONFIG_INDEX]; - superblock_size = swap_if_big32(header[REPLAY_HEADER_SUPERBLOCK_SIZE_INDEX]); - block_size = swap_if_big32(header[REPLAY_HEADER_BLOCK_SIZE_INDEX]); - handle->commit_interval = commit_settings >> 24; - handle->commit_threshold = (commit_settings >> 16) & 0x000000FF; - handle->checkpoint_compression = (commit_settings >> 8) & 0x000000FF; - handle->superblocks = uint32s_index_new(superblock_size,handle->commit_interval,handle->commit_threshold); - handle->blocks = uint32s_index_new(block_size/4,handle->commit_interval,handle->commit_threshold); -#endif - - if (intfstream_read(handle->file, &(compression), sizeof(uint8_t)) != sizeof(uint8_t) || - intfstream_read(handle->file, &(encoding), sizeof(uint8_t)) != sizeof(uint8_t)) - return false; - if (!bsv_movie_load_checkpoint(handle, compression, encoding, false)) - return false; - } - - if(vsn > 0) - bsv_movie_read_next_events(handle, false); - - - return true; + handle->min_file_pos = header_size + state_size; + return bsv_movie_reset_playback(handle); } static bool bsv_movie_init_record( @@ -277,6 +223,14 @@ static bsv_movie_t *bsv_movie_init_internal(const char *path, enum rarch_movie_t if (!handle) return NULL; + /* Just pick something really large + * ~1 million frames rewind should do the trick. */ + if (!(frame_pos = (size_t*)calloc((1 << 20), sizeof(size_t)))) + goto error; + + handle->frame_pos = frame_pos; + handle->frame_mask = (1 << 20) - 1; + if (type == RARCH_MOVIE_PLAYBACK) { if (!bsv_movie_init_playback(handle, path)) @@ -285,14 +239,7 @@ static bsv_movie_t *bsv_movie_init_internal(const char *path, enum rarch_movie_t else if (!bsv_movie_init_record(handle, path)) goto error; - /* Just pick something really large - * ~1 million frames rewind should do the trick. */ - if (!(frame_pos = (size_t*)calloc((1 << 20), sizeof(size_t)))) - goto error; - - handle->frame_pos = frame_pos; handle->frame_pos[0] = handle->min_file_pos; - handle->frame_mask = (1 << 20) - 1; return handle; From 384b131df40933cf6c4f635be482b150f66abdc8 Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Fri, 5 Sep 2025 20:18:26 -0700 Subject: [PATCH 05/12] Update per @jamiras review --- input/bsv/bsvmovie.c | 8 ++++++++ input/bsv/uint32s_index.c | 1 + 2 files changed, 9 insertions(+) diff --git a/input/bsv/bsvmovie.c b/input/bsv/bsvmovie.c index 3e1505b1cfe3..456a56c8d536 100644 --- a/input/bsv/bsvmovie.c +++ b/input/bsv/bsvmovie.c @@ -1592,6 +1592,10 @@ bool movie_skip_to_next_checkpoint(input_driver_state_t *input_st) /* For now, can't skip forward in a recording replay */ if (!input_st->bsv_movie_state_handle || !input_st->bsv_movie_state_handle->playback) return false; +#ifdef HAVE_CHEEVOS + if (rcheevos_hardcore_active()) + return false; +#endif input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_NEXT_CHECKPOINT; return true; } @@ -1626,6 +1630,10 @@ bool movie_skip_to_prev_checkpoint(input_driver_state_t *input_st) { if (!input_st->bsv_movie_state_handle) return false; +#ifdef HAVE_CHEEVOS + if (rcheevos_hardcore_active()) + return false; +#endif input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_PREV_CHECKPOINT; return true; } diff --git a/input/bsv/uint32s_index.c b/input/bsv/uint32s_index.c index 5cfad0784ef0..226cf62b4d6d 100644 --- a/input/bsv/uint32s_index.c +++ b/input/bsv/uint32s_index.c @@ -259,6 +259,7 @@ uint32_t *uint32s_index_get(uint32s_index_t *index, uint32_t which) if (which >= index->additions[i].first_index) { RARCH_LOG("[STATESTREAM] originally allocated on frame %ld\n", index->additions[i].frame_counter); + break; } } return NULL; From 18a307a62e12e4836a08ea27072f6f43ac7688fa Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Fri, 5 Sep 2025 22:04:19 -0700 Subject: [PATCH 06/12] add command to seek to a specific frame number --- command.c | 36 +++++++ command.h | 2 + input/bsv/bsvmovie.c | 230 ++++++++++++++++++++++++++++++------------- input/input_driver.h | 8 +- 4 files changed, 205 insertions(+), 71 deletions(-) diff --git a/command.c b/command.c index 4cd95c136ea9..b36383d926af 100644 --- a/command.c +++ b/command.c @@ -15,6 +15,7 @@ * If not, see . */ +#include "input/input_driver.h" #include #include #include @@ -802,6 +803,41 @@ bool command_play_replay_slot(command_t *cmd, const char *arg) #endif } +bool command_seek_replay(command_t *cmd, const char *arg) +{ +#ifdef HAVE_BSV_MOVIE + char reply[32]; + bool ret = true; + char *endptr; + size_t _len; + int64_t frame = strtoll(arg, &endptr, 10), target_frame; + input_driver_state_t *input_st = input_state_get_ptr(); + if (!endptr) + ret = false; + if (!(input_st->bsv_movie_state.flags & (BSV_FLAG_MOVIE_PLAYBACK | BSV_FLAG_MOVIE_RECORDING))) + ret = false; +#ifdef HAVE_CHEEVOS + ret = !rcheevos_hardcore_active(); +#endif + if (ret) + ret = movie_seek_to_frame(input_st, frame); + if (ret) + { + _len = strlcpy(reply, "OK ", sizeof(reply)); + sprintf(reply+_len, "%ld", input_st->bsv_movie_state.seek_target); + } + else + _len = strlcpy(reply, "NO", sizeof(reply)); + reply[_len] = '\n'; + reply[++_len] = '\0'; + cmd->replier(cmd, reply, _len); + return ret; +#else + cmd->replier(cmd, "NO\n", 4); + return false; +#endif +} + bool command_save_savefiles(command_t *cmd, const char* arg) { char reply[4]; diff --git a/command.h b/command.h index 57f4a0cdc61f..7d2c01a710d6 100644 --- a/command.h +++ b/command.h @@ -424,6 +424,7 @@ bool command_get_config_param(command_t *cmd, const char* arg); bool command_show_osd_msg(command_t *cmd, const char* arg); bool command_load_state_slot(command_t *cmd, const char* arg); bool command_play_replay_slot(command_t *cmd, const char* arg); +bool command_seek_replay(command_t *cmd, const char *arg); bool command_save_savefiles(command_t *cmd, const char* arg); bool command_load_savefiles(command_t *cmd, const char* arg); #ifdef HAVE_CHEEVOS @@ -452,6 +453,7 @@ static const struct cmd_action_map action_map[] = { { "LOAD_STATE_SLOT",command_load_state_slot, ""}, { "PLAY_REPLAY_SLOT",command_play_replay_slot, ""}, + { "SEEK_REPLAY",command_seek_replay, ""}, { "SAVE_FILES", command_save_savefiles, "No argument"}, { "LOAD_FILES", command_load_savefiles, "No argument"}, diff --git a/input/bsv/bsvmovie.c b/input/bsv/bsvmovie.c index 456a56c8d536..2d0dae2f12ba 100644 --- a/input/bsv/bsvmovie.c +++ b/input/bsv/bsvmovie.c @@ -58,6 +58,7 @@ bool bsv_movie_read_deduped_state(bsv_movie_t *movie, uint8_t *encoded, size_t e bool bsv_movie_skip_to_next_checkpoint_impl(bsv_movie_t *movie); bool bsv_movie_skip_to_prev_checkpoint_impl(bsv_movie_t *movie); +bool bsv_movie_seek_to_pos_impl(bsv_movie_t *movie, int64_t pos); bool bsv_movie_reset_playback(bsv_movie_t *handle) { @@ -743,19 +744,26 @@ bool bsv_movie_read_next_events(bsv_movie_t *handle, replay_checkpoint_behavior return true; } +void bsv_movie_scan_to(bsv_movie_t *movie, int64_t pos) +{ + if (!movie || movie->version == 0) + return; /* Old movies don't store enough information to fixup the frame counters. */ + while(intfstream_tell(movie->file) < pos && bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_UPDATE, false)) + { + movie->frame_counter += 1; + movie->frame_pos[movie->frame_counter & movie->frame_mask] = intfstream_tell(movie->file); + } +} + void bsv_movie_scan_from_start(bsv_movie_t *movie, int32_t len) { - if (movie->version == 0) + if (!movie || movie->version == 0) return; /* Old movies don't store enough information to fixup the frame counters. */ intfstream_seek(movie->file, movie->min_file_pos, SEEK_SET); movie->frame_counter = 0; movie->frame_pos[0] = intfstream_tell(movie->file); movie->cur_save_valid = false; - while(intfstream_tell(movie->file) < len && bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_UPDATE, false)) - { - movie->frame_counter += 1; - movie->frame_pos[movie->frame_counter & movie->frame_mask] = intfstream_tell(movie->file); - } + bsv_movie_scan_to(movie, len); } void bsv_movie_next_frame(input_driver_state_t *input_st) @@ -843,7 +851,12 @@ void bsv_movie_next_frame(input_driver_state_t *input_st) bsv_movie_read_next_events(handle, checkpoint_deserialize ? REPLAY_CPBEHAVIOR_DESERIALIZE : REPLAY_CPBEHAVIOR_UPDATE, true); handle->frame_pos[handle->frame_counter & handle->frame_mask] = intfstream_tell(handle->file); - if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_PREV_CHECKPOINT) + if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_SEEK_TO_FRAME) + { + bsv_movie_seek_to_pos_impl(handle, input_st->bsv_movie_state.seek_target_pos); + input_st->bsv_movie_state.flags &= ~BSV_FLAG_MOVIE_SEEK_TO_FRAME; + } + else if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_PREV_CHECKPOINT) { bsv_movie_skip_to_prev_checkpoint_impl(handle); input_st->bsv_movie_state.flags &= ~BSV_FLAG_MOVIE_PREV_CHECKPOINT; @@ -1560,13 +1573,15 @@ bool movie_commit_checkpoint(input_driver_state_t *input_st) input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_FORCE_CHECKPOINT; return true; } -uint8_t bsv_movie_peek_frame_token(bsv_movie_t *movie) +bool bsv_movie_peek_frame_info(bsv_movie_t *movie, uint8_t *token, uint64_t *len) { - uint8_t keycount, tok = REPLAY_TOKEN_INVALID; + uint8_t keycount; uint16_t event_count; + uint8_t tok; int64_t pos; + bool ret = false; if (!movie || movie->version == 0) - return REPLAY_TOKEN_INVALID; + return ret; pos = intfstream_tell(movie->file); if (movie->version > 1 && intfstream_seek(movie->file, sizeof(uint32_t), SEEK_CUR) < 0) @@ -1582,15 +1597,132 @@ uint8_t bsv_movie_peek_frame_token(bsv_movie_t *movie) goto end; if (intfstream_read(movie->file, &tok, 1) != 1) goto end; + if (len) + { + if (tok == REPLAY_TOKEN_CHECKPOINT_FRAME) + { + uint64_t state_length; + if (intfstream_read(movie->file, &(state_length), sizeof(uint64_t)) != sizeof(uint64_t)) + goto end; + state_length = swap_if_big64(state_length); + ret = intfstream_seek(movie->file, state_length, SEEK_CUR) >= 0; + } + else if (tok == REPLAY_TOKEN_CHECKPOINT2_FRAME) + { + uint32_t state_length; + /* skip compression, encoding, uncompressed unencoded size, uncompressed encoded size */ + if (intfstream_seek(movie->file, 2+2*sizeof(uint32_t), SEEK_CUR) < 0) + goto end; + /* read compressed encoded size */ + if (intfstream_read(movie->file, &(state_length), sizeof(uint32_t)) != sizeof(uint32_t)) + goto end; + /* seek past the state data */ + ret = intfstream_seek(movie->file, state_length, SEEK_CUR) >= 0; + } + else + { + RARCH_LOG("[Replay] Unrecognized frame token type %c\n", token); + goto end; + } + } + ret = true; end: + if (ret && token) + *token = tok; + if (ret && len) + *len = intfstream_tell(movie->file) - pos; if (intfstream_seek(movie->file, pos, SEEK_SET) < 0) - return REPLAY_TOKEN_INVALID; - return tok; + ret = false; + return ret; } +bool movie_find_checkpoint_before(bsv_movie_t *movie, int64_t frame, bool consider_paused, + int64_t *cp_pos_out, int64_t *cp_frame_out) +{ + /* skip to prev needs to go back at least 60 frames if rewinding when not paused */ + runloop_state_t *runloop_st = runloop_state_get_ptr(); + bool paused = !!(runloop_st->flags & RUNLOOP_FLAG_PAUSED) || consider_paused; + const int64_t prev_skip_min_distance = 60; + int64_t target_frame = (int64_t)movie->frame_counter, cur_frame = 0; + bool ret = false; + int64_t initial_pos, cp_pos=-1, cp_frame=-1; + uint64_t frame_len; + uint8_t tok; + if (!movie || movie->version == 0) + return false; + initial_pos = intfstream_tell(movie->file); + /* Find the right checkpoint to jump to. + In the future, backrefs could be used to make this faster */ + intfstream_seek(movie->file, movie->min_file_pos, SEEK_SET); + while (cur_frame < target_frame && bsv_movie_peek_frame_info(movie, &tok, &frame_len)) + { + if (tok == REPLAY_TOKEN_INVALID) + break; + if (tok == REPLAY_TOKEN_CHECKPOINT_FRAME || tok == REPLAY_TOKEN_CHECKPOINT2_FRAME) + { + if (!paused && target_frame - cur_frame < prev_skip_min_distance) + break; + cp_pos = intfstream_tell(movie->file); + cp_frame = cur_frame; + } + cur_frame += 1; + intfstream_seek(movie->file, frame_len, SEEK_CUR); + } + if (cp_pos_out) + *cp_pos_out = cp_pos; + if (cp_frame_out) + *cp_frame_out = cp_frame; + intfstream_seek(movie->file, initial_pos, SEEK_SET); + return cp_frame; +} + +bool movie_seek_to_frame(input_driver_state_t *input_st, int64_t frame) +{ + if (!input_st->bsv_movie_state_handle) + return false; +#ifdef HAVE_CHEEVOS + if (rcheevos_hardcore_active()) + return false; +#endif + if (!movie_find_checkpoint_before(input_st->bsv_movie_state_handle, frame, true, + &input_st->bsv_movie_state.seek_target_frame, + &input_st->bsv_movie_state.seek_target_pos)) + return false; + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_SEEK_TO_FRAME; + return true; +} +bool bsv_movie_seek_to_pos_impl(bsv_movie_t *movie, int64_t pos) +{ + int64_t movie_pos; + bool ret; + if (!movie || movie->version == 0) + return false; + movie_pos = intfstream_tell(movie->file); + /* assume file is at a frame boundary and frame is at a checkpoint boundary. */ + if (pos < movie_pos) + { + /* TODO: this could be made more efficient with backrefs if we + had a way to scan backwards; we wouldn't need to reset to go + backwards. */ + if (movie->playback) + bsv_movie_reset_playback(movie); + else + bsv_movie_reset_recording(movie); + } + if (pos != movie_pos) + bsv_movie_scan_to(movie, pos); + ret = bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_DESERIALIZE, false); + /* truncate recording after seek */ + if (!movie->playback) + intfstream_truncate(movie->file, intfstream_tell(movie->file)); + return ret; +} + bool movie_skip_to_next_checkpoint(input_driver_state_t *input_st) { /* For now, can't skip forward in a recording replay */ - if (!input_st->bsv_movie_state_handle || !input_st->bsv_movie_state_handle->playback) + if (!input_st->bsv_movie_state_handle || + !input_st->bsv_movie_state_handle->playback || + input_st->bsv_movie_state_handle->version == 0) return false; #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) @@ -1602,33 +1734,24 @@ bool movie_skip_to_next_checkpoint(input_driver_state_t *input_st) bool bsv_movie_skip_to_next_checkpoint_impl(bsv_movie_t *movie) { uint8_t tok = REPLAY_TOKEN_INVALID; - int64_t start_pos, start_frame; + uint64_t frame_len; + int64_t frame = (int64_t)movie->frame_counter, initial_pos, cp_pos; if (!movie || movie->version == 0) return false; + initial_pos = intfstream_tell(movie->file); /* scan forward until peek shows a checkpoint or checkpoint2 */ - /* if we get to the end, come back to here instead */ - start_pos = intfstream_tell(movie->file); - start_frame = movie->frame_counter; - do { - tok = REPLAY_TOKEN_INVALID; - if (!bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_UPDATE, false)) - break; - movie->frame_counter += 1; - tok = bsv_movie_peek_frame_token(movie); - } while (tok != REPLAY_TOKEN_CHECKPOINT_FRAME && - tok != REPLAY_TOKEN_CHECKPOINT2_FRAME && - tok != REPLAY_TOKEN_INVALID); - if (tok != REPLAY_TOKEN_INVALID) - return bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_DESERIALIZE, true); - /* There was no next savepoint, move the file cursor back to here */ - intfstream_seek(movie->file, start_pos, SEEK_SET); - movie->frame_counter = start_frame; - bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_UPDATE, true); - return false; + while (bsv_movie_peek_frame_info(movie, &tok, &frame_len) && + (tok != REPLAY_TOKEN_INVALID && + tok != REPLAY_TOKEN_CHECKPOINT_FRAME && + tok != REPLAY_TOKEN_CHECKPOINT2_FRAME)) + intfstream_seek(movie->file, frame_len, SEEK_CUR); + cp_pos = intfstream_tell(movie->file); + return bsv_movie_seek_to_pos_impl(movie, cp_pos); } bool movie_skip_to_prev_checkpoint(input_driver_state_t *input_st) { - if (!input_st->bsv_movie_state_handle) + if (!input_st->bsv_movie_state_handle || + input_st->bsv_movie_state_handle->version == 0) return false; #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) @@ -1639,41 +1762,10 @@ bool movie_skip_to_prev_checkpoint(input_driver_state_t *input_st) } bool bsv_movie_skip_to_prev_checkpoint_impl(bsv_movie_t *movie) { - /* skip to prev needs to go back at least 60 frames if rewinding when not paused */ - runloop_state_t *runloop_st = runloop_state_get_ptr(); - bool paused = !!(runloop_st->flags & RUNLOOP_FLAG_PAUSED); - const int64_t prev_skip_min_distance = 60; - int64_t cp_pos = -1; - int64_t target_frame = (int64_t)movie->frame_counter; - bool ret = false; + int64_t cp_pos; if (!movie || movie->version == 0) return false; - /* Find the right checkpoint to jump to. - In the future, backrefs could be used to make this faster */ - movie->frame_counter = 0; - intfstream_seek(movie->file, movie->min_file_pos, SEEK_SET); - while ((int64_t)movie->frame_counter < target_frame) - { - uint8_t tok = bsv_movie_peek_frame_token(movie); - uint8_t keycount; - uint16_t evtcount; - if (tok == REPLAY_TOKEN_INVALID) - break; - if (tok == REPLAY_TOKEN_CHECKPOINT_FRAME || tok == REPLAY_TOKEN_CHECKPOINT2_FRAME) - { - if (target_frame - (int64_t)movie->frame_counter < prev_skip_min_distance && !paused) - break; - cp_pos = intfstream_tell(movie->file); - } - movie->frame_counter += 1; - bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_SKIP, false); - } - /* Scan from the start up to the target checkpoint pos. If - statestream blocks stored their file position we wouldn't need - to do this and could just use backrefs */ - bsv_movie_reset_playback(movie); - if (cp_pos >= 0) - bsv_movie_scan_from_start(movie, cp_pos); - ret = bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_DESERIALIZE, true); - return ret; + if (!movie_find_checkpoint_before(movie, movie->frame_counter, false, &cp_pos, NULL)) + return false; + return bsv_movie_seek_to_pos_impl(movie, cp_pos); } diff --git a/input/input_driver.h b/input/input_driver.h index abd21c6ffac1..22b974326a32 100644 --- a/input/input_driver.h +++ b/input/input_driver.h @@ -205,7 +205,8 @@ enum bsv_flags BSV_FLAG_MOVIE_EOF_EXIT = (1 << 5), BSV_FLAG_MOVIE_FORCE_CHECKPOINT = (1 << 6), BSV_FLAG_MOVIE_PREV_CHECKPOINT = (1 << 7), - BSV_FLAG_MOVIE_NEXT_CHECKPOINT = (1 << 8) + BSV_FLAG_MOVIE_NEXT_CHECKPOINT = (1 << 8), + BSV_FLAG_MOVIE_SEEK_TO_FRAME = (1 << 9) }; struct bsv_state @@ -215,6 +216,8 @@ struct bsv_state char movie_auto_path[PATH_MAX_LENGTH]; /* Immediate playback/recording. */ char movie_start_path[PATH_MAX_LENGTH]; + /* Target frame/position to seek to next iteration. */ + int64_t seek_target_frame, seek_target_pos; }; /* These data are always little-endian. */ @@ -1098,7 +1101,6 @@ void input_overlay_check_mouse_cursor(void); #endif #ifdef HAVE_BSV_MOVIE -void replay_maybe_skip_prev_next(input_driver_state_t *input_st); void bsv_movie_frame_rewind(void); void bsv_movie_next_frame(input_driver_state_t *input_st); bool bsv_movie_read_next_events(bsv_movie_t *handle, replay_checkpoint_behavior checkpoint_behavior, bool end_movie_on_eof); @@ -1112,6 +1114,8 @@ void bsv_movie_enqueue(input_driver_state_t *input_st, bsv_movie_t *state, enum bool movie_commit_checkpoint(input_driver_state_t *input_st); bool movie_skip_to_prev_checkpoint(input_driver_state_t *input_st); bool movie_skip_to_next_checkpoint(input_driver_state_t *input_st); +bool movie_seek_to_frame(input_driver_state_t *input_st, int64_t frame); +bool movie_find_checkpoint_before(bsv_movie_t *movie, uint64_t frame, bool assume_paused, int64_t *cp_pos_out, int64_t *cp_frame_out); bool movie_start_playback(input_driver_state_t *input_st, char *path); bool movie_start_record(input_driver_state_t *input_st, char *path); bool movie_stop_playback(input_driver_state_t *input_st); From dfcc29095bef2768724843b41a26190e5a85c10b Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Sat, 6 Sep 2025 09:38:06 -0700 Subject: [PATCH 07/12] Add message queue toasts for seek forward/back --- command.c | 2 +- input/bsv/bsvmovie.c | 46 ++++++++++++++++++++++++++++++++++++++++---- input/input_driver.h | 1 - intl/msg_hash_us.h | 24 +++++++++++++++++++++++ msg_hash.h | 6 ++++++ 5 files changed, 73 insertions(+), 6 deletions(-) diff --git a/command.c b/command.c index b36383d926af..3f7c1f5214c7 100644 --- a/command.c +++ b/command.c @@ -824,7 +824,7 @@ bool command_seek_replay(command_t *cmd, const char *arg) if (ret) { _len = strlcpy(reply, "OK ", sizeof(reply)); - sprintf(reply+_len, "%ld", input_st->bsv_movie_state.seek_target); + sprintf(reply+_len, "%ld", input_st->bsv_movie_state.seek_target_frame); } else _len = strlcpy(reply, "NO", sizeof(reply)); diff --git a/input/bsv/bsvmovie.c b/input/bsv/bsvmovie.c index 2d0dae2f12ba..200027c4cb7b 100644 --- a/input/bsv/bsvmovie.c +++ b/input/bsv/bsvmovie.c @@ -853,17 +853,50 @@ void bsv_movie_next_frame(input_driver_state_t *input_st) if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_SEEK_TO_FRAME) { - bsv_movie_seek_to_pos_impl(handle, input_st->bsv_movie_state.seek_target_pos); + if (bsv_movie_seek_to_pos_impl(handle, input_st->bsv_movie_state.seek_target_pos)) + { + const char *_msg = msg_hash_to_str(MSG_REPLAY_SEEK_TO_FRAME); + runloop_msg_queue_push(_msg, strlen(_msg), 10, 15, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_SUCCESS); + } + else + { + const char *_msg = msg_hash_to_str(MSG_REPLAY_SEEK_TO_FRAME_FAILED); + runloop_msg_queue_push(_msg, strlen(_msg), 1, 180, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); + } input_st->bsv_movie_state.flags &= ~BSV_FLAG_MOVIE_SEEK_TO_FRAME; } else if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_PREV_CHECKPOINT) { - bsv_movie_skip_to_prev_checkpoint_impl(handle); + if (bsv_movie_skip_to_prev_checkpoint_impl(handle)) + { + const char *_msg = msg_hash_to_str(MSG_REPLAY_SEEK_TO_PREV_CHECKPOINT); + runloop_msg_queue_push(_msg, strlen(_msg), 10, 15, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_SUCCESS); + } + else + { + const char *_msg = msg_hash_to_str(MSG_REPLAY_SEEK_TO_PREV_CHECKPOINT_FAILED); + runloop_msg_queue_push(_msg, strlen(_msg), 1, 180, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); + } input_st->bsv_movie_state.flags &= ~BSV_FLAG_MOVIE_PREV_CHECKPOINT; } else if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_NEXT_CHECKPOINT) { - bsv_movie_skip_to_next_checkpoint_impl(handle); + if (bsv_movie_skip_to_next_checkpoint_impl(handle)) + { + const char *_msg = msg_hash_to_str(MSG_REPLAY_SEEK_TO_NEXT_CHECKPOINT); + runloop_msg_queue_push(_msg, strlen(_msg), 10, 15, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_SUCCESS); + } + else + { + const char *_msg = msg_hash_to_str(MSG_REPLAY_SEEK_TO_NEXT_CHECKPOINT_FAILED); + runloop_msg_queue_push(_msg, strlen(_msg), 1, 180, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); + } input_st->bsv_movie_state.flags &= ~BSV_FLAG_MOVIE_NEXT_CHECKPOINT; } } @@ -1619,6 +1652,10 @@ bool bsv_movie_peek_frame_info(bsv_movie_t *movie, uint8_t *token, uint64_t *len /* seek past the state data */ ret = intfstream_seek(movie->file, state_length, SEEK_CUR) >= 0; } + else if (tok == REPLAY_TOKEN_REGULAR_FRAME) + { + /* we are already at the end of the frame */ + } else { RARCH_LOG("[Replay] Unrecognized frame token type %c\n", token); @@ -1735,7 +1772,7 @@ bool bsv_movie_skip_to_next_checkpoint_impl(bsv_movie_t *movie) { uint8_t tok = REPLAY_TOKEN_INVALID; uint64_t frame_len; - int64_t frame = (int64_t)movie->frame_counter, initial_pos, cp_pos; + int64_t frame = (int64_t)movie->frame_counter, cp_pos, initial_pos; if (!movie || movie->version == 0) return false; initial_pos = intfstream_tell(movie->file); @@ -1746,6 +1783,7 @@ bool bsv_movie_skip_to_next_checkpoint_impl(bsv_movie_t *movie) tok != REPLAY_TOKEN_CHECKPOINT2_FRAME)) intfstream_seek(movie->file, frame_len, SEEK_CUR); cp_pos = intfstream_tell(movie->file); + intfstream_seek(movie->file, initial_pos, SEEK_SET); return bsv_movie_seek_to_pos_impl(movie, cp_pos); } bool movie_skip_to_prev_checkpoint(input_driver_state_t *input_st) diff --git a/input/input_driver.h b/input/input_driver.h index 22b974326a32..8dc2177f0eec 100644 --- a/input/input_driver.h +++ b/input/input_driver.h @@ -1115,7 +1115,6 @@ bool movie_commit_checkpoint(input_driver_state_t *input_st); bool movie_skip_to_prev_checkpoint(input_driver_state_t *input_st); bool movie_skip_to_next_checkpoint(input_driver_state_t *input_st); bool movie_seek_to_frame(input_driver_state_t *input_st, int64_t frame); -bool movie_find_checkpoint_before(bsv_movie_t *movie, uint64_t frame, bool assume_paused, int64_t *cp_pos_out, int64_t *cp_frame_out); bool movie_start_playback(input_driver_state_t *input_st, char *path); bool movie_start_record(input_driver_state_t *input_st, char *path); bool movie_stop_playback(input_driver_state_t *input_st); diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 1423c7c48cad..944c41c3ee5d 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -14922,6 +14922,30 @@ MSG_HASH( MSG_REPLAY_LOAD_STATE_OVERWRITING_REPLAY, "Wrong timeline; overwriting recording" ) +MSG_HASH( + MSG_REPLAY_SEEK_TO_PREV_CHECKPOINT, + "Seek Back" + ) +MSG_HASH( + MSG_REPLAY_SEEK_TO_PREV_CHECKPOINT_FAILED, + "Seek Back Failed" + ) +MSG_HASH( + MSG_REPLAY_SEEK_TO_NEXT_CHECKPOINT, + "Seek Forward" + ) +MSG_HASH( + MSG_REPLAY_SEEK_TO_NEXT_CHECKPOINT_FAILED, + "Seek Forward Failed" + ) +MSG_HASH( + MSG_REPLAY_SEEK_TO_FRAME, + "Seek Complete" + ) +MSG_HASH( + MSG_REPLAY_SEEK_TO_FRAME_FAILED, + "Seek Failed" + ) MSG_HASH( MSG_FOUND_SHADER, "Found shader" diff --git a/msg_hash.h b/msg_hash.h index 2f50eccab392..3095f572b4fd 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -487,6 +487,12 @@ enum msg_hash_enums MSG_FAILED_TO_START_MOVIE_RECORD, MSG_STATE_SLOT, MSG_REPLAY_SLOT, + MSG_REPLAY_SEEK_TO_PREV_CHECKPOINT, + MSG_REPLAY_SEEK_TO_PREV_CHECKPOINT_FAILED, + MSG_REPLAY_SEEK_TO_NEXT_CHECKPOINT, + MSG_REPLAY_SEEK_TO_NEXT_CHECKPOINT_FAILED, + MSG_REPLAY_SEEK_TO_FRAME, + MSG_REPLAY_SEEK_TO_FRAME_FAILED, MSG_STARTING_MOVIE_RECORD_TO, MSG_FAILED_TO_APPLY_SHADER, MSG_FAILED_TO_APPLY_SHADER_PRESET, From c5b2e02e729b5a66c0cca27729dd634ac6f470de Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Sat, 6 Sep 2025 20:42:57 -0700 Subject: [PATCH 08/12] Fix basis for seek, produce full message on reply --- command.c | 3 ++- input/bsv/bsvmovie.c | 15 ++++++++------- runloop.c | 3 ++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/command.c b/command.c index 3f7c1f5214c7..6e3e1a111562 100644 --- a/command.c +++ b/command.c @@ -824,7 +824,8 @@ bool command_seek_replay(command_t *cmd, const char *arg) if (ret) { _len = strlcpy(reply, "OK ", sizeof(reply)); - sprintf(reply+_len, "%ld", input_st->bsv_movie_state.seek_target_frame); + _len += snprintf(reply+_len, sizeof(reply)-_len, + "%ld", input_st->bsv_movie_state.seek_target_frame); } else _len = strlcpy(reply, "NO", sizeof(reply)); diff --git a/input/bsv/bsvmovie.c b/input/bsv/bsvmovie.c index 200027c4cb7b..ad186319121a 100644 --- a/input/bsv/bsvmovie.c +++ b/input/bsv/bsvmovie.c @@ -1679,7 +1679,7 @@ bool movie_find_checkpoint_before(bsv_movie_t *movie, int64_t frame, bool consid runloop_state_t *runloop_st = runloop_state_get_ptr(); bool paused = !!(runloop_st->flags & RUNLOOP_FLAG_PAUSED) || consider_paused; const int64_t prev_skip_min_distance = 60; - int64_t target_frame = (int64_t)movie->frame_counter, cur_frame = 0; + int64_t target_frame = frame, cur_frame = 0; bool ret = false; int64_t initial_pos, cp_pos=-1, cp_frame=-1; uint64_t frame_len; @@ -1696,10 +1696,11 @@ bool movie_find_checkpoint_before(bsv_movie_t *movie, int64_t frame, bool consid break; if (tok == REPLAY_TOKEN_CHECKPOINT_FRAME || tok == REPLAY_TOKEN_CHECKPOINT2_FRAME) { - if (!paused && target_frame - cur_frame < prev_skip_min_distance) - break; - cp_pos = intfstream_tell(movie->file); - cp_frame = cur_frame; + if (target_frame - cur_frame >= prev_skip_min_distance || paused) + { + cp_pos = intfstream_tell(movie->file); + cp_frame = cur_frame; + } } cur_frame += 1; intfstream_seek(movie->file, frame_len, SEEK_CUR); @@ -1721,8 +1722,8 @@ bool movie_seek_to_frame(input_driver_state_t *input_st, int64_t frame) return false; #endif if (!movie_find_checkpoint_before(input_st->bsv_movie_state_handle, frame, true, - &input_st->bsv_movie_state.seek_target_frame, - &input_st->bsv_movie_state.seek_target_pos)) + &input_st->bsv_movie_state.seek_target_pos, + &input_st->bsv_movie_state.seek_target_frame)) return false; input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_SEEK_TO_FRAME; return true; diff --git a/runloop.c b/runloop.c index ed543254996c..daabd5bc429c 100644 --- a/runloop.c +++ b/runloop.c @@ -7292,7 +7292,8 @@ int runloop_iterate(void) if (input_st->bsv_movie_state.flags & (BSV_FLAG_MOVIE_FORCE_CHECKPOINT | BSV_FLAG_MOVIE_PREV_CHECKPOINT | - BSV_FLAG_MOVIE_NEXT_CHECKPOINT)) + BSV_FLAG_MOVIE_NEXT_CHECKPOINT | + BSV_FLAG_MOVIE_SEEK_TO_FRAME)) { runloop_st->flags &= ~RUNLOOP_FLAG_PAUSED; runloop_st->run_frames_and_pause = 2; From 7792f1b4737b797080db1d4b34f26207c36ba1da Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Sun, 7 Sep 2025 12:33:39 -0700 Subject: [PATCH 09/12] fix bugs seeking back during record --- input/bsv/bsvmovie.c | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/input/bsv/bsvmovie.c b/input/bsv/bsvmovie.c index ad186319121a..572fbfd3cf20 100644 --- a/input/bsv/bsvmovie.c +++ b/input/bsv/bsvmovie.c @@ -149,7 +149,6 @@ bool bsv_movie_reset_recording(bsv_movie_t *handle) handle->cur_save_valid = false; intfstream_seek(handle->file, REPLAY_HEADER_LEN_BYTES, SEEK_SET); - intfstream_truncate(handle->file, REPLAY_HEADER_LEN_BYTES); intfstream_write(handle->file, &compression, 1); intfstream_write(handle->file, &encoding, 1); @@ -241,7 +240,7 @@ void bsv_movie_frame_rewind() RARCH_LOG("[REPLAY] rewound to %d\n", handle->frame_counter); intfstream_seek(handle->file, (int)handle->frame_pos[handle->frame_counter & handle->frame_mask], SEEK_SET); if (recording) - intfstream_truncate(handle->file, (int)handle->frame_pos[handle->frame_counter & handle->frame_mask]); + intfstream_truncate(handle->file, intfstream_tell(handle->file)); else bsv_movie_read_next_events(handle, REPLAY_CPBEHAVIOR_DESERIALIZE, true); } @@ -251,19 +250,11 @@ void bsv_movie_frame_rewind() RARCH_LOG("[Replay] rewound past beginning\n"); /* We rewound past the beginning. */ if (handle->playback) - { - intfstream_seek(handle->file, (int)handle->min_file_pos, SEEK_SET); -#ifdef HAVE_STATESTREAM - if (handle->superblocks) - uint32s_index_remove_after(handle->superblocks, 0); - if (handle->blocks) - uint32s_index_remove_after(handle->blocks, 0); -#endif - bsv_movie_read_next_events(handle, REPLAY_CPBEHAVIOR_DESERIALIZE, true); - } + bsv_movie_reset_playback(handle); else { bsv_movie_reset_recording(handle); + intfstream_truncate(handle->file, intfstream_tell(handle->file)); } } } @@ -844,6 +835,10 @@ void bsv_movie_next_frame(input_driver_state_t *input_st) /* write "next frame is not a checkpoint" */ intfstream_write(handle->file, (uint8_t *)(&frame_tok), sizeof(uint8_t)); } + /* To support seeking forwards during a paused replay, we would + need to *not* truncate here if we are in the "just paused, + running a frame to get the updated image, then will pause + again" state. */ intfstream_truncate(handle->file, intfstream_tell(handle->file)); } @@ -1591,7 +1586,7 @@ bool bsv_movie_read_deduped_state(bsv_movie_t *movie, uint8_t *encoded, size_t e if(!ret) { RARCH_ERR("[STATESTREAM] made it to end without superblock seq\n"); - abort(); + return false; } total_decode_micros += cpu_features_get_time_usec() - start; RARCH_DBG("[STATESTREAM] Total statestream decodes %d ; net time (secs): %f\n", total_decode_count, (double)total_decode_micros / (1000000.0)); @@ -1730,6 +1725,10 @@ bool movie_seek_to_frame(input_driver_state_t *input_st, int64_t frame) } bool bsv_movie_seek_to_pos_impl(bsv_movie_t *movie, int64_t pos) { + /* TODO: + 1. fix under "no previous replay" while recording + 2. fix under "some previous replay" while recording + */ int64_t movie_pos; bool ret; if (!movie || movie->version == 0) @@ -1737,21 +1736,17 @@ bool bsv_movie_seek_to_pos_impl(bsv_movie_t *movie, int64_t pos) movie_pos = intfstream_tell(movie->file); /* assume file is at a frame boundary and frame is at a checkpoint boundary. */ if (pos < movie_pos) - { /* TODO: this could be made more efficient with backrefs if we had a way to scan backwards; we wouldn't need to reset to go backwards. */ - if (movie->playback) - bsv_movie_reset_playback(movie); - else - bsv_movie_reset_recording(movie); - } + /* It seems strange, but we want `reset_playback` here and not + `reset_recording`, even if the movie is in record mode. This + is because we don't want to re-serialize the initial state or + whatever and act "as if" we just started recording. */ + bsv_movie_reset_playback(movie); if (pos != movie_pos) bsv_movie_scan_to(movie, pos); ret = bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_DESERIALIZE, false); - /* truncate recording after seek */ - if (!movie->playback) - intfstream_truncate(movie->file, intfstream_tell(movie->file)); return ret; } From 61052e1e78c28d82795bfa0ea43a413f4bcb04b6 Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Sun, 7 Sep 2025 12:56:24 -0700 Subject: [PATCH 10/12] Tidy up --- input/bsv/bsvmovie.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/input/bsv/bsvmovie.c b/input/bsv/bsvmovie.c index 572fbfd3cf20..73f55f4c5b64 100644 --- a/input/bsv/bsvmovie.c +++ b/input/bsv/bsvmovie.c @@ -1734,6 +1734,8 @@ bool bsv_movie_seek_to_pos_impl(bsv_movie_t *movie, int64_t pos) if (!movie || movie->version == 0) return false; movie_pos = intfstream_tell(movie->file); + if (pos == movie_pos) + return true; /* assume file is at a frame boundary and frame is at a checkpoint boundary. */ if (pos < movie_pos) /* TODO: this could be made more efficient with backrefs if we @@ -1746,15 +1748,16 @@ bool bsv_movie_seek_to_pos_impl(bsv_movie_t *movie, int64_t pos) bsv_movie_reset_playback(movie); if (pos != movie_pos) bsv_movie_scan_to(movie, pos); - ret = bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_DESERIALIZE, false); - return ret; + return bsv_movie_read_next_events(movie, REPLAY_CPBEHAVIOR_DESERIALIZE, false); } bool movie_skip_to_next_checkpoint(input_driver_state_t *input_st) { + runloop_state_t *runloop_st = runloop_state_get_ptr(); + bool paused = !!(runloop_st->flags & RUNLOOP_FLAG_PAUSED); /* For now, can't skip forward in a recording replay */ if (!input_st->bsv_movie_state_handle || - !input_st->bsv_movie_state_handle->playback || + (!input_st->bsv_movie_state_handle->playback && !paused) || input_st->bsv_movie_state_handle->version == 0) return false; #ifdef HAVE_CHEEVOS @@ -1768,7 +1771,7 @@ bool bsv_movie_skip_to_next_checkpoint_impl(bsv_movie_t *movie) { uint8_t tok = REPLAY_TOKEN_INVALID; uint64_t frame_len; - int64_t frame = (int64_t)movie->frame_counter, cp_pos, initial_pos; + int64_t cp_pos, initial_pos; if (!movie || movie->version == 0) return false; initial_pos = intfstream_tell(movie->file); @@ -1778,6 +1781,8 @@ bool bsv_movie_skip_to_next_checkpoint_impl(bsv_movie_t *movie) tok != REPLAY_TOKEN_CHECKPOINT_FRAME && tok != REPLAY_TOKEN_CHECKPOINT2_FRAME)) intfstream_seek(movie->file, frame_len, SEEK_CUR); + if (tok == REPLAY_TOKEN_INVALID) + return false; cp_pos = intfstream_tell(movie->file); intfstream_seek(movie->file, initial_pos, SEEK_SET); return bsv_movie_seek_to_pos_impl(movie, cp_pos); From 3c434ac57273df10a52926520b71e82c32a2db4e Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Mon, 8 Sep 2025 08:41:04 -0700 Subject: [PATCH 11/12] Allow back and forwards seeking of recording replays while paused --- input/bsv/bsvmovie.c | 14 ++++++++++---- input/input_driver.h | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/input/bsv/bsvmovie.c b/input/bsv/bsvmovie.c index 73f55f4c5b64..1cde9e47ba5e 100644 --- a/input/bsv/bsvmovie.c +++ b/input/bsv/bsvmovie.c @@ -781,7 +781,7 @@ void bsv_movie_next_frame(input_driver_state_t *input_st) return; #endif - if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_RECORDING) + if (!handle->playback && !(input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_SEEKING)) { int i; uint16_t evt_count = swap_if_big16(handle->input_event_count); @@ -841,9 +841,12 @@ void bsv_movie_next_frame(input_driver_state_t *input_st) again" state. */ intfstream_truncate(handle->file, intfstream_tell(handle->file)); } - - if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_PLAYBACK) + else /* either playback or seeking while recording */ + { bsv_movie_read_next_events(handle, checkpoint_deserialize ? REPLAY_CPBEHAVIOR_DESERIALIZE : REPLAY_CPBEHAVIOR_UPDATE, true); + /* clear seeking flag since we did read one frame */ + input_st->bsv_movie_state.flags &= ~BSV_FLAG_MOVIE_SEEKING; + } handle->frame_pos[handle->frame_counter & handle->frame_mask] = intfstream_tell(handle->file); if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_SEEK_TO_FRAME) @@ -853,6 +856,7 @@ void bsv_movie_next_frame(input_driver_state_t *input_st) const char *_msg = msg_hash_to_str(MSG_REPLAY_SEEK_TO_FRAME); runloop_msg_queue_push(_msg, strlen(_msg), 10, 15, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_SUCCESS); + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_SEEKING; } else { @@ -869,6 +873,7 @@ void bsv_movie_next_frame(input_driver_state_t *input_st) const char *_msg = msg_hash_to_str(MSG_REPLAY_SEEK_TO_PREV_CHECKPOINT); runloop_msg_queue_push(_msg, strlen(_msg), 10, 15, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_SUCCESS); + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_SEEKING; } else { @@ -885,6 +890,7 @@ void bsv_movie_next_frame(input_driver_state_t *input_st) const char *_msg = msg_hash_to_str(MSG_REPLAY_SEEK_TO_NEXT_CHECKPOINT); runloop_msg_queue_push(_msg, strlen(_msg), 10, 15, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_SUCCESS); + input_st->bsv_movie_state.flags |= BSV_FLAG_MOVIE_SEEKING; } else { @@ -1755,7 +1761,7 @@ bool movie_skip_to_next_checkpoint(input_driver_state_t *input_st) { runloop_state_t *runloop_st = runloop_state_get_ptr(); bool paused = !!(runloop_st->flags & RUNLOOP_FLAG_PAUSED); - /* For now, can't skip forward in a recording replay */ + /* Can't skip forward in an unpaused recording replay. */ if (!input_st->bsv_movie_state_handle || (!input_st->bsv_movie_state_handle->playback && !paused) || input_st->bsv_movie_state_handle->version == 0) diff --git a/input/input_driver.h b/input/input_driver.h index 8dc2177f0eec..b03162995a74 100644 --- a/input/input_driver.h +++ b/input/input_driver.h @@ -206,7 +206,8 @@ enum bsv_flags BSV_FLAG_MOVIE_FORCE_CHECKPOINT = (1 << 6), BSV_FLAG_MOVIE_PREV_CHECKPOINT = (1 << 7), BSV_FLAG_MOVIE_NEXT_CHECKPOINT = (1 << 8), - BSV_FLAG_MOVIE_SEEK_TO_FRAME = (1 << 9) + BSV_FLAG_MOVIE_SEEK_TO_FRAME = (1 << 9), + BSV_FLAG_MOVIE_SEEKING = (1 << 10) }; struct bsv_state From 8bf1c7b99e88546cd6757410380656f2d249ea6b Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Mon, 8 Sep 2025 08:52:47 -0700 Subject: [PATCH 12/12] Update changelog --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b252ae132342..1dc68816b6f8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,6 +36,8 @@ - INPUT/BSV/REPLAY: Add checkpoint and initial savestate compression, following the `savestate_file_compression` config boolean. Use zstd if available, or fall back to zlib. - INPUT/BSV/REPLAY: Add incremental checkpoints based on statestreams (depending on `HAVE_STATESTREAM` compile time flag). As an example, 60 `pcsx_rearmed` savestates would take 267MB uncompressed; with incremental encoding this is reduced to 77MB. Compressing the result can reduce the size to just 4MB. - INPUT/BSV/REPLAY: Checkpoint compression and encoding can be combined. For example, 60 `pcsx_rearmed` checkpoints can take up just 15MB if each state is incremental and compressed. This is not as optimal as using incremental states without save state compression followed by offline compression, but is a good compromise in many use cases. +- INPUT/BSV/REPLAY: Add hotkeys and text commands to force a checkpoint insertion into the currently recording replay, and to seek backwards to the previous checkpoint and forwards to the next checkpoint. +- INPUT/BSV/REPLAY: Add a text command to seek to a specific frame of the currently playing/recording replay; it will return via the command replier the actual seeked-to frame (right now it only supports seeking to checkpoints). - INTL: Add Irish Gaelic to selectable languages - IOS: Fix crash on iOS9 when fetching refresh rate - LINUX: Add full complement of key/value pairs to desktop entry