From e2ee6d3057f1ae3634955b75e490a5450e981c2d Mon Sep 17 00:00:00 2001 From: Eladash Date: Fri, 16 Jun 2023 12:51:56 +0300 Subject: [PATCH] LV2/FS: Implement timeless file time --- rpcs3/Emu/Cell/lv2/sys_fs.cpp | 182 +++++++++++++++-- rpcs3/Emu/VFS.cpp | 17 +- rpcs3/Emu/VFS.h | 2 +- rpcs3/Emu/associative_map_file.cpp | 305 +++++++++++++++++++++++++++++ rpcs3/Emu/associative_map_file.h | 64 ++++++ rpcs3/emucore.vcxproj | 2 + rpcs3/emucore.vcxproj.filters | 6 + 7 files changed, 561 insertions(+), 17 deletions(-) create mode 100644 rpcs3/Emu/associative_map_file.cpp create mode 100644 rpcs3/Emu/associative_map_file.h diff --git a/rpcs3/Emu/Cell/lv2/sys_fs.cpp b/rpcs3/Emu/Cell/lv2/sys_fs.cpp index 47cb6e06c657..447ad3c44020 100644 --- a/rpcs3/Emu/Cell/lv2/sys_fs.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_fs.cpp @@ -13,6 +13,7 @@ #include "Emu/IdManager.h" #include "Emu/system_utils.hpp" #include "Emu/Cell/lv2/sys_process.h" +#include "Emu/associative_map_file.h" #include @@ -121,6 +122,120 @@ bool verify_mself(const fs::file& mself_file) return true; } +void init_fxo_vfs(); + +struct lv2_file_alternative_timestamps +{ + std::map> m_map; + + shared_mutex m_mutex; + + lv2_file_alternative_timestamps() noexcept + { + init_fxo_vfs(); + g_fxo->init(); + } + + struct file_times + { + s64 atime; + s64 mtime; + }; + + static constexpr std::string_view s_version = "1"; + + std::pair get_times(std::string_view filename) + { + if (filename.empty()) + { + return {}; + } + + auto [mp_times, file_key] = get_keys(filename); + + if (!mp_times) + { + return {}; + } + + file_times times{}; + + const std::string data = mp_times->get_entry(file_key); + + usz a_index = data.find("atime: "sv); + usz m_index = data.find("mtime: "sv); + + if (a_index != umax) + { + if (std::from_chars(data.data() + a_index + std::size("atime: "sv), data.data() + data.size(), times.atime).ec != std::errc()) + { + return {}; + } + } + + if (m_index != umax) + { + if (std::from_chars(data.data() + m_index + std::size("mtime: "sv), data.data() + data.size(), times.mtime).ec != std::errc()) + { + return {}; + } + } + + times.ctime = file_times.mtime; + return {true, times}; + } + + bool set_times(std::string_view filename, file_times times) + { + if (filename.empty()) + { + return false; + } + + const auto [mp_times, file_key] = get_keys(filename); + + if (!mp_times) + { + return false; + } + + mp_times->set_entry(file_key, fmt::format("version: %s, mtime: %d, atime: %d.", s_version, times.mtime, times.atime)); + return true; + } + + std::pair get_keys(std::string view filename) const + { + std::string root = lv2_fs_object::get_device_root(filename); + std::string vfs_root = vfs::get(root); + + if (!m_map.contains(vfs_root)) + { + m_map.emplace(vfs_root, associative_map_file(vfs_root + u8"/$vfs-data")); + } + + return {&m_map.at(vfs_root), "file-timestamp-" + vfs::get(filename).substr(vfs_root.size())}; + } + + bool move_times(std::string_view source, std::string_view dest) + { + const auto [mp_times_source, file_key_sorce] = get_keys(source); + const auto [mp_times_dest, file_key_dest] = get_keys(filename); + + if (mp_times_dest != mp_times_source) + { + return false; + } + + return mp_times_source->move_entries_with_prefix(source, dest); + } + + bool remove_times(std::string_view filename) + { + const auto [mp_times, file_key] = get_keys(filename); + return mp_times->remove_entry(file_key); + } +}; + lv2_fs_mount_info_map::lv2_fs_mount_info_map() { for (auto mp = &g_mp_sys_dev_root; mp; mp = mp->next) // Scan and keep track of pre-mounted devices @@ -1310,6 +1425,16 @@ error_code sys_fs_opendir(ppu_thread& ppu, vm::cptr path, vm::ptr fd) continue; } + if (!mp.read_only) + { + if (auto [ok, times] = g_fxo->get().get_times(std::string(vpath) + "/" + data.back().name); ok) + { + data.back().atime = times.atime; + data.back().mtime = times.mtime; + data.back().ctime = times.mtime; + } + } + // Add additional entries for split file candidates (while ends with .66600) while (data.back().name.ends_with(".66600")) { @@ -1504,6 +1629,16 @@ error_code sys_fs_stat(ppu_thread& ppu, vm::cptr path, vm::ptr } } + if (!mp.read_only) + { + if (auto [ok, times] = g_fxo->get().get_times(vpath); ok) + { + info.atime = times.atime; + info.mtime = times.mtime; + info.ctime = times.mtime; + } + } + lock.unlock(); ppu.check_state(); @@ -1553,7 +1688,18 @@ error_code sys_fs_fstat(ppu_thread& ppu, u32 fd, vm::ptr sb) return CELL_EIO; } - const fs::stat_t info = file->file.stat(); + fs::stat_t info = file->file.stat(); + + if (!mp.read_only) + { + if (auto [ok, times] = g_fxo->get().get_times(vfs::retrieve(vpath)); ok) + { + info.atime = times.atime; + info.mtime = times.mtime; + info.ctime = times.mtime; + } + } + lock.unlock(); ppu.check_state(); @@ -1687,10 +1833,9 @@ error_code sys_fs_rename(ppu_thread& ppu, vm::cptr from, vm::cptr to return CELL_EROFS; } - // Done in vfs::host::rename - //std::lock_guard lock(mp->mutex); + std::lock_guard lock(mp->mutex); - if (!vfs::host::rename(local_from, local_to, mp.mp, false)) + if (!vfs::host::rename(local_from, local_to, mp.mp, false, false)) { switch (auto error = fs::g_tls_error) { @@ -1702,6 +1847,8 @@ error_code sys_fs_rename(ppu_thread& ppu, vm::cptr from, vm::cptr to return {CELL_EIO, from}; // ??? } + g_fxo->get().move_times(from, to); + sys_fs.notice("sys_fs_rename(): %s renamed to %s", from, to); return CELL_OK; } @@ -1753,6 +1900,8 @@ error_code sys_fs_rmdir(ppu_thread& ppu, vm::cptr path) return {CELL_EIO, path}; // ??? } + g_fxo->get().remove_times(vpath); + sys_fs.notice("sys_fs_rmdir(): directory %s removed", path); return CELL_OK; } @@ -1812,6 +1961,8 @@ error_code sys_fs_unlink(ppu_thread& ppu, vm::cptr path) return {CELL_EIO, path}; // ??? } + ensure(g_fxo->get().remove_times(vpath)); + sys_fs.notice("sys_fs_unlink(): file %s deleted", path); return CELL_OK; } @@ -2884,22 +3035,25 @@ error_code sys_fs_utime(ppu_thread& ppu, vm::cptr path, vm::cptractime; + const s64 modtime = timep->modtime; + std::lock_guard lock(mp->mutex); - if (!fs::utime(local_path, timep->actime, timep->modtime)) + // Not actually required to succeed + if (fs::utime(local_path, actime, modtime)) { - switch (auto error = fs::g_tls_error) - { - case fs::error::noent: - { - return {mp == &g_mp_sys_dev_hdd1 ? sys_fs.warning : sys_fs.error, CELL_ENOENT, path}; - } - default: sys_fs.error("sys_fs_utime(): unknown error %s", error); - } + g_fxo->get().remove_times(vpath); + return CELL_OK; + } - return {CELL_EIO, path}; // ??? + if (!fs::exists(local_path)) + { + return {mp == &g_mp_sys_dev_hdd1 ? sys_fs.warning : sys_fs.erro, CELL_ENOENT, path}; } + ensure(g_fxo->get().set_times(vpath, {.atime = actime, .mtime = modtime})); + return CELL_OK; } diff --git a/rpcs3/Emu/VFS.cpp b/rpcs3/Emu/VFS.cpp index 9fe7cb214d14..f88d57f45e95 100644 --- a/rpcs3/Emu/VFS.cpp +++ b/rpcs3/Emu/VFS.cpp @@ -35,6 +35,11 @@ struct vfs_manager SAVESTATE_INIT_POS(48); }; +extern void init_fxo_vfs() +{ + g_fxo->need(); +} + bool vfs::mount(std::string_view vpath, std::string_view path, bool is_dir) { if (vpath.empty()) @@ -927,13 +932,16 @@ std::string vfs::host::hash_path(const std::string& path, const std::string& dev return fmt::format(u8"%s/$%s%s", dev_root, fmt::base57(std::hash()(path)), fmt::base57(utils::get_unique_tsc())); } -bool vfs::host::rename(const std::string& from, const std::string& to, const lv2_fs_mount_point* mp, bool overwrite) +bool vfs::host::rename(const std::string& from, const std::string& to, const lv2_fs_mount_point* mp, bool overwrite, bool unlocked) noexcept { // Lock mount point, close file descriptors, retry const auto from0 = std::string_view(from).substr(0, from.find_last_not_of(fs::delim) + 1); const auto escaped_from = Emu.GetCallbacks().resolve_path(from); - std::lock_guard lock(mp->mutex); + if (!unlocked) + { + mp->mutex.lock(); + } auto check_path = [&](std::string_view path) { @@ -1007,6 +1015,11 @@ bool vfs::host::rename(const std::string& from, const std::string& to, const lv2 } }); + if (!unlocked) + { + mp->mutex.unlock(); + } + fs::g_tls_error = fs_error; return res; } diff --git a/rpcs3/Emu/VFS.h b/rpcs3/Emu/VFS.h index 0f69a2b6976a..176bb79a2519 100644 --- a/rpcs3/Emu/VFS.h +++ b/rpcs3/Emu/VFS.h @@ -34,7 +34,7 @@ namespace vfs std::string hash_path(const std::string& path, const std::string& dev_root); // Call fs::rename with retry on access error - bool rename(const std::string& from, const std::string& to, const lv2_fs_mount_point* mp, bool overwrite); + bool rename(const std::string& from, const std::string& to, const lv2_fs_mount_point* mp, bool overwrite, bool unlocked = false); // Delete file without deleting its contents, emulated with MoveFileEx on Windows bool unlink(const std::string& path, const std::string& dev_root); diff --git a/rpcs3/Emu/associative_map_file.cpp b/rpcs3/Emu/associative_map_file.cpp new file mode 100644 index 000000000000..8576bc0b37dc --- /dev/null +++ b/rpcs3/Emu/associative_map_file.cpp @@ -0,0 +1,305 @@ +#include "stdafx.h" +#include "associative_map_file.h" + +#include "util/yaml.hpp" +#include "Utilities/File.h" +#include "assoicative_map_file.h" + +associative_map_file::associative_map_file(std::string path) noexcept +{ + m_path = std::move(path); + load(); +} + +associative_map_file::~associative_map_file() noexcept +{ + if (m_dirty) + { + save(); + } +} + +const std::map> associative_map_file::get_all_entries() const +{ + reader_lock lock(m_mutex); + return m_map; +} + +std::string associative_map_file::get_entry(std::string_view key, bool unlocked) const noexcept +{ + if (key.empty()) + { + return {}; + } + + if (!unlocked) + { + m_mutex.lock(); + } + + std::result result; + + if (const auto it = std::as_const(m_map).find(key); it != m_map.cend()) + { + result = it->second; + } + + if (!unlocked) + { + m_mutex.unlock(); + } + + return result; +} + +void associative_map_file::move_entries_with_suffix(std::string_view old_prefix, std::string view new_prefix, bool unlocked) noexcept +{ + if (old_prefix.empty() || new_prefix.empty()) + { + return; + } + + if (old_prefix.starts_with(new_prefix) || new_prefix.starts_with(old_prefix)) + { + // Makes no sense for its purpose and requires rewriting the algorithm to use temporary storage + return; + } + + if (!unlocked) + { + m_mutex.lock(); + } + + // Step 1: clear all previously existing entries with new prefix + for (const auto it = std::lower_bound(m_map, new_prefix); it != m_map.end() && it->first.starts_with(new_prefix); it++) + { + if (!it->second.empty()) + { + m_dirty = true; + } + + it->second.clear(); + } + + // Step 2: move contents + for (const auto source_it = std::lower_bound(m_map, old_prefix); source_it != m_map.end() && source_it->first.starts_with(old_prefix); source_it++) + { + if (const auto it = m_map.find(dest_key); it != m_map.end()) + { + if (it->second == source_it->second) + { + continue; + } + + it->second = std::move(source_it->second); + m_dirty = true; + } + else + { + m_dirty = true; + m_map.emplace(std::string(dest_key), std::move(source_it->second)); + } + } + + save_nl(); + + if (!unlocked) + { + m_mutex.unlock(); + } +} + +bool associative_map_file::remove_entry(std::string_view key, bool unlocked) noexcept +{ + if (key.empty()) + { + return {}; + } + + if (!unlocked) + { + m_mutex.lock(); + } + + std::result result; + + if (const auto it = m_map.find(key); it != m_map.end()) + { + if (!it->second.empty()) + { + it->second.clear(); + + m_dirty = true; + save_nl(); + + if (!unlocked) + { + m_mutex.unlock(); + } + + return true; + } + } + + if (!unlocked) + { + m_mutex.unlock(); + } + + return false; +} + +bool associative_map_file::move_entry(std::string_view source_key, std::string_view dest_key, bool unlocked) noexcept +{ + if (source_key.empty() || dest_key.empty()) + { + return false; + } + + if (!unlocked) + { + m_mutex.lock(); + } + + if (source_key == dest_key) + { + if (!unlocked) + { + m_mutex.unlock(); + } + + return true; + } + + std::result data; + + if (const auto it = m_map.find(source_key); it != m_map.end()) + { + data = std::move(it->second); + } + + if (const auto it = m_map.find(dest_key); it != m_map.end()) + { + it->second = std::move(data); + } + else + { + m_map.emplace(std::string(dest_key), std::move(data)); + } + + m_dirty = true; + save_nl(); + + if (!unlocked) + { + m_mutex.unlock(); + } + + return true; +} + +bool associative_map_file::set_entry(const std::string& key, const std::string& data, bool unlocked) noexcept +{ + if (!unlocked) + { + m_mutex.lock(); + } + + // Access or create node if does not exist + if (auto it = m_map.find(key); it != m_map.end()) + { + if (it->second == data) + { + // Nothing to do + if (!unlocked) + { + m_mutex.unlock(); + } + + return true; + } + + it->second = data; + } + else + { + m_map.emplace(key, data); + } + + m_dirty = true; + save_nl(); + + if (!unlocked) + { + m_mutex.unlock(); + } + + return true; +} + +bool associative_map_file::save_nl() +{ + if (!m_save_on_dirty || !m_dirty) + { + return true; + } + + YAML::Emitter out; + out << m_map; + + fs::pending_file temp(m_path); + + if (temp.file && temp.file.write(out.c_str(), out.size()), temp.commit()) + { + m_dirty = false; + return true; + } + + return false; +} + +bool associative_map_file::save() +{ + std::lock_guard lock(m_mutex); + return save_nl(); +} + +const std::string& associative_map_file::get_file_path() const +{ + return m_path; +} + +void associative_map_file::load() +{ + std::lock_guard lock(m_mutex); + + m_map.clear(); + + if (fs::file f{m_path, fs::read + fs::create}) + { + auto [result, error] = yaml_load(f.to_string()); + + if (!error.empty()) + { + //cfg_log.error("Failed to load games.yml: %s", error); + } + + if (!result.IsMap()) + { + if (!result.IsNull()) + { + //cfg_log.error("Failed to load games.yml: type %d not a map", result.Type()); + } + return; + } + + for (const auto& entry : result) + { + if (!entry.first.Scalar().empty() && entry.second.IsScalar() && !entry.second.Scalar().empty()) + { + m_map.emplace(entry.first.Scalar(), entry.second.Scalar()); + } + } + } +} + diff --git a/rpcs3/Emu/associative_map_file.h b/rpcs3/Emu/associative_map_file.h new file mode 100644 index 000000000000..53b0f5aa550d --- /dev/null +++ b/rpcs3/Emu/associative_map_file.h @@ -0,0 +1,64 @@ +#pragma once + +#include "Utilities/mutex.h" +#include + +class associative_map_file +{ +public: + associative_map_file(std::string path); + ~associative_map_file(); + + void set_save_on_dirty(bool enabled) { m_save_on_dirty = enabled; } + + const std::map get_all_entries() const; + bool is_dirty() const { return m_dirty; } + + std::string get_entry(std::string_view key, bool unlocked = false) const noexcept; + bool move_entry(std::string_view key, bool unlocked = false) noexcept; + usz move_entries_with_suffix(std::string_view suffix, bool unlocked = false) noexcept; + bool remove_entry(std::string_view key, bool unlocked = false) noexcept; + + template + std::string get_entry(std::string_view key, Func&& func, Args&&... args) const noexcept + { + if (key.empty()) + { + return {}; + } + + reader_lock lock(m_mutex); + + std::string data = get_entry(key); + + if (data.empty()) + { + return std::invoke(std::forward(func), std::forward(args)...); + } + + return data; + } + + template + auto atomic_op(Func&& func, Args&&... args) const noexcept + { + std::lock_guard lock(m_mutex); + return std::forward(func)(std::forward(args)...); + } + + bool set_entry(const std::string& key, const std::string& data, bool unlocked = false) noexcept; + bool save(); + + const std::string& get_file_path() const; +private: + bool save_nl(); + void load(); + + std::map> m_map; + mutable shared_mutex m_mutex; + + bool m_dirty = false; + bool m_save_on_dirty = true; + + std::string m_path; +}; diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index 7298809bc156..a2e40c9599f9 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -54,6 +54,7 @@ + true @@ -483,6 +484,7 @@ + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index a3505ea837f7..0ab94f0f496a 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -1165,6 +1165,9 @@ Emu\Io + + Emu + @@ -2368,6 +2371,9 @@ Emu\GPU\RSX\Common + + Emu +