From 1cc76cd64db31731aa942b0d4cf3fe7783deac73 Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Wed, 16 Jul 2025 10:09:09 -0700 Subject: [PATCH 1/2] Add "same timeline check" and "future state check" for replays When loading and saving states taken during a replay, their contents should be compared with the current replay. - If in playback mode, states from past the end of the current replay should be disallowed, as should states from an alternate timeline of the current replay (inconsistent input history) - This is because in playback mode, RA replays are read-only - If in recording mode, states from the future or from another timeline should overwrite the current replay's data with a warning - In either playback or recording, loads of states from the past that have a consistent series of inputs (same timeline) should be loaded and the replay should be rewound (and truncated in case of recording) This behavior is as specified in https://tasvideos.org/LawsOfTAS/OnSavestates The existing code only implemented some of these checks. --- input/input_driver.c | 223 ++++++++++++++++++++++++++++++++++++++----- intl/msg_hash_us.h | 12 +++ msg_hash.h | 3 + 3 files changed, 212 insertions(+), 26 deletions(-) diff --git a/input/input_driver.c b/input/input_driver.c index 49a77e0746..300874c2f5 100644 --- a/input/input_driver.c +++ b/input/input_driver.c @@ -18,6 +18,9 @@ * with RetroArch. If not, see . **/ +#include "libretro.h" +#include "queues/message_queue.h" +#include "streams/interface_stream.h" #define _USE_MATH_DEFINES #include #include @@ -6461,7 +6464,7 @@ size_t replay_get_serialize_size(void) { input_driver_state_t *input_st = &input_driver_st; if (input_st->bsv_movie_state.flags & (BSV_FLAG_MOVIE_RECORDING | BSV_FLAG_MOVIE_PLAYBACK)) - return sizeof(int32_t)+intfstream_tell(input_st->bsv_movie_state_handle->file); + return sizeof(uint32_t)+intfstream_tell(input_st->bsv_movie_state_handle->file); return 0; } @@ -6472,18 +6475,13 @@ bool replay_get_serialized_data(void* buffer) if (input_st->bsv_movie_state.flags & (BSV_FLAG_MOVIE_RECORDING | BSV_FLAG_MOVIE_PLAYBACK)) { - int64_t file_end = intfstream_tell(handle->file); + int32_t file_end = (uint32_t)intfstream_tell(handle->file); int64_t read_amt = 0; - long file_end_lil = swap_if_big32(file_end); - uint8_t *file_end_bytes = (uint8_t *)(&file_end_lil); - uint8_t *buf = buffer; - buf[0] = file_end_bytes[0]; - buf[1] = file_end_bytes[1]; - buf[2] = file_end_bytes[2]; - buf[3] = file_end_bytes[3]; - buf += 4; + int32_t file_end_ = swap_if_big32(file_end); + ((uint32_t *)buffer)[0] = file_end_; + uint8_t *buf = ((uint8_t *)buffer) + sizeof(uint32_t); intfstream_rewind(handle->file); - read_amt = intfstream_read(handle->file, (void *)buf, file_end); + read_amt = intfstream_read(handle->file, buf, file_end); if (read_amt != file_end) RARCH_ERR("[Replay] Failed to write correct number of replay bytes into state file: %d / %d.\n", read_amt, file_end); @@ -6491,13 +6489,163 @@ bool replay_get_serialized_data(void* buffer) return true; } +bool replay_check_same_timeline(bsv_movie_t *movie, uint8_t *other_movie, int64_t other_len) +{ + int64_t check_limit = MIN(other_len, intfstream_tell(movie->file)); + intfstream_t *check_stream = intfstream_open_memory(other_movie, RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE, other_len); + bool ret = true; + int64_t check_cap = MAX(128 << 10, MAX(128*sizeof(bsv_key_data_t), 512*sizeof(bsv_input_data_t))); + uint8_t *buf1 = calloc(check_cap,1), *buf2 = calloc(check_cap,1); + size_t movie_pos = intfstream_tell(movie->file); + uint8_t keycount1, keycount2, frametok1, frametok2; + uint16_t btncount1, btncount2; + uint64_t size1, size2; + intfstream_rewind(movie->file); + intfstream_read(movie->file, buf1, 6*sizeof(uint32_t)); + intfstream_read(check_stream, buf2, 6*sizeof(uint32_t)); + if (memcmp(buf1, buf2, 6*sizeof(uint32_t)) != 0) + { + RARCH_ERR("[Replay] Headers of two movies differ, not same timeline\n"); + ret = false; + goto exit; + } + intfstream_seek(movie->file, movie->min_file_pos, SEEK_SET); + /* assumption: both headers have the same state size */ + intfstream_seek(check_stream, movie->min_file_pos, SEEK_SET); + if (movie->version == 0) + { + /* no choice but to memcmp the whole stream against the other */ + for (int64_t i = 0; ret && i < check_limit; i+=check_cap) + { + int64_t read_end = MIN(check_limit - i, check_cap); + int64_t read1 = intfstream_read(movie->file, buf1, read_end); + int64_t read2 = intfstream_read(check_stream, buf2, read_end); + if (read1 != read_end || read2 != read_end || memcmp(buf1, buf2, read_end) != 0) + { + RARCH_ERR("[Replay] One or the other replay checkpoint has different byte values\n"); + ret = false; + goto exit; + } + } + goto exit; + } + while(intfstream_tell(movie->file) < check_limit && intfstream_tell(check_stream) < check_limit) + { + if (intfstream_tell(movie->file) < 0 || intfstream_tell(check_stream) < 0) + { + RARCH_ERR("[Replay] One or the other replay checkpoint has ended prematurely\n"); + ret = false; + goto exit; + } + if (intfstream_read(movie->file, &keycount1, 1) < 1 || + intfstream_read(check_stream, &keycount2, 1) < 1 || + keycount1 != keycount2) + { + RARCH_ERR("[Replay] Replay checkpoints disagree on key count, %d vs %d\n", keycount1, keycount2); + ret = false; + goto exit; + } + if ((uint64_t)intfstream_read(movie->file, buf1, keycount1*sizeof(bsv_key_data_t)) < keycount1*sizeof(bsv_key_data_t) || + (uint64_t)intfstream_read(check_stream, buf2, keycount2*sizeof(bsv_key_data_t)) < keycount2*sizeof(bsv_key_data_t) || + memcmp(buf1, buf2, keycount1*sizeof(bsv_key_data_t)) != 0) + { + RARCH_ERR("[Replay] Replay checkpoints disagree on key data\n"); + ret = false; + goto exit; + } + if (intfstream_read(movie->file, &btncount1, 2) < 2 || + intfstream_read(check_stream, &btncount2, 2) < 2 || + btncount1 != btncount2) + { + RARCH_ERR("[Replay] Replay checkpoints disagree on input count\n"); + ret = false; + goto exit; + } + btncount1 = swap_if_big16(btncount1); + btncount2 = swap_if_big16(btncount2); + if ((uint64_t)intfstream_read(movie->file, buf1, btncount1*sizeof(bsv_input_data_t)) < btncount1*sizeof(bsv_input_data_t) || + (uint64_t)intfstream_read(check_stream, buf2, btncount2*sizeof(bsv_input_data_t)) < btncount2*sizeof(bsv_input_data_t) || + memcmp(buf1, buf2, btncount1*sizeof(bsv_input_data_t)) != 0) + { + RARCH_ERR("[Replay] Replay checkpoints disagree on input data\n"); + ret = false; + goto exit; + } + if (intfstream_read(movie->file, &frametok1, 1) < 1 || + intfstream_read(check_stream, &frametok2, 1) < 1 || + frametok1 != frametok2) + { + RARCH_ERR("[Replay] Replay checkpoints disagree on frame token\n"); + ret = false; + goto exit; + } + switch (frametok1) + { + case REPLAY_TOKEN_INVALID: + RARCH_ERR("[Replay] Both replays are somehow invalid\n"); + ret = false; + goto exit; + case REPLAY_TOKEN_REGULAR_FRAME: + break; + case REPLAY_TOKEN_CHECKPOINT_FRAME: + if ((uint64_t)intfstream_read(movie->file, &size1, sizeof(uint64_t)) < sizeof(uint64_t) || + (uint64_t)intfstream_read(check_stream, &size2, sizeof(uint64_t)) < sizeof(uint64_t) || + size1 != size2) + { + RARCH_ERR("[Replay] Replay checkpoints disagree on size or scheme\n"); + ret = false; + goto exit; + } + size1 = swap_if_big64(size1); + intfstream_seek(movie->file, size1, SEEK_CUR); + intfstream_seek(check_stream, size1, SEEK_CUR); + break; + case REPLAY_TOKEN_CHECKPOINT2_FRAME: + { + uint32_t cpsize1, cpsize2; + /* read cp2 header: + - one byte compression codec, one byte encoding scheme + - 4 byte uncompressed unencoded size, 4 byte uncompressed encoded size + - 4 byte compressed, encoded size + - the data will follow + */ + if (intfstream_read(movie->file, buf1, 2+sizeof(uint32_t)*3) != 2+sizeof(uint32_t)*3 || + intfstream_read(check_stream, buf2, 2+sizeof(uint32_t)*3) != 2+sizeof(uint32_t)*3 || + memcmp(buf1, buf2, 2+sizeof(uint32_t)*3) != 0 + ) + { + ret = false; + goto exit; + } + memcpy(&cpsize1, buf1+10, sizeof(uint32_t)); + memcpy(&cpsize2, buf2+10, sizeof(uint32_t)); + cpsize1 = swap_if_big32(cpsize1); + cpsize2 = swap_if_big32(cpsize2); + intfstream_seek(movie->file, cpsize1, SEEK_CUR); + intfstream_seek(check_stream, cpsize2, SEEK_CUR); + break; + } + default: + RARCH_ERR("[Replay] Unrecognized frame token in both replays\n"); + ret = false; + goto exit; + } + } + exit: + free(buf1); + free(buf2); + intfstream_close(check_stream); + intfstream_seek(movie->file, movie_pos, SEEK_SET); + return ret; +} + bool replay_set_serialized_data(void* buf) { uint8_t *buffer = buf; input_driver_state_t *input_st = &input_driver_st; bool playback = (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_PLAYBACK) ? true : false; bool recording = (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_RECORDING) ? true : false; - + bsv_movie_t *movie = input_st->bsv_movie_state_handle; /* If there is no current replay, ignore this entirely. TODO/FIXME: Later, consider loading up the replay and allow the user to continue it? @@ -6529,15 +6677,18 @@ bool replay_set_serialized_data(void* buf) else { /* TODO: should factor the next few lines away, magic numbers ahoy */ - uint32_t *header = (uint32_t *)(buffer + sizeof(int32_t)); + uint32_t *header = (uint32_t *)(buffer + sizeof(uint32_t)); int64_t *ident_spot = (int64_t *)(header + 4); - int64_t ident = swap_if_big64(*ident_spot); + int64_t ident; + /* avoid unaligned 8-byte read */ + memcpy(&ident, ident_spot, sizeof(int64_t)); + ident = swap_if_big64(ident); - if (ident == input_st->bsv_movie_state_handle->identifier) /* is compatible? */ + if (ident == movie->identifier) /* is compatible? */ { - int32_t loaded_len = swap_if_big32(((int32_t *)buffer)[0]); - int64_t handle_idx = intfstream_tell( - input_st->bsv_movie_state_handle->file); + int64_t loaded_len = (int64_t)swap_if_big32(((uint32_t *)buffer)[0]); + int64_t handle_idx = intfstream_tell(movie->file); + bool same_timeline = replay_check_same_timeline(movie, (uint8_t *)header, loaded_len); /* If the state is part of this replay, go back to that state and rewind/fast forward the replay. @@ -6548,19 +6699,39 @@ bool replay_set_serialized_data(void* buf) This can truncate the current replay if we're in recording mode. */ - if (loaded_len > handle_idx) + if (playback && loaded_len > handle_idx) { - /* TODO: Really, to be very careful, we should be - checking that the events in the loaded state are the - same up to handle_idx. Right? */ - intfstream_rewind(input_st->bsv_movie_state_handle->file); - intfstream_write(input_st->bsv_movie_state_handle->file, buffer+sizeof(int32_t), loaded_len); + const char *_msg = msg_hash_to_str(MSG_REPLAY_LOAD_STATE_FAILED_FUTURE_STATE); + runloop_msg_queue_push(_msg, strlen(_msg), 1, 180, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); + RARCH_ERR("[Replay] %s.\n", _msg); + return false; + } + else if (playback && !same_timeline) + { + const char *_msg = msg_hash_to_str(MSG_REPLAY_LOAD_STATE_FAILED_WRONG_TIMELINE); + runloop_msg_queue_push(_msg, strlen(_msg), 1, 180, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR); + RARCH_ERR("[Replay] %s.\n", _msg); + return false; + } + else if (recording && (loaded_len > handle_idx || !same_timeline)) + { + if (!same_timeline) + { + const char *_msg = msg_hash_to_str(MSG_REPLAY_LOAD_STATE_OVERWRITING_REPLAY); + runloop_msg_queue_push(_msg, strlen(_msg), 1, 180, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); + RARCH_WARN("[Replay] %s.\n", _msg); + } + intfstream_rewind(movie->file); + intfstream_write(movie->file, buffer+sizeof(int32_t), loaded_len); } else { - intfstream_seek(input_st->bsv_movie_state_handle->file, loaded_len, SEEK_SET); + intfstream_seek(movie->file, loaded_len, SEEK_SET); if (recording) - intfstream_truncate(input_st->bsv_movie_state_handle->file, loaded_len); + intfstream_truncate(movie->file, loaded_len); } } else diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 93a0427348..902912c6f3 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -14829,6 +14829,18 @@ MSG_HASH( MSG_REPLAY_LOAD_STATE_HALT_INCOMPAT, "Not compatible with replay" ) +MSG_HASH( + MSG_REPLAY_LOAD_STATE_FAILED_FUTURE_STATE, + "Can't load future state during playback" + ) +MSG_HASH( + MSG_REPLAY_LOAD_STATE_FAILED_WRONG_TIMELINE, + "Wrong timeline error during playback" + ) +MSG_HASH( + MSG_REPLAY_LOAD_STATE_OVERWRITING_REPLAY, + "Wrong timeline; overwriting recording" + ) MSG_HASH( MSG_FOUND_SHADER, "Found shader" diff --git a/msg_hash.h b/msg_hash.h index 11a8ade127..1dc3f16b99 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -285,6 +285,9 @@ enum msg_hash_enums MSG_FOUND_LAST_REPLAY_SLOT, MSG_REPLAY_LOAD_STATE_HALT_INCOMPAT, MSG_REPLAY_LOAD_STATE_FAILED_INCOMPAT, + MSG_REPLAY_LOAD_STATE_FAILED_FUTURE_STATE, + MSG_REPLAY_LOAD_STATE_FAILED_WRONG_TIMELINE, + MSG_REPLAY_LOAD_STATE_OVERWRITING_REPLAY, MSG_RESTORED_OLD_SAVE_STATE, MSG_NO_STATE_HAS_BEEN_LOADED_YET, MSG_GOT_CONNECTION_FROM, From 7c05359933e807c3bb576174c90051514f17b3b5 Mon Sep 17 00:00:00 2001 From: "Joseph C. Osborn" Date: Wed, 16 Jul 2025 10:35:33 -0700 Subject: [PATCH 2/2] c89 fixes --- input/input_driver.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/input/input_driver.c b/input/input_driver.c index 300874c2f5..ef4a8df532 100644 --- a/input/input_driver.c +++ b/input/input_driver.c @@ -6478,8 +6478,9 @@ bool replay_get_serialized_data(void* buffer) int32_t file_end = (uint32_t)intfstream_tell(handle->file); int64_t read_amt = 0; int32_t file_end_ = swap_if_big32(file_end); + uint8_t *buf; ((uint32_t *)buffer)[0] = file_end_; - uint8_t *buf = ((uint8_t *)buffer) + sizeof(uint32_t); + buf = ((uint8_t *)buffer) + sizeof(uint32_t); intfstream_rewind(handle->file); read_amt = intfstream_read(handle->file, buf, file_end); if (read_amt != file_end) @@ -6514,8 +6515,9 @@ bool replay_check_same_timeline(bsv_movie_t *movie, uint8_t *other_movie, int64_ intfstream_seek(check_stream, movie->min_file_pos, SEEK_SET); if (movie->version == 0) { + int64_t i; /* no choice but to memcmp the whole stream against the other */ - for (int64_t i = 0; ret && i < check_limit; i+=check_cap) + for (i = 0; ret && i < check_limit; i+=check_cap) { int64_t read_end = MIN(check_limit - i, check_cap); int64_t read1 = intfstream_read(movie->file, buf1, read_end);