* 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
This commit is contained in:
otavepto 2024-03-12 21:21:09 +02:00 committed by otavepto
parent 37426bac82
commit 0b86464374
6 changed files with 175 additions and 160 deletions

View File

@ -1,9 +1,10 @@
# 2024/3/11 # 2024/3/11
* manage overlay cursor input/clipping and internal frame processing in a better way, * manage overlay cursor input/clipping and internal frame processing in a better way,
should prevent more games from pausing to display notifications should prevent more games from pausing to display notifications
* allow notifications of these types to steal/obscure input: * load the icons of a single achievement each overlay callback invokation, will slow things down during startup
- `notification_type_message` but this avoids having to load the achievement icon during gameplay which causes micro-stutter
- `notification_type_invite` * avoid loading and resizing the achievement icon each time it's unlocked
* Local_Storage: avoid allocating buffers unless `stbi_load()` was successfull
--- ---

View File

@ -839,26 +839,25 @@ std::vector<image_pixel_t> 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 Local_Storage::load_image_resized(std::string const& image_path, std::string const& image_data, int resolution)
{ {
std::string resized_image{}; std::string resized_image{};
char *resized_img = (char*)malloc(sizeof(char) * resolution * resolution * 4); const size_t resized_img_size = 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));
if (resized_img != nullptr) { if (image_path.length() > 0) {
if (image_path.length() > 0) { int width = 0;
int width, height; int height = 0;
unsigned char *img = stbi_load(image_path.c_str(), &width, &height, nullptr, 4); 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")); 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) { if (img != nullptr) {
stbir_resize_uint8(img, width, height, 0, (unsigned char*)resized_img, resolution, resolution, 0, 4); std::vector<char> out_resized(resized_img_size);
resized_image = std::string(resized_img, resolution * resolution * 4); stbir_resize_uint8(img, width, height, 0, (unsigned char*)&out_resized[0], resolution, resolution, 0, 4);
stbi_image_free(img); resized_image = std::string((char*)&out_resized[0], out_resized.size());
} 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);
} }
free(resized_img); } else if (image_data.length() > 0) {
std::vector<char> 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(); reset_LastError();
return resized_image; return resized_image;
} }

View File

