1
0
mirror of https://github.com/RPCS3/rpcs3.git synced 2025-01-31 12:31:45 +01:00

Multi-Slot Savestates

This commit is contained in:
Elad 2025-01-24 14:34:49 +02:00
parent 67703b49d8
commit 8f3b9a9864
7 changed files with 181 additions and 10 deletions

View File

@ -2557,7 +2557,7 @@ void Emulator::FixGuestTime()
// Mark a known savestate location and the one we try to boot (in case we boot a moved/copied savestate)
if (g_cfg.savestate.suspend_emu)
{
for (std::string old_path : std::initializer_list<std::string>{m_ar ? m_path_old : "", m_title_id.empty() ? "" : get_savestate_file(m_title_id, m_path_old, 0, 0)})
for (std::string old_path : std::initializer_list<std::string>{m_ar ? m_path_old : "", m_title_id.empty() ? "" : get_savestate_file(m_title_id, m_path_old, -1, 0)})
{
if (old_path.empty())
{
@ -3360,7 +3360,7 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s
{
set_progress_message("Creating File");
path = get_savestate_file(m_title_id, m_path, 0, 0);
path = get_savestate_file(m_title_id, m_path, 0, umax);
// The function is meant for reading files, so if there is no ZST file it would not return compressed file path
// So this is the only place where the result is edited if need to be
@ -3600,13 +3600,49 @@ void Emulator::Kill(bool allow_autoexit, bool savestate, savestate_stage* save_s
sys_log.success("Old savestate has been removed: path='%s'", old_path2);
}
sys_log.success("Saved savestate! path='%s' (file_size=0x%x, time_to_save=%gs)", path, file_stat.size, (get_system_time() - start_time) / 1000000.);
sys_log.success("Saved savestate! path='%s' (file_size=0x%x (%d MiB), time_to_save=%gs)", path, file_stat.size, utils::aligned_div<u64>(file_stat.size, 1u << 20), (get_system_time() - start_time) / 1000000.);
if (!g_cfg.savestate.suspend_emu)
{
// Allow to reboot from GUI
m_path = path;
}
// Clean savestates
// Cap by number and aggregate file size
const u64 max_files = g_cfg.savestate.max_files;
const u64 max_files_size = g_cfg.savestate.max_files_size;
bool logged_limits = false;
while (true)
{
std::string to_remove = get_savestate_file(m_title_id, m_path, max_files + 1, max_files_size == 0 ? u64{umax} : (max_files_size << 20));
if (to_remove.empty())
{
break;
}
if (!fs::remove_file(to_remove))
{
sys_log.error("Failed to remove savestate file at '%s'! (error: %s)", to_remove, fs::g_tls_error);
break;
}
else
{
if (!logged_limits)
{
sys_log.success("Maximum save state files set: %d.\nMaximum save state disk space set: %d (MiB).\nRemoved old savestate file at '%s'.\n"
, max_files, max_files_size, to_remove);
logged_limits = true;
}
else
{
sys_log.success("Removed old savestate file at '%s'.", to_remove);
}
}
}
}
}

View File

@ -272,17 +272,131 @@ bool is_savestate_version_compatible(const std::vector<version_entry>& data, boo
return ok;
}
std::string get_savestate_file(std::string_view title_id, std::string_view boot_path, s64 abs_id, s64 rel_id)
std::string get_savestate_file(std::string_view title_id, std::string_view boot_path, s64 rel_id, u64 aggregate_file_size)
{
const std::string title = std::string{title_id.empty() ? boot_path.substr(boot_path.find_last_of(fs::delim) + 1) : title_id};
if (abs_id == -1 && rel_id == -1)
// Internal functionality ATM
constexpr s64 abs_id = 0;
if (aggregate_file_size == umax && rel_id == -1)
{
// Return directory
return fs::get_config_dir() + "savestates/" + title + "/";
}
ensure(rel_id < 0 || abs_id >= 0, "Unimplemented!");
if (rel_id >= 0)
{
const std::string dir_path = fs::get_config_dir() + "/savestates/" + title + "/";
fs::dir dir_view{dir_path};
std::map<std::string, usz, std::greater<>> save_files;
for (auto&& dir_entry : dir_view)
{
if (dir_entry.is_directory || dir_entry.size <= 1024)
{
continue;
}
const std::string& entry = dir_entry.name;
if (entry.ends_with(".SAVESTAT.zst") || entry.ends_with(".SAVESTAT.gz") || entry.ends_with(".SAVESTAT"))
{
if (usz dot_idx = entry.rfind(".SAVESTAT"); dot_idx && dot_idx != umax)
{
if (usz uc_pos = entry.rfind("_", 0, dot_idx); uc_pos != umax && uc_pos + 1 < dot_idx)
{
if (std::all_of(entry.begin() + uc_pos + 1, entry.begin() + dot_idx, [](char c) { return c >= '0' && c <= '9'; }))
{
save_files.emplace(entry, dir_entry.size);
}
}
}
}
}
std::string rel_path;
std::string size_based_path;
if (rel_id > 0)
{
if (rel_id - 1 < save_files.size())
{
rel_path = std::next(save_files.begin(), rel_id - 1)->first;
}
}
if (aggregate_file_size != umax)
{
usz size_sum = 0;
for (auto&& [path, size] : save_files)
{
if (size_sum >= aggregate_file_size)
{
size_based_path = path;
break;
}
size_sum += size;
}
}
if (!rel_path.empty() || !size_based_path.empty())
{
if (rel_path > size_based_path)
{
return std::move(rel_path);
}
else
{
return std::move(size_based_path);
}
}
if (rel_id > 0 || aggregate_file_size != umax)
{
return {};
}
// Increment number in string in reverse
// Return index of new character if appended a character, umax otherwise
auto increment_string = [](std::string& out, usz pos) -> usz
{
while (pos != umax && out[pos] == '9')
{
out[pos] = '0';
pos--;
}
if (pos == umax || (out[pos] < '0' || out[pos] > '9'))
{
out.insert(out.begin() + (pos + 1), '1');
return pos + 1;
}
out[pos]++;
return umax;
};
if (!save_files.empty())
{
std::string last_entry = save_files.begin()->first;
// Increment entry ID
if (usz inc_pos = increment_string(last_entry, last_entry.rfind(".SAVESTAT") - 1); inc_pos != umax)
{
// Increment entry suffix - ID has become wider in length (keeps the string in alphbetic ordering)
ensure(inc_pos >= 2);
ensure(last_entry[inc_pos - 2]++ != 'z');
}
return last_entry;
}
// Fallback - create new file
}
const std::string save_id = fmt::format("%d", abs_id);

View File

@ -44,4 +44,4 @@ std::vector<version_entry> get_savestate_versioning_data(fs::file&& file, std::s
bool is_savestate_compatible(fs::file&& file, std::string_view filepath);
bool is_savestate_compatible(const std::string& filepath);
std::vector<version_entry> read_used_savestate_versions();
std::string get_savestate_file(std::string_view title_id, std::string_view boot_path, s64 abs_id, s64 rel_id);
std::string get_savestate_file(std::string_view title_id, std::string_view boot_path, s64 rel_id, u64 aggregate_file_size = umax);

View File

@ -332,6 +332,8 @@ struct cfg_root : cfg::node
cfg::_bool compatible_mode{ this, "Compatible Savestate Mode", false }; // SPU emulation optimized for savestate compatibility (off by default for performance reasons)
cfg::_bool state_inspection_mode{ this, "Inspection Mode Savestates" }; // Save memory stored in executable files, thus allowing to view state without any files (for debugging)
cfg::_bool save_disc_game_data{ this, "Save Disc Game Data", false };
cfg::uint<0, 64> max_files{ this, "Maximum SaveState Files", 4 };
cfg::uint<0, 1024 * 512> max_files_size{ this, "Maximum SaveState Files Space (MiB)", 4096 };
} savestate{this};
struct node_misc : cfg::node

View File

@ -50,7 +50,7 @@ LOG_CHANNEL(sys_log, "SYS");
extern atomic_t<bool> g_system_progress_canceled;
std::string get_savestate_file(std::string_view title_id, std::string_view boot_pat, s64 abs_id, s64 rel_id);
std::string get_savestate_file(std::string_view title_id, std::string_view boot_pat, s64 rel_id, u64 aggregate_file_size = umax);
game_list_frame::game_list_frame(std::shared_ptr<gui_settings> gui_settings, std::shared_ptr<emu_settings> emu_settings, std::shared_ptr<persistent_settings> persistent_settings, QWidget* parent)
: custom_dock_widget(tr("Game List"), parent)
@ -1199,13 +1199,20 @@ void game_list_frame::ShowContextMenu(const QPoint &pos)
extern bool is_savestate_compatible(const std::string& filepath);
if (const std::string sstate = get_savestate_file(current_game.serial, current_game.path, 0, 0); is_savestate_compatible(sstate))
if (const std::string sstate = get_savestate_file(current_game.serial, current_game.path, 1); is_savestate_compatible(sstate))
{
QAction* boot_state = menu.addAction(is_current_running_game
? tr("&Reboot with savestate")
: tr("&Boot with savestate"));
connect(boot_state, &QAction::triggered, [this, gameinfo, sstate]
connect(boot_state, &QAction::triggered, [this, gameinfo, sstate, current_game]
{
if (!get_savestate_file(current_game.serial, current_game.path, 2).empty())
{
// If there is any ambiguity, launch the savestate manager
Q_EMIT RequestSaveStateManager(gameinfo);
return;
}
sys_log.notice("Booting savestate from gamelist per context menu...");
Q_EMIT RequestBoot(gameinfo, cfg_mode::custom, "", sstate);
});

View File

@ -95,6 +95,7 @@ Q_SIGNALS:
void NotifyEmuSettingsChange();
void FocusToSearchBar();
void Refreshed();
void RequestSaveStateManager(const game_info& game);
public:
template <typename KeyType>

View File

@ -3412,6 +3412,17 @@ void main_window::CreateConnects()
ResizeIcons(idx);
});
connect(m_game_list_frame, &game_list_frame::RequestSaveStateManager, this, [this](const game_info& gameinfo)
{
savestate_manager_dialog* manager = new savestate_manager_dialog(m_gui_settings, std::vector<game_info>{gameinfo});
connect(this, &main_window::RequestDialogRepaint, manager, &savestate_manager_dialog::HandleRepaintUiRequest);
connect(manager, &savestate_manager_dialog::RequestBoot, this, [this, gameinfo](const std::string& path)
{
Boot(path, gameinfo->info.serial, false, false, cfg_mode::custom, "");
});
manager->show();
});
connect(m_list_mode_act_group, &QActionGroup::triggered, this, [this](QAction* act)
{
const bool is_list_act = act == ui->setlistModeListAct;