From 3538a83860bf673f436b8f674fa1e8df799a55c4 Mon Sep 17 00:00:00 2001 From: Nick Benthem Date: Thu, 19 Mar 2026 13:29:20 -0400 Subject: [PATCH 1/4] build(mac): minimal support updates for local builds --- BUILDING.md | 26 +++++++++++++++++++++---- CMakeLists.txt | 49 ++++++++++++++++++++++++++++++++++++++++++++++-- patches/Makefile | 2 +- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index e1dfea4..8f88785 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -7,6 +7,12 @@ These steps cover: decompressing the ROM, running the recompiler and finally bui ## 1. Building the Bomberman 64 decompilation You will need a decompressed ROM. Build the project at https://github.com/Bomberhackers/bm64 which will generate a decompressed ROM from an existing locally dumped BM64 US ROM. +Place the decompressed ROM in the repository root as: + +```bash +bm64.decomp.us.z64 +``` + ## 2. Clone the Bomberman 64 Recompiled Repository This project makes use of submodules so you will need to clone the repository with the `--recurse-submodules` flag. @@ -26,6 +32,16 @@ For Linux the instructions for Ubuntu are provided, but you can find the equival sudo apt-get install cmake ninja libsdl2-dev libgtk-3-dev lld llvm clang-15 ``` +### macOS +For macOS, install dependencies with Homebrew: + +```bash +brew install cmake ninja sdl2 freetype llvm lld +python3 -m pip install --user macholib +``` + +`macholib` is required by the macOS linker wrapper used by this project. + ### Windows You will need to install [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/). In the setup process you'll need to select the following options and tools for installation: @@ -33,7 +49,7 @@ In the setup process you'll need to select the following options and tools for i - C++ Clang Compiler for Windows - C++ CMake tools for Windows -The other tool necessary will be `make` which can be installe via [Chocolatey](https://chocolatey.org/): +The other tool necessary will be `make` which can be installed via [Chocolatey](https://chocolatey.org/): ```bash choco install make ``` @@ -58,12 +74,14 @@ If you prefer the command line or you're on a Unix platform you can build the pr ```bash cmake -S . -B build-cmake -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -G Ninja -DCMAKE_BUILD_TYPE=Release # or Debug if you want to debug -cmake --build build-cmake --target BM64Recompiled -j$(nproc) --config Release # or Debug +cmake --build build-cmake --target BM64Recompiled --config Release --parallel # or Debug ``` ## 6. Success -VoilĂ ! You should now have a `BM64Recompiled` executable in the build directory! If you used Visual Studio this will be `out/build/x64-[Configuration]` and if you used the provided CMake commands then this will be `build-cmake`. You will need to run the executable out of the root folder of this project or copy the assets folder to the build folder to run it. +VoilĂ ! You should now have a BM64Recompiled executable in the build directory! If you used Visual Studio this will be `out/build/x64-[Configuration]` and if you used the provided CMake commands then this will be `build-cmake`. + +On macOS, the output is `build-cmake/BM64Recompiled.app`. -> [!IMPORTANT] +> [!IMPORTANT] > In the game itself, you should be using a standard ROM, not the decompressed one. diff --git a/CMakeLists.txt b/CMakeLists.txt index c104f86..e253150 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,20 @@ endif() if (APPLE) enable_language(OBJC OBJCXX) + + execute_process( + COMMAND /usr/bin/python3 -c "import macholib" + RESULT_VARIABLE MACHOLIB_IMPORT_RESULT + OUTPUT_QUIET + ERROR_QUIET + ) + if (NOT MACHOLIB_IMPORT_RESULT EQUAL 0) + message(WARNING + "Python package 'macholib' was not found for /usr/bin/python3. " + "The custom macOS linker wrapper at .github/macos/ld64 will fail at link time without it. " + "Install it with: python3 -m pip install --user macholib" + ) + endif() endif() if (CMAKE_SYSTEM_NAME MATCHES "Linux") @@ -113,13 +127,44 @@ set_source_files_properties(${CMAKE_SOURCE_DIR}/RecompiledPatches/patches.c PROP # Build patches elf if(NOT DEFINED PATCHES_C_COMPILER) - set(PATCHES_C_COMPILER clang) + if (APPLE) + if (EXISTS "/opt/local/bin/clang-mp-18") + set(PATCHES_C_COMPILER "/opt/local/bin/clang-mp-18") + elseif (EXISTS "/opt/homebrew/opt/llvm/bin/clang") + set(PATCHES_C_COMPILER "/opt/homebrew/opt/llvm/bin/clang") + elseif (EXISTS "/usr/local/opt/llvm/bin/clang") + set(PATCHES_C_COMPILER "/usr/local/opt/llvm/bin/clang") + else() + set(PATCHES_C_COMPILER clang) + endif() + else() + set(PATCHES_C_COMPILER clang) + endif() endif() if(NOT DEFINED PATCHES_LD) - set(PATCHES_LD ld.lld) + if (APPLE) + if (EXISTS "/opt/local/bin/ld.lld-mp-18") + set(PATCHES_LD "/opt/local/bin/ld.lld-mp-18") + elseif (EXISTS "/opt/homebrew/opt/lld/bin/ld.lld") + set(PATCHES_LD "/opt/homebrew/opt/lld/bin/ld.lld") + elseif (EXISTS "/opt/homebrew/opt/llvm/bin/ld.lld") + set(PATCHES_LD "/opt/homebrew/opt/llvm/bin/ld.lld") + elseif (EXISTS "/usr/local/opt/lld/bin/ld.lld") + set(PATCHES_LD "/usr/local/opt/lld/bin/ld.lld") + elseif (EXISTS "/usr/local/opt/llvm/bin/ld.lld") + set(PATCHES_LD "/usr/local/opt/llvm/bin/ld.lld") + else() + set(PATCHES_LD ld.lld) + endif() + else() + set(PATCHES_LD ld.lld) + endif() endif() +message(STATUS "PATCHES_C_COMPILER = ${PATCHES_C_COMPILER}") +message(STATUS "PATCHES_LD = ${PATCHES_LD}") + add_custom_target(PatchesBin COMMAND ${CMAKE_COMMAND} -E env CC=${PATCHES_C_COMPILER} LD=${PATCHES_LD} make WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/patches diff --git a/patches/Makefile b/patches/Makefile index 13f0e4a..e6f58d7 100644 --- a/patches/Makefile +++ b/patches/Makefile @@ -5,7 +5,7 @@ LD ?= ld.lld CFLAGS := -target mips -mips2 -mabi=32 -O0 -G0 -mno-abicalls -mno-odd-spreg -mno-check-zero-division \ -fomit-frame-pointer -ffast-math -fno-unsafe-math-optimizations -fno-builtin-memset \ - -Wall -Wextra -Wno-incompatible-library-redeclaration -Wno-unused-parameter -Wno-unknown-pragmas -Wno-unused-variable -Wno-unused-but-set-variable -Wno-missing-braces -Wno-unsupported-floating-point-opt -Wno-switch -D_MIPS_SZLONG=32 + -Wall -Wextra -Wno-incompatible-library-redeclaration -Wno-unused-parameter -Wno-unknown-pragmas -Wno-unused-variable -Wno-unused-but-set-variable -Wno-missing-braces -Wno-unsupported-floating-point-opt -Wno-switch -Wno-incompatible-pointer-types -Wno-int-conversion -D_MIPS_SZLONG=32 CPPFLAGS := -nostdinc -DF3DEX_GBI -D_LANGUAGE_C -DMIPS -I dummy_headers -I ../lib/sf64/include -I ../lib/sf64/include/libultra -I ../lib/sf64/src -I ../lib/rt64/include -I ../lib/N64ModernRuntime/ultramodern/include -I ../lib/N64ModernRuntime/ultramodern/include/ultramodern LDFLAGS := -nostdlib -T patches.ld -T syms.ld -Map patches.map --unresolved-symbols=ignore-all --emit-relocs From 6729182900c6c0e8ae287a52abd5150d3ff71180 Mon Sep 17 00:00:00 2001 From: Nick Benthem Date: Wed, 25 Mar 2026 16:45:43 -0400 Subject: [PATCH 2/4] Add N64ModernRuntime controller pak patch Apply controller pak support patch from github.com/gcsmith/N64ModernRuntime/tree/controller_pak at CMake configure time. Moves files.hpp from librecomp to ultramodern and adds controller pak device reporting. --- CMakeLists.txt | 23 + patches/n64modernruntime-controller-pak.patch | 2043 +++++++++++++++++ 2 files changed, 2066 insertions(+) create mode 100644 patches/n64modernruntime-controller-pak.patch diff --git a/CMakeLists.txt b/CMakeLists.txt index e253150..188c37d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,29 @@ SET(RMLUI_TESTS_ENABLED OFF CACHE BOOL "" FORCE) add_subdirectory(${CMAKE_SOURCE_DIR}/lib/RmlUi) target_compile_definitions(rmlui_core PRIVATE LUNASVG_BUILD_STATIC) +# Apply controller pak patch to N64ModernRuntime if not already applied. +# Source: https://github.com/gcsmith/N64ModernRuntime/tree/controller_pak +execute_process( + COMMAND git apply --check ${CMAKE_SOURCE_DIR}/patches/n64modernruntime-controller-pak.patch + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime + RESULT_VARIABLE PATCH_CHECK_RESULT + OUTPUT_QUIET + ERROR_QUIET +) +if (PATCH_CHECK_RESULT EQUAL 0) + execute_process( + COMMAND git apply ${CMAKE_SOURCE_DIR}/patches/n64modernruntime-controller-pak.patch + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime + RESULT_VARIABLE PATCH_APPLY_RESULT + ) + if (NOT PATCH_APPLY_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to apply N64ModernRuntime controller pak patch") + endif() + message(STATUS "Applied N64ModernRuntime controller pak patch") +else() + message(STATUS "N64ModernRuntime controller pak patch already applied (or conflicts exist)") +endif() + add_subdirectory(${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime) target_include_directories(rt64 PRIVATE ${CMAKE_BINARY_DIR}/rt64/src) diff --git a/patches/n64modernruntime-controller-pak.patch b/patches/n64modernruntime-controller-pak.patch new file mode 100644 index 0000000..02a3d78 --- /dev/null +++ b/patches/n64modernruntime-controller-pak.patch @@ -0,0 +1,2043 @@ +diff --git a/librecomp/CMakeLists.txt b/librecomp/CMakeLists.txt +index fcc3337..5e000ed 100644 +--- a/librecomp/CMakeLists.txt ++++ b/librecomp/CMakeLists.txt +@@ -13,7 +13,6 @@ add_library(librecomp STATIC + "${CMAKE_CURRENT_SOURCE_DIR}/src/eep.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/euc-jp.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/extensions.cpp" +- "${CMAKE_CURRENT_SOURCE_DIR}/src/files.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/flash.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/heap.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/math_routines.cpp" +@@ -23,8 +22,8 @@ add_library(librecomp STATIC + "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_manifest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_config_api.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/overlays.cpp" +- "${CMAKE_CURRENT_SOURCE_DIR}/src/pak.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/patcher.cpp" ++ "${CMAKE_CURRENT_SOURCE_DIR}/src/pfs.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/pi.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/print.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/recomp.cpp" +diff --git a/librecomp/include/librecomp/game.hpp b/librecomp/include/librecomp/game.hpp +index 6df55ee..1695765 100644 +--- a/librecomp/include/librecomp/game.hpp ++++ b/librecomp/include/librecomp/game.hpp +@@ -7,17 +7,10 @@ + #include "recomp.h" + #include "rsp.hpp" + #include ++#include + + namespace recomp { +- enum class SaveType { +- None, +- Eep4k, +- Eep16k, +- Sram, +- Flashram, +- AllowAll, // Allows all save types to work and reports eeprom size as 16kbit. +- }; +- ++ using SaveType = ultramodern::SaveType; + struct GameEntry { + uint64_t rom_hash; + std::string internal_name; +@@ -109,11 +102,6 @@ namespace recomp { + /// + void start(const Configuration& cfg); + +- SaveType get_save_type(); +- bool eeprom_allowed(); +- bool sram_allowed(); +- bool flashram_allowed(); +- + void start_game(const std::u8string& game_id); + std::u8string current_game_id(); + std::string current_mod_game_id(); +diff --git a/librecomp/include/librecomp/helpers.hpp b/librecomp/include/librecomp/helpers.hpp +index d8f5afd..eb9f86f 100644 +--- a/librecomp/include/librecomp/helpers.hpp ++++ b/librecomp/include/librecomp/helpers.hpp +@@ -7,8 +7,7 @@ + #include + + template +-T _arg(uint8_t* rdram, recomp_context* ctx) { +- static_assert(index < 4, "Only args 0 through 3 supported"); ++T _arg(uint8_t* rdram, recomp_context* ctx) requires(index < 4) { + gpr raw_arg = (&ctx->r4)[index]; + if constexpr (std::is_same_v) { + if constexpr (index < 2) { +@@ -38,6 +37,25 @@ T _arg(uint8_t* rdram, recomp_context* ctx) { + } + } + ++template ++T _arg(uint8_t* rdram, recomp_context* ctx) requires(index >= 4) { ++ const auto raw_arg = MEM_W(index * 4, ctx->r29); ++ if constexpr (std::is_pointer_v) { ++ static_assert (!std::is_pointer_v>, "Double pointers not supported"); ++ return TO_PTR(std::remove_pointer_t, raw_arg); ++ } ++ else if constexpr (std::is_integral_v) { ++ static_assert(sizeof(T) <= 4, "64-bit args not supported"); ++ return static_cast(raw_arg); ++ } ++ else { ++ // static_assert in else workaround ++ [] () { ++ static_assert(flag, "Unsupported type"); ++ }(); ++ } ++} ++ + inline float _arg_float_a1(uint8_t* rdram, recomp_context* ctx) { + (void)rdram; + union { +diff --git a/librecomp/src/eep.cpp b/librecomp/src/eep.cpp +index 048246e..94fedf6 100644 +--- a/librecomp/src/eep.cpp ++++ b/librecomp/src/eep.cpp +@@ -1,20 +1,19 @@ + #include "recomp.h" + #include "librecomp/game.hpp" + +-#include "ultramodern/ultra64.h" +- +-void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count); +-void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count); ++#include ++#include + + constexpr int eeprom_block_size = 8; ++static std::vector save_buffer; + + extern "C" void osEepromProbe_recomp(uint8_t* rdram, recomp_context* ctx) { +- switch (recomp::get_save_type()) { +- case recomp::SaveType::AllowAll: +- case recomp::SaveType::Eep16k: ++ switch (ultramodern::get_save_type()) { ++ case ultramodern::SaveType::AllowAll: ++ case ultramodern::SaveType::Eep16k: + ctx->r2 = 0x02; // EEPROM_TYPE_16K + break; +- case recomp::SaveType::Eep4k: ++ case ultramodern::SaveType::Eep4k: + ctx->r2 = 0x01; // EEPROM_TYPE_4K + break; + default: +@@ -24,7 +23,7 @@ extern "C" void osEepromProbe_recomp(uint8_t* rdram, recomp_context* ctx) { + } + + extern "C" void osEepromWrite_recomp(uint8_t* rdram, recomp_context* ctx) { +- if (!recomp::eeprom_allowed()) { ++ if (!ultramodern::eeprom_allowed()) { + ultramodern::error_handling::message_box("Attempted to use EEPROM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -33,13 +32,17 @@ extern "C" void osEepromWrite_recomp(uint8_t* rdram, recomp_context* ctx) { + gpr buffer = ctx->r6; + int32_t nbytes = eeprom_block_size; + +- save_write(rdram, buffer, eep_address * eeprom_block_size, nbytes); ++ save_buffer.resize(nbytes); ++ for (uint32_t i = 0; i < nbytes; i++) { ++ save_buffer[i] = MEM_B(i, buffer); ++ } ++ ultramodern::save_write_ptr(save_buffer.data(), eep_address * eeprom_block_size, nbytes); + + ctx->r2 = 0; + } + + extern "C" void osEepromLongWrite_recomp(uint8_t* rdram, recomp_context* ctx) { +- if (!recomp::eeprom_allowed()) { ++ if (!ultramodern::eeprom_allowed()) { + ultramodern::error_handling::message_box("Attempted to use EEPROM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -50,13 +53,17 @@ extern "C" void osEepromLongWrite_recomp(uint8_t* rdram, recomp_context* ctx) { + + assert((nbytes % eeprom_block_size) == 0); + +- save_write(rdram, buffer, eep_address * eeprom_block_size, nbytes); ++ save_buffer.resize(nbytes); ++ for (uint32_t i = 0; i < nbytes; i++) { ++ save_buffer[i] = MEM_B(i, buffer); ++ } ++ ultramodern::save_write_ptr(save_buffer.data(), eep_address * eeprom_block_size, nbytes); + + ctx->r2 = 0; + } + + extern "C" void osEepromRead_recomp(uint8_t* rdram, recomp_context* ctx) { +- if (!recomp::eeprom_allowed()) { ++ if (!ultramodern::eeprom_allowed()) { + ultramodern::error_handling::message_box("Attempted to use EEPROM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -65,13 +72,17 @@ extern "C" void osEepromRead_recomp(uint8_t* rdram, recomp_context* ctx) { + gpr buffer = ctx->r6; + int32_t nbytes = eeprom_block_size; + +- save_read(rdram, buffer, eep_address * eeprom_block_size, nbytes); ++ save_buffer.resize(nbytes); ++ ultramodern::save_read_ptr(save_buffer.data(), eep_address * eeprom_block_size, nbytes); ++ for (uint32_t i = 0; i < nbytes; i++) { ++ MEM_B(i, buffer) = save_buffer[i]; ++ } + + ctx->r2 = 0; + } + + extern "C" void osEepromLongRead_recomp(uint8_t* rdram, recomp_context* ctx) { +- if (!recomp::eeprom_allowed()) { ++ if (!ultramodern::eeprom_allowed()) { + ultramodern::error_handling::message_box("Attempted to use EEPROM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -82,7 +93,11 @@ extern "C" void osEepromLongRead_recomp(uint8_t* rdram, recomp_context* ctx) { + + assert((nbytes % eeprom_block_size) == 0); + +- save_read(rdram, buffer, eep_address * eeprom_block_size, nbytes); ++ save_buffer.resize(nbytes); ++ ultramodern::save_read_ptr(save_buffer.data(), eep_address * eeprom_block_size, nbytes); ++ for (uint32_t i = 0; i < nbytes; i++) { ++ MEM_B(i, buffer) = save_buffer[i]; ++ } + + ctx->r2 = 0; + } +diff --git a/librecomp/src/flash.cpp b/librecomp/src/flash.cpp +index f46261c..9a877aa 100644 +--- a/librecomp/src/flash.cpp ++++ b/librecomp/src/flash.cpp +@@ -1,5 +1,6 @@ + #include + #include ++#include + #include + #include + #include "recomp.h" +@@ -15,15 +16,11 @@ constexpr uint32_t page_count = flash_size / page_size; + constexpr uint32_t sector_size = page_size * pages_per_sector; + constexpr uint32_t sector_count = flash_size / sector_size; + +-void save_write_ptr(const void* in, uint32_t offset, uint32_t count); +-void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count); +-void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count); +-void save_clear(uint32_t start, uint32_t size, char value); +- + std::array write_buffer; ++std::vector save_buffer; + + extern "C" void osFlashInit_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -32,7 +29,7 @@ extern "C" void osFlashInit_recomp(uint8_t * rdram, recomp_context * ctx) { + } + + extern "C" void osFlashReadStatus_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -43,7 +40,7 @@ extern "C" void osFlashReadStatus_recomp(uint8_t * rdram, recomp_context * ctx) + } + + extern "C" void osFlashReadId_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -57,7 +54,7 @@ extern "C" void osFlashReadId_recomp(uint8_t * rdram, recomp_context * ctx) { + } + + extern "C" void osFlashClearStatus_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -66,30 +63,30 @@ extern "C" void osFlashClearStatus_recomp(uint8_t * rdram, recomp_context * ctx) + } + + extern "C" void osFlashAllErase_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } + +- save_clear(0, ultramodern::save_size, 0xFF); ++ ultramodern::save_clear(0, ultramodern::save_size, 0xFF); + + ctx->r2 = 0; + } + + extern "C" void osFlashAllEraseThrough_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } + +- save_clear(0, ultramodern::save_size, 0xFF); ++ ultramodern::save_clear(0, ultramodern::save_size, 0xFF); + + ctx->r2 = 0; + } + + // This function is named sector but really means page. + extern "C" void osFlashSectorErase_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -102,14 +99,14 @@ extern "C" void osFlashSectorErase_recomp(uint8_t * rdram, recomp_context * ctx) + return; + } + +- save_clear(page_num * page_size, page_size, 0xFF); ++ ultramodern::save_clear(page_num * page_size, page_size, 0xFF); + + ctx->r2 = 0; + } + + // Same naming issue as above. + extern "C" void osFlashSectorEraseThrough_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -122,13 +119,13 @@ extern "C" void osFlashSectorEraseThrough_recomp(uint8_t * rdram, recomp_context + return; + } + +- save_clear(page_num * page_size, page_size, 0xFF); ++ ultramodern::save_clear(page_num * page_size, page_size, 0xFF); + + ctx->r2 = 0; + } + + extern "C" void osFlashCheckEraseEnd_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -138,7 +135,7 @@ extern "C" void osFlashCheckEraseEnd_recomp(uint8_t * rdram, recomp_context * ct + } + + extern "C" void osFlashWriteBuffer_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -160,7 +157,7 @@ extern "C" void osFlashWriteBuffer_recomp(uint8_t * rdram, recomp_context * ctx) + } + + extern "C" void osFlashWriteArray_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -168,13 +165,13 @@ extern "C" void osFlashWriteArray_recomp(uint8_t * rdram, recomp_context * ctx) + uint32_t page_num = ctx->r4; + + // Copy the write buffer into the save file +- save_write_ptr(write_buffer.data(), page_num * page_size, page_size); ++ ultramodern::save_write_ptr(write_buffer.data(), page_num * page_size, page_size); + + ctx->r2 = 0; + } + + extern "C" void osFlashReadArray_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +@@ -190,7 +187,11 @@ extern "C" void osFlashReadArray_recomp(uint8_t * rdram, recomp_context * ctx) { + uint32_t count = n_pages * page_size; + + // Read from the save file into the provided buffer +- save_read(PASS_RDRAM dramAddr, offset, count); ++ save_buffer.resize(count); ++ ultramodern::save_read_ptr(save_buffer.data(), offset, count); ++ for (uint32_t i = 0; i < count; i++) { ++ MEM_B(i, dramAddr) = save_buffer[i]; ++ } + + // Send the message indicating read completion + ultramodern::enqueue_external_message_src(mq, 0, false, ultramodern::EventMessageSource::Pi); +@@ -199,7 +200,7 @@ extern "C" void osFlashReadArray_recomp(uint8_t * rdram, recomp_context * ctx) { + } + + extern "C" void osFlashChange_recomp(uint8_t * rdram, recomp_context * ctx) { +- if (!recomp::flashram_allowed()) { ++ if (!ultramodern::flashram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } +diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp +index a75d253..b9b5134 100644 +--- a/librecomp/src/mod_manifest.cpp ++++ b/librecomp/src/mod_manifest.cpp +@@ -3,8 +3,8 @@ + #include "json/json.hpp" + + #include "recompiler/context.h" +-#include "librecomp/files.hpp" + #include "librecomp/mods.hpp" ++#include + + static bool read_json(std::ifstream input_file, nlohmann::json &json_out) { + if (!input_file.good()) { +@@ -27,7 +27,7 @@ static bool read_json_with_backups(const std::filesystem::path &path, nlohmann:: + } + + // Try reading and parsing the backup file. +- if (read_json(recomp::open_input_backup_file(path), json_out)) { ++ if (read_json(ultramodern::open_input_backup_file(path), json_out)) { + return true; + } + +diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp +index ff40212..e4ef3c8 100644 +--- a/librecomp/src/mods.cpp ++++ b/librecomp/src/mods.cpp +@@ -3,7 +3,7 @@ + #include + #include + +-#include "librecomp/files.hpp" ++#include + #include "librecomp/mods.hpp" + #include "librecomp/overlays.hpp" + #include "librecomp/game.hpp" +@@ -32,7 +32,7 @@ static bool read_json_with_backups(const std::filesystem::path &path, nlohmann:: + } + + // Try reading and parsing the backup file. +- if (read_json(recomp::open_input_backup_file(path), json_out)) { ++ if (read_json(ultramodern::open_input_backup_file(path), json_out)) { + return true; + } + +@@ -679,7 +679,7 @@ bool save_mod_config_storage(const std::filesystem::path &path, const std::strin + } + } + +- std::ofstream output_file = recomp::open_output_file_with_backup(path); ++ std::ofstream output_file = ultramodern::open_output_file_with_backup(path); + if (!output_file.good()) { + return false; + } +@@ -687,7 +687,7 @@ bool save_mod_config_storage(const std::filesystem::path &path, const std::strin + output_file << std::setw(4) << config_json; + output_file.close(); + +- return recomp::finalize_output_file_with_backup(path); ++ return ultramodern::finalize_output_file_with_backup(path); + } + + bool parse_mods_config(const std::filesystem::path &path, std::unordered_set &enabled_mods, std::vector &mod_order) { +@@ -720,7 +720,7 @@ bool save_mods_config(const std::filesystem::path &path, const std::unordered_se + config_json["enabled_mods"] = enabled_mods; + config_json["mod_order"] = mod_order; + +- std::ofstream output_file = recomp::open_output_file_with_backup(path); ++ std::ofstream output_file = ultramodern::open_output_file_with_backup(path); + if (!output_file.good()) { + return false; + } +@@ -728,7 +728,7 @@ bool save_mods_config(const std::filesystem::path &path, const std::unordered_se + output_file << std::setw(4) << config_json; + output_file.close(); + +- return recomp::finalize_output_file_with_backup(path); ++ return ultramodern::finalize_output_file_with_backup(path); + } + + void recomp::mods::ModContext::dirty_mod_configuration_thread_process() { +diff --git a/librecomp/src/pak.cpp b/librecomp/src/pak.cpp +deleted file mode 100644 +index 0be1513..0000000 +--- a/librecomp/src/pak.cpp ++++ /dev/null +@@ -1,51 +0,0 @@ +-#include "ultramodern/ultra64.h" +-#include "ultramodern/ultramodern.hpp" +- +-#include "recomp.h" +-#include "helpers.hpp" +- +-extern "C" void osPfsInitPak_recomp(uint8_t * rdram, recomp_context* ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsFreeBlocks_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsAllocateFile_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsDeleteFile_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsFileState_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsFindFile_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsReadWriteFile_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsChecker_recomp(uint8_t * rdram, recomp_context * ctx) { +- ctx->r2 = 1; // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsNumFiles_recomp(uint8_t * rdram, recomp_context * ctx) { +- s32* max_files = _arg<1, s32*>(rdram, ctx); +- s32* files_used = _arg<2, s32*>(rdram, ctx); +- +- *max_files = 0; +- *files_used = 0; +- +- _return(ctx, 1); // PFS_ERR_NOPACK +-} +- +-extern "C" void osPfsRepairId_recomp(uint8_t * rdram, recomp_context * ctx) { +- _return(ctx, 1); // PFS_ERR_NOPACK +-} +diff --git a/librecomp/src/pfs.cpp b/librecomp/src/pfs.cpp +new file mode 100644 +index 0000000..2145056 +--- /dev/null ++++ b/librecomp/src/pfs.cpp +@@ -0,0 +1,189 @@ ++#include "ultramodern/ultramodern.hpp" ++ ++#include "helpers.hpp" ++ ++extern "C" void osPfsInitPak_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSMesgQueue) mq = _arg<0, PTR(OSMesgQueue)>(rdram, ctx); ++ PTR(OSPfs) pfs = _arg<1, PTR(OSPfs)>(rdram, ctx); ++ int channel = _arg<2, int>(rdram, ctx); ++ ++ s32 ret = osPfsInitPak(PASS_RDRAM mq, pfs, channel); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsRepairId_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ ++ s32 ret = osPfsRepairId(PASS_RDRAM pfs); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsInit_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSMesgQueue) mq = _arg<0, PTR(OSMesgQueue)>(rdram, ctx); ++ PTR(OSPfs) pfs = _arg<1, PTR(OSPfs)>(rdram, ctx); ++ int channel = _arg<2, int>(rdram, ctx); ++ ++ s32 ret = osPfsInit(PASS_RDRAM mq, pfs, channel); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsReFormat_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ PTR(OSMesgQueue) mq = _arg<1, PTR(OSMesgQueue)>(rdram, ctx); ++ int channel = _arg<2, int>(rdram, ctx); ++ ++ s32 ret = osPfsReFormat(PASS_RDRAM pfs, mq, channel); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsChecker_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ ++ s32 ret = osPfsChecker(PASS_RDRAM pfs); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsAllocateFile_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ u16 company_code = _arg<1, u16>(rdram, ctx); ++ u32 game_code = _arg<2, u32>(rdram, ctx); ++ PTR(u8) game_name = _arg<3, PTR(u8)>(rdram, ctx); ++ PTR(u8) ext_name = _arg<4, PTR(u8)>(rdram, ctx); ++ int nbytes = _arg<5, int>(rdram, ctx); ++ PTR(s32) file_no = _arg<6, PTR(s32)>(rdram, ctx); ++ u8 game_name_proxy[PFS_FILE_NAME_LEN]; ++ u8 ext_name_proxy[PFS_FILE_EXT_LEN]; ++ ++ for (uint32_t i = 0; i < PFS_FILE_NAME_LEN; i++) { ++ game_name_proxy[i] = MEM_B(i, (gpr)game_name); ++ } ++ for (uint32_t i = 0; i < PFS_FILE_EXT_LEN; i++) { ++ ext_name_proxy[i] = MEM_B(i, (gpr)ext_name); ++ } ++ s32 ret = osPfsAllocateFile(PASS_RDRAM pfs, company_code, game_code, game_name_proxy, ext_name_proxy, nbytes, file_no); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsFindFile_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ u16 company_code = _arg<1, u16>(rdram, ctx); ++ u32 game_code = _arg<2, u32>(rdram, ctx); ++ PTR(u8) game_name = _arg<3, PTR(u8)>(rdram, ctx); ++ PTR(u8) ext_name = _arg<4, PTR(u8)>(rdram, ctx); ++ PTR(s32) file_no = _arg<5, PTR(s32)>(rdram, ctx); ++ u8 game_name_proxy[PFS_FILE_NAME_LEN]; ++ u8 ext_name_proxy[PFS_FILE_EXT_LEN]; ++ ++ for (uint32_t i = 0; i < PFS_FILE_NAME_LEN; i++) { ++ game_name_proxy[i] = MEM_B(i, (gpr)game_name); ++ } ++ for (uint32_t i = 0; i < PFS_FILE_EXT_LEN; i++) { ++ ext_name_proxy[i] = MEM_B(i, (gpr)ext_name); ++ } ++ s32 ret = osPfsFindFile(PASS_RDRAM pfs, company_code, game_code, game_name_proxy, ext_name_proxy, file_no); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsDeleteFile_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ u16 company_code = _arg<1, u16>(rdram, ctx); ++ u32 game_code = _arg<2, u32>(rdram, ctx); ++ PTR(u8) game_name = _arg<3, PTR(u8)>(rdram, ctx); ++ PTR(u8) ext_name = _arg<4, PTR(u8)>(rdram, ctx); ++ u8 game_name_proxy[PFS_FILE_NAME_LEN]; ++ u8 ext_name_proxy[PFS_FILE_EXT_LEN]; ++ ++ for (uint32_t i = 0; i < PFS_FILE_NAME_LEN; i++) { ++ game_name_proxy[i] = MEM_B(i, (gpr)game_name); ++ } ++ for (uint32_t i = 0; i < PFS_FILE_EXT_LEN; i++) { ++ ext_name_proxy[i] = MEM_B(i, (gpr)ext_name); ++ } ++ s32 ret = osPfsDeleteFile(PASS_RDRAM pfs, company_code, game_code, game_name_proxy, ext_name_proxy); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsReadWriteFile_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ s32 file_no = _arg<1, s32>(rdram, ctx); ++ u8 flag = _arg<2, u8>(rdram, ctx); ++ int offset = _arg<3, int>(rdram, ctx); ++ int nbytes = _arg<4, int>(rdram, ctx); ++ PTR(u8) data_buffer = _arg<5, PTR(u8)>(rdram, ctx); ++ std::vector data_buffer_proxy(nbytes); ++ ++ if (flag == PFS_WRITE) { ++ for (uint32_t i = 0; i < nbytes; i++) { ++ data_buffer_proxy[i] = MEM_B(i, (gpr)data_buffer); ++ } ++ } ++ s32 ret = osPfsReadWriteFile(PASS_RDRAM pfs, file_no, flag, offset, nbytes, data_buffer_proxy.data()); ++ if (flag == PFS_READ) { ++ for (uint32_t i = 0; i < nbytes; i++) { ++ MEM_B(i, (gpr)data_buffer) = data_buffer_proxy[i]; ++ } ++ } ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsFileState_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ s32 file_no = _arg<1, s32>(rdram, ctx); ++ PTR(OSPfsState) state = _arg<2, PTR(OSPfsState)>(rdram, ctx); ++ ++ s32 ret = osPfsFileState(PASS_RDRAM pfs, file_no, state); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsGetLabel_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ PTR(u8) label = _arg<1, PTR(u8)>(rdram, ctx); ++ PTR(int) len = _arg<2, PTR(int)>(rdram, ctx); ++ u8 label_proxy[32]; ++ ++ s32 ret = osPfsGetLabel(PASS_RDRAM pfs, label_proxy, len); ++ for (uint32_t i = 0; i < 32; i++) { ++ MEM_B(i, (gpr)label) = label_proxy[i]; ++ } ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsSetLabel_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ PTR(u8) label = _arg<1, PTR(u8)>(rdram, ctx); ++ u8 label_proxy[32]; ++ ++ for (uint32_t i = 0; i < 32; i++) { ++ label_proxy[i] = MEM_B(i, (gpr)label); ++ } ++ s32 ret = osPfsSetLabel(PASS_RDRAM pfs, label_proxy); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsIsPlug_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSMesgQueue) mq = _arg<0, PTR(OSMesgQueue)>(rdram, ctx); ++ PTR(u8) pattern = _arg<1, PTR(u8)>(rdram, ctx); ++ u8 pattern_proxy = 0; ++ ++ s32 ret = osPfsIsPlug(PASS_RDRAM mq, &pattern_proxy); ++ MEM_B(0, (gpr)pattern) = pattern_proxy; ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsFreeBlocks_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ PTR(s32) bytes_not_used = _arg<1, PTR(s32)>(rdram, ctx); ++ ++ s32 ret = osPfsFreeBlocks(PASS_RDRAM pfs, bytes_not_used); ++ _return(ctx, ret); ++} ++ ++extern "C" void osPfsNumFiles_recomp(uint8_t* rdram, recomp_context* ctx) { ++ PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); ++ PTR(s32) max_files = _arg<1, PTR(s32)>(rdram, ctx); ++ PTR(s32) files_used = _arg<2, PTR(s32)>(rdram, ctx); ++ ++ s32 ret = osPfsNumFiles(PASS_RDRAM pfs, max_files, files_used); ++ _return(ctx, ret); ++} ++ +diff --git a/librecomp/src/pi.cpp b/librecomp/src/pi.cpp +index 43509ba..67cd86d 100644 +--- a/librecomp/src/pi.cpp ++++ b/librecomp/src/pi.cpp +@@ -7,11 +7,12 @@ + #include "recomp.h" + #include "librecomp/addresses.hpp" + #include "librecomp/game.hpp" +-#include "librecomp/files.hpp" ++#include + #include + #include + + static std::vector rom; ++static std::vector save_buffer; + + bool recomp::is_rom_loaded() { + return !rom.empty(); +@@ -84,188 +85,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 +@@ -277,12 +96,16 @@ void do_dma(RDRAM_ARG PTR(OSMesgQueue) mq, gpr rdram_address, uint32_t physical_ + // Send a message to the mq to indicate that the transfer completed + ultramodern::enqueue_external_message_src(mq, 0, false, ultramodern::EventMessageSource::Pi); + } else if (physical_addr >= recomp::sram_base) { +- if (!recomp::sram_allowed()) { ++ if (!ultramodern::sram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use SRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } + // read sram +- save_read(rdram, rdram_address, physical_addr - recomp::sram_base, size); ++ save_buffer.resize(size); ++ ultramodern::save_read_ptr(save_buffer.data(), physical_addr - recomp::sram_base, size); ++ for (uint32_t i = 0; i < size; i++) { ++ MEM_B(i, rdram_address) = save_buffer[i]; ++ } + + // Send a message to the mq to indicate that the transfer completed + ultramodern::enqueue_external_message_src(mq, 0, false, ultramodern::EventMessageSource::Pi); +@@ -294,12 +117,16 @@ void do_dma(RDRAM_ARG PTR(OSMesgQueue) mq, gpr rdram_address, uint32_t physical_ + // write cart rom + throw std::runtime_error("ROM DMA write unimplemented"); + } else if (physical_addr >= recomp::sram_base) { +- if (!recomp::sram_allowed()) { ++ if (!ultramodern::sram_allowed()) { + ultramodern::error_handling::message_box("Attempted to use SRAM saving with other save type"); + ULTRAMODERN_QUICK_EXIT(); + } + // write sram +- save_write(rdram, rdram_address, physical_addr - recomp::sram_base, size); ++ save_buffer.resize(size); ++ for (uint32_t i = 0; i < size; i++) { ++ save_buffer[i] = MEM_B(i, rdram_address); ++ } ++ ultramodern::save_write_ptr(save_buffer.data(), physical_addr - recomp::sram_base, size); + + // Send a message to the mq to indicate that the transfer completed + ultramodern::enqueue_external_message_src(mq, 0, false, ultramodern::EventMessageSource::Pi); +diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp +index ea17675..a166c85 100644 +--- a/librecomp/src/recomp.cpp ++++ b/librecomp/src/recomp.cpp +@@ -21,6 +21,7 @@ + #include "xxHash/xxh3.h" + #include "ultramodern/ultramodern.hpp" + #include "ultramodern/error_handling.hpp" ++#include "ultramodern/save.hpp" + #include "librecomp/addresses.hpp" + #include "librecomp/mods.hpp" + #include "recompiler/live_recompiler.h" +@@ -57,8 +58,6 @@ std::unordered_map game_roms {}; + std::unique_ptr mod_context = std::make_unique(); + // The project's version. + recomp::Version project_version; +-// The current game's save type. +-recomp::SaveType save_type = recomp::SaveType::None; + + std::u8string recomp::GameEntry::stored_filename() const { + return game_id + u8".z64"; +@@ -687,8 +686,8 @@ bool wait_for_game_started(uint8_t* rdram, recomp_context* context) { + + recomp::init_heap(rdram, recomp::mod_rdram_start + mod_ram_used); + +- save_type = game_entry.save_type; +- ultramodern::init_saving(rdram); ++ ultramodern::set_save_type(game_entry.save_type); ++ ultramodern::init_saving(rdram, recomp::current_game_id()); + + try { + game_entry.entrypoint(rdram, context); +@@ -706,29 +705,6 @@ bool wait_for_game_started(uint8_t* rdram, recomp_context* context) { + } + } + +-recomp::SaveType recomp::get_save_type() { +- return save_type; +-} +- +-bool recomp::eeprom_allowed() { +- return +- save_type == SaveType::Eep4k || +- save_type == SaveType::Eep16k || +- save_type == SaveType::AllowAll; +-} +- +-bool recomp::sram_allowed() { +- return +- save_type == SaveType::Sram || +- save_type == SaveType::AllowAll; +-} +- +-bool recomp::flashram_allowed() { +- return +- save_type == SaveType::Flashram || +- save_type == SaveType::AllowAll; +-} +- + void recomp::start(const recomp::Configuration& cfg) { + project_version = cfg.project_version; + recomp::check_all_stored_roms(); +diff --git a/ultramodern/CMakeLists.txt b/ultramodern/CMakeLists.txt +index 5593172..54cdd9f 100644 +--- a/ultramodern/CMakeLists.txt ++++ b/ultramodern/CMakeLists.txt +@@ -10,11 +10,14 @@ add_library(ultramodern STATIC + "${CMAKE_CURRENT_SOURCE_DIR}/src/error_handling.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/events.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/extensions.cpp" ++ "${CMAKE_CURRENT_SOURCE_DIR}/src/files.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/input.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/mesgqueue.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/misc_ultra.cpp" ++ "${CMAKE_CURRENT_SOURCE_DIR}/src/pfs.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/renderer_context.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/rsp.cpp" ++ "${CMAKE_CURRENT_SOURCE_DIR}/src/save.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/scheduling.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/task_win32.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/threadqueue.cpp" +diff --git a/librecomp/include/librecomp/files.hpp b/ultramodern/include/ultramodern/files.hpp +similarity index 80% +rename from librecomp/include/librecomp/files.hpp +rename to ultramodern/include/ultramodern/files.hpp +index 63e3e9d..497d6d9 100644 +--- a/librecomp/include/librecomp/files.hpp ++++ b/ultramodern/include/ultramodern/files.hpp +@@ -1,14 +1,15 @@ +-#ifndef __RECOMP_FILES_H__ +-#define __RECOMP_FILES_H__ ++#ifndef __ULTRAMODERN_FILES_HPP__ ++#define __ULTRAMODERN_FILES_HPP__ + + #include + #include + +-namespace recomp { ++namespace ultramodern { + std::ifstream open_input_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::in); + std::ifstream open_input_backup_file(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::in); + std::ofstream open_output_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::out); + bool finalize_output_file_with_backup(const std::filesystem::path& filepath); +-}; ++} ++ ++#endif // __ULTRAMODERN_FILES_HPP__ + +-#endif +diff --git a/ultramodern/include/ultramodern/input.hpp b/ultramodern/include/ultramodern/input.hpp +index 1dab1b8..abb8e76 100644 +--- a/ultramodern/include/ultramodern/input.hpp ++++ b/ultramodern/include/ultramodern/input.hpp +@@ -15,7 +15,7 @@ namespace ultramodern { + enum class Pak { + None, + RumblePak, +- // ControllerPak, ++ ControllerPak, + // TransferPak + }; + +@@ -58,6 +58,11 @@ namespace ultramodern { + + void set_callbacks(const callbacks_t& callbacks); + } ++ ++ input::connected_device_info_t get_connected_device_info(int channel); ++ ++ int get_max_controllers(); + } + + #endif ++ +diff --git a/ultramodern/include/ultramodern/save.hpp b/ultramodern/include/ultramodern/save.hpp +new file mode 100644 +index 0000000..576c17c +--- /dev/null ++++ b/ultramodern/include/ultramodern/save.hpp +@@ -0,0 +1,49 @@ ++#ifndef __ULTRAMODERN_SAVE_HPP__ ++#define __ULTRAMODERN_SAVE_HPP__ ++ ++#include ++#include ++ ++namespace ultramodern { ++ enum class SaveType { ++ None, ++ Eep4k, ++ Eep16k, ++ Sram, ++ Flashram, ++ AllowAll, // Allows all save types to work and reports eeprom size as 16kbit. ++ }; ++ ++ void set_save_type(SaveType type); ++ ++ void set_save_file_path(const std::u8string& subfolder, const std::u8string& name); ++ ++ void init_saving(RDRAM_ARG const std::u8string& name); ++ ++ void change_save_file(const std::u8string& subfolder, const std::u8string& name); ++ ++ void join_saving_thread(); ++ ++ void save_write_ptr(const void* in, uint32_t offset, uint32_t count); ++ ++ void save_read_ptr(void *out, uint32_t offset, uint32_t count); ++ ++ void save_clear(uint32_t start, uint32_t size, char value); ++ ++ SaveType get_save_type(); ++ ++ size_t get_save_size(SaveType save_type); ++ ++ std::filesystem::path get_save_base_path(); ++ ++ std::filesystem::path get_save_file_path(); ++ ++ bool eeprom_allowed(); ++ ++ bool sram_allowed(); ++ ++ bool flashram_allowed(); ++} ++ ++#endif // __ULTRAMODERN_SAVE_HPP__ ++ +diff --git a/ultramodern/include/ultramodern/ultra64.h b/ultramodern/include/ultramodern/ultra64.h +index 31eb106..7ca579e 100644 +--- a/ultramodern/include/ultramodern/ultra64.h ++++ b/ultramodern/include/ultramodern/ultra64.h +@@ -74,6 +74,59 @@ typedef u64 OSTime; + #define OS_EVENT_THREADSTATUS 13 /* CPU thread status: used by rmon */ + #define OS_EVENT_PRENMI 14 /* Pre NMI interrupt */ + ++/* Controller errors */ ++ ++#define CONT_NO_RESPONSE_ERROR 0x8 ++#define CONT_OVERRUN_ERROR 0x4 ++#define CONT_RANGE_ERROR -1 ++#define CONT_FRAME_ERROR 0x2 ++#define CONT_COLLISION_ERROR 0x1 ++ ++/* Controller type */ ++ ++#define CONT_TYPE_NORMAL 0x0005 ++#define CONT_TYPE_MOUSE 0x0002 ++#define CONT_TYPE_VOICE 0x0100 ++ ++/* File System error number */ ++ ++#define PFS_ERR_NOPACK 1 /* no memory card is plugged or */ ++#define PFS_ERR_NEW_PACK 2 /* ram pack has been changed to a different one */ ++#define PFS_ERR_INCONSISTENT 3 /* need to run Pfschecker*/ ++#define PFS_ERR_CONTRFAIL CONT_OVERRUN_ERROR ++#define PFS_ERR_INVALID 5 /* invalid parameter or file not exist*/ ++#define PFS_ERR_BAD_DATA 6 /* the data read from pack are bad*/ ++#define PFS_DATA_FULL 7 /* no free pages on ram pack*/ ++#define PFS_DIR_FULL 8 /* no free directories on ram pack*/ ++#define PFS_ERR_EXIST 9 /* file exists*/ ++#define PFS_ERR_ID_FATAL 10 /* dead ram pack */ ++#define PFS_ERR_DEVICE 11 /* wrong device type*/ ++#define PFS_ERR_NO_GBCART 12 /* no gb cartridge (64GB-PAK) */ ++#define PFS_ERR_NEW_GBCART 13 /* gb cartridge may be changed */ ++ ++/* File System size */ ++ ++#define PFS_INODE_SIZE_PER_PAGE 128 ++#define PFS_FILE_NAME_LEN 16 ++#define PFS_FILE_EXT_LEN 4 ++#define PFS_BLOCKSIZE 32 /* bytes */ ++#define PFS_ONE_PAGE 8 /* blocks */ ++#define PFS_MAX_BANKS 62 ++ ++/* File System flag */ ++ ++#define PFS_READ 0 ++#define PFS_WRITE 1 ++#define PFS_CREATE 2 ++ ++/* File System status */ ++ ++#define PFS_INITIALIZED 0x1 ++#define PFS_CORRUPTED 0x2 ++#define PFS_ID_BROKEN 0x4 ++#define PFS_MOTOR_INITIALIZED 0x8 ++#define PFS_GBPAK_INITIALIZED 0x10 ++ + #define M_GFXTASK 1 + #define M_AUDTASK 2 + #define M_VIDTASK 3 +@@ -235,6 +288,17 @@ typedef struct { + u8 banks; + } OSPfs; + ++typedef struct { ++ u32 file_size; ++ u32 game_code; ++ char ext_name_0[2]; // insane layout due to ext_name starting on halfword boundary ++ u16 company_code; ++ char game_name_0[2]; ++ char ext_name_1[2]; ++ char game_name_1[12]; ++ char padding[2]; ++ char game_name_2[2]; ++} OSPfsState; // size = 0x20 + + // Controller + +@@ -314,6 +378,24 @@ s32 osMotorStop(RDRAM_ARG PTR(OSPfs)); + s32 osMotorStart(RDRAM_ARG PTR(OSPfs)); + s32 __osMotorAccess(RDRAM_ARG PTR(OSPfs), s32); + ++/* Controller PAK interface */ ++ ++s32 osPfsInitPak(RDRAM_ARG PTR(OSMesgQueue) queue, PTR(OSPfs) pfs, int channel); ++s32 osPfsRepairId(RDRAM_ARG PTR(OSPfs) pfs); ++s32 osPfsInit(RDRAM_ARG PTR(OSMesgQueue) queue, PTR(OSPfs) pfs, int channel); ++s32 osPfsReFormat(RDRAM_ARG PTR(OSPfs) pfs, PTR(OSMesgQueue) queue, int channel); ++s32 osPfsChecker(RDRAM_ARG PTR(OSPfs) pfs); ++s32 osPfsAllocateFile(RDRAM_ARG PTR(OSPfs) pfs, u16 company_code, u32 game_code, u8* game_name, u8* ext_name, int nbytes, PTR(s32) file_no); ++s32 osPfsFindFile(RDRAM_ARG PTR(OSPfs) pfs, u16 company_code, u32 game_code, u8* game_name, u8* ext_name, PTR(s32) file_no); ++s32 osPfsDeleteFile(RDRAM_ARG PTR(OSPfs) pfs, u16 company_code, u32 game_code, u8* game_name, u8* ext_name); ++s32 osPfsReadWriteFile(RDRAM_ARG PTR(OSPfs) pfs, s32 file_no, u8 flag, int offset, int nbytes, u8* data_buffer); ++s32 osPfsFileState(RDRAM_ARG PTR(OSPfs) pfs, s32 file_no, PTR(OSPfsState) state); ++s32 osPfsGetLabel(RDRAM_ARG PTR(OSPfs) pfs, u8* label, PTR(int) len); ++s32 osPfsSetLabel(RDRAM_ARG PTR(OSPfs) pfs, u8* label); ++s32 osPfsIsPlug(RDRAM_ARG PTR(OSMesgQueue) mq, u8* pattern); ++s32 osPfsFreeBlocks(RDRAM_ARG PTR(OSPfs) pfs, PTR(s32) bytes_not_used); ++s32 osPfsNumFiles(RDRAM_ARG PTR(OSPfs) pfs, PTR(s32) max_files, PTR(s32) files_used); ++ + #ifdef __cplusplus + } // extern "C" + #endif +diff --git a/librecomp/src/files.cpp b/ultramodern/src/files.cpp +similarity index 74% +rename from librecomp/src/files.cpp +rename to ultramodern/src/files.cpp +index af6f18d..0dd1a2f 100644 +--- a/librecomp/src/files.cpp ++++ b/ultramodern/src/files.cpp +@@ -1,15 +1,15 @@ +-#include "files.hpp" ++#include + + constexpr std::u8string_view backup_suffix = u8".bak"; + constexpr std::u8string_view temp_suffix = u8".temp"; + +-std::ifstream recomp::open_input_backup_file(const std::filesystem::path& filepath, std::ios_base::openmode mode) { ++std::ifstream ultramodern::open_input_backup_file(const std::filesystem::path& filepath, std::ios_base::openmode mode) { + std::filesystem::path backup_path{filepath}; + backup_path += backup_suffix; + return std::ifstream{backup_path, mode}; + } + +-std::ifstream recomp::open_input_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) { ++std::ifstream ultramodern::open_input_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) { + std::ifstream ret{filepath, mode}; + + // Check if the file failed to open and open the corresponding backup file instead if so. +@@ -20,7 +20,7 @@ std::ifstream recomp::open_input_file_with_backup(const std::filesystem::path& f + return ret; + } + +-std::ofstream recomp::open_output_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) { ++std::ofstream ultramodern::open_output_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) { + std::filesystem::path temp_path{filepath}; + temp_path += temp_suffix; + std::ofstream temp_file_out{ temp_path, mode }; +@@ -28,7 +28,7 @@ std::ofstream recomp::open_output_file_with_backup(const std::filesystem::path& + return temp_file_out; + } + +-bool recomp::finalize_output_file_with_backup(const std::filesystem::path& filepath) { ++bool ultramodern::finalize_output_file_with_backup(const std::filesystem::path& filepath) { + std::filesystem::path backup_path{filepath}; + backup_path += backup_suffix; + +diff --git a/ultramodern/src/input.cpp b/ultramodern/src/input.cpp +index 8e86cb1..5d723c5 100644 +--- a/ultramodern/src/input.cpp ++++ b/ultramodern/src/input.cpp +@@ -4,23 +4,28 @@ + #include "ultramodern/ultra64.h" + #include "ultramodern/ultramodern.hpp" + +-#define PFS_ERR_NOPACK 1 // no device inserted +-#define PFS_ERR_CONTRFAIL 4 // data transmission failure +-#define PFS_ERR_INVALID 5 // invalid parameter or invalid file +-#define PFS_ERR_DEVICE 11 // different type of device inserted ++#define MAXCONTROLLERS 4 + +-#define PFS_INITIALIZED 1 +-#define PFS_CORRUPTED 2 +-#define PFS_ID_BROKEN 4 +-#define PFS_MOTOR_INITIALIZED 8 +-#define PFS_GBPAK_INITIALIZED 16 ++static int max_controllers = 0; + + static ultramodern::input::callbacks_t input_callbacks {}; + ++int ultramodern::get_max_controllers() { ++ return max_controllers; ++} ++ + void ultramodern::input::set_callbacks(const callbacks_t& callbacks) { + input_callbacks = callbacks; + } + ++ultramodern::input::connected_device_info_t ultramodern::get_connected_device_info(int channel) { ++ ultramodern::input::connected_device_info_t device_info{}; ++ if (input_callbacks.get_connected_device_info != nullptr) { ++ device_info = input_callbacks.get_connected_device_info(channel); ++ } ++ return device_info; ++} ++ + static std::chrono::high_resolution_clock::time_point input_poll_time; + + static void update_poll_time() { +@@ -33,16 +38,6 @@ void ultramodern::measure_input_latency() { + #endif + } + +-#define MAXCONTROLLERS 4 +- +-#define CONT_NO_RESPONSE_ERROR 0x8 +- +-#define CONT_TYPE_NORMAL 0x0005 +-#define CONT_TYPE_MOUSE 0x0002 +-#define CONT_TYPE_VOICE 0x0100 +- +-static int max_controllers = 0; +- + /* Plain controller */ + + static u16 get_controller_type(ultramodern::input::Device device_type) { +@@ -69,12 +64,7 @@ static void __osContGetInitData(u8* pattern, OSContStatus *data) { + *pattern = 0x00; + + for (int controller = 0; controller < max_controllers; controller++) { +- ultramodern::input::connected_device_info_t device_info{}; +- +- if (input_callbacks.get_connected_device_info != nullptr) { +- device_info = input_callbacks.get_connected_device_info(controller); +- } +- ++ const auto device_info = ultramodern::get_connected_device_info(controller); + if (device_info.connected_device != ultramodern::input::Device::None) { + // Mark controller as present + +@@ -160,22 +150,18 @@ extern "C" void osContGetReadData(OSContPad *data) { + } + } + +-/* Rumble */ ++/* RumblePak */ + +-s32 osMotorInit(RDRAM_ARG PTR(OSMesgQueue) mq, PTR(OSPfs) pfs_, int channel) { ++extern "C" s32 osMotorInit(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSPfs) pfs_, int channel) { + OSPfs *pfs = TO_PTR(OSPfs, pfs_); + + // basic initialization performed regardless of connected/disconnected status +- pfs->queue = mq; ++ pfs->queue = mq_; + pfs->channel = channel; + pfs->activebank = 0xFF; + pfs->status = 0; + +- ultramodern::input::connected_device_info_t device_info{}; +- if (input_callbacks.get_connected_device_info != nullptr) { +- device_info = input_callbacks.get_connected_device_info(channel); +- } +- ++ const auto device_info = ultramodern::get_connected_device_info(channel); + if (device_info.connected_device != ultramodern::input::Device::Controller) { + return PFS_ERR_CONTRFAIL; + } +@@ -190,15 +176,15 @@ s32 osMotorInit(RDRAM_ARG PTR(OSMesgQueue) mq, PTR(OSPfs) pfs_, int channel) { + return 0; + } + +-s32 osMotorStop(RDRAM_ARG PTR(OSPfs) pfs) { ++extern "C" s32 osMotorStop(RDRAM_ARG PTR(OSPfs) pfs) { + return __osMotorAccess(PASS_RDRAM pfs, false); + } + +-s32 osMotorStart(RDRAM_ARG PTR(OSPfs) pfs) { ++extern "C" s32 osMotorStart(RDRAM_ARG PTR(OSPfs) pfs) { + return __osMotorAccess(PASS_RDRAM pfs, true); + } + +-s32 __osMotorAccess(RDRAM_ARG PTR(OSPfs) pfs_, s32 flag) { ++extern "C" s32 __osMotorAccess(RDRAM_ARG PTR(OSPfs) pfs_, s32 flag) { + OSPfs *pfs = TO_PTR(OSPfs, pfs_); + + if (!(pfs->status & PFS_MOTOR_INITIALIZED)) { +diff --git a/ultramodern/src/pfs.cpp b/ultramodern/src/pfs.cpp +new file mode 100644 +index 0000000..36ba29c +--- /dev/null ++++ b/ultramodern/src/pfs.cpp +@@ -0,0 +1,389 @@ ++#include ++#include ++#include ++#include ++#include ++ ++#define ALIGN_UP(x, align) (((x) + ((align) - 1)) & ~((align) - 1)) ++#define ARRLEN(x) (sizeof(x) / sizeof((x)[0])) ++#define DEF_DIR_PAGES 2 ++#define MAX_FILES 16 ++#define MAX_PAGES 123 // 128 total, 5 reserved for filesystem ++ ++/* PFS Context */ ++ ++struct pfs_header_t { // same layout as OSPfsState, but non-byteswapped ++ uint32_t file_size; ++ uint32_t game_code; ++ uint16_t company_code; ++ std::array ext_name; ++ std::array game_name; ++ uint16_t padding; ++ ++ pfs_header_t() = default; ++ pfs_header_t(uint32_t fs, uint32_t gc, uint16_t cc, const char* en, const char* gn) ++ : file_size{fs}, game_code{gc}, company_code{cc} { ++ std::memcpy(ext_name.data(), en, sizeof(ext_name)); ++ std::memcpy(game_name.data(), gn, sizeof(game_name)); ++ } ++ inline bool valid() const { ++ return game_code != 0 && company_code != 0; ++ } ++ inline bool compare(uint32_t gcode, uint16_t ccode, const char* ename, const char* gname) const { ++ return game_code == gcode && company_code == ccode && ++ std::memcmp(ext_name.data(), ename, sizeof(ext_name)) == 0 && ++ std::memcmp(game_name.data(), gname, sizeof(game_name)) == 0; ++ } ++}; ++ ++inline std::filesystem::path pfs_header_path() { ++ const auto filename = "controllerpak_header.bin"; ++ return ultramodern::get_save_base_path() / filename; ++} ++ ++inline std::filesystem::path pfs_file_path(size_t file_no) { ++ const auto filename = "controllerpak_file_" + std::to_string(file_no) + ".bin"; ++ return ultramodern::get_save_base_path() / filename; ++} ++ ++inline bool pfs_header_alloc() { ++ if (!std::filesystem::exists(pfs_header_path())) { ++ std::vector zero_block(MAX_FILES * sizeof(pfs_header_t)); ++ std::ofstream out(pfs_header_path(), std::ios::binary | std::ios::out | std::ios::trunc); ++ out.write(zero_block.data(), zero_block.size()); ++ return out.good(); ++ } ++ return true; ++} ++ ++inline bool pfs_header_write(int file_no, const pfs_header_t& hdr) { ++ std::fstream out(pfs_header_path(), std::ios::binary | std::ios::out | std::ios::in); ++ if (out.is_open() && out.good()) { ++ out.seekp(file_no * sizeof(pfs_header_t), std::ios::beg); ++ out.write((const char*)&hdr.file_size, sizeof(hdr.file_size)); ++ out.write((const char*)&hdr.game_code, sizeof(hdr.game_code)); ++ out.write((const char*)&hdr.company_code, sizeof(hdr.company_code)); ++ out.write((const char*)&hdr.ext_name[0], hdr.ext_name.size()); ++ out.write((const char*)&hdr.game_name[0], hdr.game_name.size()); ++ out.write((const char*)&hdr.padding, sizeof(hdr.padding)); ++ } ++ return out.good(); ++} ++ ++inline bool pfs_header_read(int file_no, pfs_header_t& hdr) { ++ hdr = {}; // reset ++ std::ifstream in(pfs_header_path(), std::ios::binary | std::ios::in); ++ if (in.is_open() && in.good()) { ++ in.seekg(file_no * sizeof(pfs_header_t), std::ios::beg); ++ in.read((char*)&hdr.file_size, sizeof(hdr.file_size)); ++ in.read((char*)&hdr.game_code, sizeof(hdr.game_code)); ++ in.read((char*)&hdr.company_code, sizeof(hdr.company_code)); ++ in.read((char*)&hdr.ext_name[0], hdr.ext_name.size()); ++ in.read((char*)&hdr.game_name[0], hdr.game_name.size()); ++ in.read((char*)&hdr.padding, sizeof(hdr.padding)); ++ } ++ return in.good(); ++} ++ ++inline bool pfs_file_alloc(int file_no, int nbytes) { ++ std::vector zero_block(ALIGN_UP(nbytes, PFS_ONE_PAGE * PFS_BLOCKSIZE)); ++ std::ofstream out(pfs_file_path(file_no), std::ios::binary | std::ios::out | std::ios::trunc); ++ if (out.is_open() && out.good()) { ++ out.write(zero_block.data(), zero_block.size()); ++ } ++ return out.good(); ++} ++ ++inline bool pfs_file_write(int file_no, int offset, const char* data_buffer, int nbytes) { ++ std::fstream out(pfs_file_path(file_no), std::ios::binary | std::ios::out | std::ios::in); ++ if (out.is_open() && out.good()) { ++ out.seekp(offset, std::ios::beg); ++ out.write((const char*)data_buffer, nbytes); ++ } ++ return out.good(); ++} ++ ++inline bool pfs_file_read(int file_no, int offset, char* data_buffer, int nbytes) { ++ std::ifstream in(pfs_file_path(file_no), std::ios::binary | std::ios::in); ++ if (in.is_open() && in.good()) { ++ in.seekg(offset, std::ios::beg); ++ in.read((char*)data_buffer, nbytes); ++ } ++ return in.good(); ++} ++ ++/* ControllerPak */ ++ ++static s32 __osPfsGetStatus(RDRAM_ARG PTR(OSMesgQueue) queue, int channel) { ++ const auto device_info = ultramodern::get_connected_device_info(channel); ++ if (device_info.connected_device != ultramodern::input::Device::Controller) { ++ return PFS_ERR_CONTRFAIL; ++ } ++ if (device_info.connected_pak == ultramodern::input::Pak::None) { ++ return PFS_ERR_NOPACK; ++ } ++ if (device_info.connected_pak != ultramodern::input::Pak::ControllerPak) { ++ return PFS_ERR_DEVICE; ++ } ++ ++ pfs_header_alloc(); ++ return 0; ++} ++ ++static s32 __osGetId(RDRAM_ARG PTR(OSPfs) pfs_) { ++ OSPfs* pfs = TO_PTR(OSPfs, pfs_); ++ ++ // we don't implement the real filesystem, so just mimic initialization ++ pfs->version = 0; ++ pfs->banks = 1; ++ pfs->activebank = 0; ++ pfs->inode_start_page = 1 + DEF_DIR_PAGES + (2 * pfs->banks); ++ pfs->dir_size = DEF_DIR_PAGES * PFS_ONE_PAGE; ++ pfs->inode_table = 1 * PFS_ONE_PAGE; ++ pfs->minode_table = (1 + pfs->banks) * PFS_ONE_PAGE; ++ pfs->dir_table = pfs->minode_table + (pfs->banks * PFS_ONE_PAGE); ++ ++ std::memset(pfs->id, 0, ARRLEN(pfs->id)); ++ std::memset(pfs->label, 0, ARRLEN(pfs->label)); ++ return 0; ++} ++ ++extern "C" s32 osPfsInitPak(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSPfs) pfs_, int channel) { ++ OSPfs* pfs = TO_PTR(OSPfs, pfs_); ++ ++ const auto status = __osPfsGetStatus(PASS_RDRAM mq_, channel); ++ if (status != 0) { ++ return status; ++ } ++ ++ pfs->queue = mq_; ++ pfs->channel = channel; ++ pfs->status = 0; ++ __osGetId(PASS_RDRAM pfs_); ++ ++ const s32 ret = osPfsChecker(PASS_RDRAM pfs_); ++ pfs->status |= PFS_INITIALIZED; ++ return ret; ++} ++ ++extern "C" s32 osPfsRepairId(RDRAM_ARG PTR(OSPfs) pfs) { ++ return 0; ++} ++ ++extern "C" s32 osPfsInit(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSPfs) pfs_, int channel) { ++ OSPfs* pfs = TO_PTR(OSPfs, pfs_); ++ ++ const auto status = __osPfsGetStatus(PASS_RDRAM mq_, channel); ++ if (status != 0) { ++ return status; ++ } ++ ++ pfs->queue = mq_; ++ pfs->channel = channel; ++ pfs->status = 0; ++ pfs->activebank = -1; ++ __osGetId(PASS_RDRAM pfs_); ++ ++ const s32 ret = osPfsChecker(PASS_RDRAM pfs_); ++ pfs->status |= PFS_INITIALIZED; ++ return ret; ++} ++ ++extern "C" s32 osPfsReFormat(RDRAM_ARG PTR(OSPfs) pfs, PTR(OSMesgQueue) mq_, int channel) { ++ return 0; ++} ++ ++extern "C" s32 osPfsChecker(RDRAM_ARG PTR(OSPfs) pfs) { ++ return 0; ++} ++ ++extern "C" s32 osPfsAllocateFile(RDRAM_ARG PTR(OSPfs) pfs, u16 company_code, u32 game_code, u8* game_name, u8* ext_name, int nbytes, PTR(s32) file_no_) { ++ s32* file_no = TO_PTR(s32, file_no_); ++ ++ if (company_code == 0 || game_code == 0) { ++ return PFS_ERR_INVALID; ++ } ++ ++ pfs_header_t hdr{}; ++ u8 free_file_index = 0; ++ for (size_t i = 0; i < MAX_FILES; i++) { ++ pfs_header_read(i, hdr); ++ if (!hdr.valid()) { ++ free_file_index = i; ++ break; ++ } ++ } ++ ++ if (free_file_index == MAX_FILES) { ++ return PFS_DIR_FULL; ++ } ++ if (!pfs_header_write(free_file_index, pfs_header_t{(uint32_t)nbytes, game_code, company_code, (char*)ext_name, (char*)game_name})) { ++ return PFS_ERR_INVALID; ++ } ++ if (!pfs_file_alloc(free_file_index, nbytes)) { ++ return PFS_ERR_INVALID; ++ } ++ *file_no = free_file_index; ++ return 0; ++} ++ ++extern "C" s32 osPfsFindFile(RDRAM_ARG PTR(OSPfs) pfs_, u16 company_code, u32 game_code, u8* game_name, u8* ext_name, PTR(s32) file_no_) { ++ s32* file_no = TO_PTR(s32, file_no_); ++ ++ if (company_code == 0 || game_code == 0) { ++ return PFS_ERR_INVALID; ++ } ++ ++ pfs_header_t hdr{}; ++ for (size_t i = 0; i < MAX_FILES; i++) { ++ pfs_header_read(i, hdr); ++ if (hdr.compare(game_code, company_code, (char*)ext_name, (char*)game_name)) { ++ *file_no = i; ++ return 0; ++ } ++ } ++ return PFS_ERR_INVALID; ++} ++ ++extern "C" s32 osPfsDeleteFile(RDRAM_ARG PTR(OSPfs) pfs_, u16 company_code, u32 game_code, u8* game_name, u8* ext_name) { ++ if (company_code == 0 || game_code == 0) { ++ return PFS_ERR_INVALID; ++ } ++ ++ pfs_header_t hdr{}; ++ for (int i = 0; i < MAX_FILES; i++) { ++ pfs_header_read(i, hdr); ++ if (hdr.compare(game_code, company_code, (char*)ext_name, (char*)game_name)) { ++ pfs_header_write(i, pfs_header_t{}); ++ std::filesystem::remove(pfs_file_path(i)); ++ return 0; ++ } ++ } ++ return PFS_ERR_INVALID; ++} ++ ++extern "C" s32 osPfsReadWriteFile(RDRAM_ARG PTR(OSPfs) pfs_, s32 file_no, u8 flag, int offset, int nbytes, u8* data_buffer) { ++ if (!std::filesystem::exists(pfs_file_path(file_no))) { ++ return PFS_ERR_INVALID; ++ } ++ ++ const auto file_size = std::filesystem::file_size(pfs_file_path(file_no)); ++ if (offset % PFS_BLOCKSIZE || nbytes % PFS_BLOCKSIZE || (offset + nbytes) > file_size) { ++ return PFS_ERR_INVALID; ++ } ++ else if ((flag == PFS_READ) && !pfs_file_read(file_no, offset, (char*)data_buffer, nbytes)) { ++ return PFS_ERR_INVALID; ++ } ++ else if ((flag == PFS_WRITE) && !pfs_file_write(file_no, offset, (const char*)data_buffer, nbytes)) { ++ return PFS_ERR_INVALID; ++ } ++ return 0; ++} ++ ++inline void bswap_copy(char* dst, const char* src, int offset, int n) { ++ for (int i = 0; i < n; i++) { dst[(i + offset) ^ 3] = src[i + offset]; } ++} ++ ++extern "C" s32 osPfsFileState(RDRAM_ARG PTR(OSPfs) pfs_, s32 file_no, PTR(OSPfsState) state_) { ++ OSPfsState *state = TO_PTR(OSPfsState, state_); ++ ++ if (!std::filesystem::exists(pfs_file_path(file_no))) { ++ return PFS_ERR_INVALID; ++ } ++ ++ pfs_header_t hdr{}; ++ pfs_header_read(file_no, hdr); ++ ++ state->file_size = hdr.file_size; ++ state->company_code = hdr.company_code; ++ state->game_code = hdr.game_code; ++ ++ // FIXME OSPfsState layout is an absoute mess. giving up and byte swapping ++ bswap_copy((char*)state, (char*)&hdr, 10, 20); ++ return 0; ++} ++ ++extern "C" s32 osPfsGetLabel(RDRAM_ARG PTR(OSPfs) pfs_, u8* label, PTR(int) len_) { ++ OSPfs* pfs = TO_PTR(OSPfs, pfs_); ++ int* len = TO_PTR(int, len_); ++ ++ if (label == NULL) { ++ return PFS_ERR_INVALID; ++ } ++// if (__osCheckId(pfs) == PFS_ERR_NEW_PACK) { ++// return PFS_ERR_NEW_PACK; ++// } ++ ++ int i; ++ for (i = 0; i < ARRLEN(pfs->label); i++) { ++ if (pfs->label[i] == 0) { ++ break; ++ } ++ *label++ = pfs->label[i]; ++ } ++ *len = i; ++ return 0; ++} ++ ++extern "C" s32 osPfsSetLabel(RDRAM_ARG PTR(OSPfs) pfs_, u8* label) { ++ OSPfs* pfs = TO_PTR(OSPfs, pfs_); ++ ++ if (label != NULL) { ++ for (int i = 0; i < ARRLEN(pfs->label); i++) { ++ if (*label == 0) { ++ break; ++ } ++ pfs->label[i] = *label++; ++ } ++ } ++ return 0; ++} ++ ++extern "C" s32 osPfsIsPlug(RDRAM_ARG PTR(OSMesgQueue) mq_, u8* pattern) { ++ u8 bits = 0; ++ ++ for (int channel = 0; channel < ultramodern::get_max_controllers(); channel++) { ++ if (__osPfsGetStatus(PASS_RDRAM mq_, channel) == 0) { ++ bits |= (1 << channel); ++ } ++ } ++ *pattern = bits; ++ return 0; ++} ++ ++extern "C" s32 osPfsFreeBlocks(RDRAM_ARG PTR(OSPfs) pfs_, PTR(s32) bytes_not_used_) { ++ OSPfs *pfs = TO_PTR(OSPfs, pfs_); ++ s32 *bytes_not_used = TO_PTR(s32, bytes_not_used_); ++ ++ s32 pages_used = 0; ++ pfs_header_t hdr{}; ++ for (size_t i = 0; i < MAX_FILES; i++) { ++ pfs_header_read(i, hdr); ++ if (hdr.valid()) { ++ pages_used += hdr.file_size >> 8; ++ } ++ } ++ ++ *bytes_not_used = (MAX_PAGES - pages_used) << 8; ++ return 0; ++} ++ ++extern "C" s32 osPfsNumFiles(RDRAM_ARG PTR(OSPfs) pfs_, PTR(s32) max_files_, PTR(s32) files_used_) { ++ OSPfs *pfs = TO_PTR(OSPfs, pfs_); ++ s32 *max_files = TO_PTR(s32, max_files_); ++ s32 *files_used = TO_PTR(s32, files_used_); ++ ++ u8 num_files = 0; ++ pfs_header_t hdr{}; ++ for (size_t i = 0; i < MAX_FILES; i++) { ++ pfs_header_read(i, hdr); ++ if (hdr.valid()) { ++ num_files++; ++ } ++ } ++ ++ *max_files = MAX_FILES; ++ *files_used = num_files; ++ return 0; ++} ++ +diff --git a/ultramodern/src/save.cpp b/ultramodern/src/save.cpp +new file mode 100644 +index 0000000..9a216f4 +--- /dev/null ++++ b/ultramodern/src/save.cpp +@@ -0,0 +1,212 @@ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++struct { ++ std::vector save_buffer; ++ std::thread saving_thread; ++ std::filesystem::path save_base_path; ++ 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_base_path() { ++ return save_context.save_base_path; ++} ++ ++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) { ++ save_context.save_base_path = config_path / save_folder; ++ if (!subfolder.empty()) { ++ save_context.save_base_path = save_context.save_base_path / subfolder; ++ } ++ save_context.save_file_path = save_context.save_base_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(); ++ } ++} ++ From 386d36f991ec1c048e511a5ff9a0b5c58fbba552 Mon Sep 17 00:00:00 2001 From: Nick Benthem Date: Wed, 25 Mar 2026 16:46:11 -0400 Subject: [PATCH 3/4] Add per-port pak type selection and config persistence Add PakType enum (ControllerPak/RumblePak) with per-port atomic state, config serialization, and connected device info reporting. Defaults all ports to ControllerPak. Update config.cpp to follow the runtime's files.hpp namespace move from librecomp to ultramodern. --- include/recomp_input.h | 14 ++++++++++++++ src/game/config.cpp | 15 +++++++++------ src/game/input.cpp | 31 ++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/include/recomp_input.h b/include/recomp_input.h index 2abda38..5f7b294 100644 --- a/include/recomp_input.h +++ b/include/recomp_input.h @@ -102,12 +102,26 @@ namespace recomp { {recomp::ControllerPortMode::Controller, "Controller"} }); + enum class PakType { + ControllerPak, + RumblePak, + OptionCount + }; + + NLOHMANN_JSON_SERIALIZE_ENUM(recomp::PakType, { + {recomp::PakType::ControllerPak, "ControllerPak"}, + {recomp::PakType::RumblePak, "RumblePak"} + }); + constexpr int max_ports = 4; ControllerPortMode get_port_mode(int port); void set_port_mode(int port, ControllerPortMode mode); int get_port_count(); + PakType get_port_pak_type(int port); + void set_port_pak_type(int port, PakType type); + void start_scanning_input(InputDevice device); void stop_scanning_input(); void finish_scanning_input(InputField scanned_field); diff --git a/src/game/config.cpp b/src/game/config.cpp index c82d5fd..d4a01f7 100644 --- a/src/game/config.cpp +++ b/src/game/config.cpp @@ -4,7 +4,7 @@ #include "zelda_render.h" #include "zelda_support.h" #include "ultramodern/config.hpp" -#include "librecomp/files.hpp" +#include "ultramodern/files.hpp" #include #include #include @@ -208,7 +208,7 @@ bool read_json_with_backups(const std::filesystem::path& path, nlohmann::json& j } // Try reading and parsing the backup file. - if (read_json(recomp::open_input_backup_file(path), json_out)) { + if (read_json(ultramodern::open_input_backup_file(path), json_out)) { return true; } @@ -218,14 +218,14 @@ bool read_json_with_backups(const std::filesystem::path& path, nlohmann::json& j bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::json& json_data) { { - std::ofstream output_file = recomp::open_output_file_with_backup(path); + std::ofstream output_file = ultramodern::open_output_file_with_backup(path); if (!output_file.good()) { return false; } output_file << std::setw(4) << json_data; } - return recomp::finalize_output_file_with_backup(path); + return ultramodern::finalize_output_file_with_backup(path); } bool save_general_config(const std::filesystem::path& path) { @@ -407,8 +407,9 @@ bool save_controls_config(const std::filesystem::path& path) { for (int p = 0; p < recomp::max_ports; p++) { nlohmann::json port_json{}; - // Save mode + // Save mode and pak type recomp::to_json(port_json["mode"], recomp::get_port_mode(p)); + recomp::to_json(port_json["pak_type"], recomp::get_port_pak_type(p)); // Save bindings port_json["keyboard"] = {}; @@ -481,10 +482,12 @@ bool load_controls_config(const std::filesystem::path& path) { for (int p = 0; p < recomp::max_ports && p < (int)ports_array.size(); p++) { const nlohmann::json& port_json = ports_array[p]; - // Load mode + // Load mode and pak type recomp::ControllerPortMode mode = from_or_default(port_json, "mode", p == 0 ? recomp::ControllerPortMode::Keyboard : recomp::ControllerPortMode::Off); recomp::set_port_mode(p, mode); + recomp::PakType pak_type = from_or_default(port_json, "pak_type", recomp::PakType::ControllerPak); + recomp::set_port_pak_type(p, pak_type); // Load bindings if (!load_input_device_from_json(port_json, recomp::InputDevice::Keyboard, "keyboard", p)) { diff --git a/src/game/input.cpp b/src/game/input.cpp index 040d114..ef18e0a 100644 --- a/src/game/input.cpp +++ b/src/game/input.cpp @@ -31,6 +31,14 @@ static std::array, recomp::max_ports> po recomp::ControllerPortMode::Off, }; +// Per-port pak type state (default to ControllerPak) +static std::array, recomp::max_ports> port_pak_types = { + recomp::PakType::ControllerPak, + recomp::PakType::ControllerPak, + recomp::PakType::ControllerPak, + recomp::PakType::ControllerPak, +}; + recomp::ControllerPortMode recomp::get_port_mode(int port) { if (port < 0 || port >= recomp::max_ports) return recomp::ControllerPortMode::Off; return port_modes[port].load(); @@ -45,6 +53,16 @@ int recomp::get_port_count() { return recomp::max_ports; } +recomp::PakType recomp::get_port_pak_type(int port) { + if (port < 0 || port >= recomp::max_ports) return recomp::PakType::ControllerPak; + return port_pak_types[port].load(); +} + +void recomp::set_port_pak_type(int port, recomp::PakType type) { + if (port < 0 || port >= recomp::max_ports) return; + port_pak_types[port].store(type); +} + static struct { const Uint8* keys = nullptr; SDL_Keymod keymod = SDL_Keymod::KMOD_NONE; @@ -639,9 +657,20 @@ void recomp::set_rumble(int controller_num, bool on) { ultramodern::input::connected_device_info_t recomp::get_connected_device_info(int controller_num) { if (controller_num >= 0 && controller_num < recomp::max_ports) { if (port_modes[controller_num].load() != recomp::ControllerPortMode::Off) { + ultramodern::input::Pak pak = ultramodern::input::Pak::None; + switch (port_pak_types[controller_num].load()) { + case recomp::PakType::ControllerPak: + pak = ultramodern::input::Pak::ControllerPak; + break; + case recomp::PakType::RumblePak: + pak = ultramodern::input::Pak::RumblePak; + break; + default: + break; + } return ultramodern::input::connected_device_info_t { .connected_device = ultramodern::input::Device::Controller, - .connected_pak = ultramodern::input::Pak::RumblePak, + .connected_pak = pak, }; } } From 1742742d3a6d3ad5b7662dbeaab4c59f5f5c2fc1 Mon Sep 17 00:00:00 2001 From: Nick Benthem Date: Wed, 25 Mar 2026 16:46:19 -0400 Subject: [PATCH 4/4] Add controller pak / rumble pak UI selector Add pak type toggle to the controls config menu, always visible regardless of port mode. Defaults to controller pak for all ports. Resets pak type on defaults reset and syncs on port switch. --- assets/config_menu/controls.rml | 20 ++++++++++++++++++++ src/ui/ui_config.cpp | 28 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/assets/config_menu/controls.rml b/assets/config_menu/controls.rml index 862579d..d7ae5f3 100644 --- a/assets/config_menu/controls.rml +++ b/assets/config_menu/controls.rml @@ -81,6 +81,26 @@
Reset to defaults
+ +
+ + + +
diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp index c52f184..32998c1 100644 --- a/src/ui/ui_config.cpp +++ b/src/ui/ui_config.cpp @@ -97,6 +97,7 @@ static int focused_input_index = -1; static int focused_config_option_index = -1; static int selected_port = 0; static int port_mode_index = 0; // 0=Off, 1=Keyboard, 2=Controller +static int port_pak_type_index = 0; // 0=ControllerPak, 1=RumblePak static int port_controller_index = -1; static std::vector connected_controller_names; @@ -788,6 +789,10 @@ class ConfigMenu : public recompui::MenuController { selected_port = 0; cur_device = recomp::InputDevice::Keyboard; port_mode_index = static_cast(recomp::ControllerPortMode::Keyboard); + port_pak_type_index = static_cast(recomp::PakType::ControllerPak); + for (int p = 0; p < recomp::max_ports; p++) { + recomp::set_port_pak_type(p, recomp::PakType::ControllerPak); + } refresh_controller_list(); zelda64::save_config(); model_handle.DirtyAllVariables(); @@ -820,6 +825,7 @@ class ConfigMenu : public recompui::MenuController { selected_port = inputs.at(0).Get(); recomp::ControllerPortMode mode = recomp::get_port_mode(selected_port); port_mode_index = static_cast(mode); + port_pak_type_index = static_cast(recomp::get_port_pak_type(selected_port)); // Sync cur_device with the selected port's mode if (mode == recomp::ControllerPortMode::Controller) { cur_device = recomp::InputDevice::Controller; @@ -960,6 +966,7 @@ class ConfigMenu : public recompui::MenuController { constructor.Bind("active_binding_slot", &scanned_binding_index); constructor.Bind("selected_port", &selected_port); constructor.Bind("port_mode_index", &port_mode_index); + constructor.Bind("port_pak_type_index", &port_pak_type_index); constructor.Bind("port_controller_index", &port_controller_index); constructor.RegisterArray>(); constructor.Bind("connected_controller_names", &connected_controller_names); @@ -974,6 +981,27 @@ class ConfigMenu : public recompui::MenuController { } }); + constructor.BindFunc("pak_type", [](Rml::Variant& out) { + recomp::PakType type = recomp::get_port_pak_type(selected_port); + switch (type) { + case recomp::PakType::ControllerPak: out = "Controller Pak"; break; + case recomp::PakType::RumblePak: out = "Rumble Pak"; break; + default: out = "Controller Pak"; break; + } + }); + + constructor.BindEventCallback("set_pak_type", + [](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) { + std::string pak_str = inputs.at(0).Get(); + recomp::PakType pak = recomp::PakType::ControllerPak; + if (pak_str == "RumblePak") pak = recomp::PakType::RumblePak; + recomp::set_port_pak_type(selected_port, pak); + port_pak_type_index = static_cast(pak); + model_handle.DirtyVariable("pak_type"); + model_handle.DirtyVariable("port_pak_type_index"); + zelda64::save_config(); + }); + constructor.BindFunc("assigned_controller_name", [](Rml::Variant& out) { out = recomp::get_port_controller_name(selected_port); });