System: Add memory-only save states and rewind

This commit is contained in:
Connor McLaughlin
2021-01-23 19:00:54 +10:00
parent 6c6fdeb15e
commit 516d685dd0
22 changed files with 537 additions and 70 deletions

View File

@ -9,6 +9,7 @@
#include "common/log.h"
#include "common/state_wrapper.h"
#include "common/string_util.h"
#include "common/timestamp.h"
#include "controller.h"
#include "cpu_code_cache.h"
#include "cpu_core.h"
@ -30,7 +31,10 @@
#include "texture_replacements.h"
#include "timers.h"
#include <cctype>
#include <cinttypes>
#include <cmath>
#include <cstdio>
#include <deque>
#include <fstream>
#include <limits>
Log_SetChannel(System);
@ -51,6 +55,12 @@ SystemBootParameters::~SystemBootParameters() = default;
namespace System {
struct MemorySaveState
{
std::unique_ptr<HostDisplayTexture> vram_texture;
std::unique_ptr<GrowableMemoryByteStream> state_stream;
};
static bool LoadEXE(const char* filename);
static bool SetExpansionROM(const char* filename);
@ -58,7 +68,10 @@ static bool SetExpansionROM(const char* filename);
static std::unique_ptr<CDImage> OpenCDImage(const char* path, bool force_preload);
static bool DoLoadState(ByteStream* stream, bool force_software_renderer, bool update_display);
static bool DoState(StateWrapper& sw, bool update_display);
static bool DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display);
static void DoRunFrame();
static void DoRewind();
static void DoMemorySaveStates();
static bool CreateGPU(GPURenderer renderer);
static bool Initialize(bool force_software_renderer);
@ -104,6 +117,15 @@ static std::string s_media_playlist_filename;
static std::unique_ptr<CheatList> s_cheat_list;
static bool s_memory_saves_enabled = false;
static std::deque<MemorySaveState> s_rewind_states;
static s32 s_rewind_load_frequency = -1;
static s32 s_rewind_load_counter = -1;
static s32 s_rewind_save_frequency = -1;
static s32 s_rewind_save_counter = -1;
static bool s_rewinding_first_save = false;
State GetState()
{
return s_state;
@ -521,12 +543,13 @@ std::optional<DiscRegion> GetRegionForPath(const char* image_path)
bool RecreateGPU(GPURenderer renderer, bool update_display /* = true*/)
{
ClearMemorySaveStates();
g_gpu->RestoreGraphicsAPIState();
// save current state
std::unique_ptr<ByteStream> state_stream = ByteStream_CreateGrowableMemoryStream();
StateWrapper sw(state_stream.get(), StateWrapper::Mode::Write, SAVE_STATE_VERSION);
const bool state_valid = g_gpu->DoState(sw, false) && TimingEvents::DoState(sw);
const bool state_valid = g_gpu->DoState(sw, nullptr, false) && TimingEvents::DoState(sw);
if (!state_valid)
Log_ErrorPrintf("Failed to save old GPU state when switching renderers");
@ -548,7 +571,7 @@ bool RecreateGPU(GPURenderer renderer, bool update_display /* = true*/)
state_stream->SeekAbsolute(0);
sw.SetMode(StateWrapper::Mode::Read);
g_gpu->RestoreGraphicsAPIState();
g_gpu->DoState(sw, update_display);
g_gpu->DoState(sw, nullptr, update_display);
TimingEvents::DoState(sw);
g_gpu->ResetGraphicsAPIState();
}
@ -802,6 +825,7 @@ bool Initialize(bool force_software_renderer)
}
UpdateThrottlePeriod();
UpdateMemorySaveStateSettings();
return true;
}
@ -810,6 +834,8 @@ void Shutdown()
if (s_state == State::Shutdown)
return;
ClearMemorySaveStates();
g_texture_replacements.Shutdown();
g_sio.Shutdown();
@ -868,11 +894,11 @@ bool CreateGPU(GPURenderer renderer)
}
// we put this here rather than in Initialize() because of the virtual calls
g_gpu->Reset();
g_gpu->Reset(true);
return true;
}
bool DoState(StateWrapper& sw, bool update_display)
bool DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display)
{
if (!sw.DoMarker("System"))
return false;
@ -897,7 +923,7 @@ bool DoState(StateWrapper& sw, bool update_display)
return false;
g_gpu->RestoreGraphicsAPIState();
const bool gpu_result = sw.DoMarker("GPU") && g_gpu->DoState(sw, update_display);
const bool gpu_result = sw.DoMarker("GPU") && g_gpu->DoState(sw, host_texture, update_display);
g_gpu->ResetGraphicsAPIState();
if (!gpu_result)
return false;
@ -963,7 +989,7 @@ void Reset()
Bus::Reset();
g_dma.Reset();
g_interrupt_controller.Reset();
g_gpu->Reset();
g_gpu->Reset(true);
g_cdrom.Reset();
g_pad.Reset();
g_timers.Reset();
@ -1076,6 +1102,7 @@ bool DoLoadState(ByteStream* state, bool force_software_renderer, bool update_di
}
UpdateRunningGame(media_filename.c_str(), media.get());
ClearMemorySaveStates();
if (s_state == State::Starting)
{
@ -1117,7 +1144,7 @@ bool DoLoadState(ByteStream* state, bool force_software_renderer, bool update_di
return false;
StateWrapper sw(state, StateWrapper::Mode::Read, header.version);
if (!DoState(sw, update_display))
if (!DoState(sw, nullptr, update_display))
return false;
if (s_state == State::Starting)
@ -1184,7 +1211,7 @@ bool SaveState(ByteStream* state, u32 screenshot_size /* = 128 */)
g_gpu->RestoreGraphicsAPIState();
StateWrapper sw(state, StateWrapper::Mode::Write, SAVE_STATE_VERSION);
const bool result = DoState(sw, false);
const bool result = DoState(sw, nullptr, false);
g_gpu->ResetGraphicsAPIState();
@ -1224,10 +1251,8 @@ void SingleStepCPU()
g_gpu->ResetGraphicsAPIState();
}
void RunFrame()
void DoRunFrame()
{
s_frame_timer.Reset();
g_gpu->RestoreGraphicsAPIState();
if (CPU::g_state.use_debug_dispatcher)
@ -1266,6 +1291,22 @@ void RunFrame()
g_gpu->ResetGraphicsAPIState();
}
void RunFrame()
{
s_frame_timer.Reset();
if (s_rewind_load_counter >= 0)
{
DoRewind();
return;
}
DoRunFrame();
if (s_memory_saves_enabled)
DoMemorySaveStates();
}
float GetTargetSpeed()
{
return s_target_speed;
@ -1739,12 +1780,14 @@ bool InsertMedia(const char* path)
if (g_settings.IsUsingCodeCache())
CPU::CodeCache::Reinitialize();
ClearMemorySaveStates();
return true;
}
void RemoveMedia()
{
g_cdrom.RemoveMedia();
ClearMemorySaveStates();
}
void UpdateRunningGame(const char* path, CDImage* image)
@ -1907,4 +1950,152 @@ void SetCheatList(std::unique_ptr<CheatList> cheats)
s_cheat_list = std::move(cheats);
}
void CalculateRewindMemoryUsage(u32 num_saves, u64* ram_usage, u64* vram_usage)
{
*ram_usage = MAX_SAVE_STATE_SIZE * static_cast<u64>(num_saves);
*vram_usage = (VRAM_WIDTH * VRAM_HEIGHT * 4) * static_cast<u64>(std::max(g_settings.gpu_resolution_scale, 1u)) *
static_cast<u64>(num_saves);
}
void ClearMemorySaveStates()
{
s_rewind_states.clear();
}
void UpdateMemorySaveStateSettings()
{
ClearMemorySaveStates();
s_memory_saves_enabled = g_settings.rewind_enable;
if (g_settings.rewind_enable)
{
s_rewind_save_frequency = static_cast<s32>(std::ceil(g_settings.rewind_save_frequency * s_throttle_frequency));
s_rewind_save_counter = 0;
u64 ram_usage, vram_usage;
CalculateRewindMemoryUsage(g_settings.rewind_save_slots, &ram_usage, &vram_usage);
Log_InfoPrintf(
"Rewind is enabled, saving every %d frames, with %u slots and %" PRIu64 "MB RAM and %" PRIu64 "MB VRAM usage",
std::max(s_rewind_save_frequency, 1), g_settings.rewind_save_slots, ram_usage / 1048576, vram_usage / 1048576);
}
else
{
s_rewind_save_frequency = -1;
s_rewind_save_counter = -1;
}
}
bool SaveRewindState()
{
Common::Timer save_timer;
const u32 save_slots = g_settings.rewind_save_slots;
while (s_rewind_states.size() >= save_slots)
s_rewind_states.pop_front();
MemorySaveState mss;
mss.state_stream = std::make_unique<GrowableMemoryByteStream>(nullptr, MAX_SAVE_STATE_SIZE);
HostDisplayTexture* host_texture = nullptr;
StateWrapper sw(mss.state_stream.get(), StateWrapper::Mode::Write, SAVE_STATE_VERSION);
if (!DoState(sw, &host_texture, false))
{
Log_ErrorPrint("Failed to create rewind state.");
return false;
}
mss.vram_texture.reset(host_texture);
s_rewind_states.push_back(std::move(mss));
Log_DevPrintf("Saved rewind state (%u bytes, took %.4f ms)", s_rewind_states.back().state_stream->GetSize(),
save_timer.GetTimeMilliseconds());
return true;
}
bool LoadRewindState(u32 skip_saves /*= 0*/, bool consume_state /*=true */)
{
while (skip_saves > 0 && !s_rewind_states.empty())
{
s_rewind_states.pop_back();
skip_saves--;
}
if (s_rewind_states.empty())
return false;
Common::Timer load_timer;
const MemorySaveState& mss = s_rewind_states.back();
mss.state_stream->SeekAbsolute(0);
StateWrapper sw(mss.state_stream.get(), StateWrapper::Mode::Read, SAVE_STATE_VERSION);
HostDisplayTexture* host_texture = mss.vram_texture.get();
if (!DoState(sw, &host_texture, true))
{
g_host_interface->ReportError("Failed to load rewind state from memory, resetting.");
Reset();
return false;
}
if (consume_state)
s_rewind_states.pop_back();
Log_DevPrintf("Rewind load took %.4f ms", load_timer.GetTimeMilliseconds());
return true;
}
void SetRewinding(bool enabled)
{
if (enabled)
{
// Try to rewind at the replay speed, or one per second maximum.
const float load_frequency = std::min(g_settings.rewind_save_frequency, 1.0f);
s_rewind_load_frequency = static_cast<s32>(std::ceil(load_frequency * s_throttle_frequency));
s_rewind_load_counter = 0;
}
else
{
s_rewind_load_frequency = -1;
s_rewind_load_counter = -1;
}
s_rewinding_first_save = true;
}
void DoRewind()
{
s_frame_timer.Reset();
if (s_rewind_load_counter == 0)
{
const u32 skip_saves = BoolToUInt32(!s_rewinding_first_save);
s_rewinding_first_save = false;
LoadRewindState(skip_saves, false);
ResetPerformanceCounters();
s_rewind_load_counter = s_rewind_load_frequency;
}
else
{
s_rewind_load_counter--;
}
}
void DoMemorySaveStates()
{
if (s_rewind_save_counter >= 0)
{
if (s_rewind_save_counter == 0)
{
SaveRewindState();
s_rewind_save_counter = s_rewind_save_frequency;
}
else
{
s_rewind_save_counter--;
}
}
}
} // namespace System