From ebaa225bcbf823db358632bdc6f3662e9aaabc96 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Mon, 29 Jun 2020 22:32:24 +0800 Subject: [PATCH 01/14] core: Add read-only mode and separate savestate slots for movies The read-only mode switch affects how movies interact with savestates after you start a movie playback and load a savestate. When you are in read-only mode, the movie will resume playing from the loaded savestate. When you are in read+write mode however, your input will be recorded over the original movie ('rerecording'). If you wish to start rerecording immediately, you should switch to R+W mode, save a state and then load it. To make this more user-friendly, I also added a unique ID to the movies, which allows each movie to have an individual set of savestate slots (plus another set for when not doing any movies). This is as recommended by staff at TASVideos. --- src/core/movie.cpp | 71 ++++++++++++++++++++++++++++++++++++++++-- src/core/movie.h | 32 +++++++++++++------ src/core/savestate.cpp | 12 +++++-- 3 files changed, 102 insertions(+), 13 deletions(-) diff --git a/src/core/movie.cpp b/src/core/movie.cpp index 97c96ba3c..f680e9226 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -3,10 +3,12 @@ // Refer to the license.txt file included. #include +#include #include #include #include #include +#include #include "common/bit_field.h" #include "common/common_types.h" #include "common/file_util.h" @@ -117,12 +119,61 @@ struct CTMHeader { u64_le program_id; /// ID of the ROM being executed. Also called title_id std::array revision; /// Git hash of the revision this movie was created with u64_le clock_init_time; /// The init time of the system clock + // Unique identifier of the movie, used to support separate savestate slots for TASing + u64_le id; - std::array reserved; /// Make heading 256 bytes so it has consistent size + std::array reserved; /// Make heading 256 bytes so it has consistent size }; static_assert(sizeof(CTMHeader) == 256, "CTMHeader should be 256 bytes"); #pragma pack(pop) +template +void Movie::serialize(Archive& ar, const unsigned int file_version) { + // Only serialize what's needed to make savestates useful for TAS: + u64 _current_byte = static_cast(current_byte); + ar& _current_byte; + current_byte = static_cast(_current_byte); + + std::vector recorded_input_ = recorded_input; + ar& recorded_input_; + + ar& init_time; + + if (file_version > 0) { + if (Archive::is_loading::value) { + u64 savestate_movie_id; + ar& savestate_movie_id; + if (id != savestate_movie_id) { + if (savestate_movie_id == 0) { + throw std::runtime_error("You must close your movie to load this state"); + } else { + throw std::runtime_error("You must load the same movie to load this state"); + } + } + } else { + ar& id; + } + } + + if (Archive::is_loading::value && id != 0) { + if (read_only) { // Do not replace the previously recorded input. + if (play_mode == PlayMode::Recording) { + SaveMovie(); + } + if (current_byte >= recorded_input.size()) { + throw std::runtime_error( + "This savestate was created at a later point and must be loaded in R+W mode"); + } + play_mode = PlayMode::Playing; + } else { + recorded_input = std::move(recorded_input_); + play_mode = PlayMode::Recording; + } + } +} + +SERIALIZE_IMPL(Movie) + bool Movie::IsPlayingInput() const { return play_mode == PlayMode::Playing; } @@ -135,6 +186,7 @@ void Movie::CheckInputEnd() { LOG_INFO(Movie, "Playback finished"); play_mode = PlayMode::None; init_time = 0; + id = 0; playback_completion_callback(); } } @@ -394,6 +446,7 @@ void Movie::SaveMovie() { CTMHeader header = {}; header.filetype = header_magic_bytes; header.clock_init_time = init_time; + header.id = id; Core::System::GetInstance().GetAppLoader().ReadProgramId(header.program_id); @@ -421,10 +474,14 @@ void Movie::StartPlayback(const std::string& movie_file, save_record.ReadArray(&header, 1); if (ValidateHeader(header) != ValidationResult::Invalid) { play_mode = PlayMode::Playing; + record_movie_file = movie_file; recorded_input.resize(size - sizeof(CTMHeader)); save_record.ReadArray(recorded_input.data(), recorded_input.size()); current_byte = 0; + id = header.id; playback_completion_callback = completion_callback; + + LOG_INFO(Movie, "Loaded Movie, ID: {:016X}", id); } } else { LOG_ERROR(Movie, "Failed to playback movie: Unable to open '{}'", movie_file); @@ -432,9 +489,18 @@ void Movie::StartPlayback(const std::string& movie_file, } void Movie::StartRecording(const std::string& movie_file) { - LOG_INFO(Movie, "Enabling Movie recording"); play_mode = PlayMode::Recording; record_movie_file = movie_file; + + // Generate a random ID + CryptoPP::AutoSeededRandomPool rng; + rng.GenerateBlock(reinterpret_cast(&id), sizeof(id)); + + LOG_INFO(Movie, "Enabling Movie recording, ID: {:016X}", id); +} + +void Movie::SetReadOnly(bool read_only_) { + read_only = read_only_; } static boost::optional ReadHeader(const std::string& movie_file) { @@ -496,6 +562,7 @@ void Movie::Shutdown() { record_movie_file.clear(); current_byte = 0; init_time = 0; + id = 0; } template diff --git a/src/core/movie.h b/src/core/movie.h index e578b909c..0a4ab410a 100644 --- a/src/core/movie.h +++ b/src/core/movie.h @@ -46,6 +46,18 @@ public: const std::string& movie_file, std::function completion_callback = [] {}); void StartRecording(const std::string& movie_file); + /** + * Sets the read-only status. + * When true, movies will be opened in read-only mode. Loading a state will resume playback + * from that state. + * When false, movies will be opened in read/write mode. Loading a state will start recording + * from that state (rerecording). To start rerecording without loading a state, one can save + * and then immediately load while in R/W. + * + * The default is true. + */ + void SetReadOnly(bool read_only); + /// Prepare to override the clock before playing back movies void PrepareForPlayback(const std::string& movie_file); @@ -58,6 +70,11 @@ public: u64 GetOverrideInitTime() const; u64 GetMovieProgramID(const std::string& movie_file) const; + /// Get the current movie's unique ID. Used to provide separate savestate slots for movies. + u64 GetCurrentMovieID() const { + return id; + } + void Shutdown(); /** @@ -133,16 +150,13 @@ private: u64 init_time; std::function playback_completion_callback; std::size_t current_byte = 0; + u64 id = 0; // ID of the current movie loaded + bool read_only = true; template - void serialize(Archive& ar, const unsigned int) { - // Only serialize what's needed to make savestates useful for TAS: - u64 _current_byte = static_cast(current_byte); - ar& _current_byte; - current_byte = static_cast(_current_byte); - ar& recorded_input; - ar& init_time; - } + void serialize(Archive& ar, const unsigned int file_version); friend class boost::serialization::access; }; -} // namespace Core \ No newline at end of file +} // namespace Core + +BOOST_CLASS_VERSION(Core::Movie, 1) diff --git a/src/core/savestate.cpp b/src/core/savestate.cpp index 26fc0cbed..a5bdb49d8 100644 --- a/src/core/savestate.cpp +++ b/src/core/savestate.cpp @@ -11,6 +11,7 @@ #include "common/zstd_compression.h" #include "core/cheats/cheats.h" #include "core/core.h" +#include "core/movie.h" #include "core/savestate.h" #include "network/network.h" #include "video_core/video_core.h" @@ -37,8 +38,15 @@ static_assert(sizeof(CSTHeader) == 256, "CSTHeader should be 256 bytes"); constexpr std::array header_magic_bytes{{'C', 'S', 'T', 0x1B}}; std::string GetSaveStatePath(u64 program_id, u32 slot) { - return fmt::format("{}{:016X}.{:02d}.cst", FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), - program_id, slot); + const u64 movie_id = Movie::GetInstance().GetCurrentMovieID(); + if (movie_id) { + return fmt::format("{}{:016X}.movie{:016X}.{:02d}.cst", + FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), program_id, + movie_id, slot); + } else { + return fmt::format("{}{:016X}.{:02d}.cst", + FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), program_id, slot); + } } std::vector ListSaveStates(u64 program_id) { From bd8866724754338f5e3fb986271e241ec00f17c5 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Tue, 30 Jun 2020 22:48:10 +0800 Subject: [PATCH 02/14] core/movie: Add a few fields These fields are included in most emulators and required by TASVideos. `input_count` is implemented by counting the number of 'PadAndCircle' states, as this is always polled regularly and can act as a time/length indicator. TASVideos also require the input count/frame count to be verified by the emulator before playback, which is also implemented in this commit. --- src/core/movie.cpp | 85 ++++++++++++++++++++++++++++++++++++++++------ src/core/movie.h | 27 +++++++++++---- 2 files changed, 95 insertions(+), 17 deletions(-) diff --git a/src/core/movie.cpp b/src/core/movie.cpp index f680e9226..35157508b 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -119,10 +119,12 @@ struct CTMHeader { u64_le program_id; /// ID of the ROM being executed. Also called title_id std::array revision; /// Git hash of the revision this movie was created with u64_le clock_init_time; /// The init time of the system clock - // Unique identifier of the movie, used to support separate savestate slots for TASing - u64_le id; + u64_le id; /// Unique identifier of the movie, used to support separate savestate slots + std::array author; /// Author of the movie + u32_le rerecord_count; /// Number of rerecords when making the movie + u64_le input_count; /// Number of inputs (button and pad states) when making the movie - std::array reserved; /// Make heading 256 bytes so it has consistent size + std::array reserved; /// Make heading 256 bytes so it has consistent size }; static_assert(sizeof(CTMHeader) == 256, "CTMHeader should be 256 bytes"); #pragma pack(pop) @@ -168,6 +170,7 @@ void Movie::serialize(Archive& ar, const unsigned int file_version) { } else { recorded_input = std::move(recorded_input_); play_mode = PlayMode::Recording; + rerecord_count++; } } } @@ -434,6 +437,28 @@ Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header, u64 progr return ValidationResult::OK; } +static u64 GetInputCount(const std::vector& input) { + u64 input_count = 0; + for (std::size_t pos = 0; pos < input.size(); pos += sizeof(ControllerState)) { + if (input.size() < pos + sizeof(ControllerState)) { + break; + } + + ControllerState state; + std::memcpy(&state, input.data() + pos, sizeof(ControllerState)); + if (state.type == ControllerStateType::PadAndCircle) { + input_count++; + } + } + return input_count; +} + +Movie::ValidationResult Movie::ValidateInput(const std::vector& input, + u64 expected_count) const { + return GetInputCount(input) == expected_count ? ValidationResult::OK + : ValidationResult::InputCountDismatch; +} + void Movie::SaveMovie() { LOG_INFO(Movie, "Saving recorded movie to '{}'", record_movie_file); FileUtil::IOFile save_record(record_movie_file, "wb"); @@ -448,6 +473,12 @@ void Movie::SaveMovie() { header.clock_init_time = init_time; header.id = id; + std::memcpy(header.author.data(), record_movie_author.data(), + std::min(header.author.size(), record_movie_author.size())); + + header.rerecord_count = rerecord_count; + header.input_count = GetInputCount(recorded_input); + Core::System::GetInstance().GetAppLoader().ReadProgramId(header.program_id); std::string rev_bytes; @@ -475,8 +506,16 @@ void Movie::StartPlayback(const std::string& movie_file, if (ValidateHeader(header) != ValidationResult::Invalid) { play_mode = PlayMode::Playing; record_movie_file = movie_file; + + std::array author{}; // Add a null terminator + std::memcpy(author.data(), header.author.data(), header.author.size()); + record_movie_author = author.data(); + + rerecord_count = header.rerecord_count; + recorded_input.resize(size - sizeof(CTMHeader)); save_record.ReadArray(recorded_input.data(), recorded_input.size()); + current_byte = 0; id = header.id; playback_completion_callback = completion_callback; @@ -488,9 +527,11 @@ void Movie::StartPlayback(const std::string& movie_file, } } -void Movie::StartRecording(const std::string& movie_file) { +void Movie::StartRecording(const std::string& movie_file, const std::string& author) { play_mode = PlayMode::Recording; record_movie_file = movie_file; + record_movie_author = author; + rerecord_count = 1; // Generate a random ID CryptoPP::AutoSeededRandomPool rng; @@ -537,19 +578,41 @@ void Movie::PrepareForRecording() { Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file, u64 program_id) const { LOG_INFO(Movie, "Validating Movie file '{}'", movie_file); - auto header = ReadHeader(movie_file); - if (header == boost::none) - return ValidationResult::Invalid; - return ValidateHeader(header.value(), program_id); + FileUtil::IOFile save_record(movie_file, "rb"); + const u64 size = save_record.GetSize(); + + if (!save_record || size <= sizeof(CTMHeader)) { + return ValidationResult::Invalid; + } + + CTMHeader header; + save_record.ReadArray(&header, 1); + + if (header_magic_bytes != header.filetype) { + return ValidationResult::Invalid; + } + + auto result = ValidateHeader(header, program_id); + if (result != ValidationResult::OK) { + return result; + } + + std::vector input(size - sizeof(header)); + save_record.ReadArray(input.data(), input.size()); + return ValidateInput(input, header.input_count); } -u64 Movie::GetMovieProgramID(const std::string& movie_file) const { +Movie::MovieMetadata Movie::GetMovieMetadata(const std::string& movie_file) const { auto header = ReadHeader(movie_file); if (header == boost::none) - return 0; + return {}; - return static_cast(header.value().program_id); + std::array author{}; // Add a null terminator + std::memcpy(author.data(), header->author.data(), header->author.size()); + + return {header->program_id, std::string{author.data()}, header->rerecord_count, + header->input_count}; } void Movie::Shutdown() { diff --git a/src/core/movie.h b/src/core/movie.h index 0a4ab410a..7093290c2 100644 --- a/src/core/movie.h +++ b/src/core/movie.h @@ -32,6 +32,7 @@ public: OK, RevisionDismatch, GameDismatch, + InputCountDismatch, Invalid, }; /** @@ -42,9 +43,9 @@ public: return s_instance; } - void StartPlayback( - const std::string& movie_file, std::function completion_callback = [] {}); - void StartRecording(const std::string& movie_file); + void StartPlayback(const std::string& movie_file, + std::function completion_callback = [] {}); + void StartRecording(const std::string& movie_file, const std::string& author); /** * Sets the read-only status. @@ -68,7 +69,14 @@ public: /// Get the init time that would override the one in the settings u64 GetOverrideInitTime() const; - u64 GetMovieProgramID(const std::string& movie_file) const; + + struct MovieMetadata { + u64 program_id; + std::string author; + u32 rerecord_count; + u64 input_count; + }; + MovieMetadata GetMovieMetadata(const std::string& movie_file) const; /// Get the current movie's unique ID. Used to provide separate savestate slots for movies. u64 GetCurrentMovieID() const { @@ -141,18 +149,25 @@ private: void Record(const Service::IR::ExtraHIDResponse& extra_hid_response); ValidationResult ValidateHeader(const CTMHeader& header, u64 program_id = 0) const; + ValidationResult ValidateInput(const std::vector& input, u64 expected_count) const; void SaveMovie(); PlayMode play_mode; + std::string record_movie_file; + std::string record_movie_author; + std::vector recorded_input; - u64 init_time; - std::function playback_completion_callback; std::size_t current_byte = 0; + u64 id = 0; // ID of the current movie loaded + u64 init_time; + u32 rerecord_count = 1; bool read_only = true; + std::function playback_completion_callback; + template void serialize(Archive& ar, const unsigned int file_version); friend class boost::serialization::access; From 06bc37a67d6de975ceac8bffb57d93dc359e851a Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Fri, 3 Jul 2020 22:14:26 +0800 Subject: [PATCH 03/14] core/movie: Remove program ID checks Most other emulators handle this automatically in the frontend, booting/restarting the corresponding game instead of reporting an error. Therefore, remove these checks and errors from the module. --- src/core/movie.cpp | 18 +++++++----------- src/core/movie.h | 5 ++--- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/core/movie.cpp b/src/core/movie.cpp index 35157508b..2be5dfd27 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -413,21 +413,13 @@ u64 Movie::GetOverrideInitTime() const { return init_time; } -Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header, u64 program_id) const { +Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header) const { if (header_magic_bytes != header.filetype) { LOG_ERROR(Movie, "Playback file does not have valid header"); return ValidationResult::Invalid; } std::string revision = fmt::format("{:02x}", fmt::join(header.revision, "")); - - if (!program_id) - Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id); - if (program_id != header.program_id) { - LOG_WARNING(Movie, "This movie was recorded using a ROM with a different program id"); - return ValidationResult::GameDismatch; - } - if (revision != Common::g_scm_rev) { LOG_WARNING(Movie, "This movie was created on a different version of Citra, playback may desync"); @@ -576,7 +568,7 @@ void Movie::PrepareForRecording() { : Settings::values.init_time); } -Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file, u64 program_id) const { +Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file) const { LOG_INFO(Movie, "Validating Movie file '{}'", movie_file); FileUtil::IOFile save_record(movie_file, "rb"); @@ -593,11 +585,15 @@ Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file, u64 return ValidationResult::Invalid; } - auto result = ValidateHeader(header, program_id); + auto result = ValidateHeader(header); if (result != ValidationResult::OK) { return result; } + if (!header.input_count) { // Probably created by an older version. + return ValidationResult::OK; + } + std::vector input(size - sizeof(header)); save_record.ReadArray(input.data(), input.size()); return ValidateInput(input, header.input_count); diff --git a/src/core/movie.h b/src/core/movie.h index 7093290c2..09912c3f8 100644 --- a/src/core/movie.h +++ b/src/core/movie.h @@ -31,7 +31,6 @@ public: enum class ValidationResult { OK, RevisionDismatch, - GameDismatch, InputCountDismatch, Invalid, }; @@ -65,7 +64,7 @@ public: /// Prepare to override the clock before recording movies void PrepareForRecording(); - ValidationResult ValidateMovie(const std::string& movie_file, u64 program_id = 0) const; + ValidationResult ValidateMovie(const std::string& movie_file) const; /// Get the init time that would override the one in the settings u64 GetOverrideInitTime() const; @@ -148,7 +147,7 @@ private: void Record(const Service::IR::PadState& pad_state, const s16& c_stick_x, const s16& c_stick_y); void Record(const Service::IR::ExtraHIDResponse& extra_hid_response); - ValidationResult ValidateHeader(const CTMHeader& header, u64 program_id = 0) const; + ValidationResult ValidateHeader(const CTMHeader& header) const; ValidationResult ValidateInput(const std::vector& input, u64 expected_count) const; void SaveMovie(); From 5a42a80f408c0598d3fb59d7a4213370f362fbb4 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Tue, 7 Jul 2020 16:38:24 +0800 Subject: [PATCH 04/14] core/movie: Allow setting a playback completion callback Instead of specifying it when starting playback. This is necessary as you can end up playing the movie even if you started as Recording (for example, loading a state in R/O mode will switch to Playing mode) --- src/core/movie.cpp | 8 +++++--- src/core/movie.h | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/core/movie.cpp b/src/core/movie.cpp index 2be5dfd27..51746e744 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -486,8 +486,11 @@ void Movie::SaveMovie() { } } -void Movie::StartPlayback(const std::string& movie_file, - std::function completion_callback) { +void Movie::SetPlaybackCompletionCallback(std::function completion_callback) { + playback_completion_callback = completion_callback; +} + +void Movie::StartPlayback(const std::string& movie_file) { LOG_INFO(Movie, "Loading Movie for playback"); FileUtil::IOFile save_record(movie_file, "rb"); const u64 size = save_record.GetSize(); @@ -510,7 +513,6 @@ void Movie::StartPlayback(const std::string& movie_file, current_byte = 0; id = header.id; - playback_completion_callback = completion_callback; LOG_INFO(Movie, "Loaded Movie, ID: {:016X}", id); } diff --git a/src/core/movie.h b/src/core/movie.h index 09912c3f8..dcbe8129b 100644 --- a/src/core/movie.h +++ b/src/core/movie.h @@ -42,8 +42,8 @@ public: return s_instance; } - void StartPlayback(const std::string& movie_file, - std::function completion_callback = [] {}); + void SetPlaybackCompletionCallback(std::function completion_callback); + void StartPlayback(const std::string& movie_file); void StartRecording(const std::string& movie_file, const std::string& author); /** @@ -165,7 +165,7 @@ private: u32 rerecord_count = 1; bool read_only = true; - std::function playback_completion_callback; + std::function playback_completion_callback = [] {}; template void serialize(Archive& ar, const unsigned int file_version); From 113e0c7331780ad2e975bf43c9b9c1848f67b9e5 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Mon, 6 Jul 2020 21:44:17 +0800 Subject: [PATCH 05/14] citra_qt: Rebuilt movie frontend This is completely rebuilt, in order to allow setting author, displaying movie metadata, and toggling read-only mode. The UX is changed to more closely match other emulators' behaviour. Now you can only record/play from start/reset (In the future, we might want to introduce 'record from savestate') Also fixed a critical bug where movie file can be corrupted when ending the recording while game is still running. --- src/citra_qt/CMakeLists.txt | 6 + src/citra_qt/game_list.cpp | 10 +- src/citra_qt/game_list.h | 4 +- src/citra_qt/main.cpp | 167 ++++++--------------- src/citra_qt/main.h | 4 +- src/citra_qt/main.ui | 34 +++-- src/citra_qt/movie/movie_play_dialog.cpp | 130 ++++++++++++++++ src/citra_qt/movie/movie_play_dialog.h | 30 ++++ src/citra_qt/movie/movie_play_dialog.ui | 136 +++++++++++++++++ src/citra_qt/movie/movie_record_dialog.cpp | 61 ++++++++ src/citra_qt/movie/movie_record_dialog.h | 27 ++++ src/citra_qt/movie/movie_record_dialog.ui | 71 +++++++++ src/core/hle/service/hid/hid.cpp | 6 - src/core/hle/service/hid/hid.h | 6 + 14 files changed, 541 insertions(+), 151 deletions(-) create mode 100644 src/citra_qt/movie/movie_play_dialog.cpp create mode 100644 src/citra_qt/movie/movie_play_dialog.h create mode 100644 src/citra_qt/movie/movie_play_dialog.ui create mode 100644 src/citra_qt/movie/movie_record_dialog.cpp create mode 100644 src/citra_qt/movie/movie_record_dialog.h create mode 100644 src/citra_qt/movie/movie_record_dialog.ui diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 24c2ca601..320e727a1 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -125,6 +125,12 @@ add_executable(citra-qt main.cpp main.h main.ui + movie/movie_play_dialog.cpp + movie/movie_play_dialog.h + movie/movie_play_dialog.ui + movie/movie_record_dialog.cpp + movie/movie_record_dialog.h + movie/movie_record_dialog.ui multiplayer/chat_room.cpp multiplayer/chat_room.h multiplayer/chat_room.ui diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index d75df2859..613839807 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -722,17 +722,17 @@ void GameList::RefreshGameDirectory() { } } -QString GameList::FindGameByProgramID(u64 program_id) { - return FindGameByProgramID(item_model->invisibleRootItem(), program_id); +QString GameList::FindGameByProgramID(u64 program_id, int role) { + return FindGameByProgramID(item_model->invisibleRootItem(), program_id, role); } -QString GameList::FindGameByProgramID(QStandardItem* current_item, u64 program_id) { +QString GameList::FindGameByProgramID(QStandardItem* current_item, u64 program_id, int role) { if (current_item->type() == static_cast(GameListItemType::Game) && current_item->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) { - return current_item->data(GameListItemPath::FullPathRole).toString(); + return current_item->data(role).toString(); } else if (current_item->hasChildren()) { for (int child_id = 0; child_id < current_item->rowCount(); child_id++) { - QString path = FindGameByProgramID(current_item->child(child_id, 0), program_id); + QString path = FindGameByProgramID(current_item->child(child_id, 0), program_id, role); if (!path.isEmpty()) return path; } diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h index 8383f9aaf..e76c0edee 100644 --- a/src/citra_qt/game_list.h +++ b/src/citra_qt/game_list.h @@ -70,7 +70,7 @@ public: QStandardItemModel* GetModel() const; - QString FindGameByProgramID(u64 program_id); + QString FindGameByProgramID(u64 program_id, int role); void RefreshGameDirectory(); @@ -105,7 +105,7 @@ private: void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); - QString FindGameByProgramID(QStandardItem* current_item, u64 program_id); + QString FindGameByProgramID(QStandardItem* current_item, u64 program_id, int role); GameListSearchField* search_field; GMainWindow* main_window = nullptr; diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index c8639f616..b3242bcef 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -51,6 +51,8 @@ #include "citra_qt/hotkeys.h" #include "citra_qt/loading_screen.h" #include "citra_qt/main.h" +#include "citra_qt/movie/movie_play_dialog.h" +#include "citra_qt/movie/movie_record_dialog.h" #include "citra_qt/multiplayer/state.h" #include "citra_qt/qt_image_interface.h" #include "citra_qt/uisettings.h" @@ -174,6 +176,9 @@ GMainWindow::GMainWindow() Network::Init(); + Core::Movie::GetInstance().SetPlaybackCompletionCallback( + [this] { QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted"); }); + InitializeWidgets(); InitializeDebugWidgets(); InitializeRecentFileMenuActions(); @@ -742,8 +747,9 @@ void GMainWindow::ConnectMenuEvents() { // Movie connect(ui->action_Record_Movie, &QAction::triggered, this, &GMainWindow::OnRecordMovie); connect(ui->action_Play_Movie, &QAction::triggered, this, &GMainWindow::OnPlayMovie); - connect(ui->action_Stop_Recording_Playback, &QAction::triggered, this, - &GMainWindow::OnStopRecordingPlayback); + connect(ui->action_Close_Movie, &QAction::triggered, this, &GMainWindow::OnCloseMovie); + connect(ui->action_Movie_Read_Only_Mode, &QAction::toggled, this, + [this](bool checked) { Core::Movie::GetInstance().SetReadOnly(checked); }); connect(ui->action_Enable_Frame_Advancing, &QAction::triggered, this, [this] { if (emulation_running) { Core::System::GetInstance().frame_limiter.SetFrameAdvancing( @@ -1105,7 +1111,7 @@ void GMainWindow::ShutdownGame() { AllowOSSleep(); discord_rpc->Pause(); - OnStopRecordingPlayback(); + OnCloseMovie(true); emu_thread->RequestStop(); // Release emu threads from any breakpoints @@ -1534,9 +1540,11 @@ void GMainWindow::OnStartGame() { Camera::QtMultimediaCameraHandler::ResumeCameras(); if (movie_record_on_start) { - Core::Movie::GetInstance().StartRecording(movie_record_path.toStdString()); + Core::Movie::GetInstance().StartRecording(movie_record_path.toStdString(), + movie_record_author.toStdString()); movie_record_on_start = false; movie_record_path.clear(); + movie_record_author.clear(); } PreventOSSleep(); @@ -1839,144 +1847,63 @@ void GMainWindow::OnCreateGraphicsSurfaceViewer() { } void GMainWindow::OnRecordMovie() { - if (emulation_running) { - QMessageBox::StandardButton answer = QMessageBox::warning( - this, tr("Record Movie"), - tr("To keep consistency with the RNG, it is recommended to record the movie from game " - "start.
Are you sure you still want to record movies now?"), - QMessageBox::Yes | QMessageBox::No); - if (answer == QMessageBox::No) - return; - } - const QString path = - QFileDialog::getSaveFileName(this, tr("Record Movie"), UISettings::values.movie_record_path, - tr("Citra TAS Movie (*.ctm)")); - if (path.isEmpty()) + MovieRecordDialog dialog(this); + if (dialog.exec() != QDialog::Accepted) { return; - UISettings::values.movie_record_path = QFileInfo(path).path(); + } + if (emulation_running) { - Core::Movie::GetInstance().StartRecording(path.toStdString()); + // Restart game + BootGame(QString(game_path)); + Core::Movie::GetInstance().StartRecording(dialog.GetPath().toStdString(), + dialog.GetAuthor().toStdString()); } else { movie_record_on_start = true; - movie_record_path = path; - QMessageBox::information(this, tr("Record Movie"), - tr("Recording will start once you boot a game.")); + movie_record_path = dialog.GetPath(); + movie_record_author = dialog.GetAuthor(); } - ui->action_Record_Movie->setEnabled(false); - ui->action_Play_Movie->setEnabled(false); - ui->action_Stop_Recording_Playback->setEnabled(true); -} - -bool GMainWindow::ValidateMovie(const QString& path, u64 program_id) { - using namespace Core; - Movie::ValidationResult result = - Core::Movie::GetInstance().ValidateMovie(path.toStdString(), program_id); - const QString revision_dismatch_text = - tr("The movie file you are trying to load was created on a different revision of Citra." - "
Citra has had some changes during the time, and the playback may desync or not " - "work as expected." - "

