diff --git a/CHANGELOG.md b/CHANGELOG.md index da48052d..0791e4c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # 2024/3/11 * manage overlay cursor input/clipping and internal frame processing in a better way, should prevent more games from pausing to display notifications -* allow notifications of these types to steal/obscure input: - - `notification_type_message` - - `notification_type_invite` +* load the icons of a single achievement each overlay callback invokation, will slow things down during startup + but this avoids having to load the achievement icon during gameplay which causes micro-stutter +* avoid loading and resizing the achievement icon each time it's unlocked +* Local_Storage: avoid allocating buffers unless `stbi_load()` was successfull --- diff --git a/dll/local_storage.cpp b/dll/local_storage.cpp index 6ad3fef9..ed5eaa0b 100644 --- a/dll/local_storage.cpp +++ b/dll/local_storage.cpp @@ -839,26 +839,25 @@ std::vector Local_Storage::load_image(std::string const& image_pa std::string Local_Storage::load_image_resized(std::string const& image_path, std::string const& image_data, int resolution) { std::string resized_image{}; - char *resized_img = (char*)malloc(sizeof(char) * resolution * resolution * 4); - PRINT_DEBUG("Local_Storage::load_image_resized: %s for resized image (%i)\n", (resized_img == nullptr ? "could not allocate memory" : "memory allocated"), (resolution * resolution * 4)); + const size_t resized_img_size = resolution * resolution * 4; - if (resized_img != nullptr) { - if (image_path.length() > 0) { - int width, height; - unsigned char *img = stbi_load(image_path.c_str(), &width, &height, nullptr, 4); - PRINT_DEBUG("Local_Storage::load_image_resized: \"%s\" %s\n", image_path.c_str(), (img == nullptr ? stbi_failure_reason() : "loaded")); - if (img != nullptr) { - stbir_resize_uint8(img, width, height, 0, (unsigned char*)resized_img, resolution, resolution, 0, 4); - resized_image = std::string(resized_img, resolution * resolution * 4); - stbi_image_free(img); - } - } else if (image_data.length() > 0) { - stbir_resize_uint8((unsigned char*)image_data.c_str(), 184, 184, 0, (unsigned char*)resized_img, resolution, resolution, 0, 4); - resized_image = std::string(resized_img, resolution * resolution * 4); + if (image_path.length() > 0) { + int width = 0; + int height = 0; + unsigned char *img = stbi_load(image_path.c_str(), &width, &height, nullptr, 4); + PRINT_DEBUG("Local_Storage::load_image_resized: stbi_load %s '%s'\n", (img == nullptr ? stbi_failure_reason() : "loaded"), image_path.c_str()); + if (img != nullptr) { + std::vector out_resized(resized_img_size); + stbir_resize_uint8(img, width, height, 0, (unsigned char*)&out_resized[0], resolution, resolution, 0, 4); + resized_image = std::string((char*)&out_resized[0], out_resized.size()); + stbi_image_free(img); } - free(resized_img); + } else if (image_data.length() > 0) { + std::vector out_resized(resized_img_size); + stbir_resize_uint8((unsigned char*)image_data.c_str(), 184, 184, 0, (unsigned char*)&out_resized[0], resolution, resolution, 0, 4); + resized_image = std::string((char*)&out_resized[0], out_resized.size()); } - + reset_LastError(); return resized_image; } diff --git a/overlay_experimental/overlay/steam_overlay.h b/overlay_experimental/overlay/steam_overlay.h index ba782132..48b17b5c 100644 --- a/overlay_experimental/overlay/steam_overlay.h +++ b/overlay_experimental/overlay/steam_overlay.h @@ -5,6 +5,13 @@ #include #include +#ifdef EMU_OVERLAY + +#include +#include +#include +#include "InGameOverlay/RendererHook.h" + static constexpr size_t max_chat_len = 768; enum window_state @@ -81,17 +88,12 @@ struct Overlay_Achievement std::weak_ptr icon; std::weak_ptr icon_gray; + // avoids spam loading on failure constexpr const static int ICON_LOAD_MAX_TRIALS = 3; uint8_t icon_load_trials = ICON_LOAD_MAX_TRIALS; uint8_t icon_gray_load_trials = ICON_LOAD_MAX_TRIALS; }; -#ifdef EMU_OVERLAY - -#include -#include -#include "InGameOverlay/RendererHook.h" - struct NotificationsIndexes { int top_left = 0, top_center = 0, top_right = 0; @@ -100,6 +102,8 @@ struct NotificationsIndexes class Steam_Overlay { + constexpr static const char ACH_FALLBACK_DIR[] = "achievement_images"; + Settings* settings; SteamCallResults* callback_results; SteamCallBacks* callbacks; @@ -110,13 +114,17 @@ class Steam_Overlay std::map friends; // avoids spam loading on failure - std::atomic load_achievements_trials = 3; + constexpr const static int LOAD_ACHIEVEMENTS_MAX_TRIALS = 3; + std::atomic load_achievements_trials = LOAD_ACHIEVEMENTS_MAX_TRIALS; bool is_ready = false; bool show_overlay; ENotificationPosition notif_position; int h_inset, v_inset; std::string show_url; std::vector achievements; + // index of the next achievement whose icons will be loaded + // used by the callback + int next_ach_to_load = 0; bool show_achievements, show_settings; // disable input when force_*.txt file is used @@ -205,6 +213,9 @@ class Steam_Overlay bool open_overlay_hook(bool toggle); + bool try_load_ach_icon(Overlay_Achievement &ach); + bool try_load_ach_gray_icon(Overlay_Achievement &ach); + public: Steam_Overlay(Settings* settings, 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 9b2430f9..89b9a62d 100644 --- a/overlay_experimental/steam_overlay.cpp +++ b/overlay_experimental/steam_overlay.cpp @@ -13,6 +13,8 @@ #include #include #include +#include + #include "InGameOverlay/ImGui/imgui.h" #include "dll/dll.h" @@ -565,23 +567,24 @@ bool Steam_Overlay::submit_notification(notification_type type, const std::strin notifications.emplace_back(notif); allow_renderer_frame_processing(true); - switch (type) { - // we want to steal focus for these ones - case notification_type_message: - case notification_type_invite: - obscure_cursor_input(true); - break; + // uncomment this block to obscure cursor input and steal focus for these specific notifications + // switch (type) { + // // we want to steal focus for these ones + // case notification_type_message: + // case notification_type_invite: + // obscure_cursor_input(true); + // break; - // not effective - case notification_type_achievement: - case notification_type_auto_accept_invite: - // nothing - break; + // // not effective + // case notification_type_achievement: + // case notification_type_auto_accept_invite: + // // nothing + // break; - default: - PRINT_DEBUG("Steam_Overlay::submit_notification error unhandled type %i\n", (int)type); - break; - } + // default: + // PRINT_DEBUG("Steam_Overlay::submit_notification error unhandled type %i\n", (int)type); + // break; + // } return true; } @@ -869,16 +872,16 @@ void Steam_Overlay::build_notifications(int width, int height) } // some extra window flags for each notification type - ImGuiWindowFlags extra_flags = 0; + ImGuiWindowFlags extra_flags = ImGuiWindowFlags_NoFocusOnAppearing; switch (it->type) { // games like "Mafia Definitive Edition" will pause the entire game/scene if focus was stolen // be less intrusive for notifications that do not require interaction case notification_type_achievement: case notification_type_auto_accept_invite: - extra_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs; + case notification_type_message: + extra_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoInputs; break; - case notification_type_message: case notification_type_invite: // nothing break; @@ -947,23 +950,24 @@ void Steam_Overlay::build_notifications(int width, int height) if ((now - item.start_time) > Notification::show_time) { PRINT_DEBUG("Steam_Overlay::build_notifications removing a notification\n"); allow_renderer_frame_processing(false); - switch (item.type) { - // we want to restore focus for these ones - case notification_type_message: - case notification_type_invite: - obscure_cursor_input(false); - break; + // uncomment this block to restore app input focus + // switch (item.type) { + // // we want to restore focus for these ones + // case notification_type_message: + // case notification_type_invite: + // obscure_cursor_input(false); + // break; - // not effective - case notification_type_achievement: - case notification_type_auto_accept_invite: - // nothing - break; + // // not effective + // case notification_type_achievement: + // case notification_type_auto_accept_invite: + // // nothing + // break; - default: - PRINT_DEBUG("Steam_Overlay::build_notifications error unhandled remove for type %i\n", (int)item.type); - break; - } + // default: + // PRINT_DEBUG("Steam_Overlay::build_notifications error unhandled remove for type %i\n", (int)item.type); + // break; + // } return true; } @@ -1019,6 +1023,62 @@ void Steam_Overlay::invite_friend(uint64 friend_id, class Steam_Friends* steamFr } } +bool Steam_Overlay::try_load_ach_icon(Overlay_Achievement &ach) +{ + if (!_renderer) return false; + if (!ach.icon.expired()) return true; + + if (ach.icon_load_trials && ach.icon_name.size()) { + --ach.icon_load_trials; + std::string file_path = std::move(Local_Storage::get_game_settings_path() + ach.icon_name); + unsigned long long file_size = file_size_(file_path); + if (!file_size) { + file_path = std::move(Local_Storage::get_game_settings_path() + Steam_Overlay::ACH_FALLBACK_DIR + "/" + ach.icon_name); + file_size = file_size_(file_path); + } + if (file_size) { + std::string img = Local_Storage::load_image_resized(file_path, "", settings->overlay_appearance.icon_size); + if (img.length() > 0) { + ach.icon = _renderer->CreateImageResource( + (void*)img.c_str(), + settings->overlay_appearance.icon_size, settings->overlay_appearance.icon_size); + if (!ach.icon.expired()) ach.icon_load_trials = Overlay_Achievement::ICON_LOAD_MAX_TRIALS; + PRINT_DEBUG("Steam_Overlay::try_load_ach_icon '%s' (result=%i)\n", ach.name.c_str(), (int)!ach.icon.expired()); + } + } + } + + return !ach.icon.expired(); +} + +bool Steam_Overlay::try_load_ach_gray_icon(Overlay_Achievement &ach) +{ + if (!_renderer) return false; + if (!ach.icon_gray.expired()) return true; + + if (ach.icon_gray_load_trials && ach.icon_gray_name.size()) { + --ach.icon_gray_load_trials; + std::string file_path = std::move(Local_Storage::get_game_settings_path() + ach.icon_gray_name); + unsigned long long file_size = file_size_(file_path); + if (!file_size) { + file_path = std::move(Local_Storage::get_game_settings_path() + Steam_Overlay::ACH_FALLBACK_DIR + "/" + ach.icon_gray_name); + file_size = file_size_(file_path); + } + if (file_size) { + std::string img = Local_Storage::load_image_resized(file_path, "", settings->overlay_appearance.icon_size); + if (img.length() > 0) { + ach.icon_gray = _renderer->CreateImageResource( + (void*)img.c_str(), + settings->overlay_appearance.icon_size, settings->overlay_appearance.icon_size); + if (!ach.icon_gray.expired()) ach.icon_gray_load_trials = Overlay_Achievement::ICON_LOAD_MAX_TRIALS; + PRINT_DEBUG("Steam_Overlay::try_load_ach_gray_icon '%s' (result=%i)\n", ach.name.c_str(), (int)!ach.icon_gray.expired()); + } + } + } + + return !ach.icon_gray.expired(); +} + // Try to make this function as short as possible or it might affect game's fps. void Steam_Overlay::overlay_proc() { @@ -1157,38 +1217,8 @@ void Steam_Overlay::overlay_proc() bool achieved = x.achieved; bool hidden = x.hidden && !achieved; - if (x.icon.expired() && x.icon_load_trials) { - --x.icon_load_trials; - std::string file_path = Local_Storage::get_game_settings_path() + x.icon_name; - unsigned long long file_size = file_size_(file_path); - if (!file_size) { - file_path = Local_Storage::get_game_settings_path() + "achievement_images/" + x.icon_name; - file_size = file_size_(file_path); - } - if (file_size) { - std::string img = Local_Storage::load_image_resized(file_path, "", settings->overlay_appearance.icon_size); - if (img.length() > 0) { - if (_renderer) x.icon = _renderer->CreateImageResource((void*)img.c_str(), settings->overlay_appearance.icon_size, settings->overlay_appearance.icon_size); - if (!x.icon.expired()) x.icon_load_trials = Overlay_Achievement::ICON_LOAD_MAX_TRIALS; - } - } - } - if (x.icon_gray.expired() && x.icon_gray_load_trials) { - --x.icon_gray_load_trials; - std::string file_path = Local_Storage::get_game_settings_path() + x.icon_gray_name; - unsigned long long file_size = file_size_(file_path); - if (!file_size) { - file_path = Local_Storage::get_game_settings_path() + "achievement_images/" + x.icon_gray_name; - file_size = file_size_(file_path); - } - if (file_size) { - std::string img = Local_Storage::load_image_resized(file_path, "", settings->overlay_appearance.icon_size); - if (img.length() > 0) { - if (_renderer) x.icon_gray = _renderer->CreateImageResource((void*)img.c_str(), settings->overlay_appearance.icon_size, settings->overlay_appearance.icon_size); - if (!x.icon_gray.expired()) x.icon_gray_load_trials = Overlay_Achievement::ICON_LOAD_MAX_TRIALS; - } - } - } + try_load_ach_icon(x); + try_load_ach_gray_icon(x); ImGui::Separator(); @@ -1397,8 +1427,15 @@ void Steam_Overlay::UnSetupOverlay() if (_renderer) { PRINT_DEBUG("Steam_Overlay::UnSetupOverlay will free any images resources\n"); for (auto &ach : achievements) { - if (!ach.icon.expired()) _renderer->ReleaseImageResource(ach.icon); - if (!ach.icon_gray.expired()) _renderer->ReleaseImageResource(ach.icon_gray); + if (!ach.icon.expired()) { + _renderer->ReleaseImageResource(ach.icon); + ach.icon.reset(); + } + + if (!ach.icon_gray.expired()) { + _renderer->ReleaseImageResource(ach.icon_gray); + ach.icon_gray.reset(); + } } _renderer = nullptr; @@ -1567,12 +1604,15 @@ void Steam_Overlay::AddAchievementNotification(nlohmann::json const& ach) std::lock_guard lock(overlay_mutex); if (!Ready()) return; + std::vector found_achs{}; { std::lock_guard lock2(global_mutex); std::string ach_name = ach.value("name", std::string()); for (auto &a : achievements) { if (a.name == ach_name) { + found_achs.push_back(&a); + bool achieved = false; uint32 unlock_time = 0; get_steam_client()->steam_user_stats->GetAchievementAndUnlockTime(a.name.c_str(), &achieved, &unlock_time); @@ -1583,32 +1623,16 @@ void Steam_Overlay::AddAchievementNotification(nlohmann::json const& ach) } if (!settings->disable_overlay_achievement_notification) { - // Load achievement image - std::weak_ptr icon_rsrc{}; - std::string icon_path = ach.value("icon", std::string()); - if (icon_path.size()) { - std::string file_path{}; - unsigned long long file_size = 0; - file_path = Local_Storage::get_game_settings_path() + icon_path; - file_size = file_size_(file_path); - if (!file_size) { - file_path = Local_Storage::get_game_settings_path() + "achievement_images/" + icon_path; - file_size = file_size_(file_path); - } - if (file_size) { - std::string img = Local_Storage::load_image_resized(file_path, "", settings->overlay_appearance.icon_size); - if (img.length() > 0) { - icon_rsrc = _renderer->CreateImageResource((void*)img.c_str(), settings->overlay_appearance.icon_size, settings->overlay_appearance.icon_size); - } - } + for (auto found_ach : found_achs) { + try_load_ach_icon(*found_ach); + submit_notification( + notification_type_achievement, + ach.value("displayName", std::string()) + "\n" + ach.value("description", std::string()), + {}, + found_ach->icon + ); } - - submit_notification( - notification_type_achievement, - ach.value("displayName", std::string()) + "\n" + ach.value("description", std::string()), - {}, - icon_rsrc - ); + notify_sound_user_achievement(); } } @@ -1648,7 +1672,7 @@ void Steam_Overlay::RunCallbacks() if (achievements_num) { PRINT_DEBUG("Steam_Overlay POPULATE OVERLAY ACHIEVEMENTS\n"); for (unsigned i = 0; i < achievements_num; ++i) { - Overlay_Achievement ach; + Overlay_Achievement ach{}; ach.name = steamUserStats->GetAchievementName(i); ach.title = steamUserStats->GetAchievementDisplayAttribute(ach.name.c_str(), "name"); ach.description = steamUserStats->GetAchievementDisplayAttribute(ach.name.c_str(), "desc"); @@ -1676,15 +1700,24 @@ void Steam_Overlay::RunCallbacks() } // don't punish successfull attempts - if (achievements.size()) { - ++load_achievements_trials; - } - PRINT_DEBUG("Steam_Overlay POPULATE OVERLAY ACHIEVEMENTS DONE\n"); + if (achievements.size()) load_achievements_trials = Steam_Overlay::LOAD_ACHIEVEMENTS_MAX_TRIALS; + PRINT_DEBUG("Steam_Overlay POPULATE OVERLAY ACHIEVEMENTS DONE (count=%lu, loaded=%zu)\n", achievements_num, achievements.size()); } } if (!Ready()) return; + // load images/icons for the next ach + if (next_ach_to_load < achievements.size()) { + try_load_ach_icon(achievements[next_ach_to_load]); + try_load_ach_gray_icon(achievements[next_ach_to_load]); + + ++next_ach_to_load; + // this allows the callback to keep trying forever in case the image resource was reset + // each icon has a limit though, so it won't slow things down forever + if (next_ach_to_load >= achievements.size()) next_ach_to_load = 0; + } + if (overlay_state_changed) { overlay_state_changed = false; diff --git a/post_build/steam_settings.EXAMPLE/overlay_appearance.EXAMPLE.txt b/post_build/steam_settings.EXAMPLE/overlay_appearance.EXAMPLE.txt index 83ade74f..a54a4a5e 100644 --- a/post_build/steam_settings.EXAMPLE/overlay_appearance.EXAMPLE.txt +++ b/post_build/steam_settings.EXAMPLE/overlay_appearance.EXAMPLE.txt @@ -1,9 +1,16 @@ +; global font size Font_Size 13.5 +; achievement icon size Icon_Size 64.0 +; spacing between characters Font_Glyph_Extra_Spacing_x 1.0 Font_Glyph_Extra_Spacing_y 0.0 +; increase these values by 1 if the font is blurry +Font_Oversample_H 1 +Font_Oversample_V 1 + Notification_R 0.16 Notification_G 0.29 Notification_B 0.48 diff --git a/post_build/win/ColdClientLoader.EXAMPLE/ColdClientLoader.EXAMPLE.ini b/post_build/win/ColdClientLoader.EXAMPLE/ColdClientLoader.EXAMPLE.ini deleted file mode 100644 index a9ff96d5..00000000 --- a/post_build/win/ColdClientLoader.EXAMPLE/ColdClientLoader.EXAMPLE.ini +++ /dev/null @@ -1,36 +0,0 @@ -# modified version of ColdClientLoader originally by Rat431 -[SteamClient] -# path to game exe, absolute or relative to the loader -Exe=my_app.exe -# empty means the folder of the exe -ExeRunDir= -# any additional args to pass, ex: -dx11, also any args passed to the loader will be passed to the app -ExeCommandLine= -# IMPORTANT -AppId=123 - -# path to the steamclient dlls, both must be set, -# absolute paths or relative to the loader -SteamClientDll=steamclient.dll -SteamClient64Dll=steamclient64.dll - -# force inject steamclient dll instead of waiting for the app to load it -ForceInjectSteamClient=0 - -[Debug] -# don't call `ResumeThread()` on the main thread after spawning the .exe -ResumeByDebugger=0 - -[Extra] -# path to a folder containing some dlls to inject into the app upon start -# this folder will be traversed recursively -# additionally, inside this folder you can create a file called `load_order.txt` and -# inside it, specify line by line the order of the dlls that have to be injected -# each line should be a relative path of the dll, relative to the injection folder -DllsToInjectFolder=extra_dlls.EXAMPLE -# don't display an error message when a dll injection fails -IgnoreInjectionError=0 -# don't display an error message if the architecture of the loader is different from the app -# this will result in a silent failure if a dll injection didn't succeed -# both the loader and the app must have the same arch for the injection to work -IgnoreLoaderArchDifference=0