From 86e420b9ce56db8b2126daa3ba48455e9c2bef38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 19 Oct 2026 18:51:10 +0200 Subject: [PATCH] EmuScreen: Show the loading spinner while a savestate saves or loads The boot spinner, in the same corner without the background, while a save, load or rewind is queued or in progress. Only after a quarter of a second, so it doesn't flash up on every quick save; on desktop that's rarely reached, but slow devices get some feedback. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/SaveState.cpp | 6 ++++++ Core/SaveState.h | 4 ++++ UI/EmuScreen.cpp | 18 +++++++++++++++++- UI/EmuScreen.h | 3 +++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index ff2c2ac433..9f042f2cea 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -454,6 +454,12 @@ int g_screenshotFailures; return !rewindStates.Empty(); } + bool IsBusy() { + std::lock_guard guard(mutex); + // A save leaves the queue once its snapshot is taken, but stays pending until it's written. + return !g_pendingOperations.empty() || !g_pendingSaves.empty(); + } + // Slot utilities std::string AppendSlotTitle(const std::string &filename, const std::string &title) { diff --git a/Core/SaveState.h b/Core/SaveState.h index c826d70332..4984c6a84d 100644 --- a/Core/SaveState.h +++ b/Core/SaveState.h @@ -112,6 +112,10 @@ namespace SaveState { // Returns true if there are rewind snapshots available. bool CanRewind(); + // Returns true while a save, load or rewind is queued or still in progress (rewind snapshots + // don't count). Saves are done once the file is written. + bool IsBusy(); + // Returns true if a savestate has been used during this session. bool HasLoadedState(); diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 8eb5a6efe6..93f6394d32 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1329,6 +1329,11 @@ void EmuScreen::CreateViews() { Spinner *loadingSpinner = root_->Add(new Spinner(symbols, ARRAY_SIZE(symbols), new AnchorLayoutParams(NONE, NONE, 70, 70, Centering::Both))); loadingSpinner_ = loadingSpinner; + // The same spinner in the same place, without the background. + saveStateSpinner_ = root_->Add(new Spinner(symbols, ARRAY_SIZE(symbols), new AnchorLayoutParams(NONE, NONE, 70, 70, Centering::Both))); + saveStateSpinner_->SetColor(alphaMul(0xFFFFFFFF, 0.7f)); + saveStateSpinner_->SetVisibility(V_GONE); + loadingBG->SetTag("LoadingBG"); loadingSpinner->SetTag("LoadingSpinner"); @@ -1417,6 +1422,17 @@ void EmuScreen::update() { releaseMapperInput_ = false; } + // Only once a savestate operation has taken a while, or it'd flash up on every quick save. + if (SaveState::IsBusy()) { + if (saveStateBusySince_ < 0.0) { + saveStateBusySince_ = time_now_d(); + } + } else { + saveStateBusySince_ = -1.0; + } + const bool showSaveStateSpinner = saveStateBusySince_ >= 0.0 && time_now_d() - saveStateBusySince_ > 0.25 && loadingSpinner_->GetVisibility() != V_VISIBLE; + saveStateSpinner_->SetVisibility(showSaveStateSpinner ? V_VISIBLE : V_GONE); + resumeButton_->SetVisibility(coreState == CoreState::CORE_RUNTIME_ERROR && Memory::MemFault_MayBeResumable() ? V_VISIBLE : V_GONE); resetButton_->SetVisibility(coreState == CoreState::CORE_RUNTIME_ERROR ? V_VISIBLE : V_GONE); backButton_->SetVisibility(coreState == CoreState::CORE_RUNTIME_ERROR ? V_VISIBLE : V_GONE); @@ -1853,7 +1869,7 @@ ScreenRenderFlags EmuScreen::RunEmulation(bool skipBufferEffects) { bool EmuScreen::hasVisibleUI() { // Regular but uncommon UI. - if (saveStatePreview_->GetVisibility() != UI::V_GONE || loadingSpinner_->GetVisibility() == UI::V_VISIBLE) + if (saveStatePreview_->GetVisibility() != UI::V_GONE || loadingSpinner_->GetVisibility() == UI::V_VISIBLE || saveStateSpinner_->GetVisibility() == UI::V_VISIBLE) return true; if (!g_OSD.IsEmpty() || g_Config.bShowTouchControls || g_Config.iShowStatusFlags != 0) return true; diff --git a/UI/EmuScreen.h b/UI/EmuScreen.h index 8b6d77e0d6..ca99064229 100644 --- a/UI/EmuScreen.h +++ b/UI/EmuScreen.h @@ -124,6 +124,9 @@ private: UI::CallbackColorTween *loadingViewColor_ = nullptr; UI::VisibilityTween *loadingViewVisible_ = nullptr; UI::Spinner *loadingSpinner_ = nullptr; + // Shown while a savestate is being saved or loaded, if that takes long enough to notice. + UI::Spinner *saveStateSpinner_ = nullptr; + double saveStateBusySince_ = -1.0; UI::Button *resumeButton_ = nullptr; UI::Button *resetButton_ = nullptr; UI::Button *backButton_ = nullptr;