mirror of
https://github.com/N64Recomp/N64ModernRuntime.git
synced 2025-12-17 13:32:44 +00:00
* Implement osViRepeatLine * Address feedback * Include helpers header * Address feedback
570 lines
20 KiB
C++
570 lines
20 KiB
C++
#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 "ultramodern/ultra64.h"
|
|
#include "ultramodern/ultramodern.hpp"
|
|
|
|
#include "ultramodern/rsp.hpp"
|
|
#include "ultramodern/renderer_context.hpp"
|
|
|
|
static ultramodern::events::callbacks_t events_callbacks{};
|
|
|
|
void ultramodern::events::set_callbacks(const ultramodern::events::callbacks_t& callbacks) {
|
|
events_callbacks = callbacks;
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (events_callbacks.vi_callback != nullptr) {
|
|
events_callbacks.vi_callback();
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (!ultramodern::rsp::run_task(PASS_RDRAM task)) {
|
|
fprintf(stderr, "Failed to execute task type: %" PRIu32 "\n", task->t.type);
|
|
ULTRAMODERN_QUICK_EXIT();
|
|
}
|
|
|
|
// Tell the game that the RSP has completed
|
|
sp_complete();
|
|
}
|
|
}
|
|
|
|
std::atomic_uint32_t display_refresh_rate = 60;
|
|
std::atomic<float> resolution_scale = 1.0f;
|
|
|
|
uint32_t ultramodern::get_target_framerate(uint32_t original) {
|
|
auto& config = ultramodern::renderer::get_graphics_config();
|
|
|
|
switch (config.rr_option) {
|
|
case ultramodern::renderer::RefreshRate::Original:
|
|
default:
|
|
return original;
|
|
case ultramodern::renderer::RefreshRate::Manual:
|
|
return config.rr_manual_value;
|
|
case ultramodern::renderer::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});
|
|
}
|
|
|
|
void ultramodern::trigger_config_action() {
|
|
events_context.action_queue.enqueue(UpdateConfigAction{});
|
|
}
|
|
|
|
std::atomic<ultramodern::renderer::SetupResult> renderer_setup_result = ultramodern::renderer::SetupResult::Success;
|
|
|
|
void gfx_thread_func(uint8_t* rdram, moodycamel::LightweightSemaphore* thread_ready, ultramodern::renderer::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);
|
|
|
|
auto old_config = ultramodern::renderer::get_graphics_config();
|
|
|
|
auto renderer_context = ultramodern::renderer::create_render_context(rdram, window_handle, ultramodern::renderer::get_graphics_config().developer_mode);
|
|
|
|
if (!renderer_context->valid()) {
|
|
renderer_setup_result.store(renderer_context->get_setup_result());
|
|
// Notify the caller thread that this thread is ready.
|
|
thread_ready->signal();
|
|
return;
|
|
}
|
|
|
|
if (events_callbacks.gfx_init_callback != nullptr) {
|
|
events_callbacks.gfx_init_callback();
|
|
}
|
|
|
|
ultramodern::rsp::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) {
|
|
renderer_context->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 renderer_start = std::chrono::high_resolution_clock::now();
|
|
renderer_context->send_dl(&task_action->task);
|
|
auto renderer_end = std::chrono::high_resolution_clock::now();
|
|
dp_complete();
|
|
// printf("Renderer ProcessDList time: %d us\n", static_cast<u32>(std::chrono::duration_cast<std::chrono::microseconds>(renderer_end - renderer_start).count()));
|
|
}
|
|
else if (const auto* swap_action = std::get_if<SwapBuffersAction>(&action)) {
|
|
events_context.vi.current_buffer = events_context.vi.next_buffer;
|
|
renderer_context->update_screen(swap_action->origin);
|
|
display_refresh_rate = renderer_context->get_display_framerate();
|
|
resolution_scale = renderer_context->get_resolution_scale();
|
|
}
|
|
else if (const auto* config_action = std::get_if<UpdateConfigAction>(&action)) {
|
|
auto new_config = ultramodern::renderer::get_graphics_config();
|
|
if (renderer_context->update_config(old_config, new_config)) {
|
|
old_config = new_config;
|
|
}
|
|
}
|
|
else if (const auto* load_shader_cache_action = std::get_if<LoadShaderCacheAction>(&action)) {
|
|
renderer_context->load_shader_cache(load_shader_cache_action->data);
|
|
}
|
|
}
|
|
}
|
|
|
|
renderer_context->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;
|
|
|
|
#define VI_STATE_BLACK 0x20
|
|
#define VI_STATE_REPEATLINE 0x40
|
|
|
|
uint32_t hstart = 0;
|
|
uint32_t vi_origin_offset = 320 * sizeof(uint16_t);
|
|
static uint16_t vi_state = 0;
|
|
|
|
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) {
|
|
VI_H_START_REG = hstart;
|
|
if (vi_state & VI_STATE_BLACK) {
|
|
VI_H_START_REG = 0;
|
|
}
|
|
|
|
if (vi_state & VI_STATE_REPEATLINE) {
|
|
VI_Y_SCALE_REG = 0;
|
|
VI_ORIGIN_REG = osVirtualToPhysical(frameBufPtr);
|
|
}
|
|
|
|
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) {
|
|
if (active) {
|
|
vi_state |= VI_STATE_BLACK;
|
|
} else {
|
|
vi_state &= ~VI_STATE_BLACK;
|
|
}
|
|
}
|
|
|
|
extern "C" void osViRepeatLine(uint8_t active) {
|
|
if (active) {
|
|
vi_state |= VI_STATE_REPEATLINE;
|
|
} else {
|
|
vi_state &= ~VI_STATE_REPEATLINE;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void ultramodern::init_events(RDRAM_ARG ultramodern::renderer::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::renderer::SetupResult setup_result = renderer_setup_result.load();
|
|
if (renderer_setup_result != ultramodern::renderer::SetupResult::Success) {
|
|
auto show_renderer_error = [](const std::string& msg) {
|
|
std::string error_msg = "An error has been encountered on startup: " + msg;
|
|
|
|
ultramodern::error_handling::message_box(error_msg.c_str());
|
|
};
|
|
|
|
const std::string driver_os_suffix = "\nPlease make sure your GPU drivers and your OS are up to date.";
|
|
switch (renderer_setup_result) {
|
|
case ultramodern::renderer::SetupResult::Success:
|
|
break;
|
|
case ultramodern::renderer::SetupResult::DynamicLibrariesNotFound:
|
|
show_renderer_error("Failed to load dynamic libraries. Make sure the DLLs are next to the recomp executable.");
|
|
break;
|
|
case ultramodern::renderer::SetupResult::InvalidGraphicsAPI:
|
|
show_renderer_error(ultramodern::renderer::get_graphics_api_name(ultramodern::renderer::get_graphics_config()) + " is not supported on this platform. Please select a different graphics API.");
|
|
break;
|
|
case ultramodern::renderer::SetupResult::GraphicsAPINotFound:
|
|
show_renderer_error("Unable to initialize " + ultramodern::renderer::get_graphics_api_name(ultramodern::renderer::get_graphics_config()) + "." + driver_os_suffix);
|
|
break;
|
|
case ultramodern::renderer::SetupResult::GraphicsDeviceNotFound:
|
|
show_renderer_error("Unable to find compatible graphics device." + driver_os_suffix);
|
|
break;
|
|
}
|
|
throw std::runtime_error("Failed to initialize the renderer");
|
|
}
|
|
|
|
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();
|
|
}
|