IconCache: Expire only avatars after a day, other icons after 30 days

Achievement badges and game icons don't change, so re-downloading them
daily was wasted. InsertIcon now takes a max age, and each entry saves
its expiry time. Leaderboard avatars get a day, everything else 30 days.
Bumps the cache version.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-30 12:28:35 -06:00
1 parent 70b046285c
commit 19c57faadc
5 files changed
+21 -18

No files matched your search

+9 -11
View File
@@ -9,8 +9,8 @@
#include "Common/GPU/thin3d.h"
#include "Common/File/FileUtil.h"
// 2: insertedTimestamp became wall-clock time.
#define ICON_CACHE_VERSION 2
// 3: Entries store a wall-clock expiry time.
#define ICON_CACHE_VERSION 3
#define MK_FOURCC(str) (str[0] | ((uint8_t)str[1] << 8) | ((uint8_t)str[2] << 16) | ((uint8_t)str[3] << 24))
#define MAX_RUNTIME_CACHE_SIZE (1024 * 1024 * 4)
@@ -18,8 +18,6 @@
// Seconds before MarkPending accepts a key whose download failed.
constexpr double FAILED_RETRY_DELAY = 30.0;
// Seconds a saved icon is kept across sessions, so that ones that change (user avatars) get refreshed.
constexpr double MAX_SAVED_ICON_AGE = 24 * 60 * 60.0;
// Seconds before BindIconTexture tries again to create a texture that failed.
constexpr double UPLOAD_RETRY_DELAY = 5.0;
@@ -38,7 +36,7 @@ struct DiskCacheEntry {
uint32_t dataLen;
IconFormat format;
uint32_t padding; // Explicit, so that 32-bit x86 Linux (which aligns double to 4) has the same layout.
double insertedTimestamp;
double expireTimestamp;
};
static_assert(sizeof(DiskCacheEntry) == 24, "DiskCacheEntry is written to disk as is");
@@ -75,7 +73,7 @@ void IconCache::SaveToFile(FILE *file) {
const auto &entry = iter.second;
entryHeader.dataLen = (uint32_t)entry.data.size();
entryHeader.format = entry.format;
entryHeader.insertedTimestamp = entry.insertedTimeStamp;
entryHeader.expireTimestamp = entry.expireTimeStamp;
fwrite(&entryHeader, 1, sizeof(entryHeader), file);
fwrite(iter.first.c_str(), 1, iter.first.size(), file);
fwrite(entry.data.data(), 1, entry.data.size(), file);
@@ -114,8 +112,8 @@ bool IconCache::LoadFromFile(FILE *file) {
break;
}
// Skip it if we already have the entry somehow, or it's too old.
if (cache_.find(key) != cache_.end() || nowUnix - entryHeader.insertedTimestamp > MAX_SAVED_ICON_AGE) {
// Skip it if we already have the entry somehow, or it has expired.
if (cache_.find(key) != cache_.end() || nowUnix > entryHeader.expireTimestamp) {
// Seek past the data and go to the next entry.
File::Fseek(file, entryHeader.dataLen, SEEK_CUR);
continue;
@@ -132,7 +130,7 @@ bool IconCache::LoadFromFile(FILE *file) {
Entry entry{};
entry.data = data;
entry.format = entryHeader.format;
entry.insertedTimeStamp = entryHeader.insertedTimestamp;
entry.expireTimeStamp = entryHeader.expireTimestamp;
entry.usedTimeStamp = now;
PeekIconSize(entry.data, entry.format, &entry.width, &entry.height);
cache_.emplace(key, entry);
@@ -295,7 +293,7 @@ void IconCache::CancelPending(std::string_view key) {
pending_.erase(iter);
}
bool IconCache::InsertIcon(std::string_view key, IconFormat format, std::string &&data) {
bool IconCache::InsertIcon(std::string_view key, IconFormat format, std::string &&data, double maxAge) {
if (key.empty()) {
return false;
}
@@ -323,7 +321,7 @@ bool IconCache::InsertIcon(std::string_view key, IconFormat format, std::string
}
double now = time_now_d();
Entry entry{ std::move(data), format, nullptr, time_now_unix_utc(), now };
Entry entry{ std::move(data), format, nullptr, time_now_unix_utc() + maxAge, now };
PeekIconSize(entry.data, entry.format, &entry.width, &entry.height);
cache_.emplace(key, std::move(entry));
return true;
+6 -2
View File
@@ -19,6 +19,10 @@ namespace Draw {
class Texture;
}
// How long an icon is kept in the saved cache, in seconds. It's downloaded again after that.
constexpr double ICON_MAX_AGE_DEFAULT = 30 * 24 * 60 * 60.0; // Most icons, like achievement badges, don't change.
constexpr double ICON_MAX_AGE_AVATAR = 24 * 60 * 60.0; // User avatars can change at any time.
// TODO: Possibly make this smarter and use instead of ManagedTexture?
struct IconCacheStats {
@@ -39,7 +43,7 @@ public:
void CancelPending(std::string_view key);
// Like CancelPending, but MarkPending refuses the key for a while, so a failing download isn't retried every frame.
void MarkFailed(std::string_view key);
bool InsertIcon(std::string_view key, IconFormat format, std::string &&pngData);
bool InsertIcon(std::string_view key, IconFormat format, std::string &&pngData, double maxAge = ICON_MAX_AGE_DEFAULT);
bool GetDimensions(std::string_view key, int *width, int *height);
bool Contains(std::string_view key);
@@ -60,7 +64,7 @@ private:
std::string data;
IconFormat format;
Draw::Texture *texture;
double insertedTimeStamp; // Wall-clock (time_now_unix_utc), since it's saved.
double expireTimeStamp; // Wall-clock (time_now_unix_utc), since it's saved.
double usedTimeStamp;
double uploadFailedTime; // When CreateTexture last failed, or 0.
int width; // From the image header, 0 if unknown.
+3 -3
View File
@@ -1061,11 +1061,11 @@ bool HasAchievementsOrLeaderboards() {
return IsActive();
}
void DownloadImageIfMissing(std::string_view url) {
void DownloadImageIfMissing(std::string_view url, double maxAge) {
// On Linux for example, we currently have no way of doing a HTTPS request.
if (g_iconCache.MarkPending(url)) {
INFO_LOG(Log::Achievements, "Downloading image: %.*s", STR_VIEW(url));
g_DownloadManager.StartDownload(url, Path(), http::RequestFlags::Default, nullptr, "", [](http::Request &download) {
g_DownloadManager.StartDownload(url, Path(), http::RequestFlags::Default, nullptr, "", [maxAge](http::Request &download) {
std::string data;
if (download.ResultCode() == 200) {
download.buffer().TakeAll(&data);
@@ -1075,7 +1075,7 @@ void DownloadImageIfMissing(std::string_view url) {
g_iconCache.MarkFailed(download.url());
return;
}
g_iconCache.InsertIcon(download.url(), IconFormat::PNG, std::move(data));
g_iconCache.InsertIcon(download.url(), IconFormat::PNG, std::move(data), maxAge);
});
}
}
+2 -1
View File
@@ -13,6 +13,7 @@
#include <set>
#include "Common/CommonTypes.h"
#include "Common/UI/IconCache.h" // for ICON_MAX_AGE_DEFAULT
#include "Core/Loaders.h" // for IdentifiedFileType
class Path;
@@ -82,7 +83,7 @@ bool HasToken();
/// Pass false only when pending requests are about to be cancelled, as at app exit.
bool Shutdown(bool waitForRequests = true);
void DownloadImageIfMissing(std::string_view url);
void DownloadImageIfMissing(std::string_view url, double maxAge = ICON_MAX_AGE_DEFAULT);
/// Called once a frame at vsync time on the CPU thread, during gameplay.
void FrameUpdate();
+1 -1
View File
@@ -796,7 +796,7 @@ static void RenderLeaderboardEntry(UIContext &dc, const rc_client_leaderboard_en
char userImageUrl[512];
if (RC_OK == rc_client_leaderboard_entry_get_user_image_url(entry, userImageUrl, sizeof(userImageUrl))) {
std::string imageUrl = http::RemoveHttpsIfNeeded(userImageUrl);
Achievements::DownloadImageIfMissing(imageUrl);
Achievements::DownloadImageIfMissing(imageUrl, ICON_MAX_AGE_AVATAR);
if (g_iconCache.BindIconTexture(&dc, imageUrl)) {
dc.Draw()->DrawTexRect(Bounds(bounds.x + iconLeft, bounds.y + 4.0f, 64.0f, 64.0f), 0.0f, 0.0f, 1.0f, 1.0f, whiteAlpha(alpha));
}