Migrate to N64ModernRuntime (#354)

This commit is contained in:
David Chavez 2024-06-05 01:12:43 +02:00 committed by GitHub
parent 6e9ee3498b
commit bec699f0bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 513 additions and 9741 deletions

View File

@ -56,7 +56,7 @@ jobs:
run: |
git clone https://github.com/Mr-Wiseguy/N64Recomp.git --recurse-submodules N64RecompSource
cd N64RecompSource
git checkout 6eb7d5bd3ee7f0b79f3fd7adbe931dccbacf7e1b
git checkout 8dfed04919b7bfdd0fd34ff049eed7020dea0d71
git submodule update --init --recursive
# enable ccache
@ -130,7 +130,7 @@ jobs:
run: |
git clone https://github.com/Mr-Wiseguy/N64Recomp.git --recurse-submodules N64RecompSource
cd N64RecompSource
git checkout 6eb7d5bd3ee7f0b79f3fd7adbe931dccbacf7e1b
git checkout 8dfed04919b7bfdd0fd34ff049eed7020dea0d71
git submodule update --init --recursive
# enable ccache

2
.gitignore vendored
View File

@ -1,6 +1,7 @@
# VSCode file settings
.vscode/settings.json
.vscode/c_cpp_properties.json
.vscode/launch.json
# Input elf and rom files
*.elf
@ -56,3 +57,4 @@ node_modules/
# Recompiler Linux binary
N64Recomp
.DS_Store

3
.gitmodules vendored
View File

@ -16,3 +16,6 @@
[submodule "lib/sse2neon"]
path = lib/sse2neon
url = https://github.com/DLTcollab/sse2neon.git
[submodule "lib/N64ModernRuntime"]
path = lib/N64ModernRuntime
url = git@github.com:N64Recomp/N64ModernRuntime.git

View File

@ -13,10 +13,6 @@ if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
cmake_policy(SET CMP0135 NEW)
endif()
if(UNIX AND NOT APPLE)
set(LINUX TRUE)
endif()
set (CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
@ -35,6 +31,8 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/lib/lunasvg)
SET(ENABLE_SVG_PLUGIN ON CACHE BOOL "" FORCE)
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/RmlUi)
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime)
target_include_directories(rt64 PRIVATE ${CMAKE_BINARY_DIR}/rt64/src)
# RecompiledFuncs - Library containing the primary recompiler output
@ -48,6 +46,8 @@ target_compile_options(RecompiledFuncs PRIVATE
target_include_directories(RecompiledFuncs PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/ultramodern/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/librecomp/include
)
file(GLOB FUNC_C_SOURCES ${CMAKE_SOURCE_DIR}/RecompiledFuncs/*.c)
@ -65,6 +65,8 @@ target_compile_options(PatchesLib PRIVATE
target_include_directories(PatchesLib PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/ultramodern/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/librecomp/include
)
target_sources(PatchesLib PRIVATE
@ -92,8 +94,8 @@ add_custom_command(OUTPUT
${CMAKE_SOURCE_DIR}/RecompiledPatches/patches.c
${CMAKE_SOURCE_DIR}/RecompiledPatches/recomp_overlays.inl
${CMAKE_SOURCE_DIR}/RecompiledPatches/funcs.h
${CMAKE_SOURCE_DIR}/src/recomp/patch_loading.cpp
COMMAND ./N64Recomp patches.toml && ${CMAKE_COMMAND} -E touch ${CMAKE_SOURCE_DIR}/src/recomp/patch_loading.cpp
# TODO: Look into why modifying patches requires two builds to take
COMMAND ./N64Recomp patches.toml
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
DEPENDS ${CMAKE_SOURCE_DIR}/patches/patches.bin
)
@ -112,39 +114,9 @@ if (EXISTS ${CMAKE_SOURCE_DIR}/shadercache/mm_shader_cache.bin)
endif()
set (SOURCES
${CMAKE_SOURCE_DIR}/ultramodern/audio.cpp
${CMAKE_SOURCE_DIR}/ultramodern/events.cpp
${CMAKE_SOURCE_DIR}/ultramodern/mesgqueue.cpp
${CMAKE_SOURCE_DIR}/ultramodern/misc_ultra.cpp
${CMAKE_SOURCE_DIR}/ultramodern/port_main.c
${CMAKE_SOURCE_DIR}/ultramodern/scheduling.cpp
${CMAKE_SOURCE_DIR}/ultramodern/threadqueue.cpp
${CMAKE_SOURCE_DIR}/ultramodern/task_win32.cpp
${CMAKE_SOURCE_DIR}/ultramodern/threads.cpp
${CMAKE_SOURCE_DIR}/ultramodern/timer.cpp
${CMAKE_SOURCE_DIR}/ultramodern/ultrainit.cpp
${CMAKE_SOURCE_DIR}/ultramodern/rt64_layer.cpp
${CMAKE_SOURCE_DIR}/src/recomp/ai.cpp
${CMAKE_SOURCE_DIR}/src/recomp/cont.cpp
${CMAKE_SOURCE_DIR}/src/recomp/dp.cpp
${CMAKE_SOURCE_DIR}/src/recomp/eep.cpp
${CMAKE_SOURCE_DIR}/src/recomp/euc-jp.cpp
${CMAKE_SOURCE_DIR}/src/recomp/files.cpp
${CMAKE_SOURCE_DIR}/src/recomp/flash.cpp
${CMAKE_SOURCE_DIR}/src/recomp/math_routines.cpp
${CMAKE_SOURCE_DIR}/src/recomp/overlays.cpp
${CMAKE_SOURCE_DIR}/src/recomp/patch_loading.cpp
${CMAKE_SOURCE_DIR}/src/recomp/pak.cpp
${CMAKE_SOURCE_DIR}/src/recomp/pi.cpp
${CMAKE_SOURCE_DIR}/src/recomp/ultra_stubs.cpp
${CMAKE_SOURCE_DIR}/src/recomp/ultra_translation.cpp
${CMAKE_SOURCE_DIR}/src/recomp/print.cpp
${CMAKE_SOURCE_DIR}/src/recomp/recomp.cpp
${CMAKE_SOURCE_DIR}/src/recomp/sp.cpp
${CMAKE_SOURCE_DIR}/src/recomp/vi.cpp
${CMAKE_SOURCE_DIR}/src/main/main.cpp
${CMAKE_SOURCE_DIR}/src/main/register_overlays.cpp
${CMAKE_SOURCE_DIR}/src/main/register_patches.cpp
${CMAKE_SOURCE_DIR}/src/game/input.cpp
${CMAKE_SOURCE_DIR}/src/game/controls.cpp
@ -238,7 +210,7 @@ if (WIN32)
target_sources(Zelda64Recompiled PRIVATE ${CMAKE_SOURCE_DIR}/icons/app.rc)
endif()
if (LINUX)
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
find_package(SDL2 REQUIRED)
find_package(X11 REQUIRED)
@ -282,6 +254,8 @@ target_link_libraries(Zelda64Recompiled PRIVATE
PatchesLib
RecompiledFuncs
SDL2
librecomp
ultramodern
rt64
RmlCore
RmlDebugger

9
include/ovl_patches.hpp Normal file
View File

@ -0,0 +1,9 @@
#ifndef __OVL_PATCHES_HPP__
#define __OVL_PATCHES_HPP__
namespace zelda64 {
void register_overlays();
void register_patches();
}
#endif

View File

@ -1,327 +0,0 @@
#ifndef __RECOMP_H__
#define __RECOMP_H__
#include <stdint.h>
#include <math.h>
#include <assert.h>
#include <setjmp.h>
#include <malloc.h>
#if 0 // treat GPRs as 32-bit, should be better codegen
typedef uint32_t gpr;
#define SIGNED(val) \
((int32_t)(val))
#else
typedef uint64_t gpr;
#define SIGNED(val) \
((int64_t)(val))
#endif
#define ADD32(a, b) \
((gpr)(int32_t)((a) + (b)))
#define SUB32(a, b) \
((gpr)(int32_t)((a) - (b)))
#define MEM_W(offset, reg) \
(*(int32_t*)(rdram + ((((reg) + (offset))) - 0xFFFFFFFF80000000)))
//(*(int32_t*)(rdram + ((((reg) + (offset))) & 0x3FFFFFF)))
#define MEM_H(offset, reg) \
(*(int16_t*)(rdram + ((((reg) + (offset)) ^ 2) - 0xFFFFFFFF80000000)))
//(*(int16_t*)(rdram + ((((reg) + (offset)) ^ 2) & 0x3FFFFFF)))
#define MEM_B(offset, reg) \
(*(int8_t*)(rdram + ((((reg) + (offset)) ^ 3) - 0xFFFFFFFF80000000)))
//(*(int8_t*)(rdram + ((((reg) + (offset)) ^ 3) & 0x3FFFFFF)))
#define MEM_HU(offset, reg) \
(*(uint16_t*)(rdram + ((((reg) + (offset)) ^ 2) - 0xFFFFFFFF80000000)))
//(*(uint16_t*)(rdram + ((((reg) + (offset)) ^ 2) & 0x3FFFFFF)))
#define MEM_BU(offset, reg) \
(*(uint8_t*)(rdram + ((((reg) + (offset)) ^ 3) - 0xFFFFFFFF80000000)))
//(*(uint8_t*)(rdram + ((((reg) + (offset)) ^ 3) & 0x3FFFFFF)))
#define SD(val, offset, reg) { \
*(uint32_t*)(rdram + ((((reg) + (offset) + 4)) - 0xFFFFFFFF80000000)) = (uint32_t)((gpr)(val) >> 0); \
*(uint32_t*)(rdram + ((((reg) + (offset) + 0)) - 0xFFFFFFFF80000000)) = (uint32_t)((gpr)(val) >> 32); \
}
//#define SD(val, offset, reg) { \
// *(uint32_t*)(rdram + ((((reg) + (offset) + 4)) & 0x3FFFFFF)) = (uint32_t)((val) >> 32); \
// *(uint32_t*)(rdram + ((((reg) + (offset) + 0)) & 0x3FFFFFF)) = (uint32_t)((val) >> 0); \
//}
static inline uint64_t load_doubleword(uint8_t* rdram, gpr reg, gpr offset) {
uint64_t ret = 0;
uint64_t lo = (uint64_t)(uint32_t)MEM_W(reg, offset + 4);
uint64_t hi = (uint64_t)(uint32_t)MEM_W(reg, offset + 0);
ret = (lo << 0) | (hi << 32);
return ret;
}
#define LD(offset, reg) \
load_doubleword(rdram, offset, reg)
static inline gpr do_lwl(uint8_t* rdram, gpr initial_value, gpr offset, gpr reg) {
// Calculate the overall address
gpr address = (offset + reg);
// Load the aligned word
gpr word_address = address & ~0x3;
uint32_t loaded_value = MEM_W(0, word_address);
// Mask the existing value and shift the loaded value appropriately
gpr misalignment = address & 0x3;
gpr masked_value = initial_value & ~(0xFFFFFFFFu << (misalignment * 8));
loaded_value <<= (misalignment * 8);
// Cast to int32_t to sign extend first
return (gpr)(int32_t)(masked_value | loaded_value);
}
static inline gpr do_lwr(uint8_t* rdram, gpr initial_value, gpr offset, gpr reg) {
// Calculate the overall address
gpr address = (offset + reg);
// Load the aligned word
gpr word_address = address & ~0x3;
uint32_t loaded_value = MEM_W(0, word_address);
// Mask the existing value and shift the loaded value appropriately
gpr misalignment = address & 0x3;
gpr masked_value = initial_value & ~(0xFFFFFFFFu >> (24 - misalignment * 8));
loaded_value >>= (24 - misalignment * 8);
// Cast to int32_t to sign extend first
return (gpr)(int32_t)(masked_value | loaded_value);
}
static inline void do_swl(uint8_t* rdram, gpr offset, gpr reg, gpr val) {
// Calculate the overall address
gpr address = (offset + reg);
// Get the initial value of the aligned word
gpr word_address = address & ~0x3;
uint32_t initial_value = MEM_W(0, word_address);
// Mask the initial value and shift the input value appropriately
gpr misalignment = address & 0x3;
uint32_t masked_initial_value = initial_value & ~(0xFFFFFFFFu >> (misalignment * 8));
uint32_t shifted_input_value = ((uint32_t)val) >> (misalignment * 8);
MEM_W(0, word_address) = masked_initial_value | shifted_input_value;
}
static inline void do_swr(uint8_t* rdram, gpr offset, gpr reg, gpr val) {
// Calculate the overall address
gpr address = (offset + reg);
// Get the initial value of the aligned word
gpr word_address = address & ~0x3;
uint32_t initial_value = MEM_W(0, word_address);
// Mask the initial value and shift the input value appropriately
gpr misalignment = address & 0x3;
uint32_t masked_initial_value = initial_value & ~(0xFFFFFFFFu << (24 - misalignment * 8));
uint32_t shifted_input_value = ((uint32_t)val) << (24 - misalignment * 8);
MEM_W(0, word_address) = masked_initial_value | shifted_input_value;
}
#define S32(val) \
((int32_t)(val))
#define U32(val) \
((uint32_t)(val))
#define S64(val) \
((int64_t)(val))
#define U64(val) \
((uint64_t)(val))
#define MUL_S(val1, val2) \
((val1) * (val2))
#define MUL_D(val1, val2) \
((val1) * (val2))
#define DIV_S(val1, val2) \
((val1) / (val2))
#define DIV_D(val1, val2) \
((val1) / (val2))
#define CVT_S_W(val) \
((float)((int32_t)(val)))
#define CVT_D_W(val) \
((double)((int32_t)(val)))
#define CVT_D_S(val) \
((double)(val))
#define CVT_S_D(val) \
((float)(val))
#define TRUNC_W_S(val) \
((int32_t)(val))
#define TRUNC_W_D(val) \
((int32_t)(val))
#define TRUNC_L_S(val) \
((int64_t)(val))
#define TRUNC_L_D(val) \
((int64_t)(val))
#define DEFAULT_ROUNDING_MODE 0
static inline int32_t do_cvt_w_s(float val, unsigned int rounding_mode) {
switch (rounding_mode) {
case 0: // round to nearest value
return (int32_t)lroundf(val);
case 1: // round to zero (truncate)
return (int32_t)val;
case 2: // round to positive infinity (ceil)
return (int32_t)ceilf(val);
case 3: // round to negative infinity (floor)
return (int32_t)floorf(val);
}
assert(0);
return 0;
}
#define CVT_W_S(val) \
do_cvt_w_s(val, rounding_mode)
static inline int32_t do_cvt_w_d(double val, unsigned int rounding_mode) {
switch (rounding_mode) {
case 0: // round to nearest value
return (int32_t)lround(val);
case 1: // round to zero (truncate)
return (int32_t)val;
case 2: // round to positive infinity (ceil)
return (int32_t)ceil(val);
case 3: // round to negative infinity (floor)
return (int32_t)floor(val);
}
assert(0);
return 0;
}
#define CVT_W_D(val) \
do_cvt_w_d(val, rounding_mode)
#define NAN_CHECK(val) \
assert(val == val)
//#define NAN_CHECK(val)
typedef union {
double d;
struct {
float fl;
float fh;
};
struct {
uint32_t u32l;
uint32_t u32h;
};
uint64_t u64;
} fpr;
typedef struct {
gpr r0, r1, r2, r3, r4, r5, r6, r7,
r8, r9, r10, r11, r12, r13, r14, r15,
r16, r17, r18, r19, r20, r21, r22, r23,
r24, r25, r26, r27, r28, r29, r30, r31;
fpr f0, f1, f2, f3, f4, f5, f6, f7,
f8, f9, f10, f11, f12, f13, f14, f15,
f16, f17, f18, f19, f20, f21, f22, f23,
f24, f25, f26, f27, f28, f29, f30, f31;
uint64_t hi, lo;
uint32_t* f_odd;
uint32_t status_reg;
uint8_t mips3_float_mode;
} recomp_context;
// Checks if the target is an even float register or that mips3 float mode is enabled
#define CHECK_FR(ctx, idx) \
assert(((idx) & 1) == 0 || (ctx)->mips3_float_mode)
#ifdef __cplusplus
extern "C" {
#endif
void cop0_status_write(recomp_context* ctx, gpr value);
gpr cop0_status_read(recomp_context* ctx);
void switch_error(const char* func, uint32_t vram, uint32_t jtbl);
void do_break(uint32_t vram);
typedef void (recomp_func_t)(uint8_t* rdram, recomp_context* ctx);
recomp_func_t* get_function(int32_t vram);
#define LOOKUP_FUNC(val) \
get_function((int32_t)(val))
extern int32_t section_addresses[];
#define LO16(x) \
((x) & 0xFFFF)
#define HI16(x) \
(((x) >> 16) + (((x) >> 15) & 1))
#define RELOC_HI16(section_index, offset) \
HI16(section_addresses[section_index] + (offset))
#define RELOC_LO16(section_index, offset) \
LO16(section_addresses[section_index] + (offset))
// For Banjo-Tooie
void recomp_syscall_handler(uint8_t* rdram, recomp_context* ctx, int32_t instruction_vram);
// For the Mario Party games (not working)
//// This has to be in this file so it can be inlined
//struct jmp_buf_storage {
// jmp_buf buffer;
//};
//
//struct RecompJmpBuf {
// int32_t owner;
// struct jmp_buf_storage* storage;
// uint64_t magic;
//};
//
//// Randomly generated constant
//#define SETJMP_MAGIC 0xe17afdfa939a437bu
//
//int32_t osGetThreadEx(void);
//
//#define setjmp_recomp(rdram, ctx) { \
// struct RecompJmpBuf* buf = (struct RecompJmpBuf*)(&rdram[(uint64_t)ctx->r4 - 0xFFFFFFFF80000000]); \
// \
// /* Check if this jump buffer was previously set up */ \
// if (buf->magic == SETJMP_MAGIC) { \
// /* If so, free the old jmp_buf */ \
// free(buf->storage); \
// } \
// \
// buf->magic = SETJMP_MAGIC; \
// buf->owner = osGetThreadEx(); \
// buf->storage = (struct jmp_buf_storage*)calloc(1, sizeof(struct jmp_buf_storage)); \
// ctx->r2 = setjmp(buf->storage->buffer); \
//}
void pause_self(uint8_t *rdram);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -1,54 +0,0 @@
#ifndef __RECOMP_CONFIG_H__
#define __RECOMP_CONFIG_H__
#include <filesystem>
#include <string_view>
#include "../ultramodern/config.hpp"
namespace recomp {
constexpr std::u8string_view program_id = u8"Zelda64Recompiled";
constexpr std::u8string_view mm_game_id = u8"mm.n64.us.1.0";
constexpr std::string_view program_name = "Zelda 64: Recompiled";
void load_config();
void save_config();
void reset_input_bindings();
void reset_cont_input_bindings();
void reset_kb_input_bindings();
std::filesystem::path get_app_folder_path();
bool get_debug_mode_enabled();
void set_debug_mode_enabled(bool enabled);
enum class AutosaveMode {
On,
Off,
OptionCount
};
enum class AnalogCamMode {
On,
Off,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(recomp::AutosaveMode, {
{recomp::AutosaveMode::On, "On"},
{recomp::AutosaveMode::Off, "Off"}
});
NLOHMANN_JSON_SERIALIZE_ENUM(recomp::AnalogCamMode, {
{recomp::AnalogCamMode::On, "On"},
{recomp::AnalogCamMode::Off, "Off"}
});
AutosaveMode get_autosave_mode();
void set_autosave_mode(AutosaveMode mode);
AnalogCamMode get_analog_cam_mode();
void set_analog_cam_mode(AnalogCamMode mode);
};
#endif

View File

@ -1,40 +0,0 @@
#ifndef __RECOMP_GAME__
#define __RECOMP_GAME__
#include <vector>
#include <filesystem>
#include "recomp.h"
#include "../ultramodern/ultramodern.hpp"
#include "rt64_layer.h"
namespace recomp {
enum class Game {
OoT,
MM,
None,
Quit
};
enum class RomValidationError {
Good,
FailedToOpen,
NotARom,
IncorrectRom,
NotYet,
IncorrectVersion,
OtherError
};
void check_all_stored_roms();
bool load_stored_rom(Game game);
RomValidationError select_rom(const std::filesystem::path& rom_path, Game game);
bool is_rom_valid(Game game);
bool is_rom_loaded();
void set_rom_contents(std::vector<uint8_t>&& new_rom);
void do_rom_read(uint8_t* rdram, gpr ram_address, uint32_t physical_addr, size_t num_bytes);
void do_rom_pio(uint8_t* rdram, gpr ram_address, uint32_t physical_addr);
void start(ultramodern::WindowHandle window_handle, const ultramodern::audio_callbacks_t& audio_callbacks, const ultramodern::input_callbacks_t& input_callbacks, const ultramodern::gfx_callbacks_t& gfx_callbacks);
void start_game(Game game);
void message_box(const char* message);
}
#endif

View File

@ -1,50 +0,0 @@
#ifndef __RECOMP_HELPERS__
#define __RECOMP_HELPERS__
#include "recomp.h"
#include "../ultramodern/ultra64.h"
template<int index, typename T>
T _arg(uint8_t* rdram, recomp_context* ctx) {
static_assert(index < 4, "Only args 0 through 3 supported");
gpr raw_arg = (&ctx->r4)[index];
if constexpr (std::is_same_v<T, float>) {
if constexpr (index < 2) {
static_assert(index != 1, "Floats in arg 1 not supported");
return ctx->f12.fl;
}
else {
// static_assert in else workaround
[] <bool flag = false>() {
static_assert(flag, "Floats in a2/a3 not supported");
}();
}
}
else if constexpr (std::is_pointer_v<T>) {
static_assert (!std::is_pointer_v<std::remove_pointer_t<T>>, "Double pointers not supported");
return TO_PTR(std::remove_pointer_t<T>, raw_arg);
}
else if constexpr (std::is_integral_v<T>) {
static_assert(sizeof(T) <= 4, "64-bit args not supported");
return static_cast<T>(raw_arg);
}
else {
// static_assert in else workaround
[] <bool flag = false>() {
static_assert(flag, "Unsupported type");
}();
}
}
template <typename T>
void _return(recomp_context* ctx, T val) {
static_assert(sizeof(T) <= 4 && "Only 32-bit value returns supported currently");
if (std::is_same_v<T, float>) {
ctx->f0.fl = val;
}
else if (std::is_integral_v<T> && sizeof(T) <= 4) {
ctx->r2 = int32_t(val);
}
}
#endif

View File

@ -14,6 +14,7 @@
namespace recomp {
// x-macros to build input enums and arrays.
// First parameter is the enum name, second parameter is the bit field for the input (or 0 if there is no associated one), third is the readable name.
// TODO refactor this to allow projects to rename these, or get rid of the readable name and leave that up to individual projects to map.
#define DEFINE_N64_BUTTON_INPUTS() \
DEFINE_INPUT(A, 0x8000, "Action") \
DEFINE_INPUT(B, 0x4000, "Attack/Cancel") \
@ -168,20 +169,6 @@ namespace recomp {
void apply_joystick_deadzone(float x_in, float y_in, float* x_out, float* y_out);
void set_right_analog_suppressed(bool suppressed);
enum class TargetingMode {
Switch,
Hold,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(recomp::TargetingMode, {
{recomp::TargetingMode::Switch, "Switch"},
{recomp::TargetingMode::Hold, "Hold"}
});
TargetingMode get_targeting_mode();
void set_targeting_mode(TargetingMode mode);
enum class BackgroundInputMode {
On,
Off,
@ -196,35 +183,8 @@ namespace recomp {
BackgroundInputMode get_background_input_mode();
void set_background_input_mode(BackgroundInputMode mode);
enum class CameraInvertMode {
InvertNone,
InvertX,
InvertY,
InvertBoth,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(recomp::CameraInvertMode, {
{recomp::CameraInvertMode::InvertNone, "InvertNone"},
{recomp::CameraInvertMode::InvertX, "InvertX"},
{recomp::CameraInvertMode::InvertY, "InvertY"},
{recomp::CameraInvertMode::InvertBoth, "InvertBoth"}
});
CameraInvertMode get_camera_invert_mode();
void set_camera_invert_mode(CameraInvertMode mode);
CameraInvertMode get_analog_camera_invert_mode();
void set_analog_camera_invert_mode(CameraInvertMode mode);
bool game_input_disabled();
bool all_input_disabled();
// TODO move these
void quicksave_save();
void quicksave_load();
void open_quit_game_prompt();
}
#endif

View File

@ -1,10 +0,0 @@
#ifndef __RECOMP_OVERLAYS_H__
#define __RECOMP_OVERLAYS_H__
#include <cstdint>
extern "C" void load_overlays(uint32_t rom, int32_t ram_addr, uint32_t size);
extern "C" void unload_overlays(int32_t ram_addr, uint32_t size);
void init_overlays();
#endif

View File

@ -14,7 +14,7 @@ namespace Rml {
class Event;
}
namespace recomp {
namespace recompui {
class UiEventListenerInstancer;
class MenuController {
@ -118,6 +118,8 @@ namespace recomp {
bool get_cont_active(void);
void set_cont_active(bool active);
void activate_mouse();
void message_box(const char* msg);
}
#endif

View File

@ -1,94 +0,0 @@
#ifndef __RSP_H__
#define __RSP_H__
#include "rsp_vu.h"
#include "recomp.h"
#include <cstdio>
enum class RspExitReason {
Invalid,
Broke,
ImemOverrun,
UnhandledJumpTarget,
Unsupported
};
extern uint8_t dmem[];
extern uint16_t rspReciprocals[512];
extern uint16_t rspInverseSquareRoots[512];
#define RSP_MEM_B(offset, addr) \
(*reinterpret_cast<int8_t*>(dmem + (0xFFF & (((offset) + (addr)) ^ 3))))
#define RSP_MEM_BU(offset, addr) \
(*reinterpret_cast<uint8_t*>(dmem + (0xFFF & (((offset) + (addr)) ^ 3))))
static inline uint32_t RSP_MEM_W_LOAD(uint32_t offset, uint32_t addr) {
uint32_t out;
for (int i = 0; i < 4; i++) {
reinterpret_cast<uint8_t*>(&out)[i ^ 3] = RSP_MEM_BU(offset + i, addr);
}
return out;
}
static inline void RSP_MEM_W_STORE(uint32_t offset, uint32_t addr, uint32_t val) {
for (int i = 0; i < 4; i++) {
RSP_MEM_BU(offset + i, addr) = reinterpret_cast<uint8_t*>(&val)[i ^ 3];
}
}
static inline uint32_t RSP_MEM_HU_LOAD(uint32_t offset, uint32_t addr) {
uint16_t out;
for (int i = 0; i < 2; i++) {
reinterpret_cast<uint8_t*>(&out)[(i + 2) ^ 3] = RSP_MEM_BU(offset + i, addr);
}
return out;
}
static inline uint32_t RSP_MEM_H_LOAD(uint32_t offset, uint32_t addr) {
int16_t out;
for (int i = 0; i < 2; i++) {
reinterpret_cast<uint8_t*>(&out)[(i + 2) ^ 3] = RSP_MEM_BU(offset + i, addr);
}
return out;
}
static inline void RSP_MEM_H_STORE(uint32_t offset, uint32_t addr, uint32_t val) {
for (int i = 0; i < 2; i++) {
RSP_MEM_BU(offset + i, addr) = reinterpret_cast<uint8_t*>(&val)[(i + 2) ^ 3];
}
}
#define RSP_ADD32(a, b) \
((int32_t)((a) + (b)))
#define RSP_SUB32(a, b) \
((int32_t)((a) - (b)))
#define RSP_SIGNED(val) \
((int32_t)(val))
#define SET_DMA_DMEM(dmem_addr) dma_dmem_address = (dmem_addr)
#define SET_DMA_DRAM(dram_addr) dma_dram_address = (dram_addr)
#define DO_DMA_READ(rd_len) dma_rdram_to_dmem(rdram, dma_dmem_address, dma_dram_address, (rd_len))
#define DO_DMA_WRITE(wr_len) dma_dmem_to_rdram(rdram, dma_dmem_address, dma_dram_address, (wr_len))
static inline void dma_rdram_to_dmem(uint8_t* rdram, uint32_t dmem_addr, uint32_t dram_addr, uint32_t rd_len) {
rd_len += 1; // Read length is inclusive
dram_addr &= 0xFFFFF8;
assert(dmem_addr + rd_len <= 0x1000);
for (uint32_t i = 0; i < rd_len; i++) {
RSP_MEM_B(i, dmem_addr) = MEM_B(0, (int64_t)(int32_t)(dram_addr + i + 0x80000000));
}
}
static inline void dma_dmem_to_rdram(uint8_t* rdram, uint32_t dmem_addr, uint32_t dram_addr, uint32_t wr_len) {
wr_len += 1; // Write length is inclusive
dram_addr &= 0xFFFFF8;
assert(dmem_addr + wr_len <= 0x1000);
for (uint32_t i = 0; i < wr_len; i++) {
MEM_B(0, (int64_t)(int32_t)(dram_addr + i + 0x80000000)) = RSP_MEM_B(i, dmem_addr);
}
}
#endif

View File

@ -1,203 +0,0 @@
// This file is modified from the Ares N64 emulator core. Ares can
// be found at https://github.com/ares-emulator/ares. The original license
// for this portion of Ares is as follows:
// ----------------------------------------------------------------------
// ares
//
// Copyright(c) 2004 - 2021 ares team, Near et al
//
// Permission to use, copy, modify, and /or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright noticeand this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS.IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// ----------------------------------------------------------------------
#include <cstdint>
#if defined(__x86_64__) || defined(_M_X64)
#define ARCHITECTURE_SUPPORTS_SSE4_1 1
#include <nmmintrin.h>
using v128 = __m128i;
#elif defined(__aarch64__) || defined(_M_ARM64)
#define ARCHITECTURE_SUPPORTS_SSE4_1 1
#include "sse2neon.h"
using v128 = __m128i;
#endif
namespace Accuracy {
namespace RSP {
#if ARCHITECTURE_SUPPORTS_SSE4_1
constexpr bool SISD = false;
constexpr bool SIMD = true;
#else
constexpr bool SISD = true;
constexpr bool SIMD = false;
#endif
}
}
using u8 = uint8_t;
using s8 = int8_t;
using u16 = uint16_t;
using s16 = int16_t;
using u32 = uint32_t;
using s32 = int32_t;
using u64 = uint64_t;
using s64 = int64_t;
using uint128_t = uint64_t[2];
template<u32 bits> inline auto sclamp(s64 x) -> s64 {
enum : s64 { b = 1ull << (bits - 1), m = b - 1 };
return (x > m) ? m : (x < -b) ? -b : x;
}
template<u32 bits> inline auto sclip(s64 x) -> s64 {
enum : u64 { b = 1ull << (bits - 1), m = b * 2 - 1 };
return ((x & m) ^ b) - b;
}
struct RSP {
using r32 = uint32_t;
using cr32 = const r32;
union r128 {
struct { uint64_t u128[2]; };
#if ARCHITECTURE_SUPPORTS_SSE4_1
struct { __m128i v128; };
operator __m128i() const { return v128; }
auto operator=(__m128i value) { v128 = value; }
#endif
auto byte(u32 index) -> uint8_t& { return ((uint8_t*)&u128)[15 - index]; }
auto byte(u32 index) const -> uint8_t { return ((uint8_t*)&u128)[15 - index]; }
auto element(u32 index) -> uint16_t& { return ((uint16_t*)&u128)[7 - index]; }
auto element(u32 index) const -> uint16_t { return ((uint16_t*)&u128)[7 - index]; }
auto u8(u32 index) -> uint8_t& { return ((uint8_t*)&u128)[15 - index]; }
auto u8(u32 index) const -> uint8_t { return ((uint8_t*)&u128)[15 - index]; }
auto s16(u32 index) -> int16_t& { return ((int16_t*)&u128)[7 - index]; }
auto s16(u32 index) const -> int16_t { return ((int16_t*)&u128)[7 - index]; }
auto u16(u32 index) -> uint16_t& { return ((uint16_t*)&u128)[7 - index]; }
auto u16(u32 index) const -> uint16_t { return ((uint16_t*)&u128)[7 - index]; }
//VCx registers
auto get(u32 index) const -> bool { return u16(index) != 0; }
auto set(u32 index, bool value) -> bool { return u16(index) = 0 - value, value; }
//vu-registers.cpp
inline auto operator()(u32 index) const -> r128;
};
using cr128 = const r128;
struct VU {
r128 r[32];
r128 acch, accm, accl;
r128 vcoh, vcol; //16-bit little endian
r128 vcch, vccl; //16-bit little endian
r128 vce; // 8-bit little endian
s16 divin;
s16 divout;
bool divdp;
} vpu;
static constexpr r128 zero{0};
static constexpr r128 invert{(uint64_t)-1, (uint64_t)-1};
inline auto accumulatorGet(u32 index) const -> u64;
inline auto accumulatorSet(u32 index, u64 value) -> void;
inline auto accumulatorSaturate(u32 index, bool slice, u16 negative, u16 positive) const -> u16;
inline auto CFC2(r32& rt, u8 rd) -> void;
inline auto CTC2(cr32& rt, u8 rd) -> void;
template<u8 e> inline auto LBV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LDV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LFV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LHV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LLV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LPV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LQV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LRV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LSV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LTV(u8 vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LUV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto LWV(r128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto MFC2(r32& rt, cr128& vs) -> void;
template<u8 e> inline auto MTC2(cr32& rt, r128& vs) -> void;
template<u8 e> inline auto SBV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SDV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SFV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SHV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SLV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SPV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SQV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SRV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SSV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto STV(u8 vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SUV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto SWV(cr128& vt, cr32& rs, s8 imm) -> void;
template<u8 e> inline auto VABS(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VADD(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VADDC(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VAND(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VCH(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VCL(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VCR(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VEQ(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VGE(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VLT(r128& vd, cr128& vs, cr128& vt) -> void;
template<bool U, u8 e>
inline auto VMACF(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMACF(r128& vd, cr128& vs, cr128& vt) -> void { VMACF<0, e>(vd, vs, vt); }
template<u8 e> inline auto VMACU(r128& vd, cr128& vs, cr128& vt) -> void { VMACF<1, e>(vd, vs, vt); }
inline auto VMACQ(r128& vd) -> void;
template<u8 e> inline auto VMADH(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMADL(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMADM(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMADN(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMOV(r128& vd, u8 de, cr128& vt) -> void;
template<u8 e> inline auto VMRG(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMUDH(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMUDL(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMUDM(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMUDN(r128& vd, cr128& vs, cr128& vt) -> void;
template<bool U, u8 e>
inline auto VMULF(r128& rd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VMULF(r128& rd, cr128& vs, cr128& vt) -> void { VMULF<0, e>(rd, vs, vt); }
template<u8 e> inline auto VMULU(r128& rd, cr128& vs, cr128& vt) -> void { VMULF<1, e>(rd, vs, vt); }
template<u8 e> inline auto VMULQ(r128& rd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VNAND(r128& rd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VNE(r128& vd, cr128& vs, cr128& vt) -> void;
inline auto VNOP() -> void;
template<u8 e> inline auto VNOR(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VNXOR(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VOR(r128& vd, cr128& vs, cr128& vt) -> void;
template<bool L, u8 e>
inline auto VRCP(r128& vd, u8 de, cr128& vt) -> void;
template<u8 e> inline auto VRCP(r128& vd, u8 de, cr128& vt) -> void { VRCP<0, e>(vd, de, vt); }
template<u8 e> inline auto VRCPL(r128& vd, u8 de, cr128& vt) -> void { VRCP<1, e>(vd, de, vt); }
template<u8 e> inline auto VRCPH(r128& vd, u8 de, cr128& vt) -> void;
template<bool D, u8 e>
inline auto VRND(r128& vd, u8 vs, cr128& vt) -> void;
template<u8 e> inline auto VRNDN(r128& vd, u8 vs, cr128& vt) -> void { VRND<0, e>(vd, vs, vt); }
template<u8 e> inline auto VRNDP(r128& vd, u8 vs, cr128& vt) -> void { VRND<1, e>(vd, vs, vt); }
template<bool L, u8 e>
inline auto VRSQ(r128& vd, u8 de, cr128& vt) -> void;
template<u8 e> inline auto VRSQ(r128& vd, u8 de, cr128& vt) -> void { VRSQ<0, e>(vd, de, vt); }
template<u8 e> inline auto VRSQL(r128& vd, u8 de, cr128& vt) -> void { VRSQ<1, e>(vd, de, vt); }
template<u8 e> inline auto VRSQH(r128& vd, u8 de, cr128& vt) -> void;
template<u8 e> inline auto VSAR(r128& vd, cr128& vs) -> void;
template<u8 e> inline auto VSUB(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VSUBC(r128& vd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VXOR(r128& rd, cr128& vs, cr128& vt) -> void;
template<u8 e> inline auto VZERO(r128& rd, cr128& vs, cr128& vt) -> void;
};

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +0,0 @@
#ifndef __RT64_LAYER_H__
#define __RT64_LAYER_H__
#include "../ultramodern/ultramodern.hpp"
#include "../ultramodern/config.hpp"
namespace RT64 {
struct Application;
}
namespace ultramodern {
enum class RT64SetupResult {
Success,
DynamicLibrariesNotFound,
InvalidGraphicsAPI,
GraphicsAPINotFound,
GraphicsDeviceNotFound
};
struct WindowHandle;
struct RT64Context {
public:
~RT64Context();
RT64Context(uint8_t* rdram, WindowHandle window_handle, bool developer_mode);
bool valid() { return static_cast<bool>(app); }
RT64SetupResult get_setup_result() { return setup_result; }
void update_config(const GraphicsConfig& old_config, const GraphicsConfig& new_config);
void enable_instant_present();
void send_dl(const OSTask* task);
void update_screen(uint32_t vi_origin);
void shutdown();
void set_dummy_vi();
uint32_t get_display_framerate();
float get_resolution_scale();
void load_shader_cache(std::span<const char> cache_binary);
private:
RT64SetupResult setup_result;
std::unique_ptr<RT64::Application> app;
};
RT64::UserConfiguration::Antialiasing RT64MaxMSAA();
bool RT64SamplePositionsSupported();
bool RT64HighPrecisionFBEnabled();
}
void set_rt64_hooks();
#endif

View File

@ -1,23 +0,0 @@
#ifndef __SECTIONS_H__
#define __SECTIONS_H__
#include <stdint.h>
#include "recomp.h"
#define ARRLEN(x) (sizeof(x) / sizeof((x)[0]))
typedef struct {
recomp_func_t* func;
uint32_t offset;
} FuncEntry;
typedef struct {
uint32_t rom_addr;
uint32_t ram_addr;
uint32_t size;
FuncEntry *funcs;
size_t num_funcs;
size_t index;
} SectionTableEntry;
#endif

91
include/zelda_config.h Normal file
View File

@ -0,0 +1,91 @@
#ifndef __ZELDA_CONFIG_H__
#define __ZELDA_CONFIG_H__
#include <filesystem>
#include <string_view>
#include "ultramodern/config.hpp"
namespace zelda64 {
constexpr std::u8string_view program_id = u8"Zelda64Recompiled";
constexpr std::string_view program_name = "Zelda 64: Recompiled";
// TODO: Move loading configs to the runtime once we have a way to allow per-project customization.
void load_config();
void save_config();
void reset_input_bindings();
void reset_cont_input_bindings();
void reset_kb_input_bindings();
std::filesystem::path get_app_folder_path();
bool get_debug_mode_enabled();
void set_debug_mode_enabled(bool enabled);
enum class AutosaveMode {
On,
Off,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(zelda64::AutosaveMode, {
{zelda64::AutosaveMode::On, "On"},
{zelda64::AutosaveMode::Off, "Off"}
});
enum class TargetingMode {
Switch,
Hold,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(zelda64::TargetingMode, {
{zelda64::TargetingMode::Switch, "Switch"},
{zelda64::TargetingMode::Hold, "Hold"}
});
TargetingMode get_targeting_mode();
void set_targeting_mode(TargetingMode mode);
enum class CameraInvertMode {
InvertNone,
InvertX,
InvertY,
InvertBoth,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(zelda64::CameraInvertMode, {
{zelda64::CameraInvertMode::InvertNone, "InvertNone"},
{zelda64::CameraInvertMode::InvertX, "InvertX"},
{zelda64::CameraInvertMode::InvertY, "InvertY"},
{zelda64::CameraInvertMode::InvertBoth, "InvertBoth"}
});
CameraInvertMode get_camera_invert_mode();
void set_camera_invert_mode(CameraInvertMode mode);
CameraInvertMode get_analog_camera_invert_mode();
void set_analog_camera_invert_mode(CameraInvertMode mode);
enum class AnalogCamMode {
On,
Off,
OptionCount
};
NLOHMANN_JSON_SERIALIZE_ENUM(zelda64::AnalogCamMode, {
{zelda64::AnalogCamMode::On, "On"},
{zelda64::AnalogCamMode::Off, "Off"}
});
AutosaveMode get_autosave_mode();
void set_autosave_mode(AutosaveMode mode);
AnalogCamMode get_analog_cam_mode();
void set_analog_cam_mode(AnalogCamMode mode);
void open_quit_game_prompt();
};
#endif

View File

@ -1,10 +1,10 @@
#ifndef __RECOMP_DEBUG_H__
#define __RECOMP_DEBUG_H__
#ifndef __ZELDA_DEBUG_H__
#define __ZELDA_DEBUG_H__
#include <vector>
#include <string>
namespace recomp {
namespace zelda64 {
struct SceneWarps {
int index;
std::string name;

9
include/zelda_game.h Normal file
View File

@ -0,0 +1,9 @@
#ifndef __ZELDA_GAME_H__
#define __ZELDA_GAME_H__
namespace zelda64 {
void quicksave_save();
void quicksave_load();
};
#endif

View File

@ -1,7 +1,7 @@
#ifndef __RECOMP_SOUND_H__
#define __RECOMP_SOUND_H__
#ifndef __ZELDA_SOUND_H__
#define __ZELDA_SOUND_H__
namespace recomp {
namespace zelda64 {
void reset_sound_settings();
void set_main_volume(int volume);
int get_main_volume();

1
lib/N64ModernRuntime Submodule

@ -0,0 +1 @@
Subproject commit ec7e81b45d9a622cb3e45865ce5d7d3536b26534

View File

@ -27,7 +27,7 @@ $(C_OBJS): %.o : %.c
$(CC) $(CFLAGS) $(CPPFLAGS) $< -MMD -MF $(@:.o=.d) -c -o $@
clean:
rm -rf $(C_OBJS) $(TARGET) $(DATABIN)
rm -rf $(C_OBJS) $(TARGET) $(DATABIN) $(C_DEPS)
-include $(C_DEPS)

View File

@ -4,7 +4,7 @@
#ifdef MIPS
#include "ultra64.h"
#else
#include "recomp.h"
#include "librecomp/recomp.h"
#endif
#ifdef __cplusplus

View File

@ -1,8 +1,8 @@
#include "recomp_config.h"
#include "zelda_config.h"
#include "recomp_input.h"
#include "recomp_sound.h"
#include "recomp_files.h"
#include "../../ultramodern/config.hpp"
#include "zelda_sound.h"
#include "ultramodern/config.hpp"
#include "librecomp/files.hpp"
#include <filesystem>
#include <fstream>
#include <iomanip>
@ -18,6 +18,7 @@ constexpr std::u8string_view general_filename = u8"general.json";
constexpr std::u8string_view graphics_filename = u8"graphics.json";
constexpr std::u8string_view controls_filename = u8"controls.json";
constexpr std::u8string_view sound_filename = u8"sound.json";
constexpr std::u8string_view program_id = u8"Zelda64Recompiled";
constexpr auto res_default = ultramodern::Resolution::Auto;
constexpr auto hr_default = ultramodern::HUDRatioMode::Clamp16x9;
@ -127,7 +128,7 @@ namespace recomp {
}
}
std::filesystem::path recomp::get_app_folder_path() {
std::filesystem::path zelda64::get_app_folder_path() {
std::filesystem::path recomp_dir{};
#if defined(_WIN32)
@ -135,7 +136,7 @@ std::filesystem::path recomp::get_app_folder_path() {
PWSTR known_path = NULL;
HRESULT result = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &known_path);
if (result == S_OK) {
recomp_dir = std::filesystem::path{known_path} / recomp::program_id;
recomp_dir = std::filesystem::path{known_path} / zelda64::program_id;
}
CoTaskMemFree(known_path);
@ -147,7 +148,7 @@ std::filesystem::path recomp::get_app_folder_path() {
}
if (homedir != nullptr) {
recomp_dir = std::filesystem::path{homedir} / (std::u8string{u8".config/"} + std::u8string{recomp::program_id});
recomp_dir = std::filesystem::path{homedir} / (std::u8string{u8".config/"} + std::u8string{zelda64::program_id});
}
#endif
@ -198,33 +199,33 @@ bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::j
bool save_general_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
recomp::to_json(config_json["targeting_mode"], recomp::get_targeting_mode());
zelda64::to_json(config_json["targeting_mode"], zelda64::get_targeting_mode());
recomp::to_json(config_json["background_input_mode"], recomp::get_background_input_mode());
config_json["rumble_strength"] = recomp::get_rumble_strength();
config_json["gyro_sensitivity"] = recomp::get_gyro_sensitivity();
config_json["mouse_sensitivity"] = recomp::get_mouse_sensitivity();
config_json["joystick_deadzone"] = recomp::get_joystick_deadzone();
config_json["autosave_mode"] = recomp::get_autosave_mode();
config_json["camera_invert_mode"] = recomp::get_camera_invert_mode();
config_json["analog_cam_mode"] = recomp::get_analog_cam_mode();
config_json["analog_camera_invert_mode"] = recomp::get_analog_camera_invert_mode();
config_json["debug_mode"] = recomp::get_debug_mode_enabled();
config_json["autosave_mode"] = zelda64::get_autosave_mode();
config_json["camera_invert_mode"] = zelda64::get_camera_invert_mode();
config_json["analog_cam_mode"] = zelda64::get_analog_cam_mode();
config_json["analog_camera_invert_mode"] = zelda64::get_analog_camera_invert_mode();
config_json["debug_mode"] = zelda64::get_debug_mode_enabled();
return save_json_with_backups(path, config_json);
}
void set_general_settings_from_json(const nlohmann::json& config_json) {
recomp::set_targeting_mode(from_or_default(config_json, "targeting_mode", recomp::TargetingMode::Switch));
zelda64::set_targeting_mode(from_or_default(config_json, "targeting_mode", zelda64::TargetingMode::Switch));
recomp::set_background_input_mode(from_or_default(config_json, "background_input_mode", recomp::BackgroundInputMode::On));
recomp::set_rumble_strength(from_or_default(config_json, "rumble_strength", 25));
recomp::set_gyro_sensitivity(from_or_default(config_json, "gyro_sensitivity", 50));
recomp::set_mouse_sensitivity(from_or_default(config_json, "mouse_sensitivity", is_steam_deck ? 50 : 0));
recomp::set_joystick_deadzone(from_or_default(config_json, "joystick_deadzone", 5));
recomp::set_autosave_mode(from_or_default(config_json, "autosave_mode", recomp::AutosaveMode::On));
recomp::set_camera_invert_mode(from_or_default(config_json, "camera_invert_mode", recomp::CameraInvertMode::InvertY));
recomp::set_analog_cam_mode(from_or_default(config_json, "analog_cam_mode", recomp::AnalogCamMode::Off));
recomp::set_analog_camera_invert_mode(from_or_default(config_json, "analog_camera_invert_mode", recomp::CameraInvertMode::InvertNone));
recomp::set_debug_mode_enabled(from_or_default(config_json, "debug_mode", false));
zelda64::set_autosave_mode(from_or_default(config_json, "autosave_mode", zelda64::AutosaveMode::On));
zelda64::set_camera_invert_mode(from_or_default(config_json, "camera_invert_mode", zelda64::CameraInvertMode::InvertY));
zelda64::set_analog_cam_mode(from_or_default(config_json, "analog_cam_mode", zelda64::AnalogCamMode::Off));
zelda64::set_analog_camera_invert_mode(from_or_default(config_json, "analog_camera_invert_mode", zelda64::CameraInvertMode::InvertNone));
zelda64::set_debug_mode_enabled(from_or_default(config_json, "debug_mode", false));
}
bool load_general_config(const std::filesystem::path& path) {
@ -277,16 +278,16 @@ void assign_all_mappings(recomp::InputDevice device, const recomp::DefaultN64Map
assign_mapping_complete(device, recomp::GameInput::TOGGLE_MENU, values.toggle_menu);
};
void recomp::reset_input_bindings() {
void zelda64::reset_input_bindings() {
assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings);
assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings);
}
void recomp::reset_cont_input_bindings() {
void zelda64::reset_cont_input_bindings() {
assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings);
}
void recomp::reset_kb_input_bindings() {
void zelda64::reset_kb_input_bindings() {
assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings);
}
@ -369,8 +370,8 @@ bool load_input_device_from_json(const nlohmann::json& config_json, recomp::Inpu
cur_input,
recomp::get_default_mapping_for_input(
device == recomp::InputDevice::Keyboard ?
recomp::default_n64_keyboard_mappings :
recomp::default_n64_controller_mappings,
recomp::default_n64_keyboard_mappings :
recomp::default_n64_controller_mappings,
cur_input
)
);
@ -408,9 +409,9 @@ bool load_controls_config(const std::filesystem::path& path) {
bool save_sound_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
config_json["main_volume"] = recomp::get_main_volume();
config_json["bgm_volume"] = recomp::get_bgm_volume();
config_json["low_health_beeps"] = recomp::get_low_health_beeps_enabled();
config_json["main_volume"] = zelda64::get_main_volume();
config_json["bgm_volume"] = zelda64::get_bgm_volume();
config_json["low_health_beeps"] = zelda64::get_low_health_beeps_enabled();
return save_json_with_backups(path, config_json);
}
@ -421,17 +422,17 @@ bool load_sound_config(const std::filesystem::path& path) {
return false;
}
recomp::reset_sound_settings();
call_if_key_exists(recomp::set_main_volume, config_json, "main_volume");
call_if_key_exists(recomp::set_bgm_volume, config_json, "bgm_volume");
call_if_key_exists(recomp::set_low_health_beeps_enabled, config_json, "low_health_beeps");
zelda64::reset_sound_settings();
call_if_key_exists(zelda64::set_main_volume, config_json, "main_volume");
call_if_key_exists(zelda64::set_bgm_volume, config_json, "bgm_volume");
call_if_key_exists(zelda64::set_low_health_beeps_enabled, config_json, "low_health_beeps");
return true;
}
void recomp::load_config() {
void zelda64::load_config() {
detect_steam_deck();
std::filesystem::path recomp_dir = recomp::get_app_folder_path();
std::filesystem::path recomp_dir = zelda64::get_app_folder_path();
std::filesystem::path general_path = recomp_dir / general_filename;
std::filesystem::path graphics_path = recomp_dir / graphics_filename;
std::filesystem::path controls_path = recomp_dir / controls_filename;
@ -455,18 +456,18 @@ void recomp::load_config() {
}
if (!load_controls_config(controls_path)) {
recomp::reset_input_bindings();
zelda64::reset_input_bindings();
save_controls_config(controls_path);
}
if (!load_sound_config(sound_path)) {
recomp::reset_sound_settings();
zelda64::reset_sound_settings();
save_sound_config(sound_path);
}
}
void recomp::save_config() {
std::filesystem::path recomp_dir = recomp::get_app_folder_path();
void zelda64::save_config() {
std::filesystem::path recomp_dir = zelda64::get_app_folder_path();
if (recomp_dir.empty()) {
return;

View File

@ -1,8 +1,8 @@
#include <array>
#include "recomp_helpers.h"
#include "librecomp/helpers.hpp"
#include "recomp_input.h"
#include "../ultramodern/ultramodern.hpp"
#include "ultramodern/ultramodern.hpp"
// Arrays that hold the mappings for every input for keyboard and controller respectively.
using input_mapping = std::array<recomp::InputField, recomp::bindings_per_input>;

View File

@ -1,13 +1,13 @@
#include <atomic>
#include "recomp_debug.h"
#include "recomp_helpers.h"
#include "zelda_debug.h"
#include "librecomp/helpers.hpp"
#include "../patches/input.h"
std::atomic<uint16_t> pending_warp = 0xFFFF;
std::atomic<uint32_t> pending_set_time = 0xFFFF;
void recomp::do_warp(int area, int scene, int entrance) {
const recomp::SceneWarps game_scene = recomp::game_warps[area].scenes[scene];
void zelda64::do_warp(int area, int scene, int entrance) {
const zelda64::SceneWarps game_scene = zelda64::game_warps[area].scenes[scene];
int game_scene_index = game_scene.index;
pending_warp.store(((game_scene_index & 0xFF) << 8) | ((entrance & 0x0F) << 4));
}
@ -17,7 +17,7 @@ extern "C" void recomp_get_pending_warp(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, pending_warp.exchange(0xFFFF));
}
void recomp::set_time(uint8_t day, uint8_t hour, uint8_t minute) {
void zelda64::set_time(uint8_t day, uint8_t hour, uint8_t minute) {
pending_set_time.store((day << 16) | (uint16_t(hour) << 8) | minute);
}

View File

@ -1,12 +1,12 @@
#include <atomic>
#include <mutex>
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
#include "ultramodern/ultramodern.hpp"
#include "librecomp/recomp.h"
#include "recomp_input.h"
#include "zelda_config.h"
#include "recomp_ui.h"
#include "SDL.h"
#include "rt64_layer.h"
#include "promptfont.h"
#include "GamepadMotion.hpp"
@ -75,13 +75,13 @@ void recomp::stop_scanning_input() {
void queue_if_enabled(SDL_Event* event) {
if (!recomp::all_input_disabled()) {
recomp::queue_event(*event);
recompui::queue_event(*event);
}
}
static std::atomic_bool cursor_enabled = true;
void recomp::set_cursor_visible(bool visible) {
void recompui::set_cursor_visible(bool visible) {
cursor_enabled.store(visible);
}
@ -110,7 +110,7 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
if ((keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_RETURN && (keyevent->keysym.mod & SDL_Keymod::KMOD_ALT)) ||
keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_F11
) {
recomp::toggle_fullscreen();
recompui::toggle_fullscreen();
}
if (scanning_device != recomp::InputDevice::COUNT) {
if (keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_ESCAPE) {
@ -155,12 +155,12 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
return true;
}
if (recomp::get_current_menu() != recomp::Menu::Config) {
recomp::set_current_menu(recomp::Menu::Config);
if (recompui::get_current_menu() != recompui::Menu::Config) {
recompui::set_current_menu(recompui::Menu::Config);
}
recomp::open_quit_game_prompt();
recomp::activate_mouse();
zelda64::open_quit_game_prompt();
recompui::activate_mouse();
break;
}
case SDL_EventType::SDL_MOUSEWHEEL:
@ -435,10 +435,10 @@ void recomp::poll_inputs() {
bool save_is_held = InputState.keys[SDL_SCANCODE_F5] != 0;
bool load_is_held = InputState.keys[SDL_SCANCODE_F7] != 0;
if (save_is_held && !save_was_held) {
recomp::quicksave_save();
zelda64::quicksave_save();
}
else if (load_is_held && !load_was_held) {
recomp::quicksave_load();
zelda64::quicksave_load();
}
save_was_held = save_is_held;
}
@ -649,7 +649,7 @@ void recomp::set_right_analog_suppressed(bool suppressed) {
bool recomp::game_input_disabled() {
// Disable input if any menu is open.
return recomp::get_current_menu() != recomp::Menu::None;
return recompui::get_current_menu() != recompui::Menu::None;
}
bool recomp::all_input_disabled() {

View File

@ -2,9 +2,9 @@
#if 0
#include "recomp_helpers.h"
#include "recomp_input.h"
#include "../ultramodern/ultramodern.hpp"
#include "librecomp/helpers.hpp"
#include "librecomp/input.hpp"
#include "ultramodern/ultramodern.hpp"
enum class QuicksaveAction {
None,
@ -14,11 +14,11 @@ enum class QuicksaveAction {
std::atomic<QuicksaveAction> cur_quicksave_action = QuicksaveAction::None;
void recomp::quicksave_save() {
void zelda64::quicksave_save() {
cur_quicksave_action.store(QuicksaveAction::Save);
}
void recomp::quicksave_load() {
void zelda64::quicksave_load() {
cur_quicksave_action.store(QuicksaveAction::Load);
}

View File

@ -1,18 +1,18 @@
#include <cmath>
#include "recomp.h"
#include "recomp_overlays.h"
#include "recomp_config.h"
#include "librecomp/recomp.h"
#include "librecomp/overlays.hpp"
#include "zelda_config.h"
#include "recomp_input.h"
#include "recomp_ui.h"
#include "recomp_sound.h"
#include "recomp_helpers.h"
#include "rt64_layer.h"
#include "zelda_sound.h"
#include "librecomp/helpers.hpp"
#include "../patches/input.h"
#include "../patches/graphics.h"
#include "../patches/sound.h"
#include "../ultramodern/ultramodern.hpp"
#include "../ultramodern/config.hpp"
#include "ultramodern/ultramodern.hpp"
#include "ultramodern/config.hpp"
#include "ultramodern/rt64_layer.hpp"
extern "C" void recomp_update_inputs(uint8_t* rdram, recomp_context* ctx) {
recomp::poll_inputs();
@ -62,7 +62,7 @@ extern "C" void recomp_get_aspect_ratio(uint8_t* rdram, recomp_context* ctx) {
ultramodern::GraphicsConfig graphics_config = ultramodern::get_graphics_config();
float original = _arg<0, float>(rdram, ctx);
int width, height;
recomp::get_window_size(width, height);
recompui::get_window_size(width, height);
switch (graphics_config.ar_option) {
case RT64::UserConfiguration::AspectRatio::Original:
@ -76,15 +76,15 @@ extern "C" void recomp_get_aspect_ratio(uint8_t* rdram, recomp_context* ctx) {
}
extern "C" void recomp_get_targeting_mode(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, static_cast<int>(recomp::get_targeting_mode()));
_return(ctx, static_cast<int>(zelda64::get_targeting_mode()));
}
extern "C" void recomp_get_bgm_volume(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, recomp::get_bgm_volume() / 100.0f);
_return(ctx, zelda64::get_bgm_volume() / 100.0f);
}
extern "C" void recomp_get_low_health_beeps_enabled(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, static_cast<u32>(recomp::get_low_health_beeps_enabled()));
_return(ctx, static_cast<u32>(zelda64::get_low_health_beeps_enabled()));
}
extern "C" void recomp_time_us(uint8_t* rdram, recomp_context* ctx) {
@ -92,7 +92,7 @@ extern "C" void recomp_time_us(uint8_t* rdram, recomp_context* ctx) {
}
extern "C" void recomp_autosave_enabled(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, static_cast<s32>(recomp::get_autosave_mode() == recomp::AutosaveMode::On));
_return(ctx, static_cast<s32>(zelda64::get_autosave_mode() == zelda64::AutosaveMode::On));
}
extern "C" void recomp_load_overlays(uint8_t * rdram, recomp_context * ctx) {
@ -115,24 +115,24 @@ extern "C" void recomp_get_inverted_axes(uint8_t* rdram, recomp_context* ctx) {
s32* x_out = _arg<0, s32*>(rdram, ctx);
s32* y_out = _arg<1, s32*>(rdram, ctx);
recomp::CameraInvertMode mode = recomp::get_camera_invert_mode();
zelda64::CameraInvertMode mode = zelda64::get_camera_invert_mode();
*x_out = (mode == recomp::CameraInvertMode::InvertX || mode == recomp::CameraInvertMode::InvertBoth);
*y_out = (mode == recomp::CameraInvertMode::InvertY || mode == recomp::CameraInvertMode::InvertBoth);
*x_out = (mode == zelda64::CameraInvertMode::InvertX || mode == zelda64::CameraInvertMode::InvertBoth);
*y_out = (mode == zelda64::CameraInvertMode::InvertY || mode == zelda64::CameraInvertMode::InvertBoth);
}
extern "C" void recomp_get_analog_inverted_axes(uint8_t* rdram, recomp_context* ctx) {
s32* x_out = _arg<0, s32*>(rdram, ctx);
s32* y_out = _arg<1, s32*>(rdram, ctx);
recomp::CameraInvertMode mode = recomp::get_analog_camera_invert_mode();
zelda64::CameraInvertMode mode = zelda64::get_analog_camera_invert_mode();
*x_out = (mode == recomp::CameraInvertMode::InvertX || mode == recomp::CameraInvertMode::InvertBoth);
*y_out = (mode == recomp::CameraInvertMode::InvertY || mode == recomp::CameraInvertMode::InvertBoth);
*x_out = (mode == zelda64::CameraInvertMode::InvertX || mode == zelda64::CameraInvertMode::InvertBoth);
*y_out = (mode == zelda64::CameraInvertMode::InvertY || mode == zelda64::CameraInvertMode::InvertBoth);
}
extern "C" void recomp_analog_cam_enabled(uint8_t* rdram, recomp_context* ctx) {
_return<s32>(ctx, recomp::get_analog_cam_mode() == recomp::AnalogCamMode::On);
_return<s32>(ctx, zelda64::get_analog_cam_mode() == zelda64::AnalogCamMode::On);
}
extern "C" void recomp_get_camera_inputs(uint8_t* rdram, recomp_context* ctx) {

View File

@ -1,9 +1,9 @@
#include <cstdint>
#include <vector>
#include <string>
#include "recomp_debug.h"
#include "zelda_debug.h"
std::vector<recomp::AreaWarps> recomp::game_warps {
std::vector<zelda64::AreaWarps> zelda64::game_warps {
{ "Clock Town", {
{
0, "Mayor's Residence", {

View File

@ -6,11 +6,12 @@
#include <filesystem>
#include <numeric>
#include <stdexcept>
#include <cinttypes>
#include "nfd.h"
#include "../../ultramodern/ultra64.h"
#include "../../ultramodern/ultramodern.hpp"
#include "ultramodern/ultra64.h"
#include "ultramodern/ultramodern.hpp"
#define SDL_MAIN_HANDLED
#ifdef _WIN32
#include "SDL.h"
@ -21,9 +22,14 @@
#include "recomp_ui.h"
#include "recomp_input.h"
#include "recomp_config.h"
#include "recomp_game.h"
#include "recomp_sound.h"
#include "zelda_config.h"
#include "zelda_sound.h"
#include "ovl_patches.hpp"
#include "librecomp/game.hpp"
#ifdef HAS_MM_SHADER_CACHE
#include "mm_shader_cache.h"
#endif
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
@ -192,7 +198,7 @@ void queue_samples(int16_t* audio_data, size_t sample_count) {
// Convert the audio from 16-bit values to floats and swap the audio channels into the
// swap buffer to correct for the address xor caused by endianness handling.
float cur_main_volume = recomp::get_main_volume() / 100.0f; // Get the current main volume, normalized to 0.0-1.0.
float cur_main_volume = zelda64::get_main_volume() / 100.0f; // Get the current main volume, normalized to 0.0-1.0.
for (size_t i = 0; i < sample_count; i += input_channels) {
swap_buffer[i + 0 + duplicated_input_frames * input_channels] = audio_data[i + 1] * (0.5f / 32768.0f) * cur_main_volume;
swap_buffer[i + 1 + duplicated_input_frames * input_channels] = audio_data[i + 0] * (0.5f / 32768.0f) * cur_main_volume;
@ -302,6 +308,42 @@ void reset_audio(uint32_t output_freq) {
update_audio_converter();
}
extern RspUcodeFunc njpgdspMain;
extern RspUcodeFunc aspMain;
RspUcodeFunc* get_rsp_microcode(const OSTask* task) {
switch (task->t.type) {
case M_AUDTASK:
return aspMain;
case M_NJPEGTASK:
return njpgdspMain;
default:
fprintf(stderr, "Unknown task: %" PRIu32 "\n", task->t.type);
return nullptr;
}
}
extern "C" void recomp_entrypoint(uint8_t * rdram, recomp_context * ctx);
gpr get_entrypoint_address();
// array of supported GameEntry objects
std::vector<recomp::GameEntry> supported_games = {
{
.rom_hash = 0xEF18B4A9E2386169ULL,
.internal_name = "ZELDA MAJORA'S MASK",
.game_id = u8"mm.n64.us.1.0",
#ifdef HAS_MM_SHADER_CACHE
.cache_data = {mm_shader_cache_bytes, sizeof(mm_shader_cache_bytes)},
#endif
.is_enabled = true,
.entrypoint_address = get_entrypoint_address(),
.entrypoint = recomp_entrypoint,
},
};
int main(int argc, char** argv) {
#ifdef _WIN32
@ -333,7 +375,20 @@ int main(int argc, char** argv) {
SDL_InitSubSystem(SDL_INIT_AUDIO);
reset_audio(48000);
recomp::load_config();
// Register supported games and patches
for (const auto& game : supported_games) {
recomp::register_game(game);
}
zelda64::register_overlays();
zelda64::register_patches();
recomp::register_config_path(zelda64::get_app_folder_path());
zelda64::load_config();
recomp::rsp::callbacks_t rsp_callbacks{
.get_rsp_microcode = get_rsp_microcode,
};
ultramodern::gfx_callbacks_t gfx_callbacks{
.create_gfx = create_gfx,
@ -353,7 +408,16 @@ int main(int argc, char** argv) {
.set_rumble = recomp::set_rumble,
};
recomp::start({}, audio_callbacks, input_callbacks, gfx_callbacks);
ultramodern::events::callbacks_t thread_callbacks{
.vi_callback = recomp::update_rumble,
.gfx_init_callback = recompui::update_supported_options,
};
ultramodern::error_handling::callbacks_t error_handling_callbacks{
.message_box = recompui::message_box,
};
recomp::start({}, rsp_callbacks, audio_callbacks, input_callbacks, gfx_callbacks, thread_callbacks, error_handling_callbacks);
NFD_Quit();

View File

@ -0,0 +1,19 @@
#include "ovl_patches.hpp"
#include "../../RecompiledFuncs/recomp_overlays.inl"
#include "librecomp/overlays.hpp"
void zelda64::register_overlays() {
recomp::overlay_section_table_data_t sections {
.code_sections = section_table,
.num_code_sections = ARRLEN(section_table),
.total_num_sections = num_sections,
};
recomp::overlays_by_index_t overlays {
.table = overlay_sections_by_index,
.len = ARRLEN(overlay_sections_by_index),
};
recomp::register_overlays(sections, overlays);
}

View File

@ -0,0 +1,12 @@
#include "ovl_patches.hpp"
#include "../../RecompiledPatches/patches_bin.h"
#include "../../RecompiledPatches/recomp_overlays.inl"
#include "librecomp/overlays.hpp"
#include "librecomp/game.hpp"
void zelda64::register_patches() {
// TODO: This is a bit awful, maybe provide only one functions that does both in librecomp?
recomp::register_patch(mm_patches_bin, sizeof(mm_patches_bin));
recomp::register_patch_section(section_table);
}

View File

@ -1,29 +0,0 @@
#include "recomp.h"
#include <cstdio>
#include <string>
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
#define VI_NTSC_CLOCK 48681812
extern "C" void osAiSetFrequency_recomp(uint8_t* rdram, recomp_context* ctx) {
uint32_t freq = ctx->r4;
// This makes actual audio frequency more accurate to console, but may not be desirable
//uint32_t dacRate = (uint32_t)(((float)VI_NTSC_CLOCK / freq) + 0.5f);
//freq = VI_NTSC_CLOCK / dacRate;
ctx->r2 = freq;
ultramodern::set_audio_frequency(freq);
}
extern "C" void osAiSetNextBuffer_recomp(uint8_t* rdram, recomp_context* ctx) {
ultramodern::queue_audio_buffer(rdram, ctx->r4, ctx->r5);
ctx->r2 = 0;
}
extern "C" void osAiGetLength_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = ultramodern::get_remaining_audio_bytes();
}
extern "C" void osAiGetStatus_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = 0x00000000; // Pretend the audio DMAs finish instantly
}

View File

@ -1,155 +0,0 @@
#include "../ultramodern/ultramodern.hpp"
#include "recomp_helpers.h"
static ultramodern::input_callbacks_t input_callbacks;
std::chrono::high_resolution_clock::time_point input_poll_time;
void update_poll_time() {
input_poll_time = std::chrono::high_resolution_clock::now();
}
extern "C" void recomp_set_current_frame_poll_id(uint8_t* rdram, recomp_context* ctx) {
// TODO reimplement the system for tagging polls with IDs to handle games with multithreaded input polling.
}
extern "C" void recomp_measure_latency(uint8_t* rdram, recomp_context* ctx) {
ultramodern::measure_input_latency();
}
void ultramodern::measure_input_latency() {
// printf("Delta: %ld micros\n", std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::high_resolution_clock::now() - input_poll_time));
}
void set_input_callbacks(const ultramodern::input_callbacks_t& callbacks) {
input_callbacks = callbacks;
}
static int max_controllers = 0;
extern "C" void osContInit_recomp(uint8_t* rdram, recomp_context* ctx) {
PTR(void) bitpattern = _arg<1, PTR(void)>(rdram, ctx);
PTR(void) status = _arg<2, PTR(void)>(rdram, ctx);
// Set bit 0 to indicate that controller 0 is present
MEM_B(0, bitpattern) = 0x01;
// Mark controller 0 as present
MEM_H(0, status) = 0x0005; // type: CONT_TYPE_NORMAL (from joybus)
MEM_B(2, status) = 0x00; // status: 0 (from joybus)
MEM_B(3, status) = 0x00; // errno: 0 (from libultra)
max_controllers = 4;
// Mark controllers 1-3 as not connected
for (size_t controller = 1; controller < max_controllers; controller++) {
// Libultra doesn't write status or type for absent controllers
MEM_B(4 * controller + 3, status) = 0x80 >> 4; // errno: CONT_NO_RESPONSE_ERROR >> 4
}
_return<s32>(ctx, 0);
}
extern "C" void osContStartReadData_recomp(uint8_t* rdram, recomp_context* ctx) {
if (input_callbacks.poll_input) {
input_callbacks.poll_input();
}
update_poll_time();
ultramodern::send_si_message(rdram);
}
extern "C" void osContGetReadData_recomp(uint8_t* rdram, recomp_context* ctx) {
PTR(void) pad = _arg<0, PTR(void)>(rdram, ctx);
uint16_t buttons = 0;
float x = 0.0f;
float y = 0.0f;
if (input_callbacks.get_input) {
input_callbacks.get_input(&buttons, &x, &y);
}
if (max_controllers > 0) {
// button
MEM_H(0, pad) = buttons;
// stick_x
MEM_B(2, pad) = (int8_t)(127 * x);
// stick_y
MEM_B(3, pad) = (int8_t)(127 * y);
// errno
MEM_B(4, pad) = 0;
}
for (int controller = 1; controller < max_controllers; controller++) {
MEM_B(6 * controller + 4, pad) = 0x80 >> 4; // errno: CONT_NO_RESPONSE_ERROR >> 4
}
}
extern "C" void osContStartQuery_recomp(uint8_t * rdram, recomp_context * ctx) {
ultramodern::send_si_message(rdram);
}
extern "C" void osContGetQuery_recomp(uint8_t * rdram, recomp_context * ctx) {
PTR(void) status = _arg<0, PTR(void)>(rdram, ctx);
// Mark controller 0 as present
MEM_H(0, status) = 0x0005; // type: CONT_TYPE_NORMAL (from joybus)
MEM_B(2, status) = 0x01; // status: 0x01 (from joybus, indicates that a pak is plugged into the controller)
MEM_B(3, status) = 0x00; // errno: 0 (from libultra)
// Mark controllers 1-3 as not connected
for (size_t controller = 1; controller < max_controllers; controller++) {
// Libultra doesn't write status or type for absent controllers
MEM_B(4 * controller + 3, status) = 0x80 >> 4; // errno: CONT_NO_RESPONSE_ERROR >> 4
}
}
extern "C" void osContSetCh_recomp(uint8_t* rdram, recomp_context* ctx) {
max_controllers = std::min(_arg<0, u8>(rdram, ctx), u8(4));
_return<s32>(ctx, 0);
}
extern "C" void __osMotorAccess_recomp(uint8_t* rdram, recomp_context* ctx) {
PTR(void) pfs = _arg<0, PTR(void)>(rdram, ctx);
s32 flag = _arg<1, s32>(rdram, ctx);
s32 channel = MEM_W(8, pfs);
// Only respect accesses to controller 0.
if (channel == 0) {
input_callbacks.set_rumble(flag);
}
_return<s32>(ctx, 0);
}
extern "C" void osMotorInit_recomp(uint8_t* rdram, recomp_context* ctx) {
PTR(void) pfs = _arg<1, PTR(void)>(rdram, ctx);
s32 channel = _arg<2, s32>(rdram, ctx);
MEM_W(8, pfs) = channel;
_return<s32>(ctx, 0);
}
extern "C" void osMotorStart_recomp(uint8_t* rdram, recomp_context* ctx) {
PTR(void) pfs = _arg<0, PTR(void)>(rdram, ctx);
s32 channel = MEM_W(8, pfs);
// Only respect accesses to controller 0.
if (channel == 0) {
input_callbacks.set_rumble(true);
}
_return<s32>(ctx, 0);
}
extern "C" void osMotorStop_recomp(uint8_t* rdram, recomp_context* ctx) {
PTR(void) pfs = _arg<0, PTR(void)>(rdram, ctx);
s32 channel = MEM_W(8, pfs);
// Only respect accesses to controller 0.
if (channel == 0) {
input_callbacks.set_rumble(false);
}
_return<s32>(ctx, 0);
}

View File

@ -1,44 +0,0 @@
#include "recomp.h"
enum class RDPStatusBit {
XbusDmem = 0,
Freeze = 1,
Flush = 2,
CommandBusy = 6,
BufferReady = 7,
DmaBusy = 8,
EndValid = 9,
StartValid = 10,
};
constexpr void update_bit(uint32_t& state, uint32_t flags, RDPStatusBit bit) {
int set_bit_pos = (int)bit * 2 + 0;
int reset_bit_pos = (int)bit * 2 + 1;
bool set = (flags & (1U << set_bit_pos)) != 0;
bool reset = (flags & (1U << reset_bit_pos)) != 0;
if (set ^ reset) {
if (set) {
state |= (1U << (int)bit);
}
else {
state &= ~(1U << (int)bit);
}
}
}
uint32_t rdp_state = 1 << (int)RDPStatusBit::BufferReady;
extern "C" void osDpSetNextBuffer_recomp(uint8_t* rdram, recomp_context* ctx) {
assert(false);
}
extern "C" void osDpGetStatus_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = rdp_state;
}
extern "C" void osDpSetStatus_recomp(uint8_t* rdram, recomp_context* ctx) {
update_bit(rdp_state, ctx->r4, RDPStatusBit::XbusDmem);
update_bit(rdp_state, ctx->r4, RDPStatusBit::Freeze);
update_bit(rdp_state, ctx->r4, RDPStatusBit::Flush);
}

View File

@ -1,49 +0,0 @@
#include "recomp.h"
#include "../ultramodern/ultra64.h"
void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count);
void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count);
constexpr int eeprom_block_size = 8;
constexpr int eep4_size = 4096;
constexpr int eep4_block_count = eep4_size / eeprom_block_size;
constexpr int eep16_size = 16384;
constexpr int eep16_block_count = eep16_size / eeprom_block_size;
extern "C" void osEepromProbe_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = 0x02; // EEP16K
}
extern "C" void osEepromWrite_recomp(uint8_t* rdram, recomp_context* ctx) {
assert(false);// ctx->r2 = 8; // CONT_NO_RESPONSE_ERROR
}
extern "C" void osEepromLongWrite_recomp(uint8_t* rdram, recomp_context* ctx) {
uint8_t eep_address = ctx->r5;
gpr buffer = ctx->r6;
int32_t nbytes = ctx->r7;
assert(!(nbytes & 7));
assert(eep_address * eeprom_block_size + nbytes <= eep16_size);
save_write(rdram, buffer, eep_address * eeprom_block_size, nbytes);
ctx->r2 = 0;
}
extern "C" void osEepromRead_recomp(uint8_t* rdram, recomp_context* ctx) {
assert(false);// ctx->r2 = 8; // CONT_NO_RESPONSE_ERROR
}
extern "C" void osEepromLongRead_recomp(uint8_t* rdram, recomp_context* ctx) {
uint8_t eep_address = ctx->r5;
gpr buffer = ctx->r6;
int32_t nbytes = ctx->r7;
assert(!(nbytes & 7));
assert(eep_address * eeprom_block_size + nbytes <= eep16_size);
save_read(rdram, buffer, eep_address * eeprom_block_size, nbytes);
ctx->r2 = 0;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
#ifndef __EUC_JP_H__
#define __EUC_JP_H__
#include <string>
#include <string_view>
namespace Encoding {
std::string decode_eucjp(std::string_view src);
}
#endif

View File

@ -1,51 +0,0 @@
#include "recomp_files.h"
constexpr std::u8string_view backup_suffix = u8".bak";
constexpr std::u8string_view temp_suffix = u8".temp";
std::ifstream recomp::open_input_backup_file(const std::filesystem::path& filepath, std::ios_base::openmode mode) {
std::filesystem::path backup_path{filepath};
backup_path += backup_suffix;
return std::ifstream{backup_path, mode};
}
std::ifstream recomp::open_input_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) {
std::ifstream ret{filepath, mode};
// Check if the file failed to open and open the corresponding backup file instead if so.
if (!ret.good()) {
return open_input_backup_file(filepath, mode);
}
return ret;
}
std::ofstream recomp::open_output_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) {
std::filesystem::path temp_path{filepath};
temp_path += temp_suffix;
std::ofstream temp_file_out{ temp_path, mode };
return temp_file_out;
}
bool recomp::finalize_output_file_with_backup(const std::filesystem::path& filepath) {
std::filesystem::path backup_path{filepath};
backup_path += backup_suffix;
std::filesystem::path temp_path{filepath};
temp_path += temp_suffix;
std::error_code ec;
if (std::filesystem::exists(filepath, ec)) {
std::filesystem::copy_file(filepath, backup_path, std::filesystem::copy_options::overwrite_existing, ec);
if (ec) {
return false;
}
}
std::filesystem::copy_file(temp_path, filepath, std::filesystem::copy_options::overwrite_existing, ec);
if (ec) {
return false;
}
std::filesystem::remove(temp_path, ec);
return true;
}

View File

@ -1,141 +0,0 @@
#include <array>
#include <cassert>
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
// TODO move this out into ultramodern code
constexpr uint32_t flash_size = 1024 * 1024 / 8; // 1Mbit
constexpr uint32_t page_size = 128;
constexpr uint32_t pages_per_sector = 128;
constexpr uint32_t page_count = flash_size / page_size;
constexpr uint32_t sector_size = page_size * pages_per_sector;
constexpr uint32_t sector_count = flash_size / sector_size;
void save_write_ptr(const void* in, uint32_t offset, uint32_t count);
void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count);
void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count);
void save_clear(uint32_t start, uint32_t size, char value);
std::array<char, page_size> write_buffer;
extern "C" void osFlashInit_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = ultramodern::flash_handle;
}
extern "C" void osFlashReadStatus_recomp(uint8_t * rdram, recomp_context * ctx) {
PTR(u8) flash_status = ctx->r4;
MEM_B(0, flash_status) = 0;
}
extern "C" void osFlashReadId_recomp(uint8_t * rdram, recomp_context * ctx) {
PTR(u32) flash_type = ctx->r4;
PTR(u32) flash_maker = ctx->r5;
// Mimic a real flash chip's type and maker, as some games actually check if one is present.
MEM_W(0, flash_type) = 0x11118001;
MEM_W(0, flash_maker) = 0x00C2001E;
}
extern "C" void osFlashClearStatus_recomp(uint8_t * rdram, recomp_context * ctx) {
}
extern "C" void osFlashAllErase_recomp(uint8_t * rdram, recomp_context * ctx) {
save_clear(0, ultramodern::save_size, 0xFF);
ctx->r2 = 0;
}
extern "C" void osFlashAllEraseThrough_recomp(uint8_t * rdram, recomp_context * ctx) {
save_clear(0, ultramodern::save_size, 0xFF);
ctx->r2 = 0;
}
// This function is named sector but really means page.
extern "C" void osFlashSectorErase_recomp(uint8_t * rdram, recomp_context * ctx) {
uint32_t page_num = (uint32_t)ctx->r4;
// Prevent out of bounds erase
if (page_num >= page_count) {
ctx->r2 = -1;
return;
}
save_clear(page_num * page_size, page_size, 0xFF);
ctx->r2 = 0;
}
// Same naming issue as above.
extern "C" void osFlashSectorEraseThrough_recomp(uint8_t * rdram, recomp_context * ctx) {
uint32_t page_num = (uint32_t)ctx->r4;
// Prevent out of bounds erase
if (page_num >= page_count) {
ctx->r2 = -1;
return;
}
save_clear(page_num * page_size, page_size, 0xFF);
ctx->r2 = 0;
}
extern "C" void osFlashCheckEraseEnd_recomp(uint8_t * rdram, recomp_context * ctx) {
// All erases are blocking in this implementation, so this should always return OK.
ctx->r2 = 0; // FLASH_STATUS_ERASE_OK
}
extern "C" void osFlashWriteBuffer_recomp(uint8_t * rdram, recomp_context * ctx) {
OSIoMesg* mb = TO_PTR(OSIoMesg, ctx->r4);
int32_t pri = ctx->r5;
PTR(void) dramAddr = ctx->r6;
PTR(OSMesgQueue) mq = ctx->r7;
// Copy the input data into the write buffer
for (size_t i = 0; i < page_size; i++) {
write_buffer[i] = MEM_B(i, dramAddr);
}
// Send the message indicating write completion
osSendMesg(PASS_RDRAM mq, 0, OS_MESG_NOBLOCK);
ctx->r2 = 0;
}
extern "C" void osFlashWriteArray_recomp(uint8_t * rdram, recomp_context * ctx) {
uint32_t page_num = ctx->r4;
// Copy the write buffer into the save file
save_write_ptr(write_buffer.data(), page_num * page_size, page_size);
ctx->r2 = 0;
}
extern "C" void osFlashReadArray_recomp(uint8_t * rdram, recomp_context * ctx) {
OSIoMesg* mb = TO_PTR(OSIoMesg, ctx->r4);
int32_t pri = ctx->r5;
uint32_t page_num = ctx->r6;
PTR(void) dramAddr = ctx->r7;
uint32_t n_pages = MEM_W(0x10, ctx->r29);
PTR(OSMesgQueue) mq = MEM_W(0x14, ctx->r29);
uint32_t offset = page_num * page_size;
uint32_t count = n_pages * page_size;
// Read from the save file into the provided buffer
save_read(PASS_RDRAM dramAddr, offset, count);
// Send the message indicating read completion
osSendMesg(PASS_RDRAM mq, 0, OS_MESG_NOBLOCK);
ctx->r2 = 0;
}
extern "C" void osFlashChange_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}

View File

@ -1,80 +0,0 @@
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
extern "C" void __udivdi3_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
uint64_t ret = a / b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __divdi3_recomp(uint8_t * rdram, recomp_context * ctx) {
int64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
int64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
int64_t ret = a / b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __umoddi3_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
uint64_t ret = a % b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __ull_div_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
uint64_t ret = a / b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __ll_div_recomp(uint8_t * rdram, recomp_context * ctx) {
int64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
int64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
int64_t ret = a / b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __ll_mul_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
uint64_t ret = a * b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __ull_rem_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
uint64_t ret = a % b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __ull_to_d_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
double ret = (double)a;
ctx->f0.d = ret;
}
extern "C" void __ull_to_f_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
float ret = (float)a;
ctx->f0.fl = ret;
}

View File

@ -1,165 +0,0 @@
#include <unordered_map>
#include <algorithm>
#include <vector>
#include "recomp.h"
#include "recomp_overlays.h"
#include "../RecompiledFuncs/recomp_overlays.inl"
constexpr size_t num_code_sections = ARRLEN(section_table);
// SectionTableEntry sections[] defined in recomp_overlays.inl
struct LoadedSection {
int32_t loaded_ram_addr;
size_t section_table_index;
LoadedSection(int32_t loaded_ram_addr_, size_t section_table_index_) {
loaded_ram_addr = loaded_ram_addr_;
section_table_index = section_table_index_;
}
bool operator<(const LoadedSection& rhs) {
return loaded_ram_addr < rhs.loaded_ram_addr;
}
};
std::vector<LoadedSection> loaded_sections{};
std::unordered_map<int32_t, recomp_func_t*> func_map{};
void load_overlay(size_t section_table_index, int32_t ram) {
const SectionTableEntry& section = section_table[section_table_index];
for (size_t function_index = 0; function_index < section.num_funcs; function_index++) {
const FuncEntry& func = section.funcs[function_index];
func_map[ram + func.offset] = func.func;
}
loaded_sections.emplace_back(ram, section_table_index);
section_addresses[section.index] = ram;
}
void load_special_overlay(const SectionTableEntry& section, int32_t ram) {
for (size_t function_index = 0; function_index < section.num_funcs; function_index++) {
const FuncEntry& func = section.funcs[function_index];
func_map[ram + func.offset] = func.func;
}
}
extern "C" {
int32_t section_addresses[num_sections];
}
extern "C" void load_overlays(uint32_t rom, int32_t ram_addr, uint32_t size) {
// Search for the first section that's included in the loaded rom range
// Sections were sorted by `init_overlays` so we can use the bounds functions
auto lower = std::lower_bound(&section_table[0], &section_table[num_code_sections], rom,
[](const SectionTableEntry& entry, uint32_t addr) {
return entry.rom_addr < addr;
}
);
auto upper = std::upper_bound(&section_table[0], &section_table[num_code_sections], (uint32_t)(rom + size),
[](uint32_t addr, const SectionTableEntry& entry) {
return addr < entry.size + entry.rom_addr;
}
);
// Load the overlays that were found
for (auto it = lower; it != upper; ++it) {
load_overlay(std::distance(&section_table[0], it), it->rom_addr - rom + ram_addr);
}
}
extern "C" void unload_overlays(int32_t ram_addr, uint32_t size);
extern "C" void unload_overlay_by_id(uint32_t id) {
uint32_t section_table_index = overlay_sections_by_index[id];
const SectionTableEntry& section = section_table[section_table_index];
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; });
if (find_it != loaded_sections.end()) {
// Determine where each function was loaded to and remove that entry from the function map
for (size_t func_index = 0; func_index < section.num_funcs; func_index++) {
const auto& func = section.funcs[func_index];
uint32_t func_address = func.offset + find_it->loaded_ram_addr;
func_map.erase(func_address);
}
// Reset the section's address in the address table
section_addresses[section.index] = section.ram_addr;
// Remove the section from the loaded section map
loaded_sections.erase(find_it);
}
}
extern "C" void load_overlay_by_id(uint32_t id, uint32_t ram_addr) {
uint32_t section_table_index = overlay_sections_by_index[id];
const SectionTableEntry& section = section_table[section_table_index];
int32_t prev_address = section_addresses[section.index];
if (/*ram_addr >= 0x80000000 && ram_addr < 0x81000000) {*/ prev_address == section.ram_addr) {
load_overlay(section_table_index, ram_addr);
}
else {
int32_t new_address = prev_address + ram_addr;
unload_overlay_by_id(id);
load_overlay(section_table_index, new_address);
}
}
extern "C" void unload_overlays(int32_t ram_addr, uint32_t size) {
for (auto it = loaded_sections.begin(); it != loaded_sections.end();) {
const auto& section = section_table[it->section_table_index];
// Check if the unloaded region overlaps with the loaded section
if (ram_addr < (it->loaded_ram_addr + section.size) && (ram_addr + size) >= it->loaded_ram_addr) {
// Check if the section isn't entirely in the loaded region
if (ram_addr > it->loaded_ram_addr || (ram_addr + size) < (it->loaded_ram_addr + section.size)) {
fprintf(stderr,
"Cannot partially unload section\n"
" rom: 0x%08X size: 0x%08X loaded_addr: 0x%08X\n"
" unloaded_ram: 0x%08X unloaded_size : 0x%08X\n",
section.rom_addr, section.size, it->loaded_ram_addr, ram_addr, size);
assert(false);
std::exit(EXIT_FAILURE);
}
// Determine where each function was loaded to and remove that entry from the function map
for (size_t func_index = 0; func_index < section.num_funcs; func_index++) {
const auto& func = section.funcs[func_index];
uint32_t func_address = func.offset + it->loaded_ram_addr;
func_map.erase(func_address);
}
// Reset the section's address in the address table
section_addresses[section.index] = section.ram_addr;
// Remove the section from the loaded section map
it = loaded_sections.erase(it);
// Skip incrementing the iterator
continue;
}
++it;
}
}
void load_patch_functions();
void init_overlays() {
for (size_t section_index = 0; section_index < num_code_sections; section_index++) {
section_addresses[section_table[section_index].index] = section_table[section_index].ram_addr;
}
// Sort the executable sections by rom address
std::sort(&section_table[0], &section_table[num_code_sections],
[](const SectionTableEntry& a, const SectionTableEntry& b) {
return a.rom_addr < b.rom_addr;
}
);
load_patch_functions();
}
extern "C" recomp_func_t * get_function(int32_t addr) {
auto func_find = func_map.find(addr);
if (func_find == func_map.end()) {
fprintf(stderr, "Failed to find function at 0x%08X\n", addr);
assert(false);
std::exit(EXIT_FAILURE);
}
return func_find->second;
}

View File

@ -1,35 +0,0 @@
#include "recomp.h"
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
extern "C" void osPfsInitPak_recomp(uint8_t * rdram, recomp_context* ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsFreeBlocks_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsAllocateFile_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsDeleteFile_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsFileState_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsFindFile_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsReadWriteFile_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}
extern "C" void osPfsChecker_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 1; // PFS_ERR_NOPACK
}

View File

@ -1,11 +0,0 @@
#include <unordered_map>
#include <algorithm>
#include <vector>
#include "recomp.h"
#include "../../RecompiledPatches/recomp_overlays.inl"
void load_special_overlay(const SectionTableEntry& section, int32_t ram);
void load_patch_functions() {
load_special_overlay(section_table[0], section_table[0].ram_addr);
}

View File

@ -1,302 +0,0 @@
#include <memory>
#include <fstream>
#include <array>
#include <cstring>
#include <string>
#include <mutex>
#include "recomp.h"
#include "recomp_game.h"
#include "recomp_config.h"
#include "recomp_files.h"
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
static std::vector<uint8_t> rom;
bool recomp::is_rom_loaded() {
return !rom.empty();
}
void recomp::set_rom_contents(std::vector<uint8_t>&& new_rom) {
rom = std::move(new_rom);
}
// Flashram occupies the same physical address as sram, but that issue is avoided because libultra exposes
// a high-level interface for flashram. Because that high-level interface is reimplemented, low level accesses
// that involve physical addresses don't need to be handled for flashram.
constexpr uint32_t sram_base = 0x08000000;
constexpr uint32_t rom_base = 0x10000000;
constexpr uint32_t drive_base = 0x06000000;
constexpr uint32_t k1_to_phys(uint32_t addr) {
return addr & 0x1FFFFFFF;
}
constexpr uint32_t phys_to_k1(uint32_t addr) {
return addr | 0xA0000000;
}
extern "C" void __osPiGetAccess_recomp(uint8_t* rdram, recomp_context* ctx) {
}
extern "C" void __osPiRelAccess_recomp(uint8_t* rdram, recomp_context* ctx) {
}
extern "C" void osCartRomInit_recomp(uint8_t* rdram, recomp_context* ctx) {
OSPiHandle* handle = TO_PTR(OSPiHandle, ultramodern::cart_handle);
handle->type = 0; // cart
handle->baseAddress = phys_to_k1(rom_base);
handle->domain = 0;
ctx->r2 = (gpr)ultramodern::cart_handle;
}
extern "C" void osDriveRomInit_recomp(uint8_t * rdram, recomp_context * ctx) {
OSPiHandle* handle = TO_PTR(OSPiHandle, ultramodern::drive_handle);
handle->type = 1; // bulk
handle->baseAddress = phys_to_k1(drive_base);
handle->domain = 0;
ctx->r2 = (gpr)ultramodern::drive_handle;
}
extern "C" void osCreatePiManager_recomp(uint8_t* rdram, recomp_context* ctx) {
;
}
void recomp::do_rom_read(uint8_t* rdram, gpr ram_address, uint32_t physical_addr, size_t num_bytes) {
// TODO use word copies when possible
// TODO handle misaligned DMA
assert((physical_addr & 0x1) == 0 && "Only PI DMA from aligned ROM addresses is currently supported");
assert((ram_address & 0x7) == 0 && "Only PI DMA to aligned RDRAM addresses is currently supported");
assert((num_bytes & 0x1) == 0 && "Only PI DMA with aligned sizes is currently supported");
uint8_t* rom_addr = rom.data() + physical_addr - rom_base;
for (size_t i = 0; i < num_bytes; i++) {
MEM_B(i, ram_address) = *rom_addr;
rom_addr++;
}
}
void recomp::do_rom_pio(uint8_t* rdram, gpr ram_address, uint32_t physical_addr) {
assert((physical_addr & 0x3) == 0 && "PIO not 4-byte aligned in device, currently unsupported");
assert((ram_address & 0x3) == 0 && "PIO not 4-byte aligned in RDRAM, currently unsupported");
uint8_t* rom_addr = rom.data() + physical_addr - rom_base;
MEM_B(0, ram_address) = *rom_addr++;
MEM_B(1, ram_address) = *rom_addr++;
MEM_B(2, ram_address) = *rom_addr++;
MEM_B(3, ram_address) = *rom_addr++;
}
struct {
std::array<char, 0x20000> save_buffer;
std::thread saving_thread;
moodycamel::LightweightSemaphore write_sempahore;
std::mutex save_buffer_mutex;
} save_context;
const std::u8string save_folder = u8"saves";
const std::u8string save_filename = std::u8string{recomp::mm_game_id} + u8".bin";
std::filesystem::path get_save_file_path() {
return recomp::get_app_folder_path() / save_folder / save_filename;
}
void update_save_file() {
bool saving_failed = false;
{
std::ofstream save_file = recomp::open_output_file_with_backup(get_save_file_path(), std::ios_base::binary);
if (save_file.good()) {
std::lock_guard lock{ save_context.save_buffer_mutex };
save_file.write(save_context.save_buffer.data(), save_context.save_buffer.size());
}
else {
saving_failed = false;
}
}
if (!saving_failed) {
saving_failed = !recomp::finalize_output_file_with_backup(get_save_file_path());
}
if (saving_failed) {
recomp::message_box("Failed to write to the save file. Check your file permissions and whether the save folder has been moved to Dropbox or similar, as this can cause issues.");
}
}
extern std::atomic_bool exited;
void saving_thread_func(RDRAM_ARG1) {
while (!exited) {
bool save_buffer_updated = false;
// Repeatedly wait for a new action to be sent.
constexpr int64_t wait_time_microseconds = 10000;
constexpr int max_actions = 128;
int num_actions = 0;
// Wait up to the given timeout for a write to come in. Allow multiple writes to coalesce together into a single save.
// Cap the number of coalesced writes to guarantee that the save buffer eventually gets written out to the file even if the game
// is constantly sending writes.
while (save_context.write_sempahore.wait(wait_time_microseconds) && num_actions < max_actions) {
save_buffer_updated = true;
num_actions++;
}
// If an action came through that affected the save file, save the updated contents.
if (save_buffer_updated) {
update_save_file();
}
}
}
void save_write_ptr(const void* in, uint32_t offset, uint32_t count) {
{
std::lock_guard lock { save_context.save_buffer_mutex };
memcpy(&save_context.save_buffer[offset], in, count);
}
save_context.write_sempahore.signal();
}
void save_write(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count) {
{
std::lock_guard lock { save_context.save_buffer_mutex };
for (uint32_t i = 0; i < count; i++) {
save_context.save_buffer[offset + i] = MEM_B(i, rdram_address);
}
}
save_context.write_sempahore.signal();
}
void save_read(RDRAM_ARG PTR(void) rdram_address, uint32_t offset, uint32_t count) {
std::lock_guard lock { save_context.save_buffer_mutex };
for (size_t i = 0; i < count; i++) {
MEM_B(i, rdram_address) = save_context.save_buffer[offset + i];
}
}
void save_clear(uint32_t start, uint32_t size, char value) {
{
std::lock_guard lock { save_context.save_buffer_mutex };
std::fill_n(save_context.save_buffer.begin() + start, size, value);
}
save_context.write_sempahore.signal();
}
void ultramodern::init_saving(RDRAM_ARG1) {
std::filesystem::path save_file_path = get_save_file_path();
// Ensure the save file directory exists.
std::filesystem::create_directories(save_file_path.parent_path());
// Read the save file if it exists.
std::ifstream save_file = recomp::open_input_file_with_backup(save_file_path, std::ios_base::binary);
if (save_file.good()) {
save_file.read(save_context.save_buffer.data(), save_context.save_buffer.size());
}
else {
// Otherwise clear the save file to all zeroes.
save_context.save_buffer.fill(0);
}
save_context.saving_thread = std::thread{saving_thread_func, PASS_RDRAM};
}
void ultramodern::join_saving_thread() {
save_context.saving_thread.join();
}
void do_dma(RDRAM_ARG PTR(OSMesgQueue) mq, gpr rdram_address, uint32_t physical_addr, uint32_t size, uint32_t direction) {
// TODO asynchronous transfer
// TODO implement unaligned DMA correctly
if (direction == 0) {
if (physical_addr >= rom_base) {
// read cart rom
recomp::do_rom_read(rdram, rdram_address, physical_addr, size);
// Send a message to the mq to indicate that the transfer completed
osSendMesg(rdram, mq, 0, OS_MESG_NOBLOCK);
} else if (physical_addr >= sram_base) {
// read sram
save_read(rdram, rdram_address, physical_addr - sram_base, size);
// Send a message to the mq to indicate that the transfer completed
osSendMesg(rdram, mq, 0, OS_MESG_NOBLOCK);
} else {
fprintf(stderr, "[WARN] PI DMA read from unknown region, phys address 0x%08X\n", physical_addr);
}
} else {
if (physical_addr >= rom_base) {
// write cart rom
throw std::runtime_error("ROM DMA write unimplemented");
} else if (physical_addr >= sram_base) {
// write sram
save_write(rdram, rdram_address, physical_addr - sram_base, size);
// Send a message to the mq to indicate that the transfer completed
osSendMesg(rdram, mq, 0, OS_MESG_NOBLOCK);
} else {
fprintf(stderr, "[WARN] PI DMA write to unknown region, phys address 0x%08X\n", physical_addr);
}
}
}
extern "C" void osPiStartDma_recomp(RDRAM_ARG recomp_context* ctx) {
uint32_t mb = ctx->r4;
uint32_t pri = ctx->r5;
uint32_t direction = ctx->r6;
uint32_t devAddr = ctx->r7 | rom_base;
gpr dramAddr = MEM_W(0x10, ctx->r29);
uint32_t size = MEM_W(0x14, ctx->r29);
PTR(OSMesgQueue) mq = MEM_W(0x18, ctx->r29);
uint32_t physical_addr = k1_to_phys(devAddr);
debug_printf("[pi] DMA from 0x%08X into 0x%08X of size 0x%08X\n", devAddr, dramAddr, size);
do_dma(PASS_RDRAM mq, dramAddr, physical_addr, size, direction);
ctx->r2 = 0;
}
extern "C" void osEPiStartDma_recomp(RDRAM_ARG recomp_context* ctx) {
OSPiHandle* handle = TO_PTR(OSPiHandle, ctx->r4);
OSIoMesg* mb = TO_PTR(OSIoMesg, ctx->r5);
uint32_t direction = ctx->r6;
uint32_t devAddr = handle->baseAddress | mb->devAddr;
gpr dramAddr = mb->dramAddr;
uint32_t size = mb->size;
PTR(OSMesgQueue) mq = mb->hdr.retQueue;
uint32_t physical_addr = k1_to_phys(devAddr);
debug_printf("[pi] DMA from 0x%08X into 0x%08X of size 0x%08X\n", devAddr, dramAddr, size);
do_dma(PASS_RDRAM mq, dramAddr, physical_addr, size, direction);
ctx->r2 = 0;
}
extern "C" void osEPiReadIo_recomp(RDRAM_ARG recomp_context * ctx) {
OSPiHandle* handle = TO_PTR(OSPiHandle, ctx->r4);
uint32_t devAddr = handle->baseAddress | ctx->r5;
gpr dramAddr = ctx->r6;
uint32_t physical_addr = k1_to_phys(devAddr);
if (physical_addr > rom_base) {
// cart rom
recomp::do_rom_pio(PASS_RDRAM dramAddr, physical_addr);
} else {
// sram
assert(false && "SRAM ReadIo unimplemented");
}
ctx->r2 = 0;
}
extern "C" void osPiGetStatus_recomp(RDRAM_ARG recomp_context * ctx) {
ctx->r2 = 0;
}
extern "C" void osPiRawStartDma_recomp(RDRAM_ARG recomp_context * ctx) {
ctx->r2 = 0;
}

View File

@ -1,72 +0,0 @@
#include <vector>
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
#include "euc-jp.h"
extern "C" void __checkHardware_msp_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 0;
}
extern "C" void __checkHardware_kmc_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 0;
}
extern "C" void __checkHardware_isv_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 0;
}
extern "C" void __osInitialize_msp_recomp(uint8_t * rdram, recomp_context * ctx) {
}
extern "C" void __osInitialize_kmc_recomp(uint8_t * rdram, recomp_context * ctx) {
}
extern "C" void __osInitialize_isv_recomp(uint8_t * rdram, recomp_context * ctx) {
}
extern "C" void isPrintfInit_recomp(uint8_t * rdram, recomp_context * ctx) {
}
extern "C" void __osRdbSend_recomp(uint8_t * rdram, recomp_context * ctx) {
gpr buf = ctx->r4;
size_t size = ctx->r5;
u32 type = (u32)ctx->r6;
std::unique_ptr<char[]> to_print = std::make_unique<char[]>(size + 1);
for (size_t i = 0; i < size; i++) {
to_print[i] = MEM_B(i, buf);
}
to_print[size] = '\x00';
fwrite(to_print.get(), 1, size, stdout);
ctx->r2 = size;
}
extern "C" void is_proutSyncPrintf_recomp(uint8_t * rdram, recomp_context * ctx) {
// Buffering to speed up print performance
static std::vector<char> print_buffer;
gpr buf = ctx->r5;
size_t size = ctx->r6;
//for (size_t i = 0; i < size; i++) {
// // Add the new character to the buffer
// char cur_char = MEM_B(i, buf);
// // If the new character is a newline, flush the buffer
// if (cur_char == '\n') {
// std::string utf8_str = Encoding::decode_eucjp(std::string_view{ print_buffer.data(), print_buffer.size() });
// puts(utf8_str.c_str());
// print_buffer.clear();
// } else {
// print_buffer.push_back(cur_char);
// }
//}
//fwrite(to_print.get(), size, 1, stdout);
ctx->r2 = 1;
}

View File

@ -1,445 +0,0 @@
#ifdef _WIN32
#include <Windows.h>
#endif
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <cmath>
#include <unordered_map>
#include <unordered_set>
#include <fstream>
#include <iostream>
#include "recomp.h"
#include "recomp_overlays.h"
#include "recomp_game.h"
#include "recomp_config.h"
#include "xxHash/xxh3.h"
#include "../ultramodern/ultramodern.hpp"
#include "../../RecompiledPatches/patches_bin.h"
#ifdef HAS_MM_SHADER_CACHE
#include "mm_shader_cache.h"
#endif
#ifdef _MSC_VER
inline uint32_t byteswap(uint32_t val) {
return _byteswap_ulong(val);
}
#else
constexpr uint32_t byteswap(uint32_t val) {
return __builtin_bswap32(val);
}
#endif
struct RomEntry {
uint64_t xxhash3_value;
std::u8string stored_filename;
std::string internal_rom_name;
};
const std::unordered_map<recomp::Game, RomEntry> game_roms {
{ recomp::Game::MM, { 0xEF18B4A9E2386169ULL, std::u8string{recomp::mm_game_id} + u8".z64", "ZELDA MAJORA'S MASK" }},
};
bool check_hash(const std::vector<uint8_t>& rom_data, uint64_t expected_hash) {
uint64_t calculated_hash = XXH3_64bits(rom_data.data(), rom_data.size());
return calculated_hash == expected_hash;
}
static std::vector<uint8_t> read_file(const std::filesystem::path& path) {
std::vector<uint8_t> ret;
std::ifstream file{ path, std::ios::binary};
if (file.good()) {
file.seekg(0, std::ios::end);
ret.resize(file.tellg());
file.seekg(0, std::ios::beg);
file.read(reinterpret_cast<char*>(ret.data()), ret.size());
}
return ret;
}
bool write_file(const std::filesystem::path& path, const std::vector<uint8_t>& data) {
std::ofstream out_file{ path, std::ios::binary };
if (!out_file.good()) {
return false;
}
out_file.write(reinterpret_cast<const char*>(data.data()), data.size());
return true;
}
bool check_stored_rom(const RomEntry& game_entry) {
std::vector stored_rom_data = read_file(recomp::get_app_folder_path() / game_entry.stored_filename);
if (!check_hash(stored_rom_data, game_entry.xxhash3_value)) {
// Incorrect hash, remove the stored ROM file if it exists.
std::filesystem::remove(recomp::get_app_folder_path() / game_entry.stored_filename);
return false;
}
return true;
}
static std::unordered_set<recomp::Game> valid_game_roms;
bool recomp::is_rom_valid(recomp::Game game) {
return valid_game_roms.contains(game);
}
void recomp::check_all_stored_roms() {
for (const auto& cur_rom_entry: game_roms) {
if (check_stored_rom(cur_rom_entry.second)) {
valid_game_roms.insert(cur_rom_entry.first);
}
}
}
bool recomp::load_stored_rom(recomp::Game game) {
auto find_it = game_roms.find(game);
if (find_it == game_roms.end()) {
return false;
}
std::vector<uint8_t> stored_rom_data = read_file(recomp::get_app_folder_path() / find_it->second.stored_filename);
if (!check_hash(stored_rom_data, find_it->second.xxhash3_value)) {
// The ROM no longer has the right hash, delete it.
std::filesystem::remove(recomp::get_app_folder_path() / find_it->second.stored_filename);
return false;
}
recomp::set_rom_contents(std::move(stored_rom_data));
return true;
}
const std::array<uint8_t, 4> first_rom_bytes { 0x80, 0x37, 0x12, 0x40 };
enum class ByteswapType {
NotByteswapped,
Byteswapped4,
Byteswapped2,
Invalid
};
ByteswapType check_rom_start(const std::vector<uint8_t>& rom_data) {
if (rom_data.size() < 4) {
return ByteswapType::Invalid;
}
bool matched_all = true;
auto check_match = [&](uint8_t index0, uint8_t index1, uint8_t index2, uint8_t index3) {
return
rom_data[0] == first_rom_bytes[index0] &&
rom_data[1] == first_rom_bytes[index1] &&
rom_data[2] == first_rom_bytes[index2] &&
rom_data[3] == first_rom_bytes[index3];
};
// Check if the ROM is already in the correct byte order.
if (check_match(0,1,2,3)) {
return ByteswapType::NotByteswapped;
}
// Check if the ROM has been byteswapped in groups of 4 bytes.
if (check_match(3,2,1,0)) {
return ByteswapType::Byteswapped4;
}
// Check if the ROM has been byteswapped in groups of 2 bytes.
if (check_match(1,0,3,2)) {
return ByteswapType::Byteswapped2;
}
// No match found.
return ByteswapType::Invalid;
}
void byteswap_data(std::vector<uint8_t>& rom_data, size_t index_xor) {
for (size_t rom_pos = 0; rom_pos < rom_data.size(); rom_pos += 4) {
uint8_t temp0 = rom_data[rom_pos + 0];
uint8_t temp1 = rom_data[rom_pos + 1];
uint8_t temp2 = rom_data[rom_pos + 2];
uint8_t temp3 = rom_data[rom_pos + 3];
rom_data[rom_pos + (0 ^ index_xor)] = temp0;
rom_data[rom_pos + (1 ^ index_xor)] = temp1;
rom_data[rom_pos + (2 ^ index_xor)] = temp2;
rom_data[rom_pos + (3 ^ index_xor)] = temp3;
}
}
recomp::RomValidationError recomp::select_rom(const std::filesystem::path& rom_path, Game game) {
auto find_it = game_roms.find(game);
if (find_it == game_roms.end()) {
return recomp::RomValidationError::OtherError;
}
const RomEntry& game_entry = find_it->second;
std::vector<uint8_t> rom_data = read_file(rom_path);
if (rom_data.empty()) {
return recomp::RomValidationError::FailedToOpen;
}
// Pad the rom to the nearest multiple of 4 bytes.
rom_data.resize((rom_data.size() + 3) & ~3);
ByteswapType byteswap_type = check_rom_start(rom_data);
switch (byteswap_type) {
case ByteswapType::Invalid:
return recomp::RomValidationError::NotARom;
case ByteswapType::Byteswapped2:
byteswap_data(rom_data, 1);
break;
case ByteswapType::Byteswapped4:
byteswap_data(rom_data, 3);
break;
case ByteswapType::NotByteswapped:
break;
}
if (!check_hash(rom_data, game_entry.xxhash3_value)) {
const std::string_view name{ reinterpret_cast<const char*>(rom_data.data()) + 0x20, game_entry.internal_rom_name.size()};
if (name == game_entry.internal_rom_name) {
return recomp::RomValidationError::IncorrectVersion;
}
else {
if (game == recomp::Game::MM && std::string_view{ reinterpret_cast<const char*>(rom_data.data()) + 0x20, 19 } == "THE LEGEND OF ZELDA") {
return recomp::RomValidationError::NotYet;
}
else {
return recomp::RomValidationError::IncorrectRom;
}
}
}
write_file(recomp::get_app_folder_path() / game_entry.stored_filename, rom_data);
return recomp::RomValidationError::Good;
}
extern "C" void osGetMemSize_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 8 * 1024 * 1024;
}
enum class StatusReg {
FR = 0x04000000,
};
extern "C" void cop0_status_write(recomp_context* ctx, gpr value) {
uint32_t old_sr = ctx->status_reg;
uint32_t new_sr = (uint32_t)value;
uint32_t changed = old_sr ^ new_sr;
// Check if the FR bit changed
if (changed & (uint32_t)StatusReg::FR) {
// Check if the FR bit was set
if (new_sr & (uint32_t)StatusReg::FR) {
// FR = 1, odd single floats point to their own registers
ctx->f_odd = &ctx->f1.u32l;
ctx->mips3_float_mode = true;
}
// Otherwise, it was cleared
else {
// FR = 0, odd single floats point to the upper half of the previous register
ctx->f_odd = &ctx->f0.u32h;
ctx->mips3_float_mode = false;
}
// Remove the FR bit from the changed bits as it's been handled
changed &= ~(uint32_t)StatusReg::FR;
}
// If any other bits were changed, assert false as they're not handled currently
if (changed) {
printf("Unhandled status register bits changed: 0x%08X\n", changed);
assert(false);
exit(EXIT_FAILURE);
}
// Update the status register in the context
ctx->status_reg = new_sr;
}
extern "C" gpr cop0_status_read(recomp_context* ctx) {
return (gpr)(int32_t)ctx->status_reg;
}
extern "C" void switch_error(const char* func, uint32_t vram, uint32_t jtbl) {
printf("Switch-case out of bounds in %s at 0x%08X for jump table at 0x%08X\n", func, vram, jtbl);
assert(false);
exit(EXIT_FAILURE);
}
extern "C" void do_break(uint32_t vram) {
printf("Encountered break at original vram 0x%08X\n", vram);
assert(false);
exit(EXIT_FAILURE);
}
void run_thread_function(uint8_t* rdram, uint64_t addr, uint64_t sp, uint64_t arg) {
recomp_context ctx{};
ctx.r29 = sp;
ctx.r4 = arg;
ctx.mips3_float_mode = 0;
ctx.f_odd = &ctx.f0.u32h;
recomp_func_t* func = get_function(addr);
func(rdram, &ctx);
}
// Recomp generation functions
extern "C" void recomp_entrypoint(uint8_t * rdram, recomp_context * ctx);
gpr get_entrypoint_address();
const char* get_rom_name();
void read_patch_data(uint8_t* rdram, gpr patch_data_address) {
for (size_t i = 0; i < sizeof(mm_patches_bin); i++) {
MEM_B(i, patch_data_address) = mm_patches_bin[i];
}
}
void init(uint8_t* rdram, recomp_context* ctx) {
// Initialize the overlays
init_overlays();
// Get entrypoint from recomp function
gpr entrypoint = get_entrypoint_address();
// Load overlays in the first 1MB
load_overlays(0x1000, (int32_t)entrypoint, 1024 * 1024);
// Initial 1MB DMA (rom address 0x1000 = physical address 0x10001000)
recomp::do_rom_read(rdram, entrypoint, 0x10001000, 0x100000);
// Read in any extra data from patches
read_patch_data(rdram, (gpr)(s32)0x80801000);
// Set up stack pointer
ctx->r29 = 0xFFFFFFFF803FFFF0u;
// Set up context floats
ctx->f_odd = &ctx->f0.u32h;
ctx->mips3_float_mode = false;
// Initialize variables normally set by IPL3
constexpr int32_t osTvType = 0x80000300;
constexpr int32_t osRomType = 0x80000304;
constexpr int32_t osRomBase = 0x80000308;
constexpr int32_t osResetType = 0x8000030c;
constexpr int32_t osCicId = 0x80000310;
constexpr int32_t osVersion = 0x80000314;
constexpr int32_t osMemSize = 0x80000318;
constexpr int32_t osAppNMIBuffer = 0x8000031c;
MEM_W(osTvType, 0) = 1; // NTSC
MEM_W(osRomBase, 0) = 0xB0000000u; // standard rom base
MEM_W(osResetType, 0) = 0; // cold reset
MEM_W(osMemSize, 0) = 8 * 1024 * 1024; // 8MB
}
std::atomic<recomp::Game> game_started = recomp::Game::None;
void recomp::start_game(recomp::Game game) {
game_started.store(game);
game_started.notify_all();
}
bool ultramodern::is_game_started() {
return game_started.load() != recomp::Game::None;
}
void set_audio_callbacks(const ultramodern::audio_callbacks_t& callbacks);
void set_input_callbacks(const ultramodern::input_callbacks_t& callback);
std::atomic_bool exited = false;
void ultramodern::quit() {
exited.store(true);
recomp::Game desired = recomp::Game::None;
game_started.compare_exchange_strong(desired, recomp::Game::Quit);
game_started.notify_all();
}
void recomp::start(ultramodern::WindowHandle window_handle, const ultramodern::audio_callbacks_t& audio_callbacks, const ultramodern::input_callbacks_t& input_callbacks, const ultramodern::gfx_callbacks_t& gfx_callbacks_) {
recomp::check_all_stored_roms();
set_audio_callbacks(audio_callbacks);
set_input_callbacks(input_callbacks);
ultramodern::gfx_callbacks_t gfx_callbacks = gfx_callbacks_;
ultramodern::gfx_callbacks_t::gfx_data_t gfx_data{};
if (gfx_callbacks.create_gfx) {
gfx_data = gfx_callbacks.create_gfx();
}
if (window_handle == ultramodern::WindowHandle{}) {
if (gfx_callbacks.create_window) {
window_handle = gfx_callbacks.create_window(gfx_data);
}
else {
assert(false && "No create_window callback provided");
}
}
// Allocate rdram_buffer
std::unique_ptr<uint8_t[]> rdram_buffer = std::make_unique<uint8_t[]>(ultramodern::rdram_size);
std::memset(rdram_buffer.get(), 0, ultramodern::rdram_size);
std::thread game_thread{[](ultramodern::WindowHandle window_handle, uint8_t* rdram) {
debug_printf("[Recomp] Starting\n");
ultramodern::set_native_thread_name("Game Start Thread");
ultramodern::preinit(rdram, window_handle);
game_started.wait(recomp::Game::None);
recomp_context context{};
switch (game_started.load()) {
case recomp::Game::MM:
if (!recomp::load_stored_rom(recomp::Game::MM)) {
recomp::message_box("Error opening stored ROM! Please restart this program.");
}
#ifdef HAS_MM_SHADER_CACHE
ultramodern::load_shader_cache({mm_shader_cache_bytes, sizeof(mm_shader_cache_bytes)});
#endif
init(rdram, &context);
try {
recomp_entrypoint(rdram, &context);
} catch (ultramodern::thread_terminated& terminated) {
}
break;
case recomp::Game::Quit:
break;
}
debug_printf("[Recomp] Quitting\n");
}, window_handle, rdram_buffer.get()};
while (!exited) {
ultramodern::sleep_milliseconds(1);
if (gfx_callbacks.update_gfx != nullptr) {
gfx_callbacks.update_gfx(gfx_data);
}
}
game_thread.join();
ultramodern::join_event_threads();
ultramodern::join_thread_cleaner_thread();
ultramodern::join_saving_thread();
}

View File

@ -1,50 +0,0 @@
#include <cstdio>
#include <fstream>
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
extern "C" void osSpTaskLoad_recomp(uint8_t* rdram, recomp_context* ctx) {
// Nothing to do here
}
bool dump_frame = false;
extern "C" void osSpTaskStartGo_recomp(uint8_t* rdram, recomp_context* ctx) {
//printf("[sp] osSpTaskStartGo(0x%08X)\n", (uint32_t)ctx->r4);
OSTask* task = TO_PTR(OSTask, ctx->r4);
if (task->t.type == M_GFXTASK) {
//printf("[sp] Gfx task: %08X\n", (uint32_t)ctx->r4);
} else if (task->t.type == M_AUDTASK) {
//printf("[sp] Audio task: %08X\n", (uint32_t)ctx->r4);
}
// For debugging
if (dump_frame) {
char addr_str[32];
constexpr size_t ram_size = 0x800000;
std::unique_ptr<char[]> ram_unswapped = std::make_unique<char[]>(ram_size);
snprintf(addr_str, sizeof(addr_str) - 1, "%08X", task->t.data_ptr);
addr_str[sizeof(addr_str) - 1] = '\0';
std::ofstream dump_file{ "ramdump" + std::string{ addr_str } + ".bin", std::ios::binary};
for (size_t i = 0; i < ram_size; i++) {
ram_unswapped[i] = rdram[i ^ 3];
}
dump_file.write(ram_unswapped.get(), ram_size);
dump_frame = false;
}
ultramodern::submit_rsp_task(rdram, ctx->r4);
}
extern "C" void osSpTaskYield_recomp(uint8_t* rdram, recomp_context* ctx) {
// Ignore yield requests (acts as if the task completed before it received the yield request)
}
extern "C" void osSpTaskYielded_recomp(uint8_t* rdram, recomp_context* ctx) {
// Task yield requests are ignored, so always return 0 as tasks will never be yielded
ctx->r2 = 0;
}
extern "C" void __osSpSetPc_recomp(uint8_t* rdram, recomp_context* ctx) {
assert(false);
}

View File

@ -1,45 +0,0 @@
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
// None of these functions need to be reimplemented, so stub them out
extern "C" void osUnmapTLBAll_recomp(uint8_t * rdram, recomp_context * ctx) {
// TODO this will need to be implemented in the future for any games that actually use the TLB
}
extern "C" void osVoiceInit_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 11; // CONT_ERR_DEVICE
}
extern "C" void osVoiceSetWord_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceCheckWord_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceStopReadData_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceMaskDictionary_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceStartReadData_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceControlGain_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceGetReadData_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}
extern "C" void osVoiceClearDictionary_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
}

View File

@ -1,163 +0,0 @@
#include <memory>
#include "../ultramodern/ultra64.h"
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
extern "C" void osInitialize_recomp(uint8_t * rdram, recomp_context * ctx) {
osInitialize();
}
extern "C" void __osInitialize_common_recomp(uint8_t * rdram, recomp_context * ctx) {
osInitialize();
}
extern "C" void osCreateThread_recomp(uint8_t* rdram, recomp_context* ctx) {
osCreateThread(rdram, (int32_t)ctx->r4, (OSId)ctx->r5, (int32_t)ctx->r6, (int32_t)ctx->r7,
(int32_t)MEM_W(0x10, ctx->r29), (OSPri)MEM_W(0x14, ctx->r29));
}
extern "C" void osStartThread_recomp(uint8_t* rdram, recomp_context* ctx) {
osStartThread(rdram, (int32_t)ctx->r4);
}
extern "C" void osStopThread_recomp(uint8_t * rdram, recomp_context * ctx) {
osStopThread(rdram, (int32_t)ctx->r4);
}
extern "C" void osDestroyThread_recomp(uint8_t * rdram, recomp_context * ctx) {
osDestroyThread(rdram, (int32_t)ctx->r4);
}
extern "C" void osYieldThread_recomp(uint8_t * rdram, recomp_context * ctx) {
assert(false);
// osYieldThread(rdram);
}
extern "C" void osSetThreadPri_recomp(uint8_t* rdram, recomp_context* ctx) {
osSetThreadPri(rdram, (int32_t)ctx->r4, (OSPri)ctx->r5);
}
extern "C" void osGetThreadPri_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = osGetThreadPri(rdram, (int32_t)ctx->r4);
}
extern "C" void osGetThreadId_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = osGetThreadId(rdram, (int32_t)ctx->r4);
}
extern "C" void osCreateMesgQueue_recomp(uint8_t* rdram, recomp_context* ctx) {
osCreateMesgQueue(rdram, (int32_t)ctx->r4, (int32_t)ctx->r5, (s32)ctx->r6);
}
extern "C" void osRecvMesg_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = osRecvMesg(rdram, (int32_t)ctx->r4, (int32_t)ctx->r5, (s32)ctx->r6);
}
extern "C" void osSendMesg_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = osSendMesg(rdram, (int32_t)ctx->r4, (OSMesg)ctx->r5, (s32)ctx->r6);
}
extern "C" void osJamMesg_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = osJamMesg(rdram, (int32_t)ctx->r4, (OSMesg)ctx->r5, (s32)ctx->r6);
}
extern "C" void osSetEventMesg_recomp(uint8_t* rdram, recomp_context* ctx) {
osSetEventMesg(rdram, (OSEvent)ctx->r4, (int32_t)ctx->r5, (OSMesg)ctx->r6);
}
extern "C" void osViSetEvent_recomp(uint8_t * rdram, recomp_context * ctx) {
osViSetEvent(rdram, (int32_t)ctx->r4, (OSMesg)ctx->r5, (u32)ctx->r6);
}
extern "C" void osGetCount_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = osGetCount();
}
extern "C" void osGetTime_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t total_count = osGetTime();
ctx->r2 = (int32_t)(total_count >> 32);
ctx->r3 = (int32_t)(total_count >> 0);
}
extern "C" void osSetTimer_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t countdown = ((uint64_t)(ctx->r6) << 32) | ((ctx->r7) & 0xFFFFFFFFu);
uint64_t interval = load_doubleword(rdram, ctx->r29, 0x10);
ctx->r2 = osSetTimer(rdram, (int32_t)ctx->r4, countdown, interval, (int32_t)MEM_W(0x18, ctx->r29), (OSMesg)MEM_W(0x1C, ctx->r29));
}
extern "C" void osStopTimer_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = osStopTimer(rdram, (int32_t)ctx->r4);
}
extern "C" void osVirtualToPhysical_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = osVirtualToPhysical((int32_t)ctx->r4);
}
extern "C" void osInvalDCache_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void osInvalICache_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void osWritebackDCache_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void osWritebackDCacheAll_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void osSetIntMask_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void __osDisableInt_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void __osRestoreInt_recomp(uint8_t * rdram, recomp_context * ctx) {
;
}
extern "C" void __osSetFpcCsr_recomp(uint8_t * rdram, recomp_context * ctx) {
ctx->r2 = 0;
}
// For the Mario Party games (not working)
//extern "C" void longjmp_recomp(uint8_t * rdram, recomp_context * ctx) {
// RecompJmpBuf* buf = TO_PTR(RecompJmpBuf, ctx->r4);
//
// // Check if this is a buffer that was set up with setjmp
// if (buf->magic == SETJMP_MAGIC) {
// // If so, longjmp to it
// // Setjmp/longjmp does not work across threads, so verify that this buffer was made by this thread
// assert(buf->owner == ultramodern::this_thread());
// longjmp(buf->storage->buffer, ctx->r5);
// } else {
// // Otherwise, check if it was one built manually by the game with $ra pointing to a function
// gpr sp = MEM_W(0, ctx->r4);
// gpr ra = MEM_W(4, ctx->r4);
// ctx->r29 = sp;
// recomp_func_t* target = LOOKUP_FUNC(ra);
// if (target == nullptr) {
// fprintf(stderr, "Failed to find function for manual longjmp\n");
// std::quick_exit(EXIT_FAILURE);
// }
// target(rdram, ctx);
//
// // TODO kill this thread if the target function returns
// assert(false);
// }
//}
//
//#undef setjmp_recomp
//extern "C" void setjmp_recomp(uint8_t * rdram, recomp_context * ctx) {
// fprintf(stderr, "Program called setjmp_recomp\n");
// std::quick_exit(EXIT_FAILURE);
//}
//
//extern "C" int32_t osGetThreadEx(void) {
// return ultramodern::this_thread();
//}

View File

@ -1,47 +0,0 @@
#include "../ultramodern/ultramodern.hpp"
#include "recomp.h"
extern "C" void osViSetYScale_recomp(uint8_t* rdram, recomp_context * ctx) {
osViSetYScale(ctx->f12.fl);
}
extern "C" void osViSetXScale_recomp(uint8_t* rdram, recomp_context * ctx) {
osViSetXScale(ctx->f12.fl);
}
extern "C" void osCreateViManager_recomp(uint8_t* rdram, recomp_context* ctx) {
;
}
extern "C" void osViBlack_recomp(uint8_t* rdram, recomp_context* ctx) {
osViBlack((uint32_t)ctx->r4);
}
extern "C" void osViSetSpecialFeatures_recomp(uint8_t* rdram, recomp_context* ctx) {
osViSetSpecialFeatures((uint32_t)ctx->r4);
}
extern "C" void osViGetCurrentFramebuffer_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = (gpr)(int32_t)osViGetCurrentFramebuffer();
}
extern "C" void osViGetNextFramebuffer_recomp(uint8_t* rdram, recomp_context* ctx) {
ctx->r2 = (gpr)(int32_t)osViGetNextFramebuffer();
}
extern "C" void osViSwapBuffer_recomp(uint8_t* rdram, recomp_context* ctx) {
osViSwapBuffer(rdram, (int32_t)ctx->r4);
}
extern "C" void osViSetMode_recomp(uint8_t* rdram, recomp_context* ctx) {
osViSetMode(rdram, (int32_t)ctx->r4);
}
extern uint64_t total_vis;
extern "C" void wait_one_frame(uint8_t* rdram, recomp_context* ctx) {
uint64_t cur_vis = total_vis;
while (cur_vis == total_vis) {
std::this_thread::yield();
}
}

View File

@ -6,7 +6,7 @@
using ColourMap = Rml::UnorderedMap<Rml::String, Rml::Colourb>;
namespace recomp {
namespace recompui {
class PropertyParserColorHack : public Rml::PropertyParser {
public:
PropertyParserColorHack();

View File

@ -1,13 +1,13 @@
#include "recomp_ui.h"
#include "recomp_input.h"
#include "recomp_sound.h"
#include "recomp_config.h"
#include "recomp_debug.h"
#include "zelda_sound.h"
#include "zelda_config.h"
#include "zelda_debug.h"
#include "promptfont.h"
#include "../../ultramodern/config.hpp"
#include "../../ultramodern/ultramodern.hpp"
#include "ultramodern/config.hpp"
#include "ultramodern/ultramodern.hpp"
#include "RmlUi/Core.h"
#include "rt64_layer.h"
#include "ultramodern/rt64_layer.hpp"
ultramodern::GraphicsConfig new_options;
Rml::DataModelHandle nav_help_model_handle;
@ -16,9 +16,9 @@ Rml::DataModelHandle controls_model_handle;
Rml::DataModelHandle graphics_model_handle;
Rml::DataModelHandle sound_options_model_handle;
recomp::PromptContext prompt_context;
recompui::PromptContext prompt_context;
namespace recomp {
namespace recompui {
const std::unordered_map<ButtonVariant, std::string> button_variants {
{ButtonVariant::Primary, "primary"},
{ButtonVariant::Secondary, "secondary"},
@ -164,7 +164,7 @@ static bool cont_active = true;
static recomp::InputDevice cur_device = recomp::InputDevice::Controller;
void recomp::finish_scanning_input(recomp::InputField scanned_field) {
recomp::set_input_binding(static_cast<recomp::GameInput>(scanned_input_index), scanned_binding_index, cur_device, scanned_field);
recomp::set_input_binding(static_cast<recomp::GameInput>(scanned_input_index), scanned_binding_index, cur_device, scanned_field);
scanned_input_index = -1;
scanned_binding_index = -1;
controls_model_handle.DirtyVariable("inputs");
@ -173,7 +173,7 @@ void recomp::finish_scanning_input(recomp::InputField scanned_field) {
}
void recomp::cancel_scanning_input() {
recomp::stop_scanning_input();
recomp::stop_scanning_input();
scanned_input_index = -1;
scanned_binding_index = -1;
controls_model_handle.DirtyVariable("inputs");
@ -198,13 +198,13 @@ void recomp::config_menu_set_cont_or_kb(bool cont_interacted) {
}
void close_config_menu_impl() {
recomp::save_config();
zelda64::save_config();
if (ultramodern::is_game_started()) {
recomp::set_current_menu(recomp::Menu::None);
recompui::set_current_menu(recompui::Menu::None);
}
else {
recomp::set_current_menu(recomp::Menu::Launcher);
recompui::set_current_menu(recompui::Menu::Launcher);
}
}
@ -241,8 +241,8 @@ void close_config_menu() {
graphics_model_handle.DirtyAllVariables();
close_config_menu_impl();
},
recomp::ButtonVariant::Success,
recomp::ButtonVariant::Error,
recompui::ButtonVariant::Success,
recompui::ButtonVariant::Error,
true,
"config__close-menu-button"
);
@ -252,7 +252,7 @@ void close_config_menu() {
close_config_menu_impl();
}
void recomp::open_quit_game_prompt() {
void zelda64::open_quit_game_prompt() {
prompt_context.open_prompt(
"Are you sure you want to quit?",
"Any progress since your last save will be lost.",
@ -262,8 +262,8 @@ void recomp::open_quit_game_prompt() {
ultramodern::quit();
},
[]() {},
recomp::ButtonVariant::Error,
recomp::ButtonVariant::Tertiary,
recompui::ButtonVariant::Error,
recompui::ButtonVariant::Tertiary,
true,
"config__quit-game-button"
);
@ -275,12 +275,12 @@ struct ControlOptionsContext {
int gyro_sensitivity; // 0 to 100
int mouse_sensitivity; // 0 to 100
int joystick_deadzone; // 0 to 100
recomp::TargetingMode targeting_mode;
zelda64::TargetingMode targeting_mode;
recomp::BackgroundInputMode background_input_mode;
recomp::AutosaveMode autosave_mode;
recomp::CameraInvertMode camera_invert_mode;
recomp::AnalogCamMode analog_cam_mode;
recomp::CameraInvertMode analog_camera_invert_mode;
zelda64::AutosaveMode autosave_mode;
zelda64::CameraInvertMode camera_invert_mode;
zelda64::AnalogCamMode analog_cam_mode;
zelda64::CameraInvertMode analog_camera_invert_mode;
};
ControlOptionsContext control_options_context;
@ -329,11 +329,11 @@ void recomp::set_joystick_deadzone(int deadzone) {
}
}
recomp::TargetingMode recomp::get_targeting_mode() {
zelda64::TargetingMode zelda64::get_targeting_mode() {
return control_options_context.targeting_mode;
}
void recomp::set_targeting_mode(recomp::TargetingMode mode) {
void zelda64::set_targeting_mode(zelda64::TargetingMode mode) {
control_options_context.targeting_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("targeting_mode");
@ -357,44 +357,44 @@ void recomp::set_background_input_mode(recomp::BackgroundInputMode mode) {
);
}
recomp::AutosaveMode recomp::get_autosave_mode() {
zelda64::AutosaveMode zelda64::get_autosave_mode() {
return control_options_context.autosave_mode;
}
void recomp::set_autosave_mode(recomp::AutosaveMode mode) {
void zelda64::set_autosave_mode(zelda64::AutosaveMode mode) {
control_options_context.autosave_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("autosave_mode");
}
}
recomp::CameraInvertMode recomp::get_camera_invert_mode() {
zelda64::CameraInvertMode zelda64::get_camera_invert_mode() {
return control_options_context.camera_invert_mode;
}
void recomp::set_camera_invert_mode(recomp::CameraInvertMode mode) {
void zelda64::set_camera_invert_mode(zelda64::CameraInvertMode mode) {
control_options_context.camera_invert_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("camera_invert_mode");
}
}
recomp::AnalogCamMode recomp::get_analog_cam_mode() {
zelda64::AnalogCamMode zelda64::get_analog_cam_mode() {
return control_options_context.analog_cam_mode;
}
void recomp::set_analog_cam_mode(recomp::AnalogCamMode mode) {
void zelda64::set_analog_cam_mode(zelda64::AnalogCamMode mode) {
control_options_context.analog_cam_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("analog_cam_mode");
}
}
recomp::CameraInvertMode recomp::get_analog_camera_invert_mode() {
zelda64::CameraInvertMode zelda64::get_analog_camera_invert_mode() {
return control_options_context.analog_camera_invert_mode;
}
void recomp::set_analog_camera_invert_mode(recomp::CameraInvertMode mode) {
void zelda64::set_analog_camera_invert_mode(zelda64::CameraInvertMode mode) {
control_options_context.analog_camera_invert_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("analog_camera_invert_mode");
@ -417,43 +417,43 @@ struct SoundOptionsContext {
SoundOptionsContext sound_options_context;
void recomp::reset_sound_settings() {
void zelda64::reset_sound_settings() {
sound_options_context.reset();
if (sound_options_model_handle) {
sound_options_model_handle.DirtyAllVariables();
}
}
void recomp::set_main_volume(int volume) {
void zelda64::set_main_volume(int volume) {
sound_options_context.main_volume.store(volume);
if (sound_options_model_handle) {
sound_options_model_handle.DirtyVariable("main_volume");
}
}
int recomp::get_main_volume() {
int zelda64::get_main_volume() {
return sound_options_context.main_volume.load();
}
void recomp::set_bgm_volume(int volume) {
void zelda64::set_bgm_volume(int volume) {
sound_options_context.bgm_volume.store(volume);
if (sound_options_model_handle) {
sound_options_model_handle.DirtyVariable("bgm_volume");
}
}
int recomp::get_bgm_volume() {
int zelda64::get_bgm_volume() {
return sound_options_context.bgm_volume.load();
}
void recomp::set_low_health_beeps_enabled(bool enabled) {
void zelda64::set_low_health_beeps_enabled(bool enabled) {
sound_options_context.low_health_beeps_enabled.store((int)enabled);
if (sound_options_model_handle) {
sound_options_model_handle.DirtyVariable("low_health_beeps_enabled");
}
}
bool recomp::get_low_health_beeps_enabled() {
bool zelda64::get_low_health_beeps_enabled() {
return (bool)sound_options_context.low_health_beeps_enabled.load();
}
@ -471,7 +471,7 @@ struct DebugContext {
bool debug_enabled = false;
DebugContext() {
for (const auto& area : recomp::game_warps) {
for (const auto& area : zelda64::game_warps) {
area_names.emplace_back(area.name);
}
update_warp_names();
@ -479,15 +479,15 @@ struct DebugContext {
void update_warp_names() {
scene_names.clear();
for (const auto& scene : recomp::game_warps[area_index].scenes) {
for (const auto& scene : zelda64::game_warps[area_index].scenes) {
scene_names.emplace_back(scene.name);
}
entrance_names = recomp::game_warps[area_index].scenes[scene_index].entrances;
entrance_names = zelda64::game_warps[area_index].scenes[scene_index].entrances;
}
};
void recomp::update_rml_display_refresh_rate() {
void recompui::update_rml_display_refresh_rate() {
static uint32_t lastRate = 0;
if (!graphics_model_handle) return;
@ -500,7 +500,7 @@ void recomp::update_rml_display_refresh_rate() {
DebugContext debug_context;
class ConfigMenu : public recomp::MenuController {
class ConfigMenu : public recompui::MenuController {
public:
ConfigMenu() {
@ -511,13 +511,13 @@ public:
Rml::ElementDocument* load_document(Rml::Context* context) override {
return context->LoadDocument("assets/config_menu.rml");
}
void register_events(recomp::UiEventListenerInstancer& listener) override {
recomp::register_event(listener, "apply_options",
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "apply_options",
[](const std::string& param, Rml::Event& event) {
graphics_model_handle.DirtyVariable("options_changed");
apply_graphics_config();
});
recomp::register_event(listener, "config_keydown",
recompui::register_event(listener, "config_keydown",
[](const std::string& param, Rml::Event& event) {
if (!prompt_context.open && event.GetId() == Rml::EventId::Keydown) {
auto key = event.GetParameter<Rml::Input::KeyIdentifier>("key_identifier", Rml::Input::KeyIdentifier::KI_UNKNOWN);
@ -533,23 +533,23 @@ public:
}
});
// This needs to be separate from `close_config_menu` so it ensures that the event is only on the target
recomp::register_event(listener, "close_config_menu_backdrop",
recompui::register_event(listener, "close_config_menu_backdrop",
[](const std::string& param, Rml::Event& event) {
if (event.GetPhase() == Rml::EventPhase::Target) {
close_config_menu();
}
});
recomp::register_event(listener, "close_config_menu",
recompui::register_event(listener, "close_config_menu",
[](const std::string& param, Rml::Event& event) {
close_config_menu();
});
recomp::register_event(listener, "open_quit_game_prompt",
recompui::register_event(listener, "open_quit_game_prompt",
[](const std::string& param, Rml::Event& event) {
recomp::open_quit_game_prompt();
zelda64::open_quit_game_prompt();
});
recomp::register_event(listener, "toggle_input_device",
recompui::register_event(listener, "toggle_input_device",
[](const std::string& param, Rml::Event& event) {
cur_device = cur_device == recomp::InputDevice::Controller
? recomp::InputDevice::Keyboard
@ -558,7 +558,7 @@ public:
controls_model_handle.DirtyVariable("inputs");
});
recomp::register_event(listener, "area_index_changed",
recompui::register_event(listener, "area_index_changed",
[](const std::string& param, Rml::Event& event) {
debug_context.area_index = event.GetParameter<int>("value", 0);
debug_context.scene_index = 0;
@ -570,7 +570,7 @@ public:
debug_context.model_handle.DirtyVariable("entrance_names");
});
recomp::register_event(listener, "scene_index_changed",
recompui::register_event(listener, "scene_index_changed",
[](const std::string& param, Rml::Event& event) {
debug_context.scene_index = event.GetParameter<int>("value", 0);
debug_context.entrance_index = 0;
@ -579,14 +579,14 @@ public:
debug_context.model_handle.DirtyVariable("entrance_names");
});
recomp::register_event(listener, "do_warp",
recompui::register_event(listener, "do_warp",
[](const std::string& param, Rml::Event& event) {
recomp::do_warp(debug_context.area_index, debug_context.scene_index, debug_context.entrance_index);
zelda64::do_warp(debug_context.area_index, debug_context.scene_index, debug_context.entrance_index);
});
recomp::register_event(listener, "set_time",
recompui::register_event(listener, "set_time",
[](const std::string& param, Rml::Event& event) {
recomp::set_time(debug_context.set_time_day, debug_context.set_time_hour, debug_context.set_time_minute);
zelda64::set_time(debug_context.set_time_day, debug_context.set_time_hour, debug_context.set_time_minute);
});
}
@ -722,7 +722,7 @@ public:
[](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
scanned_input_index = inputs.at(0).Get<size_t>();
scanned_binding_index = inputs.at(1).Get<size_t>();
recomp::start_scanning_input(cur_device);
recomp::start_scanning_input(cur_device);
model_handle.DirtyVariable("active_binding_input");
model_handle.DirtyVariable("active_binding_slot");
});
@ -730,18 +730,18 @@ public:
constructor.BindEventCallback("reset_input_bindings_to_defaults",
[](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
if (cur_device == recomp::InputDevice::Controller) {
recomp::reset_cont_input_bindings();
zelda64::reset_cont_input_bindings();
} else {
recomp::reset_kb_input_bindings();
zelda64::reset_kb_input_bindings();
}
model_handle.DirtyAllVariables();
});
constructor.BindEventCallback("clear_input_bindings",
[](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
recomp::GameInput input = static_cast<recomp::GameInput>(inputs.at(0).Get<size_t>());
recomp::GameInput input = static_cast<recomp::GameInput>(inputs.at(0).Get<size_t>());
for (size_t binding_index = 0; binding_index < recomp::bindings_per_input; binding_index++) {
recomp::set_input_binding(input, binding_index, cur_device, recomp::InputField{});
recomp::set_input_binding(input, binding_index, cur_device, recomp::InputField{});
}
model_handle.DirtyVariable("inputs");
});
@ -776,7 +776,7 @@ public:
virtual int Size(void* ptr) override { return recomp::bindings_per_input; }
virtual Rml::DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override {
recomp::GameInput input = static_cast<recomp::GameInput>((uintptr_t)ptr);
recomp::GameInput input = static_cast<recomp::GameInput>((uintptr_t)ptr);
return Rml::DataVariable{&input_field_definition_instance, &recomp::get_input_binding(input, address.index, cur_device)};
}
};
@ -812,7 +812,7 @@ public:
return Rml::DataVariable(&binding_array_var_instance, nullptr);
}
else {
recomp::GameInput input = recomp::get_input_from_enum_name(address.name);
recomp::GameInput input = recomp::get_input_from_enum_name(address.name);
if (input != recomp::GameInput::COUNT) {
return Rml::DataVariable(&binding_container_var_instance, (void*)(uintptr_t)input);
}
@ -966,14 +966,14 @@ public:
constructor.Bind("prompt__confirmLabel", &prompt_context.confirmLabel);
constructor.Bind("prompt__cancelLabel", &prompt_context.cancelLabel);
constructor.BindEventCallback("prompt__on_click", &recomp::PromptContext::on_click, &prompt_context);
constructor.BindEventCallback("prompt__on_click", &recompui::PromptContext::on_click, &prompt_context);
prompt_context.model_handle = constructor.GetModelHandle();
}
void make_bindings(Rml::Context* context) override {
// initially set cont state for ui help
recomp::config_menu_set_cont_or_kb(recomp::get_cont_active());
recomp::config_menu_set_cont_or_kb(recompui::get_cont_active());
make_nav_help_bindings(context);
make_general_bindings(context);
make_controls_bindings(context);
@ -984,22 +984,22 @@ public:
}
};
std::unique_ptr<recomp::MenuController> recomp::create_config_menu() {
std::unique_ptr<recompui::MenuController> recompui::create_config_menu() {
return std::make_unique<ConfigMenu>();
}
bool recomp::get_debug_mode_enabled() {
bool zelda64::get_debug_mode_enabled() {
return debug_context.debug_enabled;
}
void recomp::set_debug_mode_enabled(bool enabled) {
void zelda64::set_debug_mode_enabled(bool enabled) {
debug_context.debug_enabled = enabled;
if (debug_context.model_handle) {
debug_context.model_handle.DirtyVariable("debug_enabled");
}
}
void recomp::update_supported_options() {
void recompui::update_supported_options() {
msaa2x_supported = ultramodern::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA2X;
msaa4x_supported = ultramodern::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA4X;
msaa8x_supported = ultramodern::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA8X;
@ -1010,7 +1010,7 @@ void recomp::update_supported_options() {
graphics_model_handle.DirtyAllVariables();
}
void recomp::toggle_fullscreen() {
void recompui::toggle_fullscreen() {
new_options.wm_option = (new_options.wm_option == ultramodern::WindowMode::Windowed) ? ultramodern::WindowMode::Fullscreen : ultramodern::WindowMode::Windowed;
apply_graphics_config();
graphics_model_handle.DirtyVariable("wm_option");

View File

@ -1,7 +1,7 @@
#include "recomp_ui.h"
#include "recomp_config.h"
#include "recomp_game.h"
#include "../../ultramodern/ultramodern.hpp"
#include "zelda_config.h"
#include "librecomp/game.hpp"
#include "ultramodern/ultramodern.hpp"
#include "RmlUi/Core.h"
#include "nfd.h"
#include <filesystem>
@ -11,6 +11,8 @@ std::string version_number = "v1.1.1";
Rml::DataModelHandle model_handle;
bool mm_rom_valid = false;
extern std::vector<recomp::GameEntry> supported_games;
void select_rom() {
nfdnchar_t* native_path = nullptr;
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
@ -21,39 +23,39 @@ void select_rom() {
NFD_FreePathN(native_path);
native_path = nullptr;
recomp::RomValidationError rom_error = recomp::select_rom(path, recomp::Game::MM);
switch (rom_error) {
case recomp::RomValidationError::Good:
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recomp::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recomp::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recomp::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recomp::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recomp::message_box("This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recomp::message_box("An unknown error has occurred.");
break;
}
}
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
}
}
}
class LauncherMenu : public recomp::MenuController {
class LauncherMenu : public recompui::MenuController {
public:
LauncherMenu() {
mm_rom_valid = recomp::is_rom_valid(recomp::Game::MM);
mm_rom_valid = recomp::is_rom_valid(supported_games[0].game_id);
}
~LauncherMenu() override {
@ -61,37 +63,37 @@ public:
Rml::ElementDocument* load_document(Rml::Context* context) override {
return context->LoadDocument("assets/launcher.rml");
}
void register_events(recomp::UiEventListenerInstancer& listener) override {
recomp::register_event(listener, "select_rom",
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "select_rom",
[](const std::string& param, Rml::Event& event) {
select_rom();
}
);
recomp::register_event(listener, "rom_selected",
recompui::register_event(listener, "rom_selected",
[](const std::string& param, Rml::Event& event) {
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
}
);
recomp::register_event(listener, "start_game",
recompui::register_event(listener, "start_game",
[](const std::string& param, Rml::Event& event) {
recomp::start_game(recomp::Game::MM);
recomp::set_current_menu(recomp::Menu::None);
recomp::start_game(supported_games[0].game_id);
recompui::set_current_menu(recompui::Menu::None);
}
);
recomp::register_event(listener, "open_controls",
recompui::register_event(listener, "open_controls",
[](const std::string& param, Rml::Event& event) {
recomp::set_current_menu(recomp::Menu::Config);
recomp::set_config_submenu(recomp::ConfigSubmenu::Controls);
recompui::set_current_menu(recompui::Menu::Config);
recompui::set_config_submenu(recompui::ConfigSubmenu::Controls);
}
);
recomp::register_event(listener, "open_settings",
recompui::register_event(listener, "open_settings",
[](const std::string& param, Rml::Event& event) {
recomp::set_current_menu(recomp::Menu::Config);
recomp::set_config_submenu(recomp::ConfigSubmenu::General);
recompui::set_current_menu(recompui::Menu::Config);
recompui::set_config_submenu(recompui::ConfigSubmenu::General);
}
);
recomp::register_event(listener, "exit_game",
recompui::register_event(listener, "exit_game",
[](const std::string& param, Rml::Event& event) {
ultramodern::quit();
}
@ -107,6 +109,6 @@ public:
}
};
std::unique_ptr<recomp::MenuController> recomp::create_launcher_menu() {
std::unique_ptr<recompui::MenuController> recompui::create_launcher_menu() {
return std::make_unique<LauncherMenu>();
}

View File

@ -4,16 +4,22 @@
#include <fstream>
#include <filesystem>
#ifdef _WIN32
#include <SDL_video.h>
#else
#include <SDL2/SDL_video.h>
#endif
#include "recomp_ui.h"
#include "recomp_input.h"
#include "recomp_game.h"
#include "recomp_config.h"
#include "librecomp/game.hpp"
#include "zelda_config.h"
#include "ui_rml_hacks.hpp"
#include "concurrentqueue.h"
#include "rt64_layer.h"
#include "ultramodern/rt64_layer.hpp"
#include "rt64_render_hooks.h"
#include "rt64_render_interface_builders.h"
@ -718,7 +724,7 @@ Rml::Element* get_target(Rml::ElementDocument* document, Rml::Element* element)
return element;
}
namespace recomp {
namespace recompui {
class UiEventListener : public Rml::EventListener {
event_handler_t* handler_;
Rml::String param_;
@ -759,7 +765,7 @@ namespace recomp {
};
}
void recomp::register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler) {
void recompui::register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler) {
listener.register_event(name, handler);
}
@ -779,8 +785,8 @@ Rml::Element* find_autofocus_element(Rml::Element* start) {
struct UIContext {
struct UIRenderContext render;
class {
std::unordered_map<recomp::Menu, std::unique_ptr<recomp::MenuController>> menus;
std::unordered_map<recomp::Menu, Rml::ElementDocument*> documents;
std::unordered_map<recompui::Menu, std::unique_ptr<recompui::MenuController>> menus;
std::unordered_map<recompui::Menu, Rml::ElementDocument*> documents;
Rml::ElementDocument* current_document;
Rml::Element* prev_focused;
bool mouse_is_active_changed = false;
@ -794,13 +800,13 @@ struct UIContext {
std::unique_ptr<SystemInterface_SDL> system_interface;
std::unique_ptr<RmlRenderInterface_RT64> render_interface;
Rml::Context* context;
recomp::UiEventListenerInstancer event_listener_instancer;
recompui::UiEventListenerInstancer event_listener_instancer;
void unload() {
render_interface.reset();
}
void swap_document(recomp::Menu menu) {
void swap_document(recompui::Menu menu) {
if (current_document != nullptr) {
Rml::Element* window_el = current_document->GetElementById("window");
if (window_el != nullptr) {
@ -831,7 +837,7 @@ struct UIContext {
mouse_is_active_initialized = false;
}
void swap_config_menu(recomp::ConfigSubmenu submenu) {
void swap_config_menu(recompui::ConfigSubmenu submenu) {
if (current_document != nullptr) {
Rml::Element* config_tabset_base = current_document->GetElementById("config_tabset");
if (config_tabset_base != nullptr) {
@ -911,7 +917,7 @@ struct UIContext {
}
if (mouse_is_active_initialized) {
recomp::set_cursor_visible(mouse_is_active);
recompui::set_cursor_visible(mouse_is_active);
}
if (current_document == nullptr) {
@ -939,7 +945,6 @@ struct UIContext {
if (cont_is_active || non_mouse_interacted) {
if (non_mouse_interacted) {
auto focusedEl = current_document->GetFocusLeafNode();
Rml::Variant* ti = focusedEl == nullptr ? nullptr : focusedEl->GetAttribute("tab-index");
if (focusedEl == nullptr || RecompRml::CanFocusElement(focusedEl) != RecompRml::CanFocus::Yes) {
Rml::Element* element = find_autofocus_element(current_document);
if (element != nullptr) {
@ -981,14 +986,14 @@ struct UIContext {
}
}
void add_menu(recomp::Menu menu, std::unique_ptr<recomp::MenuController>&& controller) {
void add_menu(recompui::Menu menu, std::unique_ptr<recompui::MenuController>&& controller) {
menus.emplace(menu, std::move(controller));
}
void update_config_menu_loop(bool menu_changed) {
static int prevTab = -1;
if (menu_changed) prevTab = -1;
recomp::update_rml_display_refresh_rate();
recompui::update_rml_display_refresh_rate();
Rml::ElementTabSet *tabset = (Rml::ElementTabSet *)current_document->GetElementById("config_tabset");
if (tabset == nullptr) return;
@ -1022,7 +1027,7 @@ struct UIContext {
void update_prompt_loop(void) {
static bool wasShowingPrompt = false;
recomp::PromptContext *ctx = recomp::get_prompt_context();
recompui::PromptContext *ctx = recompui::get_prompt_context();
if (!ctx->open && wasShowingPrompt) {
Rml::Element* focused = current_document->GetFocusLeafNode();
if (focused) focused->Blur();
@ -1088,8 +1093,8 @@ struct UIContext {
Rml::Element *confirmButton = current_document->GetElementById("prompt__confirm-button");
Rml::Element *cancelButton = current_document->GetElementById("prompt__cancel-button");
if (confirmButton != nullptr) confirmButton->SetClassNames("button button--" + recomp::button_variants.at(ctx->confirmVariant));
if (cancelButton != nullptr) cancelButton->SetClassNames( "button button--" + recomp::button_variants.at(ctx->cancelVariant));
if (confirmButton != nullptr) confirmButton->SetClassNames("button button--" + recompui::button_variants.at(ctx->confirmVariant));
if (cancelButton != nullptr) cancelButton->SetClassNames( "button button--" + recompui::button_variants.at(ctx->cancelVariant));
}
} rml;
};
@ -1100,7 +1105,7 @@ std::mutex ui_context_mutex{};
// TODO make this not be global
extern SDL_Window* window;
void recomp::get_window_size(int& width, int& height) {
void recompui::get_window_size(int& width, int& height) {
SDL_GetWindowSizeInPixels(window, &width, &height);
}
@ -1110,8 +1115,8 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
#endif
ui_context = std::make_unique<UIContext>();
ui_context->rml.add_menu(recomp::Menu::Config, recomp::create_config_menu());
ui_context->rml.add_menu(recomp::Menu::Launcher, recomp::create_launcher_menu());
ui_context->rml.add_menu(recompui::Menu::Config, recompui::create_config_menu());
ui_context->rml.add_menu(recompui::Menu::Launcher, recompui::create_launcher_menu());
ui_context->render.interface = interface;
ui_context->render.device = device;
@ -1129,7 +1134,7 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
Rml::Initialise();
// Apply the hack to replace RmlUi's default color parser with one that conforms to HTML5 alpha parsing for SASS compatibility
recomp::apply_color_hack();
recompui::apply_color_hack();
int width, height;
SDL_GetWindowSizeInPixels(window, &width, &height);
@ -1167,16 +1172,16 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
moodycamel::ConcurrentQueue<SDL_Event> ui_event_queue{};
void recomp::queue_event(const SDL_Event& event) {
void recompui::queue_event(const SDL_Event& event) {
ui_event_queue.enqueue(event);
}
bool recomp::try_deque_event(SDL_Event& out) {
bool recompui::try_deque_event(SDL_Event& out) {
return ui_event_queue.try_dequeue(out);
}
std::atomic<recomp::Menu> open_menu = recomp::Menu::Launcher;
std::atomic<recomp::ConfigSubmenu> open_config_submenu = recomp::ConfigSubmenu::Count;
std::atomic<recompui::Menu> open_menu = recompui::Menu::Launcher;
std::atomic<recompui::ConfigSubmenu> open_config_submenu = recompui::ConfigSubmenu::Count;
int cont_button_to_key(SDL_ControllerButtonEvent& button) {
switch (button.button) {
@ -1236,15 +1241,15 @@ void apply_background_input_mode() {
last_input_mode = cur_input_mode;
}
bool recomp::get_cont_active() {
bool recompui::get_cont_active() {
return ui_context->rml.cont_is_active;
}
void recomp::set_cont_active(bool active) {
void recompui::set_cont_active(bool active) {
ui_context->rml.cont_is_active = active;
}
void recomp::activate_mouse() {
void recompui::activate_mouse() {
ui_context->rml.update_primary_input(true, false);
ui_context->rml.update_focus(true, false);
}
@ -1267,12 +1272,12 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
bool reload_sheets = is_reload_held && !was_reload_held;
was_reload_held = is_reload_held;
static recomp::Menu prev_menu = recomp::Menu::None;
recomp::Menu cur_menu = open_menu.load();
static recompui::Menu prev_menu = recompui::Menu::None;
recompui::Menu cur_menu = open_menu.load();
if (reload_sheets) {
ui_context->rml.load_documents();
prev_menu = recomp::Menu::None;
prev_menu = recompui::Menu::None;
}
bool menu_changed = cur_menu != prev_menu;
@ -1280,10 +1285,10 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
ui_context->rml.swap_document(cur_menu);
}
recomp::ConfigSubmenu config_submenu = open_config_submenu.load();
if (config_submenu != recomp::ConfigSubmenu::Count) {
recompui::ConfigSubmenu config_submenu = open_config_submenu.load();
if (config_submenu != recompui::ConfigSubmenu::Count) {
ui_context->rml.swap_config_menu(config_submenu);
open_config_submenu.store(recomp::ConfigSubmenu::Count);
open_config_submenu.store(recompui::ConfigSubmenu::Count);
}
prev_menu = cur_menu;
@ -1296,15 +1301,15 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
bool cont_interacted = false;
bool kb_interacted = false;
if (cur_menu == recomp::Menu::Config) {
if (cur_menu == recompui::Menu::Config) {
ui_context->rml.update_config_menu_loop(menu_changed);
}
if (cur_menu != recomp::Menu::None) {
if (cur_menu != recompui::Menu::None) {
ui_context->rml.update_prompt_loop();
}
while (recomp::try_deque_event(cur_event)) {
bool menu_is_open = cur_menu != recomp::Menu::None;
while (recompui::try_deque_event(cur_event)) {
bool menu_is_open = cur_menu != recompui::Menu::None;
if (!recomp::all_input_disabled()) {
// Implement some additional behavior for specific events on top of what RmlUi normally does with them.
@ -1323,7 +1328,7 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
last_mouse_pos[1] = cur_event.motion.y;
// if controller is the primary input, don't use mouse movement to allow cursor to reactivate
if (recomp::get_cont_active()) {
if (recompui::get_cont_active()) {
break;
}
}
@ -1401,15 +1406,15 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
}
if (open_config) {
cur_menu = recomp::Menu::Config;
open_menu.store(recomp::Menu::Config);
cur_menu = recompui::Menu::Config;
open_menu.store(recompui::Menu::Config);
ui_context->rml.swap_document(cur_menu);
}
}
} // end dequeue event loop
if (cont_interacted || kb_interacted || mouse_clicked) {
recomp::set_cont_active(cont_interacted);
recompui::set_cont_active(cont_interacted);
}
recomp::config_menu_set_cont_or_kb(ui_context->rml.cont_is_active);
@ -1421,7 +1426,7 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
ui_context->rml.update_primary_input(mouse_moved, non_mouse_interacted);
ui_context->rml.update_focus(mouse_moved, non_mouse_interacted);
if (cur_menu != recomp::Menu::None) {
if (cur_menu != recompui::Menu::None) {
int width = swap_chain_framebuffer->getWidth();
int height = swap_chain_framebuffer->getHeight();
@ -1457,25 +1462,25 @@ void set_rt64_hooks() {
RT64::SetRenderHooks(init_hook, draw_hook, deinit_hook);
}
void recomp::set_current_menu(Menu menu) {
void recompui::set_current_menu(Menu menu) {
open_menu.store(menu);
if (menu == recomp::Menu::None) {
if (menu == recompui::Menu::None) {
ui_context->rml.system_interface->SetMouseCursor("arrow");
}
}
void recomp::set_config_submenu(recomp::ConfigSubmenu submenu) {
void recompui::set_config_submenu(recompui::ConfigSubmenu submenu) {
open_config_submenu.store(submenu);
}
void recomp::destroy_ui() {
void recompui::destroy_ui() {
}
recomp::Menu recomp::get_current_menu() {
recompui::Menu recompui::get_current_menu() {
return open_menu.load();
}
void recomp::message_box(const char* msg) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, recomp::program_name.data(), msg, nullptr);
void recompui::message_box(const char* msg) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, zelda64::program_name.data(), msg, nullptr);
printf("[ERROR] %s\n", msg);
}

View File

@ -1,67 +0,0 @@
#include "ultra64.h"
#include "ultramodern.hpp"
#include <cassert>
static uint32_t sample_rate = 48000;
static ultramodern::audio_callbacks_t audio_callbacks;
void set_audio_callbacks(const ultramodern::audio_callbacks_t& callbacks) {
audio_callbacks = callbacks;
}
void ultramodern::init_audio() {
// Pick an initial dummy sample rate; this will be set by the game later to the true sample rate.
set_audio_frequency(48000);
}
void ultramodern::set_audio_frequency(uint32_t freq) {
if (audio_callbacks.set_frequency) {
audio_callbacks.set_frequency(freq);
}
sample_rate = freq;
}
void ultramodern::queue_audio_buffer(RDRAM_ARG PTR(int16_t) audio_data_, uint32_t byte_count) {
// Ensure that the byte count is an integer multiple of samples.
assert((byte_count & 1) == 0);
// Calculate the number of samples from the number of bytes.
uint32_t sample_count = byte_count / sizeof(int16_t);
// Queue the swapped audio data.
if (audio_callbacks.queue_samples) {
audio_callbacks.queue_samples(TO_PTR(int16_t, audio_data_), sample_count);
}
}
// For SDL2
//uint32_t buffer_offset_frames = 1;
// For Godot
float buffer_offset_frames = 0.5f;
// If there's ever any audio popping, check here first. Some games are very sensitive to
// the remaining sample count and reporting a number that's too high here can lead to issues.
// Reporting a number that's too low can lead to audio lag in some games.
uint32_t ultramodern::get_remaining_audio_bytes() {
// Get the number of remaining buffered audio bytes.
uint32_t buffered_byte_count;
if (audio_callbacks.get_frames_remaining != nullptr) {
buffered_byte_count = audio_callbacks.get_frames_remaining() * 2 * sizeof(int16_t);
}
else {
buffered_byte_count = 100;
}
// Adjust the reported count to be some number of refreshes in the future, which helps ensure that
// there are enough samples even if the audio thread experiences a small amount of lag. This prevents
// audio popping on games that use the buffered audio byte count to determine how many samples
// to generate.
uint32_t samples_per_vi = (sample_rate / 60);
if (buffered_byte_count > static_cast<uint32_t>(buffer_offset_frames * sizeof(int16_t) * samples_per_vi)) {
buffered_byte_count -= static_cast<uint32_t>(buffer_offset_frames * sizeof(int16_t) * samples_per_vi);
}
else {
buffered_byte_count = 0;
}
return buffered_byte_count;
}

View File

@ -1,86 +0,0 @@
#ifndef __CONFIG_HPP__
#define __CONFIG_HPP__
#include "common/rt64_user_configuration.h"
namespace ultramodern {
enum class Resolution {
Original,
Original2x,
Auto,
OptionCount
};
enum class WindowMode {
Windowed,
Fullscreen,
OptionCount
};
enum class HUDRatioMode {
Original,
Clamp16x9,
Full,
OptionCount
};
enum class GraphicsApi {
Auto,
D3D12,
Vulkan,
OptionCount
};
enum class HighPrecisionFramebuffer {
Auto,
On,
Off,
OptionCount
};
struct GraphicsConfig {
Resolution res_option;
WindowMode wm_option;
HUDRatioMode hr_option;
GraphicsApi api_option;
RT64::UserConfiguration::AspectRatio ar_option;
RT64::UserConfiguration::Antialiasing msaa_option;
RT64::UserConfiguration::RefreshRate rr_option;
HighPrecisionFramebuffer hpfb_option;
int rr_manual_value;
int ds_option;
bool developer_mode;
auto operator<=>(const GraphicsConfig& rhs) const = default;
};
void set_graphics_config(const GraphicsConfig& config);
GraphicsConfig get_graphics_config();
NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::Resolution, {
{ultramodern::Resolution::Original, "Original"},
{ultramodern::Resolution::Original2x, "Original2x"},
{ultramodern::Resolution::Auto, "Auto"},
});
NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::WindowMode, {
{ultramodern::WindowMode::Windowed, "Windowed"},
{ultramodern::WindowMode::Fullscreen, "Fullscreen"}
});
NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::HUDRatioMode, {
{ultramodern::HUDRatioMode::Original, "Original"},
{ultramodern::HUDRatioMode::Clamp16x9, "Clamp16x9"},
{ultramodern::HUDRatioMode::Full, "Full"},
});
NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::GraphicsApi, {
{ultramodern::GraphicsApi::Auto, "Auto"},
{ultramodern::GraphicsApi::D3D12, "D3D12"},
{ultramodern::GraphicsApi::Vulkan, "Vulkan"},
});
NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::HighPrecisionFramebuffer, {
{ultramodern::HighPrecisionFramebuffer::Auto, "Auto"},
{ultramodern::HighPrecisionFramebuffer::On, "On"},
{ultramodern::HighPrecisionFramebuffer::Off, "Off"},
});
};
#endif

View File

@ -1,620 +0,0 @@
#include <thread>
#include <atomic>
#include <chrono>
#include <cinttypes>
#include <variant>
#include <unordered_map>
#include <utility>
#include <mutex>
#include <queue>
#include <cstring>
#include "blockingconcurrentqueue.h"
#include "ultra64.h"
#include "ultramodern.hpp"
#include "config.hpp"
#include "rt64_layer.h"
#include "recomp.h"
#include "recomp_game.h"
#include "recomp_ui.h"
#include "recomp_input.h"
#include "rsp.h"
struct SpTaskAction {
OSTask task;
};
struct SwapBuffersAction {
uint32_t origin;
};
struct UpdateConfigAction {
};
struct LoadShaderCacheAction {
std::span<const char> data;
};
using Action = std::variant<SpTaskAction, SwapBuffersAction, UpdateConfigAction, LoadShaderCacheAction>;
static struct {
struct {
std::thread thread;
PTR(OSMesgQueue) mq = NULLPTR;
PTR(void) current_buffer = NULLPTR;
PTR(void) next_buffer = NULLPTR;
OSMesg msg = (OSMesg)0;
int retrace_count = 1;
} vi;
struct {
std::thread gfx_thread;
std::thread task_thread;
PTR(OSMesgQueue) mq = NULLPTR;
OSMesg msg = (OSMesg)0;
} sp;
struct {
PTR(OSMesgQueue) mq = NULLPTR;
OSMesg msg = (OSMesg)0;
} dp;
struct {
PTR(OSMesgQueue) mq = NULLPTR;
OSMesg msg = (OSMesg)0;
} ai;
struct {
PTR(OSMesgQueue) mq = NULLPTR;
OSMesg msg = (OSMesg)0;
} si;
// The same message queue may be used for multiple events, so share a mutex for all of them
std::mutex message_mutex;
uint8_t* rdram;
moodycamel::BlockingConcurrentQueue<Action> action_queue{};
moodycamel::BlockingConcurrentQueue<OSTask*> sp_task_queue{};
moodycamel::ConcurrentQueue<OSThread*> deleted_threads{};
} events_context{};
extern "C" void osSetEventMesg(RDRAM_ARG OSEvent event_id, PTR(OSMesgQueue) mq_, OSMesg msg) {
OSMesgQueue* mq = TO_PTR(OSMesgQueue, mq_);
std::lock_guard lock{ events_context.message_mutex };
switch (event_id) {
case OS_EVENT_SP:
events_context.sp.msg = msg;
events_context.sp.mq = mq_;
break;
case OS_EVENT_DP:
events_context.dp.msg = msg;
events_context.dp.mq = mq_;
break;
case OS_EVENT_AI:
events_context.ai.msg = msg;
events_context.ai.mq = mq_;
break;
case OS_EVENT_SI:
events_context.si.msg = msg;
events_context.si.mq = mq_;
}
}
extern "C" void osViSetEvent(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, u32 retrace_count) {
std::lock_guard lock{ events_context.message_mutex };
events_context.vi.mq = mq_;
events_context.vi.msg = msg;
events_context.vi.retrace_count = retrace_count;
}
uint64_t total_vis = 0;
extern std::atomic_bool exited;
void set_dummy_vi();
void vi_thread_func() {
ultramodern::set_native_thread_name("VI Thread");
// This thread should be prioritized over every other thread in the application, as it's what allows
// the game to generate new audio and gfx lists.
ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::Critical);
using namespace std::chrono_literals;
int remaining_retraces = events_context.vi.retrace_count;
while (!exited) {
// Determine the next VI time (more accurate than adding 16ms each VI interrupt)
auto next = ultramodern::get_start() + (total_vis * 1000000us) / (60 * ultramodern::get_speed_multiplier());
//if (next > std::chrono::high_resolution_clock::now()) {
// printf("Sleeping for %" PRIu64 " us to get from %" PRIu64 " us to %" PRIu64 " us \n",
// (next - std::chrono::high_resolution_clock::now()) / 1us,
// (std::chrono::high_resolution_clock::now() - events_context.start) / 1us,
// (next - events_context.start) / 1us);
//} else {
// printf("No need to sleep\n");
//}
// Detect if there's more than a second to wait and wait a fixed amount instead for the next VI if so, as that usually means the system clock went back in time.
if (std::chrono::floor<std::chrono::seconds>(next - std::chrono::high_resolution_clock::now()) > 1s) {
// printf("Skipping the next VI wait\n");
next = std::chrono::high_resolution_clock::now();
}
ultramodern::sleep_until(next);
// Calculate how many VIs have passed
uint64_t new_total_vis = (ultramodern::time_since_start() * (60 * ultramodern::get_speed_multiplier()) / 1000ms) + 1;
if (new_total_vis > total_vis + 1) {
//printf("Skipped % " PRId64 " frames in VI interupt thread!\n", new_total_vis - total_vis - 1);
}
total_vis = new_total_vis;
remaining_retraces--;
{
std::lock_guard lock{ events_context.message_mutex };
uint8_t* rdram = events_context.rdram;
if (remaining_retraces == 0) {
remaining_retraces = events_context.vi.retrace_count;
if (ultramodern::is_game_started()) {
if (events_context.vi.mq != NULLPTR) {
if (osSendMesg(PASS_RDRAM events_context.vi.mq, events_context.vi.msg, OS_MESG_NOBLOCK) == -1) {
//printf("Game skipped a VI frame!\n");
}
}
}
else {
set_dummy_vi();
static bool swap = false;
uint32_t vi_origin = 0x400 + 0x280; // Skip initial RDRAM contents and add the usual origin offset
// Offset by one FB every other frame so RT64 continues drawing
if (swap) {
vi_origin += 0x25800;
}
osViSwapBuffer(rdram, vi_origin);
swap = !swap;
}
}
if (events_context.ai.mq != NULLPTR) {
if (osSendMesg(PASS_RDRAM events_context.ai.mq, events_context.ai.msg, OS_MESG_NOBLOCK) == -1) {
//printf("Game skipped a AI frame!\n");
}
}
}
// TODO move recomp code out of ultramodern.
recomp::update_rumble();
}
}
void sp_complete() {
uint8_t* rdram = events_context.rdram;
std::lock_guard lock{ events_context.message_mutex };
osSendMesg(PASS_RDRAM events_context.sp.mq, events_context.sp.msg, OS_MESG_NOBLOCK);
}
void dp_complete() {
uint8_t* rdram = events_context.rdram;
std::lock_guard lock{ events_context.message_mutex };
osSendMesg(PASS_RDRAM events_context.dp.mq, events_context.dp.msg, OS_MESG_NOBLOCK);
}
uint8_t dmem[0x1000];
uint16_t rspReciprocals[512];
uint16_t rspInverseSquareRoots[512];
using RspUcodeFunc = RspExitReason(uint8_t* rdram);
extern RspUcodeFunc njpgdspMain;
extern RspUcodeFunc aspMain;
// From Ares emulator. For license details, see rsp_vu.h
void rsp_constants_init() {
rspReciprocals[0] = u16(~0);
for (u16 index = 1; index < 512; index++) {
u64 a = index + 512;
u64 b = (u64(1) << 34) / a;
rspReciprocals[index] = u16((b + 1) >> 8);
}
for (u16 index = 0; index < 512; index++) {
u64 a = (index + 512) >> ((index % 2 == 1) ? 1 : 0);
u64 b = 1 << 17;
//find the largest b where b < 1.0 / sqrt(a)
while (a * (b + 1) * (b + 1) < (u64(1) << 44)) b++;
rspInverseSquareRoots[index] = u16(b >> 1);
}
}
// Runs a recompiled RSP microcode
void run_rsp_microcode(uint8_t* rdram, const OSTask* task, RspUcodeFunc* ucode_func) {
// Load the OSTask into DMEM
memcpy(&dmem[0xFC0], task, sizeof(OSTask));
// Load the ucode data into DMEM
dma_rdram_to_dmem(rdram, 0x0000, task->t.ucode_data, 0xF80 - 1);
// Run the ucode
RspExitReason exit_reason = ucode_func(rdram);
// Ensure that the ucode exited correctly
assert(exit_reason == RspExitReason::Broke);
}
void task_thread_func(uint8_t* rdram, moodycamel::LightweightSemaphore* thread_ready) {
ultramodern::set_native_thread_name("SP Task Thread");
ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::Normal);
// Notify the caller thread that this thread is ready.
thread_ready->signal();
while (true) {
// Wait until an RSP task has been sent
OSTask* task;
events_context.sp_task_queue.wait_dequeue(task);
if (task == nullptr) {
return;
}
// Run the correct function based on the task type
if (task->t.type == M_AUDTASK) {
run_rsp_microcode(rdram, task, aspMain);
}
else if (task->t.type == M_NJPEGTASK) {
run_rsp_microcode(rdram, task, njpgdspMain);
}
else {
fprintf(stderr, "Unknown task type: %" PRIu32 "\n", task->t.type);
assert(false);
std::quick_exit(EXIT_FAILURE);
}
// Tell the game that the RSP has completed
sp_complete();
}
}
static std::atomic<ultramodern::GraphicsConfig> cur_config{};
void ultramodern::set_graphics_config(const ultramodern::GraphicsConfig& config) {
cur_config = config;
events_context.action_queue.enqueue(UpdateConfigAction{});
}
ultramodern::GraphicsConfig ultramodern::get_graphics_config() {
return cur_config;
}
std::atomic_uint32_t display_refresh_rate = 60;
std::atomic<float> resolution_scale = 1.0f;
uint32_t ultramodern::get_target_framerate(uint32_t original) {
ultramodern::GraphicsConfig graphics_config = ultramodern::get_graphics_config();
switch (graphics_config.rr_option) {
case RT64::UserConfiguration::RefreshRate::Original:
default:
return original;
case RT64::UserConfiguration::RefreshRate::Manual:
return graphics_config.rr_manual_value;
case RT64::UserConfiguration::RefreshRate::Display:
return display_refresh_rate.load();
}
}
uint32_t ultramodern::get_display_refresh_rate() {
return display_refresh_rate.load();
}
float ultramodern::get_resolution_scale() {
return resolution_scale.load();
}
void ultramodern::load_shader_cache(std::span<const char> cache_data) {
events_context.action_queue.enqueue(LoadShaderCacheAction{cache_data});
}
std::atomic<ultramodern::RT64SetupResult> rt64_setup_result = ultramodern::RT64SetupResult::Success;
void gfx_thread_func(uint8_t* rdram, moodycamel::LightweightSemaphore* thread_ready, ultramodern::WindowHandle window_handle) {
bool enabled_instant_present = false;
using namespace std::chrono_literals;
ultramodern::set_native_thread_name("Gfx Thread");
ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::Normal);
ultramodern::GraphicsConfig old_config = ultramodern::get_graphics_config();
ultramodern::RT64Context rt64{rdram, window_handle, cur_config.load().developer_mode};
if (!rt64.valid()) {
// TODO move recomp code out of ultramodern.
rt64_setup_result.store(rt64.get_setup_result());
// Notify the caller thread that this thread is ready.
thread_ready->signal();
return;
}
// TODO move recomp code out of ultramodern.
recomp::update_supported_options();
rsp_constants_init();
// Notify the caller thread that this thread is ready.
thread_ready->signal();
while (!exited) {
// Try to pull an action from the queue
Action action;
if (events_context.action_queue.wait_dequeue_timed(action, 1ms)) {
// Determine the action type and act on it
if (const auto* task_action = std::get_if<SpTaskAction>(&action)) {
// Turn on instant present if the game has been started and it hasn't been turned on yet.
if (ultramodern::is_game_started() && !enabled_instant_present) {
rt64.enable_instant_present();
enabled_instant_present = true;
}
// Tell the game that the RSP completed instantly. This will allow it to queue other task types, but it won't
// start another graphics task until the RDP is also complete. Games usually preserve the RSP inputs until the RDP
// is finished as well, so sending this early shouldn't be an issue in most cases.
// If this causes issues then the logic can be replaced with responding to yield requests.
sp_complete();
ultramodern::measure_input_latency();
auto rt64_start = std::chrono::high_resolution_clock::now();
rt64.send_dl(&task_action->task);
auto rt64_end = std::chrono::high_resolution_clock::now();
dp_complete();
// printf("RT64 ProcessDList time: %d us\n", static_cast<u32>(std::chrono::duration_cast<std::chrono::microseconds>(rt64_end - rt64_start).count()));
}
else if (const auto* swap_action = std::get_if<SwapBuffersAction>(&action)) {
events_context.vi.current_buffer = events_context.vi.next_buffer;
rt64.update_screen(swap_action->origin);
display_refresh_rate = rt64.get_display_framerate();
resolution_scale = rt64.get_resolution_scale();
}
else if (const auto* config_action = std::get_if<UpdateConfigAction>(&action)) {
ultramodern::GraphicsConfig new_config = cur_config;
if (old_config != new_config) {
rt64.update_config(old_config, new_config);
old_config = new_config;
}
}
else if (const auto* load_shader_cache_action = std::get_if<LoadShaderCacheAction>(&action)) {
rt64.load_shader_cache(load_shader_cache_action->data);
}
}
}
// TODO move recomp code out of ultramodern.
recomp::destroy_ui();
rt64.shutdown();
}
extern unsigned int VI_STATUS_REG;
extern unsigned int VI_ORIGIN_REG;
extern unsigned int VI_WIDTH_REG;
extern unsigned int VI_INTR_REG;
extern unsigned int VI_V_CURRENT_LINE_REG;
extern unsigned int VI_TIMING_REG;
extern unsigned int VI_V_SYNC_REG;
extern unsigned int VI_H_SYNC_REG;
extern unsigned int VI_LEAP_REG;
extern unsigned int VI_H_START_REG;
extern unsigned int VI_V_START_REG;
extern unsigned int VI_V_BURST_REG;
extern unsigned int VI_X_SCALE_REG;
extern unsigned int VI_Y_SCALE_REG;
uint32_t hstart = 0;
uint32_t vi_origin_offset = 320 * sizeof(uint16_t);
bool vi_black = false;
void set_dummy_vi() {
VI_STATUS_REG = 0x311E;
VI_WIDTH_REG = 0x140;
VI_V_SYNC_REG = 0x20D;
VI_H_SYNC_REG = 0xC15;
VI_LEAP_REG = 0x0C150C15;
hstart = 0x006C02EC;
VI_X_SCALE_REG = 0x200;
VI_V_CURRENT_LINE_REG = 0x0;
vi_origin_offset = 0x280;
VI_Y_SCALE_REG = 0x400;
VI_V_START_REG = 0x2501FF;
VI_V_BURST_REG = 0xE0204;
VI_INTR_REG = 0x2;
}
extern "C" void osViSwapBuffer(RDRAM_ARG PTR(void) frameBufPtr) {
if (vi_black) {
VI_H_START_REG = 0;
} else {
VI_H_START_REG = hstart;
}
events_context.vi.next_buffer = frameBufPtr;
events_context.action_queue.enqueue(SwapBuffersAction{ osVirtualToPhysical(frameBufPtr) + vi_origin_offset });
}
extern "C" void osViSetMode(RDRAM_ARG PTR(OSViMode) mode_) {
OSViMode* mode = TO_PTR(OSViMode, mode_);
VI_STATUS_REG = mode->comRegs.ctrl;
VI_WIDTH_REG = mode->comRegs.width;
// burst
VI_V_SYNC_REG = mode->comRegs.vSync;
VI_H_SYNC_REG = mode->comRegs.hSync;
VI_LEAP_REG = mode->comRegs.leap;
hstart = mode->comRegs.hStart;
VI_X_SCALE_REG = mode->comRegs.xScale;
VI_V_CURRENT_LINE_REG = mode->comRegs.vCurrent;
// TODO swap these every VI to account for fields changing
vi_origin_offset = mode->fldRegs[0].origin;
VI_Y_SCALE_REG = mode->fldRegs[0].yScale;
VI_V_START_REG = mode->fldRegs[0].vStart;
VI_V_BURST_REG = mode->fldRegs[0].vBurst;
VI_INTR_REG = mode->fldRegs[0].vIntr;
}
#define VI_CTRL_TYPE_16 0x00002
#define VI_CTRL_TYPE_32 0x00003
#define VI_CTRL_GAMMA_DITHER_ON 0x00004
#define VI_CTRL_GAMMA_ON 0x00008
#define VI_CTRL_DIVOT_ON 0x00010
#define VI_CTRL_SERRATE_ON 0x00040
#define VI_CTRL_ANTIALIAS_MASK 0x00300
#define VI_CTRL_ANTIALIAS_MODE_1 0x00100
#define VI_CTRL_ANTIALIAS_MODE_2 0x00200
#define VI_CTRL_ANTIALIAS_MODE_3 0x00300
#define VI_CTRL_PIXEL_ADV_MASK 0x01000
#define VI_CTRL_PIXEL_ADV_1 0x01000
#define VI_CTRL_PIXEL_ADV_2 0x02000
#define VI_CTRL_PIXEL_ADV_3 0x03000
#define VI_CTRL_DITHER_FILTER_ON 0x10000
#define OS_VI_GAMMA_ON 0x0001
#define OS_VI_GAMMA_OFF 0x0002
#define OS_VI_GAMMA_DITHER_ON 0x0004
#define OS_VI_GAMMA_DITHER_OFF 0x0008
#define OS_VI_DIVOT_ON 0x0010
#define OS_VI_DIVOT_OFF 0x0020
#define OS_VI_DITHER_FILTER_ON 0x0040
#define OS_VI_DITHER_FILTER_OFF 0x0080
extern "C" void osViSetSpecialFeatures(uint32_t func) {
if ((func & OS_VI_GAMMA_ON) != 0) {
VI_STATUS_REG |= VI_CTRL_GAMMA_ON;
}
if ((func & OS_VI_GAMMA_OFF) != 0) {
VI_STATUS_REG &= ~VI_CTRL_GAMMA_ON;
}
if ((func & OS_VI_GAMMA_DITHER_ON) != 0) {
VI_STATUS_REG |= VI_CTRL_GAMMA_DITHER_ON;
}
if ((func & OS_VI_GAMMA_DITHER_OFF) != 0) {
VI_STATUS_REG &= ~VI_CTRL_GAMMA_DITHER_ON;
}
if ((func & OS_VI_DIVOT_ON) != 0) {
VI_STATUS_REG |= VI_CTRL_DIVOT_ON;
}
if ((func & OS_VI_DIVOT_OFF) != 0) {
VI_STATUS_REG &= ~VI_CTRL_DIVOT_ON;
}
if ((func & OS_VI_DITHER_FILTER_ON) != 0) {
VI_STATUS_REG |= VI_CTRL_DITHER_FILTER_ON;
VI_STATUS_REG &= ~VI_CTRL_ANTIALIAS_MASK;
}
if ((func & OS_VI_DITHER_FILTER_OFF) != 0) {
VI_STATUS_REG &= ~VI_CTRL_DITHER_FILTER_ON;
//VI_STATUS_REG |= __osViNext->modep->comRegs.ctrl & VI_CTRL_ANTIALIAS_MASK;
}
}
extern "C" void osViBlack(uint8_t active) {
vi_black = active;
}
extern "C" void osViSetXScale(float scale) {
if (scale != 1.0f) {
assert(false);
}
}
extern "C" void osViSetYScale(float scale) {
if (scale != 1.0f) {
assert(false);
}
}
extern "C" PTR(void) osViGetNextFramebuffer() {
return events_context.vi.next_buffer;
}
extern "C" PTR(void) osViGetCurrentFramebuffer() {
return events_context.vi.current_buffer;
}
void ultramodern::submit_rsp_task(RDRAM_ARG PTR(OSTask) task_) {
OSTask* task = TO_PTR(OSTask, task_);
// Send gfx tasks to the graphics action queue
if (task->t.type == M_GFXTASK) {
events_context.action_queue.enqueue(SpTaskAction{ *task });
}
// Set all other tasks as the RSP task
else {
events_context.sp_task_queue.enqueue(task);
}
}
void ultramodern::send_si_message(RDRAM_ARG1) {
osSendMesg(PASS_RDRAM events_context.si.mq, events_context.si.msg, OS_MESG_NOBLOCK);
}
std::string get_graphics_api_name(ultramodern::GraphicsApi api) {
if (api == ultramodern::GraphicsApi::Auto) {
#if defined(_WIN32)
api = ultramodern::GraphicsApi::D3D12;
#elif defined(__gnu_linux__)
api = ultramodern::GraphicsApi::Vulkan;
#else
static_assert(false && "Unimplemented")
#endif
}
switch (api) {
case ultramodern::GraphicsApi::D3D12:
return "D3D12";
case ultramodern::GraphicsApi::Vulkan:
return "Vulkan";
default:
return "[Unknown graphics API]";
}
}
void ultramodern::init_events(RDRAM_ARG ultramodern::WindowHandle window_handle) {
moodycamel::LightweightSemaphore gfx_thread_ready;
moodycamel::LightweightSemaphore task_thread_ready;
events_context.rdram = rdram;
events_context.sp.gfx_thread = std::thread{ gfx_thread_func, rdram, &gfx_thread_ready, window_handle };
events_context.sp.task_thread = std::thread{ task_thread_func, rdram, &task_thread_ready };
// Wait for the two sp threads to be ready before continuing to prevent the game from
// running before we're able to handle RSP tasks.
gfx_thread_ready.wait();
task_thread_ready.wait();
ultramodern::RT64SetupResult setup_result = rt64_setup_result.load();
if (rt64_setup_result != ultramodern::RT64SetupResult::Success) {
auto show_rt64_error = [](const std::string& msg) {
// TODO move recomp code out of ultramodern (message boxes).
recomp::message_box(("An error has been encountered on startup: " + msg).c_str());
};
const std::string driver_os_suffix = "\nPlease make sure your GPU drivers and your OS are up to date.";
switch (rt64_setup_result) {
case ultramodern::RT64SetupResult::DynamicLibrariesNotFound:
show_rt64_error("Failed to load dynamic libraries. Make sure the DLLs are next to the recomp executable.");
break;
case ultramodern::RT64SetupResult::InvalidGraphicsAPI:
show_rt64_error(get_graphics_api_name(cur_config.load().api_option) + " is not supported on this platform. Please select a different graphics API.");
break;
case ultramodern::RT64SetupResult::GraphicsAPINotFound:
show_rt64_error("Unable to initialize " + get_graphics_api_name(cur_config.load().api_option) + "." + driver_os_suffix);
break;
case ultramodern::RT64SetupResult::GraphicsDeviceNotFound:
show_rt64_error("Unable to find compatible graphics device." + driver_os_suffix);
break;
}
throw std::runtime_error("Failed to initialize RT64");
}
events_context.vi.thread = std::thread{ vi_thread_func };
}
void ultramodern::join_event_threads() {
events_context.sp.gfx_thread.join();
events_context.vi.thread.join();
// Send a null RSP task to indicate that the RSP task thread should exit.
events_context.sp_task_queue.enqueue(nullptr);
events_context.sp.task_thread.join();
}

View File

@ -1,188 +0,0 @@
#include <thread>
#include "blockingconcurrentqueue.h"
#include "ultra64.h"
#include "ultramodern.hpp"
#include "recomp.h"
struct QueuedMessage {
PTR(OSMesgQueue) mq;
OSMesg mesg;
bool jam;
};
static moodycamel::BlockingConcurrentQueue<QueuedMessage> external_messages {};
void enqueue_external_message(PTR(OSMesgQueue) mq, OSMesg msg, bool jam) {
external_messages.enqueue({mq, msg, jam});
}
bool do_send(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, bool jam, bool block);
void dequeue_external_messages(RDRAM_ARG1) {
QueuedMessage to_send;
while (external_messages.try_dequeue(to_send)) {
do_send(PASS_RDRAM to_send.mq, to_send.mesg, to_send.jam, false);
}
}
void ultramodern::wait_for_external_message(RDRAM_ARG1) {
QueuedMessage to_send;
external_messages.wait_dequeue(to_send);
do_send(PASS_RDRAM to_send.mq, to_send.mesg, to_send.jam, false);
}
extern "C" void osCreateMesgQueue(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSMesg) msg, s32 count) {
OSMesgQueue *mq = TO_PTR(OSMesgQueue, mq_);
mq->blocked_on_recv = NULLPTR;
mq->blocked_on_send = NULLPTR;
mq->msgCount = count;
mq->msg = msg;
mq->validCount = 0;
mq->first = 0;
}
s32 MQ_GET_COUNT(OSMesgQueue *mq) {
return mq->validCount;
}
s32 MQ_IS_EMPTY(OSMesgQueue *mq) {
return mq->validCount == 0;
}
s32 MQ_IS_FULL(OSMesgQueue* mq) {
return MQ_GET_COUNT(mq) >= mq->msgCount;
}
bool do_send(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, bool jam, bool block) {
OSMesgQueue* mq = TO_PTR(OSMesgQueue, mq_);
if (!block) {
// If non-blocking, fail if the queue is full.
if (MQ_IS_FULL(mq)) {
return false;
}
}
else {
// Otherwise, yield this thread until the queue has room.
while (MQ_IS_FULL(mq)) {
debug_printf("[Message Queue] Thread %d is blocked on send\n", TO_PTR(OSThread, ultramodern::this_thread())->id);
ultramodern::thread_queue_insert(PASS_RDRAM GET_MEMBER(OSMesgQueue, mq_, blocked_on_send), ultramodern::this_thread());
ultramodern::run_next_thread_and_wait(PASS_RDRAM1);
}
}
if (jam) {
// Jams insert at the head of the message queue's buffer.
mq->first = (mq->first + mq->msgCount - 1) % mq->msgCount;
TO_PTR(OSMesg, mq->msg)[mq->first] = msg;
mq->validCount++;
}
else {
// Sends insert at the tail of the message queue's buffer.
s32 last = (mq->first + mq->validCount) % mq->msgCount;
TO_PTR(OSMesg, mq->msg)[last] = msg;
mq->validCount++;
}
// If any threads were blocked on receiving from this message queue, pop the first one and schedule it.
PTR(PTR(OSThread)) blocked_queue = GET_MEMBER(OSMesgQueue, mq_, blocked_on_recv);
if (!ultramodern::thread_queue_empty(PASS_RDRAM blocked_queue)) {
ultramodern::schedule_running_thread(PASS_RDRAM ultramodern::thread_queue_pop(PASS_RDRAM blocked_queue));
}
return true;
}
bool do_recv(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSMesg) msg_, bool block) {
OSMesgQueue* mq = TO_PTR(OSMesgQueue, mq_);
if (!block) {
// If non-blocking, fail if the queue is empty
if (MQ_IS_EMPTY(mq)) {
return false;
}
} else {
// Otherwise, yield this thread in a loop until the queue is no longer full
while (MQ_IS_EMPTY(mq)) {
debug_printf("[Message Queue] Thread %d is blocked on receive\n", TO_PTR(OSThread, ultramodern::this_thread())->id);
ultramodern::thread_queue_insert(PASS_RDRAM GET_MEMBER(OSMesgQueue, mq_, blocked_on_recv), ultramodern::this_thread());
ultramodern::run_next_thread_and_wait(PASS_RDRAM1);
}
}
if (msg_ != NULLPTR) {
*TO_PTR(OSMesg, msg_) = TO_PTR(OSMesg, mq->msg)[mq->first];
}
mq->first = (mq->first + 1) % mq->msgCount;
mq->validCount--;
// If any threads were blocked on sending to this message queue, pop the first one and schedule it.
PTR(PTR(OSThread)) blocked_queue = GET_MEMBER(OSMesgQueue, mq_, blocked_on_send);
if (!ultramodern::thread_queue_empty(PASS_RDRAM blocked_queue)) {
ultramodern::schedule_running_thread(PASS_RDRAM ultramodern::thread_queue_pop(PASS_RDRAM blocked_queue));
}
return true;
}
extern "C" s32 osSendMesg(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, s32 flags) {
OSMesgQueue *mq = TO_PTR(OSMesgQueue, mq_);
bool jam = false;
// Don't directly send to the message queue if this isn't a game thread to avoid contention.
if (!ultramodern::is_game_thread()) {
enqueue_external_message(mq_, msg, jam);
return 0;
}
// Handle any messages that have been received from an external thread.
dequeue_external_messages(PASS_RDRAM1);
// Try to send the message.
bool sent = do_send(PASS_RDRAM mq_, msg, jam, flags == OS_MESG_BLOCK);
// Check the queue to see if this thread should swap execution to another.
ultramodern::check_running_queue(PASS_RDRAM1);
return sent ? 0 : -1;
}
extern "C" s32 osJamMesg(RDRAM_ARG PTR(OSMesgQueue) mq_, OSMesg msg, s32 flags) {
OSMesgQueue *mq = TO_PTR(OSMesgQueue, mq_);
bool jam = true;
// Don't directly send to the message queue if this isn't a game thread to avoid contention.
if (!ultramodern::is_game_thread()) {
enqueue_external_message(mq_, msg, jam);
return 0;
}
// Handle any messages that have been received from an external thread.
dequeue_external_messages(PASS_RDRAM1);
// Try to send the message.
bool sent = do_send(PASS_RDRAM mq_, msg, jam, flags == OS_MESG_BLOCK);
// Check the queue to see if this thread should swap execution to another.
ultramodern::check_running_queue(PASS_RDRAM1);
return sent ? 0 : -1;
}
extern "C" s32 osRecvMesg(RDRAM_ARG PTR(OSMesgQueue) mq_, PTR(OSMesg) msg_, s32 flags) {
OSMesgQueue *mq = TO_PTR(OSMesgQueue, mq_);
assert(ultramodern::is_game_thread() && "RecvMesg not allowed outside of game threads.");
// Handle any messages that have been received from an external thread.
dequeue_external_messages(PASS_RDRAM1);
// Try to receive a message.
bool received = do_recv(PASS_RDRAM mq_, msg_, flags == OS_MESG_BLOCK);
// Check the queue to see if this thread should swap execution to another.
ultramodern::check_running_queue(PASS_RDRAM1);
return received ? 0 : -1;
}

View File

@ -1,22 +0,0 @@
#include "ultra64.h"
#define K0BASE 0x80000000
#define K1BASE 0xA0000000
#define K2BASE 0xC0000000
#define IS_KSEG0(x) ((u32)(x) >= K0BASE && (u32)(x) < K1BASE)
#define IS_KSEG1(x) ((u32)(x) >= K1BASE && (u32)(x) < K2BASE)
#define K0_TO_PHYS(x) ((u32)(x)&0x1FFFFFFF) /* kseg0 to physical */
#define K1_TO_PHYS(x) ((u32)(x)&0x1FFFFFFF) /* kseg1 to physical */
u32 osVirtualToPhysical(PTR(void) addr) {
uintptr_t addr_val = (uintptr_t)addr;
if (IS_KSEG0(addr_val)) {
return K0_TO_PHYS(addr_val);
} else if (IS_KSEG1(addr_val)) {
return K1_TO_PHYS(addr_val);
} else {
// TODO handle TLB mappings
return (u32)addr_val;
}
}

View File

@ -1,83 +0,0 @@
#if 0
#include <stdio.h>
#include <stdlib.h>
#include "ultra64.h"
#define THREAD_STACK_SIZE 0x1000
u8 idle_stack[THREAD_STACK_SIZE] ALIGNED(16);
u8 main_stack[THREAD_STACK_SIZE] ALIGNED(16);
u8 thread3_stack[THREAD_STACK_SIZE] ALIGNED(16);
u8 thread4_stack[THREAD_STACK_SIZE] ALIGNED(16);
OSThread idle_thread;
OSThread main_thread;
OSThread thread3;
OSThread thread4;
OSMesgQueue queue;
OSMesg buf[1];
void thread3_func(UNUSED void *arg) {
OSMesg val;
printf("Thread3 recv\n");
fflush(stdout);
osRecvMesg(&queue, &val, OS_MESG_BLOCK);
printf("Thread3 complete: %d\n", (int)(intptr_t)val);
fflush(stdout);
}
void thread4_func(void *arg) {
printf("Thread4 send %d\n", (int)(intptr_t)arg);
fflush(stdout);
osSendMesg(&queue, arg, OS_MESG_BLOCK);
printf("Thread4 complete\n");
fflush(stdout);
}
void main_thread_func(UNUSED void* arg) {
osCreateMesgQueue(&queue, buf, sizeof(buf) / sizeof(buf[0]));
printf("main thread creating thread 3\n");
osCreateThread(&thread3, 3, thread3_func, NULL, &thread3_stack[THREAD_STACK_SIZE], 14);
printf("main thread starting thread 3\n");
osStartThread(&thread3);
printf("main thread creating thread 4\n");
osCreateThread(&thread4, 4, thread4_func, (void*)10, &thread4_stack[THREAD_STACK_SIZE], 13);
printf("main thread starting thread 4\n");
osStartThread(&thread4);
while (1) {
printf("main thread doin stuff\n");
sleep(1);
}
}
void idle_thread_func(UNUSED void* arg) {
printf("idle thread\n");
printf("creating main thread\n");
osCreateThread(&main_thread, 2, main_thread_func, NULL, &main_stack[THREAD_STACK_SIZE], 11);
printf("starting main thread\n");
osStartThread(&main_thread);
// Set this thread's priority to 0, making it the idle thread
osSetThreadPri(NULL, 0);
// idle
while (1) {
printf("idle thread doin stuff\n");
sleep(1);
}
}
void bootproc(void) {
osInitialize();
osCreateThread(&idle_thread, 1, idle_thread_func, NULL, &idle_stack[THREAD_STACK_SIZE], 127);
printf("Starting idle thread\n");
osStartThread(&idle_thread);
}
#endif

View File

@ -1,319 +0,0 @@
#include <memory>
#include <cstring>
// #include <Windows.h>
#define HLSL_CPU
#include "hle/rt64_application.h"
#include "rt64_layer.h"
#include "rt64_render_hooks.h"
ultramodern::RT64Context::~RT64Context() = default;
static RT64::UserConfiguration::Antialiasing device_max_msaa = RT64::UserConfiguration::Antialiasing::None;
static bool sample_positions_supported = false;
static bool high_precision_fb_enabled = false;
static uint8_t DMEM[0x1000];
static uint8_t IMEM[0x1000];
unsigned int MI_INTR_REG = 0;
unsigned int DPC_START_REG = 0;
unsigned int DPC_END_REG = 0;
unsigned int DPC_CURRENT_REG = 0;
unsigned int DPC_STATUS_REG = 0;
unsigned int DPC_CLOCK_REG = 0;
unsigned int DPC_BUFBUSY_REG = 0;
unsigned int DPC_PIPEBUSY_REG = 0;
unsigned int DPC_TMEM_REG = 0;
unsigned int VI_STATUS_REG = 0;
unsigned int VI_ORIGIN_REG = 0;
unsigned int VI_WIDTH_REG = 0;
unsigned int VI_INTR_REG = 0;
unsigned int VI_V_CURRENT_LINE_REG = 0;
unsigned int VI_TIMING_REG = 0;
unsigned int VI_V_SYNC_REG = 0;
unsigned int VI_H_SYNC_REG = 0;
unsigned int VI_LEAP_REG = 0;
unsigned int VI_H_START_REG = 0;
unsigned int VI_V_START_REG = 0;
unsigned int VI_V_BURST_REG = 0;
unsigned int VI_X_SCALE_REG = 0;
unsigned int VI_Y_SCALE_REG = 0;
void dummy_check_interrupts() {
}
RT64::UserConfiguration::Antialiasing compute_max_supported_aa(RT64::RenderSampleCounts bits) {
if (bits & RT64::RenderSampleCount::Bits::COUNT_2) {
if (bits & RT64::RenderSampleCount::Bits::COUNT_4) {
if (bits & RT64::RenderSampleCount::Bits::COUNT_8) {
return RT64::UserConfiguration::Antialiasing::MSAA8X;
}
return RT64::UserConfiguration::Antialiasing::MSAA4X;
}
return RT64::UserConfiguration::Antialiasing::MSAA2X;
};
return RT64::UserConfiguration::Antialiasing::None;
}
RT64::UserConfiguration::InternalColorFormat to_rt64(ultramodern::HighPrecisionFramebuffer option) {
switch (option) {
case ultramodern::HighPrecisionFramebuffer::Off:
return RT64::UserConfiguration::InternalColorFormat::Standard;
case ultramodern::HighPrecisionFramebuffer::On:
return RT64::UserConfiguration::InternalColorFormat::High;
case ultramodern::HighPrecisionFramebuffer::Auto:
return RT64::UserConfiguration::InternalColorFormat::Automatic;
default:
return RT64::UserConfiguration::InternalColorFormat::OptionCount;
}
}
void set_application_user_config(RT64::Application* application, const ultramodern::GraphicsConfig& config) {
switch (config.res_option) {
default:
case ultramodern::Resolution::Auto:
application->userConfig.resolution = RT64::UserConfiguration::Resolution::WindowIntegerScale;
application->userConfig.downsampleMultiplier = 1;
break;
case ultramodern::Resolution::Original:
application->userConfig.resolution = RT64::UserConfiguration::Resolution::Manual;
application->userConfig.resolutionMultiplier = config.ds_option;
application->userConfig.downsampleMultiplier = config.ds_option;
break;
case ultramodern::Resolution::Original2x:
application->userConfig.resolution = RT64::UserConfiguration::Resolution::Manual;
application->userConfig.resolutionMultiplier = 2.0 * config.ds_option;
application->userConfig.downsampleMultiplier = config.ds_option;
break;
}
switch (config.hr_option) {
default:
case ultramodern::HUDRatioMode::Original:
application->userConfig.extAspectRatio = RT64::UserConfiguration::AspectRatio::Original;
break;
case ultramodern::HUDRatioMode::Clamp16x9:
application->userConfig.extAspectRatio = RT64::UserConfiguration::AspectRatio::Manual;
application->userConfig.extAspectTarget = 16.0/9.0;
break;
case ultramodern::HUDRatioMode::Full:
application->userConfig.extAspectRatio = RT64::UserConfiguration::AspectRatio::Expand;
break;
}
application->userConfig.aspectRatio = config.ar_option;
application->userConfig.antialiasing = config.msaa_option;
application->userConfig.refreshRate = config.rr_option;
application->userConfig.refreshRateTarget = config.rr_manual_value;
application->userConfig.internalColorFormat = to_rt64(config.hpfb_option);
}
ultramodern::RT64SetupResult map_setup_result(RT64::Application::SetupResult rt64_result) {
switch (rt64_result) {
case RT64::Application::SetupResult::Success:
return ultramodern::RT64SetupResult::Success;
case RT64::Application::SetupResult::DynamicLibrariesNotFound:
return ultramodern::RT64SetupResult::DynamicLibrariesNotFound;
case RT64::Application::SetupResult::InvalidGraphicsAPI:
return ultramodern::RT64SetupResult::InvalidGraphicsAPI;
case RT64::Application::SetupResult::GraphicsAPINotFound:
return ultramodern::RT64SetupResult::GraphicsAPINotFound;
case RT64::Application::SetupResult::GraphicsDeviceNotFound:
return ultramodern::RT64SetupResult::GraphicsDeviceNotFound;
}
}
ultramodern::RT64Context::RT64Context(uint8_t* rdram, ultramodern::WindowHandle window_handle, bool debug) {
static unsigned char dummy_rom_header[0x40];
set_rt64_hooks();
// Set up the RT64 application core fields.
RT64::Application::Core appCore{};
#if defined(_WIN32)
appCore.window = window_handle.window;
#elif defined(__ANDROID__)
assert(false && "Unimplemented");
#elif defined(__linux__)
appCore.window.display = window_handle.display;
appCore.window.window = window_handle.window;
#endif
appCore.checkInterrupts = dummy_check_interrupts;
appCore.HEADER = dummy_rom_header;
appCore.RDRAM = rdram;
appCore.DMEM = DMEM;
appCore.IMEM = IMEM;
appCore.MI_INTR_REG = &MI_INTR_REG;
appCore.DPC_START_REG = &DPC_START_REG;
appCore.DPC_END_REG = &DPC_END_REG;
appCore.DPC_CURRENT_REG = &DPC_CURRENT_REG;
appCore.DPC_STATUS_REG = &DPC_STATUS_REG;
appCore.DPC_CLOCK_REG = &DPC_CLOCK_REG;
appCore.DPC_BUFBUSY_REG = &DPC_BUFBUSY_REG;
appCore.DPC_PIPEBUSY_REG = &DPC_PIPEBUSY_REG;
appCore.DPC_TMEM_REG = &DPC_TMEM_REG;
appCore.VI_STATUS_REG = &VI_STATUS_REG;
appCore.VI_ORIGIN_REG = &VI_ORIGIN_REG;
appCore.VI_WIDTH_REG = &VI_WIDTH_REG;
appCore.VI_INTR_REG = &VI_INTR_REG;
appCore.VI_V_CURRENT_LINE_REG = &VI_V_CURRENT_LINE_REG;
appCore.VI_TIMING_REG = &VI_TIMING_REG;
appCore.VI_V_SYNC_REG = &VI_V_SYNC_REG;
appCore.VI_H_SYNC_REG = &VI_H_SYNC_REG;
appCore.VI_LEAP_REG = &VI_LEAP_REG;
appCore.VI_H_START_REG = &VI_H_START_REG;
appCore.VI_V_START_REG = &VI_V_START_REG;
appCore.VI_V_BURST_REG = &VI_V_BURST_REG;
appCore.VI_X_SCALE_REG = &VI_X_SCALE_REG;
appCore.VI_Y_SCALE_REG = &VI_Y_SCALE_REG;
// Set up the RT64 application configuration fields.
RT64::ApplicationConfiguration appConfig;
appConfig.useConfigurationFile = false;
// Create the RT64 application.
app = std::make_unique<RT64::Application>(appCore, appConfig);
// Set initial user config settings based on the current settings.
ultramodern::GraphicsConfig cur_config = ultramodern::get_graphics_config();
set_application_user_config(app.get(), cur_config);
app->userConfig.developerMode = debug;
// Force gbi depth branches to prevent LODs from kicking in.
app->enhancementConfig.f3dex.forceBranch = true;
// Scale LODs based on the output resolution.
app->enhancementConfig.textureLOD.scale = true;
// Pick an API if the user has set an override.
switch (cur_config.api_option) {
case ultramodern::GraphicsApi::D3D12:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::D3D12;
break;
case ultramodern::GraphicsApi::Vulkan:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Vulkan;
break;
default:
case ultramodern::GraphicsApi::Auto:
// Don't override if auto is selected.
break;
}
// Set up the RT64 application.
uint32_t thread_id = 0;
#ifdef _WIN32
thread_id = window_handle.thread_id;
#endif
setup_result = map_setup_result(app->setup(thread_id));
if (setup_result != ultramodern::RT64SetupResult::Success) {
app = nullptr;
return;
}
// Set the application's fullscreen state.
app->setFullScreen(cur_config.wm_option == ultramodern::WindowMode::Fullscreen);
// Check if the selected device actually supports MSAA sample positions and MSAA for for the formats that will be used
// and downgrade the configuration accordingly.
if (app->device->getCapabilities().sampleLocations) {
RT64::RenderSampleCounts color_sample_counts = app->device->getSampleCountsSupported(RT64::RenderFormat::R8G8B8A8_UNORM);
RT64::RenderSampleCounts depth_sample_counts = app->device->getSampleCountsSupported(RT64::RenderFormat::D32_FLOAT);
RT64::RenderSampleCounts common_sample_counts = color_sample_counts & depth_sample_counts;
device_max_msaa = compute_max_supported_aa(common_sample_counts);
sample_positions_supported = true;
}
else {
device_max_msaa = RT64::UserConfiguration::Antialiasing::None;
sample_positions_supported = false;
}
high_precision_fb_enabled = app->shaderLibrary->usesHDR;
}
void ultramodern::RT64Context::send_dl(const OSTask* task) {
app->state->rsp->reset();
app->interpreter->loadUCodeGBI(task->t.ucode & 0x3FFFFFF, task->t.ucode_data & 0x3FFFFFF, true);
app->processDisplayLists(app->core.RDRAM, task->t.data_ptr & 0x3FFFFFF, 0, true);
}
void ultramodern::RT64Context::update_screen(uint32_t vi_origin) {
VI_ORIGIN_REG = vi_origin;
app->updateScreen();
}
void ultramodern::RT64Context::shutdown() {
if (app != nullptr) {
app->end();
}
}
void ultramodern::RT64Context::update_config(const ultramodern::GraphicsConfig& old_config, const ultramodern::GraphicsConfig& new_config) {
if (new_config.wm_option != old_config.wm_option) {
app->setFullScreen(new_config.wm_option == ultramodern::WindowMode::Fullscreen);
}
set_application_user_config(app.get(), new_config);
app->updateUserConfig(true);
if (new_config.msaa_option != old_config.msaa_option) {
app->updateMultisampling();
}
}
void ultramodern::RT64Context::enable_instant_present() {
// Enable the present early presentation mode for minimal latency.
app->enhancementConfig.presentation.mode = RT64::EnhancementConfiguration::Presentation::Mode::PresentEarly;
app->updateEnhancementConfig();
}
uint32_t ultramodern::RT64Context::get_display_framerate() {
return app->presentQueue->ext.sharedResources->swapChainRate;
}
float ultramodern::RT64Context::get_resolution_scale() {
constexpr int ReferenceHeight = 240;
switch (app->userConfig.resolution) {
case RT64::UserConfiguration::Resolution::WindowIntegerScale:
if (app->sharedQueueResources->swapChainHeight > 0) {
return std::max(float((app->sharedQueueResources->swapChainHeight + ReferenceHeight - 1) / ReferenceHeight), 1.0f);
}
else {
return 1.0f;
}
case RT64::UserConfiguration::Resolution::Manual:
return float(app->userConfig.resolutionMultiplier);
case RT64::UserConfiguration::Resolution::Original:
default:
return 1.0f;
}
}
void ultramodern::RT64Context::load_shader_cache(std::span<const char> cache_binary) {
// TODO figure out how to avoid a copy here.
std::istringstream cache_stream{std::string{cache_binary.data(), cache_binary.size()}};
if (!app->rasterShaderCache->loadOfflineList(cache_stream)) {
printf("Failed to preload shader cache!\n");
assert(false);
}
}
RT64::UserConfiguration::Antialiasing ultramodern::RT64MaxMSAA() {
return device_max_msaa;
}
bool ultramodern::RT64SamplePositionsSupported() {
return sample_positions_supported;
}
bool ultramodern::RT64HighPrecisionFBEnabled() {
return high_precision_fb_enabled;
}

View File

@ -1,38 +0,0 @@
#include "ultramodern.hpp"
void ultramodern::schedule_running_thread(RDRAM_ARG PTR(OSThread) t_) {
debug_printf("[Scheduling] Adding thread %d to the running queue\n", TO_PTR(OSThread, t_)->id);
thread_queue_insert(PASS_RDRAM running_queue, t_);
TO_PTR(OSThread, t_)->state = OSThreadState::QUEUED;
}
void swap_to_thread(RDRAM_ARG OSThread *to) {
debug_printf("[Scheduling] Thread %d giving execution to thread %d\n", TO_PTR(OSThread, ultramodern::this_thread())->id, to->id);
// Insert this thread in the running queue.
ultramodern::thread_queue_insert(PASS_RDRAM ultramodern::running_queue, ultramodern::this_thread());
TO_PTR(OSThread, ultramodern::this_thread())->state = OSThreadState::QUEUED;
// Unpause the target thread and wait for this one to be unpaused.
ultramodern::resume_thread_and_wait(PASS_RDRAM to);
}
void ultramodern::check_running_queue(RDRAM_ARG1) {
// Check if there are any threads in the running queue.
if (!thread_queue_empty(PASS_RDRAM running_queue)) {
// Check if the highest priority thread in the queue is higher priority than the current thread.
OSThread* next_thread = TO_PTR(OSThread, ultramodern::thread_queue_peek(PASS_RDRAM running_queue));
OSThread* self = TO_PTR(OSThread, ultramodern::this_thread());
if (next_thread->priority > self->priority) {
ultramodern::thread_queue_pop(PASS_RDRAM running_queue);
// Swap to the higher priority thread.
swap_to_thread(PASS_RDRAM next_thread);
}
}
}
extern "C" void pause_self(RDRAM_ARG1) {
while (true) {
// Wait until an external message arrives, then allow the next thread to run.
ultramodern::wait_for_external_message(PASS_RDRAM1);
ultramodern::check_running_queue(PASS_RDRAM1);
}
}

View File

@ -1,13 +0,0 @@
#ifdef _WIN32
#include <Windows.h>
#include "ultra64.h"
#include "ultramodern.hpp"
extern "C" unsigned int sleep(unsigned int seconds) {
Sleep(seconds * 1000);
return 0;
}
#endif

View File

@ -1,66 +0,0 @@
#include <cassert>
#include "ultramodern.hpp"
static PTR(OSThread) running_queue_impl = NULLPTR;
static PTR(OSThread)* queue_to_ptr(RDRAM_ARG PTR(PTR(OSThread)) queue) {
if (queue == ultramodern::running_queue) {
return &running_queue_impl;
}
return TO_PTR(PTR(OSThread), queue);
}
void ultramodern::thread_queue_insert(RDRAM_ARG PTR(PTR(OSThread)) queue_, PTR(OSThread) toadd_) {
PTR(OSThread)* cur = queue_to_ptr(PASS_RDRAM queue_);
OSThread* toadd = TO_PTR(OSThread, toadd_);
debug_printf("[Thread Queue] Inserting thread %d into queue 0x%08X\n", toadd->id, (uintptr_t)queue_);
while (*cur && TO_PTR(OSThread, *cur)->priority > toadd->priority) {
cur = &TO_PTR(OSThread, *cur)->next;
}
toadd->next = (*cur);
toadd->queue = queue_;
*cur = toadd_;
debug_printf(" Contains:");
cur = queue_to_ptr(PASS_RDRAM queue_);
while (*cur) {
debug_printf("%d (%d) ", TO_PTR(OSThread, *cur)->id, TO_PTR(OSThread, *cur)->priority);
cur = &TO_PTR(OSThread, *cur)->next;
}
debug_printf("\n");
}
PTR(OSThread) ultramodern::thread_queue_pop(RDRAM_ARG PTR(PTR(OSThread)) queue_) {
PTR(OSThread)* queue = queue_to_ptr(PASS_RDRAM queue_);
PTR(OSThread) ret = *queue;
*queue = TO_PTR(OSThread, ret)->next;
TO_PTR(OSThread, ret)->queue = NULLPTR;
debug_printf("[Thread Queue] Popped thread %d from queue 0x%08X\n", TO_PTR(OSThread, ret)->id, (uintptr_t)queue_);
return ret;
}
bool ultramodern::thread_queue_remove(RDRAM_ARG PTR(PTR(OSThread)) queue_, PTR(OSThread) t_) {
debug_printf("[Thread Queue] Removing thread %d from queue 0x%08X\n", TO_PTR(OSThread, t_)->id, (uintptr_t)queue_);
PTR(PTR(OSThread)) cur = queue_;
while (cur != NULLPTR) {
PTR(OSThread)* cur_ptr = queue_to_ptr(PASS_RDRAM queue_);
if (*cur_ptr == t_) {
return true;
}
cur = TO_PTR(OSThread, *cur_ptr)->next;
}
return false;
}
bool ultramodern::thread_queue_empty(RDRAM_ARG PTR(PTR(OSThread)) queue_) {
PTR(OSThread)* queue = queue_to_ptr(PASS_RDRAM queue_);
return *queue == NULLPTR;
}
PTR(OSThread) ultramodern::thread_queue_peek(RDRAM_ARG PTR(PTR(OSThread)) queue_) {
PTR(OSThread)* queue = queue_to_ptr(PASS_RDRAM queue_);
return *queue;
}

View File

@ -1,346 +0,0 @@
#include <cstdio>
#include <thread>
#include <cassert>
#include <string>
#include "ultra64.h"
#include "ultramodern.hpp"
#include "blockingconcurrentqueue.h"
// Native APIs only used to set thread names for easier debugging
#ifdef _WIN32
#include <Windows.h>
#endif
extern "C" void bootproc();
thread_local bool is_main_thread = false;
// Whether this thread is part of the game (i.e. the start thread or one spawned by osCreateThread)
thread_local bool is_game_thread = false;
thread_local PTR(OSThread) thread_self = NULLPTR;
void ultramodern::set_main_thread() {
::is_game_thread = true;
is_main_thread = true;
}
bool ultramodern::is_game_thread() {
return ::is_game_thread;
}
#if 0
int main(int argc, char** argv) {
ultramodern::set_main_thread();
bootproc();
}
#endif
#if 1
void run_thread_function(uint8_t* rdram, uint64_t addr, uint64_t sp, uint64_t arg);
#else
#define run_thread_function(func, sp, arg) func(arg)
#endif
#if defined(_WIN32)
void ultramodern::set_native_thread_name(const std::string& name) {
std::wstring wname{name.begin(), name.end()};
HRESULT r;
r = SetThreadDescription(
GetCurrentThread(),
wname.c_str()
);
}
void ultramodern::set_native_thread_priority(ThreadPriority pri) {
int nPriority = THREAD_PRIORITY_NORMAL;
// Convert ThreadPriority to Win32 priority
switch (pri) {
case ThreadPriority::Low:
nPriority = THREAD_PRIORITY_BELOW_NORMAL;
break;
case ThreadPriority::Normal:
nPriority = THREAD_PRIORITY_NORMAL;
break;
case ThreadPriority::High:
nPriority = THREAD_PRIORITY_ABOVE_NORMAL;
break;
case ThreadPriority::VeryHigh:
nPriority = THREAD_PRIORITY_HIGHEST;
break;
case ThreadPriority::Critical:
nPriority = THREAD_PRIORITY_TIME_CRITICAL;
break;
default:
throw std::runtime_error("Invalid thread priority!");
break;
}
// SetThreadPriority(GetCurrentThread(), nPriority);
}
#elif defined(__linux__)
void ultramodern::set_native_thread_name(const std::string& name) {
pthread_setname_np(pthread_self(), name.c_str());
}
void ultramodern::set_native_thread_priority(ThreadPriority pri) {
// TODO linux thread priority
// printf("set_native_thread_priority unimplemented\n");
// int nPriority = THREAD_PRIORITY_NORMAL;
// // Convert ThreadPriority to Win32 priority
// switch (pri) {
// case ThreadPriority::Low:
// nPriority = THREAD_PRIORITY_BELOW_NORMAL;
// break;
// case ThreadPriority::Normal:
// nPriority = THREAD_PRIORITY_NORMAL;
// break;
// case ThreadPriority::High:
// nPriority = THREAD_PRIORITY_ABOVE_NORMAL;
// break;
// case ThreadPriority::VeryHigh:
// nPriority = THREAD_PRIORITY_HIGHEST;
// break;
// case ThreadPriority::Critical:
// nPriority = THREAD_PRIORITY_TIME_CRITICAL;
// break;
// default:
// throw std::runtime_error("Invalid thread priority!");
// break;
// }
}
#endif
std::atomic_int temporary_threads = 0;
std::atomic_int permanent_threads = 0;
void wait_for_resumed(RDRAM_ARG UltraThreadContext* thread_context) {
TO_PTR(OSThread, ultramodern::this_thread())->context->running.wait();
// If this thread's context was replaced by another thread or deleted, destroy it again from its own context.
// This will trigger thread cleanup instead.
if (TO_PTR(OSThread, ultramodern::this_thread())->context != thread_context) {
osDestroyThread(PASS_RDRAM NULLPTR);
}
}
void resume_thread(OSThread* t) {
debug_printf("[Thread] Resuming execution of thread %d\n", t->id);
t->context->running.signal();
}
void run_next_thread(RDRAM_ARG1) {
if (ultramodern::thread_queue_empty(PASS_RDRAM ultramodern::running_queue)) {
throw std::runtime_error("No threads left to run!\n");
}
OSThread* to_run = TO_PTR(OSThread, ultramodern::thread_queue_pop(PASS_RDRAM ultramodern::running_queue));
debug_printf("[Scheduling] Resuming execution of thread %d\n", to_run->id);
to_run->context->running.signal();
}
void ultramodern::run_next_thread_and_wait(RDRAM_ARG1) {
UltraThreadContext* cur_context = TO_PTR(OSThread, thread_self)->context;
run_next_thread(PASS_RDRAM1);
wait_for_resumed(PASS_RDRAM cur_context);
}
void ultramodern::resume_thread_and_wait(RDRAM_ARG OSThread *t) {
UltraThreadContext* cur_context = TO_PTR(OSThread, thread_self)->context;
resume_thread(t);
wait_for_resumed(PASS_RDRAM cur_context);
}
static void _thread_func(RDRAM_ARG PTR(OSThread) self_, PTR(thread_func_t) entrypoint, PTR(void) arg, UltraThreadContext* thread_context) {
OSThread *self = TO_PTR(OSThread, self_);
debug_printf("[Thread] Thread created: %d\n", self->id);
thread_self = self_;
is_game_thread = true;
// Set the thread name
ultramodern::set_native_thread_name("Game Thread " + std::to_string(self->id));
ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::High);
// TODO fix these being hardcoded (this is only used for quicksaving)
if ((self->id == 2 && self->priority == 5) || self->id == 13) { // slowly, flashrom
temporary_threads.fetch_add(1);
}
else if (self->id != 1 && self->id != 2) { // ignore idle and fault
permanent_threads.fetch_add(1);
}
// Signal the initialized semaphore to indicate that this thread can be started.
thread_context->initialized.signal();
debug_printf("[Thread] Thread waiting to be started: %d\n", self->id);
// Wait until the thread is marked as running.
wait_for_resumed(PASS_RDRAM thread_context);
// Make sure the thread wasn't replaced or destroyed before it was started.
if (self->context == thread_context) {
debug_printf("[Thread] Thread started: %d\n", self->id);
try {
// Run the thread's function with the provided argument.
run_thread_function(PASS_RDRAM entrypoint, self->sp, arg);
} catch (ultramodern::thread_terminated& terminated) {
}
}
else {
debug_printf("[Thread] Thread destroyed before being started: %d\n", self->id);
}
// Check if the thread hasn't been destroyed or replaced. If so, then the thread terminated or destroyed itself,
// so mark this thread as destroyed and run the next queued thread.
if (self->context == thread_context) {
self->context = nullptr;
run_next_thread(PASS_RDRAM1);
}
// Dispose of this thread now that it's completed or terminated.
ultramodern::cleanup_thread(thread_context);
// TODO fix these being hardcoded (this is only used for quicksaving)
if ((self->id == 2 && self->priority == 5) || self->id == 13) { // slowly, flashrom
temporary_threads.fetch_sub(1);
}
}
uint32_t ultramodern::permanent_thread_count() {
return permanent_threads.load();
}
uint32_t ultramodern::temporary_thread_count() {
return temporary_threads.load();
}
extern "C" void osStartThread(RDRAM_ARG PTR(OSThread) t_) {
OSThread* t = TO_PTR(OSThread, t_);
debug_printf("[os] Start Thread %d\n", t->id);
// Wait until the thread is initialized to indicate that it's ready to be started.
t->context->initialized.wait();
debug_printf("[os] Thread %d is ready to be started\n", t->id);
// If this is a game thread, insert the new thread into the running queue and then check the running queue.
if (thread_self) {
ultramodern::schedule_running_thread(PASS_RDRAM t_);
ultramodern::check_running_queue(PASS_RDRAM1);
}
// Otherwise, immediately start the thread and terminate this one.
else {
t->state = OSThreadState::QUEUED;
resume_thread(t);
//throw ultramodern::thread_terminated{};
}
}
extern "C" void osCreateThread(RDRAM_ARG PTR(OSThread) t_, OSId id, PTR(thread_func_t) entrypoint, PTR(void) arg, PTR(void) sp, OSPri pri) {
debug_printf("[os] Create Thread %d\n", id);
OSThread *t = TO_PTR(OSThread, t_);
t->next = NULLPTR;
t->queue = NULLPTR;
t->priority = pri;
t->id = id;
t->state = OSThreadState::STOPPED;
t->sp = sp - 0x10; // Set up the first stack frame
// Spawn a new thread, which will immediately pause itself and wait until it's been started.
// Pass the context as an argument to the thread function to ensure that it can't get cleared before the thread captures its value.
t->context = new UltraThreadContext{};
t->context->host_thread = std::thread{_thread_func, PASS_RDRAM t_, entrypoint, arg, t->context};
}
extern "C" void osStopThread(RDRAM_ARG PTR(OSThread) t_) {
assert(false);
}
extern "C" void osDestroyThread(RDRAM_ARG PTR(OSThread) t_) {
if (t_ == NULLPTR) {
t_ = thread_self;
}
OSThread* t = TO_PTR(OSThread, t_);
// Check if the thread is destroying itself (arg is null or thread_self)
if (t_ == thread_self) {
throw ultramodern::thread_terminated{};
}
// Otherwise if the thread isn't stopped, remove it from its currrent queue.,
if (t->state != OSThreadState::STOPPED) {
ultramodern::thread_queue_remove(PASS_RDRAM t->queue, t_);
}
// Check if the thread has already been destroyed to prevent destroying it again.
UltraThreadContext* cur_context = t->context;
if (cur_context != nullptr) {
// Mark the target thread as destroyed and resume it. When it starts it'll check this and terminate itself instead of resuming.
t->context = nullptr;
cur_context->running.signal();
}
}
extern "C" void osSetThreadPri(RDRAM_ARG PTR(OSThread) t_, OSPri pri) {
if (t_ == NULLPTR) {
t_ = thread_self;
}
OSThread* t = TO_PTR(OSThread, t_);
if (t->priority != pri) {
t->priority = pri;
if (t_ != ultramodern::this_thread() && t->state != OSThreadState::STOPPED) {
ultramodern::thread_queue_remove(PASS_RDRAM t->queue, t_);
ultramodern::thread_queue_insert(PASS_RDRAM t->queue, t_);
}
ultramodern::check_running_queue(PASS_RDRAM1);
}
}
extern "C" OSPri osGetThreadPri(RDRAM_ARG PTR(OSThread) t) {
if (t == NULLPTR) {
t = thread_self;
}
return TO_PTR(OSThread, t)->priority;
}
extern "C" OSId osGetThreadId(RDRAM_ARG PTR(OSThread) t) {
if (t == NULLPTR) {
t = thread_self;
}
return TO_PTR(OSThread, t)->id;
}
PTR(OSThread) ultramodern::this_thread() {
return thread_self;
}
static std::thread thread_cleaner_thread;
static moodycamel::BlockingConcurrentQueue<UltraThreadContext*> deleted_threads{};
extern std::atomic_bool exited;
void thread_cleaner_func() {
using namespace std::chrono_literals;
while (!exited) {
UltraThreadContext* to_delete;
if (deleted_threads.wait_dequeue_timed(to_delete, 10ms)) {
debug_printf("[Cleanup] Deleting thread context %p\n", to_delete);
to_delete->host_thread.join();
delete to_delete;
}
}
}
void ultramodern::init_thread_cleanup() {
thread_cleaner_thread = std::thread{thread_cleaner_func};
}
void ultramodern::cleanup_thread(UltraThreadContext *cur_context) {
deleted_threads.enqueue(cur_context);
}
void ultramodern::join_thread_cleaner_thread() {
thread_cleaner_thread.join();
}

View File

@ -1,224 +0,0 @@
#include <thread>
#include <variant>
#include <set>
#include "blockingconcurrentqueue.h"
#include "ultra64.h"
#include "ultramodern.hpp"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include "Windows.h"
#endif
// Start time for the program
static std::chrono::high_resolution_clock::time_point start_time = std::chrono::high_resolution_clock::now();
// Game speed multiplier (1 means no speedup)
constexpr uint32_t speed_multiplier = 1;
// N64 CPU counter ticks per millisecond
constexpr uint32_t counter_per_ms = 46'875 * speed_multiplier;
struct OSTimer {
PTR(OSTimer) unused1;
PTR(OSTimer) unused2;
OSTime interval;
OSTime timestamp;
PTR(OSMesgQueue) mq;
OSMesg msg;
};
struct AddTimerAction {
PTR(OSTimer) timer;
};
struct RemoveTimerAction {
PTR(OSTimer) timer;
};
using Action = std::variant<AddTimerAction, RemoveTimerAction>;
struct {
std::thread thread;
moodycamel::BlockingConcurrentQueue<Action> action_queue{};
} timer_context;
uint64_t duration_to_ticks(std::chrono::high_resolution_clock::duration duration) {
uint64_t delta_micros = std::chrono::duration_cast<std::chrono::microseconds>(duration).count();
// More accurate than using a floating point timer, will only overflow after running for 12.47 years
// Units: (micros * (counts/millis)) / (micros/millis) = counts
uint64_t total_count = (delta_micros * counter_per_ms) / 1000;
return total_count;
}
std::chrono::microseconds ticks_to_duration(uint64_t ticks) {
using namespace std::chrono_literals;
return ticks * 1000us / counter_per_ms;
}
std::chrono::high_resolution_clock::time_point ticks_to_timepoint(uint64_t ticks) {
return start_time + ticks_to_duration(ticks);
}
uint64_t time_now() {
return duration_to_ticks(std::chrono::high_resolution_clock::now() - start_time);
}
void timer_thread(RDRAM_ARG1) {
ultramodern::set_native_thread_name("Timer Thread");
ultramodern::set_native_thread_priority(ultramodern::ThreadPriority::VeryHigh);
// Lambda comparator function to keep the set ordered
auto timer_sort = [PASS_RDRAM1](PTR(OSTimer) a_, PTR(OSTimer) b_) {
OSTimer* a = TO_PTR(OSTimer, a_);
OSTimer* b = TO_PTR(OSTimer, b_);
// Order by timestamp if the timers have different timestamps
if (a->timestamp != b->timestamp) {
return a->timestamp < b->timestamp;
}
// If they have the exact same timestamp then order by address instead
return a < b;
};
// Ordered set of timers that are currently active
std::set<PTR(OSTimer), decltype(timer_sort)> active_timers{timer_sort};
// Lambda to process a timer action to handle adding and removing timers
auto process_timer_action = [&](const Action& action) {
// Determine the action type and act on it
if (const auto* add_action = std::get_if<AddTimerAction>(&action)) {
active_timers.insert(add_action->timer);
} else if (const auto* remove_action = std::get_if<RemoveTimerAction>(&action)) {
active_timers.erase(remove_action->timer);
}
};
while (true) {
// Empty the action queue
Action cur_action;
while (timer_context.action_queue.try_dequeue(cur_action)) {
process_timer_action(cur_action);
}
// If there's no timer to act on, wait for one to come in from the action queue
while (active_timers.empty()) {
timer_context.action_queue.wait_dequeue(cur_action);
process_timer_action(cur_action);
}
// Get the timer that's closest to running out
PTR(OSTimer) cur_timer_ = *active_timers.begin();
OSTimer* cur_timer = TO_PTR(OSTimer, cur_timer_);
// Remove the timer from the queue (it may get readded if waiting is interrupted)
active_timers.erase(cur_timer_);
// Determine how long to wait to reach the timer's timestamp
auto wait_duration = ticks_to_timepoint(cur_timer->timestamp) - std::chrono::high_resolution_clock::now();
// Wait for either the duration to complete or a new action to come through
if (wait_duration.count() >= 0 && timer_context.action_queue.wait_dequeue_timed(cur_action, wait_duration)) {
// Timer was interrupted by a new action
// Add the current timer back to the queue (done first in case the action is to remove this timer)
active_timers.insert(cur_timer_);
// Process the new action
process_timer_action(cur_action);
}
else {
// Waiting for the timer completed, so send the timer's message to its message queue
osSendMesg(PASS_RDRAM cur_timer->mq, cur_timer->msg, OS_MESG_NOBLOCK);
// If the timer has a specified interval then reload it with that value
if (cur_timer->interval != 0) {
cur_timer->timestamp = cur_timer->interval + time_now();
active_timers.insert(cur_timer_);
}
}
}
}
void ultramodern::init_timers(RDRAM_ARG1) {
timer_context.thread = std::thread{ timer_thread, PASS_RDRAM1 };
timer_context.thread.detach();
}
uint32_t ultramodern::get_speed_multiplier() {
return speed_multiplier;
}
std::chrono::high_resolution_clock::time_point ultramodern::get_start() {
return start_time;
}
std::chrono::high_resolution_clock::duration ultramodern::time_since_start() {
return std::chrono::high_resolution_clock::now() - start_time;
}
extern "C" u32 osGetCount() {
uint64_t total_count = time_now();
// Allow for overflows, which is how osGetCount behaves
return (uint32_t)total_count;
}
extern "C" OSTime osGetTime() {
uint64_t total_count = time_now();
return total_count;
}
extern "C" int osSetTimer(RDRAM_ARG PTR(OSTimer) t_, OSTime countdown, OSTime interval, PTR(OSMesgQueue) mq, OSMesg msg) {
OSTimer* t = TO_PTR(OSTimer, t_);
// Determine the time when this timer will trigger off
if (countdown == 0) {
// Set the timestamp based on the interval
t->timestamp = interval + time_now();
} else {
t->timestamp = countdown + time_now();
}
t->interval = interval;
t->mq = mq;
t->msg = msg;
timer_context.action_queue.enqueue(AddTimerAction{ t_ });
return 0;
}
extern "C" int osStopTimer(RDRAM_ARG PTR(OSTimer) t_) {
timer_context.action_queue.enqueue(RemoveTimerAction{ t_ });
// TODO don't blindly return 0 here; requires some response from the timer thread to know what the returned value was
return 0;
}
#ifdef _WIN32
// The implementations of std::chrono::sleep_until and sleep_for were affected by changing the system clock backwards in older versions
// of Microsoft's STL. This was fixed as of Visual Studio 2022 17.9, but to be safe ultramodern uses Win32 Sleep directly.
void ultramodern::sleep_milliseconds(uint32_t millis) {
Sleep(millis);
}
void ultramodern::sleep_until(const std::chrono::high_resolution_clock::time_point& time_point) {
auto time_now = std::chrono::high_resolution_clock::now();
if (time_point > time_now) {
long long delta_ms = std::chrono::ceil<std::chrono::milliseconds>(time_point - time_now).count();
// printf("Sleeping %lld %d ms\n", delta_ms, (uint32_t)delta_ms);
Sleep(delta_ms);
}
}
#else
void ultramodern::sleep_milliseconds(uint32_t millis) {
std::this_thread::sleep_for(std::chrono::milliseconds{millis});
}
void ultramodern::sleep_until(const std::chrono::high_resolution_clock::time_point& time_point) {
std::this_thread::sleep_until(time_point);
}
#endif

View File

@ -1,260 +0,0 @@
#ifndef __ULTRA64_ultramodern_H__
#define __ULTRA64_ultramodern_H__
#include <stdint.h>
#ifdef __GNUC__
#define UNUSED __attribute__((unused))
#define ALIGNED(x) __attribute__((aligned(x)))
#else
#define UNUSED
#define ALIGNED(x)
#endif
typedef int64_t s64;
typedef uint64_t u64;
typedef int32_t s32;
typedef uint32_t u32;
typedef int16_t s16;
typedef uint16_t u16;
typedef int8_t s8;
typedef uint8_t u8;
#if 0 // For native compilation
# define PTR(x) x*
# define RDRAM_ARG
# define RDRAM_ARG1
# define PASS_RDRAM
# define PASS_RDRAM1
# define TO_PTR(type, var) var
# define GET_MEMBER(type, addr, member) (&addr->member)
# ifdef __cplusplus
# define NULLPTR nullptr
# endif
#else
# define PTR(x) int32_t
# define RDRAM_ARG uint8_t *rdram,
# define RDRAM_ARG1 uint8_t *rdram
# define PASS_RDRAM rdram,
# define PASS_RDRAM1 rdram
# define TO_PTR(type, var) ((type*)(&rdram[(uint64_t)var - 0xFFFFFFFF80000000]))
# define GET_MEMBER(type, addr, member) (addr + (intptr_t)&(((type*)nullptr)->member))
# ifdef __cplusplus
# define NULLPTR (PTR(void))0
# endif
#endif
#ifndef NULL
#define NULL (PTR(void) 0)
#endif
#define OS_MESG_NOBLOCK 0
#define OS_MESG_BLOCK 1
typedef s32 OSPri;
typedef s32 OSId;
typedef u64 OSTime;
#define OS_EVENT_SW1 0 /* CPU SW1 interrupt */
#define OS_EVENT_SW2 1 /* CPU SW2 interrupt */
#define OS_EVENT_CART 2 /* Cartridge interrupt: used by rmon */
#define OS_EVENT_COUNTER 3 /* Counter int: used by VI/Timer Mgr */
#define OS_EVENT_SP 4 /* SP task done interrupt */
#define OS_EVENT_SI 5 /* SI (controller) interrupt */
#define OS_EVENT_AI 6 /* AI interrupt */
#define OS_EVENT_VI 7 /* VI interrupt: used by VI/Timer Mgr */
#define OS_EVENT_PI 8 /* PI interrupt: used by PI Manager */
#define OS_EVENT_DP 9 /* DP full sync interrupt */
#define OS_EVENT_CPU_BREAK 10 /* CPU breakpoint: used by rmon */
#define OS_EVENT_SP_BREAK 11 /* SP breakpoint: used by rmon */
#define OS_EVENT_FAULT 12 /* CPU fault event: used by rmon */
#define OS_EVENT_THREADSTATUS 13 /* CPU thread status: used by rmon */
#define OS_EVENT_PRENMI 14 /* Pre NMI interrupt */
#define M_GFXTASK 1
#define M_AUDTASK 2
#define M_VIDTASK 3
#define M_NJPEGTASK 4
/////////////
// Structs //
/////////////
// Threads
typedef struct UltraThreadContext UltraThreadContext;
typedef enum {
STOPPED,
QUEUED,
RUNNING,
BLOCKED
} OSThreadState;
typedef struct OSThread_t {
PTR(struct OSThread_t) next; // Next thread in the given queue
OSPri priority;
PTR(PTR(struct OSThread_t)) queue; // Queue this thread is in, if any
uint32_t pad2;
uint16_t flags; // These two are swapped to reflect rdram byteswapping
uint16_t state;
OSId id;
int32_t pad3;
UltraThreadContext* context; // An actual pointer regardless of platform
int32_t sp;
} OSThread;
typedef u32 OSEvent;
typedef PTR(void) OSMesg;
typedef struct OSMesgQueue {
PTR(OSThread) blocked_on_recv; /* Linked list of threads blocked on receiving from this queue */
PTR(OSThread) blocked_on_send; /* Linked list of threads blocked on sending to this queue */
s32 validCount; /* Number of messages in the queue */
s32 first; /* Index of the first message in the ring buffer */
s32 msgCount; /* Size of message buffer */
PTR(OSMesg) msg; /* Pointer to circular buffer to store messages */
} OSMesgQueue;
// RSP
typedef struct {
u32 type;
u32 flags;
PTR(u64) ucode_boot;
u32 ucode_boot_size;
PTR(u64) ucode;
u32 ucode_size;
PTR(u64) ucode_data;
u32 ucode_data_size;
PTR(u64) dram_stack;
u32 dram_stack_size;
PTR(u64) output_buff;
PTR(u64) output_buff_size;
PTR(u64) data_ptr;
u32 data_size;
PTR(u64) yield_data_ptr;
u32 yield_data_size;
} OSTask_s;
typedef union {
OSTask_s t;
int64_t force_structure_alignment;
} OSTask;
// PI
struct OSIoMesgHdr {
// These 3 reversed due to endianness
u8 status; /* Return status */
u8 pri; /* Message priority (High or Normal) */
u16 type; /* Message type */
PTR(OSMesgQueue) retQueue; /* Return message queue to notify I/O completion */
};
struct OSIoMesg {
OSIoMesgHdr hdr; /* Message header */
PTR(void) dramAddr; /* RDRAM buffer address (DMA) */
u32 devAddr; /* Device buffer address (DMA) */
u32 size; /* DMA transfer size in bytes */
u32 piHandle; /* PI device handle */
};
struct OSPiHandle {
PTR(OSPiHandle_s) unused; /* point to next handle on the table */
// These four members reversed due to endianness
u8 relDuration; /* domain release duration */
u8 pageSize; /* domain page size */
u8 latency; /* domain latency */
u8 type; /* DEVICE_TYPE_BULK for disk */
// These three members reversed due to endianness
u16 padding; /* struct alignment padding */
u8 domain; /* which domain */
u8 pulse; /* domain pulse width */
u32 baseAddress; /* Domain address */
u32 speed; /* for roms only */
/* The following are "private" elements" */
u32 transferInfo[18]; /* for disk only */
};
typedef struct {
u32 ctrl;
u32 width;
u32 burst;
u32 vSync;
u32 hSync;
u32 leap;
u32 hStart;
u32 xScale;
u32 vCurrent;
} OSViCommonRegs;
typedef struct {
u32 origin;
u32 yScale;
u32 vStart;
u32 vBurst;
u32 vIntr;
} OSViFieldRegs;
typedef struct {
u8 padding[3];
u8 type;
OSViCommonRegs comRegs;
OSViFieldRegs fldRegs[2];
} OSViMode;
///////////////
// Functions //
///////////////
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
void osInitialize(void);
typedef void (thread_func_t)(PTR(void));
void osCreateThread(RDRAM_ARG PTR(OSThread) t, OSId id, PTR(thread_func_t) entry, PTR(void) arg, PTR(void) sp, OSPri p);
void osStartThread(RDRAM_ARG PTR(OSThread) t);
void osStopThread(RDRAM_ARG PTR(OSThread) t);
void osDestroyThread(RDRAM_ARG PTR(OSThread) t);
void osYieldThread(RDRAM_ARG1);
void osSetThreadPri(RDRAM_ARG PTR(OSThread) t, OSPri pri);
OSPri osGetThreadPri(RDRAM_ARG PTR(OSThread) thread);
OSId osGetThreadId(RDRAM_ARG PTR(OSThread) t);
void osCreateMesgQueue(RDRAM_ARG PTR(OSMesgQueue), PTR(OSMesg), s32);
s32 osSendMesg(RDRAM_ARG PTR(OSMesgQueue), OSMesg, s32);
s32 osJamMesg(RDRAM_ARG PTR(OSMesgQueue), OSMesg, s32);
s32 osRecvMesg(RDRAM_ARG PTR(OSMesgQueue), PTR(OSMesg), s32);
void osSetEventMesg(RDRAM_ARG OSEvent, PTR(OSMesgQueue), OSMesg);
void osViSetEvent(RDRAM_ARG PTR(OSMesgQueue), OSMesg, u32);
void osViSwapBuffer(RDRAM_ARG PTR(void) frameBufPtr);
void osViSetMode(RDRAM_ARG PTR(OSViMode));
void osViSetSpecialFeatures(uint32_t func);
void osViBlack(uint8_t active);
void osViSetXScale(float scale);
void osViSetYScale(float scale);
PTR(void) osViGetNextFramebuffer();
PTR(void) osViGetCurrentFramebuffer();
u32 osGetCount();
OSTime osGetTime();
int osSetTimer(RDRAM_ARG PTR(OSTimer) timer, OSTime countdown, OSTime interval, PTR(OSMesgQueue) mq, OSMesg msg);
int osStopTimer(RDRAM_ARG PTR(OSTimer) timer);
u32 osVirtualToPhysical(PTR(void) addr);
#ifdef __cplusplus
} // extern "C"
#endif
#endif

View File

@ -1,14 +0,0 @@
#include "ultra64.h"
#include "ultramodern.hpp"
void ultramodern::preinit(RDRAM_ARG ultramodern::WindowHandle window_handle) {
ultramodern::set_main_thread();
ultramodern::init_events(PASS_RDRAM window_handle);
ultramodern::init_timers(PASS_RDRAM1);
ultramodern::init_audio();
ultramodern::init_saving(PASS_RDRAM1);
ultramodern::init_thread_cleanup();
}
extern "C" void osInitialize() {
}

View File

@ -1,166 +0,0 @@
#ifndef __ultramodern_HPP__
#define __ultramodern_HPP__
#include <thread>
#include <cassert>
#include <stdexcept>
#include <span>
#undef MOODYCAMEL_DELETE_FUNCTION
#define MOODYCAMEL_DELETE_FUNCTION = delete
#include "lightweightsemaphore.h"
#include "ultra64.h"
#if defined(_WIN32)
# define WIN32_LEAN_AND_MEAN
# include <Windows.h>
#elif defined(__ANDROID__)
# include "android/native_window.h"
#elif defined(__linux__)
# include "X11/Xlib.h"
# undef None
# undef Status
# undef LockMask
# undef Always
# undef Success
#endif
struct UltraThreadContext {
std::thread host_thread;
moodycamel::LightweightSemaphore running;
moodycamel::LightweightSemaphore initialized;
};
namespace ultramodern {
#if defined(_WIN32)
// Native HWND handle to the target window.
struct WindowHandle {
HWND window;
DWORD thread_id = (DWORD)-1;
auto operator<=>(const WindowHandle&) const = default;
};
#elif defined(__ANDROID__)
using WindowHandle = ANativeWindow*;
#elif defined(__linux__)
struct WindowHandle {
Display* display;
Window window;
auto operator<=>(const WindowHandle&) const = default;
};
#endif
// We need a place in rdram to hold the PI handles, so pick an address in extended rdram
constexpr uint32_t rdram_size = 1024 * 1024 * 16; // 16MB to give extra room for anything custom
constexpr int32_t cart_handle = 0x80800000;
constexpr int32_t drive_handle = (int32_t)(cart_handle + sizeof(OSPiHandle));
constexpr int32_t flash_handle = (int32_t)(drive_handle + sizeof(OSPiHandle));
constexpr uint32_t save_size = 1024 * 1024 / 8; // Maximum save size, 1Mbit for flash
// Initialization.
void preinit(RDRAM_ARG WindowHandle window_handle);
void init_saving(RDRAM_ARG1);
void init_events(RDRAM_ARG WindowHandle window_handle);
void init_timers(RDRAM_ARG1);
void init_thread_cleanup();
// Thread queues.
constexpr PTR(PTR(OSThread)) running_queue = (PTR(PTR(OSThread)))-1;
void thread_queue_insert(RDRAM_ARG PTR(PTR(OSThread)) queue, PTR(OSThread) toadd);
PTR(OSThread) thread_queue_pop(RDRAM_ARG PTR(PTR(OSThread)) queue);
bool thread_queue_remove(RDRAM_ARG PTR(PTR(OSThread)) queue_, PTR(OSThread) t_);
bool thread_queue_empty(RDRAM_ARG PTR(PTR(OSThread)) queue);
PTR(OSThread) thread_queue_peek(RDRAM_ARG PTR(PTR(OSThread)) queue);
// Message queues.
void wait_for_external_message(RDRAM_ARG1);
// Thread scheduling.
void check_running_queue(RDRAM_ARG1);
void run_next_thread_and_wait(RDRAM_ARG1);
void resume_thread_and_wait(RDRAM_ARG OSThread* t);
void schedule_running_thread(RDRAM_ARG PTR(OSThread) t);
void cleanup_thread(UltraThreadContext* thread_context);
uint32_t permanent_thread_count();
uint32_t temporary_thread_count();
struct thread_terminated : std::exception {};
enum class ThreadPriority {
Low,
Normal,
High,
VeryHigh,
Critical
};
void set_native_thread_name(const std::string& name);
void set_native_thread_priority(ThreadPriority pri);
PTR(OSThread) this_thread();
void set_main_thread();
bool is_game_thread();
void submit_rsp_task(RDRAM_ARG PTR(OSTask) task);
void send_si_message(RDRAM_ARG1);
uint32_t get_speed_multiplier();
// Time
std::chrono::high_resolution_clock::time_point get_start();
std::chrono::high_resolution_clock::duration time_since_start();
void measure_input_latency();
void sleep_milliseconds(uint32_t millis);
void sleep_until(const std::chrono::high_resolution_clock::time_point& time_point);
// Graphics
uint32_t get_target_framerate(uint32_t original);
uint32_t get_display_refresh_rate();
float get_resolution_scale();
void load_shader_cache(std::span<const char> cache_data);
// Audio
void init_audio();
void set_audio_frequency(uint32_t freq);
void queue_audio_buffer(RDRAM_ARG PTR(s16) audio_data, uint32_t byte_count);
uint32_t get_remaining_audio_bytes();
struct audio_callbacks_t {
using queue_samples_t = void(int16_t*, size_t);
using get_samples_remaining_t = size_t();
using set_frequency_t = void(uint32_t);
queue_samples_t* queue_samples;
get_samples_remaining_t* get_frames_remaining;
set_frequency_t* set_frequency;
};
// Input
struct input_callbacks_t {
using poll_input_t = void(void);
using get_input_t = void(uint16_t*, float*, float*);
using set_rumble_t = void(bool);
poll_input_t* poll_input;
get_input_t* get_input;
set_rumble_t* set_rumble;
};
struct gfx_callbacks_t {
using gfx_data_t = void*;
using create_gfx_t = gfx_data_t();
using create_window_t = WindowHandle(gfx_data_t);
using update_gfx_t = void(gfx_data_t);
create_gfx_t* create_gfx;
create_window_t* create_window;
update_gfx_t* update_gfx;
};
bool is_game_started();
void quit();
void join_event_threads();
void join_thread_cleaner_thread();
void join_saving_thread();
} // namespace ultramodern
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define debug_printf(...)
//#define debug_printf(...) printf(__VA_ARGS__);
#endif