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:
parent
67703b49d8
commit
8f3b9a9864
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -95,6 +95,7 @@ Q_SIGNALS:
|
||||
void NotifyEmuSettingsChange();
|
||||
void FocusToSearchBar();
|
||||
void Refreshed();
|
||||
void RequestSaveStateManager(const game_info& game);
|
||||
|
||||
public:
|
||||
template <typename KeyType>
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user