made savestates synchronous and immediate. this allows saving or loading while the emulator is paused, fixes issues where savestate hotkeys would get ignored if pressed too close together, might speed up savestates in some cases, and hopefully makes savestates more stable too.

the intent is to replace the haphazard scheduling and finger-crossing associated with saving/loading with the correct and minimal necessary wait for each thread to reach a known safe location before commencing the savestate operation, and for any already-paused components to not need to be resumed to do so.
This commit is contained in:
nitsuja
2011-12-30 20:16:12 -08:00
committed by skidau
parent 108f69eaa9
commit a81631b58e
33 changed files with 518 additions and 323 deletions

View File

@ -55,11 +55,7 @@ static unsigned char __LZO_MMODEL out[OUT_LEN];
static HEAP_ALLOC(wrkmem, LZO1X_1_MEM_COMPRESS);
static volatile bool g_op_in_progress = false;
static int ev_FileSave, ev_BufferSave, ev_FileLoad, ev_BufferLoad, ev_FileVerify, ev_BufferVerify;
static std::string g_current_filename, g_last_filename;
static std::string g_last_filename;
static CallbackFunc g_onAfterLoadCb = NULL;
@ -68,16 +64,14 @@ static std::vector<u8> g_undo_load_buffer;
static std::vector<u8> g_current_buffer;
static int g_loadDepth = 0;
static std::mutex g_cs_undo_load_buffer;
static std::mutex g_cs_current_buffer;
static Common::Event g_compressAndDumpStateSyncEvent;
static std::thread g_save_thread;
static const u8 NUM_HOOKS = 2;
static u8 waiting;
static u8 waitingslot;
static u64 lastCheckedStates[NUM_HOOKS];
static u8 hook;
// Don't forget to increase this after doing changes on the savestate system
static const int STATE_VERSION = 7;
static const int STATE_VERSION = 8;
struct StateHeader
{
@ -121,8 +115,6 @@ void DoState(PointerWrap &p)
p.DoMarker("Version");
// Begin with video backend, so that it gets a chance to clear it's caches and writeback modified things to RAM
// Pause the video thread in multi-threaded mode
g_video_backend->RunLoop(false);
g_video_backend->DoState(p);
p.DoMarker("video_backend");
@ -138,74 +130,78 @@ void DoState(PointerWrap &p)
p.DoMarker("CoreTiming");
Movie::DoState(p);
p.DoMarker("Movie");
// Resume the video thread
g_video_backend->RunLoop(true);
}
void ResetCounters()
void LoadFromBuffer(std::vector<u8>& buffer)
{
for (int i = 0; i < NUM_HOOKS; ++i)
lastCheckedStates[i] = CoreTiming::GetTicks();
}
bool wasUnpaused = Core::PauseAndLock(true);
void LoadBufferStateCallback(u64 userdata, int cyclesLate)
{
u8* ptr = &g_current_buffer[0];
u8* ptr = &buffer[0];
PointerWrap p(&ptr, PointerWrap::MODE_READ);
DoState(p);
Core::DisplayMessage("Loaded state", 2000);
g_op_in_progress = false;
Core::PauseAndLock(false, wasUnpaused);
}
void SaveBufferStateCallback(u64 userdata, int cyclesLate)
void SaveToBuffer(std::vector<u8>& buffer)
{
bool wasUnpaused = Core::PauseAndLock(true);
u8* ptr = NULL;
PointerWrap p(&ptr, PointerWrap::MODE_MEASURE);
DoState(p);
const size_t buffer_size = reinterpret_cast<size_t>(ptr);
g_current_buffer.resize(buffer_size);
ptr = &g_current_buffer[0];
buffer.resize(buffer_size);
ptr = &buffer[0];
p.SetMode(PointerWrap::MODE_WRITE);
DoState(p);
g_op_in_progress = false;
Core::PauseAndLock(false, wasUnpaused);
}
void VerifyBufferStateCallback(u64 userdata, int cyclesLate)
void VerifyBuffer(std::vector<u8>& buffer)
{
u8* ptr = &g_current_buffer[0];
bool wasUnpaused = Core::PauseAndLock(true);
u8* ptr = &buffer[0];
PointerWrap p(&ptr, PointerWrap::MODE_VERIFY);
DoState(p);
Core::DisplayMessage("Verified state", 2000);
g_op_in_progress = false;
Core::PauseAndLock(false, wasUnpaused);
}
void CompressAndDumpState(const std::vector<u8>* save_arg)
struct CompressAndDumpState_args
{
const u8* const buffer_data = &(*save_arg)[0];
const size_t buffer_size = save_arg->size();
std::vector<u8>* buffer_vector;
std::mutex* buffer_mutex;
std::string filename;
};
void CompressAndDumpState(CompressAndDumpState_args save_args)
{
std::lock_guard<std::mutex> lk(*save_args.buffer_mutex);
g_compressAndDumpStateSyncEvent.Set();
const u8* const buffer_data = &(*(save_args.buffer_vector))[0];
const size_t buffer_size = (save_args.buffer_vector)->size();
std::string& filename = save_args.filename;
// For easy debugging
Common::SetCurrentThreadName("SaveState thread");
// Moving to last overwritten save-state
if (File::Exists(g_current_filename))
if (File::Exists(filename))
{
if (File::Exists(File::GetUserPath(D_STATESAVES_IDX) + "lastState.sav"))
File::Delete((File::GetUserPath(D_STATESAVES_IDX) + "lastState.sav"));
if (!File::Rename(g_current_filename, File::GetUserPath(D_STATESAVES_IDX) + "lastState.sav"))
if (!File::Rename(filename, File::GetUserPath(D_STATESAVES_IDX) + "lastState.sav"))
Core::DisplayMessage("Failed to move previous state to state undo backup", 1000);
}
File::IOFile f(g_current_filename, "wb");
File::IOFile f(filename, "wb");
if (!f)
{
Core::DisplayMessage("Could not save state", 2000);
@ -251,17 +247,13 @@ void CompressAndDumpState(const std::vector<u8>* save_arg)
}
Core::DisplayMessage(StringFromFormat("Saved State to %s",
g_current_filename.c_str()).c_str(), 2000);
g_op_in_progress = false;
filename.c_str()).c_str(), 2000);
}
void SaveFileStateCallback(u64 userdata, int cyclesLate)
void SaveAs(const std::string& filename)
{
// Pause the core while we save the state
CCPU::EnableStepping(true);
Flush();
bool wasUnpaused = Core::PauseAndLock(true);
// Measure the size of the buffer.
u8 *ptr = NULL;
@ -270,26 +262,46 @@ void SaveFileStateCallback(u64 userdata, int cyclesLate)
const size_t buffer_size = reinterpret_cast<size_t>(ptr);
// Then actually do the write.
g_current_buffer.resize(buffer_size);
ptr = &g_current_buffer[0];
p.SetMode(PointerWrap::MODE_WRITE);
DoState(p);
if ((Movie::IsRecordingInput() || Movie::IsPlayingInput()) && !Movie::IsJustStartingRecordingInputFromSaveState())
Movie::SaveRecording((g_current_filename + ".dtm").c_str());
else if (!Movie::IsRecordingInput() && !Movie::IsPlayingInput())
File::Delete(g_current_filename + ".dtm");
{
std::lock_guard<std::mutex> lk(g_cs_current_buffer);
g_current_buffer.resize(buffer_size);
ptr = &g_current_buffer[0];
p.SetMode(PointerWrap::MODE_WRITE);
DoState(p);
}
Core::DisplayMessage("Saving State...", 1000);
if (p.GetMode() == PointerWrap::MODE_WRITE)
{
Core::DisplayMessage("Saving State...", 1000);
if ((Movie::IsRecordingInput() || Movie::IsPlayingInput()) && !Movie::IsJustStartingRecordingInputFromSaveState())
Movie::SaveRecording((filename + ".dtm").c_str());
else if (!Movie::IsRecordingInput() && !Movie::IsPlayingInput())
File::Delete(filename + ".dtm");
g_save_thread = std::thread(CompressAndDumpState, &g_current_buffer);
CompressAndDumpState_args save_args;
save_args.buffer_vector = &g_current_buffer;
save_args.buffer_mutex = &g_cs_current_buffer;
save_args.filename = filename;
Flush();
g_save_thread = std::thread(CompressAndDumpState, save_args);
g_compressAndDumpStateSyncEvent.Wait();
g_last_filename = filename;
}
else
{
// someone aborted the save by changing the mode?
Core::DisplayMessage("Unable to Save : Internal DoState Error", 4000);
}
// Resume the core and disable stepping
CCPU::EnableStepping(false);
Core::PauseAndLock(false, wasUnpaused);
}
void LoadFileStateData(std::string& filename, std::vector<u8>& ret_data)
void LoadFileStateData(const std::string& filename, std::vector<u8>& ret_data)
{
Flush();
File::IOFile f(filename, "rb");
if (!f)
{
@ -353,35 +365,54 @@ void LoadFileStateData(std::string& filename, std::vector<u8>& ret_data)
ret_data.swap(buffer);
}
void LoadFileStateCallback(u64 userdata, int cyclesLate)
void LoadAs(const std::string& filename)
{
// Stop the core while we load the state
CCPU::EnableStepping(true);
bool wasUnpaused = Core::PauseAndLock(true);
#if defined _DEBUG && defined _WIN32
// we use _CRTDBG_DELAY_FREE_MEM_DF (as a speed hack?),
// but it was causing us to leak gigantic amounts of memory here,
// enough that only a few savestates could be loaded before crashing,
// so let's disable it temporarily.
int tmpflag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);
if (g_loadDepth == 0)
_CrtSetDbgFlag(tmpflag & ~_CRTDBG_DELAY_FREE_MEM_DF);
#endif
g_loadDepth++;
Flush();
// Save temp buffer for undo load state
// TODO: this should be controlled by a user option,
// because it slows down every savestate load to provide an often-unused feature.
SaveBufferStateCallback(userdata, cyclesLate);
g_undo_load_buffer.swap(g_current_buffer);
std::vector<u8> buffer;
LoadFileStateData(g_current_filename, buffer);
if (!buffer.empty())
{
u8 *ptr = &buffer[0];
PointerWrap p(&ptr, PointerWrap::MODE_READ);
DoState(p);
std::lock_guard<std::mutex> lk(g_cs_undo_load_buffer);
SaveToBuffer(g_undo_load_buffer);
}
if (p.GetMode() == PointerWrap::MODE_READ)
bool loaded = false;
bool loadedSuccessfully = false;
// brackets here are so buffer gets freed ASAP
{
std::vector<u8> buffer;
LoadFileStateData(filename, buffer);
if (!buffer.empty())
{
Core::DisplayMessage(StringFromFormat("Loaded state from %s", g_current_filename.c_str()).c_str(), 2000);
if (File::Exists(g_current_filename + ".dtm"))
Movie::LoadInput((g_current_filename + ".dtm").c_str());
u8 *ptr = &buffer[0];
PointerWrap p(&ptr, PointerWrap::MODE_READ);
DoState(p);
loaded = true;
loadedSuccessfully = (p.GetMode() == PointerWrap::MODE_READ);
}
}
if (loaded)
{
if (loadedSuccessfully)
{
Core::DisplayMessage(StringFromFormat("Loaded state from %s", filename.c_str()).c_str(), 2000);
if (File::Exists(filename + ".dtm"))
Movie::LoadInput((filename + ".dtm").c_str());
else if (!Movie::IsJustStartingRecordingInputFromSaveState())
Movie::EndPlayInput(false);
}
@ -390,25 +421,27 @@ void LoadFileStateCallback(u64 userdata, int cyclesLate)
// failed to load
Core::DisplayMessage("Unable to Load : Can't load state from other revisions !", 4000);
// since we're probably in an inconsistent state now (and might crash or whatever), undo.
// since we could be in an inconsistent state now (and might crash or whatever), undo.
if (g_loadDepth < 2)
UndoLoadState();
}
}
ResetCounters();
HW::OnAfterLoad();
g_op_in_progress = false;
if (g_onAfterLoadCb)
g_onAfterLoadCb();
g_loadDepth--;
#if defined _DEBUG && defined _WIN32
// restore _CRTDBG_DELAY_FREE_MEM_DF
if (g_loadDepth == 0)
_CrtSetDbgFlag(tmpflag);
#endif
// resume dat core
CCPU::EnableStepping(false);
Core::PauseAndLock(false, wasUnpaused);
}
void SetOnAfterLoadCallback(CallbackFunc callback)
@ -416,12 +449,12 @@ void SetOnAfterLoadCallback(CallbackFunc callback)
g_onAfterLoadCb = callback;
}
void VerifyFileStateCallback(u64 userdata, int cyclesLate)
void VerifyAt(const std::string& filename)
{
Flush();
bool wasUnpaused = Core::PauseAndLock(true);
std::vector<u8> buffer;
LoadFileStateData(g_current_filename, buffer);
LoadFileStateData(filename, buffer);
if (!buffer.empty())
{
@ -430,29 +463,17 @@ void VerifyFileStateCallback(u64 userdata, int cyclesLate)
DoState(p);
if (p.GetMode() == PointerWrap::MODE_VERIFY)
Core::DisplayMessage(StringFromFormat("Verified state at %s", g_current_filename.c_str()).c_str(), 2000);
Core::DisplayMessage(StringFromFormat("Verified state at %s", filename.c_str()).c_str(), 2000);
else
Core::DisplayMessage("Unable to Verify : Can't verify state from other revisions !", 4000);
}
g_op_in_progress = false;
Core::PauseAndLock(false, wasUnpaused);
}
void Init()
{
ev_FileLoad = CoreTiming::RegisterEvent("LoadState", &LoadFileStateCallback);
ev_FileSave = CoreTiming::RegisterEvent("SaveState", &SaveFileStateCallback);
ev_FileVerify = CoreTiming::RegisterEvent("VerifyState", &VerifyFileStateCallback);
ev_BufferLoad = CoreTiming::RegisterEvent("LoadBufferState", &LoadBufferStateCallback);
ev_BufferSave = CoreTiming::RegisterEvent("SaveBufferState", &SaveBufferStateCallback);
ev_BufferVerify = CoreTiming::RegisterEvent("VerifyBufferState", &VerifyBufferStateCallback);
waiting = STATE_NONE;
waitingslot = 0;
hook = 0;
ResetCounters();
if (lzo_init() != LZO_E_OK)
PanicAlertT("Internal LZO Error - lzo_init() failed");
}
@ -462,15 +483,15 @@ void Shutdown()
Flush();
// swapping with an empty vector, rather than clear()ing
// this gives a better guarantee to free the allocated memory right NOW
// this gives a better guarantee to free the allocated memory right NOW (as opposed to, actually, never)
{
std::vector<u8> tmp;
g_current_buffer.swap(tmp);
std::lock_guard<std::mutex> lk(g_cs_current_buffer);
std::vector<u8>().swap(g_current_buffer);
}
{
std::vector<u8> tmp;
g_undo_load_buffer.swap(tmp);
std::lock_guard<std::mutex> lk(g_cs_undo_load_buffer);
std::vector<u8>().swap(g_undo_load_buffer);
}
}
@ -480,102 +501,14 @@ static std::string MakeStateFilename(int number)
SConfig::GetInstance().m_LocalCoreStartupParameter.GetUniqueID().c_str(), number);
}
void ScheduleFileEvent(const std::string &filename, int ev, bool immediate)
{
if (g_op_in_progress)
Flush();
if (g_op_in_progress)
return;
g_op_in_progress = true;
g_current_filename = filename;
if (immediate)
CoreTiming::ScheduleEvent_Threadsafe_Immediate(ev);
else
CoreTiming::ScheduleEvent_Threadsafe(0, ev);
}
void SaveAs(const std::string &filename, bool immediate)
{
g_last_filename = filename;
ScheduleFileEvent(filename, ev_FileSave, immediate);
}
void SaveAs(const std::string &filename)
{
SaveAs(filename, true);
}
void LoadAs(const std::string &filename, bool immediate)
{
ScheduleFileEvent(filename, ev_FileLoad, immediate);
}
void LoadAs(const std::string &filename)
{
LoadAs(filename, true);
}
void VerifyAt(const std::string &filename)
{
ScheduleFileEvent(filename, ev_FileVerify, true);
}
bool ProcessRequestedStates(int priority)
{
bool save = true;
if (hook == priority)
{
if (waiting == STATE_SAVE)
{
SaveAs(MakeStateFilename(waitingslot), false);
waitingslot = 0;
waiting = STATE_NONE;
}
else if (waiting == STATE_LOAD)
{
LoadAs(MakeStateFilename(waitingslot), false);
waitingslot = 0;
waiting = STATE_NONE;
save = false;
}
}
// Change hooks if the new hook gets called frequently (at least once a frame) and if the old
// hook has not been called for the last 5 seconds
if ((CoreTiming::GetTicks() - lastCheckedStates[priority]) < (VideoInterface::GetTicksPerFrame()))
{
lastCheckedStates[priority] = CoreTiming::GetTicks();
if (hook < NUM_HOOKS && priority >= (hook + 1) &&
(lastCheckedStates[priority] - lastCheckedStates[hook]) > (SystemTimers::GetTicksPerSecond() * 5))
{
hook++;
}
}
else
lastCheckedStates[priority] = CoreTiming::GetTicks();
return save;
}
void Save(int slot)
{
if (waiting == STATE_NONE)
{
waiting = STATE_SAVE;
waitingslot = slot;
}
SaveAs(MakeStateFilename(slot));
}
void Load(int slot)
{
if (waiting == STATE_NONE)
{
waiting = STATE_LOAD;
waitingslot = slot;
}
LoadAs(MakeStateFilename(slot));
}
void Verify(int slot)
@ -591,31 +524,6 @@ void LoadLastSaved()
LoadAs(g_last_filename);
}
void ScheduleBufferEvent(std::vector<u8>& buffer, int ev)
{
if (g_op_in_progress)
return;
g_op_in_progress = true;
g_current_buffer.swap(buffer);
CoreTiming::ScheduleEvent_Threadsafe_Immediate(ev);
}
void SaveToBuffer(std::vector<u8>& buffer)
{
ScheduleBufferEvent(buffer, ev_BufferSave);
}
void LoadFromBuffer(std::vector<u8>& buffer)
{
ScheduleBufferEvent(buffer, ev_BufferLoad);
}
void VerifyBuffer(std::vector<u8>& buffer)
{
ScheduleBufferEvent(buffer, ev_BufferVerify);
}
void Flush()
{
// If already saving state, wait for it to finish
@ -626,6 +534,7 @@ void Flush()
// Load the last state before loading the state
void UndoLoadState()
{
std::lock_guard<std::mutex> lk(g_cs_undo_load_buffer);
if (!g_undo_load_buffer.empty())
LoadFromBuffer(g_undo_load_buffer);
else