From 5d7cb6c5dcbf3e23026f48774d3f689bb986a654 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Mon, 9 Dec 2024 14:16:50 +1000 Subject: [PATCH] System: Move state compression/writing to worker thread Reduce hitches when saving. --- src/core/fullscreen_ui.cpp | 6 +- src/core/hotkeys.cpp | 2 +- src/core/imgui_overlays.cpp | 4 +- src/core/system.cpp | 94 +++++++++++++++++++------------ src/core/system.h | 8 +-- src/duckstation-qt/mainwindow.cpp | 38 +++++++------ src/duckstation-qt/mainwindow.h | 4 +- src/duckstation-qt/qthost.cpp | 2 +- 8 files changed, 92 insertions(+), 66 deletions(-) diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 0827c859d..e9059aff2 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -6532,10 +6532,10 @@ void FullscreenUI::DoSaveState(s32 slot, bool global) if (!System::IsValid()) return; - std::string filename(global ? System::GetGlobalSaveStateFileName(slot) : - System::GetGameSaveStateFileName(System::GetGameSerial(), slot)); + std::string path(global ? System::GetGlobalSaveStateFileName(slot) : + System::GetGameSaveStateFileName(System::GetGameSerial(), slot)); Error error; - if (!System::SaveState(filename.c_str(), &error, g_settings.create_save_state_backups, false)) + if (!System::SaveState(std::move(path), &error, g_settings.create_save_state_backups, false)) { ShowToast(std::string(), fmt::format(TRANSLATE_FS("System", "Failed to save state: {}"), error.GetDescription())); } diff --git a/src/core/hotkeys.cpp b/src/core/hotkeys.cpp index 53cff0902..2d1857d20 100644 --- a/src/core/hotkeys.cpp +++ b/src/core/hotkeys.cpp @@ -111,7 +111,7 @@ static void HotkeySaveStateSlot(bool global, s32 slot) std::string path(global ? System::GetGlobalSaveStateFileName(slot) : System::GetGameSaveStateFileName(System::GetGameSerial(), slot)); Error error; - if (!System::SaveState(path.c_str(), &error, g_settings.create_save_state_backups, false)) + if (!System::SaveState(std::move(path), &error, g_settings.create_save_state_backups, false)) { Host::AddIconOSDMessage( "SaveState", ICON_FA_EXCLAMATION_TRIANGLE, diff --git a/src/core/imgui_overlays.cpp b/src/core/imgui_overlays.cpp index c246ed696..d0fba384f 100644 --- a/src/core/imgui_overlays.cpp +++ b/src/core/imgui_overlays.cpp @@ -389,7 +389,7 @@ void ImGuiManager::DrawPerformanceOverlay(float& position_y, float scale, float if (g_settings.display_show_resolution) { const u32 resolution_scale = g_gpu->GetResolutionScale(); - const auto [display_width, display_height] = g_gpu->GetFullDisplayResolution();// wrong + const auto [display_width, display_height] = g_gpu->GetFullDisplayResolution(); // wrong const bool interlaced = g_gpu->IsInterlacedDisplayEnabled(); const bool pal = g_gpu->IsInPALMode(); text.format("{}x{} {} {} [{}x]", display_width * resolution_scale, display_height * resolution_scale, @@ -1277,7 +1277,7 @@ void SaveStateSelectorUI::SaveCurrentSlot() if (std::string path = GetCurrentSlotPath(); !path.empty()) { Error error; - if (!System::SaveState(path.c_str(), &error, g_settings.create_save_state_backups, false)) + if (!System::SaveState(std::move(path), &error, g_settings.create_save_state_backups, false)) { Host::AddIconOSDMessage("SaveState", ICON_EMOJI_WARNING, fmt::format(TRANSLATE_FS("OSDMessage", "Failed to save state to slot {0}:\n{1}"), diff --git a/src/core/system.cpp b/src/core/system.cpp index bad57f340..a2084c5c9 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1663,8 +1663,8 @@ bool System::SaveResumeState(Error* error) return false; } - const std::string path(GetGameSaveStateFileName(s_state.running_game_serial, -1)); - return SaveState(path.c_str(), error, false, true); + std::string path(GetGameSaveStateFileName(s_state.running_game_serial, -1)); + return SaveState(std::move(path), error, false, true); } bool System::BootSystem(SystemBootParameters parameters, Error* error) @@ -3010,7 +3010,7 @@ bool System::ReadAndDecompressStateData(std::FILE* fp, std::span dst, u32 fi } } -bool System::SaveState(const char* path, Error* error, bool backup_existing_save, bool ignore_memcard_busy) +bool System::SaveState(std::string path, Error* error, bool backup_existing_save, bool ignore_memcard_busy) { if (!IsValid() || IsReplayingGPUDump()) { @@ -3029,40 +3029,62 @@ bool System::SaveState(const char* path, Error* error, bool backup_existing_save if (!SaveStateToBuffer(&buffer, error, 256)) return false; - // TODO: Do this on a thread pool + VERBOSE_LOG("Preparing state save took {:.2f} msec", save_timer.GetTimeMilliseconds()); - if (backup_existing_save && FileSystem::FileExists(path)) - { - Error backup_error; - const std::string backup_filename = Path::ReplaceExtension(path, "bak"); - if (!FileSystem::RenamePath(path, backup_filename.c_str(), &backup_error)) + std::string osd_key = fmt::format("save_state_{}", path); + Host::AddIconOSDMessage(osd_key, ICON_EMOJI_FLOPPY_DISK, + fmt::format(TRANSLATE_FS("System", "Saving state to '{}'."), Path::GetFileName(path)), 60.0f); + + QueueTaskOnThread([path = std::move(path), buffer = std::move(buffer), osd_key = std::move(osd_key), + backup_existing_save, compression = g_settings.save_state_compression]() { + INFO_LOG("Saving state to '{}'...", path); + + Error lerror; + Timer lsave_timer; + + if (backup_existing_save && FileSystem::FileExists(path.c_str())) { - ERROR_LOG("Failed to rename save state backup '{}': {}", Path::GetFileName(backup_filename), - backup_error.GetDescription()); + const std::string backup_filename = Path::ReplaceExtension(path, "bak"); + if (!FileSystem::RenamePath(path.c_str(), backup_filename.c_str(), &lerror)) + { + ERROR_LOG("Failed to rename save state backup '{}': {}", Path::GetFileName(backup_filename), + lerror.GetDescription()); + } } - } - auto fp = FileSystem::CreateAtomicRenamedFile(path, error); - if (!fp) - { - Error::AddPrefixFmt(error, "Cannot open '{}': ", Path::GetFileName(path)); - return false; - } + auto fp = FileSystem::CreateAtomicRenamedFile(path, &lerror); + bool result = false; + if (fp) + { + if (SaveStateBufferToFile(buffer, fp.get(), &lerror, compression)) + result = FileSystem::CommitAtomicRenamedFile(fp, &lerror); + else + FileSystem::DiscardAtomicRenamedFile(fp); + } + else + { + lerror.AddPrefixFmt("Cannot open '{}': ", Path::GetFileName(path)); + } - INFO_LOG("Saving state to '{}'...", path); + VERBOSE_LOG("Saving state took {:.2f} msec", lsave_timer.GetTimeMilliseconds()); + if (result) + { + Host::AddIconOSDMessage(std::move(osd_key), ICON_EMOJI_FLOPPY_DISK, + fmt::format(TRANSLATE_FS("System", "State saved to '{}'."), Path::GetFileName(path)), + Host::OSD_QUICK_DURATION); + } + else + { + Host::AddIconOSDMessage(std::move(osd_key), ICON_EMOJI_WARNING, + fmt::format(TRANSLATE_FS("System", "Failed to save state to '{0}':\n{1}"), + Path::GetFileName(path), lerror.GetDescription()), + Host::OSD_ERROR_DURATION); + } - if (!SaveStateBufferToFile(buffer, fp.get(), error, g_settings.save_state_compression)) - { - FileSystem::DiscardAtomicRenamedFile(fp); - return false; - } + System::RemoveSelfFromTaskThreads(); + }); - Host::AddIconOSDMessage("save_state", ICON_EMOJI_FLOPPY_DISK, - fmt::format(TRANSLATE_FS("OSDMessage", "State saved to '{}'."), Path::GetFileName(path)), - 5.0f); - - VERBOSE_LOG("Saving state took {:.2f} msec", save_timer.GetTimeMilliseconds()); - return FileSystem::CommitAtomicRenamedFile(fp, error); + return true; } bool System::SaveStateToBuffer(SaveStateBuffer* buffer, Error* error, u32 screenshot_size /* = 256 */) @@ -3261,7 +3283,7 @@ u32 System::CompressAndWriteStateData(std::FILE* fp, std::span src, Sa buffer.resize(buffer_size); const int level = - ((method == SaveStateCompressionMode::ZstLow) ? 1 : ((method == SaveStateCompressionMode::ZstHigh) ? 19 : 0)); + ((method == SaveStateCompressionMode::ZstLow) ? 1 : ((method == SaveStateCompressionMode::ZstHigh) ? 18 : 0)); const size_t compressed_size = ZSTD_compress(buffer.data(), buffer_size, src.data(), src.size(), level); if (ZSTD_isError(compressed_size)) [[unlikely]] { @@ -5399,7 +5421,7 @@ std::string System::GetGlobalSaveStateFileName(s32 slot) return Path::Combine(EmuFolders::SaveStates, fmt::format("savestate_{}.sav", slot)); } -std::vector System::GetAvailableSaveStates(const char* serial) +std::vector System::GetAvailableSaveStates(std::string_view serial) { std::vector si; std::string path; @@ -5412,7 +5434,7 @@ std::vector System::GetAvailableSaveStates(const char* serial) si.push_back(SaveStateInfo{std::move(path), sd.ModificationTime, static_cast(slot), global}); }; - if (serial && std::strlen(serial) > 0) + if (!serial.empty()) { add_path(GetGameSaveStateFileName(serial, -1), -1, false); for (s32 i = 1; i <= PER_GAME_SAVE_STATE_SLOTS; i++) @@ -5425,9 +5447,9 @@ std::vector System::GetAvailableSaveStates(const char* serial) return si; } -std::optional System::GetSaveStateInfo(const char* serial, s32 slot) +std::optional System::GetSaveStateInfo(std::string_view serial, s32 slot) { - const bool global = (!serial || serial[0] == 0); + const bool global = serial.empty(); std::string path = global ? GetGlobalSaveStateFileName(slot) : GetGameSaveStateFileName(serial, slot); FILESYSTEM_STAT_DATA sd; @@ -5468,7 +5490,7 @@ std::optional System::GetExtendedSaveStateInfo(const char return ssi; } -void System::DeleteSaveStates(const char* serial, bool resume) +void System::DeleteSaveStates(std::string_view serial, bool resume) { const std::vector states(GetAvailableSaveStates(serial)); for (const SaveStateInfo& si : states) diff --git a/src/core/system.h b/src/core/system.h index 33c544bdf..6fda00d7d 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -256,7 +256,7 @@ void ResetSystem(); /// Loads state from the specified path. bool LoadState(const char* path, Error* error, bool save_undo_state); -bool SaveState(const char* path, Error* error, bool backup_existing_save, bool ignore_memcard_busy); +bool SaveState(std::string path, Error* error, bool backup_existing_save, bool ignore_memcard_busy); bool SaveResumeState(Error* error); /// Runs the VM until the CPU execution is canceled. @@ -358,16 +358,16 @@ std::optional GetUndoSaveStateInfo(); bool UndoLoadState(); /// Returns a list of save states for the specified game code. -std::vector GetAvailableSaveStates(const char* serial); +std::vector GetAvailableSaveStates(std::string_view serial); /// Returns save state info if present. If serial is null or empty, assumes global state. -std::optional GetSaveStateInfo(const char* serial, s32 slot); +std::optional GetSaveStateInfo(std::string_view serial, s32 slot); /// Returns save state info from opened save state stream. std::optional GetExtendedSaveStateInfo(const char* path); /// Deletes save states for the specified game code. If resume is set, the resume state is deleted too. -void DeleteSaveStates(const char* serial, bool resume); +void DeleteSaveStates(std::string_view serial, bool resume); /// Returns the path to the memory card for the specified game, considering game settings. std::string GetGameMemoryCardPath(std::string_view serial, std::string_view path, u32 slot, diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 02aab3b49..72a50df27 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -811,7 +811,7 @@ void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidg if (!entry->serial.empty()) { - std::vector available_states(System::GetAvailableSaveStates(entry->serial.c_str())); + std::vector available_states(System::GetAvailableSaveStates(entry->serial)); const QString timestamp_format = QLocale::system().dateTimeFormat(QLocale::ShortFormat); const bool challenge_mode = Achievements::IsHardcoreModeActive(); for (SaveStateInfo& ssi : available_states) @@ -869,7 +869,7 @@ void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidg return; } - System::DeleteSaveStates(entry->serial.c_str(), true); + System::DeleteSaveStates(entry->serial, true); }); } } @@ -881,10 +881,11 @@ static QString FormatTimestampForSaveStateMenu(u64 timestamp) return qtime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)); } -void MainWindow::populateLoadStateMenu(const char* game_serial, QMenu* menu) +void MainWindow::populateLoadStateMenu(std::string_view game_serial, QMenu* menu) { - auto add_slot = [this, game_serial, menu](const QString& title, const QString& empty_title, bool global, s32 slot) { - std::optional ssi = System::GetSaveStateInfo(global ? nullptr : game_serial, slot); + auto add_slot = [this, menu](const QString& title, const QString& empty_title, const std::string_view& serial, + s32 slot) { + std::optional ssi = System::GetSaveStateInfo(serial, slot); const QString menu_title = ssi.has_value() ? title.arg(slot).arg(FormatTimestampForSaveStateMenu(ssi->timestamp)) : empty_title.arg(slot); @@ -913,28 +914,30 @@ void MainWindow::populateLoadStateMenu(const char* game_serial, QMenu* menu) connect(load_from_state, &QAction::triggered, g_emu_thread, &EmuThread::undoLoadState); menu->addSeparator(); - if (game_serial && std::strlen(game_serial) > 0) + if (!game_serial.empty()) { for (u32 slot = 1; slot <= System::PER_GAME_SAVE_STATE_SLOTS; slot++) - add_slot(tr("Game Save %1 (%2)"), tr("Game Save %1 (Empty)"), false, static_cast(slot)); + add_slot(tr("Game Save %1 (%2)"), tr("Game Save %1 (Empty)"), game_serial, static_cast(slot)); menu->addSeparator(); } + std::string_view empty_serial; for (u32 slot = 1; slot <= System::GLOBAL_SAVE_STATE_SLOTS; slot++) - add_slot(tr("Global Save %1 (%2)"), tr("Global Save %1 (Empty)"), true, static_cast(slot)); + add_slot(tr("Global Save %1 (%2)"), tr("Global Save %1 (Empty)"), empty_serial, static_cast(slot)); } -void MainWindow::populateSaveStateMenu(const char* game_serial, QMenu* menu) +void MainWindow::populateSaveStateMenu(std::string_view game_serial, QMenu* menu) { - auto add_slot = [game_serial, menu](const QString& title, const QString& empty_title, bool global, s32 slot) { - std::optional ssi = System::GetSaveStateInfo(global ? nullptr : game_serial, slot); + auto add_slot = [menu](const QString& title, const QString& empty_title, const std::string_view& serial, s32 slot) { + std::optional ssi = System::GetSaveStateInfo(serial, slot); const QString menu_title = ssi.has_value() ? title.arg(slot).arg(FormatTimestampForSaveStateMenu(ssi->timestamp)) : empty_title.arg(slot); QAction* save_action = menu->addAction(menu_title); - connect(save_action, &QAction::triggered, [global, slot]() { g_emu_thread->saveState(global, slot); }); + connect(save_action, &QAction::triggered, + [global = serial.empty(), slot]() { g_emu_thread->saveState(global, slot); }); }; menu->clear(); @@ -952,16 +955,17 @@ void MainWindow::populateSaveStateMenu(const char* game_serial, QMenu* menu) }); menu->addSeparator(); - if (game_serial && std::strlen(game_serial) > 0) + if (!game_serial.empty()) { for (u32 slot = 1; slot <= System::PER_GAME_SAVE_STATE_SLOTS; slot++) - add_slot(tr("Game Save %1 (%2)"), tr("Game Save %1 (Empty)"), false, static_cast(slot)); + add_slot(tr("Game Save %1 (%2)"), tr("Game Save %1 (Empty)"), game_serial, static_cast(slot)); menu->addSeparator(); } + std::string_view empty_serial; for (u32 slot = 1; slot <= System::GLOBAL_SAVE_STATE_SLOTS; slot++) - add_slot(tr("Global Save %1 (%2)"), tr("Global Save %1 (Empty)"), true, static_cast(slot)); + add_slot(tr("Global Save %1 (%2)"), tr("Global Save %1 (Empty)"), empty_serial, static_cast(slot)); } void MainWindow::populateChangeDiscSubImageMenu(QMenu* menu, QActionGroup* action_group) @@ -1238,12 +1242,12 @@ void MainWindow::onChangeDiscMenuAboutToHide() void MainWindow::onLoadStateMenuAboutToShow() { - populateLoadStateMenu(s_current_game_serial.toUtf8().constData(), m_ui.menuLoadState); + populateLoadStateMenu(s_current_game_serial.toStdString(), m_ui.menuLoadState); } void MainWindow::onSaveStateMenuAboutToShow() { - populateSaveStateMenu(s_current_game_serial.toUtf8().constData(), m_ui.menuSaveState); + populateSaveStateMenu(s_current_game_serial.toStdString(), m_ui.menuSaveState); } void MainWindow::onStartFullscreenUITriggered() diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 2d8d06faa..1f8e573c6 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -268,8 +268,8 @@ private: /// Fills menu with save state info and handlers. void populateGameListContextMenu(const GameList::Entry* entry, QWidget* parent_window, QMenu* menu); - void populateLoadStateMenu(const char* game_serial, QMenu* menu); - void populateSaveStateMenu(const char* game_serial, QMenu* menu); + void populateLoadStateMenu(std::string_view game_serial, QMenu* menu); + void populateSaveStateMenu(std::string_view game_serial, QMenu* menu); /// Fills menu with the current playlist entries. The disc index is marked as checked. void populateChangeDiscSubImageMenu(QMenu* menu, QActionGroup* action_group); diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 4d73b8abe..c9c2213a1 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1412,7 +1412,7 @@ void EmuThread::saveState(const QString& filename, bool block_until_done /* = fa return; Error error; - if (!System::SaveState(filename.toUtf8().data(), &error, g_settings.create_save_state_backups, false)) + if (!System::SaveState(filename.toStdString(), &error, g_settings.create_save_state_backups, false)) emit errorReported(tr("Error"), tr("Failed to save state: %1").arg(QString::fromStdString(error.GetDescription()))); }