diff --git a/runtime/kernel.c b/runtime/kernel.c index e3cb0dda..57798e26 100644 --- a/runtime/kernel.c +++ b/runtime/kernel.c @@ -364,7 +364,7 @@ void kern_free_cores_note(void) { // NX2NAVI_CORES = 1..NCORE: how many virtual cores run threads (default all NCORE). A core at or above the count is // folded onto core (c mod count): a thread pinned to core 5 with NX2NAVI_CORES=3 runs on core 2, one thread per core -// at a time as always (diagnosis of races that more parallel cores expose). +// at a time as always (diagnosis of races that more parallel cores expose; the Steam Deck profile's four). static int core_allowed(const RtThread *t, int c) { if (c >= g_active_cores) return 0; for (int j = c; j < NCORE; j += g_active_cores) @@ -372,6 +372,21 @@ static int core_allowed(const RtThread *t, int c) { return 0; } +// The core number a thread sees (GetCurrentProcessorNumber) while it holds virtual core c. With all NCORE cores that +// is c. With fewer it is one of the thread's own cores that fold onto c, its ideal core first: the game keys per-core +// data by this number without locks, so a thread pinned to core 5 must go on using core 5's data when it takes +// turns with core 1's threads on one virtual core. Seeing 1 there, it used core 1's data in turn with core 1's own +// threads, and the game died in its draw set-up within seconds of the first frame in play (issue #445; with +// NX2NAVI_CORES=4 on any machine). No two threads that run guest code see the same number: they hold different +// virtual cores, and the numbers that fold onto different virtual cores differ. +static int core_seen(const RtThread *t, int c) { + if (g_active_cores >= NCORE) return c; + if (t->core >= 0 && t->core < NCORE && t->core % g_active_cores == c && ((t->mask >> t->core) & 1)) return t->core; + for (int j = c; j < NCORE; j += g_active_cores) + if ((t->mask >> j) & 1) return j; + return c; +} + // ---- asynchronous preemption (NX2NAVI_ASYNC_PREEMPT, on unless 0) // Contains code Copyright (c) 2026 MacNativeGames contributors (MIT; see THIRD_PARTY_NOTICES.md). // Horizon gives a core to a strictly better thread the moment that thread is ready. @@ -1707,9 +1722,9 @@ static int kern_svc_inner(Cpu *c, uint32_t imm) { unlock(); return 1; } - case 0x10: { // GetCurrentProcessorNumber: the virtual core held + case 0x10: { // GetCurrentProcessorNumber: the core seen on the virtual core held (core_seen) RtThread *t = cur(imm); - x[0] = (uint64_t)(uint32_t)(t->cur_core >= 0 ? t->cur_core : t->core); + x[0] = (uint64_t)(uint32_t)(t->cur_core >= 0 ? core_seen(t, t->cur_core) : t->core); return 1; } case 0x36: { // SynchronizePreemptionState: the SDK's spin locks call it after clearing TLR+0x100 (disable_count) diff --git a/runtime/kernel_probe.c b/runtime/kernel_probe.c index 0323177f..25efbc1e 100644 --- a/runtime/kernel_probe.c +++ b/runtime/kernel_probe.c @@ -474,6 +474,46 @@ static void test_cores(Cpu *c) { CHECK(ok || !gate, "virtual cores"); } +// ---- pinned threads: each sees the number of the core it is pinned to, also where NX2NAVI_CORES folds that core +// onto another one (kernel.c core_seen), and no two that run guest code at once see the same number. Run the probe +// with NX2NAVI_CORES=4 (the Steam Deck profile's value) to check the folded case; with all cores it holds trivially. +#define PIN_CORES 6 // kernel.c NCORE +static volatile int32_t g_pin_wrong, g_pin_clash, g_pin_use[PROBE_CORES]; +static uint64_t a_pin_worker; + +static void pin_worker(Cpu *c) { + uint32_t own = *(uint32_t *)arg_p(c, 0); + for (int i = 0; i < CORE_RUNS; i++) { + uint32_t core = sv(c, 0x10, 0, 0, 0, 0); + if (core != own) plat_inc32(&g_pin_wrong); + int known = core < PROBE_CORES; + if (!known || plat_inc32(&g_pin_use[core]) != 1) plat_inc32(&g_pin_clash); + uint64_t until = kern_now_ns() + 20000; + while (kern_now_ns() < until) {} + if (known) plat_dec32(&g_pin_use[core]); + sv(c, 0x0B, i % 8 == 7 ? 50000 : 0, 0, 0, 0); + } + exit_thread(c); +} + +static void test_pinned_cores(Cpu *c) { + static uint32_t own[2 * PIN_CORES]; + uint32_t h[2 * PIN_CORES]; + g_pin_wrong = g_pin_clash = 0; + for (int i = 0; i < 2 * PIN_CORES; i++) { // two threads per core, so each core's own threads take turns too + own[i] = (uint32_t)(i % PIN_CORES); + h[i] = create(c, a_pin_worker, &own[i], 44, i % PIN_CORES); + start(c, h[i]); + } + for (int i = 0; i < 2 * PIN_CORES; i++) join(c, h[i]); + int gate = !cfg_bool("NX2NAVI_FREE_CORES"); + int ok = !g_pin_wrong && !g_pin_clash; + printf("pinned cores: %d threads on %d cores (%d virtual cores run threads) x %d runs: %d saw another core's " + "number, %d core-number clashes: %s\n", 2 * PIN_CORES, PIN_CORES, kern_cores(), CORE_RUNS, g_pin_wrong, + g_pin_clash, !gate ? "not checked (NX2NAVI_FREE_CORES is set)" : ok ? "ok" : "FAIL"); + CHECK(ok || !gate, "pinned cores"); +} + // ---- fibers: two fibers switch to each other; each checks its callee-saved registers, sp and tpidr survive typedef struct { uint64_t self, other; int id; } FiberJob; static volatile int32_t g_switches, g_runs[3]; @@ -670,6 +710,7 @@ static void probe_main(Cpu *c) { test_arbiter(c); test_events(c); test_cores(c); + test_pinned_cores(c); test_fibers(c); test_exit(c); test_lifted_ticks(c); @@ -740,6 +781,7 @@ int probe_kernel(void) { a_arb_waiter = hostfn_register("probe-kernel arbiter waiter", arb_waiter); a_event_waiter = hostfn_register("probe-kernel event waiter", event_waiter); a_core_worker = hostfn_register("probe-kernel core worker", core_worker); + a_pin_worker = hostfn_register("probe-kernel pin worker", pin_worker); a_exit_entry = hostfn_register("probe-kernel exit entry", exit_entry); a_exit_nested = hostfn_register("probe-kernel exit nested", exit_nested); a_fiber_fn = hostfn_register("probe-kernel fiber", fiber_fn);