From 08c8d1a52193f51bfe64b8c3515df0628161ae64 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 21 Apr 2020 02:50:45 +1000 Subject: [PATCH] System: Support saving screenshots in save states --- src/core/host_display.cpp | 86 +++++++++++++++++-- src/core/host_display.h | 7 +- src/core/host_interface.cpp | 56 +++++++++++++ src/core/host_interface.h | 56 +++++++++---- src/core/save_state_version.h | 28 ++++++- src/core/system.cpp | 92 +++++++++++++++++---- src/duckstation-qt/d3d11displaywidget.cpp | 2 +- src/duckstation-qt/opengldisplaywidget.cpp | 2 +- src/duckstation-sdl/d3d11_host_display.cpp | 2 +- src/duckstation-sdl/opengl_host_display.cpp | 2 +- 10 files changed, 286 insertions(+), 47 deletions(-) diff --git a/src/core/host_display.cpp b/src/core/host_display.cpp index 2d87f722c..ef7c3a208 100644 --- a/src/core/host_display.cpp +++ b/src/core/host_display.cpp @@ -18,7 +18,7 @@ void HostDisplay::WindowResized(s32 new_window_width, s32 new_window_height) m_window_height = new_window_height; } -std::tuple HostDisplay::CalculateDrawRect() const +std::tuple HostDisplay::CalculateDrawRect(s32 window_width, s32 window_height, s32 top_margin) const { const float y_scale = (static_cast(m_display_width) / static_cast(m_display_height)) / m_display_pixel_aspect_ratio; @@ -30,8 +30,6 @@ std::tuple HostDisplay::CalculateDrawRect() const const float active_height = static_cast(m_display_active_height) * y_scale; // now fit it within the window - const s32 window_width = m_window_width; - const s32 window_height = m_window_height - m_display_top_margin; const float window_ratio = static_cast(window_width) / static_cast(window_height); float scale; @@ -41,12 +39,12 @@ std::tuple HostDisplay::CalculateDrawRect() const { // align in middle vertically scale = static_cast(window_width) / display_width; - top_padding = (window_height - static_cast(display_height * scale)) / 2; + top_padding = (window_height - top_margin - static_cast(display_height * scale)) / 2; } else { // align in middle horizontally - scale = static_cast(window_height) / display_height; + scale = static_cast(window_height - top_margin) / display_height; left_padding = (window_width - static_cast(display_width * scale)) / 2; } @@ -60,7 +58,7 @@ std::tuple HostDisplay::CalculateDrawRect() const top += std::max(top_padding, 0); // add in margin - top += m_display_top_margin; + top += top_margin; return std::tie(left, top, width, height); } @@ -175,13 +173,13 @@ bool HostDisplay::WriteDisplayTextureToFile(const char* filename, bool full_reso } else if (apply_aspect_ratio) { - const auto [left, top, right, bottom] = CalculateDrawRect(); + const auto [left, top, right, bottom] = CalculateDrawRect(m_window_width, m_window_height, m_display_top_margin); resize_width = right - left; resize_height = bottom - top; } else if (!full_resolution) { - const auto [left, top, right, bottom] = CalculateDrawRect(); + const auto [left, top, right, bottom] = CalculateDrawRect(m_window_width, m_window_height, m_display_top_margin); const float ratio = static_cast(m_display_texture_view_width) / static_cast(std::abs(m_display_texture_view_height)); if (ratio > 1.0f) @@ -214,3 +212,75 @@ bool HostDisplay::WriteDisplayTextureToFile(const char* filename, bool full_reso read_height, filename, true, flip_y, static_cast(resize_width), static_cast(resize_height)); } + +bool HostDisplay::WriteDisplayTextureToBuffer(std::vector* buffer, u32 resize_width /* = 0 */, + u32 resize_height /* = 0 */, bool clear_alpha /* = true */) +{ + if (!m_display_texture_handle) + return false; + + const bool flip_y = (m_display_texture_view_height < 0); + s32 read_width = m_display_texture_view_width; + s32 read_height = m_display_texture_view_height; + s32 read_x = m_display_texture_view_x; + s32 read_y = m_display_texture_view_y; + if (flip_y) + { + read_height = -m_display_texture_view_height; + read_y = (m_display_texture_height - read_height) - (m_display_texture_height - m_display_texture_view_y); + } + + u32 width = static_cast(read_width); + u32 height = static_cast(read_height); + std::vector texture_data(width * height); + u32 texture_data_stride = sizeof(u32) * width; + if (!DownloadTexture(m_display_texture_handle, read_x, read_y, width, height, texture_data.data(), + texture_data_stride)) + { + Log_ErrorPrintf("Failed to download texture from GPU."); + return false; + } + + if (clear_alpha) + { + for (u32& pixel : texture_data) + pixel |= 0xFF000000; + } + + if (flip_y) + { + std::vector temp(width); + for (u32 flip_row = 0; flip_row < (height / 2); flip_row++) + { + u32* top_ptr = &texture_data[flip_row * width]; + u32* bottom_ptr = &texture_data[((height - 1) - flip_row) * width]; + std::memcpy(temp.data(), top_ptr, texture_data_stride); + std::memcpy(top_ptr, bottom_ptr, texture_data_stride); + std::memcpy(bottom_ptr, temp.data(), texture_data_stride); + } + } + + if (resize_width > 0 && resize_height > 0 && (resize_width != width || resize_height != height)) + { + std::vector resized_texture_data(resize_width * resize_height); + u32 resized_texture_stride = sizeof(u32) * resize_width; + if (!stbir_resize_uint8(reinterpret_cast(texture_data.data()), width, height, texture_data_stride, + reinterpret_cast(resized_texture_data.data()), resize_width, resize_height, + resized_texture_stride, 4)) + { + Log_ErrorPrintf("Failed to resize texture data from %ux%u to %ux%u", width, height, resize_width, resize_height); + return false; + } + + width = resize_width; + height = resize_height; + *buffer = std::move(resized_texture_data); + texture_data_stride = resized_texture_stride; + } + else + { + *buffer = texture_data; + } + + return true; +} diff --git a/src/core/host_display.h b/src/core/host_display.h index 6bb969835..0d1066223 100644 --- a/src/core/host_display.h +++ b/src/core/host_display.h @@ -3,6 +3,7 @@ #include "types.h" #include #include +#include // An abstracted RGBA8 texture. class HostDisplayTexture @@ -97,7 +98,7 @@ public: void SetDisplayTopMargin(s32 height) { m_display_top_margin = height; } /// Helper function for computing the draw rectangle in a larger window. - std::tuple CalculateDrawRect() const; + std::tuple CalculateDrawRect(s32 window_width, s32 window_height, s32 top_margin) const; /// Helper function to save texture data to a PNG. If flip_y is set, the image will be flipped aka OpenGL. bool WriteTextureToFile(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, const char* filename, @@ -106,6 +107,10 @@ public: /// Helper function to save current display texture to PNG. bool WriteDisplayTextureToFile(const char* filename, bool full_resolution = true, bool apply_aspect_ratio = true); + /// Helper function to save current display texture to a buffer. + bool WriteDisplayTextureToBuffer(std::vector* buffer, u32 resize_width = 0, u32 resize_height = 0, + bool clear_alpha = true); + protected: s32 m_window_width = 0; s32 m_window_height = 0; diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp index 0d5691f15..4a97f671b 100644 --- a/src/core/host_interface.cpp +++ b/src/core/host_interface.cpp @@ -11,6 +11,7 @@ #include "gpu.h" #include "host_display.h" #include "mdec.h" +#include "save_state_version.h" #include "spu.h" #include "system.h" #include "timers.h" @@ -809,6 +810,61 @@ std::optional HostInterface::GetSaveStateInfo(cons return SaveStateInfo{std::move(path), sd.ModificationTime.AsUnixTimestamp(), slot, global}; } +std::optional HostInterface::GetExtendedSaveStateInfo(const char* game_code, + s32 slot) +{ + const bool global = (!game_code || game_code[0] == 0); + std::string path = global ? GetGlobalSaveStateFileName(slot) : GetGameSaveStateFileName(game_code, slot); + + FILESYSTEM_STAT_DATA sd; + if (!FileSystem::StatFile(path.c_str(), &sd)) + return std::nullopt; + + std::unique_ptr stream = + FileSystem::OpenFile(path.c_str(), BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_SEEKABLE); + if (!stream) + return std::nullopt; + + SAVE_STATE_HEADER header; + if (!stream->Read(&header, sizeof(header)) || header.magic != SAVE_STATE_MAGIC) + return std::nullopt; + + ExtendedSaveStateInfo ssi; + ssi.path = std::move(path); + ssi.timestamp = sd.ModificationTime.AsUnixTimestamp(); + ssi.slot = slot; + ssi.global = global; + + if (header.version != SAVE_STATE_VERSION) + { + ssi.title = StringUtil::StdStringFromFormat("Invalid version %u (expected %u)", header.version, header.magic, + SAVE_STATE_VERSION); + return ssi; + } + + header.title[sizeof(header.title) - 1] = 0; + ssi.title = header.title; + header.game_code[sizeof(header.game_code) - 1] = 0; + ssi.game_code = header.game_code; + + if (header.screenshot_width > 0 && header.screenshot_height > 0 && header.screenshot_size > 0 && + (static_cast(header.offset_to_screenshot) + static_cast(header.screenshot_size)) <= stream->GetSize()) + { + ssi.screenshot_data.resize((header.screenshot_size + 3u) / 4u); + if (stream->Read2(ssi.screenshot_data.data(), header.screenshot_size)) + { + ssi.screenshot_width = header.screenshot_width; + ssi.screenshot_height = header.screenshot_height; + } + else + { + decltype(ssi.screenshot_data)().swap(ssi.screenshot_data); + } + } + + return ssi; +} + void HostInterface::DeleteSaveStates(const char* game_code, bool resume) { const std::vector states(GetAvailableSaveStates(game_code)); diff --git a/src/core/host_interface.h b/src/core/host_interface.h index 6050db5c9..c6c9d8a6b 100644 --- a/src/core/host_interface.h +++ b/src/core/host_interface.h @@ -26,6 +26,35 @@ class HostInterface friend System; public: + enum : s32 + { + PER_GAME_SAVE_STATE_SLOTS = 10, + GLOBAL_SAVE_STATE_SLOTS = 10 + }; + + struct SaveStateInfo + { + std::string path; + u64 timestamp; + s32 slot; + bool global; + }; + + struct ExtendedSaveStateInfo + { + std::string path; + u64 timestamp; + s32 slot; + bool global; + + std::string title; + std::string game_code; + + u32 screenshot_width; + u32 screenshot_height; + std::vector screenshot_data; + }; + HostInterface(); virtual ~HostInterface(); @@ -113,6 +142,15 @@ public: /// such as compiling shaders when starting up. void DisplayLoadingScreen(const char* message, int progress_min = -1, int progress_max = -1, int progress_value = -1); + /// Returns a list of save states for the specified game code. + std::vector GetAvailableSaveStates(const char* game_code) const; + + /// Returns save state info if present. If game_code is null or empty, assumes global state. + std::optional GetSaveStateInfo(const char* game_code, s32 slot); + + /// Returns save state info if present. If game_code is null or empty, assumes global state. + std::optional GetExtendedSaveStateInfo(const char* game_code, s32 slot); + /// Deletes save states for the specified game code. If resume is set, the resume state is deleted too. void DeleteSaveStates(const char* game_code, bool resume); @@ -123,9 +161,7 @@ protected: AUDIO_SAMPLE_RATE = 44100, AUDIO_CHANNELS = 2, AUDIO_BUFFER_SIZE = 2048, - AUDIO_BUFFERS = 2, - PER_GAME_SAVE_STATE_SLOTS = 10, - GLOBAL_SAVE_STATE_SLOTS = 10, + AUDIO_BUFFERS = 2 }; struct OSDMessage @@ -135,14 +171,6 @@ protected: float duration; }; - struct SaveStateInfo - { - std::string path; - u64 timestamp; - s32 slot; - bool global; - }; - virtual bool AcquireHostDisplay() = 0; virtual void ReleaseHostDisplay() = 0; virtual std::unique_ptr CreateAudioStream(AudioBackend backend) = 0; @@ -186,12 +214,6 @@ protected: /// Returns the default path to a memory card for a specific game. std::string GetGameMemoryCardPath(const char* game_code, u32 slot) const; - /// Returns a list of save states for the specified game code. - std::vector GetAvailableSaveStates(const char* game_code) const; - - /// Returns save state info if present. If game_code is null or empty, assumes global state. - std::optional GetSaveStateInfo(const char* game_code, s32 slot); - /// Returns the most recent resume save state. std::string GetMostRecentResumeSaveStatePath() const; diff --git a/src/core/save_state_version.h b/src/core/save_state_version.h index 7dec24c36..75a55330e 100644 --- a/src/core/save_state_version.h +++ b/src/core/save_state_version.h @@ -2,4 +2,30 @@ #include "types.h" static constexpr u32 SAVE_STATE_MAGIC = 0x43435544; -static constexpr u32 SAVE_STATE_VERSION = 23; +static constexpr u32 SAVE_STATE_VERSION = 24; + +#pragma pack(push, 4) +struct SAVE_STATE_HEADER +{ + enum : u32 + { + MAX_TITLE_LENGTH = 128, + MAX_GAME_CODE_LENGTH = 32 + }; + + u32 magic; + u32 version; + char title[MAX_TITLE_LENGTH]; + char game_code[MAX_GAME_CODE_LENGTH]; + + u32 screenshot_width; + u32 screenshot_height; + u32 screenshot_size; + u32 offset_to_screenshot; + + u32 data_compression_type; + u32 data_compressed_size; + u32 data_uncompressed_size; + u32 offset_to_data; +}; +#pragma pack(pop) diff --git a/src/core/system.cpp b/src/core/system.cpp index 787d829dc..5dc7bd83c 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -5,6 +5,7 @@ #include "common/audio_stream.h" #include "common/log.h" #include "common/state_wrapper.h" +#include "common/string_util.h" #include "controller.h" #include "cpu_code_cache.h" #include "cpu_core.h" @@ -309,20 +310,6 @@ bool System::CreateGPU(GPURenderer renderer) bool System::DoState(StateWrapper& sw) { - u32 magic = SAVE_STATE_MAGIC; - u32 version = SAVE_STATE_VERSION; - sw.Do(&magic); - if (magic != SAVE_STATE_MAGIC) - return false; - - sw.Do(&version); - if (version != SAVE_STATE_VERSION) - { - m_host_interface->ReportFormattedError("Save state is incompatible: expecting version %u but state is version %u.", - SAVE_STATE_VERSION, version); - return false; - } - if (!sw.DoMarker("System")) return false; @@ -424,14 +411,87 @@ void System::Reset() bool System::LoadState(ByteStream* state) { + SAVE_STATE_HEADER header; + if (!state->Read2(&header, sizeof(header))) + return false; + + if (header.magic != SAVE_STATE_MAGIC) + return false; + + if (header.version != SAVE_STATE_VERSION) + { + m_host_interface->ReportFormattedError("Save state is incompatible: expecting version %u but state is version %u.", + SAVE_STATE_VERSION, header.version); + return false; + } + + if (header.data_compression_type != 0) + { + m_host_interface->ReportFormattedError("Unknown save state compression type %u", header.data_compression_type); + return false; + } + + if (!state->SeekAbsolute(header.offset_to_data)) + return false; + StateWrapper sw(state, StateWrapper::Mode::Read); return DoState(sw); } bool System::SaveState(ByteStream* state) { - StateWrapper sw(state, StateWrapper::Mode::Write); - return DoState(sw); + SAVE_STATE_HEADER header = {}; + + const u64 header_position = state->GetPosition(); + if (!state->Write2(&header, sizeof(header))) + return false; + + // fill in header + header.magic = SAVE_STATE_MAGIC; + header.version = SAVE_STATE_VERSION; + StringUtil::Strlcpy(header.title, m_running_game_title.c_str(), sizeof(header.title)); + StringUtil::Strlcpy(header.game_code, m_running_game_code.c_str(), sizeof(header.game_code)); + + // save screenshot + { + const u32 screenshot_width = 128; + const u32 screenshot_height = 128; + + std::vector screenshot_buffer; + if (m_host_interface->GetDisplay()->WriteDisplayTextureToBuffer(&screenshot_buffer, screenshot_width, + screenshot_height) && + !screenshot_buffer.empty()) + { + header.offset_to_screenshot = static_cast(state->GetPosition()); + header.screenshot_width = screenshot_width; + header.screenshot_height = screenshot_height; + header.screenshot_size = static_cast(screenshot_buffer.size() * sizeof(u32)); + if (!state->Write2(screenshot_buffer.data(), header.screenshot_size)) + return false; + } + } + + // write data + { + header.offset_to_data = static_cast(state->GetPosition()); + + StateWrapper sw(state, StateWrapper::Mode::Write); + if (!DoState(sw)) + return false; + + header.data_compression_type = 0; + header.data_uncompressed_size = static_cast(state->GetPosition() - header.offset_to_data); + } + + // re-write header + const u64 end_position = state->GetPosition(); + if (!state->SeekAbsolute(header_position) || !state->Write2(&header, sizeof(header)) || + !state->SeekAbsolute(end_position)) + { + return false; + } + + return true; } void System::RunFrame() diff --git a/src/duckstation-qt/d3d11displaywidget.cpp b/src/duckstation-qt/d3d11displaywidget.cpp index f25c768c4..756817057 100644 --- a/src/duckstation-qt/d3d11displaywidget.cpp +++ b/src/duckstation-qt/d3d11displaywidget.cpp @@ -477,7 +477,7 @@ void D3D11DisplayWidget::renderDisplay() if (!m_display_texture_handle) return; - auto [vp_left, vp_top, vp_width, vp_height] = CalculateDrawRect(); + auto [vp_left, vp_top, vp_width, vp_height] = CalculateDrawRect(m_window_width, m_window_height, m_display_top_margin); m_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); m_context->VSSetShader(m_display_vertex_shader.Get(), nullptr, 0); diff --git a/src/duckstation-qt/opengldisplaywidget.cpp b/src/duckstation-qt/opengldisplaywidget.cpp index 906d8fbae..0b698f065 100644 --- a/src/duckstation-qt/opengldisplaywidget.cpp +++ b/src/duckstation-qt/opengldisplaywidget.cpp @@ -494,7 +494,7 @@ void OpenGLDisplayWidget::renderDisplay() if (!m_display_texture_handle) return; - const auto [vp_left, vp_top, vp_width, vp_height] = CalculateDrawRect(); + const auto [vp_left, vp_top, vp_width, vp_height] = CalculateDrawRect(m_window_width, m_window_height, m_display_top_margin); glViewport(vp_left, m_window_height - (m_display_top_margin + vp_top) - vp_height, vp_width, vp_height); glDisable(GL_BLEND); diff --git a/src/duckstation-sdl/d3d11_host_display.cpp b/src/duckstation-sdl/d3d11_host_display.cpp index 8a61216d7..434c3f26b 100644 --- a/src/duckstation-sdl/d3d11_host_display.cpp +++ b/src/duckstation-sdl/d3d11_host_display.cpp @@ -419,7 +419,7 @@ void D3D11HostDisplay::RenderDisplay() if (!m_display_texture_handle) return; - const auto [vp_left, vp_top, vp_width, vp_height] = CalculateDrawRect(); + const auto [vp_left, vp_top, vp_width, vp_height] = CalculateDrawRect(m_window_width, m_window_height, m_display_top_margin); m_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); m_context->VSSetShader(m_display_vertex_shader.Get(), nullptr, 0); diff --git a/src/duckstation-sdl/opengl_host_display.cpp b/src/duckstation-sdl/opengl_host_display.cpp index fbdd875b3..69bf3c3bf 100644 --- a/src/duckstation-sdl/opengl_host_display.cpp +++ b/src/duckstation-sdl/opengl_host_display.cpp @@ -401,7 +401,7 @@ void OpenGLHostDisplay::RenderDisplay() if (!m_display_texture_handle) return; - const auto [vp_left, vp_top, vp_width, vp_height] = CalculateDrawRect(); + const auto [vp_left, vp_top, vp_width, vp_height] = CalculateDrawRect(m_window_width, m_window_height, m_display_top_margin); glViewport(vp_left, m_window_height - vp_top - vp_height, vp_width, vp_height); glDisable(GL_BLEND);