From 764875ddbfd7860565cbdc775c333d20bba5938e Mon Sep 17 00:00:00 2001 From: chaoticgd <43898262+chaoticgd@users.noreply.github.com> Date: Sun, 30 Nov 2025 20:15:57 +0000 Subject: [PATCH] Qt: Add setting to show state load errors using a dialog or OSD message --- pcsx2-qt/MainWindow.cpp | 53 +++++++++++++++++ pcsx2-qt/MainWindow.h | 1 + pcsx2-qt/QtHost.cpp | 12 +++- pcsx2-qt/Settings/InterfaceSettingsWidget.cpp | 15 ++++- pcsx2-qt/Settings/InterfaceSettingsWidget.h | 2 + pcsx2-qt/Settings/InterfaceSettingsWidget.ui | 10 +++- pcsx2/Hotkeys.cpp | 3 +- pcsx2/ImGui/FullscreenUI.cpp | 57 +++++++++++++++---- pcsx2/ImGui/FullscreenUI.h | 2 + pcsx2/ImGui/ImGuiOverlays.cpp | 6 +- pcsx2/PINE.cpp | 5 +- pcsx2/SaveState.cpp | 22 +++++++ pcsx2/SaveState.h | 3 + 13 files changed, 166 insertions(+), 25 deletions(-) diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index c4c122591b..de168f745b 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -37,6 +37,7 @@ #include "pcsx2/PerformanceMetrics.h" #include "pcsx2/Recording/InputRecording.h" #include "pcsx2/Recording/InputRecordingControls.h" +#include "pcsx2/SaveState.h" #include "pcsx2/SIO/Sio.h" #include "pcsx2/GS/GSExtra.h" @@ -1213,6 +1214,58 @@ void MainWindow::onStatusMessage(const QString& message) m_ui.statusBar->showMessage(message); } +void MainWindow::reportStateLoadError(const QString& message, std::optional slot, bool backup) +{ + const bool prompt_on_error = Host::GetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", true); + if (!prompt_on_error) + { + SaveState_ReportLoadErrorOSD(message.toStdString(), slot, backup); + return; + } + + QString title; + if (slot.has_value()) + { + if (backup) + title = tr("Failed to Load State From Backup Slot %1").arg(*slot); + else + title = tr("Failed to Load State From Slot %1").arg(*slot); + } + else + { + title = tr("Failed to Load State"); + } + + VMLock lock(pauseAndLockVM()); + + QCheckBox* do_not_show_again = new QCheckBox(tr("Do not show again")); + + QPointer message_box = new QMessageBox(this); + message_box->setWindowTitle(title); + message_box->setText(message); + message_box->setIcon(QMessageBox::Critical); + message_box->addButton(QMessageBox::Ok); + message_box->setDefaultButton(QMessageBox::Ok); + message_box->setCheckBox(do_not_show_again); + + message_box->exec(); + if (message_box.isNull()) + return; + + if (do_not_show_again->isChecked()) + { + Host::SetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", false); + Host::CommitBaseSettingChanges(); + if (m_settings_window) + { + InterfaceSettingsWidget* interface_settings = m_settings_window->getInterfaceSettingsWidget(); + interface_settings->updatePromptOnStateLoadSaveFailureCheckbox(Qt::Unchecked); + } + } + + delete message_box; +} + void MainWindow::runOnUIThread(const std::function& func) { func(); diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index 0f5a4ed6b6..b64e939e96 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -120,6 +120,7 @@ public Q_SLOTS: void reportError(const QString& title, const QString& message); bool confirmMessage(const QString& title, const QString& message); void onStatusMessage(const QString& message); + void reportStateLoadError(const QString& message, std::optional slot, bool backup); void runOnUIThread(const std::function& func); void requestReset(); diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index be20ffef58..c60efb318e 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -299,7 +299,11 @@ void EmuThread::loadState(const QString& filename) Error error; if (!VMManager::LoadState(filename.toUtf8().constData(), &error)) - Host::ReportErrorAsync(TRANSLATE_SV("QtHost", "Failed to Load State"), error.GetDescription()); + { + QtHost::RunOnUIThread([message = QString::fromStdString(error.GetDescription())]() { + g_main_window->reportStateLoadError(message, std::nullopt, false); + }); + } } void EmuThread::loadStateFromSlot(qint32 slot, bool load_backup) @@ -315,7 +319,11 @@ void EmuThread::loadStateFromSlot(qint32 slot, bool load_backup) Error error; if (!VMManager::LoadStateFromSlot(slot, load_backup, &error)) - Host::ReportErrorAsync(TRANSLATE_SV("QtHost", "Failed to Load State"), error.GetDescription()); + { + QtHost::RunOnUIThread([message = QString::fromStdString(error.GetDescription()), slot, load_backup]() { + g_main_window->reportStateLoadError(message, slot, load_backup); + }); + } } void EmuThread::saveState(const QString& filename) diff --git a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp index 7cde58068a..66406c0a2b 100644 --- a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp +++ b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp @@ -97,15 +97,16 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* settings_dialog SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.confirmShutdown, "UI", "ConfirmShutdown", true); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnFocusLoss, "UI", "PauseOnFocusLoss", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnControllerDisconnection, "UI", "PauseOnControllerDisconnection", false); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.promptOnStateLoadSaveFailure, "UI", "PromptOnStateLoadSaveFailure", true); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.discordPresence, "EmuCore", "EnableDiscordPresence", false); -#ifdef __linux__ // Mouse locking is only supported on X11 +#ifdef __linux__ // Mouse locking is only supported on X11 const bool mouse_lock_supported = QGuiApplication::platformName().toLower() == "xcb"; #else const bool mouse_lock_supported = true; #endif - if(mouse_lock_supported) + if (mouse_lock_supported) { SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.mouseLock, "EmuCore", "EnableMouseLock", false); connect(m_ui.mouseLock, &QCheckBox::checkStateChanged, [](Qt::CheckState state) { @@ -196,6 +197,8 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* settings_dialog "and unpauses when you switch back.")); dialog()->registerWidgetHelp(m_ui.pauseOnControllerDisconnection, tr("Pause On Controller Disconnection"), tr("Unchecked"), tr("Pauses the emulator when a controller with bindings is disconnected.")); + dialog()->registerWidgetHelp(m_ui.promptOnStateLoadSaveFailure, tr("Pause On State Load/Save Failure"), + tr("Checked"), tr("Display a modal dialog when a save state load/save operation fails.")); dialog()->registerWidgetHelp(m_ui.startFullscreen, tr("Start Fullscreen"), tr("Unchecked"), tr("Automatically switches to fullscreen mode when a game is started.")); dialog()->registerWidgetHelp(m_ui.hideMouseCursor, tr("Hide Cursor In Fullscreen"), tr("Unchecked"), @@ -224,7 +227,7 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* settings_dialog dialog()->registerWidgetHelp( m_ui.backgroundBrowse, tr("Game List Background"), tr("None"), tr("Enable an animated/static background on the game list (where you launch your games).
" - "This background is only visible in the library and will be hidden once a game is launched. It will also be paused when it's not in focus.")); + "This background is only visible in the library and will be hidden once a game is launched. It will also be paused when it's not in focus.")); dialog()->registerWidgetHelp( m_ui.backgroundReset, tr("Disable/Reset Game List Background"), tr("None"), tr("Disable and reset the currently applied game list background.")); @@ -241,6 +244,12 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* settings_dialog InterfaceSettingsWidget::~InterfaceSettingsWidget() = default; +void InterfaceSettingsWidget::updatePromptOnStateLoadSaveFailureCheckbox(Qt::CheckState state) +{ + QSignalBlocker blocker(m_ui.promptOnStateLoadSaveFailure); + m_ui.promptOnStateLoadSaveFailure->setCheckState(state); +} + void InterfaceSettingsWidget::onRenderToSeparateWindowChanged() { m_ui.hideMainWindow->setEnabled(m_ui.renderToSeparateWindow->isChecked()); diff --git a/pcsx2-qt/Settings/InterfaceSettingsWidget.h b/pcsx2-qt/Settings/InterfaceSettingsWidget.h index 9c205263cb..ffdd8e370d 100644 --- a/pcsx2-qt/Settings/InterfaceSettingsWidget.h +++ b/pcsx2-qt/Settings/InterfaceSettingsWidget.h @@ -15,6 +15,8 @@ public: InterfaceSettingsWidget(SettingsWindow* settings_dialog, QWidget* parent); ~InterfaceSettingsWidget(); + void updatePromptOnStateLoadSaveFailureCheckbox(Qt::CheckState state); + Q_SIGNALS: void themeChanged(); void languageChanged(); diff --git a/pcsx2-qt/Settings/InterfaceSettingsWidget.ui b/pcsx2-qt/Settings/InterfaceSettingsWidget.ui index 319f0366ca..a89ee159a5 100644 --- a/pcsx2-qt/Settings/InterfaceSettingsWidget.ui +++ b/pcsx2-qt/Settings/InterfaceSettingsWidget.ui @@ -7,7 +7,7 @@ 0 0 725 - 625 + 637 @@ -78,6 +78,13 @@ + + + + Prompt On State Load/Save Failure + + + @@ -380,6 +387,7 @@ discordPresence pauseOnControllerDisconnection mouseLock + promptOnStateLoadSaveFailure startFullscreen doubleClickTogglesFullscreen renderToSeparateWindow diff --git a/pcsx2/Hotkeys.cpp b/pcsx2/Hotkeys.cpp index f9d2f942f1..e1ffa825c6 100644 --- a/pcsx2/Hotkeys.cpp +++ b/pcsx2/Hotkeys.cpp @@ -106,8 +106,7 @@ static void HotkeyLoadStateSlot(s32 slot) Error error; if (!VMManager::LoadStateFromSlot(slot, false, &error)) - Host::AddIconOSDMessage("LoadStateFromSlot", ICON_FA_TRIANGLE_EXCLAMATION, - error.GetDescription(), Host::OSD_INFO_DURATION); + FullscreenUI::ReportStateLoadError(error.GetDescription(), slot, false); }); } diff --git a/pcsx2/ImGui/FullscreenUI.cpp b/pcsx2/ImGui/FullscreenUI.cpp index 19333cf91f..72bb53f215 100644 --- a/pcsx2/ImGui/FullscreenUI.cpp +++ b/pcsx2/ImGui/FullscreenUI.cpp @@ -670,7 +670,7 @@ namespace FullscreenUI static void DrawSaveStateSelector(bool is_loading); static bool OpenLoadStateSelectorForGameResume(const GameList::Entry* entry); static void DrawResumeStateSelector(); - static void DoLoadState(std::string path); + static void DoLoadState(std::string path, std::optional slot, bool backup); static std::vector s_save_state_selector_slots; static std::string s_save_state_selector_game_path; @@ -4129,6 +4129,8 @@ void FullscreenUI::DrawInterfaceSettingsPage() FSUI_CSTR("Pauses the emulator when a controller with bindings is disconnected."), "UI", "PauseOnControllerDisconnection", false); DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_RECTANGLE_LIST, "Pause On Menu"), FSUI_CSTR("Pauses the emulator when you open the quick menu, and unpauses when you close it."), "UI", "PauseOnMenu", true); + DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_FLOPPY_DISK, "Prompt On State Load/Save Failure"), + FSUI_CSTR("Display a modal dialog when a save state load/save operation fails."), "UI", "PromptOnStateLoadSaveFailure", true); DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_POWER_OFF, "Confirm Shutdown"), FSUI_CSTR("Determines whether a prompt will be displayed to confirm shutting down the emulator/game when the hotkey is pressed."), "UI", "ConfirmShutdown", true); @@ -7345,7 +7347,7 @@ void FullscreenUI::DrawSaveStateSelector(bool is_loading) false, is_loading ? !Achievements::IsHardcoreModeActive() : true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) { if (is_loading) - DoLoadState(std::move(entry.path)); + DoLoadState(std::move(entry.path), entry.slot, false); else Host::RunOnCPUThread([slot = entry.slot]() { VMManager::SaveStateToSlot(slot); }); @@ -7483,7 +7485,7 @@ void FullscreenUI::DrawSaveStateSelector(bool is_loading) if (pressed) { if (is_loading) - DoLoadState(entry.path); + DoLoadState(entry.path, entry.slot, false); else Host::RunOnCPUThread([slot = entry.slot]() { VMManager::SaveStateToSlot(slot); }); @@ -7639,19 +7641,16 @@ void FullscreenUI::DrawResumeStateSelector() } } -void FullscreenUI::DoLoadState(std::string path) +void FullscreenUI::DoLoadState(std::string path, std::optional slot, bool backup) { - Host::RunOnCPUThread([boot_path = s_save_state_selector_game_path, path = std::move(path)]() { + std::string boot_path = s_save_state_selector_game_path; + Host::RunOnCPUThread([boot_path = std::move(boot_path), path = std::move(path), slot, backup]() { if (VMManager::HasValidVM()) { Error error; if (!VMManager::LoadState(path.c_str(), &error)) { - MTGS::RunOnGSThread([error = std::move(error)]() { - ImGuiFullscreen::OpenInfoMessageDialog( - FSUI_ICONSTR(ICON_FA_TRIANGLE_EXCLAMATION, "Failed to Load State"), - error.GetDescription()); - }); + ReportStateLoadError(error.GetDescription(), slot, backup); return; } @@ -9169,6 +9168,44 @@ void FullscreenUI::DrawAchievementsSettingsPage(std::unique_lock& se EndMenuButtons(); } +void FullscreenUI::ReportStateLoadError(std::string message, std::optional slot, bool backup) +{ + MTGS::RunOnGSThread([message = std::move(message), slot, backup]() { + const bool prompt_on_error = Host::GetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", true); + if (!prompt_on_error || !ImGuiManager::InitializeFullscreenUI()) + { + SaveState_ReportLoadErrorOSD(message, slot, backup); + return; + } + + std::string title; + if (slot.has_value()) + { + if (backup) + title = fmt::format(FSUI_FSTR("Failed to Load State From Backup Slot {}"), *slot); + else + title = fmt::format(FSUI_FSTR("Failed to Load State From Slot {}"), *slot); + } + else + { + title = FSUI_STR("Failed to Load State"); + } + + ImGuiFullscreen::InfoMessageDialogCallback callback; + if (VMManager::GetState() == VMState::Running) + { + Host::RunOnCPUThread([]() { VMManager::SetPaused(true); }); + callback = []() { + Host::RunOnCPUThread([]() { VMManager::SetPaused(false); }); + }; + } + + ImGuiFullscreen::OpenInfoMessageDialog( + fmt::format("{} {}", ICON_FA_TRIANGLE_EXCLAMATION, title), + std::move(message), std::move(callback)); + }); +} + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // Translation String Area // To avoid having to type T_RANSLATE("FullscreenUI", ...) everywhere, we use the shorter macros at the top diff --git a/pcsx2/ImGui/FullscreenUI.h b/pcsx2/ImGui/FullscreenUI.h index 3bdbb5eda3..97ef456a2f 100644 --- a/pcsx2/ImGui/FullscreenUI.h +++ b/pcsx2/ImGui/FullscreenUI.h @@ -10,6 +10,7 @@ #include #include #include +#include struct Pcsx2Config; @@ -26,6 +27,7 @@ namespace FullscreenUI void OpenPauseMenu(); bool OpenAchievementsWindow(); bool OpenLeaderboardsWindow(); + void ReportStateLoadError(std::string message, std::optional slot, bool backup); // NOTE: Only call from GS thread. bool IsAchievementsWindowOpen(); diff --git a/pcsx2/ImGui/ImGuiOverlays.cpp b/pcsx2/ImGui/ImGuiOverlays.cpp index 71a89fd98c..d0aeca3490 100644 --- a/pcsx2/ImGui/ImGuiOverlays.cpp +++ b/pcsx2/ImGui/ImGuiOverlays.cpp @@ -1382,8 +1382,7 @@ void SaveStateSelectorUI::LoadCurrentSlot() Host::RunOnCPUThread([slot = GetCurrentSlot()]() { Error error; if (!VMManager::LoadStateFromSlot(slot, false, &error)) - Host::AddIconOSDMessage("LoadStateFromSlot", ICON_FA_TRIANGLE_EXCLAMATION, - error.GetDescription(), Host::OSD_INFO_DURATION); + FullscreenUI::ReportStateLoadError(error.GetDescription(), slot, false); }); Close(); } @@ -1393,8 +1392,7 @@ void SaveStateSelectorUI::LoadCurrentBackupSlot() Host::RunOnCPUThread([slot = GetCurrentSlot()]() { Error error; if (!VMManager::LoadStateFromSlot(slot, true, &error)) - Host::AddIconOSDMessage("LoadStateFromSlot", ICON_FA_TRIANGLE_EXCLAMATION, - error.GetDescription(), Host::OSD_INFO_DURATION); + FullscreenUI::ReportStateLoadError(error.GetDescription(), slot, true); }); Close(); } diff --git a/pcsx2/PINE.cpp b/pcsx2/PINE.cpp index 7e4133671a..bb23a88101 100644 --- a/pcsx2/PINE.cpp +++ b/pcsx2/PINE.cpp @@ -6,6 +6,7 @@ #include "Host.h" #include "Memory.h" #include "Elfheader.h" +#include "SaveState.h" #include "PINE.h" #include "VMManager.h" #include "common/Error.h" @@ -19,7 +20,6 @@ #include #include "fmt/format.h" -#include "IconsFontAwesome6.h" #if defined(_WIN32) #define read_portable(a, b, c) (recv(a, (char*)b, c, 0)) @@ -659,8 +659,7 @@ PINEServer::IPCBuffer PINEServer::ParseCommand(std::span buf, std::vector(buf, buf_cnt)] { Error state_error; if (!VMManager::LoadStateFromSlot(slot, false, &state_error)) - Host::AddIconOSDMessage("LoadStateFromSlot", ICON_FA_TRIANGLE_EXCLAMATION, - state_error.GetDescription(), Host::OSD_INFO_DURATION); + SaveState_ReportLoadErrorOSD(state_error.GetDescription(), slot, false); }); buf_cnt += 1; break; diff --git a/pcsx2/SaveState.cpp b/pcsx2/SaveState.cpp index 5744d68719..b8c591bd76 100644 --- a/pcsx2/SaveState.cpp +++ b/pcsx2/SaveState.cpp @@ -38,6 +38,7 @@ #include "common/StringUtil.h" #include "common/ZipHelpers.h" +#include "IconsFontAwesome6.h" #include "fmt/format.h" #include @@ -1231,3 +1232,24 @@ bool SaveState_UnzipFromDisk(const std::string& filename, Error* error) PostLoadPrep(); return true; } + +void SaveState_ReportLoadErrorOSD(const std::string& message, std::optional slot, bool backup) +{ + std::string full_message; + if (slot.has_value()) + { + if (backup) + full_message = fmt::format( + TRANSLATE_FS("SaveState", "Failed to load state from slot {}: {}"), *slot, message); + else + full_message = fmt::format( + TRANSLATE_FS("SaveState", "Failed to load state from backup slot {}: {}"), *slot, message); + } + else + { + full_message = fmt::format(TRANSLATE_FS("SaveState", "Failed to load state: {}"), message); + } + + Host::AddIconOSDMessage("LoadState", ICON_FA_TRIANGLE_EXCLAMATION, + full_message, Host::OSD_WARNING_DURATION); +} diff --git a/pcsx2/SaveState.h b/pcsx2/SaveState.h index f073919477..6b32a81b25 100644 --- a/pcsx2/SaveState.h +++ b/pcsx2/SaveState.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -353,3 +354,5 @@ public: void FreezeMem(void* data, int size) override; bool IsSaving() const override { return false; } }; + +void SaveState_ReportLoadErrorOSD(const std::string& message, std::optional slot, bool backup);