diff options
author | Garrett Brown <themagnificentmrb@gmail.com> | 2018-07-05 20:06:48 -0700 |
---|---|---|
committer | Garrett Brown <themagnificentmrb@gmail.com> | 2018-08-14 11:29:15 -0700 |
commit | 3ed099ae0d9ea153f641d00e4c627ba8c8715a24 (patch) | |
tree | 5cd347caf854ead53bfde225ffee86ebb32c013c | |
parent | cd50c4f4219773548d839a94feaf57d8c6dff3b9 (diff) |
RetroPlayer: Merge savestate metadata into .sav file
This removes the auxiliary XML metadata file used for savestates. Now,
metadata is serialized into the .sav file using FlatBuffers.
17 files changed, 810 insertions, 194 deletions
diff --git a/cmake/messages/flatbuffers/retroplayer.txt b/cmake/messages/flatbuffers/retroplayer.txt new file mode 100644 index 0000000000..3d42d5c812 --- /dev/null +++ b/cmake/messages/flatbuffers/retroplayer.txt @@ -0,0 +1 @@ +xbmc/cores/RetroPlayer/messages cores/RetroPlayer/messages diff --git a/xbmc/cores/RetroPlayer/RetroPlayer.cpp b/xbmc/cores/RetroPlayer/RetroPlayer.cpp index b1065d1c95..87b8f047d4 100644 --- a/xbmc/cores/RetroPlayer/RetroPlayer.cpp +++ b/xbmc/cores/RetroPlayer/RetroPlayer.cpp @@ -18,7 +18,8 @@ #include "cores/RetroPlayer/playback/ReversiblePlayback.h" #include "cores/RetroPlayer/process/RPProcessInfo.h" #include "cores/RetroPlayer/rendering/RPRenderManager.h" -#include "cores/RetroPlayer/savestates/Savestate.h" +#include "cores/RetroPlayer/savestates/ISavestate.h" +#include "cores/RetroPlayer/savestates/SavestateDatabase.h" #include "cores/RetroPlayer/savestates/SavestateUtils.h" #include "cores/RetroPlayer/streams/RPStreamManager.h" #include "dialogs/GUIDialogYesNo.h" @@ -42,6 +43,7 @@ #include "utils/log.h" #include "utils/MathUtils.h" #include "utils/StringUtils.h" +#include "utils/URIUtils.h" #include "FileItem.h" #include "ServiceBroker.h" #include "URL.h" @@ -144,16 +146,16 @@ bool CRetroPlayer::OpenFile(const CFileItem& file, const CPlayerOptions& options if (bSuccess && !bStandalone) { - std::string savestatePath = CSavestateUtils::MakeMetadataPath(fileCopy.GetPath()); + CSavestateDatabase savestateDb; - CSavestate save; - if (save.Deserialize(savestatePath)) + std::unique_ptr<ISavestate> save = savestateDb.CreateSavestate(); + if (savestateDb.GetSavestate(fileCopy.GetPath(), *save)) { // Check if game client is the same - if (save.GameClient() != m_gameClient->ID()) + if (save->GameClientID() != m_gameClient->ID()) { ADDON::AddonPtr addon; - if (CServiceBroker::GetAddonMgr().GetAddon(save.GameClient(), addon)) + if (CServiceBroker::GetAddonMgr().GetAddon(save->GameClientID(), addon)) { // Warn the user that continuing with a different game client will // overwrite the save @@ -555,15 +557,10 @@ void CRetroPlayer::CreatePlayback(bool bRestoreState) const bool bStandalone = m_gameClient->GetGamePath().empty(); if (!bStandalone) { - std::string savestatePath = CSavestateUtils::MakeMetadataPath(m_gameClient->GetGamePath()); - if (!savestatePath.empty()) - { - std::string redactedSavestatePath = CURL::GetRedacted(savestatePath); - CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Loading savestate %s", redactedSavestatePath.c_str()); + CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Loading savestate"); - if (!SetPlayerState(savestatePath)) - CLog::Log(LOGERROR, "RetroPlayer[SAVE]: Failed to load savestate"); - } + if (!SetPlayerState(m_gameClient->GetGamePath())) + CLog::Log(LOGERROR, "RetroPlayer[SAVE]: Failed to load savestate"); } } diff --git a/xbmc/cores/RetroPlayer/messages/CMakeLists.txt b/xbmc/cores/RetroPlayer/messages/CMakeLists.txt new file mode 100644 index 0000000000..e2e2ba6625 --- /dev/null +++ b/xbmc/cores/RetroPlayer/messages/CMakeLists.txt @@ -0,0 +1,19 @@ +set(MESSAGES savestate.fbs) + +foreach(_file ${MESSAGES}) + get_filename_component(FLATC_OUTPUT ${_file} NAME_WE) + set(FLATC_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${FLATC_OUTPUT}_generated.h) + list(APPEND FLATC_OUTPUTS ${FLATC_OUTPUT}) + + add_custom_command(OUTPUT ${FLATC_OUTPUT} + COMMAND ${FLATBUFFERS_FLATC_EXECUTABLE} + ARGS -c -o "${FLATBUFFERS_MESSAGES_INCLUDE_DIR}/" ${_file} + DEPENDS ${_file} + COMMENT "Building C++ header for ${_file}" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) +endforeach() + +add_custom_target(retroplayer_messages DEPENDS ${FLATC_OUTPUTS}) +set_target_properties(retroplayer_messages PROPERTIES FOLDER "Generated Messages" + INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR} + SOURCES ${FLATC_OUTPUTS}) diff --git a/xbmc/cores/RetroPlayer/messages/savestate.fbs b/xbmc/cores/RetroPlayer/messages/savestate.fbs new file mode 100644 index 0000000000..1d708d61f9 --- /dev/null +++ b/xbmc/cores/RetroPlayer/messages/savestate.fbs @@ -0,0 +1,47 @@ +// +// Copyright (C) 2018 Team Kodi +// This file is part of Kodi - https://kodi.tv +// +// SPDX-License-Identifier: MIT +// See LICENSES/README.md for more information. +// + +namespace KODI.RETRO; + +// Savestate schema +// Version 1 + +file_identifier "SAV_"; + +enum SaveType : uint8 { + Unknown, + Auto, + Manual +} + +table Savestate { + // Schema version + version:uint8; + + // Savestate properties + type:SaveType; + slot:uint8; + label:string; + created:string; // RFC 1123 date time + + // Game properties + game_file_name:string; + + // Environment properties + timestamp_frames:uint64; + timestamp_wall_clock_ns:uint64; + + // Emulator properties + emulator_addon_id:string; + emulator_version:string; // Semantic version + + // Memory properties + memory_data:[uint8]; +} + +root_type Savestate; diff --git a/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp b/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp index 71a7132040..49b4249e87 100644 --- a/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp +++ b/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp @@ -7,16 +7,15 @@ */ #include "ReversiblePlayback.h" -#include "cores/RetroPlayer/savestates/Savestate.h" -#include "cores/RetroPlayer/savestates/SavestateReader.h" -#include "cores/RetroPlayer/savestates/SavestateWriter.h" -#include "cores/RetroPlayer/streams/memory/BasicMemoryStream.h" +#include "cores/RetroPlayer/savestates/ISavestate.h" +#include "cores/RetroPlayer/savestates/SavestateDatabase.h" #include "cores/RetroPlayer/streams/memory/DeltaPairMemoryStream.h" #include "games/addons/GameClient.h" #include "games/GameServices.h" #include "games/GameSettings.h" #include "threads/SingleLock.h" #include "utils/MathUtils.h" +#include "utils/URIUtils.h" #include "ServiceBroker.h" #include <algorithm> @@ -29,8 +28,7 @@ using namespace RETRO; CReversiblePlayback::CReversiblePlayback(GAME::CGameClient* gameClient, double fps, size_t serializeSize) : m_gameClient(gameClient), m_gameLoop(this, fps), - m_savestateWriter(new CSavestateWriter), - m_savestateReader(new CSavestateReader), + m_savestateDatabase(new CSavestateDatabase), m_totalFrameCount(0), m_pastFrameCount(0), m_futureFrameCount(0), @@ -109,106 +107,91 @@ void CReversiblePlayback::PauseAsync() std::string CReversiblePlayback::CreateSavestate() { - std::string empty; + const size_t memorySize = m_gameClient->SerializeSize(); // Game client must support serialization - if (m_gameClient->SerializeSize() == 0) - return empty; + if (memorySize == 0) + return ""; - if (!m_savestateWriter->Initialize(m_gameClient, m_totalFrameCount)) - return empty; + //! @todo Handle savestates for standalone game clients + if (m_gameClient->GetGamePath().empty()) + { + return ""; + } + + const CDateTime now = CDateTime::GetCurrentDateTime(); + const std::string label = now.GetAsLocalizedDateTime(); + const std::string gameFileName = URIUtils::GetFileName(m_gameClient->GetGamePath()); + const uint64_t timestampFrames = m_totalFrameCount; + const double timestampWallClock = (m_totalFrameCount / m_gameClient->GetFrameRate()); //! @todo Accumulate playtime instead of deriving it + const std::string gameClientId = m_gameClient->ID(); + const std::string gameClientVersion = m_gameClient->Version().asString(); + + std::unique_ptr<ISavestate> savestate = m_savestateDatabase->CreateSavestate(); + + savestate->SetType(SAVE_TYPE::AUTO); + savestate->SetLabel(label); + savestate->SetCreated(now); + savestate->SetGameFileName(gameFileName); + savestate->SetTimestampFrames(timestampFrames); + savestate->SetTimestampWallClock(timestampWallClock); + savestate->SetGameClientID(gameClientId); + savestate->SetGameClientVersion(gameClientVersion); - std::unique_ptr<IMemoryStream> memoryStream; - bool bHasMemoryStream = false; + uint8_t *memoryData = savestate->GetMemoryBuffer(memorySize); { CSingleLock lock(m_mutex); - if (m_memoryStream) + if (m_memoryStream && m_memoryStream->CurrentFrame() != nullptr) { - memoryStream = std::move(m_memoryStream); - bHasMemoryStream = true; + std::memcpy(memoryData, m_memoryStream->CurrentFrame(), memorySize); } else { lock.Leave(); - memoryStream.reset(new CBasicMemoryStream); - memoryStream->Init(m_gameClient->SerializeSize(), 1); + if (!m_gameClient->Serialize(memoryData, memorySize)) + return ""; } } - // If memory stream is empty, ask the game client for a frame - if (memoryStream->CurrentFrame() == nullptr) - { - if (m_gameClient->Serialize(memoryStream->BeginFrame(), memoryStream->FrameSize())) - memoryStream->SubmitFrame(); - } + savestate->Finalize(); - bool bSuccess = false; - - if (m_savestateWriter->WriteSave(memoryStream->CurrentFrame(), memoryStream->FrameSize())) - { - m_savestateWriter->WriteThumb(); + if (!m_savestateDatabase->AddSavestate(m_gameClient->GetGamePath(), *savestate)) + return ""; - if (m_savestateWriter->CommitToDatabase()) - bSuccess = true; - else - m_savestateWriter->CleanUpTransaction(); - } - - if (bHasMemoryStream) - { - CSingleLock lock(m_mutex); - m_memoryStream = std::move(memoryStream); - } - - return bSuccess ? m_savestateWriter->GetPath() : ""; + return m_gameClient->GetGamePath(); } bool CReversiblePlayback::LoadSavestate(const std::string& path) { - // Game client must support serialization - if (m_gameClient->SerializeSize() == 0) - return false; + const size_t memorySize = m_gameClient->SerializeSize(); - if (!m_savestateReader->Initialize(path, m_gameClient)) + // Game client must support serialization + if (memorySize == 0) return false; - std::unique_ptr<IMemoryStream> memoryStream; - bool bHasMemoryStream = false; + bool bSuccess = false; + std::unique_ptr<ISavestate> savestate = m_savestateDatabase->CreateSavestate(); + if (m_savestateDatabase->GetSavestate(path, *savestate) && savestate->GetMemorySize() == memorySize) { - CSingleLock lock(m_mutex); - if (m_memoryStream) { - memoryStream = std::move(m_memoryStream); - bHasMemoryStream = true; + CSingleLock lock(m_mutex); + if (m_memoryStream) + { + m_memoryStream->SetFrameCounter(savestate->TimestampFrames()); + std::memcpy(m_memoryStream->BeginFrame(), savestate->GetMemoryData(), memorySize); + m_memoryStream->SubmitFrame(); + } } - else + + if (m_gameClient->Deserialize(savestate->GetMemoryData(), memorySize)) { - lock.Leave(); - memoryStream.reset(new CBasicMemoryStream); - memoryStream->Init(m_gameClient->SerializeSize(), 1); + m_totalFrameCount = savestate->TimestampFrames(); + bSuccess = true; } } - bool bSuccess = false; - - if (m_savestateReader->ReadSave(memoryStream->BeginFrame(), memoryStream->FrameSize())) - { - memoryStream->SubmitFrame(); - - m_gameClient->Deserialize(memoryStream->CurrentFrame(), memoryStream->FrameSize()); - m_totalFrameCount = m_savestateReader->GetFrameCount(); - - bSuccess = true; - } - - if (bHasMemoryStream) - { - CSingleLock lock(m_mutex); - m_memoryStream = std::move(memoryStream); - } - return bSuccess; } diff --git a/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h b/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h index 45075c8458..4e2569d6cf 100644 --- a/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h +++ b/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h @@ -26,8 +26,7 @@ namespace GAME namespace RETRO { - class CSavestateReader; - class CSavestateWriter; + class CSavestateDatabase; class IMemoryStream; class CReversiblePlayback : public IPlayback, @@ -77,8 +76,7 @@ namespace RETRO CCriticalSection m_mutex; // Savestate functionality - std::unique_ptr<CSavestateWriter> m_savestateWriter; - std::unique_ptr<CSavestateReader> m_savestateReader; + std::unique_ptr<CSavestateDatabase> m_savestateDatabase; // Playback stats uint64_t m_totalFrameCount; diff --git a/xbmc/cores/RetroPlayer/savestates/CMakeLists.txt b/xbmc/cores/RetroPlayer/savestates/CMakeLists.txt index 5f835e5914..29e7b3177c 100644 --- a/xbmc/cores/RetroPlayer/savestates/CMakeLists.txt +++ b/xbmc/cores/RetroPlayer/savestates/CMakeLists.txt @@ -1,16 +1,21 @@ -set(SOURCES Savestate.cpp - SavestateDatabase.cpp - SavestateReader.cpp - SavestateTranslator.cpp +set(SOURCES SavestateDatabase.cpp + SavestateFlatBuffer.cpp SavestateUtils.cpp - SavestateWriter.cpp) +) -set(HEADERS Savestate.h +set(HEADERS ISavestate.h SavestateDatabase.h - SavestateDefines.h - SavestateReader.h - SavestateTranslator.h + SavestateFlatBuffer.h + SavestateTypes.h SavestateUtils.h - SavestateWriter.h) +) core_add_library(retroplayer_savestates) + +set(DEPENDS retroplayer_messages) + +if(ENABLE_STATIC_LIBS) + add_dependencies(retroplayer_savestates ${DEPENDS}) +else() + add_dependencies(lib${APP_NAME_LC} ${DEPENDS}) +endif() diff --git a/xbmc/cores/RetroPlayer/savestates/ISavestate.h b/xbmc/cores/RetroPlayer/savestates/ISavestate.h new file mode 100644 index 0000000000..ede53b1ce1 --- /dev/null +++ b/xbmc/cores/RetroPlayer/savestates/ISavestate.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "SavestateTypes.h" +#include "XBDateTime.h" + +#include <stdint.h> +#include <string> +#include <vector> + +namespace KODI +{ +namespace RETRO +{ + class ISavestate + { + public: + virtual ~ISavestate() = default; + + /*! + * \brief Reset to the initial state + */ + virtual void Reset() = 0; + + /*! + * Access the data representation of this savestate + */ + virtual bool Serialize(const uint8_t *&data, size_t &size) const = 0; + + /// @name Savestate properties + ///{ + /*! + * \brief The type of save action that created this savestate, either + * manual or automatic + */ + virtual SAVE_TYPE Type() const = 0; + + /*! + * \brief The slot this savestate was saved into, or 0 for no slot + * + * This allows for keyboard access of saved games using the number keys 1-9. + */ + virtual uint8_t Slot() const = 0; + + /*! + * \brief The label shown in the GUI for this savestate + */ + virtual std::string Label() const = 0; + + /*! + * \brief The timestamp of this savestate's creation + */ + virtual CDateTime Created() const = 0; + ///} + + /// @name Game properties + ///{ + /*! + * \brief The name of the file beloning to this savestate's game + */ + virtual std::string GameFileName() const = 0; + ///} + + /// @name Environment properties + ///{ + /*! + * \brief The number of frames in the entire gameplay history + */ + virtual uint64_t TimestampFrames() const = 0; + + /*! + * \brief The duration of the entire gameplay history as seen by a wall clock + */ + virtual double TimestampWallClock() const = 0; + ///} + + /// @name Game client properties + ///{ + /*! + * \brief The game client add-on ID that created this savestate + */ + virtual std::string GameClientID() const = 0; + + /*! + * \brief The semantic version of the game client + */ + virtual std::string GameClientVersion() const = 0; + ///} + + /// @name Memory properties + ///{ + /*! + * \brief A pointer to the internal memory (SRAM) of the frame + */ + virtual const uint8_t *GetMemoryData() const = 0; + + /*! + * \brief The size of the memory region returned by GetMemoryData() + */ + virtual size_t GetMemorySize() const = 0; + ///} + + // Build flatbuffer by setting individual fields + virtual void SetType(SAVE_TYPE type) = 0; + virtual void SetSlot(uint8_t slot) = 0; + virtual void SetLabel(const std::string &label) = 0; + virtual void SetCreated(const CDateTime &created) = 0; + virtual void SetGameFileName(const std::string &gameFileName) = 0; + virtual void SetTimestampFrames(uint64_t timestampFrames) = 0; + virtual void SetTimestampWallClock(double timestampWallClock) = 0; + virtual void SetGameClientID(const std::string &gameClient) = 0; + virtual void SetGameClientVersion(const std::string &gameClient) = 0; + virtual uint8_t *GetMemoryBuffer(size_t size) = 0; + virtual void Finalize() = 0; + + /*! + * \brief Take ownership and initialize the flatbuffer with the given vector + */ + virtual bool Deserialize(std::vector<uint8_t> data) = 0; + }; +} +} diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp b/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp index 6b987f7ca8..45d021cdf1 100644 --- a/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp +++ b/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp @@ -7,30 +7,92 @@ */ #include "SavestateDatabase.h" -#include "Savestate.h" -#include "SavestateDefines.h" +#include "SavestateFlatBuffer.h" #include "SavestateUtils.h" -#include "ServiceBroker.h" -#include "addons/AddonManager.h" -#include "games/GameTypes.h" -#include "games/tags/GameInfoTag.h" -#include "utils/StringUtils.h" -#include "utils/Variant.h" -#include "FileItem.h" +#include "filesystem/File.h" +#include "utils/log.h" +#include "URL.h" using namespace KODI; using namespace RETRO; CSavestateDatabase::CSavestateDatabase() = default; -bool CSavestateDatabase::AddSavestate(const CSavestate& save) +std::unique_ptr<ISavestate> CSavestateDatabase::CreateSavestate() { - return save.Serialize(CSavestateUtils::MakeMetadataPath(save.GamePath())); + std::unique_ptr<ISavestate> savestate; + + savestate.reset(new CSavestateFlatBuffer); + + return savestate; +} + +bool CSavestateDatabase::AddSavestate(const std::string &gamePath, const ISavestate& save) +{ + bool bSuccess = false; + + const std::string savestatePath = CSavestateUtils::MakePath(gamePath); + + CLog::Log(LOGDEBUG, "Saving savestate to %s", CURL::GetRedacted(savestatePath).c_str()); + + const uint8_t *data = nullptr; + size_t size = 0; + if (save.Serialize(data, size)) + { + XFILE::CFile file; + if (file.OpenForWrite(savestatePath)) + { + const ssize_t written = file.Write(data, size); + if (written == static_cast<ssize_t>(size)) + { + CLog::Log(LOGDEBUG, "Wrote savestate of %u bytes", size); + bSuccess = true; + } + } + else + CLog::Log(LOGERROR, "Failed to open savestate for writing"); + } + + return bSuccess; } -bool CSavestateDatabase::GetSavestate(const std::string& path, CSavestate& save) +bool CSavestateDatabase::GetSavestate(const std::string& gamePath, ISavestate& save) { - return save.Deserialize(path); + bool bSuccess = false; + + const std::string savestatePath = CSavestateUtils::MakePath(gamePath); + + CLog::Log(LOGDEBUG, "Loading savestate from %s", CURL::GetRedacted(savestatePath).c_str()); + + std::vector<uint8_t> savestateData; + + XFILE::CFile savestateFile; + if (savestateFile.Open(savestatePath, XFILE::READ_TRUNCATED)) + { + int64_t size = savestateFile.GetLength(); + if (size > 0) + { + savestateData.resize(static_cast<size_t>(size)); + + const ssize_t readLength = savestateFile.Read(savestateData.data(), savestateData.size()); + if (readLength != static_cast<ssize_t>(savestateData.size())) + { + CLog::Log(LOGERROR, "Failed to read savestate %s of size %d bytes", + CURL::GetRedacted(savestatePath).c_str(), + size); + savestateData.clear(); + } + } + else + CLog::Log(LOGERROR, "Failed to get savestate length: %s", CURL::GetRedacted(savestatePath).c_str()); + } + else + CLog::Log(LOGERROR, "Failed to open savestate file %s", CURL::GetRedacted(savestatePath).c_str()); + + if (!savestateData.empty()) + bSuccess = save.Deserialize(std::move(savestateData)); + + return bSuccess; } bool CSavestateDatabase::GetSavestatesNav(CFileItemList& items, const std::string& gamePath, const std::string& gameClient /* = "" */) @@ -56,34 +118,3 @@ bool CSavestateDatabase::ClearSavestatesOfGame(const std::string& gamePath, cons //! @todo return false; } - -CFileItem* CSavestateDatabase::CreateFileItem(const CVariant& object) const -{ - using namespace ADDON; - - CSavestate save; - save.Deserialize(object); - CFileItem* item = new CFileItem(save.Label()); - - item->SetPath(save.Path()); - if (!save.Thumbnail().empty()) - item->SetArt("thumb", save.Thumbnail()); - else - { - AddonPtr addon; - if (CServiceBroker::GetAddonMgr().GetAddon(save.GameClient(), addon, ADDON_GAMEDLL)) - item->SetArt("thumb", addon->Icon()); - } - - // Use the slot number as the second label - if (save.Type() == SAVETYPE::SLOT) - item->SetLabel2(StringUtils::Format("%u", save.Slot())); - - item->m_dateTime = save.Timestamp(); - item->SetProperty(FILEITEM_PROPERTY_SAVESTATE_DURATION, static_cast<uint64_t>(save.PlaytimeWallClock())); - item->GetGameInfoTag()->SetGameClient(save.GameClient()); - item->m_dwSize = save.Size(); - item->m_bIsFolder = false; - - return item; -} diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h b/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h index 9dd5c025d0..473e3cf132 100644 --- a/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h +++ b/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h @@ -8,19 +8,16 @@ #pragma once +#include <memory> #include <string> -#define SAVESTATES_DATABASE_NAME "Savestates" - -class CFileItem; class CFileItemList; -class CVariant; namespace KODI { namespace RETRO { - class CSavestate; + class ISavestate; class CSavestateDatabase { @@ -28,9 +25,11 @@ namespace RETRO CSavestateDatabase(); virtual ~CSavestateDatabase() = default; - bool AddSavestate(const CSavestate& save); + std::unique_ptr<ISavestate> CreateSavestate(); + + bool AddSavestate(const std::string &gamePath, const ISavestate& save); - bool GetSavestate(const std::string& path, CSavestate& save); + bool GetSavestate(const std::string& gamePath, ISavestate& save); bool GetSavestatesNav(CFileItemList& items, const std::string& gamePath, const std::string& gameClient = ""); @@ -39,9 +38,6 @@ namespace RETRO bool DeleteSavestate(const std::string& path); bool ClearSavestatesOfGame(const std::string& gamePath, const std::string& gameClient = ""); - - private: - CFileItem* CreateFileItem(const CVariant& object) const; }; } } diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.cpp b/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.cpp new file mode 100644 index 0000000000..bd92f89498 --- /dev/null +++ b/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.cpp @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "SavestateFlatBuffer.h" +#include "utils/log.h" + +#include "savestate_generated.h" + +using namespace KODI; +using namespace RETRO; + +namespace +{ + const uint8_t SCHEMA_VERSION = 1; + + /*! + * \brief The initial size of the FlatBuffer's memory buffer + * + * 1024 is the default size in the FlatBuffers header. We might as well use + * this until our size requirements are more known. + */ + const size_t INITIAL_FLATBUFFER_SIZE = 1024; + + /*! + * \brief Translate the save type (RetroPlayer to FlatBuffers) + */ + SaveType TranslateType(SAVE_TYPE type) + { + switch (type) + { + case SAVE_TYPE::AUTO: + return SaveType_Auto; + case SAVE_TYPE::MANUAL: + return SaveType_Manual; + default: + break; + } + + return SaveType_Unknown; + } + + /*! + * \brief Translate the save type (FlatBuffers to RetroPlayer) + */ + SAVE_TYPE TranslateType(SaveType type) + { + switch (type) + { + case SaveType_Auto: + return SAVE_TYPE::AUTO; + case SaveType_Manual: + return SAVE_TYPE::MANUAL; + default: + break; + } + + return SAVE_TYPE::UNKNOWN; + } +} + +CSavestateFlatBuffer::CSavestateFlatBuffer() +{ + Reset(); +} + +CSavestateFlatBuffer::~CSavestateFlatBuffer() = default; + +void CSavestateFlatBuffer::Reset() +{ + m_builder.reset(new flatbuffers::FlatBufferBuilder(INITIAL_FLATBUFFER_SIZE)); + m_data.clear(); + m_savestate = nullptr; +} + +bool CSavestateFlatBuffer::Serialize(const uint8_t *&data, size_t &size) const +{ + // Check if savestate was deserialized from vector or built with FlatBuffers + if (!m_data.empty()) + { + data = m_data.data(); + size = m_data.size(); + } + else + { + data = m_builder->GetBufferPointer(); + size = m_builder->GetSize(); + } + + return true; +} + +SAVE_TYPE CSavestateFlatBuffer::Type() const +{ + if (m_savestate != nullptr) + return TranslateType(m_savestate->type()); + + return SAVE_TYPE::UNKNOWN; +} + +void CSavestateFlatBuffer::SetType(SAVE_TYPE type) +{ + m_type = type; +} + +uint8_t CSavestateFlatBuffer::Slot() const +{ + if (m_savestate != nullptr) + return m_savestate->slot(); + + return 0; +} + +void CSavestateFlatBuffer::SetSlot(uint8_t slot) +{ + m_slot = slot; +} + +std::string CSavestateFlatBuffer::Label() const +{ + std::string label; + + if (m_savestate != nullptr && m_savestate->label()) + label = m_savestate->label()->c_str(); + + return label; +} + +void CSavestateFlatBuffer::SetLabel(const std::string &label) +{ + m_labelOffset.reset(new StringOffset{ m_builder->CreateString(label) }); +} + +CDateTime CSavestateFlatBuffer::Created() const +{ + CDateTime created; + + if (m_savestate != nullptr && m_savestate->created()) + created.SetFromRFC1123DateTime(m_savestate->created()->c_str()); + + return created; +} + +void CSavestateFlatBuffer::SetCreated(const CDateTime &created) +{ + m_createdOffset.reset(new StringOffset{ m_builder->CreateString(created.GetAsRFC1123DateTime()) }); +} + +std::string CSavestateFlatBuffer::GameFileName() const +{ + std::string gameFileName; + + if (m_savestate != nullptr && m_savestate->game_file_name()) + gameFileName = m_savestate->game_file_name()->c_str(); + + return gameFileName; +} + +void CSavestateFlatBuffer::SetGameFileName(const std::string &gameFileName) +{ + m_gameFileNameOffset.reset(new StringOffset{ m_builder->CreateString(gameFileName) }); +} + +uint64_t CSavestateFlatBuffer::TimestampFrames() const +{ + return m_savestate->timestamp_frames(); +} + +void CSavestateFlatBuffer::SetTimestampFrames(uint64_t timestampFrames) +{ + m_timestampFrames = timestampFrames; +} + +double CSavestateFlatBuffer::TimestampWallClock() const +{ + if (m_savestate != nullptr) + return static_cast<double>(m_savestate->timestamp_wall_clock_ns()) / 1000.0 / 1000.0 / 1000.0; + + return 0.0; +} + +void CSavestateFlatBuffer::SetTimestampWallClock(double timestampWallClock) +{ + m_timestampWallClock = timestampWallClock; +} + +std::string CSavestateFlatBuffer::GameClientID() const +{ + std::string gameClientId; + + if (m_savestate != nullptr && m_savestate->emulator_addon_id()) + gameClientId = m_savestate->emulator_addon_id()->c_str(); + + return gameClientId; +} + +void CSavestateFlatBuffer::SetGameClientID(const std::string &gameClientId) +{ + m_emulatorAddonIdOffset.reset(new StringOffset{ m_builder->CreateString(gameClientId) }); +} + +std::string CSavestateFlatBuffer::GameClientVersion() const +{ + std::string gameClientVersion; + + if (m_savestate != nullptr && m_savestate->emulator_version()) + gameClientVersion = m_savestate->emulator_version()->c_str(); + + return gameClientVersion; +} + +void CSavestateFlatBuffer::SetGameClientVersion(const std::string &gameClientVersion) +{ + m_emulatorVersionOffset.reset(new StringOffset{ m_builder->CreateString(gameClientVersion) }); +} + +const uint8_t *CSavestateFlatBuffer::GetMemoryData() const +{ + if (m_savestate != nullptr && m_savestate->memory_data()) + return m_savestate->memory_data()->data(); + + return nullptr; +} + +size_t CSavestateFlatBuffer::GetMemorySize() const +{ + if (m_savestate != nullptr && m_savestate->memory_data()) + return m_savestate->memory_data()->size(); + + return 0; +} + +uint8_t *CSavestateFlatBuffer::GetMemoryBuffer(size_t size) +{ + uint8_t *memoryBuffer = nullptr; + + m_memoryDataOffset.reset(new VectorOffset{ m_builder->CreateUninitializedVector(size, &memoryBuffer) }); + + return memoryBuffer; +} + +void CSavestateFlatBuffer::Finalize() +{ + // Helper class to build the nested Savestate table + SavestateBuilder savestateBuilder(*m_builder); + + savestateBuilder.add_version(SCHEMA_VERSION); + + savestateBuilder.add_type(TranslateType(m_type)); + + savestateBuilder.add_slot(m_slot); + + if (m_labelOffset) + { + savestateBuilder.add_label(*m_labelOffset); + m_labelOffset.reset(); + } + + if (m_createdOffset) + { + savestateBuilder.add_created(*m_createdOffset); + m_createdOffset.reset(); + } + + if (m_gameFileNameOffset) + { + savestateBuilder.add_game_file_name(*m_gameFileNameOffset); + m_gameFileNameOffset.reset(); + } + + savestateBuilder.add_timestamp_frames(m_timestampFrames); + + const uint64_t wallClockNs = static_cast<uint64_t>(m_timestampWallClock * 1000.0 * 1000.0 * 1000.0); + savestateBuilder.add_timestamp_wall_clock_ns(wallClockNs); + + if (m_emulatorAddonIdOffset) + { + savestateBuilder.add_emulator_addon_id(*m_emulatorAddonIdOffset); + m_emulatorAddonIdOffset.reset(); + } + + if (m_emulatorVersionOffset) + { + savestateBuilder.add_emulator_version(*m_emulatorVersionOffset); + m_emulatorVersionOffset.reset(); + } + + if (m_memoryDataOffset) + { + savestateBuilder.add_memory_data(*m_memoryDataOffset); + m_memoryDataOffset.reset(); + } + + auto savestate = savestateBuilder.Finish(); + FinishSavestateBuffer(*m_builder, savestate); + + m_savestate = GetSavestate(m_builder->GetBufferPointer()); +} + +bool CSavestateFlatBuffer::Deserialize(std::vector<uint8_t> data) +{ + flatbuffers::Verifier verifier(data.data(), data.size()); + if (VerifySavestateBuffer(verifier)) + { + const Savestate *savestate = GetSavestate(data.data()); + + if (savestate->version() != SCHEMA_VERSION) + { + CLog::Log(LOGERROR, "RetroPlayer[SAVE): Schema version %u not supported, must be version %u", + savestate->version(), + SCHEMA_VERSION); + } + else + { + m_data = std::move(data); + m_savestate = GetSavestate(m_data.data()); + return true; + } + } + + return false; +} diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.h b/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.h new file mode 100644 index 0000000000..b59b08ce93 --- /dev/null +++ b/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.h @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ISavestate.h" + +#include <flatbuffers/flatbuffers.h> + +#include <memory> + +namespace flatbuffers +{ + class FlatBufferBuilder; +} + +namespace KODI +{ +namespace RETRO +{ + struct Savestate; + struct SavestateBuilder; + + class CSavestateFlatBuffer : public ISavestate + { + public: + CSavestateFlatBuffer(); + ~CSavestateFlatBuffer(); + + // Implementation of ISavestate + void Reset() override; + bool Serialize(const uint8_t *&data, size_t &size) const override; + SAVE_TYPE Type() const override; + uint8_t Slot() const override; + std::string Label() const override; + CDateTime Created() const override; + std::string GameFileName() const override; + uint64_t TimestampFrames() const override; + double TimestampWallClock() const override; + std::string GameClientID() const override; + std::string GameClientVersion() const override; + const uint8_t *GetMemoryData() const override; + size_t GetMemorySize() const override; + void SetType(SAVE_TYPE type) override; + void SetSlot(uint8_t slot) override; + void SetLabel(const std::string &label) override; + void SetCreated(const CDateTime &created) override; + void SetGameFileName(const std::string &gameFileName) override; + void SetTimestampFrames(uint64_t timestampFrames) override; + void SetTimestampWallClock(double timestampWallClock) override; + void SetGameClientID(const std::string &gameClient) override; + void SetGameClientVersion(const std::string &gameClient) override; + uint8_t *GetMemoryBuffer(size_t size) override; + void Finalize() override; + bool Deserialize(std::vector<uint8_t> data) override; + + private: + /*! + * \brief Helper class to hold data needed in creation of a FlatBuffer + * + * The builder is used when deserializing from individual fields. + */ + std::unique_ptr<flatbuffers::FlatBufferBuilder> m_builder; + + /*! + * \brief System memory storage (for deserializing savestates) + * + * This memory is used when deserializing from a vector. + */ + std::vector<uint8_t> m_data; + + /*! + * \brief FlatBuffer struct used for accessing data + */ + const Savestate *m_savestate = nullptr; + + using StringOffset = flatbuffers::Offset<flatbuffers::String>; + using VectorOffset = flatbuffers::Offset<flatbuffers::Vector<uint8_t>>; + + // Temporary deserialization variables + SAVE_TYPE m_type = SAVE_TYPE::UNKNOWN; + uint8_t m_slot = 0; + std::unique_ptr<StringOffset> m_labelOffset; + std::unique_ptr<StringOffset> m_createdOffset; + std::unique_ptr<StringOffset> m_gameFileNameOffset; + uint64_t m_timestampFrames = 0; + double m_timestampWallClock = 0.0; + std::unique_ptr<StringOffset> m_emulatorAddonIdOffset; + std::unique_ptr<StringOffset> m_emulatorVersionOffset; + std::unique_ptr<VectorOffset> m_memoryDataOffset; + }; +} +} diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateTranslator.h b/xbmc/cores/RetroPlayer/savestates/SavestateTypes.h index acf35dc402..3fa2dd6147 100644 --- a/xbmc/cores/RetroPlayer/savestates/SavestateTranslator.h +++ b/xbmc/cores/RetroPlayer/savestates/SavestateTypes.h @@ -8,19 +8,21 @@ #pragma once -#include "Savestate.h" - -#include <string> - namespace KODI { namespace RETRO { - class CSavestateTranslator + /*! + * \brief Type of save action, either: + * + * - automatic (saving was not prompted by the user) + * - manual (user manually prompted the save) + */ + enum class SAVE_TYPE { - public: - static SAVETYPE TranslateType(const std::string& type); - static std::string TranslateType(const SAVETYPE& type); + UNKNOWN, + AUTO, + MANUAL, }; } } diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateUtils.cpp b/xbmc/cores/RetroPlayer/savestates/SavestateUtils.cpp index 3fd8d8470a..39809c492d 100644 --- a/xbmc/cores/RetroPlayer/savestates/SavestateUtils.cpp +++ b/xbmc/cores/RetroPlayer/savestates/SavestateUtils.cpp @@ -7,21 +7,14 @@ */ #include "SavestateUtils.h" -#include "Savestate.h" #include "utils/URIUtils.h" #define SAVESTATE_EXTENSION ".sav" -#define METADATA_EXTENSION ".xml" using namespace KODI; using namespace RETRO; -std::string CSavestateUtils::MakePath(const CSavestate& save) +std::string CSavestateUtils::MakePath(const std::string &gamePath) { - return URIUtils::ReplaceExtension(save.GamePath(), SAVESTATE_EXTENSION); -} - -std::string CSavestateUtils::MakeMetadataPath(const std::string &gamePath) -{ - return URIUtils::ReplaceExtension(gamePath, METADATA_EXTENSION); + return URIUtils::ReplaceExtension(gamePath, SAVESTATE_EXTENSION); } diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateUtils.h b/xbmc/cores/RetroPlayer/savestates/SavestateUtils.h index a252adf478..fb6e6b8b9d 100644 --- a/xbmc/cores/RetroPlayer/savestates/SavestateUtils.h +++ b/xbmc/cores/RetroPlayer/savestates/SavestateUtils.h @@ -14,25 +14,16 @@ namespace KODI { namespace RETRO { - class CSavestate; - class CSavestateUtils { public: /*! - * \brief Calculate a path for the specified savestate - * - * The savestate path is the game path with the extension replaced by ".sav". - */ - static std::string MakePath(const CSavestate& save); - - /*! - * \brief Calculate a metadata path for the specified savestate + * \brief Calculate a savestate path for the specified game * - * The savestate metadata path is the game path with the extension replaced - * by ".xml". + * The savestate path is the game path with the extension replaced + * by ".sav". */ - static std::string MakeMetadataPath(const std::string &gamePath); + static std::string MakePath(const std::string &gamePath); }; } } diff --git a/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp b/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp index 1b2cf3bf61..2f422493de 100644 --- a/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp +++ b/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp @@ -9,9 +9,8 @@ #include "GUIDialogSelectGameClient.h" #include "addons/AddonInstaller.h" #include "addons/AddonManager.h" -#include "cores/RetroPlayer/savestates/Savestate.h" +#include "cores/RetroPlayer/savestates/ISavestate.h" #include "cores/RetroPlayer/savestates/SavestateDatabase.h" -#include "cores/RetroPlayer/savestates/SavestateUtils.h" #include "dialogs/GUIDialogSelect.h" #include "filesystem/AddonsDirectory.h" #include "games/addons/GameClient.h" @@ -37,19 +36,19 @@ std::string CGUIDialogSelectGameClient::ShowAndGetGameClient(const std::string & LogGameClients(candidates, installable); std::string extension = URIUtils::GetExtension(gamePath); - std::string xmlPath = RETRO::CSavestateUtils::MakeMetadataPath(gamePath); // Load savestate - RETRO::CSavestate save; RETRO::CSavestateDatabase db; - CLog::Log(LOGDEBUG, "Select game client dialog: Loading savestate metadata %s", CURL::GetRedacted(xmlPath).c_str()); - const bool bLoaded = db.GetSavestate(xmlPath, save); + std::unique_ptr<RETRO::ISavestate> save = db.CreateSavestate(); + + CLog::Log(LOGDEBUG, "Select game client dialog: Loading savestate metadata"); + const bool bLoaded = db.GetSavestate(gamePath, *save); // Get savestate game client std::string saveGameClient; if (bLoaded) { - saveGameClient = save.GameClient(); + saveGameClient = save->GameClientID(); CLog::Log(LOGDEBUG, "Select game client dialog: Auto-selecting %s", saveGameClient.c_str()); } diff --git a/xbmc/guilib/guiinfo/GamesGUIInfo.cpp b/xbmc/guilib/guiinfo/GamesGUIInfo.cpp index c784c1d537..982d312a47 100644 --- a/xbmc/guilib/guiinfo/GamesGUIInfo.cpp +++ b/xbmc/guilib/guiinfo/GamesGUIInfo.cpp @@ -10,7 +10,6 @@ #include "FileItem.h" #include "Util.h" -#include "cores/RetroPlayer/savestates/SavestateDefines.h" #include "cores/RetroPlayer/RetroPlayerUtils.h" #include "games/tags/GameInfoTag.h" #include "settings/MediaSettings.h" @@ -25,6 +24,8 @@ using namespace KODI::GUILIB::GUIINFO; using namespace KODI::GAME; using namespace KODI::RETRO; +#define FILEITEM_PROPERTY_SAVESTATE_DURATION "duration" + bool CGamesGUIInfo::InitCurrentItem(CFileItem *item) { if (item && item->IsGame()) |