From 40172662413e7d4409605b7228714c5eff154373 Mon Sep 17 00:00:00 2001 From: a Date: Wed, 21 Aug 2024 01:50:47 +0300 Subject: [PATCH] * deprecate `lazy_load_achievements_icons` in favor of `paginated_achievements_icons` * new option `upload_achievements_icons_to_gpu` in `configs.main.ini` which controls whether the overlay should upload the achievements icons to the GPU and display them or not * synchronize overlay proc with the periodic steam callback in a better way to avoid FPS drop * prevent overlay flickering regression * upload achievements icons to the GPU in the overlay proc periodically, this dramatically decreased the startup locking/halt time * fix a potential deadlock scenario in the overlay as a result of synchonizing with 2 mutex objects --- dll/dll/settings.h | 11 +- dll/dll/steam_user_stats.h | 7 +- dll/settings.cpp | 2 +- dll/settings_parser.cpp | 12 +- dll/steam_user_stats.cpp | 4 +- dll/steam_user_stats_achievements.cpp | 58 +- overlay_experimental/overlay/steam_overlay.h | 35 +- overlay_experimental/steam_overlay.cpp | 553 ++++++++++-------- .../configs.main.EXAMPLE.ini | 17 +- .../configs.overlay.EXAMPLE.ini | 6 + 10 files changed, 409 insertions(+), 296 deletions(-) diff --git a/dll/dll/settings.h b/dll/dll/settings.h index 7110332d..9dc86352 100644 --- a/dll/dll/settings.h +++ b/dll/dll/settings.h @@ -216,6 +216,7 @@ private: public: constexpr const static int INVALID_IMAGE_HANDLE = 0; + constexpr const static int UNLOADED_IMAGE_HANDLE = -1; //Depots std::vector depots{}; @@ -258,8 +259,12 @@ public: // the stat itself is always saved regardless of that flag, only affects the achievement progress bool save_only_higher_stat_achievement_progress = true; // the emulator loads the achievements icons is memory mainly for `ISteamUserStats::GetAchievementIcon()` - // true means load icons lazily when they are requested, otherwise load icons as soon as the interface ISteamUserStats is initialized - bool lazy_load_achievements_icons = true; + // this defines how many icons to load each iteration when the periodic callback in `Steam_User_Stats` is triggered + // or when the app calls `SteamAPI_RunCallbacks()` + // -1 == functionality disabled + // 0 == load icons only when they're requested + // >0 == load icons in the background as mentioned above + int paginated_achievements_icons = 10; // bypass to make SetAchievement() always return true, prevent some games from breaking bool achievement_bypass = false; @@ -314,6 +319,8 @@ public: bool overlay_warn_local_save = false; //disable overlay warning for local save bool disable_overlay_warning_local_save = false; + // should the overlay upload icons to the GPU and display them + bool overlay_upload_achs_icons_to_gpu = true; //disable overlay warning for bad app ID (= 0) bool disable_overlay_warning_bad_appid = false; // disable all overlay warnings diff --git a/dll/dll/steam_user_stats.h b/dll/dll/steam_user_stats.h index 5f6e4316..30be4cfc 100644 --- a/dll/dll/steam_user_stats.h +++ b/dll/dll/steam_user_stats.h @@ -70,8 +70,7 @@ public ISteamUserStats011, public ISteamUserStats { public: - static constexpr auto achievements_user_file = "achievements.json"; - static constexpr int UNLOADED_ACH_ICON = -1; + static constexpr const auto achievements_user_file = "achievements.json"; private: template @@ -95,7 +94,7 @@ private: nlohmann::json defined_achievements{}; nlohmann::json user_achievements{}; std::vector sorted_achievement_names{}; - bool achievements_icons_loaded = false; + size_t last_loaded_ach_icon{}; std::map stats_cache_int{}; std::map stats_cache_float{}; @@ -213,7 +212,7 @@ public: // specified achievement. int GetAchievementIcon( const char *pchName ); - int get_achievement_icon_handle( const std::string &ach_name, bool pbAchieved ); + int get_achievement_icon_handle( const std::string &ach_name, bool pbAchieved, bool force_load = false ); std::string get_achievement_icon_name( const char *pchName, bool achieved ); diff --git a/dll/settings.cpp b/dll/settings.cpp index 844e5375..ebcc6bc2 100644 --- a/dll/settings.cpp +++ b/dll/settings.cpp @@ -375,7 +375,7 @@ int Settings::add_image(const std::string &data, uint32 width, uint32 height) Image_Data* Settings::get_image(int handle) { - if (INVALID_IMAGE_HANDLE == handle) { + if (INVALID_IMAGE_HANDLE == handle || UNLOADED_IMAGE_HANDLE == handle) { return nullptr; } diff --git a/dll/settings_parser.cpp b/dll/settings_parser.cpp index 6592b67b..a2f39758 100644 --- a/dll/settings_parser.cpp +++ b/dll/settings_parser.cpp @@ -1343,6 +1343,9 @@ static void parse_overlay_general_config(class Settings *settings_client, class settings_client->disable_overlay_warning_local_save = ini.GetBoolValue("overlay::general", "disable_warning_local_save", settings_client->disable_overlay_warning_local_save); settings_server->disable_overlay_warning_local_save = ini.GetBoolValue("overlay::general", "disable_warning_local_save", settings_server->disable_overlay_warning_local_save); + settings_client->overlay_upload_achs_icons_to_gpu = ini.GetBoolValue("overlay::general", "upload_achievements_icons_to_gpu", settings_client->overlay_upload_achs_icons_to_gpu); + settings_server->overlay_upload_achs_icons_to_gpu = ini.GetBoolValue("overlay::general", "upload_achievements_icons_to_gpu", settings_server->overlay_upload_achs_icons_to_gpu); + } // main::misc::steam_game_stats_reports_dir @@ -1437,8 +1440,13 @@ static void parse_stats_features(class Settings *settings_client, class Settings settings_client->save_only_higher_stat_achievement_progress = ini.GetBoolValue("main::stats", "save_only_higher_stat_achievement_progress", settings_client->save_only_higher_stat_achievement_progress); settings_server->save_only_higher_stat_achievement_progress = ini.GetBoolValue("main::stats", "save_only_higher_stat_achievement_progress", settings_server->save_only_higher_stat_achievement_progress); - settings_client->lazy_load_achievements_icons = ini.GetBoolValue("main::stats", "lazy_load_achievements_icons", settings_client->lazy_load_achievements_icons); - settings_server->lazy_load_achievements_icons = ini.GetBoolValue("main::stats", "lazy_load_achievements_icons", settings_server->lazy_load_achievements_icons); + { + long val_client = ini.GetLongValue("main::stats", "paginated_achievements_icons", settings_client->paginated_achievements_icons); + settings_client->paginated_achievements_icons = static_cast(val_client); + + long val_server = ini.GetLongValue("main::stats", "paginated_achievements_icons", settings_server->paginated_achievements_icons); + settings_server->paginated_achievements_icons = static_cast(val_server); + } } diff --git a/dll/steam_user_stats.cpp b/dll/steam_user_stats.cpp index e214e808..c804b087 100644 --- a/dll/steam_user_stats.cpp +++ b/dll/steam_user_stats.cpp @@ -96,8 +96,8 @@ Steam_User_Stats::Steam_User_Stats(Settings *settings, class Networking *network it["displayName"] = get_value_for_language(it, "displayName", settings->get_language()); it["description"] = get_value_for_language(it, "description", settings->get_language()); - it["icon_handle"] = UNLOADED_ACH_ICON; - it["icon_gray_handle"] = UNLOADED_ACH_ICON; + it["icon_handle"] = Settings::UNLOADED_IMAGE_HANDLE; + it["icon_gray_handle"] = Settings::UNLOADED_IMAGE_HANDLE; } //TODO: not sure if the sort is actually case insensitive, ach names seem to be treated by steam as case insensitive so I assume they are. diff --git a/dll/steam_user_stats_achievements.cpp b/dll/steam_user_stats_achievements.cpp index 4e598d3f..2a0a2c0d 100644 --- a/dll/steam_user_stats_achievements.cpp +++ b/dll/steam_user_stats_achievements.cpp @@ -80,8 +80,8 @@ void Steam_User_Stats::save_achievements() int Steam_User_Stats::load_ach_icon(nlohmann::json &defined_ach, bool achieved) { const char *icon_handle_key = achieved ? "icon_handle" : "icon_gray_handle"; - int current_handle = defined_ach.value(icon_handle_key, UNLOADED_ACH_ICON); - if (UNLOADED_ACH_ICON != current_handle) { // already loaded + int current_handle = defined_ach.value(icon_handle_key, Settings::UNLOADED_IMAGE_HANDLE); + if (Settings::UNLOADED_IMAGE_HANDLE != current_handle) { // already loaded return current_handle; } @@ -394,7 +394,12 @@ int Steam_User_Stats::GetAchievementIcon( const char *pchName ) GetAchievement(pchName, &achieved); std::string ach_name(pchName); - int handle = get_achievement_icon_handle(ach_name, achieved); + // here we force load in case the game has a lot of achievements, because otherwise some games might timeout + // this somewhat defeats the purpose of background loading but a timeout is worse + int handle = get_achievement_icon_handle(ach_name, achieved, true); + if (Settings::UNLOADED_IMAGE_HANDLE == handle) { // if the background callback didn't get a chance to load this one yet + handle = Settings::INVALID_IMAGE_HANDLE; + } UserAchievementIconFetched_t data{}; data.m_bAchieved = achieved ; @@ -406,11 +411,10 @@ int Steam_User_Stats::GetAchievementIcon( const char *pchName ) return handle; } -int Steam_User_Stats::get_achievement_icon_handle( const std::string &ach_name, bool achieved ) +int Steam_User_Stats::get_achievement_icon_handle( const std::string &ach_name, bool achieved, bool force_load ) { - PRINT_DEBUG("'%s', %i", ach_name.c_str(), (int)achieved); + PRINT_DEBUG("'%s', achieved=%i, force=%i", ach_name.c_str(), (int)achieved, (int)force_load); std::lock_guard lock(global_mutex); - if (ach_name.empty()) return Settings::INVALID_IMAGE_HANDLE; nlohmann::detail::iter_impl it = defined_achievements.end(); try { @@ -418,7 +422,20 @@ int Steam_User_Stats::get_achievement_icon_handle( const std::string &ach_name, } catch(...) { } if (defined_achievements.end() == it) return Settings::INVALID_IMAGE_HANDLE; - int handle = load_ach_icon(*it, achieved); + int handle = Settings::INVALID_IMAGE_HANDLE; + if (settings->paginated_achievements_icons < 0) { // disabled functionality + handle = Settings::INVALID_IMAGE_HANDLE; + } else if (settings->paginated_achievements_icons == 0) { // load the icon only when requested + handle = load_ach_icon(*it, achieved); + } else { // depend on the periodic callback to load the icon + if (force_load) { + handle = load_ach_icon(*it, achieved); + } else { + const char *icon_handle_key = achieved ? "icon_handle" : "icon_gray_handle"; + handle = it->value(icon_handle_key, Settings::UNLOADED_IMAGE_HANDLE); + } + } + PRINT_DEBUG("returned handle = %i", handle); return handle; } @@ -784,16 +801,25 @@ bool Steam_User_Stats::GetAchievementProgressLimits( const char *pchName, float void Steam_User_Stats::load_achievements_icons() { - if (achievements_icons_loaded) return; - if (settings->lazy_load_achievements_icons) { - achievements_icons_loaded = true; - return; + if (last_loaded_ach_icon >= defined_achievements.size() || settings->paginated_achievements_icons <= 0) return; + +#ifndef EMU_RELEASE_BUILD + auto now1 = std::chrono::high_resolution_clock::now(); +#endif + + size_t idx = 0; + for (; + idx < settings->paginated_achievements_icons && last_loaded_ach_icon < defined_achievements.size(); + ++idx, ++last_loaded_ach_icon) { + auto &ach = defined_achievements.at(last_loaded_ach_icon); + load_ach_icon(ach, true); + load_ach_icon(ach, false); } - for (auto & defined_ach : defined_achievements) { - load_ach_icon(defined_ach, true); - load_ach_icon(defined_ach, false); - } +#ifndef EMU_RELEASE_BUILD + auto now2 = std::chrono::high_resolution_clock::now(); + auto dd = (unsigned)std::chrono::duration_cast(now2 - now1).count(); + PRINT_DEBUG("attempted to load %zu achievements icons in %u ms", idx * 2, dd); +#endif - achievements_icons_loaded = true; } diff --git a/overlay_experimental/overlay/steam_overlay.h b/overlay_experimental/overlay/steam_overlay.h index 8941cf9c..467a170c 100644 --- a/overlay_experimental/overlay/steam_overlay.h +++ b/overlay_experimental/overlay/steam_overlay.h @@ -71,8 +71,10 @@ struct Overlay_Achievement bool hidden{}; bool achieved{}; uint32 unlock_time{}; - std::pair< std::weak_ptr, bool > icon{}; - std::pair< std::weak_ptr, bool > icon_gray{}; + std::weak_ptr icon{}; + std::weak_ptr icon_gray{}; + int icon_handle = Settings::UNLOADED_IMAGE_HANDLE; + int icon_gray_handle = Settings::UNLOADED_IMAGE_HANDLE; }; struct Notification @@ -99,7 +101,6 @@ struct NotificationsCoords class Steam_Overlay { constexpr static const char ACH_SOUNDS_FOLDER[] = "sounds"; - constexpr static const int renderer_detector_polling_ms = 100; class Settings* settings; @@ -120,6 +121,7 @@ class Steam_Overlay std::string show_url{}; std::vector achievements{}; + size_t last_loaded_ach_icon{}; bool show_overlay = false; bool show_user_info = false; @@ -184,12 +186,6 @@ class Steam_Overlay Steam_Overlay& operator=(Steam_Overlay const&) = delete; Steam_Overlay& operator=(Steam_Overlay&&) = delete; - static void overlay_run_callback(void* object); - static void overlay_networking_callback(void* object, Common_Message* msg); - - bool is_friend_joinable(std::pair &f); - bool got_lobby(); - bool submit_notification( notification_type type, const std::string &msg, @@ -213,9 +209,7 @@ class Steam_Overlay void add_ach_progressbar(const Overlay_Achievement &ach); ImVec4 get_notification_bg_rgba_safe(); void build_notifications(float width, float height); - // invite a single friend - void invite_friend(uint64 friend_id, class Steam_Friends* steamFriends, class Steam_Matchmaking* steamMatchmaking); - + void request_renderer_detector(); void set_renderer_hook_timeout(); void cleanup_renderer_hook(); @@ -238,14 +232,27 @@ class Steam_Overlay bool open_overlay_hook(bool toggle); - bool try_load_ach_icon(Overlay_Achievement &ach, bool achieved); + bool try_load_ach_icon(Overlay_Achievement &ach, bool achieved, bool upload_new_icon_to_gpu); void overlay_render_proc(); + void load_next_ach_icon(); uint32 apply_global_style_color(); void render_main_window(); - void networking_msg_received(Common_Message* msg); + + + void steam_run_callback_update_my_lobby(); + bool is_friend_joinable(std::pair &f); + // invite a single friend + void invite_friend(uint64 friend_id, class Steam_Friends* steamFriends, class Steam_Matchmaking* steamMatchmaking); + void steam_run_callback_friends_actions(); void steam_run_callback(); + void networking_msg_received(Common_Message* msg); + + + static void overlay_run_callback(void* object); + static void overlay_networking_callback(void* object, Common_Message* msg); + public: Steam_Overlay(Settings* settings, Local_Storage *local_storage, SteamCallResults* callback_results, SteamCallBacks* callbacks, RunEveryRunCB* run_every_runcb, Networking *network); diff --git a/overlay_experimental/steam_overlay.cpp b/overlay_experimental/steam_overlay.cpp index d8e2464b..107c3477 100644 --- a/overlay_experimental/steam_overlay.cpp +++ b/overlay_experimental/steam_overlay.cpp @@ -85,23 +85,7 @@ void Steam_Overlay::overlay_run_callback(void* object) { // PRINT_DEBUG_ENTRY(); Steam_Overlay* _this = reinterpret_cast(object); - - // bail immediately if we can't lock the overlay mutex, deadlock scenario: - // 1. ** the background thread locks the global mutex - // 2. -- the user opens the overlay - // 3. -- the overlay proc is triggered the next frame, locking the overlay mutex - // 4. ** the background thread locks the global mutex and runs this callback - // 5. ** this callback (already having the global mutex) attempts to lock the overlay mutex (already locked before) - // 6. ** this callback, and the background thread, are now blocked, note that the global mutex is still locked - // 7. -- in the same frame, some code in the overlay proc attempts to call a steam API which usually locks the global mutex - // sice the global mutex is still locked, the overlay proc is also blocked, - // and now both the background thread and the overlay proc and locked - // even worse, the global mutex is locked forever now - if (!_this->overlay_mutex.try_lock()) return; - _this->steam_run_callback(); - - _this->overlay_mutex.unlock(); } void Steam_Overlay::overlay_networking_callback(void* object, Common_Message* msg) @@ -422,7 +406,6 @@ void Steam_Overlay::overlay_state_hook(bool ready) // called when the user presses SHIFT + TAB bool Steam_Overlay::open_overlay_hook(bool toggle) { - std::lock_guard lock(overlay_mutex); if (toggle) { ShowOverlay(!show_overlay); } @@ -639,16 +622,19 @@ void Steam_Overlay::show_test_achievement() ach.description = "~~~ " + ach.title + " ~~~"; ach.achieved = true; + // random add icon if (achievements.size()) { size_t rand_idx = common_helpers::rand_number(achievements.size() - 1); auto &rand_ach = achievements[rand_idx]; bool achieved = rand_idx < (achievements.size() / 2); - try_load_ach_icon(rand_ach, achieved); - ach.icon = achieved ? rand_ach.icon : rand_ach.icon_gray; + // force upload to GPU if the pagination is request-based + try_load_ach_icon(rand_ach, achieved, settings->paginated_achievements_icons == 0); + ach.icon = rand_ach.icon; + ach.icon_gray = rand_ach.icon_gray; } - bool for_progress = false; // randomly add progress + bool for_progress = false; if (common_helpers::rand_number(1000) % 2) { for_progress = true; uint32 progress = (uint32)(common_helpers::rand_number(500) / 10 + 50); // [50, 100] @@ -662,41 +648,6 @@ void Steam_Overlay::show_test_achievement() notify_sound_user_achievement(); } -bool Steam_Overlay::is_friend_joinable(std::pair &f) -{ - PRINT_DEBUG("%" PRIu64 "", f.first.id()); - std::lock_guard lock(global_mutex); - Steam_Friends* steamFriends = get_steam_client()->steam_friends; - - if (std::string(steamFriends->get_friend_rich_presence_silent((uint64)f.first.id(), "connect")).length() > 0 ) { - PRINT_DEBUG("%" PRIu64 " true (connect string)", f.first.id()); - return true; - } - - FriendGameInfo_t friend_game_info{}; - steamFriends->GetFriendGamePlayed((uint64)f.first.id(), &friend_game_info); - if (friend_game_info.m_steamIDLobby.IsValid() && (f.second.window_state & window_state_lobby_invite)) { - PRINT_DEBUG("%" PRIu64 " true (friend in a game)", f.first.id()); - return true; - } - - PRINT_DEBUG("%" PRIu64 " false", f.first.id()); - return false; -} - -bool Steam_Overlay::got_lobby() -{ - std::lock_guard lock(global_mutex); - Steam_Friends* steamFriends = get_steam_client()->steam_friends; - if (std::string(steamFriends->get_friend_rich_presence_silent(settings->get_local_steam_id(), "connect")).length() > 0) - return true; - - if (settings->get_lobby().IsValid()) - return true; - - return false; -} - void Steam_Overlay::build_friend_context_menu(Friend const& frd, friend_window_state& state) { if (ImGui::BeginPopupContextItem("Friends_ContextMenu", 1)) { @@ -768,24 +719,21 @@ void Steam_Overlay::build_friend_window(Friend const& frd, friend_window_state& // Window id is after the ###, the window title is the friend name std::string friend_window_id = std::move("###" + std::to_string(state.id)); if (ImGui::Begin((state.window_title + friend_window_id).c_str(), &show)) { - if (state.window_state & window_state_need_attention && ImGui::IsWindowFocused()) - { + if (state.window_state & window_state_need_attention && ImGui::IsWindowFocused()) { state.window_state &= ~window_state_need_attention; } // Fill this with the chat box and maybe the invitation - if (state.window_state & (window_state_lobby_invite | window_state_rich_invite)) - { + if (state.window_state & (window_state_lobby_invite | window_state_rich_invite)) { ImGui::LabelText("##label", translationInvitedYouToJoinTheGame[current_language], frd.name().c_str(), frd.appid()); ImGui::SameLine(); - if (ImGui::Button(translationAccept[current_language])) - { + if (ImGui::Button(translationAccept[current_language])) { state.window_state |= window_state_join; this->has_friend_action.push(frd); } + ImGui::SameLine(); - if (ImGui::Button(translationRefuse[current_language])) - { + if (ImGui::Button(translationRefuse[current_language])) { state.window_state &= ~(window_state_lobby_invite | window_state_rich_invite); } } @@ -829,15 +777,12 @@ void Steam_Overlay::build_friend_window(Friend const& frd, friend_window_state& ImGui::SameLine(); - if (ImGui::Button(translationSend[current_language])) - { + if (ImGui::Button(translationSend[current_language])) { send_chat_msg = true; } - if (send_chat_msg) - { - if (!(state.window_state & window_state_send_message)) - { + if (send_chat_msg) { + if (!(state.window_state & window_state_send_message)) { has_friend_action.push(frd); state.window_state |= window_state_send_message; } @@ -845,8 +790,9 @@ void Steam_Overlay::build_friend_window(Friend const& frd, friend_window_state& } // User closed the friend window - if (!show) + if (!show) { state.window_state &= ~window_state_show; + } ImGui::End(); } @@ -1108,7 +1054,7 @@ void Steam_Overlay::build_notifications(float width, float height) case notification_type::achievement_progress: case notification_type::achievement: { const auto &ach = it->ach.value(); - auto& [icon_rsrc, _] = (notification_type)it->type == notification_type::achievement + auto &icon_rsrc = (notification_type)it->type == notification_type::achievement ? ach.icon : ach.icon_gray; if (!icon_rsrc.expired() && ImGui::BeginTable("imgui_table", 2)) { @@ -1244,7 +1190,9 @@ void Steam_Overlay::post_achievement_notification(Overlay_Achievement &ach, bool std::lock_guard lock(overlay_mutex); if (!Ready()) return; - try_load_ach_icon(ach, !for_progress); // for progress notifications we want to load the gray icon + bool achieved = !for_progress; // for progress notifications we want to load the gray icon + // force upload to GPU if the pagination is request-based + try_load_ach_icon(ach, achieved, settings->paginated_achievements_icons == 0); submit_notification( for_progress ? notification_type::achievement_progress : notification_type::achievement, ach.title + "\n" + ach.description, @@ -1253,26 +1201,22 @@ void Steam_Overlay::post_achievement_notification(Overlay_Achievement &ach, bool ); } -void Steam_Overlay::invite_friend(uint64 friend_id, class Steam_Friends* steamFriends, class Steam_Matchmaking* steamMatchmaking) -{ - std::string connect_str = steamFriends->get_friend_rich_presence_silent(settings->get_local_steam_id(), "connect"); - if (connect_str.length() > 0) { - steamFriends->InviteUserToGame(friend_id, connect_str.c_str()); - PRINT_DEBUG("sent game invitation to friend with id = %llu", friend_id); - } else if (settings->get_lobby().IsValid()) { - steamMatchmaking->InviteUserToLobby(settings->get_lobby(), friend_id); - PRINT_DEBUG("sent lobby invitation to friend with id = %llu", friend_id); - } -} - -bool Steam_Overlay::try_load_ach_icon(Overlay_Achievement &ach, bool achieved) +bool Steam_Overlay::try_load_ach_icon(Overlay_Achievement &ach, bool achieved, bool upload_new_icon_to_gpu) { if (!_renderer) return false; + if (settings->paginated_achievements_icons < 0) return false; // no icons are loaded anyway + if (!settings->overlay_upload_achs_icons_to_gpu) return false; // don't upload anything to the GPU - auto& [icon_rsrc, attempted] = achieved ? ach.icon : ach.icon_gray; - if (attempted || !icon_rsrc.expired()) return true; + auto &icon_rsrc = achieved ? ach.icon : ach.icon_gray; + if (!icon_rsrc.expired()) return true; - const int icon_handle = get_steam_client()->steam_user_stats->get_achievement_icon_handle(ach.name, achieved); + // icons needs to be loaded, but we're not allowed + if (!upload_new_icon_to_gpu) return false; + + int &icon_handle = achieved ? ach.icon_handle : ach.icon_gray_handle; + if (Settings::UNLOADED_IMAGE_HANDLE == icon_handle) { // not loaded yet + icon_handle = get_steam_client()->steam_user_stats->get_achievement_icon_handle(ach.name, achieved); + } auto image_info = settings->get_image(icon_handle); if (image_info) { int icon_size = static_cast(settings->overlay_appearance.icon_size); @@ -1283,14 +1227,14 @@ bool Steam_Overlay::try_load_ach_icon(Overlay_Achievement &ach, bool achieved) PRINT_DEBUG("'%s' (result=%i)", ach.name.c_str(), (int)!icon_rsrc.expired()); } - attempted = true; return !icon_rsrc.expired(); } // Try to make this function as short as possible or it might affect game's fps. void Steam_Overlay::overlay_render_proc() { - std::lock_guard lock(overlay_mutex); + std::lock_guard lock(overlay_mutex); + if (!Ready()) return; if (show_overlay) { @@ -1302,6 +1246,7 @@ void Steam_Overlay::overlay_render_proc() build_notifications(io.DisplaySize.x, io.DisplaySize.y); } + load_next_ach_icon(); } uint32 Steam_Overlay::apply_global_style_color() @@ -1480,13 +1425,14 @@ void Steam_Overlay::render_main_window() bool achieved = x.achieved; bool hidden = x.hidden && !achieved; - try_load_ach_icon(x, true); - try_load_ach_icon(x, false); + // force upload to GPU if the pagination is request-based + try_load_ach_icon(x, true, settings->paginated_achievements_icons == 0); + try_load_ach_icon(x, false, settings->paginated_achievements_icons == 0); ImGui::Separator(); bool could_create_ach_table_entry = false; - if (!x.icon.first.expired() || !x.icon_gray.first.expired()) { + if (!x.icon.expired() || !x.icon_gray.expired()) { if (ImGui::BeginTable(x.title.c_str(), 2)) { could_create_ach_table_entry = true; @@ -1495,7 +1441,7 @@ void Steam_Overlay::render_main_window() ImGui::TableNextRow(ImGuiTableRowFlags_None, settings->overlay_appearance.icon_size); ImGui::TableSetColumnIndex(0); - auto& [icon_rsrc, _] = achieved ? x.icon : x.icon_gray; + auto &icon_rsrc = achieved ? x.icon : x.icon_gray; if (!icon_rsrc.expired()) { ImGui::Image( (ImTextureID)*icon_rsrc.lock().get(), @@ -1633,162 +1579,45 @@ void Steam_Overlay::render_main_window() if (style_color_stack) ImGui::PopStyleColor(style_color_stack); ImGui::PopFont(); - if (!show) ShowOverlay(false); + if (!show) { + ShowOverlay(false); + } } -void Steam_Overlay::networking_msg_received(Common_Message *msg) +void Steam_Overlay::load_next_ach_icon() { - std::lock_guard lock(overlay_mutex); + // this function only works when icons pagination is active, request-based loading is not supported too (pagination=0) + if (!settings->overlay_upload_achs_icons_to_gpu || settings->paginated_achievements_icons <= 0 || achievements.empty()) return; + + size_t linear_idx = last_loaded_ach_icon / 2; // 2 icons per achievement, 1 achieved, 1 unachieved + if (linear_idx >= achievements.size()) { + last_loaded_ach_icon = 0; + linear_idx = 0; + } + +#ifndef EMU_RELEASE_BUILD + auto now1 = std::chrono::high_resolution_clock::now(); +#endif + + auto &ach = achievements.at(linear_idx); + ++last_loaded_ach_icon; + + bool achieved = last_loaded_ach_icon % 2 != 0; + auto &icon_rsrc = achieved ? ach.icon : ach.icon_gray; + // always force upload to GPU in background-loading mode (pagination > 0) + bool loaded = try_load_ach_icon(ach, achieved, true); - if (msg->has_steam_messages()) { - Friend frd; - frd.set_id(msg->source_id()); - auto friend_info = friends.find(frd); - if (friend_info != friends.end()) { - Steam_Messages const& steam_message = msg->steam_messages(); - // Change color to cyan for friend - friend_info->second.chat_history.append(friend_info->first.name() + ": " + steam_message.message()).append("\n", 1); - if (!(friend_info->second.window_state & window_state_show)) { - friend_info->second.window_state |= window_state_need_attention; - } - - add_chat_message_notification(friend_info->first.name() + ": " + steam_message.message()); - notify_sound_user_invite(friend_info->second); - } +#ifndef EMU_RELEASE_BUILD + if (loaded) { + auto now2 = std::chrono::high_resolution_clock::now(); + auto dd = (unsigned)std::chrono::duration_cast(now2 - now1).count(); + PRINT_DEBUG("uploaded an achievement icon to GPU in %u ms", dd); } +#endif + } -void Steam_Overlay::steam_run_callback() -{ - if (!Ready()) return; - - if (overlay_state_changed) { - overlay_state_changed = false; - - GameOverlayActivated_t data{}; - data.m_bActive = show_overlay; - data.m_bUserInitiated = true; - data.m_dwOverlayPID = 123; - data.m_nAppID = settings->get_local_game_id().AppID(); - callbacks->addCBResult(data.k_iCallback, &data, sizeof(data)); - } - - Steam_Friends* steamFriends = get_steam_client()->steam_friends; - Steam_Matchmaking* steamMatchmaking = get_steam_client()->steam_matchmaking; - - if (save_settings) { - save_settings = false; - - const char *language_text = valid_languages[current_language]; - save_global_settings(get_steam_client()->local_storage, username_text, language_text); - get_steam_client()->settings_client->set_local_name(username_text); - get_steam_client()->settings_server->set_local_name(username_text); - get_steam_client()->settings_client->set_language(language_text); - get_steam_client()->settings_server->set_language(language_text); - steamFriends->resend_friend_data(); - } - - i_have_lobby = got_lobby(); - std::for_each(friends.begin(), friends.end(), [this](std::pair &i) - { - i.second.joinable = is_friend_joinable(i); - }); - - while (!has_friend_action.empty()) { - auto friend_info = friends.find(has_friend_action.front()); - if (friend_info != friends.end()) { - uint64 friend_id = (uint64)friend_info->first.id(); - // The user clicked on "Send" - if (friend_info->second.window_state & window_state_send_message) { - char* input = friend_info->second.chat_input; - char* end_input = input + strlen(input); - char* printable_char = std::find_if(input, end_input, [](char c) { return std::isgraph(c); }); - - // Check if the message contains something else than blanks - if (printable_char != end_input) { - // Handle chat send - Common_Message msg; - Steam_Messages* steam_messages = new Steam_Messages; - steam_messages->set_type(Steam_Messages::FRIEND_CHAT); - steam_messages->set_message(friend_info->second.chat_input); - msg.set_allocated_steam_messages(steam_messages); - msg.set_source_id(settings->get_local_steam_id().ConvertToUint64()); - msg.set_dest_id(friend_id); - network->sendTo(&msg, true); - - friend_info->second.chat_history.append(get_steam_client()->settings_client->get_local_name()).append(": ").append(input).append("\n", 1); - } - *input = 0; // Reset the input field - - friend_info->second.window_state &= ~window_state_send_message; - } - // The user clicked on "Invite" (but invite all wasn't clicked) - if (friend_info->second.window_state & window_state_invite) { - invite_friend(friend_id, steamFriends, steamMatchmaking); - - friend_info->second.window_state &= ~window_state_invite; - } - // The user clicked on "Join" - if (friend_info->second.window_state & window_state_join) { - std::string connect = steamFriends->get_friend_rich_presence_silent(friend_id, "connect"); - // The user got a lobby invite and accepted it - if (friend_info->second.window_state & window_state_lobby_invite) { - GameLobbyJoinRequested_t data; - data.m_steamIDLobby.SetFromUint64(friend_info->second.lobbyId); - data.m_steamIDFriend.SetFromUint64(friend_id); - callbacks->addCBResult(data.k_iCallback, &data, sizeof(data)); - - friend_info->second.window_state &= ~window_state_lobby_invite; - } else { - // The user got a rich presence invite and accepted it - if (friend_info->second.window_state & window_state_rich_invite) { - GameRichPresenceJoinRequested_t data = {}; - data.m_steamIDFriend.SetFromUint64(friend_id); - strncpy(data.m_rgchConnect, friend_info->second.connect, k_cchMaxRichPresenceValueLength - 1); - callbacks->addCBResult(data.k_iCallback, &data, sizeof(data)); - - friend_info->second.window_state &= ~window_state_rich_invite; - } else if (connect.length() > 0) { - GameRichPresenceJoinRequested_t data = {}; - data.m_steamIDFriend.SetFromUint64(friend_id); - strncpy(data.m_rgchConnect, connect.c_str(), k_cchMaxRichPresenceValueLength - 1); - callbacks->addCBResult(data.k_iCallback, &data, sizeof(data)); - } - - //Not sure about this but it fixes sonic racing transformed invites - FriendGameInfo_t friend_game_info = {}; - steamFriends->GetFriendGamePlayed(friend_id, &friend_game_info); - uint64 lobby_id = friend_game_info.m_steamIDLobby.ConvertToUint64(); - if (lobby_id) { - GameLobbyJoinRequested_t data; - data.m_steamIDLobby.SetFromUint64(lobby_id); - data.m_steamIDFriend.SetFromUint64(friend_id); - callbacks->addCBResult(data.k_iCallback, &data, sizeof(data)); - } - } - - friend_info->second.window_state &= ~window_state_join; - } - } - has_friend_action.pop(); - } - - // if variable == true, then set it to false and return true (because state was changed) in that case - bool yes_clicked = true; - if (invite_all_friends_clicked.compare_exchange_weak(yes_clicked, false)) { - PRINT_DEBUG("Steam_Overlay will send invitations to [%zu] friends if they're using the same app", friends.size()); - uint32 current_appid = settings->get_local_game_id().AppID(); - for (auto &fr : friends) { - if (fr.first.appid() == current_appid) { // friend is playing the same game - uint64 friend_id = (uint64)fr.first.id(); - invite_friend(friend_id, steamFriends, steamMatchmaking); - } - } - } -} - - void Steam_Overlay::SetupOverlay() { if (settings->disable_overlay) return; @@ -1838,14 +1667,14 @@ void Steam_Overlay::UnSetupOverlay() PRINT_DEBUG("releasing any images resources"); for (auto &ach : achievements) { - if (!ach.icon.first.expired()) { - _renderer->ReleaseImageResource(ach.icon.first); - ach.icon.first.reset(); + if (!ach.icon.expired()) { + _renderer->ReleaseImageResource(ach.icon); + ach.icon.reset(); } - if (!ach.icon_gray.first.expired()) { - _renderer->ReleaseImageResource(ach.icon_gray.first); - ach.icon_gray.first.reset(); + if (!ach.icon_gray.expired()) { + _renderer->ReleaseImageResource(ach.icon_gray); + ach.icon_gray.reset(); } } @@ -2062,4 +1891,232 @@ void Steam_Overlay::AddAchievementNotification(const std::string &ach_name, nloh } } + + +// -- steam run callbacks -- +void Steam_Overlay::steam_run_callback_update_my_lobby() +{ + std::lock_guard lock(global_mutex); + Steam_Friends* steamFriends = get_steam_client()->steam_friends; + if (std::string(steamFriends->get_friend_rich_presence_silent(settings->get_local_steam_id(), "connect")).length() > 0) { + i_have_lobby = true; + } else if (settings->get_lobby().IsValid()) { + i_have_lobby = true; + } else { + i_have_lobby = false; + } +} + +bool Steam_Overlay::is_friend_joinable(std::pair &f) +{ + PRINT_DEBUG("%" PRIu64 "", f.first.id()); + std::lock_guard lock(global_mutex); + Steam_Friends* steamFriends = get_steam_client()->steam_friends; + + if (std::string(steamFriends->get_friend_rich_presence_silent((uint64)f.first.id(), "connect")).length() > 0 ) { + PRINT_DEBUG("%" PRIu64 " true (connect string)", f.first.id()); + return true; + } + + FriendGameInfo_t friend_game_info{}; + steamFriends->GetFriendGamePlayed((uint64)f.first.id(), &friend_game_info); + if (friend_game_info.m_steamIDLobby.IsValid() && (f.second.window_state & window_state_lobby_invite)) { + PRINT_DEBUG("%" PRIu64 " true (friend in a game)", f.first.id()); + return true; + } + + PRINT_DEBUG("%" PRIu64 " false", f.first.id()); + return false; +} + +void Steam_Overlay::invite_friend(uint64 friend_id, class Steam_Friends* steamFriends, class Steam_Matchmaking* steamMatchmaking) +{ + std::string connect_str = steamFriends->get_friend_rich_presence_silent(settings->get_local_steam_id(), "connect"); + if (connect_str.length() > 0) { + steamFriends->InviteUserToGame(friend_id, connect_str.c_str()); + PRINT_DEBUG("sent game invitation to friend with id = %llu", friend_id); + } else if (settings->get_lobby().IsValid()) { + steamMatchmaking->InviteUserToLobby(settings->get_lobby(), friend_id); + PRINT_DEBUG("sent lobby invitation to friend with id = %llu", friend_id); + } +} + +void Steam_Overlay::steam_run_callback_friends_actions() +{ + Steam_Friends* steamFriends = get_steam_client()->steam_friends; + Steam_Matchmaking* steamMatchmaking = get_steam_client()->steam_matchmaking; + + std::for_each(friends.begin(), friends.end(), [this](std::pair &i) { + i.second.joinable = is_friend_joinable(i); + }); + + while (!has_friend_action.empty()) { + auto friend_info = friends.find(has_friend_action.front()); + if (friend_info != friends.end()) { + uint64 friend_id = (uint64)friend_info->first.id(); + // The user clicked on "Send" + if (friend_info->second.window_state & window_state_send_message) { + char* input = friend_info->second.chat_input; + char* end_input = input + strlen(input); + char* printable_char = std::find_if(input, end_input, [](char c) { return std::isgraph(c); }); + + // Check if the message contains something else than blanks + if (printable_char != end_input) { + // Handle chat send + Common_Message msg; + Steam_Messages* steam_messages = new Steam_Messages; + steam_messages->set_type(Steam_Messages::FRIEND_CHAT); + steam_messages->set_message(friend_info->second.chat_input); + msg.set_allocated_steam_messages(steam_messages); + msg.set_source_id(settings->get_local_steam_id().ConvertToUint64()); + msg.set_dest_id(friend_id); + network->sendTo(&msg, true); + + friend_info->second.chat_history.append(get_steam_client()->settings_client->get_local_name()).append(": ").append(input).append("\n", 1); + } + *input = 0; // Reset the input field + + friend_info->second.window_state &= ~window_state_send_message; + } + // The user clicked on "Invite" (but invite all wasn't clicked) + if (friend_info->second.window_state & window_state_invite) { + invite_friend(friend_id, steamFriends, steamMatchmaking); + + friend_info->second.window_state &= ~window_state_invite; + } + // The user clicked on "Join" + if (friend_info->second.window_state & window_state_join) { + std::string connect = steamFriends->get_friend_rich_presence_silent(friend_id, "connect"); + // The user got a lobby invite and accepted it + if (friend_info->second.window_state & window_state_lobby_invite) { + GameLobbyJoinRequested_t data; + data.m_steamIDLobby.SetFromUint64(friend_info->second.lobbyId); + data.m_steamIDFriend.SetFromUint64(friend_id); + callbacks->addCBResult(data.k_iCallback, &data, sizeof(data)); + + friend_info->second.window_state &= ~window_state_lobby_invite; + } else { + // The user got a rich presence invite and accepted it + if (friend_info->second.window_state & window_state_rich_invite) { + GameRichPresenceJoinRequested_t data = {}; + data.m_steamIDFriend.SetFromUint64(friend_id); + strncpy(data.m_rgchConnect, friend_info->second.connect, k_cchMaxRichPresenceValueLength - 1); + callbacks->addCBResult(data.k_iCallback, &data, sizeof(data)); + + friend_info->second.window_state &= ~window_state_rich_invite; + } else if (connect.length() > 0) { + GameRichPresenceJoinRequested_t data = {}; + data.m_steamIDFriend.SetFromUint64(friend_id); + strncpy(data.m_rgchConnect, connect.c_str(), k_cchMaxRichPresenceValueLength - 1); + callbacks->addCBResult(data.k_iCallback, &data, sizeof(data)); + } + + //Not sure about this but it fixes sonic racing transformed invites + FriendGameInfo_t friend_game_info = {}; + steamFriends->GetFriendGamePlayed(friend_id, &friend_game_info); + uint64 lobby_id = friend_game_info.m_steamIDLobby.ConvertToUint64(); + if (lobby_id) { + GameLobbyJoinRequested_t data; + data.m_steamIDLobby.SetFromUint64(lobby_id); + data.m_steamIDFriend.SetFromUint64(friend_id); + callbacks->addCBResult(data.k_iCallback, &data, sizeof(data)); + } + } + + friend_info->second.window_state &= ~window_state_join; + } + } + has_friend_action.pop(); + } + +} + +void Steam_Overlay::steam_run_callback() +{ + if (!Ready()) return; + + if (overlay_state_changed) { + overlay_state_changed = false; + + GameOverlayActivated_t data{}; + data.m_bActive = show_overlay; + data.m_bUserInitiated = true; + data.m_dwOverlayPID = 123; + data.m_nAppID = settings->get_local_game_id().AppID(); + callbacks->addCBResult(data.k_iCallback, &data, sizeof(data)); + } + + Steam_Friends* steamFriends = get_steam_client()->steam_friends; + Steam_Matchmaking* steamMatchmaking = get_steam_client()->steam_matchmaking; + + if (save_settings) { + save_settings = false; + + const char *language_text = valid_languages[current_language]; + save_global_settings(get_steam_client()->local_storage, username_text, language_text); + get_steam_client()->settings_client->set_local_name(username_text); + get_steam_client()->settings_server->set_local_name(username_text); + get_steam_client()->settings_client->set_language(language_text); + get_steam_client()->settings_server->set_language(language_text); + steamFriends->resend_friend_data(); + } + + steam_run_callback_update_my_lobby(); + + // if variable == true, then set it to false and return true (because state was changed) in that case + bool yes_clicked = true; + if (invite_all_friends_clicked.compare_exchange_weak(yes_clicked, false)) { + PRINT_DEBUG("Steam_Overlay will send invitations to [%zu] friends if they're using the same app", friends.size()); + uint32 current_appid = settings->get_local_game_id().AppID(); + for (auto &fr : friends) { + if (fr.first.appid() == current_appid) { // friend is playing the same game + uint64 friend_id = (uint64)fr.first.id(); + invite_friend(friend_id, steamFriends, steamMatchmaking); + } + } + } + + // don't wait to lock the overlay mutex + // * the overlay proc might be active and holding the overlay mutex + // * this steam callback will be blocked, but it has the global mutex locked + // * the overlay proc tries to lock the global mutex, but since we have it, it will be blocked forever + if (overlay_mutex.try_lock()) { + if (Ready()) { + // ============================================================== + // call steam callbacks that has to change the overlay state here + // ============================================================== + + steam_run_callback_friends_actions(); + } + + overlay_mutex.unlock(); + } +} + + + +// -- steam networking callbacks -- +void Steam_Overlay::networking_msg_received(Common_Message *msg) +{ + if (msg->has_steam_messages()) { + std::lock_guard lock(overlay_mutex); + + Friend frd; + frd.set_id(msg->source_id()); + auto friend_info = friends.find(frd); + if (friend_info != friends.end()) { + Steam_Messages const& steam_message = msg->steam_messages(); + // Change color to cyan for friend + friend_info->second.chat_history.append(friend_info->first.name() + ": " + steam_message.message()).append("\n", 1); + if (!(friend_info->second.window_state & window_state_show)) { + friend_info->second.window_state |= window_state_need_attention; + } + + add_chat_message_notification(friend_info->first.name() + ": " + steam_message.message()); + notify_sound_user_invite(friend_info->second); + } + } +} + + #endif diff --git a/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini b/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini index e76f2bd1..967297a3 100644 --- a/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini +++ b/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini @@ -59,13 +59,16 @@ stat_achievement_progress_functionality=1 # also has no impact on the functions which directly change stats, achievements, or achievements progress # default=1 save_only_higher_stat_achievement_progress=1 -# the emulator loads the achievements icons is memory -# this is needed for APIs like `ISteamUserStats::GetAchievementIcon()` -# the loaded icon size is controlled by [overlay::appearance] -> Icon_Size, in configs.overlay.ini -# 1=load icons lazily when they are requested -# 0=load icons as soon as the interface ISteamUserStats is initialized -# default=1 -lazy_load_achievements_icons=1 +# the emulator loads the achievements icons is memory, this is needed for APIs like `ISteamUserStats::GetAchievementIcon()` and the overlay +# the loaded icon size is controlled by [overlay::appearance] -> Icon_Size, in the file configs.overlay.ini +# this value controls how many icons to load each iteration when the periodic callback of the emu is triggered +# or when the app/game calls `SteamAPI_RunCallbacks()` +# each achievement has 2 icons, one when it's locked and another when it's unlocked, so a value of 10 means loading 20 icons +# increasing this value will cause a huge startup delay +# -1=disable this functionality (`ISteamUserStats::GetAchievementIcon()` and the overlay won't be able to use icons) +# 0=load the icon in memory only when it's requested +# default=10 +paginated_achievements_icons=10 [main::connectivity] # 1=prevent hooking OS networking APIs and allow any external requests diff --git a/post_build/steam_settings.EXAMPLE/configs.overlay.EXAMPLE.ini b/post_build/steam_settings.EXAMPLE/configs.overlay.EXAMPLE.ini index 55e81a5e..1531fc37 100644 --- a/post_build/steam_settings.EXAMPLE/configs.overlay.EXAMPLE.ini +++ b/post_build/steam_settings.EXAMPLE/configs.overlay.EXAMPLE.ini @@ -38,6 +38,12 @@ disable_warning_bad_appid=0 # 1=disable the local_save warning in the overlay # default=0 disable_warning_local_save=0 +# by default the overlay will attempt to upload the achievements icons to the GPU +# so that they are displayed, in rare cases this might keep failing and cause FPS drop +# 0=prevent the overlay from attempting to upload the icons periodically, +# in that case achievements icons win't be displayed +# default=1 +upload_achievements_icons_to_gpu=1 [overlay::appearance] # load custom TrueType font from a path, it could be absolute, or relative