Files
NX2navi/runtime/platform/linux.c
Michael Schienbein 541c4ff84c 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 <noreply@anthropic.com>
2026-11-10 12:49:10 -05:00

2471 lines
94 KiB
C

// Linux implementation of platform.h: pthreads, CLOCK_MONOTONIC, futex locks and condition variables, POSIX files,
// /proc and sysfs for the load counters, posix_spawn for child processes.
#define _GNU_SOURCE
#include <dirent.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <linux/fs.h>
#include <linux/futex.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <signal.h>
#include <spawn.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/random.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/uio.h>
#include <sys/un.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <unwind.h>
#include <SDL3/SDL_clipboard.h>
#include "platform.h"
// The write-watch ABI of Linux 6.7 (uapi linux/fs.h and linux/userfaultfd.h), for headers older than it: the release
// builds against the Steam Runtime's 5.10 headers and runs on newer kernels. The kernel refuses what it lacks at run time.
#ifndef PAGEMAP_SCAN
struct page_region {
__u64 start;
__u64 end;
__u64 categories;
};
struct pm_scan_arg {
__u64 size;
__u64 flags;
__u64 start;
__u64 end;
__u64 walk_end;
__u64 vec;
__u64 vec_len;
__u64 max_pages;
__u64 category_inverted;
__u64 category_mask;
__u64 category_anyof_mask;
__u64 return_mask;
};
#define PAGEMAP_SCAN _IOWR('f', 16, struct pm_scan_arg)
#define PAGE_IS_WRITTEN (1 << 1)
#define PAGE_IS_PRESENT (1 << 3)
#define PAGE_IS_SWAPPED (1 << 4)
#define PAGE_IS_PFNZERO (1 << 5)
#define PM_SCAN_WP_MATCHING (1 << 0)
#define PM_SCAN_CHECK_WPASYNC (1 << 1)
#endif
#ifndef UFFD_FEATURE_WP_ASYNC
#define UFFD_FEATURE_WP_UNPOPULATED (1 << 13)
#define UFFD_FEATURE_WP_ASYNC (1 << 15)
#endif
#ifndef UFFD_USER_MODE_ONLY
#define UFFD_USER_MODE_ONLY 1
#endif
extern char **environ;
// ---- threads
struct PlatThread {
pthread_t t;
PlatThreadFn fn;
void *arg;
volatile int32_t tid; // the kernel's id once the thread runs, for plat_thread_set_priority
char name[16]; // the kernel keeps 15 characters
};
// A signal stack for the calling thread, so the fault and crash handlers still run when its own stack is exhausted.
// Never freed: threads that end are few, and a stack freed while a handler might run on it would be worse.
static void alt_stack(void) {
stack_t cur;
if (!sigaltstack(NULL, &cur) && !(cur.ss_flags & SS_DISABLE)) return;
size_t size = 64 << 10; // a signal frame with the AVX-512 state takes several KiB
void *p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED) return;
stack_t ss = {.ss_sp = p, .ss_size = size};
sigaltstack(&ss, NULL);
}
// Set in the thread that took a core in plat_cpu_reserve: the processors the threads it starts run on (not that core).
// Threads inherit the mask of the thread that starts them, which for those would be the reserved core alone.
static __thread cpu_set_t cpu_rest;
static __thread int cpu_rest_set;
static void *thread_main(void *p) {
PlatThread *t = p;
alt_stack();
__atomic_store_n(&t->tid, (int32_t)syscall(SYS_gettid), __ATOMIC_RELEASE);
if (t->name[0]) pthread_setname_np(pthread_self(), t->name);
t->fn(t->arg);
return NULL;
}
PlatThread *plat_thread_new(PlatThreadFn fn, void *arg, const char *name) {
return plat_thread_new_ex(fn, arg, name, 0);
}
PlatThread *plat_thread_new_ex(PlatThreadFn fn, void *arg, const char *name, size_t stack) {
PlatThread *t = calloc(1, sizeof *t);
if (!t) return NULL;
t->fn = fn;
t->arg = arg;
if (name) snprintf(t->name, sizeof t->name, "%s", name);
pthread_attr_t a;
pthread_attr_init(&a);
// The stack is mapped lazily, as Windows reserves it: only the pages a thread touches cost memory.
pthread_attr_setstacksize(&a, stack ? stack : PLAT_STACK_DEFAULT);
if (cpu_rest_set) pthread_attr_setaffinity_np(&a, sizeof cpu_rest, &cpu_rest);
int e = pthread_create(&t->t, &a, thread_main, t);
pthread_attr_destroy(&a);
if (e) { free(t); return NULL; }
return t;
}
static int read_u64_file(const char *path, uint64_t *v);
// The physical core of each logical processor is in /sys/devices/system/cpu/cpu<n>/topology/core_id together with its
// package; the lowest-numbered processor of each (package, core) pair is the core's first.
int plat_cpu_cores(uint64_t *all, uint64_t *first) {
cpu_set_t set;
*all = *first = 0;
if (sched_getaffinity(0, sizeof set, &set)) return 0;
int cores = 0;
uint64_t seen[64][2];
for (int i = 0; i < 64; i++) {
if (!CPU_ISSET(i, &set)) continue;
*all |= 1ull << i;
uint64_t core = 0, pkg = 0;
char path[96];
snprintf(path, sizeof path, "/sys/devices/system/cpu/cpu%d/topology/core_id", i);
if (!read_u64_file(path, &core)) core = (uint64_t)i;
snprintf(path, sizeof path, "/sys/devices/system/cpu/cpu%d/topology/physical_package_id", i);
read_u64_file(path, &pkg);
int k = 0;
while (k < cores && (seen[k][0] != core || seen[k][1] != pkg)) k++;
if (k == cores) seen[cores][0] = core, seen[cores][1] = pkg, cores++, *first |= 1ull << i;
}
return cores;
}
// The L3 cache of each logical processor: the cache/index<k> of /sys/devices/system/cpu/cpu<n> whose level is 3, its
// size in KiB ("98304K").
uint64_t plat_cpu_cache_die(uint32_t *l3_kb) {
cpu_set_t set;
*l3_kb = 0;
if (sched_getaffinity(0, sizeof set, &set)) return 0;
uint64_t die = 0, in_set = 0;
uint32_t big = 0, small = ~0u;
for (int i = 0; i < 64; i++) {
uint32_t kb = 0;
for (int k = 0; k < 8 && !kb; k++) {
char path[96], text[32] = "";
uint64_t level = 0;
snprintf(path, sizeof path, "/sys/devices/system/cpu/cpu%d/cache/index%d/level", i, k);
if (!read_u64_file(path, &level)) break;
if (level != 3) continue;
snprintf(path, sizeof path, "/sys/devices/system/cpu/cpu%d/cache/index%d/size", i, k);
FILE *f = fopen(path, "r");
if (f) {
if (fgets(text, sizeof text, f)) kb = (uint32_t)strtoul(text, NULL, 10) * (strchr(text, 'M') ? 1024 : 1);
fclose(f);
}
}
if (!kb) continue;
if (kb > big) big = kb, die = 0;
if (kb == big) die |= 1ull << i;
if (kb < small) small = kb;
if (CPU_ISSET(i, &set)) in_set |= 1ull << i;
}
if (!die || big == small) return 0;
*l3_kb = big;
return die & in_set;
}
// The core plat_cpu_reserve took: its processors and the thread that holds them (0 before the call), and the
// processors every other thread got then or at the last plat_process_affinity. cpu_lock orders the functions that set
// the masks of every thread.
static PlatMutex cpu_lock = PLAT_MUTEX_INIT;
static cpu_set_t cpu_taken, cpu_others;
static pid_t cpu_taker;
static void cpu_clear(cpu_set_t *set, const cpu_set_t *drop) {
for (int i = 0; i < CPU_SETSIZE; i++)
if (CPU_ISSET(i, drop)) CPU_CLR(i, set);
}
// Linux has no process-wide affinity: each thread of /proc/self/task gets the mask, and threads started later inherit
// it from the thread that starts them. The core plat_cpu_reserve took stays out of the mask of every thread but its
// holder, which keeps the mask it has.
int plat_process_affinity(uint64_t mask) {
cpu_set_t set;
CPU_ZERO(&set);
for (int i = 0; i < 64; i++)
if (mask >> i & 1) CPU_SET(i, &set);
plat_mutex_lock(&cpu_lock);
if (cpu_taker) cpu_clear(&set, &cpu_taken);
DIR *d = CPU_COUNT(&set) ? opendir("/proc/self/task") : NULL;
int ok = d != NULL;
if (ok && cpu_taker) cpu_others = set;
for (struct dirent *e; d && (e = readdir(d));) {
pid_t tid = (pid_t)atoi(e->d_name);
if (tid > 0 && tid != cpu_taker && sched_setaffinity(tid, sizeof set, &set)) ok = 0;
}
if (d) closedir(d);
plat_mutex_unlock(&cpu_lock);
return ok;
}
// The first field of /proc/self/task/<tid>/schedstat is the time the thread has run, in ns.
uint64_t plat_thread_cpu_ns(uint32_t tid) {
char path[64];
snprintf(path, sizeof path, "/proc/self/task/%u/schedstat", tid);
FILE *f = fopen(path, "r");
unsigned long long ns = 0;
if (f) {
if (fscanf(f, "%llu", &ns) != 1) ns = 0;
fclose(f);
}
return ns;
}
// ---- thread interruption: a signal handshake in place of Windows' SuspendThread / Get/SetThreadContext.
// The caller moves the handle from IDLE to REQ and signals the thread. The thread's handler stores where it was and
// moves REQ to STOPPED, then spins until the caller moves it on to GO (with the redirect in to), changes its own
// return context, and goes back to IDLE. A caller that sees no STOPPED within INTR_WAIT_NS (the thread is blocked in
// the kernel and runs no handler) takes REQ back to IDLE; a handler that then finds IDLE returns at once.
enum { INTR_IDLE, INTR_REQ, INTR_STOPPED, INTR_GO };
#define INTR_WAIT_NS 200000u
struct PlatIntr {
pthread_t pt;
volatile int32_t state;
uint64_t rip, rsp, to;
};
static __thread PlatIntr *intr_me;
static int intr_sig;
static void intr_handler(int sig, siginfo_t *si, void *uc) {
(void)sig, (void)si;
PlatIntr *t = intr_me;
ucontext_t *u = uc;
if (!t) return;
t->rip = (uint64_t)u->uc_mcontext.gregs[REG_RIP];
t->rsp = (uint64_t)u->uc_mcontext.gregs[REG_RSP];
int32_t req = INTR_REQ;
if (!__atomic_compare_exchange_n(&t->state, &req, INTR_STOPPED, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) return;
while (__atomic_load_n(&t->state, __ATOMIC_ACQUIRE) != INTR_GO) plat_pause();
if (t->to) u->uc_mcontext.gregs[REG_RIP] = (greg_t)t->to;
__atomic_store_n(&t->state, INTR_IDLE, __ATOMIC_RELEASE);
}
PlatIntr *plat_intr_self(void) {
static PlatMutex install = PLAT_MUTEX_INIT;
if (!__atomic_load_n(&intr_sig, __ATOMIC_ACQUIRE)) {
plat_mutex_lock(&install);
if (!intr_sig) {
int sig = SIGRTMIN + 4; // glibc keeps the first real-time signals for itself; SIGRTMIN skips them
struct sigaction sa = {.sa_sigaction = intr_handler, .sa_flags = SA_SIGINFO | SA_RESTART | SA_ONSTACK};
sigemptyset(&sa.sa_mask);
if (!sigaction(sig, &sa, NULL)) __atomic_store_n(&intr_sig, sig, __ATOMIC_RELEASE);
}
plat_mutex_unlock(&install);
}
if (!intr_sig) return NULL;
PlatIntr *t = calloc(1, sizeof *t);
if (!t) return NULL;
t->pt = pthread_self();
intr_me = t;
return t;
}
// Called by the thread itself as it ends, or by another once the thread no longer runs.
void plat_intr_free(PlatIntr *t) {
if (!t) return;
if (intr_me == t) intr_me = NULL;
free(t);
}
int plat_intr_stop(PlatIntr *t, uint64_t *rip, uint64_t *rsp) {
int32_t idle = INTR_IDLE;
if (!__atomic_compare_exchange_n(&t->state, &idle, INTR_REQ, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) return 0;
if (pthread_kill(t->pt, intr_sig)) {
__atomic_store_n(&t->state, INTR_IDLE, __ATOMIC_RELEASE);
return 0;
}
uint64_t end = plat_now_ns() + INTR_WAIT_NS;
while (__atomic_load_n(&t->state, __ATOMIC_ACQUIRE) != INTR_STOPPED) {
if (plat_now_ns() < end) {
plat_pause();
continue;
}
int32_t req = INTR_REQ;
if (__atomic_compare_exchange_n(&t->state, &req, INTR_IDLE, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) return 0;
}
*rip = t->rip;
*rsp = t->rsp;
return 1;
}
int plat_intr_resume(PlatIntr *t, uint64_t to) {
t->to = to;
__atomic_store_n(&t->state, INTR_GO, __ATOMIC_RELEASE);
return 1;
}
// ---- fibers: a stack switch of our own. glibc's swapcontext saves and restores the signal mask with a system call
// on every switch, and the game switches fibers often. fiber_swap saves the callee-saved registers, the MXCSR and the
// x87 control word on the current stack, stores the stack pointer in *save and loads the other one. A new fiber's
// stack is laid out as fiber_swap leaves one, returning into fiber_entry, which calls fn(arg) (r12 and r13).
struct PlatFiber {
void *sp;
void *stack; // mapped stack (NULL for a thread's own context)
size_t size;
};
void plat_fiber_swap(void **save, void *load);
void plat_fiber_entry(void);
__asm__(".text\n"
".globl plat_fiber_swap\n"
".type plat_fiber_swap, @function\n"
".p2align 4\n"
"plat_fiber_swap:\n"
" .cfi_startproc\n"
" pushq %rbp\n"
" pushq %rbx\n"
" pushq %r12\n"
" pushq %r13\n"
" pushq %r14\n"
" pushq %r15\n"
" subq $16, %rsp\n"
" stmxcsr 8(%rsp)\n"
" fnstcw (%rsp)\n"
" movq %rsp, (%rdi)\n"
" movq %rsi, %rsp\n"
" fldcw (%rsp)\n"
" ldmxcsr 8(%rsp)\n"
" addq $16, %rsp\n"
" popq %r15\n"
" popq %r14\n"
" popq %r13\n"
" popq %r12\n"
" popq %rbx\n"
" popq %rbp\n"
" ret\n"
" .cfi_endproc\n"
".size plat_fiber_swap, .-plat_fiber_swap\n"
".globl plat_fiber_entry\n"
".type plat_fiber_entry, @function\n"
".p2align 4\n"
"plat_fiber_entry:\n"
" andq $-16, %rsp\n" // fiber_swap's ret leaves rsp 8 off: the SysV ABI wants it aligned at the call
" movq %r13, %rdi\n"
" callq *%r12\n"
" ud2\n"
".size plat_fiber_entry, .-plat_fiber_entry\n");
static __thread PlatFiber *fiber_cur;
static __thread PlatFiber fiber_own;
PlatFiber *plat_fiber_self(void) {
fiber_own = (PlatFiber){0};
fiber_cur = &fiber_own;
return &fiber_own;
}
void plat_fiber_unself(void) { fiber_cur = NULL; }
PlatFiber *plat_fiber_new(size_t stack, void (*fn)(void *arg), void *arg) {
PlatFiber *f = calloc(1, sizeof *f);
if (!f) return NULL;
// Reserved, not committed; the lowest page stays inaccessible, so an overflow faults instead of corrupting.
void *m = mmap(NULL, stack, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_STACK, -1, 0);
if (m == MAP_FAILED) {
free(f);
return NULL;
}
mprotect(m, 4096, PROT_NONE);
f->stack = m, f->size = stack;
uint64_t *top = (uint64_t *)(((uintptr_t)m + stack) & ~(uintptr_t)15);
uint64_t *sp = top - 10; // fpu state (2 words), r15, r14, r13, r12, rbx, rbp, return address, padding
sp[0] = 0x037f; // x87 control word: the default
sp[1] = 0x1f80; // MXCSR: the default
sp[2] = sp[3] = 0; // r15, r14
sp[4] = (uint64_t)(uintptr_t)arg; // r13
sp[5] = (uint64_t)(uintptr_t)fn; // r12
sp[6] = sp[7] = 0; // rbx, rbp
sp[8] = (uint64_t)(uintptr_t)plat_fiber_entry;
sp[9] = 0;
f->sp = sp;
return f;
}
void plat_fiber_switch(PlatFiber *to) {
PlatFiber *from = fiber_cur;
fiber_cur = to; // on this thread; the switch back into from sets it on whichever thread that is
plat_fiber_swap(&from->sp, to->sp);
}
void plat_fiber_free(PlatFiber *f) {
if (!f) return;
if (f->stack) munmap(f->stack, f->size);
free(f);
}
int plat_thread_spawn(PlatThreadFn fn, void *arg, const char *name, size_t stack) {
PlatThread *t = plat_thread_new_ex(fn, arg, name, stack);
if (t) plat_thread_detach(t);
return t != NULL;
}
void plat_thread_join(PlatThread *t) {
pthread_join(t->t, NULL);
free(t);
}
int plat_thread_join_ns(PlatThread *t, uint64_t ns) {
struct timespec at;
clock_gettime(CLOCK_REALTIME, &at); // pthread_timedjoin_np takes a CLOCK_REALTIME deadline
uint64_t end = (uint64_t)at.tv_sec * 1000000000u + (uint64_t)at.tv_nsec + ns;
at.tv_sec = (time_t)(end / 1000000000u), at.tv_nsec = (long)(end % 1000000000u);
if (pthread_timedjoin_np(t->t, NULL, &at)) return 0;
free(t);
return 1;
}
// The thread keeps using t->fn and t->arg, so the struct stays allocated (a few bytes per detached thread).
void plat_thread_detach(PlatThread *t) { pthread_detach(t->t); }
// ---- a core for the recording thread (plat_cpu_reserve)
// The processors a sysfs CPU list ("0-15", "0,2,4-7") names, added to *set; 0 when s is not such a list.
static int cpu_list_parse(const char *s, cpu_set_t *set) {
for (;;) {
char *e;
long a = strtol(s, &e, 10), b = a;
if (e == s || a < 0 || a >= CPU_SETSIZE) return 0;
if (*e == '-') {
s = e + 1;
b = strtol(s, &e, 10);
if (e == s || b < a || b >= CPU_SETSIZE) return 0;
}
for (long i = a; i <= b; i++) CPU_SET((int)i, set);
if (*e != ',') return *e == '\n' || !*e;
s = e + 1;
}
}
static int cpu_list_file(const char *path, cpu_set_t *set) {
char text[512];
FILE *f = fopen(path, "r");
if (!f) return 0;
size_t n = fread(text, 1, sizeof text - 1, f);
fclose(f);
text[n] = 0;
CPU_ZERO(set);
return cpu_list_parse(text, set);
}
// cpu<n>/<file> under root/system/cpu holds one number per processor (cpu_capacity, cpufreq/cpuinfo_max_freq). When
// the processors of `in` differ in it by more than a tenth, *out gets the ones within a tenth of the highest and the
// result is 1; otherwise 0 and *out stays as it was. A processor without the file counts as the lowest.
static int cpus_highest_by(const char *root, const char *file, const cpu_set_t *in, cpu_set_t *out) {
uint64_t v[CPU_SETSIZE], top = 0, low = UINT64_MAX;
for (int i = 0; i < CPU_SETSIZE; i++) {
char path[160];
v[i] = 0;
if (!CPU_ISSET(i, in)) continue;
snprintf(path, sizeof path, "%s/system/cpu/cpu%d/%s", root, i, file);
if (!read_u64_file(path, &v[i])) v[i] = 0;
if (v[i] > top) top = v[i];
if (v[i] < low) low = v[i];
}
if (low * 10 >= top * 9) return 0;
CPU_ZERO(out);
for (int i = 0; i < CPU_SETSIZE; i++)
if (CPU_ISSET(i, in) && v[i] * 10 >= top * 9) CPU_SET(i, out);
return 1;
}
// The processors of `all` on the fastest kind of core, and what makes them so. A hybrid Intel CPU lists its performance
// cores in root/cpu_core/cpus; for the others, cores of different capacity or top clock (a tenth apart or less counts
// as the same) keep the highest; a CPU with one kind of core keeps all of `all`.
static const char *cpus_fast(const char *root, const cpu_set_t *all, cpu_set_t *fast) {
cpu_set_t perf;
char path[160];
snprintf(path, sizeof path, "%s/cpu_core/cpus", root);
if (cpu_list_file(path, &perf)) {
CPU_AND(fast, &perf, all);
if (CPU_COUNT(fast)) return "a performance core";
}
if (cpus_highest_by(root, "cpu_capacity", all, fast)) return "a core of the highest capacity";
if (cpus_highest_by(root, "cpufreq/cpuinfo_max_freq", all, fast)) return "a core of the highest clock";
*fast = *all;
return "a core";
}
// The processors plat_cpu_reserve takes into *mine: every processor of `all` on the fast core (cpus_fast) that clocks
// highest, the highest-numbered one when several do. A core with a processor outside the fast ones is no fast core.
// Returns how many processors, 0 when fewer than four cores are fast: one of them is then too much to lose. *kind
// names what makes the core fast.
static int cpu_pick_core(const char *root, const cpu_set_t *all, cpu_set_t *mine, const char **kind) {
cpu_set_t fast;
*kind = cpus_fast(root, all, &fast);
int cores = 0;
uint64_t best = 0;
for (int i = 0; i < CPU_SETSIZE; i++) {
char path[160];
cpu_set_t sib, in;
uint64_t freq = 0;
if (!CPU_ISSET(i, &fast)) continue;
snprintf(path, sizeof path, "%s/system/cpu/cpu%d/topology/thread_siblings_list", root, i);
if (!cpu_list_file(path, &sib)) CPU_ZERO(&sib);
CPU_SET(i, &sib);
CPU_AND(&sib, &sib, all);
int first = 0;
while (!CPU_ISSET(first, &sib)) first++;
CPU_AND(&in, &sib, &fast);
if (first != i || !CPU_EQUAL(&in, &sib)) continue; // counted at its first processor, or not fast
snprintf(path, sizeof path, "%s/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", root, i);
read_u64_file(path, &freq);
if (++cores == 1 || freq >= best) best = freq, *mine = sib;
}
return cores < 4 ? 0 : CPU_COUNT(mine);
}
// The calling thread gets a fast core to itself (cpu_pick_core): every other thread of the process now running loses
// the core's processors, and the caller keeps only them. Threads the caller starts later through plat_thread_new_ex
// get the others; a thread started by another one inherits that one's mask, which no longer has the core.
// plat_process_affinity leaves the core with the caller. Returns the processors reserved, 0 when no core is fast
// enough or the caller's mask cannot be set; *which (when not NULL) receives them as text, such as "10,11 of a
// performance core".
int plat_cpu_reserve(char *which, size_t size) {
cpu_set_t all, mine, rest;
const char *kind;
pid_t me = (pid_t)syscall(SYS_gettid);
if (which && size) which[0] = 0;
if (sched_getaffinity(0, sizeof all, &all) || !cpu_pick_core("/sys/devices", &all, &mine, &kind)) return 0;
CPU_XOR(&rest, &all, &mine);
plat_mutex_lock(&cpu_lock);
if (sched_setaffinity(0, sizeof mine, &mine)) {
plat_mutex_unlock(&cpu_lock);
return 0;
}
cpu_rest = rest, cpu_rest_set = 1;
cpu_taken = mine, cpu_taker = me, cpu_others = rest;
// A thread started during a pass by one it has not reached yet is found by the next.
for (int pass = 0, moved = 1; moved && pass < 3; pass++) {
DIR *d = opendir("/proc/self/task");
moved = 0;
for (struct dirent *e; d && (e = readdir(d));) {
pid_t tid = (pid_t)atoi(e->d_name);
cpu_set_t cur, now;
if (tid <= 0 || tid == me || sched_getaffinity(tid, sizeof cur, &cur)) continue;
now = cur;
cpu_clear(&now, &mine);
if (CPU_EQUAL(&now, &cur)) continue;
if (!CPU_COUNT(&now)) now = rest; // it was pinned to the core alone
if (!sched_setaffinity(tid, sizeof now, &now)) moved = 1;
}
if (d) closedir(d);
}
plat_mutex_unlock(&cpu_lock);
if (which && size) {
size_t n = 0;
for (int i = 0; i < CPU_SETSIZE && n < size; i++)
if (CPU_ISSET(i, &mine)) n += (size_t)snprintf(which + n, size - n, "%s%d", n ? "," : "", i);
if (n < size) snprintf(which + n, size - n, " of %s", kind);
}
return CPU_COUNT(&mine);
}
// A thread the holder starts by other means than plat_thread_new_ex (a library's audio thread, a driver's worker)
// inherits its mask, the core alone, after plat_cpu_reserve's sweep ran, and plat_process_affinity seldom runs again:
// each thread whose mask lies within the core, the holder aside, gets the others' processors here. A thread with
// anything beside the core in its mask keeps it. Once the holder has ended, nothing would use the core, so it goes back:
// the threads with the others' mask get its processors again and the reservation ends. Returns the threads moved, -1
// when the core went back.
int plat_cpu_reserve_sweep(void) {
plat_mutex_lock(&cpu_lock);
if (!cpu_taker) {
plat_mutex_unlock(&cpu_lock);
return 0;
}
char path[48];
snprintf(path, sizeof path, "/proc/self/task/%d", (int)cpu_taker);
int ended = access(path, F_OK) != 0, moved = 0;
cpu_set_t back;
CPU_OR(&back, &cpu_others, &cpu_taken);
DIR *d = opendir("/proc/self/task");
for (struct dirent *e; d && (e = readdir(d));) {
pid_t tid = (pid_t)atoi(e->d_name);
cpu_set_t cur, in;
if (tid <= 0 || tid == cpu_taker || sched_getaffinity(tid, sizeof cur, &cur)) continue;
if (ended) {
if (CPU_EQUAL(&cur, &cpu_others) && !sched_setaffinity(tid, sizeof back, &back)) moved++;
continue;
}
CPU_AND(&in, &cur, &cpu_taken);
if (CPU_COUNT(&cur) && CPU_EQUAL(&in, &cur) && !sched_setaffinity(tid, sizeof cpu_others, &cpu_others)) moved++;
}
if (d) closedir(d);
if (ended) {
cpu_taker = 0;
CPU_ZERO(&cpu_taken);
}
plat_mutex_unlock(&cpu_lock);
return ended ? -1 : moved;
}
uint64_t plat_thread_affinity(PlatThread *t, uint64_t mask) {
pthread_t th = t ? t->t : pthread_self();
cpu_set_t old, set;
if (pthread_getaffinity_np(th, sizeof old, &old)) return 0;
CPU_ZERO(&set);
for (int i = 0; i < 64; i++)
if (mask >> i & 1) CPU_SET(i, &set);
if (pthread_setaffinity_np(th, sizeof set, &set)) return 0;
uint64_t prev = 0;
for (int i = 0; i < 64; i++)
if (CPU_ISSET(i, &old)) prev |= 1ull << i;
return prev;
}
uint32_t plat_cpu_count(void) {
long n = sysconf(_SC_NPROCESSORS_ONLN);
return n > 0 ? (uint32_t)n : 1;
}
// The processors of the process's affinity mask (taskset, a launcher's CPU set), every online one when it is unknown.
uint32_t plat_cpu_usable(void) {
cpu_set_t set;
if (!sched_getaffinity(0, sizeof set, &set) && CPU_COUNT(&set) > 0) return (uint32_t)CPU_COUNT(&set);
return plat_cpu_count();
}
// Windows' levels map to nice values: IDLE 19, LOWEST 4, BELOW_NORMAL 2, NORMAL 0, ABOVE_NORMAL -2, HIGHEST -5,
// TIME_CRITICAL -10. Applied only when the process may lower a nice value again (platform.h says why).
static int nice_of(int prio) {
return prio <= PLAT_PRIO_IDLE ? 19 : prio == PLAT_PRIO_LOWEST ? 4 : prio == PLAT_PRIO_BELOW_NORMAL ? 2
: prio == PLAT_PRIO_NORMAL ? 0 : prio == PLAT_PRIO_ABOVE_NORMAL ? -2 : prio == PLAT_PRIO_HIGHEST ? -5 : -10;
}
static int may_renice(int nice) {
static int lowest = 100; // the lowest nice value this process may set; 100 until probed
if (lowest == 100) {
struct rlimit r;
int l = !getrlimit(RLIMIT_NICE, &r) && r.rlim_cur != RLIM_INFINITY ? 20 - (int)r.rlim_cur : -20;
// CAP_SYS_NICE shows as being allowed to set a nice value below the limit's
pid_t tid = (pid_t)syscall(SYS_gettid);
errno = 0;
int cur = getpriority(PRIO_PROCESS, (id_t)tid);
if (!errno && l > cur - 1 && !setpriority(PRIO_PROCESS, (id_t)tid, cur - 1)) {
setpriority(PRIO_PROCESS, (id_t)tid, cur);
l = -20;
}
__atomic_store_n(&lowest, l, __ATOMIC_RELAXED);
}
return lowest <= 0 && nice >= lowest;
}
static void renice(pid_t tid, int prio) {
int nice = nice_of(prio);
if (tid > 0 && may_renice(nice)) setpriority(PRIO_PROCESS, (id_t)tid, nice);
}
void plat_thread_priority(int prio) { renice((pid_t)syscall(SYS_gettid), prio); }
void plat_thread_set_priority(PlatThread *t, int prio) { renice(__atomic_load_n(&t->tid, __ATOMIC_ACQUIRE), prio); }
int plat_thread_priority_get(void) {
errno = 0;
int nice = getpriority(PRIO_PROCESS, (id_t)syscall(SYS_gettid));
if (errno) return PLAT_PRIO_NORMAL;
return nice >= 19 ? PLAT_PRIO_IDLE : nice >= 4 ? PLAT_PRIO_LOWEST : nice >= 2 ? PLAT_PRIO_BELOW_NORMAL
: nice >= 0 ? PLAT_PRIO_NORMAL : nice >= -2 ? PLAT_PRIO_ABOVE_NORMAL : nice >= -5 ? PLAT_PRIO_HIGHEST
: PLAT_PRIO_TIME_CRITICAL;
}
void plat_yield(void) { sched_yield(); }
// ---- time
uint64_t plat_now_ns(void) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (uint64_t)t.tv_sec * 1000000000u + (uint64_t)t.tv_nsec;
}
uint64_t plat_counter(void) { return plat_now_ns(); }
uint64_t plat_counter_hz(void) { return 1000000000u; }
// The timer slack (50 us by default) is how late the kernel may wake a sleeping thread to batch wakeups. New threads
// inherit it from the thread that creates them.
void plat_timer_1ms(void) { prctl(PR_SET_TIMERSLACK, 1000ul, 0, 0, 0); }
int plat_power_full(void) { return 0; } // no per-process power throttling to opt out of
// clock_nanosleep wakes within the timer slack (50 us by default) of its deadline; the last PLAT_SPIN_NS are spent
// yielding so the deadline is met closely, as on Windows.
#define PLAT_SPIN_NS 100000u
void plat_sleep_ns(uint64_t ns) {
uint64_t end = plat_now_ns() + ns;
if (ns > PLAT_SPIN_NS) {
uint64_t at = end - PLAT_SPIN_NS;
struct timespec ts = {(time_t)(at / 1000000000u), (long)(at % 1000000000u)};
while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL) == EINTR) {}
}
while (plat_now_ns() < end) sched_yield();
}
void plat_wait_ns(uint64_t ns) {
struct timespec ts = {(time_t)(ns / 1000000000u), (long)(ns % 1000000000u)};
while (nanosleep(&ts, &ts) == -1 && errno == EINTR) {}
}
void plat_sleep_ms(uint32_t ms) {
struct timespec ts = {(time_t)(ms / 1000), (long)(ms % 1000) * 1000000};
while (nanosleep(&ts, &ts) == -1 && errno == EINTR) {}
}
// ---- locks: a reader-writer lock in one 32-bit word, zero when free.
// bit 31: a writer holds it; bit 30: some thread sleeps on the word; bits 0-29: readers holding it.
// A thread that cannot take the lock sets the sleeper bit and waits on the futex; every release that finds the bit
// clears it and wakes all sleepers, which retry (and set it again if they still cannot get in). Readers do not enter
// while the sleeper bit is set, so a waiting writer is not starved by a stream of readers.
#define LK_W 0x80000000u
#define LK_S 0x40000000u
#define LK_R 0x3fffffffu
#define LK_SPINS 64
static long futex(volatile uint32_t *w, int op, uint32_t val, const struct timespec *ts) {
return syscall(SYS_futex, w, op | FUTEX_PRIVATE_FLAG, val, ts, NULL, 0);
}
static inline volatile uint32_t *lk(PlatMutex *m) { return (volatile uint32_t *)&m->p; }
// Sets the sleeper bit on s and sleeps while the word still equals the result.
static void lk_sleep(volatile uint32_t *w, uint32_t s) {
if (!(s & LK_S) && !__atomic_compare_exchange_n(w, &s, s | LK_S, 0, __ATOMIC_RELAXED, __ATOMIC_RELAXED)) return;
futex(w, FUTEX_WAIT, s | LK_S, NULL);
}
static void lk_wake(volatile uint32_t *w) {
__atomic_fetch_and(w, ~LK_S, __ATOMIC_RELAXED);
futex(w, FUTEX_WAKE, INT_MAX, NULL);
}
void plat_mutex_init(PlatMutex *m) { m->p = 0; }
int plat_mutex_trylock(PlatMutex *m) {
volatile uint32_t *w = lk(m);
uint32_t s = __atomic_load_n(w, __ATOMIC_RELAXED);
while (!(s & (LK_W | LK_R)))
if (__atomic_compare_exchange_n(w, &s, s | LK_W, 1, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) return 1;
return 0;
}
// Lock-wait timing is Windows only (platform.h): here the note is kept and never called.
void plat_lock_timing(PlatLockNote fn) { (void)fn; }
void plat_mutex_lock_at(PlatMutex *m, void *site) { (void)site; plat_mutex_lock(m); }
void plat_mutex_lock(PlatMutex *m) {
volatile uint32_t *w = lk(m);
for (int spin = 0;; spin++) {
uint32_t s = __atomic_load_n(w, __ATOMIC_RELAXED);
if (!(s & (LK_W | LK_R))) {
if (__atomic_compare_exchange_n(w, &s, s | LK_W, 1, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) return;
continue;
}
if (spin < LK_SPINS) plat_pause();
else lk_sleep(w, s);
}
}
void plat_mutex_unlock(PlatMutex *m) {
volatile uint32_t *w = lk(m);
uint32_t s = __atomic_fetch_and(w, ~LK_W, __ATOMIC_RELEASE);
if (s & LK_S) lk_wake(w);
}
int plat_mutex_trylock_shared(PlatMutex *m) {
volatile uint32_t *w = lk(m);
uint32_t s = __atomic_load_n(w, __ATOMIC_RELAXED);
while (!(s & (LK_W | LK_S)))
if (__atomic_compare_exchange_n(w, &s, s + 1, 1, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) return 1;
return 0;
}
void plat_mutex_lock_shared(PlatMutex *m) {
volatile uint32_t *w = lk(m);
for (int spin = 0;; spin++) {
uint32_t s = __atomic_load_n(w, __ATOMIC_RELAXED);
if (!(s & (LK_W | LK_S))) {
if (__atomic_compare_exchange_n(w, &s, s + 1, 1, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) return;
continue;
}
if (spin < LK_SPINS) plat_pause();
else lk_sleep(w, s);
}
}
void plat_mutex_unlock_shared(PlatMutex *m) {
volatile uint32_t *w = lk(m);
uint32_t s = __atomic_sub_fetch(w, 1, __ATOMIC_RELEASE);
if (!(s & LK_R) && (s & LK_S)) lk_wake(w);
}
// ---- condition variables: a sequence number in the first 32 bits. A waiter reads it before releasing the lock and
// sleeps only while it is unchanged, so a signal between the release and the sleep is not lost.
static inline volatile uint32_t *cv(PlatCond *c) { return (volatile uint32_t *)&c->p; }
void plat_cond_init(PlatCond *c) { c->p = 0; }
void plat_cond_wait(PlatCond *c, PlatMutex *m) {
uint32_t seq = __atomic_load_n(cv(c), __ATOMIC_RELAXED);
plat_mutex_unlock(m);
futex(cv(c), FUTEX_WAIT, seq, NULL);
plat_mutex_lock(m);
}
int plat_cond_wait_ns(PlatCond *c, PlatMutex *m, uint64_t ns) {
uint32_t seq = __atomic_load_n(cv(c), __ATOMIC_RELAXED);
struct timespec ts = {(time_t)(ns / 1000000000u), (long)(ns % 1000000000u)};
plat_mutex_unlock(m);
long r = futex(cv(c), FUTEX_WAIT, seq, &ts);
int timed_out = r == -1 && errno == ETIMEDOUT;
plat_mutex_lock(m);
return !timed_out;
}
void plat_cond_signal(PlatCond *c) {
__atomic_add_fetch(cv(c), 1, __ATOMIC_RELEASE);
futex(cv(c), FUTEX_WAKE, 1, NULL);
}
void plat_cond_broadcast(PlatCond *c) {
__atomic_add_fetch(cv(c), 1, __ATOMIC_RELEASE);
futex(cv(c), FUTEX_WAKE, INT_MAX, NULL);
}
// ---- files
int plat_file_exists(const char *path) {
struct stat st;
return !stat(path, &st) && !S_ISDIR(st.st_mode);
}
int plat_dir_exists(const char *path) {
struct stat st;
return !stat(path, &st) && S_ISDIR(st.st_mode);
}
int plat_full_path(const char *path, char *out, size_t cap) {
if (path[0] == '/') return snprintf(out, cap, "%s", path) < (int)cap;
char cwd[PATH_MAX];
if (!getcwd(cwd, sizeof cwd)) return 0;
return snprintf(out, cap, "%s/%s", cwd, path) < (int)cap;
}
void plat_file_mkdir_p(const char *path) {
char tmp[PATH_MAX];
if (snprintf(tmp, sizeof tmp, "%s", path) >= (int)sizeof tmp) return;
for (char *p = tmp + 1; *p; p++) {
if (*p == '/' || *p == '\\') {
char c = *p;
*p = 0;
mkdir(tmp, 0755);
*p = c;
}
}
mkdir(tmp, 0755);
}
static int write_all(int fd, const void *data, size_t n) {
const uint8_t *p = data;
while (n) {
ssize_t k = write(fd, p, n);
if (k < 0 && errno == EINTR) continue;
if (k <= 0) return 0;
p += k, n -= (size_t)k;
}
return 1;
}
uint8_t *plat_file_read(const char *path, uint64_t *size) {
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) return NULL;
struct stat st;
uint8_t *buf = NULL;
if (!fstat(fd, &st) && S_ISREG(st.st_mode) && st.st_size > 0 && st.st_size < (1ll << 30)) {
buf = malloc((size_t)st.st_size + 1);
size_t got = 0;
while (buf && got < (size_t)st.st_size) {
ssize_t k = read(fd, buf + got, (size_t)st.st_size - got);
if (k < 0 && errno == EINTR) continue;
if (k <= 0) break;
got += (size_t)k;
}
if (buf && got != (size_t)st.st_size) {
free(buf);
buf = NULL;
}
if (buf) {
buf[st.st_size] = 0;
*size = (uint64_t)st.st_size;
}
}
close(fd);
return buf;
}
int plat_file_write(const char *path, const void *data, size_t n) {
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd < 0) return 0;
int ok = write_all(fd, data, n);
return close(fd) == 0 && ok;
}
// O_APPEND makes each write land whole at the end of the file, from any thread.
PlatAppend *plat_append_open(const char *path) {
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_APPEND | O_CLOEXEC, 0644);
return fd < 0 ? NULL : (PlatAppend *)(intptr_t)(fd + 1);
}
int plat_append_write(PlatAppend *f, const void *data, size_t n) {
int fd = (int)(intptr_t)f - 1;
ssize_t k;
do k = write(fd, data, n);
while (k < 0 && errno == EINTR);
return k == (ssize_t)n;
}
// Flushes the directory holding path, so a rename into it survives a crash.
static void sync_dir_of(const char *path) {
char dir[PATH_MAX];
if (snprintf(dir, sizeof dir, "%s", path) >= (int)sizeof dir) return;
char *slash = strrchr(dir, '/');
if (slash == dir) slash[1] = 0;
else if (slash) *slash = 0;
else strcpy(dir, ".");
int fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd >= 0) {
fsync(fd);
close(fd);
}
}
int plat_file_replace(const char *from, const char *to, int durable) {
if (rename(from, to)) return 0;
if (durable) sync_dir_of(to);
return 1;
}
static char lock_held[4][1100];
static int lock_fd[4];
static int lock_n;
static PlatMutex lock_lock = PLAT_MUTEX_INIT;
int plat_lock_file(const char *path) {
plat_mutex_lock(&lock_lock);
int r = 1, nheld = lock_n;
for (int i = 0; i < nheld; i++)
if (!strcmp(lock_held[i], path)) goto done;
int fd = open(path, O_RDWR | O_CREAT | O_CLOEXEC, 0644); // open until plat_unlock_file or the process ends
if (fd < 0) {
r = -1;
goto done;
}
if (flock(fd, LOCK_EX | LOCK_NB)) {
r = errno == EWOULDBLOCK ? 0 : -1;
close(fd);
goto done;
}
if (nheld < 4) {
snprintf(lock_held[nheld], sizeof lock_held[0], "%s", path);
lock_fd[nheld] = fd;
lock_n = nheld + 1;
}
done:
plat_mutex_unlock(&lock_lock);
return r;
}
void plat_unlock_file(const char *path) {
plat_mutex_lock(&lock_lock);
for (int i = 0; i < lock_n; i++)
if (!strcmp(lock_held[i], path)) {
close(lock_fd[i]);
lock_n--;
memmove(&lock_held[i], &lock_held[i + 1], sizeof lock_held[0] * (size_t)(lock_n - i));
memmove(&lock_fd[i], &lock_fd[i + 1], sizeof lock_fd[0] * (size_t)(lock_n - i));
break;
}
plat_mutex_unlock(&lock_lock);
}
// path.<pid>.<tid>.tmp renamed over path; durable: the file and then its folder are synced to the disk.
static int write_swap(const char *path, const void *data, size_t n, int durable) {
// A rename replaces a read-only file here (only the folder's permission counts) but not on Windows: a file the
// player made read-only stays as it is on both.
if (access(path, W_OK) && errno != ENOENT) return 0;
char tmp[PATH_MAX + 48];
int len = snprintf(tmp, sizeof tmp, "%s.%u.%u.tmp", path, plat_process_id(), plat_thread_id());
if (len < 0 || len >= (int)sizeof tmp) return 0;
int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd < 0) return 0;
int ok = write_all(fd, data, n) && (!durable || fsync(fd) == 0);
ok = close(fd) == 0 && ok;
if (ok && !rename(tmp, path)) {
if (durable) sync_dir_of(path);
return 1;
}
unlink(tmp);
return 0;
}
int plat_file_write_atomic(const char *path, const void *data, size_t n) { return write_swap(path, data, n, 1); }
int plat_file_write_cached(const char *path, const void *data, size_t n) { return write_swap(path, data, n, 0); }
int plat_file_flush(const char *path) {
int fd = open(path, O_WRONLY | O_CLOEXEC);
if (fd < 0) return 0;
int ok = fsync(fd) == 0;
close(fd);
return ok;
}
int plat_file_sync(FILE *f) { return !fflush(f) && !fsync(fileno(f)); }
void plat_file_delete(const char *path) { unlink(path); }
int plat_file_truncate(const char *path, uint64_t size) { return truncate(path, (off_t)size) == 0; }
int64_t plat_file_size(const char *path) {
struct stat st;
return stat(path, &st) ? -1 : (int64_t)st.st_size;
}
// The owner's write permission stands for Windows' read-only attribute.
int plat_file_set_readonly(const char *path, int on) {
struct stat st;
if (stat(path, &st)) return 0;
mode_t m = on ? st.st_mode & ~(mode_t)0222 : st.st_mode | S_IWUSR;
return chmod(path, m & 07777) == 0;
}
int plat_file_list(const char *dir, const char *ext, int (*cb)(const char *name, void *ctx), void *ctx) {
DIR *d = opendir(dir);
if (!d) return 0;
size_t el = strlen(ext);
for (struct dirent *e; (e = readdir(d));) {
size_t n = strlen(e->d_name);
if (n < el || strcmp(e->d_name + n - el, ext) || e->d_name[0] == '.') continue;
if (!cb(e->d_name, ctx)) break;
}
closedir(d);
return 1;
}
// ---- virtual memory. Reservations are PROT_NONE MAP_NORESERVE mappings; commit is mprotect read-write, decommit
// MADV_DONTNEED (private anonymous pages read zero afterwards) and PROT_NONE. Linux keeps no committed/reserved
// distinction, so a byte per guest page records it (pstate, mapped lazily: only the pages touched cost memory).
//
// Write watch: a userfaultfd in asynchronous write-protect mode (Linux 6.7) over the watched reservations. The kernel
// clears a page's protection on the first write without a round trip to us, and PAGEMAP_SCAN lists the pages whose
// protection was cleared and protects them again. Without it (an older kernel), plat_mem_written reports the ranges as
// untracked and the back end uploads them in full.
#define PG 4096ull
#define PS_LIMIT (1ull << 39)
enum { PS_FREE, PS_RESERVED, PS_NONE, PS_READ, PS_RW }; // PS_NONE.. are committed: PLAT_PROT_* + PS_NONE
static uint8_t *pstate;
static int ww_fd = -1, pm_fd = -1; // userfaultfd and /proc/self/pagemap; -1 when write watch is unavailable
static PlatMutex vm_lock = PLAT_MUTEX_INIT;
static void ps_set(uint64_t addr, uint64_t n, uint8_t v) {
if (addr < PS_LIMIT) memset(pstate + addr / PG, v, (size_t)((n + PG - 1) / PG));
}
static void ww_open(void) {
int fd = (int)syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY);
struct uffdio_api api = {.api = UFFD_API,
.features = UFFD_FEATURE_WP_ASYNC | UFFD_FEATURE_WP_UNPOPULATED};
if (fd < 0 || ioctl(fd, UFFDIO_API, &api)) {
fprintf(stderr, "platform: no asynchronous userfaultfd write protection (%s): written memory is not tracked\n",
strerror(errno));
if (fd >= 0) close(fd);
return;
}
pm_fd = open("/proc/self/pagemap", O_RDONLY | O_CLOEXEC);
if (pm_fd < 0) {
close(fd);
return;
}
ww_fd = fd;
}
int plat_mem_reserve(uint64_t addr, uint64_t n, int watch) {
plat_mutex_lock(&vm_lock);
if (!pstate) {
void *p = mmap(NULL, PS_LIMIT / PG, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
pstate = p == MAP_FAILED ? NULL : p;
}
if (watch && ww_fd < 0 && pm_fd < 0) ww_open();
void *p = pstate && addr + n <= PS_LIMIT
? mmap((void *)(uintptr_t)addr, n, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0)
: MAP_FAILED;
int ok = p == (void *)(uintptr_t)addr;
if (p != MAP_FAILED && !ok) munmap(p, n); // a kernel without MAP_FIXED_NOREPLACE put it elsewhere
if (ok) {
ps_set(addr, n, PS_RESERVED);
// Transparent huge pages would populate (and count as written) 2 MiB at the first write of one page; writes are
// tracked per 4 KiB page, as on Windows.
if (watch) madvise(p, n, MADV_NOHUGEPAGE);
struct uffdio_register reg = {.range = {addr, n}, .mode = UFFDIO_REGISTER_MODE_WP};
if (watch && ww_fd >= 0 && ioctl(ww_fd, UFFDIO_REGISTER, &reg))
fprintf(stderr, "platform: write watch over %#llx+%#llx failed (%s)\n", (unsigned long long)addr,
(unsigned long long)n, strerror(errno));
}
plat_mutex_unlock(&vm_lock);
return ok;
}
void *plat_mem_alloc(size_t n) {
void *p = mmap(NULL, n, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
return p == MAP_FAILED ? NULL : p;
}
int plat_mem_commit(uint64_t addr, size_t n) {
if (mprotect((void *)(uintptr_t)addr, n, PROT_READ | PROT_WRITE)) return 0;
ps_set(addr, n, PS_RW);
return 1;
}
int plat_mem_decommit(uint64_t addr, uint64_t n) {
if (madvise((void *)(uintptr_t)addr, n, MADV_DONTNEED) || mprotect((void *)(uintptr_t)addr, n, PROT_NONE)) return 0;
ps_set(addr, n, PS_RESERVED);
return 1;
}
int plat_mem_protect(uint64_t addr, uint64_t n, int prot) {
int p = prot == PLAT_PROT_RW ? PROT_READ | PROT_WRITE : prot == PLAT_PROT_READ ? PROT_READ : PROT_NONE;
if (mprotect((void *)(uintptr_t)addr, n, p)) return 0;
ps_set(addr, n, (uint8_t)(PS_NONE + prot));
return 1;
}
int plat_mem_state(uint64_t addr, uint64_t limit, uint64_t *end) {
if (!pstate || addr >= PS_LIMIT) {
*end = limit;
return PLAT_MEM_FREE;
}
uint64_t lim = limit < PS_LIMIT ? limit : PS_LIMIT, i = addr / PG;
uint8_t v = pstate[i];
while ((i + 1) * PG < lim && pstate[i + 1] == v) i++;
*end = (i + 1) * PG < lim ? (i + 1) * PG : lim;
return v == PS_FREE ? PLAT_MEM_FREE : v == PS_RESERVED ? PLAT_MEM_RESERVED : v - PS_NONE;
}
int plat_mem_census(uint64_t *priv, uint64_t *wc, uint64_t *wc_allocs) {
*priv = *wc = *wc_allocs = 0;
return 0;
}
int plat_mem_prepare_write(uint64_t addr, uint64_t n) {
if (ww_fd < 0 || !n || addr >= PS_LIMIT || n > PS_LIMIT - addr) return 0;
uint64_t lo = addr & ~(PG - 1), hi = (addr + n + PG - 1) & ~(PG - 1);
struct uffdio_writeprotect wp = {.range = {lo, hi - lo}, .mode = 0};
// PAGEMAP_SCAN observes cleared protection as written, just as after an asynchronous write fault.
return ioctl(ww_fd, UFFDIO_WRITEPROTECT, &wp) == 0;
}
uint64_t plat_mem_written(uint64_t addr, uint64_t n, uint64_t *pages, uint64_t max, int reset) {
if (!max || !n) return 0;
if (ww_fd < 0) return UINT64_MAX;
struct page_region r[256];
uint64_t got = 0;
for (uint64_t at = addr, end = addr + n; at < end && got < max;) {
// Written means: write-protection cleared, on a populated page (present or swapped out) that is not the shared
// zero page. A page never touched since its commit has no protection marker either and counts as written to
// the kernel; a read maps the zero page there. Neither is a write, as on Windows.
struct pm_scan_arg arg = {.size = sizeof arg,
.flags = reset ? PM_SCAN_WP_MATCHING | PM_SCAN_CHECK_WPASYNC : 0,
.start = at, .end = end, .vec = (uint64_t)(uintptr_t)r, .vec_len = 256,
.max_pages = max - got,
.category_inverted = PAGE_IS_PFNZERO,
.category_mask = PAGE_IS_WRITTEN | PAGE_IS_PFNZERO,
.category_anyof_mask = PAGE_IS_PRESENT | PAGE_IS_SWAPPED,
.return_mask = PAGE_IS_WRITTEN};
long k = ioctl(pm_fd, PAGEMAP_SCAN, &arg);
if (k < 0) return UINT64_MAX;
for (long i = 0; i < k; i++)
for (uint64_t p = r[i].start; p < r[i].end && got < max; p += PG) pages[got++] = p;
if (arg.walk_end <= at) break;
at = arg.walk_end;
}
return got;
}
// One handler for the synchronous faults. A SIGSEGV whose page-fault error code has the write bit set goes to the
// write-fault hook first (memory.c thaws a frozen region); any other fault, in this executable or in a library (Mesa,
// SDL), then goes to the crash hook (main.c), which reports it and ends the process. Without a crash hook the fault
// gets the default action. Runs on the thread's signal stack.
static int (*fault_hook)(uint64_t addr);
static int (*access_hook)(uint64_t addr, int write, uint64_t ip); // every read or write fault, before fault_hook
static void (*crash_fn)(const PlatCrash *c);
static void fault_handler(int sig, siginfo_t *si, void *uc) {
ucontext_t *u = uc;
long err = u->uc_mcontext.gregs[REG_ERR];
if (sig == SIGSEGV && access_hook && !(err & 16) &&
access_hook((uint64_t)(uintptr_t)si->si_addr, (err & 2) != 0, (uint64_t)u->uc_mcontext.gregs[REG_RIP]))
return;
if (sig == SIGSEGV && fault_hook && (err & 2) && fault_hook((uint64_t)(uintptr_t)si->si_addr)) return;
uintptr_t rip = (uintptr_t)u->uc_mcontext.gregs[REG_RIP];
Dl_info at, self;
if (crash_fn) {
int found = dladdr((void *)rip, &at) && at.dli_fname;
PlatCrash c = {sig == SIGSEGV ? "access violation" : sig == SIGBUS ? "bus error"
: sig == SIGILL ? "illegal instruction" : "arithmetic exception",
(uint32_t)sig, rip, found ? rip - (uintptr_t)at.dli_fbase : rip, -1, 0};
if (!found) {
c.module = "";
} else if (!dladdr((void *)plat_crash_hook, &self) || at.dli_fbase != self.dli_fbase) {
const char *n = strrchr(at.dli_fname, '/');
c.module = n ? n + 1 : at.dli_fname;
}
if (sig == SIGSEGV || sig == SIGBUS)
c.access = (err & 16) ? 8 : (err & 2) ? 1 : 0, c.addr = (uint64_t)(uintptr_t)si->si_addr;
crash_fn(&c);
}
signal(sig, SIG_DFL); // returns into the faulting instruction, which faults again with the default action
}
static void fault_install(void) {
static int done;
if (done) return;
done = 1;
alt_stack();
struct sigaction sa = {.sa_sigaction = fault_handler, .sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER};
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
sigaction(SIGILL, &sa, NULL);
sigaction(SIGFPE, &sa, NULL);
}
void plat_mem_fault_hook(int (*fn)(uint64_t addr)) {
fault_hook = fn;
fault_install();
}
void plat_mem_access_hook(int (*fn)(uint64_t addr, int write, uint64_t ip)) {
access_hook = fn;
fault_install();
}
void plat_crash_context(const char *s) { (void)s; } // the Windows crash notes only
int plat_crash_filter_reassert(char *who, size_t n) { (void)who, (void)n; return 0; } // Windows only
// plat_jmp_save / plat_jmp_go (platform.h): leaf code that does not move rsp before it leaves, as on Windows.
_Static_assert(sizeof(PlatJmp) == 72 && __builtin_offsetof(PlatJmp, mxcsr) == 64, "plat_jmp_save layout");
__asm__(".text\n"
".globl plat_jmp_save\n"
".type plat_jmp_save, @function\n"
".p2align 4\n"
"plat_jmp_save:\n"
" movq %rbx, 0(%rdi)\n"
" movq %rbp, 8(%rdi)\n"
" movq %r12, 16(%rdi)\n"
" movq %r13, 24(%rdi)\n"
" movq %r14, 32(%rdi)\n"
" movq %r15, 40(%rdi)\n"
" leaq 8(%rsp), %rdx\n" // the caller's rsp after this call returns
" movq %rdx, 48(%rdi)\n"
" movq (%rsp), %rdx\n"
" movq %rdx, 56(%rdi)\n"
" stmxcsr 64(%rdi)\n"
" fnstcw 68(%rdi)\n"
" xorl %eax, %eax\n"
" ret\n"
".size plat_jmp_save, .-plat_jmp_save\n"
".globl plat_jmp_go\n"
".type plat_jmp_go, @function\n"
".p2align 4\n"
"plat_jmp_go:\n"
" movq 0(%rdi), %rbx\n"
" movq 8(%rdi), %rbp\n"
" movq 16(%rdi), %r12\n"
" movq 24(%rdi), %r13\n"
" movq 32(%rdi), %r14\n"
" movq 40(%rdi), %r15\n"
" ldmxcsr 64(%rdi)\n"
" fldcw 68(%rdi)\n"
" movq 48(%rdi), %rsp\n"
" movl $1, %eax\n"
" jmpq *56(%rdi)\n"
".size plat_jmp_go, .-plat_jmp_go\n");
// The DWARF unwinder of libgcc (already linked for C++), frame by frame from plat_frame_capture's caller. In each
// frame's callback the IP is its return address, the callee-saved registers are as that frame sees them, and
// _Unwind_GetCFA is its rsp once the call returns (libgcc's context holds the CFA of the frame just unwound).
typedef struct {
PlatJmp *out;
int (*want)(uintptr_t rip, void *arg);
void *arg;
int n, found;
} FrameWalk;
static _Unwind_Reason_Code frame_step(struct _Unwind_Context *ctx, void *p) {
FrameWalk *w = p;
uintptr_t ip = _Unwind_GetIP(ctx), sp = _Unwind_GetCFA(ctx);
if (w->n++ == 0) return _URC_NO_REASON; // plat_frame_capture itself
if (!ip || w->n > 256) return _URC_END_OF_STACK;
if (!w->want(ip, w->arg)) return _URC_NO_REASON;
PlatJmp *o = w->out;
o->gpr[0] = _Unwind_GetGR(ctx, 3), o->gpr[1] = _Unwind_GetGR(ctx, 6); // DWARF rbx, rbp
for (int r = 12; r <= 15; r++) o->gpr[2 + r - 12] = _Unwind_GetGR(ctx, r);
o->gpr[6] = sp, o->gpr[7] = ip;
__asm__ volatile("stmxcsr %0\n\tfnstcw %1" : "=m"(o->mxcsr), "=m"(o->fpcw));
w->found = 1;
return _URC_END_OF_STACK;
}
__attribute__((noinline)) int plat_frame_capture(PlatJmp *out, int (*want)(uintptr_t rip, void *arg), void *arg) {
FrameWalk w = {out, want, arg, 0, 0};
_Unwind_Backtrace(frame_step, &w);
return w.found;
}
void plat_crash_hook(void (*fn)(const PlatCrash *c)) {
crash_fn = fn;
fault_install();
}
void plat_reexec(void) {
static char args[16384];
char *argv[256];
int argc = 0;
FILE *f = fopen("/proc/self/cmdline", "rb");
size_t n = f ? fread(args, 1, sizeof args - 1, f) : 0;
if (f) fclose(f);
args[n] = 0;
for (size_t i = 0; i < n && argc < 255; i += strlen(args + i) + 1) argv[argc++] = args + i;
argv[argc] = NULL;
fflush(stdout);
fflush(stderr);
execv("/proc/self/exe", argv);
fprintf(stderr, "FATAL: re-running the process failed (%s)\n", strerror(errno));
_exit(70);
}
int plat_mem_read(uint64_t addr, void *out, size_t n) {
struct iovec local = {out, n}, remote = {(void *)(uintptr_t)addr, n};
return process_vm_readv(getpid(), &local, 1, &remote, 1, 0) == (ssize_t)n;
}
int plat_mem_readable(uint64_t addr, uint64_t n) {
uint8_t b;
uint64_t end = addr + (n ? n : 1);
for (uint64_t a = addr; a < end; a = (a | 4095) + 1)
if (!plat_mem_read(a, &b, 1)) return 0;
return 1;
}
void *plat_aligned_alloc(size_t size, size_t align) {
void *p = NULL;
return posix_memalign(&p, align < sizeof(void *) ? sizeof(void *) : align, size) ? NULL : p;
}
void plat_aligned_free(void *p) { free(p); }
// A PlatFile is the descriptor + 1, so descriptor 0 is not NULL.
PlatFile *plat_file_open(const char *path) {
int fd = open(path, O_RDONLY | O_CLOEXEC);
return fd < 0 ? NULL : (PlatFile *)(intptr_t)(fd + 1);
}
PlatFile *plat_file_open_unbuffered(const char *path) {
int fd = open(path, O_RDONLY | O_CLOEXEC | O_DIRECT);
return fd < 0 ? NULL : (PlatFile *)(intptr_t)(fd + 1);
}
static int fd_of(PlatFile *f) { return (int)(intptr_t)f - 1; }
void plat_file_close(PlatFile *f) {
if (f) close(fd_of(f));
}
int64_t plat_file_pread(PlatFile *f, void *buf, uint32_t n, uint64_t off) {
ssize_t k;
do k = pread(fd_of(f), buf, n, (off_t)off);
while (k < 0 && errno == EINTR);
return k >= 0 ? k : errno == EFAULT ? -2 : -1;
}
int plat_file_id(PlatFile *f, PlatFileId *out) {
struct stat st;
if (fstat(fd_of(f), &st)) return 0;
*out = (PlatFileId){(uint64_t)st.st_dev, (uint64_t)st.st_ino, (uint64_t)st.st_size,
(uint64_t)st.st_mtim.tv_sec * 1000000000u + (uint64_t)st.st_mtim.tv_nsec,
(uint64_t)st.st_ctim.tv_sec * 1000000000u + (uint64_t)st.st_ctim.tv_nsec};
return 1;
}
const void *plat_file_map(PlatFile *f) {
struct stat st;
if (fstat(fd_of(f), &st) || !st.st_size) return NULL;
void *v = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_SHARED, fd_of(f), 0);
return v == MAP_FAILED ? NULL : v;
}
int plat_fs_os_error(void) { return errno; }
static int fs_err(int e) {
switch (e) {
case ENOENT: case ENOTDIR: return PLAT_FS_NOT_FOUND;
case EEXIST: return PLAT_FS_EXISTS;
case ENOTEMPTY: return PLAT_FS_NOT_EMPTY;
case EBUSY: case ETXTBSY: return PLAT_FS_BUSY;
default: return PLAT_FS_ERROR;
}
}
int plat_fs_kind(const char *path) {
struct stat st;
if (lstat(path, &st)) return PLAT_FS_MISSING;
return S_ISLNK(st.st_mode) ? PLAT_FS_LINK : S_ISDIR(st.st_mode) ? PLAT_FS_DIR : PLAT_FS_FILE;
}
int plat_fs_mkdir(const char *path) { return mkdir(path, 0755) ? fs_err(errno) : PLAT_FS_OK; }
int plat_fs_rmdir(const char *path) {
struct stat st;
if (!lstat(path, &st) && S_ISLNK(st.st_mode)) return unlink(path) ? fs_err(errno) : PLAT_FS_OK;
return rmdir(path) ? fs_err(errno) : PLAT_FS_OK;
}
// Deleting needs write access to the directory, not to the file: a read-only file goes as any other.
int plat_fs_unlink(const char *path) { return unlink(path) ? fs_err(errno) : PLAT_FS_OK; }
// RENAME_NOREPLACE keeps Windows' rule that a rename never replaces; a file system without it (EINVAL) gets a check
// first, which another process could race but the save directories are ours.
int plat_fs_rename(const char *from, const char *to, int durable) {
if (syscall(SYS_renameat2, AT_FDCWD, from, AT_FDCWD, to, RENAME_NOREPLACE)) {
if (errno != EINVAL && errno != ENOSYS) return fs_err(errno);
if (plat_fs_kind(to) != PLAT_FS_MISSING) return PLAT_FS_EXISTS;
if (rename(from, to)) return fs_err(errno);
}
if (durable) {
sync_dir_of(from);
sync_dir_of(to);
}
return PLAT_FS_OK;
}
int plat_fs_link(const char *existing, const char *path) { return link(existing, path) ? fs_err(errno) : PLAT_FS_OK; }
int plat_fs_links(const char *path) {
struct stat st;
return stat(path, &st) ? 0 : (int)st.st_nlink;
}
void plat_fs_sync_dir(const char *path) {
int fd = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd >= 0) {
fsync(fd);
close(fd);
}
}
// Open files never stop a rename here.
void plat_fs_busy_reason(const char *dir, char *out, size_t cap) {
(void)dir;
if (cap) *out = 0;
}
int plat_fs_list(const char *dir, int (*cb)(const char *name, int kind, uint64_t size, void *ctx), void *ctx) {
DIR *d = opendir(dir);
if (!d) return fs_err(errno);
for (struct dirent *e; (e = readdir(d));) {
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
struct stat st;
if (fstatat(dirfd(d), e->d_name, &st, AT_SYMLINK_NOFOLLOW)) continue;
int kind = S_ISLNK(st.st_mode) ? PLAT_FS_LINK : S_ISDIR(st.st_mode) ? PLAT_FS_DIR : PLAT_FS_FILE;
if (!cb(e->d_name, kind, kind == PLAT_FS_FILE ? (uint64_t)st.st_size : 0, ctx)) break;
}
closedir(d);
return PLAT_FS_OK;
}
PlatFile *plat_fs_open(const char *path, int write, int *err) {
struct stat st;
int fd = open(path, (write ? O_RDWR : O_RDONLY) | O_CLOEXEC);
if (fd >= 0 && (fstat(fd, &st) || !S_ISREG(st.st_mode))) {
close(fd);
fd = -1, errno = ENOENT;
}
if (fd < 0 && err) *err = fs_err(errno);
return fd < 0 ? NULL : (PlatFile *)(intptr_t)(fd + 1);
}
PlatFile *plat_fs_create(const char *path, int *err) {
int fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
if (fd < 0 && err) *err = fs_err(errno);
return fd < 0 ? NULL : (PlatFile *)(intptr_t)(fd + 1);
}
int64_t plat_file_pwrite(PlatFile *f, const void *buf, uint32_t n, uint64_t off) {
ssize_t k;
do k = pwrite(fd_of(f), buf, n, (off_t)off);
while (k < 0 && errno == EINTR);
return k;
}
int plat_file_set_size(PlatFile *f, uint64_t size) { return ftruncate(fd_of(f), (off_t)size) ? fs_err(errno) : 0; }
int plat_file_fsync(PlatFile *f) { return fsync(fd_of(f)) == 0; }
int plat_temp_dir(char *out, size_t cap) {
const char *t = getenv("TMPDIR");
if (!t || !*t) t = "/tmp";
size_t n = strlen(t);
while (n > 1 && t[n - 1] == '/') n--;
return snprintf(out, cap, "%.*s", (int)n, t) < (int)cap;
}
uintptr_t plat_exe_base(void) {
Dl_info d;
return dladdr((void *)plat_exe_base, &d) ? (uintptr_t)d.dli_fbase : 0;
}
int plat_module_of(uintptr_t addr, char *name, size_t cap, uintptr_t *base) {
Dl_info d;
if (!dladdr((void *)addr, &d) || !d.dli_fname) return 0;
const char *n = strrchr(d.dli_fname, '/');
*base = (uintptr_t)d.dli_fbase;
return snprintf(name, cap, "%s", n ? n + 1 : d.dli_fname) < (int)cap;
}
int plat_exe_path(char *out, size_t cap) {
ssize_t n = readlink("/proc/self/exe", out, cap - 1);
if (n <= 0 || (size_t)n >= cap - 1) return 0;
out[n] = 0;
return 1;
}
size_t plat_getenv(const char *name, char *out, size_t cap) {
const char *v = getenv(name);
size_t n = v ? strlen(v) : 0;
if (!n || n >= cap) return 0;
memcpy(out, v, n + 1);
return n;
}
// Cached per thread: plat_rmutex_lock asks on every acquire, hundreds of thousands of times a second in play, and
// gettid is a full syscall on Linux (Windows reads GetCurrentThreadId from the TEB). A thread's id never changes, and
// the runtime starts child processes with posix_spawn only, so no forked copy inherits a stale id. noinline: a fiber
// may resume on another thread (platform.h), and a caller that inlined this could keep the address of tid it computed
// before a plat_fiber_switch; as a call it reads the slot of the thread it runs on.
__attribute__((noinline)) uint32_t plat_thread_id(void) {
static __thread uint32_t tid;
if (!tid) tid = (uint32_t)syscall(SYS_gettid);
return tid;
}
// The first set variable of names ("ru_RU.UTF-8") as a BCP-47 tag ("ru-RU") into tag; "" for C and POSIX.
static void locale_tag(const char *const *names, int n, char *tag, size_t cap) {
const char *v = NULL;
for (int i = 0; i < n && !(v && *v); i++) v = getenv(names[i]);
size_t k = 0;
if (v && strcmp(v, "C") && strncmp(v, "C.", 2) && strcmp(v, "POSIX"))
for (; v[k] && v[k] != '.' && v[k] != '@' && k < cap - 1; k++) tag[k] = v[k] == '_' ? '-' : v[k];
tag[k] = 0;
}
const char *plat_ui_language(void) {
static char tag[32];
static const char *const names[] = {"LC_ALL", "LC_MESSAGES", "LANG"};
if (!tag[0]) locale_tag(names, 3, tag, sizeof tag);
return tag;
}
const char *plat_user_locale(void) {
static char tag[32];
static const char *const names[] = {"LC_ALL", "LC_TIME", "LANG"};
if (!tag[0]) locale_tag(names, 3, tag, sizeof tag);
return tag;
}
// Noto CJK as the common distributions and SteamOS install it; one collection covers every CJK language.
int plat_font_path(const char *lang, char *out, size_t cap) {
static const char *files[] = {
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
};
if (strcmp(lang, "ja") && strcmp(lang, "ko") && strcmp(lang, "zh-Hans") && strcmp(lang, "zh-Hant")) return 0;
for (size_t i = 0; i < sizeof files / sizeof *files; i++)
if (plat_file_exists(files[i])) return snprintf(out, cap, "%s", files[i]) < (int)cap;
return 0;
}
// PRETTY_NAME of /etc/os-release and the kernel release: "SteamOS (Linux 6.16.12-valve)".
int plat_os_version(char *out, size_t cap) {
char name[128] = "Linux";
FILE *f = fopen("/etc/os-release", "r");
if (f) {
char line[256];
while (fgets(line, sizeof line, f))
if (!strncmp(line, "PRETTY_NAME=", 12)) {
char *v = line + 12, *e;
if (*v == '"') v++;
if ((e = strpbrk(v, "\"\n"))) *e = 0;
snprintf(name, sizeof name, "%s", v);
break;
}
fclose(f);
}
struct utsname u;
if (uname(&u)) return snprintf(out, cap, "%s", name) < (int)cap;
return snprintf(out, cap, "%s (Linux %s)", name, u.release) < (int)cap;
}
int plat_is_wine(void) { return 0; }
int plat_stderr_path(char *out, size_t cap) {
struct stat st;
if (fstat(2, &st) || !S_ISREG(st.st_mode)) return 0;
ssize_t n = readlink("/proc/self/fd/2", out, cap - 1);
if (n <= 0 || (size_t)n >= cap - 1) return 0;
out[n] = 0;
return 1;
}
// ---- process
#ifdef NX2NAVI_PGO_GEN
// compiler-rt's profile runtime (an instrumented build, tools/build_game.py --pgo gen). It writes its counters from an
// atexit handler, which _exit skips; __llvm_profile_dump writes them once and makes later writes no-ops.
int __llvm_profile_dump(void);
#endif
void (*plat_exit_hook)(void);
void plat_exit(int code) {
if (plat_exit_hook) plat_exit_hook();
#ifdef NX2NAVI_PGO_GEN
__llvm_profile_dump();
#endif
fflush(stdout);
fflush(stderr);
_exit(code);
}
int plat_setenv(const char *name, const char *value) {
return (value ? setenv(name, value, 1) : unsetenv(name)) == 0;
}
uint32_t plat_process_id(void) { return (uint32_t)getpid(); }
uint64_t plat_process_cpu_ns(void) {
struct timespec t;
if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t)) return 0;
return (uint64_t)t.tv_sec * 1000000000u + (uint64_t)t.tv_nsec;
}
// Field 22 of /proc/self/stat is the start time in clock ticks after boot; CLOCK_BOOTTIME is now after boot.
uint64_t plat_process_uptime_ns(void) {
FILE *f = fopen("/proc/self/stat", "r");
if (!f) return 0;
char buf[1024];
size_t n = fread(buf, 1, sizeof buf - 1, f);
fclose(f);
buf[n] = 0;
char *p = strrchr(buf, ')'); // the command name may hold spaces
unsigned long long start = 0;
if (!p || sscanf(p + 2, "%*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %*u %*u %*d %*d %*d %*d %*d %*d %llu",
&start) != 1)
return 0;
struct timespec t;
clock_gettime(CLOCK_BOOTTIME, &t);
uint64_t now = (uint64_t)t.tv_sec * 1000000000u + (uint64_t)t.tv_nsec;
uint64_t at = start * (1000000000u / (uint64_t)sysconf(_SC_CLK_TCK));
return now > at ? now - at : 0;
}
uint64_t plat_unix_time_ns(void) {
struct timespec t;
clock_gettime(CLOCK_REALTIME, &t);
return (uint64_t)t.tv_sec * 1000000000u + (uint64_t)t.tv_nsec;
}
void plat_local_time(struct tm *out) {
time_t t = time(NULL);
localtime_r(&t, out);
}
// The range of the Windows CRT's localtime_s and gmtime_s (1970 to 3000), which glibc exceeds: the game's time
// service reports times outside it as invalid on both hosts.
#define PLAT_TIME_MAX 32535215999ll
int plat_localtime(int64_t unix_s, struct tm *out) {
time_t t = (time_t)unix_s;
return unix_s >= 0 && unix_s <= PLAT_TIME_MAX && localtime_r(&t, out) != NULL;
}
int plat_gmtime(int64_t unix_s, struct tm *out) {
time_t t = (time_t)unix_s;
return unix_s >= 0 && unix_s <= PLAT_TIME_MAX && gmtime_r(&t, out) != NULL;
}
long plat_tz_west(void) {
tzset();
return timezone;
}
int plat_run_self(const char *env_name, const char *env_value, char *out, size_t cap, uint32_t timeout_ms) {
// the arguments, NUL-separated, from /proc/self/cmdline
static char args[16384];
char *argv[256];
int argc = 0;
FILE *f = fopen("/proc/self/cmdline", "rb");
size_t n = f ? fread(args, 1, sizeof args - 1, f) : 0;
if (f) fclose(f);
args[n] = 0;
for (size_t i = 0; i < n && argc < 255; i += strlen(args + i) + 1) argv[argc++] = args + i;
argv[argc] = NULL;
char exe[PATH_MAX];
if (!argc || !plat_exe_path(exe, sizeof exe)) return -1;
int p[2];
if (pipe2(p, O_CLOEXEC)) return -1;
posix_spawn_file_actions_t fa;
posix_spawn_file_actions_init(&fa);
posix_spawn_file_actions_adddup2(&fa, p[1], 1);
posix_spawn_file_actions_adddup2(&fa, p[1], 2);
setenv(env_name, env_value, 1);
pid_t pid;
int e = posix_spawn(&pid, exe, &fa, NULL, argv, environ);
unsetenv(env_name);
posix_spawn_file_actions_destroy(&fa);
close(p[1]);
if (e) {
close(p[0]);
return -1;
}
size_t len = 0;
char sink[512];
ssize_t k;
while (len + 1 < cap && (k = read(p[0], out + len, cap - 1 - len)) != 0)
if (k > 0) len += (size_t)k;
else if (errno != EINTR) break;
if (cap) out[len] = 0;
while ((k = read(p[0], sink, sizeof sink)) > 0 || (k < 0 && errno == EINTR)) {}
close(p[0]);
int status = 0;
uint64_t end = plat_now_ns() + (uint64_t)timeout_ms * 1000000u;
while (waitpid(pid, &status, timeout_ms ? WNOHANG : 0) == 0) {
if (plat_now_ns() >= end) {
kill(pid, SIGKILL);
waitpid(pid, &status, 0);
break;
}
plat_sleep_ms(10);
}
return WIFEXITED(status) ? WEXITSTATUS(status) : 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);
}
int plat_random(void *out, size_t n) {
uint8_t *p = out;
while (n) {
ssize_t k = getrandom(p, n, 0);
if (k < 0 && errno == EINTR) continue;
if (k <= 0) return 0;
p += k, n -= (size_t)k;
}
return 1;
}
// ---- desktop
int plat_clipboard_set(const char *utf8) { return SDL_SetClipboardText(utf8); }
static void reap_main(void *arg) {
waitpid((pid_t)(intptr_t)arg, NULL, 0);
}
// xdg-open picks the desktop's file manager; it runs detached, so a slow answer does not hold up the caller, and a
// thread of its own reaps it so it does not stay a zombie.
int plat_open_folder(const char *path) {
struct stat st;
if (stat(path, &st) || !S_ISDIR(st.st_mode)) return 0;
posix_spawnattr_t a;
posix_spawnattr_init(&a);
posix_spawnattr_setflags(&a, POSIX_SPAWN_SETSID);
char *argv[] = {"xdg-open", (char *)path, NULL};
pid_t pid;
int e = posix_spawnp(&pid, "xdg-open", NULL, &a, argv, environ);
posix_spawnattr_destroy(&a);
if (e) return 0;
plat_thread_spawn(reap_main, (void *)(intptr_t)pid, "reap", 64 << 10);
return 1;
}
// ---- dynamic libraries
void *plat_lib_open(const char *name) { return dlopen(name, RTLD_NOW | RTLD_LOCAL); }
void *plat_lib_sym(void *lib, const char *name) { return lib ? dlsym(lib, name) : NULL; }
// ---- machine load: /proc/stat and /proc/self/stat for the CPU, hwmon for the temperature, the amdgpu sysfs files of
// the render device for the GPU.
struct PlatLoad {
uint64_t busy, total, proc; // the previous call's jiffies; total 0 before the first
uint64_t proc_ns;
char gpu[256]; // /sys/class/drm/cardN/device of the GPU in use; empty when unknown
char temp[256]; // the CPU's hwmon temp1_input; empty when unknown
};
static int read_u64_file(const char *path, uint64_t *v) {
FILE *f = fopen(path, "r");
if (!f) return 0;
unsigned long long x;
int ok = fscanf(f, "%llu", &x) == 1;
fclose(f);
if (ok) *v = x;
return ok;
}
// The hwmon device of the CPU package: k10temp (AMD), coretemp (Intel), or the first that has a temp1_input.
static void find_cpu_temp(char *out, size_t cap) {
out[0] = 0;
DIR *d = opendir("/sys/class/hwmon");
if (!d) return;
for (struct dirent *e; (e = readdir(d));) {
if (e->d_name[0] == '.') continue;
char path[256], name[64] = "";
snprintf(path, sizeof path, "/sys/class/hwmon/%s/name", e->d_name);
FILE *f = fopen(path, "r");
if (!f) continue;
if (!fgets(name, sizeof name, f)) name[0] = 0;
fclose(f);
name[strcspn(name, "\n")] = 0;
int cpu = !strcmp(name, "k10temp") || !strcmp(name, "coretemp") || !strcmp(name, "zenpower");
snprintf(path, sizeof path, "/sys/class/hwmon/%s/temp1_input", e->d_name);
if ((cpu || !out[0]) && plat_file_exists(path)) snprintf(out, cap, "%s", path);
if (cpu && out[0]) break;
}
closedir(d);
}
// The DRM card with gpu_busy_percent (amdgpu) on PCI bus bus, the game's device. With bus -1 (no VK_EXT_pci_bus_info):
// of several, the one with the most VRAM in use. None when the game's GPU has no such file (NVIDIA): the overlay then
// takes its load and VRAM from NVML, not from an idle integrated GPU.
static void find_gpu(char *out, size_t cap, int bus) {
out[0] = 0;
uint64_t best = 0;
DIR *d = opendir("/sys/class/drm");
if (!d) return;
for (struct dirent *e; (e = readdir(d));) {
if (strncmp(e->d_name, "card", 4) || strchr(e->d_name, '-')) continue;
char dev[256], path[300];
snprintf(dev, sizeof dev, "/sys/class/drm/%s/device", e->d_name);
snprintf(path, sizeof path, "%s/gpu_busy_percent", dev);
uint64_t v, used = 0;
if (!read_u64_file(path, &v)) continue;
if (bus >= 0) { // device links to the PCI function, "0000:03:00.0": domain, bus, device, function
char real[PATH_MAX];
unsigned dom, b;
const char *base = realpath(dev, real) ? strrchr(real, '/') : NULL;
if (!base || sscanf(base + 1, "%x:%x:", &dom, &b) != 2 || (int)b != bus) continue;
}
snprintf(path, sizeof path, "%s/mem_info_vram_used", dev);
read_u64_file(path, &used);
if (!out[0] || used > best) snprintf(out, cap, "%s", dev), best = used;
}
closedir(d);
}
PlatLoad *plat_load_open(const uint8_t *gpu_luid, int pci_bus) {
(void)gpu_luid; // Linux has no adapter LUIDs; the PCI bus names the card
PlatLoad *l = calloc(1, sizeof *l);
if (!l) return NULL;
find_cpu_temp(l->temp, sizeof l->temp);
find_gpu(l->gpu, sizeof l->gpu, pci_bus);
return l;
}
void plat_load_close(PlatLoad *l) { free(l); }
void plat_cpu_load(PlatLoad *l, float *total_pct, float *proc_pct, float *temp_c) {
*total_pct = *proc_pct = *temp_c = -1;
if (!l) return;
FILE *f = fopen("/proc/stat", "r");
unsigned long long v[10] = {0};
int n = f ? fscanf(f, "cpu %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu", &v[0], &v[1], &v[2], &v[3], &v[4],
&v[5], &v[6], &v[7], &v[8], &v[9])
: 0;
if (f) fclose(f);
uint64_t pns = plat_process_cpu_ns();
if (n >= 8) {
uint64_t idle = v[3] + v[4], total = 0;
for (int i = 0; i < 8; i++) total += v[i]; // guest time is already inside user time
uint64_t busy = total - idle;
if (l->total && total > l->total) {
uint64_t dt = total - l->total;
*total_pct = 100.0f * (float)(busy - l->busy) / (float)dt;
// the process time is in ns, the machine's in jiffies summed over every CPU
double dt_ns = (double)dt * 1e9 / (double)sysconf(_SC_CLK_TCK);
*proc_pct = (float)(100.0 * (double)(pns - l->proc_ns) / dt_ns);
}
l->busy = busy, l->total = total, l->proc_ns = pns;
}
uint64_t mc;
if (l->temp[0] && read_u64_file(l->temp, &mc)) *temp_c = (float)mc / 1000.0f;
}
void plat_gpu_load(PlatLoad *l, float *load_pct, float *vram_used_mb) {
*load_pct = *vram_used_mb = -1;
if (!l || !l->gpu[0]) return;
char path[300];
uint64_t v;
snprintf(path, sizeof path, "%s/gpu_busy_percent", l->gpu);
if (read_u64_file(path, &v)) *load_pct = (float)v;
snprintf(path, sizeof path, "%s/mem_info_vram_used", l->gpu);
if (read_u64_file(path, &v)) *vram_used_mb = (float)(v >> 20);
}
void plat_mem_info(float *used_mb, float *total_mb, float *proc_mb) {
*used_mb = *total_mb = *proc_mb = -1;
FILE *f = fopen("/proc/meminfo", "r");
if (f) {
char line[128];
unsigned long long total = 0, avail = 0, k;
while (fgets(line, sizeof line, f)) {
if (sscanf(line, "MemTotal: %llu kB", &k) == 1) total = k;
else if (sscanf(line, "MemAvailable: %llu kB", &k) == 1) avail = k;
}
fclose(f);
if (total) *total_mb = (float)(total >> 10), *used_mb = (float)((total - avail) >> 10);
}
f = fopen("/proc/self/statm", "r");
unsigned long long size, resident;
if (f && fscanf(f, "%llu %llu", &size, &resident) == 2) *proc_mb = (float)(resident * (uint64_t)sysconf(_SC_PAGESIZE) >> 20);
if (f) fclose(f);
}
void plat_process_memory(uint64_t *resident, uint64_t *private_bytes) {
*resident = *private_bytes = 0;
FILE *f = fopen("/proc/self/status", "r");
if (!f) return;
char line[128];
unsigned long long k;
while (fgets(line, sizeof line, f)) {
if (sscanf(line, "VmRSS: %llu kB", &k) == 1) *resident = (uint64_t)k << 10;
else if (sscanf(line, "RssAnon: %llu kB", &k) == 1) *private_bytes = (uint64_t)k << 10;
}
fclose(f);
}
// ---- memory limits
//
// Linux overcommits, so memory that runs short does not fail an allocation: the OOM killer ends the process, when it
// outgrew its cgroup's memory.max or when the machine's RAM is gone, and Committed_AS against CommitLimit (which binds
// only with vm.overcommit_memory = 2) says neither. plat_commit_info reports the limit with the least headroom of
// - each cgroup v2 level from this process's cgroup up to the root that sets memory.max or memory.high (the smaller;
// the kernel throttles at memory.high), against that level's memory.current less its file cache (memory.stat file
// without shmem): the kernel drops the cache before it ends anything, so a cache that filled the limit is no
// shortage;
// - the machine's RAM: MemTotal against MemTotal less MemAvailable;
// - with vm.overcommit_memory = 2 only, where an allocation does fail: CommitLimit against Committed_AS.
// The process's share is its resident anonymous memory, the nearest to Windows' private bytes. The sweeps that ask
// run several times a second, so a read is reused for 250 ms. The first read keeps every limit it found as text, which
// the runtime logs (plat_commit_note): stderr alone does not reach the log file when the launcher starts the game.
static char cg_leaf[PATH_MAX]; // this process's cgroup v2 directory ("" when it has none)
static size_t cg_floor; // the length of the cgroup2 mount point at the start of cg_leaf
// The process's cgroup v2 directory: its path from /proc/self/cgroup below the root of the cgroup2 mount that
// /proc/self/mountinfo shows (a container's own cgroup is mounted as the root), else the nearest directory above it
// that exists.
static void cg_locate(void) {
char path[PATH_MAX] = "", mnt[PATH_MAX] = "", root[PATH_MAX] = "", line[2 * PATH_MAX];
FILE *f = fopen("/proc/self/cgroup", "r");
if (f) {
while (fgets(line, sizeof line, f))
if (!strncmp(line, "0::", 3)) {
line[strcspn(line, "\n")] = 0;
snprintf(path, sizeof path, "%s", line + 3);
}
fclose(f);
}
f = path[0] ? fopen("/proc/self/mountinfo", "r") : NULL;
while (f && fgets(line, sizeof line, f)) {
char r[PATH_MAX], m[PATH_MAX];
if (!strstr(line, " - cgroup2 ") || sscanf(line, "%*d %*d %*s %4095s %4095s", r, m) != 2) continue;
snprintf(root, sizeof root, "%s", r);
snprintf(mnt, sizeof mnt, "%s", m);
break;
}
if (f) fclose(f);
if (!mnt[0]) return;
size_t rl = strlen(root);
const char *rel = path;
// A mount of a cgroup below the root of the hierarchy shows the process's path without that prefix.
if (rl > 1 && !strncmp(path, root, rl) && (!path[rl] || path[rl] == '/')) rel = path + rl;
else if (rl > 1) rel = "";
if (!strcmp(rel, "/")) rel = "";
snprintf(cg_leaf, sizeof cg_leaf, "%s%s", mnt, rel);
cg_floor = strlen(mnt);
struct stat st;
while (stat(cg_leaf, &st) || !S_ISDIR(st.st_mode)) { // not shown by the mount: the nearest directory above
char *s = strrchr(cg_leaf, '/');
if (!s || (size_t)(s - cg_leaf) < cg_floor) {
cg_leaf[0] = 0;
return;
}
*s = 0;
}
}
// A cgroup file into buf as text; 0 when it is missing (the controller is not enabled there).
static int cg_file(const char *dir, const char *name, char *buf, size_t n) {
char p[PATH_MAX + 32];
snprintf(p, sizeof p, "%s/%s", dir, name);
int fd = open(p, O_RDONLY | O_CLOEXEC);
if (fd < 0) return 0;
ssize_t r = read(fd, buf, n - 1);
close(fd);
if (r <= 0) return 0;
buf[r] = 0;
return 1;
}
// A byte limit file: its value in bytes, -1 for "max" or when missing.
static double cg_bytes(const char *dir, const char *name) {
char b[64];
if (!cg_file(dir, name, b, sizeof b) || !strncmp(b, "max", 3)) return -1;
return (double)strtoull(b, NULL, 10);
}
// A memory.stat counter in bytes, 0 when it is missing.
static double cg_stat(const char *stat, const char *key) {
size_t kl = strlen(key);
for (const char *p = stat; p; p = strchr(p, '\n')) {
if (*p == '\n') p++;
if (!strncmp(p, key, kl) && p[kl] == ' ') return (double)strtoull(p + kl + 1, NULL, 10);
}
return 0;
}
typedef struct {
double proc, used, limit; // MB; -1 when unknown
const char *who; // the limit that is the tightest, for the first read's log line
uint64_t at; // plat_now_ms of the read, 0 before the first
} CommitSnap;
// Makes (limit, used) the snapshot's when its headroom is smaller than the one held.
static void commit_pick(CommitSnap *s, double limit, double used, const char *who) {
if (s->limit < 0 || limit - used < s->limit - s->used) s->limit = limit, s->used = used, s->who = who;
}
// Appends one limit found to the note of the first read ("; " between them; a full note takes no more).
static void note_add(char *note, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
#define NOTE_MAX 2048
static void note_add(char *note, const char *fmt, ...) {
size_t n = strlen(note);
if (n + 3 >= NOTE_MAX) return;
if (n) memcpy(note + n, "; ", 3), n += 2;
va_list ap;
va_start(ap, fmt);
vsnprintf(note + n, NOTE_MAX - n, fmt, ap);
va_end(ap);
}
// The cgroups from the process's up to the root that limit memory.
static void commit_cgroups(CommitSnap *s, char *note) {
char d[PATH_MAX];
snprintf(d, sizeof d, "%s", cg_leaf);
while (d[0]) {
double max = cg_bytes(d, "memory.max"), high = cg_bytes(d, "memory.high");
int use_high = max < 0 || (high >= 0 && high < max);
double lim = use_high ? high : max;
double cur = lim < 0 ? -1 : cg_bytes(d, "memory.current");
if (cur >= 0) {
char st[4096] = ""; // memory.stat starts with anon and file; the counters needed lie in its first lines
cg_file(d, "memory.stat", st, sizeof st);
double cache = cg_stat(st, "file") - cg_stat(st, "shmem");
cache = cache < 0 ? 0 : cache > cur ? cur : cache;
commit_pick(s, lim / 1048576.0, (cur - cache) / 1048576.0,
use_high ? "cgroup memory.high" : "cgroup memory.max");
if (note)
note_add(note, "cgroup %s %s %.0f MB (%.0f MB in use, %.0f MB of it file cache)",
d[cg_floor] ? d + cg_floor : "/", use_high ? "memory.high" : "memory.max", lim / 1048576.0,
cur / 1048576.0, cache / 1048576.0);
}
char *sl = strrchr(d, '/');
if (strlen(d) <= cg_floor || !sl || (size_t)(sl - d) < cg_floor) break;
*sl = 0;
}
}
// The machine's RAM, and the system's commit when vm.overcommit_memory = 2.
static void commit_machine(CommitSnap *s, char *note) {
static int overcommit = -1;
unsigned long long total = 0, avail = 0, cl = 0, as = 0, k;
char line[128];
FILE *f = fopen("/proc/meminfo", "r");
if (f) {
while (fgets(line, sizeof line, f)) {
if (sscanf(line, "MemTotal: %llu kB", &k) == 1) total = k;
else if (sscanf(line, "MemAvailable: %llu kB", &k) == 1) avail = k;
else if (sscanf(line, "CommitLimit: %llu kB", &k) == 1) cl = k;
else if (sscanf(line, "Committed_AS: %llu kB", &k) == 1) as = k;
}
fclose(f);
}
if (total && avail && avail <= total) {
commit_pick(s, (double)total / 1024.0, (double)(total - avail) / 1024.0, "RAM");
if (note) note_add(note, "RAM %.0f MB (%.0f MB available)", (double)total / 1024.0, (double)avail / 1024.0);
}
if (overcommit < 0) {
FILE *o = fopen("/proc/sys/vm/overcommit_memory", "r");
if (!o || fscanf(o, "%d", &overcommit) != 1) overcommit = 0;
if (o) fclose(o);
}
if (overcommit == 2 && cl) {
commit_pick(s, (double)cl / 1024.0, (double)as / 1024.0, "commit limit");
if (note)
note_add(note, "commit limit %.0f MB (%.0f MB committed, vm.overcommit_memory = 2)", (double)cl / 1024.0,
(double)as / 1024.0);
}
}
static char commit_note[NOTE_MAX + 128]; // the first read's limits (plat_commit_note); not written after it
static void commit_read(CommitSnap *s, int first) {
char note[NOTE_MAX] = "";
s->proc = s->used = s->limit = -1;
s->who = NULL;
if (first) cg_locate();
commit_cgroups(s, first ? note : NULL);
commit_machine(s, first ? note : NULL);
uint64_t resident, private_bytes;
plat_process_memory(&resident, &private_bytes);
if (private_bytes) s->proc = (double)(private_bytes >> 20);
if (first) {
if (note[0])
snprintf(commit_note, sizeof commit_note,
"memory limits: %s; the one with the least headroom counts (now: %s)", note, s->who);
else snprintf(commit_note, sizeof commit_note, "no memory limit readable");
}
}
void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
static PlatMutex lock = PLAT_MUTEX_INIT;
static CommitSnap snap;
plat_mutex_lock(&lock);
uint64_t now = plat_now_ms();
if (!snap.at || now - snap.at >= 250) {
commit_read(&snap, !snap.at);
snap.at = now ? now : 1;
}
*proc_mb = snap.proc, *used_mb = snap.used, *limit_mb = snap.limit;
plat_mutex_unlock(&lock);
}
int plat_commit_enforced(void) {
double proc, used, limit;
plat_commit_info(&proc, &used, &limit);
return limit > 0;
}
// The first read wrote the note under plat_commit_info's lock; taking that lock here orders the copy after it.
int plat_commit_note(char *out, size_t cap) {
double proc, used, limit;
plat_commit_info(&proc, &used, &limit);
snprintf(out, cap, "%s", commit_note);
return out[0] != 0;
}
// The cgroup's memory.current and MemAvailable count system memory only: a graphics card's own memory is in neither.
int plat_commit_counts_vram(void) { return 0; }
// ---- child processes
struct PlatChild {
pid_t pid;
int in, out; // in: our write end, out: our read end
};
PlatChild *plat_child_start(const char *exe, const char *arg, uint32_t *err) {
if (err) *err = 0;
int in[2], out[2];
if (pipe2(in, O_CLOEXEC)) return NULL;
if (pipe2(out, O_CLOEXEC)) {
close(in[0]), close(in[1]);
return NULL;
}
posix_spawn_file_actions_t fa;
posix_spawn_file_actions_init(&fa);
posix_spawn_file_actions_adddup2(&fa, in[0], 0);
posix_spawn_file_actions_adddup2(&fa, out[1], 1);
posix_spawn_file_actions_addopen(&fa, 2, "/dev/null", O_WRONLY, 0);
char *argv[] = {(char *)exe, (char *)arg, NULL};
pid_t pid;
int e = posix_spawn(&pid, exe, &fa, NULL, argv, environ);
posix_spawn_file_actions_destroy(&fa);
close(in[0]);
close(out[1]);
if (e) {
close(in[1]), close(out[0]);
if (err) *err = (uint32_t)e;
return NULL;
}
PlatChild *c = malloc(sizeof *c);
if (!c) {
kill(pid, SIGKILL);
waitpid(pid, NULL, 0);
close(in[1]), close(out[0]);
return NULL;
}
c->pid = pid, c->in = in[1], c->out = out[0];
return c;
}
void plat_child_stop(PlatChild *c) {
if (!c) return;
close(c->in);
close(c->out);
kill(c->pid, SIGKILL);
waitpid(c->pid, NULL, 0);
free(c);
}
void plat_child_lower(PlatChild *c) { // a higher nice value needs no privilege
if (c) setpriority(PRIO_PROCESS, (id_t)c->pid, 19);
}
// A child that died makes the write fail with EPIPE; SIGPIPE is ignored for the process (main.c), as Windows has none.
int plat_child_write(PlatChild *c, const void *data, size_t n) { return write_all(c->in, data, n); }
// See win32.c plat_child_read: a child that never answers fails the read after PLAT_CHILD_READ_MS.
long plat_child_read(PlatChild *c, void *buf, size_t cap) {
struct pollfd pf = {.fd = c->out, .events = POLLIN};
int r;
do r = poll(&pf, 1, PLAT_CHILD_READ_MS);
while (r < 0 && errno == EINTR);
if (r <= 0) return -1; // timed out or failed
ssize_t k;
do k = read(c->out, buf, cap);
while (k < 0 && errno == EINTR);
return (long)k;
}
// ---- local IPC (platform.h): Discord's Rich Presence Unix domain socket
struct PlatIpc { int fd; };
PlatIpc *plat_ipc_connect(const char *name) {
const char *dirs[] = {getenv("DISCORD_IPC_PATH"), getenv("XDG_RUNTIME_DIR"), getenv("TMPDIR"), "/tmp"};
for (size_t d = 0; d < sizeof dirs / sizeof *dirs; d++) {
if (!dirs[d] || !*dirs[d]) continue;
char path[sizeof ((struct sockaddr_un *)0)->sun_path];
if ((size_t)snprintf(path, sizeof path, "%s/%s", dirs[d], name) >= sizeof path) continue;
int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0) return NULL;
struct sockaddr_un sa = {0};
sa.sun_family = AF_UNIX;
strcpy(sa.sun_path, path);
if (connect(fd, (struct sockaddr *)&sa, sizeof sa) == 0) {
PlatIpc *p = malloc(sizeof *p);
if (!p) {
close(fd);
return NULL;
}
p->fd = fd;
return p;
}
close(fd); // nothing listens at this path: the next directory may have it
}
return NULL;
}
int plat_ipc_read(PlatIpc *p, void *buf, size_t n, uint32_t ms) {
struct pollfd pf = {.fd = p->fd, .events = POLLIN};
int r = poll(&pf, 1, (int)ms);
if (r <= 0) return r < 0 && errno != EINTR ? -1 : 0;
ssize_t got = recv(p->fd, buf, n, 0);
return got > 0 ? (int)got : -1; // 0 bytes: the peer closed the socket
}
int plat_ipc_write(PlatIpc *p, const void *buf, size_t n) {
const uint8_t *at = buf;
while (n) {
ssize_t put = send(p->fd, at, n, MSG_NOSIGNAL); // a closed peer is an error, not a SIG_PIPE
if (put <= 0) {
if (put < 0 && errno == EINTR) continue;
return 0;
}
at += put, n -= put;
}
return 1;
}
void plat_ipc_close(PlatIpc *p) {
if (!p) return;
close(p->fd);
free(p);
}
// ---- restart
// NX2NAVI_RESTART_WAIT names the process that called plat_restart, as its id and start time (field 22 of /proc/<pid>/stat,
// clock ticks after boot): a process that has only reused the id is not waited for. Waiting keeps the new process off
// the old one's files (the log it moves at start, the saves) until the old one is gone. The new process gets no
// descriptor past stderr (glibc 2.34's closefrom file action), and the default signal mask and dispositions, whatever
// the calling thread had.
#define RESTART_WAIT "NX2NAVI_RESTART_WAIT"
// The start time of process pid (field 22 of its /proc stat line), 0 when it is gone.
static unsigned long long process_start(int pid) {
char path[64], buf[1024];
snprintf(path, sizeof path, "/proc/%d/stat", pid);
FILE *f = fopen(path, "r");
if (!f) return 0;
size_t n = fread(buf, 1, sizeof buf - 1, f);
fclose(f);
buf[n] = 0;
char *p = strrchr(buf, ')'); // the command name may hold spaces
unsigned long long start = 0;
if (!p || sscanf(p + 2, "%*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %*u %*u %*d %*d %*d %*d %*d %*d %llu",
&start) != 1)
return 0;
return start;
}
// glibc 2.34 added posix_spawn_file_actions_addclosefrom_np, and the release links against the Steam Runtime's 2.31: a
// weak reference, used when the running glibc has it. Without it the child inherits the open descriptors.
extern int posix_spawn_file_actions_addclosefrom_np(posix_spawn_file_actions_t *fa, int from) __attribute__((weak));
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];
char *argv[256];
int argc = 0;
FILE *f = fopen("/proc/self/cmdline", "rb");
size_t n = f ? fread(args, 1, sizeof args - 1, f) : 0;
if (f) fclose(f);
args[n] = 0;
for (size_t i = 0; i < n && argc < 255; i += strlen(args + i) + 1) argv[argc++] = args + i;
argv[argc] = NULL;
char exe[PATH_MAX], wait[64];
if (!argc || !plat_exe_path(exe, sizeof exe)) return 0;
snprintf(wait, sizeof wait, "%d.%llu", (int)getpid(), process_start((int)getpid()));
posix_spawn_file_actions_t fa;
posix_spawn_file_actions_init(&fa);
spawn_close_from(&fa, 3);
posix_spawnattr_t at;
posix_spawnattr_init(&at);
sigset_t none, all;
sigemptyset(&none);
sigfillset(&all);
posix_spawnattr_setsigmask(&at, &none);
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);
return e == 0;
}
int plat_spawn(const char *const *argv, const char *cwd) {
posix_spawn_file_actions_t fa;
posix_spawn_file_actions_init(&fa);
spawn_close_from(&fa, 3);
posix_spawn_file_actions_addchdir_np(&fa, cwd);
posix_spawnattr_t at;
posix_spawnattr_init(&at);
sigset_t none, all;
sigemptyset(&none);
sigfillset(&all);
posix_spawnattr_setsigmask(&at, &none);
posix_spawnattr_setsigdefault(&at, &all);
posix_spawnattr_setflags(&at, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSID);
pid_t pid;
int e = posix_spawn(&pid, argv[0], &fa, &at, (char *const *)argv, environ);
posix_spawnattr_destroy(&at);
posix_spawn_file_actions_destroy(&fa);
return e == 0;
}
int plat_restart_wait(uint32_t timeout_ms) {
const char *v = getenv(RESTART_WAIT);
if (!v) return 0;
int pid = 0;
unsigned long long start = 0;
int ok = sscanf(v, "%d.%llu", &pid, &start) == 2 && pid > 0 && start;
unsetenv(RESTART_WAIT); // the processes this one starts do not wait again
if (!ok) return 0;
// The descriptor is taken first: a start time that still matches then proves it is that process, not a new one with
// the id. It reads as ready once the process has exited, reaped or not.
int fd = (int)syscall(SYS_pidfd_open, pid, 0);
if (fd < 0) return 0; // already gone
int r = 0;
if (process_start(pid) == start) {
struct pollfd p = {fd, POLLIN, 0};
do r = poll(&p, 1, (int)timeout_ms);
while (r < 0 && errno == EINTR);
r = r > 0 ? 1 : -1;
}
close(fd);
return r;
}