From d93d9b2c5aa859d1cf2f1381cefd204fb022163a Mon Sep 17 00:00:00 2001 From: Vishrut Sachan Date: Tue, 5 May 2026 14:44:16 +0530 Subject: [PATCH] game_list: Fix ISO cache bypass in is_from_yml branch for multi-game ISOs (#18683) Fixes regression from #18546 and #18679. ## Problem The is_from_yml ISO branch constructed iso_archive unconditionally, bypassing the cache check inside add_game, making the cache write-only for yml-sourced ISOs. ## Fix Added a lightweight index cache entry (iso_path + "//index") storing the subdir list + mtime. On hit, skips archive construction entirely. On miss, walks as before and writes the index --- rpcs3/Loader/iso_cache.cpp | 95 +++++++++++++++++++++++++++++-- rpcs3/Loader/iso_cache.h | 4 ++ rpcs3/rpcs3qt/game_list_frame.cpp | 26 +++++++-- 3 files changed, 115 insertions(+), 10 deletions(-) diff --git a/rpcs3/Loader/iso_cache.cpp b/rpcs3/Loader/iso_cache.cpp index 768948e421..a37927cf39 100644 --- a/rpcs3/Loader/iso_cache.cpp +++ b/rpcs3/Loader/iso_cache.cpp @@ -19,17 +19,23 @@ namespace 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) + // FNV-64 hash of the given key used as the cache filename stem. + std::string get_cache_stem(std::string_view key) { usz hash = rpcs3::fnv_seed; - for (const char c : iso_path) + for (const char c : key) { hash ^= static_cast(c); hash *= rpcs3::fnv_prime; } return fmt::format("%016llx", hash); } + + // Separate stem for the per-ISO subdir index entry. + std::string get_index_stem(const std::string& iso_path) + { + return get_cache_stem(iso_path + "//index"); + } } namespace iso_cache @@ -134,15 +140,96 @@ namespace iso_cache } } + bool load_index(const std::string& iso_path, std::vector& out_subdirs) + { + fs::stat_t iso_stat{}; + if (!fs::get_stat(iso_path, iso_stat) || iso_stat.is_directory) + { + return false; + } + + const std::string dir = get_cache_dir(); + const std::string yml_path = dir + get_index_stem(iso_path) + ".yml"; + + const fs::file yml_file(yml_path); + if (!yml_file) + { + return false; + } + + const auto [node, error] = yaml_load(yml_file.to_string()); + if (!error.empty()) + { + iso_cache_log.warning("Failed to parse index YAML for '%s': %s", iso_path, error); + return false; + } + + const s64 cached_mtime = node["mtime"].as(0); + if (cached_mtime != iso_stat.mtime) + { + return false; + } + + const YAML::Node subdirs_node = node["subdirs"]; + if (!subdirs_node || !subdirs_node.IsSequence()) + { + return false; + } + + for (const auto& entry : subdirs_node) + { + std::string name = entry.as(""); + if (!name.empty()) + { + out_subdirs.push_back(std::move(name)); + } + } + + return !out_subdirs.empty(); + } + + void save_index(const std::string& iso_path, const std::vector& subdirs) + { + fs::stat_t iso_stat{}; + if (!fs::get_stat(iso_path, iso_stat)) + { + return; + } + const std::string dir = get_cache_dir(); + const std::string yml_path = dir + get_index_stem(iso_path) + ".yml"; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "mtime" << YAML::Value << static_cast(iso_stat.mtime); + out << YAML::Key << "subdirs" << YAML::Value << YAML::BeginSeq; + for (const std::string& s : subdirs) + { + out << s; + } + out << YAML::EndSeq; + 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 index YAML for '%s'", iso_path); + } + } + void cleanup(const std::unordered_set& valid_iso_paths) { const std::string dir = get_cache_dir(); - // Build a set of stems that should exist. + // Build a set of stems that should exist, including index entries. std::unordered_set valid_stems; for (const std::string& path : valid_iso_paths) { valid_stems.insert(get_cache_stem(path)); + valid_stems.insert(get_index_stem(path)); } // Delete any cache files whose stem is not in the valid set. diff --git a/rpcs3/Loader/iso_cache.h b/rpcs3/Loader/iso_cache.h index a6c66797e6..88b080b1df 100644 --- a/rpcs3/Loader/iso_cache.h +++ b/rpcs3/Loader/iso_cache.h @@ -17,6 +17,7 @@ struct iso_metadata_cache_entry std::vector icon_data{}; std::string movie_path{}; std::string audio_path{}; + std::vector subdirs{}; }; namespace iso_cache @@ -27,6 +28,9 @@ namespace iso_cache // Persists a populated cache entry to disk. void save(const std::string& iso_path, const std::string& cache_key, const iso_metadata_cache_entry& entry); + bool load_index(const std::string& iso_path, std::vector& out_subdirs); + void save_index(const std::string& iso_path, const std::vector& subdirs); + // Remove cache entries for ISOs that are no longer in the scanned set. void cleanup(const std::unordered_set& valid_iso_paths); } diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 915eeb054d..6604020b79 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -861,10 +861,22 @@ void game_list_frame::OnParsingFinished() { if (is_iso_file(entry.path)) { + std::vector subdirs; + + if (iso_cache::load_index(entry.path, subdirs)) + { + for (const std::string& name : subdirs) + { + if (m_refresh_watcher.isCanceled()) break; + add_game(entry.path, name); + } + + return; + } + iso_archive archive(entry.path); const iso_fs_node& root = archive.root(); const std::regex ps3_gm_regex("^PS3_GM[[:digit:]]{2}$"); - bool found = false; for (const auto& child : root.children) { @@ -872,24 +884,26 @@ void game_list_frame::OnParsingFinished() { break; } - if (!child->metadata.is_directory) { continue; } const std::string& name = child->metadata.name; - if (name == "PS3_GAME" || std::regex_match(name, ps3_gm_regex)) { + subdirs.push_back(name); add_game(entry.path, name); - found = true; } } - - if (!found) + if (subdirs.empty()) { add_game(entry.path); + subdirs.push_back("PS3_GAME"); + } + if (!m_refresh_watcher.isCanceled()) + { + iso_cache::save_index(entry.path, subdirs); } return;