| #include <chrono> | |
| #include <iostream> | |
| #include <random> | |
| #include "SDL.h" | |
| #include "File.hpp" | |
| #include "System.hpp" | |
| #include "Input.hpp" | |
| #include "32blit.hpp" | |
| #include "UserCode.hpp" | |
| #include "JPEG.hpp" | |
| #include "Multiplayer.hpp" | |
| #include "engine/api_private.hpp" | |
| extern Input *blit_input; | |
| // blit audio channels | |
| static blit::AudioChannel channels[CHANNEL_COUNT]; | |
| int System::width = System::max_width; | |
| int System::height = System::max_height; | |
| // blit framebuffer memory | |
| static uint8_t framebuffer[System::max_width * System::max_height * 3]; | |
| static blit::Pen palette[256]; | |
| // blit debug callback | |
| void blit_debug(const char *message) { | |
| std::cout << message; | |
| } | |
| // blit screenmode callback | |
| blit::ScreenMode _mode = blit::ScreenMode::lores; | |
| static blit::ScreenMode requested_mode = blit::ScreenMode::lores; | |
| static blit::PixelFormat cur_format = blit::PixelFormat::RGB; | |
| static blit::PixelFormat requested_format = blit::PixelFormat::RGB; | |
| blit::SurfaceInfo cur_surf_info; | |
| static void set_screen_palette(const blit::Pen *colours, int num_cols) { | |
| memcpy(palette, colours, num_cols * sizeof(blit::Pen)); | |
| } | |
| static bool set_screen_mode_format(blit::ScreenMode new_mode, blit::SurfaceTemplate &new_surf_template) { | |
| new_surf_template.data = framebuffer; | |
| if(new_surf_template.format == (blit::PixelFormat)-1) | |
| new_surf_template.format = blit::PixelFormat::RGB; | |
| blit::Size default_bounds(System::width, System::height); | |
| if(new_surf_template.bounds.empty()) | |
| new_surf_template.bounds = default_bounds; | |
| switch(new_mode) { | |
| case blit::ScreenMode::lores: | |
| new_surf_template.bounds /= 2; | |
| break; | |
| case blit::ScreenMode::hires: | |
| case blit::ScreenMode::hires_palette: | |
| break; | |
| } | |
| if(new_surf_template.bounds != default_bounds && new_surf_template.bounds != default_bounds / 2) | |
| return false; | |
| switch(new_surf_template.format) { | |
| case blit::PixelFormat::RGB: | |
| case blit::PixelFormat::RGB565: | |
| break; | |
| case blit::PixelFormat::P: | |
| new_surf_template.palette = palette; | |
| break; | |
| default: | |
| return false; | |
| } | |
| requested_mode = new_mode; | |
| requested_format = new_surf_template.format; | |
| return true; | |
| } | |
| blit::SurfaceInfo &set_screen_mode(blit::ScreenMode new_mode) { | |
| blit::SurfaceTemplate temp{nullptr, {0, 0}, new_mode == blit::ScreenMode::hires_palette ? blit::PixelFormat::P : blit::PixelFormat::RGB}; | |
| // won't fail for the modes used here | |
| set_screen_mode_format(new_mode, temp); | |
| cur_surf_info.data = temp.data; | |
| cur_surf_info.bounds = temp.bounds; | |
| cur_surf_info.format = temp.format; | |
| cur_surf_info.palette = temp.palette; | |
| return cur_surf_info; | |
| } | |
| // blit timer callback | |
| std::chrono::steady_clock::time_point start; | |
| uint32_t now() { | |
| auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start); | |
| return (uint32_t)elapsed.count(); | |
| } | |
| // blit random callback | |
| #ifdef __MINGW32__ | |
| // Windows/MinGW doesn't support a non-deterministic source of randomness, so we fall back upon the age-old time seed once more | |
| // Without this, random_device() will always return the same number and thus our mersenne twister will always produce the same sequence. | |
| std::mt19937 random_generator(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count()); | |
| #else | |
| std::random_device random_device; | |
| std::mt19937 random_generator(random_device()); | |
| #endif | |
| std::uniform_int_distribution<uint32_t> random_distribution; | |
| uint32_t blit_random() { | |
| return random_distribution(random_generator); | |
| } | |
| // us timer used by profiler | |
| void enable_us_timer() | |
| { | |
| // Enable/initialise timer | |
| } | |
| uint32_t get_us_timer() | |
| { | |
| // get current time in us | |
| uint64_t ticksPerUs = SDL_GetPerformanceFrequency() / 1000000; | |
| return SDL_GetPerformanceCounter() / ticksPerUs; | |
| } | |
| uint32_t get_max_us_timer() | |
| { | |
| // largest us value timer can produce for wrapping | |
| return UINT32_MAX; | |
| } | |
| /* Added a command line ability to specify a launch_path parameter. */ | |
| extern const char *launch_path; | |
| static const char *get_launch_path() { | |
| return launch_path; | |
| } | |
| static blit::GameMetadata get_metadata() { | |
| blit::GameMetadata ret; | |
| ret.title = metadata_title; | |
| ret.author = metadata_author; | |
| ret.description = metadata_description; | |
| ret.version = metadata_version; | |
| ret.url = metadata_url; | |
| ret.category = metadata_category; | |
| return ret; | |
| } | |
| extern Multiplayer *blit_multiplayer; | |
| bool blit_is_multiplayer_connected() { | |
| return blit_multiplayer->is_connected(); | |
| } | |
| void blit_set_multiplayer_enabled(bool enabled) { | |
| blit_multiplayer->set_enabled(enabled); | |
| } | |
| void blit_send_message(const uint8_t *data, uint16_t length) { | |
| blit_multiplayer->send_message(data, length); | |
| } | |
| // blit API | |
| static const blit::APIConst blit_api_const { | |
| blit::api_version_major, blit::api_version_minor, | |
| channels, | |
| ::set_screen_mode, | |
| ::set_screen_palette, | |
| ::now, | |
| ::blit_random, | |
| nullptr, // exit | |
| ::blit_debug, | |
| ::open_file, | |
| ::read_file, | |
| ::write_file, | |
| ::close_file, | |
| ::get_file_length, | |
| ::list_files, | |
| ::file_exists, | |
| ::directory_exists, | |
| ::create_directory, | |
| ::rename_file, | |
| ::remove_file, | |
| ::get_save_path, | |
| ::is_storage_available, | |
| ::enable_us_timer, | |
| ::get_us_timer, | |
| ::get_max_us_timer, | |
| blit_decode_jpeg_buffer, | |
| blit_decode_jpeg_file, | |
| nullptr, // launch | |
| nullptr, // erase_game | |
| nullptr, // get_type_handler_metadata | |
| ::get_launch_path, | |
| blit_is_multiplayer_connected, | |
| blit_set_multiplayer_enabled, | |
| blit_send_message, | |
| nullptr, // flash_to_tmp | |
| nullptr, // tmp_file_closed | |
| ::get_metadata, | |
| ::set_screen_mode_format, | |
| nullptr, // i2c_send | |
| nullptr, // i2c_recieve | |
| nullptr, // set_raw_cdc_enabled | |
| nullptr, // cdc_write | |
| nullptr, // cdc_read | |
| nullptr, // list_installed_games | |
| nullptr, // can_launch | |
| nullptr, // get_screen_data | |
| nullptr, // set_framebuffer | |
| }; | |
| static blit::APIData blit_api_data; | |
| namespace blit { | |
| const APIConst &api = blit_api_const; | |
| APIData &api_data = blit_api_data; | |
| } | |
| // SDL events | |
| const Uint32 System::timer_event = SDL_RegisterEvents(2); | |
| const Uint32 System::loop_event = System::timer_event + 1; | |
| #ifndef __EMSCRIPTEN__ | |
| // Thread bouncers | |
| static int system_timer_thread(void *ptr) { | |
| // Bounce back in to the class. | |
| System *sys = (System *)ptr; | |
| return sys->timer_thread(); | |
| } | |
| static int system_loop_thread(void *ptr) { | |
| // Bounce back in to the class. | |
| System *sys = (System *)ptr; | |
| return sys->update_thread(); | |
| } | |
| #endif | |
| System::System() { | |
| m_input = SDL_CreateMutex(); | |
| s_timer_stop = SDL_CreateSemaphore(0); | |
| s_loop_update = SDL_CreateSemaphore(0); | |
| s_loop_redraw = SDL_CreateSemaphore(0); | |
| s_loop_ended = SDL_CreateSemaphore(0); | |
| } | |
| System::~System() { | |
| SDL_DestroyMutex(m_input); | |
| SDL_DestroySemaphore(s_timer_stop); | |
| SDL_DestroySemaphore(s_loop_update); | |
| SDL_DestroySemaphore(s_loop_redraw); | |
| SDL_DestroySemaphore(s_loop_ended); | |
| } | |
| void System::run() { | |
| running = true; | |
| start = std::chrono::steady_clock::now(); | |
| blit::update = ::update; | |
| blit::render = ::render; | |
| setup_base_path(); | |
| blit::set_screen_mode(blit::lores); | |
| #ifdef __EMSCRIPTEN__ | |
| ::init(); | |
| #else | |
| t_system_loop = SDL_CreateThread(system_loop_thread, "Loop", (void *)this); | |
| t_system_timer = SDL_CreateThread(system_timer_thread, "Timer", (void *)this); | |
| #endif | |
| } | |
| int System::timer_thread() { | |
| // Signal the system loop every 10 msec. | |
| int dropped = 0; | |
| SDL_Event event = {}; | |
| event.type = timer_event; | |
| while (SDL_SemWaitTimeout(s_timer_stop, 10)) { | |
| if (SDL_SemValue(s_loop_update)) { | |
| dropped++; | |
| if(dropped > 100) { | |
| dropped = 100; | |
| event.user.code = 2; | |
| SDL_PushEvent(&event); | |
| } else { | |
| event.user.code = 1; | |
| SDL_PushEvent(&event); | |
| } | |
| } else { | |
| SDL_SemPost(s_loop_update); | |
| dropped = 0; | |
| event.user.code = 0; | |
| SDL_PushEvent(&event); | |
| } | |
| } | |
| return 0; | |
| } | |
| int System::update_thread() { | |
| // Run the blit user code once every time we are signalled. | |
| SDL_Event event = {}; | |
| event.type = loop_event; | |
| ::init(); // Run init here because the user can make it hang. | |
| while (true) { | |
| SDL_SemWait(s_loop_update); | |
| if(!running) break; | |
| bool rendered = loop(); | |
| if(!running) break; | |
| if(rendered) { | |
| // present on main thread if we need to | |
| SDL_PushEvent(&event); | |
| SDL_SemWait(s_loop_redraw); | |
| } | |
| } | |
| SDL_SemPost(s_loop_ended); | |
| return 0; | |
| } | |
| bool System::loop() { | |
| SDL_LockMutex(m_input); | |
| blit::buttons = shadow_buttons; | |
| blit::tilt.x = shadow_tilt[0]; | |
| blit::tilt.y = shadow_tilt[1]; | |
| blit::tilt.z = shadow_tilt[2]; | |
| blit::tilt.normalize(); | |
| blit::joystick.x = shadow_joystick[0]; | |
| blit::joystick.y = shadow_joystick[1]; | |
| SDL_UnlockMutex(m_input); | |
| bool rendered = false; | |
| // only render at 50Hz (main loop runs at 100Hz) | |
| // however, the emscripten loop (usually) runs at the display refresh rate | |
| auto time_now = ::now(); | |
| #ifndef __EMSCRIPTEN__ | |
| if(time_now - last_render_time >= 20) | |
| #endif | |
| { | |
| blit::render(time_now); | |
| last_render_time = time_now; | |
| if(_mode != requested_mode || cur_format != requested_format) { | |
| _mode = requested_mode; | |
| cur_format = requested_format; | |
| } | |
| rendered = true; | |
| } | |
| blit::tick(::now()); | |
| blit_input->rumble_controllers(blit::vibration); | |
| blit_multiplayer->update(); | |
| return rendered; | |
| } | |
| Uint32 System::mode() { | |
| return _mode; | |
| } | |
| Uint32 System::format() { | |
| return Uint32(cur_format); | |
| } | |
| void System::update_texture(SDL_Texture *texture) { | |
| bool is_lores = _mode == blit::ScreenMode::lores; | |
| SDL_Rect dest_rect{0, 0, is_lores ? width / 2 : width, is_lores ? height / 2 : height}; | |
| auto stride = dest_rect.w * blit::pixel_format_stride[int(cur_format)]; | |
| if(cur_format == blit::PixelFormat::P) { | |
| uint8_t col_fb[max_width * max_height * 3]; | |
| auto in = framebuffer, out = col_fb; | |
| auto size = dest_rect.w * dest_rect.h; | |
| for(int i = 0; i < size; i++) { | |
| uint8_t index = *(in++); | |
| (*out++) = palette[index].r; | |
| (*out++) = palette[index].g; | |
| (*out++) = palette[index].b; | |
| } | |
| SDL_UpdateTexture(texture, &dest_rect, col_fb, stride * 3); | |
| } else | |
| SDL_UpdateTexture(texture, &dest_rect, framebuffer, stride); | |
| } | |
| void System::notify_redraw() { | |
| SDL_SemPost(s_loop_redraw); | |
| } | |
| void System::set_joystick(int axis, float value) { | |
| if (axis < 2) { | |
| SDL_LockMutex(m_input); | |
| shadow_joystick[axis] = value; | |
| SDL_UnlockMutex(m_input); | |
| } | |
| } | |
| void System::set_tilt(int axis, float value) { | |
| if (axis < 3) { | |
| SDL_LockMutex(m_input); | |
| shadow_tilt[axis] = value; | |
| SDL_UnlockMutex(m_input); | |
| } | |
| } | |
| void System::set_button(int button, bool state) { | |
| SDL_LockMutex(m_input); | |
| if (state) { | |
| shadow_buttons |= button; | |
| } else { | |
| shadow_buttons &= ~button; | |
| } | |
| SDL_UnlockMutex(m_input); | |
| } | |
| void System::stop() { | |
| int returnValue; | |
| running = false; | |
| // make sure the update thread is not waiting for a render to complete | |
| SDL_SemPost(s_loop_redraw); | |
| if(SDL_SemWaitTimeout(s_loop_ended, 500)) { | |
| std::cerr << "User code appears to have frozen. Detaching thread." << std::endl; | |
| SDL_DetachThread(t_system_loop); | |
| } else { | |
| SDL_WaitThread(t_system_loop, &returnValue); | |
| } | |
| SDL_SemPost(s_timer_stop); | |
| SDL_WaitThread(t_system_timer, &returnValue); | |
| } |