System: Add memory-only save states and rewind
This commit is contained in:
@ -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
|
||||
Reference in New Issue
Block a user