// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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/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//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); } int plat_power_full(void) { return -1; } int plat_perf_core_setup(char *why, size_t n) { snprintf(why, n, "not done on Linux"); return 0; } int plat_thread_perf_core(void) { return -1; } // 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, ®)) 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; static const int gr[16] = {REG_RAX, REG_RCX, REG_RDX, REG_RBX, REG_RSP, REG_RBP, REG_RSI, REG_RDI, REG_R8, REG_R9, REG_R10, REG_R11, REG_R12, REG_R13, REG_R14, REG_R15}; for (int i = 0; i < 16; i++) c.gpr[i] = (uint64_t)u->uc_mcontext.gregs[gr[i]]; c.has_regs = 1; 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; } void plat_lib_error(char *out, size_t cap) { const char *e = dlerror(); snprintf(out, cap, "%s", e ? e : "unknown error"); } int plat_ngx_core_path(char *out, size_t cap) { return snprintf(out, cap, "libnvidia-ngx.so.1") < (int)cap; } // UTF-8 to UTF-32 wchar_t, without the C library's locale (the runtime never sets one, so mbstowcs would stop at the // first non-ASCII byte). Returns 0 on malformed input or when out has no room. static int utf8_to_wide(const char *in, wchar_t *out, size_t cap) { const unsigned char *u = (const unsigned char *)in; size_t n = 0; while (*u) { unsigned c = *u++, more = c < 0x80 ? 0 : c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 4; if (more == 4) return 0; if (more) c &= 0x3F >> more; for (unsigned k = 0; k < more; k++, u++) { if ((*u & 0xC0) != 0x80) return 0; c = c << 6 | (*u & 0x3F); } if (n + 1 >= cap) return 0; out[n++] = (wchar_t)c; } out[n] = 0; return 1; } int plat_exe_dir_wide(wchar_t *out, size_t cap) { char dir[PLAT_PATH_MAX]; char *cut = plat_exe_path(dir, sizeof dir) ? plat_last_sep(dir) : NULL; if (!cut) return 0; cut[1] = 0; return utf8_to_wide(dir, out, cap); } // ---- 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); } void plat_mem_room(uint64_t *commit_room, uint64_t *phys_room) { *commit_room = *phys_room = UINT64_MAX; char line[128]; unsigned long long k; FILE *f = fopen("/proc/meminfo", "r"); if (!f) return; while (fgets(line, sizeof line, f)) if (sscanf(line, "MemAvailable: %llu kB", &k) == 1) *phys_room = (uint64_t)k << 10; fclose(f); } // ---- 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//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; }