From 541c4ff84c3732ddc11d77424b4b7bdad4004832 Mon Sep 17 00:00:00 2001 From: Michael Schienbein <7663497+mooki000@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:49:10 -0500 Subject: [PATCH] Start the game again after Restart on every processor it had (Linux). On Linux each Restart from the menu cost the next game one more processor core on machines with 12 or more logical processors. A process started with posix_spawn begins with the mask of the thread that spawned it, and since the recording thread has a core of its own no thread has every processor: the restarted game began without that core and took the next fast one for its own recording thread. plat_restart (runtime/platform/linux.c) now widens the calling thread's mask to all of the process's processors for the time of the spawn and puts it back. A mask set from outside is kept, helper programs started with plat_spawn are unchanged, and Windows is not touched. From pull request #435, with its test tests/platform_restart_mask_test.c, which needs Linux. Not compiled or run here: this machine has no Linux compiler. Co-Authored-By: Claude Fable 5.2 --- runtime/platform/linux.c | 19 +++++++ tests/platform_restart_mask_test.c | 82 ++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 tests/platform_restart_mask_test.c diff --git a/runtime/platform/linux.c b/runtime/platform/linux.c index 4851de73..66b7cb2d 100644 --- a/runtime/platform/linux.c +++ b/runtime/platform/linux.c @@ -2373,6 +2373,22 @@ static void spawn_close_from(posix_spawn_file_actions_t *fa, int from) { if (posix_spawn_file_actions_addclosefrom_np) posix_spawn_file_actions_addclosefrom_np(fa, from); } +// The calling thread's mask with every processor this process uses, for the time of a posix_spawn: a child starts +// with the mask of the thread that spawned it, and after plat_cpu_reserve no thread has them all (the recording thread +// has its core alone, every other thread lacks that core). The game plat_restart starts would begin without the core, +// take the next fast one for its own recording thread, and so have one core fewer after every Restart. +// 1 when the mask was widened: *saved is then the mask to put back. +static int spawn_mask_whole(cpu_set_t *saved) { + cpu_set_t whole; + plat_mutex_lock(&cpu_lock); + int taken = cpu_taker != 0; + CPU_OR(&whole, &cpu_taken, &cpu_others); + plat_mutex_unlock(&cpu_lock); + if (!taken || sched_getaffinity(0, sizeof *saved, saved)) return 0; + CPU_OR(&whole, &whole, saved); + return !CPU_EQUAL(&whole, saved) && !sched_setaffinity(0, sizeof whole, &whole); +} + int plat_restart(void) { // the arguments, NUL-separated, from /proc/self/cmdline static char args[16384]; @@ -2399,8 +2415,11 @@ int plat_restart(void) { posix_spawnattr_setsigdefault(&at, &all); posix_spawnattr_setflags(&at, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF); setenv(RESTART_WAIT, wait, 1); + cpu_set_t mask; + int whole = spawn_mask_whole(&mask); pid_t pid; int e = posix_spawn(&pid, exe, &fa, &at, argv, environ); + if (whole) sched_setaffinity(0, sizeof mask, &mask); unsetenv(RESTART_WAIT); posix_spawnattr_destroy(&at); posix_spawn_file_actions_destroy(&fa); diff --git a/tests/platform_restart_mask_test.c b/tests/platform_restart_mask_test.c new file mode 100644 index 00000000..d6678689 --- /dev/null +++ b/tests/platform_restart_mask_test.c @@ -0,0 +1,82 @@ +// plat_restart on Linux (runtime/platform/linux.c): the game it starts may run on every processor this one could +// before plat_cpu_reserve took a core for the recording thread, whichever thread asks for the restart. No game files. +// Build and run from the repository root: +// clang -O2 -ffunction-sections -fdata-sections -Iruntime -c runtime/platform/linux.c -o /tmp/plat.o +// clang -O2 -Wall -Iruntime tests/platform_restart_mask_test.c /tmp/plat.o -Wl,--gc-sections -lpthread -ldl -o /tmp/restart_mask +// /tmp/restart_mask (exit 77: this machine has no core plat_cpu_reserve would take) +// The test starts itself again through plat_restart; that copy only writes how many processors it may run on. +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include "platform/platform.h" + +#define OUT "RESTART_MASK_TEST_OUT" + +static int allowed(void) { + cpu_set_t set; + return sched_getaffinity(0, sizeof set, &set) ? -1 : CPU_COUNT(&set); +} + +// What the copy plat_restart started wrote, waiting up to 5 s for it; -1 when it never came. +static int restarted_count(const char *path) { + for (int i = 0; i < 500; i++) { + FILE *f = fopen(path, "r"); + int n = -1; + if (f) { + int got = fscanf(f, "%d", &n); + fclose(f); + if (got == 1) { + remove(path); + return n; + } + } + usleep(10000); + } + return -1; +} + +static atomic_int go, done; + +// A thread that existed before the core was taken, as the game's main thread does: it asks for the restart. +static void *other_thread(void *arg) { + (void)arg; + while (!atomic_load(&go)) usleep(1000); + plat_restart(); + atomic_store(&done, 1); + return NULL; +} + +int main(void) { + const char *out = getenv(OUT); + if (getenv("NX2NAVI_RESTART_WAIT") && out) { // the copy plat_restart started + FILE *f = fopen(out, "w"); + if (f) fprintf(f, "%d\n", allowed()), fclose(f); + return 0; + } + char path[64]; + snprintf(path, sizeof path, "/tmp/restart-mask-%d.txt", (int)getpid()); + setenv(OUT, path, 1); + remove(path); + int before = allowed(), fails = 0; + pthread_t th; + pthread_create(&th, NULL, other_thread, NULL); + char which[128]; + int core = plat_cpu_reserve(which, sizeof which); + if (!core) { puts("SKIP: no core to reserve on this machine"); return 77; } + atomic_store(&go, 1); + while (!atomic_load(&done)) usleep(1000); + pthread_join(th, NULL); + int from_other = restarted_count(path); + plat_restart(); // and from the thread that holds the core + int from_holder = restarted_count(path); + printf("%d processors before; the core taken: %s; a restart from another thread starts with %d, from the holder with %d\n", + before, which, from_other, from_holder); + if (from_other != before) puts("FAIL: the restarted process lacks processors (asked by another thread)"), fails++; + if (from_holder != before) puts("FAIL: the restarted process lacks processors (asked by the holder)"), fails++; + if (!fails) puts("restart mask: the restarted process has every processor"); + return fails != 0; +}