From 59247c84b957d8da0b1fff99cef1469ea7cf8bb3 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Mon, 6 Jul 2026 15:32:13 +1000 Subject: [PATCH] Achievements: Move game hash calculation to system Saves reading the executable a second time. --- src/core/achievements.cpp | 85 ++++++++++++++------------------------ src/core/achievements.h | 14 ++++--- src/core/game_database.cpp | 2 +- src/core/game_list.cpp | 8 ++-- src/core/system.cpp | 58 +++++++++++++++++++------- src/core/system.h | 6 +-- 6 files changed, 89 insertions(+), 84 deletions(-) diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index 2329368cb..87828e40c 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -5,7 +5,6 @@ #include "achievements.h" #include "achievements_private.h" -#include "bios.h" #include "bus.h" #include "cheats.h" #include "core.h" @@ -29,7 +28,6 @@ #include "common/file_system.h" #include "common/heap_array.h" #include "common/log.h" -#include "common/md5_digest.h" #include "common/path.h" #include "common/progress_callback.h" #include "common/ryml_helpers.h" @@ -346,45 +344,6 @@ void Achievements::ReportRCError(int err, fmt::format_string fmt, T&&... a ReportError(str); } -std::optional Achievements::GetGameHash(CDImage* image) -{ - if (!image) - return std::nullopt; - - std::string executable_name; - std::vector executable_data; - if (!System::ReadExecutableFromImage(image, &executable_name, &executable_data)) - return std::nullopt; - - return GetGameHash(executable_name, executable_data); -} - -std::optional Achievements::GetGameHash(const std::string_view executable_name, - std::span executable_data) -{ - // NOTE: Assumes executable_data is aligned to 4 bytes at least.. it should be. - const BIOS::PSEXEHeader* header = reinterpret_cast(executable_data.data()); - if (executable_data.size() < sizeof(BIOS::PSEXEHeader) || !BIOS::IsValidPSExeHeader(*header, executable_data.size())) - { - ERROR_LOG("PS-EXE header is invalid in '{}' ({} bytes)", executable_name, executable_data.size()); - return std::nullopt; - } - - const u32 hash_size = std::min(header->file_size + 2048, static_cast(executable_data.size())); - - MD5Digest digest; - digest.Update(executable_name.data(), static_cast(executable_name.size())); - if (hash_size > 0) - digest.Update(executable_data.data(), hash_size); - - std::optional ret; - digest.Final(ret.emplace()); - - INFO_COLOR_LOG(StrongOrange, "RA Hash for '{}': {} ({} bytes hashed)", executable_name, GameHashToString(ret), - hash_size); - - return ret; -} std::string Achievements::GetImageURL(const char* image_name, u32 type) { @@ -1061,15 +1020,10 @@ void Achievements::UpdateRichPresence(std::unique_lock& lo #endif } -void Achievements::OnSystemStarting(CDImage* image, bool disable_hardcore_mode) +void Achievements::OnSystemStarting(bool disable_hardcore_mode) { - const std::optional game_hash = GetGameHash(image); - std::unique_lock lock(s_state.mutex); - // Always set hash in case we late enable. - s_state.game_hash = game_hash; - if (!IsActive() || IsRAIntegrationInitializing()) return; @@ -1091,12 +1045,20 @@ void Achievements::OnSystemStarting(CDImage* image, bool disable_hardcore_mode) else { // only enable hardcore mode if we're logged in, or waiting for a login response - if (image && !disable_hardcore_mode && g_settings.achievements_hardcore_mode && IsLoggedInOrLoggingIn()) + if (!disable_hardcore_mode && g_settings.achievements_hardcore_mode && IsLoggedInOrLoggingIn()) EnableHardcoreMode(false, false); } +} + +void Achievements::OnSystemStarted() +{ + const auto lock = GetLock(); + if (!IsActive() || IsRAIntegrationInitializing()) + return; // now we can finally identify the game - BeginLoadGame(); + if (!s_state.load_game_request) + BeginLoadGame(); } void Achievements::OnSystemDestroyed() @@ -1140,19 +1102,26 @@ void Achievements::OnSystemReset() } } -void Achievements::GameChanged(CDImage* image) +void Achievements::SetGameHash(const std::optional& hash) { - const auto game_hash = GetGameHash(image); - - std::unique_lock lock(s_state.mutex); + const auto lock = GetLock(); // disc changed? - if (s_state.game_hash == game_hash) + if (s_state.game_hash == hash) return; - s_state.game_hash = game_hash; + s_state.game_hash = hash; + INFO_COLOR_LOG(StrongOrange, "RA Hash: {}", GameHashToString(s_state.game_hash)); + // just set the hash if inactive if (!IsActive() || IsRAIntegrationInitializing()) + { + DisableHardcoreMode(false, false); + return; + } + + // if session hasn't started yet, don't treat this as a change + if (!System::IsValid()) return; // cancel previous requests @@ -1170,6 +1139,12 @@ void Achievements::GameChanged(CDImage* image) s_state.reload_game_on_reset = true; } +std::optional Achievements::GetGameHash() +{ + const auto lock = GetLock(); + return s_state.game_hash; +} + void Achievements::BeginLoadGame() { if (!s_state.game_hash.has_value()) diff --git a/src/core/achievements.h b/src/core/achievements.h index 3d7dc0713..fdb8c60a7 100644 --- a/src/core/achievements.h +++ b/src/core/achievements.h @@ -65,10 +65,6 @@ private: /// Acquires the achievements lock. Must be held when accessing any achievement state from another thread. std::unique_lock GetLock(); -/// Returns the achievements game hash for a given disc. -std::optional GetGameHash(CDImage* image); -std::optional GetGameHash(const std::string_view executable_name, std::span executable_data); - /// Converts a game hash to a string for display. If the hash is nullopt, returns "[NO HASH]". TinyString GameHashToString(const std::optional& hash); @@ -82,7 +78,10 @@ bool RefreshGameList(ProgressCallback* progress, Error* error); bool RefreshAllProgressDatabase(ProgressCallback* progress, Error* error); /// Called when the system is start. Engages hardcore mode if enabled. -void OnSystemStarting(CDImage* image, bool disable_hardcore_mode); +void OnSystemStarting(bool disable_hardcore_mode); + +/// Called when the system has finished starting. Identifies the game, assuming the system has set it. +void OnSystemStarted(); /// Called when the system is shutting down. If this returns false, the shutdown should be aborted. void OnSystemDestroyed(); @@ -91,7 +90,10 @@ void OnSystemDestroyed(); void OnSystemReset(); /// Called when the system changes game. -void GameChanged(CDImage* image); +void SetGameHash(const std::optional& hash); + +/// Returns the game hash for the currently loaded game, or nullopt if no hash is set. +std::optional GetGameHash(); /// Called once a frame at vsync time on the CPU thread. void FrameUpdate(); diff --git a/src/core/game_database.cpp b/src/core/game_database.cpp index 397c95d11..b65425657 100644 --- a/src/core/game_database.cpp +++ b/src/core/game_database.cpp @@ -280,7 +280,7 @@ const GameDatabase::Entry* GameDatabase::GetEntryForDisc(CDImage* image) { std::string id; GameHash hash; - System::GetGameDetailsFromImage(image, &id, &hash); + System::GetGameDetailsFromImage(image, &id, &hash, nullptr); const Entry* entry = GetEntryForGameDetails(id, hash); if (entry) return entry; diff --git a/src/core/game_list.cpp b/src/core/game_list.cpp index 07b21a469..08f3d040b 100644 --- a/src/core/game_list.cpp +++ b/src/core/game_list.cpp @@ -341,12 +341,12 @@ bool GameList::GetDiscListEntry(const std::string& path, Entry* entry) // use the same buffer for game and achievement hashing, to avoid double decompression std::string id, executable_name; std::vector executable_data; - if (System::GetGameDetailsFromImage(cdi.get(), &id, &entry->hash, &executable_name, &executable_data)) + std::optional achievements_hash; + if (System::GetGameDetailsFromImage(cdi.get(), &id, &entry->hash, &achievements_hash) && + achievements_hash.has_value()) { // used for achievement count lookup later - const std::optional hash = Achievements::GetGameHash(executable_name, executable_data); - if (hash.has_value()) - entry->achievements_hash = hash.value(); + entry->achievements_hash = achievements_hash.value(); } // try the database first diff --git a/src/core/system.cpp b/src/core/system.cpp index fba602586..328ed7cda 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -63,6 +63,7 @@ #include "common/file_system.h" #include "common/layered_settings_interface.h" #include "common/log.h" +#include "common/md5_digest.h" #include "common/memmap.h" #include "common/path.h" #include "common/string_util.h" @@ -140,6 +141,10 @@ static bool ReadExecutableFromImage(IsoReader& iso, std::string* out_executable_ static GameHash GetGameHashFromBuffer(std::string_view exe_name, std::span exe_buffer, const IsoReader::ISOPrimaryVolumeDescriptor& iso_pvd, u32 track_1_length); +/// Returns the achievements game hash for a given executable. +static std::optional GetAchievementsHash(const std::string& executable_name, + const std::vector& executable_data); + /// Settings that are looked up on demand. static bool ShouldStartFullscreen(); static bool ShouldStartPaused(); @@ -669,7 +674,7 @@ std::string System::GetGameHashId(GameHash hash) } bool System::GetGameDetailsFromImage(CDImage* cdi, std::string* out_id, GameHash* out_hash, - std::string* out_executable_name, std::vector* out_executable_data) + std::optional>* out_achievements_hash) { IsoReader iso; std::string id; @@ -681,10 +686,8 @@ bool System::GetGameDetailsFromImage(CDImage* cdi, std::string* out_id, GameHash out_id->clear(); if (out_hash) *out_hash = 0; - if (out_executable_name) - out_executable_name->clear(); - if (out_executable_data) - out_executable_data->clear(); + if (out_achievements_hash) + out_achievements_hash->reset(); return false; } @@ -729,11 +732,8 @@ bool System::GetGameDetailsFromImage(CDImage* cdi, std::string* out_id, GameHash if (out_hash) *out_hash = hash; - - if (out_executable_name) - *out_executable_name = std::move(exe_name); - if (out_executable_data) - *out_executable_data = std::move(exe_buffer); + if (out_achievements_hash) + *out_achievements_hash = GetAchievementsHash(exe_name, exe_buffer); return true; } @@ -901,6 +901,31 @@ GameHash System::GetGameHashFromBuffer(std::string_view exe_name, std::span System::GetAchievementsHash(const std::string& executable_name, + const std::vector& executable_data) +{ + // NOTE: Assumes executable_data is aligned to 4 bytes at least.. it should be. + const BIOS::PSEXEHeader* header = reinterpret_cast(executable_data.data()); + if (executable_data.size() < sizeof(BIOS::PSEXEHeader) || !BIOS::IsValidPSExeHeader(*header, executable_data.size())) + { + ERROR_LOG("PS-EXE header is invalid in '{}' ({} bytes)", executable_name, executable_data.size()); + return std::nullopt; + } + + const u32 hash_size = std::min(header->file_size + 2048, static_cast(executable_data.size())); + + MD5Digest digest; + digest.Update(executable_name.data(), static_cast(executable_name.size())); + if (hash_size > 0) + digest.Update(executable_data.data(), hash_size); + + std::optional ret; + digest.Final(ret.emplace()); + + DEV_LOG("RA Hash for '{}': {} ({} bytes hashed)", executable_name, Achievements::GameHashToString(ret), hash_size); + return ret; +} + DiscRegion System::GetRegionForSerial(const std::string_view serial) { static constexpr const std::pair region_prefixes[] = { @@ -1607,10 +1632,10 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) s_state.gpu_dump_player = std::move(gpu_dump); Host::OnSystemStarting(); FullscreenUI::OnSystemStarting(); + Achievements::OnSystemStarting(parameters.disable_achievements_hardcore_mode); // Update running game, this will apply settings as well. UpdateRunningGame(disc ? disc->GetPath() : parameters.path, disc.get(), true); - Achievements::OnSystemStarting(disc.get(), parameters.disable_achievements_hardcore_mode); // Determine console region. Has to be done here, because gamesettings can override it. s_state.region = (g_settings.region == ConsoleRegion::Auto) ? auto_console_region : g_settings.region; @@ -1744,6 +1769,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) GDBServer::Initialize(g_settings.gdb_server_port); #endif + Achievements::OnSystemStarted(); Host::OnSystemStarted(); if (parameters.load_image_to_ram || g_settings.cdrom_load_image_to_ram) @@ -2612,7 +2638,6 @@ void System::DoMemoryState(StateWrapper& sw, MemorySaveState& mss, bool update_d #else #define SAVE_COMPONENT(name, expr) expr #endif - sw.Do(&s_state.frame_number); sw.Do(&s_state.internal_frame_number); @@ -3988,6 +4013,7 @@ void System::UpdateRunningGame(const std::string& path, CDImage* image, bool boo s_state.running_game_hash = 0; s_state.running_game_custom_title = false; + std::optional achievements_hash; if (!path.empty()) { s_state.running_game_path = path; @@ -4047,7 +4073,7 @@ void System::UpdateRunningGame(const std::string& path, CDImage* image, bool boo if (image->GetTrack(1).mode != CDImage::TrackMode::Audio) { std::string id; - GetGameDetailsFromImage(image, &id, &s_state.running_game_hash); + GetGameDetailsFromImage(image, &id, &s_state.running_game_hash, &achievements_hash); // Custom serial? s_state.running_game_entry = s_state.running_game_serial.empty() ? @@ -4088,11 +4114,11 @@ void System::UpdateRunningGame(const std::string& path, CDImage* image, bool boo if (!IsReplayingGPUDump()) { + Achievements::SetGameHash(achievements_hash); + // Cheats are loaded later in Initialize(). if (!booting) { - Achievements::GameChanged(image); - const bool had_setting_overrides = Cheats::HasAnySettingOverrides(); Cheats::ReloadCheats(true, true, false, true, true); if (had_setting_overrides) @@ -4155,6 +4181,8 @@ bool System::PopulateGameListEntryFromCurrentGame(GameList::Entry* entry, Error* } 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; diff --git a/src/core/system.h b/src/core/system.h index 93f1138c9..1fe687c41 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -8,6 +8,7 @@ #include "util/image.h" +#include #include #include #include @@ -152,9 +153,8 @@ std::string GetExecutableNameForImage(CDImage* cdi, bool strip_subdirectories); bool ReadExecutableFromImage(CDImage* cdi, std::string* out_executable_name, std::vector* out_executable_data); std::string GetGameHashId(GameHash hash); -bool GetGameDetailsFromImage(CDImage* cdi, std::string* out_id = nullptr, GameHash* out_hash = nullptr, - std::string* out_executable_name = nullptr, - std::vector* out_executable_data = nullptr); +bool GetGameDetailsFromImage(CDImage* cdi, std::string* out_id, GameHash* out_hash, + std::optional>* out_achievements_hash); GameHash GetGameHashFromFile(const char* path); GameHash GetGameHashFromBuffer(const std::string_view path, const std::span data); DiscRegion GetRegionForSerial(const std::string_view serial);