@@ -132,11 +132,15 @@ static void UpdateModeSettings(const Settings& old_config);
132132static DynamicHeapArray<u8 > SaveStateToBuffer ();
133133static void LoadStateFromBuffer (std::span<const u8 > data, std::unique_lock<std::recursive_mutex>& lock);
134134static bool SaveStateToBuffer (std::span<u8 > data);
135+ static std::string GetAchievementBadgeURL (const rc_client_achievement_t * achievement, u32 image_type);
135136static std::string GetImageURL (const char * image_name, u32 type);
136137static std::string GetLocalImagePath (const std::string_view image_name, u32 type);
137138static void DownloadImage (std::string url, std::string cache_path);
138139static void PrefetchNextAchievementBadge ();
139140static void PrefetchNextAchievementBadge (const rc_client_achievement_t * const last_cheevo);
141+ static void PrefetchAllAchievementBadges ();
142+ static void SendNextPrefetchBadgeRequest ();
143+ static void ClearPrefetchBadgeRequests ();
140144
141145static TinyString DecryptLoginToken (std::string_view encrypted_token, std::string_view username);
142146static TinyString EncryptLoginToken (std::string_view token, std::string_view username);
@@ -256,6 +260,8 @@ struct State
256260 rc_client_all_user_progress_t * fetch_all_progress_result = nullptr ;
257261 rc_client_async_handle_t * refresh_all_progress_request = nullptr ;
258262
263+ std::vector<std::pair<std::string, std::string>> prefetch_badge_requests; // (path, url)
264+
259265#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
260266 rc_client_async_handle_t * load_raintegration_request = nullptr ;
261267 bool using_raintegration = false ;
@@ -504,6 +510,93 @@ void Achievements::PrefetchNextAchievementBadge(const rc_client_achievement_t* c
504510 GetAchievementBadgePath (next_cheevo, false );
505511}
506512
513+ void Achievements::PrefetchAllAchievementBadges ()
514+ {
515+ static constexpr u32 PREFETCH_IMAGE_TYPE = RC_IMAGE_TYPE_ACHIEVEMENT;
516+
517+ // This is here so that we can hopefully avoid the delay in downloading the badge image on unlock.
518+ if (!HasAchievements ())
519+ return ;
520+
521+ rc_client_achievement_list_t * const achievements =
522+ rc_client_create_achievement_list (Achievements::GetClient (), RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL,
523+ RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE);
524+ if (!achievements)
525+ return ;
526+
527+ for (u32 i = 0 ; i < achievements->num_buckets ; i++)
528+ {
529+ // Ignore unlocked achievements, since we're not going to be showing a notification for them.
530+ const rc_client_achievement_bucket_t & bucket = achievements->buckets [i];
531+ if (bucket.bucket_type != RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED)
532+ continue ;
533+
534+ for (u32 j = 0 ; j < bucket.num_achievements ; j++)
535+ {
536+ const rc_client_achievement_t * const cheevo = bucket.achievements [j];
537+ std::string path = GetLocalImagePath (cheevo->badge_name , PREFETCH_IMAGE_TYPE);
538+ if (path.empty () || FileSystem::FileExists (path.c_str ()))
539+ continue ;
540+
541+ std::string url = GetAchievementBadgeURL (cheevo, PREFETCH_IMAGE_TYPE);
542+ VERBOSE_LOG (" Prefetching badge for locked achievement '{}' ({})" , cheevo->title , cheevo->badge_url );
543+ s_state.prefetch_badge_requests .emplace_back (std::move (path), std::move (url));
544+ }
545+ }
546+ rc_client_destroy_achievement_list (achievements);
547+ if (s_state.prefetch_badge_requests .empty ())
548+ return ;
549+
550+ // reverse the list, fetch the first achievement first since it's the most likely to be unlocked next
551+ std::ranges::reverse (s_state.prefetch_badge_requests );
552+ SendNextPrefetchBadgeRequest ();
553+ }
554+
555+ void Achievements::SendNextPrefetchBadgeRequest ()
556+ {
557+ if (s_state.prefetch_badge_requests .empty ())
558+ return ;
559+
560+ std::string cache_path = std::move (s_state.prefetch_badge_requests .back ().first );
561+ std::string url = std::move (s_state.prefetch_badge_requests .back ().second );
562+ s_state.prefetch_badge_requests .pop_back ();
563+
564+ // free memory when done
565+ if (s_state.prefetch_badge_requests .empty ())
566+ s_state.prefetch_badge_requests = {};
567+
568+ auto callback = [cache_path = std::move (cache_path)](s32 status_code, const Error& error,
569+ const std::string& content_type,
570+ HTTPDownloader::Request::Data data) mutable {
571+ if (status_code != HTTPDownloader::HTTP_STATUS_OK)
572+ {
573+ ERROR_LOG (" Failed to download badge '{}': {}" , Path::GetFileName (cache_path), error.GetDescription ());
574+ return ;
575+ }
576+
577+ Error write_error;
578+ if (!FileSystem::WriteBinaryFile (cache_path.c_str (), data, &write_error))
579+ {
580+ ERROR_LOG (" Failed to write badge image to '{}': {}" , cache_path, write_error.GetDescription ());
581+ return ;
582+ }
583+
584+ VideoThread::RunOnThread (
585+ [cache_path = std::move (cache_path)]() { FullscreenUI::InvalidateCachedTexture (cache_path); });
586+
587+ SendNextPrefetchBadgeRequest ();
588+ };
589+
590+ s_state.http_downloader ->CreateRequest (std::move (url), std::move (callback));
591+ if (!s_state.prefetch_badge_requests .empty ())
592+ VERBOSE_LOG (" {} badge requests remaining" , s_state.prefetch_badge_requests .size ());
593+ }
594+
595+ void Achievements::ClearPrefetchBadgeRequests ()
596+ {
597+ s_state.prefetch_badge_requests = {};
598+ }
599+
507600bool Achievements::IsActive ()
508601{
509602 return (s_state.client != nullptr );
@@ -1130,6 +1223,8 @@ void Achievements::GameChanged(CDImage* image)
11301223 if (!IdentifyGame (image))
11311224 return ;
11321225
1226+ ClearPrefetchBadgeRequests ();
1227+
11331228 // cancel previous requests
11341229 if (s_state.load_game_request )
11351230 {
@@ -1200,6 +1295,9 @@ void Achievements::BeginLoadGame()
12001295 return ;
12011296 }
12021297
1298+ // Clear prefetch requests, since if we're loading state we'll get blocked until they all download otherwise.
1299+ ClearPrefetchBadgeRequests ();
1300+
12031301 s_state.load_game_request = rc_client_begin_load_game (s_state.client , GameHashToString (s_state.game_hash ).c_str (),
12041302 ClientLoadGameCallback, nullptr );
12051303}
@@ -1294,7 +1392,12 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message,
12941392
12951393 // update progress database on first load, in case it was played on another PC
12961394 UpdateGameSummary (true );
1297- PrefetchNextAchievementBadge ();
1395+
1396+ // Defer starting the prefetch, because otherwise when loading state we'll block until it's all downloaded.
1397+ if (g_settings.achievements_prefetch_badges )
1398+ Host::RunOnCoreThread (&Achievements::PrefetchAllAchievementBadges);
1399+ else
1400+ PrefetchNextAchievementBadge ();
12981401
12991402 // needed for notifications
13001403 SoundEffectManager::EnsureInitialized ();
@@ -1307,6 +1410,8 @@ void Achievements::ClearGameInfo()
13071410{
13081411 FullscreenUI::ClearAchievementsState ();
13091412
1413+ ClearPrefetchBadgeRequests ();
1414+
13101415 if (s_state.load_game_request )
13111416 {
13121417 rc_client_abort_async (s_state.client , s_state.load_game_request );
@@ -1958,30 +2063,40 @@ bool Achievements::DoState(StateWrapper& sw)
19582063 }
19592064}
19602065
2066+ std::string Achievements::GetAchievementBadgeURL (const rc_client_achievement_t * achievement, u32 image_type)
2067+ {
2068+ std::string url;
2069+ const char * url_ptr;
2070+
2071+ // RAIntegration doesn't set the URL fields.
2072+ if (IsUsingRAIntegration () ||
2073+ !(url_ptr =
2074+ (image_type == RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED) ? achievement->badge_locked_url : achievement->badge_url ))
2075+ {
2076+ return GetImageURL (achievement->badge_name , image_type);
2077+ }
2078+ else
2079+ {
2080+ return std::string (url_ptr);
2081+ }
2082+ }
2083+
19612084std::string Achievements::GetAchievementBadgePath (const rc_client_achievement_t * achievement, bool locked,
19622085 bool download_if_missing)
19632086{
19642087 const u32 image_type = locked ? RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED : RC_IMAGE_TYPE_ACHIEVEMENT;
19652088 const std::string path = GetLocalImagePath (achievement->badge_name , image_type);
19662089 if (download_if_missing && !path.empty () && !FileSystem::FileExists (path.c_str ()))
19672090 {
1968- std::string url;
1969- const char * url_ptr;
1970-
1971- // RAIntegration doesn't set the URL fields.
1972- if (IsUsingRAIntegration () || !(url_ptr = locked ? achievement->badge_locked_url : achievement->badge_url ))
1973- url = GetImageURL (achievement->badge_name , image_type);
1974- else
1975- url = std::string (url_ptr);
1976-
2091+ std::string url = GetAchievementBadgeURL (achievement, image_type);
19772092 if (url.empty ()) [[unlikely]]
19782093 {
19792094 ReportFmtError (" Achievement {} with badge name {} has no badge URL" , achievement->id , achievement->badge_name );
19802095 }
19812096 else
19822097 {
19832098 DEV_LOG (" Downloading badge for achievement {} from URL: {}" , achievement->id , url);
1984- DownloadImage (std::string (url), path);
2099+ DownloadImage (std::move (url), path);
19852100 }
19862101 }
19872102
0 commit comments