GameList: Add dedicated gameicons directory
This commit is contained in:
parent
2ac2ad605e
commit
7c45ad3fed
|
@ -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
|
||||
|
|
|
@ -62,7 +62,6 @@
|
|||
<ClCompile Include="justifier.cpp" />
|
||||
<ClCompile Include="mdec.cpp" />
|
||||
<ClCompile Include="memory_card.cpp" />
|
||||
<ClCompile Include="memory_card_icon_cache.cpp" />
|
||||
<ClCompile Include="memory_card_image.cpp" />
|
||||
<ClCompile Include="multitap.cpp" />
|
||||
<ClCompile Include="guncon.cpp" />
|
||||
|
@ -144,7 +143,6 @@
|
|||
<ClInclude Include="justifier.h" />
|
||||
<ClInclude Include="mdec.h" />
|
||||
<ClInclude Include="memory_card.h" />
|
||||
<ClInclude Include="memory_card_icon_cache.h" />
|
||||
<ClInclude Include="memory_card_image.h" />
|
||||
<ClInclude Include="multitap.h" />
|
||||
<ClInclude Include="guncon.h" />
|
||||
|
|
|
@ -68,7 +68,6 @@
|
|||
<ClCompile Include="justifier.cpp" />
|
||||
<ClCompile Include="pine_server.cpp" />
|
||||
<ClCompile Include="gdb_server.cpp" />
|
||||
<ClCompile Include="memory_card_icon_cache.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="types.h" />
|
||||
|
@ -143,6 +142,5 @@
|
|||
<ClInclude Include="justifier.h" />
|
||||
<ClInclude Include="pine_server.h" />
|
||||
<ClInclude Include="gdb_server.h" />
|
||||
<ClInclude Include="memory_card_icon_cache.h" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -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<Entry>;
|
||||
|
@ -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<GameList::Entry> s_entries;
|
||||
static std::recursive_mutex s_mutex;
|
||||
static GameList::CacheMap s_cache_map;
|
||||
static std::unique_ptr<ByteStream> s_cache_write_stream;
|
||||
static std::vector<GameList::MemcardTimestampCacheEntry> s_memcard_timestamp_cache_entries;
|
||||
|
||||
static bool s_game_list_loaded = false;
|
||||
|
||||
|
@ -1629,3 +1649,222 @@ std::optional<DiscRegion> 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<s64>(sizeof(MEMCARD_TIMESTAMP_CACHE_SIGNATURE)))
|
||||
return;
|
||||
|
||||
const size_t count =
|
||||
(static_cast<size_t>(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<size_t>(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<size_t>(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<MemoryCardImage::FileInfo> 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);
|
||||
}
|
||||
|
|
|
@ -130,6 +130,13 @@ void SaveCustomTitleForPath(const std::string& path, const std::string& custom_t
|
|||
void SaveCustomRegionForPath(const std::string& path, const std::optional<DiscRegion> custom_region);
|
||||
std::string GetCustomTitleForPath(const std::string_view path);
|
||||
std::optional<DiscRegion> 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 {
|
||||
|
|
|
@ -1,215 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2024 Connor McLaughlin <stenzek@gmail.com>
|
||||
// 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<s64>(sizeof(EXPECTED_SIGNATURE)))
|
||||
return false;
|
||||
|
||||
const size_t count = (static_cast<size_t>(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<size_t>(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<size_t>(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<MemoryCardImage::FileInfo> 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);
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2024 Connor McLaughlin <stenzek@gmail.com>
|
||||
// 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<Entry> m_entries;
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 <QtGui/QIcon>
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
Log_SetChannel(GameList);
|
||||
|
||||
static constexpr std::array<const char*, GameListModel::Column_Count> 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<int>(static_cast<float>(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<const uchar*>(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<u32>(ge->type)]);
|
||||
}
|
||||
}
|
||||
|
||||
return m_type_pixmaps[static_cast<u32>(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<const uchar*>(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<int>(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");
|
||||
|
|
|
@ -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<std::string, QPixmap> m_cover_pixmap_cache;
|
||||
|
||||
mutable MemoryCardIconCache m_memcard_icon_cache;
|
||||
mutable LRUCache<std::string, QPixmap> m_memcard_pixmap_cache;
|
||||
};
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Reference in New Issue