Keep the virtual core gate on when totk.ini sets free_cores

Earlier settings menus wrote free_cores = 1 under the label "Virtual core gate", which promised the gate. Without it a
thread that holds no virtual 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 its CallCommands fails at start (#212); on 1.4.3 the game
corrupts a heap in main+0x82e00 or hangs at the title menu. A player with such a totk.ini crashed at every start and
the menu no longer shows the row.

free_cores is no longer a setting: only TOTK_FREE_CORES in the environment lifts the gate, and a free_cores value in
totk.ini is logged as ignored. The kernel probe asks the kernel whether the gate is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valiantsin DzerakhandClaude Opus 5.5 committed 2026-10-31 23:19:34 +03:00
1 parent b5286ef054
commit dafc35fb35
7 files changed
+23 -16

No files matched your search

+2 -1
View File
@@ -74,7 +74,8 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
- Spin waits. Loops that only another thread can end (857 in 1.4.3, 864 in 1.2.1, found by `tools/spin_waits.py`, the
nxnative search) send their back-edges through `rt_spin_gate`, which pauses the core once per pass and hands it to a
waiter after 50 us of polling (`TOTK_SPIN`, `TOTK_SPIN_HANDOFF_US`).
- `TOTK_FREE_CORES=1` turns the gate off, for experiments.
- `TOTK_FREE_CORES=1` in the environment turns the gate off, for experiments; totk.ini cannot. Without it the game
fails at start: its job workers all report core 0 and share per-core memory.
### 4. Graphics
+2 -2
View File
@@ -73,7 +73,7 @@ static const struct { const char *key, *section; } sections[] = {
{"BIND_RIGHT", "input"}, {"BIND_DOWN", "input"}, {"BIND_LSTICK_LEFT", "input"}, {"BIND_LSTICK_UP", "input"},
{"BIND_LSTICK_RIGHT", "input"}, {"BIND_LSTICK_DOWN", "input"}, {"BIND_RSTICK_LEFT", "input"},
{"BIND_RSTICK_UP", "input"}, {"BIND_RSTICK_RIGHT", "input"}, {"BIND_RSTICK_DOWN", "input"},
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"}, {"FREE_CORES", "performance"},
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"},
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"},
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
@@ -118,7 +118,7 @@ static const CfgMeta metas[] = {
{"MENU_LANG", "", "en|ru", LIVE},
// Advanced: scheduling
{"ASYNC_PREEMPT", "1", "0|1|2", RESTART}, {"REPLAY_ASYNC", "0", "0|1", RESTART},
{"FREE_CORES", "0", "0|1", RESTART}, {"TIMER_1MS", "1", "0|1", RESTART},
{"TIMER_1MS", "1", "0|1", RESTART},
// shaders and pipelines
{"PIPE_WAIT", "default", "default|draw", RESTART}, {"PIPE_SKIP", "fullscreen", "strict|any|fullscreen", RESTART},
{"LIB_PREBUILD", "1", "0|1", RESTART}, {"LIB_PREBUILD_THREADS", "", "1..64", RESTART},
+1 -7
View File
@@ -354,7 +354,6 @@ int probe_cfg(void) {
cfg_load("cfg-probe-missing.ini");
plat_setenv("TOTK_FPS60", NULL);
plat_setenv("TOTK_MOUSE_SENS", NULL);
plat_setenv("TOTK_FREE_CORES", NULL);
CHECK(cfg_bool("TOTK_FPS60") == 1, "default: unset FPS60 is on");
CHECK(EQ(cfg_value("TOTK_VK_PRESENT"), "mailbox") && cfg_int("TOTK_SPLIT_DRAWS") == 64, "default: text and integer");
plat_setenv("TOTK_MOUSE_SENS", "2.5");
@@ -364,12 +363,7 @@ int probe_cfg(void) {
plat_setenv("TOTK_MOUSE_SENS", NULL);
cfg_set("TOTK_MOUSE_SENS", "3");
CHECK(cfg_double("TOTK_MOUSE_SENS") == 3.0 && cfg_int("TOTK_MOUSE_SENS") == 3, "a cfg_set value beats the default");
CHECK(cfg_bool("TOTK_FREE_CORES") == 0, "free_cores unset: off");
plat_setenv("TOTK_FREE_CORES", "0");
CHECK(cfg_bool("TOTK_FREE_CORES") == 0, "free_cores 0 gives the same as unset");
plat_setenv("TOTK_FREE_CORES", "1");
CHECK(cfg_bool("TOTK_FREE_CORES") == 1, "free_cores 1: on");
plat_setenv("TOTK_FREE_CORES", NULL);
CHECK(cfg_meta("FREE_CORES") == NULL, "free_cores is no setting: only the environment lifts the core gate");
cfg_load("cfg-probe-missing.ini");
int listed = 0;
for (int i = 0; cfg_meta_at(i); i++) {
+16 -2
View File
@@ -296,7 +296,14 @@ static uint32_t block_inner(RtThread *t, int kind, int64_t ns) {
// 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. TOTK_FREE_CORES=1 turns the gate off.
// once; guest code without either keeps the core until it reaches one.
//
// TOTK_FREE_CORES=1 in the environment turns the gate off, for experiments; totk.ini cannot. Without the gate 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: two of them add the same per-core NVN control memory, which retires the command
// handle the other just recorded there, and its CallCommands finds no recording (1.2.1, at start); 1.4.3 corrupts a
// heap in main+0x82e00 or hangs at the title menu. The settings menu of earlier releases wrote free_cores = 1 to
// totk.ini under a label that promised the gate, and those files would keep failing at every start.
static RtThread *g_core_owner[NCORE], *g_core_wait;
static int g_cores_on = -1, g_held_max;
@@ -305,16 +312,23 @@ 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
const char *e = getenv("TOTK_FREE_CORES");
g_cores_on = !e || !*e || !strcmp(e, "0"); // "0" keeps the gate, like unset
const char *ini = cfg_get("TOTK_FREE_CORES"); // the environment's value when set, else totk.ini's
if (!g_cores_on)
rt_log("kernel: virtual core gate off (TOTK_FREE_CORES): guest threads run without the limit of one per "
"core that the game's per-core data needs; for experiments only, the game can fail");
else if (!e && ini && *ini && strcmp(ini, "0"))
rt_log("kernel: free_cores = %s in totk.ini ignored: the virtual core gate stays on (only TOTK_FREE_CORES "
"in the environment lifts it)", ini);
diag_init();
ap_init();
}
return g_cores_on;
}
int kern_core_gate_on(void) { return cores_enabled(); }
static int core_allowed(const RtThread *t, int c) { return (int)((t->mask >> c) & 1); }
// ---- asynchronous preemption (TOTK_ASYNC_PREEMPT, on unless 0)
+1
View File
@@ -34,6 +34,7 @@ enum { KERN_WAIT_NONE, KERN_WAIT_LOCK, KERN_WAIT_CONDVAR, KERN_WAIT_ADDRESS, KER
int kern_thread_state(uint32_t handle); // KERN_WAIT_*, KERN_THREAD_EXITED, or -1 for a bad handle
void kern_core_stats(int *held_now, int *held_max); // virtual-core holders now and the most ever at once
void kern_core_stats_reset(void);
int kern_core_gate_on(void); // 0 when TOTK_FREE_CORES in the environment lifted the gate
extern volatile int kern_test_cv_old_order; // WaitProcessWideKeyAtomic: release the lock before setting
// the key (MNG's pre-fix order, the negative control)
extern void (*volatile kern_test_cv_hook)(void); // called right after the lock release, holding the kernel lock
+1 -1
View File
@@ -416,7 +416,7 @@ static void test_cores(Cpu *c) {
int now, held_max;
kern_core_stats(&now, &held_max);
uint32_t cpus = plat_cpu_usable();
int gate = !cfg_bool("TOTK_FREE_CORES"); // the kernel's own switch
int gate = kern_core_gate_on();
int ok = g_run_max <= 3 && held_max <= 3 && !g_core_clash && (cpus < 4 || g_run_max == 3);
printf("virtual cores: %d threads x %d runs on %u host CPUs: at most %d running guest code at once, at most %d "
"core holders (kernel count), %d core-number clashes: %s\n", CORE_WORKERS, CORE_RUNS, cpus, g_run_max,
-3
View File
@@ -27,9 +27,6 @@
# async_preempt = 1
# Replay NVN submissions on their own thread, at most one frame behind the game (1 = on). Adds up to a frame of latency.
# replay_async = 0
# 1 lifts the virtual core gate: guest threads run without the console's limit of one thread per core. For experiments
# only: the game keeps per-core data without locks and can crash.
# free_cores = 0
# Windows timer resolution of 1 ms for steadier frame pacing (0 = the Windows default).
# timer_1ms = 1