Give a thread on a folded virtual core the number of its own core, so that fewer than six cores no longer crash the game.

With fewer than six virtual cores a thread pinned to a core at or above the count runs on core (c mod count), and GetCurrentProcessorNumber told it that virtual core's number. The game keys per-core data by the number without locks, so with four cores a thread pinned to core 5 used core 1's data in turn with core 1's own threads, and the game died within seconds of the first frame in play. The Steam Deck profile sets four cores, so this was every start under that profile (issue #445). The call now answers with one of the thread's own cores that fold onto the virtual core it holds, its ideal core first; with all six cores the answer is as before.

The kernel probe gets a test with two threads pinned to each of the six cores: with 6, 4, 3 and 1 virtual cores no thread sees another core's number and no two see the same at once.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-10 23:14:37 -05:00
parent 8e18097b7b
commit 7411d31803
2 changed files with 60 additions and 3 deletions

View File

@@ -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)

View File

@@ -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);