diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index 8deb14ffc..8e106b11b 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -108,7 +108,7 @@ template static void ReportRCError(int err, fmt::format_string fmt, T&&... args); static void ClearGameInfo(); static bool TryLoggingInWithToken(); -static void EnableHardcoreMode(bool display_message, bool display_game_summary); +static void SetHardcoreModeState(bool enabled, bool display_message, bool display_game_summary); static void OnHardcoreModeChanged(bool enabled, bool display_message, bool display_game_summary); static bool IsRAIntegrationInitializing(); static void FinishInitialize(); @@ -117,7 +117,7 @@ static void BeginLoadGame(); static void UpdateGameSummary(); static void UpdateModeSettings(const Settings& old_config); static DynamicHeapArray SaveStateToBuffer(); -static void LoadStateFromBuffer(std::span data, std::unique_lock& lock); +static void LoadStateFromBuffer(std::span data, std::unique_lock& lock); static bool SaveStateToBuffer(std::span data); static std::string GetImageURL(const char* image_name, u32 type); static void PrefetchNextAchievementBadge(); @@ -128,14 +128,17 @@ static void UpdatePrefetchAchievementBadgesOSDMessage(); static TinyString DecryptLoginToken(std::string_view encrypted_token, std::string_view username); static TinyString EncryptLoginToken(std::string_view token, std::string_view username); -static bool CreateClient(std::unique_lock& lock, bool is_temporary_client); -static void DestroyClient(std::unique_lock& lock); +static bool CreateClient(std::unique_lock& lock, bool is_temporary_client); +static void DestroyClient(std::unique_lock& lock); static void ClientMessageCallback(const char* message, const rc_client_t* client); static uint32_t ClientReadMemory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); static void ClientServerCall(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); static rc_api_server_response_t MakeRCAPIServerResponse(s32 status_code, const std::vector& data); -static void WaitForServerCallsWithYield(std::unique_lock& lock); + +// NOTE: Should be called **without** lock held. +static void WaitForServerCalls(); +static void WaitForServerCallsWithYield(std::unique_lock& lock); static void ClientEventHandler(const rc_client_event_t* event, rc_client_t* client); static void HandleResetEvent(const rc_client_event_t* event); @@ -164,7 +167,7 @@ static void ClientLoadGameCallback(int result, const char* error_message, rc_cli static void DisplayHardcoreDeferredMessage(); static void DisplayAchievementSummary(); -static void UpdateRichPresence(std::unique_lock& lock); +static void UpdateRichPresence(std::unique_lock& lock); static bool EnsureAchievementsDatabaseOpen(Error* error = nullptr); static void CloseAchievementsDatabase(); @@ -200,7 +203,7 @@ static void SetAchievementPinnedInDatabase(u32 achievement_id, bool pinned); #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION static void BeginLoadRAIntegration(); -static void UnloadRAIntegration(std::unique_lock& lock); +static void UnloadRAIntegration(std::unique_lock& lock); // Just to make things even more annoying, RAIntegration callbacks on a worker thread. static void RAIntegrationClientLoginWithTokenCallback(int result, const char* error_message, rc_client_t* client, @@ -236,7 +239,7 @@ struct State bool has_saved_credentials : 1 = false; bool reload_game_on_reset : 1 = false; - std::recursive_mutex mutex; // large + Threading::Mutex mutex; std::vector active_leaderboard_trackers; std::vector active_challenge_indicators; @@ -304,7 +307,7 @@ TinyString Achievements::GameHashToString(const std::optional& hash) return ret; } -std::unique_lock Achievements::GetLock() +std::unique_lock Achievements::GetLock() { return std::unique_lock(s_state.mutex); } @@ -489,11 +492,10 @@ bool Achievements::IsActive() bool Achievements::IsHardcoreModeActive() { - if (!s_state.client) - return false; - - const auto lock = GetLock(); - return rc_client_get_hardcore_enabled(s_state.client); + // Does not take the lock here because we only call on the core thread. + // This is because it is called in other places like cheats which can have a reload triggered by HC mode. + DebugAssert(Host::IsOnCoreThread()); + return (s_state.client && rc_client_get_hardcore_enabled(s_state.client)); } bool Achievements::HasActiveGame() @@ -564,7 +566,7 @@ void Achievements::Initialize() CreateClient(lock, false); } -bool Achievements::CreateClient(std::unique_lock& lock, bool is_temporary_client) +bool Achievements::CreateClient(std::unique_lock& lock, bool is_temporary_client) { Assert(!s_state.client); @@ -641,13 +643,16 @@ void Achievements::FinishInitialize() Host::OnAchievementsActiveChanged(true); } -void Achievements::DestroyClient(std::unique_lock& lock) +void Achievements::DestroyClient(std::unique_lock& lock) { DebugAssert(IsActive()); - WaitForServerCallsWithYield(lock); + + lock.unlock(); + WaitForServerCalls(); + lock.lock(); ClearGameInfo(); - DisableHardcoreMode(false, false); + SetHardcoreModeState(false, false, false); CancelFetchGameListRequest(); CancelFetchAllProgressRequest(); @@ -712,10 +717,10 @@ bool Achievements::TryLoggingInWithToken() void Achievements::UpdateSettings(const Settings& old_config) { - auto lock = GetLock(); - if (g_settings.achievements_enabled != old_config.achievements_enabled) { + auto lock = GetLock(); + // we're done here if (g_settings.achievements_enabled) { @@ -731,10 +736,14 @@ void Achievements::UpdateSettings(const Settings& old_config) return; } + if (!IsActive()) + return; + #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION if (g_settings.achievements_use_raintegration != old_config.achievements_use_raintegration) { // RAIntegration requires a full client reload? + auto lock = GetLock(); if (IsActive()) DestroyClient(lock); CreateClient(lock, false); @@ -746,33 +755,46 @@ void Achievements::UpdateSettings(const Settings& old_config) { // Enables have to wait for reset, disables can go through immediately. if (!g_settings.achievements_hardcore_mode) - DisableHardcoreMode(true, true); + { + const auto lock = GetLock(); + SetHardcoreModeState(false, true, true); + } } if (!g_settings.achievements_leaderboard_trackers) + { + const auto lock = GetLock(); s_state.active_leaderboard_trackers.clear(); + } // remove progress indicator because it won't remove normally if (g_settings.achievements_progress_indicator_mode == AchievementProgressIndicatorMode::Disabled) + { + const auto lock = GetLock(); s_state.active_progress_indicator.reset(); + } // If a game is active and these settings changed, reload the game to apply them. // Just unload and reload without destroying the client to preserve hardcore mode. // NOTE: Can't change spectator mode while game is loaded. - if (HasActiveGame() && (g_settings.achievements_encore_mode != old_config.achievements_encore_mode || - g_settings.achievements_spectator_mode != old_config.achievements_spectator_mode || - g_settings.achievements_track_unofficial != old_config.achievements_track_unofficial)) - { - // Save and restore state to preserve progress. - const DynamicHeapArray state_data = SaveStateToBuffer(); - ClearGameInfo(); - UpdateModeSettings(old_config); - BeginLoadGame(); - LoadStateFromBuffer(state_data.cspan(), lock); - } - else + if (g_settings.achievements_encore_mode != old_config.achievements_encore_mode || + g_settings.achievements_spectator_mode != old_config.achievements_spectator_mode || + g_settings.achievements_track_unofficial != old_config.achievements_track_unofficial) { - UpdateModeSettings(old_config); + auto lock = GetLock(); + if (HasActiveGame()) + { + // Save and restore state to preserve progress. + const DynamicHeapArray state_data = SaveStateToBuffer(); + ClearGameInfo(); + UpdateModeSettings(old_config); + BeginLoadGame(); + LoadStateFromBuffer(state_data.cspan(), lock); + } + else + { + UpdateModeSettings(old_config); + } } } @@ -893,10 +915,17 @@ rc_api_server_response_t Achievements::MakeRCAPIServerResponse(s32 status_code, } } -void Achievements::WaitForServerCallsWithYield(std::unique_lock& lock) +void Achievements::WaitForServerCalls() +{ + HTTPDownloader::WaitForAllRequestsFromOwner(&s_state); +} + +void Achievements::WaitForServerCallsWithYield(std::unique_lock& lock) { - HTTPDownloader::WaitForAllRequestsFromOwnerWithYield( - &s_state, [&lock]() { lock.unlock(); }, [&lock]() { lock.lock(); }); + DebugAssert(lock.owns_lock()); + lock.unlock(); + WaitForServerCalls(); + lock.lock(); } void Achievements::IdleUpdate() @@ -1010,7 +1039,7 @@ void Achievements::UpdateGameSummary() rc_client_get_user_game_summary(s_state.client, &s_state.game_summary); } -void Achievements::UpdateRichPresence(std::unique_lock& lock) +void Achievements::UpdateRichPresence(std::unique_lock& lock) { if (!s_state.has_rich_presence) return; @@ -1061,7 +1090,7 @@ void Achievements::OnSystemStarting(bool disable_hardcore_mode) { // only enable hardcore mode if we're logged in, or waiting for a login response if (!disable_hardcore_mode && g_settings.achievements_hardcore_mode && IsLoggedInOrLoggingIn()) - EnableHardcoreMode(false, false); + SetHardcoreModeState(true, false, false); } } @@ -1085,7 +1114,7 @@ void Achievements::OnSystemDestroyed() if (IsActive()) { ClearGameInfo(); - DisableHardcoreMode(false, false); + SetHardcoreModeState(false, false, false); } } @@ -1101,7 +1130,7 @@ void Achievements::OnSystemReset() { // This will raise the silly reset event, but we can safely ignore that since we're immediately resetting the client DEV_LOG("Enabling hardcore mode after reset"); - EnableHardcoreMode(true, true); + SetHardcoreModeState(true, true, true); } DEV_LOG("Reset client"); @@ -1129,9 +1158,11 @@ void Achievements::SetGameHash(const std::optional& hash) INFO_COLOR_LOG(StrongOrange, "RA Hash: {}", GameHashToString(s_state.game_hash)); // just set the hash if inactive - if (!IsActive() || IsRAIntegrationInitializing()) + if (!IsActive()) + return; + if (IsRAIntegrationInitializing()) { - DisableHardcoreMode(false, false); + SetHardcoreModeState(false, false, false); return; } @@ -1156,7 +1187,6 @@ void Achievements::SetGameHash(const std::optional& hash) std::optional Achievements::GetGameHash() { - const auto lock = GetLock(); return s_state.game_hash; } @@ -1165,7 +1195,7 @@ void Achievements::BeginLoadGame() if (!s_state.game_hash.has_value()) { // no need to go through ClientLoadGameCallback, just bail out straight away - DisableHardcoreMode(false, false); + SetHardcoreModeState(false, false, false); return; } @@ -1186,7 +1216,7 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, if (was_disc_change) ClearGameInfo(); - DisableHardcoreMode(false, false); + SetHardcoreModeState(false, false, false); return; } else if (result == RC_LOGIN_REQUIRED) @@ -1209,7 +1239,7 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, if (was_disc_change) ClearGameInfo(); - DisableHardcoreMode(false, false); + SetHardcoreModeState(false, false, false); return; } @@ -1220,7 +1250,7 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, if (was_disc_change) ClearGameInfo(); - DisableHardcoreMode(false, false); + SetHardcoreModeState(false, false, false); return; } @@ -1236,7 +1266,7 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, if (!has_achievements && !has_leaderboards) { WARNING_LOG("Game '{}' has no achievements or leaderboards, disabling hardcore mode.", info->title); - DisableHardcoreMode(false, false); + SetHardcoreModeState(false, false, false); } s_state.game_id = info->id; @@ -1314,6 +1344,8 @@ void Achievements::DisplayAchievementSummary() { if (g_settings.achievements_notifications) { + const bool hardcore_enabled = rc_client_get_hardcore_enabled(s_state.client); + SmallString summary; if (s_state.game_summary.num_core_achievements > 0) { @@ -1327,7 +1359,7 @@ void Achievements::DisplayAchievementSummary() s_state.game_summary.points_unlocked)); summary.append('\n'); - if (IsHardcoreModeActive()) + if (hardcore_enabled) { summary.append( TRANSLATE_SV("Achievements", "Hardcore mode is enabled. Cheats and save states are unavailable.")); @@ -1343,8 +1375,8 @@ void Achievements::DisplayAchievementSummary() } FullscreenUI::AddAchievementNotification("AchievementsSummary", - IsHardcoreModeActive() ? ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME_HC : - ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME, + hardcore_enabled ? ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME_HC : + ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME, s_state.game_badge_url, s_state.game_title, std::string(summary), RA_LOGO_ICON_NAME, FullscreenUI::AchievementNotificationNoteType::Image); @@ -1793,14 +1825,15 @@ void Achievements::HandleServerReconnectedEvent(const rc_client_event_t* event) TRANSLATE_STR("Achievements", "All pending unlock requests have completed.")); } -void Achievements::EnableHardcoreMode(bool display_message, bool display_game_summary) +void Achievements::SetHardcoreModeState(bool enabled, bool display_message, bool display_game_summary) { DebugAssert(IsActive()); - if (rc_client_get_hardcore_enabled(s_state.client)) + + if ((rc_client_get_hardcore_enabled(s_state.client) != 0) == enabled) return; - rc_client_set_hardcore_enabled(s_state.client, true); - OnHardcoreModeChanged(true, display_message, display_game_summary); + rc_client_set_hardcore_enabled(s_state.client, enabled); + OnHardcoreModeChanged(enabled, display_message, display_game_summary); } void Achievements::DisableHardcoreMode(bool show_message, bool display_game_summary) @@ -1809,11 +1842,7 @@ void Achievements::DisableHardcoreMode(bool show_message, bool display_game_summ return; const auto lock = GetLock(); - if (!rc_client_get_hardcore_enabled(s_state.client)) - return; - - rc_client_set_hardcore_enabled(s_state.client, false); - OnHardcoreModeChanged(false, show_message, display_game_summary); + SetHardcoreModeState(false, show_message, display_game_summary); } void Achievements::OnHardcoreModeChanged(bool enabled, bool display_message, bool display_game_summary) @@ -1858,21 +1887,26 @@ void Achievements::OnHardcoreModeChanged(bool enabled, bool display_message, boo Host::OnAchievementsHardcoreModeChanged(enabled); } -void Achievements::LoadStateFromBuffer(std::span data, std::unique_lock& lock) +void Achievements::LoadStateFromBuffer(std::span data, std::unique_lock& lock) { // if we're active, make sure we've downloaded and activated all the achievements // before deserializing, otherwise that state's going to get lost. // need to check for login requests too, because game load won't be created until login finishes if (s_state.load_game_request || s_state.login_request) { + // GetGamePath() can call GetGameID(), lock needs to be released first. + lock.unlock(); + // Fallback to game icon if we don't have a cover. std::string image = System::GetImageForLoadingScreen(System::GetGamePath()); FullscreenUI::OpenOrUpdateLoadingScreen(image.empty() ? s_state.game_badge_url : image, TRANSLATE_SV("Achievements", "Downloading achievements data...")); - WaitForServerCallsWithYield(lock); + WaitForServerCalls(); FullscreenUI::CloseLoadingScreen(); + + lock.lock(); } if (data.empty()) @@ -2196,7 +2230,7 @@ u32 Achievements::GetPauseThrottleFrames() return 0; const auto lock = GetLock(); - if (!IsHardcoreModeActive()) + if (!rc_client_get_hardcore_enabled(s_state.client)) return 0; u32 frames_remaining = 0; @@ -2221,7 +2255,7 @@ void Achievements::Logout() if (HasActiveGame()) { ClearGameInfo(); - DisableHardcoreMode(false, false); + SetHardcoreModeState(false, false, false); } CancelFetchGameListRequest(); @@ -3899,7 +3933,7 @@ void Achievements::FinishLoadRAIntegrationOnCoreThread() FinishInitialize(); } -void Achievements::UnloadRAIntegration(std::unique_lock& lock) +void Achievements::UnloadRAIntegration(std::unique_lock& lock) { DebugAssert(s_state.using_raintegration && s_state.client); diff --git a/src/core/achievements.h b/src/core/achievements.h index efae30660..97348077c 100644 --- a/src/core/achievements.h +++ b/src/core/achievements.h @@ -17,6 +17,10 @@ class Error; class ProgressCallback; +namespace Threading { +class Mutex; +} + class StateWrapper; class CDImage; @@ -63,7 +67,8 @@ private: }; /// Acquires the achievements lock. Must be held when accessing any achievement state from another thread. -std::unique_lock GetLock(); +/// The core thread is allowed to **read** achievement state without taking the lock first. +std::unique_lock GetLock(); /// Converts a game hash to a string for display. If the hash is nullopt, returns "[NO HASH]". TinyString GameHashToString(const std::optional& hash); @@ -119,6 +124,7 @@ void DisableHardcoreMode(bool show_message, bool display_game_summary); void ConfirmHardcoreModeDisableAsync(std::string_view trigger, std::function callback); /// Returns true if hardcore mode is active, and functionality should be restricted. +/// NOTE: Only safe to call on the core thread. bool IsHardcoreModeActive(); /// RAIntegration only exists for Windows, so no point checking it on other platforms. diff --git a/src/core/achievements_private.h b/src/core/achievements_private.h index 62828b211..462789dd2 100644 --- a/src/core/achievements_private.h +++ b/src/core/achievements_private.h @@ -138,6 +138,7 @@ void AddAchievementNotification(std::string key, float duration, std::string ima u16 min_width = 0, bool small_font = false); /// Clears all cached state used to render the UI. +/// NOTE: Assumes achievements lock is held. void ClearAchievementsState(); /// Updates cached data for the last progress update. diff --git a/src/core/discord_presence.cpp b/src/core/discord_presence.cpp index 89e9b8196..20c316a88 100644 --- a/src/core/discord_presence.cpp +++ b/src/core/discord_presence.cpp @@ -11,6 +11,7 @@ #include "common/error.h" #include "common/log.h" #include "common/string_util.h" +#include "common/threading.h" #include "discord_rpc.h" diff --git a/src/core/fullscreenui.cpp b/src/core/fullscreenui.cpp index e24bcdcca..0c1c1e8ac 100644 --- a/src/core/fullscreenui.cpp +++ b/src/core/fullscreenui.cpp @@ -644,7 +644,11 @@ void FullscreenUI::Shutdown() s_locals.was_paused_on_quick_menu_open = false; s_locals.has_pending_window_switch = false; - ClearAchievementsState(); + { + const auto lock = Achievements::GetLock(); + ClearAchievementsState(); + } + ClearSettingsState(); ClearGameListState(); s_locals.current_time_string = {}; diff --git a/src/core/fullscreenui_achievements.cpp b/src/core/fullscreenui_achievements.cpp index 7a545bc0f..018f980b6 100644 --- a/src/core/fullscreenui_achievements.cpp +++ b/src/core/fullscreenui_achievements.cpp @@ -226,8 +226,6 @@ void FullscreenUI::ClearAchievementsState() // NOTE: can be called on the CPU thread. don't mess with any GPU thread state // will already be held if we're clearing as a result of achievements shutting down - const auto lock = Achievements::GetLock(); - CloseLeaderboard(); s_achievements_locals.notifications = {}; @@ -2849,7 +2847,7 @@ void FullscreenUI::DrawLeaderboardsWindow() RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.NormalFontWeight, summary_bb.Min, summary_bb.Max, summary_color, text, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &summary_bb); - if (!is_leaderboard_open && !Achievements::IsHardcoreModeActive()) + if (!is_leaderboard_open && !rc_client_get_hardcore_enabled(Achievements::GetClient())) { const ImRect hardcore_warning_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.MediumFontSize)); top += UIStyle.MediumFontSize + spacing_small; diff --git a/src/core/fullscreenui_game_list.cpp b/src/core/fullscreenui_game_list.cpp index ea2db74fc..c474fc4a6 100644 --- a/src/core/fullscreenui_game_list.cpp +++ b/src/core/fullscreenui_game_list.cpp @@ -15,6 +15,7 @@ #include "common/file_system.h" #include "common/path.h" #include "common/string_util.h" +#include "common/threading.h" #include "IconsEmoji.h" #include "IconsPromptFont.h" diff --git a/src/core/system.cpp b/src/core/system.cpp index 5a1d05a61..51a3fe416 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -4300,9 +4300,12 @@ bool System::PopulateGameListEntryFromCurrentGame(GameList::Entry* entry, Error* ((s_state.region == ConsoleRegion::NTSC_J) ? DiscRegion::NTSC_J : DiscRegion::PAL)); } - entry->achievements_game_id = Achievements::GetGameID(); - if (const std::optional achievements_hash = Achievements::GetGameHash()) - entry->achievements_hash = achievements_hash.value(); + { + const auto lock = Achievements::GetLock(); + entry->achievements_game_id = Achievements::GetGameID(); + if (const std::optional achievements_hash = Achievements::GetGameHash()) + entry->achievements_hash = achievements_hash.value(); + } entry->is_runtime_populated = true; return true;