diff --git a/Core/Shared/Emulator.cpp b/Core/Shared/Emulator.cpp index 73656ad63..612ae46de 100644 --- a/Core/Shared/Emulator.cpp +++ b/Core/Shared/Emulator.cpp @@ -490,6 +490,8 @@ bool Emulator::InternalLoadRom(VirtualFile romFile, VirtualFile patchFile, bool //Restore pollcounter (used by movies when a power cycle is in the movie) _console->GetControlManager()->SetPollCounter(pollCounter); + _notificationManager->SendNotification(ConsoleNotificationType::AfterInitConsole); + _rewindManager->InitHistory(); if(debuggerActive || _settings->CheckFlag(EmulationFlags::ConsoleMode)) { diff --git a/Core/Shared/Interfaces/INotificationListener.h b/Core/Shared/Interfaces/INotificationListener.h index 076cef4aa..a726d8868 100644 --- a/Core/Shared/Interfaces/INotificationListener.h +++ b/Core/Shared/Interfaces/INotificationListener.h @@ -29,6 +29,7 @@ enum class ConsoleNotificationType CheatsChanged, RequestConfigChange, RefreshSoftwareRenderer, + AfterInitConsole, }; struct GameLoadedEventParams diff --git a/Core/Shared/Movies/MesenMovie.cpp b/Core/Shared/Movies/MesenMovie.cpp index 6fcdea943..5bc59e4b0 100644 --- a/Core/Shared/Movies/MesenMovie.cpp +++ b/Core/Shared/Movies/MesenMovie.cpp @@ -13,7 +13,6 @@ #include "Shared/CheatManager.h" #include "Utilities/ZipReader.h" #include "Utilities/StringUtilities.h" -#include "Utilities/HexUtilities.h" #include "Utilities/VirtualFile.h" #include "Utilities/magic_enum.hpp" #include "Utilities/Serializer.h" @@ -95,11 +94,35 @@ vector MesenMovie::LoadBattery(string extension) void MesenMovie::ProcessNotification(ConsoleNotificationType type, void* parameter) { - if(type == ConsoleNotificationType::GameLoaded) { + if(type == ConsoleNotificationType::AfterInitConsole) { + _controlManager = _emu->GetConsole()->GetControlManager(); + _emu->RegisterInputProvider(this); shared_ptr console = _emu->GetConsole(); if(console) { - console->GetControlManager()->SetPollCounter(_lastPollCounter); + _controlManager->SetPollCounter(_lastPollCounter + 1); + } + + //Re-apply settings - power cycling can alter some (e.g auto-configure input types, etc.) + ApplySettings(_settingsData); + + _originalCheats = _emu->GetCheatManager()->GetCheats(); + + LoadCheats(); + + if(!_playing) { + stringstream saveStateData; + if(_reader->GetStream("SaveState.mss", saveStateData)) { + if(!_emu->GetSaveStateManager()->LoadState(saveStateData)) { + _loadFailure = true; + } + } + } + + _controlManager->UpdateControlDevices(); + + if(!_playing) { + _controlManager->SetPollCounter(0); } } } @@ -114,8 +137,10 @@ bool MesenMovie::Play(VirtualFile& file) _reader.reset(new ZipReader()); _reader->LoadArchive(ss); - stringstream settingsData, inputData; - if(!_reader->GetStream("GameSettings.txt", settingsData)) { + _hasSaveState = _reader->CheckFile("SaveState.mss"); + + stringstream inputData; + if(!_reader->GetStream("GameSettings.txt", _settingsData)) { MessageManager::Log("[Movie] File not found: GameSettings.txt"); return false; } @@ -124,17 +149,7 @@ bool MesenMovie::Play(VirtualFile& file) return false; } - while(inputData) { - string line; - std::getline(inputData, line); - if(line.substr(0, 1) == "|") { - _inputData.push_back(StringUtilities::Split(line.substr(1), '|')); - } - } - - _deviceIndex = 0; - - ParseSettings(settingsData); + ParseSettings(_settingsData); string version = LoadString(_settings, MovieKeys::MesenVersion); if(version.size() < 2 || version.substr(0, 2) == "0." || version.substr(0, 2) == "1.") { @@ -143,43 +158,45 @@ bool MesenMovie::Play(VirtualFile& file) return false; } - if(LoadInt(_settings, MovieKeys::MovieFormatVersion, 0) < 2) { + uint32_t movieVersion = LoadInt(_settings, MovieKeys::MovieFormatVersion, 0); + if(movieVersion < 2) { MessageManager::DisplayMessage("Movies", "MovieIncompatibleVersion"); return false; } + while(inputData) { + string line; + std::getline(inputData, line); + if(line.substr(0, 1) == "|") { + vector lineInputData = StringUtilities::Split(line.substr(1), '|'); + if(movieVersion == 2 && _inputData.empty() && !_hasSaveState) { + //Version 2 of movies incorrectly skipped the first frame after recording from power on + //When playing back an old movie, add an extra frame of input at the start (no buttons pressed) + //to allow playback to match what it was before this bug was fixed. + _inputData.push_back(vector(lineInputData.size(), "")); + } + _inputData.push_back(lineInputData); + } + } + + _deviceIndex = 0; + auto emuLock = _emu->AcquireLock(false); - if(!ApplySettings(settingsData)) { + if(!ApplySettings(_settingsData)) { return false; } _emu->GetBatteryManager()->SetBatteryProvider(shared_from_this()); _emu->GetNotificationManager()->RegisterNotificationListener(shared_from_this()); - _emu->PowerCycle(); - - //Re-apply settings - power cycling can alter some (e.g auto-configure input types, etc.) - ApplySettings(settingsData); - - _originalCheats = _emu->GetCheatManager()->GetCheats(); - - _controlManager = _emu->GetConsole()->GetControlManager(); - - LoadCheats(); - - stringstream saveStateData; - if(_reader->GetStream("SaveState.mss", saveStateData)) { - if(!_emu->GetSaveStateManager()->LoadState(saveStateData)) { - return false; - } + _emu->GetBatteryManager()->SetBatteryProvider(nullptr); + if(_hasSaveState) { + _controlManager->SetPollCounter(0); } - _controlManager->UpdateControlDevices(); - _controlManager->SetPollCounter(0); - _playing = true; - - return true; + _playing = !_loadFailure; + return _playing; } template diff --git a/Core/Shared/Movies/MesenMovie.h b/Core/Shared/Movies/MesenMovie.h index 71e5a6420..d3ec877f3 100644 --- a/Core/Shared/Movies/MesenMovie.h +++ b/Core/Shared/Movies/MesenMovie.h @@ -27,9 +27,12 @@ class MesenMovie final : public IMovie, public INotificationListener, public IBa vector _cheats; vector _originalCheats; stringstream _emuSettingsBackup; + stringstream _settingsData; unordered_map _settings; string _filename; bool _forTest = false; + bool _loadFailure = false; + bool _hasSaveState = false; private: void ParseSettings(stringstream& data); diff --git a/Core/Shared/Movies/MovieRecorder.cpp b/Core/Shared/Movies/MovieRecorder.cpp index 9969c09bf..ef869e887 100644 --- a/Core/Shared/Movies/MovieRecorder.cpp +++ b/Core/Shared/Movies/MovieRecorder.cpp @@ -66,8 +66,12 @@ bool MovieRecorder::Record(RecordMovieOptions options) if(needSaveState) { _emu->GetSaveStateManager()->SaveState(_saveStateData); _hasSaveState = true; + + //Get rid of any inputs recorded while the game was reloaded + _inputData = stringstream(); } + _emu->GetBatteryManager()->SetBatteryProvider(nullptr); _emu->GetBatteryManager()->SetBatteryRecorder(nullptr); _emu->Unlock(); @@ -189,7 +193,7 @@ vector MovieRecorder::LoadBattery(string extension) void MovieRecorder::ProcessNotification(ConsoleNotificationType type, void* parameter) { - if(type == ConsoleNotificationType::GameLoaded) { + if(type == ConsoleNotificationType::AfterInitConsole) { _emu->RegisterInputRecorder(this); } } @@ -216,9 +220,24 @@ bool MovieRecorder::CreateMovie(string movieFile, deque& data, uint3 _inputData = stringstream(); + //When a save state is part of the data and the startPosition is 0, skip the first input + //This is a workaround to deal with the fact that the rewindmanager's first save state after loading + //the ROM is taken at a different time (before vs after the inputs for the first frame are polled) compared + //to regular movie save states. Ignoring the first frame's input allows us to get the correct result + //in the vast majority of scenarios + bool skipFirstInput = _hasSaveState && startPosition == 0; + for(uint32_t i = startPosition; i < endPosition; i++) { RewindData rewindData = data[i]; - for(uint32_t j = 0; j < RewindManager::BufferSize; j++) { + uint32_t len = 0; + + //Some blocks (like the first block after power cycle) can contain an extra set of inputs + //Check the max number of inputs to be able to export them all + for(int j = 0; j < BaseControlDevice::PortCount; j++) { + len = std::max(len, (uint32_t)rewindData.InputLogs[j].size()); + } + + for(uint32_t j = skipFirstInput ? 1 : 0; j < len; j++) { for(shared_ptr& device : devices) { uint8_t port = device->GetPort(); if(j < rewindData.InputLogs[port].size()) { @@ -228,6 +247,8 @@ bool MovieRecorder::CreateMovie(string movieFile, deque& data, uint3 } _inputData << "\n"; } + + skipFirstInput = false; } //Write the movie file diff --git a/Core/Shared/Movies/MovieRecorder.h b/Core/Shared/Movies/MovieRecorder.h index 9322e5dc7..7c5e347b3 100644 --- a/Core/Shared/Movies/MovieRecorder.h +++ b/Core/Shared/Movies/MovieRecorder.h @@ -14,7 +14,7 @@ class Emulator; class MovieRecorder final : public INotificationListener, public IInputRecorder, public IBatteryRecorder, public IBatteryProvider, public std::enable_shared_from_this { private: - static const uint32_t MovieFormatVersion = 2; + static const uint32_t MovieFormatVersion = 3; Emulator* _emu; string _filename; diff --git a/Core/Shared/RewindManager.cpp b/Core/Shared/RewindManager.cpp index df3927fdf..41ea5b209 100644 --- a/Core/Shared/RewindManager.cpp +++ b/Core/Shared/RewindManager.cpp @@ -253,6 +253,12 @@ void RewindManager::Stop() { if(_rewindState >= RewindState::Starting) { auto lock = _emu->AcquireLock(); + if(_rewindState < RewindState::Starting) { + //Rewind state was changed while waiting for the lock, stop processing + //Fixes crash when a power cycle occurs at the same time as rewind attempts to stop + return; + } + if(_rewindState == RewindState::Started) { //Move back to the save state containing the frame currently shown on the screen if(_historyBackup.size() > 1) {