diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index de168f745b..3ee4dd9988 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -1266,6 +1266,51 @@ void MainWindow::reportStateLoadError(const QString& message, std::optional delete message_box; } +void MainWindow::reportStateSaveError(const QString& message, std::optional slot) +{ + const bool prompt_on_error = Host::GetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", true); + if (!prompt_on_error) + { + SaveState_ReportSaveErrorOSD(message.toStdString(), slot); + return; + } + + QString title; + if (slot.has_value()) + title = tr("Failed to Save State To Slot %1").arg(*slot); + else + title = tr("Failed to Save 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 b64e939e96..4da2fa9133 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -121,6 +121,7 @@ public Q_SLOTS: bool confirmMessage(const QString& title, const QString& message); void onStatusMessage(const QString& message); void reportStateLoadError(const QString& message, std::optional slot, bool backup); + void reportStateSaveError(const QString& message, std::optional slot); void runOnUIThread(const std::function& func); void requestReset(); diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index c60efb318e..e367756d15 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -337,11 +337,11 @@ void EmuThread::saveState(const QString& filename) if (!VMManager::HasValidVM()) return; - if (!VMManager::SaveState(filename.toUtf8().constData())) - { - // this one is usually the result of a user-chosen path, so we can display a message box safely here - Console.Error("Failed to save state"); - } + VMManager::SaveState(filename.toUtf8().constData(), true, false, [](const std::string& error) { + QtHost::RunOnUIThread([message = QString::fromStdString(error)]() { + g_main_window->reportStateSaveError(message, std::nullopt); + }); + }); } void EmuThread::saveStateToSlot(qint32 slot) @@ -355,7 +355,11 @@ void EmuThread::saveStateToSlot(qint32 slot) if (!VMManager::HasValidVM()) return; - VMManager::SaveStateToSlot(slot); + VMManager::SaveStateToSlot(slot, true, [slot](const std::string& error) { + QtHost::RunOnUIThread([message = QString::fromStdString(error), slot]() { + g_main_window->reportStateSaveError(message, slot); + }); + }); } void EmuThread::run() diff --git a/pcsx2/Hotkeys.cpp b/pcsx2/Hotkeys.cpp index e1ffa825c6..6a7a7327bb 100644 --- a/pcsx2/Hotkeys.cpp +++ b/pcsx2/Hotkeys.cpp @@ -112,7 +112,9 @@ static void HotkeyLoadStateSlot(s32 slot) static void HotkeySaveStateSlot(s32 slot) { - VMManager::SaveStateToSlot(slot); + VMManager::SaveStateToSlot(slot, true, [slot](const std::string& error) { + FullscreenUI::ReportStateSaveError(error, slot); + }); } static bool CanPause() diff --git a/pcsx2/ImGui/FullscreenUI.cpp b/pcsx2/ImGui/FullscreenUI.cpp index 72bb53f215..8d2add5d54 100644 --- a/pcsx2/ImGui/FullscreenUI.cpp +++ b/pcsx2/ImGui/FullscreenUI.cpp @@ -671,6 +671,7 @@ namespace FullscreenUI static bool OpenLoadStateSelectorForGameResume(const GameList::Entry* entry); static void DrawResumeStateSelector(); static void DoLoadState(std::string path, std::optional slot, bool backup); + static void DoSaveState(s32 slot); static std::vector s_save_state_selector_slots; static std::string s_save_state_selector_game_path; @@ -7349,7 +7350,7 @@ void FullscreenUI::DrawSaveStateSelector(bool is_loading) if (is_loading) DoLoadState(std::move(entry.path), entry.slot, false); else - Host::RunOnCPUThread([slot = entry.slot]() { VMManager::SaveStateToSlot(slot); }); + DoSaveState(entry.slot); CloseSaveStateSelector(); ReturnToMainWindow(); @@ -7487,7 +7488,7 @@ void FullscreenUI::DrawSaveStateSelector(bool is_loading) if (is_loading) DoLoadState(entry.path, entry.slot, false); else - Host::RunOnCPUThread([slot = entry.slot]() { VMManager::SaveStateToSlot(slot); }); + DoSaveState(entry.slot); CloseSaveStateSelector(); ReturnToMainWindow(); @@ -7667,6 +7668,15 @@ void FullscreenUI::DoLoadState(std::string path, std::optional slot, bool b }); } +void FullscreenUI::DoSaveState(s32 slot) +{ + Host::RunOnCPUThread([slot]() { + VMManager::SaveStateToSlot(slot, true, [slot](const std::string& error) { + ReportStateSaveError(error, slot); + }); + }); +} + void FullscreenUI::PopulateGameListEntryList() { const int sort = Host::GetBaseIntSettingValue("UI", "FullscreenUIGameSort", 0); @@ -9168,9 +9178,9 @@ void FullscreenUI::DrawAchievementsSettingsPage(std::unique_lock& se EndMenuButtons(); } -void FullscreenUI::ReportStateLoadError(std::string message, std::optional slot, bool backup) +void FullscreenUI::ReportStateLoadError(const std::string& message, std::optional slot, bool backup) { - MTGS::RunOnGSThread([message = std::move(message), slot, backup]() { + MTGS::RunOnGSThread([message, slot, backup]() { const bool prompt_on_error = Host::GetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", true); if (!prompt_on_error || !ImGuiManager::InitializeFullscreenUI()) { @@ -9206,6 +9216,37 @@ void FullscreenUI::ReportStateLoadError(std::string message, std::optional }); } +void FullscreenUI::ReportStateSaveError(const std::string& message, std::optional slot) +{ + MTGS::RunOnGSThread([message, slot]() { + const bool prompt_on_error = Host::GetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", true); + if (!prompt_on_error || !ImGuiManager::InitializeFullscreenUI()) + { + SaveState_ReportSaveErrorOSD(message, slot); + return; + } + + std::string title; + if (slot.has_value()) + title = fmt::format(FSUI_FSTR("Failed to Save State To Slot {}"), *slot); + else + title = FSUI_STR("Failed to Save 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 97ef456a2f..a0afb7acef 100644 --- a/pcsx2/ImGui/FullscreenUI.h +++ b/pcsx2/ImGui/FullscreenUI.h @@ -27,7 +27,8 @@ namespace FullscreenUI void OpenPauseMenu(); bool OpenAchievementsWindow(); bool OpenLeaderboardsWindow(); - void ReportStateLoadError(std::string message, std::optional slot, bool backup); + void ReportStateLoadError(const std::string& message, std::optional slot, bool backup); + void ReportStateSaveError(const std::string& message, std::optional slot); // NOTE: Only call from GS thread. bool IsAchievementsWindowOpen(); diff --git a/pcsx2/ImGui/ImGuiOverlays.cpp b/pcsx2/ImGui/ImGuiOverlays.cpp index d0aeca3490..9a8bad41f4 100644 --- a/pcsx2/ImGui/ImGuiOverlays.cpp +++ b/pcsx2/ImGui/ImGuiOverlays.cpp @@ -1400,7 +1400,9 @@ void SaveStateSelectorUI::LoadCurrentBackupSlot() void SaveStateSelectorUI::SaveCurrentSlot() { Host::RunOnCPUThread([slot = GetCurrentSlot()]() { - VMManager::SaveStateToSlot(slot); + VMManager::SaveStateToSlot(slot, true, [slot](const std::string& error) { + FullscreenUI::ReportStateSaveError(error, slot); + }); }); Close(); } diff --git a/pcsx2/PINE.cpp b/pcsx2/PINE.cpp index bb23a88101..f8d251cf88 100644 --- a/pcsx2/PINE.cpp +++ b/pcsx2/PINE.cpp @@ -646,7 +646,11 @@ PINEServer::IPCBuffer PINEServer::ParseCommand(std::span buf, std::vector(buf, buf_cnt)] { VMManager::SaveStateToSlot(slot); }); + Host::RunOnCPUThread([slot = FromSpan(buf, buf_cnt)] { + VMManager::SaveStateToSlot(slot, true, [slot](const std::string& error) { + SaveState_ReportSaveErrorOSD(error, slot); + }); + }); buf_cnt += 1; break; } diff --git a/pcsx2/Recording/InputRecording.cpp b/pcsx2/Recording/InputRecording.cpp index fe2c6f5bbd..84c04c1097 100644 --- a/pcsx2/Recording/InputRecording.cpp +++ b/pcsx2/Recording/InputRecording.cpp @@ -56,8 +56,9 @@ bool InputRecording::create(const std::string& fileName, const bool fromSaveStat m_initial_load_complete = true; m_watching_for_rerecords = true; setStartingFrame(g_FrameCount); - // TODO - error handling - VMManager::SaveState(savestatePath.c_str()); + VMManager::SaveState(savestatePath.c_str(), true, false, [](const std::string& error) { + SaveState_ReportSaveErrorOSD(error, std::nullopt); + }); } else { @@ -395,8 +396,7 @@ void InputRecording::InformGSThread() TinyString frame_data_message = TinyString::from_format(TRANSLATE_FS("InputRecording", "Frame: {}/{} ({})"), g_InputRecording.getFrameCounter(), g_InputRecording.getData().getTotalFrames(), g_InputRecording.getFrameCounterStateless()); TinyString undo_count_message = TinyString::from_format(TRANSLATE_FS("InputRecording", "Undo Count: {}"), g_InputRecording.getData().getUndoCount()); - MTGS::RunOnGSThread([recording_active_message, frame_data_message, undo_count_message](bool is_recording = g_InputRecording.getControls().isRecording()) - { + MTGS::RunOnGSThread([recording_active_message, frame_data_message, undo_count_message](bool is_recording = g_InputRecording.getControls().isRecording()) { g_InputRecordingData.is_recording = is_recording; g_InputRecordingData.recording_active_message = recording_active_message; g_InputRecordingData.frame_data_message = frame_data_message; diff --git a/pcsx2/SaveState.cpp b/pcsx2/SaveState.cpp index b8c591bd76..2c37e2605c 100644 --- a/pcsx2/SaveState.cpp +++ b/pcsx2/SaveState.cpp @@ -1253,3 +1253,16 @@ void SaveState_ReportLoadErrorOSD(const std::string& message, std::optional Host::AddIconOSDMessage("LoadState", ICON_FA_TRIANGLE_EXCLAMATION, full_message, Host::OSD_WARNING_DURATION); } + +void SaveState_ReportSaveErrorOSD(const std::string& message, std::optional slot) +{ + std::string full_message; + if (slot.has_value()) + full_message = fmt::format( + TRANSLATE_FS("SaveState", "Failed to save state to slot {}: {}"), *slot, message); + else + full_message = fmt::format(TRANSLATE_FS("SaveState", "Failed to save state: {}"), message); + + Host::AddIconOSDMessage("SaveState", ICON_FA_TRIANGLE_EXCLAMATION, + full_message, Host::OSD_WARNING_DURATION); +} diff --git a/pcsx2/SaveState.h b/pcsx2/SaveState.h index 6b32a81b25..43d1e6e5ef 100644 --- a/pcsx2/SaveState.h +++ b/pcsx2/SaveState.h @@ -356,3 +356,4 @@ public: }; void SaveState_ReportLoadErrorOSD(const std::string& message, std::optional slot, bool backup); +void SaveState_ReportSaveErrorOSD(const std::string& message, std::optional slot); diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index 7f6ec2cf9b..b34e2671c7 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -115,13 +115,13 @@ namespace VMManager static std::string GetCurrentSaveStateFileName(s32 slot, bool backup = false); static bool DoLoadState(const char* filename, Error* error = nullptr); - static bool DoSaveState(const char* filename, s32 slot_for_message, bool zip_on_thread, bool backup_old_state); + static void DoSaveState(const char* filename, s32 slot_for_message, bool zip_on_thread, bool backup_old_state, std::function error_callback); static void ZipSaveState(std::unique_ptr elist, - std::unique_ptr screenshot, std::string osd_key, const char* filename, - s32 slot_for_message); + std::unique_ptr screenshot, const char* filename, + s32 slot_for_message, std::function error_callback); static void ZipSaveStateOnThread(std::unique_ptr elist, - std::unique_ptr screenshot, std::string osd_key, std::string filename, - s32 slot_for_message); + std::unique_ptr screenshot, std::string filename, + s32 slot_for_message, std::function error_callback); static void LoadSettings(); static void LoadCoreSettings(SettingsInterface& si); @@ -1617,8 +1617,13 @@ void VMManager::Shutdown(bool save_resume_state) if (!GSDumpReplayer::IsReplayingDump() && save_resume_state) { std::string resume_file_name(GetCurrentSaveStateFileName(-1)); - if (!resume_file_name.empty() && !DoSaveState(resume_file_name.c_str(), -1, true, false)) - Console.Error("Failed to save resume state"); + if (!resume_file_name.empty()) + { + DoSaveState(resume_file_name.c_str(), -1, true, false, [](const std::string& error) { + Host::AddIconOSDMessage("SaveResumeState", ICON_FA_TRIANGLE_EXCLAMATION, + fmt::format(TRANSLATE_FS("VMManager", "Failed to save resume state: {}"), error), Host::OSD_QUICK_DURATION); + }); + } } // end input recording before clearing state @@ -1829,7 +1834,7 @@ bool VMManager::DoLoadState(const char* filename, Error* error) { if (GSDumpReplayer::IsReplayingDump()) { - Error::SetString(error, TRANSLATE_STR("VMManager", "Cannot load save state while replaying GS dump.")); + Error::SetString(error, TRANSLATE_STR("VMManager", "Cannot load state while replaying GS dump.")); return false; } @@ -1849,21 +1854,21 @@ bool VMManager::DoLoadState(const char* filename, Error* error) return true; } -bool VMManager::DoSaveState(const char* filename, s32 slot_for_message, bool zip_on_thread, bool backup_old_state) +void VMManager::DoSaveState(const char* filename, s32 slot_for_message, bool zip_on_thread, bool backup_old_state, std::function error_callback) { if (GSDumpReplayer::IsReplayingDump()) - return false; + { + error_callback(TRANSLATE_STR("VMManager", "Cannot save state while replaying GS dump.")); + return; + } - std::string osd_key(fmt::format("SaveStateSlot{}", slot_for_message)); Error error; - std::unique_ptr elist = SaveState_DownloadState(&error); if (!elist) { - Host::AddIconOSDMessage(std::move(osd_key), ICON_FA_TRIANGLE_EXCLAMATION, - fmt::format(TRANSLATE_FS("VMManager", "Failed to save state: {}."), error.GetDescription()), - Host::OSD_ERROR_DURATION); - return false; + error_callback(fmt::format( + TRANSLATE_FS("VMManager", "Failed to save state: {}."), error.GetDescription())); + return; } std::unique_ptr screenshot = SaveState_SaveScreenshot(); @@ -1874,10 +1879,10 @@ bool VMManager::DoSaveState(const char* filename, s32 slot_for_message, bool zip Console.WriteLn(fmt::format("Creating save state backup {}...", backup_filename)); if (!FileSystem::RenamePath(filename, backup_filename.c_str())) { - Host::AddIconOSDMessage(osd_key, ICON_FA_TRIANGLE_EXCLAMATION, - fmt::format( - TRANSLATE_FS("VMManager", "Failed to back up old save state {}."), Path::GetFileName(filename)), - Host::OSD_ERROR_DURATION); + error_callback(fmt::format( + TRANSLATE_FS("VMManager", "Cannot back up old save state '{}'."), + Path::GetFileName(filename))); + return; } } @@ -1886,21 +1891,22 @@ bool VMManager::DoSaveState(const char* filename, s32 slot_for_message, bool zip // lock order here is important; the thread could exit before we resume here. std::unique_lock lock(s_save_state_threads_mutex); s_save_state_threads.emplace_back(&VMManager::ZipSaveStateOnThread, std::move(elist), std::move(screenshot), - std::move(osd_key), std::string(filename), slot_for_message); + std::string(filename), slot_for_message, std::move(error_callback)); } else { - ZipSaveState(std::move(elist), std::move(screenshot), std::move(osd_key), filename, slot_for_message); + ZipSaveState( + std::move(elist), std::move(screenshot), filename, slot_for_message, std::move(error_callback)); } Host::OnSaveStateSaved(filename); MemcardBusy::CheckSaveStateDependency(); - return true; + return; } void VMManager::ZipSaveState(std::unique_ptr elist, - std::unique_ptr screenshot, std::string osd_key, const char* filename, - s32 slot_for_message) + std::unique_ptr screenshot, const char* filename, + s32 slot_for_message, std::function error_callback) { Common::Timer timer; @@ -1908,26 +1914,26 @@ void VMManager::ZipSaveState(std::unique_ptr elist, { if (slot_for_message >= 0 && VMManager::HasValidVM()) { - Host::AddIconOSDMessage(std::move(osd_key), ICON_FA_FLOPPY_DISK, + Host::AddIconOSDMessage("SaveState", ICON_FA_FLOPPY_DISK, fmt::format(TRANSLATE_FS("VMManager", "State saved to slot {}."), slot_for_message), Host::OSD_QUICK_DURATION); } } else { - Host::AddIconOSDMessage(std::move(osd_key), ICON_FA_TRIANGLE_EXCLAMATION, - fmt::format(TRANSLATE_FS("VMManager", "Failed to save state to slot {}."), slot_for_message, - Host::OSD_ERROR_DURATION)); + error_callback(fmt::format( + TRANSLATE_FS("VMManager", "Failed to save state to slot {}."), slot_for_message)); } DevCon.WriteLn("Zipping save state to '%s' took %.2f ms", filename, timer.GetTimeMilliseconds()); } void VMManager::ZipSaveStateOnThread(std::unique_ptr elist, - std::unique_ptr screenshot, std::string osd_key, std::string filename, - s32 slot_for_message) + std::unique_ptr screenshot, std::string filename, + s32 slot_for_message, std::function error_callback) { - ZipSaveState(std::move(elist), std::move(screenshot), std::move(osd_key), filename.c_str(), slot_for_message); + ZipSaveState( + std::move(elist), std::move(screenshot), filename.c_str(), slot_for_message, std::move(error_callback)); // remove ourselves from the thread list. if we're joining, we might not be in there. const auto this_id = std::this_thread::get_id(); @@ -2046,37 +2052,45 @@ bool VMManager::LoadStateFromSlot(s32 slot, bool backup, Error* error) return DoLoadState(filename.c_str(), error); } -bool VMManager::SaveState(const char* filename, bool zip_on_thread, bool backup_old_state) +void VMManager::SaveState( + const char* filename, bool zip_on_thread, bool backup_old_state, std::function error_callback) { if (MemcardBusy::IsBusy()) { - Host::AddIconOSDMessage("LoadStateFromSlot", ICON_FA_TRIANGLE_EXCLAMATION, - fmt::format(TRANSLATE_FS("VMManager", "Failed to save state (Memory card is busy)")), - Host::OSD_QUICK_DURATION); - return false; + error_callback(TRANSLATE_STR("VMManager", "Failed to save state (memory card is busy).")); + return; } - return DoSaveState(filename, -1, zip_on_thread, backup_old_state); + DoSaveState(filename, -1, zip_on_thread, backup_old_state, std::move(error_callback)); } -bool VMManager::SaveStateToSlot(s32 slot, bool zip_on_thread) +void VMManager::SaveStateToSlot(s32 slot, bool zip_on_thread, std::function error_callback) { const std::string filename(GetCurrentSaveStateFileName(slot)); if (filename.empty()) - return false; + { + error_callback(TRANSLATE_STR("VMManager", "Failed to generate filename for save state.")); + return; + } if (MemcardBusy::IsBusy()) { - Host::AddIconOSDMessage("LoadStateFromSlot", ICON_FA_TRIANGLE_EXCLAMATION, - fmt::format(TRANSLATE_FS("VMManager", "Failed to save state to slot {} (Memory card is busy)"), slot), - Host::OSD_QUICK_DURATION); - return false; + error_callback(fmt::format( + TRANSLATE_FS("VMManager", "Failed to save state to slot {} (Memory card is busy)"), slot)); + return; } // if it takes more than a minute.. well.. wtf. Host::AddIconOSDMessage(fmt::format("SaveStateSlot{}", slot), ICON_FA_FLOPPY_DISK, fmt::format(TRANSLATE_FS("VMManager", "Saving state to slot {}..."), slot), 60.0f); - return DoSaveState(filename.c_str(), slot, zip_on_thread, EmuConfig.BackupSavestate); + + auto callback = [error_callback = std::move(error_callback), slot](const std::string& error) { + Host::RemoveKeyedOSDMessage(fmt::format("SaveStateSlot{}", slot)); + error_callback(error); + }; + + return DoSaveState( + filename.c_str(), slot, zip_on_thread, EmuConfig.BackupSavestate, std::move(callback)); } LimiterModeType VMManager::GetLimiterMode() diff --git a/pcsx2/VMManager.h b/pcsx2/VMManager.h index 27e87bdc83..8df625386a 100644 --- a/pcsx2/VMManager.h +++ b/pcsx2/VMManager.h @@ -165,10 +165,11 @@ namespace VMManager bool LoadStateFromSlot(s32 slot, bool backup = false, Error* error = nullptr); /// Saves state to the specified filename. - bool SaveState(const char* filename, bool zip_on_thread = true, bool backup_old_state = false); + void SaveState(const char* filename, bool zip_on_thread, bool backup_old_state, + std::function error_callback); /// Saves state to the specified slot. - bool SaveStateToSlot(s32 slot, bool zip_on_thread = true); + void SaveStateToSlot(s32 slot, bool zip_on_thread, std::function error_callback); /// Waits until all compressing save states have finished saving to disk. void WaitForSaveStateFlush();