// Adapted from MacNativeGames games/totk/runtime/threads.c @ b75be8c (MIT, Copyright (c) 2026 MacNativeGames // contributors; itself from d2r-mobile dm/host/hle_thread.c), d2r-mobile dm/host/thread.c @ 53e7c5b (MIT, Copyright // (c) 2026 jhaizhou) and LAS Recomp runtime/kernel.c @ da35b83 (MIT, Copyright (c) 2026 Peiikai); the preemption of // virtual cores from MacNativeGames threads.c @ 33e8fa8 (MIT, as above). // Threads, synchronization, virtual cores and fibers of the Horizon kernel. // // One guest thread = one Windows thread with a 256 MiB reserved host stack (every guest BL is a host call) and its own // Cpu. The SDK's nn::os runs as lifted code; only the kernel under it is the host. Every wait hangs on one global lock // G with one condition variable per thread; wake order is the kernel's: lower priority value first, then first come. // Semantics follow the public Horizon kernel (KConditionVariable, KAddressArbiter, KSynchronization). #include #include #include #include "cfg.h" #include "host.h" #include "kernel.h" #include "platform/platform.h" #define R_OK 0u #define R_TIMEOUT KERN_R_TIMEOUT #define R_CANCELLED KERN_R_CANCELLED #define R_BAD_HANDLE KERN_R_BAD_HANDLE #define R_BAD_STATE KERN_R_BAD_STATE #define R_INVALID_ENUM KERN_R_INVALID_ENUM #define WAITERS_BIT KERN_WAITERS_BIT #define H_CUR_THREAD KERN_H_CURRENT_THREAD #define MAX_H 4096 #define NCORE 3 #define HOST_STACK (256ull << 20) // reserved, committed on demand #define TLR_SIZE 0x200 #define THREAD_OBJ_SIZE 0x400 // "current thread object" at TLR+0x1F8 (d2r-mobile thread.c) #define TLS_SIZE 0x2000 #define MAIN_STACK (1ull << 20) // main.npdm META+0x1C #define MAIN_PRIO 44 // META+0xE #define MAIN_CORE 0 // META+0xF #define FIBER_MAX 1024 enum { O_FREE, O_THREAD, O_EVENT_R, O_EVENT_W, O_SESSION = KERN_SESSION, O_TMEM = KERN_TMEM }; _Static_assert(O_EVENT_W < O_SESSION, "handle kinds"); enum { W_NONE = KERN_WAIT_NONE, W_LOCK = KERN_WAIT_LOCK, W_CONDVAR = KERN_WAIT_CONDVAR, W_ADDRESS = KERN_WAIT_ADDRESS, W_SYNC = KERN_WAIT_SYNC }; // Scheduler accounting states (TOTK_TRACE=sched). enum { S_OFF, S_RUN, S_CORE, S_HOST, S_SLEEP, S_SYNC, S_CV, S_LOCK, S_ADDR, S_N }; static const char *const s_name[S_N] = {"off", "run", "core", "host", "sleep", "sync", "cv", "lock", "addr"}; typedef struct Event { int signaled, refs; } Event; typedef struct Fiber Fiber; struct RtThread { int preempt; // first member: rt_dispatch reads it (host.h kern_preempt_pending) int host_depth; // second member: host calls and SVCs in progress, while kern_host_track (host.h) uint32_t handle; uint64_t id; Cpu *cpu; // the thread's own context Cpu *cur; // the context running now: cpu, or the current fiber's uint64_t entry, arg, stack_top; int prio, core; uint64_t mask; int started, exited, closed, cancel, is_main; PlatCond cv; // current wait int wkind, woke; uint64_t waddr, wkey; uint32_t wtag, wres; uint64_t wseq; // registration order: equal priority wakes first come first RtThread *next; // wait list RtThread *all; // every thread // virtual core int cur_core; // core held while running guest code, -1 while blocked uint64_t cseq; RtThread *cnext; uint8_t *tlr, *tls; PlatJmp exit_jmp; // the thread start; ExitThread jumps back to it // fibers PlatFiber *base_fiber; // the host thread as a host fiber, after the first switch Fiber *fiber; // current guest fiber, NULL on the thread's own context Fiber *leaving; // fiber just switched away from, released by whoever resumes next int exit_pending; // ExitThread on a fiber: unwind once back on the thread's own context // TOTK_TRACE=sched accounting (holding G) uint32_t tid; // host thread id int st; // S_* state since st_since uint64_t st_since, st_ns[S_N], nblk, npre, pre_since, cpu_last; uint64_t wobj; // what the current wait is on: owner handle, key, address or first handle uint32_t waker; // the thread whose signal ended the last wait (0: a host thread or a timeout) uint64_t wake_at, grant_at; // when a wait was signalled and a core granted, until the thread runs again uint64_t lw_ns, lw_n, lw_max, lg_ns, lg_n, lg_max, sl_n, sl_req, sl_act; // those latencies; SleepThread asked/took uint64_t sp_n, sp_ho, sp_site; // spin wait passes and core handoffs, the last loop head (the thread writes them) RtThread *target; // as a core waiter: the owner core_kick asked to give its core up for it (holding G) // TOTK_ASYNC_PREEMPT (holding G) PlatIntr *hthr; // the host thread, for plat_intr_stop / resume; open while it may own a core uint64_t ap_rip; // the interrupted host rip kern_ap_stub returns to uint64_t ap_last; // the last redirect attempt on this owner int ap_pending; // redirected to kern_ap_stub, which has not reached kern_ap_yield yet // TOTK_CORE_DIAG (holding G, except dg_host_end, which only the thread itself writes) uint64_t dg_since; // as a waiter: core wait start int dg_cls; // as a waiter: DG_* class at the start of the wait uint64_t dg_kick; // as an owner: the preempt request uint64_t dg_host_end; // as an owner: the return from the host call or SVC it was in at the request uint64_t dg_pc, dg_lr; // as an owner: guest pc and lr at the checkpoint that gave the core up int dg_host_kick; // as an owner: in a host call or SVC at the request uint32_t dg_svc; // the thread's latest SVC (TOTK_CORE_DIAG: what an owner in a host call is in) struct DgRel { uint64_t kick, host_end, pc, lr; int host, pre, prio; uint32_t h; } dg_rel; // as a waiter: the // owner that released }; static PlatMutex G = PLAT_MUTEX_INIT; static struct { int type; void *obj; } g_h[MAX_H]; static _Thread_local RtThread *tcur; _Static_assert(offsetof(RtThread, preempt) == 0, "host.h kern_preempt_pending reads the first int"); _Static_assert(offsetof(RtThread, host_depth) == sizeof(int), "host.h kern_host_depth reads the second int"); static RtThread *g_all, *g_wait, *g_main; static uint64_t g_next_id = 0x51, g_wseq, g_created; static uint64_t g_main_stack; static void lock(void) { plat_mutex_lock(&G); } static void unlock(void) { plat_mutex_unlock(&G); } uint64_t kern_now_ns(void) { return plat_now_ns(); } // ---- TOTK_CRIT=- with TOTK_TRACE=sched: every state change and wake of the guest threads and // every present over those frames, written to TOTK_CRIT_OUT (default crit.bin) at the last one, for // tools/crit_path.py, which walks the chain of waits back from each present (holding G). TOTK_CRIT=w- takes // world-clock seconds instead (frame_step_world_us): from the first present at or after lo to the first after hi, so a // session covers the same game time at any frame rate. typedef struct { uint64_t ts, obj; uint32_t a, b; uint16_t kind, st; uint32_t pad; } CritEv; enum { CRIT_STATE, CRIT_WAKE, CRIT_FRAME }; // a: thread; b: its waker (STATE), the waker (WAKE); st: new S_* state #define CRIT_N (1u << 22) // 128 MiB, about 2000 fight frames static CritEv *g_crit; static uint32_t g_crit_n; static int g_crit_on; static uint64_t g_crit_lo, g_crit_hi; // frames; UINT64_MAX in world mode until the world clock fixes them static int64_t g_crit_wlo, g_crit_whi; // world mode: the window in microseconds of world clock, else 0 static void crit_add(int kind, uint32_t a, uint32_t b, int st, uint64_t obj, uint64_t ts) { if (g_crit_n < CRIT_N) g_crit[g_crit_n++] = (CritEv){ts, obj, a, b, (uint16_t)kind, (uint16_t)st, 0}; } // ---- TOTK_TRACE=sched accounting (holding G) #define RW_N 32 static RtThread *g_render; // the thread that presents (sched trace, core diag) typedef struct { int st; uint32_t waker; uint64_t obj, ns; } WaitRec; static WaitRec g_rw[RW_N], g_mw[RW_N]; // the render and main thread's waits by object and waker static uint64_t g_pre_req, g_pre_n, g_pre_ns, g_pre_max, g_pre_stale; // preempt requests; releases after one and their latency static uint64_t g_sync_calls, g_sync_blocks; // WaitSynchronization calls and the blocks inside them static void wait_add(WaitRec *w, int st, uint64_t obj, uint32_t waker, uint64_t ns) { for (int i = 0; i < RW_N; i++) if (!w[i].ns || (w[i].st == st && w[i].obj == obj && w[i].waker == waker)) { w[i].st = st; w[i].obj = obj; w[i].waker = waker; w[i].ns += ns; return; } } // Close the current state's interval and enter st. static void acct(RtThread *t, int st) { if (!(rt_trace & RT_TRACE_SCHED)) return; uint64_t now = kern_now_ns(); if (t->st_since) { t->st_ns[t->st] += now - t->st_since; if (t->st >= S_HOST && (t == g_render || t->is_main)) { uint32_t waker = t->st == S_HOST || t->st == S_SLEEP ? 0 : t->waker; wait_add(t == g_render ? g_rw : g_mw, t->st, t->wobj, waker, now - t->st_since); } } if (g_crit_on) crit_add(CRIT_STATE, t->handle, t->waker, st, t->wobj, now); t->st = st; t->st_since = now; } static int st_of(int kind) { return kind == W_LOCK ? S_LOCK : kind == W_CONDVAR ? S_CV : kind == W_ADDRESS ? S_ADDR : S_SYNC; } // ---- handles (caller holds G) static uint32_t h_new(int type, void *obj) { for (int i = 1; i < MAX_H; i++) if (!g_h[i].type) { g_h[i].type = type; g_h[i].obj = obj; return 0x1000 + i; } rt_fatal("kernel: handle table full (%d handles)", MAX_H); } static void *h_peek(uint32_t h, int type) { if (h == H_CUR_THREAD && type == O_THREAD) return tcur; unsigned i = h - 0x1000; if (i == 0 || i >= MAX_H || g_h[i].type != type) return NULL; return g_h[i].obj; } static void *h_get(uint32_t h, int type) { void *o = h_peek(h, type); if (!o) rt_log("kernel: invalid handle %#x (want kind %d)", h, type); return o; } static RtThread *cur(uint32_t svc) { if (!tcur) rt_fatal("kernel: svc %#x on a host thread (no guest thread)", svc); return tcur; } // ---- wait primitives (holding G) static void wait_unlink(RtThread *t) { for (RtThread **p = &g_wait; *p; p = &(*p)->next) if (*p == t) { *p = t->next; break; } t->next = NULL; } // The waiter to wake in kernel order. Lock waits match by lock address, condvar waits by key, arbiter waits by // address. static RtThread *pick(int kind, uint64_t a) { RtThread *b = NULL; for (RtThread *t = g_wait; t; t = t->next) { if (t->wkind != kind || (kind == W_CONDVAR ? t->wkey : t->waddr) != a) continue; if (!b || t->prio < b->prio || (t->prio == b->prio && t->wseq < b->wseq)) b = t; } return b; } static void wake(RtThread *t, uint32_t res) { wait_unlink(t); t->wkind = W_NONE; t->woke = 1; t->wres = res; t->waker = tcur ? tcur->handle : 0; if (rt_trace & RT_TRACE_SCHED) t->wake_at = kern_now_ns(); if (g_crit_on) crit_add(CRIT_WAKE, t->handle, t->waker, 0, 0, t->wake_at); plat_cond_signal(&t->cv); } static void wake_sync_waiters(void) { for (RtThread *w = g_wait; w;) { RtThread *n = w->next; if (w->wkind == W_SYNC) wake(w, (uint32_t)-1); w = n; } } // SleepConditionVariableSRW counts milliseconds: round up (late is legal, early is not) and re-check the deadline // against the performance counter, because the wait may also end early. static uint32_t block_inner(RtThread *t, int kind, int64_t ns) { t->wkind = kind; t->woke = 0; t->wseq = ++g_wseq; t->next = g_wait; g_wait = t; if (ns == 0) { wait_unlink(t); t->wkind = W_NONE; return R_TIMEOUT; } uint64_t now = kern_now_ns(); int forever = ns < 0 || (uint64_t)ns > UINT64_MAX - now; uint64_t deadline = forever ? 0 : now + (uint64_t)ns; while (!t->woke) { if (forever) { plat_cond_wait(&t->cv, &G); continue; } now = kern_now_ns(); if (now >= deadline) { wait_unlink(t); t->wkind = W_NONE; return R_TIMEOUT; } uint64_t ms = (deadline - now + 999999) / 1000000; plat_cond_wait_ms(&t->cv, &G, ms > 0xFFFFFFF0u ? 0xFFFFFFF0u : (uint32_t)ms); } if (t->wake_at) { uint64_t d = kern_now_ns() - t->wake_at; t->lw_ns += d; t->lw_n++; if (d > t->lw_max) t->lw_max = d; t->wake_at = 0; } return t->wres; } // ---- virtual cores // Horizon runs at most one thread per core, and TotK keys per-core data (allocators, job queues) by the current core // number without locks. A guest thread therefore holds one of the three application cores while it runs guest code // and gives it up only where Horizon would switch threads (waits, sleep, yield, exit, blocking host calls); a free // core goes to the best waiting thread. A waiter better than a holder asks one holder to give its core up (core_kick), // which it does at its next SVC or indirect call (kern_checkpoint, rt_dispatch), as Horizon runs the better thread at // once; guest code without either keeps the core until it reaches one. // // free_cores = 1 (TOTK_FREE_CORES) turns the gate off, for experiments only: the game does not run that way. A thread // that holds no core reports its ideal core to GetCurrentProcessorNumber, so the game's job workers all report core 0 // and run its per-core code at once: on 1.2.1 two of them add the same per-core NVN control memory, which retires the // command handle the other just recorded, and CallCommands finds no recording at start (#212); 1.4.3 corrupts a heap // in main+0x82e00 or hangs at the title menu. The start log and every crash report say so (kern_free_cores_note). static RtThread *g_core_owner[NCORE], *g_core_wait; static int g_cores_on = -1, g_held_max; static void diag_init(void); static void ap_init(void); static int cores_enabled(void) { if (g_cores_on < 0) { g_cores_on = !cfg_bool("TOTK_FREE_CORES"); // "0" keeps the gate, like unset if (!g_cores_on) rt_log("kernel: WARNING: free_cores = 1 (TOTK_FREE_CORES) turns the virtual core gate off. This is an " "experimental mode and the game crashes or hangs in it, usually at start. Set free_cores = 0 in " "totk.ini to play"); diag_init(); ap_init(); } return g_cores_on; } void kern_free_cores_note(void) { if (!g_cores_on) rt_log("fatal: free_cores = 1 was on: the game crashes in this experimental mode; set free_cores = 0 in " "totk.ini before reporting this crash"); } static int core_allowed(const RtThread *t, int c) { return (int)((t->mask >> c) & 1); } // ---- asynchronous preemption (TOTK_ASYNC_PREEMPT, on unless 0) // Adapted from MacNativeGames games/totk/runtime/threads.c @ 33e8fa8 (8bf0893, PR #488; MIT, Copyright (c) 2026 // MacNativeGames contributors): Horizon gives a core to a strictly better thread the moment that thread is ready. // Here a better waiter only marks the owner, which gives the core up at its next SVC or indirect call; an owner in a // stretch of game code with neither keeps it (TOTK_CORE_DIAG: 0.5-1.1 ms of thread-time per frame, waits to 16 ms and // more). With it on, three parts, all of which MacNativeGames found necessary: // - The waiter interrupts the owner (plat_intr_stop) when it has waited AP_FIRST_NS, then every // millisecond: if the owner's host rip is in the lifted modules' code (g_gen_lo..g_gen_hi) and its host depth is 0 // (not in a host call or SVC, nor in game code a host call made), it holds no host lock, so its rip is moved to // kern_ap_stub, which saves every volatile register, gives the core up like a checkpoint (kern_ap_yield) and jumps // back. Otherwise it is only marked, as before. // - Priority inheritance (eff_prio): a thread owning a guest mutex counts at the priority of the best thread blocked on // it, for granting, kicking, preempting and yielding. Without it MacNativeGames' game stopped at its first // "Continue" (a better thread took the core of the low-priority owner of a mutex it then waited for). // - Horizon's preemption disable: the SDK's spin locks (1.2.1 sdk f_70051242a8, f_7005124360 and siblings) count // TLR+0x100 (disable_count) while they try or hold the lock, and after clearing it call SVC 0x36 when TLR+0x102 // (interrupt_flag) is set. Such an owner is not preempted, at a checkpoint either: interrupt_flag is set and the // switch happens at that SVC. Preempting it anyway hung MacNativeGames' game at "Continue" (the lock holder lost its // core to a thread that then spun on that lock). // TOTK_ASYNC_PREEMPT=0 keeps the old scheduling exactly: raw priorities, no interruption, no disable_count; =2 keeps // inheritance and the preemption disable without interrupting owners. #define AP_FIRST_NS 250000ull // the waiter spins this long before the first interruption (most waits end sooner) #define AP_GAP_NS 1000000ull // and interrupts the same owner at most this often static int g_ap = -1, g_ap_int; // g_ap_int: interrupt owners (TOTK_ASYNC_PREEMPT=2: inheritance and disable only) static int g_ap_log; // redirects logged so far (AP_LOG_N) #define AP_LOG_N 40 static uintptr_t g_gen_lo, g_gen_hi; // host code of the lifted modules: first and last function start static uint64_t g_ap_sent, g_ap_taken, g_ap_host, g_ap_pc, g_ap_deferred, g_ap_fail, g_ap_spin_ok; void kern_ap_stub(void); static void ap_init(void) { if (g_ap >= 0) return; const char *e = cfg_value("TOTK_ASYNC_PREEMPT"); g_ap = strcmp(e, "0") != 0; g_ap_int = g_ap && strcmp(e, "2") != 0; if (g_ap) { RtModule m[16]; int n = rt_linked_modules(m, 16); uintptr_t lo = UINTPTR_MAX, hi = 0; for (int i = 0; i < n; i++) for (uint64_t k = 0; k < m[i].count; k++) { uintptr_t f = (uintptr_t)m[i].fns[k]; if (f < lo) lo = f; if (f > hi) hi = f; } const uintptr_t host[] = {(uintptr_t)kern_ap_stub, (uintptr_t)kern_checkpoint, (uintptr_t)rt_svc, (uintptr_t)rt_dispatch, (uintptr_t)rt_call}; int bad = !n || lo >= hi; for (unsigned i = 0; i < sizeof host / sizeof *host; i++) bad |= host[i] >= lo && host[i] <= hi; if (bad) { rt_log("kernel: asynchronous preemption off: the lifted code range %#llx-%#llx holds host code", (unsigned long long)lo, (unsigned long long)hi); g_ap = 0; } else { g_gen_lo = lo; g_gen_hi = hi; kern_host_track = 1; } } rt_log("kernel: asynchronous preemption of virtual cores %s, priority inheritance and preemption disable %s; lifted " "code exe+%#llx-exe+%#llx", g_ap_int ? "on" : "off", g_ap ? "on (TOTK_ASYNC_PREEMPT=0 turns all off)" : "off", (unsigned long long)(g_gen_lo ? g_gen_lo - plat_exe_base() : 0), (unsigned long long)(g_gen_hi ? g_gen_hi - plat_exe_base() : 0)); } // Horizon's priority inheritance: o counts at the priority of the best thread blocked on a guest mutex it owns (the // owner is read from the mutex word, so it follows hand-offs). Holding G. static int eff_prio(const RtThread *o) { int p = o->prio; for (const RtThread *w = g_wait; w; w = w->next) if (w->wkind == W_LOCK && w->prio < p && w->waddr && (__atomic_load_n((const uint32_t *)(uintptr_t)w->waddr, __ATOMIC_RELAXED) & ~WAITERS_BIT) == o->handle) p = w->prio; return p; } // The priority the virtual cores go by: raw with TOTK_ASYNC_PREEMPT=0, as before. static int prio_of(const RtThread *t) { return g_ap > 0 ? eff_prio(t) : t->prio; } // TLR+0x100 disable_count, +0x102 interrupt_flag (any thread may read them; the flag is set from outside as the // kernel does). static volatile uint16_t *ap_tlr(const RtThread *t) { return (volatile uint16_t *)(t->tlr + 0x100); } static int ap_disabled(const RtThread *t) { return g_ap > 0 && ap_tlr(t)[0]; } // kern_ap_stub: entered in place of the interrupted instruction of top-level game code. It keeps every register the // game code may have live (volatile GPRs, flags, YMM0-15, MXCSR; the C calls preserve the rest), gives the core up // in kern_ap_yield and jumps back to kern_ap_rip(), whose value it first stores in the return slot it reserved at // entry, so a stack walk sees the interrupted function as its caller. No ret: the jump keeps call/return pairs // balanced. The waiter never writes the owner's stack. // An x86-64-v2 build (no __AVX__) saves XMM0-15 with SSE moves into the same 32-byte slots instead: its game code // keeps nothing in the upper YMM halves, and the CPUs it is for have no VEX encoding, so a vmovdqa there faults with // an illegal instruction the first time a core is interrupted. #ifdef __AVX__ #define AP_VMOV "vmovdqa" #define AP_V "%ymm" #define AP_VZEROUPPER " vzeroupper\n" #else #define AP_VMOV "movdqa" #define AP_V "%xmm" #define AP_VZEROUPPER "" #endif #ifdef _WIN32 __asm__(".text\n" ".globl kern_ap_stub\n" ".def kern_ap_stub; .scl 2; .type 32; .endef\n" ".p2align 4\n" "kern_ap_stub:\n" ".seh_proc kern_ap_stub\n" " leaq -8(%rsp), %rsp\n" // not sub: the flags are still the interrupted code's until pushfq ".seh_stackalloc 8\n" " pushfq\n" ".seh_stackalloc 8\n" " pushq %rax\n" ".seh_pushreg %rax\n" " pushq %rcx\n" ".seh_pushreg %rcx\n" " pushq %rdx\n" ".seh_pushreg %rdx\n" " pushq %r8\n" ".seh_pushreg %r8\n" " pushq %r9\n" ".seh_pushreg %r9\n" " pushq %r10\n" ".seh_pushreg %r10\n" " pushq %r11\n" ".seh_pushreg %r11\n" " pushq %rbp\n" ".seh_pushreg %rbp\n" " movq %rsp, %rbp\n" ".seh_setframe %rbp, 0\n" ".seh_endprologue\n" " andq $-32, %rsp\n" " subq $576, %rsp\n" // 0: shadow space, 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15 (XMM0-15 without AVX) " " AP_VMOV " " AP_V "0, 64(%rsp)\n" " " AP_VMOV " " AP_V "1, 96(%rsp)\n" " " AP_VMOV " " AP_V "2, 128(%rsp)\n" " " AP_VMOV " " AP_V "3, 160(%rsp)\n" " " AP_VMOV " " AP_V "4, 192(%rsp)\n" " " AP_VMOV " " AP_V "5, 224(%rsp)\n" " " AP_VMOV " " AP_V "6, 256(%rsp)\n" " " AP_VMOV " " AP_V "7, 288(%rsp)\n" " " AP_VMOV " " AP_V "8, 320(%rsp)\n" " " AP_VMOV " " AP_V "9, 352(%rsp)\n" " " AP_VMOV " " AP_V "10, 384(%rsp)\n" " " AP_VMOV " " AP_V "11, 416(%rsp)\n" " " AP_VMOV " " AP_V "12, 448(%rsp)\n" " " AP_VMOV " " AP_V "13, 480(%rsp)\n" " " AP_VMOV " " AP_V "14, 512(%rsp)\n" " " AP_VMOV " " AP_V "15, 544(%rsp)\n" " stmxcsr 32(%rsp)\n" " movl $0x1f80, 36(%rsp)\n" " ldmxcsr 36(%rsp)\n" " cld\n" AP_VZEROUPPER " call kern_ap_rip\n" " movq %rax, 72(%rbp)\n" // the return slot: rbp + 8 pushes + pushfq " call kern_ap_yield\n" " ldmxcsr 32(%rsp)\n" " " AP_VMOV " 64(%rsp), " AP_V "0\n" " " AP_VMOV " 96(%rsp), " AP_V "1\n" " " AP_VMOV " 128(%rsp), " AP_V "2\n" " " AP_VMOV " 160(%rsp), " AP_V "3\n" " " AP_VMOV " 192(%rsp), " AP_V "4\n" " " AP_VMOV " 224(%rsp), " AP_V "5\n" " " AP_VMOV " 256(%rsp), " AP_V "6\n" " " AP_VMOV " 288(%rsp), " AP_V "7\n" " " AP_VMOV " 320(%rsp), " AP_V "8\n" " " AP_VMOV " 352(%rsp), " AP_V "9\n" " " AP_VMOV " 384(%rsp), " AP_V "10\n" " " AP_VMOV " 416(%rsp), " AP_V "11\n" " " AP_VMOV " 448(%rsp), " AP_V "12\n" " " AP_VMOV " 480(%rsp), " AP_V "13\n" " " AP_VMOV " 512(%rsp), " AP_V "14\n" " " AP_VMOV " 544(%rsp), " AP_V "15\n" " movq %rbp, %rsp\n" " popq %rbp\n" " popq %r11\n" " popq %r10\n" " popq %r9\n" " popq %r8\n" " popq %rdx\n" " popq %rcx\n" " popq %rax\n" " popfq\n" " leaq 8(%rsp), %rsp\n" " jmpq *-8(%rsp)\n" ".seh_endproc\n"); #else // The System V version: rsi and rdi are volatile there too, and the interrupted code may keep data in the 128 bytes // below its stack pointer (the red zone), so the return slot and the saves go below that. Without a red zone in the // way the slot is at entry - 136; the exit restores rsp to the entry value and jumps through the slot. __asm__(".text\n" ".globl kern_ap_stub\n" ".type kern_ap_stub, @function\n" ".p2align 4\n" "kern_ap_stub:\n" " .cfi_startproc\n" " leaq -136(%rsp), %rsp\n" // not sub: the flags are still the interrupted code's until pushfq " pushfq\n" " pushq %rax\n" " pushq %rcx\n" " pushq %rdx\n" " pushq %rsi\n" " pushq %rdi\n" " pushq %r8\n" " pushq %r9\n" " pushq %r10\n" " pushq %r11\n" " pushq %rbp\n" " movq %rsp, %rbp\n" " andq $-32, %rsp\n" " subq $576, %rsp\n" // 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15 (XMM0-15 without AVX) " " AP_VMOV " " AP_V "0, 64(%rsp)\n" " " AP_VMOV " " AP_V "1, 96(%rsp)\n" " " AP_VMOV " " AP_V "2, 128(%rsp)\n" " " AP_VMOV " " AP_V "3, 160(%rsp)\n" " " AP_VMOV " " AP_V "4, 192(%rsp)\n" " " AP_VMOV " " AP_V "5, 224(%rsp)\n" " " AP_VMOV " " AP_V "6, 256(%rsp)\n" " " AP_VMOV " " AP_V "7, 288(%rsp)\n" " " AP_VMOV " " AP_V "8, 320(%rsp)\n" " " AP_VMOV " " AP_V "9, 352(%rsp)\n" " " AP_VMOV " " AP_V "10, 384(%rsp)\n" " " AP_VMOV " " AP_V "11, 416(%rsp)\n" " " AP_VMOV " " AP_V "12, 448(%rsp)\n" " " AP_VMOV " " AP_V "13, 480(%rsp)\n" " " AP_VMOV " " AP_V "14, 512(%rsp)\n" " " AP_VMOV " " AP_V "15, 544(%rsp)\n" " stmxcsr 32(%rsp)\n" " movl $0x1f80, 36(%rsp)\n" " ldmxcsr 36(%rsp)\n" " cld\n" AP_VZEROUPPER " call kern_ap_rip\n" " movq %rax, 88(%rbp)\n" // the return slot: rbp + 10 pushes + pushfq " call kern_ap_yield\n" " ldmxcsr 32(%rsp)\n" " " AP_VMOV " 64(%rsp), " AP_V "0\n" " " AP_VMOV " 96(%rsp), " AP_V "1\n" " " AP_VMOV " 128(%rsp), " AP_V "2\n" " " AP_VMOV " 160(%rsp), " AP_V "3\n" " " AP_VMOV " 192(%rsp), " AP_V "4\n" " " AP_VMOV " 224(%rsp), " AP_V "5\n" " " AP_VMOV " 256(%rsp), " AP_V "6\n" " " AP_VMOV " 288(%rsp), " AP_V "7\n" " " AP_VMOV " 320(%rsp), " AP_V "8\n" " " AP_VMOV " 352(%rsp), " AP_V "9\n" " " AP_VMOV " 384(%rsp), " AP_V "10\n" " " AP_VMOV " 416(%rsp), " AP_V "11\n" " " AP_VMOV " 448(%rsp), " AP_V "12\n" " " AP_VMOV " 480(%rsp), " AP_V "13\n" " " AP_VMOV " 512(%rsp), " AP_V "14\n" " " AP_VMOV " 544(%rsp), " AP_V "15\n" " movq %rbp, %rsp\n" " popq %rbp\n" " popq %r11\n" " popq %r10\n" " popq %r9\n" " popq %r8\n" " popq %rdi\n" " popq %rsi\n" " popq %rdx\n" " popq %rcx\n" " popq %rax\n" " popfq\n" " leaq 136(%rsp), %rsp\n" " jmpq *-136(%rsp)\n" " .cfi_endproc\n" ".size kern_ap_stub, .-kern_ap_stub\n"); #endif enum { AP_NONE, AP_RETRY, AP_SENT }; // The waiter w interrupts its target owner if that runs top-level game code (holding G; the owner is stopped only // between plat_intr_stop and plat_intr_resume, during which nothing here takes a lock). static int ap_try(RtThread *w) { RtThread *o = w->target; if (!o || o->cur_core < 0 || !o->preempt) return AP_NONE; if (!o->hthr || o->ap_pending) return AP_RETRY; volatile uint16_t *tl = ap_tlr(o); if (__atomic_load_n(&o->host_depth, __ATOMIC_RELAXED) > 0) { g_ap_host++; return AP_RETRY; } if (tl[0]) { // preemption disabled: SVC 0x36 switches tl[1] = 1; g_ap_deferred++; return AP_RETRY; } uint64_t now = kern_now_ns(); if (o->ap_last && now - o->ap_last < AP_GAP_NS) return AP_RETRY; o->ap_last = now; uint64_t rip, rsp; if (!plat_intr_stop(o->hthr, &rip, &rsp)) { g_ap_fail++; return AP_RETRY; } int r = AP_RETRY; uint64_t to = 0; if (rip < g_gen_lo || rip > g_gen_hi) g_ap_pc++; else if (__atomic_load_n(&o->host_depth, __ATOMIC_RELAXED) > 0) g_ap_host++; else if (tl[0]) { tl[1] = 1; g_ap_deferred++; } else { o->ap_rip = rip; o->ap_pending = 1; to = (uint64_t)(uintptr_t)kern_ap_stub; } if (!plat_intr_resume(o->hthr, to)) { o->ap_pending = 0; g_ap_fail++; } else if (to) { g_ap_sent++; r = AP_SENT; } if (r == AP_SENT && g_ap_log < AP_LOG_N) { g_ap_log++; rt_log("kernel: interrupt %#x p%d at exe+%#llx rsp %#llx for %#x p%d", o->handle, o->prio, (unsigned long long)(o->ap_rip - plat_exe_base()), (unsigned long long)rsp, w->handle, w->prio); } return r; } // ---- TOTK_CORE_DIAG=1: why ready threads wait for a virtual core, and for how long // Each core wait is split into classes: the owner asked to give the core up ran game code (DG_GAME, the part an // asynchronous preemption would remove), was in a host call or SVC that kept the core (DG_HOST), every owner the waiter // may displace was of better or equal priority (DG_EQ: Horizon waits too), or the worse owners were claimed by better // waiters (DG_NOCORE). Per 60 presents: time per frame and a wait-length histogram per class, the owners that cost // most; a line per present over 25 ms; a line per wait with over 2 ms of DG_GAME (where the owner reached its // checkpoint), and one host RIP sample of the owner per such wait (exe RVA, for totk.map). enum { DG_GAME, DG_HOST, DG_EQ, DG_NOCORE, DG_N }; static const char *const dg_name[DG_N] = {"game", "host", "eq", "nocore"}; #define DG_NB 8 // wait lengths: < 0.05, 0.25, 1, 2, 4, 8, 16 ms, longer static const uint64_t dg_edge[DG_NB - 1] = {50000, 250000, 1000000, 2000000, 4000000, 8000000, 16000000}; #define DG_LONG_LINES 400 #define DG_SAMPLES 600 #define DG_OWN 32 int kern_host_track; static int g_diag = -1; typedef struct { uint64_t ns[DG_N], n[DG_N], hist[DG_N][DG_NB]; // this window struct { uint32_t h; int prio, cls; uint64_t ns; } own[DG_OWN]; // this window: owners by class uint64_t long_frames, long_game_frames; } DgWindow; static DgWindow g_dgw; static uint64_t g_dgf[DG_N], g_dgfr[DG_N]; // this frame: all threads, the render thread static uint64_t g_dg_samples, g_dg_lines, g_sync36, g_sync36_flag; static void diag_init(void) { if (g_diag >= 0) return; const char *e = cfg_get("TOTK_CORE_DIAG"); g_diag = e && *e && strcmp(e, "0"); if (g_diag) { kern_host_track = 1; rt_log("core-diag: on (TOTK_CORE_DIAG)"); } } int kern_core_diag_on(void) { return g_diag > 0; } // A tracked host call or SVC returned to depth 0 with a preempt request pending (host.h); on the thread itself. void kern_host_left(void *thread) { RtThread *t = thread; if (g_diag > 0 && t->dg_kick && t->dg_host_kick && !t->dg_host_end) t->dg_host_end = kern_now_ns(); } static int dg_in_host(const RtThread *o) { return __atomic_load_n(&o->host_depth, __ATOMIC_RELAXED) > 0; } static void dg_add(const RtThread *w, int cls, uint64_t ns, uint32_t oh, int oprio) { if (!ns) return; g_dgw.ns[cls] += ns; g_dgf[cls] += ns; if (w == g_render) g_dgfr[cls] += ns; if (!oh) return; int i = 0; for (; i < DG_OWN && g_dgw.own[i].ns; i++) if (g_dgw.own[i].h == oh && g_dgw.own[i].cls == cls) break; if (i == DG_OWN) return; g_dgw.own[i].h = oh; g_dgw.own[i].prio = oprio; g_dgw.own[i].cls = cls; g_dgw.own[i].ns += ns; } // The waiter's class at the start of its wait, after core_kick claimed (or did not claim) an owner for it. static int dg_class0(const RtThread *t) { if (t->target) return dg_in_host(t->target) ? DG_HOST : DG_GAME; for (int i = 0; i < NCORE; i++) if (core_allowed(t, i) && g_core_owner[i] && g_core_owner[i]->prio > t->prio) return DG_NOCORE; return DG_EQ; } // The waiter got a core: split [dg_since, now] by what the owner that released it did after its preempt request. static void dg_done(RtThread *t) { uint64_t now = kern_now_ns(), t0 = t->dg_since, w = now - t0; t->dg_since = 0; if (!t0) return; const struct DgRel *r = &t->dg_rel; uint64_t part[DG_N] = {0}; uint32_t oh = t->target ? t->target->handle : r->h; int oprio = t->target ? t->target->prio : r->prio; if (r->pre && r->kick) { uint64_t k = r->kick > t0 ? (r->kick < now ? r->kick : now) : t0; part[t->dg_cls] += k - t0; if (r->host) { uint64_t he = r->host_end > k && r->host_end < now ? r->host_end : now; part[DG_HOST] += he - k; part[DG_GAME] += now - he; } else part[DG_GAME] += now - k; oh = r->h; oprio = r->prio; } else part[t->dg_cls] += w; int top = 0; for (int c = 0; c < DG_N; c++) { dg_add(t, c, part[c], oh, oprio); if (part[c] > part[top]) top = c; } int b = 0; while (b < DG_NB - 1 && w >= dg_edge[b]) b++; g_dgw.n[top]++; g_dgw.hist[top][b]++; if ((part[DG_GAME] >= 2000000 || part[DG_HOST] >= 2000000) && g_dg_lines < DG_LONG_LINES) { g_dg_lines++; char a[128], l[128]; rt_log("core-diag: wait %.2f ms (game %.2f host %.2f): %#x p%d mask %llx; released by %#x p%d%s at %s lr %s", w / 1e6, part[DG_GAME] / 1e6, part[DG_HOST] / 1e6, t->handle, t->prio, (unsigned long long)t->mask, r->h, r->prio, r->pre ? " (preempted)" : "", r->pc ? ldr_describe(r->pc, a, sizeof a) : "-", r->lr ? ldr_describe(r->lr, l, sizeof l) : "-"); } t->target = NULL; memset(&t->dg_rel, 0, sizeof t->dg_rel); } // Once per long DG_GAME or DG_HOST wait: where the owner's host thread runs (module and offset), its latest SVC and // its guest lr. Holding G; the owner is resumed before anything that could take a lock it holds. static void dg_sample(RtThread *t) { RtThread *o = t->target; if (!o || o->cur_core < 0 || !o->hthr || g_dg_samples >= DG_SAMPLES) return; uint64_t rip, rsp; if (!plat_intr_stop(o->hthr, &rip, &rsp)) return; int depth = __atomic_load_n(&o->host_depth, __ATOMIC_RELAXED); plat_intr_resume(o->hthr, 0); g_dg_samples++; char mn[256] = "?"; uintptr_t m = 0; plat_module_of((uintptr_t)rip, mn, sizeof mn, &m); char lr[128]; rt_log("core-diag: sample owner %#x p%d depth %d rip %s+%#llx, last svc %#x, guest lr %s (waiter %#x p%d, %s, " "%.2f ms)", o->handle, o->prio, depth, mn, (unsigned long long)(rip - m), o->dg_svc, o->cur ? ldr_describe(o->cur->x[30], lr, sizeof lr) : "-", t->handle, t->prio, t->dg_cls == DG_HOST ? "host" : "game", (kern_now_ns() - t->dg_since) / 1e6); } // perf_present, every present: a line for a present over 25 ms, the 60-present summary. void kern_core_diag_frame(uint64_t frame, uint64_t interval_ns) { if (g_ap > 0 && frame && frame % 600 == 0) { // TOTK_ASYNC_PREEMPT counters since the start lock(); uint64_t v[8] = {g_ap_sent, g_ap_taken, g_ap_host, g_ap_pc, g_ap_deferred, g_ap_fail, g_ap_spin_ok, g_sync36}; unlock(); rt_log("kernel: frame %llu: asynchronous preemption: %llu interrupted, %llu cores given up there; refused: " "%llu in a host call, %llu outside game code, %llu preemption disabled, %llu failed; %llu waits ended " "in the first spin; SVC 0x36 %llu", (unsigned long long)frame, (unsigned long long)v[0], (unsigned long long)v[1], (unsigned long long)v[2], (unsigned long long)v[3], (unsigned long long)v[4], (unsigned long long)v[5], (unsigned long long)v[6], (unsigned long long)v[7]); } if (g_diag <= 0) return; DgWindow w; uint64_t f[DG_N], fr[DG_N], s36, s36f; lock(); g_render = tcur; memcpy(f, g_dgf, sizeof f); memcpy(fr, g_dgfr, sizeof fr); memset(g_dgf, 0, sizeof g_dgf); memset(g_dgfr, 0, sizeof g_dgfr); int lng = interval_ns > 25000000ull; if (lng) { g_dgw.long_frames++; if (f[DG_GAME] > 1000000) g_dgw.long_game_frames++; } int rep = frame && frame % 60 == 0; if (rep) { w = g_dgw; memset(&g_dgw, 0, sizeof g_dgw); } s36 = g_sync36; s36f = g_sync36_flag; unlock(); if (lng) rt_log("core-diag: frame %llu: %.1f ms; core wait game %.2f host %.2f eq %.2f nocore %.2f ms; render thread game " "%.2f host %.2f eq %.2f nocore %.2f ms", (unsigned long long)frame, interval_ns / 1e6, f[0] / 1e6, f[1] / 1e6, f[2] / 1e6, f[3] / 1e6, fr[0] / 1e6, fr[1] / 1e6, fr[2] / 1e6, fr[3] / 1e6); if (!rep) return; char s[1024]; int o = 0; for (int c = 0; c < DG_N; c++) { o += snprintf(s + o, sizeof s - o, "%s %.3f ms/frame %llu waits [", dg_name[c], w.ns[c] / 60e6, (unsigned long long)w.n[c]); for (int b = 0; b < DG_NB; b++) o += snprintf(s + o, sizeof s - o, "%s%llu", b ? " " : "", (unsigned long long)w.hist[c][b]); o += snprintf(s + o, sizeof s - o, "]%s", c < DG_N - 1 ? "; " : ""); } rt_log("core-diag: frames %llu-%llu: %s; long frames %llu (game wait > 1 ms in %llu); SVC 0x36 %llu (flag set %llu)", (unsigned long long)frame - 59, (unsigned long long)frame, s, (unsigned long long)w.long_frames, (unsigned long long)w.long_game_frames, (unsigned long long)s36, (unsigned long long)s36f); o = 0; for (int k = 0; k < 6; k++) { int bi = -1; for (int i = 0; i < DG_OWN; i++) if (w.own[i].ns && (bi < 0 || w.own[i].ns > w.own[bi].ns)) bi = i; if (bi < 0) break; o += snprintf(s + o, sizeof s - o, " %#x p%d %s %.3f;", w.own[bi].h, w.own[bi].prio, dg_name[w.own[bi].cls], w.own[bi].ns / 60e6); w.own[bi].ns = 0; } if (o) rt_log("core-diag: owners (ms/frame):%s", s); } static void core_count(void) { int n = 0; for (int i = 0; i < NCORE; i++) n += g_core_owner[i] != NULL; if (n > g_held_max) g_held_max = n; } static struct DgRel g_dg_rel; // TOTK_CORE_DIAG: the owner core_put is releasing, for core_grant static void core_grant(int c) { RtThread *b = NULL, **bp = NULL; int bpri = 0; for (RtThread **p = &g_core_wait; *p; p = &(*p)->cnext) { if (!core_allowed(*p, c)) continue; int pr = prio_of(*p); if (!b || pr < bpri || (pr == bpri && (*p)->cseq < b->cseq)) { b = *p; bp = p; bpri = pr; } } if (!b) return; *bp = b->cnext; b->cnext = NULL; b->cur_core = c; g_core_owner[c] = b; core_count(); if (g_diag > 0) b->dg_rel = g_dg_rel; if (rt_trace & RT_TRACE_SCHED) b->grant_at = kern_now_ns(); plat_cond_signal(&b->cv); } static void core_put(RtThread *t) { if (t->cur_core < 0) return; int c = t->cur_core; if (t->preempt && t->pre_since) { // sched trace: time from the preempt request to this release uint64_t d = kern_now_ns() - t->pre_since; g_pre_n++; g_pre_ns += d; if (d > g_pre_max) g_pre_max = d; t->npre++; } t->pre_since = 0; if (g_diag > 0) { g_dg_rel = (struct DgRel){.kick = t->dg_kick, .host_end = t->dg_host_end, .pc = t->dg_pc, .lr = t->dg_lr, .host = t->dg_host_kick, .pre = t->preempt, .prio = t->prio, .h = t->handle}; t->dg_kick = t->dg_host_end = t->dg_pc = t->dg_lr = 0; t->dg_host_kick = 0; } t->cur_core = -1; t->preempt = 0; g_core_owner[c] = NULL; core_grant(c); } // Whether a waiter better than t may run on core c. static int core_better_waiter(const RtThread *t, int c) { int tp = prio_of(t); for (const RtThread *w = g_core_wait; w; w = w->cnext) if (core_allowed(w, c) && prio_of(w) < tp) return 1; return 0; } // Asks holders to give their cores up, one holder per waiter: waiters best first, each claims the worst holder worse // than itself on a core it may run on that no better waiter claimed (a request already pending is reused). Asking // every worse holder made several drop their cores for one waiter; the spare cores went to worse waiters, which asked // again. static void core_kick(void) { RtThread *w[64]; int wp[64], op[NCORE], n = 0, claimed = 0; for (int i = 0; i < NCORE; i++) op[i] = g_core_owner[i] ? prio_of(g_core_owner[i]) : 0; for (RtThread *p = g_core_wait; p && n < 64; p = p->cnext) { int i = n++, pp = prio_of(p); for (; i > 0 && (wp[i - 1] > pp || (wp[i - 1] == pp && w[i - 1]->cseq > p->cseq)); i--) { w[i] = w[i - 1]; wp[i] = wp[i - 1]; } w[i] = p; wp[i] = pp; } for (int k = 0; k < n && claimed != (1 << NCORE) - 1; k++) { int v = -1; for (int i = 0; i < NCORE; i++) { RtThread *o = g_core_owner[i]; if ((claimed >> i & 1) || !core_allowed(w[k], i) || !o || op[i] <= wp[k]) continue; RtThread *b = v < 0 ? NULL : g_core_owner[v]; if (!b || op[i] > op[v] || (op[i] == op[v] && o->preempt && !b->preempt)) v = i; } if (v < 0) continue; claimed |= 1 << v; RtThread *o = g_core_owner[v]; w[k]->target = o; if (o->preempt) continue; if (rt_trace & RT_TRACE_SCHED) { o->pre_since = kern_now_ns(); g_pre_req++; } if (g_diag > 0) { o->dg_kick = kern_now_ns(); o->dg_host_kick = dg_in_host(o); o->dg_host_end = 0; } __atomic_store_n(&o->preempt, 1, __ATOMIC_RELEASE); } } static void core_take(RtThread *t) { if (t->cur_core >= 0) return; if (!cores_enabled()) { acct(t, S_RUN); return; } if (!(t->mask & ((1u << NCORE) - 1))) rt_fatal("kernel: thread %#x core mask %#llx has no application core", t->handle, (unsigned long long)t->mask); int c = t->core >= 0 && t->core < NCORE && core_allowed(t, t->core) && !g_core_owner[t->core] ? t->core : -1; for (int i = 0; c < 0 && i < NCORE; i++) if (core_allowed(t, i) && !g_core_owner[i]) c = i; if (c >= 0) { t->cur_core = c; g_core_owner[c] = t; core_count(); acct(t, S_RUN); return; } acct(t, S_CORE); t->cseq = ++g_wseq; t->cnext = g_core_wait; g_core_wait = t; t->target = NULL; if (g_diag > 0) { t->dg_since = kern_now_ns(); memset(&t->dg_rel, 0, sizeof t->dg_rel); } core_kick(); if (g_diag > 0) t->dg_cls = dg_class0(t); if (g_ap_int && t->target && t->cur_core < 0) { // most waits end within microseconds: spin before interrupting uint64_t end = kern_now_ns() + AP_FIRST_NS; unlock(); while (__atomic_load_n(&t->cur_core, __ATOMIC_RELAXED) < 0 && kern_now_ns() < end) plat_pause(); lock(); if (t->cur_core >= 0) g_ap_spin_ok++; } int sampled = 0; while (t->cur_core < 0) { uint32_t ms = 0; // 0: until signalled if (g_ap_int && ap_try(t) != AP_NONE) ms = 1; // the owner keeps its core: again in a millisecond int sample = g_diag > 0 && !sampled && (t->dg_cls == DG_GAME || t->dg_cls == DG_HOST); if (sample && !ms) ms = 2; if (ms) plat_cond_wait_ms(&t->cv, &G, ms); else plat_cond_wait(&t->cv, &G); if (sample && t->cur_core < 0 && kern_now_ns() - t->dg_since >= 2000000) { sampled = 1; dg_sample(t); } } if (g_diag > 0) dg_done(t); if (t->grant_at) { uint64_t d = kern_now_ns() - t->grant_at; t->lg_ns += d; t->lg_n++; if (d > t->lg_max) t->lg_max = d; t->grant_at = 0; } t->target = NULL; acct(t, S_RUN); } static uint32_t block(RtThread *t, int kind, int64_t ns) { t->waker = 0; if (ns == 0) return block_inner(t, kind, ns); core_put(t); acct(t, st_of(kind)); t->nblk++; uint32_t r = block_inner(t, kind, ns); core_take(t); return r; } int kern_core_away(void) { RtThread *t = tcur; if (!t || t->cur_core < 0) return 0; lock(); core_put(t); acct(t, S_HOST); if (rt_trace & RT_TRACE_SCHED) // the caller as an exe RVA (totk.map) t->wobj = (uintptr_t)__builtin_return_address(0) - plat_exe_base(); unlock(); return 1; } void kern_core_back(int *away) { RtThread *t = tcur; if (!*away || !t) return; lock(); core_take(t); unlock(); *away = 0; } // ---- spin waits (recomp/patches.py `spin` lines, found by tools/spin_waits.py) // Adapted from MacNativeGames games/totk/runtime/threads.c runtime_spin_wait @ a038e6b and nxnative dm/host/hle_thread.c // dm_spin_wait @ 6d34984 (MIT, Copyright (c) 2026 MacNativeGames contributors, Copyright (c) 2026 seedds). // A pass of a guest loop that polls memory until another thread changes it. On the Switch it fills only its own core, // where a better thread pinned there preempts it; here it holds a virtual core and a host core, and pollers that take a // lock on every pass hand its cache line between their host cores so the thread whose item it is rarely gets it // (nxnative found this in Breath of the Wild's job queue). Every pass pauses the host core once. A wait is a run of // passes through the same loop at most SPIN_GAP_NS apart; once one has lasted TOTK_SPIN_HANDOFF_US (default 50) and a // thread waits for a virtual core, each pass gives the core to the waiter, whatever its priority, yields the host CPU // and takes a core again. Shorter waits keep the core: a lock held for a moment would otherwise make the next pass wait // for another thread's next SVC (MacNativeGames). TOTK_SPIN=0 keeps the core always (the gate's own cost only). #define SPIN_GAP_NS 20000ull static int g_spin_on = -1; static uint64_t g_spin_handoff_ns; static _Thread_local uint64_t sw_head, sw_t0, sw_last; void rt_spin_gate(Cpu *c, uint64_t head) { (void)c; plat_pause(); RtThread *t = tcur; if (!t) return; if (g_spin_on < 0) { const char *e = cfg_get("TOTK_SPIN_HANDOFF_US"); g_spin_handoff_ns = (uint64_t)(e && *e ? atoll(e) : 50) * 1000; g_spin_on = !(cfg_get("TOTK_SPIN") && !cfg_bool("TOTK_SPIN")); } t->sp_n++; t->sp_site = head; uint64_t now = kern_now_ns(); if (head != sw_head || now - sw_last > SPIN_GAP_NS) { // a new wait sw_head = head; sw_t0 = now; } sw_last = now; if (!g_spin_on || t->cur_core < 0 || now - sw_t0 < g_spin_handoff_ns || !__atomic_load_n(&g_core_wait, __ATOMIC_RELAXED)) // a racy look: a waiter missed now is seen at the next pass return; t->sp_ho++; int away = kern_core_away(); plat_yield(); kern_core_back(&away); sw_last = kern_now_ns(); } // A pending preempt request, at an SVC or an indirect call. The core goes over only if a better thread still waits // for it, so core_put grants it to a waiter better than t; a stale request (that waiter got another core) is dropped. void kern_checkpoint(void) { RtThread *t = tcur; if (!t || !__atomic_load_n(&t->preempt, __ATOMIC_RELAXED)) return; lock(); if (g_diag > 0 && t->cur) { t->dg_pc = t->cur->pc; t->dg_lr = t->cur->x[30]; } int better = t->cur_core >= 0 && core_better_waiter(t, t->cur_core); if (better && ap_disabled(t)) { // preemption disabled: the switch comes at SVC 0x36 ap_tlr(t)[1] = 1; t->preempt = 0; g_ap_deferred++; } else if (better) { core_put(t); // clears preempt (after the sched trace reads it) core_take(t); } else { t->preempt = 0; t->pre_since = 0; t->dg_kick = t->dg_host_end = 0; t->dg_host_kick = 0; g_pre_stale++; } unlock(); } // kern_ap_stub, on the interrupted owner: where to jump back to, then the core given up as at a checkpoint. uint64_t kern_ap_rip(void) { return tcur->ap_rip; } void kern_ap_yield(void) { RtThread *t = tcur; lock(); t->ap_pending = 0; if (t->preempt && t->cur_core >= 0 && !ap_disabled(t) && core_better_waiter(t, t->cur_core)) { if (g_diag > 0) { t->dg_pc = t->cur->pc; t->dg_lr = t->cur->x[30]; } g_ap_taken++; core_put(t); core_take(t); } unlock(); } // SVC 0x36 SynchronizePreemptionState: a preemption deferred by disable_count happens here. static void ap_sync(RtThread *t) { lock(); if (g_ap > 0 && t->cur_core >= 0 && core_better_waiter(t, t->cur_core)) { core_put(t); core_take(t); } unlock(); } void kern_core_stats(int *held_now, int *held_max) { lock(); int n = 0; for (int i = 0; i < NCORE; i++) n += g_core_owner[i] != NULL; *held_now = n; *held_max = g_held_max; unlock(); } void kern_core_stats_reset(void) { lock(); g_held_max = 0; core_count(); unlock(); } // TOTK_TRACE=sched: every 120 presents, the time split of the busiest threads (running, waiting for a core, host // calls) and of the render thread, the render and the main thread's longest waits by object and by the thread whose // signal ended them, and the preemption latency. #define REP_N 8 // At each present (holding G): starts recording at TOTK_CRIT's first frame, stops and writes the file at its last. // File: "CRIT", thread count, event count (u32 each); per thread handle, priority, flags (1 main, 2 presents), host // tid (u32 each); then the CritEv records. static void crit_frame(RtThread *self, uint64_t frame) { static int parsed; if (!parsed) { parsed = 1; const char *e = cfg_get("TOTK_CRIT"); unsigned long long lo, hi; double wl, wh; if (e && e[0] == 'w' && sscanf(e + 1, "%lf-%lf", &wl, &wh) == 2 && wl >= 0 && wl < wh && (g_crit = malloc(CRIT_N * sizeof *g_crit))) { g_crit_wlo = (int64_t)(wl * 1e6), g_crit_whi = (int64_t)(wh * 1e6); g_crit_lo = g_crit_hi = UINT64_MAX; rt_log("crit: recording world seconds %g-%g", wl, wh); } else if (e && sscanf(e, "%llu-%llu", &lo, &hi) == 2 && lo < hi && (g_crit = malloc(CRIT_N * sizeof *g_crit))) { g_crit_lo = lo; g_crit_hi = hi; rt_log("crit: recording frames %llu-%llu", lo, hi); } } if (g_crit && g_crit_whi && g_crit_hi == UINT64_MAX) { // world mode: fix the frames as the clock passes lo and hi int64_t w = frame_step_world_us(); if (g_crit_lo == UINT64_MAX) { if (w < g_crit_wlo) return; g_crit_lo = frame; } if (w > g_crit_whi) g_crit_hi = frame; } if (!g_crit || frame < g_crit_lo || frame > g_crit_hi) return; g_crit_on = 1; crit_add(CRIT_FRAME, self ? self->handle : 0, 0, 0, frame, kern_now_ns()); if (frame < g_crit_hi) return; g_crit_on = 0; const char *path = cfg_get("TOTK_CRIT_OUT"); FILE *f = fopen(path && *path ? path : "crit.bin", "wb"); if (!f) { rt_log("crit: cannot write %s", path && *path ? path : "crit.bin"); return; } uint32_t nt = 0; for (RtThread *t = g_all; t; t = t->all) nt++; fwrite("CRIT", 1, 4, f); fwrite(&nt, 4, 1, f); fwrite(&g_crit_n, 4, 1, f); for (RtThread *t = g_all; t; t = t->all) { uint32_t row[4] = {t->handle, (uint32_t)t->prio, (uint32_t)(t->is_main | (t == g_render) << 1), t->tid}; fwrite(row, 4, 4, f); } fwrite(g_crit, sizeof *g_crit, g_crit_n, f); fclose(f); rt_log("crit: %u events, %u threads, frames %llu-%llu%s", g_crit_n, nt, (unsigned long long)g_crit_lo, (unsigned long long)g_crit_hi, g_crit_n >= CRIT_N ? " (buffer full: the last frames are incomplete)" : ""); } void kern_sched_report(uint64_t frame) { static uint64_t last; typedef struct { uint32_t h; int prio, render; uint64_t mask, entry, ns[S_N], nblk, npre, cpu, lw_ns, lw_n, lw_max, lg_ns, lg_n, lg_max, sl_n, sl_req, sl_act; uint64_t sp_n, sp_ho, sp_site; } Row; Row rows[REP_N + 1]; WaitRec rw[2][4] = {0}; // the render and the main thread's longest waits int nr = 0; RtThread *self = tcur; lock(); if (g_render != self) { g_render = self; memset(g_rw, 0, sizeof g_rw); } crit_frame(self, frame); if (frame % 120) { unlock(); return; } uint64_t now = kern_now_ns(), span = last ? now - last : 0; last = now; RtThread *top[REP_N + 1] = {0}; uint64_t busy[REP_N + 1] = {0}, all_run = 0, all_core = 0; for (RtThread *t = g_all; t; t = t->all) { acct(t, t->st); // close the open interval uint64_t b = t->st_ns[S_RUN] + t->st_ns[S_CORE] + t->st_ns[S_HOST]; all_run += t->st_ns[S_RUN]; all_core += t->st_ns[S_CORE]; if (t == self) continue; for (int i = 0; i < REP_N; i++) if (!top[i] || b > busy[i]) { memmove(top + i + 1, top + i, (REP_N - 1 - i) * sizeof *top); memmove(busy + i + 1, busy + i, (REP_N - 1 - i) * sizeof *busy); top[i] = t; busy[i] = b; break; } } if (self) { // the render thread first memmove(top + 1, top, REP_N * sizeof *top); top[0] = self; } for (int i = 0; i <= REP_N && top[i]; i++) { RtThread *t = top[i]; Row *r = &rows[nr++]; r->h = t->handle; r->prio = t->prio; r->render = t == self; r->mask = t->mask; r->entry = t->entry; memcpy(r->ns, t->st_ns, sizeof r->ns); r->nblk = t->nblk; r->npre = t->npre; r->lw_ns = t->lw_ns, r->lw_n = t->lw_n, r->lw_max = t->lw_max; r->lg_ns = t->lg_ns, r->lg_n = t->lg_n, r->lg_max = t->lg_max; r->sl_n = t->sl_n, r->sl_req = t->sl_req, r->sl_act = t->sl_act; r->sp_n = t->sp_n, r->sp_ho = t->sp_ho, r->sp_site = t->sp_site; r->cpu = 0; uint64_t c = t->tid ? plat_thread_cpu_ns(t->tid) : 0; if (c) { r->cpu = t->cpu_last ? c - t->cpu_last : 0; t->cpu_last = c; } } for (int k = 0; k < 2; k++) { const WaitRec *src = k ? g_mw : g_rw; for (int i = 0; i < RW_N && src[i].ns; i++) for (int j = 0; j < 4; j++) if (src[i].ns > rw[k][j].ns) { memmove(rw[k] + j + 1, rw[k] + j, (3 - j) * sizeof **rw); rw[k][j] = src[i]; break; } } uint64_t pre_stale = g_pre_stale, pre_req = g_pre_req, pre_n = g_pre_n, pre_ns = g_pre_ns, pre_max = g_pre_max; uint64_t sc = g_sync_calls, sb = g_sync_blocks; for (RtThread *t = g_all; t; t = t->all) { memset(t->st_ns, 0, sizeof t->st_ns); t->nblk = t->npre = 0; t->lw_ns = t->lw_n = t->lw_max = t->lg_ns = t->lg_n = t->lg_max = t->sl_n = t->sl_req = t->sl_act = 0; t->sp_n = t->sp_ho = 0; // racy against the thread's own increments: a few passes may be lost } memset(g_rw, 0, sizeof g_rw); memset(g_mw, 0, sizeof g_mw); g_pre_req = g_pre_n = g_pre_ns = g_pre_max = g_pre_stale = 0; g_sync_calls = g_sync_blocks = 0; unlock(); if (!span) return; rt_log("sched: frame %llu: %.2f s, cores busy %.2f of %d, core wait %.2f thread-s, preempt req %llu released %llu " "avg %.2f ms max %.2f ms, WaitSynchronization %llu calls %llu blocks, stale preempt %llu%s", (unsigned long long)frame, span / 1e9, (double)all_run / span, NCORE, all_core / 1e9, (unsigned long long)pre_req, (unsigned long long)pre_n, pre_n ? pre_ns / 1e6 / pre_n : 0.0, pre_max / 1e6, (unsigned long long)sc, (unsigned long long)sb, (unsigned long long)pre_stale, self ? "" : " (render thread is not a guest thread)"); for (int i = 0; i < nr; i++) { Row *r = &rows[i]; char d[128], s[256]; int o = 0; for (int k = S_RUN; k < S_N; k++) o += snprintf(s + o, sizeof s - o, " %s %.0f%%", s_name[k], 100.0 * r->ns[k] / span); ldr_describe(r->entry, d, sizeof d); rt_log("sched: %s%#x p%d m%llx%s cpu %.0f%% blocks %llu preempted %llu (%s)", r->render ? "render " : "", r->h, r->prio, (unsigned long long)r->mask, s, 100.0 * r->cpu / span, (unsigned long long)r->nblk, (unsigned long long)r->npre, r->entry ? d : "main"); rt_log("sched: %#x wake to run avg %.1f us max %.0f us (%llu), core grant to run avg %.1f us max %.0f us " "(%llu), sleep %llu asked avg %.1f us took avg %.1f us", r->h, r->lw_n ? r->lw_ns / 1e3 / r->lw_n : 0.0, r->lw_max / 1e3, (unsigned long long)r->lw_n, r->lg_n ? r->lg_ns / 1e3 / r->lg_n : 0.0, r->lg_max / 1e3, (unsigned long long)r->lg_n, (unsigned long long)r->sl_n, r->sl_n ? r->sl_req / 1e3 / r->sl_n : 0.0, r->sl_n ? r->sl_act / 1e3 / r->sl_n : 0.0); if (r->sp_n) { ldr_describe(r->sp_site, d, sizeof d); rt_log("sched: %#x spin wait passes %llu, core handoffs %llu, last at %s", r->h, (unsigned long long)r->sp_n, (unsigned long long)r->sp_ho, d); } } for (int k = 0; k < 2; k++) for (int j = 0; j < 4 && rw[k][j].ns; j++) { char by[32] = ""; if (rw[k][j].waker) snprintf(by, sizeof by, ", woken by %#x", rw[k][j].waker); rt_log("sched: %s wait %d: %s on %s%#llx%s: %.1f%%", k ? "main" : "render", j + 1, s_name[rw[k][j].st], rw[k][j].st == S_HOST ? "exe+" : "", (unsigned long long)rw[k][j].obj, by, 100.0 * rw[k][j].ns / span); } } // ---- locks: tag word = owner handle | WAITERS_BIT static inline uint32_t ld32(uint64_t a) { return __atomic_load_n((uint32_t *)(uintptr_t)a, __ATOMIC_SEQ_CST); } static inline void st32(uint64_t a, uint32_t v) { __atomic_store_n((uint32_t *)(uintptr_t)a, v, __ATOMIC_SEQ_CST); } static inline int cas32(uint64_t a, uint32_t *exp, uint32_t v) { return __atomic_compare_exchange_n((uint32_t *)(uintptr_t)a, exp, v, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); } // Release the lock at addr: hand it to the best waiter, or clear it. static void lock_handoff(uint64_t addr) { RtThread *w = pick(W_LOCK, addr); int more = 0; if (!w) { st32(addr, 0); return; } for (RtThread *t = g_wait; t; t = t->next) if (t != w && t->wkind == W_LOCK && t->waddr == addr) { more = 1; break; } st32(addr, w->wtag | (more ? WAITERS_BIT : 0)); wake(w, 0); } // A condvar waiter takes its lock: directly if free (and wakes), else it becomes a lock waiter behind the others. static void lock_acquire_for(RtThread *t, uint64_t addr, uint32_t tag) { uint32_t v = ld32(addr); for (;;) { if (v == 0) { if (cas32(addr, &v, tag)) { wake(t, 0); return; } continue; } if (cas32(addr, &v, v | WAITERS_BIT)) break; } acct(t, S_LOCK); t->wobj = v & ~WAITERS_BIT; t->wkind = W_LOCK; t->waddr = addr; t->wtag = tag; t->wseq = ++g_wseq; } // ---- threads static RtThread *thr_new(int prio, int core, uint64_t mask) { RtThread *t = calloc(1, sizeof *t); Cpu *c = plat_aligned_alloc(sizeof(Cpu), 64); if (!t || !c) rt_fatal("kernel: thread allocation"); memset(c, 0, sizeof *c); plat_cond_init(&t->cv); t->prio = prio; t->core = core; t->mask = mask; t->cur_core = -1; // TLR (0x200) and the "current thread object" the SDK reads through TLR+0x1F8, in one guest-visible block. t->tlr = mem_kernel_alloc(TLR_SIZE + THREAD_OBJ_SIZE, TLR_SIZE); *(uint64_t *)(t->tlr + 0x1F8) = (uint64_t)(uintptr_t)(t->tlr + TLR_SIZE); t->tls = mem_kernel_alloc(TLS_SIZE, 16); c->tpidrro = (uint64_t)(uintptr_t)t->tlr; c->tpidr = (uint64_t)(uintptr_t)t->tls; c->excl_addr = ~0ull; c->thread = t; t->cpu = t->cur = c; return t; } static void thr_link(RtThread *t) { // holding G t->handle = h_new(O_THREAD, t); t->id = g_next_id++; t->all = g_all; g_all = t; g_created++; } static void thr_free(RtThread *t) { // holding G; t has exited and its handle is closed for (RtThread **p = &g_all; *p; p = &(*p)->all) if (*p == t) { *p = t->all; break; } mem_kernel_free(t->tlr, TLR_SIZE + THREAD_OBJ_SIZE); mem_kernel_free(t->tls, TLS_SIZE); plat_aligned_free(t->cpu); free(t); } // The end of every guest thread, on its own host context after the unwind: give the core up, signal the thread // object, free it if its handle is already closed. t is not touched after the unlock. static void thread_finish(RtThread *t) { if (t->base_fiber) plat_fiber_unself(); t->base_fiber = NULL; tcur = NULL; lock(); core_put(t); acct(t, S_OFF); if (t->hthr) plat_intr_free(t->hthr); // no waiter interrupts a thread without a core t->hthr = NULL; t->exited = 1; wake_sync_waiters(); if (t->closed) thr_free(t); unlock(); } // ExitThread: back to the thread start with plat_jmp_go, which unwinds nothing: no SEH unwinding through lifted // frames. (__builtin_setjmp is not usable: on Win64 LLVM saves the frame address, which is not rbp when the frame // register is offset, and the landing code then reads its locals through the wrong base.) static __attribute__((noreturn)) void exit_now(RtThread *t) { plat_jmp_go(&t->exit_jmp); } // The calling host thread, for ap_try (TOTK_ASYNC_PREEMPT) and dg_sample (TOTK_CORE_DIAG). static PlatIntr *ap_open_self(void) { if (!cores_enabled() || (!g_ap_int && g_diag <= 0)) return NULL; PlatIntr *h = plat_intr_self(); if (!h) rt_log("kernel: no interruption handle for this thread: it is not preempted asynchronously"); return h; } static void thread_main(void *p) { RtThread *t = p; tcur = t; t->tid = plat_thread_id(); PlatIntr *self = ap_open_self(); lock(); t->hthr = self; while (!t->started) plat_cond_wait(&t->cv, &G); core_take(t); unlock(); Cpu *c = t->cpu; c->sp = t->stack_top & ~15ull; if (!plat_jmp_save(&t->exit_jmp)) { rt_call(c, t->entry, 1, t->arg); char d[128]; rt_fatal("guest thread %#x: entry %s returned (Horizon threads end with ExitThread)", t->handle, ldr_describe(t->entry, d, sizeof d)); } thread_finish(tcur); return; } uint64_t kern_threads_created(void) { lock(); uint64_t n = g_created; unlock(); return n; } Cpu *kern_cpu(void) { return tcur ? tcur->cur : NULL; } uint32_t kern_main_thread(Cpu **cpu) { if (tcur) { if (!tcur->is_main) rt_fatal("kern_main_thread on guest thread %#x, not the main thread", tcur->handle); *cpu = tcur->cpu; return tcur->handle; } if (g_main) rt_fatal("kern_main_thread: the main thread already runs on another host thread"); const RtGuestLayout *L = mem_layout(); uint64_t stack = L->stack.base + L->stack.size - MAIN_STACK; if (!plat_mem_commit(stack, MAIN_STACK)) rt_fatal("kernel: commit the main thread stack at %#llx failed", (unsigned long long)stack); RtThread *t = thr_new(MAIN_PRIO, MAIN_CORE, 7); t->started = 1; t->is_main = 1; t->stack_top = stack + MAIN_STACK; t->cpu->sp = t->stack_top; t->tid = plat_thread_id(); perf_main_thread(); PlatIntr *self = ap_open_self(); lock(); t->hthr = self; thr_link(t); g_main = t; g_main_stack = stack; core_take(t); unlock(); tcur = t; *cpu = t->cpu; return t->handle; } void kern_main_stack(uint64_t *base, uint64_t *size) { uint64_t b = __atomic_load_n(&g_main_stack, __ATOMIC_ACQUIRE); *base = b; *size = b ? MAIN_STACK : 0; } static void guest_main(void *p) { void (*fn)(Cpu *) = (void (*)(Cpu *))p; Cpu *c; kern_main_thread(&c); if (!plat_jmp_save(&tcur->exit_jmp)) fn(c); thread_finish(tcur); return; } void kern_run_on_guest_main(void (*fn)(Cpu *c)) { PlatThread *h = plat_thread_new_ex(guest_main, (void *)fn, "guest main", HOST_STACK); if (!h) rt_fatal("kernel: cannot start the guest main thread"); plat_thread_join(h); } // The OS timer ends a wait late by up to PLAT_WAIT_LATE_NS (Windows' high-resolution waitable timer: 0.3-0.5 ms; the // SDK's 20 us polling sleeps, main thread and job workers, about 1.5 per frame each, took 460-515 us). Sleeps up to // twice that spin on the clock instead (1 ms on Windows, 120 us on Linux); the virtual core is already given up. #define SLEEP_SPIN_NS (2 * PLAT_WAIT_LATE_NS) static void sleep_ns(RtThread *t, int64_t ns) { (void)t; if (ns <= SLEEP_SPIN_NS) { uint64_t end = kern_now_ns() + (uint64_t)ns; while (kern_now_ns() < end) plat_pause(); return; } plat_wait_ns((uint64_t)ns); } // ---- synchronization objects static int sig_obj(uint32_t h, int *bad) { // 1 = signaled (holding G) unsigned i = h - 0x1000; if (h == H_CUR_THREAD) return 0; if (i == 0 || i >= MAX_H) { *bad = 1; return 0; } switch (g_h[i].type) { case O_THREAD: return ((RtThread *)g_h[i].obj)->exited; case O_EVENT_R: return ((Event *)g_h[i].obj)->signaled; default: *bad = 1; return 0; } } int kern_thread_state(uint32_t h) { lock(); RtThread *t = h_peek(h, O_THREAD); int r = !t ? -1 : t->exited ? KERN_THREAD_EXITED : t->wkind; unlock(); return r; } // CloseHandle: an event goes with its last end; a thread object once it has also exited. static uint32_t handle_close(uint32_t h) { unsigned i = h - 0x1000; int ok = 0, kind = 0; void *ipc = NULL; lock(); if (i > 0 && i < MAX_H) { int type = g_h[i].type; void *o = g_h[i].obj; if (type == O_EVENT_R || type == O_EVENT_W) { Event *e = o; if (--e->refs == 0) free(e); ok = 1; } else if (type == O_SESSION || type == O_TMEM) { kind = type; ipc = o; ok = 1; } else if (type == O_THREAD) { RtThread *t = o; t->closed = 1; if (t->exited) thr_free(t); ok = 1; } if (ok) { g_h[i].type = O_FREE; g_h[i].obj = NULL; } } unlock(); if (ipc) ipc_handle_closed(kind, ipc); return ok ? R_OK : R_BAD_HANDLE; } void kern_handle_close(uint32_t h) { if (handle_close(h)) rt_fatal("kern_handle_close(%#x): bad handle", h); } // Hooks of the probe's condvar negative control. volatile int kern_test_cv_old_order; void (*volatile kern_test_cv_hook)(void); static void cv_hook(void) { void (*h)(void) = kern_test_cv_hook; if (h) h(); } int kern_svc(Cpu *c, uint32_t imm) { uint64_t *x = c->x; if (g_diag > 0 && tcur) tcur->dg_svc = imm; switch (imm) { case 0x08: { // CreateThread(-> w1 handle; x1 entry, x2 arg, x3 stack top, w4 priority, w5 core) int prio = (int)x[4], core = (int)x[5]; if (!x[1] || !x[3] || prio < 28 || prio > 59 || core < -3 || core > 2) { x[0] = R_BAD_STATE; return 1; } RtThread *t = thr_new(prio, core >= 0 ? core : MAIN_CORE, core >= 0 ? 1ull << core : 7); t->entry = x[1]; t->arg = x[2]; t->stack_top = x[3] & ~15ull; lock(); thr_link(t); unlock(); if (!plat_thread_spawn(thread_main, t, "guest", HOST_STACK)) rt_fatal("kernel: cannot start a guest thread"); if (rt_trace & RT_TRACE_SVC) { char d[128]; rt_log("CreateThread %#x entry %s stack %#llx prio %d core %d", t->handle, ldr_describe(t->entry, d, sizeof d), (unsigned long long)t->stack_top, prio, core); } x[0] = R_OK; x[1] = t->handle; return 1; } case 0x09: { // StartThread(w0) lock(); RtThread *t = h_get((uint32_t)x[0], O_THREAD); if (t) { t->started = 1; plat_cond_signal(&t->cv); } unlock(); x[0] = t ? R_OK : R_BAD_HANDLE; return 1; } case 0x0A: { // ExitThread RtThread *t = cur(imm); if (t->fiber) { // on a guest fiber: unwind from the thread's own context (kern_fiber_switch resumes it) t->exit_pending = 1; t->leaving = t->fiber; t->fiber = NULL; t->cur = t->cpu; plat_fiber_switch(t->base_fiber); rt_fatal("kernel: ExitThread on a fiber came back"); } exit_now(t); } case 0x0B: { // SleepThread(x0 ns); 0, -1, -2 yield int64_t ns = (int64_t)x[0]; RtThread *t = cur(imm); if (ns > 0) { lock(); core_put(t); acct(t, S_SLEEP); t->wobj = (uint64_t)ns; t->nblk++; unlock(); uint64_t t0 = kern_now_ns(); sleep_ns(t, ns); uint64_t took = kern_now_ns() - t0; lock(); t->sl_n++; t->sl_req += (uint64_t)ns; t->sl_act += took; core_take(t); unlock(); return 1; } // Yield: hand the core to a waiting thread of equal or better priority. lock(); int give = 0; int tp = prio_of(t); for (RtThread *w = g_core_wait; w && !give; w = w->cnext) give = t->cur_core >= 0 && core_allowed(w, t->cur_core) && prio_of(w) <= tp; if (give) { core_put(t); core_take(t); } unlock(); if (!give) plat_yield(); return 1; } case 0x0C: { // GetThreadPriority(w1 handle -> w1) lock(); RtThread *t = h_get((uint32_t)x[1], O_THREAD); x[0] = t ? R_OK : R_BAD_HANDLE; if (t) x[1] = (uint64_t)t->prio; unlock(); return 1; } case 0x0D: { // SetThreadPriority(w0 handle, w1 priority) if ((uint32_t)x[1] < 28 || (uint32_t)x[1] > 59) { x[0] = R_BAD_STATE; return 1; } lock(); RtThread *t = h_get((uint32_t)x[0], O_THREAD); if (t) t->prio = (int)x[1]; x[0] = t ? R_OK : R_BAD_HANDLE; unlock(); return 1; } case 0x0E: { // GetThreadCoreMask(w2 handle -> w1 core, x2 mask) lock(); RtThread *t = h_get((uint32_t)x[2], O_THREAD); x[0] = t ? R_OK : R_BAD_HANDLE; if (t) { x[1] = (uint64_t)(uint32_t)t->core; x[2] = t->mask; } unlock(); return 1; } case 0x0F: { // SetThreadCoreMask(w0 handle, w1 ideal core, x2 mask): -1 process default, -2/-3 keep the ideal core lock(); RtThread *t = h_get((uint32_t)x[0], O_THREAD); if (t) { int core = (int)x[1]; if (core >= 0) t->core = core; else if (core == -1) t->core = MAIN_CORE; if (x[2]) t->mask = x[2]; if (t == tcur && t->cur_core >= 0 && !core_allowed(t, t->cur_core)) { // move off a forbidden core core_put(t); core_take(t); } } x[0] = t ? R_OK : R_BAD_HANDLE; unlock(); return 1; } case 0x10: { // GetCurrentProcessorNumber: the virtual core held RtThread *t = cur(imm); x[0] = (uint64_t)(uint32_t)(t->cur_core >= 0 ? t->cur_core : t->core); return 1; } case 0x36: { // SynchronizePreemptionState: the SDK's spin locks call it after clearing TLR+0x100 (disable_count) // when TLR+0x102 (interrupt_flag) is set; rt_svc's checkpoint has already honoured a pending preempt RtThread *t = cur(imm); volatile uint16_t *flag = (volatile uint16_t *)(t->tlr + 0x102); __atomic_fetch_add(&g_sync36, 1, __ATOMIC_RELAXED); if (*flag) __atomic_fetch_add(&g_sync36_flag, 1, __ATOMIC_RELAXED); *flag = 0; ap_sync(t); x[0] = R_OK; return 1; } case 0x25: { // GetThreadId(w1 handle -> x1) lock(); RtThread *t = h_get((uint32_t)x[1], O_THREAD); x[0] = t ? R_OK : R_BAD_HANDLE; if (t) x[1] = t->id; unlock(); return 1; } case 0x1A: { // ArbitrateLock(w0 owner handle, x1 address, w2 tag) RtThread *t = cur(imm); uint64_t addr = x[1]; uint32_t r = R_OK; lock(); if (ld32(addr) == ((uint32_t)x[0] | WAITERS_BIT)) { t->waddr = addr; t->wtag = (uint32_t)x[2]; t->wobj = (uint32_t)x[0]; r = block(t, W_LOCK, -1); } unlock(); x[0] = r; return 1; } case 0x1B: // ArbitrateUnlock(x0 address) lock(); lock_handoff(x[0]); unlock(); x[0] = R_OK; return 1; case 0x1C: { // WaitProcessWideKeyAtomic(x0 lock, x1 key, w2 tag, x3 timeout) // The key is set before the lock is released. The SDK's Signal reads the key in user mode and skips svc 0x1D // when it is 0, and user mode takes the lock without G: releasing first lets another thread take the lock, // change the condition, read key 0 and wake nobody, after which this thread waits forever (MNG threads.c // 439-446). Linking into the wait list happens in block, still holding G, so svc 0x1D cannot come between. RtThread *t = cur(imm); uint64_t addr = x[0], key = x[1]; lock(); if (!kern_test_cv_old_order) { st32(key, 1); t->wkey = key; t->waddr = addr; t->wtag = (uint32_t)x[2]; lock_handoff(addr); cv_hook(); } else { // the probe's negative control: MNG's pre-fix order lock_handoff(addr); cv_hook(); st32(key, 1); t->wkey = key; t->waddr = addr; t->wtag = (uint32_t)x[2]; } t->wobj = key; uint32_t r = block(t, W_CONDVAR, (int64_t)x[3]); // on timeout user mode takes the lock itself unlock(); x[0] = r; return 1; } case 0x1D: { // SignalProcessWideKey(x0 key, w1 count; <= 0 all) uint64_t key = x[0]; int cnt = (int)x[1], n = 0; lock(); for (RtThread *w; (cnt <= 0 || n < cnt) && (w = pick(W_CONDVAR, key)); n++) lock_acquire_for(w, w->waddr, w->wtag); int left = 0; for (RtThread *w = g_wait; w && !left; w = w->next) left = w->wkind == W_CONDVAR && w->wkey == key; if (!left) st32(key, 0); unlock(); x[0] = R_OK; return 1; } case 0x34: { // WaitForAddress(x0 address, w1 type, w2 value, x3 timeout): 0 if less, 1 decrement if less, 2 if equal RtThread *t = cur(imm); uint64_t addr = x[0]; int type = (int)x[1]; int32_t val = (int32_t)x[2]; if (type < 0 || type > 2) { x[0] = R_INVALID_ENUM; return 1; } lock(); uint32_t r, v = ld32(addr); int ok = type == 2 ? (int32_t)v == val : (int32_t)v < val; if (ok && type == 1) while (!cas32(addr, &v, v - 1)) if (!((int32_t)v < val)) { ok = 0; break; } if (!ok) r = R_BAD_STATE; else { t->waddr = addr; t->wobj = addr; r = block(t, W_ADDRESS, (int64_t)x[3]); } unlock(); x[0] = r; return 1; } case 0x35: { // SignalToAddress(x0 address, w1 type, w2 value, w3 count): 0 wake, 1 increment if equal, // 2 if equal modify by the number of waiters uint64_t addr = x[0]; int type = (int)x[1], cnt = (int)x[3]; int32_t val = (int32_t)x[2]; if (type < 0 || type > 2) { x[0] = R_INVALID_ENUM; return 1; } lock(); int nw = 0; for (RtThread *w = g_wait; w; w = w->next) nw += w->wkind == W_ADDRESS && w->waddr == addr; uint32_t r = R_OK; if (type != 0) { int32_t nv = val + 1; if (type == 2) { // Waking all (count <= 0) with waiters subtracts 2, not 1 (mesosphere // KAddressArbiter::SignalAndModifyByWaitingCountIfEqual; yuzu, LAS Recomp and nxnative agree). if (cnt <= 0) nv = nw > 0 ? val - 2 : val + 1; else nv = nw == 0 ? val + 1 : nw <= cnt ? val - 1 : val; } uint32_t e = (uint32_t)val; if (!cas32(addr, &e, (uint32_t)nv)) r = R_BAD_STATE; } if (!r) for (int n = 0; cnt <= 0 || n < cnt; n++) { RtThread *w = pick(W_ADDRESS, addr); if (!w) break; wake(w, 0); } unlock(); x[0] = r; return 1; } case 0x45: { // CreateEvent(-> w1 writable, w2 readable) Event *e = calloc(1, sizeof *e); if (!e) rt_fatal("kernel: event allocation"); e->refs = 2; lock(); uint32_t hw = h_new(O_EVENT_W, e), hr = h_new(O_EVENT_R, e); unlock(); x[0] = R_OK; x[1] = hw; x[2] = hr; return 1; } case 0x11: case 0x12: case 0x17: { // SignalEvent(writable), ClearEvent(either end), ResetSignal(readable) uint32_t h = (uint32_t)x[0]; uint32_t r = R_OK; lock(); Event *e = imm == 0x11 ? h_peek(h, O_EVENT_W) : imm == 0x17 ? h_peek(h, O_EVENT_R) : h_peek(h, O_EVENT_W) ? h_peek(h, O_EVENT_W) : h_peek(h, O_EVENT_R); if (!e) { r = imm == 0x17 && h_peek(h, O_THREAD) ? R_BAD_STATE : R_BAD_HANDLE; if (r == R_BAD_HANDLE) rt_log("kernel: event svc %#x: invalid handle %#x", imm, h); } else if (imm == 0x11) { e->signaled = 1; wake_sync_waiters(); } else if (imm == 0x17 && !e->signaled) { r = R_BAD_STATE; } else { e->signaled = 0; } unlock(); x[0] = r; return 1; } case 0x18: { // WaitSynchronization(-> w1 index; x1 handles, w2 count, x3 timeout) RtThread *t = cur(imm); const uint32_t *hs = (const uint32_t *)(uintptr_t)x[1]; int n = (int)x[2]; int64_t ns = (int64_t)x[3]; if (n < 0 || n > 64 || (!hs && n)) { x[0] = R_BAD_STATE; return 1; } uint64_t start = kern_now_ns(); uint32_t r; int idx = -1; lock(); g_sync_calls++; // Horizon's order (mesosphere KSynchronizationObject::Wait): a signalled object, then timeout 0, then a pending // CancelSynchronization. The cancel is for the next wait that would block: a poll or a wait on a signalled // object leaves it pending. A cancel that wakes this wait (CancelSynchronization wakes it with R_CANCELLED; a // signal wakes it with -1 to look again) ends it cancelled, signalled objects or not. uint32_t woke = 0; for (;;) { if (woke == R_CANCELLED) { t->cancel = 0; r = R_CANCELLED; break; } int bad = 0; for (int i = 0; i < n && idx < 0; i++) if (sig_obj(hs[i], &bad)) idx = i; if (bad) { r = R_BAD_HANDLE; break; } if (idx >= 0) { r = R_OK; break; } if (ns == 0) { r = R_TIMEOUT; break; } if (t->cancel) { t->cancel = 0; r = R_CANCELLED; break; } int64_t left = ns; if (ns > 0) { uint64_t spent = kern_now_ns() - start; if (spent >= (uint64_t)ns) { r = R_TIMEOUT; break; } left = ns - (int64_t)spent; } t->wobj = n ? hs[0] | (uint64_t)n << 32 : 0; g_sync_blocks++; woke = block(t, W_SYNC, left); if (woke == R_TIMEOUT) { r = R_TIMEOUT; break; } } unlock(); x[0] = r; x[1] = idx >= 0 ? (uint64_t)idx : 0; return 1; } case 0x19: { // CancelSynchronization(w0 thread) lock(); RtThread *t = h_get((uint32_t)x[0], O_THREAD); if (t) { t->cancel = 1; if (t->wkind == W_SYNC) wake(t, R_CANCELLED); } unlock(); x[0] = t ? R_OK : R_BAD_HANDLE; return 1; } case 0x16: // CloseHandle(w0) x[0] = handle_close((uint32_t)x[0]); return 1; } return 0; } // ---- handles and events for host services (ipc.c and the service overrides) uint32_t kern_handle_new(int kind, void *obj) { if (kind != KERN_SESSION && kind != KERN_TMEM) rt_fatal("kern_handle_new: kind %d", kind); lock(); uint32_t h = h_new(kind, obj); unlock(); return h; } void *kern_handle_get(uint32_t h, int kind) { lock(); void *o = h_peek(h, kind); unlock(); return o; } uint32_t kern_event_new(void) { Event *e = calloc(1, sizeof *e); if (!e) rt_fatal("kernel: event allocation"); e->refs = 1; lock(); uint32_t hw = h_new(O_EVENT_W, e); unlock(); return hw; } // Copy-handle transfer of the readable end: each call gives the client its own handle to the same event. uint32_t kern_event_readable(uint32_t hw) { lock(); Event *e = h_peek(hw, O_EVENT_W); if (!e) rt_fatal("kern_event_readable: %#x is not a writable event handle", hw); e->refs++; uint32_t hr = h_new(O_EVENT_R, e); unlock(); return hr; } void kern_event_signal(uint32_t hw) { lock(); Event *e = h_peek(hw, O_EVENT_W); if (!e) rt_fatal("kern_event_signal: %#x is not a writable event handle", hw); e->signaled = 1; wake_sync_waiters(); unlock(); } // System events of host services: the SDK's own nn::os::CreateSystemEvent creates the kernel event (SVC 0x45) on a // guest thread; the writable handle is kept, so any thread (host audio tick included) can signal it later. // The guest may destroy such an event and attach the same SystemEventType again: the game does that with the nfp // activate and deactivate events before each detection. nn::os::DestroySystemEvent zeroes the state byte at +0x28 // (0 not initialised, 1 event, 2 inter-process event; nn::os::TryWaitSystemEvent aborts on 0) and closes both // handles, so a kept handle counts only while that byte is nonzero. #define SE_STATE 0x28 static struct { uint64_t ev; uint32_t wh; } g_se[512]; static int g_nse; static PlatMutex g_se_lock = PLAT_MUTEX_INIT; static int se_live(uint64_t ev) { return __atomic_load_n((const uint8_t *)(uintptr_t)(ev + SE_STATE), __ATOMIC_RELAXED) != 0; } static uint32_t se_find(uint64_t ev) { plat_mutex_lock_shared(&g_se_lock); uint32_t wh = 0; for (int i = 0; i < g_nse && !wh; i++) if (g_se[i].ev == ev) wh = g_se[i].wh; plat_mutex_unlock_shared(&g_se_lock); return wh; } // A bound event the guest destroyed since is created again and its table entry takes the new writable handle. void kern_bind_system_event(Cpu *c, uint64_t ev, int autoclear) { if (se_find(ev) && se_live(ev)) return; if (!tcur || !c) rt_fatal("kern_bind_system_event(%#llx) on a host thread", (unsigned long long)ev); static uint64_t create, getw; if (!create) { create = ldr_symbol("_ZN2nn2os17CreateSystemEventEPNS0_15SystemEventTypeENS0_14EventClearModeEb"); getw = ldr_symbol("_ZN2nn2os30GetWritableHandleOfSystemEventEPKNS0_15SystemEventTypeE"); if (!create || !getw) rt_fatal("kern_bind_system_event: the SDK's system event functions are not loaded"); } uint32_t r = (uint32_t)rt_call(c, create, 3, ev, (uint64_t)(autoclear ? 1 : 0), (uint64_t)1); if (r) rt_fatal("kern_bind_system_event: CreateSystemEvent(%#llx) failed %#x", (unsigned long long)ev, r); uint32_t wh = (uint32_t)rt_call(c, getw, 1, ev); plat_mutex_lock(&g_se_lock); int i = 0; while (i < g_nse && g_se[i].ev != ev) i++; if (i == g_nse) { if (g_nse == (int)(sizeof g_se / sizeof *g_se)) rt_fatal("kern_bind_system_event: table full"); g_se[g_nse++].ev = ev; } g_se[i].wh = wh; plat_mutex_unlock(&g_se_lock); } // An event not bound yet is bound on the spot with manual clear, which needs a guest thread. A bound event the guest // destroyed is not signalled: its handles are closed and the kernel hands their numbers to new objects. void kern_signal_system_event(uint64_t ev) { uint32_t wh = se_find(ev); if (wh && !se_live(ev)) return; if (!wh) { kern_bind_system_event(kern_cpu(), ev, 0); wh = se_find(ev); } kern_event_signal(wh); } // ---- fibers // The SDK switches fibers by swapping register context, which breaks "RET = C return". Each guest fiber gets a host // fiber (own 256 MiB reserved host stack) and its own Cpu, so a suspended fiber's registers and host frames stay // untouched while others run, and a fiber may resume on another thread. A switch carries the thread state (TLR, TLS // pointer, FPCR/FPSR) over to the target's Cpu and clears the exclusive monitor. enum { F_NEW, F_STARTED, F_DONE }; struct Fiber { uint64_t addr, func, arg, stack, size; Cpu *cpu; PlatFiber *host; int state; volatile int running; }; static Fiber *g_fibers[FIBER_MAX]; static PlatMutex g_fiber_lock = PLAT_MUTEX_INIT; static Fiber *fiber_find(uint64_t addr) { Fiber *f = NULL; plat_mutex_lock_shared(&g_fiber_lock); for (int i = 0; i < FIBER_MAX && !f; i++) if (g_fibers[i] && g_fibers[i]->addr == addr) f = g_fibers[i]; plat_mutex_unlock_shared(&g_fiber_lock); return f; } static void fiber_switch_to(RtThread *t, const Cpu *from, Fiber *to); // After a host fiber switch: release the context left behind, and finish an ExitThread issued on a fiber. static void fiber_resumed(void) { RtThread *t = tcur; rt_fpcr_sync(t->cur); // the host fiber resumes with its own MXCSR (platform.h fibers switch it) if (t->leaving) { __atomic_store_n(&t->leaving->running, 0, __ATOMIC_RELEASE); t->leaving = NULL; } if (t->exit_pending && !t->fiber) exit_now(t); } static void fiber_main(void *p) { Fiber *f = p; fiber_resumed(); Cpu *c = f->cpu; tcur->host_depth = 0; // a new fiber starts at the top level; each context restores its own depth on resume uint64_t next = rt_call(c, f->func, 1, f->arg); f->state = F_DONE; Fiber *n = NULL; if (next && !(n = fiber_find(next))) rt_fatal("fiber %#llx returned %#llx, which is not an initialized fiber", (unsigned long long)f->addr, (unsigned long long)next); fiber_switch_to(tcur, c, n); // NULL: back to the thread's own context rt_fatal("fiber %#llx resumed after it finished", (unsigned long long)f->addr); } static void fiber_switch_to(RtThread *t, const Cpu *from, Fiber *to) { if (!t->base_fiber) { t->base_fiber = plat_fiber_self(); if (!t->base_fiber) rt_fatal("kernel: cannot make the thread a fiber"); } Cpu *dst = to ? to->cpu : t->cpu; if (to) { if (to->state == F_DONE) rt_fatal("SwitchToFiber(%#llx): the fiber has finished", (unsigned long long)to->addr); int idle = 0; if (!__atomic_compare_exchange_n(&to->running, &idle, 1, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) rt_fatal("SwitchToFiber(%#llx): the fiber is running", (unsigned long long)to->addr); if (to->state == F_NEW) { dst->sp = (to->stack + to->size) & ~15ull; to->state = F_STARTED; } } dst->tpidr = from->tpidr; dst->tpidrro = from->tpidrro; dst->fpcr = from->fpcr; dst->fpsr = from->fpsr; dst->excl_addr = ~0ull; dst->thread = t; t->leaving = t->fiber; t->fiber = to; t->cur = dst; int depth = t->host_depth; plat_fiber_switch(to ? to->host : t->base_fiber); t->host_depth = depth; fiber_resumed(); } // InitializeFiber(x0 FiberType*, x1 function, x2 argument, x3 stack, x4 stack size, w5 flags) void kern_fiber_initialize(Cpu *c) { uint64_t addr = c->x[0]; Fiber *f = fiber_find(addr); if (f && f->running) rt_fatal("InitializeFiber(%#llx): the fiber is running", (unsigned long long)addr); if (!f) { f = calloc(1, sizeof *f); Cpu *fc = plat_aligned_alloc(sizeof(Cpu), 64); if (!f || !fc) rt_fatal("kernel: fiber allocation"); f->cpu = fc; plat_mutex_lock(&g_fiber_lock); int i = 0; while (i < FIBER_MAX && g_fibers[i]) i++; if (i == FIBER_MAX) rt_fatal("kernel: more than %d fibers", FIBER_MAX); g_fibers[i] = f; plat_mutex_unlock(&g_fiber_lock); } else if (f->host) { plat_fiber_free(f->host); } memset(f->cpu, 0, sizeof *f->cpu); f->cpu->excl_addr = ~0ull; f->addr = addr; f->func = c->x[1]; f->arg = c->x[2]; f->stack = c->x[3]; f->size = c->x[4]; f->state = F_NEW; f->running = 0; f->host = plat_fiber_new(HOST_STACK, fiber_main, f); if (!f->host) rt_fatal("kernel: cannot create a host fiber"); } // FinalizeFiber(x0 FiberType*) void kern_fiber_finalize(Cpu *c) { uint64_t addr = c->x[0]; plat_mutex_lock(&g_fiber_lock); Fiber *f = NULL; for (int i = 0; i < FIBER_MAX && !f; i++) if (g_fibers[i] && g_fibers[i]->addr == addr) { f = g_fibers[i]; g_fibers[i] = NULL; } plat_mutex_unlock(&g_fiber_lock); if (!f) rt_fatal("FinalizeFiber(%#llx): not an initialized fiber", (unsigned long long)addr); if (f->running) rt_fatal("FinalizeFiber(%#llx): the fiber is running", (unsigned long long)addr); plat_fiber_free(f->host); plat_aligned_free(f->cpu); free(f); } // SwitchToFiber(x0 FiberType*) void kern_fiber_switch(Cpu *c) { RtThread *t = tcur; if (!t) rt_fatal("SwitchToFiber on a host thread"); Fiber *to = fiber_find(c->x[0]); if (!to) rt_fatal("SwitchToFiber(%#llx): not an initialized fiber", (unsigned long long)c->x[0]); if (to == t->fiber) return; fiber_switch_to(t, c, to); } // GetCurrentFiber() -> x0, NULL on the thread's own context void kern_fiber_current(Cpu *c) { RtThread *t = tcur; c->x[0] = t && t->fiber ? t->fiber->addr : 0; } HOST_OVERRIDE(KERN_SYM_INITIALIZE_FIBER, kern_fiber_initialize); HOST_OVERRIDE(KERN_SYM_FINALIZE_FIBER, kern_fiber_finalize); HOST_OVERRIDE(KERN_SYM_SWITCH_TO_FIBER, kern_fiber_switch); HOST_OVERRIDE(KERN_SYM_GET_CURRENT_FIBER, kern_fiber_current);