diff --git a/docs/architecture.md b/docs/architecture.md index 12f75987..b9ef58df 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/runtime/cfg.c b/runtime/cfg.c index d9c4cd61..e6c1ccbb 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -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}, diff --git a/runtime/cfg_probe.c b/runtime/cfg_probe.c index ca7449b2..e7c4f8e9 100644 --- a/runtime/cfg_probe.c +++ b/runtime/cfg_probe.c @@ -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++) { diff --git a/runtime/kernel.c b/runtime/kernel.c index cd75797b..6efebb52 100644 --- a/runtime/kernel.c +++ b/runtime/kernel.c @@ -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) diff --git a/runtime/kernel.h b/runtime/kernel.h index d719b301..10897245 100644 --- a/runtime/kernel.h +++ b/runtime/kernel.h @@ -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 diff --git a/runtime/kernel_probe.c b/runtime/kernel_probe.c index e226d4a5..5c8a0057 100644 --- a/runtime/kernel_probe.c +++ b/runtime/kernel_probe.c @@ -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, diff --git a/totk.ini.example b/totk.ini.example index 9dee23b7..0b7df377 100644 --- a/totk.ini.example +++ b/totk.ini.example @@ -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