Are you sure you still want to load the movie file?"); - const QString game_dismatch_text = - tr("The movie file you are trying to load was recorded with a different game." - "
The playback may not work as expected, and it may cause unexpected results." - "

Are you sure you still want to load the movie file?"); - const QString invalid_movie_text = - tr("The movie file you are trying to load is invalid." - "
Either the file is corrupted, or Citra has had made some major changes to the " - "Movie module." - "
Please choose a different movie file and try again."); - int answer; - switch (result) { - case Movie::ValidationResult::RevisionDismatch: - answer = QMessageBox::question(this, tr("Revision Dismatch"), revision_dismatch_text, - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - if (answer != QMessageBox::Yes) - return false; - break; - case Movie::ValidationResult::GameDismatch: - answer = QMessageBox::question(this, tr("Game Dismatch"), game_dismatch_text, - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - if (answer != QMessageBox::Yes) - return false; - break; - case Movie::ValidationResult::Invalid: - QMessageBox::critical(this, tr("Invalid Movie File"), invalid_movie_text); - return false; - default: - break; - } - return true; + ui->action_Close_Movie->setEnabled(true); } void GMainWindow::OnPlayMovie() { - if (emulation_running) { - QMessageBox::StandardButton answer = QMessageBox::warning( - this, tr("Play Movie"), - tr("To keep consistency with the RNG, it is recommended to play the movie from game " - "start.
Are you sure you still want to play movies now?"), - QMessageBox::Yes | QMessageBox::No); - if (answer == QMessageBox::No) - return; - } - - const QString path = - QFileDialog::getOpenFileName(this, tr("Play Movie"), UISettings::values.movie_playback_path, - tr("Citra TAS Movie (*.ctm)")); - if (path.isEmpty()) + MoviePlayDialog dialog(this, game_list); + if (dialog.exec() != QDialog::Accepted) { return; - UISettings::values.movie_playback_path = QFileInfo(path).path(); - - if (emulation_running) { - if (!ValidateMovie(path)) - return; - } else { - const QString invalid_movie_text = - tr("The movie file you are trying to load is invalid." - "
Either the file is corrupted, or Citra has had made some major changes to the " - "Movie module." - "
Please choose a different movie file and try again."); - u64 program_id = Core::Movie::GetInstance().GetMovieProgramID(path.toStdString()); - if (!program_id) { - QMessageBox::critical(this, tr("Invalid Movie File"), invalid_movie_text); - return; - } - QString game_path = game_list->FindGameByProgramID(program_id); - if (game_path.isEmpty()) { - QMessageBox::warning(this, tr("Game Not Found"), - tr("The movie you are trying to play is from a game that is not " - "in the game list. If you own the game, please add the game " - "folder to the game list and try to play the movie again.")); - return; - } - if (!ValidateMovie(path, program_id)) - return; - Core::Movie::GetInstance().PrepareForPlayback(path.toStdString()); - BootGame(game_path); } - Core::Movie::GetInstance().StartPlayback(path.toStdString(), [this] { - QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted"); - }); - ui->action_Record_Movie->setEnabled(false); - ui->action_Play_Movie->setEnabled(false); - ui->action_Stop_Recording_Playback->setEnabled(true); + + const auto movie_path = dialog.GetMoviePath().toStdString(); + Core::Movie::GetInstance().PrepareForPlayback(movie_path); + BootGame(dialog.GetGamePath()); + + Core::Movie::GetInstance().StartPlayback(movie_path); + ui->action_Close_Movie->setEnabled(true); } -void GMainWindow::OnStopRecordingPlayback() { +void GMainWindow::OnCloseMovie(bool shutting_down) { if (movie_record_on_start) { QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled.")); movie_record_on_start = false; movie_record_path.clear(); + movie_record_author.clear(); } else { + const bool was_running = !shutting_down && emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + const bool was_recording = Core::Movie::GetInstance().IsRecordingInput(); Core::Movie::GetInstance().Shutdown(); if (was_recording) { QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved.")); } + + if (was_running) { + OnStartGame(); + } } - ui->action_Record_Movie->setEnabled(true); - ui->action_Play_Movie->setEnabled(true); - ui->action_Stop_Recording_Playback->setEnabled(false); + + ui->action_Close_Movie->setEnabled(false); } void GMainWindow::OnCaptureScreenshot() { @@ -2345,9 +2272,7 @@ void GMainWindow::OnLanguageChanged(const QString& locale) { void GMainWindow::OnMoviePlaybackCompleted() { QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed.")); - ui->action_Record_Movie->setEnabled(true); - ui->action_Play_Movie->setEnabled(true); - ui->action_Stop_Recording_Playback->setEnabled(false); + ui->action_Close_Movie->setEnabled(false); } void GMainWindow::UpdateWindowTitle() { diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 241365a38..eff5ea7b5 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -208,7 +208,7 @@ private slots: void OnCreateGraphicsSurfaceViewer(); void OnRecordMovie(); void OnPlayMovie(); - void OnStopRecordingPlayback(); + void OnCloseMovie(bool shutting_down = false); void OnCaptureScreenshot(); #ifdef ENABLE_FFMPEG_VIDEO_DUMPER void OnStartVideoDumping(); @@ -224,7 +224,6 @@ private slots: void OnMouseActivity(); private: - bool ValidateMovie(const QString& path, u64 program_id = 0); Q_INVOKABLE void OnMoviePlaybackCompleted(); void UpdateStatusBar(); void LoadTranslation(); @@ -267,6 +266,7 @@ private: // Movie bool movie_record_on_start = false; QString movie_record_path; + QString movie_record_author; // Video dumping bool video_dumping_on_start = false; diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index 2eff98083..5d2d4f0ca 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -163,7 +163,9 @@ - + + + @@ -318,27 +320,29 @@ - - true - - Record Movie + Record... - + + Play... + + + + + Close + + + + + true + + true - Play Movie - - - - - false - - - Stop Recording / Playback + Read-Only Mode diff --git a/src/citra_qt/movie/movie_play_dialog.cpp b/src/citra_qt/movie/movie_play_dialog.cpp new file mode 100644 index 000000000..bfb38341a --- /dev/null +++ b/src/citra_qt/movie/movie_play_dialog.cpp @@ -0,0 +1,130 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "citra_qt/game_list.h" +#include "citra_qt/game_list_p.h" +#include "citra_qt/movie/movie_play_dialog.h" +#include "citra_qt/uisettings.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/hle/service/hid/hid.h" +#include "core/movie.h" +#include "ui_movie_play_dialog.h" + +MoviePlayDialog::MoviePlayDialog(QWidget* parent, GameList* game_list_) + : QDialog(parent), ui(std::make_unique()), game_list(game_list_) { + ui->setupUi(this); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + connect(ui->filePathButton, &QToolButton::clicked, this, &MoviePlayDialog::OnToolButtonClicked); + connect(ui->filePath, &QLineEdit::editingFinished, this, &MoviePlayDialog::UpdateUIDisplay); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &MoviePlayDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &MoviePlayDialog::reject); + + if (Core::System::GetInstance().IsPoweredOn()) { + QString note_text; + note_text = tr("Current running game will be stopped."); + if (Core::Movie::GetInstance().IsRecordingInput()) { + note_text.append(tr("
Current recording will be discarded.")); + } + ui->note2Label->setText(note_text); + } +} + +MoviePlayDialog::~MoviePlayDialog() = default; + +QString MoviePlayDialog::GetMoviePath() const { + return ui->filePath->text(); +} + +QString MoviePlayDialog::GetGamePath() const { + const auto metadata = Core::Movie::GetInstance().GetMovieMetadata(GetMoviePath().toStdString()); + return game_list->FindGameByProgramID(metadata.program_id, GameListItemPath::FullPathRole); +} + +void MoviePlayDialog::OnToolButtonClicked() { + const QString path = + QFileDialog::getOpenFileName(this, tr("Play Movie"), UISettings::values.movie_playback_path, + tr("Citra TAS Movie (*.ctm)")); + if (path.isEmpty()) { + return; + } + ui->filePath->setText(path); + UISettings::values.movie_playback_path = path; + UpdateUIDisplay(); +} + +void MoviePlayDialog::UpdateUIDisplay() { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + ui->gameLineEdit->clear(); + ui->authorLineEdit->clear(); + ui->rerecordCountLineEdit->clear(); + ui->lengthLineEdit->clear(); + ui->note1Label->setVisible(true); + + const auto path = GetMoviePath().toStdString(); + + const auto validation_result = Core::Movie::GetInstance().ValidateMovie(path); + if (validation_result == Core::Movie::ValidationResult::Invalid) { + ui->note1Label->setText(tr("Invalid movie file.")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + return; + } + + ui->note2Label->setVisible(true); + ui->infoGroupBox->setVisible(true); + + switch (validation_result) { + case Core::Movie::ValidationResult::OK: + ui->note1Label->setText(QString{}); + break; + case Core::Movie::ValidationResult::RevisionDismatch: + ui->note1Label->setText(tr("Revision dismatch, playback may desync.")); + break; + case Core::Movie::ValidationResult::InputCountDismatch: + ui->note1Label->setText(tr("Indicated length is incorrect, file may be corrupted.")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + break; + default: + UNREACHABLE(); + } + + const auto metadata = Core::Movie::GetInstance().GetMovieMetadata(path); + + // Format game title + const auto title = + game_list->FindGameByProgramID(metadata.program_id, GameListItemPath::TitleRole); + if (title.isEmpty()) { + ui->gameLineEdit->setText(tr("(unknown)")); + ui->note1Label->setText(tr("Game used in this movie is not in game list.")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + } else { + ui->gameLineEdit->setText(title); + } + + ui->authorLineEdit->setText(metadata.author.empty() ? tr("(unknown)") + : QString::fromStdString(metadata.author)); + ui->rerecordCountLineEdit->setText( + metadata.rerecord_count == 0 ? tr("(unknown)") : QString::number(metadata.rerecord_count)); + + // Format length + if (metadata.input_count == 0) { + ui->lengthLineEdit->setText(tr("(unknown)")); + } else { + if (metadata.input_count > + BASE_CLOCK_RATE_ARM11 * 24 * 60 * 60 / Service::HID::Module::pad_update_ticks) { + // More than a day + ui->lengthLineEdit->setText(tr("(>1 day)")); + } else { + const u64 msecs = Service::HID::Module::pad_update_ticks * metadata.input_count * 1000 / + BASE_CLOCK_RATE_ARM11; + ui->lengthLineEdit->setText( + QTime::fromMSecsSinceStartOfDay(msecs).toString(QStringLiteral("hh:mm:ss.zzz"))); + } + } +} diff --git a/src/citra_qt/movie/movie_play_dialog.h b/src/citra_qt/movie/movie_play_dialog.h new file mode 100644 index 000000000..dc4f344a5 --- /dev/null +++ b/src/citra_qt/movie/movie_play_dialog.h @@ -0,0 +1,30 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include + +class GameList; + +namespace Ui { +class MoviePlayDialog; +} + +class MoviePlayDialog : public QDialog { + Q_OBJECT + +public: + explicit MoviePlayDialog(QWidget* parent, GameList* game_list); + ~MoviePlayDialog() override; + + QString GetMoviePath() const; + QString GetGamePath() const; + +private: + void OnToolButtonClicked(); + void UpdateUIDisplay(); + + std::unique_ptr ui; + GameList* game_list; +}; diff --git a/src/citra_qt/movie/movie_play_dialog.ui b/src/citra_qt/movie/movie_play_dialog.ui new file mode 100644 index 000000000..ad9b595cd --- /dev/null +++ b/src/citra_qt/movie/movie_play_dialog.ui @@ -0,0 +1,136 @@ + + + MoviePlayDialog + + + + 0 + 0 + 600 + 100 + + + + Play Movie + + + + + + + + File: + + + + + + + + + + ... + + + + + + + + + false + + + + + + + Info + + + false + + + + + + Game: + + + + + + + true + + + + + + + Author: + + + + + + + true + + + + + + + Rerecord Count: + + + + + + + true + + + + + + + Length: + + + + + + + true + + + + + + + + + + Qt::Vertical + + + + + + + false + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + diff --git a/src/citra_qt/movie/movie_record_dialog.cpp b/src/citra_qt/movie/movie_record_dialog.cpp new file mode 100644 index 000000000..6f0954909 --- /dev/null +++ b/src/citra_qt/movie/movie_record_dialog.cpp @@ -0,0 +1,61 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "citra_qt/movie/movie_record_dialog.h" +#include "citra_qt/uisettings.h" +#include "core/core.h" +#include "core/movie.h" +#include "ui_movie_record_dialog.h" + +MovieRecordDialog::MovieRecordDialog(QWidget* parent) + : QDialog(parent), ui(std::make_unique()) { + ui->setupUi(this); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + connect(ui->filePathButton, &QToolButton::clicked, this, + &MovieRecordDialog::OnToolButtonClicked); + connect(ui->filePath, &QLineEdit::editingFinished, this, &MovieRecordDialog::UpdateUIDisplay); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &MovieRecordDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &MovieRecordDialog::reject); + + QString note_text; + if (Core::System::GetInstance().IsPoweredOn()) { + note_text = tr("Current running game will be restarted."); + if (Core::Movie::GetInstance().IsRecordingInput()) { + note_text.append(tr("
Current recording will be discarded.")); + } + } else { + note_text = tr("Recording will start once you boot a game."); + } + ui->noteLabel->setText(note_text); +} + +MovieRecordDialog::~MovieRecordDialog() = default; + +QString MovieRecordDialog::GetPath() const { + return ui->filePath->text(); +} + +QString MovieRecordDialog::GetAuthor() const { + return ui->authorLineEdit->text(); +} + +void MovieRecordDialog::OnToolButtonClicked() { + const QString path = + QFileDialog::getSaveFileName(this, tr("Record Movie"), UISettings::values.movie_record_path, + tr("Citra TAS Movie (*.ctm)")); + if (path.isEmpty()) { + return; + } + ui->filePath->setText(path); + UISettings::values.movie_record_path = path; + UpdateUIDisplay(); +} + +void MovieRecordDialog::UpdateUIDisplay() { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!ui->filePath->text().isEmpty()); +} diff --git a/src/citra_qt/movie/movie_record_dialog.h b/src/citra_qt/movie/movie_record_dialog.h new file mode 100644 index 000000000..c91f1f414 --- /dev/null +++ b/src/citra_qt/movie/movie_record_dialog.h @@ -0,0 +1,27 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include + +namespace Ui { +class MovieRecordDialog; +} + +class MovieRecordDialog : public QDialog { + Q_OBJECT + +public: + explicit MovieRecordDialog(QWidget* parent); + ~MovieRecordDialog() override; + + QString GetPath() const; + QString GetAuthor() const; + +private: + void OnToolButtonClicked(); + void UpdateUIDisplay(); + + std::unique_ptr ui; +}; diff --git a/src/citra_qt/movie/movie_record_dialog.ui b/src/citra_qt/movie/movie_record_dialog.ui new file mode 100644 index 000000000..96298b8e4 --- /dev/null +++ b/src/citra_qt/movie/movie_record_dialog.ui @@ -0,0 +1,71 @@ + + + MovieRecordDialog + + + + 0 + 0 + 600 + 150 + + + + Record Movie + + + + + + + + File: + + + + + + + + + + ... + + + + + + + Author: + + + + + + + 32 + + + + + + + + + Qt::Vertical + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + diff --git a/src/core/hle/service/hid/hid.cpp b/src/core/hle/service/hid/hid.cpp index c3034b824..c59826551 100644 --- a/src/core/hle/service/hid/hid.cpp +++ b/src/core/hle/service/hid/hid.cpp @@ -12,7 +12,6 @@ #include "common/logging/log.h" #include "core/3ds.h" #include "core/core.h" -#include "core/core_timing.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/event.h" #include "core/hle/kernel/handle_table.h" @@ -55,11 +54,6 @@ void Module::serialize(Archive& ar, const unsigned int file_version) { } SERIALIZE_IMPL(Module) -// Updating period for each HID device. These empirical values are measured from a 11.2 3DS. -constexpr u64 pad_update_ticks = BASE_CLOCK_RATE_ARM11 / 234; -constexpr u64 accelerometer_update_ticks = BASE_CLOCK_RATE_ARM11 / 104; -constexpr u64 gyroscope_update_ticks = BASE_CLOCK_RATE_ARM11 / 101; - constexpr float accelerometer_coef = 512.0f; // measured from hw test result constexpr float gyroscope_coef = 14.375f; // got from hwtest GetGyroscopeLowRawToDpsCoefficient call diff --git a/src/core/hle/service/hid/hid.h b/src/core/hle/service/hid/hid.h index bdd106018..b364c4be8 100644 --- a/src/core/hle/service/hid/hid.h +++ b/src/core/hle/service/hid/hid.h @@ -13,6 +13,7 @@ #include "common/bit_field.h" #include "common/common_funcs.h" #include "common/common_types.h" +#include "core/core_timing.h" #include "core/frontend/input.h" #include "core/hle/service/service.h" #include "core/settings.h" @@ -299,6 +300,11 @@ public: const PadState& GetState() const; + // Updating period for each HID device. These empirical values are measured from a 11.2 3DS. + static constexpr u64 pad_update_ticks = BASE_CLOCK_RATE_ARM11 / 234; + static constexpr u64 accelerometer_update_ticks = BASE_CLOCK_RATE_ARM11 / 104; + static constexpr u64 gyroscope_update_ticks = BASE_CLOCK_RATE_ARM11 / 101; + private: void LoadInputDevices(); void UpdatePadCallback(u64 userdata, s64 cycles_late); From fb14bd956a98fe25bf25c6b9c07e03344c207874 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Tue, 7 Jul 2020 00:15:26 +0800 Subject: [PATCH 06/14] citra_qt: Add indicator in status bar Since we do not have an overlay yet, it can be confusing whether movie is being recorded or played. This makes it clear. Status messages (e.g. system archive missing) will be overriden, but that shouldn't be too important when recording movies. Doubled the status bar updating frequency to provide a better experience. It now updates every second. --- src/citra_qt/main.cpp | 21 +++++++++++++++++- src/citra_qt/main.h | 1 + src/core/movie.cpp | 50 +++++++++++++++++++++++++++++-------------- src/core/movie.h | 6 ++++++ 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index b3242bcef..d9612fce8 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -1055,7 +1055,7 @@ void GMainWindow::BootGame(const QString& filename) { game_list->hide(); game_list_placeholder->hide(); } - status_bar_update_timer.start(2000); + status_bar_update_timer.start(1000); if (UISettings::values.hide_mouse) { mouse_hide_timer.start(); @@ -1165,6 +1165,7 @@ void GMainWindow::ShutdownGame() { // Disable status bar updates status_bar_update_timer.stop(); message_label->setVisible(false); + message_label_used_for_movie = false; emu_speed_label->setVisible(false); game_fps_label->setVisible(false); emu_frametime_label->setVisible(false); @@ -1982,6 +1983,23 @@ void GMainWindow::UpdateStatusBar() { return; } + // Update movie status + const u64 current = Core::Movie::GetInstance().GetCurrentInputIndex(); + const u64 total = Core::Movie::GetInstance().GetTotalInputCount(); + if (Core::Movie::GetInstance().IsRecordingInput()) { + message_label->setText(tr("Recording %1").arg(current)); + message_label->setVisible(true); + message_label_used_for_movie = true; + } else if (Core::Movie::GetInstance().IsPlayingInput()) { + message_label->setText(tr("Playing %1 / %2").arg(current).arg(total)); + message_label->setVisible(true); + message_label_used_for_movie = true; + } else if (message_label_used_for_movie) { // Clear the label if movie was just closed + message_label->setText(QString{}); + message_label->setVisible(false); + message_label_used_for_movie = false; + } + auto results = Core::System::GetInstance().GetAndResetPerfStats(); if (Settings::values.use_frame_limit_alternate) { @@ -2093,6 +2111,7 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det emu_thread->SetRunning(true); message_label->setText(status_message); message_label->setVisible(true); + message_label_used_for_movie = false; } } } diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index eff5ea7b5..99451d308 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -248,6 +248,7 @@ private: QLabel* game_fps_label = nullptr; QLabel* emu_frametime_label = nullptr; QTimer status_bar_update_timer; + bool message_label_used_for_movie = false; MultiplayerState* multiplayer_state = nullptr; std::unique_ptr config; diff --git a/src/core/movie.cpp b/src/core/movie.cpp index 51746e744..1e27b45f2 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -129,6 +129,22 @@ struct CTMHeader { static_assert(sizeof(CTMHeader) == 256, "CTMHeader should be 256 bytes"); #pragma pack(pop) +static u64 GetInputCount(const std::vector& input) { + u64 input_count = 0; + for (std::size_t pos = 0; pos < input.size(); pos += sizeof(ControllerState)) { + if (input.size() < pos + sizeof(ControllerState)) { + break; + } + + ControllerState state; + std::memcpy(&state, input.data() + pos, sizeof(ControllerState)); + if (state.type == ControllerStateType::PadAndCircle) { + input_count++; + } + } + return input_count; +} + template void Movie::serialize(Archive& ar, const unsigned int file_version) { // Only serialize what's needed to make savestates useful for TAS: @@ -136,6 +152,10 @@ void Movie::serialize(Archive& ar, const unsigned int file_version) { ar& _current_byte; current_byte = static_cast(_current_byte); + if (file_version > 0) { + ar& current_input; + } + std::vector recorded_input_ = recorded_input; ar& recorded_input_; @@ -167,6 +187,7 @@ void Movie::serialize(Archive& ar, const unsigned int file_version) { "This savestate was created at a later point and must be loaded in R+W mode"); } play_mode = PlayMode::Playing; + total_input = GetInputCount(recorded_input); } else { recorded_input = std::move(recorded_input_); play_mode = PlayMode::Recording; @@ -184,6 +205,13 @@ bool Movie::IsRecordingInput() const { return play_mode == PlayMode::Recording; } +u64 Movie::GetCurrentInputIndex() const { + return current_input; +} +u64 Movie::GetTotalInputCount() const { + return total_input; +} + void Movie::CheckInputEnd() { if (current_byte + sizeof(ControllerState) > recorded_input.size()) { LOG_INFO(Movie, "Playback finished"); @@ -198,6 +226,7 @@ void Movie::Play(Service::HID::PadState& pad_state, s16& circle_pad_x, s16& circ ControllerState s; std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState)); current_byte += sizeof(ControllerState); + current_input++; if (s.type != ControllerStateType::PadAndCircle) { LOG_ERROR(Movie, @@ -325,6 +354,8 @@ void Movie::Record(const ControllerState& controller_state) { void Movie::Record(const Service::HID::PadState& pad_state, const s16& circle_pad_x, const s16& circle_pad_y) { + current_input++; + ControllerState s; s.type = ControllerStateType::PadAndCircle; @@ -429,22 +460,6 @@ Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header) const { return ValidationResult::OK; } -static u64 GetInputCount(const std::vector& input) { - u64 input_count = 0; - for (std::size_t pos = 0; pos < input.size(); pos += sizeof(ControllerState)) { - if (input.size() < pos + sizeof(ControllerState)) { - break; - } - - ControllerState state; - std::memcpy(&state, input.data() + pos, sizeof(ControllerState)); - if (state.type == ControllerStateType::PadAndCircle) { - input_count++; - } - } - return input_count; -} - Movie::ValidationResult Movie::ValidateInput(const std::vector& input, u64 expected_count) const { return GetInputCount(input) == expected_count ? ValidationResult::OK @@ -507,11 +522,13 @@ void Movie::StartPlayback(const std::string& movie_file) { record_movie_author = author.data(); rerecord_count = header.rerecord_count; + total_input = header.input_count; recorded_input.resize(size - sizeof(CTMHeader)); save_record.ReadArray(recorded_input.data(), recorded_input.size()); current_byte = 0; + current_input = 0; id = header.id; LOG_INFO(Movie, "Loaded Movie, ID: {:016X}", id); @@ -622,6 +639,7 @@ void Movie::Shutdown() { recorded_input.resize(0); record_movie_file.clear(); current_byte = 0; + current_input = 0; init_time = 0; id = 0; } diff --git a/src/core/movie.h b/src/core/movie.h index dcbe8129b..e62dfe5c2 100644 --- a/src/core/movie.h +++ b/src/core/movie.h @@ -123,6 +123,9 @@ public: bool IsPlayingInput() const; bool IsRecordingInput() const; + u64 GetCurrentInputIndex() const; + u64 GetTotalInputCount() const; + private: static Movie s_instance; @@ -159,6 +162,9 @@ private: std::vector recorded_input; std::size_t current_byte = 0; + u64 current_input = 0; + // Total input count of the current movie being played. Not used for recording. + u64 total_input = 0; u64 id = 0; // ID of the current movie loaded u64 init_time; From b6f8cc884f5dfb90fb9ea3c564ca5b6f24780352 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Tue, 7 Jul 2020 19:24:44 +0800 Subject: [PATCH 07/14] citra: Update SDL frontend --- src/citra/citra.cpp | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index a1455d373..fc7e12718 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -66,6 +66,7 @@ static void PrintHelp(const char* argv0) { "-m, --multiplayer=nick:password@address:port" " Nickname, password, address and port for multiplayer\n" "-r, --movie-record=[file] Record a movie (game inputs) to the given file\n" + "-a, --movie-record-author=AUTHOR Sets the author of the movie to be recorded\n" "-p, --movie-play=[file] Playback the movie (game inputs) from the given file\n" "-d, --dump-video=[file] Dumps audio and video to the given video file\n" "-f, --fullscreen Start in fullscreen mode\n" @@ -192,6 +193,7 @@ int main(int argc, char** argv) { bool use_gdbstub = Settings::values.use_gdbstub; u32 gdb_port = static_cast(Settings::values.gdbstub_port); std::string movie_record; + std::string movie_record_author; std::string movie_play; std::string dump_video; @@ -217,11 +219,17 @@ int main(int argc, char** argv) { u16 port = Network::DefaultRoomPort; static struct option long_options[] = { - {"gdbport", required_argument, 0, 'g'}, {"install", required_argument, 0, 'i'}, - {"multiplayer", required_argument, 0, 'm'}, {"movie-record", required_argument, 0, 'r'}, - {"movie-play", required_argument, 0, 'p'}, {"dump-video", required_argument, 0, 'd'}, - {"fullscreen", no_argument, 0, 'f'}, {"help", no_argument, 0, 'h'}, - {"version", no_argument, 0, 'v'}, {0, 0, 0, 0}, + {"gdbport", required_argument, 0, 'g'}, + {"install", required_argument, 0, 'i'}, + {"multiplayer", required_argument, 0, 'm'}, + {"movie-record", required_argument, 0, 'r'}, + {"movie-record-author", required_argument, 0, 'a'}, + {"movie-play", required_argument, 0, 'p'}, + {"dump-video", required_argument, 0, 'd'}, + {"fullscreen", no_argument, 0, 'f'}, + {"help", no_argument, 0, 'h'}, + {"version", no_argument, 0, 'v'}, + {0, 0, 0, 0}, }; while (optind < argc) { @@ -285,6 +293,9 @@ int main(int argc, char** argv) { case 'r': movie_record = optarg; break; + case 'a': + movie_record_author = optarg; + break; case 'p': movie_play = optarg; break; @@ -401,10 +412,14 @@ int main(int argc, char** argv) { } if (!movie_play.empty()) { + auto metadata = Core::Movie::GetInstance().GetMovieMetadata(movie_play); + LOG_INFO(Movie, "Author: {}", metadata.author); + LOG_INFO(Movie, "Rerecord count: {}", metadata.rerecord_count); + LOG_INFO(Movie, "Input count: {}", metadata.input_count); Core::Movie::GetInstance().StartPlayback(movie_play); } if (!movie_record.empty()) { - Core::Movie::GetInstance().StartRecording(movie_record); + Core::Movie::GetInstance().StartRecording(movie_record, movie_record_author); } if (!dump_video.empty()) { Layout::FramebufferLayout layout{ From e188f86582fb50d2343118254f932c162dc94fe9 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 8 Jul 2020 21:24:50 +0800 Subject: [PATCH 08/14] core/movie: Add future event and timeline check As specified in TASVideos [Laws of TAS](http://tasvideos.org/LawsOfTAS/OnSavestates.html) --- src/core/movie.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/movie.cpp b/src/core/movie.cpp index 1e27b45f2..0c90950b4 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include #include @@ -182,10 +183,16 @@ void Movie::serialize(Archive& ar, const unsigned int file_version) { if (play_mode == PlayMode::Recording) { SaveMovie(); } - if (current_byte >= recorded_input.size()) { - throw std::runtime_error( - "This savestate was created at a later point and must be loaded in R+W mode"); + if (recorded_input_.size() >= recorded_input.size()) { + throw std::runtime_error("Future event savestate not allowed in R/O mode"); } + // Ensure that the current movie and savestate movie are in the same timeline + if (std::mismatch(recorded_input_.begin(), recorded_input_.end(), + recorded_input.begin()) + .first != recorded_input_.end()) { + throw std::runtime_error("Timeline mismatch not allowed in R/O mode"); + } + play_mode = PlayMode::Playing; total_input = GetInputCount(recorded_input); } else { From 1780f8b5b81ef9a80b41dd5e614d09605b703c44 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 8 Jul 2020 23:56:37 +0800 Subject: [PATCH 09/14] core/movie: Add MovieFinished mode Also mentioned in Laws of TAS. --- src/citra_qt/main.cpp | 13 +++++--- src/citra_qt/movie/movie_play_dialog.cpp | 2 +- src/citra_qt/movie/movie_record_dialog.cpp | 2 +- src/core/movie.cpp | 37 +++++++++++++--------- src/core/movie.h | 5 ++- 5 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index d9612fce8..6e9b9d553 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -1892,7 +1892,8 @@ void GMainWindow::OnCloseMovie(bool shutting_down) { OnPauseGame(); } - const bool was_recording = Core::Movie::GetInstance().IsRecordingInput(); + const bool was_recording = + Core::Movie::GetInstance().GetPlayMode() == Core::Movie::PlayMode::Recording; Core::Movie::GetInstance().Shutdown(); if (was_recording) { QMessageBox::information(this, tr("Movie Saved"), @@ -1986,14 +1987,19 @@ void GMainWindow::UpdateStatusBar() { // Update movie status const u64 current = Core::Movie::GetInstance().GetCurrentInputIndex(); const u64 total = Core::Movie::GetInstance().GetTotalInputCount(); - if (Core::Movie::GetInstance().IsRecordingInput()) { + const auto play_mode = Core::Movie::GetInstance().GetPlayMode(); + if (play_mode == Core::Movie::PlayMode::Recording) { message_label->setText(tr("Recording %1").arg(current)); message_label->setVisible(true); message_label_used_for_movie = true; - } else if (Core::Movie::GetInstance().IsPlayingInput()) { + } else if (play_mode == Core::Movie::PlayMode::Playing) { message_label->setText(tr("Playing %1 / %2").arg(current).arg(total)); message_label->setVisible(true); message_label_used_for_movie = true; + } else if (play_mode == Core::Movie::PlayMode::MovieFinished) { + message_label->setText(tr("Movie Finished")); + message_label->setVisible(true); + message_label_used_for_movie = true; } else if (message_label_used_for_movie) { // Clear the label if movie was just closed message_label->setText(QString{}); message_label->setVisible(false); @@ -2291,7 +2297,6 @@ void GMainWindow::OnLanguageChanged(const QString& locale) { void GMainWindow::OnMoviePlaybackCompleted() { QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed.")); - ui->action_Close_Movie->setEnabled(false); } void GMainWindow::UpdateWindowTitle() { diff --git a/src/citra_qt/movie/movie_play_dialog.cpp b/src/citra_qt/movie/movie_play_dialog.cpp index bfb38341a..0a389985a 100644 --- a/src/citra_qt/movie/movie_play_dialog.cpp +++ b/src/citra_qt/movie/movie_play_dialog.cpp @@ -29,7 +29,7 @@ MoviePlayDialog::MoviePlayDialog(QWidget* parent, GameList* game_list_) if (Core::System::GetInstance().IsPoweredOn()) { QString note_text; note_text = tr("Current running game will be stopped."); - if (Core::Movie::GetInstance().IsRecordingInput()) { + if (Core::Movie::GetInstance().GetPlayMode() == Core::Movie::PlayMode::Recording) { note_text.append(tr("
Current recording will be discarded.")); } ui->note2Label->setText(note_text); diff --git a/src/citra_qt/movie/movie_record_dialog.cpp b/src/citra_qt/movie/movie_record_dialog.cpp index 6f0954909..9b7967d9a 100644 --- a/src/citra_qt/movie/movie_record_dialog.cpp +++ b/src/citra_qt/movie/movie_record_dialog.cpp @@ -25,7 +25,7 @@ MovieRecordDialog::MovieRecordDialog(QWidget* parent) QString note_text; if (Core::System::GetInstance().IsPoweredOn()) { note_text = tr("Current running game will be restarted."); - if (Core::Movie::GetInstance().IsRecordingInput()) { + if (Core::Movie::GetInstance().GetPlayMode() == Core::Movie::PlayMode::Recording) { note_text.append(tr("
Current recording will be discarded.")); } } else { diff --git a/src/core/movie.cpp b/src/core/movie.cpp index 0c90950b4..f96842d5b 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -28,8 +28,6 @@ namespace Core { /*static*/ Movie Movie::s_instance; -enum class PlayMode { None, Recording, Playing }; - enum class ControllerStateType : u8 { PadAndCircle, Touch, @@ -178,8 +176,23 @@ void Movie::serialize(Archive& ar, const unsigned int file_version) { } } + // Whether the state was made in MovieFinished state + bool post_movie = play_mode == PlayMode::MovieFinished; + if (file_version > 0) { + ar& post_movie; + } + if (Archive::is_loading::value && id != 0) { - if (read_only) { // Do not replace the previously recorded input. + if (!read_only) { + recorded_input = std::move(recorded_input_); + } + + if (post_movie) { + play_mode = PlayMode::MovieFinished; + return; + } + + if (read_only) { if (play_mode == PlayMode::Recording) { SaveMovie(); } @@ -196,7 +209,6 @@ void Movie::serialize(Archive& ar, const unsigned int file_version) { play_mode = PlayMode::Playing; total_input = GetInputCount(recorded_input); } else { - recorded_input = std::move(recorded_input_); play_mode = PlayMode::Recording; rerecord_count++; } @@ -205,11 +217,8 @@ void Movie::serialize(Archive& ar, const unsigned int file_version) { SERIALIZE_IMPL(Movie) -bool Movie::IsPlayingInput() const { - return play_mode == PlayMode::Playing; -} -bool Movie::IsRecordingInput() const { - return play_mode == PlayMode::Recording; +Movie::PlayMode Movie::GetPlayMode() const { + return play_mode; } u64 Movie::GetCurrentInputIndex() const { @@ -222,9 +231,7 @@ u64 Movie::GetTotalInputCount() const { void Movie::CheckInputEnd() { if (current_byte + sizeof(ControllerState) > recorded_input.size()) { LOG_INFO(Movie, "Playback finished"); - play_mode = PlayMode::None; - init_time = 0; - id = 0; + play_mode = PlayMode::MovieFinished; playback_completion_callback(); } } @@ -638,7 +645,7 @@ Movie::MovieMetadata Movie::GetMovieMetadata(const std::string& movie_file) cons } void Movie::Shutdown() { - if (IsRecordingInput()) { + if (play_mode == PlayMode::Recording) { SaveMovie(); } @@ -653,11 +660,11 @@ void Movie::Shutdown() { template void Movie::Handle(Targs&... Fargs) { - if (IsPlayingInput()) { + if (play_mode == PlayMode::Playing) { ASSERT(current_byte + sizeof(ControllerState) <= recorded_input.size()); Play(Fargs...); CheckInputEnd(); - } else if (IsRecordingInput()) { + } else if (play_mode == PlayMode::Recording) { Record(Fargs...); } } diff --git a/src/core/movie.h b/src/core/movie.h index e62dfe5c2..15cd631ca 100644 --- a/src/core/movie.h +++ b/src/core/movie.h @@ -24,10 +24,10 @@ union PadState; namespace Core { struct CTMHeader; struct ControllerState; -enum class PlayMode; class Movie { public: + enum class PlayMode { None, Recording, Playing, MovieFinished }; enum class ValidationResult { OK, RevisionDismatch, @@ -120,8 +120,7 @@ public: * When playing: Replaces the given input states with the ones stored in the playback file */ void HandleExtraHidResponse(Service::IR::ExtraHIDResponse& extra_hid_response); - bool IsPlayingInput() const; - bool IsRecordingInput() const; + PlayMode GetPlayMode() const; u64 GetCurrentInputIndex() const; u64 GetTotalInputCount() const; From d6b64f6b0903c94d1266930e24aba210bef7e606 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Thu, 23 Jul 2020 14:10:16 +0800 Subject: [PATCH 10/14] citra_qt: Simplify movie recording code The behavior shouldn't be changed. --- src/citra_qt/main.cpp | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 6e9b9d553..aee6de481 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -1074,6 +1074,14 @@ void GMainWindow::BootGame(const QString& filename) { ShowFullscreen(); } + if (movie_record_on_start) { + Core::Movie::GetInstance().StartRecording(movie_record_path.toStdString(), + movie_record_author.toStdString()); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } + if (video_dumping_on_start) { Layout::FramebufferLayout layout{ Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; @@ -1540,14 +1548,6 @@ void GMainWindow::OnMenuRecentFile() { void GMainWindow::OnStartGame() { Camera::QtMultimediaCameraHandler::ResumeCameras(); - if (movie_record_on_start) { - Core::Movie::GetInstance().StartRecording(movie_record_path.toStdString(), - movie_record_author.toStdString()); - movie_record_on_start = false; - movie_record_path.clear(); - movie_record_author.clear(); - } - PreventOSSleep(); emu_thread->SetRunning(true); @@ -1853,15 +1853,12 @@ void GMainWindow::OnRecordMovie() { return; } - if (emulation_running) { - // Restart game + movie_record_on_start = true; + movie_record_path = dialog.GetPath(); + movie_record_author = dialog.GetAuthor(); + + if (emulation_running) { // Restart game BootGame(QString(game_path)); - Core::Movie::GetInstance().StartRecording(dialog.GetPath().toStdString(), - dialog.GetAuthor().toStdString()); - } else { - movie_record_on_start = true; - movie_record_path = dialog.GetPath(); - movie_record_author = dialog.GetAuthor(); } ui->action_Close_Movie->setEnabled(true); } From e60e20666ec0f1d3961cb0c860c65f5cf8aec7ce Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Thu, 23 Jul 2020 18:49:17 +0800 Subject: [PATCH 11/14] citra_qt: Allow enabling frame advance before emulation start Effectively allows starting emulation as paused. --- src/citra_qt/bootmanager.cpp | 8 ++++++++ src/citra_qt/bootmanager.h | 2 ++ src/citra_qt/main.cpp | 12 +++++++++--- src/citra_qt/main.ui | 3 --- src/core/perf_stats.cpp | 4 ++++ src/core/perf_stats.h | 1 + 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp index 0607a23af..f90cc93b6 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/citra_qt/bootmanager.cpp @@ -21,6 +21,7 @@ #include "core/3ds.h" #include "core/core.h" #include "core/frontend/scope_acquire_context.h" +#include "core/perf_stats.h" #include "core/settings.h" #include "input_common/keyboard.h" #include "input_common/main.h" @@ -55,6 +56,13 @@ void EmuThread::run() { emit LoadProgress(VideoCore::LoadCallbackStage::Complete, 0, 0); + if (Core::System::GetInstance().frame_limiter.IsFrameAdvancing()) { + // Usually the loading screen is hidden after the first frame is drawn. In this case + // we hide it immediately as we need to wait for user input to start the emulation. + emit HideLoadingScreen(); + Core::System::GetInstance().frame_limiter.WaitOnce(); + } + // Holds whether the cpu was running during the last iteration, // so that the DebugModeLeft signal can be emitted before the // next execution step. diff --git a/src/citra_qt/bootmanager.h b/src/citra_qt/bootmanager.h index 3dec9751b..38cd494ed 100644 --- a/src/citra_qt/bootmanager.h +++ b/src/citra_qt/bootmanager.h @@ -122,6 +122,8 @@ signals: void ErrorThrown(Core::System::ResultStatus, std::string); void LoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total); + + void HideLoadingScreen(); }; class OpenGLWindow : public QWindow { diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index aee6de481..edea64511 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -1048,6 +1048,8 @@ void GMainWindow::BootGame(const QString& filename) { connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); + connect(emu_thread.get(), &EmuThread::HideLoadingScreen, loading_screen, + &LoadingScreen::OnLoadComplete); // Update the GUI registersWidget->OnDebugModeEntered(); @@ -1082,6 +1084,13 @@ void GMainWindow::BootGame(const QString& filename) { movie_record_author.clear(); } + if (ui->action_Enable_Frame_Advancing->isChecked()) { + ui->action_Advance_Frame->setEnabled(true); + Core::System::GetInstance().frame_limiter.SetFrameAdvancing(true); + } else { + ui->action_Advance_Frame->setEnabled(false); + } + if (video_dumping_on_start) { Layout::FramebufferLayout layout{ Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; @@ -1155,8 +1164,6 @@ void GMainWindow::ShutdownGame() { ui->action_Load_Amiibo->setEnabled(false); ui->action_Remove_Amiibo->setEnabled(false); ui->action_Report_Compatibility->setEnabled(false); - ui->action_Enable_Frame_Advancing->setEnabled(false); - ui->action_Enable_Frame_Advancing->setChecked(false); ui->action_Advance_Frame->setEnabled(false); ui->action_Capture_Screenshot->setEnabled(false); render_window->hide(); @@ -1564,7 +1571,6 @@ void GMainWindow::OnStartGame() { ui->action_Cheats->setEnabled(true); ui->action_Load_Amiibo->setEnabled(true); ui->action_Report_Compatibility->setEnabled(true); - ui->action_Enable_Frame_Advancing->setEnabled(true); ui->action_Capture_Screenshot->setEnabled(true); discord_rpc->Update(); diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index 5d2d4f0ca..e192f1062 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -349,9 +349,6 @@ true - - false - Enable Frame Advancing diff --git a/src/core/perf_stats.cpp b/src/core/perf_stats.cpp index e5b01f086..5db1df403 100644 --- a/src/core/perf_stats.cpp +++ b/src/core/perf_stats.cpp @@ -169,6 +169,10 @@ void FrameLimiter::DoFrameLimiting(microseconds current_system_time_us) { previous_walltime = now; } +bool FrameLimiter::IsFrameAdvancing() const { + return frame_advancing_enabled; +} + void FrameLimiter::SetFrameAdvancing(bool value) { const bool was_enabled = frame_advancing_enabled.exchange(value); if (was_enabled && !value) { diff --git a/src/core/perf_stats.h b/src/core/perf_stats.h index 9038e4ca2..e90c4c1ac 100644 --- a/src/core/perf_stats.h +++ b/src/core/perf_stats.h @@ -90,6 +90,7 @@ public: void DoFrameLimiting(std::chrono::microseconds current_system_time_us); + bool IsFrameAdvancing() const; /** * Sets whether frame advancing is enabled or not. * Note: The frontend must cancel frame advancing before shutting down in order From f8eb9a541d26380693c2215eb42b01b210d54f67 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Thu, 27 Aug 2020 22:27:29 +0800 Subject: [PATCH 12/14] citra_qt: Fix potential indeterminstism caused by starting record/playback Previously the movie was started *after* core starts running, causing potential indeterminism. Some desyncs are still not fixed; they may be caused by core timing. More investigation is required. --- src/citra_qt/main.cpp | 80 ++++++++++++++++++++++++------------------- src/citra_qt/main.h | 5 ++- src/core/movie.cpp | 7 ++-- src/core/movie.h | 4 ++- 4 files changed, 57 insertions(+), 39 deletions(-) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index edea64511..ec57d0e5b 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -1018,6 +1018,9 @@ void GMainWindow::BootGame(const QString& filename) { if (movie_record_on_start) { Core::Movie::GetInstance().PrepareForRecording(); } + if (movie_playback_on_start) { + Core::Movie::GetInstance().PrepareForPlayback(movie_playback_path.toStdString()); + } // Save configurations UpdateUISettings(); @@ -1027,6 +1030,42 @@ void GMainWindow::BootGame(const QString& filename) { if (!LoadROM(filename)) return; + // Set everything up + if (movie_record_on_start) { + Core::Movie::GetInstance().StartRecording(movie_record_path.toStdString(), + movie_record_author.toStdString()); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } + if (movie_playback_on_start) { + Core::Movie::GetInstance().StartPlayback(movie_playback_path.toStdString()); + movie_playback_on_start = false; + movie_playback_path.clear(); + } + + if (ui->action_Enable_Frame_Advancing->isChecked()) { + ui->action_Advance_Frame->setEnabled(true); + Core::System::GetInstance().frame_limiter.SetFrameAdvancing(true); + } else { + ui->action_Advance_Frame->setEnabled(false); + } + + if (video_dumping_on_start) { + Layout::FramebufferLayout layout{ + Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; + if (!Core::System::GetInstance().VideoDumper().StartDumping( + video_dumping_path.toStdString(), layout)) { + + QMessageBox::critical( + this, tr("Citra"), + tr("Could not start video dumping.
Refer to the log for details.")); + ui->action_Dump_Video->setChecked(false); + } + video_dumping_on_start = false; + video_dumping_path.clear(); + } + // Create and start the emulation thread emu_thread = std::make_unique(*render_window); emit EmulationStarting(emu_thread.get()); @@ -1076,35 +1115,6 @@ void GMainWindow::BootGame(const QString& filename) { ShowFullscreen(); } - if (movie_record_on_start) { - Core::Movie::GetInstance().StartRecording(movie_record_path.toStdString(), - movie_record_author.toStdString()); - movie_record_on_start = false; - movie_record_path.clear(); - movie_record_author.clear(); - } - - if (ui->action_Enable_Frame_Advancing->isChecked()) { - ui->action_Advance_Frame->setEnabled(true); - Core::System::GetInstance().frame_limiter.SetFrameAdvancing(true); - } else { - ui->action_Advance_Frame->setEnabled(false); - } - - if (video_dumping_on_start) { - Layout::FramebufferLayout layout{ - Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; - if (!Core::System::GetInstance().VideoDumper().StartDumping( - video_dumping_path.toStdString(), layout)) { - - QMessageBox::critical( - this, tr("Citra"), - tr("Could not start video dumping.
Refer to the log for details.")); - ui->action_Dump_Video->setChecked(false); - } - video_dumping_on_start = false; - video_dumping_path.clear(); - } OnStartGame(); } @@ -1128,7 +1138,6 @@ void GMainWindow::ShutdownGame() { AllowOSSleep(); discord_rpc->Pause(); - OnCloseMovie(true); emu_thread->RequestStop(); // Release emu threads from any breakpoints @@ -1147,6 +1156,8 @@ void GMainWindow::ShutdownGame() { emu_thread->wait(); emu_thread = nullptr; + OnCloseMovie(); + discord_rpc->Update(); Camera::QtMultimediaCameraHandler::ReleaseHandlers(); @@ -1875,22 +1886,21 @@ void GMainWindow::OnPlayMovie() { return; } - const auto movie_path = dialog.GetMoviePath().toStdString(); - Core::Movie::GetInstance().PrepareForPlayback(movie_path); + movie_playback_on_start = true; + movie_playback_path = dialog.GetMoviePath(); BootGame(dialog.GetGamePath()); - Core::Movie::GetInstance().StartPlayback(movie_path); ui->action_Close_Movie->setEnabled(true); } -void GMainWindow::OnCloseMovie(bool shutting_down) { +void GMainWindow::OnCloseMovie() { if (movie_record_on_start) { QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled.")); movie_record_on_start = false; movie_record_path.clear(); movie_record_author.clear(); } else { - const bool was_running = !shutting_down && emu_thread && emu_thread->IsRunning(); + const bool was_running = emu_thread && emu_thread->IsRunning(); if (was_running) { OnPauseGame(); } diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 99451d308..9b29edd2a 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -208,7 +208,7 @@ private slots: void OnCreateGraphicsSurfaceViewer(); void OnRecordMovie(); void OnPlayMovie(); - void OnCloseMovie(bool shutting_down = false); + void OnCloseMovie(); void OnCaptureScreenshot(); #ifdef ENABLE_FFMPEG_VIDEO_DUMPER void OnStartVideoDumping(); @@ -269,6 +269,9 @@ private: QString movie_record_path; QString movie_record_author; + bool movie_playback_on_start = false; + QString movie_playback_path; + // Video dumping bool video_dumping_on_start = false; QString video_dumping_path; diff --git a/src/core/movie.cpp b/src/core/movie.cpp index f96842d5b..109cf08dd 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -491,6 +491,7 @@ void Movie::SaveMovie() { CTMHeader header = {}; header.filetype = header_magic_bytes; + header.program_id = program_id; header.clock_init_time = init_time; header.id = id; @@ -500,8 +501,6 @@ void Movie::SaveMovie() { header.rerecord_count = rerecord_count; header.input_count = GetInputCount(recorded_input); - Core::System::GetInstance().GetAppLoader().ReadProgramId(header.program_id); - std::string rev_bytes; CryptoPP::StringSource(Common::g_scm_rev, true, new CryptoPP::HexDecoder(new CryptoPP::StringSink(rev_bytes))); @@ -562,6 +561,10 @@ void Movie::StartRecording(const std::string& movie_file, const std::string& aut CryptoPP::AutoSeededRandomPool rng; rng.GenerateBlock(reinterpret_cast(&id), sizeof(id)); + // Get program ID + program_id = 0; + Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id); + LOG_INFO(Movie, "Enabling Movie recording, ID: {:016X}", id); } diff --git a/src/core/movie.h b/src/core/movie.h index 15cd631ca..0d1b689df 100644 --- a/src/core/movie.h +++ b/src/core/movie.h @@ -159,6 +159,8 @@ private: std::string record_movie_file; std::string record_movie_author; + u64 init_time; // Clock init time override for RNG consistency + std::vector recorded_input; std::size_t current_byte = 0; u64 current_input = 0; @@ -166,7 +168,7 @@ private: u64 total_input = 0; u64 id = 0; // ID of the current movie loaded - u64 init_time; + u64 program_id = 0; u32 rerecord_count = 1; bool read_only = true; From b2531310b4036d9ae39812398e552d1d1ddc816d Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 14 Oct 2020 22:35:20 +0800 Subject: [PATCH 13/14] Minor improvements to the UX 1. Game will now be paused on movie finish 2. Added 'Save without Closing' --- src/citra_qt/main.cpp | 32 ++++++++++++++++++++++++++++++-- src/citra_qt/main.h | 1 + src/citra_qt/main.ui | 9 +++++++++ src/core/movie.cpp | 1 + src/core/movie.h | 8 ++++++-- 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index ec57d0e5b..9fc736660 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -176,8 +176,9 @@ GMainWindow::GMainWindow() Network::Init(); - Core::Movie::GetInstance().SetPlaybackCompletionCallback( - [this] { QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted"); }); + Core::Movie::GetInstance().SetPlaybackCompletionCallback([this] { + QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted", Qt::BlockingQueuedConnection); + }); InitializeWidgets(); InitializeDebugWidgets(); @@ -748,6 +749,7 @@ void GMainWindow::ConnectMenuEvents() { connect(ui->action_Record_Movie, &QAction::triggered, this, &GMainWindow::OnRecordMovie); connect(ui->action_Play_Movie, &QAction::triggered, this, &GMainWindow::OnPlayMovie); connect(ui->action_Close_Movie, &QAction::triggered, this, &GMainWindow::OnCloseMovie); + connect(ui->action_Save_Movie, &QAction::triggered, this, &GMainWindow::OnSaveMovie); connect(ui->action_Movie_Read_Only_Mode, &QAction::toggled, this, [this](bool checked) { Core::Movie::GetInstance().SetReadOnly(checked); }); connect(ui->action_Enable_Frame_Advancing, &QAction::triggered, this, [this] { @@ -1878,6 +1880,7 @@ void GMainWindow::OnRecordMovie() { BootGame(QString(game_path)); } ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(true); } void GMainWindow::OnPlayMovie() { @@ -1891,6 +1894,7 @@ void GMainWindow::OnPlayMovie() { BootGame(dialog.GetGamePath()); ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(false); } void GMainWindow::OnCloseMovie() { @@ -1919,6 +1923,25 @@ void GMainWindow::OnCloseMovie() { } ui->action_Close_Movie->setEnabled(false); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnSaveMovie() { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + if (Core::Movie::GetInstance().GetPlayMode() == Core::Movie::PlayMode::Recording) { + Core::Movie::GetInstance().SaveMovie(); + QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved.")); + } else { + LOG_ERROR(Frontend, "Tried to save movie while movie is not being recorded"); + } + + if (was_running) { + OnStartGame(); + } } void GMainWindow::OnCaptureScreenshot() { @@ -2005,18 +2028,22 @@ void GMainWindow::UpdateStatusBar() { message_label->setText(tr("Recording %1").arg(current)); message_label->setVisible(true); message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(true); } else if (play_mode == Core::Movie::PlayMode::Playing) { message_label->setText(tr("Playing %1 / %2").arg(current).arg(total)); message_label->setVisible(true); message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); } else if (play_mode == Core::Movie::PlayMode::MovieFinished) { message_label->setText(tr("Movie Finished")); message_label->setVisible(true); message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); } else if (message_label_used_for_movie) { // Clear the label if movie was just closed message_label->setText(QString{}); message_label->setVisible(false); message_label_used_for_movie = false; + ui->action_Save_Movie->setEnabled(false); } auto results = Core::System::GetInstance().GetAndResetPerfStats(); @@ -2309,6 +2336,7 @@ void GMainWindow::OnLanguageChanged(const QString& locale) { } void GMainWindow::OnMoviePlaybackCompleted() { + OnPauseGame(); QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed.")); } diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 9b29edd2a..970f19cad 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -209,6 +209,7 @@ private slots: void OnRecordMovie(); void OnPlayMovie(); void OnCloseMovie(); + void OnSaveMovie(); void OnCaptureScreenshot(); #ifdef ENABLE_FFMPEG_VIDEO_DUMPER void OnStartVideoDumping(); diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index e192f1062..1436017c5 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -166,6 +166,7 @@ +
@@ -334,6 +335,14 @@ Close + + + false + + + Save without Closing + + true diff --git a/src/core/movie.cpp b/src/core/movie.cpp index 109cf08dd..49d1e654f 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -543,6 +543,7 @@ void Movie::StartPlayback(const std::string& movie_file) { current_byte = 0; current_input = 0; id = header.id; + program_id = header.program_id; LOG_INFO(Movie, "Loaded Movie, ID: {:016X}", id); } diff --git a/src/core/movie.h b/src/core/movie.h index 0d1b689df..d4b615876 100644 --- a/src/core/movie.h +++ b/src/core/movie.h @@ -125,6 +125,12 @@ public: u64 GetCurrentInputIndex() const; u64 GetTotalInputCount() const; + /** + * Saves the movie immediately, in its current state. + * This is called in Shutdown. + */ + void SaveMovie(); + private: static Movie s_instance; @@ -152,8 +158,6 @@ private: ValidationResult ValidateHeader(const CTMHeader& header) const; ValidationResult ValidateInput(const std::vector& input, u64 expected_count) const; - void SaveMovie(); - PlayMode play_mode; std::string record_movie_file; From 996ca25a2e596770212b7b7f5f5d851e13a022ec Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Mon, 8 Feb 2021 11:24:05 +0800 Subject: [PATCH 14/14] core_timing: Lock CoreTiming event queue while deserializing To handle those classic asymmetric constructor/destructor side effects --- src/core/core.cpp | 1 + src/core/core_timing.cpp | 10 ++++++++++ src/core/core_timing.h | 12 ++++++++++++ 3 files changed, 23 insertions(+) diff --git a/src/core/core.cpp b/src/core/core.cpp index 0d7b58e2c..f1d5c7b00 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -629,6 +629,7 @@ void System::serialize(Archive& ar, const unsigned int file_version) { // This needs to be set from somewhere - might as well be here! if (Archive::is_loading::value) { + timing->UnlockEventQueue(); Service::GSP::SetGlobalModule(*this); memory->SetDSP(*dsp_core); cheat_engine->Connect(); diff --git a/src/core/core_timing.cpp b/src/core/core_timing.cpp index 3d77932b6..4aa6d870e 100644 --- a/src/core/core_timing.cpp +++ b/src/core/core_timing.cpp @@ -49,6 +49,10 @@ TimingEventType* Timing::RegisterEvent(const std::string& name, TimedCallback ca void Timing::ScheduleEvent(s64 cycles_into_future, const TimingEventType* event_type, u64 userdata, std::size_t core_id) { + if (event_queue_locked) { + return; + } + ASSERT(event_type != nullptr); Timing::Timer* timer = nullptr; if (core_id == std::numeric_limits::max()) { @@ -74,6 +78,9 @@ void Timing::ScheduleEvent(s64 cycles_into_future, const TimingEventType* event_ } void Timing::UnscheduleEvent(const TimingEventType* event_type, u64 userdata) { + if (event_queue_locked) { + return; + } for (auto timer : timers) { auto itr = std::remove_if( timer->event_queue.begin(), timer->event_queue.end(), @@ -89,6 +96,9 @@ void Timing::UnscheduleEvent(const TimingEventType* event_type, u64 userdata) { } void Timing::RemoveEvent(const TimingEventType* event_type) { + if (event_queue_locked) { + return; + } for (auto timer : timers) { auto itr = std::remove_if(timer->event_queue.begin(), timer->event_queue.end(), [&](const Event& e) { return e.type == event_type; }); diff --git a/src/core/core_timing.h b/src/core/core_timing.h index aebe5c742..611122211 100644 --- a/src/core/core_timing.h +++ b/src/core/core_timing.h @@ -280,6 +280,11 @@ public: std::shared_ptr GetTimer(std::size_t cpu_id); + // Used after deserializing to unprotect the event queue. + void UnlockEventQueue() { + event_queue_locked = false; + } + private: // unordered_map stores each element separately as a linked list node so pointers to // elements remain stable regardless of rehashes/resizing. @@ -292,6 +297,10 @@ private: // under/overclocking the guest cpu double cpu_clock_scale = 1.0; + // When true, the event queue can't be modified. Used while deserializing to workaround + // destructor side effects. + bool event_queue_locked = false; + template void serialize(Archive& ar, const unsigned int file_version) { // event_types set during initialization of other things @@ -303,6 +312,9 @@ private: } else { ar& current_timer; } + if (Archive::is_loading::value) { + event_queue_locked = true; + } } friend class boost::serialization::access; };