Rewrite Save Data & Impl Save Data Dialog (#824)

* core: Rewrite PSF parser & add encoder

add .sfo hex pattern to /scripts

* core/fs: allow to mount path as read-only

* common: Add CString wrapper to handle native null-terminated strings

* SaveData: rewrite to implement full functionality

* mock value for SYSTEM_VER

* SavaData: backup features

* SavaData: SaveDataMemory features

* imgui Ref-counted textures

- has a background thread to decode textures

* imgui: rework gamepad navigation

* PSF: fixed psf not using enum class for PSFEntryFmt (was a standard old ugly enum)

- Add null check to CString when itself is used in a nullable field

* SaveDataDialog implementation

- Fix Mounting/Unmounting check of SaveInstance
This commit is contained in:
Vinicius Rangel 2024-09-20 06:34:19 -03:00 committed by GitHub
parent 077f8981a7
commit 0f4bcd8c83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 4919 additions and 1134 deletions

View file

@ -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

View file

@ -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<auto fmt, auto size> {
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<auto KeyTableOffset, auto DataTableOffset> {
u64 begin = $;
IndexEntry index;
KeyEntry key @ KeyTableOffset + index.key_offset;
DataEntry<index.param_fmt, index.param_len> 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<header.key_table_offset, header.data_table_offset> list[header.index_table_entries] @ 0x14;

133
src/common/cstring.h Normal file
View file

@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string_view>
#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 <size_t N, typename T = char>
class CString {
T data[N]{};
public:
class Iterator;
CString() = default;
template <size_t M>
explicit CString(const CString<M>& other)
requires(M <= N)
{
std::ranges::copy(other.begin(), other.end(), data);
}
void FromString(const std::basic_string_view<T>& 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<T>() const {
if (this == nullptr) {
return {};
}
return std::basic_string_view<T>{data};
}
explicit operator std::basic_string<T>() const {
if (this == nullptr) {
return {};
}
return std::basic_string<T>{data};
}
std::basic_string<T> to_string() const {
if (this == nullptr) {
return {};
}
return std::basic_string<T>{data};
}
std::basic_string_view<T> to_view() const {
if (this == nullptr) {
return {};
}
return std::basic_string_view<T>{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<CString<13>::Iterator>);
} // namespace Common

View file

@ -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

View file

@ -219,4 +219,6 @@ private:
uintptr_t file_mapping = 0;
};
u64 GetDirectorySize(const std::filesystem::path& path);
} // namespace Common::FS

View file

@ -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<char>(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<std::string> SplitString(const std::string& str, char delimiter) {
std::istringstream iss(str);
std::vector<std::string> output(1);

View file

@ -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<std::string> SplitString(const std::string& str, char delimiter);

View file

@ -2,61 +2,275 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#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<std::string_view, u32> 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::filesystem::path& filepath) {
if (std::filesystem::exists(filepath)) {
last_write = std::filesystem::last_write_time(filepath);
}
bool PSF::open(const std::string& filepath, const std::vector<u8>& 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);
std::vector<u8> psf(psfSize);
file.Seek(0);
file.Read(psf);
file.Close();
return Open(psf);
}
bool PSF::Open(const std::vector<u8>& 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));
for (u32 i = 0; i < header.index_table_entries; i++) {
PSFEntry entry;
std::memcpy(&entry, &psf[sizeof(PSFHeader) + i * sizeof(PSFEntry)], sizeof(entry));
PSFHeader header{};
std::memcpy(&header, psf_data, sizeof(header));
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 (header.magic != PSF_MAGIC) {
LOG_ERROR(Core, "Invalid PSF magic number");
return false;
}
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;
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<PSFEntryFmt>(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<u8> 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<const char*>(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);
}
return "";
bool PSF::Encode(const std::filesystem::path& filepath) const {
Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Write);
if (!file.IsOpen()) {
return false;
}
u32 PSF::GetInteger(const std::string& key) {
if (map_integers.find(key) != map_integers.end()) {
return map_integers.at(key);
last_write = std::filesystem::file_time_type::clock::now();
const auto psf_buffer = Encode();
return file.Write(psf_buffer) == psf_buffer.size();
}
return -1;
std::vector<u8> PSF::Encode() const {
std::vector<u8> psf_buffer;
Encode(psf_buffer);
return psf_buffer;
}
void PSF::Encode(std::vector<u8>& 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<u16>(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<const u8*>(&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<std::span<const u8>> 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<std::string_view> 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<s32> 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<u8> 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<std::vector<PSF::Entry>::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<std::vector<PSF::Entry>::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)};
}

View file

@ -3,11 +3,18 @@
#pragma once
#include <filesystem>
#include <span>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#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<u8>& 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<u8>& psf_buffer);
std::unordered_map<std::string, std::string> map_strings;
std::unordered_map<std::string, u32> map_integers;
[[nodiscard]] std::vector<u8> Encode() const;
void Encode(std::vector<u8>& buf) const;
bool Encode(const std::filesystem::path& filepath) const;
std::optional<std::span<const u8>> GetBinary(std::string_view key) const;
std::optional<std::string_view> GetString(std::string_view key) const;
std::optional<s32> GetInteger(std::string_view key) const;
void AddBinary(std::string key, std::vector<u8> 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<Entry>& GetEntries() const {
return entry_list;
}
private:
std::vector<u8> psf;
mutable std::filesystem::file_time_type last_write;
std::vector<Entry> entry_list;
std::unordered_map<size_t, std::vector<u8>> map_binaries;
std::unordered_map<size_t, std::string> map_strings;
std::unordered_map<size_t, s32> map_integers;
[[nodiscard]] std::pair<std::vector<Entry>::iterator, size_t> FindEntry(std::string_view key);
[[nodiscard]] std::pair<std::vector<Entry>::const_iterator, size_t> FindEntry(
std::string_view key) const;
};

View file

@ -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;

View file

@ -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};

View file

@ -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<PSF>::Instance();
std::optional<s32> 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<PSF>::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)) {

View file

@ -179,11 +179,16 @@ int PS4_SYSV_ABI sceKernelUnlink(const char* path) {
auto* h = Common::Singleton<Core::FileSys::HandleTable>::Instance();
auto* mnt = Common::Singleton<Core::FileSys::MntPoints>::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<Core::FileSys::MntPoints>::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<Core::FileSys::MntPoints>::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<Core::FileSys::MntPoints>::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) {

View file

@ -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<PSF>::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;

View file

@ -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);

View file

@ -31,10 +31,6 @@ public:
void Finish();
void Draw() override;
bool ShouldGrabGamepad() override {
return false;
}
};
}; // namespace Libraries::NpTrophy

View file

@ -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<SaveDialogState::ProgressBarState>().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<SaveDialogState::ProgressBarState>().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

View file

@ -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

View file

@ -0,0 +1,802 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fmt/chrono.h>
#include <imgui.h>
#include <magic_enum.hpp>
#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<PSF>::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<FocusPos>(state->focus_pos)) {
auto pos = std::get<FocusPos>(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<std::string>(state->focus_pos)) {
auto dir_name = std::get<std::string>(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<SaveDialogState::UserState>();
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<float>(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<SaveDialogState::SystemState>();
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<float>(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<SaveDialogState::ErrorCodeState>();
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<SaveDialogState::ProgressBarState>();
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<float>(bar_state.progress) / 100.0f,
{PROGRESS_BAR_WIDTH * ws.x, BUTTON_SIZE.y});
}
}; // namespace Libraries::SaveData::Dialog

View file

@ -0,0 +1,317 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#include <variant>
#include <vector>
#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<u8, 32> _reserved;
};
struct SaveDialogNewItem {
const char* title;
void* iconBuf;
size_t iconSize;
std::array<u8, 32> _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<u8, 32> _reserved;
};
struct UserMessageParam {
ButtonType buttonType;
UserMessageType msgType;
const char* msg;
std::array<u8, 32> _reserved;
};
struct SystemMessageParam {
SystemMessageType msgType;
s32 : 32;
u64 value;
std::array<u8, 32> _reserved;
};
struct ErrorCodeParam {
u32 errorCode;
std::array<u8, 32> _reserved;
};
struct ProgressBarParam {
ProgressBarType barType;
s32 : 32;
const char* msg;
ProgressSystemMessageType sysMsgType;
std::array<u8, 28> _reserved;
};
struct OptionParam {
OptionBack back;
std::array<u8, 32> _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<u8, 24> _reserved;
};
struct OrbisSaveDataDialogResult {
SaveDataDialogMode mode{};
CommonDialog::Result result{};
ButtonId buttonId{};
s32 : 32;
OrbisSaveDataDirName* dirName;
OrbisSaveDataParam* param;
void* userData;
std::array<u8, 32> _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<bool> enable_back{};
OrbisUserServiceUserId user_id{};
std::string title_id{};
std::vector<Item> save_list{};
std::variant<FocusPos, std::string, std::monostate> focus_pos{std::monostate{}};
ItemStyle style{};
std::optional<Item> new_item{};
std::variant<UserState, SystemState, ErrorCodeState, ProgressBarState, std::monostate> state{
std::monostate{}};
public:
explicit SaveDialogState(const OrbisSaveDataDialogParam& param);
SaveDialogState() = default;
[[nodiscard]] SaveDataDialogMode GetMode() const {
return mode;
}
template <typename T>
[[nodiscard]] T& GetState() {
return std::get<T>(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

View file

@ -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;

View file

@ -0,0 +1,207 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <deque>
#include <mutex>
#include <semaphore>
#include <magic_enum.hpp>
#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<BackupRequest> g_backup_queue;
static std::deque<BackupRequest> 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<std::filesystem::path> 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<int>(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<BackupRequest> 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<float>(g_backup_progress) / 100.0f;
}
void ClearProgress() {
g_backup_progress = 0;
}
} // namespace Libraries::SaveData::Backup

View file

@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#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<BackupRequest> PopLastEvent();
void PushBackupEvent(BackupRequest&& req);
float GetProgress();
void ClearProgress();
} // namespace Backup
} // namespace Libraries::SaveData

View file

@ -0,0 +1,228 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <iostream>
#include <magic_enum.hpp>
#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<Core::FileSys::MntPoints>::Instance();
namespace fs = std::filesystem;
// clang-format off
static const std::unordered_map<std::string, std::string> 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

View file

@ -0,0 +1,138 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#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

View file

@ -0,0 +1,287 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "save_memory.h"
#include <condition_variable>
#include <filesystem>
#include <mutex>
#include <utility>
#include <fmt/format.h>
#include <core/libraries/system/msgdialog_ui.h>
#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<Core::FileSys::MntPoints>::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<u8> g_save_memory;
static std::filesystem::path g_icon_path;
static std::vector<u8> 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<u8>(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<u8> 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<u8>(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<u8>(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<u8>(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<u8> 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

View file

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#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<u8> 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

File diff suppressed because it is too large Load diff

View file

@ -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<OrbisSaveDataTitleIdDataSize> data;
std::array<char, 6> _pad;
};
struct OrbisSaveDataDirName {
char data[ORBIS_SAVE_DATA_DIRNAME_DATA_MAXSIZE];
Common::CString<OrbisSaveDataDirnameDataMaxsize> 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<OrbisSaveDataTitleMaxsize> title;
Common::CString<OrbisSaveDataSubtitleMaxsize> subTitle;
Common::CString<OrbisSaveDataDetailMaxsize> detail;
u32 userParam;
int : 32;
time_t mtime;
u8 reserved[32];
std::array<u8, 32> _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,12 +95,12 @@ 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,
Error PS4_SYSV_ABI sceSaveDataDirNameSearch(const OrbisSaveDataDirNameSearchCond* cond,
OrbisSaveDataDirNameSearchResult* result);
int PS4_SYSV_ABI sceSaveDataDirNameSearchInternal();
int PS4_SYSV_ABI sceSaveDataDownload();
@ -292,36 +114,36 @@ 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,
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,
Error 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 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,
Error PS4_SYSV_ABI sceSaveDataLoadIcon(const OrbisSaveDataMountPoint* mountPoint,
OrbisSaveDataIcon* icon);
int PS4_SYSV_ABI sceSaveDataMount(const OrbisSaveDataMount* mount,
Error PS4_SYSV_ABI sceSaveDataMount(const OrbisSaveDataMount* mount,
OrbisSaveDataMountResult* mount_result);
s32 PS4_SYSV_ABI sceSaveDataMount2(const OrbisSaveDataMount2* mount,
Error PS4_SYSV_ABI sceSaveDataMount2(const OrbisSaveDataMount2* mount,
OrbisSaveDataMountResult* mount_result);
int PS4_SYSV_ABI sceSaveDataMount5();
int PS4_SYSV_ABI sceSaveDataMountInternal();
@ -329,33 +151,33 @@ 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,
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,
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,
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();

View file

@ -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;
}

View file

@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <thread>
#include <imgui.h>
#include "common/assert.h"
#include "imgui/imgui_std.h"
@ -31,18 +33,6 @@ struct {
};
static_assert(std::size(user_button_texts) == static_cast<int>(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<MsgDialogState::UserState>();
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<u32>(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();
@ -270,3 +280,15 @@ void MsgDialogUi::Draw() {
first_render = false;
}
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;
}

View file

@ -3,6 +3,7 @@
#pragma once
#include <string>
#include <variant>
#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

View file

@ -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

View file

@ -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

View file

@ -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<PSF>::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<PlaygoFile>::Instance();

View file

@ -27,3 +27,6 @@ extern void assert_fail_debug_msg(const char* msg);
#define IM_VEC2_CLASS_EXTRA \
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) {}

View file

@ -12,10 +12,6 @@ public:
static void RemoveLayer(Layer* layer);
virtual void Draw() = 0;
virtual bool ShouldGrabGamepad() {
return false;
}
};
} // namespace ImGui

View file

@ -3,12 +3,25 @@
#pragma once
#include <cmath>
#include <imgui.h>
#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

45
src/imgui/imgui_texture.h Normal file
View file

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include <imgui.h>
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<u8> 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

View file

@ -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();

View file

@ -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;
}
}

View file

@ -4,6 +4,8 @@
// Based on imgui_impl_vulkan.cpp from Dear ImGui repository
#include <cstdio>
#include <mutex>
#include <imgui.h>
#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

View file

@ -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);

View file

@ -0,0 +1,243 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <deque>
#include <utility>
#include <externals/stb_image.h>
#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<u8> 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<u8> 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<Job> g_job_list;
static std::mutex g_upload_mtx;
static std::deque<UploadJob> 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<u8>(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<u8> 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

View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include <vector>
#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<u8> data, Inner* core);
void DecodePngFile(std::filesystem::path path, Inner* core);
void Submit();
}; // namespace ImGui::Core::TextureManager

View file

@ -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;
}

View file

@ -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++;
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;
}
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)));
tableWidget->setItem(row, 0, keyItem);
tableWidget->setItem(row, 1, valueItem);

View file

@ -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) {

View file

@ -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, '.');

View file

@ -33,7 +33,6 @@ private:
PKGHeader pkgheader;
PKGEntry entry;
PSFHeader header;
PSFEntry psfentry;
char pkgTitleID[9];
std::vector<u8> pkg;
u64 pkgSize = 0;

View file

@ -4,6 +4,7 @@
#include <mutex>
#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());