Files
totk-pc-recomp/runtime/platform/linux.c
T
Valiantsin DzerakhandClaude Opus 5.5 71831771cd Merge PR #767: default the game resolution to the display's shape (Wintego)
Keep 1920x1080 as the default with prompts = xbox or pc: the setup builds those prompts' folders for 16:9 only, so the
shaped default would have swapped the chosen prompts for the Switch ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:53:57 +03:00

1855 lines
68 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 <stdio.h>
#include <stdlib.h>
#include <string.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 <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);
}
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);
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;
}
// 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.
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);
DIR *d = opendir("/proc/self/task");
if (!d) return 0;
int ok = 1;
for (struct dirent *e; (e = readdir(d));)
if (e->d_name[0] != '.' && sched_setaffinity((pid_t)atoi(e->d_name), sizeof set, &set)) ok = 0;
closedir(d);
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 realigns the stack and 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"
// fiber_swap's ret lands here with rsp = top - 8, as at the entry of a called function. The call below must
// be made with rsp 16-byte aligned so that fn starts with rsp = 8 mod 16, as the System V ABI demands; without
// the realignment every 16-byte local of fn is off by 8, which the VEX instructions of a v3 build tolerate
// but the SSE ones of a v2 or k10 build (movdqa, pcmpeqb with a memory operand) fault on.
" andq $-16, %rsp\n"
" 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); }
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); }
// 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;
}
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) {
if (rename(from, to)) return 0;
sync_dir_of(to);
return 1;
}
int plat_file_write_atomic(const char *path, const void *data, size_t n) {
// 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) && fsync(fd) == 0;
ok = close(fd) == 0 && ok;
if (ok && !rename(tmp, path)) {
sync_dir_of(path);
return 1;
}
unlink(tmp);
return 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;
}
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 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 && 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_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;
}
// Not known before the SDL window exists; the Steam Deck has its own profile (cfg.c)
int plat_screen_size(int *w, int *h) {
(void)w, (void)h;
return 0;
}
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 TOTK_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(int code) {
#ifdef TOTK_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);
}
// Linux overcommits by default, so CommitLimit binds only with vm.overcommit_memory = 2; Committed_AS is what the
// system has promised. The process's share is its resident anonymous memory, the nearest to Windows' private bytes.
void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
*proc_mb = *used_mb = *limit_mb = -1;
char line[128];
unsigned long long k;
FILE *f = fopen("/proc/meminfo", "r");
if (f) {
while (fgets(line, sizeof line, f)) {
if (sscanf(line, "CommitLimit: %llu kB", &k) == 1) *limit_mb = (double)(k >> 10);
else if (sscanf(line, "Committed_AS: %llu kB", &k) == 1) *used_mb = (double)(k >> 10);
}
fclose(f);
}
uint64_t resident, private_bytes;
plat_process_memory(&resident, &private_bytes);
if (private_bytes) *proc_mb = (double)(private_bytes >> 20);
}
// ---- 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);
}
// 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); }
long plat_child_read(PlatChild *c, void *buf, size_t cap) {
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
// TOTK_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 "TOTK_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);
}
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);
pid_t pid;
int e = posix_spawn(&pid, exe, &fa, &at, argv, environ);
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;
}