David Chavez 19fcd9bf31
Update to latest N64ModernRuntime (#392)
- updates to new controller changes
- updates to new render context changes
- updates to new controller number changes
- fix for crash on save thread
2024-06-12 09:40:14 +02:00

429 lines
15 KiB
C++

#include <cstdio>
#include <cassert>
#include <unordered_map>
#include <vector>
#include <array>
#include <filesystem>
#include <numeric>
#include <stdexcept>
#include <cinttypes>
#include "nfd.h"
#include "ultramodern/ultra64.h"
#include "ultramodern/ultramodern.hpp"
#define SDL_MAIN_HANDLED
#ifdef _WIN32
#include "SDL.h"
#else
#include "SDL2/SDL.h"
#include "SDL2/SDL_syswm.h"
#endif
#include "recomp_ui.h"
#include "recomp_input.h"
#include "zelda_config.h"
#include "zelda_sound.h"
#include "zelda_render.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
#include <Windows.h>
#include "SDL_syswm.h"
#endif
#define STB_IMAGE_IMPLEMENTATION
#include "../../lib/rt64/src/contrib/stb/stb_image.h"
template<typename... Ts>
void exit_error(const char* str, Ts ...args) {
// TODO pop up an error
((void)fprintf(stderr, str, args), ...);
assert(false);
std::quick_exit(EXIT_FAILURE);
}
ultramodern::gfx_callbacks_t::gfx_data_t create_gfx() {
SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2");
SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
#if defined(__linux__)
SDL_SetHint(SDL_HINT_VIDEODRIVER, "x11");
#endif
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) > 0) {
exit_error("Failed to initialize SDL2: %s\n", SDL_GetError());
}
return {};
}
#if defined(__gnu_linux__)
#include "icon_bytes.h"
bool SetImageAsIcon(const char* filename, SDL_Window* window)
{
// Read data
int width, height, bytesPerPixel;
void* data = stbi_load_from_memory(reinterpret_cast<const uint8_t*>(icon_bytes), sizeof(icon_bytes), &width, &height, &bytesPerPixel, 4);
// Calculate pitch
int pitch;
pitch = width * 4;
pitch = (pitch + 3) & ~3;
// Setup relevance bitmask
int Rmask, Gmask, Bmask, Amask;
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
Rmask = 0x000000FF;
Gmask = 0x0000FF00;
Bmask = 0x00FF0000;
Amask = 0xFF000000;
#else
Rmask = 0xFF000000;
Gmask = 0x00FF0000;
Bmask = 0x0000FF00;
Amask = 0x000000FF;
#endif
SDL_Surface* surface = nullptr;
if (data != nullptr) {
surface = SDL_CreateRGBSurfaceFrom(data, width, height, 32, pitch, Rmask, Gmask,
Bmask, Amask);
}
if (surface == nullptr) {
if (data != nullptr) {
stbi_image_free(data);
}
return false;
} else {
SDL_SetWindowIcon(window,surface);
SDL_FreeSurface(surface);
stbi_image_free(data);
return true;
}
}
#endif
SDL_Window* window;
ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t) {
window = SDL_CreateWindow("Zelda 64: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, SDL_WINDOW_RESIZABLE );
#if defined(__linux__)
SetImageAsIcon("icons/512.png",window);
if (ultramodern::renderer::get_graphics_config().wm_option == ultramodern::renderer::WindowMode::Fullscreen) { // TODO: Remove once RT64 gets native fullscreen support on Linux
SDL_SetWindowFullscreen(window,SDL_WINDOW_FULLSCREEN_DESKTOP);
} else {
SDL_SetWindowFullscreen(window,0);
}
#endif
if (window == nullptr) {
exit_error("Failed to create window: %s\n", SDL_GetError());
}
SDL_SysWMinfo wmInfo;
SDL_VERSION(&wmInfo.version);
SDL_GetWindowWMInfo(window, &wmInfo);
#if defined(_WIN32)
return ultramodern::renderer::WindowHandle{ wmInfo.info.win.window, GetCurrentThreadId() };
#elif defined(__ANDROID__)
static_assert(false && "Unimplemented");
#elif defined(__linux__)
if (wmInfo.subsystem != SDL_SYSWM_X11) {
exit_error("Unsupported SDL2 video driver \"%s\". Only X11 is supported on Linux.\n", SDL_GetCurrentVideoDriver());
}
return ultramodern::renderer::WindowHandle{ wmInfo.info.x11.display, wmInfo.info.x11.window };
#else
static_assert(false && "Unimplemented");
#endif
}
void update_gfx(void*) {
recomp::handle_events();
}
static SDL_AudioCVT audio_convert;
static SDL_AudioDeviceID audio_device = 0;
// Samples per channel per second.
static uint32_t sample_rate = 48000;
static uint32_t output_sample_rate = 48000;
// Channel count.
constexpr uint32_t input_channels = 2;
static uint32_t output_channels = 2;
// Terminology: a frame is a collection of samples for each channel. e.g. 2 input samples is one input frame. This is unrelated to graphical frames.
// Number of frames to duplicate for fixing interpolation at the start and end of a chunk.
constexpr uint32_t duplicated_input_frames = 4;
// The number of output frames to skip for playback (to avoid playing duplicate inputs twice).
static uint32_t discarded_output_frames;
constexpr uint32_t bytes_per_frame = input_channels * sizeof(float);
void queue_samples(int16_t* audio_data, size_t sample_count) {
// Buffer for holding the output of swapping the audio channels. This is reused across
// calls to reduce runtime allocations.
static std::vector<float> swap_buffer;
static std::array<float, duplicated_input_frames * input_channels> duplicated_sample_buffer;
// Make sure the swap buffer is large enough to hold the audio data, including any extra space needed for resampling.
size_t resampled_sample_count = sample_count + duplicated_input_frames * input_channels;
size_t max_sample_count = std::max(resampled_sample_count, resampled_sample_count * audio_convert.len_mult);
if (max_sample_count > swap_buffer.size()) {
swap_buffer.resize(max_sample_count);
}
// Copy the duplicated frames from last chunk into this chunk
for (size_t i = 0; i < duplicated_input_frames * input_channels; i++) {
swap_buffer[i] = duplicated_sample_buffer[i];
}
// 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 = 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;
}
// TODO handle cases where a chunk is smaller than the duplicated frame count.
assert(sample_count > duplicated_input_frames * input_channels);
// Copy the last converted samples into the duplicated sample buffer to reuse in resampling the next queued chunk.
for (size_t i = 0; i < duplicated_input_frames * input_channels; i++) {
duplicated_sample_buffer[i] = swap_buffer[i + sample_count];
}
audio_convert.buf = reinterpret_cast<Uint8*>(swap_buffer.data());
audio_convert.len = (sample_count + duplicated_input_frames * input_channels) * sizeof(swap_buffer[0]);
int ret = SDL_ConvertAudio(&audio_convert);
if (ret < 0) {
printf("Error using SDL audio converter: %s\n", SDL_GetError());
throw std::runtime_error("Error using SDL audio converter");
}
uint64_t cur_queued_microseconds = uint64_t(SDL_GetQueuedAudioSize(audio_device)) / bytes_per_frame * 1000000 / sample_rate;
uint32_t num_bytes_to_queue = audio_convert.len_cvt - output_channels * discarded_output_frames * sizeof(swap_buffer[0]);
float* samples_to_queue = swap_buffer.data() + output_channels * discarded_output_frames / 2;
// Prevent audio latency from building up by skipping samples in incoming audio when too many samples are already queued.
// Skip samples based on how many microseconds of samples are queued already.
uint32_t skip_factor = cur_queued_microseconds / 100000;
if (skip_factor != 0) {
uint32_t skip_ratio = 1 << skip_factor;
num_bytes_to_queue /= skip_ratio;
for (size_t i = 0; i < num_bytes_to_queue / (output_channels * sizeof(swap_buffer[0])); i++) {
samples_to_queue[2 * i + 0] = samples_to_queue[2 * skip_ratio * i + 0];
samples_to_queue[2 * i + 1] = samples_to_queue[2 * skip_ratio * i + 1];
}
}
// Queue the swapped audio data.
// Offset the data start by only half the discarded frame count as the other half of the discarded frames are at the end of the buffer.
SDL_QueueAudio(audio_device, samples_to_queue, num_bytes_to_queue);
}
size_t get_frames_remaining() {
constexpr float buffer_offset_frames = 1.0f;
// Get the number of remaining buffered audio bytes.
uint64_t buffered_byte_count = SDL_GetQueuedAudioSize(audio_device);
// Scale the byte count based on the ratio of sample rates and channel counts.
buffered_byte_count = buffered_byte_count * 2 * sample_rate / output_sample_rate / output_channels;
// 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 frames_per_vi = (sample_rate / 60);
if (buffered_byte_count > (buffer_offset_frames * bytes_per_frame * frames_per_vi)) {
buffered_byte_count -= (buffer_offset_frames * bytes_per_frame * frames_per_vi);
}
else {
buffered_byte_count = 0;
}
// Convert from byte count to sample count.
return static_cast<uint32_t>(buffered_byte_count / bytes_per_frame);
}
void update_audio_converter() {
int ret = SDL_BuildAudioCVT(&audio_convert, AUDIO_F32, input_channels, sample_rate, AUDIO_F32, output_channels, output_sample_rate);
if (ret < 0) {
printf("Error creating SDL audio converter: %s\n", SDL_GetError());
throw std::runtime_error("Error creating SDL audio converter");
}
// Calculate the number of samples to discard based on the sample rate ratio and the duplicate frame count.
discarded_output_frames = duplicated_input_frames * output_sample_rate / sample_rate;
}
void set_frequency(uint32_t freq) {
sample_rate = freq;
update_audio_converter();
}
void reset_audio(uint32_t output_freq) {
SDL_AudioSpec spec_desired{
.freq = (int)output_freq,
.format = AUDIO_F32,
.channels = (Uint8)output_channels,
.silence = 0, // calculated
.samples = 0x100, // Fairly small sample count to reduce the latency of internal buffering
.padding = 0, // unused
.size = 0, // calculated
.callback = nullptr,
.userdata = nullptr
};
audio_device = SDL_OpenAudioDevice(nullptr, false, &spec_desired, nullptr, 0);
if (audio_device == 0) {
exit_error("SDL error opening audio device: %s\n", SDL_GetError());
}
SDL_PauseAudioDevice(audio_device, 0);
output_sample_rate = 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
// Set up console output to accept UTF-8 on windows
SetConsoleOutputCP(CP_UTF8);
// Initialize native file dialogs.
NFD_Init();
// Change to a font that supports Japanese characters
CONSOLE_FONT_INFOEX cfi;
cfi.cbSize = sizeof cfi;
cfi.nFont = 0;
cfi.dwFontSize.X = 0;
cfi.dwFontSize.Y = 16;
cfi.FontFamily = FF_DONTCARE;
cfi.FontWeight = FW_NORMAL;
wcscpy_s(cfi.FaceName, L"NSimSun");
SetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi);
#endif
#ifdef _WIN32
// Force wasapi on Windows, as there seems to be some issue with sample queueing with directsound currently.
SDL_setenv("SDL_AUDIODRIVER", "wasapi", true);
#endif
//printf("Current dir: %ls\n", std::filesystem::current_path().c_str());
// Initialize SDL audio and set the output frequency.
SDL_InitSubSystem(SDL_INIT_AUDIO);
reset_audio(48000);
// 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::renderer::callbacks_t renderer_callbacks{
.create_render_context = zelda64::renderer::create_render_context,
};
ultramodern::gfx_callbacks_t gfx_callbacks{
.create_gfx = create_gfx,
.create_window = create_window,
.update_gfx = update_gfx,
};
ultramodern::audio_callbacks_t audio_callbacks{
.queue_samples = queue_samples,
.get_frames_remaining = get_frames_remaining,
.set_frequency = set_frequency,
};
ultramodern::input::callbacks_t input_callbacks{
.poll_input = recomp::poll_inputs,
.get_input = recomp::get_n64_input,
.set_rumble = recomp::set_rumble,
.get_connected_device_info = recomp::get_connected_device_info,
};
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, renderer_callbacks, audio_callbacks, input_callbacks, gfx_callbacks, thread_callbacks, error_handling_callbacks);
NFD_Quit();
return EXIT_SUCCESS;
}