SaveState: Rework error handling when saving states

This commit is contained in:
chaoticgd 2025-11-30 22:24:00 +00:00 committed by Ty
parent 764875ddbf
commit e8c2cfa843
13 changed files with 194 additions and 65 deletions

View File

@ -1266,6 +1266,51 @@ void MainWindow::reportStateLoadError(const QString& message, std::optional<s32>
delete message_box;
}
void MainWindow::reportStateSaveError(const QString& message, std::optional<s32> 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<QMessageBox> 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<void()>& func)
{
func();

View File

@ -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<s32> slot, bool backup);
void reportStateSaveError(const QString& message, std::optional<s32> slot);
void runOnUIThread(const std::function<void()>& func);
void requestReset();

View File

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

View File

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

View File

@ -671,6 +671,7 @@ namespace FullscreenUI
static bool OpenLoadStateSelectorForGameResume(const GameList::Entry* entry);
static void DrawResumeStateSelector();
static void DoLoadState(std::string path, std::optional<s32> slot, bool backup);
static void DoSaveState(s32 slot);
static std::vector<SaveStateListEntry> 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<s32> 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<std::mutex>& se
EndMenuButtons();
}
void FullscreenUI::ReportStateLoadError(std::string message, std::optional<s32> slot, bool backup)
void FullscreenUI::ReportStateLoadError(const std::string& message, std::optional<s32> 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<s32>
});
}
void FullscreenUI::ReportStateSaveError(const std::string& message, std::optional<s32> 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

View File

@ -27,7 +27,8 @@ namespace FullscreenUI
void OpenPauseMenu();
bool OpenAchievementsWindow();
bool OpenLeaderboardsWindow();
void ReportStateLoadError(std::string message, std::optional<s32> slot, bool backup);
void ReportStateLoadError(const std::string& message, std::optional<s32> slot, bool backup);
void ReportStateSaveError(const std::string& message, std::optional<s32> slot);
// NOTE: Only call from GS thread.
bool IsAchievementsWindowOpen();

View File

@ -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();
}

View File

@ -646,7 +646,11 @@ PINEServer::IPCBuffer PINEServer::ParseCommand(std::span<u8> buf, std::vector<u8
goto error;
if (!SafetyChecks(buf_cnt, 1, ret_cnt, 0, buf_size)) [[unlikely]]
goto error;
Host::RunOnCPUThread([slot = FromSpan<u8>(buf, buf_cnt)] { VMManager::SaveStateToSlot(slot); });
Host::RunOnCPUThread([slot = FromSpan<u8>(buf, buf_cnt)] {
VMManager::SaveStateToSlot(slot, true, [slot](const std::string& error) {
SaveState_ReportSaveErrorOSD(error, slot);
});
});
buf_cnt += 1;
break;
}

View File

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

View File

@ -1253,3 +1253,16 @@ void SaveState_ReportLoadErrorOSD(const std::string& message, std::optional<s32>
Host::AddIconOSDMessage("LoadState", ICON_FA_TRIANGLE_EXCLAMATION,
full_message, Host::OSD_WARNING_DURATION);
}
void SaveState_ReportSaveErrorOSD(const std::string& message, std::optional<s32> 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);
}

View File

@ -356,3 +356,4 @@ public:
};
void SaveState_ReportLoadErrorOSD(const std::string& message, std::optional<s32> slot, bool backup);
void SaveState_ReportSaveErrorOSD(const std::string& message, std::optional<s32> slot);

View File

@ -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<void(const std::string&)> error_callback);
static void ZipSaveState(std::unique_ptr<ArchiveEntryList> elist,
std::unique_ptr<SaveStateScreenshotData> screenshot, std::string osd_key, const char* filename,
s32 slot_for_message);
std::unique_ptr<SaveStateScreenshotData> screenshot, const char* filename,
s32 slot_for_message, std::function<void(const std::string&)> error_callback);
static void ZipSaveStateOnThread(std::unique_ptr<ArchiveEntryList> elist,
std::unique_ptr<SaveStateScreenshotData> screenshot, std::string osd_key, std::string filename,
s32 slot_for_message);
std::unique_ptr<SaveStateScreenshotData> screenshot, std::string filename,
s32 slot_for_message, std::function<void(const std::string&)> 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<void(const std::string&)> 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<ArchiveEntryList> 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<SaveStateScreenshotData> 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<ArchiveEntryList> elist,
std::unique_ptr<SaveStateScreenshotData> screenshot, std::string osd_key, const char* filename,
s32 slot_for_message)
std::unique_ptr<SaveStateScreenshotData> screenshot, const char* filename,
s32 slot_for_message, std::function<void(const std::string&)> error_callback)
{
Common::Timer timer;
@ -1908,26 +1914,26 @@ void VMManager::ZipSaveState(std::unique_ptr<ArchiveEntryList> 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<ArchiveEntryList> elist,
std::unique_ptr<SaveStateScreenshotData> screenshot, std::string osd_key, std::string filename,
s32 slot_for_message)
std::unique_ptr<SaveStateScreenshotData> screenshot, std::string filename,
s32 slot_for_message, std::function<void(const std::string&)> 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<void(const std::string&)> 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<void(const std::string&)> 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()

View File

@ -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<void(const std::string&)> 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<void(const std::string&)> error_callback);
/// Waits until all compressing save states have finished saving to disk.
void WaitForSaveStateFlush();