input-rec: refactor main input recording class

This commit is contained in:
Tyler Wilding 2022-06-15 20:46:21 -04:00 committed by refractionpcsx2
parent 9e30fa81de
commit c5298cf12d
2 changed files with 308 additions and 371 deletions

View File

@ -465,20 +465,29 @@ wxString InputRecording::resolveGameName()
#else
#include "InputRecording.h"
#include "InputRecordingControls.h"
#include "Utilities/InputRecordingLogger.h"
#include "common/FileSystem.h"
#include "common/StringUtil.h"
#include "SaveState.h"
#include "Counters.h"
#include "SaveState.h"
#include "VMManager.h"
#include "DebugTools/Debug.h"
#include "GameDatabase.h"
#include "fmt/format.h"
#include "InputRecording.h"
#include "InputRecordingControls.h"
#include "Utilities/InputRecordingLogger.h"
// Future TODOs
// - restart
// - tooltips on GUI options
// - Controller Logging (virtual pad related)
// - persist last browsed IR path
// - logs are weirdly formatted
// - force OSD updates since a lot of input recording occurs during a paused state
// - differentiating OSD logs somehow would be nice (color / a preceding icon?)
#include <queue>
#include <fmt/format.h>
@ -489,42 +498,107 @@ void SaveStateBase::InputRecordingFreeze()
// CHANGING THIS WILL BREAK BACKWARDS COMPATIBILITY ON SAVESTATES
FreezeTag("InputRecording");
Freeze(g_FrameCount);
// Loading a save-state is an asynchronous task. If we are playing a recording
// that starts from a savestate (not power-on) and the starting (pcsx2 internal) frame
// marker has not been set (which comes from the save-state), we initialize it.
if (g_InputRecording.IsInitialLoad())
g_InputRecording.SetupInitialState(g_FrameCount);
else if (g_InputRecording.IsActive() && IsLoading())
g_InputRecording.SetFrameCounter(g_FrameCount);
}
InputRecording g_InputRecording;
InputRecording::InputRecordingPad::InputRecordingPad()
bool InputRecording::create(const std::string_view& fileName, const bool fromSaveState, const std::string_view& authorName)
{
padData = new PadData;
}
InputRecording::InputRecordingPad::~InputRecordingPad()
{
delete padData;
}
void InputRecording::RecordingReset()
{
// Booting is an asynchronous task. If we are playing a recording
// that starts from power-on and the starting (pcsx2 internal) frame
// marker has not been set, we initialize it.
if (g_InputRecording.IsInitialLoad())
g_InputRecording.SetupInitialState(0);
else if (g_InputRecording.IsActive())
if (!m_file.OpenNew(fileName, fromSaveState))
{
g_InputRecording.SetFrameCounter(0);
g_InputRecordingControls.Lock(0);
return false;
}
m_controls.setRecordMode();
if (fromSaveState)
{
std::string savestatePath = fmt::format("{}_SaveState.p2s", fileName);
if (FileSystem::FileExists(savestatePath.c_str()))
{
FileSystem::CopyFilePath(savestatePath.c_str(), fmt::format("{}.bak", savestatePath).c_str(), true);
}
m_initialSavestateLoadComplete = false;
m_type = Type::FROM_SAVESTATE;
m_isActive = true;
// TODO - error handling
VMManager::SaveState(savestatePath.c_str());
}
else
g_InputRecordingControls.Resume();
{
m_startingFrame = 0;
m_type = Type::POWER_ON;
m_isActive = true;
// TODO - should this be an explicit [full] boot instead of a reset?
VMManager::Reset();
}
m_file.getHeader().SetEmulatorVersion();
m_file.getHeader().SetAuthor(authorName);
m_file.getHeader().SetGameName(resolveGameName());
m_file.WriteHeader();
initializeState();
InputRec::log("Started new input recording");
InputRec::consoleLog(fmt::format("Filename {}", m_file.getFilename()));
return true;
}
bool InputRecording::play(const std::string_view& filename)
{
if (!m_file.OpenExisting(filename))
{
return false;
}
// Either load the savestate, or restart the game
if (m_file.FromSaveState())
{
std::string savestatePath = fmt::format("{}_SaveState.p2s", m_file.getFilename());
if (!FileSystem::FileExists(savestatePath.c_str()))
{
InputRec::consoleLog(fmt::format("Could not locate savestate file at location - {}", savestatePath));
InputRec::log("Savestate load failed");
m_file.Close();
return false;
}
m_type = Type::FROM_SAVESTATE;
m_initialSavestateLoadComplete = false;
m_isActive = true;
const auto loaded = VMManager::LoadState(savestatePath.c_str());
if (!loaded)
{
InputRec::log("Savestate load failed, unsupported version?");
m_file.Close();
m_isActive = false;
return false;
}
}
else
{
m_startingFrame = 0;
m_type = Type::POWER_ON;
m_isActive = true;
// TODO - should this be an explicit [full] boot instead of a reset?
VMManager::Reset();
}
m_controls.setReplayMode();
initializeState();
InputRec::log("Replaying input recording");
m_file.logRecordingMetadata();
if (resolveGameName() != m_file.getHeader().gameName)
{
InputRec::consoleLog(fmt::format("Input recording was possibly constructed for a different game. Expected: {}, Actual: {}", m_file.getHeader().gameName, resolveGameName()));
}
return true;
}
void InputRecording::stop()
{
m_isActive = false;
if (m_file.Close())
{
InputRec::log("Input recording stopped");
}
}
// TODO: Refactor this
@ -538,278 +612,42 @@ void InputRecording::ControllerInterrupt(u8 port, size_t fifoSize, u8 dataIn, u8
if (dataOut != READ_DATA_AND_VIBRATE_SECOND_BYTE)
fInterruptFrame = false;
}
else if (fInterruptFrame)
// If there is data to read (previous two bytes looked correct)
if (bufCount >= 3 && m_padDataAvailable)
{
u8& bufVal = dataOut;
const u16 bufIndex = fifoSize - 3;
if (state == InputRecordingMode::Replaying)
{
if (frameCounter >= 0 && frameCounter < INT_MAX)
if (!m_file.ReadKeyBuffer(bufVal, m_frameCounter, port, bufIndex))
{
if (!inputRecordingData.ReadKeyBuffer(bufVal, frameCounter, port, bufIndex))
inputRec::consoleLog(fmt::format("Failed to read input data at frame {}", frameCounter));
// Update controller data state for future VirtualPad / logging usage.
pads[port].padData->UpdateControllerData(bufIndex, bufVal);
InputRec::consoleLog(fmt::format("Failed to read input data at frame {}", m_frameCounter));
}
// Update controller data state for future VirtualPad / logging usage.
//pads[port].padData->UpdateControllerData(bufIndex, bufVal);
}
else
{
// Update controller data state for future VirtualPad / logging usage.
pads[port].padData->UpdateControllerData(bufIndex, bufVal);
//pads[port].padData->UpdateControllerData(bufIndex, bufVal);
// Commit the byte to the movie file if we are recording
if (state == InputRecordingMode::Recording)
if (m_controls.isRecording())
{
if (frameCounter >= 0)
if (!m_file.WriteKeyBuffer(m_frameCounter, port, bufIndex, bufVal))
{
if (incrementUndo)
{
inputRecordingData.IncrementUndoCount();
incrementUndo = false;
}
if (frameCounter < INT_MAX && !inputRecordingData.WriteKeyBuffer(frameCounter, port, bufIndex, bufVal))
inputRec::consoleLog(fmt::format("Failed to write input data at frame {}", frameCounter));
InputRec::consoleLog(fmt::format("Failed to write input data at frame {}", m_frameCounter));
}
}
}
}
}
s32 InputRecording::GetFrameCounter()
{
return frameCounter;
}
InputRecordingFile& InputRecording::GetInputRecordingData()
{
return inputRecordingData;
}
u32 InputRecording::GetStartingFrame()
{
return startingFrame;
}
void InputRecording::IncrementFrameCounter()
{
if (frameCounter < INT_MAX)
if (bufCount > 20)
{
frameCounter++;
switch (state)
{
case InputRecordingMode::Recording:
inputRecordingData.SetTotalFrames(frameCounter);
[[fallthrough]];
case InputRecordingMode::Replaying:
if (frameCounter == inputRecordingData.GetTotalFrames())
incrementUndo = false;
default:
break;
}
m_padDataAvailable = false;
}
}
void InputRecording::LogAndRedraw()
{
for (u8 port = 0; port < 2; port++)
{
pads[port].padData->LogPadData(port);
}
}
bool InputRecording::IsInterruptFrame()
{
return fInterruptFrame;
}
bool InputRecording::IsActive()
{
return state != InputRecordingMode::NotActive;
}
bool InputRecording::IsInitialLoad()
{
return initialLoad;
}
bool InputRecording::IsReplaying()
{
return state == InputRecordingMode::Replaying;
}
bool InputRecording::IsRecording()
{
return state == InputRecordingMode::Recording;
}
void InputRecording::SetToRecordMode()
{
state = InputRecordingMode::Recording;
inputRec::log("Record mode ON");
}
void InputRecording::SetToReplayMode()
{
state = InputRecordingMode::Replaying;
inputRec::log("Replay mode ON");
}
void InputRecording::SetFrameCounter(u32 newGFrameCount)
{
if (newGFrameCount > startingFrame + (u32)inputRecordingData.GetTotalFrames())
{
inputRec::consoleLog("Warning, you've loaded PCSX2 emulation to a point after the end of the original recording. This should be avoided.");
inputRec::consoleLog("Savestate's framecount has been ignored.");
frameCounter = inputRecordingData.GetTotalFrames();
if (state == InputRecordingMode::Replaying)
SetToRecordMode();
incrementUndo = false;
}
else
{
if (newGFrameCount < startingFrame)
{
inputRec::consoleLog("Warning, you've loaded PCSX2 emulation to a point before the start of the original recording. This should be avoided.");
if (state == InputRecordingMode::Recording)
SetToReplayMode();
}
else if (newGFrameCount == 0 && state == InputRecordingMode::Recording)
SetToReplayMode();
frameCounter = newGFrameCount - (s32)startingFrame;
incrementUndo = true;
}
}
void InputRecording::SetupInitialState(u32 newStartingFrame)
{
startingFrame = newStartingFrame;
if (state != InputRecordingMode::Replaying)
{
inputRec::log("Started new input recording");
inputRec::consoleLog(fmt::format("Filename {}", inputRecordingData.GetFilename()));
SetToRecordMode();
}
else
{
// Check if the current game matches with the one used to make the original recording
// TODO - Vaser - this should be the CRC in hindsight anyway
if (!VMManager::GetDiscPath().empty())
if (resolveGameName() != inputRecordingData.GetHeader().gameName)
inputRec::consoleLog("Input recording was possibly constructed for a different game.");
incrementUndo = true;
inputRec::log("Replaying input recording");
inputRec::consoleMultiLog({fmt::format("File: {}", inputRecordingData.GetFilename()),
fmt::format("PCSX2 Version Used: {}", std::string(inputRecordingData.GetHeader().emu)),
fmt::format("Recording File Version: {}", inputRecordingData.GetHeader().version),
fmt::format("Associated Game Name or ISO Filename: {}", std::string(inputRecordingData.GetHeader().gameName)),
fmt::format("Author: {}", inputRecordingData.GetHeader().author),
fmt::format("Total Frames: {}", inputRecordingData.GetTotalFrames()),
fmt::format("Undo Count: {}", inputRecordingData.GetUndoCount())});
SetToReplayMode();
}
if (inputRecordingData.FromSaveState())
inputRec::consoleLog(fmt::format("Internal Starting Frame: {}", startingFrame));
frameCounter = 0;
initialLoad = false;
g_InputRecordingControls.Lock(startingFrame);
}
void InputRecording::FailedSavestate()
{
inputRec::consoleLog(fmt::format("{} is not compatible with this version of PCSX2", savestate));
inputRec::consoleLog(fmt::format("Original PCSX2 version used: {}", inputRecordingData.GetHeader().emu));
inputRecordingData.Close();
initialLoad = false;
state = InputRecordingMode::NotActive;
g_InputRecordingControls.Resume();
}
void InputRecording::Stop()
{
state = InputRecordingMode::NotActive;
incrementUndo = false;
if (inputRecordingData.Close())
inputRec::log("Input recording stopped");
}
bool InputRecording::Create(const std::string_view& fileName, const bool fromSaveState, const std::string_view& authorName)
{
if (!inputRecordingData.OpenNew(fileName, fromSaveState))
return false;
initialLoad = true;
state = InputRecordingMode::Recording;
if (fromSaveState)
{
savestate = fmt::format("{}_SaveState.p2s", fileName);
if (FileSystem::FileExists(savestate.c_str()))
{
FileSystem::CopyFilePath(savestate.c_str(), fmt::format("{}.bak", savestate).c_str(), true);
}
VMManager::SaveState(savestate.c_str());
}
else
{
// Vaser - CHECK - don't need to specify a source anymore? (cdvd/etc?)
VMManager::Execute();
}
// Set emulator version
inputRecordingData.GetHeader().SetEmulatorVersion();
// Set author name
if (!authorName.empty())
inputRecordingData.GetHeader().SetAuthor(authorName);
// Set Game Name
inputRecordingData.GetHeader().SetGameName(resolveGameName());
// Write header contents
inputRecordingData.WriteHeader();
return true;
}
bool InputRecording::Play(const std::string_view& filename)
{
if (!inputRecordingData.OpenExisting(filename))
return false;
// Either load the savestate, or restart the game
if (inputRecordingData.FromSaveState())
{
// TODO - Vaser - VM State is atomic, be careful.
if (VMManager::GetState() != VMState::Running && VMManager::GetState() != VMState::Paused)
{
inputRec::consoleLog("Game is not open, aborting playing input recording which starts on a save-state.");
inputRecordingData.Close();
return false;
}
savestate = fmt::format("{}_SaveState.p2s", inputRecordingData.GetFilename());
if (!FileSystem::FileExists(savestate.c_str()))
{
inputRec::consoleLog(fmt::format("Could not locate savestate file at location - {}", savestate));
inputRec::log("Savestate load failed");
inputRecordingData.Close();
return false;
}
state = InputRecordingMode::Replaying;
initialLoad = true;
VMManager::LoadState(savestate.c_str());
}
else
{
state = InputRecordingMode::Replaying;
initialLoad = true;
// Vaser - CHECK - don't need to specify a source anymore? (cdvd/etc?)
VMManager::Execute();
}
return true;
}
std::string InputRecording::resolveGameName()
{
std::string gameName;
@ -819,11 +657,160 @@ std::string InputRecording::resolveGameName()
auto game = GameDatabase::findGame(gameKey);
if (game)
{
gameName = game->name;
gameName += " (" + game->region + ")";
gameName = fmt::format("{} ({})", game->name, game->region);
}
}
return !gameName.empty() ? gameName : VMManager::GetGameName();
}
void InputRecording::incFrameCounter()
{
if (m_frameCounter >= std::numeric_limits<u64>::max())
{
// TODO - log the incredible achievment of playing for longer than 4 billion years, and end the recording
stop();
return;
}
m_frameCounter++;
if (m_controls.isReplaying())
{
// If we've reached the end of the recording while replaying, pause
if (m_frameCounter == m_file.getTotalFrames() - 1)
{
VMManager::SetPaused(true);
// Can also stop watching for re-records, they've watched to the end of the recording
m_watchingForRerecords = false;
}
}
if (m_controls.isRecording())
{
m_file.SetTotalFrames(m_frameCounter);
// If we've been in record mode and moved to the next frame, we've overrote something
// if this was following a save-state loading, this is considered a re-record, a.k.a an undo
if (m_watchingForRerecords)
{
m_file.IncrementUndoCount();
m_watchingForRerecords = false;
}
}
}
u64 InputRecording::getFrameCounter() const
{
return m_frameCounter;
}
bool InputRecording::isInitialSavestateLoadComplete() const
{
return m_initialSavestateLoadComplete;
}
bool InputRecording::isActive() const
{
return m_isActive;
}
void InputRecording::handleExceededFrameCounter()
{
// if we go past the end, switch to recording mode so nothing is lost
if (m_frameCounter >= m_file.getTotalFrames() && m_controls.isReplaying())
{
m_controls.setRecordMode();
}
}
void InputRecording::handleLoadingSavestate()
{
// We need to keep track of the starting internal frame of the recording
// - For a power-on recording this should already be done - it starts at 0
// - For save state recordings, this is stored inside the initial save-state
//
// Why?
// - When you re-record you load another save-state which has it's own frame counter
// stored within, we use this to adjust the frame we are replaying/recording to
if (isTypeSavestate() && !isInitialSavestateLoadComplete())
{
setStartingFrame(g_FrameCount);
setInitialSavestateLoaded();
}
else
{
adjustFrameCounterOnReRecord(g_FrameCount);
watchForRerecords();
}
}
bool InputRecording::isTypeSavestate() const
{
return m_type == Type::FROM_SAVESTATE;
}
void InputRecording::setStartingFrame(u64 startingFrame)
{
if (m_type == Type::POWER_ON)
{
return;
}
InputRec::consoleLog(fmt::format("Internal Starting Frame: {}", startingFrame));
m_startingFrame = startingFrame;
}
void InputRecording::setInitialSavestateLoaded()
{
m_initialSavestateLoadComplete = true;
}
void InputRecording::adjustFrameCounterOnReRecord(u64 newFrameCounter)
{
if (newFrameCounter > m_startingFrame + (u64)m_file.getTotalFrames())
{
InputRec::consoleLog("Warning, you've loaded PCSX2 emulation to a point after the end of the original recording. This should be avoided.");
InputRec::consoleLog("Savestate's framecount has been ignored, using the max length of the recording instead.");
m_frameCounter = m_file.getTotalFrames();
if (getControls().isReplaying())
{
getControls().setRecordMode();
}
return;
}
if (newFrameCounter < m_startingFrame)
{
InputRec::consoleLog("Warning, you've loaded PCSX2 emulation to a point before the start of the original recording. This should be avoided.");
InputRec::consoleLog("Savestate's framecount has been ignored, starting from the beginning in replay mode.");
m_frameCounter = 0;
if (getControls().isRecording())
{
getControls().setReplayMode();
}
return;
}
else if (newFrameCounter == 0 && getControls().isRecording())
{
getControls().setReplayMode();
}
m_frameCounter = newFrameCounter - m_startingFrame;
}
void InputRecording::watchForRerecords()
{
m_watchingForRerecords = true;
}
InputRecordingControls& InputRecording::getControls()
{
return m_controls;
}
const InputRecordingFile& InputRecording::getData() const
{
return m_file;
}
void InputRecording::initializeState()
{
m_frameCounter = 0;
m_watchingForRerecords = false;
}
#endif

