diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index e287f68cd..571da9924 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -272,8 +272,9 @@ static void DoStartBIOS(); static void DoStartDisc(std::string path); static void DoStartDisc(); static void DoToggleFastForward(); -static void DoShutdown(bool save_state); -static void DoReset(); +static void ConfirmIfSavingMemoryCards(std::string_view action, std::function callback); +static void RequestShutdown(bool save_state); +static void RequestReset(); static void DoChangeDiscFromFile(); static void DoChangeDisc(); static void DoRequestExit(); @@ -1026,14 +1027,44 @@ void FullscreenUI::DoStartDisc() }); } -void FullscreenUI::DoShutdown(bool save_state) +void FullscreenUI::ConfirmIfSavingMemoryCards(std::string_view action, std::function callback) { - Host::RunOnCPUThread([save_state]() { Host::RequestSystemShutdown(false, save_state); }); + if (!System::IsSavingMemoryCards()) + { + callback(true); + return; + } + + OpenConfirmMessageDialog( + FSUI_ICONSTR(ICON_PF_MEMORY_CARD, "Memory Card Busy"), + fmt::format(FSUI_FSTR("WARNING: Your game is still saving to the memory card. Continuing to {0} may IRREVERSIBLY " + "DESTROY YOUR MEMORY CARD. We recommend resuming your game and waiting 5 seconds for it to " + "finish saving.\n\nDo you want to {0} anyway?"), + action), + std::move(callback), + fmt::format( + fmt::runtime(FSUI_ICONSTR(ICON_FA_EXCLAMATION_TRIANGLE, "Yes, {} now and risk memory card corruption.")), action), + FSUI_ICONSTR(ICON_FA_PLAY, "No, resume the game.")); } -void FullscreenUI::DoReset() +void FullscreenUI::RequestShutdown(bool save_state) { - Host::RunOnCPUThread(System::ResetSystem); + ConfirmIfSavingMemoryCards(FSUI_VSTR("shut down"), [save_state](bool result) { + if (result) + Host::RunOnCPUThread([save_state]() { Host::RequestSystemShutdown(false, save_state); }); + else + ClosePauseMenu(); + }); +} + +void FullscreenUI::RequestReset() +{ + ConfirmIfSavingMemoryCards(FSUI_VSTR("reset"), [](bool result) { + if (result) + Host::RunOnCPUThread(System::ResetSystem); + else + ClosePauseMenu(); + }); } void FullscreenUI::DoToggleFastForward() @@ -1048,27 +1079,35 @@ void FullscreenUI::DoToggleFastForward() void FullscreenUI::DoChangeDiscFromFile() { - auto callback = [](const std::string& path) { - if (!path.empty()) + ConfirmIfSavingMemoryCards(FSUI_VSTR("change disc"), [](bool result) { + if (!result) { - if (!GameList::IsScannableFilename(path)) - { - ShowToast({}, - fmt::format(FSUI_FSTR("{} is not a valid disc image."), FileSystem::GetDisplayNameFromPath(path))); - } - else - { - Host::RunOnCPUThread([path]() { System::InsertMedia(path.c_str()); }); - } + ClosePauseMenu(); + return; } - QueueResetFocus(); - CloseFileSelector(); - ReturnToPreviousWindow(); - }; + auto callback = [](const std::string& path) { + if (!path.empty()) + { + if (!GameList::IsScannableFilename(path)) + { + ShowToast({}, + fmt::format(FSUI_FSTR("{} is not a valid disc image."), FileSystem::GetDisplayNameFromPath(path))); + } + else + { + Host::RunOnCPUThread([path]() { System::InsertMedia(path.c_str()); }); + } + } - OpenFileSelector(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Select Disc Image"), false, std::move(callback), - GetDiscImageFilters(), std::string(Path::GetDirectory(System::GetDiscPath()))); + QueueResetFocus(); + CloseFileSelector(); + ReturnToPreviousWindow(); + }; + + OpenFileSelector(FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Select Disc Image"), false, std::move(callback), + GetDiscImageFilters(), std::string(Path::GetDirectory(System::GetDiscPath()))); + }); } void FullscreenUI::DoChangeDisc() @@ -5277,7 +5316,7 @@ void FullscreenUI::DrawPauseMenu() { // skip submenu when we can't save anyway if (!has_game) - DoShutdown(false); + RequestShutdown(false); else OpenPauseSubMenu(PauseSubMenu::Exit); } @@ -5298,14 +5337,14 @@ void FullscreenUI::DrawPauseMenu() if (ActiveButton(FSUI_ICONSTR(ICON_FA_SYNC, "Reset System"), false)) { ClosePauseMenu(); - DoReset(); + RequestReset(); } if (ActiveButton(FSUI_ICONSTR(ICON_FA_SAVE, "Exit And Save State"), false)) - DoShutdown(true); + RequestShutdown(true); if (ActiveButton(FSUI_ICONSTR(ICON_FA_POWER_OFF, "Exit Without Saving"), false)) - DoShutdown(false); + RequestShutdown(false); } break; diff --git a/src/core/memory_card.cpp b/src/core/memory_card.cpp index 76e5dda94..da04d8511 100644 --- a/src/core/memory_card.cpp +++ b/src/core/memory_card.cpp @@ -270,6 +270,11 @@ bool MemoryCard::Transfer(const u8 data_in, u8* data_out) return ack; } +bool MemoryCard::IsOrWasRecentlyWriting() const +{ + return (m_state == State::WriteData || m_save_event->IsActive()); +} + std::unique_ptr MemoryCard::Create() { std::unique_ptr mc = std::make_unique(); diff --git a/src/core/memory_card.h b/src/core/memory_card.h index aef10d6d0..314547645 100644 --- a/src/core/memory_card.h +++ b/src/core/memory_card.h @@ -35,6 +35,8 @@ public: void ResetTransferState(); bool Transfer(const u8 data_in, u8* data_out); + bool IsOrWasRecentlyWriting() const; + void Format(); private: diff --git a/src/core/system.cpp b/src/core/system.cpp index d49c375a3..94f4e709c 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -3300,6 +3300,18 @@ bool System::HasMemoryCard(u32 slot) return (Pad::GetMemoryCard(slot) != nullptr); } +bool System::IsSavingMemoryCards() +{ + for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) + { + MemoryCard* card = Pad::GetMemoryCard(i); + if (card && card->IsOrWasRecentlyWriting()) + return true; + } + + return false; +} + void System::SwapMemoryCards() { if (!IsValid()) diff --git a/src/core/system.h b/src/core/system.h index cc6926cc1..bd735444a 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -301,6 +301,7 @@ void ResetControllers(); void UpdateMemoryCardTypes(); void UpdatePerGameMemoryCards(); bool HasMemoryCard(u32 slot); +bool IsSavingMemoryCards(); /// Swaps memory cards in slot 1/2. void SwapMemoryCards(); diff --git a/src/duckstation-qt/achievementlogindialog.cpp b/src/duckstation-qt/achievementlogindialog.cpp index 1f1413957..f79b4234d 100644 --- a/src/duckstation-qt/achievementlogindialog.cpp +++ b/src/duckstation-qt/achievementlogindialog.cpp @@ -110,7 +110,7 @@ void AchievementLoginDialog::processLoginResult(bool result, const QString& mess tr("Hardcore mode will not be enabled until the system is reset. Do you want to reset the system now?")) == QMessageBox::Yes) { - g_emu_thread->resetSystem(); + g_emu_thread->resetSystem(true); } } diff --git a/src/duckstation-qt/achievementsettingswidget.cpp b/src/duckstation-qt/achievementsettingswidget.cpp index d24dc221f..b4a75c93d 100644 --- a/src/duckstation-qt/achievementsettingswidget.cpp +++ b/src/duckstation-qt/achievementsettingswidget.cpp @@ -154,7 +154,7 @@ void AchievementSettingsWidget::onHardcoreModeStateChanged() return; } - g_emu_thread->resetSystem(); + g_emu_thread->resetSystem(true); } void AchievementSettingsWidget::onAchievementsNotificationDurationSliderChanged() diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 9f8bd6413..588664f9c 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -700,7 +700,7 @@ void MainWindow::quit() // Make sure VM is gone. It really should be if we're here. if (s_system_valid) { - g_emu_thread->shutdownSystem(false); + g_emu_thread->shutdownSystem(false, true); while (s_system_valid) QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); } @@ -990,7 +990,7 @@ void MainWindow::populateChangeDiscSubImageMenu(QMenu* menu, QActionGroup* actio QString path = QString::fromStdString(glentry->path); action->setCheckable(true); action->setChecked(path == s_current_game_path); - connect(action, &QAction::triggered, [path = std::move(path)]() { g_emu_thread->changeDisc(path); }); + connect(action, &QAction::triggered, [path = std::move(path)]() { g_emu_thread->changeDisc(path, false, true); }); menu->addAction(action); } } @@ -1232,9 +1232,7 @@ void MainWindow::promptForDiscChange(const QString& path) switchToEmulationView(); - g_emu_thread->changeDisc(path); - if (reset_system) - g_emu_thread->resetSystem(); + g_emu_thread->changeDisc(path, reset_system, true); } void MainWindow::onStartDiscActionTriggered() @@ -1258,7 +1256,7 @@ void MainWindow::onChangeDiscFromFileActionTriggered() if (filename.isEmpty()) return; - g_emu_thread->changeDisc(filename); + g_emu_thread->changeDisc(filename, false, true); } void MainWindow::onChangeDiscFromGameListActionTriggered() @@ -1273,7 +1271,7 @@ void MainWindow::onChangeDiscFromDeviceActionTriggered() if (path.empty()) return; - g_emu_thread->changeDisc(QString::fromStdString(path)); + g_emu_thread->changeDisc(QString::fromStdString(path), false, true); } void MainWindow::onChangeDiscMenuAboutToShow() @@ -1317,7 +1315,7 @@ void MainWindow::onFullscreenUIStateChange(bool running) void MainWindow::onRemoveDiscActionTriggered() { - g_emu_thread->changeDisc(QString()); + g_emu_thread->changeDisc(QString(), false, true); } void MainWindow::onViewToolbarActionToggled(bool checked) @@ -1511,7 +1509,7 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point) else { connect(menu.addAction(tr("Change Disc")), &QAction::triggered, [this, entry]() { - g_emu_thread->changeDisc(QString::fromStdString(entry->path)); + g_emu_thread->changeDisc(QString::fromStdString(entry->path), false, true); g_emu_thread->setSystemPaused(false); switchToEmulationView(); }); @@ -2031,8 +2029,8 @@ void MainWindow::connectSignals() [this]() { requestShutdown(true, true, g_settings.save_state_on_exit); }); connect(m_ui.actionPowerOffWithoutSaving, &QAction::triggered, this, [this]() { requestShutdown(false, false, false); }); - connect(m_ui.actionReset, &QAction::triggered, g_emu_thread, &EmuThread::resetSystem); - connect(m_ui.actionPause, &QAction::toggled, [](bool active) { g_emu_thread->setSystemPaused(active); }); + connect(m_ui.actionReset, &QAction::triggered, this, []() { g_emu_thread->resetSystem(true); }); + connect(m_ui.actionPause, &QAction::toggled, this, [](bool active) { g_emu_thread->setSystemPaused(active); }); connect(m_ui.actionScreenshot, &QAction::triggered, g_emu_thread, &EmuThread::saveScreenshot); connect(m_ui.actionScanForNewGames, &QAction::triggered, this, [this]() { refreshGameList(false); }); connect(m_ui.actionRescanAllGames, &QAction::triggered, this, [this]() { refreshGameList(true); }); @@ -2862,7 +2860,7 @@ bool MainWindow::requestShutdown(bool allow_confirm /* = true */, bool allow_sav updateWindowState(true); // Now we can actually shut down the VM. - g_emu_thread->shutdownSystem(save_state); + g_emu_thread->shutdownSystem(save_state, true); return true; } diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 9e57fa0b5..299c4acca 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1067,23 +1067,72 @@ void EmuThread::enumerateVibrationMotors() onVibrationMotorsEnumerated(qmotors); } -void EmuThread::shutdownSystem(bool save_state /* = true */) +void EmuThread::confirmActionIfMemoryCardBusy(const QString& action, bool cancel_resume_on_accept, + std::function callback) const +{ + DebugAssert(isOnThread()); + + if (!System::IsValid() || !System::IsSavingMemoryCards()) + { + callback(true); + return; + } + + QtHost::RunOnUIThread([action, cancel_resume_on_accept, callback = std::move(callback)]() mutable { + auto lock = g_main_window->pauseAndLockSystem(); + + const bool result = + (QMessageBox::question(lock.getDialogParent(), tr("Memory Card Busy"), + tr("WARNING: Your game is still saving to the memory card. Continuing to %1 may " + "IRREVERSIBLY DESTROY YOUR MEMORY CARD. We recommend resuming your game and waiting 5 " + "seconds for it to finish saving.\n\nDo you want to %1 anyway?") + .arg(action)) != QMessageBox::No); + + if (cancel_resume_on_accept) + lock.cancelResume(); + + Host::RunOnCPUThread([result, callback = std::move(callback)]() { callback(result); }); + }); +} + +void EmuThread::shutdownSystem(bool save_state, bool check_memcard_busy) { if (!isOnThread()) { System::CancelPendingStartup(); - QMetaObject::invokeMethod(this, "shutdownSystem", Qt::QueuedConnection, Q_ARG(bool, save_state)); + QMetaObject::invokeMethod(this, "shutdownSystem", Qt::QueuedConnection, Q_ARG(bool, save_state), + Q_ARG(bool, check_memcard_busy)); + return; + } + + if (check_memcard_busy && System::IsSavingMemoryCards()) + { + confirmActionIfMemoryCardBusy(tr("shut down"), true, [save_state](bool result) { + if (result) + g_emu_thread->shutdownSystem(save_state, false); + else + g_emu_thread->setSystemPaused(false); + }); return; } System::ShutdownSystem(save_state); } -void EmuThread::resetSystem() +void EmuThread::resetSystem(bool check_memcard_busy) { if (!isOnThread()) { - QMetaObject::invokeMethod(this, &EmuThread::resetSystem, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "resetSystem", Qt::QueuedConnection, Q_ARG(bool, check_memcard_busy)); + return; + } + + if (check_memcard_busy && System::IsSavingMemoryCards()) + { + confirmActionIfMemoryCardBusy(tr("reset"), false, [](bool result) { + if (result) + g_emu_thread->resetSystem(false); + }); return; } @@ -1103,11 +1152,21 @@ void EmuThread::setSystemPaused(bool paused, bool wait_until_paused /* = false * System::PauseSystem(paused); } -void EmuThread::changeDisc(const QString& new_disc_filename) +void EmuThread::changeDisc(const QString& new_disc_filename, bool reset_system, bool check_memcard_busy) { if (!isOnThread()) { - QMetaObject::invokeMethod(this, "changeDisc", Qt::QueuedConnection, Q_ARG(const QString&, new_disc_filename)); + QMetaObject::invokeMethod(this, "changeDisc", Qt::QueuedConnection, Q_ARG(const QString&, new_disc_filename), + Q_ARG(bool, reset_system), Q_ARG(bool, check_memcard_busy)); + return; + } + + if (check_memcard_busy && System::IsSavingMemoryCards()) + { + confirmActionIfMemoryCardBusy(tr("change disc"), false, [new_disc_filename, reset_system](bool result) { + if (result) + g_emu_thread->changeDisc(new_disc_filename, reset_system, false); + }); return; } @@ -1118,6 +1177,9 @@ void EmuThread::changeDisc(const QString& new_disc_filename) System::InsertMedia(new_disc_filename.toStdString().c_str()); else System::RemoveMedia(); + + if (reset_system) + System::ResetSystem(); } void EmuThread::changeDiscFromPlaylist(quint32 index) diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index 4f0aee4b6..d10abc3ef 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -168,10 +168,10 @@ public Q_SLOTS: void stopFullscreenUI(); void bootSystem(std::shared_ptr params); void resumeSystemFromMostRecentState(); - void shutdownSystem(bool save_state = true); - void resetSystem(); + void shutdownSystem(bool save_state, bool check_memcard_busy); + void resetSystem(bool check_memcard_busy); void setSystemPaused(bool paused, bool wait_until_paused = false); - void changeDisc(const QString& new_disc_filename); + void changeDisc(const QString& new_disc_filename, bool reset_system, bool check_memcard_busy); void changeDiscFromPlaylist(quint32 index); void loadState(const QString& filename); void loadState(bool global, qint32 slot); @@ -218,6 +218,8 @@ private: void createBackgroundControllerPollTimer(); void destroyBackgroundControllerPollTimer(); void setInitialState(std::optional override_fullscreen); + void confirmActionIfMemoryCardBusy(const QString& action, bool cancel_resume_on_accept, + std::function callback) const; QThread* m_ui_thread; QSemaphore m_started_semaphore;