N64ModernRuntime/ultramodern/src/save.cpp
2026-01-18 12:01:53 -08:00

207 lines
6.8 KiB
C++

#include <filesystem>
#include <mutex>
#include <thread>
#include <vector>
#include <ultramodern/files.hpp>
#include <ultramodern/save.hpp>
#include <ultramodern/ultramodern.hpp>
struct {
std::vector<char> save_buffer;
std::thread saving_thread;
std::filesystem::path save_file_path;
moodycamel::LightweightSemaphore write_sempahore;
// Used to tell the saving thread that a file swap is pending.
moodycamel::LightweightSemaphore swap_file_pending_sempahore;
// Used to tell the consumer thread that the saving thread is ready for a file swap.
moodycamel::LightweightSemaphore swap_file_ready_sempahore;
std::mutex save_buffer_mutex;
} save_context;
// The current game's save directory within the config path.
const std::u8string save_folder = u8"saves";
// The current game's config directory path.
extern std::filesystem::path config_path;
// The current game's save type.
ultramodern::SaveType save_type = ultramodern::SaveType::None;
void ultramodern::set_save_type(ultramodern::SaveType type) {
save_type = type;
}
ultramodern::SaveType ultramodern::get_save_type() {
return save_type;
}
bool ultramodern::eeprom_allowed() {
return
save_type == SaveType::Eep4k ||
save_type == SaveType::Eep16k ||
save_type == SaveType::AllowAll;
}
bool ultramodern::sram_allowed() {
return
save_type == SaveType::Sram ||
save_type == SaveType::AllowAll;
}
bool ultramodern::flashram_allowed() {
return
save_type == SaveType::Flashram ||
save_type == SaveType::AllowAll;
}
std::filesystem::path ultramodern::get_save_file_path() {
return save_context.save_file_path;
}
void ultramodern::set_save_file_path(const std::u8string& subfolder, const std::u8string& name) {
std::filesystem::path save_folder_path = config_path / save_folder;
if (!subfolder.empty()) {
save_folder_path = save_folder_path / subfolder;
}
save_context.save_file_path = save_folder_path / (name + u8".bin");
}
void update_save_file() {
bool saving_failed = false;
{
std::ofstream save_file = ultramodern::open_output_file_with_backup(ultramodern::get_save_file_path(), std::ios_base::binary);
if (save_file.good()) {
std::lock_guard lock{ save_context.save_buffer_mutex };
save_file.write(save_context.save_buffer.data(), save_context.save_buffer.size());
}
else {
saving_failed = true;
}
}
if (!saving_failed) {
saving_failed = !ultramodern::finalize_output_file_with_backup(ultramodern::get_save_file_path());
}
if (saving_failed) {
ultramodern::error_handling::message_box("Failed to write to the save file. Check your file permissions and whether the save folder has been moved to Dropbox or similar, as this can cause issues.");
}
}
extern std::atomic_bool exited;
void saving_thread_func(RDRAM_ARG1) {
while (!exited) {
bool save_buffer_updated = false;
// Repeatedly wait for a new action to be sent.
constexpr int64_t wait_time_microseconds = 10000;
constexpr int max_actions = 128;
int num_actions = 0;
// Wait up to the given timeout for a write to come in. Allow multiple writes to coalesce together into a single save.
// Cap the number of coalesced writes to guarantee that the save buffer eventually gets written out to the file even if the game
// is constantly sending writes.
while (save_context.write_sempahore.wait(wait_time_microseconds) && num_actions < max_actions) {
save_buffer_updated = true;
num_actions++;
}
// If an action came through that affected the save file, save the updated contents.
if (save_buffer_updated) {
update_save_file();
}
if (save_context.swap_file_pending_sempahore.tryWait()) {
save_context.swap_file_ready_sempahore.signal();
}
}
}
void ultramodern::save_write_ptr(const void* in, uint32_t offset, uint32_t count) {
assert(offset + count <= save_context.save_buffer.size());
{
std::lock_guard lock { save_context.save_buffer_mutex };
memcpy(&save_context.save_buffer[offset], in, count);
}
save_context.write_sempahore.signal();
}
void ultramodern::save_read_ptr(void *out, uint32_t offset, uint32_t count) {
assert(offset + count <= save_context.save_buffer.size());
std::lock_guard lock { save_context.save_buffer_mutex };
std::memcpy(out, &save_context.save_buffer[offset], count);
}
void ultramodern::save_clear(uint32_t start, uint32_t size, char value) {
assert(start + size < save_context.save_buffer.size());
{
std::lock_guard lock { save_context.save_buffer_mutex };
std::fill_n(save_context.save_buffer.begin() + start, size, value);
}
save_context.write_sempahore.signal();
}
size_t ultramodern::get_save_size(ultramodern::SaveType save_type) {
switch (save_type) {
case ultramodern::SaveType::AllowAll:
case ultramodern::SaveType::Flashram:
return 0x20000;
case ultramodern::SaveType::Sram:
return 0x8000;
case ultramodern::SaveType::Eep16k:
return 0x800;
case ultramodern::SaveType::Eep4k:
return 0x200;
case ultramodern::SaveType::None:
return 0;
}
return 0;
}
void read_save_file() {
std::filesystem::path save_file_path = ultramodern::get_save_file_path();
// Ensure the save file directory exists.
std::filesystem::create_directories(save_file_path.parent_path());
// Read the save file if it exists.
std::ifstream save_file = ultramodern::open_input_file_with_backup(save_file_path, std::ios_base::binary);
if (save_file.good()) {
save_file.read(save_context.save_buffer.data(), save_context.save_buffer.size());
}
else {
// Otherwise clear the save file to all zeroes.
std::fill(save_context.save_buffer.begin(), save_context.save_buffer.end(), 0);
}
}
void ultramodern::init_saving(RDRAM_ARG const std::u8string& name) {
set_save_file_path(u8"", name);
save_context.save_buffer.resize(get_save_size(ultramodern::get_save_type()));
read_save_file();
save_context.saving_thread = std::thread{saving_thread_func, PASS_RDRAM};
}
void ultramodern::change_save_file(const std::u8string& subfolder, const std::u8string& name) {
// Tell the saving thread that a file swap is pending.
save_context.swap_file_pending_sempahore.signal();
// Wait until the saving thread indicates it's ready to swap files.
save_context.swap_file_ready_sempahore.wait();
// Perform the save file swap.
set_save_file_path(subfolder, name);
read_save_file();
}
void ultramodern::join_saving_thread() {
if (save_context.saving_thread.joinable()) {
save_context.saving_thread.join();
}
}