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 =