├── .github └── workflows │ └── validate.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── COPYING ├── README.md ├── librecomp ├── CMakeLists.txt ├── include │ └── librecomp │ │ ├── addresses.hpp │ │ ├── euc-jp.hpp │ │ ├── files.hpp │ │ ├── game.hpp │ │ ├── helpers.hpp │ │ ├── mods.hpp │ │ ├── overlays.hpp │ │ ├── rsp.hpp │ │ ├── rsp_vu.hpp │ │ ├── rsp_vu_impl.hpp │ │ └── sections.h └── src │ ├── ai.cpp │ ├── cont.cpp │ ├── dp.cpp │ ├── eep.cpp │ ├── euc-jp.cpp │ ├── files.cpp │ ├── flash.cpp │ ├── heap.cpp │ ├── math_routines.cpp │ ├── mod_config_api.cpp │ ├── mod_events.cpp │ ├── mod_hooks.cpp │ ├── mod_manifest.cpp │ ├── mods.cpp │ ├── overlays.cpp │ ├── pak.cpp │ ├── pi.cpp │ ├── print.cpp │ ├── recomp.cpp │ ├── rsp.cpp │ ├── sp.cpp │ ├── ultra_stubs.cpp │ ├── ultra_translation.cpp │ └── vi.cpp ├── thirdparty ├── concurrentqueue │ ├── blockingconcurrentqueue.h │ ├── concurrentqueue.h │ └── lightweightsemaphore.h ├── json │ └── json.hpp └── sse2neon │ └── sse2neon.h └── ultramodern ├── CMakeLists.txt ├── include └── ultramodern │ ├── config.hpp │ ├── error_handling.hpp │ ├── events.hpp │ ├── input.hpp │ ├── renderer_context.hpp │ ├── rsp.hpp │ ├── threads.hpp │ ├── ultra64.h │ └── ultramodern.hpp └── src ├── audio.cpp ├── error_handling.cpp ├── events.cpp ├── input.cpp ├── mesgqueue.cpp ├── misc_ultra.cpp ├── port_main.c ├── renderer_context.cpp ├── rsp.cpp ├── scheduling.cpp ├── task_win32.cpp ├── threadqueue.cpp ├── threads.cpp ├── timer.cpp └── ultrainit.cpp /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: [opened, synchronize] 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build-linux: 14 | runs-on: ${{ matrix.arch == 'x64' && 'ubuntu-22.04' || 'blaze/ubuntu-22.04' }} 15 | strategy: 16 | matrix: 17 | type: [ Debug, Release ] 18 | arch: [ x64, arm64 ] 19 | name: ubuntu (${{ matrix.arch }}, ${{ matrix.type }}) 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | submodules: recursive 25 | - name: ccache 26 | uses: hendrikmuhs/ccache-action@v1.2 27 | with: 28 | key: ${{ runner.os }}-z64re-ccache-${{ matrix.type }}-${{ matrix.arch }} 29 | - name: Install Linux Dependencies 30 | run: | 31 | sudo apt-get update 32 | sudo apt-get install -y ninja-build lld llvm clang-15 33 | - name: Generate CMake Project 34 | run: | 35 | # enable ccache 36 | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" 37 | cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER=clang++-15 -DCMAKE_C_COMPILER=clang-15 -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build 38 | - name: Build ultramodern 39 | run: | 40 | # enable ccache 41 | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" 42 | cmake --build cmake-build --config ${{ matrix.type }} --target ultramodern -j $(nproc) 43 | - name: Build librecomp 44 | run: | 45 | # enable ccache 46 | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" 47 | cmake --build cmake-build --config ${{ matrix.type }} --target librecomp -j $(nproc) 48 | 49 | build-windows: 50 | runs-on: windows-latest 51 | strategy: 52 | matrix: 53 | type: [ Debug, Release ] 54 | name: windows (x64, ${{ matrix.type }}) 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v4 58 | with: 59 | submodules: recursive 60 | - name: ccache 61 | uses: hendrikmuhs/ccache-action@v1.2 62 | with: 63 | key: ${{ runner.os }}-z64re-ccache-${{ matrix.type }} 64 | - name: Install Windows Dependencies 65 | run: | 66 | choco install ninja 67 | Remove-Item -Path "C:\ProgramData\Chocolatey\bin\ccache.exe" -Force -ErrorAction SilentlyContinue 68 | - name: Configure Developer Command Prompt 69 | uses: ilammy/msvc-dev-cmd@v1 70 | - name: Generate CMake Project 71 | run: | 72 | # enable ccache 73 | set $env:PATH="$env:USERPROFILE/.cargo/bin;$env:PATH" 74 | 75 | # remove LLVM from PATH so it doesn't overshadow the one provided by VS 76 | $env:PATH = ($env:PATH -split ';' | Where-Object { $_ -ne 'C:\Program Files\LLVM\bin' }) -join ';' 77 | 78 | cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER=clang-cl -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build -DCMAKE_CXX_FLAGS="-Xclang -fexceptions -Xclang -fcxx-exceptions" 79 | - name: Build ultramodern 80 | run: | 81 | # enable ccache 82 | set $env:PATH="$env:USERPROFILE/.cargo/bin;$env:PATH" 83 | $cpuCores = (Get-CimInstance -ClassName Win32_Processor).NumberOfLogicalProcessors 84 | 85 | cmake --build cmake-build --config ${{ matrix.type }} --target ultramodern -j $cpuCores 86 | - name: Build librecomp 87 | run: | 88 | # enable ccache 89 | set $env:PATH="$env:USERPROFILE/.cargo/bin;$env:PATH" 90 | $cpuCores = (Get-CimInstance -ClassName Win32_Processor).NumberOfLogicalProcessors 91 | 92 | cmake --build cmake-build --config ${{ matrix.type }} --target librecomp -j $cpuCores 93 | 94 | build-macos: 95 | runs-on: ${{ matrix.arch == 'x64' && 'macos-13' || 'macos-14' }} 96 | strategy: 97 | matrix: 98 | type: [ Debug, Release ] 99 | arch: [ x64, arm64 ] 100 | name: macos (${{ matrix.arch }}, ${{ matrix.type }}) 101 | steps: 102 | - name: Checkout 103 | uses: actions/checkout@v4 104 | with: 105 | submodules: recursive 106 | - name: ccache 107 | uses: hendrikmuhs/ccache-action@v1.2 108 | with: 109 | key: ${{ runner.os }}-z64re-ccache-${{ matrix.type }}-${{ matrix.arch }} 110 | - name: Install macOS Dependencies 111 | run: | 112 | brew install ninja 113 | - name: Generate CMake Project 114 | run: | 115 | # enable ccache 116 | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" 117 | cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build 118 | - name: Build ultramodern 119 | run: | 120 | # enable ccache 121 | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" 122 | cmake --build cmake-build --config ${{ matrix.type }} --target ultramodern -j $(sysctl -n hw.ncpu) 123 | - name: Build librecomp 124 | run: | 125 | # enable ccache 126 | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" 127 | cmake --build cmake-build --config ${{ matrix.type }} --target librecomp -j $(sysctl -n hw.ncpu) 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | temp/ 3 | .vscode/ 4 | 5 | *.o 6 | *.elf 7 | *.z64 8 | *.n64 9 | *.v64 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "thirdparty/xxHash"] 2 | path = thirdparty/xxHash 3 | url = https://github.com/Cyan4973/xxHash.git 4 | [submodule "thirdparty/miniz"] 5 | path = thirdparty/miniz 6 | url = https://github.com/richgel999/miniz 7 | [submodule "N64Recomp"] 8 | path = N64Recomp 9 | url = https://github.com/N64Recomp/N64Recomp 10 | [submodule "thirdparty/o1heap"] 11 | path = thirdparty/o1heap 12 | url = https://github.com/N64Recomp/o1heap 13 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | 3 | project(N64ModernRuntime) 4 | 5 | set(CMAKE_CXX_STANDARD 20) 6 | set(CMAKE_CXX_STANDARD_REQUIRED True) 7 | 8 | add_subdirectory(ultramodern) 9 | add_subdirectory(librecomp) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # N64 Modern Runtime 2 | 3 | A modern runtime for traditional ports and recompilations of N64 games. \ 4 | The runtime is consists of two libraries: [ultramodern](#ultramodern) and [librecomp](#librecomp). 5 | 6 | ## ultramodern 7 | 8 | ultramodern is a reimplementation of much of the core functionality of libultra. It can be used with either statically recompiled projects that use N64Recomp or direct source ports. It implements the following libultra functionality: 9 | 10 | * Threads 11 | * Controllers 12 | * Audio 13 | * Message Queues 14 | * Timers 15 | * RSP Task Handling 16 | * VI timing 17 | 18 | Platform-specific I/O is handled via callbacks that are provided by the project using ultramodern. This includes reading from controllers and playing back audio samples. 19 | 20 | ultramodern expects the user to provide and register a graphics renderer. The recommended one is [RT64](https://github.com/rt64/rt64). 21 | 22 | ## librecomp 23 | 24 | librecomp is a library meant to be used to bridge the gap between code generated by N64Recomp and ultramodern. It provides wrappers to allow recompiled code to call ultramodern. Librecomp also provides some of the remaining libultra functionality that ultramodern doesn't provide, which includes: 25 | 26 | * Overlay handling 27 | * PI DMA (ROM reads) 28 | * EEPROM, SRAM and Flashram saving (these may be partially moved to ultramodern in the future) 29 | 30 | ## Building 31 | 32 | The recommended usage of these libraries is to include them in your project's CMakeLists.txt file via add_subdirectory. This project requires C++20 support and was developed using Clang 15, older versions of clang may not work. Recent enough versions of MSVC and GCC should work as well, but are not regularly tested. 33 | 34 | These libraries can be built in a standalone environment (ie, developing new features for the libraries of this project) via the following: 35 | 36 | ```bash 37 | cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_BUILD_TYPE=Debug 38 | cmake --build build 39 | ``` 40 | -------------------------------------------------------------------------------- /librecomp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | 3 | project(librecomp) 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_CXX_STANDARD_REQUIRED True) 6 | set(CMAKE_CXX_EXTENSIONS OFF) 7 | 8 | # Define the library 9 | add_library(librecomp STATIC 10 | "${CMAKE_CURRENT_SOURCE_DIR}/src/ai.cpp" 11 | "${CMAKE_CURRENT_SOURCE_DIR}/src/cont.cpp" 12 | "${CMAKE_CURRENT_SOURCE_DIR}/src/dp.cpp" 13 | "${CMAKE_CURRENT_SOURCE_DIR}/src/eep.cpp" 14 | "${CMAKE_CURRENT_SOURCE_DIR}/src/euc-jp.cpp" 15 | "${CMAKE_CURRENT_SOURCE_DIR}/src/files.cpp" 16 | "${CMAKE_CURRENT_SOURCE_DIR}/src/flash.cpp" 17 | "${CMAKE_CURRENT_SOURCE_DIR}/src/heap.cpp" 18 | "${CMAKE_CURRENT_SOURCE_DIR}/src/math_routines.cpp" 19 | "${CMAKE_CURRENT_SOURCE_DIR}/src/mods.cpp" 20 | "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_events.cpp" 21 | "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_hooks.cpp" 22 | "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_manifest.cpp" 23 | "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_config_api.cpp" 24 | "${CMAKE_CURRENT_SOURCE_DIR}/src/overlays.cpp" 25 | "${CMAKE_CURRENT_SOURCE_DIR}/src/pak.cpp" 26 | "${CMAKE_CURRENT_SOURCE_DIR}/src/pi.cpp" 27 | "${CMAKE_CURRENT_SOURCE_DIR}/src/print.cpp" 28 | "${CMAKE_CURRENT_SOURCE_DIR}/src/recomp.cpp" 29 | "${CMAKE_CURRENT_SOURCE_DIR}/src/rsp.cpp" 30 | "${CMAKE_CURRENT_SOURCE_DIR}/src/sp.cpp" 31 | "${CMAKE_CURRENT_SOURCE_DIR}/src/ultra_stubs.cpp" 32 | "${CMAKE_CURRENT_SOURCE_DIR}/src/ultra_translation.cpp" 33 | "${CMAKE_CURRENT_SOURCE_DIR}/src/vi.cpp" 34 | "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/o1heap/o1heap/o1heap.c" 35 | ) 36 | 37 | target_include_directories(librecomp PUBLIC 38 | "${CMAKE_CURRENT_SOURCE_DIR}/include" 39 | "${CMAKE_CURRENT_SOURCE_DIR}/src" 40 | "${PROJECT_SOURCE_DIR}/../ultramodern/include" 41 | "${PROJECT_SOURCE_DIR}/../thirdparty" 42 | "${PROJECT_SOURCE_DIR}/../thirdparty/concurrentqueue" 43 | "${PROJECT_SOURCE_DIR}/../thirdparty/o1heap" 44 | ) 45 | target_include_directories(librecomp PRIVATE 46 | "${CMAKE_CURRENT_SOURCE_DIR}/include/librecomp" 47 | ) 48 | 49 | target_compile_options(librecomp PRIVATE 50 | # -Wall 51 | # -Wextra 52 | -Wno-unused-parameter 53 | ) 54 | 55 | if (WIN32) 56 | add_compile_definitions(NOMINMAX) 57 | endif() 58 | 59 | add_subdirectory(${PROJECT_SOURCE_DIR}/../thirdparty/miniz ${CMAKE_CURRENT_BINARY_DIR}/miniz) 60 | add_subdirectory(${PROJECT_SOURCE_DIR}/../N64Recomp ${CMAKE_CURRENT_BINARY_DIR}/N64Recomp EXCLUDE_FROM_ALL) 61 | 62 | target_link_libraries(librecomp PRIVATE ultramodern N64Recomp LiveRecomp) 63 | target_link_libraries(librecomp PUBLIC miniz) 64 | -------------------------------------------------------------------------------- /librecomp/include/librecomp/addresses.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __RECOMP_ADDRESSES_HPP__ 2 | #define __RECOMP_ADDRESSES_HPP__ 3 | 4 | #include 5 | #include "ultramodern/ultra64.h" 6 | #include "recomp.h" 7 | 8 | namespace recomp { 9 | // 512GB (kseg0 size) 10 | constexpr size_t mem_size = 512ULL * 1024ULL * 1024ULL; 11 | // 4GB (the full address space) 12 | constexpr size_t allocation_size = 4096ULL * 1024ULL * 1024ULL; 13 | // We need a place in rdram to hold the PI handles, so pick an address in extended rdram 14 | constexpr int32_t cart_handle = 0x80800000; 15 | constexpr int32_t drive_handle = (int32_t)(cart_handle + sizeof(OSPiHandle)); 16 | constexpr int32_t flash_handle = (int32_t)(drive_handle + sizeof(OSPiHandle)); 17 | constexpr int32_t flash_handle_end = (int32_t)(flash_handle + sizeof(OSPiHandle)); 18 | constexpr int32_t patch_rdram_start = 0x80801000; 19 | static_assert(patch_rdram_start >= flash_handle_end); 20 | constexpr int32_t mod_rdram_start = 0x81000000; 21 | 22 | // Flashram occupies the same physical address as sram, but that issue is avoided because libultra exposes 23 | // a high-level interface for flashram. Because that high-level interface is reimplemented, low level accesses 24 | // that involve physical addresses don't need to be handled for flashram. 25 | constexpr uint32_t sram_base = 0x08000000; 26 | constexpr uint32_t rom_base = 0x10000000; 27 | constexpr uint32_t drive_base = 0x06000000; 28 | 29 | void register_heap_exports(); 30 | void init_heap(uint8_t* rdram, uint32_t address); 31 | void* alloc(uint8_t* rdram, size_t size); 32 | void free(uint8_t* rdram, void* mem); 33 | } 34 | 35 | extern "C" void recomp_alloc(uint8_t* rdram, recomp_context* ctx); 36 | extern "C" void recomp_free(uint8_t* rdram, recomp_context* ctx); 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /librecomp/include/librecomp/euc-jp.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __EUC_JP_H__ 2 | #define __EUC_JP_H__ 3 | 4 | #include 5 | #include 6 | 7 | namespace Encoding { 8 | std::string decode_eucjp(std::string_view src); 9 | } 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /librecomp/include/librecomp/files.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __RECOMP_FILES_H__ 2 | #define __RECOMP_FILES_H__ 3 | 4 | #include 5 | #include 6 | 7 | namespace recomp { 8 | std::ifstream open_input_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::in); 9 | std::ifstream open_input_backup_file(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::in); 10 | std::ofstream open_output_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::out); 11 | bool finalize_output_file_with_backup(const std::filesystem::path& filepath); 12 | }; 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /librecomp/include/librecomp/game.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __RECOMP_GAME__ 2 | #define __RECOMP_GAME__ 3 | 4 | #include 5 | #include 6 | 7 | #include "recomp.h" 8 | #include "rsp.hpp" 9 | #include 10 | 11 | namespace recomp { 12 | enum class SaveType { 13 | None, 14 | Eep4k, 15 | Eep16k, 16 | Sram, 17 | Flashram, 18 | AllowAll, // Allows all save types to work and reports eeprom size as 16kbit. 19 | }; 20 | 21 | struct GameEntry { 22 | uint64_t rom_hash; 23 | std::string internal_name; 24 | std::u8string game_id; 25 | std::string mod_game_id; 26 | SaveType save_type = SaveType::None; 27 | bool is_enabled; 28 | // Only needed for mod function hooking support, not needed if `has_compressed_code` is false. 29 | std::vector (*decompression_routine)(std::span compressed_rom) = nullptr; 30 | bool has_compressed_code = false; 31 | 32 | gpr entrypoint_address; 33 | void (*entrypoint)(uint8_t* rdram, recomp_context* context) = nullptr; 34 | 35 | void (*thread_create_callback)(uint8_t* rdram, recomp_context* context) = nullptr; 36 | 37 | void (*on_init_callback)(uint8_t* rdram, recomp_context* context) = nullptr; 38 | 39 | std::u8string stored_filename() const; 40 | }; 41 | struct Version { 42 | int major = -1; 43 | int minor = -1; 44 | int patch = -1; 45 | std::string suffix; 46 | 47 | std::string to_string() const { 48 | return std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch) + suffix; 49 | } 50 | 51 | static bool from_string(const std::string& str, Version& out); 52 | 53 | auto operator<=>(const Version& rhs) const { 54 | if (major != rhs.major) { 55 | return major <=> rhs.major; 56 | } 57 | if (minor != rhs.minor) { 58 | return minor <=> rhs.minor; 59 | } 60 | return patch <=> rhs.patch; 61 | } 62 | }; 63 | enum class RomValidationError { 64 | Good, 65 | FailedToOpen, 66 | NotARom, 67 | IncorrectRom, 68 | NotYet, 69 | IncorrectVersion, 70 | OtherError 71 | }; 72 | void register_config_path(std::filesystem::path path); 73 | bool register_game(const recomp::GameEntry& entry); 74 | void check_all_stored_roms(); 75 | bool load_stored_rom(std::u8string& game_id); 76 | RomValidationError select_rom(const std::filesystem::path& rom_path, std::u8string& game_id); 77 | bool is_rom_valid(std::u8string& game_id); 78 | bool is_rom_loaded(); 79 | void set_rom_contents(std::vector&& new_rom); 80 | std::span get_rom(); 81 | void do_rom_read(uint8_t* rdram, gpr ram_address, uint32_t physical_addr, size_t num_bytes); 82 | void do_rom_pio(uint8_t* rdram, gpr ram_address, uint32_t physical_addr); 83 | const Version& get_project_version(); 84 | 85 | /** 86 | * The following arguments contain mandatory callbacks that need to be registered (i.e., can't be `nullptr`): 87 | * - `rsp_callbacks` 88 | * - `renderer_callbacks` 89 | * 90 | * It must be called only once and it must be called before `ultramodern::preinit`. 91 | */ 92 | void start( 93 | const Version& project_version, 94 | ultramodern::renderer::WindowHandle window_handle, 95 | const recomp::rsp::callbacks_t& rsp_callbacks, 96 | const ultramodern::renderer::callbacks_t& renderer_callbacks, 97 | const ultramodern::audio_callbacks_t& audio_callbacks, 98 | const ultramodern::input::callbacks_t& input_callbacks, 99 | const ultramodern::gfx_callbacks_t& gfx_callbacks, 100 | const ultramodern::events::callbacks_t& events_callbacks, 101 | const ultramodern::error_handling::callbacks_t& error_handling_callbacks, 102 | const ultramodern::threads::callbacks_t& threads_callbacks 103 | ); 104 | 105 | SaveType get_save_type(); 106 | bool eeprom_allowed(); 107 | bool sram_allowed(); 108 | bool flashram_allowed(); 109 | 110 | void start_game(const std::u8string& game_id); 111 | std::u8string current_game_id(); 112 | std::string current_mod_game_id(); 113 | } 114 | 115 | #endif 116 | -------------------------------------------------------------------------------- /librecomp/include/librecomp/helpers.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __RECOMP_HELPERS__ 2 | #define __RECOMP_HELPERS__ 3 | 4 | #include 5 | 6 | #include "recomp.h" 7 | #include 8 | 9 | template 10 | T _arg(uint8_t* rdram, recomp_context* ctx) { 11 | static_assert(index < 4, "Only args 0 through 3 supported"); 12 | gpr raw_arg = (&ctx->r4)[index]; 13 | if constexpr (std::is_same_v) { 14 | if constexpr (index < 2) { 15 | static_assert(index != 1, "Floats in arg 1 not supported"); 16 | return ctx->f12.fl; 17 | } 18 | else { 19 | // static_assert in else workaround 20 | [] () { 21 | static_assert(flag, "Floats in a2/a3 not supported"); 22 | }(); 23 | } 24 | } 25 | else if constexpr (std::is_pointer_v) { 26 | static_assert (!std::is_pointer_v>, "Double pointers not supported"); 27 | return TO_PTR(std::remove_pointer_t, raw_arg); 28 | } 29 | else if constexpr (std::is_integral_v) { 30 | static_assert(sizeof(T) <= 4, "64-bit args not supported"); 31 | return static_cast(raw_arg); 32 | } 33 | else { 34 | // static_assert in else workaround 35 | [] () { 36 | static_assert(flag, "Unsupported type"); 37 | }(); 38 | } 39 | } 40 | 41 | inline float _arg_float_a1(uint8_t* rdram, recomp_context* ctx) { 42 | (void)rdram; 43 | union { 44 | u32 as_u32; 45 | float as_float; 46 | } ret{}; 47 | ret.as_u32 = _arg<1, u32>(rdram, ctx); 48 | return ret.as_float; 49 | } 50 | 51 | inline float _arg_float_f14(uint8_t* rdram, recomp_context* ctx) { 52 | (void)rdram; 53 | return ctx->f14.fl; 54 | } 55 | 56 | template 57 | std::string _arg_string(uint8_t* rdram, recomp_context* ctx) { 58 | PTR(char) str = _arg(rdram, ctx); 59 | 60 | // Get the length of the byteswapped string. 61 | size_t len = 0; 62 | while (MEM_B(str, len) != 0x00) { 63 | len++; 64 | } 65 | 66 | std::string ret{}; 67 | ret.reserve(len + 1); 68 | 69 | for (size_t i = 0; i < len; i++) { 70 | ret += (char)MEM_B(str, i); 71 | } 72 | 73 | return ret; 74 | } 75 | 76 | template 77 | void _return(recomp_context* ctx, T val) { 78 | static_assert(sizeof(T) <= 4 && "Only 32-bit value returns supported currently"); 79 | if constexpr (std::is_same_v) { 80 | ctx->f0.fl = val; 81 | } 82 | else if constexpr (std::is_integral_v && sizeof(T) <= 4) { 83 | ctx->r2 = int32_t(val); 84 | } 85 | else { 86 | // static_assert in else workaround 87 | [] () { 88 | static_assert(flag, "Unsupported type"); 89 | }(); 90 | } 91 | } 92 | 93 | #endif 94 | -------------------------------------------------------------------------------- /librecomp/include/librecomp/overlays.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __RECOMP_OVERLAYS_H__ 2 | #define __RECOMP_OVERLAYS_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "sections.h" 10 | 11 | namespace recomp { 12 | namespace overlays { 13 | struct overlay_section_table_data_t { 14 | SectionTableEntry* code_sections; 15 | size_t num_code_sections; 16 | size_t total_num_sections; 17 | }; 18 | 19 | struct overlays_by_index_t { 20 | int* table; 21 | size_t len; 22 | }; 23 | 24 | void register_overlays(const overlay_section_table_data_t& sections, const overlays_by_index_t& overlays); 25 | 26 | void register_patches(const char* patch_data, size_t patch_size, SectionTableEntry* code_sections, size_t num_sections); 27 | void register_base_export(const std::string& name, recomp_func_t* func); 28 | void register_ext_base_export(const std::string& name, recomp_func_ext_t* func); 29 | void register_base_exports(const FunctionExport* exports); 30 | void register_base_events(char const* const* event_names); 31 | void register_manual_patch_symbols(const ManualPatchSymbol* manual_patch_symbols); 32 | void read_patch_data(uint8_t* rdram, gpr patch_data_address); 33 | 34 | void init_overlays(); 35 | const std::unordered_map& get_vrom_to_section_map(); 36 | uint32_t get_section_ram_addr(uint16_t code_section_index); 37 | std::span get_section_relocs(uint16_t code_section_index); 38 | recomp_func_t* get_func_by_section_rom_function_vram(uint32_t section_rom, uint32_t function_vram); 39 | bool get_func_entry_by_section_index_function_offset(uint16_t code_section_index, uint32_t function_offset, FuncEntry& func_out); 40 | recomp_func_t* get_func_by_section_index_function_offset(uint16_t code_section_index, uint32_t function_offset); 41 | recomp_func_t* get_base_export(const std::string& export_name); 42 | recomp_func_ext_t* get_ext_base_export(const std::string& export_name); 43 | size_t get_base_event_index(const std::string& event_name); 44 | size_t num_base_events(); 45 | 46 | void add_loaded_function(int32_t ram_addr, recomp_func_t* func); 47 | 48 | struct BasePatchedFunction { 49 | size_t patch_section; 50 | size_t function_index; 51 | }; 52 | 53 | std::unordered_map get_base_patched_funcs(); 54 | const std::unordered_map& get_patch_vrom_to_section_map(); 55 | uint32_t get_patch_section_ram_addr(uint16_t patch_code_section_index); 56 | uint32_t get_patch_section_rom_addr(uint16_t patch_code_section_index); 57 | const FuncEntry* get_patch_function_entry(uint16_t patch_code_section_index, size_t function_index); 58 | bool get_patch_func_entry_by_section_index_function_offset(uint16_t code_section_index, uint32_t function_offset, FuncEntry& func_out); 59 | std::span get_patch_section_relocs(uint16_t patch_code_section_index); 60 | std::span get_patch_binary(); 61 | } 62 | }; 63 | 64 | extern "C" void load_overlays(uint32_t rom, int32_t ram_addr, uint32_t size); 65 | extern "C" void unload_overlays(int32_t ram_addr, uint32_t size); 66 | 67 | #endif 68 | -------------------------------------------------------------------------------- /librecomp/include/librecomp/rsp.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __RSP_H__ 2 | #define __RSP_H__ 3 | 4 | #include 5 | 6 | #include "rsp_vu.hpp" 7 | #include "recomp.h" 8 | #include "ultramodern/ultra64.h" 9 | 10 | // TODO: Move these to recomp namespace? 11 | 12 | enum class RspExitReason { 13 | Invalid, 14 | Broke, 15 | ImemOverrun, 16 | UnhandledJumpTarget, 17 | Unsupported, 18 | SwapOverlay, 19 | UnhandledResumeTarget 20 | }; 21 | 22 | struct RspContext { 23 | uint32_t r1, r2, r3, r4, r5, r6, r7, 24 | r8, r9, r10, r11, r12, r13, r14, r15, 25 | r16, r17, r18, r19, r20, r21, r22, r23, 26 | r24, r25, r26, r27, r28, r29, r30, r31; 27 | uint32_t dma_mem_address; 28 | uint32_t dma_dram_address; 29 | uint32_t jump_target; 30 | RSP rsp; 31 | uint32_t resume_address; 32 | bool resume_delay; 33 | }; 34 | 35 | using RspUcodeFunc = RspExitReason(uint8_t* rdram, uint32_t ucode_addr); 36 | 37 | extern uint8_t dmem[]; 38 | extern uint16_t rspReciprocals[512]; 39 | extern uint16_t rspInverseSquareRoots[512]; 40 | 41 | #define RSP_MEM_B(offset, addr) \ 42 | (*reinterpret_cast(dmem + (0xFFF & (((offset) + (addr)) ^ 3)))) 43 | 44 | #define RSP_MEM_BU(offset, addr) \ 45 | (*reinterpret_cast(dmem + (0xFFF & (((offset) + (addr)) ^ 3)))) 46 | 47 | static inline uint32_t RSP_MEM_W_LOAD(uint32_t offset, uint32_t addr) { 48 | uint32_t out; 49 | for (int i = 0; i < 4; i++) { 50 | reinterpret_cast(&out)[i ^ 3] = RSP_MEM_BU(offset + i, addr); 51 | } 52 | return out; 53 | } 54 | 55 | static inline void RSP_MEM_W_STORE(uint32_t offset, uint32_t addr, uint32_t val) { 56 | for (int i = 0; i < 4; i++) { 57 | RSP_MEM_BU(offset + i, addr) = reinterpret_cast(&val)[i ^ 3]; 58 | } 59 | } 60 | 61 | static inline uint32_t RSP_MEM_HU_LOAD(uint32_t offset, uint32_t addr) { 62 | uint16_t out; 63 | for (int i = 0; i < 2; i++) { 64 | reinterpret_cast(&out)[(i + 2) ^ 3] = RSP_MEM_BU(offset + i, addr); 65 | } 66 | return out; 67 | } 68 | 69 | static inline uint32_t RSP_MEM_H_LOAD(uint32_t offset, uint32_t addr) { 70 | int16_t out; 71 | for (int i = 0; i < 2; i++) { 72 | reinterpret_cast(&out)[(i + 2) ^ 3] = RSP_MEM_BU(offset + i, addr); 73 | } 74 | return out; 75 | } 76 | 77 | static inline void RSP_MEM_H_STORE(uint32_t offset, uint32_t addr, uint32_t val) { 78 | for (int i = 0; i < 2; i++) { 79 | RSP_MEM_BU(offset + i, addr) = reinterpret_cast(&val)[(i + 2) ^ 3]; 80 | } 81 | } 82 | 83 | #define RSP_ADD32(a, b) \ 84 | ((int32_t)((a) + (b))) 85 | 86 | #define RSP_SUB32(a, b) \ 87 | ((int32_t)((a) - (b))) 88 | 89 | #define RSP_SIGNED(val) \ 90 | ((int32_t)(val)) 91 | 92 | #define SET_DMA_MEM(mem_addr) dma_mem_address = (mem_addr) 93 | #define SET_DMA_DRAM(dram_addr) dma_dram_address = (dram_addr) 94 | #define DO_DMA_READ(rd_len) dma_rdram_to_dmem(rdram, dma_mem_address, dma_dram_address, (rd_len)) 95 | #define DO_DMA_WRITE(wr_len) dma_dmem_to_rdram(rdram, dma_mem_address, dma_dram_address, (wr_len)) 96 | 97 | static inline void dma_rdram_to_dmem(uint8_t* rdram, uint32_t dmem_addr, uint32_t dram_addr, uint32_t rd_len) { 98 | rd_len += 1; // Read length is inclusive 99 | dram_addr &= 0xFFFFF8; 100 | assert(dmem_addr + rd_len <= 0x1000); 101 | for (uint32_t i = 0; i < rd_len; i++) { 102 | RSP_MEM_B(i, dmem_addr) = MEM_B(0, (int64_t)(int32_t)(dram_addr + i + 0x80000000)); 103 | } 104 | } 105 | 106 | static inline void dma_dmem_to_rdram(uint8_t* rdram, uint32_t dmem_addr, uint32_t dram_addr, uint32_t wr_len) { 107 | wr_len += 1; // Write length is inclusive 108 | dram_addr &= 0xFFFFF8; 109 | assert(dmem_addr + wr_len <= 0x1000); 110 | for (uint32_t i = 0; i < wr_len; i++) { 111 | MEM_B(0, (int64_t)(int32_t)(dram_addr + i + 0x80000000)) = RSP_MEM_B(i, dmem_addr); 112 | } 113 | } 114 | 115 | namespace recomp { 116 | namespace rsp { 117 | struct callbacks_t { 118 | using get_rsp_microcode_t = RspUcodeFunc*(const OSTask* task); 119 | 120 | /** 121 | * Return a function pointer to the corresponding RSP microcode function for the given `task_type`. 122 | * 123 | * The full OSTask (`task` parameter) is passed in case the `task_type` number is not enough information to distinguish out the exact microcode function. 124 | * 125 | * This function is allowed to return `nullptr` if no microcode matches the specified task. In this case a message will be printed to stderr and the program will exit. 126 | */ 127 | get_rsp_microcode_t* get_rsp_microcode; 128 | }; 129 | 130 | void set_callbacks(const callbacks_t& callbacks); 131 | 132 | void constants_init(); 133 | 134 | bool run_task(uint8_t* rdram, const OSTask* task); 135 | } 136 | } 137 | 138 | #endif 139 | -------------------------------------------------------------------------------- /librecomp/include/librecomp/rsp_vu.hpp: -------------------------------------------------------------------------------- 1 | // This file is modified from the Ares N64 emulator core. Ares can 2 | // be found at https://github.com/ares-emulator/ares. The original license 3 | // for this portion of Ares is as follows: 4 | // ---------------------------------------------------------------------- 5 | // ares 6 | // 7 | // Copyright(c) 2004 - 2021 ares team, Near et al 8 | // 9 | // Permission to use, copy, modify, and /or distribute this software for any 10 | // purpose with or without fee is hereby granted, provided that the above 11 | // copyright noticeand this permission notice appear in all copies. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 14 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 15 | // MERCHANTABILITY AND FITNESS.IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 16 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 17 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 18 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 19 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 20 | // ---------------------------------------------------------------------- 21 | #include 22 | 23 | #if defined(__x86_64__) || defined(_M_X64) 24 | #define ARCHITECTURE_SUPPORTS_SSE4_1 1 25 | #include 26 | using v128 = __m128i; 27 | #elif defined(__aarch64__) || defined(_M_ARM64) 28 | #define ARCHITECTURE_SUPPORTS_SSE4_1 1 29 | #include 30 | using v128 = __m128i; 31 | #endif 32 | 33 | namespace Accuracy { 34 | namespace RSP { 35 | #if ARCHITECTURE_SUPPORTS_SSE4_1 36 | constexpr bool SISD = false; 37 | constexpr bool SIMD = true; 38 | #else 39 | constexpr bool SISD = true; 40 | constexpr bool SIMD = false; 41 | #endif 42 | } 43 | } 44 | 45 | using u8 = uint8_t; 46 | using s8 = int8_t; 47 | using u16 = uint16_t; 48 | using s16 = int16_t; 49 | using u32 = uint32_t; 50 | using s32 = int32_t; 51 | using u64 = uint64_t; 52 | using s64 = int64_t; 53 | using uint128_t = uint64_t[2]; 54 | 55 | template inline auto sclamp(s64 x) -> s64 { 56 | enum : s64 { b = 1ull << (bits - 1), m = b - 1 }; 57 | return (x > m) ? m : (x < -b) ? -b : x; 58 | } 59 | 60 | template inline auto sclip(s64 x) -> s64 { 61 | enum : u64 { b = 1ull << (bits - 1), m = b * 2 - 1 }; 62 | return ((x & m) ^ b) - b; 63 | } 64 | 65 | struct RSP { 66 | using r32 = uint32_t; 67 | using cr32 = const r32; 68 | 69 | union r128 { 70 | struct { uint64_t u128[2]; }; 71 | #if ARCHITECTURE_SUPPORTS_SSE4_1 72 | struct { __m128i v128; }; 73 | 74 | operator __m128i() const { return v128; } 75 | auto operator=(__m128i value) { v128 = value; } 76 | #endif 77 | 78 | auto byte(u32 index) -> uint8_t& { return ((uint8_t*)&u128)[15 - index]; } 79 | auto byte(u32 index) const -> uint8_t { return ((uint8_t*)&u128)[15 - index]; } 80 | 81 | auto element(u32 index) -> uint16_t& { return ((uint16_t*)&u128)[7 - index]; } 82 | auto element(u32 index) const -> uint16_t { return ((uint16_t*)&u128)[7 - index]; } 83 | 84 | auto u8(u32 index) -> uint8_t& { return ((uint8_t*)&u128)[15 - index]; } 85 | auto u8(u32 index) const -> uint8_t { return ((uint8_t*)&u128)[15 - index]; } 86 | 87 | auto s16(u32 index) -> int16_t& { return ((int16_t*)&u128)[7 - index]; } 88 | auto s16(u32 index) const -> int16_t { return ((int16_t*)&u128)[7 - index]; } 89 | 90 | auto u16(u32 index) -> uint16_t& { return ((uint16_t*)&u128)[7 - index]; } 91 | auto u16(u32 index) const -> uint16_t { return ((uint16_t*)&u128)[7 - index]; } 92 | 93 | //VCx registers 94 | auto get(u32 index) const -> bool { return u16(index) != 0; } 95 | auto set(u32 index, bool value) -> bool { return u16(index) = 0 - value, value; } 96 | 97 | //vu-registers.cpp 98 | inline auto operator()(u32 index) const -> r128; 99 | }; 100 | using cr128 = const r128; 101 | 102 | struct VU { 103 | r128 r[32]; 104 | r128 acch, accm, accl; 105 | r128 vcoh, vcol; //16-bit little endian 106 | r128 vcch, vccl; //16-bit little endian 107 | r128 vce; // 8-bit little endian 108 | s16 divin; 109 | s16 divout; 110 | bool divdp; 111 | } vpu; 112 | 113 | static constexpr r128 zero{{0}}; 114 | static constexpr r128 invert{{(uint64_t)-1, (uint64_t)-1}}; 115 | 116 | inline auto accumulatorGet(u32 index) const -> u64; 117 | inline auto accumulatorSet(u32 index, u64 value) -> void; 118 | inline auto accumulatorSaturate(u32 index, bool slice, u16 negative, u16 positive) const -> u16; 119 | 120 | inline auto CFC2(r32& rt, u8 rd) -> void; 121 | inline auto CTC2(cr32& rt, u8 rd) -> void; 122 | template inline auto LBV(r128& vt, cr32& rs, s8 imm) -> void; 123 | template inline auto LDV(r128& vt, cr32& rs, s8 imm) -> void; 124 | template inline auto LFV(r128& vt, cr32& rs, s8 imm) -> void; 125 | template inline auto LHV(r128& vt, cr32& rs, s8 imm) -> void; 126 | template inline auto LLV(r128& vt, cr32& rs, s8 imm) -> void; 127 | template inline auto LPV(r128& vt, cr32& rs, s8 imm) -> void; 128 | template inline auto LQV(r128& vt, cr32& rs, s8 imm) -> void; 129 | template inline auto LRV(r128& vt, cr32& rs, s8 imm) -> void; 130 | template inline auto LSV(r128& vt, cr32& rs, s8 imm) -> void; 131 | template inline auto LTV(u8 vt, cr32& rs, s8 imm) -> void; 132 | template inline auto LUV(r128& vt, cr32& rs, s8 imm) -> void; 133 | template inline auto LWV(r128& vt, cr32& rs, s8 imm) -> void; 134 | template inline auto MFC2(r32& rt, cr128& vs) -> void; 135 | template inline auto MTC2(cr32& rt, r128& vs) -> void; 136 | template inline auto SBV(cr128& vt, cr32& rs, s8 imm) -> void; 137 | template inline auto SDV(cr128& vt, cr32& rs, s8 imm) -> void; 138 | template inline auto SFV(cr128& vt, cr32& rs, s8 imm) -> void; 139 | template inline auto SHV(cr128& vt, cr32& rs, s8 imm) -> void; 140 | template inline auto SLV(cr128& vt, cr32& rs, s8 imm) -> void; 141 | template inline auto SPV(cr128& vt, cr32& rs, s8 imm) -> void; 142 | template inline auto SQV(cr128& vt, cr32& rs, s8 imm) -> void; 143 | template inline auto SRV(cr128& vt, cr32& rs, s8 imm) -> void; 144 | template inline auto SSV(cr128& vt, cr32& rs, s8 imm) -> void; 145 | template inline auto STV(u8 vt, cr32& rs, s8 imm) -> void; 146 | template inline auto SUV(cr128& vt, cr32& rs, s8 imm) -> void; 147 | template inline auto SWV(cr128& vt, cr32& rs, s8 imm) -> void; 148 | template inline auto VABS(r128& vd, cr128& vs, cr128& vt) -> void; 149 | template inline auto VADD(r128& vd, cr128& vs, cr128& vt) -> void; 150 | template inline auto VADDC(r128& vd, cr128& vs, cr128& vt) -> void; 151 | template inline auto VAND(r128& vd, cr128& vs, cr128& vt) -> void; 152 | template inline auto VCH(r128& vd, cr128& vs, cr128& vt) -> void; 153 | template inline auto VCL(r128& vd, cr128& vs, cr128& vt) -> void; 154 | template inline auto VCR(r128& vd, cr128& vs, cr128& vt) -> void; 155 | template inline auto VEQ(r128& vd, cr128& vs, cr128& vt) -> void; 156 | template inline auto VGE(r128& vd, cr128& vs, cr128& vt) -> void; 157 | template inline auto VLT(r128& vd, cr128& vs, cr128& vt) -> void; 158 | template 159 | inline auto VMACF(r128& vd, cr128& vs, cr128& vt) -> void; 160 | template inline auto VMACF(r128& vd, cr128& vs, cr128& vt) -> void { VMACF<0, e>(vd, vs, vt); } 161 | template inline auto VMACU(r128& vd, cr128& vs, cr128& vt) -> void { VMACF<1, e>(vd, vs, vt); } 162 | inline auto VMACQ(r128& vd) -> void; 163 | template inline auto VMADH(r128& vd, cr128& vs, cr128& vt) -> void; 164 | template inline auto VMADL(r128& vd, cr128& vs, cr128& vt) -> void; 165 | template inline auto VMADM(r128& vd, cr128& vs, cr128& vt) -> void; 166 | template inline auto VMADN(r128& vd, cr128& vs, cr128& vt) -> void; 167 | template inline auto VMOV(r128& vd, u8 de, cr128& vt) -> void; 168 | template inline auto VMRG(r128& vd, cr128& vs, cr128& vt) -> void; 169 | template inline auto VMUDH(r128& vd, cr128& vs, cr128& vt) -> void; 170 | template inline auto VMUDL(r128& vd, cr128& vs, cr128& vt) -> void; 171 | template inline auto VMUDM(r128& vd, cr128& vs, cr128& vt) -> void; 172 | template inline auto VMUDN(r128& vd, cr128& vs, cr128& vt) -> void; 173 | template 174 | inline auto VMULF(r128& rd, cr128& vs, cr128& vt) -> void; 175 | template inline auto VMULF(r128& rd, cr128& vs, cr128& vt) -> void { VMULF<0, e>(rd, vs, vt); } 176 | template inline auto VMULU(r128& rd, cr128& vs, cr128& vt) -> void { VMULF<1, e>(rd, vs, vt); } 177 | template inline auto VMULQ(r128& rd, cr128& vs, cr128& vt) -> void; 178 | template inline auto VNAND(r128& rd, cr128& vs, cr128& vt) -> void; 179 | template inline auto VNE(r128& vd, cr128& vs, cr128& vt) -> void; 180 | inline auto VNOP() -> void; 181 | template inline auto VNOR(r128& vd, cr128& vs, cr128& vt) -> void; 182 | template inline auto VNXOR(r128& vd, cr128& vs, cr128& vt) -> void; 183 | template inline auto VOR(r128& vd, cr128& vs, cr128& vt) -> void; 184 | template 185 | inline auto VRCP(r128& vd, u8 de, cr128& vt) -> void; 186 | template inline auto VRCP(r128& vd, u8 de, cr128& vt) -> void { VRCP<0, e>(vd, de, vt); } 187 | template inline auto VRCPL(r128& vd, u8 de, cr128& vt) -> void { VRCP<1, e>(vd, de, vt); } 188 | template inline auto VRCPH(r128& vd, u8 de, cr128& vt) -> void; 189 | template 190 | inline auto VRND(r128& vd, u8 vs, cr128& vt) -> void; 191 | template inline auto VRNDN(r128& vd, u8 vs, cr128& vt) -> void { VRND<0, e>(vd, vs, vt); } 192 | template inline auto VRNDP(r128& vd, u8 vs, cr128& vt) -> void { VRND<1, e>(vd, vs, vt); } 193 | template 194 | inline auto VRSQ(r128& vd, u8 de, cr128& vt) -> void; 195 | template inline auto VRSQ(r128& vd, u8 de, cr128& vt) -> void { VRSQ<0, e>(vd, de, vt); } 196 | template inline auto VRSQL(r128& vd, u8 de, cr128& vt) -> void { VRSQ<1, e>(vd, de, vt); } 197 | template inline auto VRSQH(r128& vd, u8 de, cr128& vt) -> void; 198 | template inline auto VSAR(r128& vd, cr128& vs) -> void; 199 | template inline auto VSUB(r128& vd, cr128& vs, cr128& vt) -> void; 200 | template inline auto VSUBC(r128& vd, cr128& vs, cr128& vt) -> void; 201 | template inline auto VXOR(r128& rd, cr128& vs, cr128& vt) -> void; 202 | template inline auto VZERO(r128& rd, cr128& vs, cr128& vt) -> void; 203 | }; 204 | -------------------------------------------------------------------------------- /librecomp/include/librecomp/sections.h: -------------------------------------------------------------------------------- 1 | #ifndef __SECTIONS_H__ 2 | #define __SECTIONS_H__ 3 | 4 | #include 5 | #include "recomp.h" 6 | 7 | #define ARRLEN(x) (sizeof(x) / sizeof((x)[0])) 8 | 9 | typedef struct { 10 | recomp_func_t* func; 11 | uint32_t offset; 12 | uint32_t rom_size; 13 | } FuncEntry; 14 | 15 | typedef enum { 16 | R_MIPS_NONE = 0, 17 | R_MIPS_16, 18 | R_MIPS_32, 19 | R_MIPS_REL32, 20 | R_MIPS_26, 21 | R_MIPS_HI16, 22 | R_MIPS_LO16, 23 | R_MIPS_GPREL16, 24 | } RelocEntryType; 25 | 26 | typedef struct { 27 | // Offset into the section of the word to relocate. 28 | uint32_t offset; 29 | // Reloc addend from the target section's address. 30 | uint32_t target_section_offset; 31 | // Index of the target section (indexes into `section_addresses`). 32 | uint16_t target_section; 33 | // Relocation type. 34 | RelocEntryType type; 35 | } RelocEntry; 36 | 37 | typedef struct { 38 | uint32_t rom_addr; 39 | uint32_t ram_addr; 40 | uint32_t size; 41 | FuncEntry *funcs; 42 | size_t num_funcs; 43 | RelocEntry* relocs; 44 | size_t num_relocs; 45 | size_t index; 46 | } SectionTableEntry; 47 | 48 | typedef struct { 49 | const char* name; 50 | uint32_t ram_addr; 51 | } FunctionExport; 52 | 53 | typedef struct { 54 | uint32_t ram_addr; 55 | recomp_func_t* func; 56 | } ManualPatchSymbol; 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /librecomp/src/ai.cpp: -------------------------------------------------------------------------------- 1 | #include "recomp.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define VI_NTSC_CLOCK 48681812 8 | 9 | extern "C" void osAiSetFrequency_recomp(uint8_t* rdram, recomp_context* ctx) { 10 | uint32_t freq = ctx->r4; 11 | // This makes actual audio frequency more accurate to console, but may not be desirable 12 | //uint32_t dacRate = (uint32_t)(((float)VI_NTSC_CLOCK / freq) + 0.5f); 13 | //freq = VI_NTSC_CLOCK / dacRate; 14 | ctx->r2 = freq; 15 | ultramodern::set_audio_frequency(freq); 16 | } 17 | 18 | extern "C" void osAiSetNextBuffer_recomp(uint8_t* rdram, recomp_context* ctx) { 19 | ultramodern::queue_audio_buffer(rdram, ctx->r4, ctx->r5); 20 | ctx->r2 = 0; 21 | } 22 | 23 | extern "C" void osAiGetLength_recomp(uint8_t* rdram, recomp_context* ctx) { 24 | ctx->r2 = ultramodern::get_remaining_audio_bytes(); 25 | } 26 | 27 | extern "C" void osAiGetStatus_recomp(uint8_t* rdram, recomp_context* ctx) { 28 | ctx->r2 = 0x00000000; // Pretend the audio DMAs finish instantly 29 | } 30 | -------------------------------------------------------------------------------- /librecomp/src/cont.cpp: -------------------------------------------------------------------------------- 1 | #include "ultramodern/ultramodern.hpp" 2 | 3 | #include "helpers.hpp" 4 | 5 | #define MAXCONTROLLERS 4 6 | 7 | extern "C" void recomp_set_current_frame_poll_id(uint8_t* rdram, recomp_context* ctx) { 8 | // TODO reimplement the system for tagging polls with IDs to handle games with multithreaded input polling. 9 | } 10 | 11 | extern "C" void recomp_measure_latency(uint8_t* rdram, recomp_context* ctx) { 12 | ultramodern::measure_input_latency(); 13 | } 14 | 15 | extern "C" void osContInit_recomp(uint8_t* rdram, recomp_context* ctx) { 16 | PTR(OSMesgQueue) mq = _arg<0, PTR(OSMesgQueue)>(rdram, ctx); 17 | PTR(u8) bitpattern = _arg<1, PTR(u8)>(rdram, ctx); 18 | PTR(OSContStatus) data = _arg<2, PTR(OSContStatus)>(rdram, ctx); 19 | u8 bitpattern_local = 0; 20 | 21 | s32 ret = osContInit(PASS_RDRAM mq, &bitpattern_local, data); 22 | 23 | MEM_B(0, bitpattern) = bitpattern_local; 24 | 25 | _return(ctx, ret); 26 | } 27 | 28 | extern "C" void osContReset_recomp(uint8_t* rdram, recomp_context* ctx) { 29 | PTR(OSMesgQueue) mq = _arg<0, PTR(OSMesgQueue)>(rdram, ctx); 30 | PTR(OSContStatus) data = _arg<1, PTR(OSContStatus)>(rdram, ctx); 31 | 32 | s32 ret = osContReset(PASS_RDRAM mq, data); 33 | 34 | _return(ctx, ret); 35 | } 36 | 37 | extern "C" void osContStartReadData_recomp(uint8_t* rdram, recomp_context* ctx) { 38 | PTR(OSMesgQueue) mq = _arg<0, PTR(OSMesgQueue)>(rdram, ctx); 39 | 40 | s32 ret = osContStartReadData(PASS_RDRAM mq); 41 | 42 | _return(ctx, ret); 43 | } 44 | 45 | extern "C" void osContGetReadData_recomp(uint8_t* rdram, recomp_context* ctx) { 46 | PTR(OSContPad) data = _arg<0, PTR(OSContPad)>(rdram, ctx); 47 | 48 | OSContPad dummy_data[MAXCONTROLLERS]; 49 | 50 | osContGetReadData(dummy_data); 51 | 52 | for (int controller = 0; controller < MAXCONTROLLERS; controller++) { 53 | MEM_H(6 * controller + 0, data) = dummy_data[controller].button; 54 | MEM_B(6 * controller + 2, data) = dummy_data[controller].stick_x; 55 | MEM_B(6 * controller + 3, data) = dummy_data[controller].stick_y; 56 | MEM_B(6 * controller + 4, data) = dummy_data[controller].err_no; 57 | } 58 | } 59 | 60 | extern "C" void osContStartQuery_recomp(uint8_t * rdram, recomp_context * ctx) { 61 | PTR(OSMesgQueue) mq = _arg<0, PTR(OSMesgQueue)>(rdram, ctx); 62 | 63 | s32 ret = osContStartQuery(PASS_RDRAM mq); 64 | 65 | _return(ctx, ret); 66 | } 67 | 68 | extern "C" void osContGetQuery_recomp(uint8_t * rdram, recomp_context * ctx) { 69 | PTR(OSContStatus) data = _arg<0, PTR(OSContStatus)>(rdram, ctx); 70 | 71 | osContGetQuery(PASS_RDRAM data); 72 | } 73 | 74 | extern "C" void osContSetCh_recomp(uint8_t* rdram, recomp_context* ctx) { 75 | u8 ch = _arg<0, u8>(rdram, ctx); 76 | 77 | s32 ret = osContSetCh(PASS_RDRAM ch); 78 | 79 | _return(ctx, ret); 80 | } 81 | 82 | extern "C" void __osMotorAccess_recomp(uint8_t* rdram, recomp_context* ctx) { 83 | PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); 84 | s32 flag = _arg<1, s32>(rdram, ctx); 85 | 86 | s32 ret = __osMotorAccess(PASS_RDRAM pfs, flag); 87 | 88 | _return(ctx, ret); 89 | } 90 | 91 | extern "C" void osMotorInit_recomp(uint8_t* rdram, recomp_context* ctx) { 92 | PTR(OSMesgQueue) mq = _arg<0, PTR(OSMesgQueue)>(rdram, ctx); 93 | PTR(OSPfs) pfs = _arg<1, PTR(OSPfs)>(rdram, ctx); 94 | int channel = _arg<2, s32>(rdram, ctx); 95 | 96 | s32 ret = osMotorInit(PASS_RDRAM mq, pfs, channel); 97 | 98 | _return(ctx, ret); 99 | } 100 | 101 | extern "C" void osMotorStart_recomp(uint8_t* rdram, recomp_context* ctx) { 102 | PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); 103 | 104 | s32 ret = osMotorStart(PASS_RDRAM pfs); 105 | 106 | _return(ctx, ret); 107 | } 108 | 109 | extern "C" void osMotorStop_recomp(uint8_t* rdram, recomp_context* ctx) { 110 | PTR(OSPfs) pfs = _arg<0, PTR(OSPfs)>(rdram, ctx); 111 | 112 | s32 ret = osMotorStop(PASS_RDRAM pfs); 113 | 114 | _return(ctx, ret); 115 | } 116 | -------------------------------------------------------------------------------- /librecomp/src/dp.cpp: -------------------------------------------------------------------------------- 1 | #include "recomp.h" 2 | 3 | enum class RDPStatusBit { 4 | XbusDmem = 0, 5 | Freeze = 1, 6 | Flush = 2, 7 | CommandBusy = 6, 8 | BufferReady = 7, 9 | DmaBusy = 8, 10 | EndValid = 9, 11 | StartValid = 10, 12 | }; 13 | 14 | constexpr void update_bit(uint32_t& state, uint32_t flags, RDPStatusBit bit) { 15 | int reset_bit_pos = (int)bit * 2 + 0; 16 | int set_bit_pos = (int)bit * 2 + 1; 17 | bool set = (flags & (1U << set_bit_pos)) != 0; 18 | bool reset = (flags & (1U << reset_bit_pos)) != 0; 19 | 20 | if (set ^ reset) { 21 | if (set) { 22 | state |= (1U << (int)bit); 23 | } 24 | else { 25 | state &= ~(1U << (int)bit); 26 | } 27 | } 28 | } 29 | 30 | uint32_t rdp_state = 1 << (int)RDPStatusBit::BufferReady; 31 | 32 | extern "C" void osDpSetNextBuffer_recomp(uint8_t* rdram, recomp_context* ctx) { 33 | assert(false); 34 | } 35 | 36 | extern "C" void osDpGetStatus_recomp(uint8_t* rdram, recomp_context* ctx) { 37 | ctx->r2 = rdp_state; 38 | } 39 | 40 | extern "C" void osDpSetStatus_recomp(uint8_t* rdram, recomp_context* ctx) { 41 | update_bit(rdp_state, ctx->r4, RDPStatusBit::XbusDmem); 42 | update_bit(rdp_state, ctx->r4, RDPStatusBit::Freeze); 43 | update_bit(rdp_state, ctx->r4, RDPStatusBit::Flush); 44 | } 45 | -------------------------------------------------------------------------------- /librecomp/src/eep.cpp: -------------------------------------------------------------------------------- 1 | #include "recomp.h" 2 | #include "librecomp/game.hpp" 3 | 4 | #include "ultramodern/ultra64.h" 5 | 6 | void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count); 7 | void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count); 8 | 9 | constexpr int eeprom_block_size = 8; 10 | 11 | extern "C" void osEepromProbe_recomp(uint8_t* rdram, recomp_context* ctx) { 12 | switch (recomp::get_save_type()) { 13 | case recomp::SaveType::AllowAll: 14 | case recomp::SaveType::Eep16k: 15 | ctx->r2 = 0x02; // EEPROM_TYPE_16K 16 | break; 17 | case recomp::SaveType::Eep4k: 18 | ctx->r2 = 0x01; // EEPROM_TYPE_4K 19 | break; 20 | default: 21 | ctx->r2 = 0x00; 22 | break; 23 | } 24 | } 25 | 26 | extern "C" void osEepromWrite_recomp(uint8_t* rdram, recomp_context* ctx) { 27 | if (!recomp::eeprom_allowed()) { 28 | ultramodern::error_handling::message_box("Attempted to use EEPROM saving with other save type"); 29 | ULTRAMODERN_QUICK_EXIT(); 30 | } 31 | 32 | uint8_t eep_address = ctx->r5; 33 | gpr buffer = ctx->r6; 34 | int32_t nbytes = eeprom_block_size; 35 | 36 | save_write(rdram, buffer, eep_address * eeprom_block_size, nbytes); 37 | 38 | ctx->r2 = 0; 39 | } 40 | 41 | extern "C" void osEepromLongWrite_recomp(uint8_t* rdram, recomp_context* ctx) { 42 | if (!recomp::eeprom_allowed()) { 43 | ultramodern::error_handling::message_box("Attempted to use EEPROM saving with other save type"); 44 | ULTRAMODERN_QUICK_EXIT(); 45 | } 46 | 47 | uint8_t eep_address = ctx->r5; 48 | gpr buffer = ctx->r6; 49 | int32_t nbytes = ctx->r7; 50 | 51 | assert((nbytes % eeprom_block_size) == 0); 52 | 53 | save_write(rdram, buffer, eep_address * eeprom_block_size, nbytes); 54 | 55 | ctx->r2 = 0; 56 | } 57 | 58 | extern "C" void osEepromRead_recomp(uint8_t* rdram, recomp_context* ctx) { 59 | if (!recomp::eeprom_allowed()) { 60 | ultramodern::error_handling::message_box("Attempted to use EEPROM saving with other save type"); 61 | ULTRAMODERN_QUICK_EXIT(); 62 | } 63 | 64 | uint8_t eep_address = ctx->r5; 65 | gpr buffer = ctx->r6; 66 | int32_t nbytes = eeprom_block_size; 67 | 68 | save_read(rdram, buffer, eep_address * eeprom_block_size, nbytes); 69 | 70 | ctx->r2 = 0; 71 | } 72 | 73 | extern "C" void osEepromLongRead_recomp(uint8_t* rdram, recomp_context* ctx) { 74 | if (!recomp::eeprom_allowed()) { 75 | ultramodern::error_handling::message_box("Attempted to use EEPROM saving with other save type"); 76 | ULTRAMODERN_QUICK_EXIT(); 77 | } 78 | 79 | uint8_t eep_address = ctx->r5; 80 | gpr buffer = ctx->r6; 81 | int32_t nbytes = ctx->r7; 82 | 83 | assert((nbytes % eeprom_block_size) == 0); 84 | 85 | save_read(rdram, buffer, eep_address * eeprom_block_size, nbytes); 86 | 87 | ctx->r2 = 0; 88 | } 89 | -------------------------------------------------------------------------------- /librecomp/src/files.cpp: -------------------------------------------------------------------------------- 1 | #include "files.hpp" 2 | 3 | constexpr std::u8string_view backup_suffix = u8".bak"; 4 | constexpr std::u8string_view temp_suffix = u8".temp"; 5 | 6 | std::ifstream recomp::open_input_backup_file(const std::filesystem::path& filepath, std::ios_base::openmode mode) { 7 | std::filesystem::path backup_path{filepath}; 8 | backup_path += backup_suffix; 9 | return std::ifstream{backup_path, mode}; 10 | } 11 | 12 | std::ifstream recomp::open_input_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) { 13 | std::ifstream ret{filepath, mode}; 14 | 15 | // Check if the file failed to open and open the corresponding backup file instead if so. 16 | if (!ret.good()) { 17 | return open_input_backup_file(filepath, mode); 18 | } 19 | 20 | return ret; 21 | } 22 | 23 | std::ofstream recomp::open_output_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) { 24 | std::filesystem::path temp_path{filepath}; 25 | temp_path += temp_suffix; 26 | std::ofstream temp_file_out{ temp_path, mode }; 27 | 28 | return temp_file_out; 29 | } 30 | 31 | bool recomp::finalize_output_file_with_backup(const std::filesystem::path& filepath) { 32 | std::filesystem::path backup_path{filepath}; 33 | backup_path += backup_suffix; 34 | 35 | std::filesystem::path temp_path{filepath}; 36 | temp_path += temp_suffix; 37 | 38 | std::error_code ec; 39 | if (std::filesystem::exists(filepath, ec)) { 40 | std::filesystem::copy_file(filepath, backup_path, std::filesystem::copy_options::overwrite_existing, ec); 41 | if (ec) { 42 | return false; 43 | } 44 | } 45 | std::filesystem::copy_file(temp_path, filepath, std::filesystem::copy_options::overwrite_existing, ec); 46 | if (ec) { 47 | return false; 48 | } 49 | std::filesystem::remove(temp_path, ec); 50 | return true; 51 | } 52 | -------------------------------------------------------------------------------- /librecomp/src/flash.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "recomp.h" 6 | #include "librecomp/addresses.hpp" 7 | #include "librecomp/game.hpp" 8 | 9 | // TODO move this out into ultramodern code 10 | 11 | constexpr uint32_t flash_size = 1024 * 1024 / 8; // 1Mbit 12 | constexpr uint32_t page_size = 128; 13 | constexpr uint32_t pages_per_sector = 128; 14 | constexpr uint32_t page_count = flash_size / page_size; 15 | constexpr uint32_t sector_size = page_size * pages_per_sector; 16 | constexpr uint32_t sector_count = flash_size / sector_size; 17 | 18 | void save_write_ptr(const void* in, uint32_t offset, uint32_t count); 19 | void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count); 20 | void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count); 21 | void save_clear(uint32_t start, uint32_t size, char value); 22 | 23 | std::array write_buffer; 24 | 25 | extern "C" void osFlashInit_recomp(uint8_t * rdram, recomp_context * ctx) { 26 | if (!recomp::flashram_allowed()) { 27 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 28 | ULTRAMODERN_QUICK_EXIT(); 29 | } 30 | 31 | ctx->r2 = recomp::flash_handle; 32 | } 33 | 34 | extern "C" void osFlashReadStatus_recomp(uint8_t * rdram, recomp_context * ctx) { 35 | if (!recomp::flashram_allowed()) { 36 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 37 | ULTRAMODERN_QUICK_EXIT(); 38 | } 39 | 40 | PTR(u8) flash_status = ctx->r4; 41 | 42 | MEM_B(0, flash_status) = 0; 43 | } 44 | 45 | extern "C" void osFlashReadId_recomp(uint8_t * rdram, recomp_context * ctx) { 46 | if (!recomp::flashram_allowed()) { 47 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 48 | ULTRAMODERN_QUICK_EXIT(); 49 | } 50 | 51 | PTR(u32) flash_type = ctx->r4; 52 | PTR(u32) flash_maker = ctx->r5; 53 | 54 | // Mimic a real flash chip's type and maker, as some games actually check if one is present. 55 | MEM_W(0, flash_type) = 0x11118001; 56 | MEM_W(0, flash_maker) = 0x00C2001E; 57 | } 58 | 59 | extern "C" void osFlashClearStatus_recomp(uint8_t * rdram, recomp_context * ctx) { 60 | if (!recomp::flashram_allowed()) { 61 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 62 | ULTRAMODERN_QUICK_EXIT(); 63 | } 64 | 65 | // Nothing to do here. 66 | } 67 | 68 | extern "C" void osFlashAllErase_recomp(uint8_t * rdram, recomp_context * ctx) { 69 | if (!recomp::flashram_allowed()) { 70 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 71 | ULTRAMODERN_QUICK_EXIT(); 72 | } 73 | 74 | save_clear(0, ultramodern::save_size, 0xFF); 75 | 76 | ctx->r2 = 0; 77 | } 78 | 79 | extern "C" void osFlashAllEraseThrough_recomp(uint8_t * rdram, recomp_context * ctx) { 80 | if (!recomp::flashram_allowed()) { 81 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 82 | ULTRAMODERN_QUICK_EXIT(); 83 | } 84 | 85 | save_clear(0, ultramodern::save_size, 0xFF); 86 | 87 | ctx->r2 = 0; 88 | } 89 | 90 | // This function is named sector but really means page. 91 | extern "C" void osFlashSectorErase_recomp(uint8_t * rdram, recomp_context * ctx) { 92 | if (!recomp::flashram_allowed()) { 93 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 94 | ULTRAMODERN_QUICK_EXIT(); 95 | } 96 | 97 | uint32_t page_num = (uint32_t)ctx->r4; 98 | 99 | // Prevent out of bounds erase 100 | if (page_num >= page_count) { 101 | ctx->r2 = -1; 102 | return; 103 | } 104 | 105 | save_clear(page_num * page_size, page_size, 0xFF); 106 | 107 | ctx->r2 = 0; 108 | } 109 | 110 | // Same naming issue as above. 111 | extern "C" void osFlashSectorEraseThrough_recomp(uint8_t * rdram, recomp_context * ctx) { 112 | if (!recomp::flashram_allowed()) { 113 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 114 | ULTRAMODERN_QUICK_EXIT(); 115 | } 116 | 117 | uint32_t page_num = (uint32_t)ctx->r4; 118 | 119 | // Prevent out of bounds erase 120 | if (page_num >= page_count) { 121 | ctx->r2 = -1; 122 | return; 123 | } 124 | 125 | save_clear(page_num * page_size, page_size, 0xFF); 126 | 127 | ctx->r2 = 0; 128 | } 129 | 130 | extern "C" void osFlashCheckEraseEnd_recomp(uint8_t * rdram, recomp_context * ctx) { 131 | if (!recomp::flashram_allowed()) { 132 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 133 | ULTRAMODERN_QUICK_EXIT(); 134 | } 135 | 136 | // All erases are blocking in this implementation, so this should always return OK. 137 | ctx->r2 = 0; // FLASH_STATUS_ERASE_OK 138 | } 139 | 140 | extern "C" void osFlashWriteBuffer_recomp(uint8_t * rdram, recomp_context * ctx) { 141 | if (!recomp::flashram_allowed()) { 142 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 143 | ULTRAMODERN_QUICK_EXIT(); 144 | } 145 | 146 | OSIoMesg* mb = TO_PTR(OSIoMesg, ctx->r4); 147 | int32_t pri = ctx->r5; 148 | PTR(void) dramAddr = ctx->r6; 149 | PTR(OSMesgQueue) mq = ctx->r7; 150 | 151 | // Copy the input data into the write buffer 152 | for (size_t i = 0; i < page_size; i++) { 153 | write_buffer[i] = MEM_B(i, dramAddr); 154 | } 155 | 156 | // Send the message indicating write completion 157 | osSendMesg(PASS_RDRAM mq, 0, OS_MESG_NOBLOCK); 158 | 159 | ctx->r2 = 0; 160 | } 161 | 162 | extern "C" void osFlashWriteArray_recomp(uint8_t * rdram, recomp_context * ctx) { 163 | if (!recomp::flashram_allowed()) { 164 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 165 | ULTRAMODERN_QUICK_EXIT(); 166 | } 167 | 168 | uint32_t page_num = ctx->r4; 169 | 170 | // Copy the write buffer into the save file 171 | save_write_ptr(write_buffer.data(), page_num * page_size, page_size); 172 | 173 | ctx->r2 = 0; 174 | } 175 | 176 | extern "C" void osFlashReadArray_recomp(uint8_t * rdram, recomp_context * ctx) { 177 | if (!recomp::flashram_allowed()) { 178 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 179 | ULTRAMODERN_QUICK_EXIT(); 180 | } 181 | 182 | OSIoMesg* mb = TO_PTR(OSIoMesg, ctx->r4); 183 | int32_t pri = ctx->r5; 184 | uint32_t page_num = ctx->r6; 185 | PTR(void) dramAddr = ctx->r7; 186 | uint32_t n_pages = MEM_W(0x10, ctx->r29); 187 | PTR(OSMesgQueue) mq = MEM_W(0x14, ctx->r29); 188 | 189 | uint32_t offset = page_num * page_size; 190 | uint32_t count = n_pages * page_size; 191 | 192 | // Read from the save file into the provided buffer 193 | save_read(PASS_RDRAM dramAddr, offset, count); 194 | 195 | // Send the message indicating read completion 196 | osSendMesg(PASS_RDRAM mq, 0, OS_MESG_NOBLOCK); 197 | 198 | ctx->r2 = 0; 199 | } 200 | 201 | extern "C" void osFlashChange_recomp(uint8_t * rdram, recomp_context * ctx) { 202 | if (!recomp::flashram_allowed()) { 203 | ultramodern::error_handling::message_box("Attempted to use FlashRAM saving with other save type"); 204 | ULTRAMODERN_QUICK_EXIT(); 205 | } 206 | 207 | assert(false); 208 | } 209 | -------------------------------------------------------------------------------- /librecomp/src/heap.cpp: -------------------------------------------------------------------------------- 1 | #include "o1heap/o1heap.h" 2 | 3 | #include "librecomp/addresses.hpp" 4 | #include "librecomp/overlays.hpp" 5 | #include "librecomp/helpers.hpp" 6 | 7 | static uint32_t heap_offset; 8 | 9 | static inline O1HeapInstance* get_heap(uint8_t* rdram) { 10 | return reinterpret_cast(&rdram[heap_offset]); 11 | } 12 | 13 | extern "C" void recomp_alloc(uint8_t* rdram, recomp_context* ctx) { 14 | uint32_t offset = reinterpret_cast(recomp::alloc(rdram, ctx->r4)) - rdram; 15 | ctx->r2 = offset + 0xFFFFFFFF80000000ULL; 16 | } 17 | 18 | extern "C" void recomp_free(uint8_t* rdram, recomp_context* ctx) { 19 | PTR(void) to_free = _arg<0, PTR(void)>(rdram, ctx); 20 | // Handle NULL frees. 21 | if (to_free == NULLPTR) { 22 | return; 23 | } 24 | recomp::free(rdram, TO_PTR(void, to_free)); 25 | } 26 | 27 | void recomp::register_heap_exports() { 28 | recomp::overlays::register_base_export("recomp_alloc", recomp_alloc); 29 | recomp::overlays::register_base_export("recomp_free", recomp_free); 30 | } 31 | 32 | void recomp::init_heap(uint8_t* rdram, uint32_t address) { 33 | // Align the heap start to 16 bytes. 34 | address = (address + 15U) & ~15U; 35 | 36 | // Calculate the offset of the heap from the start of rdram and the size of the heap. 37 | heap_offset = address - 0x80000000U; 38 | size_t heap_size = recomp::mem_size - heap_offset; 39 | 40 | printf("Initializing recomp heap at offset 0x%08X with size 0x%08X\n", heap_offset, static_cast(heap_size)); 41 | 42 | o1heapInit(&rdram[heap_offset], heap_size); 43 | } 44 | 45 | void* recomp::alloc(uint8_t* rdram, size_t size) { 46 | return o1heapAllocate(get_heap(rdram), size); 47 | } 48 | 49 | void recomp::free(uint8_t* rdram, void* mem) { 50 | o1heapFree(get_heap(rdram), mem); 51 | } 52 | -------------------------------------------------------------------------------- /librecomp/src/math_routines.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "recomp.h" 3 | 4 | // TODO remove these by implementing the necessary instructions and control flow handling in the recompiler. 5 | // This has already been partially completed. 6 | 7 | extern "C" void __udivdi3_recomp(uint8_t * rdram, recomp_context * ctx) { 8 | uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu); 9 | uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu); 10 | uint64_t ret = a / b; 11 | 12 | ctx->r2 = (int32_t)(ret >> 32); 13 | ctx->r3 = (int32_t)(ret >> 0); 14 | } 15 | 16 | extern "C" void __divdi3_recomp(uint8_t * rdram, recomp_context * ctx) { 17 | int64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu); 18 | int64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu); 19 | int64_t ret = a / b; 20 | 21 | ctx->r2 = (int32_t)(ret >> 32); 22 | ctx->r3 = (int32_t)(ret >> 0); 23 | } 24 | 25 | extern "C" void __umoddi3_recomp(uint8_t * rdram, recomp_context * ctx) { 26 | uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu); 27 | uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu); 28 | uint64_t ret = a % b; 29 | 30 | ctx->r2 = (int32_t)(ret >> 32); 31 | ctx->r3 = (int32_t)(ret >> 0); 32 | } 33 | 34 | extern "C" void __ull_div_recomp(uint8_t * rdram, recomp_context * ctx) { 35 | uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu); 36 | uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu); 37 | uint64_t ret = a / b; 38 | 39 | ctx->r2 = (int32_t)(ret >> 32); 40 | ctx->r3 = (int32_t)(ret >> 0); 41 | } 42 | 43 | extern "C" void __ll_div_recomp(uint8_t * rdram, recomp_context * ctx) { 44 | int64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu); 45 | int64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu); 46 | int64_t ret = a / b; 47 | 48 | ctx->r2 = (int32_t)(ret >> 32); 49 | ctx->r3 = (int32_t)(ret >> 0); 50 | } 51 | 52 | extern "C" void __ll_mul_recomp(uint8_t * rdram, recomp_context * ctx) { 53 | uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu); 54 | uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu); 55 | uint64_t ret = a * b; 56 | 57 | ctx->r2 = (int32_t)(ret >> 32); 58 | ctx->r3 = (int32_t)(ret >> 0); 59 | } 60 | 61 | extern "C" void __ull_rem_recomp(uint8_t * rdram, recomp_context * ctx) { 62 | uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu); 63 | uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu); 64 | uint64_t ret = a % b; 65 | 66 | ctx->r2 = (int32_t)(ret >> 32); 67 | ctx->r3 = (int32_t)(ret >> 0); 68 | } 69 | 70 | extern "C" void __ull_to_d_recomp(uint8_t * rdram, recomp_context * ctx) { 71 | uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu); 72 | double ret = (double)a; 73 | 74 | ctx->f0.d = ret; 75 | } 76 | 77 | extern "C" void __ull_to_f_recomp(uint8_t * rdram, recomp_context * ctx) { 78 | uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu); 79 | float ret = (float)a; 80 | 81 | ctx->f0.fl = ret; 82 | } 83 | -------------------------------------------------------------------------------- /librecomp/src/mod_config_api.cpp: -------------------------------------------------------------------------------- 1 | #include "librecomp/mods.hpp" 2 | #include "librecomp/helpers.hpp" 3 | #include "librecomp/addresses.hpp" 4 | 5 | void recomp_get_config_u32(uint8_t* rdram, recomp_context* ctx, size_t mod_index) { 6 | recomp::mods::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx)); 7 | if (uint32_t* as_u32 = std::get_if(&val)) { 8 | _return(ctx, *as_u32); 9 | } 10 | else if (double* as_double = std::get_if(&val)) { 11 | _return(ctx, uint32_t(int32_t(*as_double))); 12 | } 13 | else { 14 | _return(ctx, uint32_t{0}); 15 | } 16 | } 17 | 18 | void recomp_get_config_double(uint8_t* rdram, recomp_context* ctx, size_t mod_index) { 19 | recomp::mods::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx)); 20 | if (uint32_t* as_u32 = std::get_if(&val)) { 21 | ctx->f0.d = double(*as_u32); 22 | } 23 | else if (double* as_double = std::get_if(&val)) { 24 | ctx->f0.d = *as_double; 25 | } 26 | else { 27 | ctx->f0.d = 0.0; 28 | } 29 | } 30 | 31 | template 32 | void return_string(uint8_t* rdram, recomp_context* ctx, const StringType& str) { 33 | // Allocate space in the recomp heap to hold the string, including the null terminator. 34 | size_t alloc_size = (str.size() + 1 + 15) & ~15; 35 | gpr offset = reinterpret_cast(recomp::alloc(rdram, alloc_size)) - rdram; 36 | gpr addr = offset + 0xFFFFFFFF80000000ULL; 37 | 38 | // Copy the string's data into the allocated memory and null terminate it. 39 | for (size_t i = 0; i < str.size(); i++) { 40 | MEM_B(i, addr) = str[i]; 41 | } 42 | MEM_B(str.size(), addr) = 0; 43 | 44 | // Return the allocated memory. 45 | ctx->r2 = addr; 46 | } 47 | 48 | void recomp_get_config_string(uint8_t* rdram, recomp_context* ctx, size_t mod_index) { 49 | recomp::mods::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx)); 50 | if (std::string* as_string = std::get_if(&val)) { 51 | return_string(rdram, ctx, *as_string); 52 | } 53 | else { 54 | _return(ctx, NULLPTR); 55 | } 56 | } 57 | 58 | void recomp_free_config_string(uint8_t* rdram, recomp_context* ctx) { 59 | gpr str_rdram = (gpr)_arg<0, PTR(char)>(rdram, ctx); 60 | gpr offset = str_rdram - 0xFFFFFFFF80000000ULL; 61 | 62 | recomp::free(rdram, rdram + offset); 63 | } 64 | 65 | void recomp_get_mod_version(uint8_t* rdram, recomp_context* ctx, size_t mod_index) { 66 | uint32_t* major_out = _arg<0, uint32_t*>(rdram, ctx); 67 | uint32_t* minor_out = _arg<1, uint32_t*>(rdram, ctx); 68 | uint32_t* patch_out = _arg<2, uint32_t*>(rdram, ctx); 69 | 70 | recomp::Version version = recomp::mods::get_mod_version(mod_index); 71 | 72 | *major_out = version.major; 73 | *minor_out = version.minor; 74 | *patch_out = version.patch; 75 | } 76 | 77 | void recomp_change_save_file(uint8_t* rdram, recomp_context* ctx, size_t mod_index) { 78 | std::string name = _arg_string<0>(rdram, ctx); 79 | std::u8string name_u8 = std::u8string{reinterpret_cast(name.data()), name.size()}; 80 | 81 | std::string mod_id = recomp::mods::get_mod_id(mod_index); 82 | std::u8string mod_id_u8 = std::u8string{reinterpret_cast(mod_id.data()), mod_id.size()}; 83 | 84 | ultramodern::change_save_file(mod_id_u8, name_u8); 85 | } 86 | 87 | void recomp_get_save_file_path(uint8_t* rdram, recomp_context* ctx) { 88 | std::filesystem::path save_file_path = ultramodern::get_save_file_path(); 89 | 90 | return_string(rdram, ctx, std::filesystem::absolute(save_file_path).u8string()); 91 | } 92 | 93 | void recomp_get_mod_folder_path(uint8_t* rdram, recomp_context* ctx) { 94 | std::filesystem::path mod_folder_path = recomp::mods::get_mods_directory(); 95 | 96 | return_string(rdram, ctx, std::filesystem::absolute(mod_folder_path).u8string()); 97 | } 98 | 99 | void recomp::mods::register_config_exports() { 100 | recomp::overlays::register_ext_base_export("recomp_get_config_u32", recomp_get_config_u32); 101 | recomp::overlays::register_ext_base_export("recomp_get_config_double", recomp_get_config_double); 102 | recomp::overlays::register_ext_base_export("recomp_get_config_string", recomp_get_config_string); 103 | recomp::overlays::register_base_export("recomp_free_config_string", recomp_free_config_string); 104 | recomp::overlays::register_ext_base_export("recomp_get_mod_version", recomp_get_mod_version); 105 | recomp::overlays::register_ext_base_export("recomp_change_save_file", recomp_change_save_file); 106 | recomp::overlays::register_base_export("recomp_get_save_file_path", recomp_get_save_file_path); 107 | recomp::overlays::register_base_export("recomp_get_mod_folder_path", recomp_get_mod_folder_path); 108 | } 109 | -------------------------------------------------------------------------------- /librecomp/src/mod_events.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "librecomp/mods.hpp" 3 | #include "librecomp/overlays.hpp" 4 | #include "ultramodern/error_handling.hpp" 5 | 6 | template 7 | struct overloaded : Ts... { using Ts::operator()...; }; 8 | template 9 | overloaded(Ts...) -> overloaded; 10 | 11 | // Vector of callbacks for each registered event. 12 | std::vector> event_callbacks{}; 13 | 14 | extern "C" { 15 | // This can stay at 0 since the base events are always first in the list. 16 | uint32_t builtin_base_event_index = 0; 17 | } 18 | 19 | extern "C" void recomp_trigger_event(uint8_t* rdram, recomp_context* ctx, uint32_t event_index) { 20 | // Sanity check the event index. 21 | if (event_index >= event_callbacks.size()) { 22 | printf("Event %u triggered, but only %zu events have been registered!\n", event_index, event_callbacks.size()); 23 | assert(false); 24 | ultramodern::error_handling::message_box("Encountered an error with loaded mods: event index out of bounds"); 25 | ULTRAMODERN_QUICK_EXIT(); 26 | } 27 | 28 | // Copy the initial context state to restore it after running each callback. 29 | recomp_context initial_context = *ctx; 30 | 31 | // Call every callback attached to the event. 32 | const std::vector& callbacks = event_callbacks[event_index]; 33 | for (recomp::mods::GenericFunction func : callbacks) { 34 | // Run the callback. 35 | std::visit(overloaded { 36 | [rdram, ctx](recomp_func_t* native_func) { 37 | native_func(rdram, ctx); 38 | }, 39 | }, func); 40 | 41 | // Restore the original context. 42 | *ctx = initial_context; 43 | } 44 | } 45 | 46 | void recomp::mods::setup_events(size_t num_events) { 47 | event_callbacks.resize(num_events); 48 | } 49 | 50 | void recomp::mods::register_event_callback(size_t event_index, GenericFunction callback) { 51 | event_callbacks[event_index].emplace_back(callback); 52 | } 53 | 54 | void recomp::mods::reset_events() { 55 | event_callbacks.clear(); 56 | } 57 | -------------------------------------------------------------------------------- /librecomp/src/mod_hooks.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "librecomp/mods.hpp" 3 | #include "librecomp/overlays.hpp" 4 | #include "ultramodern/error_handling.hpp" 5 | 6 | template 7 | struct overloaded : Ts... { using Ts::operator()...; }; 8 | template 9 | overloaded(Ts...) -> overloaded; 10 | 11 | // Vector of individual hooks for each hook slot. 12 | std::vector> hook_table{}; 13 | 14 | void recomp::mods::run_hook(uint8_t* rdram, recomp_context* ctx, size_t hook_slot_index) { 15 | // Sanity check the hook slot index. 16 | if (hook_slot_index >= hook_table.size()) { 17 | printf("Hook slot %zu triggered, but only %zu hook slots have been registered!\n", hook_slot_index, hook_table.size()); 18 | assert(false); 19 | ultramodern::error_handling::message_box("Encountered an error with loaded mods: hook slot out of bounds"); 20 | ULTRAMODERN_QUICK_EXIT(); 21 | } 22 | 23 | // Copy the initial context state to restore it after running each callback. 24 | recomp_context initial_context = *ctx; 25 | 26 | // Call every hook attached to the hook slot. 27 | const std::vector& hooks = hook_table[hook_slot_index]; 28 | for (recomp::mods::GenericFunction func : hooks) { 29 | // Run the hook. 30 | std::visit(overloaded { 31 | [rdram, ctx](recomp_func_t* native_func) { 32 | native_func(rdram, ctx); 33 | }, 34 | }, func); 35 | 36 | // Restore the original context. 37 | *ctx = initial_context; 38 | } 39 | } 40 | 41 | void recomp::mods::setup_hooks(size_t num_hook_slots) { 42 | hook_table.resize(num_hook_slots); 43 | } 44 | 45 | void recomp::mods::register_hook(size_t hook_slot_index, GenericFunction callback) { 46 | hook_table[hook_slot_index].emplace_back(callback); 47 | } 48 | 49 | void recomp::mods::reset_hooks() { 50 | hook_table.clear(); 51 | } 52 | -------------------------------------------------------------------------------- /librecomp/src/overlays.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "ultramodern/ultramodern.hpp" 9 | 10 | #include "recomp.h" 11 | #include "recompiler/context.h" 12 | #include "overlays.hpp" 13 | #include "sections.h" 14 | 15 | static recomp::overlays::overlay_section_table_data_t sections_info {}; 16 | static recomp::overlays::overlays_by_index_t overlays_info {}; 17 | 18 | static SectionTableEntry* patch_code_sections = nullptr; 19 | size_t num_patch_code_sections = 0; 20 | static std::vector patch_data; 21 | 22 | struct LoadedSection { 23 | int32_t loaded_ram_addr; 24 | size_t section_table_index; 25 | 26 | LoadedSection(int32_t loaded_ram_addr_, size_t section_table_index_) { 27 | loaded_ram_addr = loaded_ram_addr_; 28 | section_table_index = section_table_index_; 29 | } 30 | 31 | bool operator<(const LoadedSection& rhs) { 32 | return loaded_ram_addr < rhs.loaded_ram_addr; 33 | } 34 | }; 35 | 36 | static std::unordered_map code_sections_by_rom{}; 37 | static std::unordered_map patch_code_sections_by_rom{}; 38 | static std::vector loaded_sections{}; 39 | static std::unordered_map func_map{}; 40 | static std::unordered_map base_exports{}; 41 | static std::unordered_map ext_base_exports{}; 42 | static std::unordered_map base_events; 43 | static std::unordered_map manual_patch_symbols_by_vram; 44 | 45 | extern "C" { 46 | int32_t* section_addresses = nullptr; 47 | } 48 | 49 | void recomp::overlays::register_overlays(const overlay_section_table_data_t& sections, const overlays_by_index_t& overlays) { 50 | sections_info = sections; 51 | overlays_info = overlays; 52 | } 53 | 54 | void recomp::overlays::register_patches(const char* patch, std::size_t size, SectionTableEntry* sections, size_t num_sections) { 55 | patch_code_sections = sections; 56 | num_patch_code_sections = num_sections; 57 | 58 | patch_data.resize(size); 59 | std::memcpy(patch_data.data(), patch, size); 60 | 61 | patch_code_sections_by_rom.reserve(num_patch_code_sections); 62 | for (size_t i = 0; i < num_patch_code_sections; i++) { 63 | patch_code_sections_by_rom.emplace(patch_code_sections[i].rom_addr, i); 64 | } 65 | } 66 | 67 | void recomp::overlays::register_base_export(const std::string& name, recomp_func_t* func) { 68 | base_exports.emplace(name, func); 69 | } 70 | 71 | void recomp::overlays::register_ext_base_export(const std::string& name, recomp_func_ext_t* func) { 72 | ext_base_exports.emplace(name, func); 73 | } 74 | 75 | void recomp::overlays::register_base_exports(const FunctionExport* export_list) { 76 | std::unordered_map patch_func_vram_map{}; 77 | 78 | // Iterate over all patch functions to set up a mapping of their vram address. 79 | for (size_t patch_section_index = 0; patch_section_index < num_patch_code_sections; patch_section_index++) { 80 | const SectionTableEntry* cur_section = &patch_code_sections[patch_section_index]; 81 | 82 | for (size_t func_index = 0; func_index < cur_section->num_funcs; func_index++) { 83 | const FuncEntry* cur_func = &cur_section->funcs[func_index]; 84 | patch_func_vram_map.emplace(cur_section->ram_addr + cur_func->offset, cur_func->func); 85 | } 86 | } 87 | 88 | // Iterate over exports, using the vram mapping to create a name mapping. 89 | for (const FunctionExport* cur_export = &export_list[0]; cur_export->name != nullptr; cur_export++) { 90 | auto it = patch_func_vram_map.find(cur_export->ram_addr); 91 | if (it == patch_func_vram_map.end()) { 92 | assert(false && "Failed to find exported function in patch function sections!"); 93 | } 94 | base_exports.emplace(cur_export->name, it->second); 95 | } 96 | } 97 | 98 | recomp_func_t* recomp::overlays::get_base_export(const std::string& export_name) { 99 | auto it = base_exports.find(export_name); 100 | if (it == base_exports.end()) { 101 | return nullptr; 102 | } 103 | return it->second; 104 | } 105 | 106 | recomp_func_ext_t* recomp::overlays::get_ext_base_export(const std::string& export_name) { 107 | auto it = ext_base_exports.find(export_name); 108 | if (it == ext_base_exports.end()) { 109 | return nullptr; 110 | } 111 | return it->second; 112 | } 113 | 114 | void recomp::overlays::register_base_events(char const* const* event_names) { 115 | for (size_t event_index = 0; event_names[event_index] != nullptr; event_index++) { 116 | base_events.emplace(event_names[event_index], event_index); 117 | } 118 | } 119 | 120 | size_t recomp::overlays::get_base_event_index(const std::string& event_name) { 121 | auto it = base_events.find(event_name); 122 | if (it == base_events.end()) { 123 | return (size_t)-1; 124 | } 125 | return it->second; 126 | } 127 | 128 | size_t recomp::overlays::num_base_events() { 129 | return base_events.size(); 130 | } 131 | 132 | const std::unordered_map& recomp::overlays::get_vrom_to_section_map() { 133 | return code_sections_by_rom; 134 | } 135 | 136 | uint32_t recomp::overlays::get_section_ram_addr(uint16_t code_section_index) { 137 | return sections_info.code_sections[code_section_index].ram_addr; 138 | } 139 | 140 | std::span recomp::overlays::get_section_relocs(uint16_t code_section_index) { 141 | if (code_section_index < sections_info.num_code_sections) { 142 | const auto& section = sections_info.code_sections[code_section_index]; 143 | return std::span{ section.relocs, section.num_relocs }; 144 | } 145 | assert(false); 146 | return {}; 147 | } 148 | 149 | void recomp::overlays::add_loaded_function(int32_t ram, recomp_func_t* func) { 150 | func_map[ram] = func; 151 | } 152 | 153 | void load_overlay(size_t section_table_index, int32_t ram) { 154 | const SectionTableEntry& section = sections_info.code_sections[section_table_index]; 155 | 156 | for (size_t function_index = 0; function_index < section.num_funcs; function_index++) { 157 | const FuncEntry& func = section.funcs[function_index]; 158 | func_map[ram + func.offset] = func.func; 159 | } 160 | 161 | loaded_sections.emplace_back(ram, section_table_index); 162 | section_addresses[section.index] = ram; 163 | } 164 | 165 | static void load_special_overlay(const SectionTableEntry& section, int32_t ram) { 166 | for (size_t function_index = 0; function_index < section.num_funcs; function_index++) { 167 | const FuncEntry& func = section.funcs[function_index]; 168 | func_map[ram + func.offset] = func.func; 169 | } 170 | } 171 | 172 | static void load_patch_functions() { 173 | if (patch_code_sections == nullptr) { 174 | debug_printf("[Patch] No patch section was registered\n"); 175 | return; 176 | } 177 | for (size_t i = 0; i < num_patch_code_sections; i++) { 178 | load_special_overlay(patch_code_sections[i], patch_code_sections[i].ram_addr); 179 | } 180 | } 181 | 182 | void recomp::overlays::read_patch_data(uint8_t* rdram, gpr patch_data_address) { 183 | for (size_t i = 0; i < patch_data.size(); i++) { 184 | MEM_B(i, patch_data_address) = patch_data[i]; 185 | } 186 | } 187 | 188 | extern "C" void load_overlays(uint32_t rom, int32_t ram_addr, uint32_t size) { 189 | // Search for the first section that's included in the loaded rom range 190 | // Sections were sorted by `init_overlays` so we can use the bounds functions 191 | auto lower = std::lower_bound(§ions_info.code_sections[0], §ions_info.code_sections[sections_info.num_code_sections], rom, 192 | [](const SectionTableEntry& entry, uint32_t addr) { 193 | return entry.rom_addr < addr; 194 | } 195 | ); 196 | auto upper = std::upper_bound(§ions_info.code_sections[0], §ions_info.code_sections[sections_info.num_code_sections], (uint32_t)(rom + size), 197 | [](uint32_t addr, const SectionTableEntry& entry) { 198 | return addr < entry.size + entry.rom_addr; 199 | } 200 | ); 201 | // Load the overlays that were found 202 | for (auto it = lower; it != upper; ++it) { 203 | load_overlay(std::distance(§ions_info.code_sections[0], it), it->rom_addr - rom + ram_addr); 204 | } 205 | } 206 | 207 | extern "C" void unload_overlay_by_id(uint32_t id) { 208 | uint32_t section_table_index = overlays_info.table[id]; 209 | const SectionTableEntry& section = sections_info.code_sections[section_table_index]; 210 | 211 | auto find_it = std::find_if(loaded_sections.begin(), loaded_sections.end(), [section_table_index](const LoadedSection& s) { return s.section_table_index == section_table_index; }); 212 | 213 | if (find_it != loaded_sections.end()) { 214 | // Determine where each function was loaded to and remove that entry from the function map 215 | for (size_t func_index = 0; func_index < section.num_funcs; func_index++) { 216 | const auto& func = section.funcs[func_index]; 217 | uint32_t func_address = func.offset + find_it->loaded_ram_addr; 218 | func_map.erase(func_address); 219 | } 220 | // Reset the section's address in the address table 221 | section_addresses[section.index] = section.ram_addr; 222 | // Remove the section from the loaded section map 223 | loaded_sections.erase(find_it); 224 | } 225 | } 226 | 227 | extern "C" void load_overlay_by_id(uint32_t id, uint32_t ram_addr) { 228 | uint32_t section_table_index = overlays_info.table[id]; 229 | const SectionTableEntry& section = sections_info.code_sections[section_table_index]; 230 | int32_t prev_address = section_addresses[section.index]; 231 | if (/*ram_addr >= 0x80000000 && ram_addr < 0x81000000) {*/ prev_address == section.ram_addr) { 232 | load_overlay(section_table_index, ram_addr); 233 | } 234 | else { 235 | int32_t new_address = prev_address + ram_addr; 236 | unload_overlay_by_id(id); 237 | load_overlay(section_table_index, new_address); 238 | } 239 | } 240 | 241 | extern "C" void unload_overlays(int32_t ram_addr, uint32_t size) { 242 | for (auto it = loaded_sections.begin(); it != loaded_sections.end();) { 243 | const auto& section = sections_info.code_sections[it->section_table_index]; 244 | 245 | // Check if the unloaded region overlaps with the loaded section 246 | if (ram_addr < (it->loaded_ram_addr + section.size) && (ram_addr + size) >= it->loaded_ram_addr) { 247 | // Check if the section isn't entirely in the loaded region 248 | if (ram_addr > it->loaded_ram_addr || (ram_addr + size) < (it->loaded_ram_addr + section.size)) { 249 | fprintf(stderr, 250 | "Cannot partially unload section\n" 251 | " rom: 0x%08X size: 0x%08X loaded_addr: 0x%08X\n" 252 | " unloaded_ram: 0x%08X unloaded_size : 0x%08X\n", 253 | section.rom_addr, section.size, it->loaded_ram_addr, ram_addr, size); 254 | assert(false); 255 | std::exit(EXIT_FAILURE); 256 | } 257 | // Determine where each function was loaded to and remove that entry from the function map 258 | for (size_t func_index = 0; func_index < section.num_funcs; func_index++) { 259 | const auto& func = section.funcs[func_index]; 260 | uint32_t func_address = func.offset + it->loaded_ram_addr; 261 | func_map.erase(func_address); 262 | } 263 | // Reset the section's address in the address table 264 | section_addresses[section.index] = section.ram_addr; 265 | // Remove the section from the loaded section map 266 | it = loaded_sections.erase(it); 267 | // Skip incrementing the iterator 268 | continue; 269 | } 270 | ++it; 271 | } 272 | } 273 | 274 | void recomp::overlays::init_overlays() { 275 | func_map.clear(); 276 | section_addresses = (int32_t *)calloc(sections_info.total_num_sections, sizeof(int32_t)); 277 | 278 | // Sort the executable sections by rom address 279 | std::sort(§ions_info.code_sections[0], §ions_info.code_sections[sections_info.num_code_sections], 280 | [](const SectionTableEntry& a, const SectionTableEntry& b) { 281 | return a.rom_addr < b.rom_addr; 282 | } 283 | ); 284 | 285 | for (size_t section_index = 0; section_index < sections_info.num_code_sections; section_index++) { 286 | SectionTableEntry* code_section = §ions_info.code_sections[section_index]; 287 | 288 | section_addresses[sections_info.code_sections[section_index].index] = code_section->ram_addr; 289 | code_sections_by_rom[code_section->rom_addr] = section_index; 290 | } 291 | 292 | load_patch_functions(); 293 | } 294 | 295 | // Finds a function given a section's index and the function's offset into the section. 296 | bool recomp::overlays::get_func_entry_by_section_index_function_offset(uint16_t code_section_index, uint32_t function_offset, FuncEntry& func_out) { 297 | if (code_section_index >= sections_info.num_code_sections) { 298 | return false; 299 | } 300 | 301 | SectionTableEntry* section = §ions_info.code_sections[code_section_index]; 302 | if (function_offset >= section->size) { 303 | return false; 304 | } 305 | 306 | // TODO avoid a linear lookup here. 307 | for (size_t func_index = 0; func_index < section->num_funcs; func_index++) { 308 | if (section->funcs[func_index].offset == function_offset) { 309 | func_out = section->funcs[func_index]; 310 | return true; 311 | } 312 | } 313 | 314 | return false; 315 | } 316 | 317 | void recomp::overlays::register_manual_patch_symbols(const ManualPatchSymbol* manual_patch_symbols) { 318 | for (size_t i = 0; manual_patch_symbols[i].func != nullptr; i++) { 319 | if (!manual_patch_symbols_by_vram.emplace(manual_patch_symbols[i].ram_addr, manual_patch_symbols[i].func).second) { 320 | printf("Duplicate manual patch symbol address: %08X\n", manual_patch_symbols[i].ram_addr); 321 | ultramodern::error_handling::message_box("Duplicate manual patch symbol address (syms.ld)!"); 322 | assert(false && "Duplicate manual patch symbol address (syms.ld)!"); 323 | ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__); 324 | } 325 | } 326 | } 327 | 328 | // TODO use N64Recomp::is_manual_patch_symbol instead after updating submodule. 329 | bool is_manual_patch_symbol(uint32_t vram) { 330 | return vram >= 0x8F000000 && vram < 0x90000000; 331 | } 332 | 333 | // Finds a function given a section's index and the function's offset into the section and returns its native pointer. 334 | recomp_func_t* recomp::overlays::get_func_by_section_index_function_offset(uint16_t code_section_index, uint32_t function_offset) { 335 | FuncEntry entry; 336 | 337 | if (get_func_entry_by_section_index_function_offset(code_section_index, function_offset, entry)) { 338 | return entry.func; 339 | } 340 | 341 | if (code_section_index == N64Recomp::SectionAbsolute && is_manual_patch_symbol(function_offset)) { 342 | auto find_it = manual_patch_symbols_by_vram.find(function_offset); 343 | if (find_it != manual_patch_symbols_by_vram.end()) { 344 | return find_it->second; 345 | } 346 | } 347 | 348 | return nullptr; 349 | } 350 | 351 | // Finds a function given a section's rom address and the function's vram address. 352 | recomp_func_t* recomp::overlays::get_func_by_section_rom_function_vram(uint32_t section_rom, uint32_t function_vram) { 353 | auto find_section_it = code_sections_by_rom.find(section_rom); 354 | if (find_section_it == code_sections_by_rom.end()) { 355 | return nullptr; 356 | } 357 | 358 | SectionTableEntry* section = §ions_info.code_sections[find_section_it->second]; 359 | int32_t func_offset = function_vram - section->ram_addr; 360 | 361 | return get_func_by_section_index_function_offset(find_section_it->second, func_offset); 362 | } 363 | 364 | extern "C" recomp_func_t * get_function(int32_t addr) { 365 | auto func_find = func_map.find(addr); 366 | if (func_find == func_map.end()) { 367 | fprintf(stderr, "Failed to find function at 0x%08X\n", addr); 368 | assert(false); 369 | std::exit(EXIT_FAILURE); 370 | } 371 | return func_find->second; 372 | } 373 | 374 | std::unordered_map recomp::overlays::get_base_patched_funcs() { 375 | std::unordered_map ret{}; 376 | 377 | // Collect the set of all functions in the patches. 378 | std::unordered_map all_patch_funcs{}; 379 | for (size_t patch_section_index = 0; patch_section_index < num_patch_code_sections; patch_section_index++) { 380 | const auto& patch_section = patch_code_sections[patch_section_index]; 381 | for (size_t func_index = 0; func_index < patch_section.num_funcs; func_index++) { 382 | all_patch_funcs.emplace(patch_section.funcs[func_index].func, BasePatchedFunction{ .patch_section = patch_section_index, .function_index = func_index }); 383 | } 384 | } 385 | 386 | // Check every vanilla function against the full patch function set. 387 | // Any functions in both are patched. 388 | for (size_t code_section_index = 0; code_section_index < sections_info.num_code_sections; code_section_index++) { 389 | const auto& code_section = sections_info.code_sections[code_section_index]; 390 | for (size_t func_index = 0; func_index < code_section.num_funcs; func_index++) { 391 | recomp_func_t* cur_func = code_section.funcs[func_index].func; 392 | // If this function also exists in the patches function set then it's a vanilla function that was patched. 393 | auto find_it = all_patch_funcs.find(cur_func); 394 | if (find_it != all_patch_funcs.end()) { 395 | ret.emplace(cur_func, find_it->second); 396 | } 397 | } 398 | } 399 | 400 | return ret; 401 | } 402 | 403 | const std::unordered_map& recomp::overlays::get_patch_vrom_to_section_map() { 404 | return patch_code_sections_by_rom; 405 | } 406 | 407 | uint32_t recomp::overlays::get_patch_section_ram_addr(uint16_t patch_code_section_index) { 408 | if (patch_code_section_index < num_patch_code_sections) { 409 | return patch_code_sections[patch_code_section_index].ram_addr; 410 | } 411 | assert(false); 412 | return -1; 413 | } 414 | 415 | uint32_t recomp::overlays::get_patch_section_rom_addr(uint16_t patch_code_section_index) { 416 | if (patch_code_section_index < num_patch_code_sections) { 417 | return patch_code_sections[patch_code_section_index].rom_addr; 418 | } 419 | assert(false); 420 | return -1; 421 | } 422 | 423 | const FuncEntry* recomp::overlays::get_patch_function_entry(uint16_t patch_code_section_index, size_t function_index) { 424 | if (patch_code_section_index < num_patch_code_sections) { 425 | const auto& section = patch_code_sections[patch_code_section_index]; 426 | if (function_index < section.num_funcs) { 427 | return §ion.funcs[function_index]; 428 | } 429 | } 430 | assert(false); 431 | return nullptr; 432 | } 433 | 434 | // Finds a base patched function given a patch section's index and the function's offset into the section. 435 | bool recomp::overlays::get_patch_func_entry_by_section_index_function_offset(uint16_t patch_code_section_index, uint32_t function_offset, FuncEntry& func_out) { 436 | if (patch_code_section_index >= num_patch_code_sections) { 437 | return false; 438 | } 439 | 440 | SectionTableEntry* section = &patch_code_sections[patch_code_section_index]; 441 | if (function_offset >= section->size) { 442 | return false; 443 | } 444 | 445 | // TODO avoid a linear lookup here. 446 | for (size_t func_index = 0; func_index < section->num_funcs; func_index++) { 447 | if (section->funcs[func_index].offset == function_offset) { 448 | func_out = section->funcs[func_index]; 449 | return true; 450 | } 451 | } 452 | 453 | return false; 454 | } 455 | 456 | std::span recomp::overlays::get_patch_section_relocs(uint16_t patch_code_section_index) { 457 | if (patch_code_section_index < num_patch_code_sections) { 458 | const auto& section = patch_code_sections[patch_code_section_index]; 459 | return std::span{ section.relocs, section.num_relocs }; 460 | } 461 | assert(false); 462 | return {}; 463 | } 464 | 465 | std::span recomp::overlays::get_patch_binary() { 466 | return std::span{ reinterpret_cast(patch_data.data()), patch_data.size() }; 467 | } 468 | -------------------------------------------------------------------------------- /librecomp/src/pak.cpp: -------------------------------------------------------------------------------- 1 | #include "ultramodern/ultra64.h" 2 | #include "ultramodern/ultramodern.hpp" 3 | 4 | #include "recomp.h" 5 | #include "helpers.hpp" 6 | 7 | extern "C" void osPfsInitPak_recomp(uint8_t * rdram, recomp_context* ctx) { 8 | ctx->r2 = 1; // PFS_ERR_NOPACK 9 | } 10 | 11 | extern "C" void osPfsFreeBlocks_recomp(uint8_t * rdram, recomp_context * ctx) { 12 | ctx->r2 = 1; // PFS_ERR_NOPACK 13 | } 14 | 15 | extern "C" void osPfsAllocateFile_recomp(uint8_t * rdram, recomp_context * ctx) { 16 | ctx->r2 = 1; // PFS_ERR_NOPACK 17 | } 18 | 19 | extern "C" void osPfsDeleteFile_recomp(uint8_t * rdram, recomp_context * ctx) { 20 | ctx->r2 = 1; // PFS_ERR_NOPACK 21 | } 22 | 23 | extern "C" void osPfsFileState_recomp(uint8_t * rdram, recomp_context * ctx) { 24 | ctx->r2 = 1; // PFS_ERR_NOPACK 25 | } 26 | 27 | extern "C" void osPfsFindFile_recomp(uint8_t * rdram, recomp_context * ctx) { 28 | ctx->r2 = 1; // PFS_ERR_NOPACK 29 | } 30 | 31 | extern "C" void osPfsReadWriteFile_recomp(uint8_t * rdram, recomp_context * ctx) { 32 | ctx->r2 = 1; // PFS_ERR_NOPACK 33 | } 34 | 35 | extern "C" void osPfsChecker_recomp(uint8_t * rdram, recomp_context * ctx) { 36 | ctx->r2 = 1; // PFS_ERR_NOPACK 37 | } 38 | 39 | extern "C" void osPfsNumFiles_recomp(uint8_t * rdram, recomp_context * ctx) { 40 | s32* max_files = _arg<1, s32*>(rdram, ctx); 41 | s32* files_used = _arg<2, s32*>(rdram, ctx); 42 | 43 | *max_files = 0; 44 | *files_used = 0; 45 | 46 | _return(ctx, 1); // PFS_ERR_NOPACK 47 | } 48 | 49 | extern "C" void osPfsRepairId_recomp(uint8_t * rdram, recomp_context * ctx) { 50 | _return(ctx, 1); // PFS_ERR_NOPACK 51 | } 52 | -------------------------------------------------------------------------------- /librecomp/src/pi.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "recomp.h" 8 | #include "librecomp/addresses.hpp" 9 | #include "librecomp/game.hpp" 10 | #include "librecomp/files.hpp" 11 | #include 12 | #include 13 | 14 | static std::vector rom; 15 | 16 | bool recomp::is_rom_loaded() { 17 | return !rom.empty(); 18 | } 19 | 20 | void recomp::set_rom_contents(std::vector&& new_rom) { 21 | rom = std::move(new_rom); 22 | } 23 | 24 | std::span recomp::get_rom() { 25 | return rom; 26 | } 27 | 28 | constexpr uint32_t k1_to_phys(uint32_t addr) { 29 | return addr & 0x1FFFFFFF; 30 | } 31 | 32 | constexpr uint32_t phys_to_k1(uint32_t addr) { 33 | return addr | 0xA0000000; 34 | } 35 | 36 | extern "C" void __osPiGetAccess_recomp(uint8_t* rdram, recomp_context* ctx) { 37 | } 38 | 39 | extern "C" void __osPiRelAccess_recomp(uint8_t* rdram, recomp_context* ctx) { 40 | } 41 | 42 | extern "C" void osCartRomInit_recomp(uint8_t* rdram, recomp_context* ctx) { 43 | OSPiHandle* handle = TO_PTR(OSPiHandle, recomp::cart_handle); 44 | handle->type = 0; // cart 45 | handle->baseAddress = phys_to_k1(recomp::rom_base); 46 | handle->domain = 0; 47 | 48 | ctx->r2 = (gpr)recomp::cart_handle; 49 | } 50 | 51 | extern "C" void osDriveRomInit_recomp(uint8_t * rdram, recomp_context * ctx) { 52 | OSPiHandle* handle = TO_PTR(OSPiHandle, recomp::drive_handle); 53 | handle->type = 1; // bulk 54 | handle->baseAddress = phys_to_k1(recomp::drive_base); 55 | handle->domain = 0; 56 | 57 | ctx->r2 = (gpr)recomp::drive_handle; 58 | } 59 | 60 | extern "C" void osCreatePiManager_recomp(uint8_t* rdram, recomp_context* ctx) { 61 | ; 62 | } 63 | 64 | void recomp::do_rom_read(uint8_t* rdram, gpr ram_address, uint32_t physical_addr, size_t num_bytes) { 65 | // TODO use word copies when possible 66 | 67 | // TODO handle misaligned DMA 68 | assert((physical_addr & 0x1) == 0 && "Only PI DMA from aligned ROM addresses is currently supported"); 69 | assert((ram_address & 0x7) == 0 && "Only PI DMA to aligned RDRAM addresses is currently supported"); 70 | assert((num_bytes & 0x1) == 0 && "Only PI DMA with aligned sizes is currently supported"); 71 | uint8_t* rom_addr = rom.data() + physical_addr - recomp::rom_base; 72 | for (size_t i = 0; i < num_bytes; i++) { 73 | MEM_B(i, ram_address) = *rom_addr; 74 | rom_addr++; 75 | } 76 | } 77 | 78 | void recomp::do_rom_pio(uint8_t* rdram, gpr ram_address, uint32_t physical_addr) { 79 | assert((physical_addr & 0x3) == 0 && "PIO not 4-byte aligned in device, currently unsupported"); 80 | assert((ram_address & 0x3) == 0 && "PIO not 4-byte aligned in RDRAM, currently unsupported"); 81 | uint8_t* rom_addr = rom.data() + physical_addr - recomp::rom_base; 82 | MEM_B(0, ram_address) = *rom_addr++; 83 | MEM_B(1, ram_address) = *rom_addr++; 84 | MEM_B(2, ram_address) = *rom_addr++; 85 | MEM_B(3, ram_address) = *rom_addr++; 86 | } 87 | 88 | struct { 89 | std::vector save_buffer; 90 | std::thread saving_thread; 91 | std::filesystem::path save_file_path; 92 | moodycamel::LightweightSemaphore write_sempahore; 93 | // Used to tell the saving thread that a file swap is pending. 94 | moodycamel::LightweightSemaphore swap_file_pending_sempahore; 95 | // Used to tell the consumer thread that the saving thread is ready for a file swap. 96 | moodycamel::LightweightSemaphore swap_file_ready_sempahore; 97 | std::mutex save_buffer_mutex; 98 | } save_context; 99 | 100 | const std::u8string save_folder = u8"saves"; 101 | 102 | extern std::filesystem::path config_path; 103 | 104 | std::filesystem::path ultramodern::get_save_file_path() { 105 | return save_context.save_file_path; 106 | } 107 | 108 | void set_save_file_path(const std::u8string& subfolder, const std::u8string& name) { 109 | std::filesystem::path save_folder_path = config_path / save_folder; 110 | if (!subfolder.empty()) { 111 | save_folder_path = save_folder_path / subfolder; 112 | } 113 | save_context.save_file_path = save_folder_path / (name + u8".bin"); 114 | } 115 | 116 | void update_save_file() { 117 | bool saving_failed = false; 118 | { 119 | std::ofstream save_file = recomp::open_output_file_with_backup(ultramodern::get_save_file_path(), std::ios_base::binary); 120 | 121 | if (save_file.good()) { 122 | std::lock_guard lock{ save_context.save_buffer_mutex }; 123 | save_file.write(save_context.save_buffer.data(), save_context.save_buffer.size()); 124 | } 125 | else { 126 | saving_failed = true; 127 | } 128 | } 129 | if (!saving_failed) { 130 | saving_failed = !recomp::finalize_output_file_with_backup(ultramodern::get_save_file_path()); 131 | } 132 | if (saving_failed) { 133 | 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."); 134 | } 135 | } 136 | 137 | extern std::atomic_bool exited; 138 | 139 | void saving_thread_func(RDRAM_ARG1) { 140 | while (!exited) { 141 | bool save_buffer_updated = false; 142 | // Repeatedly wait for a new action to be sent. 143 | constexpr int64_t wait_time_microseconds = 10000; 144 | constexpr int max_actions = 128; 145 | int num_actions = 0; 146 | 147 | // Wait up to the given timeout for a write to come in. Allow multiple writes to coalesce together into a single save. 148 | // Cap the number of coalesced writes to guarantee that the save buffer eventually gets written out to the file even if the game 149 | // is constantly sending writes. 150 | while (save_context.write_sempahore.wait(wait_time_microseconds) && num_actions < max_actions) { 151 | save_buffer_updated = true; 152 | num_actions++; 153 | } 154 | 155 | // If an action came through that affected the save file, save the updated contents. 156 | if (save_buffer_updated) { 157 | update_save_file(); 158 | } 159 | 160 | if (save_context.swap_file_pending_sempahore.tryWait()) { 161 | save_context.swap_file_ready_sempahore.signal(); 162 | } 163 | } 164 | } 165 | 166 | void save_write_ptr(const void* in, uint32_t offset, uint32_t count) { 167 | assert(offset + count <= save_context.save_buffer.size()); 168 | 169 | { 170 | std::lock_guard lock { save_context.save_buffer_mutex }; 171 | memcpy(&save_context.save_buffer[offset], in, count); 172 | } 173 | 174 | save_context.write_sempahore.signal(); 175 | } 176 | 177 | void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count) { 178 | assert(offset + count <= save_context.save_buffer.size()); 179 | 180 | { 181 | std::lock_guard lock { save_context.save_buffer_mutex }; 182 | for (gpr i = 0; i < count; i++) { 183 | save_context.save_buffer[offset + i] = MEM_B(i, rdram_address); 184 | } 185 | } 186 | 187 | save_context.write_sempahore.signal(); 188 | } 189 | 190 | void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count) { 191 | assert(offset + count <= save_context.save_buffer.size()); 192 | 193 | std::lock_guard lock { save_context.save_buffer_mutex }; 194 | for (gpr i = 0; i < count; i++) { 195 | MEM_B(i, rdram_address) = save_context.save_buffer[offset + i]; 196 | } 197 | } 198 | 199 | void save_clear(uint32_t start, uint32_t size, char value) { 200 | assert(start + size < save_context.save_buffer.size()); 201 | 202 | { 203 | std::lock_guard lock { save_context.save_buffer_mutex }; 204 | std::fill_n(save_context.save_buffer.begin() + start, size, value); 205 | } 206 | 207 | save_context.write_sempahore.signal(); 208 | } 209 | 210 | size_t get_save_size(recomp::SaveType save_type) { 211 | switch (save_type) { 212 | case recomp::SaveType::AllowAll: 213 | case recomp::SaveType::Flashram: 214 | return 0x20000; 215 | case recomp::SaveType::Sram: 216 | return 0x8000; 217 | case recomp::SaveType::Eep16k: 218 | return 0x800; 219 | case recomp::SaveType::Eep4k: 220 | return 0x200; 221 | case recomp::SaveType::None: 222 | return 0; 223 | } 224 | return 0; 225 | } 226 | 227 | void read_save_file() { 228 | std::filesystem::path save_file_path = ultramodern::get_save_file_path(); 229 | 230 | // Ensure the save file directory exists. 231 | std::filesystem::create_directories(save_file_path.parent_path()); 232 | 233 | // Read the save file if it exists. 234 | std::ifstream save_file = recomp::open_input_file_with_backup(save_file_path, std::ios_base::binary); 235 | if (save_file.good()) { 236 | save_file.read(save_context.save_buffer.data(), save_context.save_buffer.size()); 237 | } 238 | else { 239 | // Otherwise clear the save file to all zeroes. 240 | std::fill(save_context.save_buffer.begin(), save_context.save_buffer.end(), 0); 241 | } 242 | } 243 | 244 | void ultramodern::init_saving(RDRAM_ARG1) { 245 | set_save_file_path(u8"", recomp::current_game_id()); 246 | 247 | save_context.save_buffer.resize(get_save_size(recomp::get_save_type())); 248 | 249 | read_save_file(); 250 | 251 | save_context.saving_thread = std::thread{saving_thread_func, PASS_RDRAM}; 252 | } 253 | 254 | void ultramodern::change_save_file(const std::u8string& subfolder, const std::u8string& name) { 255 | // Tell the saving thread that a file swap is pending. 256 | save_context.swap_file_pending_sempahore.signal(); 257 | // Wait until the saving thread indicates it's ready to swap files. 258 | save_context.swap_file_ready_sempahore.wait(); 259 | // Perform the save file swap. 260 | set_save_file_path(subfolder, name); 261 | read_save_file(); 262 | } 263 | 264 | void ultramodern::join_saving_thread() { 265 | if (save_context.saving_thread.joinable()) { 266 | save_context.saving_thread.join(); 267 | } 268 | } 269 | 270 | void do_dma(RDRAM_ARG PTR(OSMesgQueue) mq, gpr rdram_address, uint32_t physical_addr, uint32_t size, uint32_t direction) { 271 | // TODO asynchronous transfer 272 | // TODO implement unaligned DMA correctly 273 | if (direction == 0) { 274 | if (physical_addr >= recomp::rom_base) { 275 | // read cart rom 276 | recomp::do_rom_read(rdram, rdram_address, physical_addr, size); 277 | 278 | // Send a message to the mq to indicate that the transfer completed 279 | osSendMesg(rdram, mq, 0, OS_MESG_NOBLOCK); 280 | } else if (physical_addr >= recomp::sram_base) { 281 | if (!recomp::sram_allowed()) { 282 | ultramodern::error_handling::message_box("Attempted to use SRAM saving with other save type"); 283 | ULTRAMODERN_QUICK_EXIT(); 284 | } 285 | // read sram 286 | save_read(rdram, rdram_address, physical_addr - recomp::sram_base, size); 287 | 288 | // Send a message to the mq to indicate that the transfer completed 289 | osSendMesg(rdram, mq, 0, OS_MESG_NOBLOCK); 290 | } else { 291 | fprintf(stderr, "[WARN] PI DMA read from unknown region, phys address 0x%08X\n", physical_addr); 292 | } 293 | } else { 294 | if (physical_addr >= recomp::rom_base) { 295 | // write cart rom 296 | throw std::runtime_error("ROM DMA write unimplemented"); 297 | } else if (physical_addr >= recomp::sram_base) { 298 | if (!recomp::sram_allowed()) { 299 | ultramodern::error_handling::message_box("Attempted to use SRAM saving with other save type"); 300 | ULTRAMODERN_QUICK_EXIT(); 301 | } 302 | // write sram 303 | save_write(rdram, rdram_address, physical_addr - recomp::sram_base, size); 304 | 305 | // Send a message to the mq to indicate that the transfer completed 306 | osSendMesg(rdram, mq, 0, OS_MESG_NOBLOCK); 307 | } else { 308 | fprintf(stderr, "[WARN] PI DMA write to unknown region, phys address 0x%08X\n", physical_addr); 309 | } 310 | } 311 | } 312 | 313 | extern "C" void osPiStartDma_recomp(RDRAM_ARG recomp_context* ctx) { 314 | uint32_t mb = ctx->r4; 315 | uint32_t pri = ctx->r5; 316 | uint32_t direction = ctx->r6; 317 | uint32_t devAddr = ctx->r7 | recomp::rom_base; 318 | gpr dramAddr = MEM_W(0x10, ctx->r29); 319 | uint32_t size = MEM_W(0x14, ctx->r29); 320 | PTR(OSMesgQueue) mq = MEM_W(0x18, ctx->r29); 321 | uint32_t physical_addr = k1_to_phys(devAddr); 322 | 323 | debug_printf("[pi] DMA from 0x%08X into 0x%08X of size 0x%08X\n", devAddr, dramAddr, size); 324 | 325 | do_dma(PASS_RDRAM mq, dramAddr, physical_addr, size, direction); 326 | 327 | ctx->r2 = 0; 328 | } 329 | 330 | extern "C" void osEPiStartDma_recomp(RDRAM_ARG recomp_context* ctx) { 331 | OSPiHandle* handle = TO_PTR(OSPiHandle, ctx->r4); 332 | OSIoMesg* mb = TO_PTR(OSIoMesg, ctx->r5); 333 | uint32_t direction = ctx->r6; 334 | uint32_t devAddr = handle->baseAddress | mb->devAddr; 335 | gpr dramAddr = mb->dramAddr; 336 | uint32_t size = mb->size; 337 | PTR(OSMesgQueue) mq = mb->hdr.retQueue; 338 | uint32_t physical_addr = k1_to_phys(devAddr); 339 | 340 | debug_printf("[pi] DMA from 0x%08X into 0x%08X of size 0x%08X\n", devAddr, dramAddr, size); 341 | 342 | do_dma(PASS_RDRAM mq, dramAddr, physical_addr, size, direction); 343 | 344 | ctx->r2 = 0; 345 | } 346 | 347 | extern "C" void osEPiReadIo_recomp(RDRAM_ARG recomp_context * ctx) { 348 | OSPiHandle* handle = TO_PTR(OSPiHandle, ctx->r4); 349 | uint32_t devAddr = handle->baseAddress | ctx->r5; 350 | gpr dramAddr = ctx->r6; 351 | uint32_t physical_addr = k1_to_phys(devAddr); 352 | 353 | if (physical_addr > recomp::rom_base) { 354 | // cart rom 355 | recomp::do_rom_pio(PASS_RDRAM dramAddr, physical_addr); 356 | } else { 357 | // sram 358 | assert(false && "SRAM ReadIo unimplemented"); 359 | } 360 | 361 | ctx->r2 = 0; 362 | } 363 | 364 | extern "C" void osPiGetStatus_recomp(RDRAM_ARG recomp_context * ctx) { 365 | ctx->r2 = 0; 366 | } 367 | 368 | extern "C" void osPiRawStartDma_recomp(RDRAM_ARG recomp_context * ctx) { 369 | ultramodern::error_handling::message_box( 370 | "Stub `osPiRawStartDma_recomp` function called!\n" 371 | "Most games do not call this function directly, which means the libultra function\n" 372 | "that uses this function was not properly named.\n" 373 | "\n" 374 | "If you triggered this message, please make sure you have properly identified\n" 375 | "every libultra function on your recompiled game. If you are sure every libultra\n" 376 | "function has been identified and you still get this problem then open an issue on\n" 377 | "the N64ModernRuntime Github repository mentioning the game you are trying to\n" 378 | "recompile and steps to reproduce the issue.\n" 379 | "\n" 380 | "The application will close now, bye and good luck!" 381 | ); 382 | ULTRAMODERN_QUICK_EXIT(); 383 | } 384 | 385 | extern "C" void osEPiRawStartDma_recomp(RDRAM_ARG recomp_context * ctx) { 386 | ultramodern::error_handling::message_box( 387 | "Stub `osEPiRawStartDma_recomp` function called!\n" 388 | "Most games do not call this function directly, which means the libultra function\n" 389 | "that uses this function was not properly named.\n" 390 | "\n" 391 | "If you triggered this message, please make sure you have properly identified\n" 392 | "every libultra function on your recompiled game. If you are sure every libultra\n" 393 | "function has been identified and you still get this problem then open an issue on\n" 394 | "the N64ModernRuntime Github repository mentioning the game you are trying to\n" 395 | "recompile and steps to reproduce the issue.\n" 396 | "\n" 397 | "The application will close now, bye and good luck!" 398 | ); 399 | ULTRAMODERN_QUICK_EXIT(); 400 | } 401 | -------------------------------------------------------------------------------- /librecomp/src/print.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include "recomp.h" 6 | #include "euc-jp.hpp" 7 | 8 | extern "C" void __checkHardware_msp_recomp(uint8_t * rdram, recomp_context * ctx) { 9 | ctx->r2 = 0; 10 | } 11 | 12 | extern "C" void __checkHardware_kmc_recomp(uint8_t * rdram, recomp_context * ctx) { 13 | ctx->r2 = 0; 14 | } 15 | 16 | extern "C" void __checkHardware_isv_recomp(uint8_t * rdram, recomp_context * ctx) { 17 | ctx->r2 = 0; 18 | } 19 | 20 | extern "C" void __osInitialize_msp_recomp(uint8_t * rdram, recomp_context * ctx) { 21 | } 22 | 23 | extern "C" void __osInitialize_kmc_recomp(uint8_t * rdram, recomp_context * ctx) { 24 | } 25 | 26 | extern "C" void __osInitialize_isv_recomp(uint8_t * rdram, recomp_context * ctx) { 27 | } 28 | 29 | extern "C" void isPrintfInit_recomp(uint8_t * rdram, recomp_context * ctx) { 30 | } 31 | 32 | extern "C" void __osRdbSend_recomp(uint8_t * rdram, recomp_context * ctx) { 33 | gpr buf = ctx->r4; 34 | size_t size = ctx->r5; 35 | u32 type = (u32)ctx->r6; 36 | std::unique_ptr to_print = std::make_unique(size + 1); 37 | 38 | for (size_t i = 0; i < size; i++) { 39 | to_print[i] = MEM_B(i, buf); 40 | } 41 | to_print[size] = '\x00'; 42 | 43 | fwrite(to_print.get(), 1, size, stdout); 44 | 45 | ctx->r2 = size; 46 | } 47 | 48 | extern "C" void is_proutSyncPrintf_recomp(uint8_t * rdram, recomp_context * ctx) { 49 | // Buffering to speed up print performance 50 | static std::vector print_buffer; 51 | 52 | gpr buf = ctx->r5; 53 | size_t size = ctx->r6; 54 | 55 | //for (size_t i = 0; i < size; i++) { 56 | // // Add the new character to the buffer 57 | // char cur_char = MEM_B(i, buf); 58 | 59 | // // If the new character is a newline, flush the buffer 60 | // if (cur_char == '\n') { 61 | // std::string utf8_str = Encoding::decode_eucjp(std::string_view{ print_buffer.data(), print_buffer.size() }); 62 | // puts(utf8_str.c_str()); 63 | // print_buffer.clear(); 64 | // } else { 65 | // print_buffer.push_back(cur_char); 66 | // } 67 | //} 68 | 69 | //fwrite(to_print.get(), size, 1, stdout); 70 | 71 | ctx->r2 = 1; 72 | } 73 | -------------------------------------------------------------------------------- /librecomp/src/rsp.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "rsp.hpp" 6 | 7 | static recomp::rsp::callbacks_t rsp_callbacks {}; 8 | 9 | void recomp::rsp::set_callbacks(const callbacks_t& callbacks) { 10 | rsp_callbacks = callbacks; 11 | } 12 | 13 | uint8_t dmem[0x1000]; 14 | uint16_t rspReciprocals[512]; 15 | uint16_t rspInverseSquareRoots[512]; 16 | 17 | // From Ares emulator. For license details, see rsp_vu.h 18 | void recomp::rsp::constants_init() { 19 | rspReciprocals[0] = u16(~0); 20 | for (u16 index = 1; index < 512; index++) { 21 | u64 a = index + 512; 22 | u64 b = (u64(1) << 34) / a; 23 | rspReciprocals[index] = u16((b + 1) >> 8); 24 | } 25 | 26 | for (u16 index = 0; index < 512; index++) { 27 | u64 a = (index + 512) >> ((index % 2 == 1) ? 1 : 0); 28 | u64 b = 1 << 17; 29 | //find the largest b where b < 1.0 / sqrt(a) 30 | while (a * (b + 1) * (b + 1) < (u64(1) << 44)) b++; 31 | rspInverseSquareRoots[index] = u16(b >> 1); 32 | } 33 | } 34 | 35 | // Runs a recompiled RSP microcode 36 | bool recomp::rsp::run_task(uint8_t* rdram, const OSTask* task) { 37 | assert(rsp_callbacks.get_rsp_microcode != nullptr); 38 | RspUcodeFunc* ucode_func = rsp_callbacks.get_rsp_microcode(task); 39 | 40 | if (ucode_func == nullptr) { 41 | fprintf(stderr, "No registered RSP ucode for %" PRIu32 " (returned `nullptr`)\n", task->t.type); 42 | return false; 43 | } 44 | 45 | // Load the OSTask into DMEM 46 | memcpy(&dmem[0xFC0], task, sizeof(OSTask)); 47 | 48 | // Load the ucode data into DMEM 49 | dma_rdram_to_dmem(rdram, 0x0000, task->t.ucode_data, 0xF80 - 1); 50 | 51 | // Run the ucode 52 | RspExitReason exit_reason = ucode_func(rdram, task->t.ucode); 53 | 54 | // Ensure that the ucode exited correctly 55 | if (exit_reason != RspExitReason::Broke) { 56 | fprintf(stderr, "RSP ucode %" PRIu32 " exited unexpectedly. exit_reason: %i\n", task->t.type, static_cast(exit_reason)); 57 | assert(exit_reason == RspExitReason::Broke); 58 | return false; 59 | } 60 | 61 | return true; 62 | } 63 | -------------------------------------------------------------------------------- /librecomp/src/sp.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "recomp.h" 5 | 6 | extern "C" void osSpTaskLoad_recomp(uint8_t* rdram, recomp_context* ctx) { 7 | // Nothing to do here 8 | } 9 | 10 | bool dump_frame = false; 11 | 12 | extern "C" void osSpTaskStartGo_recomp(uint8_t* rdram, recomp_context* ctx) { 13 | //printf("[sp] osSpTaskStartGo(0x%08X)\n", (uint32_t)ctx->r4); 14 | OSTask* task = TO_PTR(OSTask, ctx->r4); 15 | if (task->t.type == M_GFXTASK) { 16 | //printf("[sp] Gfx task: %08X\n", (uint32_t)ctx->r4); 17 | } else if (task->t.type == M_AUDTASK) { 18 | //printf("[sp] Audio task: %08X\n", (uint32_t)ctx->r4); 19 | } 20 | // For debugging 21 | if (dump_frame) { 22 | char addr_str[32]; 23 | constexpr size_t ram_size = 0x800000; 24 | std::unique_ptr ram_unswapped = std::make_unique(ram_size); 25 | snprintf(addr_str, sizeof(addr_str) - 1, "%08X", task->t.data_ptr); 26 | addr_str[sizeof(addr_str) - 1] = '\0'; 27 | std::ofstream dump_file{ "ramdump" + std::string{ addr_str } + ".bin", std::ios::binary}; 28 | 29 | for (size_t i = 0; i < ram_size; i++) { 30 | ram_unswapped[i] = rdram[i ^ 3]; 31 | } 32 | 33 | dump_file.write(ram_unswapped.get(), ram_size); 34 | dump_frame = false; 35 | } 36 | ultramodern::submit_rsp_task(rdram, ctx->r4); 37 | } 38 | 39 | extern "C" void osSpTaskYield_recomp(uint8_t* rdram, recomp_context* ctx) { 40 | // Ignore yield requests (acts as if the task completed before it received the yield request) 41 | } 42 | 43 | extern "C" void osSpTaskYielded_recomp(uint8_t* rdram, recomp_context* ctx) { 44 | // Task yield requests are ignored, so always return 0 as tasks will never be yielded 45 | ctx->r2 = 0; 46 | } 47 | 48 | extern "C" void __osSpSetPc_recomp(uint8_t* rdram, recomp_context* ctx) { 49 | assert(false); 50 | } 51 | -------------------------------------------------------------------------------- /librecomp/src/ultra_stubs.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "recomp.h" 4 | 5 | // None of these functions need to be reimplemented, so stub them out 6 | extern "C" void osUnmapTLBAll_recomp(uint8_t * rdram, recomp_context * ctx) { 7 | // TODO this will need to be implemented in the future for any games that actually use the TLB 8 | } 9 | 10 | extern "C" void osVoiceInit_recomp(uint8_t * rdram, recomp_context * ctx) { 11 | ctx->r2 = 11; // CONT_ERR_DEVICE 12 | } 13 | 14 | extern "C" void osVoiceSetWord_recomp(uint8_t * rdram, recomp_context * ctx) { 15 | assert(false); 16 | } 17 | 18 | extern "C" void osVoiceCheckWord_recomp(uint8_t * rdram, recomp_context * ctx) { 19 | assert(false); 20 | } 21 | 22 | extern "C" void osVoiceStopReadData_recomp(uint8_t * rdram, recomp_context * ctx) { 23 | assert(false); 24 | } 25 | 26 | extern "C" void osVoiceMaskDictionary_recomp(uint8_t * rdram, recomp_context * ctx) { 27 | assert(false); 28 | } 29 | 30 | extern "C" void osVoiceStartReadData_recomp(uint8_t * rdram, recomp_context * ctx) { 31 | assert(false); 32 | } 33 | 34 | extern "C" void osVoiceControlGain_recomp(uint8_t * rdram, recomp_context * ctx) { 35 | assert(false); 36 | } 37 | 38 | extern "C" void osVoiceGetReadData_recomp(uint8_t * rdram, recomp_context * ctx) { 39 | assert(false); 40 | } 41 | 42 | extern "C" void osVoiceClearDictionary_recomp(uint8_t * rdram, recomp_context * ctx) { 43 | assert(false); 44 | } 45 | 46 | -------------------------------------------------------------------------------- /librecomp/src/ultra_translation.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "recomp.h" 5 | 6 | extern "C" void osInitialize_recomp(uint8_t * rdram, recomp_context * ctx) { 7 | osInitialize(); 8 | } 9 | 10 | extern "C" void __osInitialize_common_recomp(uint8_t * rdram, recomp_context * ctx) { 11 | osInitialize(); 12 | } 13 | 14 | extern "C" void osCreateThread_recomp(uint8_t* rdram, recomp_context* ctx) { 15 | osCreateThread(rdram, (int32_t)ctx->r4, (OSId)ctx->r5, (int32_t)ctx->r6, (int32_t)ctx->r7, 16 | (int32_t)MEM_W(0x10, ctx->r29), (OSPri)MEM_W(0x14, ctx->r29)); 17 | } 18 | 19 | extern "C" void osStartThread_recomp(uint8_t* rdram, recomp_context* ctx) { 20 | osStartThread(rdram, (int32_t)ctx->r4); 21 | } 22 | 23 | extern "C" void osStopThread_recomp(uint8_t * rdram, recomp_context * ctx) { 24 | osStopThread(rdram, (int32_t)ctx->r4); 25 | } 26 | 27 | extern "C" void osDestroyThread_recomp(uint8_t * rdram, recomp_context * ctx) { 28 | osDestroyThread(rdram, (int32_t)ctx->r4); 29 | } 30 | 31 | extern "C" void osYieldThread_recomp(uint8_t * rdram, recomp_context * ctx) { 32 | assert(false); 33 | // osYieldThread(rdram); 34 | } 35 | 36 | extern "C" void osSetThreadPri_recomp(uint8_t* rdram, recomp_context* ctx) { 37 | osSetThreadPri(rdram, (int32_t)ctx->r4, (OSPri)ctx->r5); 38 | } 39 | 40 | extern "C" void osGetThreadPri_recomp(uint8_t * rdram, recomp_context * ctx) { 41 | ctx->r2 = osGetThreadPri(rdram, (int32_t)ctx->r4); 42 | } 43 | 44 | extern "C" void osGetThreadId_recomp(uint8_t * rdram, recomp_context * ctx) { 45 | ctx->r2 = osGetThreadId(rdram, (int32_t)ctx->r4); 46 | } 47 | 48 | extern "C" void osCreateMesgQueue_recomp(uint8_t* rdram, recomp_context* ctx) { 49 | osCreateMesgQueue(rdram, (int32_t)ctx->r4, (int32_t)ctx->r5, (s32)ctx->r6); 50 | } 51 | 52 | extern "C" void osRecvMesg_recomp(uint8_t* rdram, recomp_context* ctx) { 53 | ctx->r2 = osRecvMesg(rdram, (int32_t)ctx->r4, (int32_t)ctx->r5, (s32)ctx->r6); 54 | } 55 | 56 | extern "C" void osSendMesg_recomp(uint8_t* rdram, recomp_context* ctx) { 57 | ctx->r2 = osSendMesg(rdram, (int32_t)ctx->r4, (OSMesg)ctx->r5, (s32)ctx->r6); 58 | } 59 | 60 | extern "C" void osJamMesg_recomp(uint8_t* rdram, recomp_context* ctx) { 61 | ctx->r2 = osJamMesg(rdram, (int32_t)ctx->r4, (OSMesg)ctx->r5, (s32)ctx->r6); 62 | } 63 | 64 | extern "C" void osSetEventMesg_recomp(uint8_t* rdram, recomp_context* ctx) { 65 | osSetEventMesg(rdram, (OSEvent)ctx->r4, (int32_t)ctx->r5, (OSMesg)ctx->r6); 66 | } 67 | 68 | extern "C" void osViSetEvent_recomp(uint8_t * rdram, recomp_context * ctx) { 69 | osViSetEvent(rdram, (int32_t)ctx->r4, (OSMesg)ctx->r5, (u32)ctx->r6); 70 | } 71 | 72 | extern "C" void osGetCount_recomp(uint8_t * rdram, recomp_context * ctx) { 73 | ctx->r2 = osGetCount(); 74 | } 75 | 76 | extern "C" void osGetTime_recomp(uint8_t * rdram, recomp_context * ctx) { 77 | uint64_t total_count = osGetTime(); 78 | ctx->r2 = (int32_t)(total_count >> 32); 79 | ctx->r3 = (int32_t)(total_count >> 0); 80 | } 81 | 82 | extern "C" void osSetTimer_recomp(uint8_t * rdram, recomp_context * ctx) { 83 | uint64_t countdown = ((uint64_t)(ctx->r6) << 32) | ((ctx->r7) & 0xFFFFFFFFu); 84 | uint64_t interval = load_doubleword(rdram, ctx->r29, 0x10); 85 | ctx->r2 = osSetTimer(rdram, (int32_t)ctx->r4, countdown, interval, (int32_t)MEM_W(0x18, ctx->r29), (OSMesg)MEM_W(0x1C, ctx->r29)); 86 | } 87 | 88 | extern "C" void osStopTimer_recomp(uint8_t * rdram, recomp_context * ctx) { 89 | ctx->r2 = osStopTimer(rdram, (int32_t)ctx->r4); 90 | } 91 | 92 | extern "C" void osVirtualToPhysical_recomp(uint8_t * rdram, recomp_context * ctx) { 93 | ctx->r2 = osVirtualToPhysical((int32_t)ctx->r4); 94 | } 95 | 96 | extern "C" void osInvalDCache_recomp(uint8_t * rdram, recomp_context * ctx) { 97 | ; 98 | } 99 | 100 | extern "C" void osInvalICache_recomp(uint8_t * rdram, recomp_context * ctx) { 101 | ; 102 | } 103 | 104 | extern "C" void osWritebackDCache_recomp(uint8_t * rdram, recomp_context * ctx) { 105 | ; 106 | } 107 | 108 | extern "C" void osWritebackDCacheAll_recomp(uint8_t * rdram, recomp_context * ctx) { 109 | ; 110 | } 111 | 112 | extern "C" void osSetIntMask_recomp(uint8_t * rdram, recomp_context * ctx) { 113 | ; 114 | } 115 | 116 | extern "C" void __osDisableInt_recomp(uint8_t * rdram, recomp_context * ctx) { 117 | ; 118 | } 119 | 120 | extern "C" void __osRestoreInt_recomp(uint8_t * rdram, recomp_context * ctx) { 121 | ; 122 | } 123 | 124 | extern "C" void __osSetFpcCsr_recomp(uint8_t * rdram, recomp_context * ctx) { 125 | ctx->r2 = 0; 126 | } 127 | 128 | // For the Mario Party games (not working) 129 | //extern "C" void longjmp_recomp(uint8_t * rdram, recomp_context * ctx) { 130 | // RecompJmpBuf* buf = TO_PTR(RecompJmpBuf, ctx->r4); 131 | // 132 | // // Check if this is a buffer that was set up with setjmp 133 | // if (buf->magic == SETJMP_MAGIC) { 134 | // // If so, longjmp to it 135 | // // Setjmp/longjmp does not work across threads, so verify that this buffer was made by this thread 136 | // assert(buf->owner == ultramodern::this_thread()); 137 | // longjmp(buf->storage->buffer, ctx->r5); 138 | // } else { 139 | // // Otherwise, check if it was one built manually by the game with $ra pointing to a function 140 | // gpr sp = MEM_W(0, ctx->r4); 141 | // gpr ra = MEM_W(4, ctx->r4); 142 | // ctx->r29 = sp; 143 | // recomp_func_t* target = LOOKUP_FUNC(ra); 144 | // if (target == nullptr) { 145 | // fprintf(stderr, "Failed to find function for manual longjmp\n"); 146 | // std::quick_exit(EXIT_FAILURE); 147 | // } 148 | // target(rdram, ctx); 149 | // 150 | // // TODO kill this thread if the target function returns 151 | // assert(false); 152 | // } 153 | //} 154 | // 155 | //#undef setjmp_recomp 156 | //extern "C" void setjmp_recomp(uint8_t * rdram, recomp_context * ctx) { 157 | // fprintf(stderr, "Program called setjmp_recomp\n"); 158 | // std::quick_exit(EXIT_FAILURE); 159 | //} 160 | // 161 | //extern "C" int32_t osGetThreadEx(void) { 162 | // return ultramodern::this_thread(); 163 | //} 164 | -------------------------------------------------------------------------------- /librecomp/src/vi.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "recomp.h" 3 | #include "helpers.hpp" 4 | 5 | extern "C" void osViSetYScale_recomp(uint8_t* rdram, recomp_context * ctx) { 6 | osViSetYScale(ctx->f12.fl); 7 | } 8 | 9 | extern "C" void osViSetXScale_recomp(uint8_t* rdram, recomp_context * ctx) { 10 | osViSetXScale(ctx->f12.fl); 11 | } 12 | 13 | extern "C" void osCreateViManager_recomp(uint8_t* rdram, recomp_context* ctx) { 14 | ; 15 | } 16 | 17 | extern "C" void osViBlack_recomp(uint8_t* rdram, recomp_context* ctx) { 18 | osViBlack((uint32_t)ctx->r4); 19 | } 20 | 21 | extern "C" void osViRepeatLine_recomp(uint8_t* rdram, recomp_context* ctx) { 22 | osViRepeatLine(_arg<0, u8>(rdram, ctx)); 23 | } 24 | 25 | extern "C" void osViSetSpecialFeatures_recomp(uint8_t* rdram, recomp_context* ctx) { 26 | osViSetSpecialFeatures((uint32_t)ctx->r4); 27 | } 28 | 29 | extern "C" void osViGetCurrentFramebuffer_recomp(uint8_t* rdram, recomp_context* ctx) { 30 | ctx->r2 = (gpr)(int32_t)osViGetCurrentFramebuffer(); 31 | } 32 | 33 | extern "C" void osViGetNextFramebuffer_recomp(uint8_t* rdram, recomp_context* ctx) { 34 | ctx->r2 = (gpr)(int32_t)osViGetNextFramebuffer(); 35 | } 36 | 37 | extern "C" void osViSwapBuffer_recomp(uint8_t* rdram, recomp_context* ctx) { 38 | osViSwapBuffer(rdram, (int32_t)ctx->r4); 39 | } 40 | 41 | extern "C" void osViSetMode_recomp(uint8_t* rdram, recomp_context* ctx) { 42 | osViSetMode(rdram, (int32_t)ctx->r4); 43 | } 44 | 45 | extern uint64_t total_vis; 46 | 47 | extern "C" void wait_one_frame(uint8_t* rdram, recomp_context* ctx) { 48 | uint64_t cur_vis = total_vis; 49 | while (cur_vis == total_vis) { 50 | std::this_thread::yield(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /thirdparty/concurrentqueue/lightweightsemaphore.h: -------------------------------------------------------------------------------- 1 | // Provides an efficient implementation of a semaphore (LightweightSemaphore). 2 | // This is an extension of Jeff Preshing's sempahore implementation (licensed 3 | // under the terms of its separate zlib license) that has been adapted and 4 | // extended by Cameron Desrochers. 5 | 6 | #pragma once 7 | 8 | #include // For std::size_t 9 | #include 10 | #include // For std::make_signed 11 | 12 | #if defined(_WIN32) 13 | // Avoid including windows.h in a header; we only need a handful of 14 | // items, so we'll redeclare them here (this is relatively safe since 15 | // the API generally has to remain stable between Windows versions). 16 | // I know this is an ugly hack but it still beats polluting the global 17 | // namespace with thousands of generic names or adding a .cpp for nothing. 18 | extern "C" { 19 | struct _SECURITY_ATTRIBUTES; 20 | __declspec(dllimport) void* __stdcall CreateSemaphoreW(_SECURITY_ATTRIBUTES* lpSemaphoreAttributes, long lInitialCount, long lMaximumCount, const wchar_t* lpName); 21 | __declspec(dllimport) int __stdcall CloseHandle(void* hObject); 22 | __declspec(dllimport) unsigned long __stdcall WaitForSingleObject(void* hHandle, unsigned long dwMilliseconds); 23 | __declspec(dllimport) int __stdcall ReleaseSemaphore(void* hSemaphore, long lReleaseCount, long* lpPreviousCount); 24 | } 25 | #elif defined(__MACH__) 26 | #include 27 | #elif defined(__unix__) 28 | #include 29 | 30 | #if defined(__GLIBC_PREREQ) && defined(_GNU_SOURCE) 31 | #if __GLIBC_PREREQ(2,30) 32 | #define MOODYCAMEL_LIGHTWEIGHTSEMAPHORE_MONOTONIC 33 | #endif 34 | #endif 35 | #endif 36 | 37 | namespace moodycamel 38 | { 39 | namespace details 40 | { 41 | 42 | // Code in the mpmc_sema namespace below is an adaptation of Jeff Preshing's 43 | // portable + lightweight semaphore implementations, originally from 44 | // https://github.com/preshing/cpp11-on-multicore/blob/master/common/sema.h 45 | // LICENSE: 46 | // Copyright (c) 2015 Jeff Preshing 47 | // 48 | // This software is provided 'as-is', without any express or implied 49 | // warranty. In no event will the authors be held liable for any damages 50 | // arising from the use of this software. 51 | // 52 | // Permission is granted to anyone to use this software for any purpose, 53 | // including commercial applications, and to alter it and redistribute it 54 | // freely, subject to the following restrictions: 55 | // 56 | // 1. The origin of this software must not be misrepresented; you must not 57 | // claim that you wrote the original software. If you use this software 58 | // in a product, an acknowledgement in the product documentation would be 59 | // appreciated but is not required. 60 | // 2. Altered source versions must be plainly marked as such, and must not be 61 | // misrepresented as being the original software. 62 | // 3. This notice may not be removed or altered from any source distribution. 63 | #if defined(_WIN32) 64 | class Semaphore 65 | { 66 | private: 67 | void* m_hSema; 68 | 69 | Semaphore(const Semaphore& other) MOODYCAMEL_DELETE_FUNCTION; 70 | Semaphore& operator=(const Semaphore& other) MOODYCAMEL_DELETE_FUNCTION; 71 | 72 | public: 73 | Semaphore(int initialCount = 0) 74 | { 75 | assert(initialCount >= 0); 76 | const long maxLong = 0x7fffffff; 77 | m_hSema = CreateSemaphoreW(nullptr, initialCount, maxLong, nullptr); 78 | assert(m_hSema); 79 | } 80 | 81 | ~Semaphore() 82 | { 83 | CloseHandle(m_hSema); 84 | } 85 | 86 | bool wait() 87 | { 88 | const unsigned long infinite = 0xffffffff; 89 | return WaitForSingleObject(m_hSema, infinite) == 0; 90 | } 91 | 92 | bool try_wait() 93 | { 94 | return WaitForSingleObject(m_hSema, 0) == 0; 95 | } 96 | 97 | bool timed_wait(std::uint64_t usecs) 98 | { 99 | return WaitForSingleObject(m_hSema, (unsigned long)(usecs / 1000)) == 0; 100 | } 101 | 102 | void signal(int count = 1) 103 | { 104 | while (!ReleaseSemaphore(m_hSema, count, nullptr)); 105 | } 106 | }; 107 | #elif defined(__MACH__) 108 | //--------------------------------------------------------- 109 | // Semaphore (Apple iOS and OSX) 110 | // Can't use POSIX semaphores due to http://lists.apple.com/archives/darwin-kernel/2009/Apr/msg00010.html 111 | //--------------------------------------------------------- 112 | class Semaphore 113 | { 114 | private: 115 | semaphore_t m_sema; 116 | 117 | Semaphore(const Semaphore& other) MOODYCAMEL_DELETE_FUNCTION; 118 | Semaphore& operator=(const Semaphore& other) MOODYCAMEL_DELETE_FUNCTION; 119 | 120 | public: 121 | Semaphore(int initialCount = 0) 122 | { 123 | assert(initialCount >= 0); 124 | kern_return_t rc = semaphore_create(mach_task_self(), &m_sema, SYNC_POLICY_FIFO, initialCount); 125 | assert(rc == KERN_SUCCESS); 126 | (void)rc; 127 | } 128 | 129 | ~Semaphore() 130 | { 131 | semaphore_destroy(mach_task_self(), m_sema); 132 | } 133 | 134 | bool wait() 135 | { 136 | return semaphore_wait(m_sema) == KERN_SUCCESS; 137 | } 138 | 139 | bool try_wait() 140 | { 141 | return timed_wait(0); 142 | } 143 | 144 | bool timed_wait(std::uint64_t timeout_usecs) 145 | { 146 | mach_timespec_t ts; 147 | ts.tv_sec = static_cast(timeout_usecs / 1000000); 148 | ts.tv_nsec = static_cast((timeout_usecs % 1000000) * 1000); 149 | 150 | // added in OSX 10.10: https://developer.apple.com/library/prerelease/mac/documentation/General/Reference/APIDiffsMacOSX10_10SeedDiff/modules/Darwin.html 151 | kern_return_t rc = semaphore_timedwait(m_sema, ts); 152 | return rc == KERN_SUCCESS; 153 | } 154 | 155 | void signal() 156 | { 157 | while (semaphore_signal(m_sema) != KERN_SUCCESS); 158 | } 159 | 160 | void signal(int count) 161 | { 162 | while (count-- > 0) 163 | { 164 | while (semaphore_signal(m_sema) != KERN_SUCCESS); 165 | } 166 | } 167 | }; 168 | #elif defined(__unix__) 169 | //--------------------------------------------------------- 170 | // Semaphore (POSIX, Linux) 171 | //--------------------------------------------------------- 172 | class Semaphore 173 | { 174 | private: 175 | sem_t m_sema; 176 | 177 | Semaphore(const Semaphore& other) MOODYCAMEL_DELETE_FUNCTION; 178 | Semaphore& operator=(const Semaphore& other) MOODYCAMEL_DELETE_FUNCTION; 179 | 180 | public: 181 | Semaphore(int initialCount = 0) 182 | { 183 | assert(initialCount >= 0); 184 | int rc = sem_init(&m_sema, 0, static_cast(initialCount)); 185 | assert(rc == 0); 186 | (void)rc; 187 | } 188 | 189 | ~Semaphore() 190 | { 191 | sem_destroy(&m_sema); 192 | } 193 | 194 | bool wait() 195 | { 196 | // http://stackoverflow.com/questions/2013181/gdb-causes-sem-wait-to-fail-with-eintr-error 197 | int rc; 198 | do { 199 | rc = sem_wait(&m_sema); 200 | } while (rc == -1 && errno == EINTR); 201 | return rc == 0; 202 | } 203 | 204 | bool try_wait() 205 | { 206 | int rc; 207 | do { 208 | rc = sem_trywait(&m_sema); 209 | } while (rc == -1 && errno == EINTR); 210 | return rc == 0; 211 | } 212 | 213 | bool timed_wait(std::uint64_t usecs) 214 | { 215 | struct timespec ts; 216 | const int usecs_in_1_sec = 1000000; 217 | const int nsecs_in_1_sec = 1000000000; 218 | #ifdef MOODYCAMEL_LIGHTWEIGHTSEMAPHORE_MONOTONIC 219 | clock_gettime(CLOCK_MONOTONIC, &ts); 220 | #else 221 | clock_gettime(CLOCK_REALTIME, &ts); 222 | #endif 223 | ts.tv_sec += (time_t)(usecs / usecs_in_1_sec); 224 | ts.tv_nsec += (long)(usecs % usecs_in_1_sec) * 1000; 225 | // sem_timedwait bombs if you have more than 1e9 in tv_nsec 226 | // so we have to clean things up before passing it in 227 | if (ts.tv_nsec >= nsecs_in_1_sec) { 228 | ts.tv_nsec -= nsecs_in_1_sec; 229 | ++ts.tv_sec; 230 | } 231 | 232 | int rc; 233 | do { 234 | #ifdef MOODYCAMEL_LIGHTWEIGHTSEMAPHORE_MONOTONIC 235 | rc = sem_clockwait(&m_sema, CLOCK_MONOTONIC, &ts); 236 | #else 237 | rc = sem_timedwait(&m_sema, &ts); 238 | #endif 239 | } while (rc == -1 && errno == EINTR); 240 | return rc == 0; 241 | } 242 | 243 | void signal() 244 | { 245 | while (sem_post(&m_sema) == -1); 246 | } 247 | 248 | void signal(int count) 249 | { 250 | while (count-- > 0) 251 | { 252 | while (sem_post(&m_sema) == -1); 253 | } 254 | } 255 | }; 256 | #else 257 | #error Unsupported platform! (No semaphore wrapper available) 258 | #endif 259 | 260 | } // end namespace details 261 | 262 | 263 | //--------------------------------------------------------- 264 | // LightweightSemaphore 265 | //--------------------------------------------------------- 266 | class LightweightSemaphore 267 | { 268 | public: 269 | typedef std::make_signed::type ssize_t; 270 | 271 | private: 272 | std::atomic m_count; 273 | details::Semaphore m_sema; 274 | int m_maxSpins; 275 | 276 | bool waitWithPartialSpinning(std::int64_t timeout_usecs = -1) 277 | { 278 | ssize_t oldCount; 279 | int spin = m_maxSpins; 280 | while (--spin >= 0) 281 | { 282 | oldCount = m_count.load(std::memory_order_relaxed); 283 | if ((oldCount > 0) && m_count.compare_exchange_strong(oldCount, oldCount - 1, std::memory_order_acquire, std::memory_order_relaxed)) 284 | return true; 285 | std::atomic_signal_fence(std::memory_order_acquire); // Prevent the compiler from collapsing the loop. 286 | } 287 | oldCount = m_count.fetch_sub(1, std::memory_order_acquire); 288 | if (oldCount > 0) 289 | return true; 290 | if (timeout_usecs < 0) 291 | { 292 | if (m_sema.wait()) 293 | return true; 294 | } 295 | if (timeout_usecs > 0 && m_sema.timed_wait((std::uint64_t)timeout_usecs)) 296 | return true; 297 | // At this point, we've timed out waiting for the semaphore, but the 298 | // count is still decremented indicating we may still be waiting on 299 | // it. So we have to re-adjust the count, but only if the semaphore 300 | // wasn't signaled enough times for us too since then. If it was, we 301 | // need to release the semaphore too. 302 | while (true) 303 | { 304 | oldCount = m_count.load(std::memory_order_acquire); 305 | if (oldCount >= 0 && m_sema.try_wait()) 306 | return true; 307 | if (oldCount < 0 && m_count.compare_exchange_strong(oldCount, oldCount + 1, std::memory_order_relaxed, std::memory_order_relaxed)) 308 | return false; 309 | } 310 | } 311 | 312 | ssize_t waitManyWithPartialSpinning(ssize_t max, std::int64_t timeout_usecs = -1) 313 | { 314 | assert(max > 0); 315 | ssize_t oldCount; 316 | int spin = m_maxSpins; 317 | while (--spin >= 0) 318 | { 319 | oldCount = m_count.load(std::memory_order_relaxed); 320 | if (oldCount > 0) 321 | { 322 | ssize_t newCount = oldCount > max ? oldCount - max : 0; 323 | if (m_count.compare_exchange_strong(oldCount, newCount, std::memory_order_acquire, std::memory_order_relaxed)) 324 | return oldCount - newCount; 325 | } 326 | std::atomic_signal_fence(std::memory_order_acquire); 327 | } 328 | oldCount = m_count.fetch_sub(1, std::memory_order_acquire); 329 | if (oldCount <= 0) 330 | { 331 | if ((timeout_usecs == 0) || (timeout_usecs < 0 && !m_sema.wait()) || (timeout_usecs > 0 && !m_sema.timed_wait((std::uint64_t)timeout_usecs))) 332 | { 333 | while (true) 334 | { 335 | oldCount = m_count.load(std::memory_order_acquire); 336 | if (oldCount >= 0 && m_sema.try_wait()) 337 | break; 338 | if (oldCount < 0 && m_count.compare_exchange_strong(oldCount, oldCount + 1, std::memory_order_relaxed, std::memory_order_relaxed)) 339 | return 0; 340 | } 341 | } 342 | } 343 | if (max > 1) 344 | return 1 + tryWaitMany(max - 1); 345 | return 1; 346 | } 347 | 348 | public: 349 | LightweightSemaphore(ssize_t initialCount = 0, int maxSpins = 10000) : m_count(initialCount), m_maxSpins(maxSpins) 350 | { 351 | assert(initialCount >= 0); 352 | assert(maxSpins >= 0); 353 | } 354 | 355 | bool tryWait() 356 | { 357 | ssize_t oldCount = m_count.load(std::memory_order_relaxed); 358 | while (oldCount > 0) 359 | { 360 | if (m_count.compare_exchange_weak(oldCount, oldCount - 1, std::memory_order_acquire, std::memory_order_relaxed)) 361 | return true; 362 | } 363 | return false; 364 | } 365 | 366 | bool wait() 367 | { 368 | return tryWait() || waitWithPartialSpinning(); 369 | } 370 | 371 | bool wait(std::int64_t timeout_usecs) 372 | { 373 | return tryWait() || waitWithPartialSpinning(timeout_usecs); 374 | } 375 | 376 | // Acquires between 0 and (greedily) max, inclusive 377 | ssize_t tryWaitMany(ssize_t max) 378 | { 379 | assert(max >= 0); 380 | ssize_t oldCount = m_count.load(std::memory_order_relaxed); 381 | while (oldCount > 0) 382 | { 383 | ssize_t newCount = oldCount > max ? oldCount - max : 0; 384 | if (m_count.compare_exchange_weak(oldCount, newCount, std::memory_order_acquire, std::memory_order_relaxed)) 385 | return oldCount - newCount; 386 | } 387 | return 0; 388 | } 389 | 390 | // Acquires at least one, and (greedily) at most max 391 | ssize_t waitMany(ssize_t max, std::int64_t timeout_usecs) 392 | { 393 | assert(max >= 0); 394 | ssize_t result = tryWaitMany(max); 395 | if (result == 0 && max > 0) 396 | result = waitManyWithPartialSpinning(max, timeout_usecs); 397 | return result; 398 | } 399 | 400 | ssize_t waitMany(ssize_t max) 401 | { 402 | ssize_t result = waitMany(max, -1); 403 | assert(result > 0); 404 | return result; 405 | } 406 | 407 | void signal(ssize_t count = 1) 408 | { 409 | assert(count >= 0); 410 | ssize_t oldCount = m_count.fetch_add(count, std::memory_order_release); 411 | ssize_t toRelease = -oldCount < count ? -oldCount : count; 412 | if (toRelease > 0) 413 | { 414 | m_sema.signal((int)toRelease); 415 | } 416 | } 417 | 418 | std::size_t availableApprox() const 419 | { 420 | ssize_t count = m_count.load(std::memory_order_relaxed); 421 | return count > 0 ? static_cast(count) : 0; 422 | } 423 | }; 424 | 425 | } // end namespace moodycamel 426 | -------------------------------------------------------------------------------- /ultramodern/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | 3 | project(ultramodern) 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_CXX_STANDARD_REQUIRED True) 6 | set(CMAKE_CXX_EXTENSIONS OFF) 7 | 8 | add_library(ultramodern STATIC 9 | "${CMAKE_CURRENT_SOURCE_DIR}/src/audio.cpp" 10 | "${CMAKE_CURRENT_SOURCE_DIR}/src/error_handling.cpp" 11 | "${CMAKE_CURRENT_SOURCE_DIR}/src/events.cpp" 12 | "${CMAKE_CURRENT_SOURCE_DIR}/src/input.cpp" 13 | "${CMAKE_CURRENT_SOURCE_DIR}/src/mesgqueue.cpp" 14 | "${CMAKE_CURRENT_SOURCE_DIR}/src/misc_ultra.cpp" 15 | "${CMAKE_CURRENT_SOURCE_DIR}/src/renderer_context.cpp" 16 | "${CMAKE_CURRENT_SOURCE_DIR}/src/rsp.cpp" 17 | "${CMAKE_CURRENT_SOURCE_DIR}/src/scheduling.cpp" 18 | "${CMAKE_CURRENT_SOURCE_DIR}/src/task_win32.cpp" 19 | "${CMAKE_CURRENT_SOURCE_DIR}/src/threadqueue.cpp" 20 | "${CMAKE_CURRENT_SOURCE_DIR}/src/threads.cpp" 21 | "${CMAKE_CURRENT_SOURCE_DIR}/src/timer.cpp" 22 | "${CMAKE_CURRENT_SOURCE_DIR}/src/ultrainit.cpp" 23 | ) 24 | 25 | target_include_directories(ultramodern PUBLIC 26 | "${CMAKE_CURRENT_SOURCE_DIR}/include/" 27 | "${PROJECT_SOURCE_DIR}/../thirdparty" 28 | "${PROJECT_SOURCE_DIR}/../thirdparty/concurrentqueue" 29 | "${PROJECT_SOURCE_DIR}/../thirdparty/sse2neon" 30 | ) 31 | 32 | target_compile_options(ultramodern PRIVATE 33 | # -Wall 34 | # -Wextra 35 | -Wno-unused-parameter 36 | ) 37 | 38 | if (WIN32) 39 | add_compile_definitions(NOMINMAX) 40 | endif() 41 | -------------------------------------------------------------------------------- /ultramodern/include/ultramodern/config.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __CONFIG_HPP__ 2 | #define __CONFIG_HPP__ 3 | 4 | #include 5 | #include 6 | 7 | #include "json/json.hpp" 8 | 9 | namespace ultramodern { 10 | namespace renderer { 11 | enum class Resolution { 12 | Original, 13 | Original2x, 14 | Auto, 15 | OptionCount 16 | }; 17 | enum class WindowMode { 18 | Windowed, 19 | Fullscreen, 20 | OptionCount 21 | }; 22 | enum class HUDRatioMode { 23 | Original, 24 | Clamp16x9, 25 | Full, 26 | OptionCount 27 | }; 28 | enum class GraphicsApi { 29 | Auto, 30 | D3D12, 31 | Vulkan, 32 | Metal, 33 | OptionCount 34 | }; 35 | enum class AspectRatio { 36 | Original, 37 | Expand, 38 | Manual, 39 | OptionCount 40 | }; 41 | enum class Antialiasing { 42 | None, 43 | MSAA2X, 44 | MSAA4X, 45 | MSAA8X, 46 | OptionCount 47 | }; 48 | enum class RefreshRate { 49 | Original, 50 | Display, 51 | Manual, 52 | OptionCount 53 | }; 54 | enum class HighPrecisionFramebuffer { 55 | Auto, 56 | On, 57 | Off, 58 | OptionCount 59 | }; 60 | 61 | class GraphicsConfig { 62 | public: 63 | bool developer_mode; 64 | Resolution res_option; 65 | WindowMode wm_option; 66 | HUDRatioMode hr_option; 67 | GraphicsApi api_option; 68 | AspectRatio ar_option; 69 | Antialiasing msaa_option; 70 | RefreshRate rr_option; 71 | HighPrecisionFramebuffer hpfb_option; 72 | int rr_manual_value; 73 | int ds_option; 74 | 75 | virtual ~GraphicsConfig() = default; 76 | 77 | auto operator<=>(const GraphicsConfig& rhs) const = default; 78 | }; 79 | 80 | const GraphicsConfig& get_graphics_config(); 81 | void set_graphics_config(const GraphicsConfig& new_config); 82 | 83 | NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::renderer::Resolution, { 84 | {ultramodern::renderer::Resolution::Original, "Original"}, 85 | {ultramodern::renderer::Resolution::Original2x, "Original2x"}, 86 | {ultramodern::renderer::Resolution::Auto, "Auto"}, 87 | }); 88 | 89 | NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::renderer::WindowMode, { 90 | {ultramodern::renderer::WindowMode::Windowed, "Windowed"}, 91 | {ultramodern::renderer::WindowMode::Fullscreen, "Fullscreen"} 92 | }); 93 | 94 | NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::renderer::HUDRatioMode, { 95 | {ultramodern::renderer::HUDRatioMode::Original, "Original"}, 96 | {ultramodern::renderer::HUDRatioMode::Clamp16x9, "Clamp16x9"}, 97 | {ultramodern::renderer::HUDRatioMode::Full, "Full"}, 98 | }); 99 | 100 | NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::renderer::GraphicsApi, { 101 | {ultramodern::renderer::GraphicsApi::Auto, "Auto"}, 102 | {ultramodern::renderer::GraphicsApi::D3D12, "D3D12"}, 103 | {ultramodern::renderer::GraphicsApi::Vulkan, "Vulkan"}, 104 | {ultramodern::renderer::GraphicsApi::Metal, "Metal"}, 105 | }); 106 | 107 | NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::renderer::AspectRatio, { 108 | {ultramodern::renderer::AspectRatio::Original, "Original"}, 109 | {ultramodern::renderer::AspectRatio::Expand, "Expand"}, 110 | {ultramodern::renderer::AspectRatio::Manual, "Manual"}, 111 | }); 112 | 113 | NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::renderer::Antialiasing, { 114 | {ultramodern::renderer::Antialiasing::None, "None"}, 115 | {ultramodern::renderer::Antialiasing::MSAA2X, "MSAA2X"}, 116 | {ultramodern::renderer::Antialiasing::MSAA4X, "MSAA4X"}, 117 | {ultramodern::renderer::Antialiasing::MSAA8X, "MSAA8X"}, 118 | }); 119 | 120 | NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::renderer::RefreshRate, { 121 | {ultramodern::renderer::RefreshRate::Original, "Original"}, 122 | {ultramodern::renderer::RefreshRate::Display, "Display"}, 123 | {ultramodern::renderer::RefreshRate::Manual, "Manual"}, 124 | }); 125 | 126 | NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::renderer::HighPrecisionFramebuffer, { 127 | {ultramodern::renderer::HighPrecisionFramebuffer::Auto, "Auto"}, 128 | {ultramodern::renderer::HighPrecisionFramebuffer::On, "On"}, 129 | {ultramodern::renderer::HighPrecisionFramebuffer::Off, "Off"}, 130 | }); 131 | } 132 | } 133 | 134 | #endif 135 | -------------------------------------------------------------------------------- /ultramodern/include/ultramodern/error_handling.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ERROR_HANDLING_HPP__ 2 | #define __ERROR_HANDLING_HPP__ 3 | 4 | #include 5 | 6 | #define ULTRAMODERN_QUICK_EXIT() ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __func__) 7 | 8 | namespace ultramodern { 9 | namespace error_handling { 10 | struct callbacks_t { 11 | using message_box_t = void(const char* msg); 12 | 13 | /** 14 | * Show an OS dialog with the given `msg`. 15 | * 16 | * The `msg` parameter is always non-`nullptr`. 17 | */ 18 | message_box_t *message_box; 19 | }; 20 | 21 | void set_callbacks(const callbacks_t& callbacks); 22 | 23 | void message_box(const char* msg); 24 | 25 | [[noreturn]] void quick_exit(const char* filename, int line, const char *func, int exit_status = EXIT_FAILURE); 26 | } 27 | } 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /ultramodern/include/ultramodern/events.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __EVENTS_HPP__ 2 | #define __EVENTS_HPP__ 3 | 4 | namespace ultramodern { 5 | namespace events { 6 | struct callbacks_t { 7 | using vi_callback_t = void(); 8 | using gfx_init_callback_t = void(); 9 | 10 | /** 11 | * Called in each VI. 12 | */ 13 | vi_callback_t* vi_callback; 14 | 15 | /** 16 | * Called before entering the gfx main loop. 17 | */ 18 | gfx_init_callback_t* gfx_init_callback; 19 | }; 20 | 21 | void set_callbacks(const callbacks_t& callbacks); 22 | } 23 | } 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /ultramodern/include/ultramodern/input.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ULTRAMODERN_INPUT_HPP__ 2 | #define __ULTRAMODERN_INPUT_HPP__ 3 | 4 | #include 5 | 6 | namespace ultramodern { 7 | namespace input { 8 | enum class Device { 9 | None, 10 | Controller, 11 | // Mouse, 12 | // VRU, 13 | }; 14 | 15 | enum class Pak { 16 | None, 17 | RumblePak, 18 | // ControllerPak, 19 | // TransferPak 20 | }; 21 | 22 | struct connected_device_info_t { 23 | Device connected_device; 24 | Pak connected_pak; 25 | }; 26 | 27 | struct callbacks_t { 28 | using poll_input_t = void(void); 29 | using get_input_t = bool(int controller_num, uint16_t* buttons, float* x, float* y); 30 | using set_rumble_t = void(int controller_num, bool rumble); 31 | using get_connected_device_info_t = connected_device_info_t(int controller_num); 32 | 33 | poll_input_t* poll_input; 34 | 35 | /** 36 | * Requests the state of the pressed buttons and the analog stick for the given `controller_num`. 37 | * 38 | * `controller_num` is zero-indexed, meaning 0 corresponds to the first controller. 39 | * 40 | * Returns `true` if was able to fetch the specified data, `false` otherwise and the parameter arguments are left untouched. 41 | */ 42 | get_input_t* get_input; 43 | 44 | /** 45 | * Turns on or off rumbling for the specified controller. 46 | * 47 | * `controller_num` is zero-indexed, meaning 0 corresponds to the first controller. 48 | */ 49 | set_rumble_t* set_rumble; 50 | 51 | /** 52 | * Returns the connected device info for the given `controller_num` (as in, the controller port of the console). 53 | * 54 | * `controller_num` is zero-indexed, meaning 0 corresponds to the first controller. 55 | */ 56 | get_connected_device_info_t* get_connected_device_info; 57 | }; 58 | 59 | void set_callbacks(const callbacks_t& callbacks); 60 | } 61 | } 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /ultramodern/include/ultramodern/renderer_context.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __RENDERER_WRAPPER_HPP__ 2 | #define __RENDERER_WRAPPER_HPP__ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #if defined(_WIN32) 10 | # define WIN32_LEAN_AND_MEAN 11 | # include 12 | #elif defined(__ANDROID__) 13 | # include "android/native_window.h" 14 | #elif defined(__linux__) 15 | # include "X11/Xlib.h" 16 | # undef None 17 | # undef Status 18 | # undef LockMask 19 | # undef Always 20 | # undef Success 21 | # undef False 22 | # undef True 23 | #endif 24 | 25 | #include "ultra64.h" 26 | #include "config.hpp" 27 | 28 | struct SDL_Window; 29 | 30 | namespace ultramodern { 31 | namespace renderer { 32 | 33 | #if defined(_WIN32) 34 | // Native HWND handle to the target window. 35 | struct WindowHandle { 36 | HWND window; 37 | DWORD thread_id = (DWORD)-1; 38 | auto operator<=>(const WindowHandle&) const = default; 39 | }; 40 | // TODO add a native window handle option here (Display/Window for x11 and ANativeWindow for Android) as a compile-time option. 41 | #elif defined(__linux__) || defined(__ANDROID__) 42 | using WindowHandle = SDL_Window*; 43 | #elif defined(__APPLE__) 44 | struct WindowHandle { 45 | void* window; 46 | void* view; 47 | auto operator<=>(const WindowHandle&) const = default; 48 | }; 49 | #endif 50 | 51 | enum class SetupResult { 52 | Success, 53 | DynamicLibrariesNotFound, 54 | InvalidGraphicsAPI, 55 | GraphicsAPINotFound, 56 | GraphicsDeviceNotFound 57 | }; 58 | 59 | class RendererContext { 60 | public: 61 | virtual ~RendererContext() = default; 62 | 63 | virtual bool valid() = 0; 64 | virtual SetupResult get_setup_result() const { return setup_result; } 65 | virtual GraphicsApi get_chosen_api() const { return chosen_api; } 66 | 67 | virtual bool update_config(const GraphicsConfig& old_config, const GraphicsConfig& new_config) = 0; 68 | 69 | virtual void enable_instant_present() = 0; 70 | virtual void send_dl(const OSTask* task) = 0; 71 | virtual void update_screen(uint32_t vi_origin) = 0; 72 | virtual void shutdown() = 0; 73 | virtual uint32_t get_display_framerate() const = 0; 74 | virtual float get_resolution_scale() const = 0; 75 | 76 | protected: 77 | SetupResult setup_result; 78 | GraphicsApi chosen_api; 79 | }; 80 | 81 | struct callbacks_t { 82 | using create_render_context_t = std::unique_ptr(uint8_t* rdram, WindowHandle window_handle, bool developer_mode); 83 | using get_graphics_api_name_t = std::string(GraphicsApi api); 84 | 85 | /** 86 | * Instances a subclass of RendererContext that is used to render the game. 87 | * 88 | * This callback is mandatory for using the library. 89 | */ 90 | create_render_context_t *create_render_context; 91 | 92 | /** 93 | * This callback is optional. If not provided a library default will be used. 94 | */ 95 | get_graphics_api_name_t *get_graphics_api_name = nullptr; 96 | }; 97 | 98 | void set_callbacks(const callbacks_t& callbacks); 99 | 100 | std::unique_ptr create_render_context(uint8_t* rdram, WindowHandle window_handle, bool developer_mode); 101 | 102 | std::string get_graphics_api_name(GraphicsApi api); 103 | } 104 | } 105 | 106 | #endif 107 | -------------------------------------------------------------------------------- /ultramodern/include/ultramodern/rsp.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __RSP_HPP__ 2 | #define __RSP_HPP__ 3 | 4 | #include 5 | 6 | #include "ultra64.h" 7 | 8 | namespace ultramodern { 9 | namespace rsp { 10 | struct callbacks_t { 11 | using init_t = void(); 12 | using run_microcode_t = bool(RDRAM_ARG const OSTask* task); 13 | 14 | init_t* init; 15 | 16 | /** 17 | * Executes the given RSP task. 18 | * 19 | * Returns true if task was executed successfully. 20 | */ 21 | run_microcode_t* run_task; 22 | }; 23 | 24 | void set_callbacks(const callbacks_t& callbacks); 25 | 26 | void init(); 27 | bool run_task(RDRAM_ARG const OSTask* task); 28 | }; 29 | } // namespace ultramodern 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /ultramodern/include/ultramodern/threads.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __THREADS_HPP__ 2 | #define __THREADS_HPP__ 3 | 4 | #include 5 | 6 | #include "ultra64.h" 7 | 8 | namespace ultramodern { 9 | namespace threads { 10 | struct callbacks_t { 11 | using get_game_thread_name_t = std::string(const OSThread* t); 12 | 13 | /** 14 | * Allows to specifying a custom name for each thread. Mainly for debugging purposes. 15 | * 16 | * For maximum cross-platform compatibility the returned name should be at most 15 bytes long (16 bytes including the null terminator). 17 | * 18 | * If this function is not provided then the thread id will be used as the name of the thread. 19 | */ 20 | get_game_thread_name_t *get_game_thread_name; 21 | }; 22 | 23 | void set_callbacks(const callbacks_t& callbacks); 24 | 25 | std::string get_game_thread_name(const OSThread* t); 26 | } 27 | } 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /ultramodern/include/ultramodern/ultra64.h: -------------------------------------------------------------------------------- 1 | #ifndef __ULTRA64_ultramodern_H__ 2 | #define __ULTRA64_ultramodern_H__ 3 | 4 | #include 5 | 6 | #ifdef __GNUC__ 7 | #define UNUSED __attribute__((unused)) 8 | #define ALIGNED(x) __attribute__((aligned(x))) 9 | #else 10 | #define UNUSED 11 | #define ALIGNED(x) 12 | #endif 13 | 14 | typedef int64_t s64; 15 | typedef uint64_t u64; 16 | typedef int32_t s32; 17 | typedef uint32_t u32; 18 | typedef int16_t s16; 19 | typedef uint16_t u16; 20 | typedef int8_t s8; 21 | typedef uint8_t u8; 22 | 23 | // TODO allow a compile-time flag to be set to switch between recomp mode and 24 | // fully native mode. 25 | #if 0 // For native compilation 26 | # define PTR(x) x* 27 | # define RDRAM_ARG 28 | # define RDRAM_ARG1 29 | # define PASS_RDRAM 30 | # define PASS_RDRAM1 31 | # define TO_PTR(type, var) var 32 | # define GET_MEMBER(type, addr, member) (&addr->member) 33 | # ifdef __cplusplus 34 | # define NULLPTR nullptr 35 | # endif 36 | #else 37 | # define PTR(x) int32_t 38 | # define RDRAM_ARG uint8_t *rdram, 39 | # define RDRAM_ARG1 uint8_t *rdram 40 | # define PASS_RDRAM rdram, 41 | # define PASS_RDRAM1 rdram 42 | # define TO_PTR(type, var) ((type*)(&rdram[(uint64_t)var - 0xFFFFFFFF80000000])) 43 | # define GET_MEMBER(type, addr, member) (addr + (intptr_t)&(((type*)nullptr)->member)) 44 | # ifdef __cplusplus 45 | # define NULLPTR (PTR(void))0 46 | # endif 47 | #endif 48 | 49 | #ifndef NULL 50 | #define NULL (PTR(void) 0) 51 | #endif 52 | 53 | #define OS_MESG_NOBLOCK 0 54 | #define OS_MESG_BLOCK 1 55 | 56 | typedef s32 OSPri; 57 | typedef s32 OSId; 58 | 59 | typedef u64 OSTime; 60 | 61 | #define OS_EVENT_SW1 0 /* CPU SW1 interrupt */ 62 | #define OS_EVENT_SW2 1 /* CPU SW2 interrupt */ 63 | #define OS_EVENT_CART 2 /* Cartridge interrupt: used by rmon */ 64 | #define OS_EVENT_COUNTER 3 /* Counter int: used by VI/Timer Mgr */ 65 | #define OS_EVENT_SP 4 /* SP task done interrupt */ 66 | #define OS_EVENT_SI 5 /* SI (controller) interrupt */ 67 | #define OS_EVENT_AI 6 /* AI interrupt */ 68 | #define OS_EVENT_VI 7 /* VI interrupt: used by VI/Timer Mgr */ 69 | #define OS_EVENT_PI 8 /* PI interrupt: used by PI Manager */ 70 | #define OS_EVENT_DP 9 /* DP full sync interrupt */ 71 | #define OS_EVENT_CPU_BREAK 10 /* CPU breakpoint: used by rmon */ 72 | #define OS_EVENT_SP_BREAK 11 /* SP breakpoint: used by rmon */ 73 | #define OS_EVENT_FAULT 12 /* CPU fault event: used by rmon */ 74 | #define OS_EVENT_THREADSTATUS 13 /* CPU thread status: used by rmon */ 75 | #define OS_EVENT_PRENMI 14 /* Pre NMI interrupt */ 76 | 77 | #define M_GFXTASK 1 78 | #define M_AUDTASK 2 79 | #define M_VIDTASK 3 80 | #define M_NJPEGTASK 4 81 | 82 | ///////////// 83 | // Structs // 84 | ///////////// 85 | 86 | // Threads 87 | 88 | typedef struct UltraThreadContext UltraThreadContext; 89 | 90 | typedef enum { 91 | STOPPED, 92 | QUEUED, 93 | RUNNING, 94 | BLOCKED 95 | } OSThreadState; 96 | 97 | typedef struct OSThread_t { 98 | PTR(struct OSThread_t) next; // Next thread in the given queue 99 | OSPri priority; 100 | PTR(PTR(struct OSThread_t)) queue; // Queue this thread is in, if any 101 | uint32_t pad2; 102 | uint16_t flags; // These two are swapped to reflect rdram byteswapping 103 | uint16_t state; 104 | OSId id; 105 | int32_t pad3; 106 | UltraThreadContext* context; // An actual pointer regardless of platform 107 | int32_t sp; 108 | } OSThread; 109 | 110 | typedef u32 OSEvent; 111 | typedef PTR(void) OSMesg; 112 | 113 | typedef struct OSMesgQueue { 114 | PTR(OSThread) blocked_on_recv; /* Linked list of threads blocked on receiving from this queue */ 115 | PTR(OSThread) blocked_on_send; /* Linked list of threads blocked on sending to this queue */ 116 | s32 validCount; /* Number of messages in the queue */ 117 | s32 first; /* Index of the first message in the ring buffer */ 118 | s32 msgCount; /* Size of message buffer */ 119 | PTR(OSMesg) msg; /* Pointer to circular buffer to store messages */ 120 | } OSMesgQueue; 121 | 122 | // RSP 123 | 124 | typedef struct { 125 | u32 type; 126 | u32 flags; 127 | 128 | PTR(u64) ucode_boot; 129 | u32 ucode_boot_size; 130 | 131 | PTR(u64) ucode; 132 | u32 ucode_size; 133 | 134 | PTR(u64) ucode_data; 135 | u32 ucode_data_size; 136 | 137 | PTR(u64) dram_stack; 138 | u32 dram_stack_size; 139 | 140 | PTR(u64) output_buff; 141 | PTR(u64) output_buff_size; 142 | 143 | PTR(u64) data_ptr; 144 | u32 data_size; 145 | 146 | PTR(u64) yield_data_ptr; 147 | u32 yield_data_size; 148 | } OSTask_s; 149 | 150 | typedef union { 151 | OSTask_s t; 152 | int64_t force_structure_alignment; 153 | } OSTask; 154 | 155 | // PI 156 | 157 | struct OSIoMesgHdr { 158 | // These 3 reversed due to endianness 159 | u8 status; /* Return status */ 160 | u8 pri; /* Message priority (High or Normal) */ 161 | u16 type; /* Message type */ 162 | PTR(OSMesgQueue) retQueue; /* Return message queue to notify I/O completion */ 163 | }; 164 | 165 | struct OSIoMesg { 166 | OSIoMesgHdr hdr; /* Message header */ 167 | PTR(void) dramAddr; /* RDRAM buffer address (DMA) */ 168 | u32 devAddr; /* Device buffer address (DMA) */ 169 | u32 size; /* DMA transfer size in bytes */ 170 | u32 piHandle; /* PI device handle */ 171 | }; 172 | 173 | struct OSPiHandle { 174 | PTR(OSPiHandle_s) unused; /* point to next handle on the table */ 175 | // These four members reversed due to endianness 176 | u8 relDuration; /* domain release duration */ 177 | u8 pageSize; /* domain page size */ 178 | u8 latency; /* domain latency */ 179 | u8 type; /* DEVICE_TYPE_BULK for disk */ 180 | // These three members reversed due to endianness 181 | u16 padding; /* struct alignment padding */ 182 | u8 domain; /* which domain */ 183 | u8 pulse; /* domain pulse width */ 184 | u32 baseAddress; /* Domain address */ 185 | u32 speed; /* for roms only */ 186 | /* The following are "private" elements" */ 187 | u32 transferInfo[18]; /* for disk only */ 188 | }; 189 | 190 | typedef struct { 191 | u32 ctrl; 192 | u32 width; 193 | u32 burst; 194 | u32 vSync; 195 | u32 hSync; 196 | u32 leap; 197 | u32 hStart; 198 | u32 xScale; 199 | u32 vCurrent; 200 | } OSViCommonRegs; 201 | 202 | typedef struct { 203 | u32 origin; 204 | u32 yScale; 205 | u32 vStart; 206 | u32 vBurst; 207 | u32 vIntr; 208 | } OSViFieldRegs; 209 | 210 | typedef struct { 211 | u8 padding[3]; 212 | u8 type; 213 | OSViCommonRegs comRegs; 214 | OSViFieldRegs fldRegs[2]; 215 | } OSViMode; 216 | 217 | /* 218 | * Structure for file system 219 | */ 220 | typedef struct { 221 | int status; 222 | PTR(OSMesgQueue) queue; 223 | int channel; 224 | u8 id[32]; // TODO: funky endianness here 225 | u8 label[32]; // TODO: funky endianness here 226 | int version; 227 | int dir_size; 228 | int inode_table; /* block location */ 229 | int minode_table; /* mirrioring inode_table */ 230 | int dir_table; /* block location */ 231 | int inode_start_page; /* page # */ 232 | // Padding and reversed members due to endianness 233 | u8 padding[2]; 234 | u8 activebank; 235 | u8 banks; 236 | } OSPfs; 237 | 238 | 239 | // Controller 240 | 241 | typedef struct { 242 | // These three members reversed due to endianness 243 | u8 err_no; 244 | u8 status; /* Controller status */ 245 | u16 type; /* Controller Type */ 246 | } OSContStatus; 247 | 248 | typedef struct { 249 | u16 button; 250 | s8 stick_x; /* -80 <= stick_x <= 80 */ 251 | s8 stick_y; /* -80 <= stick_y <= 80 */ 252 | u8 err_no; 253 | } OSContPad; 254 | 255 | 256 | /////////////// 257 | // Functions // 258 | /////////////// 259 | 260 | #ifdef __cplusplus 261 | extern "C" { 262 | #endif // __cplusplus 263 | 264 | void osInitialize(void); 265 | 266 | typedef void (thread_func_t)(PTR(void)); 267 | 268 | void osCreateThread(RDRAM_ARG PTR(OSThread) t, OSId id, PTR(thread_func_t) entry, PTR(void) arg, PTR(void) sp, OSPri p); 269 | void osStartThread(RDRAM_ARG PTR(OSThread) t); 270 | void osStopThread(RDRAM_ARG PTR(OSThread) t); 271 | void osDestroyThread(RDRAM_ARG PTR(OSThread) t); 272 | void osYieldThread(RDRAM_ARG1); 273 | void osSetThreadPri(RDRAM_ARG PTR(OSThread) t, OSPri pri); 274 | OSPri osGetThreadPri(RDRAM_ARG PTR(OSThread) thread); 275 | OSId osGetThreadId(RDRAM_ARG PTR(OSThread) t); 276 | 277 | void osCreateMesgQueue(RDRAM_ARG PTR(OSMesgQueue), PTR(OSMesg), s32); 278 | s32 osSendMesg(RDRAM_ARG PTR(OSMesgQueue), OSMesg, s32); 279 | s32 osJamMesg(RDRAM_ARG PTR(OSMesgQueue), OSMesg, s32); 280 | s32 osRecvMesg(RDRAM_ARG PTR(OSMesgQueue), PTR(OSMesg), s32); 281 | void osSetEventMesg(RDRAM_ARG OSEvent, PTR(OSMesgQueue), OSMesg); 282 | void osViSetEvent(RDRAM_ARG PTR(OSMesgQueue), OSMesg, u32); 283 | void osViSwapBuffer(RDRAM_ARG PTR(void) frameBufPtr); 284 | void osViSetMode(RDRAM_ARG PTR(OSViMode)); 285 | void osViSetSpecialFeatures(uint32_t func); 286 | void osViBlack(uint8_t active); 287 | void osViRepeatLine(uint8_t active); 288 | void osViSetXScale(float scale); 289 | void osViSetYScale(float scale); 290 | PTR(void) osViGetNextFramebuffer(); 291 | PTR(void) osViGetCurrentFramebuffer(); 292 | u32 osGetCount(); 293 | OSTime osGetTime(); 294 | int osSetTimer(RDRAM_ARG PTR(OSTimer) timer, OSTime countdown, OSTime interval, PTR(OSMesgQueue) mq, OSMesg msg); 295 | int osStopTimer(RDRAM_ARG PTR(OSTimer) timer); 296 | u32 osVirtualToPhysical(PTR(void) addr); 297 | 298 | /* Controller interface */ 299 | 300 | s32 osContInit(RDRAM_ARG PTR(OSMesgQueue), u8*, PTR(OSContStatus)); 301 | s32 osContReset(RDRAM_ARG PTR(OSMesgQueue), PTR(OSContStatus)); 302 | s32 osContStartQuery(RDRAM_ARG PTR(OSMesgQueue)); 303 | s32 osContStartReadData(RDRAM_ARG PTR(OSMesgQueue)); 304 | s32 osContSetCh(RDRAM_ARG u8); 305 | void osContGetQuery(RDRAM_ARG PTR(OSContStatus)); 306 | void osContGetReadData(OSContPad *); 307 | 308 | /* Rumble PAK interface */ 309 | 310 | s32 osMotorInit(RDRAM_ARG PTR(OSMesgQueue), PTR(OSPfs), int); 311 | s32 osMotorStop(RDRAM_ARG PTR(OSPfs)); 312 | s32 osMotorStart(RDRAM_ARG PTR(OSPfs)); 313 | s32 __osMotorAccess(RDRAM_ARG PTR(OSPfs), s32); 314 | 315 | #ifdef __cplusplus 316 | } // extern "C" 317 | #endif 318 | 319 | #endif 320 | -------------------------------------------------------------------------------- /ultramodern/include/ultramodern/ultramodern.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __ultramodern_HPP__ 2 | #define __ultramodern_HPP__ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #undef MOODYCAMEL_DELETE_FUNCTION 12 | #define MOODYCAMEL_DELETE_FUNCTION = delete 13 | #include "lightweightsemaphore.h" 14 | 15 | #include "ultra64.h" 16 | 17 | #include "ultramodern/error_handling.hpp" 18 | #include "ultramodern/events.hpp" 19 | #include "ultramodern/input.hpp" 20 | #include "ultramodern/renderer_context.hpp" 21 | #include "ultramodern/rsp.hpp" 22 | #include "ultramodern/threads.hpp" 23 | 24 | struct UltraThreadContext { 25 | std::thread host_thread; 26 | moodycamel::LightweightSemaphore running; 27 | moodycamel::LightweightSemaphore initialized; 28 | }; 29 | 30 | namespace ultramodern { 31 | 32 | constexpr uint32_t save_size = 1024 * 1024 / 8; // Maximum save size, 1Mbit for flash 33 | 34 | // Initialization. 35 | void preinit(RDRAM_ARG renderer::WindowHandle window_handle); 36 | void init_saving(RDRAM_ARG1); 37 | void init_events(RDRAM_ARG renderer::WindowHandle window_handle); 38 | void init_timers(RDRAM_ARG1); 39 | void init_thread_cleanup(); 40 | 41 | // Saving 42 | void change_save_file(const std::u8string& subfolder, const std::u8string& name); 43 | std::filesystem::path get_save_file_path(); 44 | 45 | // Thread queues. 46 | constexpr PTR(PTR(OSThread)) running_queue = (PTR(PTR(OSThread)))-1; 47 | 48 | void thread_queue_insert(RDRAM_ARG PTR(PTR(OSThread)) queue, PTR(OSThread) toadd); 49 | PTR(OSThread) thread_queue_pop(RDRAM_ARG PTR(PTR(OSThread)) queue); 50 | bool thread_queue_remove(RDRAM_ARG PTR(PTR(OSThread)) queue_, PTR(OSThread) t_); 51 | bool thread_queue_empty(RDRAM_ARG PTR(PTR(OSThread)) queue); 52 | PTR(OSThread) thread_queue_peek(RDRAM_ARG PTR(PTR(OSThread)) queue); 53 | 54 | // Message queues. 55 | void wait_for_external_message(RDRAM_ARG1); 56 | void wait_for_external_message_timed(RDRAM_ARG1, u32 millis); 57 | 58 | // Thread scheduling. 59 | void check_running_queue(RDRAM_ARG1); 60 | void run_next_thread_and_wait(RDRAM_ARG1); 61 | void resume_thread_and_wait(RDRAM_ARG OSThread* t); 62 | void schedule_running_thread(RDRAM_ARG PTR(OSThread) t); 63 | void cleanup_thread(UltraThreadContext* thread_context); 64 | struct thread_terminated : std::exception {}; 65 | 66 | enum class ThreadPriority { 67 | Low, 68 | Normal, 69 | High, 70 | VeryHigh, 71 | Critical 72 | }; 73 | 74 | void set_native_thread_name(const std::string& name); 75 | void set_native_thread_priority(ThreadPriority pri); 76 | PTR(OSThread) this_thread(); 77 | void set_main_thread(); 78 | bool is_game_thread(); 79 | void submit_rsp_task(RDRAM_ARG PTR(OSTask) task); 80 | void send_si_message(RDRAM_ARG1); 81 | uint32_t get_speed_multiplier(); 82 | 83 | // Time 84 | std::chrono::high_resolution_clock::time_point get_start(); 85 | std::chrono::high_resolution_clock::duration time_since_start(); 86 | void measure_input_latency(); 87 | void sleep_milliseconds(uint32_t millis); 88 | void sleep_until(const std::chrono::high_resolution_clock::time_point& time_point); 89 | 90 | // Graphics 91 | uint32_t get_target_framerate(uint32_t original); 92 | uint32_t get_display_refresh_rate(); 93 | float get_resolution_scale(); 94 | void trigger_config_action(); 95 | 96 | // Audio 97 | void init_audio(); 98 | void set_audio_frequency(uint32_t freq); 99 | void queue_audio_buffer(RDRAM_ARG PTR(s16) audio_data, uint32_t byte_count); 100 | uint32_t get_remaining_audio_bytes(); 101 | 102 | struct audio_callbacks_t { 103 | using queue_samples_t = void(int16_t*, size_t); 104 | using get_samples_remaining_t = size_t(); 105 | using set_frequency_t = void(uint32_t); 106 | queue_samples_t* queue_samples; 107 | get_samples_remaining_t* get_frames_remaining; 108 | set_frequency_t* set_frequency; 109 | }; 110 | 111 | // TODO: Most of the members of this struct are not used by ultramodern. Should we move them to librecomp instead? 112 | struct gfx_callbacks_t { 113 | using gfx_data_t = void*; 114 | using create_gfx_t = gfx_data_t(); 115 | using create_window_t = renderer::WindowHandle(gfx_data_t); 116 | using update_gfx_t = void(gfx_data_t); 117 | 118 | create_gfx_t* create_gfx; 119 | create_window_t* create_window; 120 | update_gfx_t* update_gfx; 121 | }; 122 | 123 | bool is_game_started(); 124 | void quit(); 125 | void join_event_threads(); 126 | void join_thread_cleaner_thread(); 127 | void join_saving_thread(); 128 | 129 | void set_audio_callbacks(const audio_callbacks_t& callbacks); 130 | 131 | /** 132 | * Register all the callbacks used by `ultramodern`, most of them being optional. 133 | * 134 | * The following arguments contain mandatory callbacks that need to be registered (i.e., can't be `nullptr`): 135 | * - `rsp_callbacks` 136 | * - `renderer_callbacks` 137 | * 138 | * It must be called only once and it must be called before `ultramodern::preinit`. 139 | */ 140 | void set_callbacks( 141 | const rsp::callbacks_t& rsp_callbacks, 142 | const renderer::callbacks_t& renderer_callbacks, 143 | const audio_callbacks_t& audio_callbacks, 144 | const input::callbacks_t& input_callbacks, 145 | const gfx_callbacks_t& gfx_callbacks, 146 | const events::callbacks_t& events_callbacks, 147 | const error_handling::callbacks_t& error_handling_callbacks, 148 | const threads::callbacks_t& threads_callbacks 149 | ); 150 | } // namespace ultramodern 151 | 152 | #define MIN(a, b) ((a) < (b) ? (a) : (b)) 153 | 154 | #define debug_printf(...) 155 | //#define debug_printf(...) printf(__VA_ARGS__); 156 | 157 | #endif 158 | -------------------------------------------------------------------------------- /ultramodern/src/audio.cpp: -------------------------------------------------------------------------------- 1 | #include "ultramodern/ultra64.h" 2 | #include "ultramodern/ultramodern.hpp" 3 | #include 4 | 5 | static uint32_t sample_rate = 48000; 6 | 7 | static ultramodern::audio_callbacks_t audio_callbacks; 8 | 9 | void ultramodern::set_audio_callbacks(const ultramodern::audio_callbacks_t& callbacks) { 10 | audio_callbacks = callbacks; 11 | } 12 | 13 | void ultramodern::init_audio() { 14 | // Pick an initial dummy sample rate; this will be set by the game later to the true sample rate. 15 | set_audio_frequency(48000); 16 | } 17 | 18 | void ultramodern::set_audio_frequency(uint32_t freq) { 19 | if (audio_callbacks.set_frequency) { 20 | audio_callbacks.set_frequency(freq); 21 | } 22 | sample_rate = freq; 23 | } 24 | 25 | void ultramodern::queue_audio_buffer(RDRAM_ARG PTR(int16_t) audio_data_, uint32_t byte_count) { 26 | // Ensure that the byte count is an integer multiple of samples. 27 | assert((byte_count & 1) == 0); 28 | 29 | // Calculate the number of samples from the number of bytes. 30 | uint32_t sample_count = byte_count / sizeof(int16_t); 31 | 32 | // Queue the swapped audio data. 33 | if (sample_count > 0 && audio_callbacks.queue_samples) { 34 | audio_callbacks.queue_samples(TO_PTR(int16_t, audio_data_), sample_count); 35 | } 36 | } 37 | 38 | // For SDL2 39 | //uint32_t buffer_offset_frames = 1; 40 | // For Godot 41 | float buffer_offset_frames = 0.5f; 42 | 43 | // If there's ever any audio popping, check here first. Some games are very sensitive to 44 | // the remaining sample count and reporting a number that's too high here can lead to issues. 45 | // Reporting a number that's too low can lead to audio lag in some games. 46 | uint32_t ultramodern::get_remaining_audio_bytes() { 47 | // Get the number of remaining buffered audio bytes. 48 | uint32_t buffered_byte_count; 49 | if (audio_callbacks.get_frames_remaining != nullptr) { 50 | buffered_byte_count = audio_callbacks.get_frames_remaining() * 2 * sizeof(int16_t); 51 | } 52 | else { 53 | buffered_byte_count = 100; 54 | } 55 | // Adjust the reported count to be some number of refreshes in the future, which helps ensure that 56 | // there are enough samples even if the audio thread experiences a small amount of lag. This prevents 57 | // audio popping on games that use the buffered audio byte count to determine how many samples 58 | // to generate. 59 | uint32_t samples_per_vi = (sample_rate / 60); 60 | if (buffered_byte_count > static_cast(buffer_offset_frames * sizeof(int16_t) * samples_per_vi)) { 61 | buffered_byte_count -= static_cast(buffer_offset_frames * sizeof(int16_t) * samples_per_vi); 62 | } 63 | else { 64 | buffered_byte_count = 0; 65 | } 66 | return buffered_byte_count; 67 | } 68 | -------------------------------------------------------------------------------- /ultramodern/src/error_handling.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "ultramodern/error_handling.hpp" 4 | 5 | static ultramodern::error_handling::callbacks_t error_handling_callbacks{}; 6 | 7 | void ultramodern::error_handling::set_callbacks(const ultramodern::error_handling::callbacks_t& callbacks) { 8 | error_handling_callbacks = callbacks; 9 | } 10 | 11 | void ultramodern::error_handling::message_box(const char* msg) { 12 | // We print the message to stderr since the user may not have provided a message_box callback 13 | 14 | fprintf(stderr, "%s\n", msg); 15 | 16 | if (error_handling_callbacks.message_box != nullptr) { 17 | error_handling_callbacks.message_box(msg); 18 | } 19 | } 20 | 21 | void ultramodern::error_handling::quick_exit(const char* filename, int line, const char *func, int exit_status) { 22 | fprintf(stderr, "Exiting with exit status '%i'. Function %s, at file %s:%i\n", exit_status, func, filename, line); 23 | 24 | #ifdef __APPLE__ 25 | std::_Exit(exit_status); 26 | #else 27 | std::quick_exit(exit_status); 28 | #endif 29 | } 30 | -------------------------------------------------------------------------------- /ultramodern/src/input.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "ultramodern/input.hpp" 4 | #include "ultramodern/ultra64.h" 5 | #include "ultramodern/ultramodern.hpp" 6 | 7 | static ultramodern::input::callbacks_t input_callbacks {}; 8 | 9 | void ultramodern::input::set_callbacks(const callbacks_t& callbacks) { 10 | input_callbacks = callbacks; 11 | } 12 | 13 | static std::chrono::high_resolution_clock::time_point input_poll_time; 14 | 15 | static void update_poll_time() { 16 | input_poll_time = std::chrono::high_resolution_clock::now(); 17 | } 18 | 19 | void ultramodern::measure_input_latency() { 20 | #if 0 21 | printf("Delta: %ld micros\n", std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - input_poll_time)); 22 | #endif 23 | } 24 | 25 | #define MAXCONTROLLERS 4 26 | 27 | #define CONT_NO_RESPONSE_ERROR 0x8 28 | 29 | #define CONT_TYPE_NORMAL 0x0005 30 | #define CONT_TYPE_MOUSE 0x0002 31 | #define CONT_TYPE_VOICE 0x0100 32 | 33 | static int max_controllers = 0; 34 | 35 | /* Plain controller */ 36 | 37 | static u16 get_controller_type(ultramodern::input::Device device_type) { 38 | switch (device_type) { 39 | case ultramodern::input::Device::None: 40 | return 0; 41 | 42 | case ultramodern::input::Device::Controller: 43 | return CONT_TYPE_NORMAL; 44 | 45 | #if 0 46 | case ultramodern::input::Device::Mouse: 47 | return CONT_TYPE_MOUSE; 48 | 49 | case ultramodern::input::Device::VRU: 50 | return CONT_TYPE_VOICE; 51 | #endif 52 | } 53 | 54 | return 0; 55 | } 56 | 57 | static void __osContGetInitData(u8* pattern, OSContStatus *data) { 58 | *pattern = 0x00; 59 | 60 | for (int controller = 0; controller < max_controllers; controller++) { 61 | ultramodern::input::connected_device_info_t device_info{}; 62 | 63 | if (input_callbacks.get_connected_device_info != nullptr) { 64 | device_info = input_callbacks.get_connected_device_info(controller); 65 | } 66 | 67 | if (device_info.connected_device != ultramodern::input::Device::None) { 68 | // Mark controller as present 69 | 70 | data[controller].type = get_controller_type(device_info.connected_device); 71 | data[controller].status = device_info.connected_pak != ultramodern::input::Pak::None; 72 | data[controller].err_no = 0x00; 73 | 74 | *pattern = 1 << controller; 75 | } 76 | else { 77 | // Mark controller as not connected 78 | 79 | // Libultra doesn't write status or type for absent controllers 80 | data[controller].err_no = CONT_NO_RESPONSE_ERROR; // CHNL_ERR_NORESP >> 4 81 | } 82 | } 83 | } 84 | 85 | extern "C" s32 osContInit(RDRAM_ARG PTR(OSMesgQueue) mq, u8* bitpattern, PTR(OSContStatus) data_) { 86 | OSContStatus *data = TO_PTR(OSContStatus, data_); 87 | 88 | max_controllers = MAXCONTROLLERS; 89 | 90 | __osContGetInitData(bitpattern, data); 91 | 92 | return 0; 93 | } 94 | 95 | extern "C" s32 osContReset(RDRAM_ARG PTR(OSMesgQueue) mq, PTR(OSContStatus) data) { 96 | assert(false); 97 | return 0; 98 | } 99 | 100 | extern "C" s32 osContStartQuery(RDRAM_ARG PTR(OSMesgQueue) mq) { 101 | ultramodern::send_si_message(PASS_RDRAM1); 102 | 103 | return 0; 104 | } 105 | 106 | extern "C" s32 osContStartReadData(RDRAM_ARG PTR(OSMesgQueue) mq) { 107 | if (input_callbacks.poll_input != nullptr) { 108 | input_callbacks.poll_input(); 109 | } 110 | update_poll_time(); 111 | 112 | ultramodern::send_si_message(rdram); 113 | 114 | return 0; 115 | } 116 | 117 | extern "C" s32 osContSetCh(RDRAM_ARG u8 ch) { 118 | max_controllers = std::min(ch, u8(MAXCONTROLLERS)); 119 | 120 | return 0; 121 | } 122 | 123 | extern "C" void osContGetQuery(RDRAM_ARG PTR(OSContStatus) data_) { 124 | OSContStatus *data = TO_PTR(OSContStatus, data_); 125 | u8 pattern; 126 | 127 | __osContGetInitData(&pattern, data); 128 | } 129 | 130 | extern "C" void osContGetReadData(OSContPad *data) { 131 | for (int controller = 0; controller < max_controllers; controller++) { 132 | uint16_t buttons = 0; 133 | float x = 0.0f; 134 | float y = 0.0f; 135 | bool got_response = false; 136 | 137 | if (input_callbacks.get_input != nullptr) { 138 | got_response = input_callbacks.get_input(controller, &buttons, &x, &y); 139 | } 140 | 141 | if (got_response) { 142 | data[controller].button = buttons; 143 | data[controller].stick_x = (int8_t)(127 * x); 144 | data[controller].stick_y = (int8_t)(127 * y); 145 | data[controller].err_no = 0; 146 | } else { 147 | data[controller].err_no = CONT_NO_RESPONSE_ERROR; // CHNL_ERR_NORESP >> 4 148 | } 149 | } 150 | } 151 | 152 | /* Rumble */ 153 | 154 | s32 osMotorInit(RDRAM_ARG PTR(OSMesgQueue) mq, PTR(OSPfs) pfs_, int channel) { 155 | OSPfs *pfs = TO_PTR(OSPfs, pfs_); 156 | 157 | pfs->channel = channel; 158 | 159 | return 0; 160 | } 161 | 162 | s32 osMotorStop(RDRAM_ARG PTR(OSPfs) pfs) { 163 | return __osMotorAccess(PASS_RDRAM pfs, false); 164 | } 165 | 166 | s32 osMotorStart(RDRAM_ARG PTR(OSPfs) pfs) { 167 | return __osMotorAccess(PASS_RDRAM pfs, true); 168 | } 169 | 170 | s32 __osMotorAccess(RDRAM_ARG PTR(OSPfs) pfs_, s32 flag) { 171 | OSPfs *pfs = TO_PTR(OSPfs, pfs_); 172 | 173 | if (input_callbacks.set_rumble != nullptr) { 174 | // TODO: Should we check if the Rumble Pak is connected? Or just rumble regardless of the connected Pak? 175 | input_callbacks.set_rumble(pfs->channel, flag); 176 | } 177 | 178 | return 0; 179 | } 180 | -------------------------------------------------------------------------------- /ultramodern/src/mesgqueue.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "blockingconcurrentqueue.h" 4 | 5 | #include "ultramodern/ultra64.h" 6 | #include "ultramodern/ultramodern.hpp" 7 | 8 | struct QueuedMessage { 9 | PTR(OSMesgQueue) mq; 10 | OSMesg mesg; 11 | bool jam; 12 | }; 13 | 14 | static moodycamel::BlockingConcurrentQueue external_messages {}; 15 | 16 | void enqueue_external_message(PTR(OSMesgQueue) mq, OSMesg msg, bool jam) { 17 | external_messages.enqueue({mq, msg, jam}); 18 | } 19 | 20 | bool do_send(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, bool jam, bool block); 21 | 22 | void dequeue_external_messages(RDRAM_ARG1) { 23 | QueuedMessage to_send; 24 | while (external_messages.try_dequeue(to_send)) { 25 | do_send(PASS_RDRAM to_send.mq, to_send.mesg, to_send.jam, false); 26 | } 27 | } 28 | 29 | void ultramodern::wait_for_external_message(RDRAM_ARG1) { 30 | QueuedMessage to_send; 31 | external_messages.wait_dequeue(to_send); 32 | do_send(PASS_RDRAM to_send.mq, to_send.mesg, to_send.jam, false); 33 | } 34 | 35 | void ultramodern::wait_for_external_message_timed(RDRAM_ARG1, u32 millis) { 36 | QueuedMessage to_send; 37 | if (external_messages.wait_dequeue_timed(to_send, std::chrono::milliseconds{millis})) { 38 | do_send(PASS_RDRAM to_send.mq, to_send.mesg, to_send.jam, false); 39 | } 40 | } 41 | 42 | extern "C" void osCreateMesgQueue(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSMesg) msg, s32 count) { 43 | OSMesgQueue *mq = TO_PTR(OSMesgQueue, mq_); 44 | mq->blocked_on_recv = NULLPTR; 45 | mq->blocked_on_send = NULLPTR; 46 | mq->msgCount = count; 47 | mq->msg = msg; 48 | mq->validCount = 0; 49 | mq->first = 0; 50 | } 51 | 52 | s32 MQ_GET_COUNT(OSMesgQueue *mq) { 53 | return mq->validCount; 54 | } 55 | 56 | s32 MQ_IS_EMPTY(OSMesgQueue *mq) { 57 | return mq->validCount == 0; 58 | } 59 | 60 | s32 MQ_IS_FULL(OSMesgQueue* mq) { 61 | return MQ_GET_COUNT(mq) >= mq->msgCount; 62 | } 63 | 64 | bool do_send(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, bool jam, bool block) { 65 | OSMesgQueue* mq = TO_PTR(OSMesgQueue, mq_); 66 | if (!block) { 67 | // If non-blocking, fail if the queue is full. 68 | if (MQ_IS_FULL(mq)) { 69 | return false; 70 | } 71 | } 72 | else { 73 | // Otherwise, yield this thread until the queue has room. 74 | while (MQ_IS_FULL(mq)) { 75 | debug_printf("[Message Queue] Thread %d is blocked on send\n", TO_PTR(OSThread, ultramodern::this_thread())->id); 76 | ultramodern::thread_queue_insert(PASS_RDRAM GET_MEMBER(OSMesgQueue, mq_, blocked_on_send), ultramodern::this_thread()); 77 | ultramodern::run_next_thread_and_wait(PASS_RDRAM1); 78 | } 79 | } 80 | 81 | if (jam) { 82 | // Jams insert at the head of the message queue's buffer. 83 | mq->first = (mq->first + mq->msgCount - 1) % mq->msgCount; 84 | TO_PTR(OSMesg, mq->msg)[mq->first] = msg; 85 | mq->validCount++; 86 | } 87 | else { 88 | // Sends insert at the tail of the message queue's buffer. 89 | s32 last = (mq->first + mq->validCount) % mq->msgCount; 90 | TO_PTR(OSMesg, mq->msg)[last] = msg; 91 | mq->validCount++; 92 | } 93 | 94 | // If any threads were blocked on receiving from this message queue, pop the first one and schedule it. 95 | PTR(PTR(OSThread)) blocked_queue = GET_MEMBER(OSMesgQueue, mq_, blocked_on_recv); 96 | if (!ultramodern::thread_queue_empty(PASS_RDRAM blocked_queue)) { 97 | ultramodern::schedule_running_thread(PASS_RDRAM ultramodern::thread_queue_pop(PASS_RDRAM blocked_queue)); 98 | } 99 | 100 | return true; 101 | } 102 | 103 | bool do_recv(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSMesg) msg_, bool block) { 104 | OSMesgQueue* mq = TO_PTR(OSMesgQueue, mq_); 105 | if (!block) { 106 | // If non-blocking, fail if the queue is empty 107 | if (MQ_IS_EMPTY(mq)) { 108 | return false; 109 | } 110 | } else { 111 | // Otherwise, yield this thread in a loop until the queue is no longer full 112 | while (MQ_IS_EMPTY(mq)) { 113 | debug_printf("[Message Queue] Thread %d is blocked on receive\n", TO_PTR(OSThread, ultramodern::this_thread())->id); 114 | ultramodern::thread_queue_insert(PASS_RDRAM GET_MEMBER(OSMesgQueue, mq_, blocked_on_recv), ultramodern::this_thread()); 115 | ultramodern::run_next_thread_and_wait(PASS_RDRAM1); 116 | } 117 | } 118 | 119 | if (msg_ != NULLPTR) { 120 | *TO_PTR(OSMesg, msg_) = TO_PTR(OSMesg, mq->msg)[mq->first]; 121 | } 122 | 123 | mq->first = (mq->first + 1) % mq->msgCount; 124 | mq->validCount--; 125 | 126 | // If any threads were blocked on sending to this message queue, pop the first one and schedule it. 127 | PTR(PTR(OSThread)) blocked_queue = GET_MEMBER(OSMesgQueue, mq_, blocked_on_send); 128 | if (!ultramodern::thread_queue_empty(PASS_RDRAM blocked_queue)) { 129 | ultramodern::schedule_running_thread(PASS_RDRAM ultramodern::thread_queue_pop(PASS_RDRAM blocked_queue)); 130 | } 131 | 132 | return true; 133 | } 134 | 135 | extern "C" s32 osSendMesg(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, s32 flags) { 136 | OSMesgQueue *mq = TO_PTR(OSMesgQueue, mq_); 137 | bool jam = false; 138 | 139 | // Don't directly send to the message queue if this isn't a game thread to avoid contention. 140 | if (!ultramodern::is_game_thread()) { 141 | enqueue_external_message(mq_, msg, jam); 142 | return 0; 143 | } 144 | 145 | // Handle any messages that have been received from an external thread. 146 | dequeue_external_messages(PASS_RDRAM1); 147 | 148 | // Try to send the message. 149 | bool sent = do_send(PASS_RDRAM mq_, msg, jam, flags == OS_MESG_BLOCK); 150 | 151 | // Check the queue to see if this thread should swap execution to another. 152 | ultramodern::check_running_queue(PASS_RDRAM1); 153 | 154 | return sent ? 0 : -1; 155 | } 156 | 157 | extern "C" s32 osJamMesg(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, s32 flags) { 158 | OSMesgQueue *mq = TO_PTR(OSMesgQueue, mq_); 159 | bool jam = true; 160 | 161 | // Don't directly send to the message queue if this isn't a game thread to avoid contention. 162 | if (!ultramodern::is_game_thread()) { 163 | enqueue_external_message(mq_, msg, jam); 164 | return 0; 165 | } 166 | 167 | // Handle any messages that have been received from an external thread. 168 | dequeue_external_messages(PASS_RDRAM1); 169 | 170 | // Try to send the message. 171 | bool sent = do_send(PASS_RDRAM mq_, msg, jam, flags == OS_MESG_BLOCK); 172 | 173 | // Check the queue to see if this thread should swap execution to another. 174 | ultramodern::check_running_queue(PASS_RDRAM1); 175 | 176 | return sent ? 0 : -1; 177 | } 178 | 179 | extern "C" s32 osRecvMesg(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSMesg) msg_, s32 flags) { 180 | OSMesgQueue *mq = TO_PTR(OSMesgQueue, mq_); 181 | 182 | assert(ultramodern::is_game_thread() && "RecvMesg not allowed outside of game threads."); 183 | 184 | // Handle any messages that have been received from an external thread. 185 | dequeue_external_messages(PASS_RDRAM1); 186 | 187 | // Try to receive a message. 188 | bool received = do_recv(PASS_RDRAM mq_, msg_, flags == OS_MESG_BLOCK); 189 | 190 | // Check the queue to see if this thread should swap execution to another. 191 | ultramodern::check_running_queue(PASS_RDRAM1); 192 | 193 | return received ? 0 : -1; 194 | } 195 | -------------------------------------------------------------------------------- /ultramodern/src/misc_ultra.cpp: -------------------------------------------------------------------------------- 1 | #include "ultramodern/ultra64.h" 2 | 3 | #define K0BASE 0x80000000 4 | #define K1BASE 0xA0000000 5 | #define K2BASE 0xC0000000 6 | #define IS_KSEG0(x) ((u32)(x) >= K0BASE && (u32)(x) < K1BASE) 7 | #define IS_KSEG1(x) ((u32)(x) >= K1BASE && (u32)(x) < K2BASE) 8 | #define K0_TO_PHYS(x) ((u32)(x)&0x1FFFFFFF) 9 | #define K1_TO_PHYS(x) ((u32)(x)&0x1FFFFFFF) 10 | 11 | u32 osVirtualToPhysical(PTR(void) addr) { 12 | uintptr_t addr_val = (uintptr_t)addr; 13 | if (IS_KSEG0(addr_val)) { 14 | return K0_TO_PHYS(addr_val); 15 | } else if (IS_KSEG1(addr_val)) { 16 | return K1_TO_PHYS(addr_val); 17 | } else { 18 | // TODO handle TLB mappings 19 | return (u32)addr_val; 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /ultramodern/src/port_main.c: -------------------------------------------------------------------------------- 1 | #if 0 2 | 3 | #include 4 | #include 5 | #include "ultra64.h" 6 | 7 | #define THREAD_STACK_SIZE 0x1000 8 | 9 | u8 idle_stack[THREAD_STACK_SIZE] ALIGNED(16); 10 | u8 main_stack[THREAD_STACK_SIZE] ALIGNED(16); 11 | u8 thread3_stack[THREAD_STACK_SIZE] ALIGNED(16); 12 | u8 thread4_stack[THREAD_STACK_SIZE] ALIGNED(16); 13 | 14 | OSThread idle_thread; 15 | OSThread main_thread; 16 | OSThread thread3; 17 | OSThread thread4; 18 | 19 | OSMesgQueue queue; 20 | OSMesg buf[1]; 21 | 22 | void thread3_func(UNUSED void *arg) { 23 | OSMesg val; 24 | printf("Thread3 recv\n"); 25 | fflush(stdout); 26 | osRecvMesg(&queue, &val, OS_MESG_BLOCK); 27 | printf("Thread3 complete: %d\n", (int)(intptr_t)val); 28 | fflush(stdout); 29 | } 30 | 31 | void thread4_func(void *arg) { 32 | printf("Thread4 send %d\n", (int)(intptr_t)arg); 33 | fflush(stdout); 34 | osSendMesg(&queue, arg, OS_MESG_BLOCK); 35 | printf("Thread4 complete\n"); 36 | fflush(stdout); 37 | } 38 | 39 | void main_thread_func(UNUSED void* arg) { 40 | osCreateMesgQueue(&queue, buf, sizeof(buf) / sizeof(buf[0])); 41 | 42 | printf("main thread creating thread 3\n"); 43 | osCreateThread(&thread3, 3, thread3_func, NULL, &thread3_stack[THREAD_STACK_SIZE], 14); 44 | printf("main thread starting thread 3\n"); 45 | osStartThread(&thread3); 46 | 47 | printf("main thread creating thread 4\n"); 48 | osCreateThread(&thread4, 4, thread4_func, (void*)10, &thread4_stack[THREAD_STACK_SIZE], 13); 49 | printf("main thread starting thread 4\n"); 50 | osStartThread(&thread4); 51 | 52 | while (1) { 53 | printf("main thread doin stuff\n"); 54 | sleep(1); 55 | } 56 | } 57 | 58 | void idle_thread_func(UNUSED void* arg) { 59 | printf("idle thread\n"); 60 | printf("creating main thread\n"); 61 | osCreateThread(&main_thread, 2, main_thread_func, NULL, &main_stack[THREAD_STACK_SIZE], 11); 62 | printf("starting main thread\n"); 63 | osStartThread(&main_thread); 64 | 65 | // Set this thread's priority to 0, making it the idle thread 66 | osSetThreadPri(NULL, 0); 67 | 68 | // idle 69 | while (1) { 70 | printf("idle thread doin stuff\n"); 71 | sleep(1); 72 | } 73 | } 74 | 75 | void bootproc(void) { 76 | osInitialize(); 77 | 78 | osCreateThread(&idle_thread, 1, idle_thread_func, NULL, &idle_stack[THREAD_STACK_SIZE], 127); 79 | printf("Starting idle thread\n"); 80 | osStartThread(&idle_thread); 81 | } 82 | 83 | #endif 84 | -------------------------------------------------------------------------------- /ultramodern/src/renderer_context.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "ultramodern/renderer_context.hpp" 5 | #include "ultramodern/ultramodern.hpp" 6 | 7 | static ultramodern::renderer::callbacks_t render_callbacks{}; 8 | 9 | void ultramodern::renderer::set_callbacks(const callbacks_t& callbacks) { 10 | render_callbacks = callbacks; 11 | } 12 | 13 | 14 | std::unique_ptr ultramodern::renderer::create_render_context(uint8_t* rdram, WindowHandle window_handle, bool developer_mode) { 15 | if (render_callbacks.create_render_context == nullptr) { 16 | error_handling::message_box("[Error] The mandatory render callback `create_render_context` was not registered"); 17 | ULTRAMODERN_QUICK_EXIT(); 18 | } 19 | 20 | return render_callbacks.create_render_context(rdram, window_handle, developer_mode); 21 | } 22 | 23 | std::string ultramodern::renderer::get_graphics_api_name(GraphicsApi api) { 24 | if (render_callbacks.get_graphics_api_name != nullptr) { 25 | return render_callbacks.get_graphics_api_name(api); 26 | } 27 | switch (api) { 28 | case ultramodern::renderer::GraphicsApi::Auto: 29 | return "Auto"; 30 | case ultramodern::renderer::GraphicsApi::D3D12: 31 | return "D3D12"; 32 | case ultramodern::renderer::GraphicsApi::Vulkan: 33 | return "Vulkan"; 34 | case ultramodern::renderer::GraphicsApi::Metal: 35 | return "Metal"; 36 | default: 37 | return "[Unknown graphics API]"; 38 | } 39 | } 40 | 41 | 42 | static ultramodern::renderer::GraphicsConfig graphic_config{}; 43 | static std::mutex graphic_config_mutex; 44 | 45 | void ultramodern::renderer::set_graphics_config(const GraphicsConfig& config) { 46 | std::lock_guard lock(graphic_config_mutex); 47 | graphic_config = config; 48 | ultramodern::trigger_config_action(); 49 | } 50 | 51 | const ultramodern::renderer::GraphicsConfig& ultramodern::renderer::get_graphics_config() { 52 | std::lock_guard lock(graphic_config_mutex); 53 | return graphic_config; 54 | } 55 | -------------------------------------------------------------------------------- /ultramodern/src/rsp.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "ultramodern/rsp.hpp" 5 | 6 | static ultramodern::rsp::callbacks_t rsp_callbacks {}; 7 | 8 | void ultramodern::rsp::set_callbacks(const callbacks_t& callbacks) { 9 | rsp_callbacks = callbacks; 10 | } 11 | 12 | void ultramodern::rsp::init() { 13 | if (rsp_callbacks.init != nullptr) { 14 | rsp_callbacks.init(); 15 | } 16 | } 17 | 18 | bool ultramodern::rsp::run_task(RDRAM_ARG const OSTask* task) { 19 | assert(rsp_callbacks.run_task != nullptr); 20 | 21 | return rsp_callbacks.run_task(PASS_RDRAM task); 22 | } 23 | -------------------------------------------------------------------------------- /ultramodern/src/scheduling.cpp: -------------------------------------------------------------------------------- 1 | #include "ultramodern/ultramodern.hpp" 2 | 3 | void ultramodern::schedule_running_thread(RDRAM_ARG PTR(OSThread) t_) { 4 | debug_printf("[Scheduling] Adding thread %d to the running queue\n", TO_PTR(OSThread, t_)->id); 5 | thread_queue_insert(PASS_RDRAM running_queue, t_); 6 | TO_PTR(OSThread, t_)->state = OSThreadState::QUEUED; 7 | } 8 | 9 | void swap_to_thread(RDRAM_ARG OSThread *to) { 10 | debug_printf("[Scheduling] Thread %d giving execution to thread %d\n", TO_PTR(OSThread, ultramodern::this_thread())->id, to->id); 11 | // Insert this thread in the running queue. 12 | ultramodern::thread_queue_insert(PASS_RDRAM ultramodern::running_queue, ultramodern::this_thread()); 13 | TO_PTR(OSThread, ultramodern::this_thread())->state = OSThreadState::QUEUED; 14 | // Unpause the target thread and wait for this one to be unpaused. 15 | ultramodern::resume_thread_and_wait(PASS_RDRAM to); 16 | } 17 | 18 | void ultramodern::check_running_queue(RDRAM_ARG1) { 19 | // Check if there are any threads in the running queue. 20 | if (!thread_queue_empty(PASS_RDRAM running_queue)) { 21 | // Check if the highest priority thread in the queue is higher priority than the current thread. 22 | OSThread* next_thread = TO_PTR(OSThread, ultramodern::thread_queue_peek(PASS_RDRAM running_queue)); 23 | OSThread* self = TO_PTR(OSThread, ultramodern::this_thread()); 24 | if (next_thread->priority > self->priority) { 25 | ultramodern::thread_queue_pop(PASS_RDRAM running_queue); 26 | // Swap to the higher priority thread. 27 | swap_to_thread(PASS_RDRAM next_thread); 28 | } 29 | } 30 | } 31 | 32 | extern "C" void pause_self(RDRAM_ARG1) { 33 | while (true) { 34 | // Wait until an external message arrives, then allow the next thread to run. 35 | ultramodern::wait_for_external_message(PASS_RDRAM1); 36 | ultramodern::check_running_queue(PASS_RDRAM1); 37 | } 38 | } 39 | 40 | extern "C" void yield_self(RDRAM_ARG1) { 41 | ultramodern::wait_for_external_message(PASS_RDRAM1); 42 | ultramodern::check_running_queue(PASS_RDRAM1); 43 | } 44 | 45 | extern "C" void yield_self_1ms(RDRAM_ARG1) { 46 | ultramodern::wait_for_external_message_timed(PASS_RDRAM1, 1); 47 | ultramodern::check_running_queue(PASS_RDRAM1); 48 | } 49 | -------------------------------------------------------------------------------- /ultramodern/src/task_win32.cpp: -------------------------------------------------------------------------------- 1 | #ifdef _WIN32 2 | 3 | #include 4 | 5 | #include "ultramodern/ultra64.h" 6 | #include "ultramodern/ultramodern.hpp" 7 | 8 | extern "C" unsigned int sleep(unsigned int seconds) { 9 | Sleep(seconds * 1000); 10 | return 0; 11 | } 12 | 13 | #endif -------------------------------------------------------------------------------- /ultramodern/src/threadqueue.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "ultramodern/ultramodern.hpp" 4 | 5 | static PTR(OSThread) running_queue_impl = NULLPTR; 6 | 7 | static PTR(OSThread)* queue_to_ptr(RDRAM_ARG PTR(PTR(OSThread)) queue) { 8 | if (queue == ultramodern::running_queue) { 9 | return &running_queue_impl; 10 | } 11 | return TO_PTR(PTR(OSThread), queue); 12 | } 13 | 14 | void ultramodern::thread_queue_insert(RDRAM_ARG PTR(PTR(OSThread)) queue_, PTR(OSThread) toadd_) { 15 | PTR(OSThread)* cur = queue_to_ptr(PASS_RDRAM queue_); 16 | OSThread* toadd = TO_PTR(OSThread, toadd_); 17 | debug_printf("[Thread Queue] Inserting thread %d into queue 0x%08X\n", toadd->id, (uintptr_t)queue_); 18 | while (*cur && TO_PTR(OSThread, *cur)->priority > toadd->priority) { 19 | cur = &TO_PTR(OSThread, *cur)->next; 20 | } 21 | toadd->next = (*cur); 22 | toadd->queue = queue_; 23 | *cur = toadd_; 24 | 25 | debug_printf(" Contains:"); 26 | cur = queue_to_ptr(PASS_RDRAM queue_); 27 | while (*cur) { 28 | debug_printf("%d (%d) ", TO_PTR(OSThread, *cur)->id, TO_PTR(OSThread, *cur)->priority); 29 | cur = &TO_PTR(OSThread, *cur)->next; 30 | } 31 | debug_printf("\n"); 32 | } 33 | 34 | PTR(OSThread) ultramodern::thread_queue_pop(RDRAM_ARG PTR(PTR(OSThread)) queue_) { 35 | PTR(OSThread)* queue = queue_to_ptr(PASS_RDRAM queue_); 36 | PTR(OSThread) ret = *queue; 37 | *queue = TO_PTR(OSThread, ret)->next; 38 | TO_PTR(OSThread, ret)->queue = NULLPTR; 39 | debug_printf("[Thread Queue] Popped thread %d from queue 0x%08X\n", TO_PTR(OSThread, ret)->id, (uintptr_t)queue_); 40 | return ret; 41 | } 42 | 43 | bool ultramodern::thread_queue_remove(RDRAM_ARG PTR(PTR(OSThread)) queue_, PTR(OSThread) t_) { 44 | debug_printf("[Thread Queue] Removing thread %d from queue 0x%08X\n", TO_PTR(OSThread, t_)->id, (uintptr_t)queue_); 45 | 46 | PTR(PTR(OSThread)) cur = queue_; 47 | while (cur != NULLPTR) { 48 | PTR(OSThread)* cur_ptr = queue_to_ptr(PASS_RDRAM queue_); 49 | if (*cur_ptr == t_) { 50 | *cur_ptr = TO_PTR(OSThread, *cur_ptr)->next; 51 | return true; 52 | } 53 | cur = TO_PTR(OSThread, *cur_ptr)->next; 54 | } 55 | 56 | return false; 57 | } 58 | 59 | bool ultramodern::thread_queue_empty(RDRAM_ARG PTR(PTR(OSThread)) queue_) { 60 | PTR(OSThread)* queue = queue_to_ptr(PASS_RDRAM queue_); 61 | return *queue == NULLPTR; 62 | } 63 | 64 | PTR(OSThread) ultramodern::thread_queue_peek(RDRAM_ARG PTR(PTR(OSThread)) queue_) { 65 | PTR(OSThread)* queue = queue_to_ptr(PASS_RDRAM queue_); 66 | return *queue; 67 | } 68 | -------------------------------------------------------------------------------- /ultramodern/src/threads.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "ultramodern/ultra64.h" 7 | #include "ultramodern/ultramodern.hpp" 8 | #include "blockingconcurrentqueue.h" 9 | 10 | #include "ultramodern/threads.hpp" 11 | 12 | // Native APIs only used to set thread names for easier debugging 13 | #ifdef _WIN32 14 | #include 15 | #endif 16 | 17 | static ultramodern::threads::callbacks_t threads_callbacks; 18 | 19 | void ultramodern::threads::set_callbacks(const callbacks_t& callbacks) { 20 | threads_callbacks = callbacks; 21 | } 22 | 23 | std::string ultramodern::threads::get_game_thread_name(const OSThread* t) { 24 | if (threads_callbacks.get_game_thread_name == nullptr) { 25 | return "Game Thread " + std::to_string(t->id); 26 | } 27 | return threads_callbacks.get_game_thread_name(t); 28 | } 29 | 30 | extern "C" void bootproc(); 31 | 32 | thread_local bool is_main_thread = false; 33 | // Whether this thread is part of the game (i.e. the start thread or one spawned by osCreateThread) 34 | thread_local bool is_game_thread = false; 35 | thread_local PTR(OSThread) thread_self = NULLPTR; 36 | 37 | void ultramodern::set_main_thread() { 38 | ::is_game_thread = true; 39 | is_main_thread = true; 40 | } 41 | 42 | bool ultramodern::is_game_thread() { 43 | return ::is_game_thread; 44 | } 45 | 46 | #if 0 47 | int main(int argc, char** argv) { 48 | ultramodern::set_main_thread(); 49 | 50 | bootproc(); 51 | } 52 | #endif 53 | 54 | #if 1 55 | void run_thread_function(uint8_t* rdram, uint64_t addr, uint64_t sp, uint64_t arg); 56 | #else 57 | #define run_thread_function(func, sp, arg) func(arg) 58 | #endif 59 | 60 | #if defined(_WIN32) 61 | void ultramodern::set_native_thread_name(const std::string& name) { 62 | std::wstring wname{name.begin(), name.end()}; 63 | 64 | HRESULT r; 65 | r = SetThreadDescription( 66 | GetCurrentThread(), 67 | wname.c_str() 68 | ); 69 | } 70 | 71 | void ultramodern::set_native_thread_priority(ThreadPriority pri) { 72 | int nPriority = THREAD_PRIORITY_NORMAL; 73 | 74 | // Convert ThreadPriority to Win32 priority 75 | switch (pri) { 76 | case ThreadPriority::Low: 77 | nPriority = THREAD_PRIORITY_BELOW_NORMAL; 78 | break; 79 | case ThreadPriority::Normal: 80 | nPriority = THREAD_PRIORITY_NORMAL; 81 | break; 82 | case ThreadPriority::High: 83 | nPriority = THREAD_PRIORITY_ABOVE_NORMAL; 84 | break; 85 | case ThreadPriority::VeryHigh: 86 | nPriority = THREAD_PRIORITY_HIGHEST; 87 | break; 88 | case ThreadPriority::Critical: 89 | nPriority = THREAD_PRIORITY_TIME_CRITICAL; 90 | break; 91 | default: 92 | throw std::runtime_error("Invalid thread priority!"); 93 | break; 94 | } 95 | // SetThreadPriority(GetCurrentThread(), nPriority); 96 | } 97 | #elif defined(__linux__) 98 | #include 99 | 100 | void ultramodern::set_native_thread_name(const std::string& name) { 101 | if (name.length() > 15) { 102 | // Linux only accepts up to 16 characters including the null terminator for a thread name. 103 | debug_printf("[Thread] The thread name '%s' will be truncated to 15 characters", name.c_str()); 104 | } 105 | 106 | prctl(PR_SET_NAME, name.c_str()); 107 | } 108 | 109 | void ultramodern::set_native_thread_priority(ThreadPriority pri) { 110 | // TODO linux thread priority 111 | // printf("set_native_thread_priority unimplemented\n"); 112 | // int nPriority = THREAD_PRIORITY_NORMAL; 113 | 114 | // // Convert ThreadPriority to Win32 priority 115 | // switch (pri) { 116 | // case ThreadPriority::Low: 117 | // nPriority = THREAD_PRIORITY_BELOW_NORMAL; 118 | // break; 119 | // case ThreadPriority::Normal: 120 | // nPriority = THREAD_PRIORITY_NORMAL; 121 | // break; 122 | // case ThreadPriority::High: 123 | // nPriority = THREAD_PRIORITY_ABOVE_NORMAL; 124 | // break; 125 | // case ThreadPriority::VeryHigh: 126 | // nPriority = THREAD_PRIORITY_HIGHEST; 127 | // break; 128 | // case ThreadPriority::Critical: 129 | // nPriority = THREAD_PRIORITY_TIME_CRITICAL; 130 | // break; 131 | // default: 132 | // throw std::runtime_error("Invalid thread priority!"); 133 | // break; 134 | // } 135 | } 136 | #elif defined(__APPLE__) 137 | void ultramodern::set_native_thread_name(const std::string& name) { 138 | if (name.length() > 15) { 139 | // Macs seem to only accept up to 16 characters including the null terminator for a thread name. 140 | debug_printf("[Thread] The thread name '%s' will be truncated to 15 characters", name.c_str()); 141 | } 142 | 143 | pthread_setname_np(name.c_str()); 144 | } 145 | 146 | void ultramodern::set_native_thread_priority(ThreadPriority pri) {} 147 | #endif 148 | 149 | void wait_for_resumed(RDRAM_ARG UltraThreadContext* thread_context) { 150 | thread_context->running.wait(); 151 | // If this thread's context was replaced by another thread or deleted, destroy it again from its own context. 152 | // This will trigger thread cleanup instead. 153 | if (TO_PTR(OSThread, ultramodern::this_thread())->context != thread_context) { 154 | osDestroyThread(PASS_RDRAM NULLPTR); 155 | } 156 | } 157 | 158 | void resume_thread(OSThread* t) { 159 | debug_printf("[Thread] Resuming execution of thread %d\n", t->id); 160 | t->context->running.signal(); 161 | } 162 | 163 | void run_next_thread(RDRAM_ARG1) { 164 | if (ultramodern::thread_queue_empty(PASS_RDRAM ultramodern::running_queue)) { 165 | throw std::runtime_error("No threads left to run!\n"); 166 | } 167 | 168 | OSThread* to_run = TO_PTR(OSThread, ultramodern::thread_queue_pop(PASS_RDRAM ultramodern::running_queue)); 169 | debug_printf("[Scheduling] Resuming execution of thread %d\n", to_run->id); 170 | to_run->context->running.signal(); 171 | } 172 | 173 | void ultramodern::run_next_thread_and_wait(RDRAM_ARG1) { 174 | UltraThreadContext* cur_context = TO_PTR(OSThread, thread_self)->context; 175 | run_next_thread(PASS_RDRAM1); 176 | wait_for_resumed(PASS_RDRAM cur_context); 177 | } 178 | 179 | void ultramodern::resume_thread_and_wait(RDRAM_ARG OSThread *t) { 180 | UltraThreadContext* cur_context = TO_PTR(OSThread, thread_self)->context; 181 | resume_thread(t); 182 | wait_for_resumed(PASS_RDRAM cur_context); 183 | } 184 | 185 | static void _thread_func(RDRAM_ARG PTR(OSThread) self_, PTR(thread_func_t) entrypoint, PTR(void) arg, UltraThreadContext* thread_context) { 186 | OSThread *self = TO_PTR(OSThread, self_); 187 | debug_printf("[Thread] Thread created: %d\n", self->id); 188 | thread_self = self_; 189 | is_game_thread = true; 190 | 191 | // Set the thread name 192 | ultramodern::set_native_thread_name(ultramodern::threads::get_game_thread_name(self)); 193 | ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::High); 194 | 195 | // Signal the initialized semaphore to indicate that this thread can be started. 196 | thread_context->initialized.signal(); 197 | 198 | debug_printf("[Thread] Thread waiting to be started: %d\n", self->id); 199 | 200 | // Wait until the thread is marked as running. 201 | try { 202 | wait_for_resumed(PASS_RDRAM thread_context); 203 | } catch (ultramodern::thread_terminated& terminated) { 204 | } 205 | 206 | // Make sure the thread wasn't replaced or destroyed before it was started. 207 | if (self->context == thread_context) { 208 | debug_printf("[Thread] Thread started: %d\n", self->id); 209 | try { 210 | // Run the thread's function with the provided argument. 211 | run_thread_function(PASS_RDRAM entrypoint, self->sp, arg); 212 | } catch (ultramodern::thread_terminated& terminated) { 213 | } 214 | } 215 | else { 216 | debug_printf("[Thread] Thread destroyed before being started: %d\n", self->id); 217 | } 218 | 219 | // Check if the thread hasn't been destroyed or replaced. If so, then the thread terminated or destroyed itself, 220 | // so mark this thread as destroyed and run the next queued thread. 221 | if (self->context == thread_context) { 222 | self->context = nullptr; 223 | run_next_thread(PASS_RDRAM1); 224 | } 225 | 226 | // Dispose of this thread now that it's completed or terminated. 227 | ultramodern::cleanup_thread(thread_context); 228 | } 229 | 230 | extern "C" void osStartThread(RDRAM_ARG PTR(OSThread) t_) { 231 | OSThread* t = TO_PTR(OSThread, t_); 232 | debug_printf("[os] Start Thread %d\n", t->id); 233 | 234 | // If this is a game thread, insert the new thread into the running queue and then check the running queue. 235 | if (thread_self) { 236 | ultramodern::schedule_running_thread(PASS_RDRAM t_); 237 | ultramodern::check_running_queue(PASS_RDRAM1); 238 | } 239 | // Otherwise, immediately start the thread and terminate this one. 240 | else { 241 | t->state = OSThreadState::QUEUED; 242 | resume_thread(t); 243 | //throw ultramodern::thread_terminated{}; 244 | } 245 | } 246 | 247 | extern "C" void osCreateThread(RDRAM_ARG PTR(OSThread) t_, OSId id, PTR(thread_func_t) entrypoint, PTR(void) arg, PTR(void) sp, OSPri pri) { 248 | debug_printf("[os] Create Thread %d\n", id); 249 | OSThread *t = TO_PTR(OSThread, t_); 250 | 251 | t->next = NULLPTR; 252 | t->queue = NULLPTR; 253 | t->priority = pri; 254 | t->id = id; 255 | t->state = OSThreadState::STOPPED; 256 | t->sp = sp - 0x10; // Set up the first stack frame 257 | 258 | // Spawn a new thread, which will immediately pause itself and wait until it's been started. 259 | // Pass the context as an argument to the thread function to ensure that it can't get cleared before the thread captures its value. 260 | UltraThreadContext* context = new UltraThreadContext{}; 261 | t->context = context; 262 | context->host_thread = std::thread{_thread_func, PASS_RDRAM t_, entrypoint, arg, t->context}; 263 | 264 | // Wait until the thread is initialized to indicate that it's ready to be started. 265 | context->initialized.wait(); 266 | debug_printf("[os] Thread %d is ready to be started\n", t->id); 267 | } 268 | 269 | extern "C" void osStopThread(RDRAM_ARG PTR(OSThread) t_) { 270 | if (t_ == NULLPTR) { 271 | t_ = thread_self; 272 | } 273 | // Check if the thread is stopping itself (arg is null or thread_self). 274 | if (t_ == thread_self) { 275 | ultramodern::run_next_thread_and_wait(PASS_RDRAM1); 276 | } 277 | else { 278 | assert(false); 279 | } 280 | } 281 | 282 | extern "C" void osDestroyThread(RDRAM_ARG PTR(OSThread) t_) { 283 | if (t_ == NULLPTR) { 284 | t_ = thread_self; 285 | } 286 | OSThread* t = TO_PTR(OSThread, t_); 287 | // Check if the thread is destroying itself (arg is null or thread_self) 288 | if (t_ == thread_self) { 289 | throw ultramodern::thread_terminated{}; 290 | } 291 | // Otherwise if the thread isn't stopped, remove it from its currrent queue., 292 | if (t->state != OSThreadState::STOPPED) { 293 | ultramodern::thread_queue_remove(PASS_RDRAM t->queue, t_); 294 | } 295 | // Check if the thread has already been destroyed to prevent destroying it again. 296 | UltraThreadContext* cur_context = t->context; 297 | if (cur_context != nullptr) { 298 | // Mark the target thread as destroyed and resume it. When it starts it'll check this and terminate itself instead of resuming. 299 | t->context = nullptr; 300 | cur_context->running.signal(); 301 | } 302 | } 303 | 304 | extern "C" void osSetThreadPri(RDRAM_ARG PTR(OSThread) t_, OSPri pri) { 305 | if (t_ == NULLPTR) { 306 | t_ = thread_self; 307 | } 308 | OSThread* t = TO_PTR(OSThread, t_); 309 | 310 | if (t->priority != pri) { 311 | t->priority = pri; 312 | 313 | if (t_ != ultramodern::this_thread() && t->state != OSThreadState::STOPPED) { 314 | ultramodern::thread_queue_remove(PASS_RDRAM t->queue, t_); 315 | ultramodern::thread_queue_insert(PASS_RDRAM t->queue, t_); 316 | } 317 | 318 | ultramodern::check_running_queue(PASS_RDRAM1); 319 | } 320 | } 321 | 322 | extern "C" OSPri osGetThreadPri(RDRAM_ARG PTR(OSThread) t) { 323 | if (t == NULLPTR) { 324 | t = thread_self; 325 | } 326 | return TO_PTR(OSThread, t)->priority; 327 | } 328 | 329 | extern "C" OSId osGetThreadId(RDRAM_ARG PTR(OSThread) t) { 330 | if (t == NULLPTR) { 331 | t = thread_self; 332 | } 333 | return TO_PTR(OSThread, t)->id; 334 | } 335 | 336 | PTR(OSThread) ultramodern::this_thread() { 337 | return thread_self; 338 | } 339 | 340 | static std::thread thread_cleaner_thread; 341 | static moodycamel::BlockingConcurrentQueue deleted_threads{}; 342 | extern std::atomic_bool exited; 343 | 344 | void thread_cleaner_func() { 345 | using namespace std::chrono_literals; 346 | while (!exited) { 347 | UltraThreadContext* to_delete; 348 | if (deleted_threads.wait_dequeue_timed(to_delete, 10ms)) { 349 | debug_printf("[Cleanup] Deleting thread context %p\n", to_delete); 350 | 351 | to_delete->host_thread.join(); 352 | delete to_delete; 353 | } 354 | } 355 | } 356 | 357 | void ultramodern::init_thread_cleanup() { 358 | thread_cleaner_thread = std::thread{thread_cleaner_func}; 359 | } 360 | 361 | void ultramodern::cleanup_thread(UltraThreadContext *cur_context) { 362 | deleted_threads.enqueue(cur_context); 363 | } 364 | 365 | void ultramodern::join_thread_cleaner_thread() { 366 | thread_cleaner_thread.join(); 367 | } 368 | -------------------------------------------------------------------------------- /ultramodern/src/timer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "blockingconcurrentqueue.h" 5 | 6 | #include "ultramodern/ultra64.h" 7 | #include "ultramodern/ultramodern.hpp" 8 | 9 | #ifdef _WIN32 10 | #define WIN32_LEAN_AND_MEAN 11 | #include "Windows.h" 12 | #endif 13 | 14 | // Start time for the program 15 | static std::chrono::high_resolution_clock::time_point start_time = std::chrono::high_resolution_clock::now(); 16 | // Game speed multiplier (1 means no speedup) 17 | constexpr uint32_t speed_multiplier = 1; 18 | // N64 CPU counter ticks per millisecond 19 | constexpr uint32_t counter_per_ms = 46'875 * speed_multiplier; 20 | 21 | struct OSTimer { 22 | PTR(OSTimer) unused1; 23 | PTR(OSTimer) unused2; 24 | OSTime interval; 25 | OSTime timestamp; 26 | PTR(OSMesgQueue) mq; 27 | OSMesg msg; 28 | }; 29 | 30 | struct AddTimerAction { 31 | PTR(OSTimer) timer; 32 | }; 33 | 34 | struct RemoveTimerAction { 35 | PTR(OSTimer) timer; 36 | }; 37 | 38 | using Action = std::variant; 39 | 40 | struct { 41 | std::thread thread; 42 | moodycamel::BlockingConcurrentQueue action_queue{}; 43 | } timer_context; 44 | 45 | uint64_t duration_to_ticks(std::chrono::high_resolution_clock::duration duration) { 46 | uint64_t delta_micros = std::chrono::duration_cast(duration).count(); 47 | // More accurate than using a floating point timer, will only overflow after running for 12.47 years 48 | // Units: (micros * (counts/millis)) / (micros/millis) = counts 49 | uint64_t total_count = (delta_micros * counter_per_ms) / 1000; 50 | 51 | return total_count; 52 | } 53 | 54 | std::chrono::microseconds ticks_to_duration(uint64_t ticks) { 55 | using namespace std::chrono_literals; 56 | return ticks * 1000us / counter_per_ms; 57 | } 58 | 59 | std::chrono::high_resolution_clock::time_point ticks_to_timepoint(uint64_t ticks) { 60 | return start_time + ticks_to_duration(ticks); 61 | } 62 | 63 | uint64_t time_now() { 64 | return duration_to_ticks(std::chrono::high_resolution_clock::now() - start_time); 65 | } 66 | 67 | void timer_thread(RDRAM_ARG1) { 68 | ultramodern::set_native_thread_name("Timer Thread"); 69 | ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::VeryHigh); 70 | 71 | // Lambda comparator function to keep the set ordered 72 | auto timer_sort = [PASS_RDRAM1](PTR(OSTimer) a_, PTR(OSTimer) b_) { 73 | OSTimer* a = TO_PTR(OSTimer, a_); 74 | OSTimer* b = TO_PTR(OSTimer, b_); 75 | 76 | // Order by timestamp if the timers have different timestamps 77 | if (a->timestamp != b->timestamp) { 78 | return a->timestamp < b->timestamp; 79 | } 80 | 81 | // If they have the exact same timestamp then order by address instead 82 | return a < b; 83 | }; 84 | 85 | // Ordered set of timers that are currently active 86 | std::set active_timers{timer_sort}; 87 | 88 | // Lambda to process a timer action to handle adding and removing timers 89 | auto process_timer_action = [&](const Action& action) { 90 | // Determine the action type and act on it 91 | if (const auto* add_action = std::get_if(&action)) { 92 | active_timers.insert(add_action->timer); 93 | } else if (const auto* remove_action = std::get_if(&action)) { 94 | active_timers.erase(remove_action->timer); 95 | } 96 | }; 97 | 98 | while (true) { 99 | // Empty the action queue 100 | Action cur_action; 101 | while (timer_context.action_queue.try_dequeue(cur_action)) { 102 | process_timer_action(cur_action); 103 | } 104 | 105 | // If there's no timer to act on, wait for one to come in from the action queue 106 | while (active_timers.empty()) { 107 | timer_context.action_queue.wait_dequeue(cur_action); 108 | process_timer_action(cur_action); 109 | } 110 | 111 | // Get the timer that's closest to running out 112 | PTR(OSTimer) cur_timer_ = *active_timers.begin(); 113 | OSTimer* cur_timer = TO_PTR(OSTimer, cur_timer_); 114 | 115 | // Remove the timer from the queue (it may get readded if waiting is interrupted) 116 | active_timers.erase(cur_timer_); 117 | 118 | // Determine how long to wait to reach the timer's timestamp 119 | auto wait_duration = ticks_to_timepoint(cur_timer->timestamp) - std::chrono::high_resolution_clock::now(); 120 | 121 | // Wait for either the duration to complete or a new action to come through 122 | if (wait_duration.count() >= 0 && timer_context.action_queue.wait_dequeue_timed(cur_action, wait_duration)) { 123 | // Timer was interrupted by a new action 124 | // Add the current timer back to the queue (done first in case the action is to remove this timer) 125 | active_timers.insert(cur_timer_); 126 | // Process the new action 127 | process_timer_action(cur_action); 128 | } 129 | else { 130 | // Waiting for the timer completed, so send the timer's message to its message queue 131 | osSendMesg(PASS_RDRAM cur_timer->mq, cur_timer->msg, OS_MESG_NOBLOCK); 132 | // If the timer has a specified interval then reload it with that value 133 | if (cur_timer->interval != 0) { 134 | cur_timer->timestamp = cur_timer->interval + time_now(); 135 | active_timers.insert(cur_timer_); 136 | } 137 | } 138 | } 139 | } 140 | 141 | void ultramodern::init_timers(RDRAM_ARG1) { 142 | timer_context.thread = std::thread{ timer_thread, PASS_RDRAM1 }; 143 | timer_context.thread.detach(); 144 | } 145 | 146 | uint32_t ultramodern::get_speed_multiplier() { 147 | return speed_multiplier; 148 | } 149 | 150 | std::chrono::high_resolution_clock::time_point ultramodern::get_start() { 151 | return start_time; 152 | } 153 | 154 | std::chrono::high_resolution_clock::duration ultramodern::time_since_start() { 155 | return std::chrono::high_resolution_clock::now() - start_time; 156 | } 157 | 158 | extern "C" u32 osGetCount() { 159 | uint64_t total_count = time_now(); 160 | 161 | // Allow for overflows, which is how osGetCount behaves 162 | return (uint32_t)total_count; 163 | } 164 | 165 | extern "C" OSTime osGetTime() { 166 | uint64_t total_count = time_now(); 167 | 168 | return total_count; 169 | } 170 | 171 | extern "C" int osSetTimer(RDRAM_ARG PTR(OSTimer) t_, OSTime countdown, OSTime interval, PTR(OSMesgQueue) mq, OSMesg msg) { 172 | OSTimer* t = TO_PTR(OSTimer, t_); 173 | 174 | // Determine the time when this timer will trigger off 175 | if (countdown == 0) { 176 | // Set the timestamp based on the interval 177 | t->timestamp = interval + time_now(); 178 | } else { 179 | t->timestamp = countdown + time_now(); 180 | } 181 | t->interval = interval; 182 | t->mq = mq; 183 | t->msg = msg; 184 | 185 | timer_context.action_queue.enqueue(AddTimerAction{ t_ }); 186 | 187 | return 0; 188 | } 189 | 190 | extern "C" int osStopTimer(RDRAM_ARG PTR(OSTimer) t_) { 191 | timer_context.action_queue.enqueue(RemoveTimerAction{ t_ }); 192 | 193 | // TODO don't blindly return 0 here; requires some response from the timer thread to know what the returned value was 194 | return 0; 195 | } 196 | 197 | #ifdef _WIN32 198 | 199 | // The implementations of std::chrono::sleep_until and sleep_for were affected by changing the system clock backwards in older versions 200 | // of Microsoft's STL. This was fixed as of Visual Studio 2022 17.9, but to be safe ultramodern uses Win32 Sleep directly. 201 | void ultramodern::sleep_milliseconds(uint32_t millis) { 202 | Sleep(millis); 203 | } 204 | 205 | void ultramodern::sleep_until(const std::chrono::high_resolution_clock::time_point& time_point) { 206 | auto time_now = std::chrono::high_resolution_clock::now(); 207 | if (time_point > time_now) { 208 | long long delta_ms = std::chrono::ceil(time_point - time_now).count(); 209 | // printf("Sleeping %lld %d ms\n", delta_ms, (uint32_t)delta_ms); 210 | Sleep(delta_ms); 211 | } 212 | } 213 | 214 | #else 215 | 216 | void ultramodern::sleep_milliseconds(uint32_t millis) { 217 | std::this_thread::sleep_for(std::chrono::milliseconds{millis}); 218 | } 219 | 220 | void ultramodern::sleep_until(const std::chrono::high_resolution_clock::time_point& time_point) { 221 | std::this_thread::sleep_until(time_point); 222 | } 223 | 224 | #endif 225 | -------------------------------------------------------------------------------- /ultramodern/src/ultrainit.cpp: -------------------------------------------------------------------------------- 1 | #include "ultramodern/ultra64.h" 2 | #include "ultramodern/ultramodern.hpp" 3 | 4 | void ultramodern::set_callbacks( 5 | const rsp::callbacks_t& rsp_callbacks, 6 | const renderer::callbacks_t& renderer_callbacks, 7 | const audio_callbacks_t& audio_callbacks, 8 | const input::callbacks_t& input_callbacks, 9 | const gfx_callbacks_t& gfx_callbacks, 10 | const events::callbacks_t& events_callbacks, 11 | const error_handling::callbacks_t& error_handling_callbacks, 12 | const threads::callbacks_t& threads_callbacks 13 | ) { 14 | ultramodern::rsp::set_callbacks(rsp_callbacks); 15 | ultramodern::renderer::set_callbacks(renderer_callbacks); 16 | ultramodern::set_audio_callbacks(audio_callbacks); 17 | ultramodern::input::set_callbacks(input_callbacks); 18 | (void)gfx_callbacks; // nothing yet 19 | ultramodern::events::set_callbacks(events_callbacks); 20 | ultramodern::error_handling::set_callbacks(error_handling_callbacks); 21 | ultramodern::threads::set_callbacks(threads_callbacks); 22 | } 23 | 24 | void ultramodern::preinit(RDRAM_ARG ultramodern::renderer::WindowHandle window_handle) { 25 | ultramodern::set_main_thread(); 26 | ultramodern::init_events(PASS_RDRAM window_handle); 27 | ultramodern::init_timers(PASS_RDRAM1); 28 | ultramodern::init_audio(); 29 | ultramodern::init_thread_cleanup(); 30 | } 31 | 32 | extern "C" void osInitialize() { 33 | } 34 | --------------------------------------------------------------------------------