From b42d7bd2e4dab31f4e29dc60700a5d0a6509f5bc Mon Sep 17 00:00:00 2001 From: Garrett Smith Date: Sat, 17 Jan 2026 22:13:33 -0800 Subject: [PATCH] move save APIs without changes --- librecomp/include/librecomp/game.hpp | 12 +- librecomp/src/pi.cpp | 182 ---------------------- ultramodern/include/ultramodern/save.hpp | 20 +++ ultramodern/src/save.cpp | 184 +++++++++++++++++++++++ 4 files changed, 205 insertions(+), 193 deletions(-) create mode 100644 ultramodern/include/ultramodern/save.hpp create mode 100644 ultramodern/src/save.cpp diff --git a/librecomp/include/librecomp/game.hpp b/librecomp/include/librecomp/game.hpp index 6df55ee..9e55b72 100644 --- a/librecomp/include/librecomp/game.hpp +++ b/librecomp/include/librecomp/game.hpp @@ -9,21 +9,12 @@ #include namespace recomp { - enum class SaveType { - None, - Eep4k, - Eep16k, - Sram, - Flashram, - AllowAll, // Allows all save types to work and reports eeprom size as 16kbit. - }; - struct GameEntry { uint64_t rom_hash; std::string internal_name; std::u8string game_id; std::string mod_game_id; - SaveType save_type = SaveType::None; + ultramodern::SaveType save_type = ultramodern::SaveType::None; bool is_enabled; // Only needed for mod function hooking support, not needed if `has_compressed_code` is false. std::vector (*decompression_routine)(std::span compressed_rom) = nullptr; @@ -109,7 +100,6 @@ namespace recomp { /// void start(const Configuration& cfg); - SaveType get_save_type(); bool eeprom_allowed(); bool sram_allowed(); bool flashram_allowed(); diff --git a/librecomp/src/pi.cpp b/librecomp/src/pi.cpp index 43509ba..546aa63 100644 --- a/librecomp/src/pi.cpp +++ b/librecomp/src/pi.cpp @@ -84,188 +84,6 @@ void recomp::do_rom_pio(uint8_t* rdram, gpr ram_address, uint32_t physical_addr) MEM_B(3, ram_address) = *rom_addr++; } -struct { - std::vector 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; - -const std::u8string save_folder = u8"saves"; - -extern std::filesystem::path config_path; - -std::filesystem::path ultramodern::get_save_file_path() { - return save_context.save_file_path; -} - -void 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 = recomp::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 = !recomp::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 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 save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count) { - assert(offset + count <= save_context.save_buffer.size()); - - { - std::lock_guard lock { save_context.save_buffer_mutex }; - for (gpr i = 0; i < count; i++) { - save_context.save_buffer[offset + i] = MEM_B(i, rdram_address); - } - } - - save_context.write_sempahore.signal(); -} - -void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count) { - assert(offset + count <= save_context.save_buffer.size()); - - std::lock_guard lock { save_context.save_buffer_mutex }; - for (gpr i = 0; i < count; i++) { - MEM_B(i, rdram_address) = save_context.save_buffer[offset + i]; - } -} - -void 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 get_save_size(recomp::SaveType save_type) { - switch (save_type) { - case recomp::SaveType::AllowAll: - case recomp::SaveType::Flashram: - return 0x20000; - case recomp::SaveType::Sram: - return 0x8000; - case recomp::SaveType::Eep16k: - return 0x800; - case recomp::SaveType::Eep4k: - return 0x200; - case recomp::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 = recomp::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_ARG1) { - set_save_file_path(u8"", recomp::current_game_id()); - - save_context.save_buffer.resize(get_save_size(recomp::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(); - } -} - void do_dma(RDRAM_ARG PTR(OSMesgQueue) mq, gpr rdram_address, uint32_t physical_addr, uint32_t size, uint32_t direction) { // TODO asynchronous transfer // TODO implement unaligned DMA correctly diff --git a/ultramodern/include/ultramodern/save.hpp b/ultramodern/include/ultramodern/save.hpp new file mode 100644 index 0000000..d813443 --- /dev/null +++ b/ultramodern/include/ultramodern/save.hpp @@ -0,0 +1,20 @@ +#ifndef __ULTRAMODERN_SAVE_HPP__ +#define __ULTRAMODERN_SAVE_HPP__ + +#include + +namespace ultramodern { + enum class SaveType { + None, + Eep4k, + Eep16k, + Sram, + Flashram, + AllowAll, // Allows all save types to work and reports eeprom size as 16kbit. + }; + + SaveType get_save_type(); +} + +#endif // __ULTRAMODERN_SAVE_HPP__ + diff --git a/ultramodern/src/save.cpp b/ultramodern/src/save.cpp new file mode 100644 index 0000000..c24c98b --- /dev/null +++ b/ultramodern/src/save.cpp @@ -0,0 +1,184 @@ +#include + +struct { + std::vector 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; + +const std::u8string save_folder = u8"saves"; + +extern std::filesystem::path config_path; + +std::filesystem::path ultramodern::get_save_file_path() { + return save_context.save_file_path; +} + +void 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 = recomp::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 = !recomp::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 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 save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count) { + assert(offset + count <= save_context.save_buffer.size()); + + { + std::lock_guard lock { save_context.save_buffer_mutex }; + for (gpr i = 0; i < count; i++) { + save_context.save_buffer[offset + i] = MEM_B(i, rdram_address); + } + } + + save_context.write_sempahore.signal(); +} + +void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count) { + assert(offset + count <= save_context.save_buffer.size()); + + std::lock_guard lock { save_context.save_buffer_mutex }; + for (gpr i = 0; i < count; i++) { + MEM_B(i, rdram_address) = save_context.save_buffer[offset + i]; + } +} + +void 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 get_save_size(recomp::SaveType save_type) { + switch (save_type) { + case recomp::SaveType::AllowAll: + case recomp::SaveType::Flashram: + return 0x20000; + case recomp::SaveType::Sram: + return 0x8000; + case recomp::SaveType::Eep16k: + return 0x800; + case recomp::SaveType::Eep4k: + return 0x200; + case recomp::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 = recomp::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_ARG1) { + set_save_file_path(u8"", recomp::current_game_id()); + + save_context.save_buffer.resize(get_save_size(recomp::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(); + } +} +