System: Move state compression/writing to worker thread

Reduce hitches when saving.
This commit is contained in:
Stenzek 2024-12-09 14:16:50 +10:00
parent a4af88bc52
commit 5d7cb6c5dc
No known key found for this signature in database
8 changed files with 92 additions and 66 deletions

View File

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

View File

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

View File

@ -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}"),

View File

@ -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<u8> 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<const u8> 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<SaveStateInfo> System::GetAvailableSaveStates(const char* serial)
std::vector<SaveStateInfo> System::GetAvailableSaveStates(std::string_view serial)
{
std::vector<SaveStateInfo> si;
std::string path;
@ -5412,7 +5434,7 @@ std::vector<SaveStateInfo> System::GetAvailableSaveStates(const char* serial)
si.push_back(SaveStateInfo{std::move(path), sd.ModificationTime, static_cast<s32>(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<SaveStateInfo> System::GetAvailableSaveStates(const char* serial)
return si;
}
std::optional<SaveStateInfo> System::GetSaveStateInfo(const char* serial, s32 slot)
std::optional<SaveStateInfo> 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<ExtendedSaveStateInfo> 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<SaveStateInfo> states(GetAvailableSaveStates(serial));
for (const SaveStateInfo& si : states)

View File

@ -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<ExtendedSaveStateInfo> GetUndoSaveStateInfo();
bool UndoLoadState();
/// Returns a list of save states for the specified game code.
std::vector<SaveStateInfo> GetAvailableSaveStates(const char* serial);
std::vector<SaveStateInfo> GetAvailableSaveStates(std::string_view serial);
/// Returns save state info if present. If serial is null or empty, assumes global state.
std::optional<SaveStateInfo> GetSaveStateInfo(const char* serial, s32 slot);
std::optional<SaveStateInfo> GetSaveStateInfo(std::string_view serial, s32 slot);
/// Returns save state info from opened save state stream.
std::optional<ExtendedSaveStateInfo> 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,

View File

@ -811,7 +811,7 @@ void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidg
if (!entry->serial.empty())
{
std::vector<SaveStateInfo> available_states(System::GetAvailableSaveStates(entry->serial.c_str()));
std::vector<SaveStateInfo> 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<SaveStateInfo> 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<SaveStateInfo> 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<s32>(slot));
add_slot(tr("Game Save %1 (%2)"), tr("Game Save %1 (Empty)"), game_serial, static_cast<s32>(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<s32>(slot));
add_slot(tr("Global Save %1 (%2)"), tr("Global Save %1 (Empty)"), empty_serial, static_cast<s32>(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<SaveStateInfo> 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<SaveStateInfo> 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<s32>(slot));
add_slot(tr("Game Save %1 (%2)"), tr("Game Save %1 (Empty)"), game_serial, static_cast<s32>(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<s32>(slot));
add_slot(tr("Global Save %1 (%2)"), tr("Global Save %1 (Empty)"), empty_serial, static_cast<s32>(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()

View File

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

View File

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