diff --git a/CMakeLists.txt b/CMakeLists.txt index 04cb3ea7..b334f594 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -232,11 +232,18 @@ set(SYSTEM_LIBS src/core/libraries/system/commondialog.cpp src/core/libraries/system/msgdialog_ui.cpp src/core/libraries/system/posix.cpp src/core/libraries/system/posix.h - src/core/libraries/save_data/error_codes.h + src/core/libraries/save_data/save_backup.cpp + src/core/libraries/save_data/save_backup.h + src/core/libraries/save_data/save_instance.cpp + src/core/libraries/save_data/save_instance.h + src/core/libraries/save_data/save_memory.cpp + src/core/libraries/save_data/save_memory.h src/core/libraries/save_data/savedata.cpp src/core/libraries/save_data/savedata.h - src/core/libraries/system/savedatadialog.cpp - src/core/libraries/system/savedatadialog.h + src/core/libraries/save_data/dialog/savedatadialog.cpp + src/core/libraries/save_data/dialog/savedatadialog.h + src/core/libraries/save_data/dialog/savedatadialog_ui.cpp + src/core/libraries/save_data/dialog/savedatadialog_ui.h src/core/libraries/system/sysmodule.cpp src/core/libraries/system/sysmodule.h src/core/libraries/system/systemservice.cpp @@ -349,6 +356,7 @@ set(COMMON src/common/logging/backend.cpp src/common/concepts.h src/common/config.cpp src/common/config.h + src/common/cstring.h src/common/debug.h src/common/disassembler.cpp src/common/disassembler.h @@ -607,6 +615,7 @@ set(VIDEO_CORE src/video_core/amdgpu/liverpool.cpp set(IMGUI src/imgui/imgui_config.h src/imgui/imgui_layer.h src/imgui/imgui_std.h + src/imgui/imgui_texture.h src/imgui/layer/video_info.cpp src/imgui/layer/video_info.h src/imgui/renderer/imgui_core.cpp @@ -615,6 +624,8 @@ set(IMGUI src/imgui/imgui_config.h src/imgui/renderer/imgui_impl_sdl3.h src/imgui/renderer/imgui_impl_vulkan.cpp src/imgui/renderer/imgui_impl_vulkan.h + src/imgui/renderer/texture_manager.cpp + src/imgui/renderer/texture_manager.h ) set(INPUT src/input/controller.cpp diff --git a/scripts/file_formats/sfo.hexpat b/scripts/file_formats/sfo.hexpat new file mode 100644 index 00000000..cfc1f878 --- /dev/null +++ b/scripts/file_formats/sfo.hexpat @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +import std.io; +import std.sys; + +struct Header { + u32 magic; + u32 version; + u32 key_table_offset; + u32 data_table_offset; + u32 index_table_entries; +}; + +struct KeyEntry { + char name[]; +} [[inline]]; + +struct DataEntry { + if (fmt == 0x0404) { + u32 int_value; + } else if(fmt == 0x0004) { + char bin_value[size]; + } else if(fmt == 0x0204) { + char str_value[size]; + } else { + std::warning("unknown fmt type"); + } +} [[inline]]; + +struct IndexEntry { + u16 key_offset; + u16 param_fmt; + u32 param_len; + u32 param_max_len; + u32 data_offset; +}; + +struct Entry { + u64 begin = $; + IndexEntry index; + KeyEntry key @ KeyTableOffset + index.key_offset; + DataEntry data @ DataTableOffset + index.data_offset; + u8 data_empty[index.param_max_len - index.param_len] @ DataTableOffset + index.data_offset + index.param_len; + $ = begin + sizeof(IndexEntry); +}; + +Header header @ 0; +std::assert(header.magic == 0x46535000, "Miss match magic"); +std::assert(header.version == 0x00000101, "Miss match version"); + +Entry list[header.index_table_entries] @ 0x14; \ No newline at end of file diff --git a/src/common/cstring.h b/src/common/cstring.h new file mode 100644 index 00000000..1b47bdbf --- /dev/null +++ b/src/common/cstring.h @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "assert.h" + +namespace Common { + +/** + * @brief A null-terminated string with a fixed maximum length + * This class is not meant to be used as a general-purpose string class + * It is meant to be used as `char[N]` where memory layout is fixed + * @tparam N Maximum length of the string + * @tparam T Type of character + */ +template +class CString { + T data[N]{}; + +public: + class Iterator; + + CString() = default; + + template + explicit CString(const CString& other) + requires(M <= N) + { + std::ranges::copy(other.begin(), other.end(), data); + } + + void FromString(const std::basic_string_view& str) { + size_t p = str.copy(data, N - 1); + data[p] = '\0'; + } + + void Zero() { + std::ranges::fill(data, 0); + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wtautological-undefined-compare" + explicit(false) operator std::basic_string_view() const { + if (this == nullptr) { + return {}; + } + return std::basic_string_view{data}; + } + + explicit operator std::basic_string() const { + if (this == nullptr) { + return {}; + } + return std::basic_string{data}; + } + + std::basic_string to_string() const { + if (this == nullptr) { + return {}; + } + return std::basic_string{data}; + } + + std::basic_string_view to_view() const { + if (this == nullptr) { + return {}; + } + return std::basic_string_view{data}; + } +#pragma clang diagnostic pop + + char* begin() { + return data; + } + + const char* begin() const { + return data; + } + + char* end() { + return data + N; + } + + const char* end() const { + return data + N; + } + + T& operator[](size_t idx) { + return data[idx]; + } + + const T& operator[](size_t idx) const { + return data[idx]; + } + + class Iterator { + T* ptr; + T* end; + + public: + using difference_type = std::ptrdiff_t; + using value_type = T; + using pointer = T*; + using reference = T&; + using iterator_category = std::random_access_iterator_tag; + + Iterator() = default; + explicit Iterator(T* ptr) : ptr(ptr), end(ptr + N) {} + + Iterator& operator++() { + ++ptr; + return *this; + } + + Iterator operator++(int) { + Iterator tmp = *this; + ++ptr; + return tmp; + } + + operator T*() { + ASSERT_MSG(ptr >= end, "CString iterator out of bounds"); + return ptr; + } + }; +}; +static_assert(sizeof(CString<13>) == sizeof(char[13])); // Ensure size still matches a simple array +static_assert(std::weakly_incrementable::Iterator>); + +} // namespace Common \ No newline at end of file diff --git a/src/common/io_file.cpp b/src/common/io_file.cpp index fbc37a10..c1d9cc59 100644 --- a/src/common/io_file.cpp +++ b/src/common/io_file.cpp @@ -396,4 +396,18 @@ s64 IOFile::Tell() const { return ftello(file); } +u64 GetDirectorySize(const std::filesystem::path& path) { + if (!fs::exists(path)) { + return 0; + } + + u64 total = 0; + for (const auto& entry : fs::recursive_directory_iterator(path)) { + if (fs::is_regular_file(entry.path())) { + total += fs::file_size(entry.path()); + } + } + return total; +} + } // namespace Common::FS diff --git a/src/common/io_file.h b/src/common/io_file.h index 2c3df3f6..177bddba 100644 --- a/src/common/io_file.h +++ b/src/common/io_file.h @@ -219,4 +219,6 @@ private: uintptr_t file_mapping = 0; }; +u64 GetDirectorySize(const std::filesystem::path& path); + } // namespace Common::FS diff --git a/src/common/string_util.cpp b/src/common/string_util.cpp index 29e6aeb4..6d5a254c 100644 --- a/src/common/string_util.cpp +++ b/src/common/string_util.cpp @@ -14,12 +14,17 @@ namespace Common { -std::string ToLower(std::string str) { - std::transform(str.begin(), str.end(), str.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); +std::string ToLower(std::string_view input) { + std::string str; + str.resize(input.size()); + std::ranges::transform(input, str.begin(), tolower); return str; } +void ToLowerInPlace(std::string& str) { + std::ranges::transform(str, str.begin(), tolower); +} + std::vector SplitString(const std::string& str, char delimiter) { std::istringstream iss(str); std::vector output(1); diff --git a/src/common/string_util.h b/src/common/string_util.h index 8dae6c75..23e82b93 100644 --- a/src/common/string_util.h +++ b/src/common/string_util.h @@ -10,7 +10,9 @@ namespace Common { /// Make a string lowercase -[[nodiscard]] std::string ToLower(std::string str); +[[nodiscard]] std::string ToLower(std::string_view str); + +void ToLowerInPlace(std::string& str); std::vector SplitString(const std::string& str, char delimiter); diff --git a/src/core/file_format/psf.cpp b/src/core/file_format/psf.cpp index 3d076acd..1df5d430 100644 --- a/src/core/file_format/psf.cpp +++ b/src/core/file_format/psf.cpp @@ -2,61 +2,275 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include + +#include "common/assert.h" #include "common/io_file.h" +#include "common/logging/log.h" #include "core/file_format/psf.h" -PSF::PSF() = default; +static const std::unordered_map psf_known_max_sizes = { + {"ACCOUNT_ID", 8}, {"CATEGORY", 4}, {"DETAIL", 1024}, {"FORMAT", 4}, + {"MAINTITLE", 128}, {"PARAMS", 1024}, {"SAVEDATA_BLOCKS", 8}, {"SAVEDATA_DIRECTORY", 32}, + {"SUBTITLE", 128}, {"TITLE_ID", 12}, +}; +static inline u32 get_max_size(std::string_view key, u32 default_value) { + if (const auto& v = psf_known_max_sizes.find(key); v != psf_known_max_sizes.end()) { + return v->second; + } + return default_value; +} -PSF::~PSF() = default; - -bool PSF::open(const std::string& filepath, const std::vector& psfBuffer) { - if (!psfBuffer.empty()) { - psf.resize(psfBuffer.size()); - psf = psfBuffer; - } else { - Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Read); - if (!file.IsOpen()) { - return false; - } - - const u64 psfSize = file.GetSize(); - psf.resize(psfSize); - file.Seek(0); - file.Read(psf); - file.Close(); +bool PSF::Open(const std::filesystem::path& filepath) { + if (std::filesystem::exists(filepath)) { + last_write = std::filesystem::last_write_time(filepath); } - // Parse file contents - PSFHeader header; - std::memcpy(&header, psf.data(), sizeof(header)); - for (u32 i = 0; i < header.index_table_entries; i++) { - PSFEntry entry; - std::memcpy(&entry, &psf[sizeof(PSFHeader) + i * sizeof(PSFEntry)], sizeof(entry)); + Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Read); + if (!file.IsOpen()) { + return false; + } - const std::string key = (char*)&psf[header.key_table_offset + entry.key_offset]; - if (entry.param_fmt == PSFEntry::Fmt::TextRaw || - entry.param_fmt == PSFEntry::Fmt::TextNormal) { - map_strings[key] = (char*)&psf[header.data_table_offset + entry.data_offset]; - } - if (entry.param_fmt == PSFEntry::Fmt::Integer) { - u32 value; - std::memcpy(&value, &psf[header.data_table_offset + entry.data_offset], sizeof(value)); - map_integers[key] = value; + const u64 psfSize = file.GetSize(); + std::vector psf(psfSize); + file.Seek(0); + file.Read(psf); + file.Close(); + return Open(psf); +} + +bool PSF::Open(const std::vector& psf_buffer) { + const u8* psf_data = psf_buffer.data(); + + entry_list.clear(); + map_binaries.clear(); + map_strings.clear(); + map_integers.clear(); + + // Parse file contents + PSFHeader header{}; + std::memcpy(&header, psf_data, sizeof(header)); + + if (header.magic != PSF_MAGIC) { + LOG_ERROR(Core, "Invalid PSF magic number"); + return false; + } + if (header.version != PSF_VERSION_1_1 && header.version != PSF_VERSION_1_0) { + LOG_ERROR(Core, "Unsupported PSF version: 0x{:08x}", header.version); + return false; + } + + for (u32 i = 0; i < header.index_table_entries; i++) { + PSFRawEntry raw_entry{}; + std::memcpy(&raw_entry, psf_data + sizeof(PSFHeader) + i * sizeof(PSFRawEntry), + sizeof(raw_entry)); + + Entry& entry = entry_list.emplace_back(); + entry.key = std::string{(char*)(psf_data + header.key_table_offset + raw_entry.key_offset)}; + entry.param_fmt = static_cast(raw_entry.param_fmt.Raw()); + entry.max_len = raw_entry.param_max_len; + + const u8* data = psf_data + header.data_table_offset + raw_entry.data_offset; + + switch (entry.param_fmt) { + case PSFEntryFmt::Binary: { + std::vector value(raw_entry.param_len); + std::memcpy(value.data(), data, raw_entry.param_len); + map_binaries.emplace(i, std::move(value)); + } break; + case PSFEntryFmt::Text: { + std::string c_str{reinterpret_cast(data)}; + map_strings.emplace(i, std::move(c_str)); + } break; + case PSFEntryFmt::Integer: { + ASSERT_MSG(raw_entry.param_len == sizeof(s32), "PSF integer entry size mismatch"); + s32 integer = *(s32*)data; + map_integers.emplace(i, integer); + } break; + default: + UNREACHABLE_MSG("Unknown PSF entry format"); } } return true; } -std::string PSF::GetString(const std::string& key) { - if (map_strings.find(key) != map_strings.end()) { - return map_strings.at(key); +bool PSF::Encode(const std::filesystem::path& filepath) const { + Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Write); + if (!file.IsOpen()) { + return false; } - return ""; + + last_write = std::filesystem::file_time_type::clock::now(); + + const auto psf_buffer = Encode(); + return file.Write(psf_buffer) == psf_buffer.size(); } -u32 PSF::GetInteger(const std::string& key) { - if (map_integers.find(key) != map_integers.end()) { - return map_integers.at(key); - } - return -1; +std::vector PSF::Encode() const { + std::vector psf_buffer; + Encode(psf_buffer); + return psf_buffer; +} + +void PSF::Encode(std::vector& psf_buffer) const { + psf_buffer.resize(sizeof(PSFHeader) + sizeof(PSFRawEntry) * entry_list.size()); + + { + auto& header = *(PSFHeader*)psf_buffer.data(); + header.magic = PSF_MAGIC; + header.version = PSF_VERSION_1_1; + header.index_table_entries = entry_list.size(); + } + + const size_t key_table_offset = psf_buffer.size(); + ((PSFHeader*)psf_buffer.data())->key_table_offset = key_table_offset; + for (size_t i = 0; i < entry_list.size(); i++) { + auto& raw_entry = ((PSFRawEntry*)(psf_buffer.data() + sizeof(PSFHeader)))[i]; + const Entry& entry = entry_list[i]; + raw_entry.key_offset = psf_buffer.size() - key_table_offset; + raw_entry.param_fmt.FromRaw(static_cast(entry.param_fmt)); + raw_entry.param_max_len = entry.max_len; + std::ranges::copy(entry.key, std::back_inserter(psf_buffer)); + psf_buffer.push_back(0); // NULL terminator + } + + const size_t data_table_offset = psf_buffer.size(); + ((PSFHeader*)psf_buffer.data())->data_table_offset = data_table_offset; + for (size_t i = 0; i < entry_list.size(); i++) { + if (psf_buffer.size() % 4 != 0) { + std::ranges::fill_n(std::back_inserter(psf_buffer), 4 - psf_buffer.size() % 4, 0); + } + auto& raw_entry = ((PSFRawEntry*)(psf_buffer.data() + sizeof(PSFHeader)))[i]; + const Entry& entry = entry_list[i]; + raw_entry.data_offset = psf_buffer.size() - data_table_offset; + + s32 additional_padding = s32(raw_entry.param_max_len); + + switch (entry.param_fmt) { + case PSFEntryFmt::Binary: { + const auto& value = map_binaries.at(i); + raw_entry.param_len = value.size(); + additional_padding -= s32(raw_entry.param_len); + std::ranges::copy(value, std::back_inserter(psf_buffer)); + } break; + case PSFEntryFmt::Text: { + const auto& value = map_strings.at(i); + raw_entry.param_len = value.size() + 1; + additional_padding -= s32(raw_entry.param_len); + std::ranges::copy(value, std::back_inserter(psf_buffer)); + psf_buffer.push_back(0); // NULL terminator + } break; + case PSFEntryFmt::Integer: { + const auto& value = map_integers.at(i); + raw_entry.param_len = sizeof(s32); + additional_padding -= s32(raw_entry.param_len); + const auto value_bytes = reinterpret_cast(&value); + std::ranges::copy(value_bytes, value_bytes + sizeof(s32), + std::back_inserter(psf_buffer)); + } break; + default: + UNREACHABLE_MSG("Unknown PSF entry format"); + } + ASSERT_MSG(additional_padding >= 0, "PSF entry max size mismatch"); + std::ranges::fill_n(std::back_inserter(psf_buffer), additional_padding, 0); + } +} + +std::optional> PSF::GetBinary(std::string_view key) const { + const auto& [it, index] = FindEntry(key); + if (it == entry_list.end()) { + return {}; + } + ASSERT(it->param_fmt == PSFEntryFmt::Binary); + return std::span{map_binaries.at(index)}; +} + +std::optional PSF::GetString(std::string_view key) const { + const auto& [it, index] = FindEntry(key); + if (it == entry_list.end()) { + return {}; + } + ASSERT(it->param_fmt == PSFEntryFmt::Text); + return std::string_view{map_strings.at(index)}; +} + +std::optional PSF::GetInteger(std::string_view key) const { + const auto& [it, index] = FindEntry(key); + if (it == entry_list.end()) { + return {}; + } + ASSERT(it->param_fmt == PSFEntryFmt::Integer); + return map_integers.at(index); +} + +void PSF::AddBinary(std::string key, std::vector value, bool update) { + auto [it, index] = FindEntry(key); + bool exist = it != entry_list.end(); + if (exist && !update) { + LOG_ERROR(Core, "PSF: Tried to add binary key that already exists: {}", key); + return; + } + if (exist) { + ASSERT_MSG(it->param_fmt == PSFEntryFmt::Binary, "PSF: Change format is not supported"); + it->max_len = get_max_size(key, value.size()); + map_binaries.at(index) = std::move(value); + return; + } + Entry& entry = entry_list.emplace_back(); + entry.max_len = get_max_size(key, value.size()); + entry.key = std::move(key); + entry.param_fmt = PSFEntryFmt::Binary; + map_binaries.emplace(entry_list.size() - 1, std::move(value)); +} + +void PSF::AddString(std::string key, std::string value, bool update) { + auto [it, index] = FindEntry(key); + bool exist = it != entry_list.end(); + if (exist && !update) { + LOG_ERROR(Core, "PSF: Tried to add string key that already exists: {}", key); + return; + } + if (exist) { + ASSERT_MSG(it->param_fmt == PSFEntryFmt::Text, "PSF: Change format is not supported"); + it->max_len = get_max_size(key, value.size() + 1); + map_strings.at(index) = std::move(value); + return; + } + Entry& entry = entry_list.emplace_back(); + entry.max_len = get_max_size(key, value.size() + 1); + entry.key = std::move(key); + entry.param_fmt = PSFEntryFmt::Text; + map_strings.emplace(entry_list.size() - 1, std::move(value)); +} + +void PSF::AddInteger(std::string key, s32 value, bool update) { + auto [it, index] = FindEntry(key); + bool exist = it != entry_list.end(); + if (exist && !update) { + LOG_ERROR(Core, "PSF: Tried to add integer key that already exists: {}", key); + return; + } + if (exist) { + ASSERT_MSG(it->param_fmt == PSFEntryFmt::Integer, "PSF: Change format is not supported"); + it->max_len = sizeof(s32); + map_integers.at(index) = value; + return; + } + Entry& entry = entry_list.emplace_back(); + entry.key = std::move(key); + entry.param_fmt = PSFEntryFmt::Integer; + entry.max_len = sizeof(s32); + map_integers.emplace(entry_list.size() - 1, value); +} + +std::pair::iterator, size_t> PSF::FindEntry(std::string_view key) { + auto entry = + std::ranges::find_if(entry_list, [&](const auto& entry) { return entry.key == key; }); + return {entry, std::distance(entry_list.begin(), entry)}; +} + +std::pair::const_iterator, size_t> PSF::FindEntry( + std::string_view key) const { + auto entry = + std::ranges::find_if(entry_list, [&](const auto& entry) { return entry.key == key; }); + return {entry, std::distance(entry_list.begin(), entry)}; } diff --git a/src/core/file_format/psf.h b/src/core/file_format/psf.h index 9a162b1d..d25b79ee 100644 --- a/src/core/file_format/psf.h +++ b/src/core/file_format/psf.h @@ -3,11 +3,18 @@ #pragma once +#include +#include #include +#include #include #include #include "common/endian.h" +constexpr u32 PSF_MAGIC = 0x00505346; +constexpr u32 PSF_VERSION_1_1 = 0x00000101; +constexpr u32 PSF_VERSION_1_0 = 0x00000100; + struct PSFHeader { u32_be magic; u32_le version; @@ -15,34 +22,72 @@ struct PSFHeader { u32_le data_table_offset; u32_le index_table_entries; }; +static_assert(sizeof(PSFHeader) == 0x14); -struct PSFEntry { - enum Fmt : u16 { - TextRaw = 0x0400, // String in UTF-8 format and not NULL terminated - TextNormal = 0x0402, // String in UTF-8 format and NULL terminated - Integer = 0x0404, // Unsigned 32-bit integer - }; - +struct PSFRawEntry { u16_le key_offset; u16_be param_fmt; u32_le param_len; u32_le param_max_len; u32_le data_offset; }; +static_assert(sizeof(PSFRawEntry) == 0x10); + +enum class PSFEntryFmt : u16 { + Binary = 0x0004, // Binary data + Text = 0x0204, // String in UTF-8 format and NULL terminated + Integer = 0x0404, // Signed 32-bit integer +}; class PSF { + struct Entry { + std::string key; + PSFEntryFmt param_fmt; + u32 max_len; + }; + public: - PSF(); - ~PSF(); + PSF() = default; + ~PSF() = default; - bool open(const std::string& filepath, const std::vector& psfBuffer); + PSF(const PSF& other) = default; + PSF(PSF&& other) noexcept = default; + PSF& operator=(const PSF& other) = default; + PSF& operator=(PSF&& other) noexcept = default; - std::string GetString(const std::string& key); - u32 GetInteger(const std::string& key); + bool Open(const std::filesystem::path& filepath); + bool Open(const std::vector& psf_buffer); - std::unordered_map map_strings; - std::unordered_map map_integers; + [[nodiscard]] std::vector Encode() const; + void Encode(std::vector& buf) const; + bool Encode(const std::filesystem::path& filepath) const; + + std::optional> GetBinary(std::string_view key) const; + std::optional GetString(std::string_view key) const; + std::optional GetInteger(std::string_view key) const; + + void AddBinary(std::string key, std::vector value, bool update = false); + void AddString(std::string key, std::string value, bool update = false); + void AddInteger(std::string key, s32 value, bool update = false); + + [[nodiscard]] std::filesystem::file_time_type GetLastWrite() const { + return last_write; + } + + [[nodiscard]] const std::vector& GetEntries() const { + return entry_list; + } private: - std::vector psf; + mutable std::filesystem::file_time_type last_write; + + std::vector entry_list; + + std::unordered_map> map_binaries; + std::unordered_map map_strings; + std::unordered_map map_integers; + + [[nodiscard]] std::pair::iterator, size_t> FindEntry(std::string_view key); + [[nodiscard]] std::pair::const_iterator, size_t> FindEntry( + std::string_view key) const; }; diff --git a/src/core/file_sys/fs.cpp b/src/core/file_sys/fs.cpp index da631d63..3b060dd8 100644 --- a/src/core/file_sys/fs.cpp +++ b/src/core/file_sys/fs.cpp @@ -9,9 +9,10 @@ namespace Core::FileSys { constexpr int RESERVED_HANDLES = 3; // First 3 handles are stdin,stdout,stderr -void MntPoints::Mount(const std::filesystem::path& host_folder, const std::string& guest_folder) { +void MntPoints::Mount(const std::filesystem::path& host_folder, const std::string& guest_folder, + bool read_only) { std::scoped_lock lock{m_mutex}; - m_mnt_pairs.emplace_back(host_folder, guest_folder); + m_mnt_pairs.emplace_back(host_folder, guest_folder, read_only); } void MntPoints::Unmount(const std::filesystem::path& host_folder, const std::string& guest_folder) { @@ -26,7 +27,7 @@ void MntPoints::UnmountAll() { m_mnt_pairs.clear(); } -std::filesystem::path MntPoints::GetHostPath(std::string_view guest_directory) { +std::filesystem::path MntPoints::GetHostPath(std::string_view guest_directory, bool* is_read_only) { // Evil games like Turok2 pass double slashes e.g /app0//game.kpf std::string corrected_path(guest_directory); size_t pos = corrected_path.find("//"); @@ -40,6 +41,10 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view guest_directory) { return ""; } + if (is_read_only) { + *is_read_only = mount->read_only; + } + // Nothing to do if getting the mount itself. if (corrected_path == mount->mount) { return mount->host_path; diff --git a/src/core/file_sys/fs.h b/src/core/file_sys/fs.h index ddd02f53..eeaeaf78 100644 --- a/src/core/file_sys/fs.h +++ b/src/core/file_sys/fs.h @@ -22,16 +22,19 @@ public: struct MntPair { std::filesystem::path host_path; std::string mount; // e.g /app0/ + bool read_only; }; explicit MntPoints() = default; ~MntPoints() = default; - void Mount(const std::filesystem::path& host_folder, const std::string& guest_folder); + void Mount(const std::filesystem::path& host_folder, const std::string& guest_folder, + bool read_only = false); void Unmount(const std::filesystem::path& host_folder, const std::string& guest_folder); void UnmountAll(); - std::filesystem::path GetHostPath(std::string_view guest_directory); + std::filesystem::path GetHostPath(std::string_view guest_directory, + bool* is_read_only = nullptr); const MntPair* GetMount(const std::string& guest_path) { std::scoped_lock lock{m_mutex}; diff --git a/src/core/libraries/app_content/app_content.cpp b/src/core/libraries/app_content/app_content.cpp index 125d1968..81ce044f 100644 --- a/src/core/libraries/app_content/app_content.cpp +++ b/src/core/libraries/app_content/app_content.cpp @@ -90,37 +90,32 @@ int PS4_SYSV_ABI sceAppContentAddcontUnmount() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAppContentAppParamGetInt(OrbisAppContentAppParamId paramId, s32* value) { - if (value == nullptr) +int PS4_SYSV_ABI sceAppContentAppParamGetInt(OrbisAppContentAppParamId paramId, s32* out_value) { + if (out_value == nullptr) return ORBIS_APP_CONTENT_ERROR_PARAMETER; auto* param_sfo = Common::Singleton::Instance(); + std::optional value; switch (paramId) { case ORBIS_APP_CONTENT_APPPARAM_ID_SKU_FLAG: - *value = ORBIS_APP_CONTENT_APPPARAM_SKU_FLAG_FULL; + value = ORBIS_APP_CONTENT_APPPARAM_SKU_FLAG_FULL; break; case ORBIS_APP_CONTENT_APPPARAM_ID_USER_DEFINED_PARAM_1: - *value = param_sfo->GetInteger("USER_DEFINED_PARAM_1"); + value = param_sfo->GetInteger("USER_DEFINED_PARAM_1"); break; case ORBIS_APP_CONTENT_APPPARAM_ID_USER_DEFINED_PARAM_2: - *value = param_sfo->GetInteger("USER_DEFINED_PARAM_2"); + value = param_sfo->GetInteger("USER_DEFINED_PARAM_2"); break; case ORBIS_APP_CONTENT_APPPARAM_ID_USER_DEFINED_PARAM_3: - *value = param_sfo->GetInteger("USER_DEFINED_PARAM_3"); + value = param_sfo->GetInteger("USER_DEFINED_PARAM_3"); break; case ORBIS_APP_CONTENT_APPPARAM_ID_USER_DEFINED_PARAM_4: - *value = param_sfo->GetInteger("USER_DEFINED_PARAM_4"); + value = param_sfo->GetInteger("USER_DEFINED_PARAM_4"); break; default: - LOG_ERROR(Lib_AppContent, " paramId = {}, value = {} paramId is not valid", paramId, - *value); - return ORBIS_APP_CONTENT_ERROR_PARAMETER; - } - if (*value == -1) { - LOG_ERROR(Lib_AppContent, - " paramId = {}, value = {} value is not valid can't read param.sfo?", paramId, - *value); + LOG_ERROR(Lib_AppContent, " paramId = {} paramId is not valid", paramId); return ORBIS_APP_CONTENT_ERROR_PARAMETER; } + *out_value = value.value_or(0); return ORBIS_OK; } @@ -251,7 +246,7 @@ int PS4_SYSV_ABI sceAppContentInitialize(const OrbisAppContentInitParam* initPar auto* param_sfo = Common::Singleton::Instance(); const auto addons_dir = Common::FS::GetUserPath(Common::FS::PathType::AddonsDir); - title_id = param_sfo->GetString("TITLE_ID"); + title_id = *param_sfo->GetString("TITLE_ID"); auto addon_path = addons_dir / title_id; if (std::filesystem::exists(addon_path)) { for (const auto& entry : std::filesystem::directory_iterator(addon_path)) { diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index ae2c6e2b..cb8e0aac 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -179,11 +179,16 @@ int PS4_SYSV_ABI sceKernelUnlink(const char* path) { auto* h = Common::Singleton::Instance(); auto* mnt = Common::Singleton::Instance(); - const auto host_path = mnt->GetHostPath(path); + bool ro = false; + const auto host_path = mnt->GetHostPath(path, &ro); if (host_path.empty()) { return SCE_KERNEL_ERROR_EACCES; } + if (ro) { + return SCE_KERNEL_ERROR_EROFS; + } + if (std::filesystem::is_directory(host_path)) { return SCE_KERNEL_ERROR_EPERM; } @@ -270,11 +275,18 @@ int PS4_SYSV_ABI sceKernelMkdir(const char* path, u16 mode) { return SCE_KERNEL_ERROR_EINVAL; } auto* mnt = Common::Singleton::Instance(); - const auto dir_name = mnt->GetHostPath(path); + + bool ro = false; + const auto dir_name = mnt->GetHostPath(path, &ro); + if (std::filesystem::exists(dir_name)) { return SCE_KERNEL_ERROR_EEXIST; } + if (ro) { + return SCE_KERNEL_ERROR_EROFS; + } + // CUSA02456: path = /aotl after sceSaveDataMount(mode = 1) if (dir_name.empty() || !std::filesystem::create_directory(dir_name)) { return SCE_KERNEL_ERROR_EIO; @@ -299,7 +311,8 @@ int PS4_SYSV_ABI posix_mkdir(const char* path, u16 mode) { int PS4_SYSV_ABI sceKernelStat(const char* path, OrbisKernelStat* sb) { LOG_INFO(Kernel_Fs, "(PARTIAL) path = {}", path); auto* mnt = Common::Singleton::Instance(); - const auto path_name = mnt->GetHostPath(path); + bool ro = false; + const auto path_name = mnt->GetHostPath(path, &ro); std::memset(sb, 0, sizeof(OrbisKernelStat)); const bool is_dir = std::filesystem::is_directory(path_name); const bool is_file = std::filesystem::is_regular_file(path_name); @@ -319,6 +332,10 @@ int PS4_SYSV_ABI sceKernelStat(const char* path, OrbisKernelStat* sb) { sb->st_blocks = (sb->st_size + 511) / 512; // TODO incomplete } + if (ro) { + sb->st_mode &= ~0000555u; + } + return ORBIS_OK; } @@ -500,11 +517,18 @@ s64 PS4_SYSV_ABI sceKernelPwrite(int d, void* buf, size_t nbytes, s64 offset) { s32 PS4_SYSV_ABI sceKernelRename(const char* from, const char* to) { auto* mnt = Common::Singleton::Instance(); - const auto src_path = mnt->GetHostPath(from); + bool ro = false; + const auto src_path = mnt->GetHostPath(from, &ro); if (!std::filesystem::exists(src_path)) { return ORBIS_KERNEL_ERROR_ENOENT; } - const auto dst_path = mnt->GetHostPath(to); + if (ro) { + return SCE_KERNEL_ERROR_EROFS; + } + const auto dst_path = mnt->GetHostPath(to, &ro); + if (ro) { + return SCE_KERNEL_ERROR_EROFS; + } const bool src_is_dir = std::filesystem::is_directory(src_path); const bool dst_is_dir = std::filesystem::is_directory(dst_path); if (src_is_dir && !dst_is_dir) { diff --git a/src/core/libraries/kernel/libkernel.cpp b/src/core/libraries/kernel/libkernel.cpp index d56f4dc4..3cbf21f0 100644 --- a/src/core/libraries/kernel/libkernel.cpp +++ b/src/core/libraries/kernel/libkernel.cpp @@ -244,7 +244,7 @@ int PS4_SYSV_ABI sceKernelConvertUtcToLocaltime(time_t time, time_t* local_time, int PS4_SYSV_ABI sceKernelGetCompiledSdkVersion(int* ver) { auto* param_sfo = Common::Singleton::Instance(); - int version = param_sfo->GetInteger("SYSTEM_VER"); + int version = param_sfo->GetInteger("SYSTEM_VER").value_or(0x4700000); LOG_INFO(Kernel, "returned system version = {:#x}", version); *ver = version; return (version > 0) ? ORBIS_OK : ORBIS_KERNEL_ERROR_EINVAL; diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index da41eaf0..5b6c17b1 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -27,12 +27,12 @@ #include "core/libraries/playgo/playgo.h" #include "core/libraries/random/random.h" #include "core/libraries/rtc/rtc.h" +#include "core/libraries/save_data/dialog/savedatadialog.h" #include "core/libraries/save_data/savedata.h" #include "core/libraries/screenshot/screenshot.h" #include "core/libraries/system/commondialog.h" #include "core/libraries/system/msgdialog.h" #include "core/libraries/system/posix.h" -#include "core/libraries/system/savedatadialog.h" #include "core/libraries/system/sysmodule.h" #include "core/libraries/system/systemservice.h" #include "core/libraries/system/userservice.h" @@ -57,11 +57,11 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::Net::RegisterlibSceNet(sym); Libraries::NetCtl::RegisterlibSceNetCtl(sym); Libraries::SaveData::RegisterlibSceSaveData(sym); + Libraries::SaveData::Dialog::RegisterlibSceSaveDataDialog(sym); Libraries::Ssl::RegisterlibSceSsl(sym); Libraries::SysModule::RegisterlibSceSysmodule(sym); Libraries::Posix::Registerlibsceposix(sym); Libraries::AudioIn::RegisterlibSceAudioIn(sym); - Libraries::SaveDataDialog::RegisterlibSceSaveDataDialog(sym); Libraries::NpManager::RegisterlibSceNpManager(sym); Libraries::NpScore::RegisterlibSceNpScore(sym); Libraries::NpTrophy::RegisterlibSceNpTrophy(sym); diff --git a/src/core/libraries/np_trophy/trophy_ui.h b/src/core/libraries/np_trophy/trophy_ui.h index d730aca5..060d80de 100644 --- a/src/core/libraries/np_trophy/trophy_ui.h +++ b/src/core/libraries/np_trophy/trophy_ui.h @@ -31,10 +31,6 @@ public: void Finish(); void Draw() override; - - bool ShouldGrabGamepad() override { - return false; - } }; }; // namespace Libraries::NpTrophy \ No newline at end of file diff --git a/src/core/libraries/save_data/dialog/savedatadialog.cpp b/src/core/libraries/save_data/dialog/savedatadialog.cpp new file mode 100644 index 00000000..a647d80f --- /dev/null +++ b/src/core/libraries/save_data/dialog/savedatadialog.cpp @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/logging/log.h" +#include "core/libraries/libs.h" +#include "core/libraries/system/commondialog.h" +#include "magic_enum.hpp" +#include "savedatadialog.h" +#include "savedatadialog_ui.h" + +namespace Libraries::SaveData::Dialog { + +using CommonDialog::Error; +using CommonDialog::Result; +using CommonDialog::Status; + +static auto g_status = Status::NONE; +static SaveDialogState g_state{}; +static SaveDialogResult g_result{}; +static SaveDialogUi g_save_dialog_ui; + +Error PS4_SYSV_ABI sceSaveDataDialogClose() { + LOG_DEBUG(Lib_SaveDataDialog, "called"); + if (g_status != Status::RUNNING) { + return Error::NOT_RUNNING; + } + g_save_dialog_ui.Finish(ButtonId::INVALID); + g_save_dialog_ui = SaveDialogUi{}; + return Error::OK; +} + +Error PS4_SYSV_ABI sceSaveDataDialogGetResult(OrbisSaveDataDialogResult* result) { + LOG_DEBUG(Lib_SaveDataDialog, "called"); + if (g_status != Status::FINISHED) { + return Error::NOT_FINISHED; + } + if (result == nullptr) { + return Error::ARG_NULL; + } + g_result.CopyTo(*result); + return Error::OK; +} + +Status PS4_SYSV_ABI sceSaveDataDialogGetStatus() { + LOG_TRACE(Lib_SaveDataDialog, "called status={}", magic_enum::enum_name(g_status)); + return g_status; +} + +Error PS4_SYSV_ABI sceSaveDataDialogInitialize() { + LOG_DEBUG(Lib_SaveDataDialog, "called"); + if (!CommonDialog::g_isInitialized) { + return Error::NOT_SYSTEM_INITIALIZED; + } + if (g_status != Status::NONE) { + return Error::ALREADY_INITIALIZED; + } + if (CommonDialog::g_isUsed) { + return Error::BUSY; + } + g_status = Status::INITIALIZED; + CommonDialog::g_isUsed = true; + + return Error::OK; +} + +s32 PS4_SYSV_ABI sceSaveDataDialogIsReadyToDisplay() { + return 1; +} + +Error PS4_SYSV_ABI sceSaveDataDialogOpen(const OrbisSaveDataDialogParam* param) { + if (g_status != Status::INITIALIZED && g_status != Status::FINISHED) { + LOG_INFO(Lib_SaveDataDialog, "called without initialize"); + return Error::INVALID_STATE; + } + if (param == nullptr) { + LOG_DEBUG(Lib_SaveDataDialog, "called param:(NULL)"); + return Error::ARG_NULL; + } + LOG_DEBUG(Lib_SaveDataDialog, "called param->mode: {}", magic_enum::enum_name(param->mode)); + ASSERT(param->size == sizeof(OrbisSaveDataDialogParam)); + ASSERT(param->baseParam.size == sizeof(CommonDialog::BaseParam)); + g_result = {}; + g_state = SaveDialogState{*param}; + g_status = Status::RUNNING; + g_save_dialog_ui = SaveDialogUi(&g_state, &g_status, &g_result); + return Error::OK; +} + +Error PS4_SYSV_ABI sceSaveDataDialogProgressBarInc(OrbisSaveDataDialogProgressBarTarget target, + u32 delta) { + LOG_DEBUG(Lib_SaveDataDialog, "called"); + if (g_status != Status::RUNNING) { + return Error::NOT_RUNNING; + } + if (g_state.GetMode() != SaveDataDialogMode::PROGRESS_BAR) { + return Error::NOT_SUPPORTED; + } + if (target != OrbisSaveDataDialogProgressBarTarget::DEFAULT) { + return Error::PARAM_INVALID; + } + g_state.GetState().progress += delta; + return Error::OK; +} + +Error PS4_SYSV_ABI sceSaveDataDialogProgressBarSetValue(OrbisSaveDataDialogProgressBarTarget target, + u32 rate) { + LOG_DEBUG(Lib_SaveDataDialog, "called"); + if (g_status != Status::RUNNING) { + return Error::NOT_RUNNING; + } + if (g_state.GetMode() != SaveDataDialogMode::PROGRESS_BAR) { + return Error::NOT_SUPPORTED; + } + if (target != OrbisSaveDataDialogProgressBarTarget::DEFAULT) { + return Error::PARAM_INVALID; + } + g_state.GetState().progress = rate; + return Error::OK; +} + +Error PS4_SYSV_ABI sceSaveDataDialogTerminate() { + LOG_DEBUG(Lib_SaveDataDialog, "called"); + if (g_status == Status::RUNNING) { + sceSaveDataDialogClose(); + } + if (g_status == Status::NONE) { + return Error::NOT_INITIALIZED; + } + g_save_dialog_ui = SaveDialogUi{}; + g_status = Status::NONE; + CommonDialog::g_isUsed = false; + return Error::OK; +} + +Status PS4_SYSV_ABI sceSaveDataDialogUpdateStatus() { + LOG_TRACE(Lib_SaveDataDialog, "called status={}", magic_enum::enum_name(g_status)); + return g_status; +} + +void RegisterlibSceSaveDataDialog(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("fH46Lag88XY", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, + sceSaveDataDialogClose); + LIB_FUNCTION("yEiJ-qqr6Cg", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, + sceSaveDataDialogGetResult); + LIB_FUNCTION("ERKzksauAJA", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, + sceSaveDataDialogGetStatus); + LIB_FUNCTION("s9e3+YpRnzw", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, + sceSaveDataDialogInitialize); + LIB_FUNCTION("en7gNVnh878", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, + sceSaveDataDialogIsReadyToDisplay); + LIB_FUNCTION("4tPhsP6FpDI", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, + sceSaveDataDialogOpen); + LIB_FUNCTION("V-uEeFKARJU", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, + sceSaveDataDialogProgressBarInc); + LIB_FUNCTION("hay1CfTmLyA", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, + sceSaveDataDialogProgressBarSetValue); + LIB_FUNCTION("YuH2FA7azqQ", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, + sceSaveDataDialogTerminate); + LIB_FUNCTION("KK3Bdg1RWK0", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, + sceSaveDataDialogUpdateStatus); +}; + +} // namespace Libraries::SaveData::Dialog diff --git a/src/core/libraries/save_data/dialog/savedatadialog.h b/src/core/libraries/save_data/dialog/savedatadialog.h new file mode 100644 index 00000000..34afe98a --- /dev/null +++ b/src/core/libraries/save_data/dialog/savedatadialog.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" +#include "core/libraries/system/commondialog.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::SaveData::Dialog { + +struct OrbisSaveDataDialogParam; +struct OrbisSaveDataDialogResult; +enum class OrbisSaveDataDialogProgressBarTarget : u32; + +CommonDialog::Error PS4_SYSV_ABI sceSaveDataDialogClose(); +CommonDialog::Error PS4_SYSV_ABI sceSaveDataDialogGetResult(OrbisSaveDataDialogResult* result); +CommonDialog::Status PS4_SYSV_ABI sceSaveDataDialogGetStatus(); +CommonDialog::Error PS4_SYSV_ABI sceSaveDataDialogInitialize(); +s32 PS4_SYSV_ABI sceSaveDataDialogIsReadyToDisplay(); +CommonDialog::Error PS4_SYSV_ABI sceSaveDataDialogOpen(const OrbisSaveDataDialogParam* param); +CommonDialog::Error PS4_SYSV_ABI +sceSaveDataDialogProgressBarInc(OrbisSaveDataDialogProgressBarTarget target, u32 delta); +CommonDialog::Error PS4_SYSV_ABI +sceSaveDataDialogProgressBarSetValue(OrbisSaveDataDialogProgressBarTarget target, u32 rate); +CommonDialog::Error PS4_SYSV_ABI sceSaveDataDialogTerminate(); +CommonDialog::Status PS4_SYSV_ABI sceSaveDataDialogUpdateStatus(); + +void RegisterlibSceSaveDataDialog(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::SaveData::Dialog diff --git a/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp b/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp new file mode 100644 index 00000000..8d8cdff4 --- /dev/null +++ b/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp @@ -0,0 +1,802 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "common/singleton.h" +#include "core/file_sys/fs.h" +#include "core/libraries/save_data/save_instance.h" +#include "imgui/imgui_std.h" +#include "savedatadialog_ui.h" + +using namespace ImGui; +using namespace Libraries::CommonDialog; + +constexpr u32 OrbisSaveDataBlockSize = 32768; // 32 KiB + +constexpr auto SAVE_ICON_SIZE = ImVec2{152.0f, 85.0f}; +constexpr auto SAVE_ICON_PADDING = ImVec2{8.0f, 2.0f}; + +static constexpr ImVec2 BUTTON_SIZE{100.0f, 30.0f}; +constexpr auto FOOTER_HEIGHT = BUTTON_SIZE.y + 15.0f; +static constexpr float PROGRESS_BAR_WIDTH{0.8f}; + +static ::Core::FileSys::MntPoints* g_mnt = + Common::Singleton<::Core::FileSys::MntPoints>::Instance(); + +static std::string SpaceSizeToString(size_t size) { + std::string size_str; + if (size > 1024 * 1024 * 1024) { // > 1GB + size_str = fmt::format("{:.2f} GB", double(size / 1024 / 1024) / 1024.0f); + } else if (size > 1024 * 1024) { // > 1MB + size_str = fmt::format("{:.2f} MB", double(size / 1024) / 1024.0f); + } else if (size > 1024) { // > 1KB + size_str = fmt::format("{:.2f} KB", double(size) / 1024.0f); + } else { + size_str = fmt::format("{} B", size); + } + return size_str; +} + +namespace Libraries::SaveData::Dialog { + +void SaveDialogResult::CopyTo(OrbisSaveDataDialogResult& result) const { + result.mode = this->mode; + result.result = this->result; + result.buttonId = this->button_id; + if (result.dirName != nullptr) { + result.dirName->data.FromString(this->dir_name); + } + if (result.param != nullptr && this->param.GetString(SaveParams::MAINTITLE).has_value()) { + result.param->FromSFO(this->param); + } + result.userData = this->user_data; +} + +SaveDialogState::SaveDialogState(const OrbisSaveDataDialogParam& param) { + this->mode = param.mode; + this->type = param.dispType; + this->user_data = param.userData; + if (param.optionParam != nullptr) { + this->enable_back = {param.optionParam->back == OptionBack::ENABLE}; + } + + static std::string game_serial{*Common::Singleton::Instance()->GetString("CONTENT_ID"), 7, + 9}; + + const auto item = param.items; + this->user_id = item->userId; + + if (item->titleId == nullptr) { + this->title_id = game_serial; + } else { + this->title_id = item->titleId->data.to_string(); + } + + for (u32 i = 0; i < item->dirNameNum; i++) { + const auto dir_name = item->dirName[i].data.to_view(); + + if (dir_name.empty()) { + continue; + } + + auto dir_path = SaveInstance::MakeDirSavePath(user_id, title_id, dir_name); + + auto param_sfo_path = dir_path / "sce_sys" / "param.sfo"; + if (!std::filesystem::exists(param_sfo_path)) { + continue; + } + + PSF param_sfo; + param_sfo.Open(param_sfo_path); + + auto last_write = param_sfo.GetLastWrite(); +#ifdef _WIN32 + auto utc_time = std::chrono::file_clock::to_utc(last_write); +#else + auto utc_time = std::chrono::file_clock::to_sys(last_write); +#endif + std::string date_str = fmt::format("{:%d %b, %Y %R}", utc_time); + + size_t size = Common::FS::GetDirectorySize(dir_path); + std::string size_str = SpaceSizeToString(size); + + auto icon_path = dir_path / "sce_sys" / "icon0.png"; + RefCountedTexture icon; + if (std::filesystem::exists(icon_path)) { + icon = RefCountedTexture::DecodePngFile(icon_path); + } + + bool is_corrupted = std::filesystem::exists(dir_path / "sce_sys" / "corrupted"); + + this->save_list.emplace_back(Item{ + .dir_name = std::string{dir_name}, + .icon = icon, + + .title = std::string{*param_sfo.GetString(SaveParams::MAINTITLE)}, + .subtitle = std::string{*param_sfo.GetString(SaveParams::SUBTITLE)}, + .details = std::string{*param_sfo.GetString(SaveParams::DETAIL)}, + .date = date_str, + .size = size_str, + .last_write = param_sfo.GetLastWrite(), + .pfo = param_sfo, + .is_corrupted = is_corrupted, + }); + } + + if (type == DialogType::SAVE) { + RefCountedTexture icon; + std::string title{"New Save"}; + + const auto new_item = item->newItem; + if (new_item != nullptr && new_item->iconBuf && new_item->iconSize) { + auto buf = (u8*)new_item->iconBuf; + icon = RefCountedTexture::DecodePngTexture({buf, buf + new_item->iconSize}); + } else { + const auto& src_icon = g_mnt->GetHostPath("/app0/sce_sys/save_data.png"); + if (std::filesystem::exists(src_icon)) { + icon = RefCountedTexture::DecodePngFile(src_icon); + } + } + if (new_item != nullptr && new_item->title != nullptr) { + title = std::string{new_item->title}; + } + + this->new_item = Item{ + .dir_name = "", + .icon = icon, + .title = title, + }; + } + + if (item->focusPos != FocusPos::DIRNAME) { + this->focus_pos = item->focusPos; + } else { + this->focus_pos = item->focusPosDirName->data.to_string(); + } + this->style = item->itemStyle; + + switch (mode) { + case SaveDataDialogMode::USER_MSG: { + this->state = UserState{param}; + } break; + case SaveDataDialogMode::SYSTEM_MSG: + this->state = SystemState{*this, param}; + break; + case SaveDataDialogMode::ERROR_CODE: { + this->state = ErrorCodeState{param}; + } break; + case SaveDataDialogMode::PROGRESS_BAR: { + this->state = ProgressBarState{*this, param}; + } break; + default: + break; + } +} + +SaveDialogState::UserState::UserState(const OrbisSaveDataDialogParam& param) { + auto& user = *param.userMsgParam; + this->type = user.buttonType; + this->msg_type = user.msgType; + this->msg = user.msg != nullptr ? std::string{user.msg} : std::string{}; +} + +SaveDialogState::SystemState::SystemState(const SaveDialogState& state, + const OrbisSaveDataDialogParam& param) { +#define M(save, load, del) \ + if (type == DialogType::SAVE) \ + this->msg = save; \ + else if (type == DialogType::LOAD) \ + this->msg = load; \ + else if (type == DialogType::DELETE) \ + this->msg = del; \ + else \ + UNREACHABLE() + + auto type = param.dispType; + auto& sys = *param.sysMsgParam; + switch (sys.msgType) { + case SystemMessageType::NODATA: { + this->msg = "There is no saved data"; + } break; + case SystemMessageType::CONFIRM: + show_no = true; + M("Do you want to save?", "Do you want to load this saved data?", + "Do you want to delete this saved data?"); + break; + case SystemMessageType::OVERWRITE: + show_no = true; + M("Do you want to overwrite the existing saved data?", "##UNKNOWN##", "##UNKNOWN##"); + break; + case SystemMessageType::NOSPACE: + M(fmt::format( + "There is not enough space to save the data. To continue {} free space is required.", + SpaceSizeToString(sys.value * OrbisSaveDataBlockSize)), + "##UNKNOWN##", "##UNKNOWN##"); + break; + case SystemMessageType::PROGRESS: + hide_ok = true; + show_cancel = state.enable_back.value_or(false); + M("Saving...", "Loading...", "Deleting..."); + break; + case SystemMessageType::FILE_CORRUPTED: + this->msg = "The saved data is corrupted."; + break; + case SystemMessageType::FINISHED: + M("Saved successfully.", "Loading complete.", "Deletion complete."); + break; + case SystemMessageType::NOSPACE_CONTINUABLE: + M(fmt::format("There is not enough space to save the data. {} free space is required.", + SpaceSizeToString(sys.value * OrbisSaveDataBlockSize)), + "##UNKNOWN##", "##UNKNOWN##"); + break; + case SystemMessageType::CORRUPTED_AND_DELETED: { + show_cancel = state.enable_back.value_or(true); + const char* msg1 = "The saved data is corrupted and will be deleted."; + M(msg1, msg1, "##UNKNOWN##"); + } break; + case SystemMessageType::CORRUPTED_AND_CREATED: { + show_cancel = state.enable_back.value_or(true); + const char* msg2 = "The saved data is corrupted. This saved data will be deleted and a new " + "one will be created."; + M(msg2, msg2, "##UNKNOWN##"); + } break; + case SystemMessageType::CORRUPTED_AND_RESTORE: { + show_cancel = state.enable_back.value_or(true); + const char* msg3 = + "The saved data is corrupted. The data that was backed up by the system will be " + "restored."; + M(msg3, msg3, "##UNKNOWN##"); + } break; + case SystemMessageType::TOTAL_SIZE_EXCEEDED: + M("Cannot create more saved data", "##UNKNOWN##", "##UNKNOWN##"); + break; + default: + msg = fmt::format("Unknown message type: {}", magic_enum::enum_name(sys.msgType)); + break; + } + +#undef M +} + +SaveDialogState::ErrorCodeState::ErrorCodeState(const OrbisSaveDataDialogParam& param) { + auto& err = *param.errorCodeParam; + constexpr auto NOT_FOUND = 0x809F0008; + constexpr auto BROKEN = 0x809F000F; + switch (err.errorCode) { + case NOT_FOUND: + this->error_msg = "There is not saved data."; + break; + case BROKEN: + this->error_msg = "The data is corrupted."; + break; + default: + this->error_msg = fmt::format("An error has occurred. ({:X})", err.errorCode); + break; + } +} +SaveDialogState::ProgressBarState::ProgressBarState(const SaveDialogState& state, + const OrbisSaveDataDialogParam& param) { + this->progress = 0; + + auto& bar = *param.progressBarParam; + switch (bar.sysMsgType) { + case ProgressSystemMessageType::INVALID: + this->msg = bar.msg != nullptr ? std::string{bar.msg} : std::string{}; + break; + case ProgressSystemMessageType::PROGRESS: + switch (state.type) { + case DialogType::SAVE: + this->msg = "Saving..."; + break; + case DialogType::LOAD: + this->msg = "Loading..."; + break; + case DialogType::DELETE: + this->msg = "Deleting..."; + break; + } + break; + case ProgressSystemMessageType::RESTORE: + this->msg = "Restoring saved data..."; + break; + } +} + +SaveDialogUi::SaveDialogUi(SaveDialogState* state, Status* status, SaveDialogResult* result) + : state(state), status(status), result(result) { + if (status && *status == Status::RUNNING) { + first_render = true; + AddLayer(this); + } +} + +SaveDialogUi::~SaveDialogUi() { + Finish(ButtonId::INVALID); +} + +SaveDialogUi::SaveDialogUi(SaveDialogUi&& other) noexcept + : Layer(other), state(other.state), status(other.status), result(other.result) { + std::scoped_lock lock(draw_mutex, other.draw_mutex); + other.state = nullptr; + other.status = nullptr; + other.result = nullptr; + if (status && *status == Status::RUNNING) { + first_render = true; + AddLayer(this); + } +} + +SaveDialogUi& SaveDialogUi::operator=(SaveDialogUi other) { + std::scoped_lock lock(draw_mutex, other.draw_mutex); + using std::swap; + swap(state, other.state); + swap(status, other.status); + swap(result, other.result); + if (status && *status == Status::RUNNING) { + first_render = true; + AddLayer(this); + } + return *this; +} + +void SaveDialogUi::Finish(ButtonId buttonId, Result r) { + std::unique_lock lock(draw_mutex); + if (result) { + result->mode = this->state->mode; + result->result = r; + result->button_id = buttonId; + result->user_data = this->state->user_data; + if (state && state->mode != SaveDataDialogMode::LIST && !state->save_list.empty()) { + result->dir_name = state->save_list.front().dir_name; + } + } + if (status) { + *status = Status::FINISHED; + } + RemoveLayer(this); +} + +void SaveDialogUi::Draw() { + std::unique_lock lock{draw_mutex}; + + if (status == nullptr || *status != Status::RUNNING || state == nullptr) { + return; + } + + const auto& ctx = *GetCurrentContext(); + const auto& io = ctx.IO; + + ImVec2 window_size; + + if (state->GetMode() == SaveDataDialogMode::LIST) { + window_size = ImVec2{ + std::min(io.DisplaySize.x - 200.0f, 1100.0f), + std::min(io.DisplaySize.y - 100.0f, 700.0f), + }; + } else { + window_size = ImVec2{ + std::min(io.DisplaySize.x, 500.0f), + std::min(io.DisplaySize.y, 300.0f), + }; + } + + CentralizeWindow(); + SetNextWindowSize(window_size); + SetNextWindowCollapsed(false); + if (first_render || !io.NavActive) { + SetNextWindowFocus(); + } + if (Begin("Save Data Dialog##SaveDataDialog", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings)) { + DrawPrettyBackground(); + + Separator(); + // Draw title bigger + SetWindowFontScale(1.7f); + switch (state->type) { + case DialogType::SAVE: + TextUnformatted("Save"); + break; + case DialogType::LOAD: + TextUnformatted("Load"); + break; + case DialogType::DELETE: + TextUnformatted("Delete"); + break; + } + SetWindowFontScale(1.0f); + Separator(); + + BeginGroup(); + switch (state->GetMode()) { + case SaveDataDialogMode::LIST: + DrawList(); + break; + case SaveDataDialogMode::USER_MSG: + DrawUser(); + break; + case SaveDataDialogMode::SYSTEM_MSG: + DrawSystemMessage(); + break; + case SaveDataDialogMode::ERROR_CODE: + DrawErrorCode(); + break; + case SaveDataDialogMode::PROGRESS_BAR: + DrawProgressBar(); + break; + default: + TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "!!!Unknown dialog mode!!!"); + } + EndGroup(); + } + End(); + + first_render = false; + if (*status == Status::FINISHED) { + if (state) { + *state = SaveDialogState{}; + } + state = nullptr; + status = nullptr; + result = nullptr; + } +} + +void SaveDialogUi::DrawItem(int _id, const SaveDialogState::Item& item, bool clickable) { + constexpr auto text_spacing = 1.2f; + + auto& ctx = *GetCurrentContext(); + auto& window = *ctx.CurrentWindow; + + auto content_region_avail = GetContentRegionAvail(); + const auto outer_pos = window.DC.CursorPos; + const auto pos = outer_pos + SAVE_ICON_PADDING; + + const ImVec2 size = {content_region_avail.x - SAVE_ICON_PADDING.x, + SAVE_ICON_SIZE.y + SAVE_ICON_PADDING.y}; + const ImRect bb{outer_pos, outer_pos + size + SAVE_ICON_PADDING}; + + const ImGuiID id = GetID(_id); + + ItemSize(size + ImVec2{0.0f, SAVE_ICON_PADDING.y * 2.0f}); + if (!ItemAdd(bb, id)) { + return; + } + + window.DrawList->AddRectFilled(bb.Min + SAVE_ICON_PADDING, bb.Max - SAVE_ICON_PADDING, + GetColorU32(ImVec4{0.3f})); + + bool hovered = false; + if (clickable) { + bool held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + if (pressed) { + result->dir_name = item.dir_name; + result->param = item.pfo; + Finish(ButtonId::INVALID); + } + RenderNavHighlight(bb, id); + } + + if (item.icon) { + auto texture = item.icon.GetTexture(); + window.DrawList->AddImage(texture.im_id, pos, pos + SAVE_ICON_SIZE); + } else { + // placeholder + window.DrawList->AddRectFilled(pos, pos + SAVE_ICON_SIZE, GetColorU32(ImVec4{0.7f})); + } + + auto pos_x = SAVE_ICON_SIZE.x + 5.0f; + auto pos_y = 2.0f; + + if (!item.title.empty()) { + const char* begin = &item.title.front(); + const char* end = &item.title.back() + 1; + SetWindowFontScale(2.0f); + RenderText(pos + ImVec2{pos_x, pos_y}, begin, end, false); + if (item.is_corrupted) { + float width = CalcTextSize(begin, end).x + 10.0f; + PushStyleColor(ImGuiCol_Text, 0xFF0000FF); + RenderText(pos + ImVec2{pos_x + width, pos_y}, "- Corrupted", nullptr, false); + PopStyleColor(); + } + pos_y += ctx.FontSize * text_spacing; + } + + SetWindowFontScale(1.3f); + + if (state->style == ItemStyle::TITLE_SUBTITLE_DATESIZE) { + if (!item.subtitle.empty()) { + const char* begin = &item.subtitle.front(); + const char* end = &item.subtitle.back() + 1; + RenderText(pos + ImVec2{pos_x, pos_y}, begin, end, false); + } + pos_y += ctx.FontSize * text_spacing; + } + + { + float width = 0.0f; + if (!item.date.empty()) { + const char* d_begin = &item.date.front(); + const char* d_end = &item.date.back() + 1; + width = CalcTextSize(d_begin, d_end).x + 15.0f; + RenderText(pos + ImVec2{pos_x, pos_y}, d_begin, d_end, false); + } + if (!item.size.empty()) { + const char* s_begin = &item.size.front(); + const char* s_end = &item.size.back() + 1; + RenderText(pos + ImVec2{pos_x + width, pos_y}, s_begin, s_end, false); + } + pos_y += ctx.FontSize * text_spacing; + } + + if (state->style == ItemStyle::TITLE_DATASIZE_SUBTITLE && !item.subtitle.empty()) { + const char* begin = &item.subtitle.front(); + const char* end = &item.subtitle.back() + 1; + RenderText(pos + ImVec2{pos_x, pos_y}, begin, end, false); + } + + SetWindowFontScale(1.0f); + + if (hovered) { + window.DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border), 0.0f, 0, 2.0f); + } +} + +void SaveDialogUi::DrawList() { + auto availableSize = GetContentRegionAvail(); + + constexpr auto footerHeight = 30.0f; + availableSize.y -= footerHeight + 1.0f; + + BeginChild("##ScrollingRegion", availableSize, ImGuiChildFlags_NavFlattened); + int i = 0; + if (state->new_item.has_value()) { + DrawItem(i++, state->new_item.value()); + } + for (const auto& item : state->save_list) { + DrawItem(i++, item); + } + if (first_render) { // Make the initial focus + if (std::holds_alternative(state->focus_pos)) { + auto pos = std::get(state->focus_pos); + if (pos == FocusPos::LISTHEAD || pos == FocusPos::DATAHEAD) { + SetItemCurrentNavFocus(GetID(0)); + } else if (pos == FocusPos::LISTTAIL || pos == FocusPos::DATATAIL) { + SetItemCurrentNavFocus(GetID(std::max(i - 1, 0))); + } else { // Date + int idx = 0; + int max_idx = 0; + bool is_min = pos == FocusPos::DATAOLDEST; + std::filesystem::file_time_type max_write{}; + if (state->new_item.has_value()) { + idx++; + } + for (const auto& item : state->save_list) { + if (item.last_write > max_write ^ is_min) { + max_write = item.last_write; + max_idx = idx; + } + idx++; + } + SetItemCurrentNavFocus(GetID(max_idx)); + } + } else if (std::holds_alternative(state->focus_pos)) { + auto dir_name = std::get(state->focus_pos); + if (dir_name.empty()) { + SetItemCurrentNavFocus(GetID(0)); + } else { + int idx = 0; + if (state->new_item.has_value()) { + if (dir_name == state->new_item->dir_name) { + SetItemCurrentNavFocus(GetID(idx)); + } + idx++; + } + for (const auto& item : state->save_list) { + if (item.dir_name == dir_name) { + SetItemCurrentNavFocus(GetID(idx)); + break; + } + idx++; + } + } + } + } + EndChild(); + + Separator(); + if (state->enable_back.value_or(true)) { + constexpr auto back = "Back"; + constexpr float pad = 7.0f; + const auto txt_size = CalcTextSize(back); + const auto button_size = ImVec2{ + std::max(txt_size.x, 100.0f) + pad * 2.0f, + footerHeight - pad, + }; + SetCursorPosX(GetContentRegionAvail().x - button_size.x); + if (Button(back, button_size)) { + result->dir_name.clear(); + Finish(ButtonId::INVALID); + } + if (IsKeyPressed(ImGuiKey_GamepadFaceRight)) { + SetItemCurrentNavFocus(); + } + } +} + +void SaveDialogUi::DrawUser() { + const auto& user_state = state->GetState(); + const auto btn_type = user_state.type; + + const auto ws = GetWindowSize(); + + if (!state->save_list.empty()) { + DrawItem(0, state->save_list.front(), false); + } + + auto has_btn = btn_type != ButtonType::NONE; + ImVec2 btn_space; + if (has_btn) { + btn_space = ImVec2{0.0f, FOOTER_HEIGHT}; + } + + const auto& msg = user_state.msg; + if (!msg.empty()) { + const char* begin = &msg.front(); + const char* end = &msg.back() + 1; + if (user_state.msg_type == UserMessageType::ERROR) { + PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); + // Maybe make the text bold? + } + DrawCenteredText(begin, end, GetContentRegionAvail() - btn_space); + if (user_state.msg_type == UserMessageType::ERROR) { + PopStyleColor(); + } + } + + if (has_btn) { + int count = 1; + if (btn_type == ButtonType::YESNO || btn_type == ButtonType::ONCANCEL) { + ++count; + } + + SetCursorPos({ + ws.x / 2.0f - BUTTON_SIZE.x / 2.0f * static_cast(count), + ws.y - FOOTER_HEIGHT + 5.0f, + }); + + BeginGroup(); + if (btn_type == ButtonType::YESNO) { + if (Button("Yes", BUTTON_SIZE)) { + Finish(ButtonId::YES); + } + SameLine(); + if (Button("No", BUTTON_SIZE)) { + Finish(ButtonId::NO); + } + if (first_render || IsKeyPressed(ImGuiKey_GamepadFaceRight)) { + SetItemCurrentNavFocus(); + } + } else { + if (Button("OK", BUTTON_SIZE)) { + Finish(ButtonId::OK); + } + if (first_render) { + SetItemCurrentNavFocus(); + } + if (btn_type == ButtonType::ONCANCEL) { + SameLine(); + if (Button("Cancel", BUTTON_SIZE)) { + Finish(ButtonId::INVALID, Result::USER_CANCELED); + } + if (IsKeyPressed(ImGuiKey_GamepadFaceRight)) { + SetItemCurrentNavFocus(); + } + } + } + EndGroup(); + } +} + +void SaveDialogUi::DrawSystemMessage() { + const auto& sys_state = state->GetState(); + + if (!state->save_list.empty()) { + DrawItem(0, state->save_list.front(), false); + } + + const auto ws = GetWindowSize(); + const auto& msg = sys_state.msg; + if (!msg.empty()) { + const char* begin = &msg.front(); + const char* end = &msg.back() + 1; + DrawCenteredText(begin, end, GetContentRegionAvail() - ImVec2{0.0f, FOOTER_HEIGHT}); + } + int count = 1; + if (sys_state.hide_ok) { + --count; + } + if (sys_state.show_no || sys_state.show_cancel) { + ++count; + } + + SetCursorPos({ + ws.x / 2.0f - BUTTON_SIZE.x / 2.0f * static_cast(count), + ws.y - FOOTER_HEIGHT + 5.0f, + }); + BeginGroup(); + if (Button(sys_state.show_no ? "Yes" : "OK", BUTTON_SIZE)) { + Finish(ButtonId::YES); + } + SameLine(); + if (sys_state.show_no) { + if (Button("No", BUTTON_SIZE)) { + Finish(ButtonId::NO); + } + } else if (sys_state.show_cancel) { + if (Button("Cancel", BUTTON_SIZE)) { + Finish(ButtonId::INVALID, Result::USER_CANCELED); + } + } + if (first_render || IsKeyPressed(ImGuiKey_GamepadFaceRight)) { + SetItemCurrentNavFocus(); + } + EndGroup(); +} + +void SaveDialogUi::DrawErrorCode() { + const auto& err_state = state->GetState(); + + if (!state->save_list.empty()) { + DrawItem(0, state->save_list.front(), false); + } + + const auto ws = GetWindowSize(); + const auto& msg = err_state.error_msg; + if (!msg.empty()) { + const char* begin = &msg.front(); + const char* end = &msg.back() + 1; + DrawCenteredText(begin, end, GetContentRegionAvail() - ImVec2{0.0f, FOOTER_HEIGHT}); + } + + SetCursorPos({ + ws.x / 2.0f - BUTTON_SIZE.x / 2.0f, + ws.y - FOOTER_HEIGHT + 5.0f, + }); + if (Button("OK", BUTTON_SIZE)) { + Finish(ButtonId::OK); + } + if (first_render) { + SetItemCurrentNavFocus(); + } +} + +void SaveDialogUi::DrawProgressBar() { + const auto& bar_state = state->GetState(); + + const auto ws = GetWindowSize(); + + if (!state->save_list.empty()) { + DrawItem(0, state->save_list.front(), false); + } + + const auto& msg = bar_state.msg; + if (!msg.empty()) { + const char* begin = &msg.front(); + const char* end = &msg.back() + 1; + DrawCenteredText(begin, end, GetContentRegionAvail() - ImVec2{0.0f, FOOTER_HEIGHT}); + } + + SetCursorPos({ + ws.x * ((1 - PROGRESS_BAR_WIDTH) / 2.0f), + ws.y - FOOTER_HEIGHT + 5.0f, + }); + + ProgressBar(static_cast(bar_state.progress) / 100.0f, + {PROGRESS_BAR_WIDTH * ws.x, BUTTON_SIZE.y}); +} +}; // namespace Libraries::SaveData::Dialog \ No newline at end of file diff --git a/src/core/libraries/save_data/dialog/savedatadialog_ui.h b/src/core/libraries/save_data/dialog/savedatadialog_ui.h new file mode 100644 index 00000000..8b9a68e1 --- /dev/null +++ b/src/core/libraries/save_data/dialog/savedatadialog_ui.h @@ -0,0 +1,317 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "core/file_format/psf.h" +#include "core/libraries/save_data/savedata.h" +#include "core/libraries/system/commondialog.h" +#include "imgui/imgui_layer.h" +#include "imgui/imgui_texture.h" + +namespace Libraries::SaveData::Dialog { + +using OrbisUserServiceUserId = s32; + +enum class SaveDataDialogMode : u32 { + INVALID = 0, + LIST = 1, + USER_MSG = 2, + SYSTEM_MSG = 3, + ERROR_CODE = 4, + PROGRESS_BAR = 5, +}; + +enum class DialogType : u32 { + SAVE = 1, + LOAD = 2, + DELETE = 3, +}; + +enum class DialogAnimation : u32 { + ON = 0, + OFF = 1, +}; + +enum class ButtonId : u32 { + INVALID = 0, + OK = 1, + YES = 1, + NO = 2, +}; + +enum class ButtonType : u32 { + OK = 0, + YESNO = 1, + NONE = 2, + ONCANCEL = 3, +}; + +enum class UserMessageType : u32 { + NORMAL = 0, + ERROR = 1, +}; + +enum class FocusPos : u32 { + LISTHEAD = 0, + LISTTAIL = 1, + DATAHEAD = 2, + DATATAIL = 3, + DATALTATEST = 4, + DATAOLDEST = 5, + DIRNAME = 6, +}; + +enum class ItemStyle : u32 { + TITLE_DATASIZE_SUBTITLE = 0, + TITLE_SUBTITLE_DATESIZE = 1, + TITLE_DATESIZE = 2, +}; + +enum class SystemMessageType : u32 { + NODATA = 1, + CONFIRM = 2, + OVERWRITE = 3, + NOSPACE = 4, + PROGRESS = 5, + FILE_CORRUPTED = 6, + FINISHED = 7, + NOSPACE_CONTINUABLE = 8, + CORRUPTED_AND_DELETED = 10, + CORRUPTED_AND_CREATED = 11, + CORRUPTED_AND_RESTORE = 13, + TOTAL_SIZE_EXCEEDED = 14, +}; + +enum class ProgressBarType : u32 { + PERCENTAGE = 0, +}; + +enum class ProgressSystemMessageType : u32 { + INVALID = 0, + PROGRESS = 1, + RESTORE = 2, +}; + +enum class OptionBack : u32 { + ENABLE = 0, + DISABLE = 1, +}; + +enum class OrbisSaveDataDialogProgressBarTarget : u32 { + DEFAULT = 0, +}; + +struct AnimationParam { + DialogAnimation userOK; + DialogAnimation userCancel; + std::array _reserved; +}; + +struct SaveDialogNewItem { + const char* title; + void* iconBuf; + size_t iconSize; + std::array _reserved; +}; + +struct SaveDialogItems { + OrbisUserServiceUserId userId; + s32 : 32; + const OrbisSaveDataTitleId* titleId; + const OrbisSaveDataDirName* dirName; + u32 dirNameNum; + s32 : 32; + const SaveDialogNewItem* newItem; + FocusPos focusPos; + s32 : 32; + const OrbisSaveDataDirName* focusPosDirName; + ItemStyle itemStyle; + std::array _reserved; +}; + +struct UserMessageParam { + ButtonType buttonType; + UserMessageType msgType; + const char* msg; + std::array _reserved; +}; + +struct SystemMessageParam { + SystemMessageType msgType; + s32 : 32; + u64 value; + std::array _reserved; +}; + +struct ErrorCodeParam { + u32 errorCode; + std::array _reserved; +}; + +struct ProgressBarParam { + ProgressBarType barType; + s32 : 32; + const char* msg; + ProgressSystemMessageType sysMsgType; + std::array _reserved; +}; + +struct OptionParam { + OptionBack back; + std::array _reserved; +}; + +struct OrbisSaveDataDialogParam { + CommonDialog::BaseParam baseParam; + s32 size; + SaveDataDialogMode mode; + DialogType dispType; + s32 : 32; + AnimationParam* animParam; + SaveDialogItems* items; + UserMessageParam* userMsgParam; + SystemMessageParam* sysMsgParam; + ErrorCodeParam* errorCodeParam; + ProgressBarParam* progressBarParam; + void* userData; + OptionParam* optionParam; + std::array _reserved; +}; + +struct OrbisSaveDataDialogResult { + SaveDataDialogMode mode{}; + CommonDialog::Result result{}; + ButtonId buttonId{}; + s32 : 32; + OrbisSaveDataDirName* dirName; + OrbisSaveDataParam* param; + void* userData; + std::array _reserved; +}; + +struct SaveDialogResult { + SaveDataDialogMode mode{}; + CommonDialog::Result result{CommonDialog::Result::OK}; + ButtonId button_id{ButtonId::INVALID}; + std::string dir_name{}; + PSF param{}; + void* user_data{}; + + void CopyTo(OrbisSaveDataDialogResult& result) const; +}; + +class SaveDialogState { + friend class SaveDialogUi; + +public: + struct UserState { + ButtonType type{}; + UserMessageType msg_type{}; + std::string msg{}; + + UserState(const OrbisSaveDataDialogParam& param); + }; + struct SystemState { + std::string msg{}; + bool hide_ok{}; + bool show_no{}; // Yes instead of OK + bool show_cancel{}; + + SystemState(const SaveDialogState& state, const OrbisSaveDataDialogParam& param); + }; + struct ErrorCodeState { + std::string error_msg{}; + + ErrorCodeState(const OrbisSaveDataDialogParam& param); + }; + struct ProgressBarState { + std::string msg{}; + u32 progress{}; + + ProgressBarState(const SaveDialogState& state, const OrbisSaveDataDialogParam& param); + }; + + struct Item { + std::string dir_name{}; + ImGui::RefCountedTexture icon{}; + + std::string title{}; + std::string subtitle{}; + std::string details{}; + std::string date{}; + std::string size{}; + + std::filesystem::file_time_type last_write{}; + PSF pfo{}; + bool is_corrupted{}; + }; + +private: + SaveDataDialogMode mode{}; + DialogType type{}; + void* user_data{}; + std::optional enable_back{}; + + OrbisUserServiceUserId user_id{}; + std::string title_id{}; + std::vector save_list{}; + std::variant focus_pos{std::monostate{}}; + ItemStyle style{}; + + std::optional new_item{}; + + std::variant state{ + std::monostate{}}; + +public: + explicit SaveDialogState(const OrbisSaveDataDialogParam& param); + + SaveDialogState() = default; + + [[nodiscard]] SaveDataDialogMode GetMode() const { + return mode; + } + + template + [[nodiscard]] T& GetState() { + return std::get(state); + } +}; + +class SaveDialogUi final : public ImGui::Layer { + bool first_render{false}; + SaveDialogState* state{}; + CommonDialog::Status* status{}; + SaveDialogResult* result{}; + + std::recursive_mutex draw_mutex{}; + +public: + explicit SaveDialogUi(SaveDialogState* state = nullptr, CommonDialog::Status* status = nullptr, + SaveDialogResult* result = nullptr); + + ~SaveDialogUi() override; + SaveDialogUi(const SaveDialogUi& other) = delete; + SaveDialogUi(SaveDialogUi&& other) noexcept; + SaveDialogUi& operator=(SaveDialogUi other); + + void Finish(ButtonId buttonId, CommonDialog::Result r = CommonDialog::Result::OK); + + void Draw() override; + +private: + void DrawItem(int id, const SaveDialogState::Item& item, bool clickable = true); + + void DrawList(); + void DrawUser(); + void DrawSystemMessage(); + void DrawErrorCode(); + void DrawProgressBar(); +}; + +}; // namespace Libraries::SaveData::Dialog \ No newline at end of file diff --git a/src/core/libraries/save_data/error_codes.h b/src/core/libraries/save_data/error_codes.h deleted file mode 100644 index a4a1b7a5..00000000 --- a/src/core/libraries/save_data/error_codes.h +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -constexpr int ORBIS_SAVE_DATA_ERROR_PARAMETER = 0x809F0000; -constexpr int ORBIS_SAVE_DATA_ERROR_NOT_INITIALIZED = 0x809F0001; -constexpr int ORBIS_SAVE_DATA_ERROR_OUT_OF_MEMORY = 0x809F0002; -constexpr int ORBIS_SAVE_DATA_ERROR_BUSY = 0x809F0003; -constexpr int ORBIS_SAVE_DATA_ERROR_NOT_MOUNTED = 0x809F0004; -constexpr int ORBIS_SAVE_DATA_ERROR_NO_PERMISSION = 0x809F0005; -constexpr int ORBIS_SAVE_DATA_ERROR_FINGERPRINT_MISMATCH = 0x809F0006; -constexpr int ORBIS_SAVE_DATA_ERROR_EXISTS = 0x809F0007; -constexpr int ORBIS_SAVE_DATA_ERROR_NOT_FOUND = 0x809F0008; -constexpr int ORBIS_SAVE_DATA_ERROR_NO_SPACE_FS = 0x809F000A; -constexpr int ORBIS_SAVE_DATA_ERROR_INTERNAL = 0x809F000B; -constexpr int ORBIS_SAVE_DATA_ERROR_MOUNT_FULL = 0x809F000C; -constexpr int ORBIS_SAVE_DATA_ERROR_BAD_MOUNTED = 0x809F000D; -constexpr int ORBIS_SAVE_DATA_ERROR_FILE_NOT_FOUND = 0x809F000E; -constexpr int ORBIS_SAVE_DATA_ERROR_BROKEN = 0x809F000F; -constexpr int ORBIS_SAVE_DATA_ERROR_INVALID_LOGIN_USER = 0x809F0011; -constexpr int ORBIS_SAVE_DATA_ERROR_MEMORY_NOT_READY = 0x809F0012; -constexpr int ORBIS_SAVE_DATA_ERROR_BACKUP_BUSY = 0x809F0013; -constexpr int ORBIS_SAVE_DATA_ERROR_NOT_REGIST_CALLBACK = 0x809F0015; -constexpr int ORBIS_SAVE_DATA_ERROR_BUSY_FOR_SAVING = 0x809F0016; -constexpr int ORBIS_SAVE_DATA_ERROR_LIMITATION_OVER = 0x809F0017; -constexpr int ORBIS_SAVE_DATA_ERROR_EVENT_BUSY = 0x809F0018; -constexpr int ORBIS_SAVE_DATA_ERROR_PARAMSFO_TRANSFER_TITLE_ID_NOT_FOUND = 0x809F0019; diff --git a/src/core/libraries/save_data/save_backup.cpp b/src/core/libraries/save_data/save_backup.cpp new file mode 100644 index 00000000..93af373a --- /dev/null +++ b/src/core/libraries/save_data/save_backup.cpp @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include + +#include "save_backup.h" +#include "save_instance.h" + +#include "common/logging/log.h" +#include "common/logging/log_entry.h" +#include "common/polyfill_thread.h" +#include "common/thread.h" + +constexpr std::string_view sce_sys = "sce_sys"; // system folder inside save +constexpr std::string_view backup_dir = "sce_backup"; // backup folder +constexpr std::string_view backup_dir_tmp = "sce_backup_tmp"; // in-progress backup folder + +namespace fs = std::filesystem; + +namespace Libraries::SaveData::Backup { + +static std::jthread g_backup_thread; +static std::counting_semaphore g_backup_thread_semaphore{0}; + +static std::mutex g_backup_queue_mutex; +static std::deque g_backup_queue; +static std::deque g_result_queue; + +static std::atomic_int g_backup_progress = 0; +static std::atomic g_backup_status = WorkerStatus::NotStarted; + +static void backup(const std::filesystem::path& dir_name) { + if (!fs::exists(dir_name)) { + return; + } + std::vector backup_files; + for (const auto& entry : fs::directory_iterator(dir_name)) { + const auto filename = entry.path().filename(); + if (filename != backup_dir && filename != backup_dir_tmp) { + backup_files.push_back(entry.path()); + } + } + const auto backup_dir = dir_name / ::backup_dir; + const auto backup_dir_tmp = dir_name / ::backup_dir_tmp; + + g_backup_progress = 0; + + int total_count = static_cast(backup_files.size()); + int current_count = 0; + + fs::remove_all(backup_dir_tmp); + fs::create_directory(backup_dir_tmp); + for (const auto& file : backup_files) { + fs::copy(file, backup_dir_tmp / file.filename(), fs::copy_options::recursive); + current_count++; + g_backup_progress = current_count * 100 / total_count; + } + bool has_existing = fs::exists(backup_dir); + if (has_existing) { + fs::rename(backup_dir, dir_name / "sce_backup_old"); + } + fs::rename(backup_dir_tmp, backup_dir); + if (has_existing) { + fs::remove_all(dir_name / "sce_backup_old"); + } +} + +static void BackupThreadBody() { + Common::SetCurrentThreadName("SaveData_BackupThread"); + while (true) { + g_backup_status = WorkerStatus::Waiting; + g_backup_thread_semaphore.acquire(); + BackupRequest req; + { + std::scoped_lock lk{g_backup_queue_mutex}; + req = g_backup_queue.front(); + } + if (req.save_path.empty()) { + break; + } + g_backup_status = WorkerStatus::Running; + LOG_INFO(Lib_SaveData, "Backing up the following directory: {}", req.save_path.string()); + backup(req.save_path); + LOG_DEBUG(Lib_SaveData, "Backing up the following directory: {} finished", + req.save_path.string()); + { + std::scoped_lock lk{g_backup_queue_mutex}; + g_backup_queue.pop_front(); + g_result_queue.push_back(std::move(req)); + if (g_result_queue.size() > 20) { + g_result_queue.pop_front(); + } + } + } + g_backup_status = WorkerStatus::NotStarted; +} + +void StartThread() { + if (g_backup_status != WorkerStatus::NotStarted) { + return; + } + LOG_DEBUG(Lib_SaveData, "Starting backup thread"); + g_backup_thread = std::jthread{BackupThreadBody}; + g_backup_status = WorkerStatus::Waiting; +} + +void StopThread() { + if (g_backup_status == WorkerStatus::NotStarted || g_backup_status == WorkerStatus::Stopping) { + return; + } + LOG_DEBUG(Lib_SaveData, "Stopping backup thread"); + { + std::scoped_lock lk{g_backup_queue_mutex}; + g_backup_queue.emplace_back(BackupRequest{}); + } + g_backup_thread_semaphore.release(); + g_backup_status = WorkerStatus::Stopping; +} + +bool NewRequest(OrbisUserServiceUserId user_id, std::string_view title_id, + std::string_view dir_name, OrbisSaveDataEventType origin) { + auto save_path = SaveInstance::MakeDirSavePath(user_id, title_id, dir_name); + + if (g_backup_status != WorkerStatus::Waiting && g_backup_status != WorkerStatus::Running) { + LOG_ERROR(Lib_SaveData, "Called backup while status is {}. Backup request to {} ignored", + magic_enum::enum_name(g_backup_status.load()), save_path.string()); + return false; + } + { + std::scoped_lock lk{g_backup_queue_mutex}; + g_backup_queue.push_back(BackupRequest{ + .user_id = user_id, + .title_id = std::string{title_id}, + .dir_name = std::string{dir_name}, + .origin = origin, + .save_path = std::move(save_path), + }); + } + g_backup_thread_semaphore.release(); + return true; +} + +bool Restore(const std::filesystem::path& save_path) { + LOG_INFO(Lib_SaveData, "Restoring backup for {}", save_path.string()); + if (!fs::exists(save_path) || !fs::exists(save_path / backup_dir)) { + return false; + } + for (const auto& entry : fs::directory_iterator(save_path)) { + const auto filename = entry.path().filename(); + if (filename != backup_dir) { + fs::remove_all(entry.path()); + } + } + + for (const auto& entry : fs::directory_iterator(save_path / backup_dir)) { + const auto filename = entry.path().filename(); + fs::copy(entry.path(), save_path / filename, fs::copy_options::recursive); + } + + return true; +} + +WorkerStatus GetWorkerStatus() { + return g_backup_status; +} + +bool IsBackupExecutingFor(const std::filesystem::path& save_path) { + std::scoped_lock lk{g_backup_queue_mutex}; + return std::ranges::find(g_backup_queue, save_path, + [](const auto& v) { return v.save_path; }) != g_backup_queue.end(); +} + +std::filesystem::path MakeBackupPath(const std::filesystem::path& save_path) { + return save_path / backup_dir; +} + +std::optional PopLastEvent() { + std::scoped_lock lk{g_backup_queue_mutex}; + if (g_result_queue.empty()) { + return std::nullopt; + } + auto req = std::move(g_result_queue.front()); + g_result_queue.pop_front(); + return req; +} + +void PushBackupEvent(BackupRequest&& req) { + std::scoped_lock lk{g_backup_queue_mutex}; + g_result_queue.push_back(std::move(req)); + if (g_result_queue.size() > 20) { + g_result_queue.pop_front(); + } +} + +float GetProgress() { + return static_cast(g_backup_progress) / 100.0f; +} + +void ClearProgress() { + g_backup_progress = 0; +} + +} // namespace Libraries::SaveData::Backup \ No newline at end of file diff --git a/src/core/libraries/save_data/save_backup.h b/src/core/libraries/save_data/save_backup.h new file mode 100644 index 00000000..f0aef369 --- /dev/null +++ b/src/core/libraries/save_data/save_backup.h @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/types.h" + +namespace Libraries::SaveData { + +using OrbisUserServiceUserId = s32; + +namespace Backup { + +enum class WorkerStatus { + NotStarted, + Waiting, + Running, + Stopping, +}; + +enum class OrbisSaveDataEventType : u32 { + UMOUNT_BACKUP = 1, + BACKUP = 2, + SAVE_DATA_MEMORY_SYNC = 3, +}; + +struct BackupRequest { + OrbisUserServiceUserId user_id{}; + std::string title_id{}; + std::string dir_name{}; + OrbisSaveDataEventType origin{}; + + std::filesystem::path save_path{}; +}; + +// No problem calling this function if the backup thread is already running +void StartThread(); + +void StopThread(); + +bool NewRequest(OrbisUserServiceUserId user_id, std::string_view title_id, + std::string_view dir_name, OrbisSaveDataEventType origin); + +bool Restore(const std::filesystem::path& save_path); + +WorkerStatus GetWorkerStatus(); + +bool IsBackupExecutingFor(const std::filesystem::path& save_path); + +std::filesystem::path MakeBackupPath(const std::filesystem::path& save_path); + +std::optional PopLastEvent(); + +void PushBackupEvent(BackupRequest&& req); + +float GetProgress(); + +void ClearProgress(); + +} // namespace Backup + +} // namespace Libraries::SaveData diff --git a/src/core/libraries/save_data/save_instance.cpp b/src/core/libraries/save_data/save_instance.cpp new file mode 100644 index 00000000..2624ca36 --- /dev/null +++ b/src/core/libraries/save_data/save_instance.cpp @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include + +#include "common/assert.h" +#include "common/config.h" +#include "common/path_util.h" +#include "common/singleton.h" +#include "core/file_sys/fs.h" +#include "save_instance.h" + +constexpr u32 OrbisSaveDataBlocksMax = 32768; // 1 GiB +constexpr std::string_view sce_sys = "sce_sys"; // system folder inside save +constexpr std::string_view max_block_file_name = "max_block.txt"; + +static Core::FileSys::MntPoints* g_mnt = Common::Singleton::Instance(); + +namespace fs = std::filesystem; + +// clang-format off +static const std::unordered_map default_title = { + {"ja_JP", "セーブデータ"}, + {"en", "Saved Data"}, + {"fr", "Données sauvegardées"}, + {"es_ES", "Datos guardados"}, + {"de", "Gespeicherte Daten"}, + {"it", "Dati salvati"}, + {"nl", "Opgeslagen data"}, + {"pt_PT", "Dados guardados"}, + {"ru", "Сохраненные данные"}, + {"ko_KR", "저장 데이터"}, + {"zh_CN", "保存数据"}, + {"fi", "Tallennetut tiedot"}, + {"sv_SE", "Sparade data"}, + {"da_DK", "Gemte data"}, + {"no_NO", "Lagrede data"}, + {"pl_PL", "Zapisane dane"}, + {"pt_BR", "Dados salvos"}, + {"tr_TR", "Kayıtlı Veriler"}, +}; +// clang-format on + +namespace Libraries::SaveData { + +std::filesystem::path SaveInstance::MakeTitleSavePath(OrbisUserServiceUserId user_id, + std::string_view game_serial) { + return Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir) / std::to_string(user_id) / + game_serial; +} + +std::filesystem::path SaveInstance::MakeDirSavePath(OrbisUserServiceUserId user_id, + std::string_view game_serial, + std::string_view dir_name) { + return Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir) / std::to_string(user_id) / + game_serial / dir_name; +} + +int SaveInstance::GetMaxBlocks(const std::filesystem::path& save_path) { + Common::FS::IOFile max_blocks_file{save_path / sce_sys / max_block_file_name, + Common::FS::FileAccessMode::Read}; + int max_blocks = 0; + if (max_blocks_file.IsOpen()) { + max_blocks = std::atoi(max_blocks_file.ReadString(16).c_str()); + } + if (max_blocks <= 0) { + max_blocks = OrbisSaveDataBlocksMax; + } + + return max_blocks; +} + +std::filesystem::path SaveInstance::GetParamSFOPath(const std::filesystem::path& dir_path) { + return dir_path / sce_sys / "param.sfo"; +} + +void SaveInstance::SetupDefaultParamSFO(PSF& param_sfo, std::string dir_name, + std::string game_serial) { + std::string locale = Config::getEmulatorLanguage(); + if (!default_title.contains(locale)) { + locale = "en"; + } + +#define P(type, key, ...) param_sfo.Add##type(std::string{key}, __VA_ARGS__) + // TODO Link with user service + P(Binary, SaveParams::ACCOUNT_ID, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + P(String, SaveParams::MAINTITLE, default_title.at(locale)); + P(String, SaveParams::SUBTITLE, ""); + P(String, SaveParams::DETAIL, ""); + P(String, SaveParams::SAVEDATA_DIRECTORY, std::move(dir_name)); + P(Integer, SaveParams::SAVEDATA_LIST_PARAM, 0); + P(String, SaveParams::TITLE_ID, std::move(game_serial)); +#undef P +} + +SaveInstance::SaveInstance(int slot_num, OrbisUserServiceUserId user_id, std::string _game_serial, + std::string_view _dir_name, int max_blocks) + : slot_num(slot_num), user_id(user_id), game_serial(std::move(_game_serial)), + dir_name(_dir_name), max_blocks(max_blocks) { + ASSERT(slot_num >= 0 && slot_num < 16); + + save_path = MakeDirSavePath(user_id, game_serial, dir_name); + + const auto sce_sys_path = save_path / sce_sys; + param_sfo_path = sce_sys_path / "param.sfo"; + corrupt_file_path = sce_sys_path / "corrupted"; + + mount_point = "/savedata" + std::to_string(slot_num); + + this->exists = fs::exists(param_sfo_path); + this->mounted = g_mnt->GetMount(mount_point) != nullptr; +} + +SaveInstance::~SaveInstance() { + if (mounted) { + Umount(); + } +} +SaveInstance::SaveInstance(SaveInstance&& other) noexcept { + if (this == &other) + return; + *this = std::move(other); +} + +SaveInstance& SaveInstance::operator=(SaveInstance&& other) noexcept { + if (this == &other) + return *this; + slot_num = other.slot_num; + user_id = other.user_id; + game_serial = std::move(other.game_serial); + dir_name = std::move(other.dir_name); + save_path = std::move(other.save_path); + param_sfo_path = std::move(other.param_sfo_path); + corrupt_file_path = std::move(other.corrupt_file_path); + corrupt_file = std::move(other.corrupt_file); + param_sfo = std::move(other.param_sfo); + mount_point = std::move(other.mount_point); + max_blocks = other.max_blocks; + exists = other.exists; + mounted = other.mounted; + read_only = other.read_only; + + other.mounted = false; + + return *this; +} + +void SaveInstance::SetupAndMount(bool read_only, bool copy_icon, bool ignore_corrupt) { + if (mounted) { + UNREACHABLE_MSG("Save instance is already mounted"); + } + this->exists = fs::exists(param_sfo_path); // check again just in case + if (!exists) { + CreateFiles(); + if (copy_icon) { + const auto& src_icon = g_mnt->GetHostPath("/app0/sce_sys/save_data.png"); + if (fs::exists(src_icon)) { + fs::copy_file(src_icon, GetIconPath()); + } + } + exists = true; + } else { + if (!ignore_corrupt && fs::exists(corrupt_file_path)) { + throw std::filesystem::filesystem_error( + "Corrupted save data", corrupt_file_path, + std::make_error_code(std::errc::illegal_byte_sequence)); + } + if (!param_sfo.Open(param_sfo_path)) { + throw std::filesystem::filesystem_error( + "Failed to read param.sfo", param_sfo_path, + std::make_error_code(std::errc::illegal_byte_sequence)); + } + } + + if (!ignore_corrupt && !read_only) { + int err = corrupt_file.Open(corrupt_file_path, Common::FS::FileAccessMode::Write); + if (err != 0) { + throw std::filesystem::filesystem_error( + "Failed to open corrupted file", corrupt_file_path, + std::make_error_code(std::errc::illegal_byte_sequence)); + } + } + + max_blocks = GetMaxBlocks(save_path); + + g_mnt->Mount(save_path, mount_point, read_only); + mounted = true; + this->read_only = read_only; +} + +void SaveInstance::Umount() { + if (!mounted) { + UNREACHABLE_MSG("Save instance is not mounted"); + return; + } + mounted = false; + const bool ok = param_sfo.Encode(param_sfo_path); + if (!ok) { + throw std::filesystem::filesystem_error("Failed to write param.sfo", param_sfo_path, + std::make_error_code(std::errc::permission_denied)); + } + param_sfo = PSF(); + + corrupt_file.Close(); + fs::remove(corrupt_file_path); + g_mnt->Unmount(save_path, mount_point); +} + +void SaveInstance::CreateFiles() { + const auto sce_sys_dir = save_path / sce_sys; + fs::create_directories(sce_sys_dir); + + SetupDefaultParamSFO(param_sfo, dir_name, game_serial); + + const bool ok = param_sfo.Encode(param_sfo_path); + if (!ok) { + throw std::filesystem::filesystem_error("Failed to write param.sfo", param_sfo_path, + std::make_error_code(std::errc::permission_denied)); + } + + Common::FS::IOFile max_block{sce_sys_dir / max_block_file_name, + Common::FS::FileAccessMode::Write}; + max_block.WriteString(std::to_string(max_blocks == 0 ? OrbisSaveDataBlocksMax : max_blocks)); +} + +} // namespace Libraries::SaveData \ No newline at end of file diff --git a/src/core/libraries/save_data/save_instance.h b/src/core/libraries/save_data/save_instance.h new file mode 100644 index 00000000..f0701104 --- /dev/null +++ b/src/core/libraries/save_data/save_instance.h @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/io_file.h" +#include "core/file_format/psf.h" + +namespace Core::FileSys { +class MntPoints; +} + +namespace Libraries::SaveData { + +// Used constexpr to easily use as string +namespace SaveParams { +constexpr std::string_view ACCOUNT_ID = "ACCOUNT_ID"; +constexpr std::string_view ATTRIBUTE = "ATTRIBUTE"; +constexpr std::string_view CATEGORY = "CATEGORY"; +constexpr std::string_view DETAIL = "DETAIL"; +constexpr std::string_view FORMAT = "FORMAT"; +constexpr std::string_view MAINTITLE = "MAINTITLE"; +constexpr std::string_view PARAMS = "PARAMS"; +constexpr std::string_view SAVEDATA_BLOCKS = "SAVEDATA_BLOCKS"; +constexpr std::string_view SAVEDATA_DIRECTORY = "SAVEDATA_DIRECTORY"; +constexpr std::string_view SAVEDATA_LIST_PARAM = "SAVEDATA_LIST_PARAM"; +constexpr std::string_view SUBTITLE = "SUBTITLE"; +constexpr std::string_view TITLE_ID = "TITLE_ID"; +} // namespace SaveParams + +using OrbisUserServiceUserId = s32; + +class SaveInstance { + int slot_num{}; + int user_id{}; + std::string game_serial; + std::string dir_name; + + std::filesystem::path save_path; + std::filesystem::path param_sfo_path; + std::filesystem::path corrupt_file_path; + + Common::FS::IOFile corrupt_file; + + PSF param_sfo; + std::string mount_point; + + int max_blocks{}; + bool exists{}; + bool mounted{}; + bool read_only{}; + +public: + // Location of all save data for a title + static std::filesystem::path MakeTitleSavePath(OrbisUserServiceUserId user_id, + std::string_view game_serial); + + // Location of a specific save data directory + static std::filesystem::path MakeDirSavePath(OrbisUserServiceUserId user_id, + std::string_view game_serial, + std::string_view dir_name); + + static int GetMaxBlocks(const std::filesystem::path& save_path); + + // Get param.sfo path from a dir_path generated by MakeDirSavePath + static std::filesystem::path GetParamSFOPath(const std::filesystem::path& dir_path); + + static void SetupDefaultParamSFO(PSF& param_sfo, std::string dir_name, std::string game_serial); + + explicit SaveInstance(int slot_num, OrbisUserServiceUserId user_id, std::string game_serial, + std::string_view dir_name, int max_blocks = 0); + + ~SaveInstance(); + + SaveInstance(const SaveInstance& other) = delete; + SaveInstance(SaveInstance&& other) noexcept; + + SaveInstance& operator=(const SaveInstance& other) = delete; + SaveInstance& operator=(SaveInstance&& other) noexcept; + + void SetupAndMount(bool read_only = false, bool copy_icon = false, bool ignore_corrupt = false); + + void Umount(); + + [[nodiscard]] std::filesystem::path GetIconPath() const noexcept { + return save_path / "sce_sys" / "icon0.png"; + } + + [[nodiscard]] bool Exists() const noexcept { + return exists; + } + + [[nodiscard]] OrbisUserServiceUserId GetUserId() const noexcept { + return user_id; + } + + [[nodiscard]] std::string_view GetTitleId() const noexcept { + return game_serial; + } + + [[nodiscard]] const std::string& GetDirName() const noexcept { + return dir_name; + } + + [[nodiscard]] const std::filesystem::path& GetSavePath() const noexcept { + return save_path; + } + + [[nodiscard]] const PSF& GetParamSFO() const noexcept { + return param_sfo; + } + + [[nodiscard]] PSF& GetParamSFO() noexcept { + return param_sfo; + } + + [[nodiscard]] const std::string& GetMountPoint() const noexcept { + return mount_point; + } + + [[nodiscard]] int GetMaxBlocks() const noexcept { + return max_blocks; + } + + [[nodiscard]] bool Mounted() const noexcept { + return mounted; + } + + [[nodiscard]] bool IsReadOnly() const noexcept { + return read_only; + } + + void CreateFiles(); +}; + +} // namespace Libraries::SaveData diff --git a/src/core/libraries/save_data/save_memory.cpp b/src/core/libraries/save_data/save_memory.cpp new file mode 100644 index 00000000..6a685ee3 --- /dev/null +++ b/src/core/libraries/save_data/save_memory.cpp @@ -0,0 +1,287 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "save_memory.h" + +#include +#include +#include +#include +#include + +#include + +#include "common/assert.h" +#include "common/logging/log.h" +#include "common/polyfill_thread.h" +#include "common/singleton.h" +#include "common/thread.h" +#include "core/file_sys/fs.h" +#include "save_instance.h" + +using Common::FS::IOFile; +namespace fs = std::filesystem; + +constexpr std::string_view sce_sys = "sce_sys"; // system folder inside save +constexpr std::string_view DirnameSaveDataMemory = "sce_sdmemory"; +constexpr std::string_view FilenameSaveDataMemory = "memory.dat"; + +namespace Libraries::SaveData::SaveMemory { + +static Core::FileSys::MntPoints* g_mnt = Common::Singleton::Instance(); + +static OrbisUserServiceUserId g_user_id{}; +static std::string g_game_serial{}; +static std::filesystem::path g_save_path{}; +static std::filesystem::path g_param_sfo_path{}; +static PSF g_param_sfo; + +static bool g_save_memory_initialized = false; +static std::mutex g_saving_memory_mutex; +static std::vector g_save_memory; + +static std::filesystem::path g_icon_path; +static std::vector g_icon_memory; + +static std::condition_variable g_trigger_save_memory; +static std::atomic_bool g_saving_memory = false; +static std::atomic_bool g_save_event = false; +static std::jthread g_save_memory_thread; + +static std::atomic_bool g_memory_dirty = false; +static std::atomic_bool g_param_dirty = false; +static std::atomic_bool g_icon_dirty = false; + +static void SaveFileSafe(void* buf, size_t count, const std::filesystem::path& path) { + const auto& dir = path.parent_path(); + const auto& name = path.filename(); + const auto tmp_path = dir / (name.string() + ".tmp"); + + IOFile file(tmp_path, Common::FS::FileAccessMode::Write); + file.WriteRaw(buf, count); + file.Close(); + + fs::remove(path); + fs::rename(tmp_path, path); +} + +[[noreturn]] void SaveThreadLoop() { + Common::SetCurrentThreadName("SaveData_SaveDataMemoryThread"); + std::mutex mtx; + while (true) { + { + std::unique_lock lk{mtx}; + g_trigger_save_memory.wait(lk); + } + // Save the memory + g_saving_memory = true; + std::scoped_lock lk{g_saving_memory_mutex}; + try { + LOG_DEBUG(Lib_SaveData, "Saving save data memory {}", g_save_path.string()); + + if (g_memory_dirty) { + g_memory_dirty = false; + SaveFileSafe(g_save_memory.data(), g_save_memory.size(), + g_save_path / FilenameSaveDataMemory); + } + if (g_param_dirty) { + g_param_dirty = false; + static std::vector buf; + g_param_sfo.Encode(buf); + SaveFileSafe(buf.data(), buf.size(), g_param_sfo_path); + } + if (g_icon_dirty) { + g_icon_dirty = false; + SaveFileSafe(g_icon_memory.data(), g_icon_memory.size(), g_icon_path); + } + + if (g_save_event) { + Backup::PushBackupEvent(Backup::BackupRequest{ + .user_id = g_user_id, + .title_id = g_game_serial, + .dir_name = std::string{DirnameSaveDataMemory}, + .origin = Backup::OrbisSaveDataEventType::SAVE_DATA_MEMORY_SYNC, + .save_path = g_save_path, + }); + g_save_event = false; + } + } catch (const fs::filesystem_error& e) { + LOG_ERROR(Lib_SaveData, "Failed to save save data memory: {}", e.what()); + MsgDialog::ShowMsgDialog(MsgDialog::MsgDialogState{ + MsgDialog::MsgDialogState::UserState{ + .type = MsgDialog::ButtonType::OK, + .msg = fmt::format("Failed to save save data memory.\nCode: <{}>\n{}", + e.code().message(), e.what()), + }, + }); + } + g_saving_memory = false; + } +} + +void SetDirectories(OrbisUserServiceUserId user_id, std::string _game_serial) { + g_user_id = user_id; + g_game_serial = std::move(_game_serial); + g_save_path = SaveInstance::MakeDirSavePath(user_id, g_game_serial, DirnameSaveDataMemory); + g_param_sfo_path = SaveInstance::GetParamSFOPath(g_save_path); + g_param_sfo = PSF(); + g_icon_path = g_save_path / sce_sys / "icon0.png"; +} + +const std::filesystem::path& GetSavePath() { + return g_save_path; +} + +size_t CreateSaveMemory(size_t memory_size) { + size_t existed_size = 0; + + static std::once_flag init_save_thread_flag; + std::call_once(init_save_thread_flag, + [] { g_save_memory_thread = std::jthread{SaveThreadLoop}; }); + + g_save_memory.resize(memory_size); + SaveInstance::SetupDefaultParamSFO(g_param_sfo, std::string{DirnameSaveDataMemory}, + g_game_serial); + + g_save_memory_initialized = true; + + if (!fs::exists(g_param_sfo_path)) { + // Create save memory + fs::create_directories(g_save_path / sce_sys); + + IOFile memory_file{g_save_path / FilenameSaveDataMemory, Common::FS::FileAccessMode::Write}; + bool ok = memory_file.SetSize(memory_size); + if (!ok) { + LOG_ERROR(Lib_SaveData, "Failed to set memory size"); + throw std::filesystem::filesystem_error( + "Failed to set save memory size", g_save_path / FilenameSaveDataMemory, + std::make_error_code(std::errc::no_space_on_device)); + } + memory_file.Close(); + } else { + // Load save memory + + bool ok = g_param_sfo.Open(g_param_sfo_path); + if (!ok) { + LOG_ERROR(Lib_SaveData, "Failed to open SFO at {}", g_param_sfo_path.string()); + throw std::filesystem::filesystem_error( + "failed to open SFO", g_param_sfo_path, + std::make_error_code(std::errc::illegal_byte_sequence)); + } + + IOFile memory_file{g_save_path / FilenameSaveDataMemory, Common::FS::FileAccessMode::Read}; + if (!memory_file.IsOpen()) { + LOG_ERROR(Lib_SaveData, "Failed to open save memory"); + throw std::filesystem::filesystem_error( + "failed to open save memory", g_save_path / FilenameSaveDataMemory, + std::make_error_code(std::errc::permission_denied)); + } + size_t save_size = memory_file.GetSize(); + existed_size = save_size; + memory_file.Seek(0); + memory_file.ReadRaw(g_save_memory.data(), std::min(save_size, memory_size)); + memory_file.Close(); + } + + return existed_size; +} + +void SetIcon(void* buf, size_t buf_size) { + if (buf == nullptr) { + const auto& src_icon = g_mnt->GetHostPath("/app0/sce_sys/save_data.png"); + if (fs::exists(src_icon)) { + fs::copy_file(src_icon, g_icon_path); + } + IOFile file(g_icon_path, Common::FS::FileAccessMode::Read); + size_t size = file.GetSize(); + file.Seek(0); + g_icon_memory.resize(size); + file.ReadRaw(g_icon_memory.data(), size); + file.Close(); + } else { + g_icon_memory.resize(buf_size); + std::memcpy(g_icon_memory.data(), buf, buf_size); + IOFile file(g_icon_path, Common::FS::FileAccessMode::Append); + file.Seek(0); + file.WriteRaw(g_icon_memory.data(), buf_size); + file.Close(); + } +} + +void WriteIcon(void* buf, size_t buf_size) { + if (buf_size != g_icon_memory.size()) { + g_icon_memory.resize(buf_size); + } + std::memcpy(g_icon_memory.data(), buf, buf_size); + g_icon_dirty = true; +} + +bool IsSaveMemoryInitialized() { + return g_save_memory_initialized; +} + +PSF& GetParamSFO() { + return g_param_sfo; +} + +std::span GetIcon() { + return {g_icon_memory}; +} + +void SaveSFO(bool sync) { + if (!sync) { + g_param_dirty = true; + return; + } + const bool ok = g_param_sfo.Encode(g_param_sfo_path); + if (!ok) { + LOG_ERROR(Lib_SaveData, "Failed to encode param.sfo"); + throw std::filesystem::filesystem_error("Failed to write param.sfo", g_param_sfo_path, + std::make_error_code(std::errc::permission_denied)); + } +} +bool IsSaving() { + return g_saving_memory; +} + +bool TriggerSaveWithoutEvent() { + if (g_saving_memory) { + return false; + } + g_trigger_save_memory.notify_one(); + return true; +} + +bool TriggerSave() { + if (g_saving_memory) { + return false; + } + g_save_event = true; + g_trigger_save_memory.notify_one(); + return true; +} + +void ReadMemory(void* buf, size_t buf_size, int64_t offset) { + std::scoped_lock lk{g_saving_memory_mutex}; + if (offset > g_save_memory.size()) { + UNREACHABLE_MSG("ReadMemory out of bounds"); + } + if (offset + buf_size > g_save_memory.size()) { + UNREACHABLE_MSG("ReadMemory out of bounds"); + } + std::memcpy(buf, g_save_memory.data() + offset, buf_size); +} + +void WriteMemory(void* buf, size_t buf_size, int64_t offset) { + std::scoped_lock lk{g_saving_memory_mutex}; + if (offset > g_save_memory.size()) { + UNREACHABLE_MSG("WriteMemory out of bounds"); + } + if (offset + buf_size > g_save_memory.size()) { + UNREACHABLE_MSG("WriteMemory out of bounds"); + } + std::memcpy(g_save_memory.data() + offset, buf, buf_size); + g_memory_dirty = true; +} + +} // namespace Libraries::SaveData::SaveMemory \ No newline at end of file diff --git a/src/core/libraries/save_data/save_memory.h b/src/core/libraries/save_data/save_memory.h new file mode 100644 index 00000000..04eeaa65 --- /dev/null +++ b/src/core/libraries/save_data/save_memory.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "save_backup.h" + +class PSF; + +namespace Libraries::SaveData { +using OrbisUserServiceUserId = s32; +} + +namespace Libraries::SaveData::SaveMemory { + +void SetDirectories(OrbisUserServiceUserId user_id, std::string game_serial); + +[[nodiscard]] const std::filesystem::path& GetSavePath(); + +// returns the size of the existed save memory +size_t CreateSaveMemory(size_t memory_size); + +// Initialize the icon. Set buf to null to read the standard icon. +void SetIcon(void* buf, size_t buf_size); + +// Update the icon +void WriteIcon(void* buf, size_t buf_size); + +[[nodiscard]] bool IsSaveMemoryInitialized(); + +[[nodiscard]] PSF& GetParamSFO(); + +[[nodiscard]] std::span GetIcon(); + +// Save now or wait for the background thread to save +void SaveSFO(bool sync = false); + +[[nodiscard]] bool IsSaving(); + +bool TriggerSaveWithoutEvent(); + +bool TriggerSave(); + +void ReadMemory(void* buf, size_t buf_size, int64_t offset); + +void WriteMemory(void* buf, size_t buf_size, int64_t offset); + +} // namespace Libraries::SaveData::SaveMemory \ No newline at end of file diff --git a/src/core/libraries/save_data/savedata.cpp b/src/core/libraries/save_data/savedata.cpp index 10a89f66..839ec335 100644 --- a/src/core/libraries/save_data/savedata.cpp +++ b/src/core/libraries/save_data/savedata.cpp @@ -4,30 +4,528 @@ #include #include +#include +#include + #include "common/assert.h" +#include "common/cstring.h" +#include "common/enum.h" #include "common/logging/log.h" #include "common/path_util.h" #include "common/singleton.h" +#include "common/string_util.h" #include "core/file_format/psf.h" #include "core/file_sys/fs.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/save_data/savedata.h" -#include "error_codes.h" +#include "core/libraries/system/msgdialog.h" +#include "save_backup.h" +#include "save_instance.h" +#include "save_memory.h" + +namespace fs = std::filesystem; +namespace chrono = std::chrono; + +using Common::CString; namespace Libraries::SaveData { -bool is_rw_mode = false; -static constexpr std::string_view g_mount_point = "/savedata0"; // temp mount point (todo) -std::string game_serial; + +enum class Error : u32 { + OK = 0, + PARAMETER = 0x809F0000, + NOT_INITIALIZED = 0x809F0001, + OUT_OF_MEMORY = 0x809F0002, + BUSY = 0x809F0003, + NOT_MOUNTED = 0x809F0004, + EXISTS = 0x809F0007, + NOT_FOUND = 0x809F0008, + NO_SPACE_FS = 0x809F000A, + INTERNAL = 0x809F000B, + MOUNT_FULL = 0x809F000C, + BAD_MOUNTED = 0x809F000D, + BROKEN = 0x809F000F, + INVALID_LOGIN_USER = 0x809F0011, + MEMORY_NOT_READY = 0x809F0012, + BACKUP_BUSY = 0x809F0013, + BUSY_FOR_SAVING = 0x809F0016, +}; + +enum class OrbisSaveDataSaveDataMemoryOption : u32 { + NONE = 0, + SET_PARAM = 1 << 0, + DOUBLE_BUFFER = 1 << 1, + UNLOCK_LIMITATIONS = 1 << 2, +}; + +using OrbisUserServiceUserId = s32; +using OrbisSaveDataBlocks = u64; + +constexpr u32 OrbisSaveDataBlockSize = 32768; // 32 KiB +constexpr u32 OrbisSaveDataBlocksMin2 = 96; // 3MiB +constexpr u32 OrbisSaveDataBlocksMax = 32768; // 1 GiB + +// Maximum size for a mount point "/savedataN", where N is a number +constexpr size_t OrbisSaveDataMountPointDataMaxsize = 16; + +constexpr size_t OrbisSaveDataFingerprintDataSize = 65; // Maximum fingerprint size + +enum class OrbisSaveDataMountMode : u32 { + RDONLY = 1 << 0, + RDWR = 1 << 1, + CREATE = 1 << 2, + DESTRUCT_OFF = 1 << 3, + COPY_ICON = 1 << 4, + CREATE2 = 1 << 5, +}; +DECLARE_ENUM_FLAG_OPERATORS(OrbisSaveDataMountMode); + +enum class OrbisSaveDataMountStatus : u32 { + NOTHING = 0, + CREATED = 1, +}; + +enum class OrbisSaveDataParamType : u32 { + ALL = 0, + TITLE = 1, + SUB_TITLE = 2, + DETAIL = 3, + USER_PARAM = 4, + MTIME = 5, +}; + +enum class OrbisSaveDataSortKey : u32 { + DIRNAME = 0, + USER_PARAM = 1, + BLOCKS = 2, + MTIME = 3, + FREE_BLOCKS = 5, +}; + +enum class OrbisSaveDataSortOrder : u32 { + ASCENT = 0, + DESCENT = 1, +}; + +struct OrbisSaveDataFingerprint { + CString data; + std::array _pad; +}; + +struct OrbisSaveDataBackup { + OrbisUserServiceUserId userId; + s32 : 32; + const OrbisSaveDataTitleId* titleId; + const OrbisSaveDataDirName* dirName; + const OrbisSaveDataFingerprint* param; + std::array _reserved; +}; + +struct OrbisSaveDataCheckBackupData { + OrbisUserServiceUserId userId; + s32 : 32; + const OrbisSaveDataTitleId* titleId; + const OrbisSaveDataDirName* dirName; + OrbisSaveDataParam* param; + OrbisSaveDataIcon* icon; + std::array _reserved; +}; + +struct OrbisSaveDataDelete { + OrbisUserServiceUserId userId; + s32 : 32; + const OrbisSaveDataTitleId* titleId; + const OrbisSaveDataDirName* dirName; + u32 _unused; + std::array _reserved; + s32 : 32; +}; + +struct OrbisSaveDataIcon { + void* buf; + size_t bufSize; + size_t dataSize; + std::array _reserved; + + Error LoadIcon(const std::filesystem::path& icon_path) { + try { + const Common::FS::IOFile file(icon_path, Common::FS::FileAccessMode::Read); + dataSize = file.GetSize(); + file.Seek(0); + file.ReadRaw(buf, std::min(bufSize, dataSize)); + } catch (const fs::filesystem_error& e) { + LOG_ERROR(Lib_SaveData, "Failed to load icon: {}", e.what()); + return Error::INTERNAL; + } + return Error::OK; + } +}; + +struct OrbisSaveDataMemoryData { + void* buf; + size_t bufSize; + s64 offset; + u8 _reserved[40]; +}; + +struct OrbisSaveDataMemoryGet2 { + OrbisUserServiceUserId userId; + std::array _pad; + OrbisSaveDataMemoryData* data; + OrbisSaveDataParam* param; + OrbisSaveDataIcon* icon; + std::array _reserved; +}; + +struct OrbisSaveDataMemorySet2 { + OrbisUserServiceUserId userId; + std::array _pad; + const OrbisSaveDataMemoryData* data; + const OrbisSaveDataParam* param; + const OrbisSaveDataIcon* icon; + std::array _reserved; +}; + +struct OrbisSaveDataMemorySetup2 { + OrbisSaveDataSaveDataMemoryOption option; + OrbisUserServiceUserId userId; + size_t memorySize; + size_t iconMemorySize; + const OrbisSaveDataParam* initParam; + const OrbisSaveDataIcon* initIcon; + std::array _reserved; +}; + +struct OrbisSaveDataMemorySetupResult { + size_t existedMemorySize; + std::array _reserved; +}; + +struct OrbisSaveDataMemorySync { + OrbisUserServiceUserId userId; + std::array _reserved; +}; + +struct OrbisSaveDataMount2 { + OrbisUserServiceUserId userId; + s32 : 32; + const OrbisSaveDataDirName* dirName; + OrbisSaveDataBlocks blocks; + OrbisSaveDataMountMode mountMode; + std::array _reserved; + s32 : 32; +}; + +struct OrbisSaveDataMount { + OrbisUserServiceUserId userId; + s32 : 32; + const OrbisSaveDataTitleId* titleId; + const OrbisSaveDataDirName* dirName; + const OrbisSaveDataFingerprint* fingerprint; + OrbisSaveDataBlocks blocks; + OrbisSaveDataMountMode mountMode; + std::array _reserved; +}; + +struct OrbisSaveDataMountInfo { + OrbisSaveDataBlocks blocks; + OrbisSaveDataBlocks freeBlocks; + std::array _reserved; +}; + +struct OrbisSaveDataMountPoint { + CString data; +}; + +struct OrbisSaveDataMountResult { + OrbisSaveDataMountPoint mount_point; + OrbisSaveDataBlocks required_blocks; + u32 _unused; + OrbisSaveDataMountStatus mount_status; + std::array _reserved; + s32 : 32; +}; + +struct OrbisSaveDataRestoreBackupData { + OrbisUserServiceUserId userId; + s32 : 32; + const OrbisSaveDataTitleId* titleId; + const OrbisSaveDataDirName* dirName; + const OrbisSaveDataFingerprint* fingerprint; + u32 _unused; + std::array _reserved; + s32 : 32; +}; + +struct OrbisSaveDataDirNameSearchCond { + OrbisUserServiceUserId userId; + int : 32; + const OrbisSaveDataTitleId* titleId; + const OrbisSaveDataDirName* dirName; + OrbisSaveDataSortKey key; + OrbisSaveDataSortOrder order; + std::array _reserved; +}; + +struct OrbisSaveDataSearchInfo { + u64 blocks; + u64 freeBlocks; + std::array _reserved; +}; + +struct OrbisSaveDataDirNameSearchResult { + u32 hitNum; + int : 32; + OrbisSaveDataDirName* dirNames; + u32 dirNamesNum; + u32 setNum; + OrbisSaveDataParam* params; + OrbisSaveDataSearchInfo* infos; + std::array _reserved; + int : 32; +}; + +struct OrbisSaveDataEventParam { // dummy structure + OrbisSaveDataEventParam() = delete; +}; + +using OrbisSaveDataEventType = Backup::OrbisSaveDataEventType; + +struct OrbisSaveDataEvent { + OrbisSaveDataEventType type; + s32 errorCode; + OrbisUserServiceUserId userId; + std::array _pad; + OrbisSaveDataTitleId titleId; + OrbisSaveDataDirName dirName; + std::array _reserved; +}; + +static bool g_initialized = false; +static std::string g_game_serial; +static std::array, 16> g_mount_slots; + +static void initialize() { + g_initialized = true; + static auto* param_sfo = Common::Singleton::Instance(); + g_game_serial = std::string(*param_sfo->GetString("CONTENT_ID"), 7, 9); +} + +// game_00other | game*other + +static bool match(std::string_view str, std::string_view pattern) { + auto str_it = str.begin(); + auto pat_it = pattern.begin(); + while (str_it != str.end() && pat_it != pattern.end()) { + if (*pat_it == '%') { // 0 or more wildcard + for (auto str_wild_it = str_it; str_wild_it <= str.end(); ++str_wild_it) { + if (match({str_wild_it, str.end()}, {pat_it + 1, pattern.end()})) { + return true; + } + } + return false; + } + if (*pat_it == '_') { // 1 character wildcard + ++str_it; + ++pat_it; + } else if (*pat_it != *str_it) { + return false; + } + ++str_it; + ++pat_it; + } + return str_it == str.end() && pat_it == pattern.end(); +} + +static Error saveDataMount(const OrbisSaveDataMount2* mount_info, + OrbisSaveDataMountResult* mount_result) { + + if (mount_info->userId < 0) { + return Error::INVALID_LOGIN_USER; + } + if (mount_info->dirName == nullptr) { + LOG_INFO(Lib_SaveData, "called without dirName"); + return Error::PARAMETER; + } + + // check backup status + { + const auto save_path = SaveInstance::MakeDirSavePath(mount_info->userId, g_game_serial, + mount_info->dirName->data); + if (Backup::IsBackupExecutingFor(save_path)) { + return Error::BACKUP_BUSY; + } + } + + auto mount_mode = mount_info->mountMode; + const bool is_ro = True(mount_mode & OrbisSaveDataMountMode::RDONLY); + + const bool create = True(mount_mode & OrbisSaveDataMountMode::CREATE); + const bool create_if_not_exist = True(mount_mode & OrbisSaveDataMountMode::CREATE2); + ASSERT(!create || !create_if_not_exist); // Can't have both + + const bool copy_icon = True(mount_mode & OrbisSaveDataMountMode::COPY_ICON); + const bool ignore_corrupt = True(mount_mode & OrbisSaveDataMountMode::DESTRUCT_OFF); + + const std::string_view dir_name{mount_info->dirName->data}; + + // find available mount point + int slot_num = -1; + for (size_t i = 0; i < g_mount_slots.size(); i++) { + const auto& instance = g_mount_slots[i]; + if (instance.has_value()) { + const auto& slot_name = instance->GetDirName(); + if (slot_name == dir_name) { + return Error::BUSY; + } + } else { + slot_num = static_cast(i); + break; + } + } + if (slot_num == -1) { + return Error::MOUNT_FULL; + } + + SaveInstance save_instance{slot_num, mount_info->userId, g_game_serial, dir_name, + (int)mount_info->blocks}; + + if (save_instance.Mounted()) { + UNREACHABLE_MSG("Save instance should not be mounted"); + } + + if (!create && !create_if_not_exist && !save_instance.Exists()) { + return Error::NOT_FOUND; + } + if (create && save_instance.Exists()) { + return Error::EXISTS; + } + + bool to_be_created = !save_instance.Exists(); + + if (to_be_created) { // Check size + + if (mount_info->blocks < OrbisSaveDataBlocksMin2 || + mount_info->blocks > OrbisSaveDataBlocksMax) { + LOG_INFO(Lib_SaveData, "called with invalid block size"); + } + + const auto root_save = Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir); + fs::create_directories(root_save); + const auto available = fs::space(root_save).available; + + auto requested_size = mount_info->blocks * OrbisSaveDataBlockSize; + if (requested_size > available) { + mount_result->required_blocks = (requested_size - available) / OrbisSaveDataBlockSize; + return Error::NO_SPACE_FS; + } + } + + try { + save_instance.SetupAndMount(is_ro, copy_icon, ignore_corrupt); + } catch (const fs::filesystem_error& e) { + if (e.code() == std::errc::illegal_byte_sequence) { + LOG_ERROR(Lib_SaveData, "Corrupted save data"); + return Error::BROKEN; + } + if (e.code() == std::errc::no_space_on_device) { + return Error::NO_SPACE_FS; + } + LOG_ERROR(Lib_SaveData, "Failed to mount save data: {}", e.what()); + return Error::INTERNAL; + } + + mount_result->mount_point.data.FromString(save_instance.GetMountPoint()); + + mount_result->mount_status = create_if_not_exist && to_be_created + ? OrbisSaveDataMountStatus::CREATED + : OrbisSaveDataMountStatus::NOTHING; + + g_mount_slots[slot_num].emplace(std::move(save_instance)); + + return Error::OK; +} + +static Error Umount(const OrbisSaveDataMountPoint* mountPoint, bool call_backup = false) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (mountPoint == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_DEBUG(Lib_SaveData, "Umount mountPoint:{}", mountPoint->data.to_view()); + const std::string_view mount_point_str{mountPoint->data}; + for (auto& instance : g_mount_slots) { + if (instance.has_value()) { + const auto& slot_name = instance->GetMountPoint(); + if (slot_name == mount_point_str) { + if (call_backup) { + Backup::StartThread(); + Backup::NewRequest(instance->GetUserId(), instance->GetTitleId(), + instance->GetDirName(), + OrbisSaveDataEventType::UMOUNT_BACKUP); + } + // TODO: check if is busy + instance->Umount(); + instance.reset(); + return Error::OK; + } + } + } + return Error::NOT_FOUND; +} + +void OrbisSaveDataParam::FromSFO(const PSF& sfo) { + memset(this, 0, sizeof(OrbisSaveDataParam)); + title.FromString(*sfo.GetString(SaveParams::MAINTITLE)); + subTitle.FromString(*sfo.GetString(SaveParams::SUBTITLE)); + detail.FromString(*sfo.GetString(SaveParams::DETAIL)); + userParam = sfo.GetInteger(SaveParams::SAVEDATA_LIST_PARAM).value_or(0); + const auto time_since_epoch = sfo.GetLastWrite().time_since_epoch(); + mtime = chrono::duration_cast(time_since_epoch).count(); +} + +void OrbisSaveDataParam::ToSFO(PSF& sfo) const { + sfo.AddString(std::string{SaveParams::MAINTITLE}, std::string{title}, true); + sfo.AddString(std::string{SaveParams::SUBTITLE}, std::string{subTitle}, true); + sfo.AddString(std::string{SaveParams::DETAIL}, std::string{detail}, true); + sfo.AddInteger(std::string{SaveParams::SAVEDATA_LIST_PARAM}, static_cast(userParam), true); +} int PS4_SYSV_ABI sceSaveDataAbort() { LOG_ERROR(Lib_SaveData, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataBackup() { - LOG_ERROR(Lib_SaveData, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataBackup(const OrbisSaveDataBackup* backup) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (backup == nullptr || backup->dirName == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + + const std::string_view dir_name{backup->dirName->data}; + LOG_DEBUG(Lib_SaveData, "called dirName: {}", dir_name); + + std::string_view title{backup->titleId != nullptr ? std::string_view{backup->titleId->data} + : std::string_view{g_game_serial}}; + + for (const auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetUserId() == backup->userId && + instance->GetTitleId() == title && instance->GetDirName() == dir_name) { + return Error::BUSY; + } + } + + Backup::StartThread(); + Backup::NewRequest(backup->userId, title, dir_name, OrbisSaveDataEventType::BACKUP); + + return Error::OK; } int PS4_SYSV_ABI sceSaveDataBindPsnAccount() { @@ -50,15 +548,54 @@ int PS4_SYSV_ABI sceSaveDataChangeInternal() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataCheckBackupData(const OrbisSaveDataCheckBackupData* check) { - auto* mnt = Common::Singleton::Instance(); - const auto mount_dir = mnt->GetHostPath(check->dirName->data); - if (!std::filesystem::exists(mount_dir)) { - return ORBIS_SAVE_DATA_ERROR_NOT_FOUND; +Error PS4_SYSV_ABI sceSaveDataCheckBackupData(const OrbisSaveDataCheckBackupData* check) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (check == nullptr || check->dirName == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; } - LOG_INFO(Lib_SaveData, "called = {}", mount_dir.string()); - return ORBIS_OK; + const std::string_view title{check->titleId != nullptr ? std::string_view{check->titleId->data} + : std::string_view{g_game_serial}}; + + const auto save_path = + SaveInstance::MakeDirSavePath(check->userId, title, check->dirName->data); + + for (const auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetSavePath() == save_path) { + return Error::BUSY; + } + } + + if (Backup::IsBackupExecutingFor(save_path)) { + return Error::BACKUP_BUSY; + } + + const auto backup_path = Backup::MakeBackupPath(save_path); + if (!fs::exists(backup_path)) { + return Error::NOT_FOUND; + } + + if (check->param != nullptr) { + PSF sfo; + if (!sfo.Open(backup_path / "sce_sys" / "param.sfo")) { + LOG_ERROR(Lib_SaveData, "Failed to read SFO at {}", backup_path.string()); + return Error::INTERNAL; + } + check->param->FromSFO(sfo); + } + + if (check->icon != nullptr) { + const auto icon_path = backup_path / "sce_sys" / "icon0.png"; + if (fs::exists(icon_path) && check->icon->LoadIcon(icon_path) != Error::OK) { + return Error::INTERNAL; + } + } + + return Error::OK; } int PS4_SYSV_ABI sceSaveDataCheckBackupDataForCdlg() { @@ -96,9 +633,14 @@ int PS4_SYSV_ABI sceSaveDataCheckSaveDataVersionLatest() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataClearProgress() { - LOG_ERROR(Lib_SaveData, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataClearProgress() { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + LOG_DEBUG(Lib_SaveData, "called"); + Backup::ClearProgress(); + return Error::OK; } int PS4_SYSV_ABI sceSaveDataCopy5() { @@ -146,15 +688,35 @@ int PS4_SYSV_ABI sceSaveDataDebugTarget() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataDelete(const OrbisSaveDataDelete* del) { - const auto& mount_dir = Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir) / - std::to_string(1) / game_serial / std::string(del->dirName->data); - LOG_INFO(Lib_SaveData, "called: dirname = {}, mount_dir = {}", (char*)del->dirName->data, - mount_dir.string()); - if (std::filesystem::exists(mount_dir) && std::filesystem::is_directory(mount_dir)) { - std::filesystem::remove_all(mount_dir); +Error PS4_SYSV_ABI sceSaveDataDelete(const OrbisSaveDataDelete* del) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; } - return ORBIS_OK; + if (del == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + const std::string_view dirName{del->dirName->data}; + LOG_DEBUG(Lib_SaveData, "called dirName: {}", dirName); + if (dirName.empty()) { + return Error::PARAMETER; + } + for (const auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetDirName() == dirName) { + return Error::BUSY; + } + } + const auto save_path = SaveInstance::MakeDirSavePath(del->userId, g_game_serial, dirName); + try { + if (fs::exists(save_path)) { + fs::remove_all(save_path); + } + } catch (const fs::filesystem_error& e) { + LOG_ERROR(Lib_SaveData, "Failed to delete save data: {}", e.what()); + return Error::INTERNAL; + } + return Error::OK; } int PS4_SYSV_ABI sceSaveDataDelete5() { @@ -177,89 +739,115 @@ int PS4_SYSV_ABI sceSaveDataDeleteUser() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataDirNameSearch(const OrbisSaveDataDirNameSearchCond* cond, - OrbisSaveDataDirNameSearchResult* result) { - if (cond == nullptr || result == nullptr) - return ORBIS_SAVE_DATA_ERROR_PARAMETER; - LOG_INFO(Lib_SaveData, "Number of directories = {}", result->dirNamesNum); - const auto& mount_dir = Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir) / - std::to_string(cond->userId) / game_serial; - if (!mount_dir.empty() && std::filesystem::exists(mount_dir)) { - int maxDirNum = result->dirNamesNum; // Games set a maximum of directories to search for - int i = 0; +Error PS4_SYSV_ABI sceSaveDataDirNameSearch(const OrbisSaveDataDirNameSearchCond* cond, + OrbisSaveDataDirNameSearchResult* result) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (cond == nullptr || result == nullptr || cond->key > OrbisSaveDataSortKey::FREE_BLOCKS || + cond->order > OrbisSaveDataSortOrder::DESCENT) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_DEBUG(Lib_SaveData, "called"); + const std::string_view title_id{cond->titleId == nullptr + ? std::string_view{g_game_serial} + : std::string_view{cond->titleId->data}}; + const auto save_path = SaveInstance::MakeTitleSavePath(cond->userId, title_id); - if (cond->dirName == nullptr || std::string_view(cond->dirName->data).empty()) { - // Look for all dirs if no dir is provided. - for (const auto& entry : std::filesystem::directory_iterator(mount_dir)) { - if (i >= maxDirNum) - break; + if (!fs::exists(save_path)) { + result->hitNum = 0; + result->setNum = 0; + return Error::OK; + } - if (std::filesystem::is_directory(entry.path()) && - entry.path().filename().string() != "sdmemory") { - // sceSaveDataDirNameSearch does not search for dataMemory1/2 dirs. - // copy dir name to be used by sceSaveDataMount in read mode. - strncpy(result->dirNames[i].data, entry.path().filename().string().c_str(), 32); - i++; - result->hitNum = i; - result->dirNamesNum = i; - result->setNum = i; - } - } - } else { - // Game checks for a specific directory. - LOG_INFO(Lib_SaveData, "dirName = {}", cond->dirName->data); + std::vector dir_list; - // Games can pass '%' as a wildcard - // e.g. `SAVELIST%` searches for all folders with names starting with `SAVELIST` - std::string baseName(cond->dirName->data); - u64 wildcardPos = baseName.find('%'); - if (wildcardPos != std::string::npos) { - baseName = baseName.substr(0, wildcardPos); - } - - for (const auto& entry : std::filesystem::directory_iterator(mount_dir)) { - if (i >= maxDirNum) - break; - - if (std::filesystem::is_directory(entry.path())) { - std::string dirName = entry.path().filename().string(); - - if (wildcardPos != std::string::npos) { - if (dirName.compare(0, baseName.size(), baseName) != 0) { - continue; - } - } else if (wildcardPos == std::string::npos && dirName != cond->dirName->data) { - continue; - } - - strncpy(result->dirNames[i].data, cond->dirName->data, 32); - - i++; - result->hitNum = i; - result->dirNamesNum = i; - result->setNum = i; - } - } + for (const auto& path : fs::directory_iterator{save_path}) { + auto dir_name = path.path().filename().string(); + // skip non-directories, sce_* and directories without param.sfo + if (fs::is_directory(path) && !dir_name.starts_with("sce_") && + fs::exists(SaveInstance::GetParamSFOPath(path))) { + dir_list.push_back(dir_name); } + } + if (cond->dirName != nullptr) { + // Filter names + const auto pat = Common::ToLower(std::string_view{cond->dirName->data}); + std::erase_if(dir_list, [&](const std::string& dir_name) { + return !match(Common::ToLower(dir_name), pat); + }); + } + + std::unordered_map map_dir_sfo; + std::unordered_map map_max_blocks; + std::unordered_map map_free_size; + + for (const auto& dir_name : dir_list) { + const auto dir_path = SaveInstance::MakeDirSavePath(cond->userId, title_id, dir_name); + const auto sfo_path = SaveInstance::GetParamSFOPath(dir_path); + PSF sfo; + if (!sfo.Open(sfo_path)) { + LOG_ERROR(Lib_SaveData, "Failed to read SFO: {}", sfo_path.string()); + ASSERT_MSG(false, "Failed to read SFO"); + } + map_dir_sfo.emplace(dir_name, std::move(sfo)); + + size_t size = Common::FS::GetDirectorySize(dir_path); + size_t total = SaveInstance::GetMaxBlocks(dir_path); + map_free_size.emplace(dir_name, total - size / OrbisSaveDataBlockSize); + map_max_blocks.emplace(dir_name, total); + } + +#define PROJ(x) [&](const auto& v) { return x; } + switch (cond->key) { + case OrbisSaveDataSortKey::DIRNAME: + std::ranges::stable_sort(dir_list); + break; + case OrbisSaveDataSortKey::USER_PARAM: + std::ranges::stable_sort( + dir_list, {}, + PROJ(map_dir_sfo.at(v).GetInteger(SaveParams::SAVEDATA_LIST_PARAM).value_or(0))); + break; + case OrbisSaveDataSortKey::BLOCKS: + std::ranges::stable_sort(dir_list, {}, PROJ(map_max_blocks.at(v))); + break; + case OrbisSaveDataSortKey::FREE_BLOCKS: + std::ranges::stable_sort(dir_list, {}, PROJ(map_free_size.at(v))); + break; + case OrbisSaveDataSortKey::MTIME: + std::ranges::stable_sort(dir_list, {}, PROJ(map_dir_sfo.at(v).GetLastWrite())); + break; + } +#undef PROJ + + if (cond->order == OrbisSaveDataSortOrder::DESCENT) { + std::ranges::reverse(dir_list); + } + + result->hitNum = dir_list.size(); + size_t max_count = std::min(static_cast(result->dirNamesNum), dir_list.size()); + result->setNum = max_count; + + for (size_t i = 0; i < max_count; i++) { + auto& name_data = result->dirNames[i].data; + name_data.FromString(dir_list[i]); if (result->params != nullptr) { - Common::FS::IOFile file(mount_dir / cond->dirName->data / "param.txt", - Common::FS::FileAccessMode::Read); - if (file.IsOpen()) { - file.ReadRaw((void*)result->params, sizeof(OrbisSaveDataParam)); - file.Close(); - } + auto& sfo = map_dir_sfo.at(dir_list[i]); + auto& param_data = result->params[i]; + param_data.FromSFO(sfo); + } + + if (result->infos != nullptr) { + auto& info = result->infos[i]; + info.blocks = map_max_blocks.at(dir_list[i]); + info.freeBlocks = map_free_size.at(dir_list[i]); } - } else { - result->hitNum = 0; - result->dirNamesNum = 0; - result->setNum = 0; } - if (result->infos != nullptr) { - result->infos->blocks = ORBIS_SAVE_DATA_BLOCK_SIZE; - result->infos->freeBlocks = ORBIS_SAVE_DATA_BLOCK_SIZE; - } - return ORBIS_OK; + + return Error::OK; } int PS4_SYSV_ABI sceSaveDataDirNameSearchInternal() { @@ -322,15 +910,30 @@ int PS4_SYSV_ABI sceSaveDataGetEventInfo() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataGetEventResult(const OrbisSaveDataEventParam* eventParam, - OrbisSaveDataEvent* event) { - // eventParam can be 0/null. - if (event == nullptr) - return ORBIS_SAVE_DATA_ERROR_NOT_INITIALIZED; +Error PS4_SYSV_ABI sceSaveDataGetEventResult(const OrbisSaveDataEventParam*, + OrbisSaveDataEvent* event) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (event == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_TRACE(Lib_SaveData, "called"); - LOG_INFO(Lib_SaveData, "called: Todo."); - event->userId = 1; - return ORBIS_OK; + auto last_event = Backup::PopLastEvent(); + if (!last_event.has_value()) { + return Error::NOT_FOUND; + } + + event->type = last_event->origin; + event->errorCode = 0; + event->userId = last_event->user_id; + event->titleId.data.FromString(last_event->title_id); + event->dirName.data.FromString(last_event->dir_name); + + return Error::OK; } int PS4_SYSV_ABI sceSaveDataGetFormat() { @@ -343,65 +946,119 @@ int PS4_SYSV_ABI sceSaveDataGetMountedSaveDataCount() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataGetMountInfo(const OrbisSaveDataMountPoint* mountPoint, - OrbisSaveDataMountInfo* info) { - LOG_INFO(Lib_SaveData, "called"); - info->blocks = ORBIS_SAVE_DATA_BLOCKS_MAX; - info->freeBlocks = ORBIS_SAVE_DATA_BLOCKS_MAX; - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataGetMountInfo(const OrbisSaveDataMountPoint* mountPoint, + OrbisSaveDataMountInfo* info) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (mountPoint == nullptr || info == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_DEBUG(Lib_SaveData, "called"); + const std::string_view mount_point_str{mountPoint->data}; + for (const auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetMountPoint() == mount_point_str) { + const u32 blocks = instance->GetMaxBlocks(); + const u64 size = Common::FS::GetDirectorySize(instance->GetSavePath()); + info->blocks = blocks; + info->freeBlocks = blocks - size / OrbisSaveDataBlockSize; + return Error::OK; + } + } + return Error::NOT_MOUNTED; } -int PS4_SYSV_ABI sceSaveDataGetParam(const OrbisSaveDataMountPoint* mountPoint, - const OrbisSaveDataParamType paramType, void* paramBuf, - const size_t paramBufSize, size_t* gotSize) { +Error PS4_SYSV_ABI sceSaveDataGetParam(const OrbisSaveDataMountPoint* mountPoint, + OrbisSaveDataParamType paramType, void* paramBuf, + size_t paramBufSize, size_t* gotSize) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (paramType > OrbisSaveDataParamType::MTIME || paramBuf == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_DEBUG(Lib_SaveData, "called: paramType = {}", magic_enum::enum_name(paramType)); + const PSF* param_sfo = nullptr; - if (mountPoint == nullptr) - return ORBIS_SAVE_DATA_ERROR_PARAMETER; - - auto* mnt = Common::Singleton::Instance(); - const auto mount_dir = mnt->GetHostPath(mountPoint->data); - Common::FS::IOFile file(mount_dir / "param.txt", Common::FS::FileAccessMode::Read); - OrbisSaveDataParam params; - file.Read(params); - - LOG_INFO(Lib_SaveData, "called"); - - switch (paramType) { - case ORBIS_SAVE_DATA_PARAM_TYPE_ALL: { - memcpy(paramBuf, ¶ms, sizeof(OrbisSaveDataParam)); - *gotSize = sizeof(OrbisSaveDataParam); - } break; - case ORBIS_SAVE_DATA_PARAM_TYPE_TITLE: { - std::memcpy(paramBuf, ¶ms.title, ORBIS_SAVE_DATA_TITLE_MAXSIZE); - *gotSize = ORBIS_SAVE_DATA_TITLE_MAXSIZE; - } break; - case ORBIS_SAVE_DATA_PARAM_TYPE_SUB_TITLE: { - std::memcpy(paramBuf, ¶ms.subTitle, ORBIS_SAVE_DATA_SUBTITLE_MAXSIZE); - *gotSize = ORBIS_SAVE_DATA_SUBTITLE_MAXSIZE; - } break; - case ORBIS_SAVE_DATA_PARAM_TYPE_DETAIL: { - std::memcpy(paramBuf, ¶ms.detail, ORBIS_SAVE_DATA_DETAIL_MAXSIZE); - *gotSize = ORBIS_SAVE_DATA_DETAIL_MAXSIZE; - } break; - case ORBIS_SAVE_DATA_PARAM_TYPE_USER_PARAM: { - std::memcpy(paramBuf, ¶ms.userParam, sizeof(u32)); - *gotSize = sizeof(u32); - } break; - case ORBIS_SAVE_DATA_PARAM_TYPE_MTIME: { - std::memcpy(paramBuf, ¶ms.mtime, sizeof(time_t)); - *gotSize = sizeof(time_t); - } break; - default: { - UNREACHABLE_MSG("Unknown Param = {}", paramType); - } break; + const std::string_view mount_point_str{mountPoint->data}; + for (const auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetMountPoint() == mount_point_str) { + param_sfo = &instance->GetParamSFO(); + break; + } + } + if (param_sfo == nullptr) { + return Error::NOT_MOUNTED; } - return ORBIS_OK; + switch (paramType) { + case OrbisSaveDataParamType::ALL: { + const auto param = static_cast(paramBuf); + ASSERT(paramBufSize == sizeof(OrbisSaveDataParam)); + param->FromSFO(*param_sfo); + if (gotSize != nullptr) { + *gotSize = sizeof(OrbisSaveDataParam); + } + break; + } + case OrbisSaveDataParamType::TITLE: + case OrbisSaveDataParamType::SUB_TITLE: + case OrbisSaveDataParamType::DETAIL: { + const auto param = static_cast(paramBuf); + std::string_view key; + if (paramType == OrbisSaveDataParamType::TITLE) { + key = SaveParams::MAINTITLE; + } else if (paramType == OrbisSaveDataParamType::SUB_TITLE) { + key = SaveParams::SUBTITLE; + } else if (paramType == OrbisSaveDataParamType::DETAIL) { + key = SaveParams::DETAIL; + } else { + UNREACHABLE(); + } + const size_t s = param_sfo->GetString(key)->copy(param, paramBufSize - 1); + param[s] = '\0'; // null terminate + if (gotSize != nullptr) { + *gotSize = s + 1; + } + } break; + case OrbisSaveDataParamType::USER_PARAM: { + const auto param = static_cast(paramBuf); + *param = param_sfo->GetInteger(SaveParams::SAVEDATA_LIST_PARAM).value_or(0); + if (gotSize != nullptr) { + *gotSize = sizeof(u32); + } + } break; + case OrbisSaveDataParamType::MTIME: { + const auto param = static_cast(paramBuf); + const auto last_write = param_sfo->GetLastWrite().time_since_epoch(); + *param = chrono::duration_cast(last_write).count(); + if (gotSize != nullptr) { + *gotSize = sizeof(time_t); + } + } break; + default: + UNREACHABLE(); + } + + return Error::OK; } -int PS4_SYSV_ABI sceSaveDataGetProgress() { - LOG_ERROR(Lib_SaveData, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataGetProgress(float* progress) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (progress == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_DEBUG(Lib_SaveData, "called"); + *progress = Backup::GetProgress(); + return Error::OK; } int PS4_SYSV_ABI sceSaveDataGetSaveDataCount() { @@ -409,44 +1066,56 @@ int PS4_SYSV_ABI sceSaveDataGetSaveDataCount() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataGetSaveDataMemory(const u32 userId, void* buf, const size_t bufSize, - const int64_t offset) { - const auto& mount_dir = Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir) / - std::to_string(userId) / game_serial / "sdmemory/save_mem1.sav"; - - Common::FS::IOFile file(mount_dir, Common::FS::FileAccessMode::Read); - if (!file.IsOpen()) { - return false; - } - file.Seek(offset); - size_t nbytes = file.ReadRaw(buf, bufSize); - LOG_INFO(Lib_SaveData, "called: bufSize = {}, offset = {}", bufSize, offset, nbytes); - - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataGetSaveDataMemory(const OrbisUserServiceUserId userId, void* buf, + const size_t bufSize, const int64_t offset) { + LOG_DEBUG(Lib_SaveData, "Redirecting to sceSaveDataGetSaveDataMemory2"); + OrbisSaveDataMemoryData data{}; + data.buf = buf; + data.bufSize = bufSize; + data.offset = offset; + OrbisSaveDataMemoryGet2 param{}; + param.userId = userId; + param.data = &data; + param.param = nullptr; + param.icon = nullptr; + return sceSaveDataGetSaveDataMemory2(¶m); } -int PS4_SYSV_ABI sceSaveDataGetSaveDataMemory2(OrbisSaveDataMemoryGet2* getParam) { - const auto& mount_dir = Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir) / - std::to_string(getParam->userId) / game_serial / "sdmemory"; - if (getParam == nullptr) - return ORBIS_SAVE_DATA_ERROR_PARAMETER; - if (getParam->data != nullptr) { - Common::FS::IOFile file(mount_dir / "save_mem2.sav", Common::FS::FileAccessMode::Read); - if (!file.IsOpen()) { - return false; - } - file.Seek(getParam->data->offset); - file.ReadRaw(getParam->data->buf, getParam->data->bufSize); - LOG_INFO(Lib_SaveData, "called: bufSize = {}, offset = {}", getParam->data->bufSize, - getParam->data->offset); +Error PS4_SYSV_ABI sceSaveDataGetSaveDataMemory2(OrbisSaveDataMemoryGet2* getParam) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (getParam == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + if (!SaveMemory::IsSaveMemoryInitialized()) { + LOG_INFO(Lib_SaveData, "called without save memory initialized"); + return Error::MEMORY_NOT_READY; + } + if (SaveMemory::IsSaving()) { + LOG_TRACE(Lib_SaveData, "called while saving"); + return Error::BUSY_FOR_SAVING; + } + LOG_DEBUG(Lib_SaveData, "called"); + auto data = getParam->data; + if (data != nullptr) { + SaveMemory::ReadMemory(data->buf, data->bufSize, data->offset); + } + auto param = getParam->param; + if (param != nullptr) { + param->FromSFO(SaveMemory::GetParamSFO()); + } + auto icon = getParam->icon; + if (icon != nullptr) { + auto icon_mem = SaveMemory::GetIcon(); + size_t total = std::min(icon->bufSize, icon_mem.size()); + std::memcpy(icon->buf, icon_mem.data(), total); + icon->dataSize = total; } - if (getParam->param != nullptr) { - Common::FS::IOFile file(mount_dir / "param.txt", Common::FS::FileAccessMode::Read); - file.ReadRaw(getParam->param, sizeof(OrbisSaveDataParam)); - } - - return ORBIS_OK; + return Error::OK; } int PS4_SYSV_ABI sceSaveDataGetSaveDataRootDir() { @@ -474,25 +1143,22 @@ int PS4_SYSV_ABI sceSaveDataGetUpdatedDataCount() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataInitialize() { - LOG_INFO(Lib_SaveData, "called"); - static auto* param_sfo = Common::Singleton::Instance(); - game_serial = std::string(param_sfo->GetString("CONTENT_ID"), 7, 9); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataInitialize(void*) { + LOG_DEBUG(Lib_SaveData, "called"); + initialize(); + return Error::OK; } -int PS4_SYSV_ABI sceSaveDataInitialize2() { - LOG_INFO(Lib_SaveData, "called"); - static auto* param_sfo = Common::Singleton::Instance(); - game_serial = std::string(param_sfo->GetString("CONTENT_ID"), 7, 9); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataInitialize2(void*) { + LOG_DEBUG(Lib_SaveData, "called"); + initialize(); + return Error::OK; } -int PS4_SYSV_ABI sceSaveDataInitialize3() { - LOG_INFO(Lib_SaveData, "called"); - static auto* param_sfo = Common::Singleton::Instance(); - game_serial = std::string(param_sfo->GetString("CONTENT_ID"), 7, 9); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataInitialize3(void*) { + LOG_DEBUG(Lib_SaveData, "called"); + initialize(); + return Error::OK; } int PS4_SYSV_ABI sceSaveDataInitializeForCdlg() { @@ -510,101 +1176,69 @@ int PS4_SYSV_ABI sceSaveDataIsMounted() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataLoadIcon(const OrbisSaveDataMountPoint* mountPoint, - OrbisSaveDataIcon* icon) { - auto* mnt = Common::Singleton::Instance(); - const auto mount_dir = mnt->GetHostPath(mountPoint->data); - LOG_INFO(Lib_SaveData, "called: dir = {}", mount_dir.string()); - - if (icon != nullptr) { - Common::FS::IOFile file(mount_dir / "save_data.png", Common::FS::FileAccessMode::Read); - icon->bufSize = file.GetSize(); - file.ReadRaw(icon->buf, icon->bufSize); +Error PS4_SYSV_ABI sceSaveDataLoadIcon(const OrbisSaveDataMountPoint* mountPoint, + OrbisSaveDataIcon* icon) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; } - return ORBIS_OK; + if (mountPoint == nullptr || icon == nullptr || icon->buf == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_DEBUG(Lib_SaveData, "called"); + std::filesystem::path path; + const std::string_view mount_point_str{mountPoint->data}; + for (const auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetMountPoint() == mount_point_str) { + path = instance->GetIconPath(); + break; + } + } + if (path.empty()) { + return Error::NOT_MOUNTED; + } + if (!fs::exists(path)) { + return Error::NOT_FOUND; + } + + return icon->LoadIcon(path); } -s32 saveDataMount(u32 user_id, char* dir_name, u32 mount_mode, - OrbisSaveDataMountResult* mount_result) { - const auto& mount_dir = Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir) / - std::to_string(user_id) / game_serial / dir_name; - auto* mnt = Common::Singleton::Instance(); - switch (mount_mode) { - case ORBIS_SAVE_DATA_MOUNT_MODE_RDONLY: - case ORBIS_SAVE_DATA_MOUNT_MODE_RDWR: - case ORBIS_SAVE_DATA_MOUNT_MODE_RDWR | ORBIS_SAVE_DATA_MOUNT_MODE_DESTRUCT_OFF: - case ORBIS_SAVE_DATA_MOUNT_MODE_RDONLY | ORBIS_SAVE_DATA_MOUNT_MODE_DESTRUCT_OFF: { - is_rw_mode = (mount_mode == ORBIS_SAVE_DATA_MOUNT_MODE_RDWR) ? true : false; - if (!std::filesystem::exists(mount_dir)) { - return ORBIS_SAVE_DATA_ERROR_NOT_FOUND; - } - mount_result->mount_status = 0; - g_mount_point.copy(mount_result->mount_point.data, 16); - mnt->Mount(mount_dir, mount_result->mount_point.data); - } break; - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE: - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE | ORBIS_SAVE_DATA_MOUNT_MODE_RDONLY: - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE | ORBIS_SAVE_DATA_MOUNT_MODE_RDWR: - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE | ORBIS_SAVE_DATA_MOUNT_MODE_RDWR | - ORBIS_SAVE_DATA_MOUNT_MODE_DESTRUCT_OFF: - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE | ORBIS_SAVE_DATA_MOUNT_MODE_RDWR | - ORBIS_SAVE_DATA_MOUNT_MODE_COPY_ICON: - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE | ORBIS_SAVE_DATA_MOUNT_MODE_COPY_ICON: - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE | ORBIS_SAVE_DATA_MOUNT_MODE_DESTRUCT_OFF | - ORBIS_SAVE_DATA_MOUNT_MODE_COPY_ICON: - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE | ORBIS_SAVE_DATA_MOUNT_MODE_RDWR | - ORBIS_SAVE_DATA_MOUNT_MODE_DESTRUCT_OFF | ORBIS_SAVE_DATA_MOUNT_MODE_COPY_ICON: { - if (std::filesystem::exists(mount_dir)) { - return ORBIS_SAVE_DATA_ERROR_EXISTS; - } - if (std::filesystem::create_directories(mount_dir)) { - g_mount_point.copy(mount_result->mount_point.data, 16); - mnt->Mount(mount_dir, mount_result->mount_point.data); - mount_result->mount_status = 1; - } - } break; - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE2: - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE2 | ORBIS_SAVE_DATA_MOUNT_MODE_RDWR: - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE2 | ORBIS_SAVE_DATA_MOUNT_MODE_COPY_ICON: - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE2 | ORBIS_SAVE_DATA_MOUNT_MODE_RDWR | - ORBIS_SAVE_DATA_MOUNT_MODE_COPY_ICON: - case ORBIS_SAVE_DATA_MOUNT_MODE_CREATE2 | ORBIS_SAVE_DATA_MOUNT_MODE_RDWR | - ORBIS_SAVE_DATA_MOUNT_MODE_DESTRUCT_OFF | ORBIS_SAVE_DATA_MOUNT_MODE_COPY_ICON: { - if (!std::filesystem::exists(mount_dir)) { - std::filesystem::create_directories(mount_dir); - } - g_mount_point.copy(mount_result->mount_point.data, 16); - mnt->Mount(mount_dir, mount_result->mount_point.data); - mount_result->mount_status = 1; - } break; - default: - UNREACHABLE_MSG("Unknown mount mode = {}", mount_mode); +Error PS4_SYSV_ABI sceSaveDataMount(const OrbisSaveDataMount* mount, + OrbisSaveDataMountResult* mount_result) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; } - mount_result->required_blocks = 0; + if (mount == nullptr && mount->dirName != nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_DEBUG(Lib_SaveData, "called dirName: {}, mode: {:0b}, blocks: {}", + mount->dirName->data.to_view(), (int)mount->mountMode, mount->blocks); - return ORBIS_OK; + OrbisSaveDataMount2 mount_info{}; + mount_info.userId = mount->userId; + mount_info.dirName = mount->dirName; + mount_info.mountMode = mount->mountMode; + mount_info.blocks = mount->blocks; + return saveDataMount(&mount_info, mount_result); } -s32 PS4_SYSV_ABI sceSaveDataMount(const OrbisSaveDataMount* mount, - OrbisSaveDataMountResult* mount_result) { - if (mount == nullptr) { - return ORBIS_SAVE_DATA_ERROR_PARAMETER; +Error PS4_SYSV_ABI sceSaveDataMount2(const OrbisSaveDataMount2* mount, + OrbisSaveDataMountResult* mount_result) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; } - LOG_INFO(Lib_SaveData, "called: dirName = {}, mode = {}, blocks = {}", mount->dir_name->data, - mount->mount_mode, mount->blocks); - return saveDataMount(mount->user_id, (char*)mount->dir_name->data, mount->mount_mode, - mount_result); -} - -s32 PS4_SYSV_ABI sceSaveDataMount2(const OrbisSaveDataMount2* mount, - OrbisSaveDataMountResult* mount_result) { - if (mount == nullptr) { - return ORBIS_SAVE_DATA_ERROR_PARAMETER; + if (mount == nullptr && mount->dirName != nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; } - LOG_INFO(Lib_SaveData, "called: dirName = {}, mode = {}, blocks = {}", mount->dir_name->data, - mount->mount_mode, mount->blocks); - return saveDataMount(mount->user_id, (char*)mount->dir_name->data, mount->mount_mode, - mount_result); + LOG_DEBUG(Lib_SaveData, "called dirName: {}, mode: {:0b}, blocks: {}", + mount->dirName->data.to_view(), (int)mount->mountMode, mount->blocks); + return saveDataMount(mount, mount_result); } int PS4_SYSV_ABI sceSaveDataMount5() { @@ -637,9 +1271,44 @@ int PS4_SYSV_ABI sceSaveDataRegisterEventCallback() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataRestoreBackupData() { - LOG_ERROR(Lib_SaveData, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataRestoreBackupData(const OrbisSaveDataRestoreBackupData* restore) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (restore == nullptr || restore->dirName == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + + const std::string_view dir_name{restore->dirName->data}; + LOG_DEBUG(Lib_SaveData, "called dirName: {}", dir_name); + + std::string_view title{restore->titleId != nullptr ? std::string_view{restore->titleId->data} + : std::string_view{g_game_serial}}; + + const auto save_path = SaveInstance::MakeDirSavePath(restore->userId, title, dir_name); + + for (const auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetSavePath() == save_path) { + return Error::BUSY; + } + } + if (Backup::IsBackupExecutingFor(save_path)) { + return Error::BACKUP_BUSY; + } + + const auto backup_path = Backup::MakeBackupPath(save_path); + if (!fs::exists(backup_path)) { + return Error::NOT_FOUND; + } + + const bool ok = Backup::Restore(save_path); + if (!ok) { + return Error::INTERNAL; + } + + return Error::OK; } int PS4_SYSV_ABI sceSaveDataRestoreBackupDataForCdlg() { @@ -652,17 +1321,41 @@ int PS4_SYSV_ABI sceSaveDataRestoreLoadSaveDataMemory() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataSaveIcon(const OrbisSaveDataMountPoint* mountPoint, - const OrbisSaveDataIcon* icon) { - auto* mnt = Common::Singleton::Instance(); - const auto mount_dir = mnt->GetHostPath(mountPoint->data); - LOG_INFO(Lib_SaveData, "called = {}", mount_dir.string()); - - if (icon != nullptr) { - Common::FS::IOFile file(mount_dir / "save_data.png", Common::FS::FileAccessMode::Write); - file.WriteRaw(icon->buf, icon->bufSize); +Error PS4_SYSV_ABI sceSaveDataSaveIcon(const OrbisSaveDataMountPoint* mountPoint, + const OrbisSaveDataIcon* icon) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; } - return ORBIS_OK; + if (mountPoint == nullptr || icon == nullptr || icon->buf == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_DEBUG(Lib_SaveData, "called"); + std::filesystem::path path; + const std::string_view mount_point_str{mountPoint->data}; + for (const auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetMountPoint() == mount_point_str) { + if (instance->IsReadOnly()) { + return Error::BAD_MOUNTED; + } + path = instance->GetIconPath(); + break; + } + } + if (path.empty()) { + return Error::NOT_MOUNTED; + } + + try { + const Common::FS::IOFile file(path, Common::FS::FileAccessMode::Write); + file.WriteRaw(icon->buf, std::min(icon->bufSize, icon->dataSize)); + } catch (const fs::filesystem_error& e) { + LOG_ERROR(Lib_SaveData, "Failed to load icon: {}", e.what()); + return Error::INTERNAL; + } + + return Error::OK; } int PS4_SYSV_ABI sceSaveDataSetAutoUploadSetting() { @@ -675,50 +1368,59 @@ int PS4_SYSV_ABI sceSaveDataSetEventInfo() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataSetParam(const OrbisSaveDataMountPoint* mountPoint, - OrbisSaveDataParamType paramType, const void* paramBuf, - size_t paramBufSize) { - if (paramBuf == nullptr) - return ORBIS_SAVE_DATA_ERROR_PARAMETER; - - auto* mnt = Common::Singleton::Instance(); - const auto mount_dir = mnt->GetHostPath(mountPoint->data) / "param.txt"; - OrbisSaveDataParam params; - if (std::filesystem::exists(mount_dir)) { - Common::FS::IOFile file(mount_dir, Common::FS::FileAccessMode::Read); - file.ReadRaw(¶ms, sizeof(OrbisSaveDataParam)); +Error PS4_SYSV_ABI sceSaveDataSetParam(const OrbisSaveDataMountPoint* mountPoint, + OrbisSaveDataParamType paramType, const void* paramBuf, + size_t paramBufSize) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (paramType > OrbisSaveDataParamType::USER_PARAM || mountPoint == nullptr || + paramBuf == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + LOG_DEBUG(Lib_SaveData, "called: paramType = {}", magic_enum::enum_name(paramType)); + PSF* param_sfo = nullptr; + const std::string_view mount_point_str{mountPoint->data}; + for (auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetMountPoint() == mount_point_str) { + param_sfo = &instance->GetParamSFO(); + break; + } + } + if (param_sfo == nullptr) { + return Error::NOT_MOUNTED; } - - LOG_INFO(Lib_SaveData, "called"); switch (paramType) { - case ORBIS_SAVE_DATA_PARAM_TYPE_ALL: { - memcpy(¶ms, paramBuf, sizeof(OrbisSaveDataParam)); + case OrbisSaveDataParamType::ALL: { + const auto param = static_cast(paramBuf); + ASSERT(paramBufSize == sizeof(OrbisSaveDataParam)); + param->ToSFO(*param_sfo); + return Error::OK; } break; - case ORBIS_SAVE_DATA_PARAM_TYPE_TITLE: { - strncpy(params.title, static_cast(paramBuf), paramBufSize); + case OrbisSaveDataParamType::TITLE: { + const auto value = static_cast(paramBuf); + param_sfo->AddString(std::string{SaveParams::MAINTITLE}, {value}, true); } break; - case ORBIS_SAVE_DATA_PARAM_TYPE_SUB_TITLE: { - strncpy(params.subTitle, static_cast(paramBuf), paramBufSize); + case OrbisSaveDataParamType::SUB_TITLE: { + const auto value = static_cast(paramBuf); + param_sfo->AddString(std::string{SaveParams::SUBTITLE}, {value}, true); } break; - case ORBIS_SAVE_DATA_PARAM_TYPE_DETAIL: { - strncpy(params.detail, static_cast(paramBuf), paramBufSize); + case OrbisSaveDataParamType::DETAIL: { + const auto value = static_cast(paramBuf); + param_sfo->AddString(std::string{SaveParams::DETAIL}, {value}, true); } break; - case ORBIS_SAVE_DATA_PARAM_TYPE_USER_PARAM: { - params.userParam = *(static_cast(paramBuf)); + case OrbisSaveDataParamType::USER_PARAM: { + const auto value = static_cast(paramBuf); + param_sfo->AddInteger(std::string{SaveParams::SAVEDATA_LIST_PARAM}, *value, true); } break; - case ORBIS_SAVE_DATA_PARAM_TYPE_MTIME: { - params.mtime = *(static_cast(paramBuf)); - } break; - default: { - UNREACHABLE_MSG("Unknown Param = {}", paramType); - } + default: + UNREACHABLE(); } - Common::FS::IOFile file(mount_dir, Common::FS::FileAccessMode::Write); - file.WriteRaw(¶ms, sizeof(OrbisSaveDataParam)); - - return ORBIS_OK; + return Error::OK; } int PS4_SYSV_ABI sceSaveDataSetSaveDataLibraryUser() { @@ -726,99 +1428,114 @@ int PS4_SYSV_ABI sceSaveDataSetSaveDataLibraryUser() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataSetSaveDataMemory(const u32 userId, const void* buf, - const size_t bufSize, const int64_t offset) { - LOG_INFO(Lib_SaveData, "called"); - const auto& mount_dir = Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir) / - std::to_string(userId) / game_serial / "sdmemory"; +Error PS4_SYSV_ABI sceSaveDataSetSaveDataMemory(OrbisUserServiceUserId userId, void* buf, + size_t bufSize, int64_t offset) { + LOG_DEBUG(Lib_SaveData, "Redirecting to sceSaveDataSetSaveDataMemory2"); + OrbisSaveDataMemoryData data{}; + data.buf = buf; + data.bufSize = bufSize; + data.offset = offset; + OrbisSaveDataMemorySet2 setParam{}; + setParam.userId = userId; + setParam.data = &data; + return sceSaveDataSetSaveDataMemory2(&setParam); +} - Common::FS::IOFile file(mount_dir / "save_mem1.sav", Common::FS::FileAccessMode::Write); - if (!file.IsOpen()) - return -1; - file.Seek(offset); - file.WriteRaw(buf, bufSize); +Error PS4_SYSV_ABI sceSaveDataSetSaveDataMemory2(const OrbisSaveDataMemorySet2* setParam) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (setParam == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + if (!SaveMemory::IsSaveMemoryInitialized()) { + LOG_INFO(Lib_SaveData, "called without save memory initialized"); + return Error::MEMORY_NOT_READY; + } + if (SaveMemory::IsSaving()) { + LOG_TRACE(Lib_SaveData, "called while saving"); + return Error::BUSY_FOR_SAVING; + } + LOG_DEBUG(Lib_SaveData, "called"); + auto data = setParam->data; + if (data != nullptr) { + SaveMemory::WriteMemory(data->buf, data->bufSize, data->offset); + } + auto param = setParam->param; + if (param != nullptr) { + param->ToSFO(SaveMemory::GetParamSFO()); + SaveMemory::SaveSFO(); + } + auto icon = setParam->icon; + if (icon != nullptr) { + SaveMemory::WriteIcon(icon->buf, icon->bufSize); + } + + SaveMemory::TriggerSaveWithoutEvent(); + return Error::OK; +} + +int PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory(/*u32 userId, size_t memorySize, + OrbisSaveDataParam* param*/) { + LOG_ERROR(Lib_SaveData, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataSetSaveDataMemory2(const OrbisSaveDataMemorySet2* setParam) { - LOG_INFO(Lib_SaveData, "called: dataNum = {}, slotId= {}", setParam->dataNum, setParam->slotId); - const auto& mount_dir = Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir) / - std::to_string(setParam->userId) / game_serial / "sdmemory"; - if (setParam->data != nullptr) { - Common::FS::IOFile file(mount_dir / "save_mem2.sav", Common::FS::FileAccessMode::Write); - if (!file.IsOpen()) - return -1; - file.Seek(setParam->data->offset); - file.WriteRaw(setParam->data->buf, setParam->data->bufSize); +Error PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory2(const OrbisSaveDataMemorySetup2* setupParam, + OrbisSaveDataMemorySetupResult* result) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; } - - if (setParam->param != nullptr) { - Common::FS::IOFile file(mount_dir / "param.txt", Common::FS::FileAccessMode::Write); - file.WriteRaw((void*)setParam->param, sizeof(OrbisSaveDataParam)); - } - - if (setParam->icon != nullptr) { - Common::FS::IOFile file(mount_dir / "save_icon.png", Common::FS::FileAccessMode::Write); - file.WriteRaw(setParam->icon->buf, setParam->icon->bufSize); - } - - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory(u32 userId, size_t memorySize, - OrbisSaveDataParam* param) { - - LOG_INFO(Lib_SaveData, "called:userId = {}, memorySize = {}", userId, memorySize); - - if (param == nullptr) { - return ORBIS_SAVE_DATA_ERROR_PARAMETER; - } - - const auto& mount_dir = Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir) / - std::to_string(userId) / game_serial / "sdmemory"; - - if (!std::filesystem::exists(mount_dir)) { - std::filesystem::create_directories(mount_dir); - } - - // NOTE: Reminder that games can pass params: - // memset(param, 0, sizeof(param_t)); - // strncpy(param->title, "Beach Buggy Racing", 127); - - std::vector buf(memorySize); - Common::FS::IOFile::WriteBytes(mount_dir / "save_mem1.sav", buf); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory2(const OrbisSaveDataMemorySetup2* setupParam, - OrbisSaveDataMemorySetupResult* result) { if (setupParam == nullptr) { - return ORBIS_SAVE_DATA_ERROR_PARAMETER; + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; } - LOG_INFO(Lib_SaveData, "called"); - // if (setupParam->option == 1) { // check this later. - const auto& mount_dir = Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir) / - std::to_string(setupParam->userId) / game_serial / "sdmemory"; - if (std::filesystem::exists(mount_dir) && - std::filesystem::exists(mount_dir / "save_mem2.sav")) { - Common::FS::IOFile file(mount_dir / "save_mem2.sav", Common::FS::FileAccessMode::Read); - if (!file.IsOpen()) - return -1; - // Bunny - CUSA07988 has a null result, having null result is checked and valid. - if (result != nullptr) - result->existedMemorySize = file.GetSize(); // Assign the saved data size. - // do not return ORBIS_SAVE_DATA_ERROR_EXISTS, as it will not trigger - // sceSaveDataGetSaveDataMemory2. - } else { - std::filesystem::create_directories(mount_dir); - std::vector buf(setupParam->memorySize); // check if > 0x1000000 (16.77mb) or x2? - Common::FS::IOFile::WriteBytes(mount_dir / "save_mem2.sav", buf); - std::vector paramBuf(sizeof(OrbisSaveDataParam)); - Common::FS::IOFile::WriteBytes(mount_dir / "param.txt", paramBuf); - std::vector iconBuf(setupParam->iconMemorySize); - Common::FS::IOFile::WriteBytes(mount_dir / "save_icon.png", iconBuf); + LOG_DEBUG(Lib_SaveData, "called"); + + SaveMemory::SetDirectories(setupParam->userId, g_game_serial); + + const auto& save_path = SaveMemory::GetSavePath(); + for (const auto& instance : g_mount_slots) { + if (instance.has_value() && instance->GetSavePath() == save_path) { + return Error::BUSY; + } } - return ORBIS_OK; + + try { + size_t existed_size = SaveMemory::CreateSaveMemory(setupParam->memorySize); + if (existed_size == 0) { // Just created + if (setupParam->initParam != nullptr) { + auto& sfo = SaveMemory::GetParamSFO(); + setupParam->initParam->ToSFO(sfo); + } + SaveMemory::SaveSFO(); + + auto init_icon = setupParam->initIcon; + if (init_icon != nullptr) { + SaveMemory::SetIcon(init_icon->buf, init_icon->bufSize); + } else { + SaveMemory::SetIcon(nullptr, 0); + } + } + if (result != nullptr) { + result->existedMemorySize = existed_size; + } + } catch (const fs::filesystem_error& e) { + LOG_ERROR(Lib_SaveData, "Failed to create/load save memory: {}", e.what()); + + const MsgDialog::MsgDialogState dialog{MsgDialog::MsgDialogState::UserState{ + .type = MsgDialog::ButtonType::OK, + .msg = "Failed to create or load save memory:\n" + std::string{e.what()}, + }}; + MsgDialog::ShowMsgDialog(dialog); + + return Error::INTERNAL; + } + + return Error::OK; } int PS4_SYSV_ABI sceSaveDataShutdownStart() { @@ -836,14 +1553,40 @@ int PS4_SYSV_ABI sceSaveDataSyncCloudList() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataSyncSaveDataMemory(OrbisSaveDataMemorySync* syncParam) { - LOG_ERROR(Lib_SaveData, "(STUBBED) called: option = {}", syncParam->option); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataSyncSaveDataMemory(OrbisSaveDataMemorySync* syncParam) { + if (!g_initialized) { + LOG_INFO(Lib_SaveData, "called without initialize"); + return Error::NOT_INITIALIZED; + } + if (syncParam == nullptr) { + LOG_INFO(Lib_SaveData, "called with invalid parameter"); + return Error::PARAMETER; + } + if (!SaveMemory::IsSaveMemoryInitialized()) { + LOG_INFO(Lib_SaveData, "called without save memory initialized"); + return Error::MEMORY_NOT_READY; + } + LOG_DEBUG(Lib_SaveData, "called"); + bool ok = SaveMemory::TriggerSave(); + if (!ok) { + return Error::BUSY_FOR_SAVING; + } + return Error::OK; } -int PS4_SYSV_ABI sceSaveDataTerminate() { - LOG_ERROR(Lib_SaveData, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataTerminate() { + LOG_DEBUG(Lib_SaveData, "called"); + if (!g_initialized) { + return Error::NOT_INITIALIZED; + } + for (const auto& instance : g_mount_slots) { + if (instance.has_value()) { + return Error::BUSY; + } + } + g_initialized = false; + Backup::StopThread(); + return Error::OK; } int PS4_SYSV_ABI sceSaveDataTransferringMount() { @@ -851,19 +1594,9 @@ int PS4_SYSV_ABI sceSaveDataTransferringMount() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceSaveDataUmount(const OrbisSaveDataMountPoint* mountPoint) { - LOG_INFO(Lib_SaveData, "mountPoint = {}", mountPoint->data); - if (std::string_view(mountPoint->data).empty()) { - return ORBIS_SAVE_DATA_ERROR_NOT_MOUNTED; - } - const auto& mount_dir = Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir) / - std::to_string(1) / game_serial / mountPoint->data; - auto* mnt = Common::Singleton::Instance(); - const auto& guest_path = mnt->GetHostPath(mountPoint->data); - if (guest_path.empty()) - return ORBIS_SAVE_DATA_ERROR_NOT_MOUNTED; - mnt->Unmount(mount_dir, mountPoint->data); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataUmount(const OrbisSaveDataMountPoint* mountPoint) { + LOG_DEBUG(Lib_SaveData, "called"); + return Umount(mountPoint); } int PS4_SYSV_ABI sceSaveDataUmountSys() { @@ -871,37 +1604,9 @@ int PS4_SYSV_ABI sceSaveDataUmountSys() { return ORBIS_OK; } -int PS4_SYSV_ABI sceSaveDataUmountWithBackup(const OrbisSaveDataMountPoint* mountPoint) { - LOG_INFO(Lib_SaveData, "called mount = {}, is_rw_mode = {}", std::string(mountPoint->data), - is_rw_mode); - auto* mnt = Common::Singleton::Instance(); - const auto mount_dir = mnt->GetHostPath(mountPoint->data); - if (!std::filesystem::exists(mount_dir)) { - return ORBIS_SAVE_DATA_ERROR_NOT_FOUND; - } - // leave disabled for now. and just unmount. - - /* if (is_rw_mode) { // backup is done only when mount mode is ReadWrite. - auto backup_path = mount_dir; - std::string save_data_dir = (mount_dir.string() + "_backup"); - backup_path.replace_filename(save_data_dir); - - std::filesystem::create_directories(backup_path); - - for (const auto& entry : std::filesystem::recursive_directory_iterator(mount_dir)) { - const auto& path = entry.path(); - if (std::filesystem::is_regular_file(path)) { - std::filesystem::copy(path, save_data_dir, - std::filesystem::copy_options::overwrite_existing); - } - } - }*/ - const auto& guest_path = mnt->GetHostPath(mountPoint->data); - if (guest_path.empty()) - return ORBIS_SAVE_DATA_ERROR_NOT_MOUNTED; - - mnt->Unmount(mount_dir, mountPoint->data); - return ORBIS_OK; +Error PS4_SYSV_ABI sceSaveDataUmountWithBackup(const OrbisSaveDataMountPoint* mountPoint) { + LOG_DEBUG(Lib_SaveData, "called"); + return Umount(mountPoint, true); } int PS4_SYSV_ABI sceSaveDataUnregisterEventCallback() { diff --git a/src/core/libraries/save_data/savedata.h b/src/core/libraries/save_data/savedata.h index 9b3cf900..5e6a8ad4 100644 --- a/src/core/libraries/save_data/savedata.h +++ b/src/core/libraries/save_data/savedata.h @@ -3,259 +3,81 @@ #pragma once +#include "common/cstring.h" #include "common/types.h" namespace Core::Loader { class SymbolsResolver; } +class PSF; + namespace Libraries::SaveData { -constexpr int ORBIS_SAVE_DATA_DIRNAME_DATA_MAXSIZE = - 32; // Maximum size for a save data directory name -constexpr int ORBIS_SAVE_DATA_MOUNT_POINT_DATA_MAXSIZE = 16; // Maximum size for a mount point name +constexpr size_t OrbisSaveDataTitleMaxsize = 128; // Maximum title name size +constexpr size_t OrbisSaveDataSubtitleMaxsize = 128; // Maximum subtitle name size +constexpr size_t OrbisSaveDataDetailMaxsize = 1024; // Maximum detail name size + +enum class Error : u32; +enum class OrbisSaveDataParamType : u32; + +using OrbisUserServiceUserId = s32; + +// Maximum size for a title ID (4 uppercase letters + 5 digits) +constexpr int OrbisSaveDataTitleIdDataSize = 10; +// Maximum save directory name size +constexpr int OrbisSaveDataDirnameDataMaxsize = 32; + +struct OrbisSaveDataTitleId { + Common::CString data; + std::array _pad; +}; struct OrbisSaveDataDirName { - char data[ORBIS_SAVE_DATA_DIRNAME_DATA_MAXSIZE]; + Common::CString data; }; -struct OrbisSaveDataMount2 { - s32 user_id; - s32 unk1; - const OrbisSaveDataDirName* dir_name; - u64 blocks; - u32 mount_mode; - u8 reserved[32]; - s32 unk2; -}; - -struct OrbisSaveDataMountPoint { - char data[ORBIS_SAVE_DATA_MOUNT_POINT_DATA_MAXSIZE]; -}; - -struct OrbisSaveDataMountResult { - OrbisSaveDataMountPoint mount_point; - u64 required_blocks; - u32 unused; - u32 mount_status; - u8 reserved[28]; - s32 unk1; -}; - -constexpr int ORBIS_SAVE_DATA_TITLE_ID_DATA_SIZE = 10; -struct OrbisSaveDataTitleId { - char data[ORBIS_SAVE_DATA_TITLE_ID_DATA_SIZE]; - char padding[6]; -}; - -constexpr int ORBIS_SAVE_DATA_FINGERPRINT_DATA_SIZE = 65; -struct OrbisSaveDataFingerprint { - char data[ORBIS_SAVE_DATA_FINGERPRINT_DATA_SIZE]; - char padding[15]; -}; - -struct OrbisSaveDataMount { - s32 user_id; - s32 pad; - const OrbisSaveDataTitleId* titleId; - const OrbisSaveDataDirName* dir_name; - const OrbisSaveDataFingerprint* fingerprint; - u64 blocks; - u32 mount_mode; - u8 reserved[32]; -}; - -typedef u32 OrbisSaveDataParamType; - -constexpr int ORBIS_SAVE_DATA_TITLE_MAXSIZE = 128; -constexpr int ORBIS_SAVE_DATA_SUBTITLE_MAXSIZE = 128; -constexpr int ORBIS_SAVE_DATA_DETAIL_MAXSIZE = 1024; struct OrbisSaveDataParam { - char title[ORBIS_SAVE_DATA_TITLE_MAXSIZE]; - char subTitle[ORBIS_SAVE_DATA_SUBTITLE_MAXSIZE]; - char detail[ORBIS_SAVE_DATA_DETAIL_MAXSIZE]; + Common::CString title; + Common::CString subTitle; + Common::CString detail; u32 userParam; int : 32; time_t mtime; - u8 reserved[32]; + std::array _reserved; + + void FromSFO(const PSF& sfo); + + void ToSFO(PSF& sfo) const; }; -struct OrbisSaveDataIcon { - void* buf; - size_t bufSize; - size_t dataSize; - u8 reserved[32]; -}; - -typedef u32 OrbisSaveDataSaveDataMemoryOption; -#define ORBIS_SAVE_DATA_MEMORY_OPTION_NONE (0x00000000) -#define ORBIS_SAVE_DATA_MEMORY_OPTION_SET_PARAM (0x00000001 << 0) -#define ORBIS_SAVE_DATA_MEMORY_OPTION_DOUBLE_BUFFER (0x00000001 << 1) - -struct OrbisSaveDataMemorySetup2 { - OrbisSaveDataSaveDataMemoryOption option; - s32 userId; - size_t memorySize; - size_t iconMemorySize; - const OrbisSaveDataParam* initParam; - const OrbisSaveDataIcon* initIcon; - u32 slotId; - u8 reserved[20]; -}; - -struct OrbisSaveDataMemorySetupResult { - size_t existedMemorySize; - u8 reserved[16]; -}; - -typedef u32 OrbisSaveDataEventType; -#define SCE_SAVE_DATA_EVENT_TYPE_INVALID (0) -#define SCE_SAVE_DATA_EVENT_TYPE_UMOUNT_BACKUP_END (1) -#define SCE_SAVE_DATA_EVENT_TYPE_BACKUP_END (2) -#define SCE_SAVE_DATA_EVENT_TYPE_SAVE_DATA_MEMORY_SYNC_END (3) - -struct OrbisSaveDataEvent { - OrbisSaveDataEventType type; - s32 errorCode; - s32 userId; - u8 padding[4]; - OrbisSaveDataTitleId titleId; - OrbisSaveDataDirName dirName; - u8 reserved[40]; -}; - -struct OrbisSaveDataMemoryData { - void* buf; - size_t bufSize; - off_t offset; - u8 reserved[40]; -}; - -struct OrbisSaveDataMemoryGet2 { - s32 userId; - u8 padding[4]; - OrbisSaveDataMemoryData* data; - OrbisSaveDataParam* param; - OrbisSaveDataIcon* icon; - u32 slotId; - u8 reserved[28]; -}; - -struct OrbisSaveDataMemorySet2 { - s32 userId; - u8 padding[4]; - const OrbisSaveDataMemoryData* data; - const OrbisSaveDataParam* param; - const OrbisSaveDataIcon* icon; - u32 dataNum; - u8 slotId; - u8 reserved[24]; -}; - -struct OrbisSaveDataCheckBackupData { - s32 userId; - int : 32; - const OrbisSaveDataTitleId* titleId; - const OrbisSaveDataDirName* dirName; - OrbisSaveDataParam* param; - OrbisSaveDataIcon* icon; - u8 reserved[32]; -}; - -struct OrbisSaveDataMountInfo { - u64 blocks; - u64 freeBlocks; - u8 reserved[32]; -}; - -#define ORBIS_SAVE_DATA_BLOCK_SIZE (32768) -#define ORBIS_SAVE_DATA_BLOCKS_MIN2 (96) -#define ORBIS_SAVE_DATA_BLOCKS_MAX (32768) - -// savedataMount2 mountModes (ORed values) -constexpr int ORBIS_SAVE_DATA_MOUNT_MODE_RDONLY = 1; -constexpr int ORBIS_SAVE_DATA_MOUNT_MODE_RDWR = 2; -constexpr int ORBIS_SAVE_DATA_MOUNT_MODE_CREATE = 4; -constexpr int ORBIS_SAVE_DATA_MOUNT_MODE_DESTRUCT_OFF = 8; -constexpr int ORBIS_SAVE_DATA_MOUNT_MODE_COPY_ICON = 16; -constexpr int ORBIS_SAVE_DATA_MOUNT_MODE_CREATE2 = 32; -typedef struct _OrbisSaveDataEventParam OrbisSaveDataEventParam; - -typedef u32 OrbisSaveDataSortKey; -#define ORBIS_SAVE_DATA_SORT_KEY_DIRNAME (0) -#define ORBIS_SAVE_DATA_SORT_KEY_USER_PARAM (1) -#define ORBIS_SAVE_DATA_SORT_KEY_BLOCKS (2) -#define ORBIS_SAVE_DATA_SORT_KEY_MTIME (3) -#define ORBIS_SAVE_DATA_SORT_KEY_FREE_BLOCKS (5) - -typedef u32 OrbisSaveDataSortOrder; -#define ORBIS_SAVE_DATA_SORT_ORDER_ASCENT (0) -#define ORBIS_SAVE_DATA_SORT_ORDER_DESCENT (1) - -struct OrbisSaveDataDirNameSearchCond { - s32 userId; - int : 32; - const OrbisSaveDataTitleId* titleId; - const OrbisSaveDataDirName* dirName; - OrbisSaveDataSortKey key; - OrbisSaveDataSortOrder order; - u8 reserved[32]; -}; - -struct OrbisSaveDataSearchInfo { - u64 blocks; - u64 freeBlocks; - u8 reserved[32]; -}; - -struct OrbisSaveDataDirNameSearchResult { - u32 hitNum; - int : 32; - OrbisSaveDataDirName* dirNames; - u32 dirNamesNum; - u32 setNum; - OrbisSaveDataParam* params; - OrbisSaveDataSearchInfo* infos; - u8 reserved[12]; - int : 32; -}; - -struct OrbisSaveDataDelete { - s32 userId; - int : 32; - const OrbisSaveDataTitleId* titleId; - const OrbisSaveDataDirName* dirName; - u32 unused; - u8 reserved[32]; - int : 32; -}; - -typedef u32 OrbisSaveDataMemorySyncOption; - -#define SCE_SAVE_DATA_MEMORY_SYNC_OPTION_NONE (0x00000000) -#define SCE_SAVE_DATA_MEMORY_SYNC_OPTION_BLOCKING (0x00000001 << 0) - -struct OrbisSaveDataMemorySync { - s32 userId; - u32 slotId; - OrbisSaveDataMemorySyncOption option; - u8 reserved[28]; -}; - -constexpr int ORBIS_SAVE_DATA_PARAM_TYPE_ALL = 0; -constexpr int ORBIS_SAVE_DATA_PARAM_TYPE_TITLE = 1; -constexpr int ORBIS_SAVE_DATA_PARAM_TYPE_SUB_TITLE = 2; -constexpr int ORBIS_SAVE_DATA_PARAM_TYPE_DETAIL = 3; -constexpr int ORBIS_SAVE_DATA_PARAM_TYPE_USER_PARAM = 4; -constexpr int ORBIS_SAVE_DATA_PARAM_TYPE_MTIME = 5; +struct OrbisSaveDataBackup; +struct OrbisSaveDataCheckBackupData; +struct OrbisSaveDataDelete; +struct OrbisSaveDataDirNameSearchCond; +struct OrbisSaveDataDirNameSearchResult; +struct OrbisSaveDataEvent; +struct OrbisSaveDataEventParam; +struct OrbisSaveDataIcon; +struct OrbisSaveDataMemoryGet2; +struct OrbisSaveDataMemorySet2; +struct OrbisSaveDataMemorySetup2; +struct OrbisSaveDataMemorySetupResult; +struct OrbisSaveDataMemorySync; +struct OrbisSaveDataMount2; +struct OrbisSaveDataMount; +struct OrbisSaveDataMountInfo; +struct OrbisSaveDataMountPoint; +struct OrbisSaveDataMountResult; +struct OrbisSaveDataRestoreBackupData; int PS4_SYSV_ABI sceSaveDataAbort(); -int PS4_SYSV_ABI sceSaveDataBackup(); +Error PS4_SYSV_ABI sceSaveDataBackup(const OrbisSaveDataBackup* backup); int PS4_SYSV_ABI sceSaveDataBindPsnAccount(); int PS4_SYSV_ABI sceSaveDataBindPsnAccountForSystemBackup(); int PS4_SYSV_ABI sceSaveDataChangeDatabase(); int PS4_SYSV_ABI sceSaveDataChangeInternal(); -int PS4_SYSV_ABI sceSaveDataCheckBackupData(const OrbisSaveDataCheckBackupData* check); +Error PS4_SYSV_ABI sceSaveDataCheckBackupData(const OrbisSaveDataCheckBackupData* check); int PS4_SYSV_ABI sceSaveDataCheckBackupDataForCdlg(); int PS4_SYSV_ABI sceSaveDataCheckBackupDataInternal(); int PS4_SYSV_ABI sceSaveDataCheckCloudData(); @@ -263,7 +85,7 @@ int PS4_SYSV_ABI sceSaveDataCheckIpmiIfSize(); int PS4_SYSV_ABI sceSaveDataCheckSaveDataBroken(); int PS4_SYSV_ABI sceSaveDataCheckSaveDataVersion(); int PS4_SYSV_ABI sceSaveDataCheckSaveDataVersionLatest(); -int PS4_SYSV_ABI sceSaveDataClearProgress(); +Error PS4_SYSV_ABI sceSaveDataClearProgress(); int PS4_SYSV_ABI sceSaveDataCopy5(); int PS4_SYSV_ABI sceSaveDataCreateUploadData(); int PS4_SYSV_ABI sceSaveDataDebug(); @@ -273,13 +95,13 @@ int PS4_SYSV_ABI sceSaveDataDebugCreateSaveDataRoot(); int PS4_SYSV_ABI sceSaveDataDebugGetThreadId(); int PS4_SYSV_ABI sceSaveDataDebugRemoveSaveDataRoot(); int PS4_SYSV_ABI sceSaveDataDebugTarget(); -int PS4_SYSV_ABI sceSaveDataDelete(const OrbisSaveDataDelete* del); +Error PS4_SYSV_ABI sceSaveDataDelete(const OrbisSaveDataDelete* del); int PS4_SYSV_ABI sceSaveDataDelete5(); int PS4_SYSV_ABI sceSaveDataDeleteAllUser(); int PS4_SYSV_ABI sceSaveDataDeleteCloudData(); int PS4_SYSV_ABI sceSaveDataDeleteUser(); -int PS4_SYSV_ABI sceSaveDataDirNameSearch(const OrbisSaveDataDirNameSearchCond* cond, - OrbisSaveDataDirNameSearchResult* result); +Error PS4_SYSV_ABI sceSaveDataDirNameSearch(const OrbisSaveDataDirNameSearchCond* cond, + OrbisSaveDataDirNameSearchResult* result); int PS4_SYSV_ABI sceSaveDataDirNameSearchInternal(); int PS4_SYSV_ABI sceSaveDataDownload(); int PS4_SYSV_ABI sceSaveDataGetAllSize(); @@ -292,70 +114,70 @@ int PS4_SYSV_ABI sceSaveDataGetClientThreadPriority(); int PS4_SYSV_ABI sceSaveDataGetCloudQuotaInfo(); int PS4_SYSV_ABI sceSaveDataGetDataBaseFilePath(); int PS4_SYSV_ABI sceSaveDataGetEventInfo(); -int PS4_SYSV_ABI sceSaveDataGetEventResult(const OrbisSaveDataEventParam* eventParam, - OrbisSaveDataEvent* event); +Error PS4_SYSV_ABI sceSaveDataGetEventResult(const OrbisSaveDataEventParam* eventParam, + OrbisSaveDataEvent* event); int PS4_SYSV_ABI sceSaveDataGetFormat(); int PS4_SYSV_ABI sceSaveDataGetMountedSaveDataCount(); -int PS4_SYSV_ABI sceSaveDataGetMountInfo(const OrbisSaveDataMountPoint* mountPoint, - OrbisSaveDataMountInfo* info); -int PS4_SYSV_ABI sceSaveDataGetParam(const OrbisSaveDataMountPoint* mountPoint, - const OrbisSaveDataParamType paramType, void* paramBuf, - const size_t paramBufSize, size_t* gotSize); -int PS4_SYSV_ABI sceSaveDataGetProgress(); +Error PS4_SYSV_ABI sceSaveDataGetMountInfo(const OrbisSaveDataMountPoint* mountPoint, + OrbisSaveDataMountInfo* info); +Error PS4_SYSV_ABI sceSaveDataGetParam(const OrbisSaveDataMountPoint* mountPoint, + OrbisSaveDataParamType paramType, void* paramBuf, + size_t paramBufSize, size_t* gotSize); +Error PS4_SYSV_ABI sceSaveDataGetProgress(float* progress); int PS4_SYSV_ABI sceSaveDataGetSaveDataCount(); -int PS4_SYSV_ABI sceSaveDataGetSaveDataMemory(const u32 userId, void* buf, const size_t bufSize, - const int64_t offset); -int PS4_SYSV_ABI sceSaveDataGetSaveDataMemory2(OrbisSaveDataMemoryGet2* getParam); +Error PS4_SYSV_ABI sceSaveDataGetSaveDataMemory(OrbisUserServiceUserId userId, void* buf, + size_t bufSize, int64_t offset); +Error PS4_SYSV_ABI sceSaveDataGetSaveDataMemory2(OrbisSaveDataMemoryGet2* getParam); int PS4_SYSV_ABI sceSaveDataGetSaveDataRootDir(); int PS4_SYSV_ABI sceSaveDataGetSaveDataRootPath(); int PS4_SYSV_ABI sceSaveDataGetSaveDataRootUsbPath(); int PS4_SYSV_ABI sceSaveDataGetSavePoint(); int PS4_SYSV_ABI sceSaveDataGetUpdatedDataCount(); -int PS4_SYSV_ABI sceSaveDataInitialize(); -int PS4_SYSV_ABI sceSaveDataInitialize2(); -int PS4_SYSV_ABI sceSaveDataInitialize3(); +Error PS4_SYSV_ABI sceSaveDataInitialize(void*); +Error PS4_SYSV_ABI sceSaveDataInitialize2(void*); +Error PS4_SYSV_ABI sceSaveDataInitialize3(void*); int PS4_SYSV_ABI sceSaveDataInitializeForCdlg(); int PS4_SYSV_ABI sceSaveDataIsDeletingUsbDb(); int PS4_SYSV_ABI sceSaveDataIsMounted(); -int PS4_SYSV_ABI sceSaveDataLoadIcon(const OrbisSaveDataMountPoint* mountPoint, - OrbisSaveDataIcon* icon); -int PS4_SYSV_ABI sceSaveDataMount(const OrbisSaveDataMount* mount, - OrbisSaveDataMountResult* mount_result); -s32 PS4_SYSV_ABI sceSaveDataMount2(const OrbisSaveDataMount2* mount, - OrbisSaveDataMountResult* mount_result); +Error PS4_SYSV_ABI sceSaveDataLoadIcon(const OrbisSaveDataMountPoint* mountPoint, + OrbisSaveDataIcon* icon); +Error PS4_SYSV_ABI sceSaveDataMount(const OrbisSaveDataMount* mount, + OrbisSaveDataMountResult* mount_result); +Error PS4_SYSV_ABI sceSaveDataMount2(const OrbisSaveDataMount2* mount, + OrbisSaveDataMountResult* mount_result); int PS4_SYSV_ABI sceSaveDataMount5(); int PS4_SYSV_ABI sceSaveDataMountInternal(); int PS4_SYSV_ABI sceSaveDataMountSys(); int PS4_SYSV_ABI sceSaveDataPromote5(); int PS4_SYSV_ABI sceSaveDataRebuildDatabase(); int PS4_SYSV_ABI sceSaveDataRegisterEventCallback(); -int PS4_SYSV_ABI sceSaveDataRestoreBackupData(); +Error PS4_SYSV_ABI sceSaveDataRestoreBackupData(const OrbisSaveDataRestoreBackupData* restore); int PS4_SYSV_ABI sceSaveDataRestoreBackupDataForCdlg(); int PS4_SYSV_ABI sceSaveDataRestoreLoadSaveDataMemory(); -int PS4_SYSV_ABI sceSaveDataSaveIcon(const OrbisSaveDataMountPoint* mountPoint, - const OrbisSaveDataIcon* icon); +Error PS4_SYSV_ABI sceSaveDataSaveIcon(const OrbisSaveDataMountPoint* mountPoint, + const OrbisSaveDataIcon* icon); int PS4_SYSV_ABI sceSaveDataSetAutoUploadSetting(); int PS4_SYSV_ABI sceSaveDataSetEventInfo(); -int PS4_SYSV_ABI sceSaveDataSetParam(const OrbisSaveDataMountPoint* mountPoint, - OrbisSaveDataParamType paramType, const void* paramBuf, - size_t paramBufSize); +Error PS4_SYSV_ABI sceSaveDataSetParam(const OrbisSaveDataMountPoint* mountPoint, + OrbisSaveDataParamType paramType, const void* paramBuf, + size_t paramBufSize); int PS4_SYSV_ABI sceSaveDataSetSaveDataLibraryUser(); -int PS4_SYSV_ABI sceSaveDataSetSaveDataMemory(const u32 userId, const void* buf, - const size_t bufSize, const int64_t offset); -int PS4_SYSV_ABI sceSaveDataSetSaveDataMemory2(const OrbisSaveDataMemorySet2* setParam); -int PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory(u32 userId, size_t memorySize, - OrbisSaveDataParam* param); -int PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory2(const OrbisSaveDataMemorySetup2* setupParam, - OrbisSaveDataMemorySetupResult* result); +Error PS4_SYSV_ABI sceSaveDataSetSaveDataMemory(OrbisUserServiceUserId userId, void* buf, + size_t bufSize, int64_t offset); +Error PS4_SYSV_ABI sceSaveDataSetSaveDataMemory2(const OrbisSaveDataMemorySet2* setParam); +int PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory(/*u32 userId, size_t memorySize, + OrbisSaveDataParam* param*/); +Error PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory2(const OrbisSaveDataMemorySetup2* setupParam, + OrbisSaveDataMemorySetupResult* result); int PS4_SYSV_ABI sceSaveDataShutdownStart(); int PS4_SYSV_ABI sceSaveDataSupportedFakeBrokenStatus(); int PS4_SYSV_ABI sceSaveDataSyncCloudList(); -int PS4_SYSV_ABI sceSaveDataSyncSaveDataMemory(OrbisSaveDataMemorySync* syncParam); -int PS4_SYSV_ABI sceSaveDataTerminate(); +Error PS4_SYSV_ABI sceSaveDataSyncSaveDataMemory(OrbisSaveDataMemorySync* syncParam); +Error PS4_SYSV_ABI sceSaveDataTerminate(); int PS4_SYSV_ABI sceSaveDataTransferringMount(); -int PS4_SYSV_ABI sceSaveDataUmount(const OrbisSaveDataMountPoint* mountPoint); +Error PS4_SYSV_ABI sceSaveDataUmount(const OrbisSaveDataMountPoint* mountPoint); int PS4_SYSV_ABI sceSaveDataUmountSys(); -int PS4_SYSV_ABI sceSaveDataUmountWithBackup(const OrbisSaveDataMountPoint* mountPoint); +Error PS4_SYSV_ABI sceSaveDataUmountWithBackup(const OrbisSaveDataMountPoint* mountPoint); int PS4_SYSV_ABI sceSaveDataUnregisterEventCallback(); int PS4_SYSV_ABI sceSaveDataUpload(); int PS4_SYSV_ABI Func_02E4C4D201716422(); diff --git a/src/core/libraries/system/msgdialog.cpp b/src/core/libraries/system/msgdialog.cpp index 94c122d9..7d924e4a 100644 --- a/src/core/libraries/system/msgdialog.cpp +++ b/src/core/libraries/system/msgdialog.cpp @@ -39,11 +39,6 @@ Error PS4_SYSV_ABI sceMsgDialogGetResult(DialogResult* result) { if (result == nullptr) { return Error::ARG_NULL; } - for (const auto v : result->reserved) { - if (v != 0) { - return Error::PARAM_INVALID; - } - } *result = g_result; return Error::OK; } diff --git a/src/core/libraries/system/msgdialog_ui.cpp b/src/core/libraries/system/msgdialog_ui.cpp index 63b3390c..15d6f4db 100644 --- a/src/core/libraries/system/msgdialog_ui.cpp +++ b/src/core/libraries/system/msgdialog_ui.cpp @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include + #include #include "common/assert.h" #include "imgui/imgui_std.h" @@ -31,18 +33,6 @@ struct { }; static_assert(std::size(user_button_texts) == static_cast(ButtonType::TWO_BUTTONS) + 1); -static void DrawCenteredText(const char* text) { - const auto ws = GetWindowSize(); - const auto text_size = CalcTextSize(text, nullptr, false, ws.x - 40.0f); - PushTextWrapPos(ws.x - 30.0f); - SetCursorPos({ - (ws.x - text_size.x) / 2.0f, - (ws.y - text_size.y) / 2.0f - 50.0f, - }); - Text("%s", text); - PopTextWrapPos(); -} - MsgDialogState::MsgDialogState(const OrbisParam& param) { this->mode = param.mode; switch (mode) { @@ -81,11 +71,29 @@ MsgDialogState::MsgDialogState(const OrbisParam& param) { } } +MsgDialogState::MsgDialogState(UserState mode) { + this->mode = MsgDialogMode::USER_MSG; + this->state = mode; +} + +MsgDialogState::MsgDialogState(ProgressState mode) { + this->mode = MsgDialogMode::PROGRESS_BAR; + this->state = mode; +} + +MsgDialogState::MsgDialogState(SystemState mode) { + this->mode = MsgDialogMode::SYSTEM_MSG; + this->state = mode; +} + void MsgDialogUi::DrawUser() { const auto& [button_type, msg, btn_param1, btn_param2] = state->GetState(); const auto ws = GetWindowSize(); - DrawCenteredText(msg.c_str()); + if (!msg.empty()) { + DrawCenteredText(&msg.front(), &msg.back() + 1, + GetContentRegionAvail() - ImVec2{0.0f, 15.0f + BUTTON_SIZE.y}); + } ASSERT(button_type <= ButtonType::TWO_BUTTONS); auto [count, text1, text2] = user_button_texts[static_cast(button_type)]; if (count == 0xFF) { // TWO_BUTTONS -> User defined message @@ -115,7 +123,7 @@ void MsgDialogUi::DrawUser() { break; } } - if (first_render && !focus_first) { + if ((first_render || IsKeyPressed(ImGuiKey_GamepadFaceRight)) && !focus_first) { SetItemCurrentNavFocus(); } PopID(); @@ -125,7 +133,7 @@ void MsgDialogUi::DrawUser() { if (Button(text1, BUTTON_SIZE)) { Finish(ButtonId::BUTTON1); } - if (first_render && focus_first) { + if ((first_render || IsKeyPressed(ImGuiKey_GamepadFaceRight)) && focus_first) { SetItemCurrentNavFocus(); } PopID(); @@ -249,11 +257,13 @@ void MsgDialogUi::Draw() { CentralizeWindow(); SetNextWindowSize(window_size); - SetNextWindowFocus(); SetNextWindowCollapsed(false); + if (first_render || !io.NavActive) { + SetNextWindowFocus(); + } KeepNavHighlight(); - // Hack to allow every dialog to have a unique window - if (Begin("Message Dialog##MessageDialog", nullptr, ImGuiWindowFlags_NoSavedSettings)) { + if (Begin("Message Dialog##MessageDialog", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings)) { switch (state->GetMode()) { case MsgDialogMode::USER_MSG: DrawUser(); @@ -269,4 +279,16 @@ void MsgDialogUi::Draw() { End(); first_render = false; -} \ No newline at end of file +} + +DialogResult Libraries::MsgDialog::ShowMsgDialog(MsgDialogState state, bool block) { + DialogResult result{}; + Status status = Status::RUNNING; + MsgDialogUi dialog(&state, &status, &result); + if (block) { + while (status == Status::RUNNING) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + return result; +} diff --git a/src/core/libraries/system/msgdialog_ui.h b/src/core/libraries/system/msgdialog_ui.h index 845abdc4..d24ec067 100644 --- a/src/core/libraries/system/msgdialog_ui.h +++ b/src/core/libraries/system/msgdialog_ui.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include "common/fixed_value.h" @@ -129,6 +130,11 @@ private: public: explicit MsgDialogState(const OrbisParam& param); + + explicit MsgDialogState(UserState mode); + explicit MsgDialogState(ProgressState mode); + explicit MsgDialogState(SystemState mode); + MsgDialogState() = default; [[nodiscard]] OrbisUserServiceUserId GetUserId() const { @@ -165,13 +171,11 @@ public: void Finish(ButtonId buttonId, CommonDialog::Result r = CommonDialog::Result::OK); - void SetProgressBarValue(u32 value, bool increment); - void Draw() override; - - bool ShouldGrabGamepad() override { - return true; - } }; +// Utility function to show a message dialog +// !!! This function can block !!! +DialogResult ShowMsgDialog(MsgDialogState state, bool block = true); + }; // namespace Libraries::MsgDialog \ No newline at end of file diff --git a/src/core/libraries/system/savedatadialog.cpp b/src/core/libraries/system/savedatadialog.cpp deleted file mode 100644 index 5aad480d..00000000 --- a/src/core/libraries/system/savedatadialog.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "common/logging/log.h" -#include "core/libraries/error_codes.h" -#include "core/libraries/libs.h" -#include "core/libraries/system/savedatadialog.h" - -namespace Libraries::SaveDataDialog { - -int PS4_SYSV_ABI sceSaveDataDialogClose() { - LOG_ERROR(Lib_SaveDataDialog, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSaveDataDialogGetResult() { - LOG_ERROR(Lib_SaveDataDialog, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSaveDataDialogGetStatus() { - LOG_ERROR(Lib_SaveDataDialog, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSaveDataDialogInitialize() { - LOG_ERROR(Lib_SaveDataDialog, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSaveDataDialogIsReadyToDisplay() { - LOG_ERROR(Lib_SaveDataDialog, "(STUBBED) called"); - return 1; -} - -int PS4_SYSV_ABI sceSaveDataDialogOpen() { - LOG_ERROR(Lib_SaveDataDialog, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSaveDataDialogProgressBarInc() { - LOG_ERROR(Lib_SaveDataDialog, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSaveDataDialogProgressBarSetValue() { - LOG_ERROR(Lib_SaveDataDialog, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSaveDataDialogTerminate() { - LOG_ERROR(Lib_SaveDataDialog, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSaveDataDialogUpdateStatus() { - LOG_ERROR(Lib_SaveDataDialog, "(STUBBED) called"); - return 3; // SCE_COMMON_DIALOG_STATUS_FINISHED -} - -void RegisterlibSceSaveDataDialog(Core::Loader::SymbolsResolver* sym) { - LIB_FUNCTION("fH46Lag88XY", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, - sceSaveDataDialogClose); - LIB_FUNCTION("yEiJ-qqr6Cg", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, - sceSaveDataDialogGetResult); - LIB_FUNCTION("ERKzksauAJA", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, - sceSaveDataDialogGetStatus); - LIB_FUNCTION("s9e3+YpRnzw", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, - sceSaveDataDialogInitialize); - LIB_FUNCTION("en7gNVnh878", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, - sceSaveDataDialogIsReadyToDisplay); - LIB_FUNCTION("4tPhsP6FpDI", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, - sceSaveDataDialogOpen); - LIB_FUNCTION("V-uEeFKARJU", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, - sceSaveDataDialogProgressBarInc); - LIB_FUNCTION("hay1CfTmLyA", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, - sceSaveDataDialogProgressBarSetValue); - LIB_FUNCTION("YuH2FA7azqQ", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, - sceSaveDataDialogTerminate); - LIB_FUNCTION("KK3Bdg1RWK0", "libSceSaveDataDialog", 1, "libSceSaveDataDialog", 1, 1, - sceSaveDataDialogUpdateStatus); -}; - -} // namespace Libraries::SaveDataDialog diff --git a/src/core/libraries/system/savedatadialog.h b/src/core/libraries/system/savedatadialog.h deleted file mode 100644 index e8fe7c75..00000000 --- a/src/core/libraries/system/savedatadialog.h +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include "common/types.h" - -namespace Core::Loader { -class SymbolsResolver; -} - -namespace Libraries::SaveDataDialog { - -int PS4_SYSV_ABI sceSaveDataDialogClose(); -int PS4_SYSV_ABI sceSaveDataDialogGetResult(); -int PS4_SYSV_ABI sceSaveDataDialogGetStatus(); -int PS4_SYSV_ABI sceSaveDataDialogInitialize(); -int PS4_SYSV_ABI sceSaveDataDialogIsReadyToDisplay(); -int PS4_SYSV_ABI sceSaveDataDialogOpen(); -int PS4_SYSV_ABI sceSaveDataDialogProgressBarInc(); -int PS4_SYSV_ABI sceSaveDataDialogProgressBarSetValue(); -int PS4_SYSV_ABI sceSaveDataDialogTerminate(); -int PS4_SYSV_ABI sceSaveDataDialogUpdateStatus(); - -void RegisterlibSceSaveDataDialog(Core::Loader::SymbolsResolver* sym); -} // namespace Libraries::SaveDataDialog diff --git a/src/emulator.cpp b/src/emulator.cpp index bf2d4588..581d0da8 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -10,6 +10,7 @@ #ifdef ENABLE_QT_GUI #include "common/memory_patcher.h" #endif +#include "common/assert.h" #include "common/ntapi.h" #include "common/path_util.h" #include "common/polyfill_thread.h" @@ -99,8 +100,9 @@ void Emulator::Run(const std::filesystem::path& file) { for (const auto& entry : std::filesystem::directory_iterator(sce_sys_folder)) { if (entry.path().filename() == "param.sfo") { auto* param_sfo = Common::Singleton::Instance(); - param_sfo->open(sce_sys_folder.string() + "/param.sfo", {}); - id = std::string(param_sfo->GetString("CONTENT_ID"), 7, 9); + const bool success = param_sfo->Open(sce_sys_folder / "param.sfo"); + ASSERT_MSG(success, "Failed to open param.sfo"); + id = std::string(*param_sfo->GetString("CONTENT_ID"), 7, 9); Libraries::NpTrophy::game_serial = id; const auto trophyDir = Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / id / "TrophyFiles"; @@ -113,10 +115,10 @@ void Emulator::Run(const std::filesystem::path& file) { #ifdef ENABLE_QT_GUI MemoryPatcher::g_game_serial = id; #endif - title = param_sfo->GetString("TITLE"); + title = *param_sfo->GetString("TITLE"); LOG_INFO(Loader, "Game id: {} Title: {}", id, title); - u32 fw_version = param_sfo->GetInteger("SYSTEM_VER"); - app_version = param_sfo->GetString("APP_VER"); + u32 fw_version = param_sfo->GetInteger("SYSTEM_VER").value_or(0x4700000); + app_version = *param_sfo->GetString("APP_VER"); LOG_INFO(Loader, "Fw: {:#x} App Version: {}", fw_version, app_version); } else if (entry.path().filename() == "playgo-chunk.dat") { auto* playgo = Common::Singleton::Instance(); diff --git a/src/imgui/imgui_config.h b/src/imgui/imgui_config.h index 4602382e..2094d56b 100644 --- a/src/imgui/imgui_config.h +++ b/src/imgui/imgui_config.h @@ -26,4 +26,7 @@ extern void assert_fail_debug_msg(const char* msg); #define IMGUI_DEFINE_MATH_OPERATORS #define IM_VEC2_CLASS_EXTRA \ - constexpr ImVec2(float _v) : x(_v), y(_v) {} \ No newline at end of file + constexpr ImVec2(float _v) : x(_v), y(_v) {} + +#define IM_VEC4_CLASS_EXTRA \ + constexpr ImVec4(float _v) : x(_v), y(_v), z(_v), w(_v) {} \ No newline at end of file diff --git a/src/imgui/imgui_layer.h b/src/imgui/imgui_layer.h index a2ec7fd2..a6c7e2a4 100644 --- a/src/imgui/imgui_layer.h +++ b/src/imgui/imgui_layer.h @@ -12,10 +12,6 @@ public: static void RemoveLayer(Layer* layer); virtual void Draw() = 0; - - virtual bool ShouldGrabGamepad() { - return false; - } }; } // namespace ImGui \ No newline at end of file diff --git a/src/imgui/imgui_std.h b/src/imgui/imgui_std.h index 6d97cc11..ec1e2f79 100644 --- a/src/imgui/imgui_std.h +++ b/src/imgui/imgui_std.h @@ -3,12 +3,25 @@ #pragma once +#include #include #include "imgui_internal.h" +#define IM_COL32_GRAY(x) IM_COL32(x, x, x, 0xFF) + namespace ImGui { +namespace Easing { + +inline float FastInFastOutCubic(float x) { + constexpr float c4 = 1.587401f; // 4^(1/3) + constexpr float c05 = 0.7937f; // 0.5^(1/3) + return std::pow(c4 * x - c05, 3.0f) + 0.5f; +} + +} // namespace Easing + inline void CentralizeWindow() { const auto display_size = GetIO().DisplaySize; SetNextWindowPos(display_size / 2.0f, ImGuiCond_Always, {0.5f}); @@ -18,10 +31,39 @@ inline void KeepNavHighlight() { GetCurrentContext()->NavDisableHighlight = false; } -inline void SetItemCurrentNavFocus() { +inline void SetItemCurrentNavFocus(const ImGuiID id = -1) { const auto ctx = GetCurrentContext(); - SetFocusID(ctx->LastItemData.ID, ctx->CurrentWindow); + SetFocusID(id == -1 ? ctx->LastItemData.ID : id, ctx->CurrentWindow); ctx->NavInitResult.Clear(); + ctx->NavDisableHighlight = false; +} + +inline void DrawPrettyBackground() { + const double time = GetTime() / 1.5f; + const float x = ((float)std::cos(time) + 1.0f) / 2.0f; + const float d = Easing::FastInFastOutCubic(x); + u8 top_left = ImLerp(0x13, 0x05, d); + u8 top_right = ImLerp(0x00, 0x07, d); + u8 bottom_right = ImLerp(0x03, 0x27, d); + u8 bottom_left = ImLerp(0x05, 0x00, d); + + auto& window = *GetCurrentWindowRead(); + auto inner_pos = window.DC.CursorPos - window.WindowPadding; + auto inner_size = GetContentRegionAvail() + window.WindowPadding * 2.0f; + GetWindowDrawList()->AddRectFilledMultiColor( + inner_pos, inner_pos + inner_size, IM_COL32_GRAY(top_left), IM_COL32_GRAY(top_right), + IM_COL32_GRAY(bottom_right), IM_COL32_GRAY(bottom_left)); +} + +static void DrawCenteredText(const char* text, const char* text_end = nullptr, + ImVec2 content = GetContentRegionAvail()) { + auto pos = GetCursorPos(); + const auto text_size = CalcTextSize(text, text_end, false, content.x - 40.0f); + PushTextWrapPos(content.x); + SetCursorPos(pos + (content - text_size) / 2.0f); + TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText); + PopTextWrapPos(); + SetCursorPos(pos + content); } } // namespace ImGui diff --git a/src/imgui/imgui_texture.h b/src/imgui/imgui_texture.h new file mode 100644 index 00000000..1a38066d --- /dev/null +++ b/src/imgui/imgui_texture.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +namespace ImGui { + +namespace Core::TextureManager { +struct Inner; +} // namespace Core::TextureManager + +class RefCountedTexture { + Core::TextureManager::Inner* inner; + + explicit RefCountedTexture(Core::TextureManager::Inner* inner); + +public: + struct Image { + ImTextureID im_id; + u32 width; + u32 height; + }; + + static RefCountedTexture DecodePngTexture(std::vector data); + + static RefCountedTexture DecodePngFile(std::filesystem::path path); + + RefCountedTexture(); + + RefCountedTexture(const RefCountedTexture& other); + RefCountedTexture(RefCountedTexture&& other) noexcept; + RefCountedTexture& operator=(const RefCountedTexture& other); + RefCountedTexture& operator=(RefCountedTexture&& other) noexcept; + + virtual ~RefCountedTexture(); + + [[nodiscard]] Image GetTexture() const; + + explicit(false) operator bool() const; +}; + +}; // namespace ImGui \ No newline at end of file diff --git a/src/imgui/layer/video_info.cpp b/src/imgui/layer/video_info.cpp index 2a60926f..bf30f870 100644 --- a/src/imgui/layer/video_info.cpp +++ b/src/imgui/layer/video_info.cpp @@ -10,7 +10,7 @@ void ImGui::Layers::VideoInfo::Draw() { m_show = IsKeyPressed(ImGuiKey_F10, false) ^ m_show; if (m_show) { - if (Begin("Video Info")) { + if (Begin("Video Info", 0, ImGuiWindowFlags_NoNav)) { Text("Frame time: %.3f ms (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); } End(); diff --git a/src/imgui/renderer/imgui_core.cpp b/src/imgui/renderer/imgui_core.cpp index 1c631397..d52536f6 100644 --- a/src/imgui/renderer/imgui_core.cpp +++ b/src/imgui/renderer/imgui_core.cpp @@ -9,7 +9,9 @@ #include "imgui_core.h" #include "imgui_impl_sdl3.h" #include "imgui_impl_vulkan.h" +#include "imgui_internal.h" #include "sdl_window.h" +#include "texture_manager.h" #include "video_core/renderer_vulkan/renderer_vulkan.h" static void CheckVkResult(const vk::Result err) { @@ -68,6 +70,8 @@ void Initialize(const ::Vulkan::Instance& instance, const Frontend::WindowSDL& w .check_vk_result_fn = &CheckVkResult, }; Vulkan::Init(vk_info); + + TextureManager::StartWorker(); } void OnResize() { @@ -77,6 +81,8 @@ void OnResize() { void Shutdown(const vk::Device& device) { device.waitIdle(); + TextureManager::StopWorker(); + const ImGuiIO& io = GetIO(); const auto ini_filename = (void*)io.IniFilename; const auto log_filename = (void*)io.LogFilename; @@ -92,24 +98,19 @@ void Shutdown(const vk::Device& device) { bool ProcessEvent(SDL_Event* event) { Sdl::ProcessEvent(event); switch (event->type) { + // Don't block release/up events case SDL_EVENT_MOUSE_MOTION: case SDL_EVENT_MOUSE_WHEEL: case SDL_EVENT_MOUSE_BUTTON_DOWN: - case SDL_EVENT_MOUSE_BUTTON_UP: return GetIO().WantCaptureMouse; case SDL_EVENT_TEXT_INPUT: case SDL_EVENT_KEY_DOWN: - case SDL_EVENT_KEY_UP: return GetIO().WantCaptureKeyboard; case SDL_EVENT_GAMEPAD_BUTTON_DOWN: - case SDL_EVENT_GAMEPAD_BUTTON_UP: case SDL_EVENT_GAMEPAD_AXIS_MOTION: - case SDL_EVENT_GAMEPAD_ADDED: - case SDL_EVENT_GAMEPAD_REMOVED: case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: - case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: - return (GetIO().BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + return GetIO().NavActive; default: return false; } @@ -130,21 +131,11 @@ void NewFrame() { } } - Vulkan::NewFrame(); Sdl::NewFrame(); ImGui::NewFrame(); - bool capture_gamepad = false; for (auto* layer : layers) { layer->Draw(); - if (layer->ShouldGrabGamepad()) { - capture_gamepad = true; - } - } - if (capture_gamepad) { - GetIO().BackendFlags |= ImGuiBackendFlags_HasGamepad; - } else { - GetIO().BackendFlags &= ~ImGuiBackendFlags_HasGamepad; } } diff --git a/src/imgui/renderer/imgui_impl_vulkan.cpp b/src/imgui/renderer/imgui_impl_vulkan.cpp index 2c1c135f..cf8c5ea4 100644 --- a/src/imgui/renderer/imgui_impl_vulkan.cpp +++ b/src/imgui/renderer/imgui_impl_vulkan.cpp @@ -4,6 +4,8 @@ // Based on imgui_impl_vulkan.cpp from Dear ImGui repository #include +#include + #include #include "imgui_impl_vulkan.h" @@ -47,13 +49,15 @@ struct VkData { vk::ShaderModule shader_module_vert{}; vk::ShaderModule shader_module_frag{}; + std::mutex command_pool_mutex; + vk::CommandPool command_pool{}; + vk::Sampler simple_sampler{}; + // Font data - vk::Sampler font_sampler{}; vk::DeviceMemory font_memory{}; vk::Image font_image{}; vk::ImageView font_view{}; vk::DescriptorSet font_descriptor_set{}; - vk::CommandPool font_command_pool{}; vk::CommandBuffer font_command_buffer{}; // Render buffers @@ -222,12 +226,53 @@ static inline vk::DeviceSize AlignBufferSize(vk::DeviceSize size, vk::DeviceSize return (size + alignment - 1) & ~(alignment - 1); } -// Register a texture -vk::DescriptorSet AddTexture(vk::Sampler sampler, vk::ImageView image_view, - vk::ImageLayout image_layout) { +void UploadTextureData::Upload() { VkData* bd = GetBackendData(); const InitInfo& v = bd->init_info; + vk::SubmitInfo submit_info{ + .commandBufferCount = 1, + .pCommandBuffers = &command_buffer, + }; + CheckVkErr(v.queue.submit({submit_info})); + CheckVkErr(v.queue.waitIdle()); + + v.device.destroyBuffer(upload_buffer, v.allocator); + v.device.freeMemory(upload_buffer_memory, v.allocator); + { + std::unique_lock lk(bd->command_pool_mutex); + v.device.freeCommandBuffers(bd->command_pool, {command_buffer}); + } + upload_buffer = VK_NULL_HANDLE; + upload_buffer_memory = VK_NULL_HANDLE; +} + +void UploadTextureData::Destroy() { + VkData* bd = GetBackendData(); + const InitInfo& v = bd->init_info; + + CheckVkErr(v.device.waitIdle()); + RemoveTexture(descriptor_set); + descriptor_set = VK_NULL_HANDLE; + + v.device.destroyImageView(image_view, v.allocator); + image_view = VK_NULL_HANDLE; + v.device.destroyImage(image, v.allocator); + image = VK_NULL_HANDLE; + v.device.freeMemory(image_memory, v.allocator); + image_memory = VK_NULL_HANDLE; +} + +// Register a texture +vk::DescriptorSet AddTexture(vk::ImageView image_view, vk::ImageLayout image_layout, + vk::Sampler sampler) { + VkData* bd = GetBackendData(); + const InitInfo& v = bd->init_info; + + if (sampler == VK_NULL_HANDLE) { + sampler = bd->simple_sampler; + } + // Create Descriptor Set: vk::DescriptorSet descriptor_set; { @@ -262,6 +307,166 @@ vk::DescriptorSet AddTexture(vk::Sampler sampler, vk::ImageView image_view, } return descriptor_set; } +UploadTextureData UploadTexture(const void* data, vk::Format format, u32 width, u32 height, + size_t size) { + ImGuiIO& io = GetIO(); + VkData* bd = GetBackendData(); + const InitInfo& v = bd->init_info; + + UploadTextureData info{}; + { + std::unique_lock lk(bd->command_pool_mutex); + info.command_buffer = + CheckVkResult(v.device.allocateCommandBuffers(vk::CommandBufferAllocateInfo{ + .commandPool = bd->command_pool, + .commandBufferCount = 1, + })) + .front(); + CheckVkErr(info.command_buffer.begin(vk::CommandBufferBeginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit, + })); + } + + // Create Image + { + vk::ImageCreateInfo image_info{ + .imageType = vk::ImageType::e2D, + .format = format, + .extent{ + .width = width, + .height = height, + .depth = 1, + }, + .mipLevels = 1, + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + .tiling = vk::ImageTiling::eOptimal, + .usage = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined, + }; + info.image = CheckVkResult(v.device.createImage(image_info, v.allocator)); + auto req = v.device.getImageMemoryRequirements(info.image); + vk::MemoryAllocateInfo alloc_info{ + .allocationSize = IM_MAX(v.min_allocation_size, req.size), + .memoryTypeIndex = + FindMemoryType(vk::MemoryPropertyFlagBits::eDeviceLocal, req.memoryTypeBits), + }; + info.image_memory = CheckVkResult(v.device.allocateMemory(alloc_info, v.allocator)); + CheckVkErr(v.device.bindImageMemory(info.image, info.image_memory, 0)); + } + + // Create Image View + { + vk::ImageViewCreateInfo view_info{ + .image = info.image, + .viewType = vk::ImageViewType::e2D, + .format = format, + .subresourceRange{ + .aspectMask = vk::ImageAspectFlagBits::eColor, + .levelCount = 1, + .layerCount = 1, + }, + }; + info.image_view = CheckVkResult(v.device.createImageView(view_info, v.allocator)); + } + + // Create descriptor set (ImTextureID) + info.descriptor_set = AddTexture(info.image_view, vk::ImageLayout::eShaderReadOnlyOptimal); + + // Create Upload Buffer + { + vk::BufferCreateInfo buffer_info{ + .size = size, + .usage = vk::BufferUsageFlagBits::eTransferSrc, + .sharingMode = vk::SharingMode::eExclusive, + }; + info.upload_buffer = CheckVkResult(v.device.createBuffer(buffer_info, v.allocator)); + auto req = v.device.getBufferMemoryRequirements(info.upload_buffer); + auto alignemtn = IM_MAX(bd->buffer_memory_alignment, req.alignment); + vk::MemoryAllocateInfo alloc_info{ + .allocationSize = IM_MAX(v.min_allocation_size, req.size), + .memoryTypeIndex = + FindMemoryType(vk::MemoryPropertyFlagBits::eHostVisible, req.memoryTypeBits), + }; + info.upload_buffer_memory = CheckVkResult(v.device.allocateMemory(alloc_info, v.allocator)); + CheckVkErr(v.device.bindBufferMemory(info.upload_buffer, info.upload_buffer_memory, 0)); + } + + // Upload to Buffer + { + char* map = (char*)CheckVkResult(v.device.mapMemory(info.upload_buffer_memory, 0, size)); + memcpy(map, data, size); + vk::MappedMemoryRange range[1]{ + { + .memory = info.upload_buffer_memory, + .size = size, + }, + }; + CheckVkErr(v.device.flushMappedMemoryRanges(range)); + v.device.unmapMemory(info.upload_buffer_memory); + } + + // Copy to Image + { + vk::ImageMemoryBarrier copy_barrier[1]{ + { + .sType = vk::StructureType::eImageMemoryBarrier, + .dstAccessMask = vk::AccessFlagBits::eTransferWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eTransferDstOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = info.image, + .subresourceRange{ + .aspectMask = vk::ImageAspectFlagBits::eColor, + .levelCount = 1, + .layerCount = 1, + }, + }, + }; + info.command_buffer.pipelineBarrier(vk::PipelineStageFlagBits::eHost, + vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, + {copy_barrier}); + + vk::BufferImageCopy region{ + .imageSubresource{ + .aspectMask = vk::ImageAspectFlagBits::eColor, + .layerCount = 1, + }, + .imageExtent{ + .width = width, + .height = height, + .depth = 1, + }, + }; + info.command_buffer.copyBufferToImage(info.upload_buffer, info.image, + vk::ImageLayout::eTransferDstOptimal, {region}); + + vk::ImageMemoryBarrier use_barrier[1]{{ + .sType = vk::StructureType::eImageMemoryBarrier, + .srcAccessMask = vk::AccessFlagBits::eTransferWrite, + .dstAccessMask = vk::AccessFlagBits::eShaderRead, + .oldLayout = vk::ImageLayout::eTransferDstOptimal, + .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = info.image, + .subresourceRange{ + .aspectMask = vk::ImageAspectFlagBits::eColor, + .levelCount = 1, + .layerCount = 1, + }, + }}; + info.command_buffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eFragmentShader, {}, {}, {}, + {use_barrier}); + } + + CheckVkErr(info.command_buffer.end()); + + return info; +} void RemoveTexture(vk::DescriptorSet descriptor_set) { VkData* bd = GetBackendData(); @@ -517,27 +722,20 @@ static bool CreateFontsTexture() { DestroyFontsTexture(); } - // Create command pool/buffer - if (bd->font_command_pool == VK_NULL_HANDLE) { - vk::CommandPoolCreateInfo info{ - .sType = vk::StructureType::eCommandPoolCreateInfo, - .flags = vk::CommandPoolCreateFlags{}, - .queueFamilyIndex = v.queue_family, - }; - bd->font_command_pool = CheckVkResult(v.device.createCommandPool(info, v.allocator)); - } + // Create command buffer if (bd->font_command_buffer == VK_NULL_HANDLE) { vk::CommandBufferAllocateInfo info{ .sType = vk::StructureType::eCommandBufferAllocateInfo, - .commandPool = bd->font_command_pool, + .commandPool = bd->command_pool, .commandBufferCount = 1, }; + std::unique_lock lk(bd->command_pool_mutex); bd->font_command_buffer = CheckVkResult(v.device.allocateCommandBuffers(info)).front(); } // Start command buffer { - CheckVkErr(v.device.resetCommandPool(bd->font_command_pool, vk::CommandPoolResetFlags{})); + CheckVkErr(bd->font_command_buffer.reset()); vk::CommandBufferBeginInfo begin_info{}; begin_info.sType = vk::StructureType::eCommandBufferBeginInfo; begin_info.flags |= vk::CommandBufferUsageFlagBits::eOneTimeSubmit; @@ -597,8 +795,7 @@ static bool CreateFontsTexture() { } // Create the Descriptor Set: - bd->font_descriptor_set = - AddTexture(bd->font_sampler, bd->font_view, vk::ImageLayout::eShaderReadOnlyOptimal); + bd->font_descriptor_set = AddTexture(bd->font_view, vk::ImageLayout::eShaderReadOnlyOptimal); // Create the Upload Buffer: vk::DeviceMemory upload_buffer_memory{}; @@ -956,25 +1153,6 @@ bool CreateDeviceObjects() { bd->descriptor_pool = CheckVkResult(v.device.createDescriptorPool(pool_info)); } - if (!bd->font_sampler) { - // Bilinear sampling is required by default. Set 'io.Fonts->Flags |= - // ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow - // point/nearest sampling. - vk::SamplerCreateInfo info{ - .sType = vk::StructureType::eSamplerCreateInfo, - .magFilter = vk::Filter::eLinear, - .minFilter = vk::Filter::eLinear, - .mipmapMode = vk::SamplerMipmapMode::eLinear, - .addressModeU = vk::SamplerAddressMode::eRepeat, - .addressModeV = vk::SamplerAddressMode::eRepeat, - .addressModeW = vk::SamplerAddressMode::eRepeat, - .maxAnisotropy = 1.0f, - .minLod = -1000, - .maxLod = 1000, - }; - bd->font_sampler = CheckVkResult(v.device.createSampler(info, v.allocator)); - } - if (!bd->descriptor_set_layout) { vk::DescriptorSetLayoutBinding binding[1]{ { @@ -1016,6 +1194,35 @@ bool CreateDeviceObjects() { CreatePipeline(v.device, v.allocator, v.pipeline_cache, nullptr, &bd->pipeline, v.subpass); + if (bd->command_pool == VK_NULL_HANDLE) { + vk::CommandPoolCreateInfo info{ + .sType = vk::StructureType::eCommandPoolCreateInfo, + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = v.queue_family, + }; + std::unique_lock lk(bd->command_pool_mutex); + bd->command_pool = CheckVkResult(v.device.createCommandPool(info, v.allocator)); + } + + if (!bd->simple_sampler) { + // Bilinear sampling is required by default. Set 'io.Fonts->Flags |= + // ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow + // point/nearest sampling. + vk::SamplerCreateInfo info{ + .sType = vk::StructureType::eSamplerCreateInfo, + .magFilter = vk::Filter::eLinear, + .minFilter = vk::Filter::eLinear, + .mipmapMode = vk::SamplerMipmapMode::eLinear, + .addressModeU = vk::SamplerAddressMode::eRepeat, + .addressModeV = vk::SamplerAddressMode::eRepeat, + .addressModeW = vk::SamplerAddressMode::eRepeat, + .maxAnisotropy = 1.0f, + .minLod = -1000, + .maxLod = 1000, + }; + bd->simple_sampler = CheckVkResult(v.device.createSampler(info, v.allocator)); + } + return true; } @@ -1026,12 +1233,14 @@ void ImGuiImplVulkanDestroyDeviceObjects() { DestroyFontsTexture(); if (bd->font_command_buffer) { - v.device.freeCommandBuffers(bd->font_command_pool, {bd->font_command_buffer}); + std::unique_lock lk(bd->command_pool_mutex); + v.device.freeCommandBuffers(bd->command_pool, {bd->font_command_buffer}); bd->font_command_buffer = VK_NULL_HANDLE; } - if (bd->font_command_pool) { - v.device.destroyCommandPool(bd->font_command_pool, v.allocator); - bd->font_command_pool = VK_NULL_HANDLE; + if (bd->command_pool) { + std::unique_lock lk(bd->command_pool_mutex); + v.device.destroyCommandPool(bd->command_pool, v.allocator); + bd->command_pool = VK_NULL_HANDLE; } if (bd->shader_module_vert) { v.device.destroyShaderModule(bd->shader_module_vert, v.allocator); @@ -1041,9 +1250,9 @@ void ImGuiImplVulkanDestroyDeviceObjects() { v.device.destroyShaderModule(bd->shader_module_frag, v.allocator); bd->shader_module_frag = VK_NULL_HANDLE; } - if (bd->font_sampler) { - v.device.destroySampler(bd->font_sampler, v.allocator); - bd->font_sampler = VK_NULL_HANDLE; + if (bd->simple_sampler) { + v.device.destroySampler(bd->simple_sampler, v.allocator); + bd->simple_sampler = VK_NULL_HANDLE; } if (bd->descriptor_set_layout) { v.device.destroyDescriptorSetLayout(bd->descriptor_set_layout, v.allocator); @@ -1095,13 +1304,4 @@ void Shutdown() { IM_DELETE(bd); } -void NewFrame() { - VkData* bd = GetBackendData(); - IM_ASSERT(bd != nullptr && - "Context or backend not initialized! Did you call ImGuiImplVulkanInit()?"); - - if (!bd->font_descriptor_set) - CreateFontsTexture(); -} - } // namespace ImGui::Vulkan diff --git a/src/imgui/renderer/imgui_impl_vulkan.h b/src/imgui/renderer/imgui_impl_vulkan.h index e68b8723..ca76fda6 100644 --- a/src/imgui/renderer/imgui_impl_vulkan.h +++ b/src/imgui/renderer/imgui_impl_vulkan.h @@ -6,6 +6,7 @@ #pragma once #define VULKAN_HPP_NO_EXCEPTIONS +#include "common/types.h" #include "video_core/renderer_vulkan/vk_common.h" struct ImDrawData; @@ -29,14 +30,33 @@ struct InitInfo { void (*check_vk_result_fn)(vk::Result err); }; -vk::DescriptorSet AddTexture(vk::Sampler sampler, vk::ImageView image_view, - vk::ImageLayout image_layout); +// Prepare all resources needed for uploading textures +// Caller should clean up the returned data. +struct UploadTextureData { + vk::Image image; + vk::ImageView image_view; + vk::DescriptorSet descriptor_set; + vk::DeviceMemory image_memory; + + vk::CommandBuffer command_buffer; // Submit to the queue + vk::Buffer upload_buffer; + vk::DeviceMemory upload_buffer_memory; + + void Upload(); + + void Destroy(); +}; + +vk::DescriptorSet AddTexture(vk::ImageView image_view, vk::ImageLayout image_layout, + vk::Sampler sampler = VK_NULL_HANDLE); + +UploadTextureData UploadTexture(const void* data, vk::Format format, u32 width, u32 height, + size_t size); void RemoveTexture(vk::DescriptorSet descriptor_set); bool Init(InitInfo info); void Shutdown(); -void NewFrame(); void RenderDrawData(ImDrawData& draw_data, vk::CommandBuffer command_buffer, vk::Pipeline pipeline = VK_NULL_HANDLE); diff --git a/src/imgui/renderer/texture_manager.cpp b/src/imgui/renderer/texture_manager.cpp new file mode 100644 index 00000000..ba4a05d0 --- /dev/null +++ b/src/imgui/renderer/texture_manager.cpp @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include + +#include "common/assert.h" +#include "common/io_file.h" +#include "common/polyfill_thread.h" +#include "imgui_impl_vulkan.h" +#include "texture_manager.h" + +namespace ImGui { + +namespace Core::TextureManager { +struct Inner { + std::atomic_int count = 0; + ImTextureID texture_id = nullptr; + u32 width = 0; + u32 height = 0; + + Vulkan::UploadTextureData upload_data; + + ~Inner(); +}; +} // namespace Core::TextureManager + +using namespace Core::TextureManager; + +RefCountedTexture::RefCountedTexture(Inner* inner) : inner(inner) { + ++inner->count; +} + +RefCountedTexture RefCountedTexture::DecodePngTexture(std::vector data) { + const auto core = new Inner; + Core::TextureManager::DecodePngTexture(std::move(data), core); + return RefCountedTexture(core); +} + +RefCountedTexture RefCountedTexture::DecodePngFile(std::filesystem::path path) { + const auto core = new Inner; + Core::TextureManager::DecodePngFile(std::move(path), core); + return RefCountedTexture(core); +} + +RefCountedTexture::RefCountedTexture() : inner(nullptr) {} + +RefCountedTexture::RefCountedTexture(const RefCountedTexture& other) : inner(other.inner) { + if (inner != nullptr) { + ++inner->count; + } +} + +RefCountedTexture::RefCountedTexture(RefCountedTexture&& other) noexcept : inner(other.inner) { + other.inner = nullptr; +} + +RefCountedTexture& RefCountedTexture::operator=(const RefCountedTexture& other) { + if (this == &other) + return *this; + inner = other.inner; + if (inner != nullptr) { + ++inner->count; + } + return *this; +} + +RefCountedTexture& RefCountedTexture::operator=(RefCountedTexture&& other) noexcept { + if (this == &other) + return *this; + std::swap(inner, other.inner); + return *this; +} + +RefCountedTexture::~RefCountedTexture() { + if (inner != nullptr) { + if (inner->count.fetch_sub(1) == 1) { + delete inner; + } + } +} +RefCountedTexture::Image RefCountedTexture::GetTexture() const { + if (inner == nullptr) { + return {}; + } + return Image{ + .im_id = inner->texture_id, + .width = inner->width, + .height = inner->height, + }; +} +RefCountedTexture::operator bool() const { + return inner != nullptr && inner->texture_id != nullptr; +} + +struct Job { + Inner* core; + std::vector data; + std::filesystem::path path; +}; + +struct UploadJob { + Inner* core = nullptr; + Vulkan::UploadTextureData data; + int tick = 0; // Used to skip the first frame when destroying to await the current frame to draw +}; + +static bool g_is_worker_running = false; +static std::jthread g_worker_thread; +static std::condition_variable g_worker_cv; + +static std::mutex g_job_list_mtx; +static std::deque g_job_list; + +static std::mutex g_upload_mtx; +static std::deque g_upload_list; + +namespace Core::TextureManager { + +Inner::~Inner() { + if (upload_data.descriptor_set != nullptr) { + std::unique_lock lk{g_upload_mtx}; + g_upload_list.emplace_back(UploadJob{ + .data = this->upload_data, + .tick = 2, + }); + } +} + +void WorkerLoop() { + std::mutex mtx; + while (g_is_worker_running) { + std::unique_lock lk{mtx}; + g_worker_cv.wait(lk); + if (!g_is_worker_running) { + break; + } + while (true) { + g_job_list_mtx.lock(); + if (g_job_list.empty()) { + g_job_list_mtx.unlock(); + break; + } + auto [core, png_raw, path] = std::move(g_job_list.front()); + g_job_list.pop_front(); + g_job_list_mtx.unlock(); + + if (!path.empty()) { // Decode PNG from file + Common::FS::IOFile file(path, Common::FS::FileAccessMode::Read); + if (!file.IsOpen()) { + LOG_ERROR(ImGui, "Failed to open PNG file: {}", path.string()); + continue; + } + png_raw.resize(file.GetSize()); + file.Seek(0); + file.ReadRaw(png_raw.data(), png_raw.size()); + file.Close(); + } + + int width, height; + const stbi_uc* pixels = + stbi_load_from_memory(png_raw.data(), png_raw.size(), &width, &height, nullptr, 4); + + auto texture = Vulkan::UploadTexture(pixels, vk::Format::eR8G8B8A8Unorm, width, height, + width * height * 4 * sizeof(stbi_uc)); + + core->upload_data = texture; + core->width = width; + core->height = height; + + std::unique_lock upload_lk{g_upload_mtx}; + g_upload_list.emplace_back(UploadJob{ + .core = core, + }); + } + } +} + +void StartWorker() { + ASSERT(!g_is_worker_running); + g_worker_thread = std::jthread(WorkerLoop); + g_is_worker_running = true; +} + +void StopWorker() { + ASSERT(g_is_worker_running); + g_is_worker_running = false; + g_worker_cv.notify_one(); +} + +void DecodePngTexture(std::vector data, Inner* core) { + ++core->count; + Job job{ + .core = core, + .data = std::move(data), + }; + std::unique_lock lk{g_job_list_mtx}; + g_job_list.push_back(std::move(job)); + g_worker_cv.notify_one(); +} + +void DecodePngFile(std::filesystem::path path, Inner* core) { + ++core->count; + Job job{ + .core = core, + .path = std::move(path), + }; + std::unique_lock lk{g_job_list_mtx}; + g_job_list.push_back(std::move(job)); + g_worker_cv.notify_one(); +} + +void Submit() { + UploadJob upload; + { + std::unique_lock lk{g_upload_mtx}; + if (g_upload_list.empty()) { + return; + } + // Upload one texture at a time to avoid slow down + upload = g_upload_list.front(); + g_upload_list.pop_front(); + if (upload.tick > 0) { + --upload.tick; + g_upload_list.emplace_back(upload); + return; + } + } + if (upload.core != nullptr) { + upload.core->upload_data.Upload(); + upload.core->texture_id = upload.core->upload_data.descriptor_set; + if (upload.core->count.fetch_sub(1) == 1) { + delete upload.core; + } + } else { + upload.data.Destroy(); + } +} +} // namespace Core::TextureManager + +} // namespace ImGui \ No newline at end of file diff --git a/src/imgui/renderer/texture_manager.h b/src/imgui/renderer/texture_manager.h new file mode 100644 index 00000000..4fa7b992 --- /dev/null +++ b/src/imgui/renderer/texture_manager.h @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "common/types.h" +#include "imgui/imgui_texture.h" + +namespace vk { +class CommandBuffer; +} + +namespace ImGui::Core::TextureManager { + +struct Inner; + +void StartWorker(); + +void StopWorker(); + +void DecodePngTexture(std::vector data, Inner* core); + +void DecodePngFile(std::filesystem::path path, Inner* core); + +void Submit(); + +}; // namespace ImGui::Core::TextureManager \ No newline at end of file diff --git a/src/qt_gui/game_info.h b/src/qt_gui/game_info.h index 6032e1c3..2d08bc08 100644 --- a/src/qt_gui/game_info.h +++ b/src/qt_gui/game_info.h @@ -27,20 +27,21 @@ public: game.path = filePath; PSF psf; - if (psf.open(game.path + "/sce_sys/param.sfo", {})) { + if (psf.Open(std::filesystem::path(game.path) / "sce_sys" / "param.sfo")) { game.icon_path = game.path + "/sce_sys/icon0.png"; QString iconpath = QString::fromStdString(game.icon_path); game.icon = QImage(iconpath); game.pic_path = game.path + "/sce_sys/pic1.png"; - game.name = psf.GetString("TITLE"); - game.serial = psf.GetString("TITLE_ID"); - game.region = GameListUtils::GetRegion(psf.GetString("CONTENT_ID").at(0)).toStdString(); - u32 fw_int = psf.GetInteger("SYSTEM_VER"); + game.name = *psf.GetString("TITLE"); + game.serial = *psf.GetString("TITLE_ID"); + game.region = + GameListUtils::GetRegion(psf.GetString("CONTENT_ID")->at(0)).toStdString(); + u32 fw_int = *psf.GetInteger("SYSTEM_VER"); QString fw = QString::number(fw_int, 16); QString fw_ = fw.length() > 7 ? QString::number(fw_int, 16).left(3).insert(2, '.') : fw.left(3).insert(1, '.'); game.fw = (fw_int == 0) ? "0.00" : fw_.toStdString(); - game.version = psf.GetString("APP_VER"); + game.version = *psf.GetString("APP_VER"); } return game; } diff --git a/src/qt_gui/gui_context_menus.h b/src/qt_gui/gui_context_menus.h index fb1994bb..bd3961dd 100644 --- a/src/qt_gui/gui_context_menus.h +++ b/src/qt_gui/gui_context_menus.h @@ -80,8 +80,8 @@ public: if (selected == &openSfoViewer) { PSF psf; - if (psf.open(m_games[itemID].path + "/sce_sys/param.sfo", {})) { - int rows = psf.map_strings.size() + psf.map_integers.size(); + if (psf.Open(std::filesystem::path(m_games[itemID].path) / "sce_sys" / "param.sfo")) { + int rows = psf.GetEntries().size(); QTableWidget* tableWidget = new QTableWidget(rows, 2); tableWidget->setAttribute(Qt::WA_DeleteOnClose); connect(widget->parent(), &QWidget::destroyed, tableWidget, @@ -90,23 +90,33 @@ public: tableWidget->verticalHeader()->setVisible(false); // Hide vertical header int row = 0; - for (const auto& pair : psf.map_strings) { + for (const auto& entry : psf.GetEntries()) { QTableWidgetItem* keyItem = - new QTableWidgetItem(QString::fromStdString(pair.first)); - QTableWidgetItem* valueItem = - new QTableWidgetItem(QString::fromStdString(pair.second)); + new QTableWidgetItem(QString::fromStdString(entry.key)); + QTableWidgetItem* valueItem; + switch (entry.param_fmt) { + case PSFEntryFmt::Binary: { - tableWidget->setItem(row, 0, keyItem); - tableWidget->setItem(row, 1, valueItem); - keyItem->setFlags(keyItem->flags() & ~Qt::ItemIsEditable); - valueItem->setFlags(valueItem->flags() & ~Qt::ItemIsEditable); - row++; - } - for (const auto& pair : psf.map_integers) { - QTableWidgetItem* keyItem = - new QTableWidgetItem(QString::fromStdString(pair.first)); - QTableWidgetItem* valueItem = new QTableWidgetItem( - QString("0x").append(QString::number(pair.second, 16))); + const auto bin = *psf.GetBinary(entry.key); + std::string text; + text.reserve(bin.size() * 2); + for (const auto& c : bin) { + static constexpr char hex[] = "0123456789ABCDEF"; + text.push_back(hex[c >> 4 & 0xF]); + text.push_back(hex[c & 0xF]); + } + valueItem = new QTableWidgetItem(QString::fromStdString(text)); + } break; + case PSFEntryFmt::Text: { + auto text = *psf.GetString(entry.key); + valueItem = new QTableWidgetItem(QString::fromStdString(std::string{text})); + } break; + case PSFEntryFmt::Integer: { + auto integer = *psf.GetInteger(entry.key); + valueItem = + new QTableWidgetItem(QString("0x") + QString::number(integer, 16)); + } break; + } tableWidget->setItem(row, 0, keyItem); tableWidget->setItem(row, 1, valueItem); diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 1945db7f..cb0129c8 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -636,9 +636,9 @@ void MainWindow::InstallDragDropPkg(std::filesystem::path file, int pkgNum, int QMessageBox msgBox; msgBox.setWindowTitle(tr("PKG Extraction")); - psf.open("", pkg.sfo); + psf.Open(pkg.sfo); - std::string content_id = psf.GetString("CONTENT_ID"); + std::string content_id{*psf.GetString("CONTENT_ID")}; std::string entitlement_label = Common::SplitString(content_id, '-')[2]; auto addon_extract_path = Common::FS::GetUserPath(Common::FS::PathType::AddonsDir) / @@ -647,9 +647,11 @@ void MainWindow::InstallDragDropPkg(std::filesystem::path file, int pkgNum, int auto category = psf.GetString("CATEGORY"); if (pkgType.contains("PATCH")) { - QString pkg_app_version = QString::fromStdString(psf.GetString("APP_VER")); - psf.open(extract_path.string() + "/sce_sys/param.sfo", {}); - QString game_app_version = QString::fromStdString(psf.GetString("APP_VER")); + QString pkg_app_version = + QString::fromStdString(std::string{*psf.GetString("APP_VER")}); + psf.Open(extract_path / "sce_sys" / "param.sfo"); + QString game_app_version = + QString::fromStdString(std::string{*psf.GetString("APP_VER")}); double appD = game_app_version.toDouble(); double pkgD = pkg_app_version.toDouble(); if (pkgD == appD) { diff --git a/src/qt_gui/pkg_viewer.cpp b/src/qt_gui/pkg_viewer.cpp index 49005c72..d41d37db 100644 --- a/src/qt_gui/pkg_viewer.cpp +++ b/src/qt_gui/pkg_viewer.cpp @@ -109,12 +109,12 @@ void PKGViewer::ProcessPKGInfo() { path = std::filesystem::path(m_pkg_list[i].toStdWString()); #endif package.Open(path); - psf.open("", package.sfo); - QString title_name = QString::fromStdString(psf.GetString("TITLE")); - QString title_id = QString::fromStdString(psf.GetString("TITLE_ID")); - QString app_type = game_list_util.GetAppType(psf.GetInteger("APP_TYPE")); - QString app_version = QString::fromStdString(psf.GetString("APP_VER")); - QString title_category = QString::fromStdString(psf.GetString("CATEGORY")); + psf.Open(package.sfo); + QString title_name = QString::fromStdString(std::string{*psf.GetString("TITLE")}); + QString title_id = QString::fromStdString(std::string{*psf.GetString("TITLE_ID")}); + QString app_type = game_list_util.GetAppType(*psf.GetInteger("APP_TYPE")); + QString app_version = QString::fromStdString(std::string{*psf.GetString("APP_VER")}); + QString title_category = QString::fromStdString(std::string{*psf.GetString("CATEGORY")}); QString pkg_size = game_list_util.FormatSize(package.GetPkgHeader().pkg_size); pkg_content_flag = package.GetPkgHeader().pkg_content_flags; QString flagss = ""; @@ -126,7 +126,7 @@ void PKGViewer::ProcessPKGInfo() { } } - u32 fw_int = psf.GetInteger("SYSTEM_VER"); + u32 fw_int = *psf.GetInteger("SYSTEM_VER"); QString fw = QString::number(fw_int, 16); QString fw_ = fw.length() > 7 ? QString::number(fw_int, 16).left(3).insert(2, '.') : fw.left(3).insert(1, '.'); diff --git a/src/qt_gui/pkg_viewer.h b/src/qt_gui/pkg_viewer.h index 9598328a..265a03b9 100644 --- a/src/qt_gui/pkg_viewer.h +++ b/src/qt_gui/pkg_viewer.h @@ -33,7 +33,6 @@ private: PKGHeader pkgheader; PKGEntry entry; PSFHeader header; - PSFEntry psfentry; char pkgTitleID[9]; std::vector pkg; u64 pkgSize = 0; diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp index 9ff332ae..b99dfdbb 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.cpp +++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp @@ -4,6 +4,7 @@ #include #include "common/assert.h" #include "common/debug.h" +#include "imgui/renderer/texture_manager.h" #include "video_core/renderer_vulkan/vk_instance.h" #include "video_core/renderer_vulkan/vk_scheduler.h" @@ -190,6 +191,7 @@ void Scheduler::SubmitExecution(SubmitInfo& info) { }; try { + ImGui::Core::TextureManager::Submit(); instance.GetGraphicsQueue().submit(submit_info, info.fence); } catch (vk::DeviceLostError& err) { UNREACHABLE_MSG("Device lost during submit: {}", err.what());