diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 23582ad21..02c661d31 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -79,8 +79,6 @@ add_library(core
mdec.h
memory_card.cpp
memory_card.h
- memory_card_icon_cache.cpp
- memory_card_icon_cache.h
memory_card_image.cpp
memory_card_image.h
multitap.cpp
diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj
index 19a94e244..ee74c8b36 100644
--- a/src/core/core.vcxproj
+++ b/src/core/core.vcxproj
@@ -62,7 +62,6 @@
-
@@ -144,7 +143,6 @@
-
diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters
index 72810d421..b02bcfd85 100644
--- a/src/core/core.vcxproj.filters
+++ b/src/core/core.vcxproj.filters
@@ -68,7 +68,6 @@
-
@@ -143,6 +142,5 @@
-
\ No newline at end of file
diff --git a/src/core/game_list.cpp b/src/core/game_list.cpp
index d9e3145d8..16f27ca55 100644
--- a/src/core/game_list.cpp
+++ b/src/core/game_list.cpp
@@ -5,12 +5,14 @@
#include "bios.h"
#include "fullscreen_ui.h"
#include "host.h"
+#include "memory_card_image.h"
#include "psf_loader.h"
#include "settings.h"
#include "system.h"
#include "util/cd_image.h"
#include "util/http_downloader.h"
+#include "util/image.h"
#include "util/ini_settings_interface.h"
#include "common/assert.h"
@@ -59,6 +61,19 @@ struct PlayedTimeEntry
std::time_t total_played_time;
};
+#pragma pack(push, 1)
+struct MemcardTimestampCacheEntry
+{
+ enum : u32
+ {
+ MAX_SERIAL_LENGTH = 32,
+ };
+
+ char serial[MAX_SERIAL_LENGTH];
+ s64 memcard_timestamp;
+};
+#pragma pack(pop)
+
} // namespace
using CacheMap = PreferUnorderedStringMap;
@@ -101,12 +116,17 @@ static PlayedTimeEntry UpdatePlayedTimeFile(const std::string& path, const std::
std::time_t add_time);
static std::string GetCustomPropertiesFile();
+
+static FileSystem::ManagedCFilePtr OpenMemoryCardTimestampCache(bool for_write);
+static bool UpdateMemcardTimestampCache(const MemcardTimestampCacheEntry& entry);
+
} // namespace GameList
static std::vector s_entries;
static std::recursive_mutex s_mutex;
static GameList::CacheMap s_cache_map;
static std::unique_ptr s_cache_write_stream;
+static std::vector s_memcard_timestamp_cache_entries;
static bool s_game_list_loaded = false;
@@ -1629,3 +1649,222 @@ std::optional GameList::GetCustomRegionForPath(const std::string_vie
else
return std::nullopt;
}
+
+static constexpr const char MEMCARD_TIMESTAMP_CACHE_SIGNATURE[] = {'M', 'C', 'D', 'I', 'C', 'N', '0', '2'};
+
+FileSystem::ManagedCFilePtr GameList::OpenMemoryCardTimestampCache(bool for_write)
+{
+ const std::string filename = Path::Combine(EmuFolders::Cache, "memcard_icons.cache");
+ const char* mode = for_write ? "r+b" : "rb";
+ const FileSystem::FileShareMode share_mode =
+ for_write ? FileSystem::FileShareMode::DenyReadWrite : FileSystem::FileShareMode::DenyWrite;
+ FileSystem::ManagedCFilePtr fp = FileSystem::OpenManagedSharedCFile(filename.c_str(), mode, share_mode, nullptr);
+ if (fp)
+ return fp;
+
+ // Doesn't exist? Create it.
+ if (errno == ENOENT)
+ {
+ if (!for_write)
+ return nullptr;
+
+ mode = "w+b";
+ fp = FileSystem::OpenManagedSharedCFile(filename.c_str(), mode, share_mode, nullptr);
+ if (fp)
+ return fp;
+ }
+
+ // If there's a sharing violation, try again for 100ms.
+ if (errno != EACCES)
+ return nullptr;
+
+ Common::Timer timer;
+ while (timer.GetTimeMilliseconds() <= 100.0f)
+ {
+ fp = FileSystem::OpenManagedSharedCFile(filename.c_str(), mode, share_mode, nullptr);
+ if (fp)
+ return fp;
+
+ if (errno != EACCES)
+ return nullptr;
+ }
+
+ ERROR_LOG("Timed out while trying to open memory card cache file.");
+ return nullptr;
+}
+
+void GameList::ReloadMemcardTimestampCache()
+{
+ s_memcard_timestamp_cache_entries.clear();
+
+ FileSystem::ManagedCFilePtr fp = OpenMemoryCardTimestampCache(false);
+ if (!fp)
+ return;
+
+#ifndef _WIN32
+ FileSystem::POSIXLock lock(fp.get());
+#endif
+
+ const s64 file_size = FileSystem::FSize64(fp.get());
+ if (file_size < static_cast(sizeof(MEMCARD_TIMESTAMP_CACHE_SIGNATURE)))
+ return;
+
+ const size_t count =
+ (static_cast(file_size) - sizeof(MEMCARD_TIMESTAMP_CACHE_SIGNATURE)) / sizeof(MemcardTimestampCacheEntry);
+ if (count <= 0)
+ return;
+
+ char signature[sizeof(MEMCARD_TIMESTAMP_CACHE_SIGNATURE)];
+ if (std::fread(signature, sizeof(signature), 1, fp.get()) != 1 ||
+ std::memcmp(signature, MEMCARD_TIMESTAMP_CACHE_SIGNATURE, sizeof(signature)) != 0)
+ {
+ return;
+ }
+
+ s_memcard_timestamp_cache_entries.resize(static_cast(count));
+ if (std::fread(s_memcard_timestamp_cache_entries.data(), sizeof(MemcardTimestampCacheEntry),
+ s_memcard_timestamp_cache_entries.size(), fp.get()) != s_memcard_timestamp_cache_entries.size())
+ {
+ s_memcard_timestamp_cache_entries = {};
+ return;
+ }
+
+ // Just in case.
+ for (MemcardTimestampCacheEntry& entry : s_memcard_timestamp_cache_entries)
+ entry.serial[sizeof(entry.serial) - 1] = 0;
+}
+
+std::string GameList::GetGameIconPath(std::string_view serial, std::string_view path)
+{
+ std::string ret;
+
+ if (serial.empty())
+ return ret;
+
+ // might exist already, or the user used a custom icon
+ ret = Path::Combine(EmuFolders::GameIcons, TinyString::from_format("{}.png", serial));
+ if (FileSystem::FileExists(ret.c_str()))
+ return ret;
+
+ MemoryCardType type;
+ std::string memcard_path = System::GetGameMemoryCardPath(serial, path, 0, &type);
+ FILESYSTEM_STAT_DATA memcard_sd;
+ if (memcard_path.empty() || type == MemoryCardType::Shared ||
+ !FileSystem::StatFile(memcard_path.c_str(), &memcard_sd))
+ {
+ ret = {};
+ return ret;
+ }
+
+ const s64 timestamp = memcard_sd.ModificationTime;
+ TinyString index_serial;
+ index_serial.assign(
+ serial.substr(0, std::min(serial.length(), MemcardTimestampCacheEntry::MAX_SERIAL_LENGTH - 1)));
+
+ MemcardTimestampCacheEntry* serial_entry = nullptr;
+ for (MemcardTimestampCacheEntry& entry : s_memcard_timestamp_cache_entries)
+ {
+ if (StringUtil::EqualNoCase(index_serial, entry.serial))
+ {
+ if (entry.memcard_timestamp == timestamp)
+ {
+ // card hasn't changed, still no icon
+ ret = {};
+ return ret;
+ }
+
+ serial_entry = &entry;
+ break;
+ }
+ }
+
+ if (!serial_entry)
+ {
+ serial_entry = &s_memcard_timestamp_cache_entries.emplace_back();
+ std::memset(serial_entry, 0, sizeof(MemcardTimestampCacheEntry));
+ }
+
+ serial_entry->memcard_timestamp = timestamp;
+ StringUtil::Strlcpy(serial_entry->serial, index_serial.view(), sizeof(serial_entry->serial));
+
+ // Try extracting an icon.
+ MemoryCardImage::DataArray data;
+ if (MemoryCardImage::LoadFromFile(&data, memcard_path.c_str()))
+ {
+ std::vector files = MemoryCardImage::EnumerateFiles(data, false);
+ if (!files.empty())
+ {
+ const MemoryCardImage::FileInfo& fi = files.front();
+ if (!fi.icon_frames.empty())
+ {
+ INFO_LOG("Extracting memory card icon from {} ({}) to {}", fi.filename, Path::GetFileTitle(memcard_path),
+ Path::GetFileTitle(ret));
+
+ RGBA8Image image(MemoryCardImage::ICON_WIDTH, MemoryCardImage::ICON_HEIGHT);
+ std::memcpy(image.GetPixels(), &fi.icon_frames.front().pixels,
+ MemoryCardImage::ICON_WIDTH * MemoryCardImage::ICON_HEIGHT * sizeof(u32));
+ if (!image.SaveToFile(ret.c_str()))
+ {
+ ERROR_LOG("Failed to save memory card icon to {}.", ret);
+ ret = {};
+ return ret;
+ }
+ }
+ }
+ }
+
+ UpdateMemcardTimestampCache(*serial_entry);
+ return ret;
+}
+
+bool GameList::UpdateMemcardTimestampCache(const MemcardTimestampCacheEntry& entry)
+{
+ FileSystem::ManagedCFilePtr fp = OpenMemoryCardTimestampCache(true);
+ if (!fp)
+ return false;
+
+#ifndef _WIN32
+ FileSystem::POSIXLock lock(fp.get());
+#endif
+
+ // check signature, write it if it's non-existent or invalid
+ char signature[sizeof(MEMCARD_TIMESTAMP_CACHE_SIGNATURE)];
+ if (std::fread(signature, sizeof(signature), 1, fp.get()) != 1 ||
+ std::memcmp(signature, MEMCARD_TIMESTAMP_CACHE_SIGNATURE, sizeof(signature)) != 0)
+ {
+ if (!FileSystem::FTruncate64(fp.get(), 0) || FileSystem::FSeek64(fp.get(), 0, SEEK_SET) != 0 ||
+ std::fwrite(MEMCARD_TIMESTAMP_CACHE_SIGNATURE, sizeof(MEMCARD_TIMESTAMP_CACHE_SIGNATURE), 1, fp.get()) != 1)
+ {
+ return false;
+ }
+ }
+
+ // need to seek to switch from read->write?
+ s64 current_pos = sizeof(MEMCARD_TIMESTAMP_CACHE_SIGNATURE);
+ if (FileSystem::FSeek64(fp.get(), current_pos, SEEK_SET) != 0)
+ return false;
+
+ for (;;)
+ {
+ MemcardTimestampCacheEntry existing_entry;
+ if (std::fread(&existing_entry, sizeof(existing_entry), 1, fp.get()) != 1)
+ break;
+
+ existing_entry.serial[sizeof(existing_entry.serial) - 1] = 0;
+ if (!StringUtil::EqualNoCase(existing_entry.serial, entry.serial))
+ {
+ current_pos += sizeof(existing_entry);
+ continue;
+ }
+
+ // found it here, so overwrite
+ return (FileSystem::FSeek64(fp.get(), current_pos, SEEK_SET) == 0 &&
+ std::fwrite(&entry, sizeof(entry), 1, fp.get()) == 1);
+ }
+
+ if (FileSystem::FSeek64(fp.get(), current_pos, SEEK_SET) != 0)
+ return false;
+
+ // append it.
+ return (std::fwrite(&entry, sizeof(entry), 1, fp.get()) == 1);
+}
diff --git a/src/core/game_list.h b/src/core/game_list.h
index e74dce9b3..b2a393f19 100644
--- a/src/core/game_list.h
+++ b/src/core/game_list.h
@@ -130,6 +130,13 @@ void SaveCustomTitleForPath(const std::string& path, const std::string& custom_t
void SaveCustomRegionForPath(const std::string& path, const std::optional custom_region);
std::string GetCustomTitleForPath(const std::string_view path);
std::optional GetCustomRegionForPath(const std::string_view path);
+
+/// The purpose of this cache is to stop us trying to constantly extract memory card icons, when we know a game
+/// doesn't have any saves yet. It caches the serial:memcard_timestamp pair, and only tries extraction when the
+/// timestamp of the memory card has changed.
+std::string GetGameIconPath(std::string_view serial, std::string_view path);
+void ReloadMemcardTimestampCache();
+
}; // namespace GameList
namespace Host {
diff --git a/src/core/memory_card_icon_cache.cpp b/src/core/memory_card_icon_cache.cpp
deleted file mode 100644
index 9745d7add..000000000
--- a/src/core/memory_card_icon_cache.cpp
+++ /dev/null
@@ -1,215 +0,0 @@
-// SPDX-FileCopyrightText: 2024 Connor McLaughlin
-// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
-
-#include "memory_card_icon_cache.h"
-#include "system.h"
-
-#include "common/assert.h"
-#include "common/error.h"
-#include "common/file_system.h"
-#include "common/log.h"
-#include "common/path.h"
-#include "common/string_util.h"
-#include "common/timer.h"
-
-Log_SetChannel(MemoryCardImage);
-
-static constexpr const char EXPECTED_SIGNATURE[] = {'M', 'C', 'D', 'I', 'C', 'N', '0', '1'};
-
-static FileSystem::ManagedCFilePtr OpenCache(const std::string& filename, bool for_write)
-{
- const char* mode = for_write ? "r+b" : "rb";
- const FileSystem::FileShareMode share_mode =
- for_write ? FileSystem::FileShareMode::DenyReadWrite : FileSystem::FileShareMode::DenyWrite;
- FileSystem::ManagedCFilePtr fp = FileSystem::OpenManagedSharedCFile(filename.c_str(), mode, share_mode, nullptr);
- if (fp)
- return fp;
-
- // Doesn't exist? Create it.
- if (errno == ENOENT)
- {
- if (!for_write)
- return nullptr;
-
- mode = "w+b";
- fp = FileSystem::OpenManagedSharedCFile(filename.c_str(), mode, share_mode, nullptr);
- if (fp)
- return fp;
- }
-
- // If there's a sharing violation, try again for 100ms.
- if (errno != EACCES)
- return nullptr;
-
- Common::Timer timer;
- while (timer.GetTimeMilliseconds() <= 100.0f)
- {
- fp = FileSystem::OpenManagedSharedCFile(filename.c_str(), mode, share_mode, nullptr);
- if (fp)
- return fp;
-
- if (errno != EACCES)
- return nullptr;
- }
-
- ERROR_LOG("Timed out while trying to open memory card cache file.");
- return nullptr;
-}
-
-MemoryCardIconCache::MemoryCardIconCache(std::string filename) : m_filename(std::move(filename))
-{
-}
-
-MemoryCardIconCache::~MemoryCardIconCache() = default;
-
-bool MemoryCardIconCache::Reload()
-{
- m_entries.clear();
-
- FileSystem::ManagedCFilePtr fp = OpenCache(m_filename, false);
- if (!fp)
- return false;
-
-#ifndef _WIN32
- FileSystem::POSIXLock lock(fp.get());
-#endif
-
- const s64 file_size = FileSystem::FSize64(fp.get());
- if (file_size < static_cast(sizeof(EXPECTED_SIGNATURE)))
- return false;
-
- const size_t count = (static_cast(file_size) - sizeof(EXPECTED_SIGNATURE)) / sizeof(Entry);
- if (count <= 0)
- return false;
-
- char signature[sizeof(EXPECTED_SIGNATURE)];
- if (std::fread(signature, sizeof(signature), 1, fp.get()) != 1 ||
- std::memcmp(signature, EXPECTED_SIGNATURE, sizeof(signature)) != 0)
- {
- return false;
- }
-
- m_entries.resize(static_cast(count));
- if (std::fread(m_entries.data(), sizeof(Entry), m_entries.size(), fp.get()) != m_entries.size())
- {
- m_entries = {};
- return false;
- }
-
- // Just in case.
- for (Entry& entry : m_entries)
- entry.serial[sizeof(entry.serial) - 1] = 0;
-
- return true;
-}
-
-const MemoryCardImage::IconFrame* MemoryCardIconCache::Lookup(std::string_view serial, std::string_view path)
-{
- MemoryCardType type;
- std::string memcard_path = System::GetGameMemoryCardPath(serial, path, 0, &type);
- if (memcard_path.empty() || type == MemoryCardType::Shared)
- return nullptr;
-
- FILESYSTEM_STAT_DATA sd;
- if (!FileSystem::StatFile(memcard_path.c_str(), &sd))
- return nullptr;
-
- const s64 timestamp = sd.ModificationTime;
- TinyString index_serial;
- index_serial.assign(serial.substr(0, std::min(serial.length(), MAX_SERIAL_LENGTH - 1)));
-
- Entry* serial_entry = nullptr;
- for (Entry& entry : m_entries)
- {
- if (StringUtil::EqualNoCase(index_serial, entry.serial))
- {
- if (entry.memcard_timestamp == timestamp)
- return entry.is_valid ? &entry.icon : nullptr;
-
- serial_entry = &entry;
- break;
- }
- }
-
- if (!serial_entry)
- {
- serial_entry = &m_entries.emplace_back();
- std::memset(serial_entry, 0, sizeof(Entry));
- }
-
- serial_entry->is_valid = false;
- serial_entry->memcard_timestamp = timestamp;
- StringUtil::Strlcpy(serial_entry->serial, index_serial.view(), sizeof(serial_entry->serial));
- std::memset(serial_entry->icon.pixels, 0, sizeof(serial_entry->icon.pixels));
-
- MemoryCardImage::DataArray data;
- if (MemoryCardImage::LoadFromFile(&data, memcard_path.c_str()))
- {
- std::vector files = MemoryCardImage::EnumerateFiles(data, false);
- if (!files.empty())
- {
- const MemoryCardImage::FileInfo& fi = files.front();
- if (!fi.icon_frames.empty())
- {
- INFO_LOG("Extracted memory card icon from {} ({})", fi.filename, Path::GetFileTitle(memcard_path));
- std::memcpy(&serial_entry->icon, &fi.icon_frames.front(), sizeof(serial_entry->icon));
- serial_entry->is_valid = true;
- }
- }
- }
-
- UpdateInFile(*serial_entry);
- return serial_entry->is_valid ? &serial_entry->icon : nullptr;
-}
-
-bool MemoryCardIconCache::UpdateInFile(const Entry& entry)
-{
- FileSystem::ManagedCFilePtr fp = OpenCache(m_filename, true);
- if (!fp)
- return false;
-
-#ifndef _WIN32
- FileSystem::POSIXLock lock(fp.get());
-#endif
-
- // check signature, write it if it's non-existent or invalid
- char signature[sizeof(EXPECTED_SIGNATURE)];
- if (std::fread(signature, sizeof(signature), 1, fp.get()) != 1 ||
- std::memcmp(signature, EXPECTED_SIGNATURE, sizeof(signature)) != 0)
- {
- if (!FileSystem::FTruncate64(fp.get(), 0) || FileSystem::FSeek64(fp.get(), 0, SEEK_SET) != 0 ||
- std::fwrite(EXPECTED_SIGNATURE, sizeof(EXPECTED_SIGNATURE), 1, fp.get()) != 1)
- {
- return false;
- }
- }
-
- // need to seek to switch from read->write?
- s64 current_pos = sizeof(EXPECTED_SIGNATURE);
- if (FileSystem::FSeek64(fp.get(), current_pos, SEEK_SET) != 0)
- return false;
-
- for (;;)
- {
- Entry existing_entry;
- if (std::fread(&existing_entry, sizeof(existing_entry), 1, fp.get()) != 1)
- break;
-
- existing_entry.serial[sizeof(existing_entry.serial) - 1] = 0;
- if (!StringUtil::EqualNoCase(existing_entry.serial, entry.serial))
- {
- current_pos += sizeof(existing_entry);
- continue;
- }
-
- // found it here, so overwrite
- return (FileSystem::FSeek64(fp.get(), current_pos, SEEK_SET) == 0 &&
- std::fwrite(&entry, sizeof(entry), 1, fp.get()) == 1);
- }
-
- if (FileSystem::FSeek64(fp.get(), current_pos, SEEK_SET) != 0)
- return false;
-
- // append it.
- return (std::fwrite(&entry, sizeof(entry), 1, fp.get()) == 1);
-}
diff --git a/src/core/memory_card_icon_cache.h b/src/core/memory_card_icon_cache.h
deleted file mode 100644
index 885c73a87..000000000
--- a/src/core/memory_card_icon_cache.h
+++ /dev/null
@@ -1,39 +0,0 @@
-// SPDX-FileCopyrightText: 2024 Connor McLaughlin
-// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
-
-#pragma once
-
-#include "memory_card_image.h"
-
-class MemoryCardIconCache
-{
-public:
- MemoryCardIconCache(std::string filename);
- ~MemoryCardIconCache();
-
- bool Reload();
-
- // NOTE: Only valid within this call to lookup.
- const MemoryCardImage::IconFrame* Lookup(std::string_view serial, std::string_view path);
-
-private:
- enum : u32
- {
- MAX_SERIAL_LENGTH = 31,
- };
-
-#pragma pack(push, 1)
- struct Entry
- {
- char serial[MAX_SERIAL_LENGTH];
- bool is_valid;
- s64 memcard_timestamp;
- MemoryCardImage::IconFrame icon;
- };
-#pragma pack(pop)
-
- bool UpdateInFile(const Entry& entry);
-
- std::string m_filename;
- std::vector m_entries;
-};
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index fa9275092..f55ae230c 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -1726,6 +1726,7 @@ std::string EmuFolders::Cache;
std::string EmuFolders::Cheats;
std::string EmuFolders::Covers;
std::string EmuFolders::Dumps;
+std::string EmuFolders::GameIcons;
std::string EmuFolders::GameSettings;
std::string EmuFolders::InputProfiles;
std::string EmuFolders::MemoryCards;
@@ -1743,6 +1744,7 @@ void EmuFolders::SetDefaults()
Cheats = Path::Combine(DataRoot, "cheats");
Covers = Path::Combine(DataRoot, "covers");
Dumps = Path::Combine(DataRoot, "dump");
+ GameIcons = Path::Combine(DataRoot, "gameicons");
GameSettings = Path::Combine(DataRoot, "gamesettings");
InputProfiles = Path::Combine(DataRoot, "inputprofiles");
MemoryCards = Path::Combine(DataRoot, "memcards");
@@ -1772,6 +1774,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si)
Cheats = LoadPathFromSettings(si, DataRoot, "Folders", "Cheats", "cheats");
Covers = LoadPathFromSettings(si, DataRoot, "Folders", "Covers", "covers");
Dumps = LoadPathFromSettings(si, DataRoot, "Folders", "Dumps", "dump");
+ GameIcons = LoadPathFromSettings(si, DataRoot, "Folders", "GameIcons", "gameicons");
GameSettings = LoadPathFromSettings(si, DataRoot, "Folders", "GameSettings", "gamesettings");
InputProfiles = LoadPathFromSettings(si, DataRoot, "Folders", "InputProfiles", "inputprofiles");
MemoryCards = LoadPathFromSettings(si, DataRoot, "MemoryCards", "Directory", "memcards");
@@ -1786,6 +1789,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si)
DEV_LOG("Cheats Directory: {}", Cheats);
DEV_LOG("Covers Directory: {}", Covers);
DEV_LOG("Dumps Directory: {}", Dumps);
+ DEV_LOG("Game Icons Directory: {}", GameIcons);
DEV_LOG("Game Settings Directory: {}", GameSettings);
DEV_LOG("Input Profile Directory: {}", InputProfiles);
DEV_LOG("MemoryCards Directory: {}", MemoryCards);
@@ -1805,6 +1809,7 @@ void EmuFolders::Save(SettingsInterface& si)
si.SetStringValue("Folders", "Cheats", Path::MakeRelative(Cheats, DataRoot).c_str());
si.SetStringValue("Folders", "Covers", Path::MakeRelative(Covers, DataRoot).c_str());
si.SetStringValue("Folders", "Dumps", Path::MakeRelative(Dumps, DataRoot).c_str());
+ si.SetStringValue("Folders", "GameIcons", Path::MakeRelative(GameIcons, DataRoot).c_str());
si.SetStringValue("Folders", "GameSettings", Path::MakeRelative(GameSettings, DataRoot).c_str());
si.SetStringValue("Folders", "InputProfiles", Path::MakeRelative(InputProfiles, DataRoot).c_str());
si.SetStringValue("MemoryCards", "Directory", Path::MakeRelative(MemoryCards, DataRoot).c_str());
@@ -1846,6 +1851,7 @@ bool EmuFolders::EnsureFoldersExist()
result = FileSystem::EnsureDirectoryExists(Dumps.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(Path::Combine(Dumps, "audio").c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(Path::Combine(Dumps, "textures").c_str(), false) && result;
+ result = FileSystem::EnsureDirectoryExists(GameIcons.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(GameSettings.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(InputProfiles.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(MemoryCards.c_str(), false) && result;
diff --git a/src/core/settings.h b/src/core/settings.h
index 401e968aa..b2a4b4884 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -536,6 +536,7 @@ extern std::string Cache;
extern std::string Cheats;
extern std::string Covers;
extern std::string Dumps;
+extern std::string GameIcons;
extern std::string GameSettings;
extern std::string InputProfiles;
extern std::string MemoryCards;
diff --git a/src/duckstation-qt/gamelistmodel.cpp b/src/duckstation-qt/gamelistmodel.cpp
index 403dc5aa3..b2267a6fb 100644
--- a/src/duckstation-qt/gamelistmodel.cpp
+++ b/src/duckstation-qt/gamelistmodel.cpp
@@ -8,6 +8,7 @@
#include "core/system.h"
#include "common/file_system.h"
+#include "common/log.h"
#include "common/path.h"
#include "common/string_util.h"
@@ -20,8 +21,10 @@
#include
#include
+Log_SetChannel(GameList);
+
static constexpr std::array s_column_names = {
- {"Type", "Serial", "Title", "File Title", "Developer", "Publisher", "Genre", "Year", "Players", "Time Played",
+ {"Icon", "Serial", "Title", "File Title", "Developer", "Publisher", "Genre", "Year", "Players", "Time Played",
"Last Played", "Size", "File Size", "Region", "Compatibility", "Cover"}};
static constexpr int COVER_ART_WIDTH = 512;
@@ -29,11 +32,6 @@ static constexpr int COVER_ART_HEIGHT = 512;
static constexpr int COVER_ART_SPACING = 32;
static constexpr int MIN_COVER_CACHE_SIZE = 256;
-static std::string getMemoryCardIconCachePath()
-{
- return Path::Combine(EmuFolders::Cache, "memcard_icons.cache");
-}
-
static int DPRScale(int size, float dpr)
{
return static_cast(static_cast(size) * dpr);
@@ -125,14 +123,14 @@ const char* GameListModel::getColumnName(Column col)
GameListModel::GameListModel(float cover_scale, bool show_cover_titles, bool show_game_icons,
QObject* parent /* = nullptr */)
: QAbstractTableModel(parent), m_show_titles_for_covers(show_cover_titles), m_show_game_icons(show_game_icons),
- m_memcard_icon_cache(getMemoryCardIconCachePath()), m_memcard_pixmap_cache(128)
+ m_memcard_pixmap_cache(128)
{
loadCommonImages();
setCoverScale(cover_scale);
setColumnDisplayNames();
if (m_show_game_icons)
- m_memcard_icon_cache.Reload();
+ GameList::ReloadMemcardTimestampCache();
}
GameListModel::~GameListModel() = default;
@@ -144,7 +142,7 @@ void GameListModel::setShowGameIcons(bool enabled)
beginResetModel();
m_memcard_pixmap_cache.Clear();
if (enabled)
- m_memcard_icon_cache.Reload();
+ GameList::ReloadMemcardTimestampCache();
endResetModel();
}
@@ -249,7 +247,7 @@ QString GameListModel::formatTimespan(time_t timespan)
return qApp->translate("GameList", "%n minutes", "", minutes);
}
-const QPixmap& GameListModel::getPixmapForEntry(const GameList::Entry* ge) const
+const QPixmap& GameListModel::getIconPixmapForEntry(const GameList::Entry* ge) const
{
// We only do this for discs/disc sets for now.
if (m_show_game_icons && (!ge->serial.empty() && (ge->IsDisc() || ge->IsDiscSet())))
@@ -258,17 +256,13 @@ const QPixmap& GameListModel::getPixmapForEntry(const GameList::Entry* ge) const
if (item)
return *item;
- const MemoryCardImage::IconFrame* icon = m_memcard_icon_cache.Lookup(ge->serial, ge->path);
- if (icon)
- {
- const QImage image(reinterpret_cast(icon->pixels), MemoryCardImage::ICON_WIDTH,
- MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888);
- return *m_memcard_pixmap_cache.Insert(ge->serial, QPixmap::fromImage(image));
- }
+ // Assumes game list lock is held.
+ const std::string path = GameList::GetGameIconPath(ge->serial, ge->path);
+ QPixmap pm;
+ if (!path.empty() && pm.load(QString::fromStdString(path)))
+ return *m_memcard_pixmap_cache.Insert(ge->serial, std::move(pm));
else
- {
return *m_memcard_pixmap_cache.Insert(ge->serial, m_type_pixmaps[static_cast(ge->type)]);
- }
}
return m_type_pixmaps[static_cast(ge->type)];
@@ -286,11 +280,12 @@ QIcon GameListModel::getIconForGame(const QString& path)
// See above.
if (entry && !entry->serial.empty() && (entry->IsDisc() || entry->IsDiscSet()))
{
- const MemoryCardImage::IconFrame* icon = m_memcard_icon_cache.Lookup(entry->serial, entry->path);
- if (icon)
+ const std::string icon_path = GameList::GetGameIconPath(entry->serial, entry->path);
+ if (!icon_path.empty())
{
- ret = QIcon(QPixmap::fromImage(QImage(reinterpret_cast(icon->pixels), MemoryCardImage::ICON_WIDTH,
- MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888)));
+ QPixmap newpm;
+ if (!icon_path.empty() && newpm.load(QString::fromStdString(icon_path)))
+ ret = QIcon(*m_memcard_pixmap_cache.Insert(entry->serial, std::move(newpm)));
}
}
}
@@ -424,7 +419,7 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const
{
switch (index.column())
{
- case Column_Type:
+ case Column_Icon:
return static_cast(ge->GetSortType());
case Column_Serial:
@@ -479,9 +474,9 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const
{
switch (index.column())
{
- case Column_Type:
+ case Column_Icon:
{
- return getPixmapForEntry(ge);
+ return getIconPixmapForEntry(ge);
}
case Column_Region:
@@ -569,7 +564,7 @@ bool GameListModel::lessThan(const QModelIndex& left_index, const QModelIndex& r
switch (column)
{
- case Column_Type:
+ case Column_Icon:
{
const GameList::EntryType lst = left->GetSortType();
const GameList::EntryType rst = right->GetSortType();
@@ -717,7 +712,7 @@ void GameListModel::loadCommonImages()
void GameListModel::setColumnDisplayNames()
{
- m_column_display_names[Column_Type] = tr("Type");
+ m_column_display_names[Column_Icon] = tr("Icon");
m_column_display_names[Column_Serial] = tr("Serial");
m_column_display_names[Column_Title] = tr("Title");
m_column_display_names[Column_FileTitle] = tr("File Title");
diff --git a/src/duckstation-qt/gamelistmodel.h b/src/duckstation-qt/gamelistmodel.h
index b550596f7..f059c158e 100644
--- a/src/duckstation-qt/gamelistmodel.h
+++ b/src/duckstation-qt/gamelistmodel.h
@@ -5,7 +5,6 @@
#include "core/game_database.h"
#include "core/game_list.h"
-#include "core/memory_card_icon_cache.h"
#include "core/types.h"
#include "common/heterogeneous_containers.h"
@@ -24,7 +23,7 @@ class GameListModel final : public QAbstractTableModel
public:
enum Column : int
{
- Column_Type,
+ Column_Icon,
Column_Serial,
Column_Title,
Column_FileTitle,
@@ -83,13 +82,29 @@ Q_SIGNALS:
void coverScaleChanged();
private:
+ /// The purpose of this cache is to stop us trying to constantly extract memory card icons, when we know a game
+ /// doesn't have any saves yet. It caches the serial:memcard_timestamp pair, and only tries extraction when the
+ /// timestamp of the memory card has changed.
+#pragma pack(push, 1)
+ struct MemcardTimestampCacheEntry
+ {
+ enum : u32
+ {
+ MAX_SERIAL_LENGTH = 32,
+ };
+
+ char serial[MAX_SERIAL_LENGTH];
+ s64 memcard_timestamp;
+ };
+#pragma pack(pop)
+
void loadCommonImages();
void loadThemeSpecificImages();
void setColumnDisplayNames();
void loadOrGenerateCover(const GameList::Entry* ge);
void invalidateCoverForPath(const std::string& path);
- const QPixmap& getPixmapForEntry(const GameList::Entry* ge) const;
+ const QPixmap& getIconPixmapForEntry(const GameList::Entry* ge) const;
static QString formatTimespan(time_t timespan);
@@ -107,6 +122,5 @@ private:
mutable LRUCache m_cover_pixmap_cache;
- mutable MemoryCardIconCache m_memcard_icon_cache;
mutable LRUCache m_memcard_pixmap_cache;
};
diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp
index da44b2e1f..c03227fd8 100644
--- a/src/duckstation-qt/gamelistwidget.cpp
+++ b/src/duckstation-qt/gamelistwidget.cpp
@@ -632,7 +632,7 @@ void GameListWidget::saveTableViewColumnVisibilitySettings(int column)
void GameListWidget::loadTableViewColumnSortSettings()
{
- const GameListModel::Column DEFAULT_SORT_COLUMN = GameListModel::Column_Type;
+ const GameListModel::Column DEFAULT_SORT_COLUMN = GameListModel::Column_Icon;
const bool DEFAULT_SORT_DESCENDING = false;
const GameListModel::Column sort_column =