347 lines
11 KiB
C++
347 lines
11 KiB
C++
#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();
|
|
}
|