From 11050a70320a3c9ed8e11e7ecd82462aa93d17cd Mon Sep 17 00:00:00 2001 From: Vishrut Sachan Date: Sun, 12 Apr 2026 14:37:25 +0530 Subject: [PATCH 01/29] ISO: Add metadata cache to speed up game list scanning (#18546) Every launch constructs a fresh iso_archive for each ISO game, which calls iso_form_hierarchy() and walks the full directory tree. On top of that, qt_utils opens a second iso_archive just for icon loading, so every ISO game ends up doing two full directory tree walks on every launch. This adds a metadata cache keyed by ISO path + mtime stored under fs::get_config_dir()/iso_cache/. Each entry stores the raw SFO binary, resolved icon/movie/audio paths and raw icon bytes. - On cache hit, iso_archive construction is skipped entirely for both game list scanning and icon loading - On cache miss archive is scanned as before and the result is persisted to disk - Cache is automatically invalidated when the ISO file's mtime changes Tested with a decrypted PS3 disc ISO (God of War III): - First launch writes cache files correctly to iso_cache/ - Second launch reads from cache with correct title and icon - touch game.iso correctly invalidates the cache and triggers a rescan --- rpcs3/Emu/CMakeLists.txt | 1 + rpcs3/Loader/iso_cache.cpp | 162 ++++++++++++++++++++++++++++++ rpcs3/Loader/iso_cache.h | 32 ++++++ rpcs3/emucore.vcxproj | 1 + rpcs3/rpcs3qt/game_list_frame.cpp | 108 ++++++++++++++++++-- rpcs3/rpcs3qt/game_list_frame.h | 1 + rpcs3/rpcs3qt/qt_utils.cpp | 10 ++ 7 files changed, 307 insertions(+), 8 deletions(-) create mode 100644 rpcs3/Loader/iso_cache.cpp create mode 100644 rpcs3/Loader/iso_cache.h diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index d691952fa0..c20b72f694 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -126,6 +126,7 @@ target_sources(rpcs3_emu PRIVATE ../Loader/PUP.cpp ../Loader/TAR.cpp ../Loader/ISO.cpp + ../Loader/iso_cache.cpp ../Loader/TROPUSR.cpp ../Loader/TRP.cpp ) diff --git a/rpcs3/Loader/iso_cache.cpp b/rpcs3/Loader/iso_cache.cpp new file mode 100644 index 0000000000..a90222a240 --- /dev/null +++ b/rpcs3/Loader/iso_cache.cpp @@ -0,0 +1,162 @@ +#include "stdafx.h" + +#include "iso_cache.h" +#include "Loader/PSF.h" +#include "util/yaml.hpp" +#include "util/fnv_hash.hpp" +#include "Utilities/File.h" + +#include + +LOG_CHANNEL(iso_cache_log, "ISOCache"); + +namespace +{ + std::string get_cache_dir() + { + const std::string dir = fs::get_cache_dir() + "cache/iso_cache/"; + fs::create_path(dir); + return dir; + } + + // FNV-64 hash of the ISO path used as the cache filename stem. + std::string get_cache_stem(const std::string& iso_path) + { + usz hash = rpcs3::fnv_seed; + for (const char c : iso_path) + { + hash ^= static_cast(c); + hash *= rpcs3::fnv_prime; + } + return fmt::format("%016llx", hash); + } +} + +namespace iso_cache +{ + bool load(const std::string& iso_path, iso_metadata_cache_entry& out_entry) + { + fs::stat_t iso_stat{}; + if (!fs::get_stat(iso_path, iso_stat) || iso_stat.is_directory) + { + return false; + } + + const std::string stem = get_cache_stem(iso_path); + const std::string dir = get_cache_dir(); + const std::string yml_path = dir + stem + ".yml"; + const std::string sfo_path = dir + stem + ".sfo"; + const std::string png_path = dir + stem + ".png"; + + const fs::file yml_file(yml_path); + if (!yml_file) + { + return false; + } + + auto [node, error] = yaml_load(yml_file.to_string()); + if (!error.empty()) + { + iso_cache_log.warning("Failed to parse cache YAML for '%s': %s", iso_path, error); + return false; + } + + // Reject stale entries. + const s64 cached_mtime = node["mtime"].as(0); + if (cached_mtime != iso_stat.mtime) + { + return false; + } + + const fs::file sfo_file(sfo_path); + if (!sfo_file) + { + return false; + } + + out_entry.mtime = cached_mtime; + out_entry.psf_data = sfo_file.to_vector(); + out_entry.icon_path = node["icon_path"].as(""); + out_entry.movie_path = node["movie_path"].as(""); + out_entry.audio_path = node["audio_path"].as(""); + + // Icon bytes are optional — game may have no icon. + if (const fs::file png_file(png_path); png_file) + { + out_entry.icon_data = png_file.to_vector(); + } + + return true; + } + + void save(const std::string& iso_path, const iso_metadata_cache_entry& entry) + { + const std::string stem = get_cache_stem(iso_path); + const std::string dir = get_cache_dir(); + const std::string yml_path = dir + stem + ".yml"; + const std::string sfo_path = dir + stem + ".sfo"; + const std::string png_path = dir + stem + ".png"; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "mtime" << YAML::Value << static_cast(entry.mtime); + out << YAML::Key << "icon_path" << YAML::Value << entry.icon_path; + out << YAML::Key << "movie_path" << YAML::Value << entry.movie_path; + out << YAML::Key << "audio_path" << YAML::Value << entry.audio_path; + out << YAML::EndMap; + + if (fs::pending_file yml_file(yml_path); yml_file.file) + { + yml_file.file.write(out.c_str(), out.size()); + yml_file.commit(); + } + else + { + iso_cache_log.warning("Failed to write cache YAML for '%s'", iso_path); + } + + if (!entry.psf_data.empty()) + { + if (fs::pending_file sfo_file(sfo_path); sfo_file.file) + { + sfo_file.file.write(entry.psf_data); + sfo_file.commit(); + } + } + + if (!entry.icon_data.empty()) + { + if (fs::pending_file png_file(png_path); png_file.file) + { + png_file.file.write(entry.icon_data); + png_file.commit(); + } + } + } + + void cleanup(const std::unordered_set& valid_iso_paths) + { + const std::string dir = get_cache_dir(); + + // Build a set of stems that should exist. + std::unordered_set valid_stems; + for (const std::string& path : valid_iso_paths) + { + valid_stems.insert(get_cache_stem(path)); + } + + // Delete any cache files whose stem is not in the valid set. + fs::dir cache_dir(dir); + fs::dir_entry entry{}; + while (cache_dir.read(entry)) + { + if (entry.name == "." || entry.name == "..") continue; + + const std::string stem = entry.name.substr(0, entry.name.find_last_of('.')); + if (valid_stems.find(stem) == valid_stems.end()) + { + fs::remove_file(dir + entry.name); + } + } + } +} diff --git a/rpcs3/Loader/iso_cache.h b/rpcs3/Loader/iso_cache.h new file mode 100644 index 0000000000..ca2f39e6a4 --- /dev/null +++ b/rpcs3/Loader/iso_cache.h @@ -0,0 +1,32 @@ +#pragma once + +#include "Loader/PSF.h" +#include "Utilities/File.h" +#include "util/types.hpp" + +#include +#include +#include + +// Cached metadata extracted from an ISO during game list scanning. +struct iso_metadata_cache_entry +{ + s64 mtime = 0; + std::vector psf_data{}; + std::string icon_path{}; + std::vector icon_data{}; + std::string movie_path{}; + std::string audio_path{}; +}; + +namespace iso_cache +{ + // Returns false if no valid cache entry exists or mtime has changed. + bool load(const std::string& iso_path, iso_metadata_cache_entry& out_entry); + + // Persists a populated cache entry to disk. + void save(const std::string& iso_path, const iso_metadata_cache_entry& entry); + + // Remove cache entries for ISOs that are no longer in the scanned set. + void cleanup(const std::unordered_set& valid_iso_paths); +} \ No newline at end of file diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index 42aab47a04..c89a066075 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -551,6 +551,7 @@ + diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 36e26cf43d..d2af663b7d 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -16,6 +16,7 @@ #include "Emu/system_utils.hpp" #include "Loader/PSF.h" #include "Loader/ISO.h" +#include "Loader/iso_cache.h" #include "util/types.hpp" #include "Utilities/File.h" #include "util/sysinfo.hpp" @@ -530,14 +531,29 @@ void game_list_frame::OnParsingFinished() (const std::string& dir_or_elf) { std::unique_ptr archive; - if (is_file_iso(dir_or_elf)) + iso_metadata_cache_entry cache_entry{}; + const bool is_iso = is_file_iso(dir_or_elf); + + if (is_iso) { - archive = std::make_unique(dir_or_elf); + // Only construct iso_archive (which walks the full directory tree) + // when no valid cache entry exists for this ISO path + mtime. + if (!iso_cache::load(dir_or_elf, cache_entry)) + { + archive = std::make_unique(dir_or_elf); + } + // Track this ISO path for cache cleanup after scan completes. + std::lock_guard lock(m_path_mutex); + m_scanned_iso_paths.insert(dir_or_elf); } - const auto file_exists = [&archive](const std::string& path) + const auto file_exists = [&archive, &cache_entry](const std::string& path) { - return archive ? archive->is_file(path) : fs::is_file(path); + if (archive) return archive->is_file(path); + // On cache hit, paths inside the ISO are not accessible via fs::is_file. + // Return false here — cache hit paths are handled separately. + if (!cache_entry.psf_data.empty()) return false; + return fs::is_file(path); }; gui_game_info game{}; @@ -545,10 +561,34 @@ void game_list_frame::OnParsingFinished() const Localized thread_localized; - const std::string sfo_dir = archive ? "PS3_GAME" : rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf); + const std::string sfo_dir = (archive || !cache_entry.psf_data.empty()) ? "PS3_GAME" : rpcs3::utils::get_sfo_dir_from_game_path(dir_or_elf); const std::string sfo_path = sfo_dir + "/PARAM.SFO"; - const psf::registry psf = archive ? archive->open_psf(sfo_path) : psf::load_object(sfo_path); + // Load PSF: from archive on cache miss, rehydrate from cached SFO bytes on hit. + psf::registry psf{}; + if (archive) + { + psf = archive->open_psf(sfo_path); + } + else if (!cache_entry.psf_data.empty()) + { + psf = psf::load_object(fs::make_stream>(std::vector(cache_entry.psf_data)), sfo_path); + // Fallback to archive scan if cached PSF is corrupted or missing critical fields. + const bool psf_valid = !psf::get_string(psf, "TITLE_ID", "").empty() + && !psf::get_string(psf, "TITLE", "").empty() + && !psf::get_string(psf, "CATEGORY", "").empty(); + if (!psf_valid) + { + archive = std::make_unique(dir_or_elf); + psf = archive->open_psf(sfo_path); + cache_entry = {}; // Reset so the cache gets rewritten after scan. + } + } + else + { + psf = psf::load_object(sfo_path); + } + const std::string_view title_id = psf::get_string(psf, "TITLE_ID", ""); if (title_id.empty()) @@ -616,19 +656,32 @@ void game_list_frame::OnParsingFinished() if (game.info.icon_path.empty()) { - if (std::string icon_path = sfo_dir + "/" + localized_icon; file_exists(icon_path)) + if (!cache_entry.icon_path.empty()) + { + // Cache hit — icon path already resolved on a previous scan. + game.info.icon_path = cache_entry.icon_path; + game.icon_in_archive = true; + } + else if (std::string icon_path = sfo_dir + "/" + localized_icon; file_exists(icon_path)) { game.info.icon_path = std::move(icon_path); + game.icon_in_archive = archive && archive->exists(game.info.icon_path); } else { game.info.icon_path = sfo_dir + "/ICON0.PNG"; + game.icon_in_archive = archive && archive->exists(game.info.icon_path); } - game.icon_in_archive = archive && archive->exists(game.info.icon_path); } if (play_hover_movies) { + if (!cache_entry.movie_path.empty() && !archive) + { + // Cache hit — restore previously resolved movie path. + game.info.movie_path = cache_entry.movie_path; + game.has_hover_pam = true; + } if (std::string movie_path = game_icon_path + game.info.serial + "/hover.gif"; file_exists(movie_path)) { game.info.movie_path = std::move(movie_path); @@ -648,6 +701,12 @@ void game_list_frame::OnParsingFinished() if (play_hover_music) { + if(!cache_entry.audio_path.empty() && !archive) + { + // Cache hit — restore previously resolved audio path. + game.info.audio_path = cache_entry.audio_path; + game.has_audio_file = true; + } if (std::string audio_path = sfo_dir + "/SND0.AT3"; file_exists(audio_path)) { game.info.audio_path = std::move(audio_path); @@ -655,6 +714,35 @@ void game_list_frame::OnParsingFinished() } } + // On cache miss for an ISO, persist the resolved metadata so subsequent + // launches skip iso_archive construction entirely. + if (archive && is_iso) + { + fs::stat_t iso_stat{}; + if (fs::get_stat(dir_or_elf, iso_stat)) + { + cache_entry.mtime = iso_stat.mtime; + cache_entry.psf_data = psf::save_object(psf); + cache_entry.icon_path = game.info.icon_path; + cache_entry.movie_path = game.info.movie_path; + cache_entry.audio_path = game.info.audio_path; + + // Cache raw icon bytes so load_iso_icon can skip archive open. + if (game.icon_in_archive) + { + auto icon_file = archive->open(game.info.icon_path); + const auto icon_size = icon_file.size(); + if (icon_size > 0) + { + cache_entry.icon_data.resize(icon_size); + icon_file.read(cache_entry.icon_data.data(), icon_size); + } + } + + iso_cache::save(dir_or_elf, cache_entry); + } + } + const QString serial = QString::fromStdString(game.info.serial); m_games_mutex.lock(); @@ -817,6 +905,9 @@ void game_list_frame::OnRefreshFinished() WaitAndAbortSizeCalcThreads(); WaitAndAbortRepaintThreads(); + // Remove cache entries for ISOs that are no longer present in the scanned paths. + iso_cache::cleanup(m_scanned_iso_paths); + for (auto&& g : m_games.pop_all()) { m_game_data.push_back(g); @@ -903,6 +994,7 @@ void game_list_frame::OnRefreshFinished() m_serials.clear(); m_path_list.clear(); m_path_entries.clear(); + m_scanned_iso_paths.clear(); Refresh(); diff --git a/rpcs3/rpcs3qt/game_list_frame.h b/rpcs3/rpcs3qt/game_list_frame.h index 637229bf60..b01dbd5a6e 100644 --- a/rpcs3/rpcs3qt/game_list_frame.h +++ b/rpcs3/rpcs3qt/game_list_frame.h @@ -181,6 +181,7 @@ private: std::vector m_path_entries; shared_mutex m_path_mutex; std::set m_path_list; + std::unordered_set m_scanned_iso_paths; QSet m_serials; QMutex m_games_mutex; lf_queue m_games; diff --git a/rpcs3/rpcs3qt/qt_utils.cpp b/rpcs3/rpcs3qt/qt_utils.cpp index ede9f6be9a..fe7bb6f8d6 100644 --- a/rpcs3/rpcs3qt/qt_utils.cpp +++ b/rpcs3/rpcs3qt/qt_utils.cpp @@ -13,6 +13,7 @@ #include "Emu/system_utils.hpp" #include "Utilities/File.h" #include "Loader/ISO.h" +#include "Loader/iso_cache.h" #include LOG_CHANNEL(gui_log, "GUI"); @@ -709,6 +710,15 @@ namespace gui if (icon_path.empty() || archive_path.empty()) return false; if (!is_file_iso(archive_path)) return false; + // Check cache first — avoids constructing a full iso_archive just for the icon. + iso_metadata_cache_entry cache_entry{}; + if (iso_cache::load(archive_path, cache_entry) && !cache_entry.icon_data.empty()) + { + const QByteArray data(reinterpret_cast(cache_entry.icon_data.data()), + static_cast(cache_entry.icon_data.size())); + return icon.loadFromData(data); + } + iso_archive archive(archive_path); if (!archive.exists(icon_path)) return false; From 6dc95dd078a0c5ad553b58465ddaef7853ccec52 Mon Sep 17 00:00:00 2001 From: luci4 <142809264+l00sy4@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:53:00 +0100 Subject: [PATCH 02/29] sysinfo.cpp: Remove registry helpers (#18557) The "caveat" is that said API bypasses compatibility shims so it will always show the true Windows version. --- rpcs3/util/sysinfo.cpp | 119 +++++------------------------------------ 1 file changed, 13 insertions(+), 106 deletions(-) diff --git a/rpcs3/util/sysinfo.cpp b/rpcs3/util/sysinfo.cpp index b894c9af0b..799d85aaca 100755 --- a/rpcs3/util/sysinfo.cpp +++ b/rpcs3/util/sysinfo.cpp @@ -78,91 +78,6 @@ namespace Darwin_ProcessInfo } #endif -#ifdef _WIN32 -#if !defined(ARCH_X64) -namespace utils -{ - // Some helpers for sanity - const auto read_reg_dword = [](HKEY hKey, std::string_view value_name) -> std::pair - { - DWORD val = 0; - DWORD len = sizeof(val); - if (ERROR_SUCCESS != RegQueryValueExA(hKey, value_name.data(), nullptr, nullptr, reinterpret_cast(&val), &len)) - { - return { false, 0 }; - } - return { true, val }; - }; - - const auto read_reg_sz = [](HKEY hKey, std::string_view value_name) -> std::pair - { - constexpr usz MAX_SZ_LEN = 255; - char sz[MAX_SZ_LEN + 1] {}; - DWORD sz_len = MAX_SZ_LEN; - - // Safety; null terminate - sz[0] = 0; - sz[MAX_SZ_LEN] = 0; - - // Read string - if (ERROR_SUCCESS != RegQueryValueExA(hKey, value_name.data(), nullptr, nullptr, reinterpret_cast(sz), &sz_len)) - { - return { false, "" }; - } - - // Safety, force null terminator - if (sz_len < MAX_SZ_LEN) - { - sz[sz_len] = 0; - } - return { true, sz }; - }; - - // Alternative way to read OS version using the registry. - static std::string get_fallback_windows_version() - { - HKEY hKey; - if (ERROR_SUCCESS != RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_READ, &hKey)) - { - return "Unknown Windows"; - } - - // ProductName (SZ) - Actual windows install name e.g Windows 10 Pro) - // CurrentMajorVersionNumber (DWORD) - e.g 10 for windows 10, 11 for windows 11 - // CurrentMinorVersionNumber (DWORD) - usually 0 for newer windows, pairs with major version - // CurrentBuildNumber (SZ) - Windows build number, e.g 19045, used to identify different releases like 23H2, 24H2, etc - // CurrentVersion (SZ) - NT kernel version, e.g 6.3 for Windows 10 - const auto [product_valid, product_name] = read_reg_sz(hKey, "ProductName"); - if (!product_valid) - { - RegCloseKey(hKey); - return "Unknown Windows"; - } - - const auto [check_major, version_major] = read_reg_dword(hKey, "CurrentMajorVersionNumber"); - const auto [check_minor, version_minor] = read_reg_dword(hKey, "CurrentMinorVersionNumber"); - const auto [check_build_no, build_no] = read_reg_sz(hKey, "CurrentBuildNumber"); - const auto [check_nt_ver, nt_ver] = read_reg_sz(hKey, "CurrentVersion"); - - // Close the registry key - RegCloseKey(hKey); - - std::string version_id = "Unknown"; - if (check_major && check_minor && check_build_no) - { - version_id = fmt::format("%u.%u.%s", version_major, version_minor, build_no); - if (check_nt_ver) - { - version_id += " NT" + nt_ver; - } - } - - return fmt::format("Operating system: %s, Version %s", product_name, version_id); - } -} -#endif -#endif - bool utils::has_ssse3() { #if defined(ARCH_X64) @@ -834,33 +749,25 @@ std::string utils::get_OS_version_string() { std::string output; #ifdef _WIN32 - // GetVersionEx is deprecated, RtlGetVersion is kernel-mode only and AnalyticsInfo is UWP only. - // So we're forced to read PEB instead to get Windows version info. It's ugly but works. -#if defined(ARCH_X64) - constexpr DWORD peb_offset = 0x60; - const INT_PTR peb = __readgsqword(peb_offset); - const DWORD version_major = *reinterpret_cast(peb + 0x118); - const DWORD version_minor = *reinterpret_cast(peb + 0x11c); - const WORD build = *reinterpret_cast(peb + 0x120); - const UNICODE_STRING service_pack = *reinterpret_cast(peb + 0x02E8); - const u64 compatibility_mode = *reinterpret_cast(peb + 0x02C8); // Two DWORDs, major & minor version + OSVERSIONINFOW osvi{}; + osvi.dwOSVersionInfoSize = sizeof(osvi); + RtlGetVersion(&osvi); - const bool has_sp = service_pack.Length > 0; - std::vector holder(service_pack.Length + 1, '\0'); + const bool has_sp = osvi.szCSDVersion[0] != L'\0'; + std::vector holder; if (has_sp) { - WideCharToMultiByte(CP_UTF8, 0, service_pack.Buffer, service_pack.Length, - static_cast(holder.data()), static_cast(holder.size()), nullptr, nullptr); + const int len = WideCharToMultiByte(CP_UTF8, 0, osvi.szCSDVersion, -1, + nullptr, 0, nullptr, nullptr); + holder.resize(len); + WideCharToMultiByte(CP_UTF8, 0, osvi.szCSDVersion, -1, + holder.data(), len, nullptr, nullptr); } fmt::append(output, - "Operating system: Windows, Major: %lu, Minor: %lu, Build: %u, Service Pack: %s, Compatibility mode: %llu", - version_major, version_minor, build, has_sp ? holder.data() : "none", compatibility_mode); -#else - // PEB cannot be easily accessed on ARM64, fall back to registry - static const auto s_windows_version = utils::get_fallback_windows_version(); - return s_windows_version; -#endif + "Operating system: Windows, Major: %lu, Minor: %lu, Build: %lu, Service Pack: %s", + osvi.dwMajorVersion, osvi.dwMinorVersion, osvi.dwBuildNumber, + has_sp ? holder.data() : "none"); #elif defined (__APPLE__) const int major_version = Darwin_Version::getNSmajorVersion(); const int minor_version = Darwin_Version::getNSminorVersion(); From 72fa4098dcdbaedaca9ba0ae858e9d4e23afd94a Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 7 Apr 2026 21:47:52 +0200 Subject: [PATCH 03/29] Add 3D screen size setting --- rpcs3/Emu/Cell/Modules/cellAvconfExt.cpp | 12 ++---------- rpcs3/Emu/system_config.h | 1 + rpcs3/rpcs3qt/emu_settings_type.h | 2 ++ rpcs3/rpcs3qt/settings_dialog.cpp | 2 ++ rpcs3/rpcs3qt/settings_dialog.ui | 12 ++++++++++++ 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/cellAvconfExt.cpp b/rpcs3/Emu/Cell/Modules/cellAvconfExt.cpp index 4851895537..f57e51d2da 100644 --- a/rpcs3/Emu/Cell/Modules/cellAvconfExt.cpp +++ b/rpcs3/Emu/Cell/Modules/cellAvconfExt.cpp @@ -524,19 +524,11 @@ error_code cellVideoOutGetScreenSize(u32 videoOut, vm::ptr screenSize) { // Return Playstation 3D display value // Some games call this function when 3D is enabled - *screenSize = 24.f; + *screenSize = static_cast(g_cfg.video.screen_size.get()); return CELL_OK; } - // TODO: Use virtual screen size -#ifdef _WIN32 - // HDC screen = GetDC(NULL); - // float diagonal = roundf(sqrtf((powf(float(GetDeviceCaps(screen, HORZSIZE)), 2) + powf(float(GetDeviceCaps(screen, VERTSIZE)), 2))) * 0.0393f); -#else - // TODO: Linux implementation, without using wx - // float diagonal = roundf(sqrtf((powf(wxGetDisplaySizeMM().GetWidth(), 2) + powf(wxGetDisplaySizeMM().GetHeight(), 2))) * 0.0393f); -#endif - + // Let's just return not set for now return CELL_VIDEO_OUT_ERROR_VALUE_IS_NOT_SET; } diff --git a/rpcs3/Emu/system_config.h b/rpcs3/Emu/system_config.h index f8c621ad25..befb64f282 100644 --- a/rpcs3/Emu/system_config.h +++ b/rpcs3/Emu/system_config.h @@ -157,6 +157,7 @@ struct cfg_root : cfg::node cfg::_bool force_hw_MSAA_resolve{ this, "Force Hardware MSAA Resolve", false, true }; cfg::_bool stereo_enabled{ this, "3D Display Enabled", false }; cfg::_enum stereo_render_mode{ this, "3D Display Mode", stereo_render_mode_options::disabled, true }; + cfg::_int<10, 99> screen_size{ this, "Screen size in inches", 24, false }; cfg::_bool debug_program_analyser{ this, "Debug Program Analyser", false }; cfg::_bool precise_zpass_count{ this, "Accurate ZCULL stats", true }; cfg::_int<1, 8> consecutive_frames_to_draw{ this, "Consecutive Frames To Draw", 1, true}; diff --git a/rpcs3/rpcs3qt/emu_settings_type.h b/rpcs3/rpcs3qt/emu_settings_type.h index bc2f1e5cce..4824719769 100644 --- a/rpcs3/rpcs3qt/emu_settings_type.h +++ b/rpcs3/rpcs3qt/emu_settings_type.h @@ -82,6 +82,7 @@ enum class emu_settings_type ShaderPrecisionQuality, StereoRenderEnabled, StereoRenderMode, + ScreenSize, AnisotropicFilterOverride, TextureLodBias, ResolutionScale, @@ -295,6 +296,7 @@ inline static const std::map settings_location { emu_settings_type::DisableFIFOReordering, { "Video", "Disable FIFO Reordering"}}, { emu_settings_type::StereoRenderEnabled, { "Video", "3D Display Enabled"}}, { emu_settings_type::StereoRenderMode, { "Video", "3D Display Mode"}}, + { emu_settings_type::ScreenSize, { "Video", "Screen size in inches"}}, { emu_settings_type::StrictTextureFlushing, { "Video", "Strict Texture Flushing"}}, { emu_settings_type::ForceCPUBlitEmulation, { "Video", "Force CPU Blit"}}, { emu_settings_type::DisableOnDiskShaderCache, { "Video", "Disable On-Disk Shader Cache"}}, diff --git a/rpcs3/rpcs3qt/settings_dialog.cpp b/rpcs3/rpcs3qt/settings_dialog.cpp index 8ff2da2018..6787caa665 100644 --- a/rpcs3/rpcs3qt/settings_dialog.cpp +++ b/rpcs3/rpcs3qt/settings_dialog.cpp @@ -581,6 +581,7 @@ settings_dialog::settings_dialog(std::shared_ptr gui_settings, std // 3D m_emu_settings->EnhanceComboBox(ui->stereoRenderMode, emu_settings_type::StereoRenderMode); m_emu_settings->EnhanceCheckBox(ui->stereoRenderEnabled, emu_settings_type::StereoRenderEnabled); + m_emu_settings->EnhanceSpinBox(ui->sb_screen_size, emu_settings_type::ScreenSize); SubscribeTooltip(ui->gb_stereo, tooltips.settings.stereo_render_mode); if (game) { @@ -591,6 +592,7 @@ settings_dialog::settings_dialog(std::shared_ptr gui_settings, std const bool stereo_enabled = ui->stereoRenderEnabled->checkState() == Qt::CheckState::Checked; ui->stereoRenderMode->setEnabled(stereo_allowed && stereo_enabled); ui->stereoRenderEnabled->setEnabled(stereo_allowed); + ui->gb_screen_size->setEnabled(stereo_allowed && stereo_enabled); }; connect(ui->resBox, &QComboBox::currentIndexChanged, this, [enable_3D_modes](int){ enable_3D_modes(); }); connect(ui->stereoRenderEnabled, &QCheckBox::checkStateChanged, this, [enable_3D_modes](Qt::CheckState){ enable_3D_modes(); }); diff --git a/rpcs3/rpcs3qt/settings_dialog.ui b/rpcs3/rpcs3qt/settings_dialog.ui index 6c8c14fc42..9e23f48f1a 100644 --- a/rpcs3/rpcs3qt/settings_dialog.ui +++ b/rpcs3/rpcs3qt/settings_dialog.ui @@ -557,6 +557,18 @@ + + + + Screen Size (Inch) + + + + + + + + From bcd9663349f1e2404976188d9997878ea036bc93 Mon Sep 17 00:00:00 2001 From: luci4 <142809264+l00sy4@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:47:44 +0100 Subject: [PATCH 04/29] Thread.cpp: Added stack trace and register logging to exception filter (#18564) --- Utilities/Thread.cpp | 33 ++++++++++++++++++++++++++++++- Utilities/stack_trace.cpp | 41 ++++++++++++++++++++++++++++----------- Utilities/stack_trace.h | 10 ++++++++++ rpcs3/Emu/CMakeLists.txt | 2 +- 4 files changed, 73 insertions(+), 13 deletions(-) diff --git a/Utilities/Thread.cpp b/Utilities/Thread.cpp index b22c1aeb52..4ec70b58bb 100644 --- a/Utilities/Thread.cpp +++ b/Utilities/Thread.cpp @@ -25,6 +25,7 @@ #include #include +#include "stack_trace.h" #include "util/dyn_lib.hpp" DYNAMIC_IMPORT_RENAME("Kernel32.dll", SetThreadDescriptionImport, "SetThreadDescription", HRESULT(HANDLE hThread, PCWSTR lpThreadDescription)); @@ -1981,9 +1982,39 @@ static LONG exception_filter(PEXCEPTION_POINTERS pExp) noexcept } fmt::append(msg, "RPCS3 image base: %p.\n", GetModuleHandle(NULL)); + +#if defined(ARCH_X64) + fmt::append(msg, "RAX: %016llX RBX: %016llX\n", pExp->ContextRecord->Rax, pExp->ContextRecord->Rbx); + fmt::append(msg, "RCX: %016llX RDX: %016llX\n", pExp->ContextRecord->Rcx, pExp->ContextRecord->Rdx); + fmt::append(msg, "RSI: %016llX RDI: %016llX\n", pExp->ContextRecord->Rsi, pExp->ContextRecord->Rdi); + fmt::append(msg, "RBP: %016llX RSP: %016llX\n", pExp->ContextRecord->Rbp, pExp->ContextRecord->Rsp); + fmt::append(msg, "R8: %016llX R9: %016llX\n", pExp->ContextRecord->R8, pExp->ContextRecord->R9); + fmt::append(msg, "R10: %016llX R11: %016llX\n", pExp->ContextRecord->R10, pExp->ContextRecord->R11); + fmt::append(msg, "R12: %016llX R13: %016llX\n", pExp->ContextRecord->R12, pExp->ContextRecord->R13); + fmt::append(msg, "R14: %016llX R15: %016llX\n", pExp->ContextRecord->R14, pExp->ContextRecord->R15); + fmt::append(msg, "RFLAGS: %08X\n", pExp->ContextRecord->EFlags); +#elif defined(ARCH_ARM64) + for (int i = 0; i < 29; i += 2) + { + if (i + 1 < 29) + fmt::append(msg, "X%-2d: %016llX X%-2d: %016llX\n", i, pExp->ContextRecord->X[i], i + 1, pExp->ContextRecord->X[i + 1]); + else + fmt::append(msg, "X%-2d: %016llX\n", i, pExp->ContextRecord->X[i]); + } + fmt::append(msg, "SP: %016llX FP: %016llX LR: %016llX\n", pExp->ContextRecord->Sp, pExp->ContextRecord->Fp, pExp->ContextRecord->Lr); + fmt::append(msg, "CPSR: %08X\n", pExp->ContextRecord->Cpsr); +#endif - // TODO: print registers and the callstack + const auto stack_trace = utils::get_backtrace(64, pExp->ContextRecord); + const auto stack_symbols = utils::get_backtrace_symbols(stack_trace); + msg += "Stack Trace:\n"; + + for (const auto& symbol : stack_symbols) + { + fmt::append(msg, "%s\n", symbol); + } + sys_log.fatal("\n%s", msg); logs::listener::sync_all(); diff --git a/Utilities/stack_trace.cpp b/Utilities/stack_trace.cpp index f44751fcda..049e0f1805 100644 --- a/Utilities/stack_trace.cpp +++ b/Utilities/stack_trace.cpp @@ -30,42 +30,61 @@ namespace utils return out.data(); } - std::vector get_backtrace(int max_depth) + std::vector get_backtrace(int max_depth, PCONTEXT ctx) { + static struct sym_initer_t + { + sym_initer_t() noexcept + { + SymInitialize(GetCurrentProcess(), NULL, TRUE); + } + ~sym_initer_t() noexcept + { + SymCleanup(GetCurrentProcess()); + } + } s_initer{}; + std::vector result = {}; const auto hProcess = ::GetCurrentProcess(); const auto hThread = ::GetCurrentThread(); CONTEXT context{}; - RtlCaptureContext(&context); + if (ctx) + context = *ctx; + else + RtlCaptureContext(&context); STACKFRAME64 stack = {}; stack.AddrPC.Mode = AddrModeFlat; stack.AddrStack.Mode = AddrModeFlat; stack.AddrFrame.Mode = AddrModeFlat; #if defined(ARCH_X64) + const DWORD machineType = IMAGE_FILE_MACHINE_AMD64; stack.AddrPC.Offset = context.Rip; stack.AddrStack.Offset = context.Rsp; stack.AddrFrame.Offset = context.Rbp; #elif defined(ARCH_ARM64) + const DWORD machineType = IMAGE_FILE_MACHINE_ARM64; stack.AddrPC.Offset = context.Pc; stack.AddrStack.Offset = context.Sp; stack.AddrFrame.Offset = context.Fp; +#else +#error "Unsupported architecture" #endif while (max_depth--) { if (!StackWalk64( - IMAGE_FILE_MACHINE_AMD64, - hProcess, - hThread, - &stack, - &context, - NULL, - SymFunctionTableAccess64, - SymGetModuleBase64, - NULL)) + machineType, + hProcess, + hThread, + &stack, + &context, + NULL, + SymFunctionTableAccess64, + SymGetModuleBase64, + NULL)) { break; } diff --git a/Utilities/stack_trace.h b/Utilities/stack_trace.h index f57175611f..d0cec0cf4c 100644 --- a/Utilities/stack_trace.h +++ b/Utilities/stack_trace.h @@ -2,6 +2,11 @@ #include #include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif + namespace utils { namespace stack_trace @@ -30,7 +35,12 @@ namespace utils }; } +#ifdef _WIN32 + std::vector get_backtrace(int max_depth = 255, PCONTEXT ctx = nullptr); +#else std::vector get_backtrace(int max_depth = 255); +#endif + std::vector get_backtrace_symbols(const std::vector& stack); FORCE_INLINE void print_trace(stack_trace::Logger auto& logger, int max_depth = 255) diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index c20b72f694..8591399ce8 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -161,7 +161,7 @@ if(WIN32) Audio/XAudio2/xaudio2_enumerator.cpp ) target_compile_definitions(rpcs3_emu PRIVATE UNICODE _UNICODE _WIN32_WINNT=0x0A00) - target_link_libraries(rpcs3_emu PRIVATE pdh bcrypt) + target_link_libraries(rpcs3_emu PRIVATE pdh bcrypt dbghelp) endif() # Cell From 9e3e01d8f6eb2b12df6a6a1a45d02b3a22938e30 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Mon, 13 Apr 2026 10:47:18 +0200 Subject: [PATCH 05/29] input: fix mouse+kb combos --- rpcs3/Input/basic_mouse_handler.cpp | 5 ++- rpcs3/Input/keyboard_pad_handler.cpp | 13 +++--- rpcs3/Input/keyboard_pad_handler.h | 61 ++++++++++++++-------------- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/rpcs3/Input/basic_mouse_handler.cpp b/rpcs3/Input/basic_mouse_handler.cpp index 4ce238441b..d9171053b9 100644 --- a/rpcs3/Input/basic_mouse_handler.cpp +++ b/rpcs3/Input/basic_mouse_handler.cpp @@ -201,8 +201,11 @@ basic_mouse_handler::mouse_button basic_mouse_handler::get_mouse_button(const cf if (it != mouse_list.cend()) { + u32 btn = it->first; + if (btn >= mouse::button) btn -= mouse::button; + return mouse_button{ - .code = static_cast(it->first), + .code = static_cast(btn), .is_key = false }; } diff --git a/rpcs3/Input/keyboard_pad_handler.cpp b/rpcs3/Input/keyboard_pad_handler.cpp index e17598f41c..5e93ce64bd 100644 --- a/rpcs3/Input/keyboard_pad_handler.cpp +++ b/rpcs3/Input/keyboard_pad_handler.cpp @@ -572,7 +572,7 @@ void keyboard_pad_handler::mousePressEvent(QMouseEvent* event) return; } - Key(event->button(), true); + Key(mouse::button + static_cast(event->button()), true); event->ignore(); } @@ -583,7 +583,7 @@ void keyboard_pad_handler::mouseReleaseEvent(QMouseEvent* event) return; } - Key(event->button(), false, 0); + Key(mouse::button + static_cast(event->button()), false, 0); event->ignore(); } @@ -902,13 +902,13 @@ std::string keyboard_pad_handler::GetKeyName(const u32& keyCode) return QKeySequence(keyCode).toString(QKeySequence::NativeText).toStdString(); } -std::vector> keyboard_pad_handler::GetKeyCombos(const cfg::string& cfg_string) +std::vector> keyboard_pad_handler::GetKeyCombos(const std::string& cfg_string) { std::vector> res; - for (const pad::combo& combo : cfg_pad::get_combos(cfg_string.to_string())) + for (const pad::combo& combo : cfg_pad::get_combos(cfg_string)) { - std::set key_codes; + std::set key_codes = find_key_codes(mouse_list, combo); for (const std::string& button : combo.buttons()) { @@ -1062,8 +1062,7 @@ bool keyboard_pad_handler::bindPadToDevice(std::shared_ptr pad) const auto find_combos = [this](const cfg::string& name) { - std::vector> combos = find_key_combos(mouse_list, name); - for (const std::set& combo : GetKeyCombos(name)) combos.push_back(combo); + const std::vector> combos = GetKeyCombos(name.to_string()); if (!combos.empty()) { diff --git a/rpcs3/Input/keyboard_pad_handler.h b/rpcs3/Input/keyboard_pad_handler.h index 74c973f388..df9b7f5756 100644 --- a/rpcs3/Input/keyboard_pad_handler.h +++ b/rpcs3/Input/keyboard_pad_handler.h @@ -9,7 +9,7 @@ #include #include -enum mouse +enum mouse : u32 { move_left = 0x05555550, move_right = 0x05555551, @@ -18,40 +18,41 @@ enum mouse wheel_up = 0x05555554, wheel_down = 0x05555555, wheel_left = 0x05555556, - wheel_right = 0x05555557 + wheel_right = 0x05555557, + button = 0x80000000 }; // Unique button names for the config files and our pad settings dialog const std::unordered_map mouse_list = { { Qt::NoButton , "" }, - { Qt::LeftButton , "Mouse Left" }, - { Qt::RightButton , "Mouse Right" }, - { Qt::MiddleButton , "Mouse Middle" }, - { Qt::BackButton , "Mouse Back" }, - { Qt::ForwardButton , "Mouse Fwd" }, - { Qt::TaskButton , "Mouse Task" }, - { Qt::ExtraButton4 , "Mouse 4" }, - { Qt::ExtraButton5 , "Mouse 5" }, - { Qt::ExtraButton6 , "Mouse 6" }, - { Qt::ExtraButton7 , "Mouse 7" }, - { Qt::ExtraButton8 , "Mouse 8" }, - { Qt::ExtraButton9 , "Mouse 9" }, - { Qt::ExtraButton10 , "Mouse 10" }, - { Qt::ExtraButton11 , "Mouse 11" }, - { Qt::ExtraButton12 , "Mouse 12" }, - { Qt::ExtraButton13 , "Mouse 13" }, - { Qt::ExtraButton14 , "Mouse 14" }, - { Qt::ExtraButton15 , "Mouse 15" }, - { Qt::ExtraButton16 , "Mouse 16" }, - { Qt::ExtraButton17 , "Mouse 17" }, - { Qt::ExtraButton18 , "Mouse 18" }, - { Qt::ExtraButton19 , "Mouse 19" }, - { Qt::ExtraButton20 , "Mouse 20" }, - { Qt::ExtraButton21 , "Mouse 21" }, - { Qt::ExtraButton22 , "Mouse 22" }, - { Qt::ExtraButton23 , "Mouse 23" }, - { Qt::ExtraButton24 , "Mouse 24" }, + { mouse::button + static_cast(Qt::LeftButton) , "Mouse Left" }, + { mouse::button + static_cast(Qt::RightButton) , "Mouse Right" }, + { mouse::button + static_cast(Qt::MiddleButton) , "Mouse Middle" }, + { mouse::button + static_cast(Qt::BackButton) , "Mouse Back" }, + { mouse::button + static_cast(Qt::ForwardButton) , "Mouse Fwd" }, + { mouse::button + static_cast(Qt::TaskButton) , "Mouse Task" }, + { mouse::button + static_cast(Qt::ExtraButton4) , "Mouse 4" }, + { mouse::button + static_cast(Qt::ExtraButton5) , "Mouse 5" }, + { mouse::button + static_cast(Qt::ExtraButton6) , "Mouse 6" }, + { mouse::button + static_cast(Qt::ExtraButton7) , "Mouse 7" }, + { mouse::button + static_cast(Qt::ExtraButton8) , "Mouse 8" }, + { mouse::button + static_cast(Qt::ExtraButton9) , "Mouse 9" }, + { mouse::button + static_cast(Qt::ExtraButton10) , "Mouse 10" }, + { mouse::button + static_cast(Qt::ExtraButton11) , "Mouse 11" }, + { mouse::button + static_cast(Qt::ExtraButton12) , "Mouse 12" }, + { mouse::button + static_cast(Qt::ExtraButton13) , "Mouse 13" }, + { mouse::button + static_cast(Qt::ExtraButton14) , "Mouse 14" }, + { mouse::button + static_cast(Qt::ExtraButton15) , "Mouse 15" }, + { mouse::button + static_cast(Qt::ExtraButton16) , "Mouse 16" }, + { mouse::button + static_cast(Qt::ExtraButton17) , "Mouse 17" }, + { mouse::button + static_cast(Qt::ExtraButton18) , "Mouse 18" }, + { mouse::button + static_cast(Qt::ExtraButton19) , "Mouse 19" }, + { mouse::button + static_cast(Qt::ExtraButton20) , "Mouse 20" }, + { mouse::button + static_cast(Qt::ExtraButton21) , "Mouse 21" }, + { mouse::button + static_cast(Qt::ExtraButton22) , "Mouse 22" }, + { mouse::button + static_cast(Qt::ExtraButton23) , "Mouse 23" }, + { mouse::button + static_cast(Qt::ExtraButton24) , "Mouse 24" }, { mouse::move_left , "Mouse MLeft" }, { mouse::move_right , "Mouse MRight" }, @@ -93,7 +94,7 @@ public: static QStringList GetKeyNames(const QKeyEvent* keyEvent); static std::string GetKeyName(const QKeyEvent* keyEvent, bool with_modifiers); static std::string GetKeyName(const u32& keyCode); - static std::vector> GetKeyCombos(const cfg::string& cfg_string); + static std::vector> GetKeyCombos(const std::string& cfg_string); static u32 GetKeyCode(const QString& keyName); static int native_scan_code_from_string(const std::string& key); From b10c742f101030f255e9a146701209db699e7df3 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Mon, 13 Apr 2026 08:50:51 +0200 Subject: [PATCH 06/29] iso stuff --- rpcs3/Loader/ISO.cpp | 47 ++++++++++++++++--------------- rpcs3/Loader/iso_cache.cpp | 2 ++ rpcs3/Loader/iso_cache.h | 2 +- rpcs3/rpcs3qt/game_list_frame.cpp | 37 +++++++++++++----------- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index f3eba4268b..8ec93ec8c7 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -11,6 +11,7 @@ #include LOG_CHANNEL(sys_log, "SYS"); +LOG_CHANNEL(iso_log, "ISO"); constexpr u64 ISO_SECTOR_SIZE = 2048; @@ -110,7 +111,7 @@ static bool decrypt_data(aes_context& aes, u64 offset, unsigned char* buffer, u6 // Partial (or even full) first sector if (aes_crypt_cbc(&aes, AES_DECRYPT, cur_size, iv.data(), &buffer[cur_offset], &buffer[cur_offset]) != 0) { - sys_log.error("decrypt_data(): Error decrypting data on first sector read"); + iso_log.error("decrypt_data: Error decrypting data on first sector read"); return false; } @@ -130,7 +131,7 @@ static bool decrypt_data(aes_context& aes, u64 offset, unsigned char* buffer, u6 if (aes_crypt_cbc(&aes, AES_DECRYPT, ISO_SECTOR_SIZE, iv.data(), &buffer[cur_offset], &buffer[cur_offset]) != 0) { - sys_log.error("decrypt_data(): Error decrypting data on inner sector(s) read"); + iso_log.error("decrypt_data: Error decrypting data on inner sector(s) read"); return false; } @@ -142,7 +143,7 @@ static bool decrypt_data(aes_context& aes, u64 offset, unsigned char* buffer, u6 // Partial (or even full) last sector if (aes_crypt_cbc(&aes, AES_DECRYPT, size - cur_offset, iv.data(), &buffer[cur_offset], &buffer[cur_offset]) != 0) { - sys_log.error("decrypt_data(): Error decrypting data on last sector read"); + iso_log.error("decrypt_data: Error decrypting data on last sector read"); return false; } @@ -164,8 +165,6 @@ bool iso_file_decryption::init(const std::string& path) return false; } - std::array sec0_sec1 {}; - // // Store the ISO region information (needed by both the "Redump" type (only on "decrypt()" method) and "3k3y" type) // @@ -174,19 +173,21 @@ bool iso_file_decryption::init(const std::string& path) if (!iso_file) { - sys_log.error("init(): Failed to open file: %s", path); + iso_log.error("init: Failed to open file: %s", path); return false; } + std::array sec0_sec1 {}; + if (iso_file.size() < sec0_sec1.size()) { - sys_log.error("init(): Found only %ull sector(s) (minimum required is 2): %s", iso_file.size(), path); + iso_log.error("init: Found only %ull sector(s) (minimum required is 2): %s", iso_file.size(), path); return false; } if (iso_file.read(sec0_sec1.data(), sec0_sec1.size()) != sec0_sec1.size()) { - sys_log.error("init(): Failed to read file: %s", path); + iso_log.error("init: Failed to read file: %s", path); return false; } @@ -195,12 +196,12 @@ bool iso_file_decryption::init(const std::string& path) // Following checks and assigned values are based on PS3 ISO specification. // E.g. all even regions (0, 2, 4 etc.) are always unencrypted while the odd ones are encrypted - size_t region_count = char_arr_BE_to_uint(sec0_sec1.data()); + const u32 region_count = char_arr_BE_to_uint(sec0_sec1.data()); // Ensure the region count is a proper value if (region_count < 1 || region_count > 31) // It's non-PS3ISO { - sys_log.error("init(): Failed to read region information: %s", path); + iso_log.error("init: Failed to read region information: '%s' (region_count=%d)", path, region_count); return false; } @@ -264,12 +265,12 @@ bool iso_file_decryption::init(const std::string& path) if (m_enc_type == iso_encryption_type::NONE) // If encryption type was not set to REDUMP for any reason { - sys_log.error("init(): Failed to process key file: %s", key_path); + iso_log.error("init: Failed to process key file: %s", key_path); } } else { - sys_log.warning("init(): Failed to open, or missing, key file: %s", key_path); + iso_log.warning("init: Failed to open, or missing, key file: %s", key_path); } // @@ -311,7 +312,7 @@ bool iso_file_decryption::init(const std::string& path) if (m_enc_type == iso_encryption_type::NONE) // If encryption type was not set to ENC_3K3Y for any reason { - sys_log.error("init(): Failed to set encryption type to ENC_3K3Y: %s", path); + iso_log.error("init: Failed to set encryption type to ENC_3K3Y: %s", path); } } else if (memcmp(&k3k3y_dec_watermark[0], &sec0_sec1[0xF70], sizeof(k3k3y_dec_watermark)) == 0) @@ -323,16 +324,16 @@ bool iso_file_decryption::init(const std::string& path) switch (m_enc_type) { case iso_encryption_type::REDUMP: - sys_log.warning("init(): Set 'enc type': REDUMP, 'reg count': %u: %s", m_region_info.size(), path); + iso_log.warning("init: Set 'enc type': REDUMP, 'reg count': %u: %s", m_region_info.size(), path); break; case iso_encryption_type::ENC_3K3Y: - sys_log.warning("init(): Set 'enc type': ENC_3K3Y, 'reg count': %u: %s", m_region_info.size(), path); + iso_log.warning("init: Set 'enc type': ENC_3K3Y, 'reg count': %u: %s", m_region_info.size(), path); break; case iso_encryption_type::DEC_3K3Y: - sys_log.warning("init(): Set 'enc type': DEC_3K3Y, 'reg count': %u: %s", m_region_info.size(), path); + iso_log.warning("init: Set 'enc type': DEC_3K3Y, 'reg count': %u: %s", m_region_info.size(), path); break; case iso_encryption_type::NONE: // If encryption type was not set for any reason - sys_log.warning("init(): Set 'enc type': NONE, 'reg count': %u: %s", m_region_info.size(), path); + iso_log.warning("init: Set 'enc type': NONE, 'reg count': %u: %s", m_region_info.size(), path); break; } @@ -384,7 +385,7 @@ bool iso_file_decryption::decrypt(u64 offset, void* buffer, u64 size, const std: } } - sys_log.error("decrypt(): %s: LBA request wasn't in the 'm_region_info' for an encrypted ISO? - RP: 0x%lx, RC: 0x%lx, LR: (0x%016lx - 0x%016lx)", + iso_log.error("decrypt: %s: LBA request wasn't in the 'm_region_info' for an encrypted ISO? - RP: 0x%lx, RC: 0x%lx, LR: (0x%016lx - 0x%016lx)", name, offset, static_cast(m_region_info.size()), @@ -492,7 +493,7 @@ static std::optional iso_read_directory_entry(fs::file& entry, utf16.resize(header.file_name_length / 2); - for (size_t i = 0; i < utf16.size(); ++i, raw++) + for (usz i = 0; i < utf16.size(); ++i, raw++) { utf16[i] = *reinterpret_cast*>(raw); } @@ -664,8 +665,8 @@ iso_fs_node* iso_archive::retrieve(const std::string& passed_path) const std::string path = std::filesystem::path(passed_path).string(); const std::string_view path_sv = path; - size_t start = 0; - size_t end = path_sv.find_first_of(fs::delim); + usz start = 0; + usz end = path_sv.find_first_of(fs::delim); std::stack search_stack; search_stack.push(&m_root); @@ -907,7 +908,7 @@ u64 iso_file::read_at(u64 offset, void* buffer, u64 size) { if (total_read != first_sec.size_aligned) { - sys_log.error("read_at(): %s: Error reading from file", m_meta.name); + iso_log.error("read_at: %s: Error reading from file", m_meta.name); seek(m_pos, fs::seek_set); return 0; @@ -949,7 +950,7 @@ u64 iso_file::read_at(u64 offset, void* buffer, u64 size) if (total_read != first_sec.size_aligned + last_sec.size_aligned + expected_inner_sector_read) { - sys_log.error("read_at(): %s: Error reading from file", m_meta.name); + iso_log.error("read_at: %s: Error reading from file", m_meta.name); seek(m_pos, fs::seek_set); return 0; diff --git a/rpcs3/Loader/iso_cache.cpp b/rpcs3/Loader/iso_cache.cpp index a90222a240..17453489e8 100644 --- a/rpcs3/Loader/iso_cache.cpp +++ b/rpcs3/Loader/iso_cache.cpp @@ -91,6 +91,8 @@ namespace iso_cache void save(const std::string& iso_path, const iso_metadata_cache_entry& entry) { + iso_cache_log.notice("Saving cache for '%s'", iso_path); + const std::string stem = get_cache_stem(iso_path); const std::string dir = get_cache_dir(); const std::string yml_path = dir + stem + ".yml"; diff --git a/rpcs3/Loader/iso_cache.h b/rpcs3/Loader/iso_cache.h index ca2f39e6a4..8f6594b76c 100644 --- a/rpcs3/Loader/iso_cache.h +++ b/rpcs3/Loader/iso_cache.h @@ -29,4 +29,4 @@ namespace iso_cache // Remove cache entries for ISOs that are no longer in the scanned set. void cleanup(const std::unordered_set& valid_iso_paths); -} \ No newline at end of file +} diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index d2af663b7d..fff6f33362 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -566,11 +566,7 @@ void game_list_frame::OnParsingFinished() // Load PSF: from archive on cache miss, rehydrate from cached SFO bytes on hit. psf::registry psf{}; - if (archive) - { - psf = archive->open_psf(sfo_path); - } - else if (!cache_entry.psf_data.empty()) + if (!cache_entry.psf_data.empty()) { psf = psf::load_object(fs::make_stream>(std::vector(cache_entry.psf_data)), sfo_path); // Fallback to archive scan if cached PSF is corrupted or missing critical fields. @@ -579,14 +575,23 @@ void game_list_frame::OnParsingFinished() && !psf::get_string(psf, "CATEGORY", "").empty(); if (!psf_valid) { + game_list_log.warning("Cached psf for iso not valid: '%s'", game.info.path); archive = std::make_unique(dir_or_elf); - psf = archive->open_psf(sfo_path); cache_entry = {}; // Reset so the cache gets rewritten after scan. + psf = {}; } } - else + + if (psf.empty()) { - psf = psf::load_object(sfo_path); + if (archive) + { + psf = archive->open_psf(sfo_path); + } + else + { + psf = psf::load_object(sfo_path); + } } const std::string_view title_id = psf::get_string(psf, "TITLE_ID", ""); @@ -676,17 +681,17 @@ void game_list_frame::OnParsingFinished() if (play_hover_movies) { - if (!cache_entry.movie_path.empty() && !archive) - { - // Cache hit — restore previously resolved movie path. - game.info.movie_path = cache_entry.movie_path; - game.has_hover_pam = true; - } if (std::string movie_path = game_icon_path + game.info.serial + "/hover.gif"; file_exists(movie_path)) { game.info.movie_path = std::move(movie_path); game.has_hover_gif = true; } + else if (!cache_entry.movie_path.empty() && !archive) + { + // Cache hit — restore previously resolved movie path. + game.info.movie_path = cache_entry.movie_path; + game.has_hover_pam = true; + } else if (std::string movie_path = sfo_dir + "/" + localized_movie; file_exists(movie_path)) { game.info.movie_path = std::move(movie_path); @@ -701,13 +706,13 @@ void game_list_frame::OnParsingFinished() if (play_hover_music) { - if(!cache_entry.audio_path.empty() && !archive) + if (!cache_entry.audio_path.empty() && !archive) { // Cache hit — restore previously resolved audio path. game.info.audio_path = cache_entry.audio_path; game.has_audio_file = true; } - if (std::string audio_path = sfo_dir + "/SND0.AT3"; file_exists(audio_path)) + else if (std::string audio_path = sfo_dir + "/SND0.AT3"; file_exists(audio_path)) { game.info.audio_path = std::move(audio_path); game.has_audio_file = true; From 6c87413f48014a09022c4eebb6192a2aca9c7405 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 14 Apr 2026 07:32:32 +0200 Subject: [PATCH 07/29] SPURecompiler: add missing default --- rpcs3/Emu/Cell/SPURecompiler.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rpcs3/Emu/Cell/SPURecompiler.h b/rpcs3/Emu/Cell/SPURecompiler.h index 6c629571d9..cb5678f72b 100644 --- a/rpcs3/Emu/Cell/SPURecompiler.h +++ b/rpcs3/Emu/Cell/SPURecompiler.h @@ -579,6 +579,10 @@ public: IMM = spu_opcode_t{imm}.si10; break; } + default: + { + break; + } } if (!is_ok) From bbeb7c4cd7cf11114fac6ed3f9cde3f304f38144 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 14 Apr 2026 07:34:05 +0200 Subject: [PATCH 08/29] Remove sneaky pragma optimize --- rpcs3/Emu/Cell/SPULLVMRecompiler.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp b/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp index f0a8c9f7db..580160fe75 100644 --- a/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp +++ b/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp @@ -60,7 +60,6 @@ const extern spu_decoder g_spu_iflag; #pragma GCC diagnostic pop #endif -#pragma optimize("", off) #ifdef ARCH_ARM64 #include "Emu/CPU/Backends/AArch64/AArch64JIT.h" #endif From 426f3c445b43f5e3e9b6e2c7c4949b640db8a395 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 14 Apr 2026 07:35:24 +0200 Subject: [PATCH 09/29] Remove some unused variables --- rpcs3/Emu/Cell/SPUCommonRecompiler.cpp | 7 ------- rpcs3/Emu/Cell/SPULLVMRecompiler.cpp | 2 -- rpcs3/Emu/Cell/SPURecompiler.h | 2 -- 3 files changed, 11 deletions(-) diff --git a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp index 53dc0df200..e08139f8c5 100644 --- a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp +++ b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp @@ -6144,8 +6144,6 @@ spu_program spu_recompiler_base::analyse(const be_t* ls, u32 entry_point, s } } - u32 reg_pos = SPU_LS_SIZE; - auto org = reduced_loop->get_reg(op_rt); u32 reg_first = s_reg_max; @@ -6773,8 +6771,6 @@ spu_program spu_recompiler_base::analyse(const be_t* ls, u32 entry_point, s break; } - u32 cond_val_incr = static_cast(reg_org->IMM); - if (reg_org->mod1_type == spu_itype::AI || reg_org->mod1_type == spu_itype::AHI) { reduced_loop->cond_val_incr_is_immediate = true; @@ -6986,7 +6982,6 @@ spu_program spu_recompiler_base::analyse(const be_t* ls, u32 entry_point, s // The loop dictator is the register that is not the argument const u32 loop_arg_reg = reg_index == op_ra ? op_rb : op_ra; - const u32 loop_dict_reg = reg_index == op_ra ? op_ra : op_rb; reduced_loop->cond_val_is_immediate = false; if (found_loop_argument_for_dictator) @@ -8639,8 +8634,6 @@ spu_program spu_recompiler_base::analyse(const be_t* ls, u32 entry_point, s if (inst_attr attr = m_inst_attrs[(loop_pc - entry_point) / 4]; attr == inst_attr::none) { - const u64 hash = loop_pc / 4 + read_from_ptr>(func_hash.data()); - add_pattern(inst_attr::reduced_loop, loop_pc - result.entry_point, 0, std::make_shared(pattern)); std::string regs = "{"; diff --git a/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp b/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp index 580160fe75..b6d0791ab9 100644 --- a/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp +++ b/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp @@ -2617,8 +2617,6 @@ public: { if (auto init_val = reduced_loop_init_regs[i]) { - llvm::Type* type = g_cfg.core.spu_xfloat_accuracy == xfloat_accuracy::accurate && bb.reg_maybe_xf[i] ? get_type() : get_reg_type(i); - const auto _phi = m_ir->CreatePHI(init_val->getType(), 2, fmt::format("reduced_0x%05x_r%u", baddr, i)); _phi->addIncoming(init_val, prev_insert_block); diff --git a/rpcs3/Emu/Cell/SPURecompiler.h b/rpcs3/Emu/Cell/SPURecompiler.h index cb5678f72b..fc74bcec90 100644 --- a/rpcs3/Emu/Cell/SPURecompiler.h +++ b/rpcs3/Emu/Cell/SPURecompiler.h @@ -540,13 +540,11 @@ public: { case spu_itype::XSBH: { - const auto prev_type = modified == 1 ? mod1_type : mod2_type; is_ok &= mod1_type == spu_itype::CEQB || mod1_type == spu_itype::CEQBI || mod1_type == spu_itype::CGTB || mod1_type == spu_itype::CGTBI || mod1_type == spu_itype::CLGTB || mod1_type == spu_itype::CLGTBI; break; } case spu_itype::ANDI: { - const auto prev_type = modified == 1 ? mod1_type : mod2_type; is_ok &= mod1_type == spu_itype::CEQB || mod1_type == spu_itype::CEQBI || mod1_type == spu_itype::CGTB || mod1_type == spu_itype::CGTBI || mod1_type == spu_itype::CLGTB || mod1_type == spu_itype::CLGTBI; is_ok &= (spu_opcode_t{imm}.si10 & 0xff) == 0xff; break; From ac3d14cf5cf637231d61771b35852c5d3ca519ec Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 14 Apr 2026 07:36:33 +0200 Subject: [PATCH 10/29] Add missing fmt case for cpu_flag::req_exit --- rpcs3/Emu/CPU/CPUThread.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/rpcs3/Emu/CPU/CPUThread.cpp b/rpcs3/Emu/CPU/CPUThread.cpp index 8eddff9e0b..78a0a3bd7b 100644 --- a/rpcs3/Emu/CPU/CPUThread.cpp +++ b/rpcs3/Emu/CPU/CPUThread.cpp @@ -61,6 +61,7 @@ void fmt_class_string::format(std::string& out, u64 arg) case cpu_flag::notify: return "ntf"; case cpu_flag::yield: return "y"; case cpu_flag::preempt: return "PREEMPT"; + case cpu_flag::req_exit: return "REQ-EXIT"; case cpu_flag::dbg_global_pause: return "G-PAUSE"; case cpu_flag::dbg_pause: return "PAUSE"; case cpu_flag::dbg_step: return "STEP"; From e43435e152cec54e91bcd62e1ca65e64d2048708 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 14 Apr 2026 07:40:30 +0200 Subject: [PATCH 11/29] Remove unused should_have_argument_increment --- rpcs3/Emu/Cell/SPUCommonRecompiler.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp index e08139f8c5..efc258a302 100644 --- a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp +++ b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp @@ -6489,7 +6489,6 @@ spu_program spu_recompiler_base::analyse(const be_t* ls, u32 entry_point, s } bool should_have_argument_dictator = false; - bool should_have_argument_increment = false; bool cond_val_incr_before_cond = false; bool ends_with_comparison = false; @@ -6498,10 +6497,6 @@ spu_program spu_recompiler_base::analyse(const be_t* ls, u32 entry_point, s switch (reg->mod1_type) { case spu_itype::A: - { - should_have_argument_increment = true; - [[fallthrough]]; - } case spu_itype::AI: case spu_itype::AHI: { @@ -6561,10 +6556,6 @@ spu_program spu_recompiler_base::analyse(const be_t* ls, u32 entry_point, s switch (reg->mod2_type) { case spu_itype::A: - { - should_have_argument_increment = true; - [[fallthrough]]; - } case spu_itype::AI: case spu_itype::AHI: { From c340eb2f17acd87be57ccaf51d73cb106fd8f8aa Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 14 Apr 2026 07:41:08 +0200 Subject: [PATCH 12/29] Remove nop loop --- rpcs3/Emu/Cell/SPUCommonRecompiler.cpp | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp index efc258a302..6fa68a2d4b 100644 --- a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp +++ b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp @@ -5921,17 +5921,6 @@ spu_program spu_recompiler_base::analyse(const be_t* ls, u32 entry_point, s } } - const auto prev_wi = wi - 1; - if (prev_wi != umax && ::at32(reg_state_it, prev_wi).reduced_loop.active) - { - const auto reduced_loop = &::at32(reg_state_it, prev_wi).reduced_loop; - - for (const auto& [reg_num, reg] : reduced_loop->regs) - { - - } - } - if (wi < reg_state_it.size()) { wa = ::at32(reg_state_it, wi).pc; From 27f857a8fe1f8c97cf52adb79d34c7d6520fbaaf Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 14 Apr 2026 08:17:08 +0200 Subject: [PATCH 13/29] Remove unused variable in find_dialog --- rpcs3/rpcs3qt/find_dialog.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/rpcs3/rpcs3qt/find_dialog.cpp b/rpcs3/rpcs3qt/find_dialog.cpp index 756e355e4f..f465ee5573 100644 --- a/rpcs3/rpcs3qt/find_dialog.cpp +++ b/rpcs3/rpcs3qt/find_dialog.cpp @@ -145,11 +145,8 @@ void find_dialog::find(find_type type) m_count_lines++; - int pos_count = 0; - for (int pos : positions) { - pos_count++; word_count++; if (is_current_block && is_current_line && pos == current_pos) From 0b9c53e254340db8c7afb4ed0b994d1278b0ead7 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 14 Apr 2026 08:17:23 +0200 Subject: [PATCH 14/29] Qt: Add missing thread name --- rpcs3/rpcs3qt/game_list_actions.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp index 25b9f06b96..30b099fb9a 100644 --- a/rpcs3/rpcs3qt/game_list_actions.cpp +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -6,6 +6,7 @@ #include "qt_utils.h" #include "progress_dialog.h" +#include "Utilities/Thread.h" #include "Utilities/File.h" #include "Loader/ISO.h" @@ -373,6 +374,8 @@ void game_list_actions::ShowDiskUsageDialog() // so run it on a concurrent thread avoiding to block the entire GUI m_disk_usage_future = QtConcurrent::run([this]() { + thread_ctrl::set_name("Disk Usage"); + const std::vector> vfs_disk_usage = rpcs3::utils::get_vfs_disk_usage(); const u64 cache_disk_usage = rpcs3::utils::get_cache_disk_usage(); From 4ffeee034434c10b64f0a4428f1403b869e0bae0 Mon Sep 17 00:00:00 2001 From: schm1dtmac Date: Tue, 14 Apr 2026 21:38:39 +0100 Subject: [PATCH 15/29] Opt out of Game Mode on macOS due to throttling --- rpcs3/rpcs3.plist.in | 2 -- 1 file changed, 2 deletions(-) diff --git a/rpcs3/rpcs3.plist.in b/rpcs3/rpcs3.plist.in index 93a4f2c186..137c76e087 100644 --- a/rpcs3/rpcs3.plist.in +++ b/rpcs3/rpcs3.plist.in @@ -28,8 +28,6 @@ Licensed under GPLv2 NSHighResolutionCapable - LSApplicationCategoryType - public.app-category.games LSMinimumSystemVersion 14.4 NSCameraUsageDescription From 1e63385dfcc2269704d629da9d62486b8d5849eb Mon Sep 17 00:00:00 2001 From: Megamouse Date: Wed, 15 Apr 2026 08:05:40 +0200 Subject: [PATCH 16/29] Update libpng to 1.6.57 --- 3rdparty/libpng/libpng | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/libpng/libpng b/3rdparty/libpng/libpng index d5515b5b8b..95ab3fdca8 160000 --- a/3rdparty/libpng/libpng +++ b/3rdparty/libpng/libpng @@ -1 +1 @@ -Subproject commit d5515b5b8be3901aac04e5bd8bd5c89f287bcd33 +Subproject commit 95ab3fdca83ea294efd3b092e9a53c5a39886444 From d53a6a87f6c6594c545b9eadc35bee2de278856d Mon Sep 17 00:00:00 2001 From: Megamouse Date: Wed, 15 Apr 2026 08:07:29 +0200 Subject: [PATCH 17/29] Update wolfssl to 5.9.1 --- 3rdparty/wolfssl/wolfssl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/wolfssl/wolfssl b/3rdparty/wolfssl/wolfssl index 922d04b356..1d363f3adc 160000 --- a/3rdparty/wolfssl/wolfssl +++ b/3rdparty/wolfssl/wolfssl @@ -1 +1 @@ -Subproject commit 922d04b3568c6428a9fb905ddee3ef5a68db3108 +Subproject commit 1d363f3adceba9d1478230ede476a37b0dcdef24 From 7d41bbdd2b645655ff8f8bab5237e0df934bbda3 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Wed, 15 Apr 2026 08:23:28 +0200 Subject: [PATCH 18/29] Fix Disk Usage thread --- Utilities/Thread.cpp | 7 +++++++ Utilities/Thread.h | 8 ++------ rpcs3/rpcs3qt/game_list_actions.cpp | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Utilities/Thread.cpp b/Utilities/Thread.cpp index 4ec70b58bb..57d7446daf 100644 --- a/Utilities/Thread.cpp +++ b/Utilities/Thread.cpp @@ -2883,6 +2883,13 @@ void thread_base::exec() } } +void thread_ctrl::set_name(std::string name) +{ + ensure(g_tls_this_thread); + g_tls_this_thread->m_tname.store(make_single(name)); + g_tls_this_thread->set_name(std::move(name)); +} + [[noreturn]] void thread_ctrl::emergency_exit(std::string_view reason) { // Print stacktrace diff --git a/Utilities/Thread.h b/Utilities/Thread.h index 7cd9a7c7ea..bafcea0b9f 100644 --- a/Utilities/Thread.h +++ b/Utilities/Thread.h @@ -129,7 +129,7 @@ public: const native_entry entry_point; // Set name for debugger - static void set_name(std::string); + static void set_name(std::string name); private: // Thread handle (platform-specific) @@ -232,11 +232,7 @@ public: } // Set current thread name (not recommended) - static void set_name(std::string name) - { - g_tls_this_thread->m_tname.store(make_single(name)); - g_tls_this_thread->set_name(std::move(name)); - } + static void set_name(std::string name); // Set thread name (not recommended) template diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp index 30b099fb9a..e1a78feb11 100644 --- a/rpcs3/rpcs3qt/game_list_actions.cpp +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -374,7 +374,7 @@ void game_list_actions::ShowDiskUsageDialog() // so run it on a concurrent thread avoiding to block the entire GUI m_disk_usage_future = QtConcurrent::run([this]() { - thread_ctrl::set_name("Disk Usage"); + thread_base::set_name("Disk Usage"); const std::vector> vfs_disk_usage = rpcs3::utils::get_vfs_disk_usage(); const u64 cache_disk_usage = rpcs3::utils::get_cache_disk_usage(); From 50d6396f9911ed93ce5d23f8f62f97217fb0681a Mon Sep 17 00:00:00 2001 From: Megamouse Date: Mon, 16 Jun 2025 07:09:59 +0200 Subject: [PATCH 19/29] implement config db --- .ci/deploy-windows-clang.sh | 1 + .ci/deploy-windows.sh | 1 + Utilities/Config.cpp | 98 ++++++---- Utilities/Config.h | 21 ++- Utilities/StrUtil.h | 10 +- rpcs3/Emu/Cell/Modules/cellVdec.cpp | 4 +- rpcs3/Emu/Cell/lv2/sys_process.cpp | 4 +- rpcs3/Emu/System.cpp | 87 +++++---- rpcs3/Emu/System.h | 9 +- rpcs3/Emu/config_mode.h | 1 + rpcs3/rpcs3.vcxproj | 17 ++ rpcs3/rpcs3.vcxproj.filters | 12 ++ rpcs3/rpcs3qt/CMakeLists.txt | 1 + rpcs3/rpcs3qt/config_database.cpp | 228 +++++++++++++++++++++++ rpcs3/rpcs3qt/config_database.h | 43 +++++ rpcs3/rpcs3qt/emu_settings.cpp | 18 +- rpcs3/rpcs3qt/emu_settings.h | 2 +- rpcs3/rpcs3qt/game_compatibility.cpp | 23 ++- rpcs3/rpcs3qt/game_compatibility.h | 4 +- rpcs3/rpcs3qt/game_list_context_menu.cpp | 51 ++++- rpcs3/rpcs3qt/game_list_frame.cpp | 28 +++ rpcs3/rpcs3qt/game_list_frame.h | 6 +- rpcs3/rpcs3qt/gui_game_info.h | 3 +- rpcs3/rpcs3qt/main_window.cpp | 26 ++- rpcs3/rpcs3qt/patch_manager_dialog.cpp | 11 +- rpcs3/rpcs3qt/pkg_install_dialog.cpp | 2 +- rpcs3/rpcs3qt/pkg_install_dialog.h | 2 +- rpcs3/rpcs3qt/settings_dialog.cpp | 4 +- rpcs3/rpcs3qt/settings_dialog.h | 2 +- rpcs3/rpcs3qt/update_manager.cpp | 11 +- 30 files changed, 607 insertions(+), 123 deletions(-) create mode 100644 rpcs3/rpcs3qt/config_database.cpp create mode 100644 rpcs3/rpcs3qt/config_database.h diff --git a/.ci/deploy-windows-clang.sh b/.ci/deploy-windows-clang.sh index 07b4866fc4..04ba1bb20a 100644 --- a/.ci/deploy-windows-clang.sh +++ b/.ci/deploy-windows-clang.sh @@ -24,6 +24,7 @@ mkdir ./bin/config mkdir ./bin/config/input_configs curl -fsSL 'https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/master/gamecontrollerdb.txt' 1> ./bin/config/input_configs/gamecontrollerdb.txt curl -fsSL 'https://rpcs3.net/compatibility?api=v1&export' | iconv -f ISO-8859-1 -t UTF-8 1> ./bin/GuiConfigs/compat_database.dat +curl -fsSL 'https://api.rpcs3.net/config/?api=v1' | iconv -f ISO-8859-1 -t UTF-8 1> ./bin/GuiConfigs/config_database.dat # Download translations mkdir -p ./bin/share/qt6/translations diff --git a/.ci/deploy-windows.sh b/.ci/deploy-windows.sh index 069f8fb637..3c59391a66 100755 --- a/.ci/deploy-windows.sh +++ b/.ci/deploy-windows.sh @@ -14,6 +14,7 @@ mkdir ./bin/config mkdir ./bin/config/input_configs curl -fsSL 'https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/master/gamecontrollerdb.txt' 1> ./bin/config/input_configs/gamecontrollerdb.txt curl -fsSL 'https://rpcs3.net/compatibility?api=v1&export' | iconv -t UTF-8 1> ./bin/GuiConfigs/compat_database.dat +curl -fsSL 'https://api.rpcs3.net/config/?api=v1' | iconv -t UTF-8 1> ./bin/GuiConfigs/config_database.dat # Download translations mkdir -p ./bin/qt6/translations diff --git a/Utilities/Config.cpp b/Utilities/Config.cpp index bd0fe7a8b8..ec2b4ca1c9 100644 --- a/Utilities/Config.cpp +++ b/Utilities/Config.cpp @@ -40,7 +40,7 @@ namespace cfg owner->m_nodes.emplace_back(this); } - bool _base::from_string(std::string_view, bool) + bool _base::from_string(std::string_view /*value*/, bool /*dynamic*/) { cfg_log.fatal("cfg::_base::from_string() purecall"); return false; @@ -68,7 +68,7 @@ namespace cfg // Incrementally load config entries from YAML::Node. // The config value is preserved if the corresponding YAML node doesn't exist. - static void decode(const YAML::Node& data, class _base& rhs, bool dynamic = false); + [[nodiscard]] static bool decode(const YAML::Node& data, class _base& rhs, bool dynamic, bool strict); } std::vector cfg::make_int_range(s64 min, s64 max) @@ -76,11 +76,11 @@ std::vector cfg::make_int_range(s64 min, s64 max) return {std::to_string(min), std::to_string(max)}; } -bool try_to_int64(s64* out, std::string_view value, s64 min, s64 max) +bool try_to_int64(s64* out, std::string_view value, s64 min, s64 max, std::string_view name) { if (value.empty()) { - if (out) cfg_log.error("cfg::try_to_int64(): called with an empty string"); + if (out) cfg_log.error("cfg::try_to_int64('%s'): called with an empty string", name); return false; } @@ -107,7 +107,7 @@ bool try_to_int64(s64* out, std::string_view value, s64 min, s64 max) if (ret.ec != std::errc() || ret.ptr != end || (start[0] == '-' && sign < 0)) { - if (out) cfg_log.error("cfg::try_to_int64('%s'): invalid integer", value); + if (out) cfg_log.error("cfg::try_to_int64('%s', '%s'): invalid integer", value, name); return false; } @@ -115,7 +115,7 @@ bool try_to_int64(s64* out, std::string_view value, s64 min, s64 max) if (result < min || result > max) { - if (out) cfg_log.error("cfg::try_to_int64('%s'): out of bounds (val=%d, min=%d, max=%d)", value, result, min, max); + if (out) cfg_log.error("cfg::try_to_int64('%s', '%s'): out of bounds (val=%d, min=%d, max=%d)", value, name, result, min, max); return false; } @@ -128,11 +128,11 @@ std::vector cfg::make_uint_range(u64 min, u64 max) return {std::to_string(min), std::to_string(max)}; } -bool try_to_uint64(u64* out, std::string_view value, u64 min, u64 max) +bool try_to_uint64(u64* out, std::string_view value, u64 min, u64 max, std::string_view name) { if (value.empty()) { - if (out) cfg_log.error("cfg::try_to_uint64(): called with an empty string"); + if (out) cfg_log.error("cfg::try_to_uint64('%s'): called with an empty string", name); return false; } @@ -152,13 +152,13 @@ bool try_to_uint64(u64* out, std::string_view value, u64 min, u64 max) if (ret.ec != std::errc() || ret.ptr != end) { - if (out) cfg_log.error("cfg::try_to_uint64('%s'): invalid integer", value); + if (out) cfg_log.error("cfg::try_to_uint64('%s', '%s'): invalid integer", value, name); return false; } if (result < min || result > max) { - if (out) cfg_log.error("cfg::try_to_uint64('%s'): out of bounds (val=%u, min=%u, max=%u)", value, result, min, max); + if (out) cfg_log.error("cfg::try_to_uint64('%s', '%s'): out of bounds (val=%u, min=%u, max=%u)", value, name, result, min, max); return false; } @@ -166,11 +166,11 @@ bool try_to_uint64(u64* out, std::string_view value, u64 min, u64 max) return true; } -bool try_to_uint128(u128* out, std::string_view value) +bool try_to_uint128(u128* out, std::string_view value, std::string_view name) { if (value.empty()) { - if (out) cfg_log.error("cfg::try_to_uint128(): called with an empty string"); + if (out) cfg_log.error("cfg::try_to_uint128('%s'): called with an empty string", name); return false; } @@ -193,7 +193,7 @@ bool try_to_uint128(u128* out, std::string_view value) if (ret.ec != std::errc() || ret.ptr != end) { - if (out) cfg_log.error("cfg::try_to_uint128('%s'): invalid integer", value); + if (out) cfg_log.error("cfg::try_to_uint128('%s', '%s'): invalid integer", value, name); return false; } @@ -207,7 +207,7 @@ bool try_to_uint128(u128* out, std::string_view value) if (ret.ec != std::errc() || ret.ptr != start_low64) { - if (out) cfg_log.error("cfg::try_to_uint128('%s'): invalid integer", value); + if (out) cfg_log.error("cfg::try_to_uint128('%s', '%s'): invalid integer", value, name); return false; } @@ -220,11 +220,11 @@ std::vector cfg::make_float_range(f64 min, f64 max) return {std::to_string(min), std::to_string(max)}; } -bool try_to_float(f64* out, std::string_view value, f64 min, f64 max) +bool try_to_float(f64* out, std::string_view value, f64 min, f64 max, std::string_view name) { if (value.empty()) { - if (out) cfg_log.error("cfg::try_to_float(): called with an empty string"); + if (out) cfg_log.error("cfg::try_to_float('%s'): called with an empty string", name); return false; } @@ -237,13 +237,13 @@ bool try_to_float(f64* out, std::string_view value, f64 min, f64 max) if (end_check != str.data() + str.size()) { - if (out) cfg_log.error("cfg::try_to_float('%s'): invalid float", value); + if (out) cfg_log.error("cfg::try_to_float('%s', '%s'): invalid float", value, name); return false; } if (result < min || result > max) { - if (out) cfg_log.error("cfg::try_to_float('%s'): out of bounds (val=%f, min=%f, max=%f)", value, result, min, max); + if (out) cfg_log.error("cfg::try_to_float('%s', '%s'): out of bounds (val=%f, min=%f, max=%f)", value, name, result, min, max); return false; } @@ -251,7 +251,7 @@ bool try_to_float(f64* out, std::string_view value, f64 min, f64 max) return true; } -bool try_to_string(std::string* out, const f64& value) +bool try_to_string(std::string* out, f64 value, std::string_view name) { #ifdef __APPLE__ if (out) *out = std::to_string(value); @@ -266,13 +266,13 @@ bool try_to_string(std::string* out, const f64& value) } else { - if (out) cfg_log.error("cfg::try_to_string(): could not convert value '%f' to string. error='%s'", value, std::make_error_code(ec).message()); + if (out) cfg_log.error("cfg::try_to_string('%s'): could not convert value '%f' to string. error='%s'", name, value, std::make_error_code(ec).message()); return false; } #endif } -bool cfg::try_to_enum_value(u64* out, decltype(&fmt_class_string::format) func, std::string_view value) +bool cfg::try_to_enum_value(u64* out, decltype(&fmt_class_string::format) func, std::string_view value, std::string_view name) { u64 max = umax; @@ -313,13 +313,13 @@ bool cfg::try_to_enum_value(u64* out, decltype(&fmt_class_string::format) f if (ret.ec != std::errc() || ret.ptr != end) { - if (out) cfg_log.error("cfg::try_to_enum_value('%s'): invalid enum or integer", value); + if (out) cfg_log.error("cfg::try_to_enum_value('%s', '%s'): invalid enum or integer", value, name); return false; } if (result > max) { - if (out) cfg_log.error("cfg::try_to_enum_value('%s'): out of bounds(val=%u, min=0, max=%u)", value, result, max); + if (out) cfg_log.error("cfg::try_to_enum_value('%s', '%s'): out of bounds(val=%u, min=0, max=%u)", value, name, result, max); return false; } @@ -468,11 +468,11 @@ void cfg::encode(YAML::Emitter& out, const cfg::_base& rhs) } } -void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) +bool cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic, bool strict) { if (dynamic && !rhs.get_is_dynamic()) { - return; + return true; } switch (rhs.get_type()) @@ -481,9 +481,11 @@ void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) { if (data.IsScalar() || data.IsSequence()) { - return; // ??? + return false; } + bool success = true; + for (const auto& pair : data) { if (!pair.first.IsScalar()) continue; @@ -493,12 +495,17 @@ void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) { if (node->get_name() == pair.first.Scalar()) { - decode(pair.second, *node, dynamic); + if (!decode(pair.second, *node, dynamic, strict) && strict) + { + success = false; + } + + break; } } } - break; + return success; } case type::set: { @@ -516,7 +523,7 @@ void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) { if (!data.IsMap()) { - return; + return false; } map_of_type values; @@ -535,7 +542,7 @@ void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) { if (data.IsScalar() || data.IsSequence()) { - return; // ??? + return false; } map_of_type values; @@ -545,10 +552,12 @@ void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) if (!pair.first.IsScalar() || !pair.second.IsScalar()) continue; u64 value; - if (cfg::try_to_enum_value(&value, &fmt_class_string::format, pair.second.Scalar())) + if (!cfg::try_to_enum_value(&value, &fmt_class_string::format, pair.second.Scalar(), pair.first.Scalar()) && strict) { - values.emplace(pair.first.Scalar(), static_cast(static_cast(value))); + return false; } + + values.emplace(pair.first.Scalar(), static_cast(static_cast(value))); } static_cast(rhs).set_map(std::move(values)); @@ -558,7 +567,7 @@ void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) { if (!data.IsMap()) { - return; // ??? + return false; } map_of_type values; @@ -598,12 +607,17 @@ void cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic) if (YAML::convert::decode(data, value)) { - rhs.from_string(value, dynamic); + if (!rhs.from_string(value, dynamic) && strict) + { + return false; + } } break; // ??? } } + + return true; } std::string cfg::node::to_string() const @@ -620,8 +634,7 @@ bool cfg::node::from_string(std::string_view value, bool dynamic) if (error.empty()) { - cfg::decode(result, *this, dynamic); - return true; + return cfg::decode(result, *this, dynamic, false); } cfg_log.error("Failed to load node: %s", error); @@ -644,6 +657,19 @@ void cfg::node::restore_defaults() } } +bool cfg::node::validate(std::string_view value) +{ + auto [result, error] = yaml_load(std::string(value)); + + if (error.empty()) + { + return cfg::decode(result, *this, false, true); + } + + cfg_log.error("Failed to load node: %s", error); + return false; +} + std::string cfg::map_entry::get_value(std::string_view key) { if (auto it = m_map.find(key); it != m_map.end()) diff --git a/Utilities/Config.h b/Utilities/Config.h index 4c79cbf31c..4f8e578fde 100644 --- a/Utilities/Config.h +++ b/Utilities/Config.h @@ -25,7 +25,7 @@ namespace cfg std::vector make_float_range(f64 min, f64 max); // Internal hack - bool try_to_enum_value(u64* out, decltype(&fmt_class_string::format) func, std::string_view); + bool try_to_enum_value(u64* out, decltype(&fmt_class_string::format) func, std::string_view value, std::string_view name = {}); // Internal hack std::vector try_to_enum_list(decltype(&fmt_class_string::format) func); @@ -110,7 +110,7 @@ namespace cfg } // Try to convert from string (optional) - virtual bool from_string(std::string_view, bool /*dynamic*/ = false); + virtual bool from_string(std::string_view value, bool dynamic = false); // Get string list (optional) virtual std::vector to_list() const @@ -161,6 +161,9 @@ namespace cfg // Restore default members void restore_defaults() override; + + // Try to convert from string and validate + bool validate(std::string_view value); }; class _bool final : public _base @@ -301,7 +304,7 @@ namespace cfg { u64 result; - if (try_to_enum_value(&result, &fmt_class_string::format, value)) + if (try_to_enum_value(&result, &fmt_class_string::format, value, m_name)) { // No narrowing check, it's hard to do right there m_value = static_cast(static_cast>(result)); @@ -382,7 +385,7 @@ namespace cfg bool from_string(std::string_view value, bool /*dynamic*/ = false) override { s64 result; - if (try_to_int64(&result, value, Min, Max)) + if (try_to_int64(&result, value, Min, Max, m_name)) { m_value = static_cast(result); return true; @@ -451,7 +454,7 @@ namespace cfg std::string to_string() const override { std::string result; - if (try_to_string(&result, m_value)) + if (try_to_string(&result, m_value, m_name)) { return result; } @@ -462,7 +465,7 @@ namespace cfg std::string def_to_string() const override { std::string result; - if (try_to_string(&result, def)) + if (try_to_string(&result, def, m_name)) { return result; } @@ -473,7 +476,7 @@ namespace cfg bool from_string(std::string_view value, bool /*dynamic*/ = false) override { f64 result; - if (try_to_float(&result, value, Min, Max)) + if (try_to_float(&result, value, Min, Max, m_name)) { m_value = static_cast(result); return true; @@ -560,7 +563,7 @@ namespace cfg bool from_string(std::string_view value, bool /*dynamic*/ = false) override { u64 result; - if (try_to_uint64(&result, value, Min, Max)) + if (try_to_uint64(&result, value, Min, Max, m_name)) { m_value = static_cast(result); return true; @@ -646,7 +649,7 @@ namespace cfg bool from_string(std::string_view value, bool /*dynamic*/ = false) override { u128 result; - if (try_to_uint128(&result, value)) + if (try_to_uint128(&result, value, m_name)) { m_value = result; return true; diff --git a/Utilities/StrUtil.h b/Utilities/StrUtil.h index 66c351d60f..3fcfe98a8a 100644 --- a/Utilities/StrUtil.h +++ b/Utilities/StrUtil.h @@ -23,19 +23,19 @@ inline void strcpy_trunc(D&& dst, const T& src) } // Convert string to signed integer -bool try_to_int64(s64* out, std::string_view value, s64 min, s64 max); +bool try_to_int64(s64* out, std::string_view value, s64 min, s64 max, std::string_view name = {}); // Convert string to unsigned integer -bool try_to_uint64(u64* out, std::string_view value, u64 min, u64 max); +bool try_to_uint64(u64* out, std::string_view value, u64 min, u64 max, std::string_view name = {}); // Convert string to unsigned int128_t -bool try_to_uint128(u128* out, std::string_view value); +bool try_to_uint128(u128* out, std::string_view value, std::string_view name = {}); // Convert string to float -bool try_to_float(f64* out, std::string_view value, f64 min, f64 max); +bool try_to_float(f64* out, std::string_view value, f64 min, f64 max, std::string_view name = {}); // Convert float to string locale independent -bool try_to_string(std::string* out, const f64& value); +bool try_to_string(std::string* out, f64 value, std::string_view name = {}); // Get the file extension of a file path ("png", "jpg", etc.) std::string get_file_extension(const std::string& file_path); diff --git a/rpcs3/Emu/Cell/Modules/cellVdec.cpp b/rpcs3/Emu/Cell/Modules/cellVdec.cpp index 3205afd786..1850416ba3 100644 --- a/rpcs3/Emu/Cell/Modules/cellVdec.cpp +++ b/rpcs3/Emu/Cell/Modules/cellVdec.cpp @@ -1385,8 +1385,8 @@ error_code cellVdecGetPictureExt(ppu_thread& ppu, u32 handle, vm::cptrsws = sws_getCachedContext(vdec->sws, w, h, in_f, w, h, out_f, SWS_POINT, nullptr, nullptr, nullptr); - u8* in_data[4] = { frame->data[0], frame->data[1], frame->data[2], alpha_plane.get() }; - int in_line[4] = { frame->linesize[0], frame->linesize[1], frame->linesize[2], w * 1 }; + const u8* in_data[4] = { frame->data[0], frame->data[1], frame->data[2], alpha_plane.get() }; + const int in_line[4] = { frame->linesize[0], frame->linesize[1], frame->linesize[2], w * 1 }; u8* out_data[4] = { outBuff.get_ptr() }; int out_line[4] = { w * 4 }; // RGBA32 or ARGB32 diff --git a/rpcs3/Emu/Cell/lv2/sys_process.cpp b/rpcs3/Emu/Cell/lv2/sys_process.cpp index b914408ec9..4ddf3720fd 100644 --- a/rpcs3/Emu/Cell/lv2/sys_process.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_process.cpp @@ -473,7 +473,7 @@ void lv2_exitspawn(ppu_thread& ppu, std::vector& argv, std::vector< }; Emu.after_kill_callback = [func = std::move(func), argv = std::move(argv), envp = std::move(envp), data = std::move(data), - disc = std::move(disc), path = std::move(path), hdd1 = std::move(hdd1), old_config = Emu.GetUsedConfig(), klic]() mutable + disc = std::move(disc), path = std::move(path), hdd1 = std::move(hdd1), old_config = Emu.GetUsedConfig(), old_db_config = Emu.GetUsedDatabaseConfig(), klic]() mutable { Emu.argv = std::move(argv); Emu.envp = std::move(envp); @@ -489,7 +489,7 @@ void lv2_exitspawn(ppu_thread& ppu, std::vector& argv, std::vector< Emu.SetForceBoot(true); - auto res = Emu.BootGame(path, "", true, cfg_mode::continuous, old_config); + auto res = Emu.BootGame(path, "", true, cfg_mode::continuous, old_config, old_db_config); if (res != game_boot_result::no_errors) { diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index f198156d47..d5facf3262 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -159,6 +159,7 @@ void fmt_class_string::format(std::string& out, u64 arg) case game_boot_result::still_running: return "Game is still running"; case game_boot_result::already_added: return "Game was already added"; case game_boot_result::currently_restricted: return "Booting is restricted at the time being"; + case game_boot_result::database_config_missing: return "Could not find config in database"; } return unknown; }); @@ -176,6 +177,7 @@ void fmt_class_string::format(std::string& out, u64 arg) case cfg_mode::global: return "global config"; case cfg_mode::config_override: return "config override"; case cfg_mode::continuous: return "continuous config"; + case cfg_mode::database_config: return "database config"; case cfg_mode::default_config: return "default config"; } return unknown; @@ -932,14 +934,14 @@ game_boot_result Emulator::GetElfPathFromDir(std::string& elf_path, const std::s return game_boot_result::invalid_file_or_folder; } -game_boot_result Emulator::BootGame(const std::string& path, const std::string& title_id, bool direct, cfg_mode config_mode, const std::string& config_path) +game_boot_result Emulator::BootGame(const std::string& path, const std::string& title_id, bool direct, cfg_mode config_mode, const std::string& config_path, const std::string& db_config) { if (m_restrict_emu_state_change) { return game_boot_result::currently_restricted; } - auto save_args = std::make_tuple(m_path, m_path_original, argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path); + auto save_args = std::make_tuple(m_path, m_path_original, argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path, m_db_config); auto restore_on_no_boot = [&](game_boot_result result) { @@ -949,7 +951,7 @@ game_boot_result Emulator::BootGame(const std::string& path, const std::string& if (m_state == system_state::stopped) { - std::tie(m_path, m_path_original, argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path) = std::move(save_args); + std::tie(m_path, m_path_original, argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path, m_db_config) = std::move(save_args); if (result != game_boot_result::no_errors) { @@ -964,7 +966,7 @@ game_boot_result Emulator::BootGame(const std::string& path, const std::string& // Execute after Kill() is done Emu.after_kill_callback = [this, result, save_args = std::move(save_args)]() mutable { - std::tie(m_path, m_path_original, argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path) = std::move(save_args); + std::tie(m_path, m_path_original, argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path, m_db_config) = std::move(save_args); if (result != game_boot_result::no_errors) { @@ -981,6 +983,7 @@ game_boot_result Emulator::BootGame(const std::string& path, const std::string& m_config_mode = config_mode; m_config_path = config_path; + m_db_config = db_config; // Handle files and special paths inside Load unmodified if (direct || !fs::is_dir(path)) @@ -1563,6 +1566,9 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, sys_log.notice("Version: APP_VER=%s VERSION=%s", version_app, version_disc); { + // We add the database configuration if it is set, unless we are using a mode that specifically selects a different configuration. + bool add_database_config = !m_db_config.empty() && (m_config_mode == cfg_mode::database_config || m_config_mode == cfg_mode::custom || m_config_mode == cfg_mode::continuous); + if (m_config_mode == cfg_mode::custom_selection || (m_config_mode == cfg_mode::continuous && !m_config_path.empty())) { if (fs::file cfg_file{ m_config_path }) @@ -1605,6 +1611,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, { g_cfg.name = config_path; m_config_path = config_path; + add_database_config = false; // A custom config exists. Do not add the database config. break; } @@ -1613,6 +1620,21 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } } + if (add_database_config) + { + // Add database config + sys_log.notice("Applying database config"); + + if (g_cfg.from_string(m_db_config)) + { + g_cfg.name = "database_config"; + } + else + { + sys_log.error("Failed to apply database config"); + } + } + // Disable incompatible settings fixup_settings(&_psf); @@ -3342,6 +3364,25 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s return; } + const auto reset_emu_state = [this]() + { + m_ar.reset(); + argv.clear(); + envp.clear(); + data.clear(); + disc.clear(); + klic.clear(); + hdd1.clear(); + init_mem_containers = nullptr; + m_db_config.clear(); + m_config_path.clear(); + m_config_mode = cfg_mode::custom; + read_used_savestate_versions(); + m_savestate_extension_flags1 = {}; + m_emu_state_close_pending = false; + m_precompilation_option = {}; + }; + if (system_state old_state = m_state.fetch_op([](system_state& state) { if (state == system_state::stopping || state == system_state::stopped) @@ -3360,21 +3401,8 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s } // Ensure clean state - m_ar.reset(); - argv.clear(); - envp.clear(); - data.clear(); - disc.clear(); - klic.clear(); - hdd1.clear(); - init_mem_containers = nullptr; + reset_emu_state(); after_kill_callback = nullptr; - m_config_path.clear(); - m_config_mode = cfg_mode::custom; - read_used_savestate_versions(); - m_savestate_extension_flags1 = {}; - m_emu_state_close_pending = false; - m_precompilation_option = {}; // Enable logging rpcs3::utils::configure_logs(true); @@ -3422,7 +3450,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s // There is no race condition because it is only accessed by the same thread std::shared_ptr> join_thread = std::make_shared>(); - *join_thread = make_ptr(new named_thread("Emulation Join Thread"sv, [join_thread, savestate, allow_autoexit, save_stage = save_stage ? *save_stage : savestate_stage{}, this]() mutable + *join_thread = make_ptr(new named_thread("Emulation Join Thread"sv, [join_thread, reset_emu_state, savestate, allow_autoexit, save_stage = save_stage ? *save_stage : savestate_stage{}, this]() mutable { fs::pending_file file; @@ -3921,7 +3949,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s set_progress_message("Resetting Objects"); // Final termination from main thread (move the last ownership of join thread in order to destroy it) - CallFromMainThread([join_thread = std::move(join_thread), verbose_message, stop_watchdog, init_mtx, allow_autoexit, this]() + CallFromMainThread([join_thread = std::move(join_thread), reset_emu_state, verbose_message, stop_watchdog, init_mtx, allow_autoexit, this]() { cpu_thread::cleanup(); @@ -3967,20 +3995,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s m_stop_ctr.notify_all(); // Boot arg cleanup (preserved in the case restarting) - argv.clear(); - envp.clear(); - data.clear(); - disc.clear(); - klic.clear(); - hdd1.clear(); - init_mem_containers = nullptr; - m_config_path.clear(); - m_config_mode = cfg_mode::custom; - m_ar.reset(); - read_used_savestate_versions(); - m_savestate_extension_flags1 = {}; - m_emu_state_close_pending = false; - m_precompilation_option = {}; + reset_emu_state(); if (!m_continuous_mode) { @@ -4055,14 +4070,14 @@ game_boot_result Emulator::Restart(bool graceful, bool reset_path) if (!IsStopped()) { - auto save_args = std::make_tuple(argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path); + auto save_args = std::make_tuple(argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path, m_db_config); if (graceful) GracefulShutdown(false, false); else Kill(false); - std::tie(argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path) = std::move(save_args); + std::tie(argv, envp, data, disc, klic, hdd1, m_config_mode, m_config_path, m_db_config) = std::move(save_args); } else { diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index a38b65089f..08db927452 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -58,6 +58,7 @@ enum class game_boot_result : u32 still_running, already_added, currently_restricted, + database_config_missing, }; constexpr bool is_error(game_boot_result res) @@ -145,6 +146,7 @@ class Emulator final cfg_mode m_config_mode = cfg_mode::custom; std::string m_config_path; + std::string m_db_config; std::string m_path; std::string m_path_old; std::string m_path_original; @@ -366,6 +368,11 @@ public: return m_config_path; } + const std::string& GetUsedDatabaseConfig() const + { + return m_db_config; + } + bool IsChildProcess() const { return m_config_mode == cfg_mode::continuous; @@ -415,7 +422,7 @@ public: return emulation_state_guard_t{this}; } - game_boot_result BootGame(const std::string& path, const std::string& title_id = "", bool direct = false, cfg_mode config_mode = cfg_mode::custom, const std::string& config_path = ""); + game_boot_result BootGame(const std::string& path, const std::string& title_id = "", bool direct = false, cfg_mode config_mode = cfg_mode::custom, const std::string& config_path = "", const std::string& db_config = ""); bool BootRsxCapture(const std::string& path); void SetForceBoot(bool force_boot); diff --git a/rpcs3/Emu/config_mode.h b/rpcs3/Emu/config_mode.h index 0ca006e0c8..23404ab2ee 100644 --- a/rpcs3/Emu/config_mode.h +++ b/rpcs3/Emu/config_mode.h @@ -7,5 +7,6 @@ enum class cfg_mode global, // Use global config. config_override, // Use config override. This does not use the global VFS settings! Fall back to global config. continuous, // Use same config as on last boot. Fall back to global config. + database_config, // Use database config. Fall back to global config. default_config // Use the default values of the config entries. }; diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index d25ca3905c..0181ce8c84 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -296,6 +296,9 @@ true + + true + true @@ -602,6 +605,9 @@ true + + true + true @@ -912,6 +918,7 @@ + @@ -1855,6 +1862,16 @@ "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing config_database.h... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing config_database.h... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) Moc%27ing game_compatibility.h... diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 28af783415..793e695d77 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -519,6 +519,15 @@ Gui\misc dialogs + + Generated Files\Debug + + + Generated Files\Release + + + Gui\game list + Generated Files\Debug @@ -1651,6 +1660,9 @@ Gui\save + + Gui\game list + Gui\game list diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index b0a75d9bfa..fdfb437fe7 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(rpcs3_ui STATIC clans_settings_dialog.cpp config_adapter.cpp config_checker.cpp + config_database.cpp curl_handle.cpp custom_dialog.cpp custom_table_widget_item.cpp diff --git a/rpcs3/rpcs3qt/config_database.cpp b/rpcs3/rpcs3qt/config_database.cpp new file mode 100644 index 0000000000..7e26252d3f --- /dev/null +++ b/rpcs3/rpcs3qt/config_database.cpp @@ -0,0 +1,228 @@ +#include "stdafx.h" +#include "config_database.h" +#include "gui_settings.h" +#include "downloader.h" +#include "Emu/system_config.h" + +LOG_CHANNEL(gui_log, "GUI"); + +config_database::config_database(std::shared_ptr settings, QWidget* parent) + : QObject(parent) + , m_gui_settings(std::move(settings)) +{ + m_filepath = m_gui_settings->GetSettingsDir() + "/config_database.dat"; + m_downloader = new downloader(parent); + request_config_database(); + + connect(m_downloader, &downloader::signal_download_error, this, &config_database::handle_download_error); + connect(m_downloader, &downloader::signal_download_finished, this, &config_database::handle_download_finished); + connect(m_downloader, &downloader::signal_download_canceled, this, &config_database::handle_download_canceled); +} + +config_database::~config_database() +{ +} + +bool config_database::has_config(const std::string& title_id) const +{ + return m_config_database.contains(title_id); +} + +std::optional config_database::get_config(const std::string& title_id) +{ + if (!m_config_database.contains(title_id)) + { + gui_log.error("Config database does not contain '%s'", title_id); + return std::nullopt; + } + + QFile file(m_filepath); + + if (!file.exists()) + { + gui_log.error("Config database file not found: %s", m_filepath); + return std::nullopt; + } + + if (!file.open(QIODevice::ReadOnly)) + { + gui_log.error("Config database error - Could not read database from file: %s", m_filepath); + return std::nullopt; + } + + const QByteArray content = file.readAll(); + file.close(); + + return read_json(content, false, title_id); +} + +void config_database::request_config_database(bool online) +{ + if (!online) + { + // Retrieve database from file + QFile file(m_filepath); + + if (!file.exists()) + { + gui_log.notice("Config database file not found: %s", m_filepath); + return; + } + + if (!file.open(QIODevice::ReadOnly)) + { + gui_log.error("Config database error - Could not read database from file: %s", m_filepath); + return; + } + + const QByteArray content = file.readAll(); + file.close(); + + gui_log.notice("Finished reading config database from file: %s", m_filepath); + + // Create new set from database + read_json(content, online); + + return; + } + + const std::string url = "https://api.rpcs3.net/config/?api=v1"; + gui_log.notice("Beginning config database download from: %s", url); + + m_downloader->start(url, true, true, true, tr("Downloading Config Database")); + + Q_EMIT download_started(); +} + +void config_database::handle_download_error(const QString& error) +{ + Q_EMIT download_error(error); +} + +void config_database::handle_download_finished(const QByteArray& content) +{ + gui_log.notice("Config database download finished"); + + // Create new map from database and write database to file if database was valid + if (read_json(content, true)) + { + // Write database to file + QFile file(m_filepath); + + if (file.exists()) + { + gui_log.notice("Config database file found: %s", m_filepath); + } + + if (!file.open(QIODevice::WriteOnly)) + { + gui_log.error("Config database error - Could not write database to file: %s", m_filepath); + return; + } + + file.write(content); + file.close(); + + gui_log.success("Wrote config database to file: %s", m_filepath); + } + + Q_EMIT download_finished(); +} + +void config_database::handle_download_canceled() +{ + Q_EMIT download_canceled(); +} + +std::optional config_database::read_json(const QByteArray& data, bool after_download, const std::string& serial) +{ + QJsonParseError error {}; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &error); + + if (!json_document.isObject()) + { + gui_log.error("Config database error - Invalid JSON: '%s'", error.errorString()); + return std::nullopt; + } + + const QJsonObject json_data = json_document.object(); + const int return_code = json_data["return_code"].toInt(-255); + + if (return_code < 0) + { + if (after_download) + { + std::string error_message; + switch (return_code) + { + case -1: error_message = "Server Error - Internal Error"; break; + case -2: error_message = "Server Error - Maintenance Mode"; break; + case -255: error_message = "Server Error - Return code not found"; break; + default: error_message = "Server Error - Unknown Error"; break; + } + gui_log.error("%s: return code %d", error_message, return_code); + Q_EMIT download_error(QString::fromStdString(error_message) + " " + QString::number(return_code)); + } + else + { + gui_log.error("Config database error - Invalid: return code %d", return_code); + } + return std::nullopt; + } + + if (!json_data["games"].isObject()) + { + gui_log.error("Config database error - No games found"); + return std::nullopt; + } + + std::unique_ptr config = std::make_unique(); + + const QJsonObject json_games = json_data["games"].toObject(); + + const auto validate = [&json_games, &config](const QString& serial) -> std::optional + { + if (!json_games[serial].isObject()) + { + gui_log.error("Config database error - Unusable object %s", serial); + return std::nullopt; + } + + const QJsonObject game = json_games[serial].toObject(); + if (!game["config"].isString()) + { + gui_log.error("Config database error - Unusable game string %s (config missing)", serial); + return std::nullopt; + } + + const std::string content = game["config"].toString().toStdString(); + + // Verify config + if (!config->validate(content)) + { + gui_log.error("Config database error - Invalid config for %s", serial); + return std::nullopt; + } + + return content; + }; + + if (serial.empty()) + { + m_config_database.clear(); + + // Retrieve status data for every valid entry + for (const QString& serial : json_games.keys()) + { + if (validate(serial)) + { + // Add title to set + m_config_database.insert(serial.toStdString()); + } + } + + return std::string(); + } + + return validate(QString::fromStdString(serial)); +} diff --git a/rpcs3/rpcs3qt/config_database.h b/rpcs3/rpcs3qt/config_database.h new file mode 100644 index 0000000000..e0283ddf7a --- /dev/null +++ b/rpcs3/rpcs3qt/config_database.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +class downloader; +class gui_settings; + +class config_database : public QObject +{ + Q_OBJECT + +public: + config_database(std::shared_ptr settings, QWidget* parent); + virtual ~config_database(); + + bool has_config(const std::string& title_id) const; + std::optional get_config(const std::string& title_id); + + /** Reads database. If online set to true: Downloads and writes the database to file */ + void request_config_database(bool online = false); + +Q_SIGNALS: + void download_started(); + void download_finished(); + void download_canceled(); + void download_error(const QString& error); + +private Q_SLOTS: + void handle_download_error(const QString& error); + void handle_download_finished(const QByteArray& content); + void handle_download_canceled(); + +private: + /** Creates new set from the database. Returns config for the optional serial. */ + std::optional read_json(const QByteArray& data, bool after_download, const std::string& serial = ""); + + std::shared_ptr m_gui_settings; + QString m_filepath; + downloader* m_downloader = nullptr; + + std::set m_config_database; +}; diff --git a/rpcs3/rpcs3qt/emu_settings.cpp b/rpcs3/rpcs3qt/emu_settings.cpp index 0ec017a02c..a5bd055432 100644 --- a/rpcs3/rpcs3qt/emu_settings.cpp +++ b/rpcs3/rpcs3qt/emu_settings.cpp @@ -117,7 +117,7 @@ bool emu_settings::Init() return true; } -void emu_settings::LoadSettings(const std::string& title_id, bool create_config_from_global) +void emu_settings::LoadSettings(const std::string& title_id, bool create_config_from_global, const std::string& db_config) { m_title_id = title_id; @@ -159,6 +159,22 @@ void emu_settings::LoadSettings(const std::string& title_id, bool create_config_ .arg(QString::fromStdString(global_config_path)).arg(QString::fromStdString(global_error)), QMessageBox::Ok); } } + else if (!db_config.empty()) + { + // Add database config + auto [config, error] = yaml_load(db_config); + + if (config && error.empty()) + { + m_current_settings += config; + } + else + { + cfg_log.fatal("Failed to load database config for '%s':\n%s", title_id, error); + QMessageBox::critical(nullptr, tr("Config Error"), tr("Failed to load database config:\nError: %1") + .arg(QString::fromStdString(error)), QMessageBox::Ok); + } + } // Add game config if (!title_id.empty()) diff --git a/rpcs3/rpcs3qt/emu_settings.h b/rpcs3/rpcs3qt/emu_settings.h index c5a1e89252..4a7f8ee40d 100644 --- a/rpcs3/rpcs3qt/emu_settings.h +++ b/rpcs3/rpcs3qt/emu_settings.h @@ -103,7 +103,7 @@ public: midi_creator m_midi_creator; /** Loads the settings from path.*/ - void LoadSettings(const std::string& title_id = "", bool create_config_from_global = true); + void LoadSettings(const std::string& title_id = "", bool create_config_from_global = true, const std::string& db_config = ""); /** Fixes all registered invalid settings after asking the user for permission.*/ void OpenCorrectionDialog(QWidget* parent = Q_NULLPTR); diff --git a/rpcs3/rpcs3qt/game_compatibility.cpp b/rpcs3/rpcs3qt/game_compatibility.cpp index a7a4a4bc34..7f408d39d3 100644 --- a/rpcs3/rpcs3qt/game_compatibility.cpp +++ b/rpcs3/rpcs3qt/game_compatibility.cpp @@ -13,9 +13,9 @@ LOG_CHANNEL(compat_log, "Compat"); -game_compatibility::game_compatibility(std::shared_ptr gui_settings, QWidget* parent) +game_compatibility::game_compatibility(std::shared_ptr settings, QWidget* parent) : QObject(parent) - , m_gui_settings(std::move(gui_settings)) + , m_gui_settings(std::move(settings)) { m_filepath = m_gui_settings->GetSettingsDir() + "/compat_database.dat"; m_downloader = new downloader(parent); @@ -58,7 +58,7 @@ void game_compatibility::handle_download_finished(const QByteArray& content) compat_log.success("Wrote database to file: %s", m_filepath); } - // We have a new database in map, therefore refresh gamelist to new state + // We have a new database in map, therefore refresh game list to new state Q_EMIT DownloadFinished(); } @@ -69,7 +69,16 @@ void game_compatibility::handle_download_canceled() bool game_compatibility::handle_json(const QByteArray& data, bool after_download) { - const QJsonObject json_data = QJsonDocument::fromJson(data).object(); + QJsonParseError error {}; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &error); + + if (!json_document.isObject()) + { + compat_log.error("Database Error - Invalid JSON: '%s'", error.errorString()); + return false; + } + + const QJsonObject json_data = json_document.object(); const int return_code = json_data["return_code"].toInt(-255); if (return_code < 0) @@ -220,11 +229,11 @@ void game_compatibility::RequestCompatibility(bool online) m_downloader->start(url, true, true, true, tr("Downloading Database")); - // We want to retrieve a new database, therefore refresh gamelist and indicate that + // We want to retrieve a new database, therefore refresh game list and indicate that Q_EMIT DownloadStarted(); } -compat::status game_compatibility::GetCompatibility(const std::string& title_id) +compat::status game_compatibility::GetCompatibility(const std::string& title_id) const { if (m_compat_database.empty()) { @@ -244,7 +253,7 @@ compat::status game_compatibility::GetStatusData(const QString& status) const return ::at32(Status_Data, status); } -compat::package_info game_compatibility::GetPkgInfo(const QString& pkg_path, game_compatibility* compat) +compat::package_info game_compatibility::GetPkgInfo(const QString& pkg_path, const game_compatibility* compat) { compat::package_info info; diff --git a/rpcs3/rpcs3qt/game_compatibility.h b/rpcs3/rpcs3qt/game_compatibility.h index 8dc1ce5aa5..7c4d1a80a1 100644 --- a/rpcs3/rpcs3qt/game_compatibility.h +++ b/rpcs3/rpcs3qt/game_compatibility.h @@ -148,13 +148,13 @@ public: void RequestCompatibility(bool online = false); /** Returns the compatibility status for the requested title */ - compat::status GetCompatibility(const std::string& title_id); + compat::status GetCompatibility(const std::string& title_id) const; /** Returns the data for the requested status */ compat::status GetStatusData(const QString& status) const; /** Returns package information like title, version, changelog etc. */ - static compat::package_info GetPkgInfo(const QString& pkg_path, game_compatibility* compat); + static compat::package_info GetPkgInfo(const QString& pkg_path, const game_compatibility* compat); Q_SIGNALS: void DownloadStarted(); diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp index 81f6f6e973..3fc831d443 100644 --- a/rpcs3/rpcs3qt/game_list_context_menu.cpp +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -11,6 +11,7 @@ #include "pad_settings_dialog.h" #include "patch_manager_dialog.h" #include "persistent_settings.h" +#include "config_database.h" #include "Utilities/File.h" #include "Emu/system_utils.hpp" @@ -65,7 +66,7 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& const bool is_current_running_game = game_list_actions::IsGameRunning(serial); // Make Actions - QAction* boot = new QAction(gameinfo->has_custom_config + QAction* boot = new QAction((gameinfo->has_custom_config || gameinfo->has_database_config) ? (is_current_running_game ? tr("&Reboot with Global Configuration") : tr("&Boot with Global Configuration")) @@ -88,7 +89,21 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& Q_EMIT m_game_list_frame->RequestBoot(gameinfo); }); } - else + + if (gameinfo->has_database_config) + { + QAction* boot_db = addAction(is_current_running_game + ? tr("&Reboot with Database Configuration") + : tr("&Boot with Database Configuration")); + boot_db->setFont(font); + connect(boot_db, &QAction::triggered, m_game_list_frame, [this, gameinfo] + { + sys_log.notice("Booting from gamelist per context menu..."); + Q_EMIT m_game_list_frame->RequestBoot(gameinfo, cfg_mode::database_config); + }); + } + + if (!gameinfo->has_custom_config && !gameinfo->has_database_config) { boot->setFont(font); } @@ -161,6 +176,8 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& : tr("&Create Custom Configuration From Global Settings")); QAction* create_game_default_config = gameinfo->has_custom_config ? nullptr : addAction(tr("&Create Custom Configuration From Default Settings")); + QAction* create_game_database_config = (gameinfo->has_custom_config || !gameinfo->has_database_config) ? nullptr + : addAction(tr("&Create Custom Configuration From Database Settings")); QAction* pad_configure = addAction(gameinfo->has_custom_pad_config ? tr("&Change Custom Gamepad Configuration") : tr("&Create Custom Gamepad Configuration")); @@ -578,6 +595,7 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& QAction* check_compat = addAction(tr("&Check Game Compatibility")); QAction* download_compat = addAction(tr("&Download Compatibility Database")); + QAction* download_config_db = addAction(tr("&Download Config Database")); addSeparator(); @@ -601,9 +619,10 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& Q_EMIT m_game_list_frame->RequestBoot(gameinfo, cfg_mode::global); }); - auto configure_l = [this, current_game, gameinfo](bool create_cfg_from_global_cfg) + const auto configure_game = [this, current_game, gameinfo](bool create_cfg_from_global_cfg, bool create_cfg_from_database) { - settings_dialog dlg(m_gui_settings, m_emu_settings, 0, m_game_list_frame, ¤t_game, create_cfg_from_global_cfg); + const std::optional db_config = create_cfg_from_database ? m_game_list_frame->GetConfigDatabase()->get_config(gameinfo->info.serial) : ""; + settings_dialog dlg(m_gui_settings, m_emu_settings, 0, m_game_list_frame, ¤t_game, create_cfg_from_global_cfg, db_config ? *db_config : ""); connect(&dlg, &settings_dialog::EmuSettingsApplied, [this, gameinfo]() { @@ -618,14 +637,16 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& dlg.exec(); }; + connect(configure, &QAction::triggered, this, [configure_game]() { configure_game(true, false); }); + if (create_game_default_config) { - connect(configure, &QAction::triggered, m_game_list_frame, [configure_l]() { configure_l(true); }); - connect(create_game_default_config, &QAction::triggered, m_game_list_frame, [configure_l = std::move(configure_l)]() { configure_l(false); }); + connect(create_game_default_config, &QAction::triggered, m_game_list_frame, [configure_game]() { configure_game(false, false); }); } - else + + if (create_game_database_config) { - connect(configure, &QAction::triggered, m_game_list_frame, [configure_l = std::move(configure_l)]() { configure_l(true); }); + connect(create_game_database_config, &QAction::triggered, m_game_list_frame, [configure_game]() { configure_game(false, true); }); } connect(pad_configure, &QAction::triggered, m_game_list_frame, [this, current_game, gameinfo]() @@ -671,7 +692,11 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& }); connect(download_compat, &QAction::triggered, m_game_list_frame, [this] { - ensure(m_game_list_frame->GetGameCompatibility())->RequestCompatibility(true); + m_game_list_frame->GetGameCompatibility()->RequestCompatibility(true); + }); + connect(download_config_db, &QAction::triggered, m_game_list_frame, [this] + { + m_game_list_frame->GetConfigDatabase()->request_config_database(true); }); connect(rename_title, &QAction::triggered, m_game_list_frame, [this, name, serial = QString::fromStdString(serial), global_pos] { @@ -935,7 +960,13 @@ void game_list_context_menu::show_multi_selection_context_menu(const std::vector QAction* download_compat = addAction(tr("&Download Compatibility Database")); connect(download_compat, &QAction::triggered, m_game_list_frame, [this] { - ensure(m_game_list_frame->GetGameCompatibility())->RequestCompatibility(true); + m_game_list_frame->GetGameCompatibility()->RequestCompatibility(true); + }); + + QAction* download_config_db = addAction(tr("&Download Config Database")); + connect(download_config_db, &QAction::triggered, m_game_list_frame, [this] + { + m_game_list_frame->GetConfigDatabase()->request_config_database(true); }); addSeparator(); diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index fff6f33362..050589240d 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -10,6 +10,7 @@ #include "game_list_table.h" #include "game_list_grid.h" #include "game_list_grid_item.h" +#include "config_database.h" #include "Emu/System.h" #include "Emu/vfs_config.h" @@ -74,6 +75,7 @@ game_list_frame::game_list_frame(std::shared_ptr gui_settings, std m_game_list->verticalScrollBar()->installEventFilter(this); m_game_compat = new game_compatibility(m_gui_settings, this); + m_config_db = new config_database(m_gui_settings, this); m_central_widget = new QStackedWidget(this); m_central_widget->addWidget(m_game_list); @@ -200,6 +202,22 @@ game_list_frame::game_list_frame(std::shared_ptr gui_settings, std QMessageBox::warning(this, tr("Warning!"), tr("Failed to retrieve the online compatibility database!\nFalling back to local database.\n\n%0").arg(error)); }); + connect(m_config_db, &config_database::download_started, this, [this]() + { + for (const auto& game : m_game_data) + { + game->has_database_config = false; + } + Refresh(); + }); + connect(m_config_db, &config_database::download_finished, this, &game_list_frame::OnConfigDatabaseFinished); + connect(m_config_db, &config_database::download_canceled, this, &game_list_frame::OnConfigDatabaseFinished); + connect(m_config_db, &config_database::download_error, this, [this](const QString& error) + { + OnConfigDatabaseFinished(); + QMessageBox::warning(this, tr("Warning!"), tr("Failed to retrieve the online config database!\nFalling back to local database.\n\n%0").arg(error)); + }); + connect(m_game_list, &game_list::FocusToSearchBar, this, &game_list_frame::FocusToSearchBar); connect(m_game_grid, &game_list_grid::FocusToSearchBar, this, &game_list_frame::FocusToSearchBar); @@ -801,6 +819,7 @@ void game_list_frame::OnParsingFinished() game.localized_category = std::move(qt_cat); game.compat = m_game_compat->GetCompatibility(game.info.serial); + game.has_database_config = m_config_db->has_config(game.info.serial); game.has_custom_config = fs::is_file(rpcs3::utils::get_custom_config_path(game.info.serial)); game.has_custom_pad_config = fs::is_file(rpcs3::utils::get_custom_input_config_path(game.info.serial)); @@ -1024,6 +1043,15 @@ void game_list_frame::OnCompatFinished() Refresh(); } +void game_list_frame::OnConfigDatabaseFinished() +{ + for (const auto& game : m_game_data) + { + game->has_database_config = m_config_db->has_config(game->info.serial); + } + Refresh(); +} + void game_list_frame::ToggleCategoryFilter(const QStringList& categories, bool show) { QStringList& filters = m_is_list_layout ? m_category_filters : m_grid_category_filters; diff --git a/rpcs3/rpcs3qt/game_list_frame.h b/rpcs3/rpcs3qt/game_list_frame.h index b01dbd5a6e..b195cdf5c7 100644 --- a/rpcs3/rpcs3qt/game_list_frame.h +++ b/rpcs3/rpcs3qt/game_list_frame.h @@ -18,6 +18,7 @@ #include #include +class config_database; class game_list_table; class game_list_grid; class gui_settings; @@ -53,7 +54,8 @@ public: void SetShowHidden(bool show); - game_compatibility* GetGameCompatibility() const { return m_game_compat; } + game_compatibility* GetGameCompatibility() const { return ensure(m_game_compat); } + config_database* GetConfigDatabase() const { return ensure(m_config_db); } const std::vector& GetGameInfo() const { return m_game_data; } std::shared_ptr actions() const { return m_game_list_actions; } std::shared_ptr get_gui_settings() const { return m_gui_settings; } @@ -95,6 +97,7 @@ private Q_SLOTS: void OnParsingFinished(); void OnRefreshFinished(); void OnCompatFinished(); + void OnConfigDatabaseFinished(); void OnColClicked(int col); void ShowContextMenu(const QPoint& pos); void doubleClickedSlot(QTableWidgetItem* item); @@ -151,6 +154,7 @@ private: // Game List game_list_table* m_game_list = nullptr; game_compatibility* m_game_compat = nullptr; + config_database* m_config_db = nullptr; progress_dialog* m_progress_dialog = nullptr; std::map m_column_acts; Qt::SortOrder m_col_sort_order{}; diff --git a/rpcs3/rpcs3qt/gui_game_info.h b/rpcs3/rpcs3qt/gui_game_info.h index 693483dd6a..b06e97e8c2 100644 --- a/rpcs3/rpcs3qt/gui_game_info.h +++ b/rpcs3/rpcs3qt/gui_game_info.h @@ -13,9 +13,10 @@ struct gui_game_info { GameInfo info{}; QString localized_category; - compat::status compat; + compat::status compat{}; QPixmap icon; QPixmap pxmap; + bool has_database_config = false; bool has_custom_config = false; bool has_custom_pad_config = false; bool has_custom_icon = false; diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 2a366705ab..22ce52d89c 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -47,6 +47,7 @@ #include "music_player_dialog.h" #include "sound_effect_manager_dialog.h" #include "recording_settings_dialog.h" +#include "config_database.h" #include #include @@ -493,6 +494,9 @@ void main_window::show_boot_error(game_boot_result status) case game_boot_result::firmware_version: message = tr("The game or PS3 application needs a more recent firmware version."); break; + case game_boot_result::database_config_missing: + message = tr("Could not find any configuration for this game in the database."); + break; case game_boot_result::firmware_missing: // Handled elsewhere case game_boot_result::already_added: // Handled elsewhere case game_boot_result::currently_restricted: @@ -530,7 +534,25 @@ void main_window::Boot(const std::string& path, const std::string& title_id, boo m_app_icon = gui::utils::get_app_icon_from_path(path, title_id); - if (const auto error = Emu.BootGame(path, title_id, direct, config_mode, config_path); error != game_boot_result::no_errors) + std::string db_config; + + // Get database config if possible or if we are in database_config mode (to ensure we see an error on invalid use) + if (config_database* db = m_game_list_frame->GetConfigDatabase(); + db->has_config(title_id) || config_mode == cfg_mode::database_config) + { + const std::optional config = db->get_config(title_id); + + if (!config) + { + gui_log.error("Boot failed: reason: no database config found for '%s'", title_id); + show_boot_error(game_boot_result::database_config_missing); + return; + } + + db_config = *config; + } + + if (const auto error = Emu.BootGame(path, title_id, direct, config_mode, config_path, db_config); error != game_boot_result::no_errors) { gui_log.error("Boot failed: reason: %s, path: %s", error, path); show_boot_error(error); @@ -940,7 +962,7 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo bool precompile_caches = false; bool canceled = false; - game_compatibility* compat = m_game_list_frame ? m_game_list_frame->GetGameCompatibility() : nullptr; + const game_compatibility* compat = m_game_list_frame ? m_game_list_frame->GetGameCompatibility() : nullptr; // Let the user choose the packages to install and select the order in which they shall be installed. pkg_install_dialog dlg(file_paths, compat, this); diff --git a/rpcs3/rpcs3qt/patch_manager_dialog.cpp b/rpcs3/rpcs3qt/patch_manager_dialog.cpp index 2e9d980d20..de6db736e4 100644 --- a/rpcs3/rpcs3qt/patch_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/patch_manager_dialog.cpp @@ -1168,7 +1168,16 @@ void patch_manager_dialog::download_update(bool automatic, bool auto_accept) bool patch_manager_dialog::handle_json(const QByteArray& data) { - const QJsonObject json_data = QJsonDocument::fromJson(data).object(); + QJsonParseError error {}; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &error); + + if (!json_document.isObject()) + { + patch_log.error("Patch download error - Invalid JSON: '%s'", error.errorString()); + return false; + } + + const QJsonObject json_data = json_document.object(); const int return_code = json_data["return_code"].toInt(-255); if (return_code < 0) diff --git a/rpcs3/rpcs3qt/pkg_install_dialog.cpp b/rpcs3/rpcs3qt/pkg_install_dialog.cpp index bd17c4eaff..a97f656153 100644 --- a/rpcs3/rpcs3qt/pkg_install_dialog.cpp +++ b/rpcs3/rpcs3qt/pkg_install_dialog.cpp @@ -24,7 +24,7 @@ enum Roles DataSizeRole = Qt::UserRole + 5, }; -pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibility* compat, QWidget* parent) +pkg_install_dialog::pkg_install_dialog(const QStringList& paths, const game_compatibility* compat, QWidget* parent) : QDialog(parent) { ensure(!paths.empty()); diff --git a/rpcs3/rpcs3qt/pkg_install_dialog.h b/rpcs3/rpcs3qt/pkg_install_dialog.h index b1f3dbeb9d..a6174276c3 100644 --- a/rpcs3/rpcs3qt/pkg_install_dialog.h +++ b/rpcs3/rpcs3qt/pkg_install_dialog.h @@ -17,7 +17,7 @@ class pkg_install_dialog : public QDialog Q_OBJECT public: - explicit pkg_install_dialog(const QStringList& paths, game_compatibility* compat, QWidget* parent = nullptr); + explicit pkg_install_dialog(const QStringList& paths, const game_compatibility* compat, QWidget* parent = nullptr); std::vector get_paths_to_install() const; bool precompile_caches() const { return m_precompile_caches; } bool create_desktop_shortcuts() const { return m_create_desktop_shortcuts; } diff --git a/rpcs3/rpcs3qt/settings_dialog.cpp b/rpcs3/rpcs3qt/settings_dialog.cpp index 6787caa665..26e062b67b 100644 --- a/rpcs3/rpcs3qt/settings_dialog.cpp +++ b/rpcs3/rpcs3qt/settings_dialog.cpp @@ -87,7 +87,7 @@ void remove_item(QComboBox* box, int data_value, int def_value) extern const std::map g_prx_list; -settings_dialog::settings_dialog(std::shared_ptr gui_settings, std::shared_ptr emu_settings, int tab_index, QWidget* parent, const GameInfo* game, bool create_cfg_from_global_cfg) +settings_dialog::settings_dialog(std::shared_ptr gui_settings, std::shared_ptr emu_settings, int tab_index, QWidget* parent, const GameInfo* game, bool create_cfg_from_global_cfg, const std::string& db_config) : QDialog(parent) , m_tab_index(tab_index) , ui(new Ui::settings_dialog) @@ -132,7 +132,7 @@ settings_dialog::settings_dialog(std::shared_ptr gui_settings, std if (game) { - m_emu_settings->LoadSettings(game->serial, create_cfg_from_global_cfg); + m_emu_settings->LoadSettings(game->serial, create_cfg_from_global_cfg, db_config); setWindowTitle(tr("Settings: [%0] %1", "Settings dialog").arg(QString::fromStdString(game->serial)).arg(QString::fromStdString(game->name))); } else diff --git a/rpcs3/rpcs3qt/settings_dialog.h b/rpcs3/rpcs3qt/settings_dialog.h index 0513227e80..a7402850a3 100644 --- a/rpcs3/rpcs3qt/settings_dialog.h +++ b/rpcs3/rpcs3qt/settings_dialog.h @@ -21,7 +21,7 @@ class settings_dialog : public QDialog Q_OBJECT public: - explicit settings_dialog(std::shared_ptr gui_settings, std::shared_ptr emu_settings, int tab_index = 0, QWidget* parent = nullptr, const GameInfo* game = nullptr, bool create_cfg_from_global_cfg = true); + explicit settings_dialog(std::shared_ptr gui_settings, std::shared_ptr emu_settings, int tab_index = 0, QWidget* parent = nullptr, const GameInfo* game = nullptr, bool create_cfg_from_global_cfg = true, const std::string& db_config = ""); ~settings_dialog(); void open() override; Q_SIGNALS: diff --git a/rpcs3/rpcs3qt/update_manager.cpp b/rpcs3/rpcs3qt/update_manager.cpp index a34292ce0e..a66ac6ea46 100644 --- a/rpcs3/rpcs3qt/update_manager.cpp +++ b/rpcs3/rpcs3qt/update_manager.cpp @@ -116,7 +116,16 @@ bool update_manager::handle_json(bool automatic, bool check_only, bool auto_acce { update_log.notice("Download of update info finished. automatic=%d, check_only=%d, auto_accept=%d", automatic, check_only, auto_accept); - const QJsonObject json_data = QJsonDocument::fromJson(data).object(); + QJsonParseError error {}; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &error); + + if (!json_document.isObject()) + { + update_log.error("Update error - Invalid JSON: '%s'", error.errorString()); + return false; + } + + const QJsonObject json_data = json_document.object(); const int return_code = json_data["return_code"].toInt(-255); m_update_info.hash_found = true; From 1cdc401cc558a367d474e7a2a01f07e738ba5478 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Mon, 13 Apr 2026 18:23:56 +0200 Subject: [PATCH 20/29] Use database per default, remove global --- rpcs3/Emu/System.cpp | 3 +-- rpcs3/Emu/config_mode.h | 3 +-- rpcs3/rpcs3qt/game_list_context_menu.cpp | 24 +++++------------------- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index d5facf3262..40a87fcab8 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -174,10 +174,9 @@ void fmt_class_string::format(std::string& out, u64 arg) { case cfg_mode::custom: return "custom config"; case cfg_mode::custom_selection: return "custom config selection"; - case cfg_mode::global: return "global config"; + case cfg_mode::database_config: return "database config"; case cfg_mode::config_override: return "config override"; case cfg_mode::continuous: return "continuous config"; - case cfg_mode::database_config: return "database config"; case cfg_mode::default_config: return "default config"; } return unknown; diff --git a/rpcs3/Emu/config_mode.h b/rpcs3/Emu/config_mode.h index 23404ab2ee..1918def27c 100644 --- a/rpcs3/Emu/config_mode.h +++ b/rpcs3/Emu/config_mode.h @@ -4,9 +4,8 @@ enum class cfg_mode { custom, // Prefer regular custom config. Fall back to global config. custom_selection, // Use user-selected custom config. Fall back to global config. - global, // Use global config. + database_config, // Use database config. Fall back to global config. config_override, // Use config override. This does not use the global VFS settings! Fall back to global config. continuous, // Use same config as on last boot. Fall back to global config. - database_config, // Use database config. Fall back to global config. default_config // Use the default values of the config entries. }; diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp index 3fc831d443..c937c53174 100644 --- a/rpcs3/rpcs3qt/game_list_context_menu.cpp +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -66,10 +66,10 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& const bool is_current_running_game = game_list_actions::IsGameRunning(serial); // Make Actions - QAction* boot = new QAction((gameinfo->has_custom_config || gameinfo->has_database_config) + QAction* boot = new QAction(gameinfo->has_custom_config ? (is_current_running_game - ? tr("&Reboot with Global Configuration") - : tr("&Boot with Global Configuration")) + ? (gameinfo->has_database_config ? tr("&Reboot with Database + Global Configuration") : tr("&Reboot with Global Configuration")) + : (gameinfo->has_database_config ? tr("&Boot with Database + Global Configuration") : tr("&Boot with Global Configuration"))) : (is_current_running_game ? tr("&Reboot") : tr("&Boot"))); @@ -89,21 +89,7 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& Q_EMIT m_game_list_frame->RequestBoot(gameinfo); }); } - - if (gameinfo->has_database_config) - { - QAction* boot_db = addAction(is_current_running_game - ? tr("&Reboot with Database Configuration") - : tr("&Boot with Database Configuration")); - boot_db->setFont(font); - connect(boot_db, &QAction::triggered, m_game_list_frame, [this, gameinfo] - { - sys_log.notice("Booting from gamelist per context menu..."); - Q_EMIT m_game_list_frame->RequestBoot(gameinfo, cfg_mode::database_config); - }); - } - - if (!gameinfo->has_custom_config && !gameinfo->has_database_config) + else { boot->setFont(font); } @@ -616,7 +602,7 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& connect(boot, &QAction::triggered, m_game_list_frame, [this, gameinfo]() { sys_log.notice("Booting from gamelist per context menu..."); - Q_EMIT m_game_list_frame->RequestBoot(gameinfo, cfg_mode::global); + Q_EMIT m_game_list_frame->RequestBoot(gameinfo, cfg_mode::database_config); }); const auto configure_game = [this, current_game, gameinfo](bool create_cfg_from_global_cfg, bool create_cfg_from_database) From a543f3870486b5db794221b217a3f657e0ce5bab Mon Sep 17 00:00:00 2001 From: Megamouse Date: Mon, 13 Apr 2026 19:56:58 +0200 Subject: [PATCH 21/29] linux/macOs: update compat_db and config_db on update --- rpcs3/rpcs3qt/main_window.cpp | 23 +++++++++++++++++++++-- rpcs3/rpcs3qt/main_window.h | 2 +- rpcs3/rpcs3qt/settings_dialog.ui | 8 ++++---- rpcs3/rpcs3qt/update_manager.cpp | 28 ++++++++++++++++++---------- rpcs3/rpcs3qt/update_manager.h | 3 ++- 5 files changed, 46 insertions(+), 18 deletions(-) diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 22ce52d89c..0bf6a060e1 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -169,7 +169,7 @@ extern void qt_events_aware_op(int repeat_duration_ms, std::function wra } } -main_window::main_window(std::shared_ptr gui_settings, std::shared_ptr emu_settings, std::shared_ptr persistent_settings, QWidget *parent) +main_window::main_window(std::shared_ptr gui_settings, std::shared_ptr emu_settings, std::shared_ptr persistent_settings, QWidget* parent) : QMainWindow(parent) , ui(new Ui::main_window) , m_gui_settings(gui_settings) @@ -234,7 +234,7 @@ bool main_window::Init([[maybe_unused]] bool with_cli_boot) connect(ui->actionDownload_Update, &QAction::triggered, this, [this] { - m_updater.update(false); + m_updater.update(false, true); }); #ifdef _WIN32 @@ -260,6 +260,25 @@ bool main_window::Init([[maybe_unused]] bool with_cli_boot) #endif #ifdef RPCS3_UPDATE_SUPPORTED +#ifndef _WIN32 + connect(&m_updater, &update_manager::signal_download_additional_files, this, [this](bool auto_accept) + { + if (!m_game_list_frame) return; + + connect(m_game_list_frame->GetGameCompatibility(), &game_compatibility::DownloadFinished, this, [this, auto_accept]() + { + connect(m_game_list_frame->GetConfigDatabase(), &config_database::download_finished, this, [this, auto_accept]() + { + m_updater.update(auto_accept, false); + }, Qt::ConnectionType::SingleShotConnection); + + m_game_list_frame->GetConfigDatabase()->request_config_database(true); + }, Qt::ConnectionType::SingleShotConnection); + + m_game_list_frame->GetGameCompatibility()->RequestCompatibility(true); + }); +#endif + if (const auto update_value = m_gui_settings->GetValue(gui::m_check_upd_start).toString(); update_value != gui::update_off) { const bool in_background = with_cli_boot || update_value == gui::update_bkg; diff --git a/rpcs3/rpcs3qt/main_window.h b/rpcs3/rpcs3qt/main_window.h index e9206070cf..b1dc4847d1 100644 --- a/rpcs3/rpcs3qt/main_window.h +++ b/rpcs3/rpcs3qt/main_window.h @@ -70,7 +70,7 @@ class main_window : public QMainWindow }; public: - explicit main_window(std::shared_ptr gui_settings, std::shared_ptr emu_settings, std::shared_ptr persistent_settings, QWidget *parent = nullptr); + explicit main_window(std::shared_ptr gui_settings, std::shared_ptr emu_settings, std::shared_ptr persistent_settings, QWidget* parent = nullptr); ~main_window(); bool Init(bool with_cli_boot); QIcon GetAppIcon() const; diff --git a/rpcs3/rpcs3qt/settings_dialog.ui b/rpcs3/rpcs3qt/settings_dialog.ui index 9e23f48f1a..77b3f26593 100644 --- a/rpcs3/rpcs3qt/settings_dialog.ui +++ b/rpcs3/rpcs3qt/settings_dialog.ui @@ -4519,11 +4519,11 @@ - + - Accurate PPU/SPU Double-Precision FMA + Accurate PPU/SPU Double-Precision FMA - + @@ -4575,7 +4575,7 @@ - + Qt::Orientation::Vertical diff --git a/rpcs3/rpcs3qt/update_manager.cpp b/rpcs3/rpcs3qt/update_manager.cpp index a66ac6ea46..87d3b445b6 100644 --- a/rpcs3/rpcs3qt/update_manager.cpp +++ b/rpcs3/rpcs3qt/update_manager.cpp @@ -46,6 +46,8 @@ LOG_CHANNEL(update_log, "UPDATER"); +constexpr bool allow_local_auto_update = false; // Set true for debugging the auto updater locally + update_manager::update_manager(QObject* parent, std::shared_ptr gui_settings) : QObject(parent), m_gui_settings(std::move(gui_settings)) { @@ -60,7 +62,7 @@ void update_manager::check_for_updates(bool automatic, bool check_only, bool aut if (automatic) { // Don't check for updates on local builds - if (rpcs3::is_local_build()) + if (!allow_local_auto_update && rpcs3::is_local_build()) { update_log.notice("Skipped automatic update check: this is a local build"); return; @@ -135,7 +137,7 @@ bool update_manager::handle_json(bool automatic, bool check_only, bool auto_acce std::string error_message; switch (return_code) { - case -1: error_message = "Hash not found(Custom/PR build)"; break; + case -1: error_message = "Hash not found (Custom/PR build)"; break; case -2: error_message = "Server Error - Maintenance Mode"; break; case -3: error_message = "Server Error - Illegal Search"; break; case -255: error_message = "Server Error - Return code not found"; break; @@ -148,14 +150,12 @@ bool update_manager::handle_json(bool automatic, bool check_only, bool auto_acce update_log.warning("Update error: %s, return code: %d", error_message, return_code); // If a user clicks "Check for Updates" with a custom build ask him if he's sure he wants to update to latest version - if (!automatic && return_code == -1) - { - m_update_info.hash_found = false; - } - else + if (!allow_local_auto_update && (automatic || return_code != -1)) { return false; } + + m_update_info.hash_found = false; } const auto& current = json_data["current_build"]; @@ -311,17 +311,17 @@ bool update_manager::handle_json(bool automatic, bool check_only, bool auto_acce return true; } - update(auto_accept); + update(auto_accept, true); return true; } -void update_manager::update(bool auto_accept) +void update_manager::update(bool auto_accept, bool is_first_call) { update_log.notice("Updating with auto_accept=%d", auto_accept); ensure(m_downloader); - if (!auto_accept) + if (!auto_accept && is_first_call) { if (!m_update_info.update_found) { @@ -425,6 +425,14 @@ void update_manager::update(bool auto_accept) return; } +#ifndef _WIN32 + if (is_first_call) + { + Q_EMIT signal_download_additional_files(auto_accept); + return; + } +#endif + m_downloader->disconnect(); connect(m_downloader, &downloader::signal_download_error, this, [this](const QString& /*error*/) diff --git a/rpcs3/rpcs3qt/update_manager.h b/rpcs3/rpcs3qt/update_manager.h index 98ef5cf3f1..62986b796b 100644 --- a/rpcs3/rpcs3qt/update_manager.h +++ b/rpcs3/rpcs3qt/update_manager.h @@ -16,10 +16,11 @@ class update_manager final : public QObject public: update_manager(QObject* parent, std::shared_ptr gui_settings); void check_for_updates(bool automatic, bool check_only, bool auto_accept, QWidget* parent = nullptr); - void update(bool auto_accept); + void update(bool auto_accept, bool is_first_call); Q_SIGNALS: void signal_update_available(bool update_available); + void signal_download_additional_files(bool auto_accept); private: downloader* m_downloader = nullptr; From e79f56bfdc1b37ea224c636250b990fcfdecf2ed Mon Sep 17 00:00:00 2001 From: Megamouse Date: Wed, 15 Apr 2026 23:20:34 +0200 Subject: [PATCH 22/29] Qt: put Custom Config with Database Settings first --- rpcs3/rpcs3qt/game_list_context_menu.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp index c937c53174..05fc9fdb8d 100644 --- a/rpcs3/rpcs3qt/game_list_context_menu.cpp +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -157,13 +157,13 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& addSeparator(); + QAction* create_game_database_config = (gameinfo->has_custom_config || !gameinfo->has_database_config) ? nullptr + : addAction(tr("&Create Custom Configuration From Database Settings")); QAction* configure = addAction(gameinfo->has_custom_config ? tr("&Change Custom Configuration") : tr("&Create Custom Configuration From Global Settings")); QAction* create_game_default_config = gameinfo->has_custom_config ? nullptr : addAction(tr("&Create Custom Configuration From Default Settings")); - QAction* create_game_database_config = (gameinfo->has_custom_config || !gameinfo->has_database_config) ? nullptr - : addAction(tr("&Create Custom Configuration From Database Settings")); QAction* pad_configure = addAction(gameinfo->has_custom_pad_config ? tr("&Change Custom Gamepad Configuration") : tr("&Create Custom Gamepad Configuration")); From ea0d8a0d78911cef7ae094079a0727365f747d4e Mon Sep 17 00:00:00 2001 From: Megamouse Date: Wed, 15 Apr 2026 23:47:13 +0200 Subject: [PATCH 23/29] Qt/input: fix mouse button names I tested the last PR with an older commit and thought it worked. --- rpcs3/Input/keyboard_pad_handler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcs3/Input/keyboard_pad_handler.cpp b/rpcs3/Input/keyboard_pad_handler.cpp index 5e93ce64bd..d48b857ccf 100644 --- a/rpcs3/Input/keyboard_pad_handler.cpp +++ b/rpcs3/Input/keyboard_pad_handler.cpp @@ -788,7 +788,7 @@ std::vector keyboard_pad_handler::list_devices() std::string keyboard_pad_handler::GetMouseName(const QMouseEvent* event) { - return GetMouseName(event->button()); + return GetMouseName(static_cast(mouse::button) + static_cast(event->button())); } std::string keyboard_pad_handler::GetMouseName(u32 button) From b7297720419a34d29c82e0ece1e034b0c7b2fb3d Mon Sep 17 00:00:00 2001 From: schm1dtmac Date: Thu, 16 Apr 2026 00:47:43 +0100 Subject: [PATCH 24/29] Try building MVK 1.4.2 privapi instead of DLing 1.4.1 privapi prebuilt --- .ci/deploy-mac.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.ci/deploy-mac.sh b/.ci/deploy-mac.sh index 819e0725bc..930a87eee2 100755 --- a/.ci/deploy-mac.sh +++ b/.ci/deploy-mac.sh @@ -4,11 +4,15 @@ cd build || exit 1 cd bin +git clone --revision=32dceb35e2c95b46cec501033cbc3a1ddf32d6e8 https://github.com/KhronosGroup/MoltenVK.git +cd MoltenVK +./fetchDependencies --macos +make macos MVK_USE_METAL_PRIVATE_API=1 +cd ../ + mkdir -p "rpcs3.app/Contents/Resources/vulkan/icd.d" || true -wget https://github.com/KhronosGroup/MoltenVK/releases/download/v1.4.1/MoltenVK-macos-privateapi.tar -tar -xvf MoltenVK-macos-privateapi.tar -cp "MoltenVK/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib" "rpcs3.app/Contents/Frameworks/libMoltenVK.dylib" -cp "MoltenVK/MoltenVK/dynamic/dylib/macOS/MoltenVK_icd.json" "rpcs3.app/Contents/Resources/vulkan/icd.d/MoltenVK_icd.json" +cp "MoltenVK/Package/Latest/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib" "rpcs3.app/Contents/Frameworks/libMoltenVK.dylib" +cp "MoltenVK/Package/Latest/MoltenVK/dynamic/dylib/macOS/MoltenVK_icd.json" "rpcs3.app/Contents/Resources/vulkan/icd.d/MoltenVK_icd.json" sed -i '' "s/.\//..\/..\/..\/Frameworks\//g" "rpcs3.app/Contents/Resources/vulkan/icd.d/MoltenVK_icd.json" cp "$(realpath $BREW_PATH/opt/llvm@$LLVM_COMPILER_VER/lib/c++/libc++abi.1.0.dylib)" "rpcs3.app/Contents/Frameworks/libc++abi.1.dylib" From 48acbbe4f56ba4b181624a53fd35b02c403bf301 Mon Sep 17 00:00:00 2001 From: Elad <18193363+elad335@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:34:14 +0300 Subject: [PATCH 25/29] sys_fs: Reimplement path analysis --- rpcs3/Emu/Cell/lv2/sys_fs.cpp | 179 +++++++++++++++++++++++----------- rpcs3/Emu/Cell/lv2/sys_fs.h | 10 +- 2 files changed, 129 insertions(+), 60 deletions(-) diff --git a/rpcs3/Emu/Cell/lv2/sys_fs.cpp b/rpcs3/Emu/Cell/lv2/sys_fs.cpp index 2534f6a8c1..cfbab23419 100644 --- a/rpcs3/Emu/Cell/lv2/sys_fs.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_fs.cpp @@ -14,7 +14,6 @@ #include "Emu/system_utils.hpp" #include "Emu/Cell/lv2/sys_process.h" -#include #include #include @@ -93,15 +92,22 @@ void fmt_class_string::format(std::string& out, u64 arg) bool has_fs_write_rights(std::string_view vpath) { // VSH has access to everything - if (g_ps3_process_info.has_root_perm()) - return true; + const bool has_root_perm = g_ps3_process_info.has_root_perm(); - const auto norm_vpath = lv2_fs_object::get_normalized_path(vpath); - const auto parent_dir = fs::get_parent_dir_view(norm_vpath); + const auto parent_dir = fs::get_parent_dir_view(vpath); + const auto [dev_root, trail] = lv2_fs_object::get_path_root_and_trail(parent_dir); // This is not exhaustive, PS3 has a unix filesystem with rights for each directory and files - // This is mostly meant to protect against games doing insane things(ie NPUB30003 => NPUB30008) - if (parent_dir == "/dev_hdd0" || parent_dir == "/dev_hdd0/game") + // This is mostly meant to protect against games doing insane things (ie NPUB30003 => NPUB30008) + if (dev_root == "dev_hdd0"sv && (trail.empty() || trail == "game"sv)) + return has_root_perm; + + // This is read-only for games + if (dev_root.starts_with("dev_flash"sv)) + return has_root_perm; + + // Technically should not reach here, but handle it anyways + if (dev_root == "dev_bdvd"sv || dev_root == "dev_ps2disc"sv || dev_root.empty()) return false; return true; @@ -205,27 +211,29 @@ bool lv2_fs_mount_info_map::remove(std::string_view path) const lv2_fs_mount_info& lv2_fs_mount_info_map::lookup(std::string_view path, bool no_cell_fs_path, std::string* mount_path) const { - if (path.starts_with("/"sv)) + const auto [dev_root, trail] = lv2_fs_object::get_path_root_and_trail(path); + + if (dev_root.empty()) + { + if (trail.empty()) + { + return map.find("/")->second; + } + + return g_mi_sys_not_found; + } + + if (const auto iterator = map.find("/" + std::string{dev_root}); iterator != map.end()) { constexpr std::string_view cell_fs_path = "CELL_FS_PATH:"sv; - const std::string normalized_path = lv2_fs_object::get_normalized_path(path); - std::string_view parent_dir; - u32 parent_level = 0; - do - { - parent_dir = fs::get_parent_dir_view(normalized_path, parent_level++); - if (const auto iterator = map.find(parent_dir); iterator != map.end()) - { - if (iterator->second == &g_mp_sys_dev_root && parent_level > 1) - break; - if (no_cell_fs_path && iterator->second.device.starts_with(cell_fs_path)) - return lookup(iterator->second.device.substr(cell_fs_path.size()), no_cell_fs_path, mount_path); // Recursively look up the parent mount info - if (mount_path) - *mount_path = iterator->first; - return iterator->second; - } - } while (parent_dir.length() > 1); // Exit the loop when parent_dir == "/" or empty + if (no_cell_fs_path && iterator->second.device.starts_with(cell_fs_path)) + return lookup(iterator->second.device.substr(cell_fs_path.size()), no_cell_fs_path, mount_path); // Recursively look up the parent mount info + + if (mount_path) + *mount_path = iterator->first; + + return iterator->second; } return g_mi_sys_not_found; @@ -287,36 +295,89 @@ bool lv2_fs_mount_info_map::vfs_unmount(std::string_view vpath, bool remove_from return result; } -std::string lv2_fs_object::get_normalized_path(std::string_view path) +std::pair lv2_fs_object::get_path_root_and_trail(std::string_view filename) { - std::string normalized_path = std::filesystem::path(path).lexically_normal().string(); - -#ifdef _WIN32 - std::replace(normalized_path.begin(), normalized_path.end(), '\\', '/'); -#endif - - if (normalized_path.ends_with('/')) - normalized_path.pop_back(); - - return normalized_path.empty() ? "/" : normalized_path; -} - -std::string lv2_fs_object::get_device_root(std::string_view filename) -{ - std::string path = get_normalized_path(filename); // Prevent getting fooled by ".." trick such as "/dev_usb000/../dev_flash" - - if (const auto first = path.find_first_not_of("/"sv); first != umax) + if (filename.empty()) { - if (const auto pos = path.substr(first).find_first_of("/"sv); pos != umax) - path = path.substr(0, first + pos); - path = path.substr(std::max>(0, first - 1)); // Remove duplicate leading '/' while keeping only one - } - else - { - path = path.substr(0, 1); + // Should CELL_ENOENT later - root cannot have a trail + return {""sv, "ENOENT"}; } - return path; + std::string_view root; + std::string trail; + + usz level = 0; + usz pos = 0; + + while (pos != umax) + { + const usz ndl_pos = filename.find_first_not_of("/", pos); + + if (ndl_pos == pos) + { + // Should CELL_ENOENT later - root cannot have a trail + return {""sv, "ENOENT"}; + } + + if (ndl_pos == umax) + { + break; + } + + const usz dl_pos = ndl_pos == umax ? usz{umax} : filename.find_first_of("/", ndl_pos); + std::string_view component = filename.substr(ndl_pos, dl_pos - ndl_pos); + + if (component == "."sv) + { + // No change + // level += 0; + pos = dl_pos; + continue; + } + + if (component == ".."sv) + { + if (level > 1) + { + ensure(!trail.empty()); + trail.resize(trail.find_last_of("/") + 1); + trail.resize(trail.find_last_not_of("/") + 1); + } + else if (level == 1) + { + // Reset root + root = {}; + } + else//if (level == 0) + { + // Should CELL_ENOENT later - root cannot have a trail + return {""sv, "ENOENT"}; + } + + ensure(level)--; + pos = dl_pos; + continue; + } + + if (level == 0) + { + root = component; + } + else if (trail.empty()) + { + trail = std::string{component}; + } + else + { + trail += "/"; + trail.append(component); + } + + level++; + pos = dl_pos; + } + + return { root, std::move(trail) }; } lv2_fs_mount_point* lv2_fs_object::get_mp(std::string_view filename, std::string* vfs_path) @@ -328,7 +389,7 @@ lv2_fs_mount_point* lv2_fs_object::get_mp(std::string_view filename, std::string filename.remove_prefix(cell_fs_path.size()); const bool is_path = filename.starts_with("/"sv); - std::string mp_name = is_path ? get_device_root(filename) : std::string(filename); + std::string mp_name = is_path ? std::string{get_path_root_and_trail(filename).first} : std::string(filename); const auto check_mp = [&]() { @@ -1405,6 +1466,10 @@ error_code sys_fs_opendir(ppu_thread& ppu, vm::cptr path, vm::ptr fd) break; } + case fs::error::notdir: + { + return { CELL_ENOTDIR, path }; + } default: { if (has_non_directory_components(local_path)) @@ -3398,7 +3463,7 @@ error_code sys_fs_mount(ppu_thread& ppu, vm::cptr dev_name, vm::cptr return {path_error, path_sv}; } - const std::string vpath = lv2_fs_object::get_normalized_path(path_sv); + const auto [root_name, trail] = lv2_fs_object::get_path_root_and_trail(path_sv); std::string vfs_path; const auto mp = lv2_fs_object::get_mp(device_name, &vfs_path); @@ -3416,8 +3481,8 @@ error_code sys_fs_mount(ppu_thread& ppu, vm::cptr dev_name, vm::cptr if (vfs_path.empty()) return {CELL_ENOTSUP, device_name}; - if (vpath.find_first_not_of('/') == umax || !vfs::get(vpath).empty()) - return {CELL_EEXIST, vpath}; + if (root_name.empty() || !vfs::get(path_sv).empty()) + return {CELL_EEXIST, path_sv}; if (mp == &g_mp_sys_dev_hdd1) { @@ -3452,7 +3517,7 @@ error_code sys_fs_mount(ppu_thread& ppu, vm::cptr dev_name, vm::cptr } } - if (!vfs::mount(vpath, vfs_path, !is_simplefs)) + if (!vfs::mount("/" + std::string{root_name}, vfs_path, !is_simplefs)) { if (is_simplefs) { @@ -3469,7 +3534,7 @@ error_code sys_fs_mount(ppu_thread& ppu, vm::cptr dev_name, vm::cptr return CELL_EIO; } - g_fxo->get().add(vpath, mp, device_name, filesystem, prot); + g_fxo->get().add("/" + std::string{root_name}, mp, device_name, filesystem, prot); return CELL_OK; } diff --git a/rpcs3/Emu/Cell/lv2/sys_fs.h b/rpcs3/Emu/Cell/lv2/sys_fs.h index e64a2b4edb..68ecf1e287 100644 --- a/rpcs3/Emu/Cell/lv2/sys_fs.h +++ b/rpcs3/Emu/Cell/lv2/sys_fs.h @@ -245,11 +245,15 @@ public: lv2_fs_object& operator=(const lv2_fs_object&) = delete; - // Normalize a virtual path - static std::string get_normalized_path(std::string_view path); + // Get the device's root path (e.g. "/dev_hdd0") from a given path + // Cut the trail and return it in seccond argument + static std::pair get_path_root_and_trail(std::string_view path); // Get the device's root path (e.g. "/dev_hdd0") from a given path - static std::string get_device_root(std::string_view filename); + static std::string get_device_root(std::string_view filename) + { + return std::string{get_path_root_and_trail(filename).first}; + } // Filename can be either a path starting with '/' or a CELL_FS device name // This should be used only when handling devices that are not mounted From a7c606c8ac8951760868fd3dce2e694631c7bb4f Mon Sep 17 00:00:00 2001 From: Elad <18193363+elad335@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:35:14 +0300 Subject: [PATCH 26/29] sys_fs: Add unit tests --- rpcs3/CMakeLists.txt | 1 + rpcs3/Emu/Cell/lv2/sys_fs.h | 1 + rpcs3/tests/rpcs3_test.vcxproj | 1 + rpcs3/tests/test_sys_fs.cpp | 51 ++++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 rpcs3/tests/test_sys_fs.cpp diff --git a/rpcs3/CMakeLists.txt b/rpcs3/CMakeLists.txt index cfc2495f15..ba65a16eaf 100644 --- a/rpcs3/CMakeLists.txt +++ b/rpcs3/CMakeLists.txt @@ -187,6 +187,7 @@ if(BUILD_RPCS3_TESTS) tests/test_tuple.cpp tests/test_simple_array.cpp tests/test_address_range.cpp + tests/test_sys_fs.cpp tests/test_rsx_cfg.cpp tests/test_rsx_fp_asm.cpp tests/test_dmux_pamf.cpp diff --git a/rpcs3/Emu/Cell/lv2/sys_fs.h b/rpcs3/Emu/Cell/lv2/sys_fs.h index 68ecf1e287..c78ad7b5a2 100644 --- a/rpcs3/Emu/Cell/lv2/sys_fs.h +++ b/rpcs3/Emu/Cell/lv2/sys_fs.h @@ -4,6 +4,7 @@ #include "Emu/Cell/ErrorCodes.h" #include "Utilities/File.h" #include "Utilities/StrUtil.h" +#include "Utilities/mutex.h" #include diff --git a/rpcs3/tests/rpcs3_test.vcxproj b/rpcs3/tests/rpcs3_test.vcxproj index fb9d0d21d4..a60b150469 100644 --- a/rpcs3/tests/rpcs3_test.vcxproj +++ b/rpcs3/tests/rpcs3_test.vcxproj @@ -100,6 +100,7 @@ + diff --git a/rpcs3/tests/test_sys_fs.cpp b/rpcs3/tests/test_sys_fs.cpp new file mode 100644 index 0000000000..66db10a554 --- /dev/null +++ b/rpcs3/tests/test_sys_fs.cpp @@ -0,0 +1,51 @@ +#include + +#define private public +#include "Emu/Cell/lv2/sys_fs.h" +#undef private + +using namespace utils; + +namespace utils +{ + TEST(cellFs, PathRoot) + { + std::string path = "/."; + auto [root, trail] = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_TRUE(root.empty()); + EXPECT_TRUE(trail.empty()); + + path = "/./././dev_bdvd/./"; + std::tie(root, trail) = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_EQ(root, "dev_bdvd"sv); + EXPECT_TRUE(trail.empty()); + + path = "/../"; + std::tie(root, trail) = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_TRUE(root.empty()); + EXPECT_EQ(trail, "ENOENT"sv); + } + + TEST(cellFs, PathSimplify) + { + std::string path = "/dev_hdd0/"; + auto [root, trail] = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_EQ(root, "dev_hdd0"sv); + EXPECT_TRUE(trail.empty()); + + path = "/dev_hdd0/game"; + std::tie(root, trail) = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_EQ(root, "dev_hdd0"sv); + EXPECT_EQ(trail, "game"sv); + + path = "/dev_hdd0/game/NP1234567"; + std::tie(root, trail) = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_EQ(root, "dev_hdd0"sv); + EXPECT_EQ(trail, "game/NP1234567"sv); + + path = "/dev_hdd0/game/NP1234567/../../NP1234568/."; + std::tie(root, trail) = lv2_fs_object::get_path_root_and_trail(path); + EXPECT_EQ(root, "dev_hdd0"sv); + EXPECT_EQ(trail, "NP1234568"sv); + } +} From 88175aa84f794d4b4a92e392a2a2e9f91bb83489 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Thu, 16 Apr 2026 18:36:03 +0200 Subject: [PATCH 27/29] Check for unknown keys during config validation --- Utilities/Config.cpp | 75 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/Utilities/Config.cpp b/Utilities/Config.cpp index ec2b4ca1c9..f242bd6172 100644 --- a/Utilities/Config.cpp +++ b/Utilities/Config.cpp @@ -479,8 +479,9 @@ bool cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic, bool str { case type::node: { - if (data.IsScalar() || data.IsSequence()) + if (!data.IsMap()) { + cfg_log.error("node node is not a map"); return false; } @@ -491,17 +492,22 @@ bool cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic, bool str if (!pair.first.IsScalar()) continue; // Find the key among existing nodes - for (const auto& node : static_cast(rhs).get_nodes()) - { - if (node->get_name() == pair.first.Scalar()) - { - if (!decode(pair.second, *node, dynamic, strict) && strict) - { - success = false; - } + const auto& nodes = static_cast(rhs).get_nodes(); + const auto it = std::find_if(nodes.cbegin(), nodes.cend(), [&pair](const auto& node) { return ensure(node)->get_name() == pair.first.Scalar(); }); - break; + if (it == nodes.cend()) + { + if (strict) + { + cfg_log.error("Unknown key found: '%s'", pair.first.Scalar()); + success = false; } + continue; + } + + if (!decode(pair.second, *ensure(*it), dynamic, strict) && strict) + { + success = false; } } @@ -513,7 +519,10 @@ bool cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic, bool str if (YAML::convert::decode(data, values)) { - rhs.from_list(std::move(values)); + if (!rhs.from_list(std::move(values)) && strict) + { + return false; + } } break; @@ -523,6 +532,7 @@ bool cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic, bool str { if (!data.IsMap()) { + cfg_log.error("map node is not a map"); return false; } @@ -540,8 +550,9 @@ bool cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic, bool str } case type::log: { - if (data.IsScalar() || data.IsSequence()) + if (!data.IsMap()) { + cfg_log.error("log node is not a map"); return false; } @@ -549,7 +560,18 @@ bool cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic, bool str for (const auto& pair : data) { - if (!pair.first.IsScalar() || !pair.second.IsScalar()) continue; + if (!pair.first.IsScalar() || !pair.second.IsScalar()) + { + if (strict) + { + if (!pair.first.IsScalar()) + cfg_log.error("Key in map is not a scalar"); + else + cfg_log.error("Value in map is not a scalar. key='%s'", pair.first.Scalar()); + return false; + } + continue; + } u64 value; if (!cfg::try_to_enum_value(&value, &fmt_class_string::format, pair.second.Scalar(), pair.first.Scalar()) && strict) @@ -567,6 +589,7 @@ bool cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic, bool str { if (!data.IsMap()) { + cfg_log.error("device node is not a map"); return false; } @@ -574,13 +597,35 @@ bool cfg::decode(const YAML::Node& data, cfg::_base& rhs, bool dynamic, bool str for (const auto& pair : data) { - if (!pair.first.IsScalar() || !pair.second.IsMap()) continue; + if (!pair.first.IsScalar() || !pair.second.IsMap()) + { + if (strict) + { + if (!pair.first.IsScalar()) + cfg_log.error("Key in device map is not a scalar"); + else + cfg_log.error("Value in device map is not a map. key='%s'", pair.first.Scalar()); + return false; + } + continue; + } device_info info{}; for (const auto& key_value : pair.second) { - if (!key_value.first.IsScalar() || !key_value.second.IsScalar()) continue; + if (!key_value.first.IsScalar() || !key_value.second.IsScalar()) + { + if (strict) + { + if (!key_value.first.IsScalar()) + cfg_log.error("Key in device info map is not a scalar"); + else + cfg_log.error("Value in device map is not a scalar. key='%s'", key_value.first.Scalar()); + return false; + } + continue; + } if (key_value.first.Scalar() == "Path") info.path = key_value.second.Scalar(); From 3b9cc0bc3ae104f86b66de0e013df58151e193f5 Mon Sep 17 00:00:00 2001 From: digant73 Date: Thu, 16 Apr 2026 23:01:38 +0200 Subject: [PATCH 28/29] fix wrong folder creation --- rpcs3/Emu/Cell/Modules/cellGame.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/cellGame.cpp b/rpcs3/Emu/Cell/Modules/cellGame.cpp index 51e5ed6a33..d7073a4e47 100644 --- a/rpcs3/Emu/Cell/Modules/cellGame.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGame.cpp @@ -520,10 +520,11 @@ error_code cellHddGameCheck(ppu_thread& ppu, u32 version, vm::cptr dirName return CELL_GAMEDATA_ERROR_PARAM; } - if (!fs::create_path(vfs::get(usrdir))) - { - return {CELL_GAME_ERROR_ACCESS_ERROR, usrdir}; - } + // Nuked until correctly reversed engineered + //if (!fs::create_path(vfs::get(usrdir))) + //{ + // return {CELL_GAME_ERROR_ACCESS_ERROR, usrdir}; + //} } // Nuked until correctly reversed engineered From 2d6ca912fe7745a2f5a26cb9a0f1f5c13590afda Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 18 Apr 2026 15:18:20 +0200 Subject: [PATCH 29/29] Qt: fix regular boot without database config --- rpcs3/rpcs3qt/main_window.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 0bf6a060e1..da66bd4f38 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -557,7 +557,7 @@ void main_window::Boot(const std::string& path, const std::string& title_id, boo // Get database config if possible or if we are in database_config mode (to ensure we see an error on invalid use) if (config_database* db = m_game_list_frame->GetConfigDatabase(); - db->has_config(title_id) || config_mode == cfg_mode::database_config) + db->has_config(title_id)) { const std::optional config = db->get_config(title_id);