From 52c7671f8299aa2177bd3d6bb8af548042d9df09 Mon Sep 17 00:00:00 2001 From: Sour Date: Fri, 8 May 2026 16:19:36 +0900 Subject: [PATCH 1/4] Movies: Fixed missing input recording/playback for the first frame after power cycle This didn't cause any obvious issues, because it would only matter if a significant button was pressed during the first frame. But when exporting a movie from the history viewer, the first frame's input was actually part of the .mmo file, causing every input to be offset by 1 frame when the movie was played back (which caused desyncs). The movie recording/playback was initialized too late, after the first frame's input was initialized, causing it to be skipped entirely. Using the new "AfterInitConsole" notification, which is triggered slightly earlier, instead of "GameLoaded" allows the first frame's input to be recorded/played properly This also fixes a rare emulator crash that could occur when you power cycle while letting go of the rewind button at the same time (that I triggered by rewinding while the movie was playing a frame that had a power cycle input) --- Core/Shared/Emulator.cpp | 2 + .../Shared/Interfaces/INotificationListener.h | 1 + Core/Shared/Movies/MesenMovie.cpp | 90 ++++++++++--------- Core/Shared/Movies/MesenMovie.h | 2 + Core/Shared/Movies/MovieRecorder.cpp | 3 +- Core/Shared/Movies/MovieRecorder.h | 2 +- Core/Shared/RewindManager.cpp | 6 ++ 7 files changed, 64 insertions(+), 42 deletions(-) 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..bc64c26e9 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,8 @@ bool MesenMovie::Play(VirtualFile& file) _reader.reset(new ZipReader()); _reader->LoadArchive(ss); - stringstream settingsData, inputData; - if(!_reader->GetStream("GameSettings.txt", settingsData)) { + stringstream inputData; + if(!_reader->GetStream("GameSettings.txt", _settingsData)) { MessageManager::Log("[Movie] File not found: GameSettings.txt"); return false; } @@ -124,17 +147,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 +156,40 @@ 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; + } else if(movieVersion == 2) { + //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({}); } + while(inputData) { + string line; + std::getline(inputData, line); + if(line.substr(0, 1) == "|") { + _inputData.push_back(StringUtilities::Split(line.substr(1), '|')); + } + } + + _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(); + _emu->GetBatteryManager()->SetBatteryProvider(nullptr); - //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; - } - } - - _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..8f5dacec4 100644 --- a/Core/Shared/Movies/MesenMovie.h +++ b/Core/Shared/Movies/MesenMovie.h @@ -27,9 +27,11 @@ 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; private: void ParseSettings(stringstream& data); diff --git a/Core/Shared/Movies/MovieRecorder.cpp b/Core/Shared/Movies/MovieRecorder.cpp index 9969c09bf..223f1459a 100644 --- a/Core/Shared/Movies/MovieRecorder.cpp +++ b/Core/Shared/Movies/MovieRecorder.cpp @@ -68,6 +68,7 @@ bool MovieRecorder::Record(RecordMovieOptions options) _hasSaveState = true; } + _emu->GetBatteryManager()->SetBatteryProvider(nullptr); _emu->GetBatteryManager()->SetBatteryRecorder(nullptr); _emu->Unlock(); @@ -189,7 +190,7 @@ vector MovieRecorder::LoadBattery(string extension) void MovieRecorder::ProcessNotification(ConsoleNotificationType type, void* parameter) { - if(type == ConsoleNotificationType::GameLoaded) { + if(type == ConsoleNotificationType::AfterInitConsole) { _emu->RegisterInputRecorder(this); } } 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) { From 45d884aad6bacb74f313ddef56c4ba6750198def Mon Sep 17 00:00:00 2001 From: Sour Date: Sat, 9 May 2026 23:49:06 +0900 Subject: [PATCH 2/4] Fixed more desync issues (when recording movies from "current state", and when exporting movies from the history viewer) --- Core/Shared/Movies/MesenMovie.cpp | 5 +++++ Core/Shared/Movies/MesenMovie.h | 1 + Core/Shared/Movies/MovieRecorder.cpp | 22 +++++++++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Core/Shared/Movies/MesenMovie.cpp b/Core/Shared/Movies/MesenMovie.cpp index bc64c26e9..f7390458b 100644 --- a/Core/Shared/Movies/MesenMovie.cpp +++ b/Core/Shared/Movies/MesenMovie.cpp @@ -115,6 +115,8 @@ void MesenMovie::ProcessNotification(ConsoleNotificationType type, void* paramet if(_reader->GetStream("SaveState.mss", saveStateData)) { if(!_emu->GetSaveStateManager()->LoadState(saveStateData)) { _loadFailure = true; + } else { + _hasSaveState = true; } } } @@ -187,6 +189,9 @@ bool MesenMovie::Play(VirtualFile& file) _emu->GetNotificationManager()->RegisterNotificationListener(shared_from_this()); _emu->PowerCycle(); _emu->GetBatteryManager()->SetBatteryProvider(nullptr); + if(_hasSaveState) { + _controlManager->SetPollCounter(0); + } _playing = !_loadFailure; return _playing; diff --git a/Core/Shared/Movies/MesenMovie.h b/Core/Shared/Movies/MesenMovie.h index 8f5dacec4..d3ec877f3 100644 --- a/Core/Shared/Movies/MesenMovie.h +++ b/Core/Shared/Movies/MesenMovie.h @@ -32,6 +32,7 @@ class MesenMovie final : public IMovie, public INotificationListener, public IBa 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 223f1459a..06b7c6142 100644 --- a/Core/Shared/Movies/MovieRecorder.cpp +++ b/Core/Shared/Movies/MovieRecorder.cpp @@ -66,6 +66,9 @@ 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); @@ -217,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 i = 0; i < BaseControlDevice::PortCount; i++) { + len = std::max(len, (uint32_t)rewindData.InputLogs[i].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()) { @@ -229,6 +247,8 @@ bool MovieRecorder::CreateMovie(string movieFile, deque& data, uint3 } _inputData << "\n"; } + + skipFirstInput = false; } //Write the movie file From e2affe41909b38ce51cad4470938840567942dc7 Mon Sep 17 00:00:00 2001 From: Sour Date: Sun, 10 May 2026 00:07:57 +0900 Subject: [PATCH 3/4] Fixed warning --- Core/Shared/Movies/MovieRecorder.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Shared/Movies/MovieRecorder.cpp b/Core/Shared/Movies/MovieRecorder.cpp index 06b7c6142..ef869e887 100644 --- a/Core/Shared/Movies/MovieRecorder.cpp +++ b/Core/Shared/Movies/MovieRecorder.cpp @@ -233,8 +233,8 @@ bool MovieRecorder::CreateMovie(string movieFile, deque& data, uint3 //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 i = 0; i < BaseControlDevice::PortCount; i++) { - len = std::max(len, (uint32_t)rewindData.InputLogs[i].size()); + 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++) { From ead788766f5cdf4b7600d40c1013227432fb64ec Mon Sep 17 00:00:00 2001 From: Sour Date: Mon, 11 May 2026 01:06:29 +0900 Subject: [PATCH 4/4] Fixed backward-compatibility with movies recorded before these fixes --- Core/Shared/Movies/MesenMovie.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Core/Shared/Movies/MesenMovie.cpp b/Core/Shared/Movies/MesenMovie.cpp index f7390458b..5bc59e4b0 100644 --- a/Core/Shared/Movies/MesenMovie.cpp +++ b/Core/Shared/Movies/MesenMovie.cpp @@ -115,8 +115,6 @@ void MesenMovie::ProcessNotification(ConsoleNotificationType type, void* paramet if(_reader->GetStream("SaveState.mss", saveStateData)) { if(!_emu->GetSaveStateManager()->LoadState(saveStateData)) { _loadFailure = true; - } else { - _hasSaveState = true; } } } @@ -139,6 +137,8 @@ bool MesenMovie::Play(VirtualFile& file) _reader.reset(new ZipReader()); _reader->LoadArchive(ss); + _hasSaveState = _reader->CheckFile("SaveState.mss"); + stringstream inputData; if(!_reader->GetStream("GameSettings.txt", _settingsData)) { MessageManager::Log("[Movie] File not found: GameSettings.txt"); @@ -162,18 +162,20 @@ bool MesenMovie::Play(VirtualFile& file) if(movieVersion < 2) { MessageManager::DisplayMessage("Movies", "MovieIncompatibleVersion"); return false; - } else if(movieVersion == 2) { - //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({}); } while(inputData) { string line; std::getline(inputData, line); if(line.substr(0, 1) == "|") { - _inputData.push_back(StringUtilities::Split(line.substr(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); } }