2023-10-23 19:32:30 +00:00
# include <cstdio>
# include <cassert>
# include <unordered_map>
2023-10-28 19:46:14 +00:00
# include <vector>
2023-12-24 04:57:22 +00:00
# include <array>
2023-10-28 19:46:14 +00:00
# include <filesystem>
2023-12-24 01:23:35 +00:00
# include <numeric>
2023-12-24 04:57:22 +00:00
# include <stdexcept>
2023-10-23 19:32:30 +00:00
2024-04-06 22:04:55 +00:00
# include "nfd.h"
2023-11-13 00:40:02 +00:00
# include "../../ultramodern/ultra64.h"
# include "../../ultramodern/ultramodern.hpp"
2023-10-23 19:32:30 +00:00
# define SDL_MAIN_HANDLED
# ifdef _WIN32
# include "SDL.h"
# else
# include "SDL2/SDL.h"
# include "SDL2/SDL_syswm.h"
# endif
2023-11-05 19:34:20 +00:00
# include "recomp_ui.h"
2023-11-24 22:10:21 +00:00
# include "recomp_input.h"
2024-01-21 19:04:56 +00:00
# include "recomp_config.h"
2024-03-04 02:00:49 +00:00
# include "recomp_game.h"
2023-11-05 19:34:20 +00:00
2023-10-23 19:32:30 +00:00
# ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <Windows.h>
# include "SDL_syswm.h"
# endif
2024-04-23 01:21:52 +00:00
# define STB_IMAGE_IMPLEMENTATION
# include "../../lib/rt64/src/contrib/stb/stb_image.h"
2023-10-23 19:32:30 +00:00
extern " C " void init ( ) ;
2023-11-13 00:40:02 +00:00
/*extern "C"*/ void start ( ultramodern : : WindowHandle window_handle , const ultramodern : : audio_callbacks_t * audio_callbacks , const ultramodern : : input_callbacks_t * input_callbacks ) ;
2023-10-23 19:32:30 +00:00
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 ) ;
}
2023-11-13 00:40:02 +00:00
ultramodern : : gfx_callbacks_t : : gfx_data_t create_gfx ( ) {
2024-02-23 04:52:04 +00:00
SDL_SetHint ( SDL_HINT_WINDOWS_DPI_AWARENESS , " permonitorv2 " ) ;
2024-01-23 04:08:59 +00:00
SDL_SetHint ( SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS , " 0 " ) ;
2024-03-21 06:46:08 +00:00
SDL_SetHint ( SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE , " 1 " ) ;
SDL_SetHint ( SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE , " 1 " ) ;
2024-03-27 17:07:03 +00:00
SDL_SetHint ( SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH , " 1 " ) ;
2024-04-16 14:43:35 +00:00
SDL_SetHint ( SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS , " 1 " ) ;
2023-10-23 19:32:30 +00:00
if ( SDL_Init ( SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER ) > 0 ) {
exit_error ( " Failed to initialize SDL2: %s \n " , SDL_GetError ( ) ) ;
}
return { } ;
}
2024-04-23 01:21:52 +00:00
# if defined(__linux__)
bool SetImageAsIcon ( const char * filename , SDL_Window * window )
{
// Read data
int width , height , bytesPerPixel ;
void * data = stbi_load ( filename , & 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 ;
if ( data ! = NULL ) {
surface = SDL_CreateRGBSurfaceFrom ( data , width , height , 32 , pitch , Rmask , Gmask ,
Bmask , Amask ) ;
}
if ( surface = = NULL ) {
if ( data ! = NULL ) {
stbi_image_free ( data ) ;
}
return false ;
} else {
SDL_SetWindowIcon ( window , surface ) ;
SDL_FreeSurface ( surface ) ;
stbi_image_free ( data ) ;
return true ;
}
}
# endif
2023-11-02 05:20:12 +00:00
SDL_Window * window ;
2023-11-13 00:40:02 +00:00
ultramodern : : WindowHandle create_window ( ultramodern : : gfx_callbacks_t : : gfx_data_t ) {
2024-03-01 23:42:05 +00:00
window = SDL_CreateWindow ( " Zelda 64: Recompiled " , SDL_WINDOWPOS_CENTERED , SDL_WINDOWPOS_CENTERED , 1600 , 960 , SDL_WINDOW_RESIZABLE ) ;
2024-04-23 01:21:52 +00:00
# if defined(__linux__)
SetImageAsIcon ( " icons/512.png " , window ) ;
2024-04-25 07:05:43 +00:00
if ( ultramodern : : get_graphics_config ( ) . wm_option = = ultramodern : : WindowMode : : Fullscreen ) { // TODO: Remove once RT64 gets native fullscreen support on Linux
SDL_SetWindowFullscreen ( window , SDL_WINDOW_FULLSCREEN_DESKTOP ) ;
} else {
SDL_SetWindowFullscreen ( window , 0 ) ;
}
2024-04-23 01:21:52 +00:00
# endif
2023-10-23 19:32:30 +00:00
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)
2023-11-13 00:40:02 +00:00
return ultramodern : : WindowHandle { wmInfo . info . win . window , GetCurrentThreadId ( ) } ;
2023-10-23 19:32:30 +00:00
# elif defined(__ANDROID__)
static_assert ( false & & " Unimplemented " ) ;
# elif defined(__linux__)
2023-11-13 00:40:02 +00:00
return ultramodern : : WindowHandle { wmInfo . info . x11 . display , wmInfo . info . x11 . window } ;
2023-10-23 19:32:30 +00:00
# else
static_assert ( false & & " Unimplemented " ) ;
# endif
}
void update_gfx ( void * ) {
2023-11-24 22:10:21 +00:00
recomp : : handle_events ( ) ;
2023-10-23 19:32:30 +00:00
}
2023-12-24 01:23:35 +00:00
static SDL_AudioCVT audio_convert ;
2023-10-23 19:32:30 +00:00
static SDL_AudioDeviceID audio_device = 0 ;
2023-12-24 01:23:35 +00:00
// Samples per channel per second.
2023-10-23 19:32:30 +00:00
static uint32_t sample_rate = 48000 ;
2023-12-24 01:23:35 +00:00
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.
2023-12-24 04:57:22 +00:00
// Number of frames to duplicate for fixing interpolation at the start and end of a chunk.
constexpr uint32_t duplicated_input_frames = 4 ;
2023-12-24 01:23:35 +00:00
// The number of output frames to skip for playback (to avoid playing duplicate inputs twice).
static uint32_t discarded_output_frames ;
2023-10-23 19:32:30 +00:00
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 ;
2023-12-24 04:57:22 +00:00
static std : : array < float , duplicated_input_frames * input_channels > duplicated_sample_buffer ;
2023-10-23 19:32:30 +00:00
2023-12-24 04:57:22 +00:00
// 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 ) ;
2023-12-24 01:23:35 +00:00
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 ] ;
2023-10-23 19:32:30 +00:00
}
// 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.
2023-12-24 01:23:35 +00:00
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 ) ;
swap_buffer [ i + 1 + duplicated_input_frames * input_channels ] = audio_data [ i + 0 ] * ( 0.5f / 32768.0f ) ;
2023-10-23 19:32:30 +00:00
}
2023-12-24 01:23:35 +00:00
2023-12-24 04:57:22 +00:00
// TODO handle cases where a chunk is smaller than the duplicated frame count.
2023-12-24 01:23:35 +00:00
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.
2023-12-24 04:57:22 +00:00
for ( size_t i = 0 ; i < duplicated_input_frames * input_channels ; i + + ) {
2023-12-24 01:23:35 +00:00
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 ] ) ;
2023-12-24 04:57:22 +00:00
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 " ) ;
}
2023-10-23 19:32:30 +00:00
// Queue the swapped audio data.
2023-12-24 04:57:22 +00:00
// 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 , swap_buffer . data ( ) + output_channels * discarded_output_frames / 2 ,
audio_convert . len_cvt - output_channels * discarded_output_frames * sizeof ( swap_buffer [ 0 ] ) ) ;
2023-10-23 19:32:30 +00:00
}
2023-12-24 01:23:35 +00:00
constexpr uint32_t bytes_per_frame = input_channels * sizeof ( float ) ;
2023-10-23 19:32:30 +00:00
size_t get_frames_remaining ( ) {
constexpr float buffer_offset_frames = 1.0f ;
// Get the number of remaining buffered audio bytes.
uint32_t buffered_byte_count = SDL_GetQueuedAudioSize ( audio_device ) ;
2023-12-24 01:23:35 +00:00
// 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 ;
2023-10-23 19:32:30 +00:00
// 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 buffered_byte_count / bytes_per_frame ;
}
2023-12-24 01:23:35 +00:00
void update_audio_converter ( ) {
2023-12-24 04:57:22 +00:00
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 ;
2023-12-24 01:23:35 +00:00
}
2023-10-23 19:32:30 +00:00
void set_frequency ( uint32_t freq ) {
2023-12-24 01:23:35 +00:00
sample_rate = freq ;
update_audio_converter ( ) ;
}
void reset_audio ( uint32_t output_freq ) {
2023-10-23 19:32:30 +00:00
SDL_AudioSpec spec_desired {
2023-12-24 01:23:35 +00:00
. freq = ( int ) output_freq ,
2023-10-23 19:32:30 +00:00
. format = AUDIO_F32 ,
2023-12-24 01:23:35 +00:00
. channels = ( Uint8 ) output_channels ,
2023-10-23 19:32:30 +00:00
. silence = 0 , // calculated
. samples = 0x100 , // Fairly small sample count to reduce the latency of internal buffering
. padding = 0 , // unused
. size = 0 , // calculated
2023-12-24 05:10:23 +00:00
. callback = nullptr ,
2023-10-23 19:32:30 +00:00
. userdata = nullptr
} ;
2023-12-24 01:23:35 +00:00
2023-10-23 19:32:30 +00:00
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 ) ;
2023-12-24 01:23:35 +00:00
output_sample_rate = output_freq ;
update_audio_converter ( ) ;
2023-10-23 19:32:30 +00:00
}
int main ( int argc , char * * argv ) {
# ifdef _WIN32
// Set up console output to accept UTF-8 on windows
SetConsoleOutputCP ( CP_UTF8 ) ;
2024-04-06 22:04:55 +00:00
// Initialize native file dialogs.
NFD_Init ( ) ;
2023-10-23 19:32:30 +00:00
// 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
2024-03-04 02:00:49 +00:00
//printf("Current dir: %ls\n", std::filesystem::current_path().c_str());
2023-10-28 19:46:14 +00:00
2023-12-24 05:10:23 +00:00
// Initialize SDL audio and set the output frequency.
2023-10-23 19:32:30 +00:00
SDL_InitSubSystem ( SDL_INIT_AUDIO ) ;
2023-12-24 01:23:35 +00:00
reset_audio ( 48000 ) ;
2023-10-23 19:32:30 +00:00
2024-01-21 19:04:56 +00:00
recomp : : load_config ( ) ;
2023-10-23 19:32:30 +00:00
2023-11-13 00:40:02 +00:00
ultramodern : : gfx_callbacks_t gfx_callbacks {
2023-10-23 19:32:30 +00:00
. create_gfx = create_gfx ,
. create_window = create_window ,
. update_gfx = update_gfx ,
} ;
2023-11-13 00:40:02 +00:00
ultramodern : : audio_callbacks_t audio_callbacks {
2023-10-23 19:32:30 +00:00
. queue_samples = queue_samples ,
. get_frames_remaining = get_frames_remaining ,
. set_frequency = set_frequency ,
} ;
2023-11-13 00:40:02 +00:00
ultramodern : : input_callbacks_t input_callbacks {
2023-12-13 07:06:56 +00:00
. poll_input = recomp : : poll_inputs ,
2023-11-24 22:10:21 +00:00
. get_input = recomp : : get_n64_input ,
2024-03-04 07:13:12 +00:00
. set_rumble = recomp : : set_rumble ,
2023-10-23 19:32:30 +00:00
} ;
2024-03-04 02:00:49 +00:00
recomp : : start ( { } , audio_callbacks , input_callbacks , gfx_callbacks ) ;
2024-04-06 22:04:55 +00:00
NFD_Quit ( ) ;
2023-10-23 19:32:30 +00:00
return EXIT_SUCCESS ;
}