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
This commit is contained in:
Vishrut Sachan 2026-05-05 14:44:16 +05:30 committed by GitHub
parent 4f23f5505a
commit d93d9b2c5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 115 additions and 10 deletions

View file

@ -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<u8>(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<std::string>& 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<s64>(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<std::string>("");
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<std::string>& 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<long long>(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<std::string>& 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<std::string> 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.

View file

@ -17,6 +17,7 @@ struct iso_metadata_cache_entry
std::vector<u8> icon_data{};
std::string movie_path{};
std::string audio_path{};
std::vector<std::string> 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<std::string>& out_subdirs);
void save_index(const std::string& iso_path, const std::vector<std::string>& subdirs);
// Remove cache entries for ISOs that are no longer in the scanned set.
void cleanup(const std::unordered_set<std::string>& valid_iso_paths);
}

View file

@ -861,10 +861,22 @@ void game_list_frame::OnParsingFinished()
{
if (is_iso_file(entry.path))
{
std::vector<std::string> 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;