From f410b7e6ea0ecbd2bfd2e0db5febea5bceb3c608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 10 Jul 2025 14:42:04 +0200 Subject: [PATCH] Show a tiny indicator in the top left of the screen when the game is saving or loading. --- Common/System/OSD.cpp | 11 ++++++++++- Common/System/OSD.h | 16 +++++++++++++++- Common/UI/Context.cpp | 5 +++++ Common/UI/Context.h | 1 + Core/Dialog/PSPSaveDialog.cpp | 23 +++++++++++++++-------- Core/Dialog/SavedataParam.cpp | 5 ++--- UI/EmuScreen.cpp | 3 ++- UI/OnScreenDisplay.cpp | 25 +++++++++++++++---------- 8 files changed, 65 insertions(+), 24 deletions(-) diff --git a/Common/System/OSD.cpp b/Common/System/OSD.cpp index 8f1ba11174..be8c967a5b 100644 --- a/Common/System/OSD.cpp +++ b/Common/System/OSD.cpp @@ -69,7 +69,7 @@ void OnScreenDisplay::ClickEntry(size_t index, double now) { } void OnScreenDisplay::Show(OSDType type, std::string_view text, std::string_view text2, std::string_view icon, float duration_s, const char *id) { - if (text.empty()) { + if (text.empty() && type != OSDType::STATUS_ICON) { // The user hacked the translation files to get rid of the message. Let's reward the dedication // by skipping it entirely. return; @@ -335,3 +335,12 @@ void OnScreenDisplay::SetClickCallback(const char *id, void (*callback)(bool, vo } } } + +void OnScreenDisplay::SetFlags(const char *id, OSDMessageFlags flags) { + for (auto &ent : entries_) { + // protect against dupes. + if (ent.id == id) { + ent.flags = flags; + } + } +} diff --git a/Common/System/OSD.h b/Common/System/OSD.h index 5797b76435..7df155636c 100644 --- a/Common/System/OSD.h +++ b/Common/System/OSD.h @@ -5,6 +5,8 @@ #include #include +#include "Common/Common.h" + // Shows a visible message to the user. // The default implementation in NativeApp.cpp uses our "osm" system (on screen messaging). enum class OSDType { @@ -29,11 +31,21 @@ enum class OSDType { PROGRESS_BAR, - TRANSPARENT_STATUS, // Use icons from the atlas + STATUS_ICON, // Use icons from the atlas VALUE_COUNT, }; +#undef None // X11, sigh. + +enum class OSDMessageFlags { + None = 0, + SpinLeft = 1, + SpinRight = 2, + Transparent = 4, +}; +ENUM_CLASS_BITOPS(OSDMessageFlags); + // Data holder for on-screen messages. class OnScreenDisplay { public: @@ -78,6 +90,7 @@ public: // Can't add an infinite number of "Show" functions, so starting to offer post-modification. void SetClickCallback(const char *id, void (*callback)(bool, void *), void *userdata); + void SetFlags(const char *id, OSDMessageFlags flag); struct Entry { OSDType type; @@ -86,6 +99,7 @@ public: std::string iconName; int numericID; std::string id; + OSDMessageFlags flags; // We could use std::function, but prefer to do it the oldschool way. void (*clickCallback)(bool, void *); diff --git a/Common/UI/Context.cpp b/Common/UI/Context.cpp index 6fcbd38649..ab996ba331 100644 --- a/Common/UI/Context.cpp +++ b/Common/UI/Context.cpp @@ -352,6 +352,11 @@ void UIContext::DrawImageVGradient(ImageID image, uint32_t color1, uint32_t colo uidrawbuffer_->DrawImageStretchVGradient(image, bounds.x, bounds.y, bounds.x2(), bounds.y2(), color1, color2); } +void UIContext::DrawImageRotated(ImageID atlas_image, float x, float y, float scale, float angle, uint32_t color, bool mirror_h) { + uidrawbuffer_->DrawImageRotated(atlas_image, x, y, scale, angle, color, mirror_h); +} + + void UIContext::PushTransform(const UITransform &transform) { Flush(); diff --git a/Common/UI/Context.h b/Common/UI/Context.h index bc4bd45f1d..bf0a912c85 100644 --- a/Common/UI/Context.h +++ b/Common/UI/Context.h @@ -95,6 +95,7 @@ public: void FillRect(const UI::Drawable &drawable, const Bounds &bounds); void DrawRectDropShadow(const Bounds &bounds, float radius, float alpha, uint32_t color = 0); void DrawImageVGradient(ImageID image, uint32_t color1, uint32_t color2, const Bounds &bounds); + void DrawImageRotated(ImageID atlas_image, float x, float y, float scale, float angle, uint32_t color, bool mirror_h); // in dps, like dp_xres and dp_yres void SetBounds(const Bounds &b) { bounds_ = b; } diff --git a/Core/Dialog/PSPSaveDialog.cpp b/Core/Dialog/PSPSaveDialog.cpp index 85502c7d56..c777541185 100755 --- a/Core/Dialog/PSPSaveDialog.cpp +++ b/Core/Dialog/PSPSaveDialog.cpp @@ -21,6 +21,7 @@ #include "Common/Data/Encoding/Utf8.h" #include "Common/Data/Text/I18n.h" +#include "Common/System/OSD.h" #include "Common/File/FileUtil.h" #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" @@ -1095,6 +1096,7 @@ int PSPSaveDialog::Update(int animSpeed) return 0; } +// It's kinda ugly how this uses the "global" 'display'... void PSPSaveDialog::ExecuteIOAction() { param.ClearSFOCache(); auto &result = param.GetPspParam()->common.result; @@ -1229,21 +1231,26 @@ void PSPSaveDialog::ExecuteNotVisibleIOAction() { param.ClearSFOCache(); } -static void DoExecuteIOAction(PSPSaveDialog *dialog) { - SetCurrentThreadName("SaveIO"); - - AndroidJNIThreadContext jniContext; - dialog->ExecuteIOAction(); -} - void PSPSaveDialog::StartIOThread() { if (ioThread.joinable()) { WARN_LOG_REPORT(Log::sceUtility, "Starting a save io thread when one already pending, uh oh."); ioThread.join(); } + // Show save indicator. It's strange how "display" is just as much an action as what to display. + if (display == DS_SAVE_SAVING || display == DS_LOAD_LOADING || display == DS_DELETE_DELETING) { + const bool left = display == DS_LOAD_LOADING; + g_OSD.Show(OSDType::STATUS_ICON, "", "", left ? "I_ROTATE_LEFT" : "I_ROTATE_RIGHT", 1.0f, "save_indicator"); + g_OSD.SetFlags("save_indicator", (left ? OSDMessageFlags::SpinLeft : OSDMessageFlags::SpinRight) | OSDMessageFlags::Transparent); + } + ioThreadStatus = SAVEIO_PENDING; - ioThread = std::thread(&DoExecuteIOAction, this); + ioThread = std::thread([this]() { + SetCurrentThreadName("SaveIO"); + + AndroidJNIThreadContext jniContext; + this->ExecuteIOAction(); + }); } int PSPSaveDialog::Shutdown(bool force) { diff --git a/Core/Dialog/SavedataParam.cpp b/Core/Dialog/SavedataParam.cpp index 2781be96ba..1e69ae5e3b 100644 --- a/Core/Dialog/SavedataParam.cpp +++ b/Core/Dialog/SavedataParam.cpp @@ -58,8 +58,7 @@ namespace void SetStringFromSFO(ParamSFOData &sfoFile, const char *name, char *str, int strLength) { - std::string value = sfoFile.GetValueString(name); - truncate_cpy(str, strLength, value.c_str()); + truncate_cpy(str, strLength, sfoFile.GetValueString(name)); } bool ReadPSPFile(const std::string &filename, u8 **data, s64 dataSize, s64 *readSize) @@ -79,7 +78,7 @@ namespace size_t result = pspFileSystem.ReadFile(handle, *data, dataSize); pspFileSystem.CloseFile(handle); - if(readSize) + if (readSize) *readSize = result; return result != 0; diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index f4614e941c..44e6c8f022 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -713,7 +713,8 @@ static void ShowFpsLimitNotice() { char temp[51]; snprintf(temp, sizeof(temp), "%d%%", (int)((float)fpsLimit / 60.0f * 100.0f)); - g_OSD.Show(OSDType::TRANSPARENT_STATUS, temp, "", "I_FASTFORWARD", 1.5f, "altspeed"); + g_OSD.Show(OSDType::STATUS_ICON, temp, "", "I_FASTFORWARD", 1.5f, "altspeed"); + g_OSD.SetFlags("altspeed", OSDMessageFlags::Transparent); } void EmuScreen::onVKey(VirtKey virtualKeyCode, bool down) { diff --git a/UI/OnScreenDisplay.cpp b/UI/OnScreenDisplay.cpp index a00a512f7d..48092f8d8f 100644 --- a/UI/OnScreenDisplay.cpp +++ b/UI/OnScreenDisplay.cpp @@ -118,12 +118,12 @@ static void MeasureOSDEntry(const UIContext &dc, const OnScreenDisplay::Entry &e } } -static void RenderNotice(UIContext &dc, Bounds bounds, float height1, NoticeLevel level, const std::string &text, const std::string &details, const std::string &iconName, int align, float alpha, bool transparent) { +static void RenderNotice(UIContext &dc, Bounds bounds, float height1, NoticeLevel level, const std::string &text, const std::string &details, const std::string &iconName, int align, float alpha, OSDMessageFlags flags, float timeVal) { UI::Drawable background = UI::Drawable(colorAlpha(GetNoticeBackgroundColor(level), alpha)); uint32_t foreGround = whiteAlpha(alpha); - if (!transparent) { + if (!(flags & OSDMessageFlags::Transparent)) { dc.DrawRectDropShadow(bounds, 12.0f, 0.7f * alpha); dc.FillRect(background, bounds); } @@ -153,7 +153,13 @@ static void RenderNotice(UIContext &dc, Bounds bounds, float height1, NoticeLeve // easily melts into the orange of warnings otherwise. dc.FillRect(UI::Drawable(0x50000000), iconBounds.Expand(2.0f)); } - dc.DrawImageVGradient(iconID, foreGround, foreGround, Bounds(bounds.x + 2.5f, bounds.y + 2.5f, iconW, iconH)); + + if (flags & (OSDMessageFlags::SpinLeft | OSDMessageFlags::SpinRight)) { + const float direction = (flags & OSDMessageFlags::SpinLeft) ? -1.5f : 1.5f; + dc.DrawImageRotated(iconID, bounds.x + 2.5f + iconW * 0.5f, bounds.y + 2.5f + iconW * 0.5f, 1.0f, direction * time_now_d(), foreGround, false); + } else { + dc.DrawImageVGradient(iconID, foreGround, foreGround, Bounds(bounds.x + 2.5f, bounds.y + 2.5f, iconW, iconH)); + } } } @@ -168,7 +174,7 @@ static void RenderNotice(UIContext &dc, Bounds bounds, float height1, NoticeLeve if (!details.empty()) { Bounds bottomTextBounds = bounds.Inset(3.0f, height1 + 5.0f, 3.0f, 3.0f); - if (!transparent) { + if (!(flags & OSDMessageFlags::Transparent)) { UI::Drawable backgroundDark = UI::Drawable(colorAlpha(darkenColor(GetNoticeBackgroundColor(level)), alpha)); dc.FillRect(backgroundDark, bottomTextBounds); } @@ -178,7 +184,7 @@ static void RenderNotice(UIContext &dc, Bounds bounds, float height1, NoticeLeve dc.SetFontScale(1.0f, 1.0f); } -static void RenderOSDEntry(UIContext &dc, const OnScreenDisplay::Entry &entry, Bounds bounds, float height1, int align, float alpha) { +static void RenderOSDEntry(UIContext &dc, const OnScreenDisplay::Entry &entry, Bounds bounds, float height1, int align, float alpha, float now) { if (entry.type == OSDType::ACHIEVEMENT_UNLOCKED) { const rc_client_achievement_t * achievement = rc_client_get_achievement_info(Achievements::GetClient(), entry.numericID); if (achievement) { @@ -186,8 +192,7 @@ static void RenderOSDEntry(UIContext &dc, const OnScreenDisplay::Entry &entry, B } return; } else { - bool transparent = entry.type == OSDType::TRANSPARENT_STATUS; - RenderNotice(dc, bounds, height1, GetNoticeLevel(entry.type), entry.text, entry.text2, entry.iconName, align, alpha, transparent); + RenderNotice(dc, bounds, height1, GetNoticeLevel(entry.type), entry.text, entry.text2, entry.iconName, align, alpha, entry.flags, now - entry.startTime); } } @@ -309,7 +314,7 @@ void OnScreenMessagesView::Draw(UIContext &dc) { typeEdges[(size_t)OSDType::ACHIEVEMENT_UNLOCKED] = (ScreenEdgePosition)g_Config.iAchievementsUnlockedPos; typeEdges[(size_t)OSDType::MESSAGE_CENTERED_WARNING] = ScreenEdgePosition::CENTER; typeEdges[(size_t)OSDType::MESSAGE_CENTERED_ERROR] = ScreenEdgePosition::CENTER; - typeEdges[(size_t)OSDType::TRANSPARENT_STATUS] = ScreenEdgePosition::TOP_LEFT; + typeEdges[(size_t)OSDType::STATUS_ICON] = ScreenEdgePosition::TOP_LEFT; typeEdges[(size_t)OSDType::PROGRESS_BAR] = ScreenEdgePosition::TOP_CENTER; // These only function at the top currently, needs fixing. dc.SetFontStyle(dc.theme->uiFont); @@ -471,7 +476,7 @@ void OnScreenMessagesView::Draw(UIContext &dc) { } float alpha = Clamp((float)(entry.endTime - now) * 4.0f, 0.0f, 1.0f); - RenderOSDEntry(dc, entry, b, measuredEntry.h1, measuredEntry.align, alpha); + RenderOSDEntry(dc, entry, b, measuredEntry.h1, measuredEntry.align, alpha, now); switch (entry.type) { case OSDType::MESSAGE_INFO: @@ -584,6 +589,6 @@ void NoticeView::GetContentDimensionsBySpec(const UIContext &dc, UI::MeasureSpec void NoticeView::Draw(UIContext &dc) { dc.PushScissor(bounds_); - RenderNotice(dc, bounds_, height1_, level_, text_, detailsText_, iconName_, 0, 1.0f, false); + RenderNotice(dc, bounds_, height1_, level_, text_, detailsText_, iconName_, 0, 1.0f, OSDMessageFlags::None, 0.0f); dc.PopScissor(); }