rpcsx/rpcs3/Emu/savestate_utils.cpp
Elad 575a245f8d
IDM: Implement lock-free smart pointers (#16403)
Replaces `std::shared_pointer` with `stx::atomic_ptr` and `stx::shared_ptr`.

Notes to programmers:

* This pr kills the use of `dynamic_cast`, `std::dynamic_pointer_cast` and `std::weak_ptr` on IDM objects, possible replacement is to save the object ID on the base object, then use idm::check/get_unlocked to the destination type via the saved ID which may be null. Null pointer check is how you can tell type mismatch (as dynamic cast) or object destruction (as weak_ptr locking).
* Double-inheritance on IDM objects should be used with care, `stx::shared_ptr` does not support constant-evaluated pointer offsetting to parent/child type.
* `idm::check/get_unlocked` can now be used anywhere.

Misc fixes:
* Fixes some segfaults with RPCN with interaction with IDM.
* Fix deadlocks in access violation handler due locking recursion.
* Fixes race condition in process exit-spawn on memory containers read.
* Fix bug that theoretically can prevent RPCS3 from booting - fix `id_manager::typeinfo` comparison to compare members instead of `memcmp` which can fail spuriously on padding bytes.
* Ensure all IDM inherited types of base, either has `id_base` or `id_type` defined locally, this allows to make getters such as `idm::get_unlocked<lv2_socket, lv2_socket_raw>()` which were broken before. (requires save-states invalidation)
* Removes broken operator[] overload of `stx::shared_ptr` and `stx::single_ptr` for non-array types.
2024-12-22 20:59:48 +02:00

481 lines
12 KiB
C++

#include "stdafx.h"
#include "util/types.hpp"
#include "util/logs.hpp"
#include "util/asm.hpp"
#include "util/v128.hpp"
#include "util/simd.hpp"
#include "Utilities/File.h"
#include "Utilities/StrFmt.h"
#include "system_config.h"
#include "savestate_utils.hpp"
#include "System.h"
#include <set>
#include <any>
#include <span>
LOG_CHANNEL(sys_log, "SYS");
struct serial_ver_t
{
std::string ver_name;
bool used = false;
u16 current_version = 0;
std::set<u16> compatible_versions;
};
static std::array<serial_ver_t, 27> s_serial_versions;
#define SERIALIZATION_VER(name, identifier, ...) \
\
const bool s_##name##_serialization_fill = []() { auto& e = ::s_serial_versions[identifier]; if (e.compatible_versions.empty()) { e.compatible_versions = {__VA_ARGS__}; e.ver_name = #name; } return true; }();\
\
extern void using_##name##_serialization()\
{\
ensure(Emu.IsStopped());\
::s_serial_versions[identifier].used = true;\
}\
\
extern s32 get_##name##_serialization_version()\
{\
return ::s_serial_versions[identifier].current_version;\
}
SERIALIZATION_VER(global_version, 0, 18) // For stuff not listed here
SERIALIZATION_VER(ppu, 1, 1, 2/*PPU sleep order*/, 3/*PPU FNID and module*/)
SERIALIZATION_VER(spu, 2, 1)
SERIALIZATION_VER(lv2_sync, 3, 1)
SERIALIZATION_VER(lv2_vm, 4, 1)
SERIALIZATION_VER(lv2_net, 5, 1, 2/*TCP Feign conection loss*/)
SERIALIZATION_VER(lv2_fs, 6, 1, 2/*NPDRM key saving*/)
SERIALIZATION_VER(lv2_prx_overlay, 7, 1)
SERIALIZATION_VER(lv2_memory, 8, 1)
SERIALIZATION_VER(lv2_config, 9, 1)
namespace rsx
{
SERIALIZATION_VER(rsx, 10, 1, 2/*Pending flip*/, 3/*avconf scan_mode*/)
}
namespace np
{
SERIALIZATION_VER(sceNp, 11, 1)
}
#ifdef _MSC_VER
// Compiler bug, lambda function body does seem to inherit used namespace atleast for function declaration
SERIALIZATION_VER(rsx, 10)
SERIALIZATION_VER(sceNp, 11)
#endif
SERIALIZATION_VER(cellVdec, 12, 1)
SERIALIZATION_VER(cellAudio, 13, 1)
SERIALIZATION_VER(cellCamera, 14, 1)
SERIALIZATION_VER(cellGem, 15, 1, 2/*calibration_status_flags*/)
SERIALIZATION_VER(sceNpTrophy, 16, 1)
SERIALIZATION_VER(cellMusic, 17, 1)
SERIALIZATION_VER(cellVoice, 18, 1)
SERIALIZATION_VER(cellGcm, 19, 1)
SERIALIZATION_VER(sysPrxForUser, 20, 1)
SERIALIZATION_VER(cellSaveData, 21, 1)
SERIALIZATION_VER(cellAudioOut, 22, 1)
SERIALIZATION_VER(sys_io, 23, 2)
// Misc versions for HLE/LLE not included so main version would not invalidated
SERIALIZATION_VER(LLE, 24, 1)
SERIALIZATION_VER(HLE, 25, 1)
SERIALIZATION_VER(cellSysutil, 26, 1, 2/*AVC2 Muting,Volume*/)
template <>
void fmt_class_string<std::remove_cvref_t<decltype(s_serial_versions)>>::format(std::string& out, u64 arg)
{
bool is_first = true;
const auto& serials = get_object(arg);
out += "{ ";
for (auto& entry : serials)
{
if (entry.current_version)
{
if (!is_first)
{
out += ", ";
}
is_first = false;
fmt::append(out, "%s=%d", entry.ver_name, entry.current_version);
}
}
out += " }";
}
std::vector<version_entry> get_savestate_versioning_data(fs::file&& file, std::string_view filepath)
{
if (!file)
{
return {};
}
file.seek(0);
utils::serial ar;
ar.set_reading_state({}, true);
if (filepath.ends_with(".zst"))
{
ar.m_file_handler = make_compressed_zstd_serialization_file_handler(std::move(file));
}
else if (filepath.ends_with(".gz"))
{
ar.m_file_handler = make_compressed_serialization_file_handler(std::move(file));
}
else
{
ar.m_file_handler = make_uncompressed_serialization_file_handler(std::move(file));
}
if (u64 r = 0; ar.try_read(r) != 0 || r != "RPCS3SAV"_u64)
{
return {};
}
ar.pos = 10;
u64 offs = ar.try_read<u64>().second;
const usz fsize = ar.get_size(offs);
if (!offs || fsize <= offs)
{
return {};
}
ar.seek_pos(offs);
ar.breathe(true);
std::vector<version_entry> ver_data = ar.pop<std::vector<version_entry>>();
return ver_data;
}
bool is_savestate_version_compatible(const std::vector<version_entry>& data, bool is_boot_check)
{
if (data.empty())
{
return false;
}
bool ok = true;
if (is_boot_check)
{
for (auto& entry : s_serial_versions)
{
// Version 0 means that the entire constructor using the version should be skipped
entry.current_version = 0;
}
}
auto& channel = (is_boot_check ? sys_log.error : sys_log.trace);
for (const auto& entry : data)
{
if (entry.type >= s_serial_versions.size())
{
channel("Savestate version identifier is unknown! (category=%u, version=%u)", entry.type, entry.version);
ok = false; // Log all mismatches
}
else if (!s_serial_versions[entry.type].compatible_versions.count(entry.version))
{
channel("Savestate version is not supported. (category=%u, version=%u)", entry.type, entry.version);
ok = false;
}
else if (is_boot_check)
{
s_serial_versions[entry.type].current_version = entry.version;
}
}
if (is_boot_check)
{
if (ok)
{
sys_log.success("Savestate versions: %s", s_serial_versions);
}
else
{
for (auto& entry : s_serial_versions)
{
entry.current_version = 0;
}
}
}
return ok;
}
std::string get_savestate_file(std::string_view title_id, std::string_view boot_path, s64 abs_id, s64 rel_id)
{
const std::string title = std::string{title_id.empty() ? boot_path.substr(boot_path.find_last_of(fs::delim) + 1) : title_id};
if (abs_id == -1 && rel_id == -1)
{
// Return directory
return fs::get_config_dir() + "savestates/" + title + "/";
}
ensure(rel_id < 0 || abs_id >= 0, "Unimplemented!");
const std::string save_id = fmt::format("%d", abs_id);
// Make sure that savestate file with higher IDs are placed at the bottom of "by name" file ordering in directory view by adding a single character prefix which tells the ID length
// While not needing to keep a 59 chars long suffix at all times for this purpose
const char prefix = ::at32("0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"sv, save_id.size());
std::string path = fs::get_config_dir() + "/savestates/" + title + "/" + title + '_' + prefix + '_' + save_id + ".SAVESTAT";
if (std::string path_compressed = path + ".zst"; fs::is_file(path_compressed))
{
return path_compressed;
}
if (std::string path_compressed = path + ".gz"; fs::is_file(path_compressed))
{
return path_compressed;
}
return path;
}
bool is_savestate_compatible(fs::file&& file, std::string_view filepath)
{
return is_savestate_version_compatible(get_savestate_versioning_data(std::move(file), filepath), false);
}
std::vector<version_entry> read_used_savestate_versions()
{
std::vector<version_entry> used_serial;
used_serial.reserve(s_serial_versions.size());
for (serial_ver_t& ver : s_serial_versions)
{
if (std::exchange(ver.used, false))
{
used_serial.push_back(version_entry{static_cast<u16>(&ver - s_serial_versions.data()), *ver.compatible_versions.rbegin()});
}
ver.current_version = 0;
}
return used_serial;
}
bool boot_last_savestate(bool testing)
{
if (!g_cfg.savestate.suspend_emu && !Emu.GetTitleID().empty() && (Emu.IsRunning() || Emu.GetStatus() == system_state::paused))
{
const std::string save_dir = get_savestate_file(Emu.GetTitleID(), Emu.GetBoot(), -1, -1);
std::string savestate_path;
s64 mtime = smin;
for (auto&& entry : fs::dir(save_dir))
{
if (entry.is_directory)
{
continue;
}
// Find the latest savestate file compatible with the game (TODO: Check app version and anything more)
if (entry.name.find(Emu.GetTitleID()) != umax && mtime <= entry.mtime)
{
if (std::string path = save_dir + entry.name + ".zst"; is_savestate_compatible(fs::file(path), path))
{
savestate_path = std::move(path);
mtime = entry.mtime;
}
else if (std::string path = save_dir + entry.name + ".gz"; is_savestate_compatible(fs::file(path), path))
{
savestate_path = std::move(path);
mtime = entry.mtime;
}
else if (std::string path = save_dir + entry.name; is_savestate_compatible(fs::file(path), path))
{
savestate_path = std::move(path);
mtime = entry.mtime;
}
}
}
const bool result = fs::is_file(savestate_path);
if (testing)
{
sys_log.trace("boot_last_savestate(true) returned %s.", result);
return result;
}
if (result)
{
sys_log.success("Booting the most recent savestate \'%s\' using the Reload shortcut.", savestate_path);
Emu.GracefulShutdown(false);
if (game_boot_result error = Emu.BootGame(savestate_path, "", true); error != game_boot_result::no_errors)
{
sys_log.error("Failed to boot savestate \'%s\' using the Reload shortcut. (error: %s)", savestate_path, error);
}
else
{
return true;
}
}
sys_log.error("No compatible savestate file found in \'%s\''", save_dir);
}
return false;
}
bool load_and_check_reserved(utils::serial& ar, usz size)
{
u8 bytes[4096];
ensure(size < std::size(bytes));
std::memset(&bytes[size & (0 - sizeof(v128))], 0, sizeof(v128));
const usz old_pos = ar.pos;
ar(std::span<u8>(bytes, size));
// Check if all are 0
for (usz i = 0; i < size; i += sizeof(v128))
{
if (v128::loadu(&bytes[i]) != v128{})
{
return false;
}
}
return old_pos + size == ar.pos;
}
namespace stx
{
extern u16 serial_breathe_and_tag(utils::serial& ar, std::string_view name, bool tag_bit)
{
thread_local std::string_view s_tls_object_name = "none";
thread_local u64 s_tls_call_count = 1, s_tls_current_pos = 0;
if (s_tls_current_pos >= ar.pos)
{
// Reset, probably a new utils::serial object
s_tls_call_count = 1;
}
s_tls_current_pos = ar.pos;
constexpr u16 data_mask = 0x7fff;
if (ar.m_file_handler && ar.m_file_handler->is_null())
{
return (tag_bit ? data_mask + 1 : 0);
}
u16 tag = static_cast<u16>((static_cast<u16>(ar.pos / 2) & data_mask) | (tag_bit ? data_mask + 1 : 0));
u16 saved = tag;
ar(saved);
sys_log.trace("serial_breathe_and_tag(%u): %s, object: '%s', next-object: '%s', expected/tag: 0x%x == 0x%x", s_tls_call_count, ar, s_tls_object_name, name, tag, saved);
if ((saved ^ tag) & data_mask)
{
ensure(!ar.is_writing());
fmt::throw_exception("serial_breathe_and_tag(%u): %s, object: '%s', next-object: '%s', expected/tag: 0x%x != 0x%x,", s_tls_call_count, ar, s_tls_object_name, name, tag, saved);
}
s_tls_object_name = name;
s_tls_call_count++;
ar.breathe();
return saved;
}
}
// MSVC bug workaround, see above similar case
extern u16 serial_breathe_and_tag(utils::serial& ar, std::string_view name, bool tag_bit)
{
return ::stx::serial_breathe_and_tag(ar, name, tag_bit);
}
[[noreturn]] void hle_locks_t::lock()
{
// Unreachable
fmt::throw_exception("Unreachable");
}
bool hle_locks_t::try_lock()
{
while (true)
{
auto [old, success] = lock_val.fetch_op([](s64& value)
{
if (value >= 0)
{
value++;
return true;
}
return false;
});
if (success)
{
return true;
}
if (old == finalized)
{
break;
}
lock_val.wait(old);
}
return false;
}
void hle_locks_t::unlock()
{
lock_val--;
}
bool hle_locks_t::try_finalize(std::function<bool()> test)
{
if (!test())
{
return false;
}
if (!lock_val.compare_and_swap_test(0, waiting_for_evaluation))
{
return false;
}
if (!test())
{
// Failed
ensure(lock_val.compare_and_swap_test(waiting_for_evaluation, 0));
return false;
}
ensure(lock_val.compare_and_swap_test(waiting_for_evaluation, finalized));
// Sanity check when debugging (the result is not expected to change after finalization)
//ensure(test());
lock_val.notify_all();
return true;
}