diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 02c661d31..23582ad21 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -79,6 +79,8 @@ 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 ee74c8b36..19a94e244 100644
--- a/src/core/core.vcxproj
+++ b/src/core/core.vcxproj
@@ -62,6 +62,7 @@
+
@@ -143,6 +144,7 @@
+
diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters
index b02bcfd85..72810d421 100644
--- a/src/core/core.vcxproj.filters
+++ b/src/core/core.vcxproj.filters
@@ -68,6 +68,7 @@
+
@@ -142,5 +143,6 @@
+
\ No newline at end of file
diff --git a/src/core/memory_card_icon_cache.cpp b/src/core/memory_card_icon_cache.cpp
new file mode 100644
index 000000000..aa7c80a57
--- /dev/null
+++ b/src/core/memory_card_icon_cache.cpp
@@ -0,0 +1,214 @@
+// 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)
+{
+ std::string memcard_path = System::GetGameMemoryCardPath(serial, path, 0);
+ if (memcard_path.empty())
+ 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
new file mode 100644
index 000000000..885c73a87
--- /dev/null
+++ b/src/core/memory_card_icon_cache.h
@@ -0,0 +1,39 @@
+// 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;
+};