From 8e80ddd99fe7a025177459306b2167633b0059ca Mon Sep 17 00:00:00 2001 From: Megamouse Date: Wed, 8 Apr 2026 10:52:03 +0200 Subject: [PATCH 01/43] ISO: optimize some file reads (#18511) - Batch some file reads in iso_read_directory_entry (speeds up indexing by ~41% on my test iso) - Fix some warnings --- rpcs3/Emu/RSX/NV47/FW/draw_call.hpp | 2 +- rpcs3/Emu/RSX/RSXThread.cpp | 4 +- rpcs3/Loader/ISO.cpp | 87 ++++++++++++++++++----------- 3 files changed, 57 insertions(+), 36 deletions(-) diff --git a/rpcs3/Emu/RSX/NV47/FW/draw_call.hpp b/rpcs3/Emu/RSX/NV47/FW/draw_call.hpp index 0fe726d417..7b2591f544 100644 --- a/rpcs3/Emu/RSX/NV47/FW/draw_call.hpp +++ b/rpcs3/Emu/RSX/NV47/FW/draw_call.hpp @@ -33,7 +33,7 @@ namespace rsx u32 draw_command_barrier_mask = 0; // Draw-time iterator to the draw_command_barriers struct - mutable rsx::simple_array::iterator current_barrier_it; + mutable rsx::simple_array::iterator current_barrier_it {}; // Subranges memory cache mutable rsx::simple_array subranges_store; diff --git a/rpcs3/Emu/RSX/RSXThread.cpp b/rpcs3/Emu/RSX/RSXThread.cpp index a7338cbf56..33fd4a662b 100644 --- a/rpcs3/Emu/RSX/RSXThread.cpp +++ b/rpcs3/Emu/RSX/RSXThread.cpp @@ -2970,7 +2970,7 @@ namespace rsx auto& cfg = g_fxo->get(); - std::unique_lock hle_lock; + std::optional> hle_lock; for (u32 i = 0; i < std::size(unmap_status); i++) { @@ -3011,7 +3011,7 @@ namespace rsx if (hle_lock) { - hle_lock.unlock(); + hle_lock->unlock(); } // Pause RSX thread momentarily to handle unmapping diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index 0ddc3e39d9..544b04d379 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -37,62 +37,82 @@ bool is_file_iso(const fs::file& file) const int ISO_BLOCK_SIZE = 2048; template -inline T read_both_endian_int(fs::file& file) +inline T retrieve_endian_int(const u8* buf) { - T out; + T out {}; - if (std::endian::little == std::endian::native) + if constexpr (std::endian::little == std::endian::native) { - out = file.read(); - file.seek(sizeof(T), fs::seek_cur); + // first half = little-endian copy + std::memcpy(&out, buf, sizeof(T)); } else { - file.seek(sizeof(T), fs::seek_cur); - out = file.read(); + // second half = big-endian copy + std::memcpy(&out, buf + sizeof(T), sizeof(T)); } return out; } // assumed that directory_entry is at file head -std::optional iso_read_directory_entry(fs::file& file, bool names_in_ucs2 = false) +static std::optional iso_read_directory_entry(fs::file& entry, bool names_in_ucs2 = false) { - const auto start_pos = file.pos(); - const u8 entry_length = file.read(); + const auto start_pos = entry.pos(); + const u8 entry_length = entry.read(); if (entry_length == 0) return std::nullopt; - file.seek(1, fs::seek_cur); - const u32 start_sector = read_both_endian_int(file); - const u32 file_size = read_both_endian_int(file); + // Batch this set of file reads. This reduces overall time spent in iso_read_directory_entry by ~41% +#pragma pack(push, 1) + struct iso_entry_header + { + //u8 entry_length; // Handled separately + u8 extended_attribute_length; + u8 start_sector[8]; + u8 file_size[8]; + u8 year; + u8 month; + u8 day; + u8 hour; + u8 minute; + u8 second; + u8 timezone_value; + u8 flags; + u8 file_unit_size; + u8 interleave; + u8 volume_sequence_number[4]; + u8 file_name_length; + //u8 file_name[file_name_length]; // Handled separately + }; +#pragma pack(pop) + static_assert(sizeof(iso_entry_header) == 32); + + const iso_entry_header header = entry.read(); + + const u32 start_sector = retrieve_endian_int(header.start_sector); + const u32 file_size = retrieve_endian_int(header.file_size); std::tm file_date = {}; - file_date.tm_year = file.read(); - file_date.tm_mon = file.read() - 1; - file_date.tm_mday = file.read(); - file_date.tm_hour = file.read(); - file_date.tm_min = file.read(); - file_date.tm_sec = file.read(); - const s16 timezone_value = file.read(); + file_date.tm_year = header.year; + file_date.tm_mon = header.month - 1; + file_date.tm_mday = header.day; + file_date.tm_hour = header.hour; + file_date.tm_min = header.minute; + file_date.tm_sec = header.second; + const s16 timezone_value = header.timezone_value; const s16 timezone_offset = (timezone_value - 50) * 15 * 60; const std::time_t date_time = std::mktime(&file_date) + timezone_offset; - const u8 flags = file.read(); - // 2nd flag bit indicates whether a given fs node is a directory - const bool is_directory = flags & 0b00000010; - const bool has_more_extents = flags & 0b10000000; - - file.seek(6, fs::seek_cur); - - const u8 file_name_length = file.read(); + const bool is_directory = header.flags & 0b00000010; + const bool has_more_extents = header.flags & 0b10000000; std::string file_name; - file.read(file_name, file_name_length); + entry.read(file_name, header.file_name_length); - if (file_name_length == 1 && file_name[0] == 0) + if (header.file_name_length == 1 && file_name[0] == 0) { file_name = "."; } @@ -104,7 +124,7 @@ std::optional iso_read_directory_entry(fs::file& file, bool nam { // characters are stored in big endian format. std::u16string utf16; - utf16.resize(file_name_length / 2); + utf16.resize(header.file_name_length / 2); const u16* raw = reinterpret_cast(file_name.data()); for (size_t i = 0; i < utf16.size(); ++i, raw++) @@ -120,13 +140,13 @@ std::optional iso_read_directory_entry(fs::file& file, bool nam file_name.erase(file_name.end() - 2, file_name.end()); } - if (file_name_length > 1 && file_name.ends_with(".")) + if (header.file_name_length > 1 && file_name.ends_with(".")) { file_name.pop_back(); } // skip the rest of the entry. - file.seek(entry_length + start_pos); + entry.seek(entry_length + start_pos); return iso_fs_metadata { @@ -180,6 +200,7 @@ void iso_form_hierarchy(fs::file& file, iso_fs_node& node, bool use_ucs2_decodin selected_node->metadata.extents.push_back(entry->extents[0]); extent_added = true; + break; } } From 6981e308a07dd61bc1dbc0acee9145ab0b955e19 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Wed, 8 Apr 2026 08:50:12 +0200 Subject: [PATCH 02/43] Update docker to 1.11 Update SDL to 3.4.4 Update ffmpeg to 8.1 Update opencv to 4.13 --- .github/workflows/rpcs3.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rpcs3.yml b/.github/workflows/rpcs3.yml index f474643e09..eb42b7aae1 100644 --- a/.github/workflows/rpcs3.yml +++ b/.github/workflows/rpcs3.yml @@ -30,23 +30,23 @@ jobs: matrix: include: - os: ubuntu-24.04 - docker_img: "rpcs3/rpcs3-ci-jammy:1.10" + docker_img: "rpcs3/rpcs3-ci-jammy:1.11" build_sh: "/rpcs3/.ci/build-linux.sh" compiler: clang UPLOAD_COMMIT_HASH: d812f1254a1157c80fd402f94446310560f54e5f UPLOAD_REPO_FULL_NAME: "rpcs3/rpcs3-binaries-linux" - os: ubuntu-24.04 - docker_img: "rpcs3/rpcs3-ci-jammy:1.10" + docker_img: "rpcs3/rpcs3-ci-jammy:1.11" build_sh: "/rpcs3/.ci/build-linux.sh" compiler: gcc - os: ubuntu-24.04-arm - docker_img: "rpcs3/rpcs3-ci-jammy-aarch64:1.10" + docker_img: "rpcs3/rpcs3-ci-jammy-aarch64:1.11" build_sh: "/rpcs3/.ci/build-linux-aarch64.sh" compiler: clang UPLOAD_COMMIT_HASH: a1d35836e8d45bfc6f63c26f0a3e5d46ef622fe1 UPLOAD_REPO_FULL_NAME: "rpcs3/rpcs3-binaries-linux-arm64" - os: ubuntu-24.04-arm - docker_img: "rpcs3/rpcs3-ci-jammy-aarch64:1.10" + docker_img: "rpcs3/rpcs3-ci-jammy-aarch64:1.11" build_sh: "/rpcs3/.ci/build-linux-aarch64.sh" compiler: gcc name: RPCS3 Linux ${{ matrix.os }} ${{ matrix.compiler }} From 1d85de6236ba0e1e7de9a582d5d052221458f505 Mon Sep 17 00:00:00 2001 From: Katalin Rebhan Date: Wed, 8 Apr 2026 17:56:40 +0200 Subject: [PATCH 03/43] Include for pthread_self Fixes compilation on Gentoo Linux with clang/libc++ 21. --- rpcs3/util/atomic.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rpcs3/util/atomic.cpp b/rpcs3/util/atomic.cpp index 59f0eebe49..4fc0316206 100644 --- a/rpcs3/util/atomic.cpp +++ b/rpcs3/util/atomic.cpp @@ -51,6 +51,10 @@ static bool has_waitv() #include #include +#ifdef __linux__ +#include +#endif + #include "asm.hpp" #include "endian.hpp" #include "tsc.hpp" From e9fb3572f95a1ff758cbe545191e68d4d8676a73 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Thu, 9 Apr 2026 08:54:36 +0200 Subject: [PATCH 04/43] Qt: Show error if any package is corrupt before installation --- rpcs3/rpcs3qt/pkg_install_dialog.cpp | 70 ++++++++++++++++++---------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/rpcs3/rpcs3qt/pkg_install_dialog.cpp b/rpcs3/rpcs3qt/pkg_install_dialog.cpp index 3d4193d4e5..bd17c4eaff 100644 --- a/rpcs3/rpcs3qt/pkg_install_dialog.cpp +++ b/rpcs3/rpcs3qt/pkg_install_dialog.cpp @@ -27,14 +27,22 @@ enum Roles pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibility* compat, QWidget* parent) : QDialog(parent) { + ensure(!paths.empty()); + + setWindowTitle(tr("PKG Installation")); + setObjectName("pkg_install_dialog"); + m_dir_list = new QListWidget(this); m_dir_list->setItemDelegate(new richtext_item_delegate(m_dir_list->itemDelegate())); + QStringList corrupt_paths; + for (const QString& path : paths) { const compat::package_info info = game_compatibility::GetPkgInfo(path, compat); if (!info.is_valid) { + corrupt_paths << path; continue; } @@ -44,12 +52,14 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil QString accumulated_info; QString tooltip; - const auto append_comma = [&accumulated_info]() + const auto append_info = [&accumulated_info](const QString& info) { if (!accumulated_info.isEmpty()) { accumulated_info += ", "; } + + accumulated_info += info; }; if (!info.title_id.isEmpty()) @@ -59,27 +69,23 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil if (info.type != compat::package_type::other) { - append_comma(); - if (info.type == compat::package_type::dlc) { - accumulated_info += tr("DLC", "Package type info (DLC)"); + append_info(tr("DLC", "Package type info (DLC)")); } else { - accumulated_info += tr("Update", "Package type info (Update)"); + append_info(tr("Update", "Package type info (Update)")); } } else if (!info.local_cat.isEmpty()) { - append_comma(); - accumulated_info += info.local_cat; + append_info(info.local_cat); } if (!info.version.isEmpty()) { - append_comma(); - accumulated_info += tr("v.%0", "Version info").arg(info.version); + append_info(tr("v.%0", "Version info").arg(info.version)); } if (info.changelog.isEmpty()) @@ -91,8 +97,7 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil tooltip = tr("Changelog:\n\n%0", "Changelog info").arg(info.changelog); } - append_comma(); - accumulated_info += file_info.fileName(); + append_info(file_info.fileName()); const QString text = tr("%0 (%1) - %2", "Package text").arg(info.title.simplified()) .arg(accumulated_info).arg(gui::utils::format_byte_size(info.data_size)); @@ -107,18 +112,8 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil item->setToolTip(tooltip); } - m_dir_list->sortItems(); - m_dir_list->setCurrentRow(0); - m_dir_list->setMinimumWidth((m_dir_list->sizeHintForColumn(0) * 125) / 100); - - // Create contextual label (updated in connect(m_dir_list, &QListWidget::itemChanged ...)) - QLabel* installation_info = new QLabel(); - installation_info->setTextFormat(Qt::RichText); // Support HTML tags - // Create buttons - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); - buttons->button(QDialogButtonBox::Ok)->setText(tr("Install")); - buttons->button(QDialogButtonBox::Ok)->setDefault(true); + QDialogButtonBox* buttons = new QDialogButtonBox(corrupt_paths.isEmpty() ? (QDialogButtonBox::Cancel | QDialogButtonBox::Ok) : QDialogButtonBox::Cancel); connect(buttons, &QDialogButtonBox::clicked, this, [this, buttons](QAbstractButton* button) { @@ -132,6 +127,35 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil } }); + if (!corrupt_paths.isEmpty()) + { + m_dir_list->hide(); + + QString text = tr("Can not install packages. The following packages seem to be corrupt:") + "\n"; + + for (const QString& path : corrupt_paths) + { + text += "\n" + path; + } + + QVBoxLayout* vbox = new QVBoxLayout; + vbox->addWidget(new QLabel(text)); + vbox->addWidget(buttons); + setLayout(vbox); + return; + } + + buttons->button(QDialogButtonBox::Ok)->setText(tr("Install")); + buttons->button(QDialogButtonBox::Ok)->setDefault(true); + + m_dir_list->sortItems(); + m_dir_list->setCurrentRow(0); + m_dir_list->setMinimumWidth((m_dir_list->sizeHintForColumn(0) * 125) / 100); + + // Create contextual label (updated in connect(m_dir_list, &QListWidget::itemChanged ...)) + QLabel* installation_info = new QLabel(); + installation_info->setTextFormat(Qt::RichText); // Support HTML tags + QHBoxLayout* hbox = nullptr; if (m_dir_list->count() > 1) { @@ -210,8 +234,6 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil vbox->addWidget(buttons); setLayout(vbox); - setWindowTitle(tr("PKG Installation")); - setObjectName("pkg_install_dialog"); update_info(installation_info, buttons); // Just to show and check available and required size } From a1a140db91ea6617aa85efe2c9111c6a79c59186 Mon Sep 17 00:00:00 2001 From: oltolm Date: Sun, 5 Apr 2026 19:06:01 +0200 Subject: [PATCH 05/43] CPUThread: fix ASAN use-after-free --- rpcs3/Emu/CPU/CPUThread.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rpcs3/Emu/CPU/CPUThread.cpp b/rpcs3/Emu/CPU/CPUThread.cpp index 4bd5fc9157..8eddff9e0b 100644 --- a/rpcs3/Emu/CPU/CPUThread.cpp +++ b/rpcs3/Emu/CPU/CPUThread.cpp @@ -729,8 +729,14 @@ void cpu_thread::operator()() { if (_this) { - sys_log.warning("CPU Thread '%s' terminated abnormally!", name); cleanup(); + + auto log_thread = named_thread("CPU Thread Cleanup Logger", [name = name]() + { + sys_log.warning("CPU Thread '%s' terminated abnormally!", name); + }); + + log_thread(); } } } cleanup; From 8121bd443ca355ab89b894099c19dc02ac535f9d Mon Sep 17 00:00:00 2001 From: Ani Date: Thu, 9 Apr 2026 22:40:19 +0200 Subject: [PATCH 06/43] ppu: Enable vector NaN fixup by default --- rpcs3/Emu/system_config.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcs3/Emu/system_config.h b/rpcs3/Emu/system_config.h index 9dbaa0c723..7a84455dae 100644 --- a/rpcs3/Emu/system_config.h +++ b/rpcs3/Emu/system_config.h @@ -69,7 +69,7 @@ struct cfg_root : cfg::node cfg::_bool use_accurate_dfma{ this, "Use Accurate DFMA", true }; // Enable accurate double-precision FMA for CPUs which do not support it natively cfg::_bool ppu_set_sat_bit{ this, "PPU Set Saturation Bit", false }; // Accuracy. If unset, completely disable saturation flag handling. cfg::_bool ppu_use_nj_bit{ this, "PPU Accurate Non-Java Mode", false }; // Accuracy. If set, accurately emulate NJ flag. Implies NJ fixup. - cfg::_bool ppu_fix_vnan{ this, "PPU Fixup Vector NaN Values", false }; // Accuracy. Partial. + cfg::_bool ppu_fix_vnan{ this, "PPU Fixup Vector NaN Values", true }; // Accuracy. Partial. cfg::_bool ppu_set_vnan{ this, "PPU Accurate Vector NaN Values", false }; // Accuracy. Implies ppu_fix_vnan. cfg::_bool ppu_set_fpcc{ this, "PPU Set FPCC Bits", false }; // Accuracy. From 1b1143094eece0fb0c6bc98e821b0f6fcdcda98b Mon Sep 17 00:00:00 2001 From: kd-11 Date: Fri, 10 Apr 2026 02:47:31 +0300 Subject: [PATCH 07/43] rsx: Improve handling of aligned memory --- rpcs3/Emu/RSX/Common/aligned_malloc.hpp | 88 +++++++++++++++++++++++++ rpcs3/Emu/RSX/Common/io_buffer.h | 9 ++- rpcs3/Emu/RSX/Common/simple_array.hpp | 60 +---------------- rpcs3/emucore.vcxproj | 1 + rpcs3/emucore.vcxproj.filters | 3 + 5 files changed, 101 insertions(+), 60 deletions(-) create mode 100644 rpcs3/Emu/RSX/Common/aligned_malloc.hpp diff --git a/rpcs3/Emu/RSX/Common/aligned_malloc.hpp b/rpcs3/Emu/RSX/Common/aligned_malloc.hpp new file mode 100644 index 0000000000..2ca59c3cf2 --- /dev/null +++ b/rpcs3/Emu/RSX/Common/aligned_malloc.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include + +namespace rsx +{ + namespace aligned_allocator + { + template + requires (Align != 0) && ((Align& (Align - 1)) == 0) + size_t align_up(size_t size) + { + return (size + (Align - 1)) & ~(Align - 1); + } + + template + requires (Align != 0) && ((Align& (Align - 1)) == 0) + void* malloc(size_t size) + { +#if defined(_WIN32) + return _aligned_malloc(size, Align); +#elif defined(__APPLE__) + constexpr size_t NativeAlign = std::max(Align, sizeof(void*)); + return std::aligned_alloc(NativeAlign, align_up(size)); +#else + return std::aligned_alloc(Align, align_up(size)); +#endif + } + + template + requires (Align != 0) && ((Align& (Align - 1)) == 0) + void* realloc(void* prev_ptr, [[maybe_unused]] size_t prev_size, size_t new_size) + { + if (align_up(prev_size) >= new_size) + { + return prev_ptr; + } + + ensure(reinterpret_cast(prev_ptr) % Align == 0, "Pointer not aligned to Align"); +#if defined(_WIN32) + return _aligned_realloc(prev_ptr, new_size, Align); +#else +#if defined(__APPLE__) + constexpr size_t NativeAlign = std::max(Align, sizeof(void*)); + void* ret = std::aligned_alloc(NativeAlign, align_up(new_size)); +#else + void* ret = std::aligned_alloc(Align, align_up(new_size)); +#endif + std::memcpy(ret, prev_ptr, std::min(prev_size, new_size)); + std::free(prev_ptr); + return ret; +#endif + } + + static inline void free(void* ptr) + { +#ifdef _WIN32 + _aligned_free(ptr); +#else + std::free(ptr); +#endif + } + } + + template + class aligned_pointer_t + { + public: + aligned_pointer_t(size_t size) + { + m_ptr = aligned_allocator::malloc(size); + } + + virtual ~aligned_pointer_t() + { + aligned_allocator::free(m_ptr); + } + + T* data() const { return m_ptr; } + + T& operator * () const { return *m_ptr; } + + T* operator -> () const { return m_ptr; } + + private: + T* m_ptr; + }; +} diff --git a/rpcs3/Emu/RSX/Common/io_buffer.h b/rpcs3/Emu/RSX/Common/io_buffer.h index 59e8e6a32e..edca80675b 100644 --- a/rpcs3/Emu/RSX/Common/io_buffer.h +++ b/rpcs3/Emu/RSX/Common/io_buffer.h @@ -81,10 +81,17 @@ namespace rsx std::span as_span() const { auto bytes = data(); - ensure((reinterpret_cast(bytes) & (sizeof(T) - 1)) == 0, "IO buffer span cast requires naturally aligned pointers."); + ensure(is_naturally_aligned(), "IO buffer span cast requires naturally aligned pointers."); return { utils::bless(bytes), m_size / sizeof(T) }; } + template + bool is_naturally_aligned() const + { + return ((reinterpret_cast(data()) & (alignof(T) - 1)) == 0) && + (m_size % sizeof(T)) == 0; + } + bool empty() const { return m_size == 0; diff --git a/rpcs3/Emu/RSX/Common/simple_array.hpp b/rpcs3/Emu/RSX/Common/simple_array.hpp index 6852e670fb..a37df9dd54 100644 --- a/rpcs3/Emu/RSX/Common/simple_array.hpp +++ b/rpcs3/Emu/RSX/Common/simple_array.hpp @@ -3,70 +3,12 @@ #include #include #include -#include +#include "aligned_malloc.hpp" #include "reverse_ptr.hpp" namespace rsx { - namespace aligned_allocator - { - template - requires (Align != 0) && ((Align & (Align - 1)) == 0) - size_t align_up(size_t size) - { - return (size + (Align - 1)) & ~(Align - 1); - } - - template - requires (Align != 0) && ((Align & (Align - 1)) == 0) - void* malloc(size_t size) - { -#if defined(_WIN32) - return _aligned_malloc(size, Align); -#elif defined(__APPLE__) - constexpr size_t NativeAlign = std::max(Align, sizeof(void*)); - return std::aligned_alloc(NativeAlign, align_up(size)); -#else - return std::aligned_alloc(Align, align_up(size)); -#endif - } - - template - requires (Align != 0) && ((Align & (Align - 1)) == 0) - void* realloc(void* prev_ptr, [[maybe_unused]] size_t prev_size, size_t new_size) - { - if (align_up(prev_size) >= new_size) - { - return prev_ptr; - } - - ensure(reinterpret_cast(prev_ptr) % Align == 0, "Pointer not aligned to Align"); -#if defined(_WIN32) - return _aligned_realloc(prev_ptr, new_size, Align); -#else -#if defined(__APPLE__) - constexpr size_t NativeAlign = std::max(Align, sizeof(void*)); - void* ret = std::aligned_alloc(NativeAlign, align_up(new_size)); -#else - void* ret = std::aligned_alloc(Align, align_up(new_size)); -#endif - std::memcpy(ret, prev_ptr, std::min(prev_size, new_size)); - std::free(prev_ptr); - return ret; -#endif - } - - static inline void free(void* ptr) - { -#ifdef _WIN32 - _aligned_free(ptr); -#else - std::free(ptr); -#endif - } - } - template concept span_like = requires(C& c) { diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index 7253171856..42aab47a04 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -661,6 +661,7 @@ + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index a5a5825f44..6b9c82c959 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -2872,6 +2872,9 @@ Emu\GPU\RSX\Overlays + + Emu\GPU\RSX\Common + From 09554c43baaad391b8679fe6d55a23babbd2dec8 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Fri, 10 Apr 2026 02:50:43 +0300 Subject: [PATCH 08/43] rsx: Allow DXT texture decoding to gracefully fall back to unaligned memory addresses --- rpcs3/Emu/RSX/Common/TextureUtils.cpp | 64 ++++++++++++++++++++------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/rpcs3/Emu/RSX/Common/TextureUtils.cpp b/rpcs3/Emu/RSX/Common/TextureUtils.cpp index 8cf3466bb9..43068bf723 100644 --- a/rpcs3/Emu/RSX/Common/TextureUtils.cpp +++ b/rpcs3/Emu/RSX/Common/TextureUtils.cpp @@ -7,6 +7,12 @@ #include "util/asm.hpp" +// Unaligned u128 alias +union x128 +{ + u8 _u8[16]; +}; + namespace utils { template @@ -520,14 +526,14 @@ struct copy_decoded_bc1_block struct copy_decoded_bc2_block { - static void copy_mipmap_level(std::span dst, std::span src, u16 width_in_block, u32 row_count, u16 depth, u32 dst_pitch_in_block, u32 src_pitch_in_block) + static void copy_mipmap_level(std::span dst, std::span src, u16 width_in_block, u32 row_count, u16 depth, u32 dst_pitch_in_block, u32 src_pitch_in_block) { u32 src_offset = 0, dst_offset = 0, destinationPitch = dst_pitch_in_block * 4; for (u32 row = 0; row < row_count * depth; row++) { for (u32 col = 0; col < width_in_block; col++) { - const u8* compressedBlock = reinterpret_cast(&src[src_offset + col]); + const u8* compressedBlock = src[src_offset + col]._u8; u8* decompressedBlock = reinterpret_cast(&dst[dst_offset + col * 4]); bcdec_bc2(compressedBlock, decompressedBlock, destinationPitch); } @@ -540,14 +546,14 @@ struct copy_decoded_bc2_block struct copy_decoded_bc3_block { - static void copy_mipmap_level(std::span dst, std::span src, u16 width_in_block, u32 row_count, u16 depth, u32 dst_pitch_in_block, u32 src_pitch_in_block) + static void copy_mipmap_level(std::span dst, std::span src, u16 width_in_block, u32 row_count, u16 depth, u32 dst_pitch_in_block, u32 src_pitch_in_block) { u32 src_offset = 0, dst_offset = 0, destinationPitch = dst_pitch_in_block * 4; for (u32 row = 0; row < row_count * depth; row++) { for (u32 col = 0; col < width_in_block; col++) { - const u8* compressedBlock = reinterpret_cast(&src[src_offset + col]); + const u8* compressedBlock = src[src_offset + col]._u8; u8* decompressedBlock = reinterpret_cast(&dst[dst_offset + col * 4]); bcdec_bc3(compressedBlock, decompressedBlock, destinationPitch); } @@ -1039,22 +1045,25 @@ namespace rsx // This is only supported using Nvidia OpenGL. // Remove the VTC tiling to support ATI and Vulkan. copy_unmodified_block_vtc::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), w, h, depth, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + break; } - else if (is_3d && !is_po2 && caps.supports_vtc_decoding) + + if (is_3d && !is_po2 && caps.supports_vtc_decoding) { // In this case, hardware expects us to feed it a VTC input, but on PS3 we only have a linear one. // We need to compress the 2D-planar DXT input into a VTC output copy_linear_block_to_vtc::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), w, h, depth, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + break; } - else if (caps.supports_zero_copy) + + if (caps.supports_zero_copy) { result.require_upload = true; result.deferred_cmds = build_transfer_cmds(src_layout.data.data(), 8, w, h, depth, 0, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + break; } - else - { - copy_unmodified_block::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), 1, w, h, depth, 0, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); - } + + copy_unmodified_block::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), 1, w, h, depth, 0, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); break; } @@ -1062,7 +1071,7 @@ namespace rsx { if (!caps.supports_dxt) { - copy_decoded_bc2_block::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), w, h, depth, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + copy_decoded_bc2_block::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), w, h, depth, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); break; } [[fallthrough]]; @@ -1071,7 +1080,7 @@ namespace rsx { if (!caps.supports_dxt) { - copy_decoded_bc3_block::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), w, h, depth, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + copy_decoded_bc3_block::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), w, h, depth, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); break; } @@ -1083,23 +1092,44 @@ namespace rsx // PS3 uses the Nvidia VTC memory layout for compressed 3D textures. // This is only supported using Nvidia OpenGL. // Remove the VTC tiling to support ATI and Vulkan. - copy_unmodified_block_vtc::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), w, h, depth, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + if (src_layout.data.is_naturally_aligned()) + { + copy_unmodified_block_vtc::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), w, h, depth, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + break; + } + + copy_unmodified_block_vtc::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), w, h, depth, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + break; } - else if (is_3d && !is_po2 && caps.supports_vtc_decoding) + + if (is_3d && !is_po2 && caps.supports_vtc_decoding) { // In this case, hardware expects us to feed it a VTC input, but on PS3 we only have a linear one. // We need to compress the 2D-planar DXT input into a VTC output - copy_linear_block_to_vtc::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), w, h, depth, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + if (src_layout.data.is_naturally_aligned()) + { + copy_linear_block_to_vtc::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), w, h, depth, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + break; + } + + copy_linear_block_to_vtc::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), w, h, depth, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + break; } - else if (caps.supports_zero_copy) + + if (caps.supports_zero_copy) { result.require_upload = true; result.deferred_cmds = build_transfer_cmds(src_layout.data.data(), 16, w, h, depth, 0, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + break; } - else + + if (src_layout.data.is_naturally_aligned()) { copy_unmodified_block::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), 1, w, h, depth, 0, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); + break; } + + copy_unmodified_block::copy_mipmap_level(dst_buffer.as_span(), src_layout.data.as_span(), 1, w, h, depth, 0, get_row_pitch_in_block(w, caps.alignment), src_layout.pitch_in_block); break; } From fb194241d529b5c888780966fbe573e4d2c22aa1 Mon Sep 17 00:00:00 2001 From: oltolm Date: Sun, 5 Apr 2026 16:55:30 +0200 Subject: [PATCH 09/43] fix LLVM assert in use_begin --- rpcs3/Emu/CPU/CPUTranslator.cpp | 71 ++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/rpcs3/Emu/CPU/CPUTranslator.cpp b/rpcs3/Emu/CPU/CPUTranslator.cpp index 6bd7924ea5..66b5c69af0 100644 --- a/rpcs3/Emu/CPU/CPUTranslator.cpp +++ b/rpcs3/Emu/CPU/CPUTranslator.cpp @@ -244,36 +244,48 @@ llvm::Value* cpu_translator::bitcast(llvm::Value* val, llvm::Type* type, std::so } } - for (auto it = source_val->use_begin(); it != source_val->use_end(); ++it) + // Skip use iteration for values that don't have use lists +#if LLVM_VERSION_MAJOR >= 21 + if (source_val->hasUseList()) +#endif { - llvm::Value* it_val = *it; - - if (!it_val) + for (llvm::Value* it_val : source_val->uses()) { - continue; - } - - llvm::CastInst* bci = llvm::dyn_cast_or_null(it_val); - - // Walk through bitcasts - while (bci && bci->getOpcode() == llvm::Instruction::BitCast) - { - if (bci->getParent() != m_ir->GetInsertBlock()) + if (!it_val) { - break; + continue; } - if (bci->getType() == type) - { - return bci; - } + llvm::CastInst* bci = llvm::dyn_cast_or_null(it_val); - if (bci->use_begin() == bci->use_end()) + // Walk through bitcasts + while (bci && bci->getOpcode() == llvm::Instruction::BitCast) { - break; - } + if (bci->getParent() != m_ir->GetInsertBlock()) + { + break; + } - bci = llvm::dyn_cast_or_null(*bci->use_begin()); + if (bci->getType() == type) + { + return bci; + } + + // Check if bci has use list before accessing use_begin() +#if LLVM_VERSION_MAJOR >= 21 + if (!bci->hasUseList()) + { + break; + } +#endif + + if (bci->use_begin() == bci->use_end()) + { + break; + } + + bci = llvm::dyn_cast_or_null(*bci->use_begin()); + } } } @@ -553,14 +565,25 @@ void cpu_translator::erase_stores(llvm::ArrayRef args) { for (auto v : args) { - for (auto it = v->use_begin(); it != v->use_end(); ++it) + // Skip use iteration for values that don't have use lists +#if LLVM_VERSION_MAJOR >= 21 + if (!v->hasUseList()) + continue; +#endif + + for (llvm::Value* i : v->uses()) { - llvm::Value* i = *it; llvm::CastInst* bci = nullptr; // Walk through bitcasts while (i && (bci = llvm::dyn_cast(i)) && bci->getOpcode() == llvm::Instruction::BitCast) { + // Check if bci has use list before accessing use_begin() +#if LLVM_VERSION_MAJOR >= 21 + if (!bci->hasUseList()) + break; +#endif + i = *bci->use_begin(); } From 110c786d802c4c17850de1d6945dc4df08ac393a Mon Sep 17 00:00:00 2001 From: digant73 Date: Fri, 3 Apr 2026 00:05:51 +0200 Subject: [PATCH 10/43] Add support to encrypted ISO fix compile errors and suppress minor warnings strip minor bug fixes and cleanup minor cleanup minor cleanup Fixed crash at boot parsing an ISO with a empty directory entry at file head applied suggested changes added missing break in switch Update rpcs3/Loader/ISO.cpp Co-authored-by: Megamouse use hex_to_bytes() instead of self made functions minor cleanup rewrite nested if() add explicit support to .key minor cleanup optimize partial sector read minor optimization fix conflict --- rpcs3/Loader/ISO.cpp | 611 +++++++++++++++++++++++++++++++++++++++---- rpcs3/Loader/ISO.h | 78 +++++- 2 files changed, 624 insertions(+), 65 deletions(-) diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index 544b04d379..f3eba4268b 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -2,6 +2,7 @@ #include "ISO.h" #include "Emu/VFS.h" +#include "Crypto/utils.h" #include #include @@ -11,6 +12,19 @@ LOG_CHANNEL(sys_log, "SYS"); +constexpr u64 ISO_SECTOR_SIZE = 2048; + +struct iso_sector +{ + u64 lba_address; + u64 offset; + u64 size; + u64 address_aligned; + u64 offset_aligned; + u64 size_aligned; + std::array buf; +}; + bool is_file_iso(const std::string& path) { if (path.empty()) return false; @@ -34,7 +48,351 @@ bool is_file_iso(const fs::file& file) && magic[4] == '1'; } -const int ISO_BLOCK_SIZE = 2048; +// Convert 4 bytes in big-endian format to an unsigned integer +static u32 char_arr_BE_to_uint(const u8* arr) +{ + return arr[0] << 24 | arr[1] << 16 | arr[2] << 8 | arr[3]; +} + +// Reset the iv to a particular LBA +static void reset_iv(std::array& iv, u32 lba) +{ + memset(iv.data(), 0, 12); + + iv[12] = (lba & 0xFF000000) >> 24; + iv[13] = (lba & 0x00FF0000) >> 16; + iv[14] = (lba & 0x0000FF00) >> 8; + iv[15] = (lba & 0x000000FF) >> 0; +} + +// Main function that will decrypt the sector(s) +static bool decrypt_data(aes_context& aes, u64 offset, unsigned char* buffer, u64 size) +{ + // The following preliminary checks are good to be provided. + // Commented out to gain a bit of performance, just because we know the caller is providing values in the expected range + + //if (size == 0) + //{ + // return false; + //} + + //if ((size % 16) != 0) + //{ + // sys_log.error("decrypt_data(): Requested ciphertext blocks' size must be a multiple of 16 (%ull)", size); + // return; + //} + + u32 cur_sector_lba = static_cast(offset / ISO_SECTOR_SIZE); // First sector's LBA + const u32 sector_count = static_cast((offset + size - 1) / ISO_SECTOR_SIZE) - cur_sector_lba + 1; + const u64 sector_offset = offset % ISO_SECTOR_SIZE; + + std::array iv; + u64 cur_offset; + u64 cur_size; + + // If the offset is not at the beginning of a sector, the first 16 bytes in the buffer + // represents the IV for decrypting the next data in the buffer. + // Otherwise, the IV is based on sector's LBA + if (sector_offset != 0) + { + memcpy(iv.data(), buffer, 16); + cur_offset = 16; + } + else + { + reset_iv(iv, cur_sector_lba); + cur_offset = 0; + } + + cur_size = sector_offset + size <= ISO_SECTOR_SIZE ? size : ISO_SECTOR_SIZE - sector_offset; + cur_size -= cur_offset; + + // 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"); + return false; + } + + if (sector_count < 2) // If no more sector(s) + { + return true; + } + + cur_offset += cur_size; + + const u32 inner_sector_count = sector_count > 2 ? sector_count - 2 : 0; // Remove first and last sector + + // Inner sector(s), if any + for (u32 i = 0; i < inner_sector_count; i++) + { + reset_iv(iv, ++cur_sector_lba); // Next sector's IV + + 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"); + return false; + } + + cur_offset += ISO_SECTOR_SIZE; + } + + reset_iv(iv, ++cur_sector_lba); // Next sector's IV + + // 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"); + return false; + } + + return true; +} + +void iso_file_decryption::reset() +{ + m_enc_type = iso_encryption_type::NONE; + m_region_info.clear(); +} + +bool iso_file_decryption::init(const std::string& path) +{ + reset(); + + if (!is_file_iso(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) + // + + fs::file iso_file(path); + + if (!iso_file) + { + sys_log.error("init(): Failed to open file: %s", path); + return false; + } + + 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); + 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); + return false; + } + + // NOTE: + // + // 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()); + + // 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); + return false; + } + + m_region_info.resize(region_count * 2 - 1); + + for (size_t i = 0; i < m_region_info.size(); ++i) + { + // Store the region information in address format + m_region_info[i].encrypted = (i % 2 == 1); + m_region_info[i].region_first_addr = (i == 0 ? 0ULL : m_region_info[i - 1].region_last_addr + 1ULL); + m_region_info[i].region_last_addr = (static_cast(char_arr_BE_to_uint(sec0_sec1.data() + 12 + (i * 4))) + - (i % 2 == 1 ? 1ULL : 0ULL)) * ISO_SECTOR_SIZE + ISO_SECTOR_SIZE - 1ULL; + } + + // + // Check for Redump type + // + + const usz ext_pos = path.rfind('.'); + std::string key_path; + + // If no file extension is provided, set "key_path" appending ".dkey" to "path". + // Otherwise, replace the extension (e.g. ".iso") with ".dkey" + key_path = ext_pos == umax ? path + ".dkey" : path.substr(0, ext_pos) + ".dkey"; + + fs::file key_file(key_path); + + // If no ".dkey" file exists, try with ".key" + if (!key_file) + { + key_path = ext_pos == umax ? path + ".key" : path.substr(0, ext_pos) + ".key"; + key_file = fs::file(key_path); + } + + // Check if "key_path" exists and create the "m_aes_dec" context if so + if (key_file) + { + char key_str[32]; + unsigned char key[16]; + + const u64 key_len = key_file.read(key_str, sizeof(key_str)); + + if (key_len == sizeof(key_str) || key_len == sizeof(key)) + { + // If the key read from the key file is 16 bytes long instead of 32, consider the file as + // binary (".key") and so not needing any further conversion from hex string to bytes + if (key_len == sizeof(key)) + { + memcpy(key, key_str, sizeof(key)); + } + else + { + hex_to_bytes(key, std::string_view(key_str, key_len), static_cast(key_len)); + } + + if (aes_setkey_dec(&m_aes_dec, key, 128) == 0) + { + m_enc_type = iso_encryption_type::REDUMP; // SET ENCRYPTION TYPE: REDUMP + } + } + + 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); + } + } + else + { + sys_log.warning("init(): Failed to open, or missing, key file: %s", key_path); + } + + // + // Check for 3k3y type + // + + // If encryption type is still set to NONE + if (m_enc_type == iso_encryption_type::NONE) + { + // The 3k3y watermarks located at offset 0xF70: (D|E)ncrypted 3K BLD + static const unsigned char k3k3y_enc_watermark[16] = + {0x45, 0x6E, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x20, 0x33, 0x4B, 0x20, 0x42, 0x4C, 0x44}; + static const unsigned char k3k3y_dec_watermark[16] = + {0x44, 0x6E, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x20, 0x33, 0x4B, 0x20, 0x42, 0x4C, 0x44}; + + if (memcmp(&k3k3y_enc_watermark[0], &sec0_sec1[0xF70], sizeof(k3k3y_enc_watermark)) == 0) + { + // Grab D1 from the 3k3y sector + unsigned char key[16]; + + memcpy(key, &sec0_sec1[0xF80], 0x10); + + // Convert D1 to KEY and generate the "m_aes_dec" context + unsigned char key_d1[] = {0x38, 11, 0xcf, 11, 0x53, 0x45, 0x5b, 60, 120, 0x17, 0xab, 0x4f, 0xa3, 0xba, 0x90, 0xed}; + unsigned char iv_d1[] = {0x69, 0x47, 0x47, 0x72, 0xaf, 0x6f, 0xda, 0xb3, 0x42, 0x74, 0x3a, 0xef, 170, 0x18, 0x62, 0x87}; + + aes_context aes_d1; + + if (aes_setkey_enc(&aes_d1, key_d1, 128) == 0) + { + if (aes_crypt_cbc(&aes_d1, AES_ENCRYPT, 16, &iv_d1[0], key, key) == 0) + { + if (aes_setkey_dec(&m_aes_dec, key, 128) == 0) + { + m_enc_type = iso_encryption_type::ENC_3K3Y; // SET ENCRYPTION TYPE: ENC_3K3Y + } + } + } + + 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); + } + } + else if (memcmp(&k3k3y_dec_watermark[0], &sec0_sec1[0xF70], sizeof(k3k3y_dec_watermark)) == 0) + { + m_enc_type = iso_encryption_type::DEC_3K3Y; // SET ENCRYPTION TYPE: DEC_3K3Y + } + } + + 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); + 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); + 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); + 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); + break; + } + + return true; +} + +bool iso_file_decryption::decrypt(u64 offset, void* buffer, u64 size, const std::string& name) +{ + // If it's a non-encrypted type, nothing more to do + if (m_enc_type == iso_encryption_type::NONE) + { + return true; + } + + // If it's a 3k3y ISO and data at offset 0xF70 is being requested, we should null it out + if (m_enc_type == iso_encryption_type::DEC_3K3Y || m_enc_type == iso_encryption_type::ENC_3K3Y) + { + if (offset + size >= 0xF70ULL && offset <= 0x1070ULL) + { + // Zero out the 0xF70 - 0x1070 overlap + unsigned char* buf = reinterpret_cast(buffer); + unsigned char* buf_overlap_start = offset < 0xF70ULL ? buf + 0xF70ULL - offset : buf; + + memset(buf_overlap_start, 0x00, offset + size < 0x1070ULL ? size - (buf_overlap_start - buf) : 0x100ULL - (buf_overlap_start - buf)); + } + + // If it's a decrypted ISO then return, otherwise go on to the decryption logic + if (m_enc_type == iso_encryption_type::DEC_3K3Y) + { + return true; + } + } + + // If it's an encrypted type, check if the request lies in an encrypted range + for (const iso_region_info& info : m_region_info) + { + if (offset >= info.region_first_addr && offset <= info.region_last_addr) + { + // We found the region, decrypt if needed + if (!info.encrypted) + { + return true; + } + + // Decrypt the region before sending it back + decrypt_data(m_aes_dec, offset, reinterpret_cast(buffer), size); + + return true; + } + } + + 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)", + name, + offset, + static_cast(m_region_info.size()), + static_cast(!m_region_info.empty() ? m_region_info.back().region_first_addr : 0), + static_cast(!m_region_info.empty() ? m_region_info.back().region_last_addr : 0)); + + return true; +} template inline T retrieve_endian_int(const u8* buf) @@ -55,13 +413,16 @@ inline T retrieve_endian_int(const u8* buf) return out; } -// assumed that directory_entry is at file head +// Assumed that directory entry is at file head static std::optional iso_read_directory_entry(fs::file& entry, bool names_in_ucs2 = false) { const auto start_pos = entry.pos(); const u8 entry_length = entry.read(); - if (entry_length == 0) return std::nullopt; + if (entry_length == 0) + { + return std::nullopt; + } // Batch this set of file reads. This reduces overall time spent in iso_read_directory_entry by ~41% #pragma pack(push, 1) @@ -94,12 +455,14 @@ static std::optional iso_read_directory_entry(fs::file& entry, const u32 file_size = retrieve_endian_int(header.file_size); std::tm file_date = {}; + file_date.tm_year = header.year; file_date.tm_mon = header.month - 1; file_date.tm_mday = header.day; file_date.tm_hour = header.hour; file_date.tm_min = header.minute; file_date.tm_sec = header.second; + const s16 timezone_value = header.timezone_value; const s16 timezone_offset = (timezone_value - 50) * 15 * 60; @@ -110,6 +473,7 @@ static std::optional iso_read_directory_entry(fs::file& entry, const bool has_more_extents = header.flags & 0b10000000; std::string file_name; + entry.read(file_name, header.file_name_length); if (header.file_name_length == 1 && file_name[0] == 0) @@ -120,13 +484,14 @@ static std::optional iso_read_directory_entry(fs::file& entry, { file_name = ".."; } - else if (names_in_ucs2) // for strings in joliet descriptor + else if (names_in_ucs2) // For strings in joliet descriptor { - // characters are stored in big endian format. + // Characters are stored in big endian format + const u16* raw = reinterpret_cast(file_name.data()); std::u16string utf16; + utf16.resize(header.file_name_length / 2); - const u16* raw = reinterpret_cast(file_name.data()); for (size_t i = 0; i < utf16.size(); ++i, raw++) { utf16[i] = *reinterpret_cast*>(raw); @@ -145,7 +510,7 @@ static std::optional iso_read_directory_entry(fs::file& entry, file_name.pop_back(); } - // skip the rest of the entry. + // Skip the rest of the entry entry.seek(entry_length + start_pos); return iso_fs_metadata @@ -165,38 +530,43 @@ static std::optional iso_read_directory_entry(fs::file& entry, }; } -void iso_form_hierarchy(fs::file& file, iso_fs_node& node, bool use_ucs2_decoding = false, const std::string& parent_path = "") +static void iso_form_hierarchy(fs::file& file, iso_fs_node& node, bool use_ucs2_decoding = false, const std::string& parent_path = "") { - if (!node.metadata.is_directory) return; + if (!node.metadata.is_directory) + { + return; + } std::vector multi_extent_node_indices; - // assuming the directory spans a single extent + // Assuming the directory spans a single extent const auto& directory_extent = node.metadata.extents[0]; + const u64 end_pos = (directory_extent.start * ISO_SECTOR_SIZE) + directory_extent.size; - file.seek(directory_extent.start * ISO_BLOCK_SIZE); + file.seek(directory_extent.start * ISO_SECTOR_SIZE); - const u64 end_pos = directory_extent.size + (directory_extent.start * ISO_BLOCK_SIZE); - - while(file.pos() < end_pos) + while (file.pos() < end_pos) { auto entry = iso_read_directory_entry(file, use_ucs2_decoding); + if (!entry) { - const u64 new_sector = (file.pos() / ISO_BLOCK_SIZE) + 1; - file.seek(new_sector * ISO_BLOCK_SIZE); + const u64 new_sector = (file.pos() / ISO_SECTOR_SIZE) + 1; + + file.seek(new_sector * ISO_SECTOR_SIZE); continue; } bool extent_added = false; - // find previous extent and merge into it, otherwise we push this node's index + // Find previous extent and merge into it, otherwise we push this node's index for (usz index : multi_extent_node_indices) { auto& selected_node = ::at32(node.children, index); + if (selected_node->metadata.name == entry->name) { - // merge into selected_node + // Merge into selected_node selected_node->metadata.extents.push_back(entry->extents[0]); extent_added = true; @@ -204,11 +574,14 @@ void iso_form_hierarchy(fs::file& file, iso_fs_node& node, bool use_ucs2_decodin } } - if (extent_added) continue; + if (extent_added) + { + continue; + } if (entry->has_multiple_extents) { - // haven't pushed entry to node.children yet so node.children::size() == entry_index + // Haven't pushed entry to node.children yet so node.children::size() == entry_index multi_extent_node_indices.push_back(node.children.size()); } @@ -241,10 +614,11 @@ iso_archive::iso_archive(const std::string& path) { m_path = path; m_file = fs::file(path); + m_dec = std::make_shared(); - if (!is_file_iso(m_file)) + if (!m_dec->init(path)) { - // not iso... TODO: throw something?? + // Not ISO... TODO: throw something?? return; } @@ -262,16 +636,21 @@ iso_archive::iso_archive(const std::string& path) { use_ucs2_decoding = descriptor_type == 2; - // skip the rest of descriptor's data + // Skip the rest of descriptor's data m_file.seek(155, fs::seek_cur); - m_root = iso_fs_node + const auto node = iso_read_directory_entry(m_file, use_ucs2_decoding); + + if (node) { - .metadata = iso_read_directory_entry(m_file, use_ucs2_decoding).value(), - }; + m_root = iso_fs_node + { + .metadata = node.value() + }; + } } - m_file.seek(descriptor_start + ISO_BLOCK_SIZE); + m_file.seek(descriptor_start + ISO_SECTOR_SIZE); } while (descriptor_type != 255); @@ -355,23 +734,27 @@ bool iso_archive::is_file(const std::string& path) iso_file iso_archive::open(const std::string& path) { - return iso_file(fs::file(m_path), *ensure(retrieve(path))); + return iso_file(fs::file(m_path), m_dec, *ensure(retrieve(path))); } psf::registry iso_archive::open_psf(const std::string& path) { auto* archive_file = retrieve(path); - if (!archive_file) return psf::registry(); - const fs::file psf_file(std::make_unique(fs::file(m_path), *archive_file)); + if (!archive_file) + { + return psf::registry(); + } + + const fs::file psf_file(std::make_unique(fs::file(m_path), m_dec, *archive_file)); return psf::load_object(psf_file, path); } -iso_file::iso_file(fs::file&& iso_handle, const iso_fs_node& node) - : m_file(std::move(iso_handle)), m_meta(node.metadata) +iso_file::iso_file(fs::file&& iso_handle, std::shared_ptr iso_dec, const iso_fs_node& node) + : m_file(std::move(iso_handle)), m_dec(iso_dec), m_meta(node.metadata) { - m_file.seek(ISO_BLOCK_SIZE * node.metadata.extents[0].start); + m_file.seek(node.metadata.extents[0].start * ISO_SECTOR_SIZE); } fs::stat_t iso_file::get_stat() @@ -410,14 +793,6 @@ std::pair iso_file::get_extent_pos(u64 pos) const return {pos, *it}; } -// assumed valid and in bounds. -u64 iso_file::file_offset(u64 pos) const -{ - const auto [local_pos, extent] = get_extent_pos(pos); - - return (extent.start * ISO_BLOCK_SIZE) + local_pos; -} - u64 iso_file::local_extent_remaining(u64 pos) const { const auto [local_pos, extent] = get_extent_pos(pos); @@ -430,29 +805,157 @@ u64 iso_file::local_extent_size(u64 pos) const return get_extent_pos(pos).second.size; } +// Assumed valid and in bounds +u64 iso_file::file_offset(u64 pos) const +{ + const auto [local_pos, extent] = get_extent_pos(pos); + + return (extent.start * ISO_SECTOR_SIZE) + local_pos; +} + u64 iso_file::read(void* buffer, u64 size) { const auto r = read_at(m_pos, buffer, size); + m_pos += r; return r; } u64 iso_file::read_at(u64 offset, void* buffer, u64 size) { - const u64 local_remaining = local_extent_remaining(offset); + const u64 max_size = std::min(size, local_extent_remaining(offset)); - const u64 total_read = m_file.read_at(file_offset(offset), buffer, std::min(size, local_remaining)); - - const auto total_size = this->size(); - - if (size > total_read && (offset + total_read) < total_size) + if (max_size == 0) { - const u64 second_total_read = read_at(offset + total_read, reinterpret_cast(buffer) + total_read, size - total_read); - - return total_read + second_total_read; + return 0; } - return total_read; + const u64 archive_first_offset = file_offset(offset); + const u64 total_size = this->size(); + u64 total_read; + + // If it's a non-encrypted type + if (m_dec->get_enc_type() == iso_encryption_type::NONE) + { + total_read = m_file.read_at(archive_first_offset, buffer, max_size); + + if (size > total_read && (offset + total_read) < total_size) + { + total_read += read_at(offset + total_read, reinterpret_cast(buffer) + total_read, size - total_read); + } + + return total_read; + } + + // If it's an encrypted type + + // IMPORTANT NOTE: + // + // "iso_file_decryption::decrypt()" method requires that offset and size are multiple of 16 bytes + // (ciphertext block's size) and that a previous ciphertext block (used as IV) is read in case + // offset is not a multiple of ISO_SECTOR_SIZE + // + // ---------------------------------------------------------------------- + // file on ISO archive: | ' ' | + // ---------------------------------------------------------------------- + // ' ' + // --------------------------------------------- + // buffer: | | + // --------------------------------------------- + // ' ' ' ' + // ------------------------------------------------------------------------------------------------------------------------------------- + // ISO archive: | sec 0 | sec 1 |xxxxx######'###########'###########'###########'##xxxxxxxxx| | ... | sec n-1 | sec n | + // ------------------------------------------------------------------------------------------------------------------------------------- + // 16 Bytes x block read: | | | | | | | '#######'###########'###########'###########'###| | | | | | | | | | | | | | | + // ' ' ' ' + // | first sec | inner sec(s) | last sec | + + const u64 archive_last_offset = archive_first_offset + max_size - 1; + iso_sector first_sec, last_sec; + u64 offset_aligned; + u64 offset_aligned_first_out; + + first_sec.lba_address = (archive_first_offset / ISO_SECTOR_SIZE) * ISO_SECTOR_SIZE; + first_sec.offset = archive_first_offset % ISO_SECTOR_SIZE; + first_sec.size = first_sec.offset + max_size <= ISO_SECTOR_SIZE ? max_size : ISO_SECTOR_SIZE - first_sec.offset; + + last_sec.lba_address = last_sec.address_aligned = (archive_last_offset / ISO_SECTOR_SIZE) * ISO_SECTOR_SIZE; + //last_sec.offset = last_sec.offset_aligned = 0; // Always 0 so no need to set and use those attributes + last_sec.size = (archive_last_offset % ISO_SECTOR_SIZE) + 1; + + // + // First sector + // + + offset_aligned = first_sec.offset & ~0xF; + offset_aligned_first_out = (first_sec.offset + first_sec.size) & ~0xF; + + first_sec.offset_aligned = offset_aligned != 0 ? offset_aligned - 16 : 0; // Eventually include the previous block (used as IV) + first_sec.size_aligned = offset_aligned_first_out != (first_sec.offset + first_sec.size) ? + offset_aligned_first_out + 16 - first_sec.offset_aligned : + offset_aligned_first_out - first_sec.offset_aligned; + first_sec.address_aligned = first_sec.lba_address + first_sec.offset_aligned; + + total_read = m_file.read_at(first_sec.address_aligned, &first_sec.buf.data()[first_sec.offset_aligned], first_sec.size_aligned); + + m_dec->decrypt(first_sec.address_aligned, &first_sec.buf.data()[first_sec.offset_aligned], first_sec.size_aligned, m_meta.name); + memcpy(buffer, &first_sec.buf.data()[first_sec.offset], first_sec.size); + + u64 sector_count = (last_sec.lba_address - first_sec.lba_address) / ISO_SECTOR_SIZE + 1; + + if (sector_count < 2) // If no more sector(s) + { + if (total_read != first_sec.size_aligned) + { + sys_log.error("read_at(): %s: Error reading from file", m_meta.name); + + seek(m_pos, fs::seek_set); + return 0; + } + + return max_size; + } + + // + // Inner sector(s), if any + // + + u64 expected_inner_sector_read = 0; + + if (sector_count > 2) // If inner sector(s) are present + { + u64 inner_sector_size = expected_inner_sector_read = (sector_count - 2) * ISO_SECTOR_SIZE; + + total_read += m_file.read_at(first_sec.lba_address + ISO_SECTOR_SIZE, &reinterpret_cast(buffer)[first_sec.size], inner_sector_size); + + m_dec->decrypt(first_sec.lba_address + ISO_SECTOR_SIZE, &reinterpret_cast(buffer)[first_sec.size], inner_sector_size, m_meta.name); + } + + // + // Last sector + // + + offset_aligned_first_out = last_sec.size & ~0xF; + last_sec.size_aligned = offset_aligned_first_out != last_sec.size ? offset_aligned_first_out + 16 : offset_aligned_first_out; + + total_read += m_file.read_at(last_sec.address_aligned, last_sec.buf.data(), last_sec.size_aligned); + + m_dec->decrypt(last_sec.address_aligned, last_sec.buf.data(), last_sec.size_aligned, m_meta.name); + memcpy(&reinterpret_cast(buffer)[max_size - last_sec.size], last_sec.buf.data(), last_sec.size); + + // + // As last, check for an unlikely reading error (decoding also failed due to use of partially initialized buffer) + // + + 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); + + seek(m_pos, fs::seek_set); + return 0; + } + + return max_size; } u64 iso_file::write(const void* /*buffer*/, u64 /*size*/) @@ -590,7 +1093,7 @@ std::unique_ptr iso_device::open(const std::string& path, bs_t(fs::file(iso_path, mode), *node); + return std::make_unique(fs::file(m_path, mode), m_archive.get_dec(), *node); } std::unique_ptr iso_device::open_dir(const std::string& path) @@ -621,7 +1124,7 @@ void iso_dir::rewind() void load_iso(const std::string& path) { - sys_log.notice("Loading iso '%s'", path); + sys_log.notice("Loading ISO '%s'", path); fs::set_virtual_device("iso_overlay_fs_dev", stx::make_shared(path)); @@ -630,7 +1133,7 @@ void load_iso(const std::string& path) void unload_iso() { - sys_log.notice("Unloading iso"); + sys_log.notice("Unloading ISO"); fs::set_virtual_device("iso_overlay_fs_dev", stx::shared_ptr()); } diff --git a/rpcs3/Loader/ISO.h b/rpcs3/Loader/ISO.h index 2665ec29e5..8a88e876bf 100644 --- a/rpcs3/Loader/ISO.h +++ b/rpcs3/Loader/ISO.h @@ -4,6 +4,7 @@ #include "Utilities/File.h" #include "util/types.hpp" +#include "Crypto/aes.h" bool is_file_iso(const std::string& path); bool is_file_iso(const fs::file& path); @@ -11,6 +12,58 @@ bool is_file_iso(const fs::file& path); void load_iso(const std::string& path); void unload_iso(); +/* +- Hijacked the "iso_archive::iso_archive" method to test if the ".iso" file is encrypted and sets a flag. + The flag is set according to the first matching encryption type found following the order below: + - Redump: ".dkey" or ".key" (as alternative) file, with the same name of the ".iso" file, + exists in the same folder of the ".iso" file + - 3k3y: 3k3y watermark exists at offset 0xF70 + If the flag is set then the "iso_file::read" method will decrypt the data on the fly + +- Supported ISO encryption type: + - Decrypted (.iso) + - 3k3y (decrypted / encrypted) (.iso) + - Redump (encrypted) (.iso + .dkey / .key) + +- Unsupported ISO encryption type: + - Encrypted split ISO files +*/ + +// Struct to store ISO region information (storing addresses instead of LBA since we need to compare +// the address anyway, so would have to multiply or divide every read if storing LBA) +struct iso_region_info +{ + bool encrypted = false; + u64 region_first_addr = 0; + u64 region_last_addr = 0; +}; + +// Enum to decide ISO encryption type +enum class iso_encryption_type +{ + NONE, + DEC_3K3Y, + ENC_3K3Y, + REDUMP +}; + +// ISO file decryption class +class iso_file_decryption +{ +private: + aes_context m_aes_dec; + iso_encryption_type m_enc_type = iso_encryption_type::NONE; + std::vector m_region_info; + + void reset(); + +public: + iso_encryption_type get_enc_type() const { return m_enc_type; } + + bool init(const std::string& path); + bool decrypt(u64 offset, void* buffer, u64 size, const std::string& name); +}; + struct iso_extent_info { u64 start = 0; @@ -38,16 +91,17 @@ class iso_file : public fs::file_base { private: fs::file m_file; - iso_fs_metadata m_meta {}; + std::shared_ptr m_dec; + iso_fs_metadata m_meta; u64 m_pos = 0; std::pair get_extent_pos(u64 pos) const; - u64 file_offset(u64 pos) const; u64 local_extent_remaining(u64 pos) const; u64 local_extent_size(u64 pos) const; + u64 file_offset(u64 pos) const; public: - iso_file(fs::file&& iso_handle, const iso_fs_node& node); + iso_file(fs::file&& iso_handle, std::shared_ptr iso_dec, const iso_fs_node& node); fs::stat_t get_stat() override; bool trunc(u64 length) override; @@ -75,45 +129,47 @@ public: void rewind() override; }; -// represents the .iso file itself. +// Represents the .iso file itself class iso_archive { private: std::string m_path; - iso_fs_node m_root {}; fs::file m_file; + std::shared_ptr m_dec; + iso_fs_node m_root {}; public: iso_archive(const std::string& path); + const std::string& path() const { return m_path; } + const std::shared_ptr get_dec() { return m_dec; } + iso_fs_node* retrieve(const std::string& path); bool exists(const std::string& path); bool is_file(const std::string& path); iso_file open(const std::string& path); - psf::registry open_psf(const std::string& path); - - const std::string& path() const { return m_path; } }; class iso_device : public fs::device_base { private: + std::string m_path; iso_archive m_archive; - std::string iso_path; public: inline static std::string virtual_device_name = "/vfsv0_virtual_iso_overlay_fs_dev"; iso_device(const std::string& iso_path, const std::string& device_name = virtual_device_name) - : m_archive(iso_path), iso_path(iso_path) + : m_path(iso_path), m_archive(iso_path) { fs_prefix = device_name; } + ~iso_device() override = default; - const std::string& get_loaded_iso() const { return iso_path; } + const std::string& get_loaded_iso() const { return m_path; } bool stat(const std::string& path, fs::stat_t& info) override; bool statfs(const std::string& path, fs::device_stat& info) override; From 7168cd566ada815a144d91e232e11770fe825895 Mon Sep 17 00:00:00 2001 From: oltolm Date: Thu, 9 Apr 2026 23:51:34 +0200 Subject: [PATCH 11/43] Fix ASan logger shutdown use-after-free --- rpcs3/rpcs3.cpp | 12 ++++++++++-- rpcs3/util/logs.cpp | 10 ++++++++++ rpcs3/util/logs.hpp | 3 +++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/rpcs3/rpcs3.cpp b/rpcs3/rpcs3.cpp index 011dfbe39f..90d92cf1bd 100644 --- a/rpcs3/rpcs3.cpp +++ b/rpcs3/rpcs3.cpp @@ -656,7 +656,7 @@ int run_rpcs3(int argc, char** argv) // Initialize thread pool finalizer (on first use) static_cast(named_thread("", [](int) {})); - static std::unique_ptr log_file; + std::unique_ptr log_file; { // Check free space fs::device_stat stats{}; @@ -669,9 +669,17 @@ int run_rpcs3(int argc, char** argv) log_file = logs::make_file_listener(log_name, stats.avail_free / 4); } - static std::unique_ptr fatal_listener = std::make_unique(); + auto fatal_listener = std::make_unique(); logs::listener::add(fatal_listener.get()); + struct log_listener_shutdown_guard + { + ~log_listener_shutdown_guard() + { + logs::listener::shutdown_all(); + } + } log_listener_shutdown; + { // Write RPCS3 version logs::stored_message ver{sys_log.always()}; diff --git a/rpcs3/util/logs.cpp b/rpcs3/util/logs.cpp index f0afc95bac..3364e16ea9 100644 --- a/rpcs3/util/logs.cpp +++ b/rpcs3/util/logs.cpp @@ -372,6 +372,16 @@ void logs::listener::sync_all() } } +void logs::listener::shutdown_all() +{ + std::lock_guard lock(g_mutex); + + for (listener* lis = get_logger()->m_next.exchange(nullptr); lis;) + { + lis = lis->m_next.exchange(nullptr); + } +} + void logs::listener::close_all_prematurely() { for (listener* lis = get_logger(); lis; lis = lis->m_next) diff --git a/rpcs3/util/logs.hpp b/rpcs3/util/logs.hpp index 1b75bd6499..a2fcc863b2 100644 --- a/rpcs3/util/logs.hpp +++ b/rpcs3/util/logs.hpp @@ -98,6 +98,9 @@ namespace logs // Flush log to disk static void sync_all(); + // Detach all listeners before controlled shutdown tears them down. + static void shutdown_all(); + // Close file handle after flushing to disk (hazardous) static void close_all_prematurely(); }; From f463c128e3dcb52f78d182f79be64ae7396035c4 Mon Sep 17 00:00:00 2001 From: oltolm Date: Thu, 9 Apr 2026 23:51:53 +0200 Subject: [PATCH 12/43] Fix ASan fatal-report access during emulator teardown --- rpcs3/Emu/System.cpp | 17 +++++++++++++++++ rpcs3/Emu/System.h | 6 ++++-- rpcs3/rpcs3.cpp | 19 +++++++++++++------ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index da7251e291..f198156d47 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -70,6 +70,8 @@ LOG_CHANNEL(sys_log, "SYS"); // Preallocate 32 MiB stx::manual_typemap g_fixed_typemap; +static constinit atomic_t s_emulator_available{false}; + std::string g_cfg_defaults; atomic_t g_watchdog_hold_ctr{0}; @@ -118,6 +120,21 @@ namespace rsx void set_native_ui_flip(); } +Emulator::Emulator() noexcept +{ + s_emulator_available = true; +} + +Emulator::~Emulator() noexcept +{ + s_emulator_available = false; +} + +bool Emulator::IsAvailable() noexcept +{ + return s_emulator_available.load(); +} + template<> void fmt_class_string::format(std::string& out, u64 arg) { diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index ac2243389c..a38b65089f 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -201,8 +201,10 @@ public: static constexpr std::string_view game_id_boot_prefix = "%RPCS3_GAMEID%:"; static constexpr std::string_view vfs_boot_prefix = "%RPCS3_VFS%:"; - Emulator() noexcept = default; - ~Emulator() noexcept = default; + Emulator() noexcept; + ~Emulator() noexcept; + + static bool IsAvailable() noexcept; void SetCallbacks(EmuCallbacks&& cb) { diff --git a/rpcs3/rpcs3.cpp b/rpcs3/rpcs3.cpp index 90d92cf1bd..2030fc58b4 100644 --- a/rpcs3/rpcs3.cpp +++ b/rpcs3/rpcs3.cpp @@ -185,16 +185,23 @@ std::set get_one_drive_paths() fmt::append(buf, "\nSerialized Object: %s", g_tls_serialize_name); } - const system_state state = Emu.GetStatus(false); - - if (state == system_state::stopped) + if (Emulator::IsAvailable()) { - fmt::append(buf, "\nEmulation is stopped"); + const system_state state = Emu.GetStatus(false); + + if (state == system_state::stopped) + { + fmt::append(buf, "\nEmulation is stopped"); + } + else + { + const std::string name = Emu.GetTitleAndTitleID(); + fmt::append(buf, "\nTitle: \"%s\" (emulation is %s)", name.empty() ? "N/A" : name.c_str(), state == system_state::stopping ? "stopping" : "running"); + } } else { - const std::string& name = Emu.GetTitleAndTitleID(); - fmt::append(buf, "\nTitle: \"%s\" (emulation is %s)", name.empty() ? "N/A" : name.data(), state == system_state::stopping ? "stopping" : "running"); + fmt::append(buf, "\nEmulation object is unavailable (process teardown)"); } fmt::append(buf, "\nBuild: \"%s\"", rpcs3::get_verbose_version()); From 934bc34685b44b97700aef6d8fd51c62b0ae7796 Mon Sep 17 00:00:00 2001 From: luci4 <142809264+l00sy4@users.noreply.github.com> Date: Sat, 11 Apr 2026 20:28:11 +0100 Subject: [PATCH 13/43] sysinfo.cpp: Replaced PEB read with ntdll function (OS version read) --- rpcs3/util/sysinfo.cpp | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/rpcs3/util/sysinfo.cpp b/rpcs3/util/sysinfo.cpp index 16b40d7e52..b894c9af0b 100755 --- a/rpcs3/util/sysinfo.cpp +++ b/rpcs3/util/sysinfo.cpp @@ -7,12 +7,13 @@ #if defined(ARCH_ARM64) #include "Emu/CPU/Backends/AArch64/AArch64Common.h" #endif - #ifdef _WIN32 #include "windows.h" #include "sysinfoapi.h" #include "subauth.h" #include "stringapiset.h" +#include "util/dyn_lib.hpp" +DYNAMIC_IMPORT("ntdll.dll", RtlGetVersion, NTSTATUS(OSVERSIONINFOW* lpVersionInformation)); #else #include #include @@ -794,29 +795,15 @@ utils::OS_version utils::get_OS_version() #endif #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); - res.version_major = *reinterpret_cast(peb + 0x118); - res.version_minor = *reinterpret_cast(peb + 0x11c); - res.version_patch = *reinterpret_cast(peb + 0x120); -#else - HKEY hKey; - if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_READ, &hKey) == ERROR_SUCCESS) + if (RtlGetVersion) { - 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, version_patch] = read_reg_sz(hKey, "CurrentBuildNumber"); - - if (check_major) res.version_major = version_major; - if (check_minor) res.version_minor = version_minor; - if (check_build) res.version_patch = stoi(version_patch); - - RegCloseKey(hKey); + OSVERSIONINFOW osvi{}; + osvi.dwOSVersionInfoSize = sizeof(osvi); + RtlGetVersion(&osvi); + res.version_major = osvi.dwMajorVersion; + res.version_minor = osvi.dwMinorVersion; + res.version_patch = osvi.dwBuildNumber; } -#endif #elif defined (__APPLE__) res.version_major = Darwin_Version::getNSmajorVersion(); res.version_minor = Darwin_Version::getNSminorVersion(); From f826f95c70b09565d923b51a6dcb10b34fe1b445 Mon Sep 17 00:00:00 2001 From: Ani Date: Sat, 11 Apr 2026 00:11:10 +0200 Subject: [PATCH 14/43] gui: Group CPU accuracy settings together --- rpcs3/Emu/system_config.h | 2 +- rpcs3/rpcs3qt/emu_settings_type.h | 4 +- rpcs3/rpcs3qt/settings_dialog.cpp | 4 +- rpcs3/rpcs3qt/settings_dialog.ui | 203 +++++++++++++++++------------- rpcs3/rpcs3qt/tooltips.h | 2 +- 5 files changed, 120 insertions(+), 95 deletions(-) diff --git a/rpcs3/Emu/system_config.h b/rpcs3/Emu/system_config.h index 7a84455dae..f8c621ad25 100644 --- a/rpcs3/Emu/system_config.h +++ b/rpcs3/Emu/system_config.h @@ -66,10 +66,10 @@ struct cfg_root : cfg::node cfg::_int<-64, 64> stub_ppu_traps{ this, "Stub PPU Traps", 0, true }; // Hack, skip PPU traps for rare cases where the trap is continueable (specify relative instructions to skip) cfg::_bool precise_spu_verification{ this, "Precise SPU Verification", false }; // Disables use of xorsum based spu verification if enabled. cfg::_bool ppu_llvm_nj_fixup{ this, "PPU LLVM Java Mode Handling", true }; // Partially respect current Java Mode for alti-vec ops by PPU LLVM + cfg::_bool ppu_fix_vnan{ this, "PPU Vector NaN Handling", true }; // Accuracy. Partial. cfg::_bool use_accurate_dfma{ this, "Use Accurate DFMA", true }; // Enable accurate double-precision FMA for CPUs which do not support it natively cfg::_bool ppu_set_sat_bit{ this, "PPU Set Saturation Bit", false }; // Accuracy. If unset, completely disable saturation flag handling. cfg::_bool ppu_use_nj_bit{ this, "PPU Accurate Non-Java Mode", false }; // Accuracy. If set, accurately emulate NJ flag. Implies NJ fixup. - cfg::_bool ppu_fix_vnan{ this, "PPU Fixup Vector NaN Values", true }; // Accuracy. Partial. cfg::_bool ppu_set_vnan{ this, "PPU Accurate Vector NaN Values", false }; // Accuracy. Implies ppu_fix_vnan. cfg::_bool ppu_set_fpcc{ this, "PPU Set FPCC Bits", false }; // Accuracy. diff --git a/rpcs3/rpcs3qt/emu_settings_type.h b/rpcs3/rpcs3qt/emu_settings_type.h index db3e3556da..bc2f1e5cce 100644 --- a/rpcs3/rpcs3qt/emu_settings_type.h +++ b/rpcs3/rpcs3qt/emu_settings_type.h @@ -42,10 +42,10 @@ enum class emu_settings_type PerformanceReport, FullWidthAVX512, PPUNJFixup, + PPUVNANFixup, AccurateDFMA, AccuratePPUSAT, AccuratePPUNJ, - FixupPPUVNAN, AccuratePPUVNAN, AccuratePPUFPCC, MaxPreemptCount, @@ -258,10 +258,10 @@ inline static const std::map settings_location { emu_settings_type::FullWidthAVX512, { "Core", "Full Width AVX-512"}}, { emu_settings_type::NumPPUThreads, { "Core", "PPU Threads"}}, { emu_settings_type::PPUNJFixup, { "Core", "PPU LLVM Java Mode Handling"}}, + { emu_settings_type::PPUVNANFixup, { "Core", "PPU Vector NaN Handling"}}, { emu_settings_type::AccurateDFMA, { "Core", "Use Accurate DFMA"}}, { emu_settings_type::AccuratePPUSAT, { "Core", "PPU Set Saturation Bit"}}, { emu_settings_type::AccuratePPUNJ, { "Core", "PPU Accurate Non-Java Mode"}}, - { emu_settings_type::FixupPPUVNAN, { "Core", "PPU Fixup Vector NaN Values"}}, { emu_settings_type::AccuratePPUVNAN, { "Core", "PPU Accurate Vector NaN Values"}}, { emu_settings_type::AccuratePPUFPCC, { "Core", "PPU Set FPCC Bits"}}, { emu_settings_type::MaxPreemptCount, { "Core", "Max CPU Preempt Count"}}, diff --git a/rpcs3/rpcs3qt/settings_dialog.cpp b/rpcs3/rpcs3qt/settings_dialog.cpp index 7e7f115d22..8ff2da2018 100644 --- a/rpcs3/rpcs3qt/settings_dialog.cpp +++ b/rpcs3/rpcs3qt/settings_dialog.cpp @@ -1499,8 +1499,8 @@ settings_dialog::settings_dialog(std::shared_ptr gui_settings, std m_emu_settings->EnhanceCheckBox(ui->ppuNJFixup, emu_settings_type::PPUNJFixup); SubscribeTooltip(ui->ppuNJFixup, tooltips.settings.fixup_ppunj); - m_emu_settings->EnhanceCheckBox(ui->fixupPPUVNAN, emu_settings_type::FixupPPUVNAN); - SubscribeTooltip(ui->fixupPPUVNAN, tooltips.settings.fixup_ppuvnan); + m_emu_settings->EnhanceCheckBox(ui->PPUVNANfixup, emu_settings_type::PPUVNANFixup); + SubscribeTooltip(ui->PPUVNANfixup, tooltips.settings.fixup_ppuvnan); m_emu_settings->EnhanceCheckBox(ui->llvmPrecompilation, emu_settings_type::LLVMPrecompilation); SubscribeTooltip(ui->llvmPrecompilation, tooltips.settings.llvm_precompilation); diff --git a/rpcs3/rpcs3qt/settings_dialog.ui b/rpcs3/rpcs3qt/settings_dialog.ui index d3b98a2608..6c8c14fc42 100644 --- a/rpcs3/rpcs3qt/settings_dialog.ui +++ b/rpcs3/rpcs3qt/settings_dialog.ui @@ -3543,7 +3543,7 @@ 1 - 0.5 + 0.500000000000000 @@ -3571,7 +3571,7 @@ 1 - 0.5 + 0.500000000000000 @@ -4309,6 +4309,13 @@ + + + + Disable Asynchronous Memory Manager + + + @@ -4316,6 +4323,20 @@ + + + + Disable Hardware ColorSpace Remapping + + + + + + + Disable On-Disk Shader Cache + + + @@ -4382,27 +4403,6 @@ - - - - Disable Asynchronous Memory Manager - - - - - - - Disable On-Disk Shader Cache - - - - - - - Disable Hardware ColorSpace Remapping - - - @@ -4428,13 +4428,6 @@ Core - - - - Accurate DFMA - - - @@ -4456,20 +4449,6 @@ - - - - PPU Debug - - - - - - - SPU Debug - - - @@ -4477,59 +4456,24 @@ + + + + PPU Debug + + + - Set DAZ and FTZ + PPU Set DAZ and FTZ - + - Accurate PPU Saturation Bit - - - - - - - Accurate PPU Non-Java Mode - - - - - - - PPU Non-Java Mode Fixup - - - - - - - Accurate PPU Vector NaN Handling - - - - - - - Accurate PPU Float Condition Control - - - - - - - Accurate Cache Line Stores - - - - - - - PPU Vector NaN Fixup + SPU Debug @@ -4556,6 +4500,87 @@ + + + + CPU Accuracy + + + + + + Accurate PPU/SPU Double-Precision FMA + + + + + + + Accurate PPU/SPU Cache Line Stores + + + + + + + Accurate PPU Float Condition Control + + + + + + + Accurate PPU Saturation Bit + + + + + + + Accurate PPU Non-Java Mode + + + + + + + Accurate PPU Vector NaN Handling + + + + + + + Approximate PPU Non-Java Mode + + + + + + + Approximate PPU Vector NaN Handling + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::MinimumExpanding + + + + 0 + 0 + + + + + + + diff --git a/rpcs3/rpcs3qt/tooltips.h b/rpcs3/rpcs3qt/tooltips.h index caccc1c47b..9ec1355392 100644 --- a/rpcs3/rpcs3qt/tooltips.h +++ b/rpcs3/rpcs3qt/tooltips.h @@ -28,8 +28,8 @@ public: const QString debug_console_mode = tr("Increases the amount of usable system memory to match a DECR console and more.\nCauses some software to behave differently than on retail hardware."); const QString accurate_rsx_access = tr("Forces RSX pauses on SPU MFC_GETLLAR and SPU MFC_PUTLLUC operations."); const QString accurate_spu_dma = tr("Accurately processes SPU DMA operations."); - const QString fixup_ppunj = tr("Legacy option. Fixup result vector values in Non-Java Mode in PPU LLVM.\nIf unsure, do not modify this setting."); const QString accurate_dfma = tr("Use accurate double-precision FMA instructions in PPU and SPU backends.\nWhile disabling it might give a decent performance boost if your CPU doesn't support FMA, it may also introduce subtle bugs that otherwise do not occur.\nYou shouldn't disable it if your CPU supports FMA."); + const QString fixup_ppunj = tr("Legacy option. Fixup result vector values in Non-Java Mode in PPU LLVM.\nIf unsure, do not modify this setting."); const QString fixup_ppuvnan = tr("Fixup NaN results in vector instructions in PPU backends.\nIf unsure, do not modify this setting."); const QString silence_all_logs = tr("Stop writing any logs after game startup. Don't use unless you believe it's necessary."); const QString read_color = tr("Initializes render target memory using vm memory."); From 11050a70320a3c9ed8e11e7ecd82462aa93d17cd Mon Sep 17 00:00:00 2001 From: Vishrut Sachan Date: Sun, 12 Apr 2026 14:37:25 +0530 Subject: [PATCH 15/43] 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 16/43] 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 17/43] 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 18/43] 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 19/43] 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 20/43] 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 21/43] 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 22/43] 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 23/43] 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 24/43] 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 25/43] 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 26/43] 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 27/43] 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 28/43] 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 29/43] 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 30/43] 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 31/43] 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 32/43] 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 33/43] 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 34/43] 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 35/43] 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 36/43] 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 37/43] 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 38/43] 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 39/43] 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 40/43] 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 41/43] 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 42/43] 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 43/43] 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);