mirror of
https://github.com/SourMesen/Mesen2.git
synced 2025-04-02 10:21:44 -04:00
This could potentially cause desyncs on subsequent rewinds, but most obviously it caused rewinding during movie playback to break movie playback entirely (because some inputs were handled by the rewind, while others were handled by the movie, causing the _deviceIndex value to be out of sync with its expected value)
486 lines
14 KiB
C++
486 lines
14 KiB
C++
#include "pch.h"
|
|
#include "Shared/RewindManager.h"
|
|
#include "Shared/MessageManager.h"
|
|
#include "Shared/Emulator.h"
|
|
#include "Shared/EmuSettings.h"
|
|
#include "Shared/Video/VideoRenderer.h"
|
|
#include "Shared/Audio/SoundMixer.h"
|
|
#include "Shared/BaseControlDevice.h"
|
|
#include "Shared/RenderedFrame.h"
|
|
#include "Shared/BaseControlManager.h"
|
|
|
|
RewindManager::RewindManager(Emulator* emu)
|
|
{
|
|
_emu = emu;
|
|
_settings = emu->GetSettings();
|
|
}
|
|
|
|
RewindManager::~RewindManager()
|
|
{
|
|
_settings->ClearFlag(EmulationFlags::MaximumSpeed);
|
|
_settings->ClearFlag(EmulationFlags::Rewind);
|
|
_emu->UnregisterInputProvider(this);
|
|
_emu->UnregisterInputRecorder(this);
|
|
}
|
|
|
|
void RewindManager::InitHistory()
|
|
{
|
|
Reset();
|
|
_emu->RegisterInputProvider(this);
|
|
_emu->RegisterInputRecorder(this);
|
|
AddHistoryBlock();
|
|
}
|
|
|
|
void RewindManager::Reset()
|
|
{
|
|
_emu->UnregisterInputProvider(this);
|
|
_emu->UnregisterInputRecorder(this);
|
|
_settings->ClearFlag(EmulationFlags::MaximumSpeed);
|
|
_settings->ClearFlag(EmulationFlags::Rewind);
|
|
ClearBuffer();
|
|
}
|
|
|
|
void RewindManager::ClearBuffer()
|
|
{
|
|
_hasHistory = false;
|
|
_history.clear();
|
|
_historyBackup.clear();
|
|
_framesToFastForward = 0;
|
|
_videoHistory.clear();
|
|
_videoHistoryBuilder.clear();
|
|
_audioHistory.clear();
|
|
_audioHistoryBuilder.clear();
|
|
_rewindState = RewindState::Stopped;
|
|
_currentHistory = {};
|
|
}
|
|
|
|
void RewindManager::ProcessNotification(ConsoleNotificationType type, void * parameter)
|
|
{
|
|
if(_emu->IsRunAheadFrame()) {
|
|
return;
|
|
}
|
|
|
|
if(type == ConsoleNotificationType::PpuFrameDone) {
|
|
_hasHistory = _history.size() >= 2;
|
|
if(_settings->GetPreferences().RewindBufferSize > 0) {
|
|
switch(_rewindState) {
|
|
case RewindState::Starting:
|
|
case RewindState::Started:
|
|
case RewindState::Debugging:
|
|
_currentHistory.FrameCount--;
|
|
break;
|
|
|
|
case RewindState::Stopping:
|
|
_framesToFastForward--;
|
|
_currentHistory.FrameCount++;
|
|
if(_framesToFastForward == 0) {
|
|
for(int i = 0; i < BaseControlDevice::PortCount; i++) {
|
|
size_t numberToRemove = _currentHistory.InputLogs[i].size();
|
|
_currentHistory.InputLogs[i] = _historyBackup.front().InputLogs[i];
|
|
for(size_t j = 0; j < numberToRemove; j++) {
|
|
_currentHistory.InputLogs[i].pop_back();
|
|
}
|
|
}
|
|
_historyBackup.clear();
|
|
_rewindState = RewindState::Stopped;
|
|
_settings->ClearFlag(EmulationFlags::Rewind);
|
|
_settings->ClearFlag(EmulationFlags::MaximumSpeed);
|
|
}
|
|
break;
|
|
|
|
case RewindState::Stopped:
|
|
_currentHistory.FrameCount++;
|
|
break;
|
|
}
|
|
} else {
|
|
ClearBuffer();
|
|
}
|
|
} else if(type == ConsoleNotificationType::StateLoaded) {
|
|
if(_rewindState == RewindState::Stopped) {
|
|
//A save state was loaded by the user, mark as the end of the current "segment" (for history viewer)
|
|
_currentHistory.EndOfSegment = true;
|
|
AddHistoryBlock();
|
|
}
|
|
}
|
|
}
|
|
|
|
RewindStats RewindManager::GetStats()
|
|
{
|
|
uint32_t memoryUsage = 0;
|
|
for(int i = (int)_history.size() - 1; i >= 0; i--) {
|
|
memoryUsage += _history[i].GetStateSize();
|
|
}
|
|
|
|
RewindStats stats = {};
|
|
stats.MemoryUsage = memoryUsage;
|
|
stats.HistorySize = (uint32_t)_history.size();
|
|
stats.HistoryDuration = stats.HistorySize * RewindManager::BufferSize;
|
|
return stats;
|
|
}
|
|
|
|
void RewindManager::AddHistoryBlock()
|
|
{
|
|
uint32_t maxHistorySize = _settings->GetPreferences().RewindBufferSize;
|
|
if(maxHistorySize > 0) {
|
|
uint32_t memoryUsage = 0;
|
|
for(int i = (int)_history.size() - 1; i >= 0; i--) {
|
|
memoryUsage += _history[i].GetStateSize();
|
|
if((memoryUsage >> 20) >= maxHistorySize) {
|
|
//Remove all old state data above the memory limit
|
|
for(int j = 0; j < i; j++) {
|
|
_history.pop_front();
|
|
}
|
|
|
|
while(_history.size() > 0 && !_history.front().IsFullState) {
|
|
//Remove everything until the next full state
|
|
_history.pop_front();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(_currentHistory.FrameCount > 0) {
|
|
_history.push_back(_currentHistory);
|
|
}
|
|
_currentHistory = RewindData();
|
|
_currentHistory.SaveState(_emu, _history);
|
|
}
|
|
}
|
|
|
|
void RewindManager::PopHistory()
|
|
{
|
|
if(_history.empty() && _currentHistory.FrameCount <= 0 && !IsStepBack()) {
|
|
StopRewinding();
|
|
} else {
|
|
if(_currentHistory.FrameCount <= 0 && !IsStepBack()) {
|
|
_currentHistory = _history.back();
|
|
_history.pop_back();
|
|
}
|
|
|
|
if(IsStepBack() && _currentHistory.FrameCount <= 1 && !_history.empty() && !_history.back().EndOfSegment) {
|
|
//Go back an extra frame to ensure step back works across 30-frame chunks
|
|
_historyBackup.push_front(_currentHistory);
|
|
_currentHistory = _history.back();
|
|
_history.pop_back();
|
|
}
|
|
|
|
_historyBackup.push_front(_currentHistory);
|
|
_currentHistory.LoadState(_emu, _history, -1, false);
|
|
|
|
if(!_audioHistoryBuilder.empty()) {
|
|
_audioHistory.insert(_audioHistory.begin(), _audioHistoryBuilder.begin(), _audioHistoryBuilder.end());
|
|
_audioHistoryBuilder.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
void RewindManager::Start(bool forDebugger)
|
|
{
|
|
if(_rewindState == RewindState::Stopped && _settings->GetPreferences().RewindBufferSize > 0) {
|
|
if(forDebugger) {
|
|
InternalStart(forDebugger);
|
|
} else {
|
|
auto lock = _emu->AcquireLock();
|
|
InternalStart(forDebugger);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RewindManager::InternalStart(bool forDebugger)
|
|
{
|
|
if(_history.empty() && _currentHistory.FrameCount <= 0 && !forDebugger) {
|
|
//No data in history, can't rewind
|
|
return;
|
|
}
|
|
|
|
_rewindState = forDebugger ? RewindState::Debugging : RewindState::Starting;
|
|
_videoHistoryBuilder.clear();
|
|
_videoHistory.clear();
|
|
_audioHistoryBuilder.clear();
|
|
_audioHistory.clear();
|
|
_historyBackup.clear();
|
|
|
|
PopHistory();
|
|
_emu->GetSoundMixer()->StopAudio(true);
|
|
_settings->SetFlag(EmulationFlags::MaximumSpeed);
|
|
_settings->SetFlag(EmulationFlags::Rewind);
|
|
}
|
|
|
|
void RewindManager::ForceStop(bool deleteFutureData)
|
|
{
|
|
if(_rewindState != RewindState::Stopped) {
|
|
if(deleteFutureData) {
|
|
//Step back reached its target - delete any future "history" beyond this
|
|
//Otherwise, subsequent step back/rewind operations won't work properly
|
|
RewindData orgHistory = _currentHistory;
|
|
int framesToRemove = _currentHistory.FrameCount;
|
|
if(!_historyBackup.empty()) {
|
|
_currentHistory = _historyBackup.front();
|
|
_currentHistory.FrameCount -= framesToRemove;
|
|
for(int i = 0; i < BaseControlDevice::PortCount; i++) {
|
|
for(int j = 0; j < orgHistory.InputLogs[i].size(); j++) {
|
|
if(!_currentHistory.InputLogs[i].empty()) {
|
|
_currentHistory.InputLogs[i].pop_back();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_historyBackup.clear();
|
|
|
|
if(!_videoHistory.empty()) {
|
|
//Update the frame on the screen to match the last frame generated during step back
|
|
//Needed to update the screen when stepping back to the previous frame
|
|
VideoFrame& frameData = _videoHistory.back();
|
|
RenderedFrame oldFrame(frameData.Data.data(), frameData.Width, frameData.Height, frameData.Scale, frameData.FrameNumber, frameData.InputData);
|
|
_emu->GetVideoRenderer()->UpdateFrame(oldFrame);
|
|
}
|
|
} else {
|
|
while(_historyBackup.size() > 1) {
|
|
_history.push_back(_historyBackup.front());
|
|
_historyBackup.pop_front();
|
|
}
|
|
_currentHistory = _historyBackup.front();
|
|
_historyBackup.clear();
|
|
}
|
|
|
|
_rewindState = RewindState::Stopped;
|
|
_settings->ClearFlag(EmulationFlags::MaximumSpeed);
|
|
_settings->ClearFlag(EmulationFlags::Rewind);
|
|
}
|
|
}
|
|
|
|
void RewindManager::Stop()
|
|
{
|
|
if(_rewindState >= RewindState::Starting) {
|
|
auto lock = _emu->AcquireLock();
|
|
if(_rewindState == RewindState::Started) {
|
|
//Move back to the save state containing the frame currently shown on the screen
|
|
if(_historyBackup.size() > 1) {
|
|
_framesToFastForward = (uint32_t)_videoHistory.size() + _historyBackup.front().FrameCount;
|
|
do {
|
|
_history.push_back(_historyBackup.front());
|
|
_framesToFastForward -= _historyBackup.front().FrameCount;
|
|
_historyBackup.pop_front();
|
|
|
|
_currentHistory = _historyBackup.front();
|
|
}
|
|
while(_framesToFastForward > RewindManager::BufferSize && _historyBackup.size() > 1);
|
|
}
|
|
} else {
|
|
//We started rewinding, but didn't actually visually rewind anything yet
|
|
//Move back to the save state containing the frame currently shown on the screen
|
|
while(_historyBackup.size() > 1) {
|
|
_history.push_back(_historyBackup.front());
|
|
_historyBackup.pop_front();
|
|
}
|
|
_currentHistory = _historyBackup.front();
|
|
_framesToFastForward = _historyBackup.front().FrameCount;
|
|
}
|
|
|
|
_currentHistory.LoadState(_emu, _history);
|
|
if(_framesToFastForward > 0) {
|
|
_rewindState = RewindState::Stopping;
|
|
_currentHistory.FrameCount = 0;
|
|
_settings->SetFlag(EmulationFlags::MaximumSpeed);
|
|
} else {
|
|
_rewindState = RewindState::Stopped;
|
|
_historyBackup.clear();
|
|
_settings->ClearFlag(EmulationFlags::MaximumSpeed);
|
|
_settings->ClearFlag(EmulationFlags::Rewind);
|
|
}
|
|
|
|
_videoHistoryBuilder.clear();
|
|
_videoHistory.clear();
|
|
_audioHistoryBuilder.clear();
|
|
_audioHistory.clear();
|
|
}
|
|
}
|
|
|
|
void RewindManager::ProcessEndOfFrame()
|
|
{
|
|
if(_rewindState >= RewindState::Starting) {
|
|
if(_currentHistory.FrameCount <= 0 && _rewindState != RewindState::Debugging) {
|
|
//If we're debugging, we want to keep running the emulation to the end of the next frame (even if it's incomplete)
|
|
//Otherwise the emulation might diverge due to missing inputs.
|
|
PopHistory();
|
|
} else if(_currentHistory.FrameCount == 0 && _rewindState == RewindState::Debugging) {
|
|
//Reached the end of the current 30-frame block, move to the next,
|
|
//the step back target cycle could be at the start of the next block
|
|
if(_historyBackup.size() > 1) {
|
|
_history.push_back(_historyBackup.front());
|
|
_historyBackup.pop_front();
|
|
_currentHistory = _historyBackup.front();
|
|
}
|
|
}
|
|
} else if(_currentHistory.FrameCount >= RewindManager::BufferSize) {
|
|
AddHistoryBlock();
|
|
}
|
|
}
|
|
|
|
void RewindManager::ProcessFrame(RenderedFrame& frame, bool forRewind)
|
|
{
|
|
if(_rewindState == RewindState::Starting || _rewindState == RewindState::Started) {
|
|
if(!forRewind) {
|
|
//Ignore any frames that occur between start of rewind process & first rewinded frame completed
|
|
//These are caused by the fact that VideoDecoder is asynchronous - a previous (extra) frame can end up
|
|
//in the rewind queue, which causes display glitches
|
|
return;
|
|
}
|
|
|
|
VideoFrame newFrame;
|
|
newFrame.Data = vector<uint32_t>((uint32_t*)frame.FrameBuffer, (uint32_t*)frame.FrameBuffer + frame.Width * frame.Height);
|
|
newFrame.Width = frame.Width;
|
|
newFrame.Height = frame.Height;
|
|
newFrame.Scale = frame.Scale;
|
|
newFrame.FrameNumber = frame.FrameNumber;
|
|
newFrame.InputData = frame.InputData;
|
|
_videoHistoryBuilder.push_back(newFrame);
|
|
|
|
if(_videoHistoryBuilder.size() == (size_t)_historyBackup.front().FrameCount) {
|
|
for(int i = (int)_videoHistoryBuilder.size() - 1; i >= 0; i--) {
|
|
_videoHistory.push_front(_videoHistoryBuilder[i]);
|
|
}
|
|
_videoHistoryBuilder.clear();
|
|
}
|
|
|
|
if(_rewindState == RewindState::Started || _videoHistory.size() >= RewindManager::BufferSize) {
|
|
_rewindState = RewindState::Started;
|
|
_settings->ClearFlag(EmulationFlags::MaximumSpeed);
|
|
if(!_videoHistory.empty()) {
|
|
VideoFrame &frameData = _videoHistory.back();
|
|
RenderedFrame oldFrame(frameData.Data.data(), frameData.Width, frameData.Height, frameData.Scale, frameData.FrameNumber, frameData.InputData);
|
|
_emu->GetVideoRenderer()->UpdateFrame(oldFrame);
|
|
_videoHistory.pop_back();
|
|
}
|
|
}
|
|
} else if(_rewindState == RewindState::Stopping) {
|
|
//Display nothing while resyncing
|
|
} else if(_rewindState == RewindState::Debugging) {
|
|
//Keep the last frame to be able to display it once step back reaches its target
|
|
VideoFrame newFrame;
|
|
newFrame.Data = vector<uint32_t>((uint32_t*)frame.FrameBuffer, (uint32_t*)frame.FrameBuffer + frame.Width * frame.Height);
|
|
newFrame.Width = frame.Width;
|
|
newFrame.Height = frame.Height;
|
|
newFrame.Scale = frame.Scale;
|
|
newFrame.FrameNumber = frame.FrameNumber;
|
|
newFrame.InputData = frame.InputData;
|
|
_videoHistory.clear();
|
|
_videoHistory.push_back(newFrame);
|
|
} else {
|
|
_emu->GetVideoRenderer()->UpdateFrame(frame);
|
|
}
|
|
}
|
|
|
|
bool RewindManager::ProcessAudio(int16_t * soundBuffer, uint32_t sampleCount)
|
|
{
|
|
if(_rewindState == RewindState::Starting || _rewindState == RewindState::Started) {
|
|
_audioHistoryBuilder.insert(_audioHistoryBuilder.end(), soundBuffer, soundBuffer + sampleCount * 2);
|
|
|
|
if(_rewindState == RewindState::Started && _audioHistory.size() > sampleCount * 2) {
|
|
for(uint32_t i = 0; i < sampleCount * 2; i++) {
|
|
soundBuffer[i] = _audioHistory.back();
|
|
_audioHistory.pop_back();
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
//Mute while we prepare to rewind
|
|
return false;
|
|
}
|
|
} else if(_rewindState == RewindState::Stopping || _rewindState == RewindState::Debugging) {
|
|
//Mute while we resync
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void RewindManager::RecordInput(vector<shared_ptr<BaseControlDevice>> devices)
|
|
{
|
|
if(_settings->GetPreferences().RewindBufferSize > 0 && _rewindState == RewindState::Stopped) {
|
|
for(shared_ptr<BaseControlDevice> &device : devices) {
|
|
_currentHistory.InputLogs[device->GetPort()].push_back(device->GetRawState());
|
|
}
|
|
}
|
|
}
|
|
|
|
bool RewindManager::SetInput(BaseControlDevice *device)
|
|
{
|
|
uint8_t port = device->GetPort();
|
|
if(IsRewinding()) {
|
|
if(!_currentHistory.InputLogs[port].empty()) {
|
|
ControlDeviceState state = _currentHistory.InputLogs[port].front();
|
|
_currentHistory.InputLogs[port].pop_front();
|
|
device->SetRawState(state);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void RewindManager::StartRewinding(bool forDebugger)
|
|
{
|
|
Start(forDebugger);
|
|
}
|
|
|
|
void RewindManager::StopRewinding(bool forDebugger, bool deleteFutureData)
|
|
{
|
|
if(forDebugger) {
|
|
ForceStop(deleteFutureData);
|
|
} else {
|
|
Stop();
|
|
}
|
|
}
|
|
|
|
bool RewindManager::IsRewinding()
|
|
{
|
|
return _rewindState != RewindState::Stopped;
|
|
}
|
|
|
|
bool RewindManager::IsStepBack()
|
|
{
|
|
return _rewindState == RewindState::Debugging;
|
|
}
|
|
|
|
void RewindManager::RewindSeconds(uint32_t seconds)
|
|
{
|
|
if(_rewindState == RewindState::Stopped) {
|
|
uint32_t removeCount = (seconds * 60 / RewindManager::BufferSize) + 1;
|
|
auto lock = _emu->AcquireLock();
|
|
|
|
for(uint32_t i = 0; i < removeCount; i++) {
|
|
if(!_history.empty()) {
|
|
_currentHistory = _history.back();
|
|
_history.pop_back();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
_currentHistory.LoadState(_emu, _history);
|
|
}
|
|
}
|
|
|
|
bool RewindManager::HasHistory()
|
|
{
|
|
return _hasHistory;
|
|
}
|
|
|
|
deque<RewindData> RewindManager::GetHistory()
|
|
{
|
|
deque<RewindData> history = _history;
|
|
history.push_back(_currentHistory);
|
|
return history;
|
|
}
|
|
|
|
void RewindManager::SendFrame(RenderedFrame& frame, bool forRewind)
|
|
{
|
|
ProcessFrame(frame, forRewind);
|
|
}
|
|
|
|
bool RewindManager::SendAudio(int16_t* soundBuffer, uint32_t sampleCount)
|
|
{
|
|
return ProcessAudio(soundBuffer, sampleCount);
|
|
}
|