View File

@ -151,6 +151,7 @@ extern InputRecording g_InputRecording;
#else
#include "Recording/InputRecordingFile.h"
#include "Recording/InputRecordingControls.h"
class InputRecording
{
@ -160,109 +161,58 @@ public:
POWER_ON,
FROM_SAVESTATE
};
bool create(const std::string_view& filename, const bool fromSaveState, const std::string_view& authorName);
bool play(const std::string_view& path);
void stop();
// Save or load PCSX2's global frame counter (g_FrameCount) along with each full/fast boot
//
// This is to prevent any inaccuracy issues caused by having a different
// internal emulation frame count than what it was at the beginning of the
// original recording
void RecordingReset();
void controllerInterrupt(u8& data, u8& port, u16& BufCount, u8 buf[]);
void incFrameCounter();
u64 getFrameCounter() const;
bool isInitialSavestateLoadComplete() const;
bool isActive() const;
// Main handler for ingesting input data and either saving it to the recording file (recording)
// or mutating it to the contents of the recording file (replaying)
void ControllerInterrupt(u8 port, size_t fifoSize, u8 dataIn, u8 dataOut);
void handleExceededFrameCounter();
void handleLoadingSavestate();
// The running frame counter for the input recording
s32 GetFrameCounter();
bool isTypeSavestate() const;
InputRecordingFile& GetInputRecordingData();
void setStartingFrame(u64 startingFrame);
void setInitialSavestateLoaded();
void adjustFrameCounterOnReRecord(u64 newFrameCounter);
// The internal PCSX2 g_FrameCount value on the first frame of the recording
u32 GetStartingFrame();
void watchForRerecords();
void IncrementFrameCounter();
// DEPRECATED: Slated for removal
// If the current frame contains controller / input data
bool IsInterruptFrame();
// If there is currently an input recording being played back or actively being recorded
bool IsActive();
// Whether or not the recording's initial state has yet to be loaded or saved and
// the rest of the recording can be initialized
// This is not applicable to recordings from a "power-on" state
bool IsInitialLoad();
// If there is currently an input recording being played back
bool IsReplaying();
// If there are inputs currently being recorded to a file
bool IsRecording();
// Sets input recording to Record Mode
void SetToRecordMode();
// Sets input recording to Replay Mode
void SetToReplayMode();
// Set the running frame counter for the input recording to an arbitrary value
void SetFrameCounter(u32 newGFrameCount);
// Sets up all values and prints console logs pertaining to the start of a recording
void SetupInitialState(u32 newStartingFrame);
/// Functions called from GUI
// Create a new input recording file
bool Create(const std::string_view& filename, const bool fromSaveState, const std::string_view& authorName);
// Play an existing input recording from a file
// TODO - Vaser - Calls a file dialog if it fails to locate the default base savestate
bool Play(const std::string_view& path);
// Stop the active input recording
void Stop();
// Logs the padData and redraws the virtualPad windows of active pads
void LogAndRedraw();
// Resets a recording if the base savestate could not be loaded at the start
void FailedSavestate();
InputRecordingControls& getControls();
const InputRecordingFile& getData() const;
private:
enum class InputRecordingMode
{
NotActive,
Recording,
Replaying,
};
// - https://github.com/PCSX2/pcsx2/blob/7db9627ff6986c2d3faeecc58525a0e32da2f29f/pcsx2/PAD/Windows/PAD.cpp#L1141
static const u8 READ_DATA_AND_VIBRATE_QUERY_FIRST_BYTE = 0x42;
// - https://github.com/PCSX2/pcsx2/blob/7db9627ff6986c2d3faeecc58525a0e32da2f29f/pcsx2/PAD/Windows/PAD.cpp#L1142
static const u8 READ_DATA_AND_VIBRATE_QUERY_SECOND_BYTE = 0x5A;
static const int CONTROLLER_PORT_ONE = 0;
static const int CONTROLLER_PORT_TWO = 1;
InputRecordingControls m_controls;
InputRecordingFile m_file;
// 0x42 is the magic number to indicate the default controller read query
// See - PAD.cpp::PADpoll - https://github.com/PCSX2/pcsx2/blob/master/pcsx2/PAD/Windows/PAD.cpp#L1255
static const u8 READ_DATA_AND_VIBRATE_FIRST_BYTE = 0x42;
// 0x5A is always the second byte in the buffer when the normal READ_DATA_AND_VIBRATE (0x42) query is executed.
// See - PAD.cpp::PADpoll - https://github.com/PCSX2/pcsx2/blob/master/pcsx2/PAD/Windows/PAD.cpp#L1256
static const u8 READ_DATA_AND_VIBRATE_SECOND_BYTE = 0x5A;
Type m_type;
// DEPRECATED: Slated for removal
bool fInterruptFrame = false;
InputRecordingFile inputRecordingData;
bool initialLoad = false;
u32 startingFrame = 0;
s32 frameCounter = 0;
bool incrementUndo = false;
InputRecordingMode state = InputRecording::InputRecordingMode::NotActive;
std::string savestate;
bool m_initialSavestateLoadComplete = false;
bool m_isActive = false;
bool m_padDataAvailable = false;
bool m_watchingForRerecords = false;
// Array of usable pads (currently, only 2)
struct InputRecordingPad
{
// Controller Data
PadData* padData;
InputRecordingPad();
~InputRecordingPad();
} pads[2];
u64 m_frameCounter = 0;
// Either 0 for a power-on movie, or the g_FrameCount that is stored on the starting frame
u64 m_startingFrame = 0;
void initializeState();
private:
// Resolve the name and region of the game currently loaded using the GameDB
// If the game cannot be found in the DB, the fallback is the ISO filename
std::string resolveGameName();