From 516d685dd076c19c65bc2b8ccb4ef760a2a87af3 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 23 Jan 2021 19:00:54 +1000 Subject: [PATCH] System: Add memory-only save states and rewind --- src/core/gpu.cpp | 36 +-- src/core/gpu.h | 5 +- src/core/gpu_backend.cpp | 2 +- src/core/gpu_backend.h | 2 +- src/core/gpu_hw.cpp | 8 +- src/core/gpu_hw.h | 4 +- src/core/gpu_hw_d3d11.cpp | 45 +++- src/core/gpu_hw_d3d11.h | 3 +- src/core/gpu_hw_opengl.cpp | 109 ++++++++- src/core/gpu_hw_opengl.h | 6 +- src/core/gpu_hw_vulkan.cpp | 70 +++++- src/core/gpu_hw_vulkan.h | 3 +- src/core/gpu_sw.cpp | 6 +- src/core/gpu_sw.h | 2 +- src/core/gpu_sw_backend.cpp | 7 +- src/core/gpu_sw_backend.h | 2 +- src/core/host_interface.cpp | 23 +- src/core/settings.cpp | 6 + src/core/settings.h | 4 + src/core/system.cpp | 215 +++++++++++++++++- src/core/system.h | 10 + src/frontend-common/common_host_interface.cpp | 39 +++- 22 files changed, 537 insertions(+), 70 deletions(-) diff --git a/src/core/gpu.cpp b/src/core/gpu.cpp index 18e11abe1..7ba93f923 100644 --- a/src/core/gpu.cpp +++ b/src/core/gpu.cpp @@ -75,7 +75,7 @@ std::tuple GPU::GetEffectiveDisplayResolution() return std::tie(m_crtc_state.display_vram_width, m_crtc_state.display_vram_height); } -void GPU::Reset() +void GPU::Reset(bool clear_vram) { SoftReset(); m_set_texture_disable_mask = false; @@ -119,12 +119,12 @@ void GPU::SoftReset() UpdateCommandTickEvent(); } -bool GPU::DoState(StateWrapper& sw, bool update_display) +bool GPU::DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display) { if (sw.IsReading()) { // perform a reset to discard all pending draws/fb state - Reset(); + Reset(host_texture == nullptr); } sw.Do(&m_GPUSTAT.bits); @@ -214,16 +214,27 @@ bool GPU::DoState(StateWrapper& sw, bool update_display) UpdateDMARequest(); } - if (!sw.DoMarker("GPU-VRAM")) - return false; + if (!host_texture) + { + if (!sw.DoMarker("GPU-VRAM")) + return false; + + if (sw.IsReading()) + { + // Still need a temporary here. + HeapArray temp; + sw.DoBytes(temp.data(), VRAM_WIDTH * VRAM_HEIGHT * sizeof(u16)); + UpdateVRAM(0, 0, VRAM_WIDTH, VRAM_HEIGHT, temp.data(), false, false); + } + else + { + ReadVRAM(0, 0, VRAM_WIDTH, VRAM_HEIGHT); + sw.DoBytes(m_vram_ptr, VRAM_WIDTH * VRAM_HEIGHT * sizeof(u16)); + } + } if (sw.IsReading()) { - // Still need a temporary here. - HeapArray temp; - sw.DoBytes(temp.data(), VRAM_WIDTH * VRAM_HEIGHT * sizeof(u16)); - UpdateVRAM(0, 0, VRAM_WIDTH, VRAM_HEIGHT, temp.data(), false, false); - UpdateCRTCConfig(); if (update_display) UpdateDisplay(); @@ -231,11 +242,6 @@ bool GPU::DoState(StateWrapper& sw, bool update_display) UpdateCRTCTickEvent(); UpdateCommandTickEvent(); } - else - { - ReadVRAM(0, 0, VRAM_WIDTH, VRAM_HEIGHT); - sw.DoBytes(m_vram_ptr, VRAM_WIDTH * VRAM_HEIGHT * sizeof(u16)); - } return !sw.HasError(); } diff --git a/src/core/gpu.h b/src/core/gpu.h index 73b24971b..41424c41e 100644 --- a/src/core/gpu.h +++ b/src/core/gpu.h @@ -15,6 +15,7 @@ class StateWrapper; class HostDisplay; +class HostDisplayTexture; class TimingEvent; class Timers; @@ -75,8 +76,8 @@ public: virtual bool IsHardwareRenderer() const = 0; virtual bool Initialize(HostDisplay* host_display); - virtual void Reset(); - virtual bool DoState(StateWrapper& sw, bool update_display); + virtual void Reset(bool clear_vram); + virtual bool DoState(StateWrapper& sw, HostDisplayTexture** save_to_texture, bool update_display); // Graphics API state reset/restore - call when drawing the UI etc. virtual void ResetGraphicsAPIState(); diff --git a/src/core/gpu_backend.cpp b/src/core/gpu_backend.cpp index 5aced6b94..e7a9a7473 100644 --- a/src/core/gpu_backend.cpp +++ b/src/core/gpu_backend.cpp @@ -19,7 +19,7 @@ bool GPUBackend::Initialize() return true; } -void GPUBackend::Reset() +void GPUBackend::Reset(bool clear_vram) { Sync(); m_drawing_area = {}; diff --git a/src/core/gpu_backend.h b/src/core/gpu_backend.h index 3c285db50..3302f76a4 100644 --- a/src/core/gpu_backend.h +++ b/src/core/gpu_backend.h @@ -23,7 +23,7 @@ public: virtual bool Initialize(); virtual void UpdateSettings(); - virtual void Reset(); + virtual void Reset(bool clear_vram); virtual void Shutdown(); GPUBackendFillVRAMCommand* NewFillVRAMCommand(); diff --git a/src/core/gpu_hw.cpp b/src/core/gpu_hw.cpp index 30dc2561d..2a691d00c 100644 --- a/src/core/gpu_hw.cpp +++ b/src/core/gpu_hw.cpp @@ -91,9 +91,9 @@ bool GPU_HW::Initialize(HostDisplay* host_display) return true; } -void GPU_HW::Reset() +void GPU_HW::Reset(bool clear_vram) { - GPU::Reset(); + GPU::Reset(clear_vram); m_batch_current_vertex_ptr = m_batch_start_vertex_ptr; @@ -107,9 +107,9 @@ void GPU_HW::Reset() SetFullVRAMDirtyRectangle(); } -bool GPU_HW::DoState(StateWrapper& sw, bool update_display) +bool GPU_HW::DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display) { - if (!GPU::DoState(sw, update_display)) + if (!GPU::DoState(sw, host_texture, update_display)) return false; // invalidate the whole VRAM read texture when loading state diff --git a/src/core/gpu_hw.h b/src/core/gpu_hw.h index 66de0a18c..096a200b2 100644 --- a/src/core/gpu_hw.h +++ b/src/core/gpu_hw.h @@ -32,8 +32,8 @@ public: virtual bool IsHardwareRenderer() const override; virtual bool Initialize(HostDisplay* host_display) override; - virtual void Reset() override; - virtual bool DoState(StateWrapper& sw, bool update_display) override; + virtual void Reset(bool clear_vram) override; + virtual bool DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display) override; void UpdateResolutionScale() override final; std::tuple GetEffectiveDisplayResolution() override final; diff --git a/src/core/gpu_hw_d3d11.cpp b/src/core/gpu_hw_d3d11.cpp index 9cd98ef70..ad5937023 100644 --- a/src/core/gpu_hw_d3d11.cpp +++ b/src/core/gpu_hw_d3d11.cpp @@ -2,6 +2,7 @@ #include "common/assert.h" #include "common/d3d11/shader_compiler.h" #include "common/log.h" +#include "common/state_wrapper.h" #include "common/timer.h" #include "gpu_hw_shadergen.h" #include "host_display.h" @@ -82,11 +83,49 @@ bool GPU_HW_D3D11::Initialize(HostDisplay* host_display) return true; } -void GPU_HW_D3D11::Reset() +void GPU_HW_D3D11::Reset(bool clear_vram) { - GPU_HW::Reset(); + GPU_HW::Reset(clear_vram); - ClearFramebuffer(); + if (clear_vram) + ClearFramebuffer(); +} + +bool GPU_HW_D3D11::DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display) +{ + if (host_texture) + { + const CD3D11_BOX src_box(0, 0, 0, static_cast(m_vram_texture.GetWidth()), + static_cast(m_vram_texture.GetHeight()), 1); + ComPtr resource; + + if (sw.IsReading()) + { + HostDisplayTexture* tex = *host_texture; + if (tex->GetWidth() != m_vram_texture.GetWidth() || tex->GetHeight() != m_vram_texture.GetHeight() || + tex->GetSamples() != m_vram_texture.GetSamples()) + { + return false; + } + + static_cast(tex->GetHandle())->GetResource(resource.GetAddressOf()); + m_context->CopySubresourceRegion(m_vram_texture.GetD3DTexture(), 0, 0, 0, 0, resource.Get(), 0, &src_box); + } + else + { + std::unique_ptr tex = + m_host_display->CreateTexture(m_vram_texture.GetWidth(), m_vram_texture.GetHeight(), 1, 1, + m_vram_texture.GetSamples(), HostDisplayPixelFormat::RGBA8, nullptr, 0, false); + if (!tex) + return false; + + static_cast(tex->GetHandle())->GetResource(resource.GetAddressOf()); + m_context->CopySubresourceRegion(resource.Get(), 0, 0, 0, 0, m_vram_texture.GetD3DTexture(), 0, &src_box); + *host_texture = tex.release(); + } + } + + return GPU_HW::DoState(sw, host_texture, update_display); } void GPU_HW_D3D11::ResetGraphicsAPIState() diff --git a/src/core/gpu_hw_d3d11.h b/src/core/gpu_hw_d3d11.h index d343e75f4..fab7a7e7f 100644 --- a/src/core/gpu_hw_d3d11.h +++ b/src/core/gpu_hw_d3d11.h @@ -21,7 +21,8 @@ public: ~GPU_HW_D3D11() override; bool Initialize(HostDisplay* host_display) override; - void Reset() override; + void Reset(bool clear_vram) override; + bool DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display) override; void ResetGraphicsAPIState() override; void RestoreGraphicsAPIState() override; diff --git a/src/core/gpu_hw_opengl.cpp b/src/core/gpu_hw_opengl.cpp index 1e0980f1d..24c2d91f9 100644 --- a/src/core/gpu_hw_opengl.cpp +++ b/src/core/gpu_hw_opengl.cpp @@ -1,6 +1,7 @@ #include "gpu_hw_opengl.h" #include "common/assert.h" #include "common/log.h" +#include "common/state_wrapper.h" #include "common/timer.h" #include "gpu_hw_shadergen.h" #include "host_display.h" @@ -95,11 +96,106 @@ bool GPU_HW_OpenGL::Initialize(HostDisplay* host_display) return true; } -void GPU_HW_OpenGL::Reset() +void GPU_HW_OpenGL::Reset(bool clear_vram) { - GPU_HW::Reset(); + GPU_HW::Reset(clear_vram); - ClearFramebuffer(); + if (clear_vram) + ClearFramebuffer(); +} + +bool GPU_HW_OpenGL::DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display) +{ + if (host_texture) + { + if (sw.IsReading()) + { + HostDisplayTexture* tex = *host_texture; + if (tex->GetWidth() != m_vram_texture.GetWidth() || tex->GetHeight() != m_vram_texture.GetHeight() || + tex->GetSamples() != m_vram_texture.GetSamples()) + { + return false; + } + + CopyFramebufferForState( + m_vram_texture.GetGLTarget(), static_cast(reinterpret_cast(tex->GetHandle())), 0, 0, 0, + m_vram_texture.GetGLId(), m_vram_fbo_id, 0, 0, m_vram_texture.GetWidth(), m_vram_texture.GetHeight()); + } + else + { + std::unique_ptr tex = + m_host_display->CreateTexture(m_vram_texture.GetWidth(), m_vram_texture.GetHeight(), 1, 1, + m_vram_texture.GetSamples(), HostDisplayPixelFormat::RGBA8, nullptr, 0, false); + if (!tex) + return false; + + CopyFramebufferForState(m_vram_texture.GetGLTarget(), m_vram_texture.GetGLId(), m_vram_fbo_id, 0, 0, + static_cast(reinterpret_cast(tex->GetHandle())), 0, 0, 0, + m_vram_texture.GetWidth(), m_vram_texture.GetHeight()); + *host_texture = tex.release(); + } + } + + return GPU_HW::DoState(sw, host_texture, update_display); +} + +void GPU_HW_OpenGL::CopyFramebufferForState(GLenum target, GLuint src_texture, u32 src_fbo, u32 src_x, u32 src_y, + GLuint dst_texture, u32 dst_fbo, u32 dst_x, u32 dst_y, u32 width, + u32 height) +{ + if (target != GL_TEXTURE_2D && GLAD_GL_VERSION_4_3) + { + glCopyImageSubData(src_texture, target, 0, src_x, src_y, 0, dst_texture, target, 0, dst_x, dst_y, 0, width, height, + 1); + } + else if (target != GL_TEXTURE_2D && GLAD_GL_EXT_copy_image) + { + glCopyImageSubDataEXT(src_texture, target, 0, src_x, src_y, 0, dst_texture, target, 0, dst_x, dst_y, 0, width, + height, 1); + } + else if (target != GL_TEXTURE_2D && GLAD_GL_OES_copy_image) + { + glCopyImageSubDataOES(src_texture, target, 0, src_x, src_y, 0, dst_texture, target, 0, dst_x, dst_y, 0, width, + height, 1); + } + else + { + if (src_fbo == 0) + { + glBindFramebuffer(GL_READ_FRAMEBUFFER, m_state_copy_fbo_id); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, src_texture, 0); + } + else + { + glBindFramebuffer(GL_READ_FRAMEBUFFER, src_fbo); + } + + if (dst_fbo == 0) + { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_state_copy_fbo_id); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, dst_texture, 0); + } + else + { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dst_fbo); + } + + glDisable(GL_SCISSOR_TEST); + glBlitFramebuffer(src_x, src_y, src_x + width, src_y + height, dst_x, dst_y, dst_x + width, dst_y + height, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + glEnable(GL_SCISSOR_TEST); + + if (src_fbo == 0) + { + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + } + else if (dst_fbo == 0) + { + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + } + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_vram_fbo_id); + } } void GPU_HW_OpenGL::ResetGraphicsAPIState() @@ -303,7 +399,9 @@ bool GPU_HW_OpenGL::CreateFramebuffer() return false; } - glGenFramebuffers(1, &m_vram_fbo_id); + if (m_vram_fbo_id == 0) + glGenFramebuffers(1, &m_vram_fbo_id); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_vram_fbo_id); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m_vram_texture.GetGLTarget(), m_vram_texture.GetGLId(), 0); @@ -320,6 +418,9 @@ bool GPU_HW_OpenGL::CreateFramebuffer() } } + if (m_state_copy_fbo_id == 0) + glGenFramebuffers(1, &m_state_copy_fbo_id); + SetFullVRAMDirtyRectangle(); return true; } diff --git a/src/core/gpu_hw_opengl.h b/src/core/gpu_hw_opengl.h index 2f6d44834..fec8b003d 100644 --- a/src/core/gpu_hw_opengl.h +++ b/src/core/gpu_hw_opengl.h @@ -17,7 +17,8 @@ public: ~GPU_HW_OpenGL() override; bool Initialize(HostDisplay* host_display) override; - void Reset() override; + void Reset(bool clear_vram) override; + bool DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display) override; void ResetGraphicsAPIState() override; void RestoreGraphicsAPIState() override; @@ -57,6 +58,8 @@ private: void SetCapabilities(HostDisplay* host_display); bool CreateFramebuffer(); void ClearFramebuffer(); + void CopyFramebufferForState(GLenum target, GLuint src_texture, u32 src_fbo, u32 src_x, u32 src_y, GLuint dst_texture, + u32 dst_fbo, u32 dst_x, u32 dst_y, u32 width, u32 height); bool CreateVertexBuffer(); bool CreateUniformBuffer(); @@ -84,6 +87,7 @@ private: GLuint m_vram_fbo_id = 0; GLuint m_vao_id = 0; GLuint m_attributeless_vao_id = 0; + GLuint m_state_copy_fbo_id = 0; std::unique_ptr m_uniform_stream_buffer; diff --git a/src/core/gpu_hw_vulkan.cpp b/src/core/gpu_hw_vulkan.cpp index e4ca7baca..8d860bf75 100644 --- a/src/core/gpu_hw_vulkan.cpp +++ b/src/core/gpu_hw_vulkan.cpp @@ -2,6 +2,7 @@ #include "common/assert.h" #include "common/log.h" #include "common/scope_guard.h" +#include "common/state_wrapper.h" #include "common/timer.h" #include "common/vulkan/builders.h" #include "common/vulkan/context.h" @@ -87,12 +88,75 @@ bool GPU_HW_Vulkan::Initialize(HostDisplay* host_display) return true; } -void GPU_HW_Vulkan::Reset() +void GPU_HW_Vulkan::Reset(bool clear_vram) { - GPU_HW::Reset(); + GPU_HW::Reset(clear_vram); EndRenderPass(); - ClearFramebuffer(); + + if (clear_vram) + ClearFramebuffer(); +} + +bool GPU_HW_Vulkan::DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display) +{ + if (host_texture) + { + EndRenderPass(); + + const VkImageCopy ic{{VK_IMAGE_ASPECT_COLOR_BIT, 0u, 0u, 1u}, + {0, 0, 0}, + {VK_IMAGE_ASPECT_COLOR_BIT, 0u, 0u, 1u}, + {0, 0, 0}, + {m_vram_texture.GetWidth(), m_vram_texture.GetHeight(), 1u}}; + + if (sw.IsReading()) + { + Vulkan::Texture* tex = static_cast((*host_texture)->GetHandle()); + if (tex->GetWidth() != m_vram_texture.GetWidth() || tex->GetHeight() != m_vram_texture.GetHeight() || + tex->GetSamples() != m_vram_texture.GetSamples()) + { + return false; + } + + VkCommandBuffer buf = g_vulkan_context->GetCurrentCommandBuffer(); + const VkImageLayout old_tex_layout = tex->GetLayout(); + const VkImageLayout old_vram_layout = m_vram_texture.GetLayout(); + tex->TransitionToLayout(buf, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + m_vram_texture.TransitionToLayout(buf, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + vkCmdCopyImage(g_vulkan_context->GetCurrentCommandBuffer(), tex->GetImage(), tex->GetLayout(), + m_vram_texture.GetImage(), m_vram_texture.GetLayout(), 1, &ic); + m_vram_texture.TransitionToLayout(buf, old_vram_layout); + tex->TransitionToLayout(buf, old_tex_layout); + } + else + { + std::unique_ptr htex = + m_host_display->CreateTexture(m_vram_texture.GetWidth(), m_vram_texture.GetHeight(), 1, 1, + m_vram_texture.GetSamples(), HostDisplayPixelFormat::RGBA8, nullptr, 0, false); + if (!htex) + return false; + + Vulkan::Texture* tex = static_cast(htex->GetHandle()); + if (tex->GetWidth() != m_vram_texture.GetWidth() || tex->GetHeight() != m_vram_texture.GetHeight() || + tex->GetSamples() != m_vram_texture.GetSamples()) + { + return false; + } + + VkCommandBuffer buf = g_vulkan_context->GetCurrentCommandBuffer(); + const VkImageLayout old_vram_layout = m_vram_texture.GetLayout(); + tex->TransitionToLayout(buf, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + m_vram_texture.TransitionToLayout(buf, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + vkCmdCopyImage(g_vulkan_context->GetCurrentCommandBuffer(), m_vram_texture.GetImage(), m_vram_texture.GetLayout(), + tex->GetImage(), tex->GetLayout(), 1, &ic); + m_vram_texture.TransitionToLayout(buf, old_vram_layout); + tex->TransitionToLayout(buf, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + *host_texture = htex.release(); + } + } + + return GPU_HW::DoState(sw, host_texture, update_display); } void GPU_HW_Vulkan::ResetGraphicsAPIState() diff --git a/src/core/gpu_hw_vulkan.h b/src/core/gpu_hw_vulkan.h index 7a7261c77..5d7ccb846 100644 --- a/src/core/gpu_hw_vulkan.h +++ b/src/core/gpu_hw_vulkan.h @@ -16,7 +16,8 @@ public: ~GPU_HW_Vulkan() override; bool Initialize(HostDisplay* host_display) override; - void Reset() override; + void Reset(bool clear_vram) override; + bool DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display) override; void ResetGraphicsAPIState() override; void RestoreGraphicsAPIState() override; diff --git a/src/core/gpu_sw.cpp b/src/core/gpu_sw.cpp index 772618026..bd7ea537c 100644 --- a/src/core/gpu_sw.cpp +++ b/src/core/gpu_sw.cpp @@ -75,11 +75,11 @@ bool GPU_SW::Initialize(HostDisplay* host_display) return true; } -void GPU_SW::Reset() +void GPU_SW::Reset(bool clear_vram) { - GPU::Reset(); + GPU::Reset(clear_vram); - m_backend.Reset(); + m_backend.Reset(clear_vram); } void GPU_SW::UpdateSettings() diff --git a/src/core/gpu_sw.h b/src/core/gpu_sw.h index dc69693f7..d67c83bff 100644 --- a/src/core/gpu_sw.h +++ b/src/core/gpu_sw.h @@ -18,7 +18,7 @@ public: bool IsHardwareRenderer() const override; bool Initialize(HostDisplay* host_display) override; - void Reset() override; + void Reset(bool clear_vram) override; void UpdateSettings() override; protected: diff --git a/src/core/gpu_sw_backend.cpp b/src/core/gpu_sw_backend.cpp index e87a262d2..aebdc0e94 100644 --- a/src/core/gpu_sw_backend.cpp +++ b/src/core/gpu_sw_backend.cpp @@ -20,11 +20,12 @@ bool GPU_SW_Backend::Initialize() return GPUBackend::Initialize(); } -void GPU_SW_Backend::Reset() +void GPU_SW_Backend::Reset(bool clear_vram) { - GPUBackend::Reset(); + GPUBackend::Reset(clear_vram); - m_vram.fill(0); + if (clear_vram) + m_vram.fill(0); } void GPU_SW_Backend::DrawPolygon(const GPUBackendDrawPolygonCommand* cmd) diff --git a/src/core/gpu_sw_backend.h b/src/core/gpu_sw_backend.h index a7105ecc2..3d1089737 100644 --- a/src/core/gpu_sw_backend.h +++ b/src/core/gpu_sw_backend.h @@ -11,7 +11,7 @@ public: ~GPU_SW_Backend() override; bool Initialize() override; - void Reset() override; + void Reset(bool clear_vram) override; ALWAYS_INLINE_RELEASE u16 GetPixel(const u32 x, const u32 y) const { return m_vram[VRAM_WIDTH * y + x]; } ALWAYS_INLINE_RELEASE const u16* GetPixelPtr(const u32 x, const u32 y) const { return &m_vram[VRAM_WIDTH * y + x]; } diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp index bc119b0e2..81abeb809 100644 --- a/src/core/host_interface.cpp +++ b/src/core/host_interface.cpp @@ -488,6 +488,9 @@ void HostInterface::SetDefaultSettings(SettingsInterface& si) si.SetBoolValue("Main", "LoadDevicesFromSaveStates", false); si.SetBoolValue("Main", "ApplyGameSettings", true); si.SetBoolValue("Main", "DisableAllEnhancements", false); + si.SetBoolValue("Main", "RewindEnable", false); + si.SetFloatValue("Main", "RewindFrequency", 10.0f); + si.SetIntValue("Main", "RewindSaveSlots", 10); si.SetStringValue("CPU", "ExecutionMode", Settings::GetCPUExecutionModeName(Settings::DEFAULT_CPU_EXECUTION_MODE)); si.SetBoolValue("CPU", "RecompilerMemoryExceptions", false); @@ -655,6 +658,13 @@ void HostInterface::FixIncompatibleSettings(bool display_osd_messages) g_settings.cpu_fastmem_mode = CPUFastmemMode::LUT; } #endif + + // rewinding causes issues with mmap fastmem, so just use LUT + if (g_settings.rewind_enable && g_settings.IsUsingFastmem() && g_settings.cpu_fastmem_mode == CPUFastmemMode::MMap) + { + Log_WarningPrintf("Disabling mmap fastmem due to rewind being enabled"); + g_settings.cpu_fastmem_mode = CPUFastmemMode::LUT; + } } void HostInterface::SaveSettings(SettingsInterface& si) @@ -676,6 +686,8 @@ void HostInterface::CheckForSettingsChanges(const Settings& old_settings) if (System::IsValid()) { + System::ClearMemorySaveStates(); + if (g_settings.cpu_overclock_active != old_settings.cpu_overclock_active || (g_settings.cpu_overclock_active && (g_settings.cpu_overclock_numerator != old_settings.cpu_overclock_numerator || @@ -755,7 +767,8 @@ void HostInterface::CheckForSettingsChanges(const Settings& old_settings) g_settings.display_active_start_offset != old_settings.display_active_start_offset || g_settings.display_active_end_offset != old_settings.display_active_end_offset || g_settings.display_line_start_offset != old_settings.display_line_start_offset || - g_settings.display_line_end_offset != old_settings.display_line_end_offset) + g_settings.display_line_end_offset != old_settings.display_line_end_offset || + g_settings.rewind_enable != old_settings.rewind_enable) { if (g_settings.IsUsingCodeCache()) CPU::CodeCache::Reinitialize(); @@ -793,6 +806,13 @@ void HostInterface::CheckForSettingsChanges(const Settings& old_settings) System::UpdateMemoryCards(); } + if (g_settings.rewind_enable != old_settings.rewind_enable || + g_settings.rewind_save_frequency != old_settings.rewind_save_frequency || + g_settings.rewind_save_slots != old_settings.rewind_save_slots) + { + System::UpdateMemorySaveStateSettings(); + } + if (g_settings.texture_replacements.enable_vram_write_replacements != old_settings.texture_replacements.enable_vram_write_replacements || g_settings.texture_replacements.preload_textures != old_settings.texture_replacements.preload_textures) @@ -999,6 +1019,7 @@ void HostInterface::ModifyResolutionScale(s32 increment) g_gpu->RestoreGraphicsAPIState(); g_gpu->UpdateSettings(); g_gpu->ResetGraphicsAPIState(); + System::ClearMemorySaveStates(); } } diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 09449a266..82c88a5bc 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -123,6 +123,9 @@ void Settings::Load(SettingsInterface& si) apply_game_settings = si.GetBoolValue("Main", "ApplyGameSettings", true); auto_load_cheats = si.GetBoolValue("Main", "AutoLoadCheats", false); disable_all_enhancements = si.GetBoolValue("Main", "DisableAllEnhancements", false); + rewind_enable = si.GetBoolValue("Main", "RewindEnable", false); + rewind_save_frequency = si.GetFloatValue("Main", "RewindFrequency", 10.0f); + rewind_save_slots = static_cast(si.GetIntValue("Main", "RewindSaveSlots", 10)); cpu_execution_mode = ParseCPUExecutionMode( @@ -295,6 +298,9 @@ void Settings::Save(SettingsInterface& si) const si.SetBoolValue("Main", "ApplyGameSettings", apply_game_settings); si.SetBoolValue("Main", "AutoLoadCheats", auto_load_cheats); si.SetBoolValue("Main", "DisableAllEnhancements", disable_all_enhancements); + si.SetBoolValue("Main", "RewindEnable", rewind_enable); + si.SetFloatValue("Main", "RewindFrequency", rewind_save_frequency); + si.SetIntValue("Main", "RewindSaveSlots", rewind_save_slots); si.SetStringValue("CPU", "ExecutionMode", GetCPUExecutionModeName(cpu_execution_mode)); si.SetBoolValue("CPU", "OverclockEnable", cpu_overclock_enable); diff --git a/src/core/settings.h b/src/core/settings.h index 64756595f..67f189e20 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -94,6 +94,10 @@ struct Settings bool auto_load_cheats = false; bool disable_all_enhancements = false; + bool rewind_enable = false; + float rewind_save_frequency = 10.0f; + u32 rewind_save_slots = 10; + GPURenderer gpu_renderer = GPURenderer::Software; std::string gpu_adapter; std::string display_post_process_chain; diff --git a/src/core/system.cpp b/src/core/system.cpp index 3be755a64..6e5d59a6e 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -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 +#include +#include #include +#include #include #include Log_SetChannel(System); @@ -51,6 +55,12 @@ SystemBootParameters::~SystemBootParameters() = default; namespace System { +struct MemorySaveState +{ + std::unique_ptr vram_texture; + std::unique_ptr 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 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 s_cheat_list; +static bool s_memory_saves_enabled = false; + +static std::deque 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 GetRegionForPath(const char* image_path) bool RecreateGPU(GPURenderer renderer, bool update_display /* = true*/) { + ClearMemorySaveStates(); g_gpu->RestoreGraphicsAPIState(); // save current state std::unique_ptr 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 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(num_saves); + *vram_usage = (VRAM_WIDTH * VRAM_HEIGHT * 4) * static_cast(std::max(g_settings.gpu_resolution_scale, 1u)) * + static_cast(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(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(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(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 \ No newline at end of file diff --git a/src/core/system.h b/src/core/system.h index 710754c03..4d85b575e 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -228,4 +228,14 @@ void ApplyCheatCode(const CheatCode& code); /// Sets or clears the provided cheat list, applying every frame. void SetCheatList(std::unique_ptr cheats); +////////////////////////////////////////////////////////////////////////// +// Memory Save States (Rewind and Runahead) +////////////////////////////////////////////////////////////////////////// +void CalculateRewindMemoryUsage(u32 num_saves, u64* ram_usage, u64* vram_usage); +void ClearMemorySaveStates(); +void UpdateMemorySaveStateSettings(); +bool SaveRewindState(); +bool LoadRewindState(u32 skip_saves = 0, bool consume_state = true); +void SetRewinding(bool enabled); + } // namespace System diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index d2b6bcd3d..d0ba80a95 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -1549,7 +1549,7 @@ void CommonHostInterface::RegisterGeneralHotkeys() RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "General")), StaticString("ToggleCheats"), StaticString(TRANSLATABLE("Hotkeys", "Toggle Cheats")), [this](bool pressed) { - if (pressed) + if (pressed && System::IsValid()) DoToggleCheats(); }); @@ -1584,7 +1584,7 @@ void CommonHostInterface::RegisterGeneralHotkeys() #else RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "General")), StaticString("TogglePatchCodes"), StaticString(TRANSLATABLE("Hotkeys", "Toggle Patch Codes")), [this](bool pressed) { - if (pressed) + if (pressed && System::IsValid()) DoToggleCheats(); }); #endif @@ -1603,25 +1603,39 @@ void CommonHostInterface::RegisterGeneralHotkeys() RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "General")), StaticString("FrameStep"), StaticString(TRANSLATABLE("Hotkeys", "Frame Step")), [this](bool pressed) { - if (pressed) + if (pressed && System::IsValid()) DoFrameStep(); }); + + RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "General")), StaticString("Rewind"), + StaticString(TRANSLATABLE("Hotkeys", "Rewind")), [this](bool pressed) { + if (System::IsValid()) + { + AddOSDMessage(pressed ? TranslateStdString("OSDMessage", "Rewinding...") : + TranslateStdString("OSDMessage", "Stopped rewinding."), + 5.0f); + System::SetRewinding(pressed); + } + }); } void CommonHostInterface::RegisterGraphicsHotkeys() { RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "Graphics")), StaticString("ToggleSoftwareRendering"), StaticString(TRANSLATABLE("Hotkeys", "Toggle Software Rendering")), [this](bool pressed) { - if (pressed) + if (pressed && System::IsValid()) ToggleSoftwareRendering(); }); RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "Graphics")), StaticString("TogglePGXP"), StaticString(TRANSLATABLE("Hotkeys", "Toggle PGXP")), [this](bool pressed) { - if (pressed) + if (pressed && System::IsValid()) { g_settings.gpu_pgxp_enable = !g_settings.gpu_pgxp_enable; + g_gpu->RestoreGraphicsAPIState(); g_gpu->UpdateSettings(); + g_gpu->ResetGraphicsAPIState(); + System::ClearMemorySaveStates(); AddOSDMessage(g_settings.gpu_pgxp_enable ? TranslateStdString("OSDMessage", "PGXP is now enabled.") : TranslateStdString("OSDMessage", "PGXP is now disabled"), @@ -1640,13 +1654,16 @@ void CommonHostInterface::RegisterGraphicsHotkeys() RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "Graphics")), StaticString("TogglePGXPDepth"), StaticString(TRANSLATABLE("Hotkeys", "Toggle PGXP Depth Buffer")), [this](bool pressed) { - if (pressed) + if (pressed && System::IsValid()) { g_settings.gpu_pgxp_depth_buffer = !g_settings.gpu_pgxp_depth_buffer; if (!g_settings.gpu_pgxp_enable) return; + g_gpu->RestoreGraphicsAPIState(); g_gpu->UpdateSettings(); + g_gpu->ResetGraphicsAPIState(); + System::ClearMemorySaveStates(); AddOSDMessage(g_settings.gpu_pgxp_depth_buffer ? TranslateStdString("OSDMessage", "PGXP Depth Buffer is now enabled.") : TranslateStdString("OSDMessage", "PGXP Depth Buffer is now disabled."), @@ -1656,31 +1673,31 @@ void CommonHostInterface::RegisterGraphicsHotkeys() RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "Graphics")), StaticString("IncreaseResolutionScale"), StaticString(TRANSLATABLE("Hotkeys", "Increase Resolution Scale")), [this](bool pressed) { - if (pressed) + if (pressed && System::IsValid()) ModifyResolutionScale(1); }); RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "Graphics")), StaticString("DecreaseResolutionScale"), StaticString(TRANSLATABLE("Hotkeys", "Decrease Resolution Scale")), [this](bool pressed) { - if (pressed) + if (pressed && System::IsValid()) ModifyResolutionScale(-1); }); RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "Graphics")), StaticString("TogglePostProcessing"), StaticString(TRANSLATABLE("Hotkeys", "Toggle Post-Processing")), [this](bool pressed) { - if (pressed) + if (pressed && System::IsValid()) TogglePostProcessing(); }); RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "Graphics")), StaticString("ReloadPostProcessingShaders"), StaticString(TRANSLATABLE("Hotkeys", "Reload Post Processing Shaders")), [this](bool pressed) { - if (pressed) + if (pressed && System::IsValid()) ReloadPostProcessingShaders(); }); RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "Graphics")), StaticString("ReloadTextureReplacements"), StaticString(TRANSLATABLE("Hotkeys", "Reload Texture Replacements")), [this](bool pressed) { - if (pressed) + if (pressed && System::IsValid()) { AddOSDMessage(TranslateStdString("OSDMessage", "Texture replacements reloaded."), 10.0f); g_texture_replacements.Reload();