@ -5,6 +5,13 @@
#include <map> #include <map>
#include <queue> #include <queue>
#ifdef EMU_OVERLAY
#include <future>
#include <atomic>
#include <memory>
#include "InGameOverlay/RendererHook.h"
static constexpr size_t max_chat_len = 768; static constexpr size_t max_chat_len = 768;
enum window_state enum window_state
@ -81,17 +88,12 @@ struct Overlay_Achievement
std::weak_ptr<uint64_t> icon; std::weak_ptr<uint64_t> icon;
std::weak_ptr<uint64_t> icon_gray; std::weak_ptr<uint64_t> icon_gray;
// avoids spam loading on failure
constexpr const static int ICON_LOAD_MAX_TRIALS = 3; constexpr const static int ICON_LOAD_MAX_TRIALS = 3;
uint8_t icon_load_trials = ICON_LOAD_MAX_TRIALS; uint8_t icon_load_trials = ICON_LOAD_MAX_TRIALS;
uint8_t icon_gray_load_trials = ICON_LOAD_MAX_TRIALS; uint8_t icon_gray_load_trials = ICON_LOAD_MAX_TRIALS;
}; };
#ifdef EMU_OVERLAY
#include <future>
#include <atomic>
#include "InGameOverlay/RendererHook.h"
struct NotificationsIndexes struct NotificationsIndexes
{ {
int top_left = 0, top_center = 0, top_right = 0; int top_left = 0, top_center = 0, top_right = 0;
@ -100,6 +102,8 @@ struct NotificationsIndexes
class Steam_Overlay class Steam_Overlay
{ {
constexpr static const char ACH_FALLBACK_DIR[] = "achievement_images";
Settings* settings; Settings* settings;
SteamCallResults* callback_results; SteamCallResults* callback_results;
SteamCallBacks* callbacks; SteamCallBacks* callbacks;
@ -110,13 +114,17 @@ class Steam_Overlay
std::map<Friend, friend_window_state, Friend_Less> friends; std::map<Friend, friend_window_state, Friend_Less> friends;
// avoids spam loading on failure // avoids spam loading on failure
std::atomic<int32_t> load_achievements_trials = 3; constexpr const static int LOAD_ACHIEVEMENTS_MAX_TRIALS = 3;
std::atomic<int32_t> load_achievements_trials = LOAD_ACHIEVEMENTS_MAX_TRIALS;
bool is_ready = false; bool is_ready = false;
bool show_overlay; bool show_overlay;
ENotificationPosition notif_position; ENotificationPosition notif_position;
int h_inset, v_inset; int h_inset, v_inset;
std::string show_url; std::string show_url;
std::vector<Overlay_Achievement> achievements; std::vector<Overlay_Achievement> 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; bool show_achievements, show_settings;
// disable input when force_*.txt file is used // disable input when force_*.txt file is used
@ -205,6 +213,9 @@ class Steam_Overlay
bool open_overlay_hook(bool toggle); bool open_overlay_hook(bool toggle);
bool try_load_ach_icon(Overlay_Achievement &ach);
bool try_load_ach_gray_icon(Overlay_Achievement &ach);
public: public:
Steam_Overlay(Settings* settings, SteamCallResults* callback_results, SteamCallBacks* callbacks, RunEveryRunCB* run_every_runcb, Networking *network); Steam_Overlay(Settings* settings, SteamCallResults* callback_results, SteamCallBacks* callbacks, RunEveryRunCB* run_every_runcb, Networking *network);

View File

@ -13,6 +13,8 @@
#include <string> #include <string>
#include <sstream> #include <sstream>
#include <cctype> #include <cctype>
#include <utility>
#include "InGameOverlay/ImGui/imgui.h" #include "InGameOverlay/ImGui/imgui.h"
#include "dll/dll.h" #include "dll/dll.h"
@ -565,23 +567,24 @@ bool Steam_Overlay::submit_notification(notification_type type, const std::strin
notifications.emplace_back(notif); notifications.emplace_back(notif);
allow_renderer_frame_processing(true); allow_renderer_frame_processing(true);
switch (type) { // uncomment this block to obscure cursor input and steal focus for these specific notifications
// we want to steal focus for these ones // switch (type) {
case notification_type_message: // // we want to steal focus for these ones
case notification_type_invite: // case notification_type_message:
obscure_cursor_input(true); // case notification_type_invite:
break; // obscure_cursor_input(true);
// break;
// not effective // // not effective
case notification_type_achievement: // case notification_type_achievement:
case notification_type_auto_accept_invite: // case notification_type_auto_accept_invite:
// nothing // // nothing
break; // break;
default: // default:
PRINT_DEBUG("Steam_Overlay::submit_notification error unhandled type %i\n", (int)type); // PRINT_DEBUG("Steam_Overlay::submit_notification error unhandled type %i\n", (int)type);
break; // break;
} // }
return true; return true;
} }
@ -869,16 +872,16 @@ void Steam_Overlay::build_notifications(int width, int height)
} }
// some extra window flags for each notification type // some extra window flags for each notification type
ImGuiWindowFlags extra_flags = 0; ImGuiWindowFlags extra_flags = ImGuiWindowFlags_NoFocusOnAppearing;
switch (it->type) { switch (it->type) {
// games like "Mafia Definitive Edition" will pause the entire game/scene if focus was stolen // 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 // be less intrusive for notifications that do not require interaction
case notification_type_achievement: case notification_type_achievement:
case notification_type_auto_accept_invite: 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; break;
case notification_type_message:
case notification_type_invite: case notification_type_invite:
// nothing // nothing
break; break;
@ -947,23 +950,24 @@ void Steam_Overlay::build_notifications(int width, int height)
if ((now - item.start_time) > Notification::show_time) { if ((now - item.start_time) > Notification::show_time) {
PRINT_DEBUG("Steam_Overlay::build_notifications removing a notification\n"); PRINT_DEBUG("Steam_Overlay::build_notifications removing a notification\n");
allow_renderer_frame_processing(false); allow_renderer_frame_processing(false);
switch (item.type) { // uncomment this block to restore app input focus
// we want to restore focus for these ones // switch (item.type) {
case notification_type_message: // // we want to restore focus for these ones
case notification_type_invite: // case notification_type_message:
obscure_cursor_input(false); // case notification_type_invite:
break; // obscure_cursor_input(false);
// break;
// not effective // // not effective
case notification_type_achievement: // case notification_type_achievement:
case notification_type_auto_accept_invite: // case notification_type_auto_accept_invite:
// nothing // // nothing
break; // break;
default: // default:
PRINT_DEBUG("Steam_Overlay::build_notifications error unhandled remove for type %i\n", (int)item.type); // PRINT_DEBUG("Steam_Overlay::build_notifications error unhandled remove for type %i\n", (int)item.type);
break; // break;
} // }
return true; 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. // Try to make this function as short as possible or it might affect game's fps.
void Steam_Overlay::overlay_proc() void Steam_Overlay::overlay_proc()
{ {
@ -1157,38 +1217,8 @@ void Steam_Overlay::overlay_proc()
bool achieved = x.achieved; bool achieved = x.achieved;
bool hidden = x.hidden && !achieved; bool hidden = x.hidden && !achieved;
if (x.icon.expired() && x.icon_load_trials) { try_load_ach_icon(x);
--x.icon_load_trials; try_load_ach_gray_icon(x);
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;
}
}
}
ImGui::Separator(); ImGui::Separator();
@ -1397,8 +1427,15 @@ void Steam_Overlay::UnSetupOverlay()
if (_renderer) { if (_renderer) {
PRINT_DEBUG("Steam_Overlay::UnSetupOverlay will free any images resources\n"); PRINT_DEBUG("Steam_Overlay::UnSetupOverlay will free any images resources\n");
for (auto &ach : achievements) { for (auto &ach : achievements) {
if (!ach.icon.expired()) _renderer->ReleaseImageResource(ach.icon); if (!ach.icon.expired()) {
if (!ach.icon_gray.expired()) _renderer->ReleaseImageResource(ach.icon_gray); _renderer->ReleaseImageResource(ach.icon);
ach.icon.reset();
}
if (!ach.icon_gray.expired()) {
_renderer->ReleaseImageResource(ach.icon_gray);
ach.icon_gray.reset();
}
} }
_renderer = nullptr; _renderer = nullptr;
@ -1567,12 +1604,15 @@ void Steam_Overlay::AddAchievementNotification(nlohmann::json const& ach)
std::lock_guard<std::recursive_mutex> lock(overlay_mutex); std::lock_guard<std::recursive_mutex> lock(overlay_mutex);
if (!Ready()) return; if (!Ready()) return;
std::vector<Overlay_Achievement*> found_achs{};
{ {
std::lock_guard<std::recursive_mutex> lock2(global_mutex); std::lock_guard<std::recursive_mutex> lock2(global_mutex);
std::string ach_name = ach.value("name", std::string()); std::string ach_name = ach.value("name", std::string());
for (auto &a : achievements) { for (auto &a : achievements) {
if (a.name == ach_name) { if (a.name == ach_name) {
found_achs.push_back(&a);
bool achieved = false; bool achieved = false;
uint32 unlock_time = 0; uint32 unlock_time = 0;
get_steam_client()->steam_user_stats->GetAchievementAndUnlockTime(a.name.c_str(), &achieved, &unlock_time); 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) { if (!settings->disable_overlay_achievement_notification) {
// Load achievement image for (auto found_ach : found_achs) {
std::weak_ptr<uint64_t> icon_rsrc{}; try_load_ach_icon(*found_ach);
std::string icon_path = ach.value("icon", std::string()); submit_notification(
if (icon_path.size()) { notification_type_achievement,
std::string file_path{}; ach.value("displayName", std::string()) + "\n" + ach.value("description", std::string()),
unsigned long long file_size = 0; {},
file_path = Local_Storage::get_game_settings_path() + icon_path; found_ach->icon
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);
}
}
} }
submit_notification(
notification_type_achievement,
ach.value("displayName", std::string()) + "\n" + ach.value("description", std::string()),
{},
icon_rsrc
);
notify_sound_user_achievement(); notify_sound_user_achievement();
} }
} }
@ -1648,7 +1672,7 @@ void Steam_Overlay::RunCallbacks()
if (achievements_num) { if (achievements_num) {
PRINT_DEBUG("Steam_Overlay POPULATE OVERLAY ACHIEVEMENTS\n"); PRINT_DEBUG("Steam_Overlay POPULATE OVERLAY ACHIEVEMENTS\n");
for (unsigned i = 0; i < achievements_num; ++i) { for (unsigned i = 0; i < achievements_num; ++i) {
Overlay_Achievement ach; Overlay_Achievement ach{};
ach.name = steamUserStats->GetAchievementName(i); ach.name = steamUserStats->GetAchievementName(i);
ach.title = steamUserStats->GetAchievementDisplayAttribute(ach.name.c_str(), "name"); ach.title = steamUserStats->GetAchievementDisplayAttribute(ach.name.c_str(), "name");
ach.description = steamUserStats->GetAchievementDisplayAttribute(ach.name.c_str(), "desc"); ach.description = steamUserStats->GetAchievementDisplayAttribute(ach.name.c_str(), "desc");
@ -1676,15 +1700,24 @@ void Steam_Overlay::RunCallbacks()
} }
// don't punish successfull attempts // don't punish successfull attempts
if (achievements.size()) { if (achievements.size()) load_achievements_trials = Steam_Overlay::LOAD_ACHIEVEMENTS_MAX_TRIALS;
++load_achievements_trials; PRINT_DEBUG("Steam_Overlay POPULATE OVERLAY ACHIEVEMENTS DONE (count=%lu, loaded=%zu)\n", achievements_num, achievements.size());
}
PRINT_DEBUG("Steam_Overlay POPULATE OVERLAY ACHIEVEMENTS DONE\n");
} }
} }
if (!Ready()) return; 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) { if (overlay_state_changed) {
overlay_state_changed = false; overlay_state_changed = false;

View File

@ -1,9 +1,16 @@
; global font size
Font_Size 13.5 Font_Size 13.5
; achievement icon size
Icon_Size 64.0 Icon_Size 64.0
; spacing between characters
Font_Glyph_Extra_Spacing_x 1.0 Font_Glyph_Extra_Spacing_x 1.0
Font_Glyph_Extra_Spacing_y 0.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_R 0.16
Notification_G 0.29 Notification_G 0.29
Notification_B 0.48 Notification_B 0.48

View File

@ -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