// OSThread.cpp - PC implementation of GameCube OSThread API // Maps GameCube cooperative threading to native OS threads via std::thread. // The OSThread struct layout is preserved so game code can read its fields. // A side-table stores the native std::thread and synchronization primitives. #include #include #include #include #include #include #include #include #include #include #include #include "JSystem/JKernel/JKRHeap.h" #include "common/TracySystem.hpp" #include "dusk/main.h" #include "dusk/os.h" #if _WIN32 #define WIN32_LEAN_AND_MEAN 1 #include #elif __APPLE__ #include #endif // ============================================================================ // Side-table: native thread data per OSThread // ============================================================================ struct PCThreadData { std::thread nativeThread; std::mutex mtx; std::condition_variable cv; void* (*func)(void*); void* param; bool started = false; bool suspended = false; ~PCThreadData() { if (dusk::IsShuttingDown && nativeThread.joinable()) { // Don't care about threads if we're shutting down. nativeThread.detach(); } } }; // Lazy-initialized to avoid DLL static init crashes (used before DllMain completes) static std::mutex& GetThreadDataMutex() { static std::mutex mtx; return mtx; } static std::unordered_map>& GetThreadDataMap() { static std::unordered_map> map; return map; } static PCThreadData* GetThreadData(OSThread* thread) { std::lock_guard mapLock(GetThreadDataMutex()); auto it = GetThreadDataMap().find(thread); if (it != GetThreadDataMap().end()) { return it->second.get(); } return nullptr; } // Side-table for OSThreadQueue -> condition_variable (for OSSleepThread/OSWakeupThread) static std::mutex& GetQueueCvMutex() { static std::mutex mtx; return mtx; } static std::unordered_map>& GetQueueCvMap() { static std::unordered_map> map; return map; } static std::condition_variable& GetQueueCV(OSThreadQueue* queue) { std::lock_guard lock(GetQueueCvMutex()); auto& map = GetQueueCvMap(); auto it = map.find(queue); if (it == map.end()) { auto result = map.emplace(queue, std::make_unique()); return *result.first->second; } return *it->second; } // ============================================================================ // Thread-local current thread pointer // ============================================================================ static thread_local OSThread* tls_currentThread = nullptr; // ============================================================================ // Global state // ============================================================================ static OSThread sDefaultThread; static u8 sDefaultStack[64 * 1024]; static u8 sDefaultStackEnd = OS_THREAD_STACK_MAGIC; // Global interrupt mutex (coarse-grained lock replacing interrupt disable) // Lazy-initialized to avoid DLL static init crashes static std::recursive_mutex& GetInterruptMutex() { static std::recursive_mutex mtx; return mtx; } static thread_local int sInterruptLockCount = 0; // Scheduler suspend count static std::atomic sSchedulerSuspendCount{0}; // Active thread count static std::atomic sActiveThreadCount{0}; // Switch thread callback static OSSwitchThreadCallback sSwitchThreadCallback = nullptr; // ============================================================================ // Internal helpers // ============================================================================ // Thread entry wrapper - runs on the new std::thread static void ThreadEntryWrapper(OSThread* thread, PCThreadData* data) { // Set thread-local pointer tls_currentThread = thread; // Set context pointers for this thread OSClearContext(&thread->context); OSSetCurrentContext(&thread->context); thread->state = OS_THREAD_STATE_RUNNING; // Call the actual thread function void* result = data->func(data->param); // Thread returned - equivalent to OSExitThread thread->val = result; thread->state = OS_THREAD_STATE_MORIBUND; } // ============================================================================ // C API functions // ============================================================================ extern "C" { void __OSThreadInit(void) { memset(&sDefaultThread, 0, sizeof(OSThread)); sDefaultThread.state = OS_THREAD_STATE_RUNNING; sDefaultThread.attr = OS_THREAD_ATTR_DETACH; sDefaultThread.priority = 16; sDefaultThread.base = 16; sDefaultThread.suspend = 0; sDefaultThread.val = (void*)(intptr_t)-1; sDefaultThread.mutex = nullptr; sDefaultThread.queue = nullptr; OSInitThreadQueue(&sDefaultThread.queueJoin); sDefaultThread.queueMutex.head = sDefaultThread.queueMutex.tail = nullptr; sDefaultThread.link.next = sDefaultThread.link.prev = nullptr; sDefaultThread.linkActive.next = sDefaultThread.linkActive.prev = nullptr; // Stack pointers (JKRThread reads these) sDefaultThread.stackBase = sDefaultStack + sizeof(sDefaultStack); sDefaultThread.stackEnd = &sDefaultStackEnd; sDefaultStackEnd = OS_THREAD_STACK_MAGIC; OSClearContext(&sDefaultThread.context); sDefaultThread.error = 0; sDefaultThread.specific[0] = nullptr; sDefaultThread.specific[1] = nullptr; // Set as current thread for main thread tls_currentThread = &sDefaultThread; // Active queue sActiveThreadCount = 1; OSReport("[PC-OSThread] Thread system initialized (multi-threaded mode)\n"); } // ============================================================================ // Thread Queue // ============================================================================ void OSInitThreadQueue(OSThreadQueue* queue) { if (queue) { queue->head = queue->tail = nullptr; } } // ============================================================================ // Current Thread // ============================================================================ OSThread* OSGetCurrentThread(void) { // Lazy-init for main thread if __OSThreadInit hasn't been called yet if (tls_currentThread == nullptr) { __OSThreadInit(); } return tls_currentThread; } // ============================================================================ // Thread Creation // ============================================================================ int OSCreateThread(OSThread* thread, void* (*func)(void*), void* param, void* stack, u32 stackSize, OSPriority priority, u16 attr) { if (!thread) return 0; if (priority < OS_PRIORITY_MIN || priority > OS_PRIORITY_MAX) return 0; // Ensure thread system is initialized OSGetCurrentThread(); memset(thread, 0, sizeof(OSThread)); thread->state = OS_THREAD_STATE_READY; thread->attr = attr & 1u; thread->base = priority; thread->priority = priority; thread->suspend = 1; // Created suspended (GC behavior) thread->val = (void*)(intptr_t)-1; thread->mutex = nullptr; OSInitThreadQueue(&thread->queueJoin); thread->queueMutex.head = thread->queueMutex.tail = nullptr; thread->link.next = thread->link.prev = nullptr; thread->linkActive.next = thread->linkActive.prev = nullptr; // Stack (stack points to TOP on GameCube) thread->stackBase = (u8*)stack; thread->stackEnd = (u8*)((uintptr_t)stack - stackSize); *thread->stackEnd = OS_THREAD_STACK_MAGIC; OSClearContext(&thread->context); thread->error = 0; thread->specific[0] = nullptr; thread->specific[1] = nullptr; // Create side-table entry (but don't start the thread yet) { auto data = std::make_unique(); data->func = func; data->param = param; std::lock_guard lock(GetThreadDataMutex()); GetThreadDataMap()[thread] = std::move(data); } // Add to active queue sActiveThreadCount++; OSReport("[PC-OSThread] Created thread %p (priority=%d, stackSize=%u)\n", thread, priority, stackSize); return 1; } // ============================================================================ // Resume / Suspend // ============================================================================ /* s32 OSResumeThread(OSThread* thread) { if (!thread) return 0; s32 prevSuspend = thread->suspend; if (thread->suspend > 0) { thread->suspend--; } if (thread->suspend == 0) { std::lock_guard lock(GetThreadDataMutex()); auto it = GetThreadDataMap().find(thread); if (it != GetThreadDataMap().end()) { PCThreadData* data = it->second.get(); if (!data->started) { // First resume: launch the native thread data->started = true; data->suspended = false; data->nativeThread = std::thread(ThreadEntryWrapper, thread, data); data->nativeThread.detach(); OSReport("[PC-OSThread] Started thread %p\n", thread); } else if (data->suspended) { // Resume from suspension: signal the CV data->suspended = false; data->cv.notify_one(); } } } return prevSuspend; } s32 OSSuspendThread(OSThread* thread) { if (!thread) return 0; s32 prevSuspend = thread->suspend; thread->suspend++; if (prevSuspend == 0) { std::lock_guard lock(GetThreadDataMutex()); auto it = GetThreadDataMap().find(thread); if (it != GetThreadDataMap().end()) { PCThreadData* data = it->second.get(); if (data->started) { data->suspended = true; // The thread must check its suspended flag and wait } } } return prevSuspend; } */ // ============================================================================ // Resume / Suspend // ============================================================================ s32 OSResumeThread(OSThread* thread) { if (!thread) return 0; s32 prevSuspend = thread->suspend; if (thread->suspend > 0) { thread->suspend--; } // Only wake up if suspend count drops to 0 if (thread->suspend == 0) { PCThreadData* data = GetThreadData(thread); if (data) { // Lock the specific thread mutex to safely modify state and notify std::unique_lock threadLock(data->mtx); if (!data->started) { // First resume: launch the native thread data->started = true; data->suspended = false; // Unlock before launching to avoid potential deadlocks in thread initialization threadLock.unlock(); data->nativeThread = std::thread(ThreadEntryWrapper, thread, data); OSReport("[PC-OSThread] Started thread %p\n", thread); } else { // Resume from suspension: signal the condition variable // IMPORTANT: Set suspended to false BEFORE notifying to pass the wait predicate data->suspended = false; data->cv.notify_all(); } } } return prevSuspend; } s32 OSSuspendThread(OSThread* thread) { if (!thread) return 0; s32 prevSuspend = thread->suspend; thread->suspend++; // If transitioning from running (0) to suspended (1) if (prevSuspend == 0) { PCThreadData* data = GetThreadData(thread); if (data && data->started) { std::unique_lock threadLock(data->mtx); data->suspended = true; // FIX: If the thread is suspending ITSELF, we must block execution here. // This replicates the GameCube behavior where OSSuspendThread yields the CPU // immediately. if (thread == OSGetCurrentThread()) { // Block until 'suspended' becomes false (set by OSResumeThread) // The predicate protects against spurious wakeups. data->cv.wait(threadLock, [data] { return !data->suspended; }); } else { // NOTE: Suspending *other* threads is difficult in C++ std::thread // without cooperative checkpoints or platform-specific hacks. // For now, we only set the flag. The target thread would need to check 'suspended' // periodically. } } } return prevSuspend; } // ============================================================================ // Sleep / Wakeup (thread queue based) // ============================================================================ void OSSleepThread(OSThreadQueue* queue) { if (!queue) return; OSThread* currentThread = OSGetCurrentThread(); if (!currentThread) return; currentThread->state = OS_THREAD_STATE_WAITING; currentThread->queue = queue; // Enqueue into the thread queue OSThread* prev = queue->tail; if (prev == nullptr) { queue->head = currentThread; } else { prev->link.next = currentThread; } currentThread->link.prev = prev; currentThread->link.next = nullptr; queue->tail = currentThread; // Wait on the condition variable for this queue std::condition_variable& cv = GetQueueCV(queue); std::unique_lock lock(GetQueueCvMutex()); cv.wait(lock, [currentThread]() { return currentThread->state != OS_THREAD_STATE_WAITING; }); } void OSWakeupThread(OSThreadQueue* queue) { if (!queue) return; // Wake all threads in the queue OSThread* thread = queue->head; while (thread) { OSThread* next = thread->link.next; thread->state = OS_THREAD_STATE_READY; thread->link.next = nullptr; thread->link.prev = nullptr; thread->queue = nullptr; thread = next; } queue->head = queue->tail = nullptr; // Notify all waiters std::condition_variable& cv = GetQueueCV(queue); cv.notify_all(); } // ============================================================================ // Exit / Cancel / Detach / Join // ============================================================================ void OSExitThread(void* val) { OSThread* currentThread = OSGetCurrentThread(); if (!currentThread) return; currentThread->val = val; if (currentThread->attr & OS_THREAD_ATTR_DETACH) { currentThread->state = 0; } else { currentThread->state = OS_THREAD_STATE_MORIBUND; } // Wake anyone waiting to join OSWakeupThread(¤tThread->queueJoin); sActiveThreadCount--; } void OSCancelThread(OSThread* thread) { CRASH("OSCancelThread not implemented"); if (!thread) return; if (thread->attr & OS_THREAD_ATTR_DETACH) { thread->state = 0; } else { thread->state = OS_THREAD_STATE_MORIBUND; } OSWakeupThread(&thread->queueJoin); sActiveThreadCount--; } void OSDetachThread(OSThread* thread) { CRASH("OSDetachThread not implemented"); if (!thread) return; thread->attr |= OS_THREAD_ATTR_DETACH; if (thread->state == OS_THREAD_STATE_MORIBUND) { thread->state = 0; } OSWakeupThread(&thread->queueJoin); } BOOL OSJoinThread(OSThread* thread, void** val) { if (!thread) return 0; if (!(thread->attr & OS_THREAD_ATTR_DETACH)) { GetThreadData(thread)->nativeThread.join(); } if (thread->state == OS_THREAD_STATE_MORIBUND) { if (val) { *(s32*)val = (s32)(intptr_t)thread->val; } thread->state = 0; return 1; } return 0; } // ============================================================================ // Yield / Terminated / Active // ============================================================================ void OSYieldThread(void) { std::this_thread::yield(); } BOOL OSIsThreadSuspended(OSThread* thread) { return (thread && thread->suspend > 0) ? TRUE : FALSE; } BOOL OSIsThreadTerminated(OSThread* thread) { if (!thread) return TRUE; return (thread->state == OS_THREAD_STATE_MORIBUND || thread->state == 0) ? TRUE : FALSE; } s32 OSCheckActiveThreads(void) { return sActiveThreadCount.load(); } // ============================================================================ // Priority // ============================================================================ int OSSetThreadPriority(OSThread* thread, OSPriority priority) { if (!thread) return 0; if (priority < OS_PRIORITY_MIN || priority > OS_PRIORITY_MAX) return 0; thread->base = priority; thread->priority = priority; return 1; } s32 OSGetThreadPriority(OSThread* thread) { if (!thread) return 16; return thread->base; } // ============================================================================ // Switch Thread Callback // ============================================================================ OSSwitchThreadCallback OSSetSwitchThreadCallback(OSSwitchThreadCallback callback) { OSSwitchThreadCallback prev = sSwitchThreadCallback; sSwitchThreadCallback = callback; return prev; } // ============================================================================ // Scheduler (atomic counter, no real effect with native OS threads) // ============================================================================ s32 OSDisableScheduler(void) { return sSchedulerSuspendCount.fetch_add(1); } s32 OSEnableScheduler(void) { return sSchedulerSuspendCount.fetch_sub(1); } // ============================================================================ // Interrupts (global recursive mutex for mutual exclusion) // ============================================================================ BOOL OSDisableInterrupts(void) { return FALSE; } BOOL OSRestoreInterrupts(BOOL level) { return FALSE; } BOOL OSEnableInterrupts(void) { return FALSE; } // ============================================================================ // Idle function (stub on PC) // ============================================================================ OSThread* OSSetIdleFunction(OSIdleFunction idleFunction, void* param, void* stack, u32 stackSize) { return nullptr; } OSThread* OSGetIdleFunction(void) { return nullptr; } // ============================================================================ // Thread-specific storage // ============================================================================ void OSSetThreadSpecific(s32 index, void* ptr) { OSThread* thread = OSGetCurrentThread(); if (thread && index >= 0 && index < OS_THREAD_SPECIFIC_MAX) { thread->specific[index] = ptr; } } void* OSGetThreadSpecific(s32 index) { OSThread* thread = OSGetCurrentThread(); if (thread && index >= 0 && index < OS_THREAD_SPECIFIC_MAX) { return thread->specific[index]; } return nullptr; } // ============================================================================ // Clear stack (minimal implementation) // ============================================================================ void OSClearStack(u8 val) { // On PC we don't clear the stack - it's managed by the OS } // ============================================================================ // Internal functions used by OSMutex // ============================================================================ s32 __OSGetEffectivePriority(OSThread* thread) { // On PC with native threads, priority inversion handling is simplified. // Just return the base priority. return thread ? thread->base : 16; } void __OSPromoteThread(OSThread* thread, s32 priority) { // Simplified: no real priority inheritance on PC if (thread && priority < thread->priority) { thread->priority = priority; } } void __OSReschedule(void) { // With native OS threads, rescheduling is handled by the OS. // Nothing to do here. } // ============================================================================ // Interrupt handler registration (stub) // ============================================================================ __OSInterruptHandler __OSSetInterruptHandler(__OSInterrupt interrupt, __OSInterruptHandler handler) { return nullptr; } OSInterruptMask __OSUnmaskInterrupts(OSInterruptMask mask) { return 0; } void OSSetCurrentThreadName(const char* name) { // "Why is this current thread only?", you might ask? // Because macOS requires that. For some reason. #if TRACY_ENABLE tracy::SetThreadName(name); #else #if _WIN32 wchar_t buffer[256]; const auto converted = MultiByteToWideChar( CP_UTF8, 0, name, -1, buffer, sizeof(buffer)/sizeof(wchar_t)); if (converted == 0) { CRASH("OSSetThreadName: MultiByteToWideChar failed"); } const auto result = SetThreadDescription(GetCurrentThread(), buffer); if (!SUCCEEDED(result)) { CRASH("OSSetThreadName: SetThreadDescription failed"); } #elif __APPLE__ pthread_setname_np(name); #endif #endif } #ifdef __cplusplus } #endif