Files
NX2navi/runtime/platform/win32.c
chrg6 359591c237 Note the module list from each image's header and GetModuleFileNameA, which takes 0.3 ms where it took 25 ms.
The review of the ExitProcess note found that platform/win32.c plat_exit_note_modules, which runs on the window thread
at every focus loss, was quadratic in the number of modules: GetModuleInformation and GetModuleBaseNameA each walk the
loader's module list through ReadProcessMemory. Its probe measured 27.5 ms a note with 292 modules.

Each module's size now comes from SizeOfImage in its own image header and its name from GetModuleFileNameA, the part
after the last backslash. The header is read through ReadProcessMemory into a 1 KiB buffer rather than in place: a
module unloaded since EnumProcessModules then fails the read and is left out, where a plain read would fault. The
table's contents are the same.

Measured with a probe beside this change (283 modules loaded, scratchpad rv_fxexit_fix\modtime_rpm): 25.3 ms a note
before, 0.27 ms after, and both ways note the same 283 bases, sizes and names. tests/exit_tls_check.py, whose note
names the program's own module from that table, and the compile check of win32.c pass, with no new -Wall warnings.
The review's optional second note after the device is made is left out: vk_device.c create_device is another
branch's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:24 -05:00

2876 lines
122 KiB
C

// Windows implementation of platform.h: CreateThread, QueryPerformanceCounter, high-resolution waitable timers,
// SRW locks and condition variables.
#include <io.h>
#include <limits.h>
#include <malloc.h> // _get_heap_handle (plat_heap_info)
#include <stdio.h>
#include <winsock2.h> // before windows.h, which would otherwise pull in the old winsock.h
#include <ws2tcpip.h>
#include <windows.h>
#include <aclapi.h> // SetNamedSecurityInfoW (plat_file_private)
#include <bcrypt.h>
#include <pdh.h>
#include <pdhmsg.h>
#include <psapi.h>
#include <restartmanager.h>
#include <shellapi.h>
#include <timeapi.h>
#include <tlhelp32.h> // CreateToolhelp32Snapshot (plat_watch_set)
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#include <wctype.h>
#include "platform.h"
_Static_assert(sizeof(PlatMutex) == sizeof(SRWLOCK), "PlatMutex holds an SRWLOCK");
_Static_assert(sizeof(PlatCond) == sizeof(CONDITION_VARIABLE), "PlatCond holds a CONDITION_VARIABLE");
// ---- threads
struct PlatThread {
HANDLE h;
PlatThreadFn fn;
void *arg;
volatile int32_t state; // 0 running and owned by its maker, 1 detached, 2 its function has returned
char name[32];
};
static __thread char thread_name[32]; // plat_thread_name: copied at the start, as a detached thread's struct may go
const char *plat_thread_name(void) { return thread_name[0] ? thread_name : NULL; }
// What a thread made here keeps for itself is given back when its function returns: the handles below (the sleep
// timer of plat_wait_ns, the events of plat_file_pread and plat_file_pwrite), what the other files keep per thread
// (plat_thread_end_hook) and, for a detached thread, its PlatThread. The game makes and ends threads all through
// play, each a host thread: nothing closed them, so every ended thread that had slept, read or written left up
// to three kernel handles behind for the rest of the run. A guest thread ends by returning here too (kernel.c:
// ExitThread jumps back to the thread's start).
static void thread_handles_close(void);
// A stack overflow leaves the faulting thread a guard page's worth of stack, about 12 KiB of which a crash handler
// gets, and the report (main.c crash_handler) needs more: rt_log's 2 KiB buffers, the frame walk, and during the
// boot's builds a file write first (plat_file_write's frame is about 4 KiB). It faulted again inside the report, and
// the process ended with no FATAL line (the audit's probe: a 16 MiB-reserve thread recursing in 1 KiB frames). With
// 32 KiB kept back, the probe's report ran whole with room to spare. The guarantee belongs to the running fiber, so
// fiber_start asks for it too. Each thread and fiber commits that much more of its stack.
void plat_stack_guarantee(void) {
ULONG g = PLAT_STACK_GUARANTEE;
SetThreadStackGuarantee(&g);
}
static DWORD WINAPI thread_main(LPVOID p) {
PlatThread *t = p;
plat_stack_guarantee();
memcpy(thread_name, t->name, sizeof thread_name);
t->fn(t->arg);
plat_thread_end_run();
thread_handles_close();
if (plat_xchg32(&t->state, 2) == 1) free(t); // detached while it ran: nobody else frees it
return 0;
}
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);
t->h = CreateThread(NULL, stack ? stack : PLAT_STACK_DEFAULT, thread_main, t,
CREATE_SUSPENDED | STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
if (!t->h) { free(t); return NULL; }
if (name) {
wchar_t w[64];
if (MultiByteToWideChar(CP_UTF8, 0, name, -1, w, 64)) SetThreadDescription(t->h, w);
}
ResumeThread(t->h);
return t;
}
void plat_thread_join(PlatThread *t) {
WaitForSingleObject(t->h, INFINITE);
CloseHandle(t->h);
free(t);
}
int plat_thread_join_ns(PlatThread *t, uint64_t ns) {
uint64_t ms = (ns + 999999) / 1000000;
if (WaitForSingleObject(t->h, ms >= INFINITE ? INFINITE - 1 : (DWORD)ms) != WAIT_OBJECT_0) return 0;
CloseHandle(t->h);
free(t);
return 1;
}
// The thread still reads t->fn and t->arg when it starts, so the struct goes when the thread's function has
// returned (thread_main), or here when it already has.
void plat_thread_detach(PlatThread *t) {
CloseHandle(t->h);
t->h = NULL;
if (plat_xchg32(&t->state, 1) == 2) free(t);
}
int plat_cpu_cores(uint64_t *all, uint64_t *first) {
DWORD_PTR proc, sys;
*all = *first = 0;
if (!GetProcessAffinityMask(GetCurrentProcess(), &proc, &sys)) return 0;
*all = proc;
DWORD len = 0;
GetLogicalProcessorInformationEx(RelationProcessorCore, NULL, &len);
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *buf = len ? malloc(len) : NULL;
int cores = 0;
if (buf && GetLogicalProcessorInformationEx(RelationProcessorCore, buf, &len))
for (DWORD off = 0; off < len;) {
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *e = (void *)((char *)buf + off);
KAFFINITY m = e->Processor.GroupMask[0].Mask & proc;
if (e->Processor.GroupMask[0].Group == 0 && m) *first |= m & -m, cores++; // the lowest of the core's
off += e->Size;
}
free(buf);
return cores;
}
uint64_t plat_cpu_cache_die(uint32_t *l3_kb) {
DWORD_PTR proc, sys;
*l3_kb = 0;
if (!GetProcessAffinityMask(GetCurrentProcess(), &proc, &sys)) return 0;
DWORD len = 0;
GetLogicalProcessorInformationEx(RelationCache, NULL, &len);
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *buf = len ? malloc(len) : NULL;
uint64_t die = 0;
DWORD big = 0, small = ~0u;
if (buf && GetLogicalProcessorInformationEx(RelationCache, buf, &len))
for (DWORD off = 0; off < len;) {
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *e = (void *)((char *)buf + off);
const CACHE_RELATIONSHIP *c = &e->Cache;
if (c->Level == 3 && c->GroupMask.Group == 0) { // every L3, also those outside the process's set
if (c->CacheSize > big) big = c->CacheSize, die = 0;
if (c->CacheSize == big) die |= c->GroupMask.Mask;
if (c->CacheSize < small) small = c->CacheSize;
}
off += e->Size;
}
free(buf);
if (!die || big == small) return 0;
*l3_kb = big >> 10;
return die & proc;
}
int plat_process_affinity(uint64_t mask) { return SetProcessAffinityMask(GetCurrentProcess(), (DWORD_PTR)mask) != 0; }
uint64_t plat_thread_cpu_ns(uint32_t tid) {
HANDLE th = OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, tid);
FILETIME a, b, k, u;
uint64_t ns = 0;
if (th && GetThreadTimes(th, &a, &b, &k, &u))
ns = (((uint64_t)k.dwHighDateTime << 32 | k.dwLowDateTime) + ((uint64_t)u.dwHighDateTime << 32 | u.dwLowDateTime)) *
100;
if (th) CloseHandle(th);
return ns;
}
// ---- thread interruption: SuspendThread, Get/SetThreadContext, ResumeThread.
struct PlatIntr {
CONTEXT ctx; // first: CONTEXT wants 16-byte alignment
HANDLE h;
};
PlatIntr *plat_intr_self(void) {
HANDLE h = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION,
FALSE, GetCurrentThreadId());
PlatIntr *t = h ? _aligned_malloc(sizeof *t, 16) : NULL;
if (!t) {
if (h) CloseHandle(h);
return NULL;
}
t->h = h;
return t;
}
void plat_intr_free(PlatIntr *t) {
if (!t) return;
CloseHandle(t->h);
_aligned_free(t);
}
int plat_intr_stop(PlatIntr *t, uint64_t *rip, uint64_t *rsp) {
if (SuspendThread(t->h) == (DWORD)-1) return 0;
memset(&t->ctx, 0, sizeof t->ctx);
t->ctx.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER; // set back whole, so the kernel restores every GPR
if (!GetThreadContext(t->h, &t->ctx)) {
ResumeThread(t->h);
return 0;
}
*rip = t->ctx.Rip;
*rsp = t->ctx.Rsp;
return 1;
}
int plat_intr_resume(PlatIntr *t, uint64_t to) {
int ok = 1;
if (to) {
t->ctx.Rip = to;
ok = SetThreadContext(t->h, &t->ctx) != 0;
}
ResumeThread(t->h);
return ok;
}
// ---- hardware data watchpoints (platform.h plat_watch_set): the debug registers DR0-DR3 hold the addresses and DR7
// enables them, each for reads and writes (R/W 11) of its length. A thread's debug registers are part of its context:
// a helper thread lists the process's threads, then stops each, sets them with SetThreadContext and lets it go on; it
// opens every handle before it stops anything and takes no lock while a thread is stopped, so a thread stopped while
// holding the heap or loader lock cannot block it. The caller waits for the helper, which also sets the caller's own
// registers. An access traps after the instruction that made it, as EXCEPTION_SINGLE_STEP with the slot's bit in DR6;
// watch_veh hands it on and writes the current set into the context it continues with. So a thread that traps after
// its registers were cleared, or whose continue would restore registers from before a change, ends up with the
// current set too.
static PlatWatchHit watch_fn;
static volatile LONG watch_on;
static DWORD64 watch_addr[4], watch_dr7;
static int watch_added;
// frames[0] the instruction after the access, then the return addresses the unwind tables give
static int watch_unwind(const CONTEXT *at, uint64_t *frames) {
CONTEXT c = *at;
int d = 0;
frames[d++] = c.Rip;
while (d < PLAT_WATCH_DEPTH) {
DWORD64 base;
PRUNTIME_FUNCTION f = RtlLookupFunctionEntry(c.Rip, &base, NULL);
if (f) {
void *hd;
DWORD64 est;
RtlVirtualUnwind(UNW_FLAG_NHANDLER, base, c.Rip, f, &c, &hd, &est, NULL);
} else { // a leaf function: the return address is on top
c.Rip = *(DWORD64 *)c.Rsp;
c.Rsp += 8;
}
if (!c.Rip) break;
frames[d++] = c.Rip;
}
return d;
}
static LONG CALLBACK watch_veh(EXCEPTION_POINTERS *ep) {
if (ep->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP) return EXCEPTION_CONTINUE_SEARCH;
CONTEXT *c = ep->ContextRecord;
unsigned bits = (unsigned)(c->Dr6 & 0xf);
if (!bits) return EXCEPTION_CONTINUE_SEARCH; // a trap-flag step, not a watchpoint
int on = watch_on != 0, stop = !on;
if (on && watch_fn) {
uint64_t frames[PLAT_WATCH_DEPTH];
int d = watch_unwind(c, frames);
for (int s = 0; s < 4; s++)
if (bits >> s & 1 && watch_dr7 >> (2 * s) & 1) stop |= watch_fn(s, frames, d);
}
c->Dr0 = watch_addr[0], c->Dr1 = watch_addr[1], c->Dr2 = watch_addr[2], c->Dr3 = watch_addr[3];
c->Dr7 = stop ? 0 : watch_dr7;
c->Dr6 = 0;
c->ContextFlags |= CONTEXT_DEBUG_REGISTERS;
return EXCEPTION_CONTINUE_EXECUTION;
}
#define WATCH_THREADS 4096
static DWORD WINAPI watch_apply(void *arg) {
(void)arg;
static DWORD tids[WATCH_THREADS];
static HANDLE hs[WATCH_THREADS];
DWORD self = GetCurrentThreadId(), pid = GetCurrentProcessId();
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (snap == INVALID_HANDLE_VALUE) return (DWORD)-1;
int nt = 0;
THREADENTRY32 te = {.dwSize = sizeof te};
for (BOOL ok = Thread32First(snap, &te); ok && nt < WATCH_THREADS; ok = Thread32Next(snap, &te))
if (te.th32OwnerProcessID == pid && te.th32ThreadID != self) tids[nt++] = te.th32ThreadID;
CloseHandle(snap);
int nh = 0;
for (int i = 0; i < nt; i++) {
HANDLE h = OpenThread(THREAD_SUSPEND_RESUME | THREAD_SET_CONTEXT, FALSE, tids[i]);
if (h) hs[nh++] = h;
}
static CONTEXT ctx __attribute__((aligned(16)));
memset(&ctx, 0, sizeof ctx);
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
ctx.Dr0 = watch_addr[0], ctx.Dr1 = watch_addr[1], ctx.Dr2 = watch_addr[2], ctx.Dr3 = watch_addr[3];
ctx.Dr7 = watch_on ? watch_dr7 : 0;
DWORD done = 0;
for (int i = 0; i < nh; i++) { // nothing between the stop and the go-on but the context call
if (SuspendThread(hs[i]) != (DWORD)-1) {
if (SetThreadContext(hs[i], &ctx)) done++;
ResumeThread(hs[i]);
}
}
for (int i = 0; i < nh; i++) CloseHandle(hs[i]);
return done;
}
int plat_watch_set(const uint64_t *addr, const int *len, int n, PlatWatchHit hit) {
if (n < 0) n = 0;
if (n > 4) n = 4;
if (!watch_added) {
if (!n) return 0;
if (!AddVectoredExceptionHandler(1, watch_veh)) return -1;
watch_added = 1;
}
DWORD64 dr7 = 0;
for (int i = 0; i < 4; i++) watch_addr[i] = i < n ? addr[i] : 0;
for (int i = 0; i < n; i++) {
DWORD64 size = len[i] == 8 ? 2 : len[i] == 4 ? 3 : len[i] == 2 ? 1 : 0; // DR7's LEN encoding
dr7 |= 1ull << (2 * i) | 3ull << (16 + 4 * i) | size << (18 + 4 * i);
}
// the hook and the registers are in place before watch_on says so, and a clear takes watch_on away first
if (n) watch_fn = hit, watch_dr7 = dr7;
InterlockedExchange(&watch_on, n > 0);
HANDLE t = CreateThread(NULL, 64 << 10, watch_apply, NULL, 0, NULL);
DWORD code = (DWORD)-1;
if (!t || WaitForSingleObject(t, 10000) != WAIT_OBJECT_0 || !GetExitCodeThread(t, &code)) code = (DWORD)-1;
if (t) CloseHandle(t);
if (code == (DWORD)-1 && n) InterlockedExchange(&watch_on, 0);
return code == (DWORD)-1 ? -1 : (int)code;
}
// ---- fibers: Windows fibers with FIBER_FLAG_FLOAT_SWITCH (the MXCSR and x87 control word switch with them).
struct PlatFiber {
void *h;
void (*fn)(void *arg);
void *arg;
};
static __thread PlatFiber self_fiber;
PlatFiber *plat_fiber_self(void) {
self_fiber.h = ConvertThreadToFiberEx(NULL, FIBER_FLAG_FLOAT_SWITCH);
return self_fiber.h ? &self_fiber : NULL;
}
void plat_fiber_unself(void) {
ConvertFiberToThread();
self_fiber.h = NULL;
}
static void WINAPI fiber_start(void *p) {
PlatFiber *f = p;
plat_stack_guarantee(); // a guest fiber's stack is its own (kernel.c kern_fiber_init)
f->fn(f->arg);
}
PlatFiber *plat_fiber_new(size_t stack, void (*fn)(void *arg), void *arg) {
PlatFiber *f = malloc(sizeof *f);
if (!f) return NULL;
f->fn = fn, f->arg = arg;
f->h = CreateFiberEx(0x10000, stack, FIBER_FLAG_FLOAT_SWITCH, fiber_start, f);
if (!f->h) {
free(f);
return NULL;
}
return f;
}
void plat_fiber_switch(PlatFiber *to) { SwitchToFiber(to->h); }
void plat_fiber_free(PlatFiber *f) {
if (!f) return;
DeleteFiber(f->h);
free(f);
}
// plat_jmp_save / plat_jmp_go: PlatJmp holds rbx, rbp, rdi, rsi, r12-r15, the caller's rsp and the return address at
// 0..72, MXCSR at 80, the x87 control word at 84 and xmm6-xmm15 from 96, the Win64 callee-saved state. Both are leaf
// code that does not move rsp before it leaves, so they need no unwind data.
_Static_assert(sizeof(PlatJmp) == 256 && __builtin_offsetof(PlatJmp, xmm) == 96, "plat_jmp_save layout");
__asm__(".text\n"
".globl plat_jmp_save\n"
".def plat_jmp_save; .scl 2; .type 32; .endef\n"
".p2align 4\n"
"plat_jmp_save:\n"
" movq %rbx, 0(%rcx)\n"
" movq %rbp, 8(%rcx)\n"
" movq %rdi, 16(%rcx)\n"
" movq %rsi, 24(%rcx)\n"
" movq %r12, 32(%rcx)\n"
" movq %r13, 40(%rcx)\n"
" movq %r14, 48(%rcx)\n"
" movq %r15, 56(%rcx)\n"
" leaq 8(%rsp), %rdx\n" // the caller's rsp after this call returns
" movq %rdx, 64(%rcx)\n"
" movq (%rsp), %rdx\n"
" movq %rdx, 72(%rcx)\n"
" stmxcsr 80(%rcx)\n"
" fnstcw 84(%rcx)\n"
" movdqu %xmm6, 96(%rcx)\n"
" movdqu %xmm7, 112(%rcx)\n"
" movdqu %xmm8, 128(%rcx)\n"
" movdqu %xmm9, 144(%rcx)\n"
" movdqu %xmm10, 160(%rcx)\n"
" movdqu %xmm11, 176(%rcx)\n"
" movdqu %xmm12, 192(%rcx)\n"
" movdqu %xmm13, 208(%rcx)\n"
" movdqu %xmm14, 224(%rcx)\n"
" movdqu %xmm15, 240(%rcx)\n"
" xorl %eax, %eax\n"
" ret\n"
".globl plat_jmp_go\n"
".def plat_jmp_go; .scl 2; .type 32; .endef\n"
".p2align 4\n"
"plat_jmp_go:\n"
" movq 0(%rcx), %rbx\n"
" movq 8(%rcx), %rbp\n"
" movq 16(%rcx), %rdi\n"
" movq 24(%rcx), %rsi\n"
" movq 32(%rcx), %r12\n"
" movq 40(%rcx), %r13\n"
" movq 48(%rcx), %r14\n"
" movq 56(%rcx), %r15\n"
" ldmxcsr 80(%rcx)\n"
" fldcw 84(%rcx)\n"
" movdqu 96(%rcx), %xmm6\n"
" movdqu 112(%rcx), %xmm7\n"
" movdqu 128(%rcx), %xmm8\n"
" movdqu 144(%rcx), %xmm9\n"
" movdqu 160(%rcx), %xmm10\n"
" movdqu 176(%rcx), %xmm11\n"
" movdqu 192(%rcx), %xmm12\n"
" movdqu 208(%rcx), %xmm13\n"
" movdqu 224(%rcx), %xmm14\n"
" movdqu 240(%rcx), %xmm15\n"
" movq 64(%rcx), %rsp\n"
" movl $1, %eax\n"
" jmpq *72(%rcx)\n");
int plat_frame_capture(PlatJmp *out, int (*want)(uintptr_t rip, void *arg), void *arg) {
CONTEXT c;
RtlCaptureContext(&c);
ULONG_PTR low, high;
GetCurrentThreadStackLimits(&low, &high);
for (int i = 0; i < 256 && c.Rsp >= low && c.Rsp + 8 <= high; i++) {
DWORD64 img;
RUNTIME_FUNCTION *fe = RtlLookupFunctionEntry(c.Rip, &img, NULL);
if (fe) {
void *data;
DWORD64 frame;
RtlVirtualUnwind(UNW_FLAG_NHANDLER, img, c.Rip, fe, &c, &data, &frame, NULL);
} else { // a leaf: the return address is on top of the stack
c.Rip = *(const DWORD64 *)c.Rsp;
c.Rsp += 8;
}
if (!c.Rip) return 0;
if (!want((uintptr_t)c.Rip, arg)) continue;
out->gpr[0] = c.Rbx, out->gpr[1] = c.Rbp, out->gpr[2] = c.Rdi, out->gpr[3] = c.Rsi;
out->gpr[4] = c.R12, out->gpr[5] = c.R13, out->gpr[6] = c.R14, out->gpr[7] = c.R15;
out->gpr[8] = c.Rsp, out->gpr[9] = c.Rip;
__asm__ volatile("stmxcsr %0\n\tfnstcw %1" : "=m"(out->mxcsr), "=m"(out->fpcw));
memcpy(out->xmm, &c.Xmm6, 160); // xmm6-xmm15, consecutive in CONTEXT
return 1;
}
return 0;
}
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;
}
uint64_t plat_thread_affinity(PlatThread *t, uint64_t mask) {
return (uint64_t)SetThreadAffinityMask(t ? t->h : GetCurrentThread(), (DWORD_PTR)mask);
}
// The calling thread gets a core of its own through CPU sets: the process's default sets, which every thread without
// sets of its own runs on (those started later too), leave the core out, and the caller's selected sets are that
// core. The core is the highest-numbered one of the fastest efficiency class (a performance core on a hybrid CPU),
// with both its logical processors when it has two; only processors in group 0 and in the process's affinity mask
// count. CPU sets are soft: the scheduler may still use the core when nothing else wants it. Returns the logical
// processors reserved, 0 when Windows has no CPU sets (before Windows 10) or fewer than two cores qualify; *which
// (when not NULL, size bytes) receives their numbers, such as "10,11".
int plat_cpu_reserve(char *which, size_t size) {
typedef BOOL(WINAPI * GetInfoFn)(PSYSTEM_CPU_SET_INFORMATION, ULONG, PULONG, HANDLE, ULONG);
typedef BOOL(WINAPI * SetProcFn)(HANDLE, const ULONG *, ULONG);
typedef BOOL(WINAPI * SetThreadFn)(HANDLE, const ULONG *, ULONG);
HMODULE k = GetModuleHandleW(L"kernel32.dll");
GetInfoFn get = (GetInfoFn)(void *)GetProcAddress(k, "GetSystemCpuSetInformation");
SetProcFn set_proc = (SetProcFn)(void *)GetProcAddress(k, "SetProcessDefaultCpuSets");
SetThreadFn set_thread = (SetThreadFn)(void *)GetProcAddress(k, "SetThreadSelectedCpuSets");
if (!get || !set_proc || !set_thread) return 0;
ULONG len = 0;
get(NULL, 0, &len, GetCurrentProcess(), 0);
uint8_t *buf = len ? malloc(len) : NULL;
if (!buf || !get((PSYSTEM_CPU_SET_INFORMATION)buf, len, &len, GetCurrentProcess(), 0)) {
free(buf);
return 0;
}
DWORD_PTR proc = 0, sys = 0;
if (!GetProcessAffinityMask(GetCurrentProcess(), &proc, &sys) || !proc) proc = ~(DWORD_PTR)0;
int best_class = -1, best_core = -1;
for (ULONG o = 0; o + sizeof(DWORD) * 2 <= len;) { // the core: fastest class, then highest index
const SYSTEM_CPU_SET_INFORMATION *s = (const void *)(buf + o);
if (!s->Size) break;
if (s->Type == CpuSetInformation && s->CpuSet.Group == 0 && s->CpuSet.LogicalProcessorIndex < 64 &&
(proc >> s->CpuSet.LogicalProcessorIndex & 1)) {
int cls = s->CpuSet.EfficiencyClass, core = s->CpuSet.CoreIndex;
if (cls > best_class || (cls == best_class && core > best_core)) best_class = cls, best_core = core;
}
o += s->Size;
}
ULONG rest[64], mine[8];
unsigned mine_cpu[8]; // the logical processor numbers of mine
int nrest = 0, nmine = 0;
for (ULONG o = 0; o + sizeof(DWORD) * 2 <= len;) {
const SYSTEM_CPU_SET_INFORMATION *s = (const void *)(buf + o);
if (!s->Size) break;
if (s->Type == CpuSetInformation && s->CpuSet.Group == 0 && s->CpuSet.LogicalProcessorIndex < 64 &&
(proc >> s->CpuSet.LogicalProcessorIndex & 1)) {
if (s->CpuSet.EfficiencyClass == best_class && s->CpuSet.CoreIndex == best_core) {
if (nmine < 8) mine_cpu[nmine] = s->CpuSet.LogicalProcessorIndex, mine[nmine++] = s->CpuSet.Id;
} else if (nrest < 64) {
rest[nrest++] = s->CpuSet.Id;
}
}
o += s->Size;
}
free(buf);
if (!nmine || nrest < 2) return 0;
if (!set_proc(GetCurrentProcess(), rest, (ULONG)nrest)) return 0;
if (!set_thread(GetCurrentThread(), mine, (ULONG)nmine)) {
set_proc(GetCurrentProcess(), NULL, 0); // back to every processor
return 0;
}
if (which && size) {
size_t n = 0;
which[0] = 0;
for (int i = 0; i < nmine && n < size; i++)
n += (size_t)snprintf(which + n, size - n, "%s%u", n ? "," : "", mine_cpu[i]);
}
return nmine;
}
int plat_cpu_reserve_sweep(void) { return 0; }
uint32_t plat_cpu_count(void) {
DWORD n = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
return n ? (uint32_t)n : 1;
}
// The processors of the process's affinity mask: what `start /affinity`, Task Manager's "Set affinity" or a launcher
// leaves this process. A process that spans processor groups (more than 64 logical processors) gets no mask from
// GetProcessAffinityMask (both masks 0): every active processor counts then.
uint32_t plat_cpu_usable(void) {
DWORD_PTR proc = 0, sys = 0;
if (GetProcessAffinityMask(GetCurrentProcess(), &proc, &sys) && proc && sys) {
uint32_t n = (uint32_t)__builtin_popcountll((unsigned long long)proc);
if (n) return n;
}
return plat_cpu_count();
}
// PLAT_PRIO_* are the THREAD_PRIORITY_* values.
void plat_thread_priority(int prio) { SetThreadPriority(GetCurrentThread(), prio); }
void plat_thread_set_priority(PlatThread *t, int prio) { SetThreadPriority(t->h, prio); }
int plat_thread_priority_get(void) { return GetThreadPriority(GetCurrentThread()); }
void plat_yield(void) { SwitchToThread(); }
// ---- time
static uint64_t qpc_freq(void) {
static uint64_t freq;
if (!freq) {
LARGE_INTEGER f;
QueryPerformanceFrequency(&f);
freq = (uint64_t)f.QuadPart;
}
return freq;
}
uint64_t plat_counter(void) {
LARGE_INTEGER t;
QueryPerformanceCounter(&t);
return (uint64_t)t.QuadPart;
}
uint64_t plat_counter_hz(void) { return qpc_freq(); }
// The default timer tick is 15.6 ms; a 1 ms wait would cost a full tick.
void plat_timer_1ms(void) { timeBeginPeriod(1); }
// Windows 11 runs a process whose window is not in front (another window focused, a second monitor's overlay) at
// efficiency speed (EcoQoS, the E-cores of a hybrid CPU) and may ignore its timer resolution request, which turns the
// 1 ms waits the pacing and short sleeps rely on into 15.6 ms ones. Opting out keeps full speed and the 1 ms timer.
// The ignore-timer-resolution control needs Windows 11; on Windows 10 the call fails and execution speed alone is asked.
#ifndef PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION
#define PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 0x4
#endif
int plat_power_full(void) {
PROCESS_POWER_THROTTLING_STATE st = {.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION,
.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION,
.StateMask = 0};
if (SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &st, sizeof st)) return 2;
st.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED;
return SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &st, sizeof st) ? 1 : 0;
}
static PlatScale qpc_ns; // set before main, so no thread reads it half written
__attribute__((constructor)) static void qpc_ns_init(void) { qpc_ns = plat_scale(1000000000u, qpc_freq()); }
uint64_t plat_now_ns(void) {
LARGE_INTEGER t;
QueryPerformanceCounter(&t);
return plat_scaled(qpc_ns, (uint64_t)t.QuadPart);
}
// A high-resolution waitable timer per sleeping thread (Windows 10 1803+; one handle per thread, closed when the
// thread's function returns, thread_handles_close)
// wakes within about 0.1-0.5 ms; the last PLAT_SPIN_NS are spent yielding so the deadline is met closely.
// Without that timer kind, Sleep(1) granularity applies.
#define PLAT_SPIN_NS 300000u
static __thread HANDLE sleep_timer;
static __thread int sleep_timer_tried;
static __thread HANDLE pread_ev, pwrite_ev; // plat_file_pread, plat_file_pwrite: the event of their overlapped call
static void thread_handles_close(void) {
if (sleep_timer) CloseHandle(sleep_timer);
if (pread_ev) CloseHandle(pread_ev);
if (pwrite_ev) CloseHandle(pwrite_ev);
sleep_timer = pread_ev = pwrite_ev = NULL, sleep_timer_tried = 0;
}
void plat_wait_ns(uint64_t ns) {
if (!sleep_timer_tried) {
sleep_timer_tried = 1;
sleep_timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
}
LARGE_INTEGER due = {.QuadPart = -(LONGLONG)(ns / 100)}; // relative, 100 ns units
if (!due.QuadPart) due.QuadPart = -1;
if (sleep_timer && SetWaitableTimerEx(sleep_timer, &due, 0, NULL, NULL, NULL, 0))
WaitForSingleObject(sleep_timer, INFINITE);
else Sleep((DWORD)((ns + 999999) / 1000000));
}
void plat_sleep_ns(uint64_t ns) {
uint64_t end = plat_now_ns() + ns;
if (ns > PLAT_SPIN_NS) plat_wait_ns(ns - PLAT_SPIN_NS);
while (plat_now_ns() < end) SwitchToThread();
}
void plat_sleep_ms(uint32_t ms) { Sleep(ms); }
// ---- locks and condition variables
void plat_mutex_init(PlatMutex *m) { InitializeSRWLock((PSRWLOCK)m); }
static _Thread_local PlatLockNote lock_note; // plat_lock_timing
void plat_lock_timing(PlatLockNote fn) { lock_note = fn; }
void plat_mutex_lock_at(PlatMutex *m, void *site) {
if (!lock_note || TryAcquireSRWLockExclusive((PSRWLOCK)m)) {
if (!lock_note) AcquireSRWLockExclusive((PSRWLOCK)m);
return;
}
uint64_t t0 = plat_now_ns();
AcquireSRWLockExclusive((PSRWLOCK)m);
lock_note(site, plat_now_ns() - t0);
}
void plat_mutex_lock(PlatMutex *m) {
if (!lock_note) { AcquireSRWLockExclusive((PSRWLOCK)m); return; }
plat_mutex_lock_at(m, __builtin_return_address(0));
}
int plat_mutex_trylock(PlatMutex *m) { return TryAcquireSRWLockExclusive((PSRWLOCK)m) != 0; }
void plat_mutex_unlock(PlatMutex *m) { ReleaseSRWLockExclusive((PSRWLOCK)m); }
void plat_mutex_lock_shared(PlatMutex *m) {
if (!lock_note || TryAcquireSRWLockShared((PSRWLOCK)m)) {
if (!lock_note) AcquireSRWLockShared((PSRWLOCK)m);
return;
}
uint64_t t0 = plat_now_ns();
AcquireSRWLockShared((PSRWLOCK)m);
lock_note(__builtin_return_address(0), plat_now_ns() - t0);
}
int plat_mutex_trylock_shared(PlatMutex *m) { return TryAcquireSRWLockShared((PSRWLOCK)m) != 0; }
void plat_mutex_unlock_shared(PlatMutex *m) { ReleaseSRWLockShared((PSRWLOCK)m); }
void plat_cond_init(PlatCond *c) { InitializeConditionVariable((PCONDITION_VARIABLE)c); }
void plat_cond_wait(PlatCond *c, PlatMutex *m) { SleepConditionVariableSRW((PCONDITION_VARIABLE)c, (PSRWLOCK)m, INFINITE, 0); }
int plat_cond_wait_ns(PlatCond *c, PlatMutex *m, uint64_t ns) {
uint64_t ms = (ns + 999999) / 1000000;
DWORD to = ms >= INFINITE ? INFINITE - 1 : (DWORD)ms;
return SleepConditionVariableSRW((PCONDITION_VARIABLE)c, (PSRWLOCK)m, to, 0) ? 1 : GetLastError() != ERROR_TIMEOUT;
}
void plat_cond_signal(PlatCond *c) { WakeConditionVariable((PCONDITION_VARIABLE)c); }
void plat_cond_broadcast(PlatCond *c) { WakeAllConditionVariable((PCONDITION_VARIABLE)c); }
// ---- files
static int widen(const char *s, wchar_t *w, int cap) {
int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, w, cap);
if (!n) w[0] = 0;
return n;
}
#define WPATH 2048
int plat_file_exists(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return 0;
DWORD a = GetFileAttributesW(w);
return a != INVALID_FILE_ATTRIBUTES && !(a & FILE_ATTRIBUTE_DIRECTORY);
}
int plat_dir_exists(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return 0;
DWORD a = GetFileAttributesW(w);
return a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_DIRECTORY);
}
int plat_full_path(const char *path, char *out, size_t cap) {
wchar_t w[WPATH], full[WPATH];
if (!widen(path, w, WPATH)) return 0;
DWORD n = GetFullPathNameW(w, WPATH, full, NULL);
if (!n || n >= WPATH) return 0;
return WideCharToMultiByte(CP_UTF8, 0, full, -1, out, (int)cap, NULL, NULL) != 0;
}
void plat_file_mkdir_p(const char *path) {
wchar_t tmp[WPATH];
if (!widen(path, tmp, WPATH)) return;
for (wchar_t *p = tmp + 1; *p; p++) {
if (*p == L'\\' || *p == L'/') {
wchar_t c = *p;
*p = 0;
CreateDirectoryW(tmp, NULL);
*p = c;
}
}
CreateDirectoryW(tmp, NULL);
}
uint8_t *plat_file_read(const char *path, uint64_t *size) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return NULL;
HANDLE h = CreateFileW(w, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return NULL;
LARGE_INTEGER sz;
uint8_t *buf = NULL;
if (GetFileSizeEx(h, &sz) && sz.QuadPart > 0 && sz.QuadPart < (1ll << 30)) {
buf = malloc((size_t)sz.QuadPart + 1);
DWORD got = 0;
if (buf && (!ReadFile(h, buf, (DWORD)sz.QuadPart, &got, NULL) || got != sz.QuadPart)) {
free(buf);
buf = NULL;
}
if (buf) {
buf[sz.QuadPart] = 0;
*size = (uint64_t)sz.QuadPart;
}
}
CloseHandle(h);
return buf;
}
int plat_file_write(const char *path, const void *data, size_t n) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return 0;
HANDLE h = CreateFileW(w, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return 0;
DWORD put = 0;
int ok = WriteFile(h, data, (DWORD)n, &put, NULL) && put == n;
CloseHandle(h);
return ok;
}
PlatAppend *plat_append_open(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return NULL;
HANDLE h = CreateFileW(w, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, NULL);
return h == INVALID_HANDLE_VALUE ? NULL : (PlatAppend *)h;
}
int plat_append_write(PlatAppend *f, const void *data, size_t n) {
// Offset and OffsetHigh all ones: WriteFile appends at the end of the file, atomically against other writers.
OVERLAPPED o = {.Offset = 0xffffffff, .OffsetHigh = 0xffffffff};
DWORD put = 0;
return WriteFile((HANDLE)f, data, (DWORD)n, &put, &o) && put == n;
}
int plat_file_replace(const char *from, const char *to, int durable) {
wchar_t a[WPATH], b[WPATH];
if (!widen(from, a, WPATH) || !widen(to, b, WPATH)) return 0;
return MoveFileExW(a, b, MOVEFILE_REPLACE_EXISTING | (durable ? MOVEFILE_WRITE_THROUGH : 0)) != 0;
}
static char lock_held[4][1100];
static HANDLE lock_handle[4];
static int lock_n;
static PlatMutex lock_lock = PLAT_MUTEX_INIT;
int plat_lock_file(const char *path) {
plat_mutex_lock(&lock_lock);
int r = 1, nheld = lock_n;
for (int i = 0; i < nheld; i++)
if (!strcmp(lock_held[i], path)) goto done;
wchar_t w[WPATH];
// no sharing at all: a second process's open fails with a sharing violation; the handle stays open until
// plat_unlock_file or the process ends
HANDLE h = widen(path, w, WPATH) ? CreateFileW(w, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL, NULL)
: INVALID_HANDLE_VALUE;
if (h == INVALID_HANDLE_VALUE) {
r = GetLastError() == ERROR_SHARING_VIOLATION ? 0 : -1;
goto done;
}
if (nheld < 4) {
snprintf(lock_held[nheld], sizeof lock_held[0], "%s", path);
lock_handle[nheld] = h;
lock_n = nheld + 1;
}
done:
plat_mutex_unlock(&lock_lock);
return r;
}
void plat_unlock_file(const char *path) {
plat_mutex_lock(&lock_lock);
for (int i = 0; i < lock_n; i++)
if (!strcmp(lock_held[i], path)) {
CloseHandle(lock_handle[i]);
lock_n--;
memmove(&lock_held[i], &lock_held[i + 1], sizeof lock_held[0] * (size_t)(lock_n - i));
memmove(&lock_handle[i], &lock_handle[i + 1], sizeof lock_handle[0] * (size_t)(lock_n - i));
break;
}
plat_mutex_unlock(&lock_lock);
}
// path.<pid>.<tid>.tmp renamed over path; durable: flushed to the disk before the rename, which is written through.
static int write_swap(const char *path, const void *data, size_t n, int durable) {
char tmp[WPATH + 48];
wchar_t a[WPATH], b[WPATH];
int len = snprintf(tmp, sizeof tmp, "%s.%u.%u.tmp", path, (unsigned)GetCurrentProcessId(), (unsigned)GetCurrentThreadId());
if (len < 0 || len >= (int)sizeof tmp || !widen(tmp, a, WPATH) || !widen(path, b, WPATH)) return 0;
HANDLE h = CreateFileW(a, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return 0;
// In pieces: WriteFile takes a 32-bit count, so a block of 4 GB or more (a pipeline cache can grow that far)
// would otherwise be cut to its low 32 bits and the write refused.
int ok = 1;
for (size_t at = 0; ok && at < n;) {
DWORD piece = n - at > (1u << 30) ? (1u << 30) : (DWORD)(n - at), put = 0;
ok = WriteFile(h, (const char *)data + at, piece, &put, NULL) && put == piece;
at += piece;
}
ok = ok && (!durable || FlushFileBuffers(h));
CloseHandle(h);
if (ok && MoveFileExW(a, b, MOVEFILE_REPLACE_EXISTING | (durable ? MOVEFILE_WRITE_THROUGH : 0))) return 1;
DeleteFileW(a);
return 0;
}
int plat_file_write_atomic(const char *path, const void *data, size_t n) { return write_swap(path, data, n, 1); }
int plat_file_write_cached(const char *path, const void *data, size_t n) { return write_swap(path, data, n, 0); }
int plat_file_flush(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return 0;
HANDLE h = CreateFileW(w, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (h == INVALID_HANDLE_VALUE) return 0;
int ok = FlushFileBuffers(h) != 0;
CloseHandle(h);
return ok;
}
int plat_file_sync(FILE *f) { return !fflush(f) && !_commit(_fileno(f)); }
void plat_file_delete(const char *path) {
wchar_t w[WPATH];
if (widen(path, w, WPATH)) DeleteFileW(w);
}
int plat_file_truncate(const char *path, uint64_t size) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return 0;
HANDLE h = CreateFileW(w, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (h == INVALID_HANDLE_VALUE) return 0;
LARGE_INTEGER at = {.QuadPart = (LONGLONG)size};
int ok = SetFilePointerEx(h, at, NULL, FILE_BEGIN) && SetEndOfFile(h);
CloseHandle(h);
return ok;
}
int64_t plat_file_size(const char *path) {
wchar_t w[WPATH];
WIN32_FILE_ATTRIBUTE_DATA d;
if (!widen(path, w, WPATH) || !GetFileAttributesExW(w, GetFileExInfoStandard, &d)) return -1;
return (int64_t)(((uint64_t)d.nFileSizeHigh << 32) | d.nFileSizeLow);
}
int plat_file_set_readonly(const char *path, int on) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return 0;
DWORD a = GetFileAttributesW(w);
if (a == INVALID_FILE_ATTRIBUTES) return 0;
return SetFileAttributesW(w, on ? a | FILE_ATTRIBUTE_READONLY : a & ~(DWORD)FILE_ATTRIBUTE_READONLY) != 0;
}
// The file or folder gets an access list of its own with one entry, full control for the account the game runs as,
// and inherits none from its folder: a game folder outside the user's own folders is often readable by every account
// of the PC. Administrators can still take ownership, as on any file.
int plat_file_private(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH) || GetFileAttributesW(w) == INVALID_FILE_ATTRIBUTES) return 0;
HANDLE tok = NULL;
union { TOKEN_USER u; BYTE raw[sizeof(TOKEN_USER) + SECURITY_MAX_SID_SIZE]; } user;
DWORD got = 0;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tok)) return 0;
BOOL have = GetTokenInformation(tok, TokenUser, &user, sizeof user, &got);
CloseHandle(tok);
if (!have) return 0;
union { ACL acl; BYTE raw[sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) + SECURITY_MAX_SID_SIZE]; } list;
if (!InitializeAcl(&list.acl, sizeof list, ACL_REVISION) ||
!AddAccessAllowedAceEx(&list.acl, ACL_REVISION, OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE, GENERIC_ALL, user.u.User.Sid))
return 0;
return SetNamedSecurityInfoW(w, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, NULL,
NULL, &list.acl, NULL) == ERROR_SUCCESS;
}
int plat_file_list(const char *dir, const char *ext, int (*cb)(const char *name, void *ctx), void *ctx) {
wchar_t wd[WPATH], we[64], pat[WPATH + 64];
if (!widen(dir, wd, WPATH) || !widen(ext, we, 64)) return 0;
swprintf(pat, WPATH + 64, L"%ls\\*%ls", wd, we);
WIN32_FIND_DATAW fd;
HANDLE h = FindFirstFileW(pat, &fd);
if (h == INVALID_HANDLE_VALUE) return 0;
do {
char name[1024];
if (!WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, name, sizeof name, NULL, NULL)) continue;
if (!cb(name, ctx)) break;
} while (FindNextFileW(h, &fd));
FindClose(h);
return 1;
}
// ---- virtual memory: VirtualAlloc reservations, MEM_WRITE_WATCH for the watched ones.
int plat_mem_reserve(uint64_t addr, uint64_t n, int watch) {
void *p = VirtualAlloc((void *)(uintptr_t)addr, n, MEM_RESERVE | (watch ? MEM_WRITE_WATCH : 0), PAGE_NOACCESS);
if (p && p != (void *)(uintptr_t)addr) {
VirtualFree(p, 0, MEM_RELEASE);
p = NULL;
}
return p != NULL;
}
void *plat_mem_alloc(size_t n) { return VirtualAlloc(NULL, n, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); }
int plat_mem_commit(uint64_t addr, size_t n) {
return VirtualAlloc((void *)(uintptr_t)addr, n, MEM_COMMIT, PAGE_READWRITE) != NULL;
}
// Decommitted pages come back zeroed when committed again.
int plat_mem_decommit(uint64_t addr, uint64_t n) { return VirtualFree((void *)(uintptr_t)addr, n, MEM_DECOMMIT) != 0; }
int plat_mem_protect(uint64_t addr, uint64_t n, int prot) {
DWORD old;
DWORD p = prot == PLAT_PROT_RW ? PAGE_READWRITE : prot == PLAT_PROT_READ ? PAGE_READONLY : PAGE_NOACCESS;
return VirtualProtect((void *)(uintptr_t)addr, n, p, &old) != 0;
}
int plat_mem_state(uint64_t addr, uint64_t limit, uint64_t *end) {
MEMORY_BASIC_INFORMATION mbi;
if (!VirtualQuery((void *)(uintptr_t)addr, &mbi, sizeof mbi)) {
*end = limit;
return PLAT_MEM_FREE;
}
uint64_t e = (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
*end = e < limit ? e : limit;
if (mbi.State == MEM_RESERVE) return PLAT_MEM_RESERVED;
if (mbi.State != MEM_COMMIT) return PLAT_MEM_FREE;
DWORD p = mbi.Protect & 0xff;
return p == PAGE_READWRITE ? PLAT_PROT_RW : p == PAGE_READONLY ? PLAT_PROT_READ : PLAT_PROT_NONE;
}
int plat_mem_census(uint64_t *priv, uint64_t *wc, uint64_t *wc_allocs) {
*priv = *wc = *wc_allocs = 0;
MEMORY_BASIC_INFORMATION mbi;
uintptr_t a = 0;
void *last = NULL;
while (VirtualQuery((void *)a, &mbi, sizeof mbi) == sizeof mbi) {
if (mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE) {
*priv += mbi.RegionSize;
if (mbi.Protect & PAGE_WRITECOMBINE) {
*wc += mbi.RegionSize;
if (mbi.AllocationBase != last) (*wc_allocs)++, last = mbi.AllocationBase;
}
}
uintptr_t next = (uintptr_t)mbi.BaseAddress + mbi.RegionSize;
if (next <= a) break;
a = next;
}
return 1;
}
int plat_mem_prepare_write(uint64_t addr, uint64_t n) {
(void)addr; (void)n;
return 0;
}
// Guest memory as dma-bufs is Linux's (platform.h).
int plat_mem_dmabuf_ok(uint64_t *limit, uint64_t *any, const char **why) { *limit = *any = 0, *why = ""; return 0; }
int plat_mem_dmabuf(uint64_t addr, uint64_t n) { (void)addr; (void)n; return -1; }
void plat_mem_dmabuf_done(uint64_t addr, uint64_t n) { (void)addr; (void)n; }
// Contains code Copyright (c) 2026 Peiikai (MIT; see THIRD_PARTY_NOTICES.md).
// GetWriteWatch with the reset flag.
uint64_t plat_mem_written(uint64_t addr, uint64_t n, uint64_t *pages, uint64_t max, int reset) {
if (!max || !n) return 0;
ULONG_PTR got = (ULONG_PTR)max;
DWORD gran;
_Static_assert(sizeof(PVOID) == sizeof(uint64_t), "page addresses are written straight into a uint64_t array");
if (GetWriteWatch(reset ? WRITE_WATCH_FLAG_RESET : 0, (void *)(uintptr_t)addr, (SIZE_T)n, (PVOID *)pages, &got,
&gran))
return UINT64_MAX;
return got;
}
static int (*fault_hook)(uint64_t addr);
static int (*access_hook)(uint64_t addr, int write, uint64_t ip);
// A read or write access violation (ExceptionInformation[0] 0 or 1) goes to the access hook when there is one, then a
// write to the write-fault hook; registered first, before the crash handler.
static LONG CALLBACK fault_veh(EXCEPTION_POINTERS *ep) {
const EXCEPTION_RECORD *er = ep->ExceptionRecord;
if (er->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || er->NumberParameters < 2 || er->ExceptionInformation[0] > 1)
return EXCEPTION_CONTINUE_SEARCH;
int write = er->ExceptionInformation[0] == 1;
uint64_t addr = (uint64_t)er->ExceptionInformation[1];
if (access_hook && access_hook(addr, write, (uint64_t)(uintptr_t)er->ExceptionAddress)) return EXCEPTION_CONTINUE_EXECUTION;
if (!write) return EXCEPTION_CONTINUE_SEARCH;
return fault_hook(addr) ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH;
}
void plat_mem_access_hook(int (*fn)(uint64_t addr, int write, uint64_t ip)) { access_hook = fn; }
void plat_mem_fault_hook(int (*fn)(uint64_t addr)) {
fault_hook = fn;
AddVectoredExceptionHandler(1, fault_veh);
}
static void (*crash_fn)(const PlatCrash *c);
// A line written straight to the standard error handle, for the crash path: no C runtime buffering or locks.
static const char *image_name(const char *base);
static __thread const char *crash_ctx;
void plat_crash_context(const char *s) { crash_ctx = s; }
const char *plat_crash_context_get(void) { return crash_ctx; }
static void raw_note(const char *tag, DWORD code, uintptr_t rip, uintptr_t addr) {
char b[256];
void *base = NULL;
RtlPcToFileHeader((void *)rip, &base);
ULONG_PTR lo = 0, hi = 0;
GetCurrentThreadStackLimits(&lo, &hi);
int n = snprintf(b, sizeof b, "[host] %s: exception %#lx at %p (%s+%#llx), address %p, thread %lu, stack %llu KiB reserved\n", tag,
(unsigned long)code, (void *)rip, base ? image_name(base) : "no module",
(unsigned long long)(rip - (uintptr_t)base), (void *)addr, (unsigned long)GetCurrentThreadId(),
(unsigned long long)(hi - lo) >> 10);
DWORD w;
if (n > 0) WriteFile(GetStdHandle(STD_ERROR_HANDLE), b, (DWORD)n, &w, NULL);
if (crash_ctx) {
n = snprintf(b, sizeof b, "[host] %s: the thread was %s\n", tag, crash_ctx);
if (n > 0) WriteFile(GetStdHandle(STD_ERROR_HANDLE), b, (DWORD)n, &w, NULL);
}
}
static LONG CALLBACK crash_veh(EXCEPTION_POINTERS *ep) {
DWORD code = ep->ExceptionRecord->ExceptionCode;
if (code == EXCEPTION_ACCESS_VIOLATION || code == EXCEPTION_STACK_OVERFLOW || code == EXCEPTION_ILLEGAL_INSTRUCTION)
raw_note("crash_veh", code, (uintptr_t)ep->ContextRecord->Rip,
ep->ExceptionRecord->NumberParameters >= 2 ? (uintptr_t)ep->ExceptionRecord->ExceptionInformation[1] : 0);
if (code == EXCEPTION_ACCESS_VIOLATION && !ep->ContextRecord->Rip) {
// a call through a null pointer: the call's return address is on top of the stack, in whichever module made it
const uintptr_t *sp = (const uintptr_t *)ep->ContextRecord->Rsp;
for (int i = 0; i < 4; i++) {
void *base = NULL;
if (!RtlPcToFileHeader((void *)sp[i], &base) || !base) continue;
char b[200];
int n = snprintf(b, sizeof b, "[host] crash_veh: called from %s+%#llx (stack word %d)\n", image_name(base),
(unsigned long long)(sp[i] - (uintptr_t)base), i);
DWORD w;
if (n > 0) WriteFile(GetStdHandle(STD_ERROR_HANDLE), b, (DWORD)n, &w, NULL);
}
// the caller's registers (the object a call through a table read its pointer from is often one of them)
const CONTEXT *x = ep->ContextRecord;
char b[600];
int n = snprintf(b, sizeof b,
"[host] crash_veh: rax %#llx rbx %#llx rcx %#llx rdx %#llx rsi %#llx rdi %#llx rbp %#llx r8 %#llx "
"r9 %#llx r12 %#llx r13 %#llx r14 %#llx r15 %#llx\n",
(unsigned long long)x->Rax, (unsigned long long)x->Rbx, (unsigned long long)x->Rcx,
(unsigned long long)x->Rdx, (unsigned long long)x->Rsi, (unsigned long long)x->Rdi,
(unsigned long long)x->Rbp, (unsigned long long)x->R8, (unsigned long long)x->R9,
(unsigned long long)x->R12, (unsigned long long)x->R13, (unsigned long long)x->R14,
(unsigned long long)x->R15);
DWORD w;
if (n > 0) WriteFile(GetStdHandle(STD_ERROR_HANDLE), b, (DWORD)n, &w, NULL);
}
const char *what = code == EXCEPTION_ACCESS_VIOLATION ? "access violation"
: code == EXCEPTION_ILLEGAL_INSTRUCTION ? "illegal instruction"
: code == EXCEPTION_INT_DIVIDE_BY_ZERO ? "integer division by zero"
: code == EXCEPTION_PRIV_INSTRUCTION ? "privileged instruction"
: code == EXCEPTION_STACK_OVERFLOW ? "stack overflow"
: code == EXCEPTION_IN_PAGE_ERROR ? "in-page error" : NULL;
if (!what) return EXCEPTION_CONTINUE_SEARCH;
uintptr_t rip = (uintptr_t)ep->ContextRecord->Rip;
MODULEINFO mi;
if (!GetModuleInformation(GetCurrentProcess(), GetModuleHandleW(NULL), &mi, sizeof mi) ||
rip - (uintptr_t)mi.lpBaseOfDll >= mi.SizeOfImage)
return EXCEPTION_CONTINUE_SEARCH; // not our code: leave it to the handler that expects it
PlatCrash c = {what, (uint32_t)code, rip, rip - (uintptr_t)mi.lpBaseOfDll, -1, 0};
if (code == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2)
c.access = (int)ep->ExceptionRecord->ExceptionInformation[0], c.addr = ep->ExceptionRecord->ExceptionInformation[1];
{ // the faulting host stack, so the report can name the runtime frame that took it
void *f[32];
USHORT n = RtlCaptureStackBackTrace(0, 32, f, NULL);
for (USHORT i = 0; i < n && c.nframes < (int)(sizeof c.frames / sizeof *c.frames); i++)
if ((uintptr_t)f[i] - (uintptr_t)mi.lpBaseOfDll < mi.SizeOfImage)
c.frames[c.nframes++] = (uintptr_t)f[i];
}
crash_fn(&c);
return EXCEPTION_CONTINUE_SEARCH;
}
// abort() and an invalid parameter to a C runtime function raise no exception: the C runtime ends the process with
// 0xc0000409 after calling the SIGABRT or invalid-parameter handler. The report names the host frames in this
// executable; the innermost is the caller of abort (assert) or of the C runtime function. Skipped frames: this function
// and the handler that called it.
__attribute__((noinline)) static void crash_from_runtime(const char *what, uint32_t code) {
PlatCrash c = {what, code, 0, 0, -1, 0};
void *f[62];
USHORT n = RtlCaptureStackBackTrace(2, 62, f, NULL);
MODULEINFO mi;
if (GetModuleInformation(GetCurrentProcess(), GetModuleHandleW(NULL), &mi, sizeof mi)) {
uintptr_t base = (uintptr_t)mi.lpBaseOfDll;
for (USHORT i = 0; i < n && c.nframes < (int)(sizeof c.frames / sizeof *c.frames); i++)
if ((uintptr_t)f[i] - base < mi.SizeOfImage) c.frames[c.nframes++] = (uintptr_t)f[i];
if (c.nframes) c.rip = c.frames[0], c.offset = c.rip - base;
}
crash_fn(&c);
}
// The name a DLL records for itself in its export directory ("amdvlk64.dll"). Read from its mapped headers, not through
// the loader, whose lock another thread may hold when the process crashes.
static const char *image_name(const char *base) {
const IMAGE_NT_HEADERS64 *nt = (const void *)(base + ((const IMAGE_DOS_HEADER *)base)->e_lfanew);
const IMAGE_DATA_DIRECTORY *d = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (!d->VirtualAddress || !d->Size) return "unnamed module";
return base + ((const IMAGE_EXPORT_DIRECTORY *)(base + d->VirtualAddress))->Name;
}
// The return addresses in [lo, lo + size) on the crashed thread's stack, innermost first: unwinds the faulting context
// with the x64 unwind tables; a function without a table entry (a leaf, or address 0 after a call through a null
// pointer) has its return address on top of the stack. The walk stops at return address 0 (the thread's first frame)
// and when the stack pointer leaves the thread's stack, so a damaged stack ends it instead of faulting.
static int frames_in(const CONTEXT *at, uintptr_t lo, uintptr_t size, uintptr_t *out, int max) {
CONTEXT c = *at;
ULONG_PTR low, high;
GetCurrentThreadStackLimits(&low, &high);
int n = 0;
for (int i = 0; i < 256 && n < max && (c.Rip || !i) && c.Rsp >= low && c.Rsp + 8 <= high; i++) {
DWORD64 img;
RUNTIME_FUNCTION *fe = RtlLookupFunctionEntry(c.Rip, &img, NULL);
if (fe) {
void *data;
DWORD64 frame;
RtlVirtualUnwind(UNW_FLAG_NHANDLER, img, c.Rip, fe, &c, &data, &frame, NULL);
} else {
c.Rip = *(const DWORD64 *)c.Rsp;
c.Rsp += 8;
}
if (c.Rip - lo < size) out[n++] = (uintptr_t)c.Rip;
}
return n;
}
// The last-chance filter: an exception no handler took, in any module. crash_veh reports faults in this executable as
// they happen; this one catches the rest, which used to end the process with no line in the log: a fault in a GPU
// driver's own thread or in a driver call (vkQueueSubmit, vkAllocateMemory), a C++ exception a library let escape, a
// call through a null pointer (rip 0, outside every module). Windows calls it only after every handler declined, so
// the exceptions drivers raise and catch themselves never get here; it costs nothing until a crash.
static LONG WINAPI crash_filter(EXCEPTION_POINTERS *ep) {
const EXCEPTION_RECORD *er = ep->ExceptionRecord;
DWORD code = er->ExceptionCode;
uintptr_t rip = (uintptr_t)ep->ContextRecord->Rip;
raw_note("crash_filter", code, rip, er->NumberParameters >= 2 ? (uintptr_t)er->ExceptionInformation[1] : 0);
PlatCrash c = {code == EXCEPTION_ACCESS_VIOLATION ? "access violation"
: code == EXCEPTION_ILLEGAL_INSTRUCTION ? "illegal instruction"
: code == EXCEPTION_INT_DIVIDE_BY_ZERO ? "integer division by zero"
: code == EXCEPTION_PRIV_INSTRUCTION ? "privileged instruction"
: code == EXCEPTION_STACK_OVERFLOW ? "stack overflow"
: code == EXCEPTION_IN_PAGE_ERROR ? "in-page error"
: code == EXCEPTION_BREAKPOINT ? "breakpoint"
: code == 0xe06d7363u ? "C++ exception"
: code == 0xc0000374u ? "heap corruption" // STATUS_HEAP_CORRUPTION
: "unhandled exception",
(uint32_t)code, rip, 0, -1, 0};
if (code == EXCEPTION_ACCESS_VIOLATION && er->NumberParameters >= 2)
c.access = (int)er->ExceptionInformation[0], c.addr = er->ExceptionInformation[1];
void *base = NULL;
RtlPcToFileHeader((void *)rip, &base);
MODULEINFO mi;
uintptr_t exe = (uintptr_t)GetModuleHandleW(NULL);
c.offset = rip - (uintptr_t)base;
if (!base) c.module = "";
else if ((uintptr_t)base != exe) c.module = image_name(base);
if (c.module && GetModuleInformation(GetCurrentProcess(), (HMODULE)exe, &mi, sizeof mi))
c.nframes = frames_in(ep->ContextRecord, exe, mi.SizeOfImage, c.frames,
(int)(sizeof c.frames / sizeof *c.frames));
crash_fn(&c);
return EXCEPTION_CONTINUE_SEARCH;
}
static void abort_signal(int sig) { crash_from_runtime("abort", (uint32_t)sig); }
static void invalid_parameter(const wchar_t *expr, const wchar_t *func, const wchar_t *file, unsigned line,
uintptr_t reserved) {
(void)expr, (void)func, (void)file, (void)line, (void)reserved; // NULL and 0 in the release C runtime
crash_from_runtime("invalid parameter to a C runtime function", 0xc0000417u); // STATUS_INVALID_CRUNTIME_PARAMETER
}
void plat_crash_hook(void (*fn)(const PlatCrash *c)) {
crash_fn = fn;
AddVectoredExceptionHandler(0, crash_veh);
SetUnhandledExceptionFilter(crash_filter); // replaces the C runtime's filter; crash_fn never returns
signal(SIGABRT, abort_signal);
_set_invalid_parameter_handler(invalid_parameter);
}
// The last-chance filter is set once, at the start, before the GPU driver, the window and audio libraries and the
// movie decoder load. A DLL that sets its own filter later takes the place of crash_filter, and a fault outside this
// executable then ends the process with no line in the log. This puts crash_filter back; when another filter had
// taken its place, `who` gets the module and offset of that filter and the call returns 1.
int plat_crash_filter_reassert(char *who, size_t n) {
LPTOP_LEVEL_EXCEPTION_FILTER prev = SetUnhandledExceptionFilter(crash_filter);
if (prev == crash_filter) return 0;
void *base = NULL;
if (prev) RtlPcToFileHeader((void *)prev, &base);
if (!prev) snprintf(who, n, "no filter (removed)");
else if (!base) snprintf(who, n, "%p, outside every module", (void *)prev);
else snprintf(who, n, "%s+%#llx", image_name(base), (unsigned long long)((uintptr_t)prev - (uintptr_t)base));
return 1;
}
// A fresh process gets a fresh layout: Windows places DLLs, heaps and thread stacks at random addresses. The parent
// waits and exits with the child's code.
void plat_reexec(void) {
plat_exit_orderly(); // this process ends with ExitProcess below, on purpose
fprintf(stderr, "[host] memory: a fixed region is taken; re-running the process\n");
fflush(stdout);
fflush(stderr);
STARTUPINFOW si = {.cb = sizeof si};
PROCESS_INFORMATION pi;
wchar_t *cmd = _wcsdup(GetCommandLineW()); // CreateProcessW may write its command line
if (!cmd || !CreateProcessW(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
fprintf(stderr, "FATAL: re-running the process failed (error %lu)\n", GetLastError());
ExitProcess(70);
}
CloseHandle(pi.hThread);
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 70;
GetExitCodeProcess(pi.hProcess, &code);
ExitProcess(code);
}
int plat_mem_read(uint64_t addr, void *out, size_t n) {
SIZE_T got = 0;
return ReadProcessMemory(GetCurrentProcess(), (const void *)(uintptr_t)addr, out, n, &got) && got == n;
}
int plat_mem_readable(uint64_t addr, uint64_t n) {
for (uint64_t a = addr, end = addr + n; a < end || a == addr;) {
MEMORY_BASIC_INFORMATION mbi;
if (!VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) || mbi.State != MEM_COMMIT) return 0;
if (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) return 0;
a = (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
if (a >= end) return 1;
}
return 1;
}
void *plat_aligned_alloc(size_t size, size_t align) { return _aligned_malloc(size, align); }
void plat_aligned_free(void *p) { _aligned_free(p); }
// ---- process
#ifdef NX2NAVI_PGO_GEN
// compiler-rt's profile runtime (an instrumented build: --pgo gen of tools/make_game.py or tools/build_game.py). It
// writes its counters from an atexit handler, which ExitProcess skips; __llvm_profile_dump writes them once and makes
// later writes no-ops.
int __llvm_profile_dump(void);
int __llvm_profile_write_file(void);
void __llvm_profile_reset_counters(void);
// A long played session must not lose its counts to a crash or a reboot: every minute the counters are added to the
// .profraw (the file merges, -fprofile-generate's %m) and zeroed. Counts taken between the write and the reset are lost.
static DWORD WINAPI pgo_flush_thread(void *arg) {
(void)arg;
for (;;) {
Sleep(60 * 1000);
if (__llvm_profile_write_file() == 0) __llvm_profile_reset_counters();
}
}
__attribute__((constructor)) static void pgo_flush_start(void) {
HANDLE h = CreateThread(NULL, 0, pgo_flush_thread, NULL, 0, NULL);
if (h) CloseHandle(h);
}
#endif
// ---- the console window (plat_console_setup)
static HANDLE con_in = INVALID_HANDLE_VALUE; // the console's input, while its QuickEdit mode is off; con_mode as it was
static DWORD con_mode;
static void (*con_close)(const char *what);
static volatile LONG con_closing;
// fs_host.c: a save Commit under way finishes first, 2 s at most and without a lock; builds without it have no saves
__attribute__((weak)) void fs_save_crash_wait(void) {}
// Runs on a thread the system starts for the event. A close returns only when on_close does, which it must not.
static BOOL WINAPI con_ctrl(DWORD ev) {
const char *what = ev == CTRL_CLOSE_EVENT ? "closed" : ev == CTRL_C_EVENT ? "Ctrl+C"
: ev == CTRL_BREAK_EVENT ? "Ctrl+Break" : NULL;
// log-off and shutdown reach a process with a window as WM_ENDSESSION (gfx_window_win.c), not here
if (!what || !con_close) return FALSE;
if (InterlockedExchange(&con_closing, 1)) {
// A second event while on_close runs: the player wants out now. Left to the default handler, its ExitProcess
// cut a save Commit in flight and ran every library's detach; the Commit finishes first, and the process ends
// with the code the default handler gives (STATUS_CONTROL_C_EXIT).
fs_save_crash_wait();
plat_exit((int)0xC000013Au);
}
con_close(what);
return TRUE;
}
int plat_console_setup(void (*on_close)(const char *what)) {
HANDLE err = GetStdHandle(STD_ERROR_HANDLE);
DWORD m;
if (!err || err == INVALID_HANDLE_VALUE || !GetConsoleMode(err, &m)) return 0;
// CONIN$: the console's input even when stdin was redirected. ENABLE_EXTENDED_FLAGS makes the call change the
// QuickEdit bit at all.
HANDLE in = CreateFileW(L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, 0, NULL);
if (in != INVALID_HANDLE_VALUE && GetConsoleMode(in, &con_mode) && (con_mode & ENABLE_QUICK_EDIT_MODE) &&
SetConsoleMode(in, (con_mode & ~(DWORD)ENABLE_QUICK_EDIT_MODE) | ENABLE_EXTENDED_FLAGS))
con_in = in;
else if (in != INVALID_HANDLE_VALUE)
CloseHandle(in);
con_close = on_close;
if (on_close) SetConsoleCtrlHandler(con_ctrl, TRUE);
return 1;
}
int plat_stderr_write(const void *p, size_t n) {
int fd = _fileno(stderr); // the descriptor fwrite(stderr) writes to; negative when the process has no stderr
if (fd < 0) return 0;
for (const char *c = p; n;) {
int k = _write(fd, c, n > (1u << 30) ? 1u << 30 : (unsigned)n);
if (k <= 0) return 0;
c += k, n -= (size_t)k;
}
return 1;
}
void (*plat_exit_hook)(void);
void plat_exit(int code) {
plat_exit_orderly();
if (plat_exit_hook) plat_exit_hook();
#ifdef NX2NAVI_PGO_GEN
__llvm_profile_dump();
#endif
// QuickEdit back on in a console that outlives the process (the one of the shell that started it)
if (con_in != INVALID_HANDLE_VALUE) SetConsoleMode(con_in, con_mode | ENABLE_EXTENDED_FLAGS);
fflush(NULL); // every open stream: nothing below runs the C runtime's own exit
// The process ends here, without the libraries' detach calls that ExitProcess makes. The callers have saved what
// is to be kept (gfx_present.c gfx_window_closed: settings, pipeline cache, profile, saves), and the game's other
// threads are still running when this is called. ExitProcess stops them wherever they are and then runs every
// library's detach on this thread: NVIDIA's NGX core, which still held the device and the DLSS features, read a
// dead pointer in its detach on the window thread after the window was closed (an access violation in
// _nvngx.dll after the last save, once in 5 scripted runs that had freed and made a DLSS feature again), and the
// crash handler wrote a FATAL for a run that had ended cleanly.
TerminateProcess(GetCurrentProcess(), (UINT)code);
ExitProcess((UINT)code); // not reached
}
void plat_terminate(int code) {
plat_exit_orderly();
TerminateProcess(GetCurrentProcess(), (UINT)code);
ExitProcess((UINT)code); // not reached
}
// ---- an end the game did not ask for (plat_exit_note_setup)
// Two runs of 2026-11-13 ended seconds after a focus loss with exit code 0, no crash event and no line of their own: an
// ExitProcess by a module in the process, or a kill from outside, could not be told apart. ExitProcess stops every
// other thread and then calls each module's detach, the executable's TLS callbacks included; TerminateProcess calls
// none. The callback runs under the loader lock with the other threads gone, so any lock they held stays held: it
// uses no C runtime (no formatting, allocation or streams: their locks), does not ask the loader (RtlPcToFileHeader
// takes its module lock) and names addresses from the module list noted earlier. RtlCaptureStackBackTrace and
// WriteFile take no lock of the process.
#define EN_MODS 384
typedef struct { uintptr_t base, size; char name[40]; } EnMod;
static EnMod en_mods[2][EN_MODS];
static int en_n[2];
static volatile LONG en_cur = -1; // the table the callback reads, -1 none yet
static PlatMutex en_lock = PLAT_MUTEX_INIT; // the writers of the tables (start, focus losses)
static PlatAppend *volatile en_log;
static void (*volatile en_queued)(const char **p, size_t *n);
static volatile LONG en_orderly;
void plat_exit_orderly(void) { InterlockedExchange(&en_orderly, 1); }
void plat_exit_note_setup(PlatAppend *log, void (*queued)(const char **p, size_t *n)) {
en_queued = queued;
en_log = log;
}
// Each module's size comes from its own image header and its name from GetModuleFileNameA. GetModuleInformation and
// GetModuleBaseNameA each walk the whole module list through ReadProcessMemory, so a note took about 27 ms with 290
// modules, on the window thread at every focus loss; this takes about 0.3 ms. The header is read through
// ReadProcessMemory too, which fails for a module unloaded since EnumProcessModules where a plain read would fault.
void plat_exit_note_modules(void) {
HMODULE m[EN_MODS];
DWORD need = 0;
if (!EnumProcessModules(GetCurrentProcess(), m, sizeof m, &need)) return;
int n = (int)(need / sizeof m[0] < EN_MODS ? need / sizeof m[0] : EN_MODS), k = 0;
plat_mutex_lock(&en_lock);
int t = InterlockedCompareExchange(&en_cur, 0, 0) == 0 ? 1 : 0; // the table the callback does not read
for (int i = 0; i < n; i++) {
unsigned char hdr[1024];
SIZE_T got = 0;
if (!ReadProcessMemory(GetCurrentProcess(), m[i], hdr, sizeof hdr, &got) || got != sizeof hdr) continue;
const IMAGE_DOS_HEADER *dos = (const IMAGE_DOS_HEADER *)hdr;
if (dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew < 0 ||
(size_t)dos->e_lfanew + sizeof(IMAGE_NT_HEADERS) > sizeof hdr)
continue;
const IMAGE_NT_HEADERS *nt = (const IMAGE_NT_HEADERS *)(hdr + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) continue;
char path[MAX_PATH];
DWORD len = GetModuleFileNameA(m[i], path, sizeof path);
if (len >= sizeof path) len = sizeof path - 1;
path[len] = 0;
const char *name = path;
for (DWORD j = 0; j < len; j++)
if (path[j] == '\\' || path[j] == '/') name = path + j + 1;
EnMod *e = &en_mods[t][k++];
e->base = (uintptr_t)m[i], e->size = nt->OptionalHeader.SizeOfImage;
snprintf(e->name, sizeof e->name, "%s", name);
}
en_n[t] = k;
InterlockedExchange(&en_cur, t);
plat_mutex_unlock(&en_lock);
}
static size_t en_put(char *o, size_t at, size_t cap, const char *s) {
while (*s && at < cap) o[at++] = *s++;
return at;
}
static size_t en_hex(char *o, size_t at, size_t cap, uintptr_t v) {
char d[18];
int n = 0;
do d[n++] = "0123456789abcdef"[v & 15], v >>= 4; while (v);
at = en_put(o, at, cap, "0x");
while (n && at < cap) o[at++] = d[--n];
return at;
}
// An executable gets DLL_PROCESS_DETACH only as the process ends (its reserved argument is NULL there all the same).
static void NTAPI exit_note_tls(PVOID h, DWORD reason, PVOID reserved) {
(void)h, (void)reserved;
PlatAppend *log = en_log;
if (reason != DLL_PROCESS_DETACH || en_orderly || !log) return;
const char *q = NULL;
size_t qn = 0;
void (*queued)(const char **, size_t *) = en_queued;
if (queued) queued(&q, &qn);
if (q && qn) plat_append_write(log, q, qn);
char line[2048];
size_t at = en_put(line, 0, sizeof line - 1, "[host] exit: the process is ending through ExitProcess, not one of "
"the game's own ends; the calls that led to it:");
void *f[32];
USHORT n = RtlCaptureStackBackTrace(1, 32, f, NULL);
LONG t = en_cur;
for (USHORT i = 0; i < n; i++) {
uintptr_t a = (uintptr_t)f[i];
const EnMod *m = NULL;
for (int k = 0; t >= 0 && k < en_n[t] && !m; k++)
if (a - en_mods[t][k].base < en_mods[t][k].size) m = &en_mods[t][k];
at = en_put(line, at, sizeof line - 1, " ");
if (m && m->name[0]) {
at = en_put(line, en_put(line, at, sizeof line - 1, m->name), sizeof line - 1, "+");
a -= m->base;
}
at = en_hex(line, at, sizeof line - 1, a);
}
line[at++] = '\n';
plat_append_write(log, line, at);
}
// The callback in the executable's TLS callback list (the .CRT$XL* sections), which the TLS directory the image has
// for its thread-local variables points to; _tls_used is that directory.
__attribute__((section(".CRT$XLB"), used)) PIMAGE_TLS_CALLBACK plat_exit_note_cb = exit_note_tls;
extern const IMAGE_TLS_DIRECTORY _tls_used;
__attribute__((used)) static const void *volatile exit_note_tls_dir = &_tls_used;
// _putenv_s, not SetEnvironmentVariableA: getenv reads the C runtime's copy of the environment, which only the CRT's
// own calls update. An empty value removes the variable.
int plat_setenv(const char *name, const char *value) { return _putenv_s(name, value ? value : "") == 0; }
uint32_t plat_process_id(void) { return GetCurrentProcessId(); }
static uint64_t ft64(FILETIME f) { return (uint64_t)f.dwHighDateTime << 32 | f.dwLowDateTime; }
uint64_t plat_process_cpu_ns(void) {
FILETIME cr, ex, k, u;
if (!GetProcessTimes(GetCurrentProcess(), &cr, &ex, &k, &u)) return 0;
return (ft64(k) + ft64(u)) * 100;
}
uint64_t plat_process_uptime_ns(void) {
FILETIME cr, ex, k, u, now;
if (!GetProcessTimes(GetCurrentProcess(), &cr, &ex, &k, &u)) return 0;
GetSystemTimeAsFileTime(&now);
return ft64(now) > ft64(cr) ? (ft64(now) - ft64(cr)) * 100 : 0;
}
uint64_t plat_unix_time_ns(void) {
FILETIME now;
GetSystemTimeAsFileTime(&now);
return (ft64(now) - 116444736000000000ull) * 100; // FILETIME counts 100 ns from 1601
}
void plat_local_time(struct tm *out) {
time_t t = time(NULL);
localtime_s(out, &t);
}
int plat_localtime(int64_t unix_s, struct tm *out) {
time_t t = (time_t)unix_s;
return !localtime_s(out, &t);
}
int plat_gmtime(int64_t unix_s, struct tm *out) {
time_t t = (time_t)unix_s;
return !gmtime_s(out, &t);
}
long plat_tz_west(void) {
long west = 0;
_tzset();
_get_timezone(&west);
return west;
}
int plat_run_self(const char *env_name, const char *env_value, char *out, size_t cap, uint32_t timeout_ms) {
SECURITY_ATTRIBUTES sa = {sizeof sa, NULL, TRUE};
HANDLE rd, wr;
if (!CreatePipe(&rd, &wr, &sa, 0)) return -1;
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOW si = {.cb = sizeof si, .dwFlags = STARTF_USESTDHANDLES, .hStdInput = GetStdHandle(STD_INPUT_HANDLE),
.hStdOutput = wr, .hStdError = wr};
PROCESS_INFORMATION pi;
wchar_t *cmdline = _wcsdup(GetCommandLineW());
SetEnvironmentVariableA(env_name, env_value);
BOOL ok = cmdline && CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
SetEnvironmentVariableA(env_name, NULL);
free(cmdline);
CloseHandle(wr);
if (!ok) {
CloseHandle(rd);
return -1;
}
size_t len = 0;
DWORD got;
char sink[512];
while (len + 1 < cap && ReadFile(rd, out + len, (DWORD)(cap - 1 - len), &got, NULL) && got) len += got;
if (cap) out[len] = 0;
while (ReadFile(rd, sink, sizeof sink, &got, NULL) && got) {}
CloseHandle(rd);
if (WaitForSingleObject(pi.hProcess, timeout_ms ? timeout_ms : INFINITE) != WAIT_OBJECT_0)
TerminateProcess(pi.hProcess, 1), WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 1;
GetExitCodeProcess(pi.hProcess, &code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return (int)code;
}
int plat_random(void *out, size_t n) {
return BCryptGenRandom(NULL, out, (ULONG)n, BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0;
}
// FILE_FLAG_OVERLAPPED handles: a read names its offset, and each thread waits on an event of its own, so concurrent
// reads on one handle do not share the file pointer or the event.
PlatFile *plat_file_open(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return NULL;
HANDLE h = CreateFileW(w, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
return h == INVALID_HANDLE_VALUE ? NULL : (PlatFile *)h;
}
PlatFile *plat_file_open_unbuffered(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return NULL;
HANDLE h = CreateFileW(w, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, NULL);
return h == INVALID_HANDLE_VALUE ? NULL : (PlatFile *)h;
}
void plat_file_close(PlatFile *f) {
if (f) CloseHandle((HANDLE)f);
}
int64_t plat_file_pread(PlatFile *f, void *buf, uint32_t n, uint64_t off) {
HANDLE ev = pread_ev;
if (!ev && !(ev = pread_ev = CreateEventW(NULL, TRUE, FALSE, NULL))) return -1;
OVERLAPPED ov = {.Offset = (DWORD)off, .OffsetHigh = (DWORD)(off >> 32), .hEvent = ev};
DWORD got = 0;
if (ReadFile((HANDLE)f, buf, n, &got, &ov)) return got;
DWORD e = GetLastError();
if (e == ERROR_IO_PENDING) {
if (GetOverlappedResult((HANDLE)f, &ov, &got, TRUE)) return got;
e = GetLastError();
}
return e == ERROR_HANDLE_EOF ? 0 : e == ERROR_NOACCESS ? -2 : -1;
}
int plat_file_id(PlatFile *f, PlatFileId *out) {
BY_HANDLE_FILE_INFORMATION i;
if (!GetFileInformationByHandle((HANDLE)f, &i)) return 0;
*out = (PlatFileId){i.dwVolumeSerialNumber, (uint64_t)i.nFileIndexHigh << 32 | i.nFileIndexLow,
(uint64_t)i.nFileSizeHigh << 32 | i.nFileSizeLow,
(uint64_t)i.ftLastWriteTime.dwHighDateTime << 32 | i.ftLastWriteTime.dwLowDateTime,
(uint64_t)i.ftCreationTime.dwHighDateTime << 32 | i.ftCreationTime.dwLowDateTime};
return 1;
}
const void *plat_file_map(PlatFile *f) {
HANDLE map = CreateFileMappingW((HANDLE)f, NULL, PAGE_READONLY, 0, 0, NULL);
const void *v = map ? MapViewOfFile(map, FILE_MAP_READ, 0, 0, 0) : NULL;
if (map) CloseHandle(map); // the view keeps the mapping alive
return v;
}
static __thread DWORD fs_last_error;
int plat_fs_os_error(void) { return (int)fs_last_error; }
static int fs_err(DWORD e) {
fs_last_error = e;
switch (e) {
case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: case ERROR_DIRECTORY: return PLAT_FS_NOT_FOUND;
case ERROR_FILE_EXISTS: case ERROR_ALREADY_EXISTS: return PLAT_FS_EXISTS;
case ERROR_DIR_NOT_EMPTY: return PLAT_FS_NOT_EMPTY;
case ERROR_ACCESS_DENIED: case ERROR_SHARING_VIOLATION: return PLAT_FS_BUSY;
default: return PLAT_FS_ERROR;
}
}
// A symbolic link or junction is a link: removed as a link, never followed. Any other reparse point (OneDrive's cloud
// placeholders, deduplicated files) is an ordinary file or directory whose content the file system fetches on access.
static int fs_kind_of(const WIN32_FIND_DATAW *fd) {
DWORD a = fd->dwFileAttributes;
if (a & FILE_ATTRIBUTE_REPARSE_POINT && (fd->dwReserved0 == IO_REPARSE_TAG_SYMLINK || fd->dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT))
return PLAT_FS_LINK;
return a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR : PLAT_FS_FILE;
}
int plat_fs_kind(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return PLAT_FS_MISSING;
DWORD a = GetFileAttributesW(w);
if (a == INVALID_FILE_ATTRIBUTES) return PLAT_FS_MISSING;
if (!(a & FILE_ATTRIBUTE_REPARSE_POINT)) return a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR : PLAT_FS_FILE;
// the reparse tag comes with the find data only; FindFirstFileW is asked only here, since it fails on a path
// with a trailing separator and reads wildcards
WIN32_FIND_DATAW fd;
HANDLE h = FindFirstFileW(w, &fd);
if (h == INVALID_HANDLE_VALUE) return PLAT_FS_LINK;
FindClose(h);
return fs_kind_of(&fd);
}
int plat_fs_mkdir(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return PLAT_FS_ERROR;
return CreateDirectoryW(w, NULL) ? PLAT_FS_OK : fs_err(GetLastError());
}
int plat_fs_rmdir(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return PLAT_FS_ERROR;
if (RemoveDirectoryW(w)) return PLAT_FS_OK;
DWORD e = GetLastError(), a = GetFileAttributesW(w);
if (e == ERROR_ACCESS_DENIED && a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_READONLY)) {
// a read-only directory (copied over from another emulator's save folder) refuses RemoveDirectoryW
SetFileAttributesW(w, a & ~(DWORD)FILE_ATTRIBUTE_READONLY);
if (RemoveDirectoryW(w)) return PLAT_FS_OK;
e = GetLastError();
}
return fs_err(e);
}
int plat_fs_unlink(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return PLAT_FS_ERROR;
DWORD a = GetFileAttributesW(w);
if (a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_READONLY))
SetFileAttributesW(w, a & ~(DWORD)FILE_ATTRIBUTE_READONLY);
return DeleteFileW(w) ? PLAT_FS_OK : fs_err(GetLastError());
}
int plat_fs_rename(const char *from, const char *to, int durable) {
wchar_t a[WPATH], b[WPATH];
if (!widen(from, a, WPATH) || !widen(to, b, WPATH)) return PLAT_FS_ERROR;
return MoveFileExW(a, b, durable ? MOVEFILE_WRITE_THROUGH : 0) ? PLAT_FS_OK : fs_err(GetLastError());
}
int plat_fs_link(const char *existing, const char *path) {
wchar_t a[WPATH], b[WPATH];
if (!widen(existing, a, WPATH) || !widen(path, b, WPATH)) return PLAT_FS_ERROR;
return CreateHardLinkW(b, a, NULL) ? PLAT_FS_OK : fs_err(GetLastError());
}
int plat_fs_links(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return 0;
HANDLE f = CreateFileW(w, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
BY_HANDLE_FILE_INFORMATION fi;
int links = f != INVALID_HANDLE_VALUE && GetFileInformationByHandle(f, &fi) ? (int)fi.nNumberOfLinks : 0;
if (f != INVALID_HANDLE_VALUE) CloseHandle(f);
return links;
}
// NTFS journals directory entries; flushing the directory handle is as far as Windows goes.
void plat_fs_sync_dir(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return;
HANDLE h = CreateFileW(w, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (h != INVALID_HANDLE_VALUE) {
FlushFileBuffers(h);
CloseHandle(h);
}
}
// Restart Manager (rstrtmgr.dll, loaded on first use) names the processes that hold a file open.
typedef DWORD(WINAPI *RmStartSession_t)(DWORD *, DWORD, WCHAR *);
typedef DWORD(WINAPI *RmRegisterResources_t)(DWORD, UINT, LPCWSTR *, UINT, RM_UNIQUE_PROCESS *, UINT, LPCWSTR *);
typedef DWORD(WINAPI *RmGetList_t)(DWORD, UINT *, UINT *, RM_PROCESS_INFO *, LPDWORD);
typedef DWORD(WINAPI *RmEndSession_t)(DWORD);
static struct {
INIT_ONCE once;
RmStartSession_t start;
RmRegisterResources_t add;
RmGetList_t list;
RmEndSession_t end;
} rm = {INIT_ONCE_STATIC_INIT};
static BOOL CALLBACK rm_load(INIT_ONCE *o, void *p, void **c) {
(void)o, (void)p, (void)c;
HMODULE l = plat_lib_open("rstrtmgr.dll");
if (!l) return TRUE;
rm.add = (RmRegisterResources_t)(void *)GetProcAddress(l, "RmRegisterResources");
rm.list = (RmGetList_t)(void *)GetProcAddress(l, "RmGetList");
rm.end = (RmEndSession_t)(void *)GetProcAddress(l, "RmEndSession");
RmStartSession_t start = (RmStartSession_t)(void *)GetProcAddress(l, "RmStartSession");
if (rm.add && rm.list && rm.end) rm.start = start; // start set: all of them are there
return TRUE;
}
// The files under dir (links not followed), at most BUSY_FILES of them, each a malloc'd path.
#define BUSY_FILES 256
typedef struct { LPCWSTR p[BUSY_FILES]; UINT n; } BusyFiles;
static void busy_files(const wchar_t *dir, BusyFiles *f) {
wchar_t pat[WPATH + 4];
swprintf(pat, WPATH + 4, L"%ls\\*", dir);
WIN32_FIND_DATAW fd;
HANDLE h = FindFirstFileW(pat, &fd);
if (h == INVALID_HANDLE_VALUE) return;
do {
DWORD a = fd.dwFileAttributes;
if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..") || (a & FILE_ATTRIBUTE_REPARSE_POINT)) continue;
size_t n = wcslen(dir) + wcslen(fd.cFileName) + 2;
wchar_t *p = n <= WPATH ? malloc(n * sizeof *p) : NULL;
if (!p) continue;
swprintf(p, n, L"%ls\\%ls", dir, fd.cFileName);
if (a & FILE_ATTRIBUTE_DIRECTORY) busy_files(p, f), free(p);
else f->p[f->n++] = p;
} while (f->n < BUSY_FILES && FindNextFileW(h, &fd));
FindClose(h);
}
void plat_fs_busy_reason(const char *dir, char *out, size_t cap) {
if (!cap) return;
*out = 0;
wchar_t w[WPATH];
if (!widen(dir, w, WPATH)) return;
for (wchar_t *q = w; *q; q++)
if (*q == L'/') *q = L'\\';
// a rename needs DELETE access to the directory: a folder made by an elevated run can refuse it
HANDLE h = CreateFileW(w, DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (h == INVALID_HANDLE_VALUE) {
if (GetLastError() == ERROR_ACCESS_DENIED) snprintf(out, cap, "no permission to rename it");
return;
}
CloseHandle(h);
InitOnceExecuteOnce(&rm.once, rm_load, NULL, NULL);
DWORD session;
WCHAR key[CCH_RM_SESSION_KEY + 1] = {0};
if (!rm.start || rm.start(&session, 0, key) != ERROR_SUCCESS) return;
BusyFiles f = {.n = 0};
busy_files(w, &f);
RM_PROCESS_INFO info[8];
UINT need = 0, n = 8;
DWORD reasons = 0, e = f.n ? rm.add(session, f.n, f.p, 0, NULL, 0, NULL) : ERROR_SUCCESS;
if (f.n && e == ERROR_SUCCESS) e = rm.list(session, &need, &n, info, &reasons);
else n = 0;
rm.end(session);
for (UINT i = 0; i < f.n; i++) free((void *)f.p[i]);
if (e == ERROR_MORE_DATA) snprintf(out, cap, "%u programs hold files in it", need); // info is not filled then
if (e != ERROR_SUCCESS) return;
size_t at = 0;
for (UINT i = 0; i < n && at < cap; i++) { // the program's display name ("Windows Explorer") and process id
char name[CCH_RM_MAX_APP_NAME + 1];
if (!WideCharToMultiByte(CP_UTF8, 0, info[i].strAppName, -1, name, sizeof name, NULL, NULL)) strcpy(name, "?");
int k = snprintf(out + at, cap - at, "%s%s %lu", i ? ", " : "", name, info[i].Process.dwProcessId);
if (k < 0) break;
at += (size_t)k;
}
}
int plat_fs_list(const char *dir, int (*cb)(const char *name, int kind, uint64_t size, void *ctx), void *ctx) {
wchar_t wd[WPATH], pat[WPATH + 4];
if (!widen(dir, wd, WPATH)) return PLAT_FS_ERROR;
swprintf(pat, WPATH + 4, L"%ls\\*", wd);
WIN32_FIND_DATAW fd;
HANDLE h = FindFirstFileW(pat, &fd);
if (h == INVALID_HANDLE_VALUE) return fs_err(GetLastError());
do {
if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..")) continue;
char name[1024];
if (!WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, name, sizeof name, NULL, NULL)) continue;
int kind = fs_kind_of(&fd);
if (!cb(name, kind, (uint64_t)fd.nFileSizeHigh << 32 | fd.nFileSizeLow, ctx)) break;
} while (FindNextFileW(h, &fd));
FindClose(h);
return PLAT_FS_OK;
}
static PlatFile *fs_open(const char *path, DWORD access, DWORD share, DWORD how, int *err) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) {
if (err) *err = PLAT_FS_ERROR;
return NULL;
}
HANDLE h = CreateFileW(w, access, share, NULL, how, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
if (h == INVALID_HANDLE_VALUE) {
if (err) *err = fs_err(GetLastError());
return NULL;
}
return (PlatFile *)h;
}
PlatFile *plat_fs_open(const char *path, int write, int *err) {
return fs_open(path, GENERIC_READ | (write ? GENERIC_WRITE : 0),
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, OPEN_EXISTING, err);
}
PlatFile *plat_fs_create(const char *path, int *err) { return fs_open(path, GENERIC_WRITE, 0, CREATE_NEW, err); }
int64_t plat_file_pwrite(PlatFile *f, const void *buf, uint32_t n, uint64_t off) {
HANDLE ev = pwrite_ev;
if (!ev && !(ev = pwrite_ev = CreateEventW(NULL, TRUE, FALSE, NULL))) return -1;
OVERLAPPED ov = {.Offset = (DWORD)off, .OffsetHigh = (DWORD)(off >> 32), .hEvent = ev};
DWORD put = 0;
if (WriteFile((HANDLE)f, buf, n, &put, &ov)) return put;
if (GetLastError() == ERROR_IO_PENDING && GetOverlappedResult((HANDLE)f, &ov, &put, TRUE)) return put;
return -1;
}
int plat_file_set_size(PlatFile *f, uint64_t size) {
FILE_END_OF_FILE_INFO eof = {.EndOfFile.QuadPart = (LONGLONG)size};
return SetFileInformationByHandle((HANDLE)f, FileEndOfFileInfo, &eof, sizeof eof) ? PLAT_FS_OK
: fs_err(GetLastError());
}
int plat_file_fsync(PlatFile *f) { return FlushFileBuffers((HANDLE)f) != 0; }
int plat_temp_dir(char *out, size_t cap) {
wchar_t w[MAX_PATH + 1];
DWORD n = GetTempPathW(MAX_PATH + 1, w);
if (!n || n > MAX_PATH) return 0;
while (n && (w[n - 1] == L'\\' || w[n - 1] == L'/')) w[--n] = 0;
return WideCharToMultiByte(CP_UTF8, 0, w, -1, out, (int)cap, NULL, NULL) != 0;
}
uintptr_t plat_exe_base(void) { return (uintptr_t)GetModuleHandleW(NULL); }
int plat_module_of(uintptr_t addr, char *name, size_t cap, uintptr_t *base) {
HMODULE m = NULL;
wchar_t w[WPATH];
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCWSTR)addr, &m) ||
!m || !GetModuleFileNameW(m, w, WPATH))
return 0;
const wchar_t *n = wcsrchr(w, L'\\') ? wcsrchr(w, L'\\') + 1 : w;
*base = (uintptr_t)m;
return WideCharToMultiByte(CP_UTF8, 0, n, -1, name, (int)cap, NULL, NULL) != 0;
}
int plat_exe_path(char *out, size_t cap) {
wchar_t w[WPATH];
DWORD n = GetModuleFileNameW(NULL, w, WPATH);
if (!n || n >= WPATH) return 0;
return WideCharToMultiByte(CP_UTF8, 0, w, -1, out, (int)cap, NULL, NULL) != 0;
}
size_t plat_getenv(const char *name, char *out, size_t cap) {
wchar_t wn[256], v[WPATH];
if (!widen(name, wn, 256)) return 0;
DWORD n = GetEnvironmentVariableW(wn, v, WPATH);
if (!n || n >= WPATH) return 0;
int m = WideCharToMultiByte(CP_UTF8, 0, v, -1, out, (int)cap, NULL, NULL);
return m ? (size_t)m - 1 : 0;
}
uint32_t plat_thread_id(void) { return GetCurrentThreadId(); }
const char *plat_ui_language(void) {
static char tag[LOCALE_NAME_MAX_LENGTH];
static int done;
if (!done) {
wchar_t w[LOCALE_NAME_MAX_LENGTH];
LCID lcid = GetUserDefaultUILanguage();
int n = LCIDToLocaleName(lcid, w, LOCALE_NAME_MAX_LENGTH, 0);
if (n <= 0 || !WideCharToMultiByte(CP_UTF8, 0, w, -1, tag, sizeof tag, NULL, NULL)) tag[0] = 0;
done = 1;
}
return tag;
}
const char *plat_user_locale(void) {
static char tag[LOCALE_NAME_MAX_LENGTH];
static int done;
if (!done) {
wchar_t w[LOCALE_NAME_MAX_LENGTH];
size_t i = 0;
if (GetUserDefaultLocaleName(w, LOCALE_NAME_MAX_LENGTH))
for (; w[i] && w[i] < 0x80 && i + 1 < sizeof tag; i++) tag[i] = (char)w[i];
tag[i] = 0;
done = 1;
}
return tag;
}
int plat_font_path(const char *lang, char *out, size_t cap) {
static const struct { const char *lang; const wchar_t *files[3]; } fonts[] = {
{"ja", {L"YuGothM.ttc", L"meiryo.ttc", L"msgothic.ttc"}},
{"ko", {L"malgun.ttf", L"gulim.ttc", NULL}},
{"zh-Hans", {L"msyh.ttc", L"simsun.ttc", NULL}},
{"zh-Hant", {L"msjh.ttc", L"mingliu.ttc", NULL}},
};
wchar_t dir[MAX_PATH], w[WPATH];
UINT n = GetWindowsDirectoryW(dir, MAX_PATH);
if (!n || n >= MAX_PATH) return 0;
for (size_t i = 0; i < sizeof fonts / sizeof *fonts; i++) {
if (strcmp(fonts[i].lang, lang)) continue;
for (int k = 0; k < 3 && fonts[i].files[k]; k++) {
swprintf(w, WPATH, L"%ls\\Fonts\\%ls", dir, fonts[i].files[k]);
DWORD a = GetFileAttributesW(w);
if (a != INVALID_FILE_ATTRIBUTES && !(a & FILE_ATTRIBUTE_DIRECTORY))
return WideCharToMultiByte(CP_UTF8, 0, w, -1, out, (int)cap, NULL, NULL) != 0;
}
}
return 0;
}
int plat_os_version(char *out, size_t cap) {
// GetVersionEx reports the version the manifest asks for; ntdll's RtlGetVersion the real one
typedef LONG(WINAPI * RtlGetVersion_t)(OSVERSIONINFOW *);
HMODULE nt = GetModuleHandleW(L"ntdll.dll");
RtlGetVersion_t get = nt ? (RtlGetVersion_t)(void *)GetProcAddress(nt, "RtlGetVersion") : NULL;
OSVERSIONINFOW v = {.dwOSVersionInfoSize = sizeof v};
if (!get || get(&v) != 0) return 0;
DWORD ubr = 0, size = sizeof ubr; // the update build revision, the number after the build
if (RegGetValueW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", L"UBR", RRF_RT_REG_DWORD,
NULL, &ubr, &size) != ERROR_SUCCESS)
ubr = 0;
snprintf(out, cap, ubr ? "Windows %lu.%lu.%lu.%lu" : "Windows %lu.%lu.%lu", v.dwMajorVersion, v.dwMinorVersion,
v.dwBuildNumber, ubr);
return 1;
}
int plat_is_wine(void) {
HMODULE nt = GetModuleHandleW(L"ntdll.dll");
return nt && GetProcAddress(nt, "wine_get_version") != NULL;
}
int plat_stderr_path(char *out, size_t cap) {
HANDLE h = GetStdHandle(STD_ERROR_HANDLE);
wchar_t w[WPATH];
if (!h || h == INVALID_HANDLE_VALUE || GetFileType(h) != FILE_TYPE_DISK) return 0;
DWORD n = GetFinalPathNameByHandleW(h, w, WPATH, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
if (!n || n >= WPATH) return 0;
const wchar_t *p = wcsncmp(w, L"\\\\?\\", 4) ? w : w + 4; // the \\?\ prefix
return WideCharToMultiByte(CP_UTF8, 0, p, -1, out, (int)cap, NULL, NULL) != 0;
}
// ---- desktop
int plat_clipboard_set(const char *utf8) {
int n = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
if (n <= 0) return 0;
HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, (SIZE_T)n * sizeof(wchar_t));
if (!mem) return 0;
wchar_t *w = GlobalLock(mem);
MultiByteToWideChar(CP_UTF8, 0, utf8, -1, w, n);
GlobalUnlock(mem);
// A clipboard opened without a window gets no owner, and SetClipboardData may then fail: a message-only window of
// the calling thread owns it for the call.
HWND owner = CreateWindowExW(0, L"STATIC", NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL);
int ok = 0;
if (OpenClipboard(owner)) {
ok = EmptyClipboard() && SetClipboardData(CF_UNICODETEXT, mem) != NULL;
CloseClipboard();
}
if (owner) DestroyWindow(owner);
if (!ok) GlobalFree(mem); // after a successful SetClipboardData the system owns it
return ok;
}
int plat_clipboard_get(char *out, size_t cap) {
if (!cap) return 0;
out[0] = 0;
if (!OpenClipboard(NULL)) return 0; // reading needs no owner window
HANDLE h = GetClipboardData(CF_UNICODETEXT);
const wchar_t *w = h ? GlobalLock(h) : NULL;
int n = w ? WideCharToMultiByte(CP_UTF8, 0, w, -1, out, (int)cap, NULL, NULL) : 0; // 0 when it does not fit
if (w) GlobalUnlock(h);
CloseClipboard();
if (n <= 0) out[0] = 0;
return out[0] != 0;
}
static DWORD WINAPI open_folder_main(void *arg) {
ShellExecuteW(NULL, L"open", arg, NULL, NULL, SW_SHOWNORMAL);
free(arg);
return 0;
}
int plat_open_folder(const char *path) {
wchar_t *w = malloc(WPATH * sizeof *w);
if (!w || !widen(path, w, WPATH)) return free(w), 0;
DWORD a = GetFileAttributesW(w);
if (a == INVALID_FILE_ATTRIBUTES || !(a & FILE_ATTRIBUTE_DIRECTORY)) return free(w), 0;
// the shell can take a while to answer: not on the caller's thread (the present path)
HANDLE t = CreateThread(NULL, 0, open_folder_main, w, 0, NULL);
if (!t) return free(w), 0;
CloseHandle(t);
return 1;
}
// ---- dynamic libraries
void *plat_lib_open(const char *name) {
wchar_t w[WPATH];
if (!widen(name, w, WPATH)) return NULL;
int bare = !wcschr(w, L'\\') && !wcschr(w, L'/');
return (void *)LoadLibraryExW(w, NULL, bare ? LOAD_LIBRARY_SEARCH_SYSTEM32 : 0);
}
void *plat_lib_sym(void *lib, const char *name) { return lib ? (void *)GetProcAddress((HMODULE)lib, name) : NULL; }
// ---- machine load: GetSystemTimes for the CPU, PDH (pdh.dll, loaded on first use) for the thermal zones and the GPU
// engine and adapter memory counters that Task Manager reads.
typedef PDH_STATUS(WINAPI *PdhOpenQueryW_t)(LPCWSTR, DWORD_PTR, PDH_HQUERY *);
typedef PDH_STATUS(WINAPI *PdhAddEnglishCounterW_t)(PDH_HQUERY, LPCWSTR, DWORD_PTR, PDH_HCOUNTER *);
typedef PDH_STATUS(WINAPI *PdhCollectQueryData_t)(PDH_HQUERY);
typedef PDH_STATUS(WINAPI *PdhGetFormattedCounterArrayW_t)(PDH_HCOUNTER, DWORD, LPDWORD, LPDWORD,
PPDH_FMT_COUNTERVALUE_ITEM_W);
typedef PDH_STATUS(WINAPI *PdhCloseQuery_t)(PDH_HQUERY);
static struct {
INIT_ONCE once;
PdhOpenQueryW_t open;
PdhAddEnglishCounterW_t add;
PdhCollectQueryData_t collect;
PdhGetFormattedCounterArrayW_t get;
PdhCloseQuery_t close;
} pdh = {INIT_ONCE_STATIC_INIT};
static BOOL CALLBACK pdh_load(INIT_ONCE *o, void *p, void **c) {
(void)o, (void)p, (void)c;
HMODULE l = plat_lib_open("pdh.dll");
if (!l) return TRUE;
pdh.add = (PdhAddEnglishCounterW_t)(void *)GetProcAddress(l, "PdhAddEnglishCounterW");
pdh.collect = (PdhCollectQueryData_t)(void *)GetProcAddress(l, "PdhCollectQueryData");
pdh.get = (PdhGetFormattedCounterArrayW_t)(void *)GetProcAddress(l, "PdhGetFormattedCounterArrayW");
pdh.close = (PdhCloseQuery_t)(void *)GetProcAddress(l, "PdhCloseQuery");
PdhOpenQueryW_t open = (PdhOpenQueryW_t)(void *)GetProcAddress(l, "PdhOpenQueryW");
if (pdh.add && pdh.collect && pdh.get && pdh.close) pdh.open = open; // open set: all of them are there
return TRUE;
}
// One PDH query with its wildcard counters ("\Object(*)\Counter"). A query whose open failed stays NULL.
typedef struct { PDH_HQUERY q; PDH_HCOUNTER c[2]; } PdhQuery;
static void pdh_query(PdhQuery *pq, const wchar_t *c0, const wchar_t *c1) {
InitOnceExecuteOnce(&pdh.once, pdh_load, NULL, NULL);
pq->q = NULL;
if (!pdh.open || pdh.open(NULL, 0, &pq->q) != ERROR_SUCCESS) {
pq->q = NULL;
return;
}
if (pdh.add(pq->q, c0, 0, &pq->c[0]) != ERROR_SUCCESS) pq->c[0] = NULL;
if (!c1 || pdh.add(pq->q, c1, 0, &pq->c[1]) != ERROR_SUCCESS) pq->c[1] = NULL;
if (!pq->c[0] && !pq->c[1]) {
pdh.close(pq->q);
pq->q = NULL;
}
}
// The counter's instances as a malloc'd array (free it), *count of them; NULL when there is no valid data yet (a rate
// counter needs two collections).
static PDH_FMT_COUNTERVALUE_ITEM_W *pdh_items(PDH_HCOUNTER c, DWORD *count) {
DWORD size = 0;
*count = 0;
if (!c || pdh.get(c, PDH_FMT_DOUBLE | PDH_FMT_NOCAP100, &size, count, NULL) != PDH_MORE_DATA || !size) return NULL;
PDH_FMT_COUNTERVALUE_ITEM_W *items = malloc(size);
if (items && pdh.get(c, PDH_FMT_DOUBLE | PDH_FMT_NOCAP100, &size, count, items) == ERROR_SUCCESS) return items;
free(items);
*count = 0;
return NULL;
}
struct PlatLoad {
uint64_t idle, kernel, user, proc; // the previous call's times; kernel 0 before the first
PdhQuery thermal, gpu; // gpu: engine utilisation, adapter dedicated memory
wchar_t luid[40]; // "luid_0x<high>_0x<low>" of the adapter, lower case; empty for any
};
PlatLoad *plat_load_open(const uint8_t *gpu_luid, int pci_bus) {
(void)pci_bus; // the LUID names the adapter
PlatLoad *l = calloc(1, sizeof *l);
if (!l) return NULL;
if (gpu_luid) { // Vulkan's deviceLUID is the LUID struct's memory: LowPart, then HighPart
uint32_t lo, hi;
memcpy(&lo, gpu_luid, 4);
memcpy(&hi, gpu_luid + 4, 4);
swprintf(l->luid, sizeof l->luid / sizeof *l->luid, L"luid_0x%08x_0x%08x", hi, lo);
}
pdh_query(&l->thermal, L"\\Thermal Zone Information(*)\\Temperature", NULL);
pdh_query(&l->gpu, L"\\GPU Engine(*)\\Utilization Percentage", L"\\GPU Adapter Memory(*)\\Dedicated Usage");
return l;
}
void plat_load_close(PlatLoad *l) {
if (!l) return;
if (l->thermal.q) pdh.close(l->thermal.q);
if (l->gpu.q) pdh.close(l->gpu.q);
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;
FILETIME idle, kern, user, cr, ex, pk, pu;
if (GetSystemTimes(&idle, &kern, &user) && GetProcessTimes(GetCurrentProcess(), &cr, &ex, &pk, &pu)) {
uint64_t i = ft64(idle), k = ft64(kern), u = ft64(user), p = ft64(pk) + ft64(pu);
uint64_t total = (k - l->kernel) + (u - l->user); // kernel time includes idle time
if (l->kernel && total) {
*total_pct = 100.0f * (float)(total - (i - l->idle)) / (float)total;
*proc_pct = 100.0f * (float)(p - l->proc) / (float)total;
}
l->idle = i, l->kernel = k, l->user = u, l->proc = p;
}
// the firmware's ACPI zones, near the CPU on most boards; the exact package temperature needs a kernel driver
DWORD n;
PDH_FMT_COUNTERVALUE_ITEM_W *it;
if (l->thermal.q && pdh.collect(l->thermal.q) == ERROR_SUCCESS && (it = pdh_items(l->thermal.c[0], &n))) {
for (DWORD i = 0; i < n; i++) { // the hottest zone, in kelvin
double c = it[i].FmtValue.doubleValue - 273.15;
if (it[i].FmtValue.CStatus == PDH_CSTATUS_VALID_DATA && c > *temp_c && c < 150) *temp_c = (float)c;
}
free(it);
}
}
// The adapter part of an instance name, "luid_0x..._0x...", lower case, into out (40 wide characters).
static void inst_luid(const wchar_t *name, wchar_t *out) {
const wchar_t *p = wcsstr(name, L"luid_");
size_t n = 0;
while (p && p[n] && n < 26 && n < 39) out[n] = (wchar_t)towlower(p[n]), n++; // luid_0x%08x_0x%08x: 26
out[n] = 0;
}
void plat_gpu_load(PlatLoad *l, float *load_pct, float *vram_used_mb) {
*load_pct = *vram_used_mb = -1;
if (!l || !l->gpu.q || pdh.collect(l->gpu.q) != ERROR_SUCCESS) return;
// Engine instances: "pid_<n>_luid_0x.._0x.._phys_<n>_eng_<n>_engtype_<type>". Per adapter and engine type the
// processes' shares add up; the load is the busiest engine type.
enum { TYPES = 32 };
struct { wchar_t luid[40], type[32]; double sum; } t[TYPES];
int nt = 0;
DWORD n;
PDH_FMT_COUNTERVALUE_ITEM_W *it = pdh_items(l->gpu.c[0], &n);
for (DWORD i = 0; it && i < n; i++) {
if (it[i].FmtValue.CStatus != PDH_CSTATUS_VALID_DATA) continue;
wchar_t lu[40];
inst_luid(it[i].szName, lu);
if (l->luid[0] && wcscmp(lu, l->luid)) continue;
const wchar_t *ty = wcsstr(it[i].szName, L"engtype_");
ty = ty ? ty + 8 : L"";
int k = 0;
while (k < nt && (wcscmp(t[k].luid, lu) || wcsncmp(t[k].type, ty, 31))) k++;
if (k == nt) {
if (nt == TYPES) continue;
wcscpy(t[k].luid, lu);
wcsncpy(t[k].type, ty, 31);
t[k].type[31] = 0;
t[k].sum = 0;
nt++;
}
t[k].sum += it[i].FmtValue.doubleValue;
}
free(it);
for (int k = 0; k < nt; k++)
if (t[k].sum > *load_pct) *load_pct = (float)(t[k].sum > 100 ? 100 : t[k].sum);
// Adapter memory instances: "luid_0x.._0x.._phys_<n>", bytes. Without a LUID: the adapter that uses the most.
it = pdh_items(l->gpu.c[1], &n);
double sum = 0, best = -1;
for (DWORD i = 0; it && i < n; i++) {
if (it[i].FmtValue.CStatus != PDH_CSTATUS_VALID_DATA) continue;
wchar_t lu[40];
inst_luid(it[i].szName, lu);
if (l->luid[0] ? !wcscmp(lu, l->luid) : 0) sum += it[i].FmtValue.doubleValue;
if (it[i].FmtValue.doubleValue > best) best = it[i].FmtValue.doubleValue;
}
if (it && (l->luid[0] ? sum > 0 : best >= 0)) *vram_used_mb = (float)((l->luid[0] ? sum : best) / 1048576.0);
free(it);
}
void plat_mem_info(float *used_mb, float *total_mb, float *proc_mb) {
*used_mb = *total_mb = *proc_mb = -1;
MEMORYSTATUSEX ms = {.dwLength = sizeof ms};
if (GlobalMemoryStatusEx(&ms)) {
*total_mb = (float)(ms.ullTotalPhys >> 20);
*used_mb = (float)((ms.ullTotalPhys - ms.ullAvailPhys) >> 20);
}
PROCESS_MEMORY_COUNTERS pmc;
if (K32GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc)) *proc_mb = (float)(pmc.WorkingSetSize >> 20);
}
void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
*proc_mb = *used_mb = *limit_mb = -1;
MEMORYSTATUSEX ms = {.dwLength = sizeof ms};
if (GlobalMemoryStatusEx(&ms)) {
*limit_mb = (double)(ms.ullTotalPageFile >> 20);
*used_mb = (double)((ms.ullTotalPageFile - ms.ullAvailPageFile) >> 20);
}
PROCESS_MEMORY_COUNTERS_EX pmc;
if (K32GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS *)&pmc, sizeof pmc))
*proc_mb = (double)(pmc.PrivateUsage >> 20);
}
int plat_commit_enforced(void) { return 1; }
// The numbers name the one limit there is, the system's commit.
int plat_commit_note(char *out, size_t cap) {
if (cap) out[0] = 0;
return 0;
}
// The driver charges the device memory it allocates to the system's commit.
int plat_commit_counts_vram(void) { return 1; }
// The PagingFiles value under Memory Management, one "<path> <initial MB> <maximum MB>" per page file: a path of
// "?:\..." (System managed size on every drive) or no sizes or "0 0" (System managed size on that drive) leaves the
// size to Windows. Read once; the setting takes effect only after a restart.
int plat_page_file_managed(void) {
static int managed = -2;
if (managed != -2) return managed;
wchar_t v[2048];
DWORD size = sizeof v - sizeof(wchar_t) * 2;
int r = -1;
if (RegGetValueW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Memory Management",
L"PagingFiles", RRF_RT_REG_MULTI_SZ, NULL, v, &size) == ERROR_SUCCESS) {
v[size / sizeof(wchar_t)] = v[size / sizeof(wchar_t) + 1] = 0;
for (wchar_t *e = v; *e; e += wcslen(e) + 1) {
wchar_t *sp = wcschr(e, L' ');
unsigned long lo = sp ? wcstoul(sp, &sp, 10) : 0, hi = sp ? wcstoul(sp, NULL, 10) : 0;
int own = e[0] == L'?' || (!lo && !hi);
r = own && r != 0 ? 1 : 0; // a page file with its sizes set is the user's choice: no advice then
}
}
return managed = r;
}
void plat_process_memory(uint64_t *resident, uint64_t *private_bytes) {
PROCESS_MEMORY_COUNTERS_EX pm = {.cb = sizeof pm};
*resident = *private_bytes = 0;
if (K32GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS *)&pm, sizeof pm))
*resident = pm.WorkingSetSize, *private_bytes = pm.PrivateUsage;
}
uint32_t plat_handle_count(void) {
DWORD n = 0;
return GetProcessHandleCount(GetCurrentProcess(), &n) ? (uint32_t)n : 0;
}
// plat_handle_census: the system's handle table (NtQuerySystemInformation, class 64, the extended handle information)
// filtered by this process's id, and the object types' names from NtQueryObject's ObjectTypesInformation, whose entries
// carry the type index the table uses (Windows 8.1 and later). The table holds every process's handles; a census took
// 1.5-4.7 ms over 3.6 MiB of it on the development PC.
typedef struct {
PVOID object;
ULONG_PTR pid, handle;
ULONG access;
USHORT backtrace, type;
ULONG attributes, reserved;
} HcEntry;
typedef struct {
ULONG_PTR n, reserved;
HcEntry e[1];
} HcTable;
typedef struct {
USHORT len, max;
PWSTR buf;
} HcName;
typedef struct { // one entry of ObjectTypesInformation; its name's characters follow it
HcName name;
ULONG totals[13]; // object and handle counts, pool use, their high-water marks, invalid attributes: not read
GENERIC_MAPPING mapping;
ULONG valid_access;
BOOLEAN security, maintain;
UCHAR index;
CHAR reserved;
ULONG pool, paged, nonpaged;
} HcType;
static void hc_type_names(char names[256][32]) {
typedef LONG(NTAPI * Qo)(HANDLE, ULONG, PVOID, ULONG, PULONG);
HMODULE nt = GetModuleHandleW(L"ntdll.dll");
Qo qo = nt ? (Qo)(void *)GetProcAddress(nt, "NtQueryObject") : NULL;
if (!qo) return;
size_t cap = 1u << 16;
uint8_t *buf = NULL;
LONG st = -1;
for (int tries = 0; tries < 6; tries++, cap *= 2) {
uint8_t *b = realloc(buf, cap);
if (!b) break;
buf = b;
ULONG need = 0;
st = qo(NULL, 3, buf, (ULONG)cap, &need); // ObjectTypesInformation
if (st != (LONG)0xC0000004L) break; // not STATUS_INFO_LENGTH_MISMATCH: done, or failed
}
if (st >= 0 && buf) {
ULONG count = *(ULONG *)buf;
const uint8_t *p = buf + sizeof(void *), *end = buf + cap; // the count, then entries at pointer alignment
for (ULONG i = 0; i < count && p + sizeof(HcType) <= end; i++) {
const HcType *t = (const HcType *)p;
WideCharToMultiByte(CP_UTF8, 0, t->name.buf, t->name.len / 2, names[t->index], 31, NULL, NULL);
p += sizeof(HcType) + ((t->name.max + sizeof(void *) - 1) & ~(sizeof(void *) - 1));
}
}
free(buf);
}
int plat_handle_census(PlatHandleKind *out, int max) {
typedef LONG(NTAPI * Qsi)(ULONG, PVOID, ULONG, PULONG);
static char names[256][32];
static int named;
static void *table;
static size_t cap;
HMODULE nt = GetModuleHandleW(L"ntdll.dll");
Qsi qsi = nt ? (Qsi)(void *)GetProcAddress(nt, "NtQuerySystemInformation") : NULL;
if (!qsi || max <= 0) return -1;
if (!named) hc_type_names(names), named = 1;
LONG st = -1;
for (int tries = 0; tries < 6; tries++) {
if (!table && !(table = malloc(cap = 4u << 20))) return -1;
ULONG need = 0;
st = qsi(64, table, (ULONG)cap, &need); // SystemExtendedHandleInformation
if (st != (LONG)0xC0000004L) break; // the table outgrew the buffer: a larger one
size_t want = need > cap ? need + (1u << 20) : cap * 2;
void *b = realloc(table, want);
if (!b) return -1;
table = b, cap = want;
}
if (st < 0) return -1;
uint32_t count[256] = {0};
const HcTable *t = table;
ULONG_PTR pid = GetCurrentProcessId();
for (ULONG_PTR i = 0; i < t->n && offsetof(HcTable, e) + (i + 1) * sizeof(HcEntry) <= cap; i++)
if (t->e[i].pid == pid) count[t->e[i].type & 255]++;
int n = 0;
for (int k = 0; k < 256; k++) {
if (!count[k]) continue;
if (n == max) { // no room: the last entry takes the rest
snprintf(out[max - 1].name, sizeof out[0].name, "%s", "(more kinds)");
out[max - 1].n += count[k];
continue;
}
if (names[k][0]) snprintf(out[n].name, sizeof out[0].name, "%s", names[k]);
else snprintf(out[n].name, sizeof out[0].name, "type %d", k);
out[n++].n = count[k];
}
return n;
}
// The UCRT's malloc takes its memory from the heap _get_heap_handle names (the process heap); HeapSummary sums it. The
// other heaps are only counted: one made with HEAP_NO_SERIALIZE has no lock to take.
void plat_heap_info(PlatHeapInfo *h) {
memset(h, 0, sizeof *h);
HANDLE crt = (HANDLE)_get_heap_handle();
HEAP_SUMMARY s = {.cb = sizeof s};
if (crt && HeapSummary(crt, 0, &s)) h->committed = s.cbCommitted, h->used = s.cbAllocated;
HANDLE hs[256];
DWORD n = GetProcessHeaps(256, hs);
for (DWORD i = 0; i < n && i < 256; i++) h->other_heaps += hs[i] != crt;
}
// ---- child processes
struct PlatChild {
HANDLE proc, in, out; // in: our write end, out: our read end
};
PlatChild *plat_child_start(const char *exe, const char *arg, uint32_t *err) {
wchar_t we[WPATH], wa[256] = L"", cmd[WPATH + 300];
if (err) *err = 0;
if (!widen(exe, we, WPATH) || (arg && !widen(arg, wa, 256))) return NULL;
swprintf(cmd, WPATH + 300, arg ? L"\"%ls\" %ls" : L"\"%ls\"", we, wa);
SECURITY_ATTRIBUTES sa = {sizeof sa, NULL, TRUE};
HANDLE in_r, in_w, out_r, out_w;
// Pipes are created and handed out one child at a time: a child started meanwhile would inherit the other pipe
// ends and keep them open after their owner closed them.
static SRWLOCK spawn = SRWLOCK_INIT;
AcquireSRWLockExclusive(&spawn);
if (!CreatePipe(&in_r, &in_w, &sa, 0)) return ReleaseSRWLockExclusive(&spawn), NULL;
if (!CreatePipe(&out_r, &out_w, &sa, 0)) {
CloseHandle(in_r), CloseHandle(in_w);
return ReleaseSRWLockExclusive(&spawn), NULL;
}
SetHandleInformation(in_w, HANDLE_FLAG_INHERIT, 0);
SetHandleInformation(out_r, HANDLE_FLAG_INHERIT, 0);
HANDLE nul = CreateFileW(L"NUL", GENERIC_WRITE, FILE_SHARE_WRITE, &sa, OPEN_EXISTING, 0, NULL);
STARTUPINFOW si = {.cb = sizeof si, .dwFlags = STARTF_USESTDHANDLES, .hStdInput = in_r, .hStdOutput = out_w,
.hStdError = nul != INVALID_HANDLE_VALUE ? nul : out_w};
PROCESS_INFORMATION pi;
BOOL ok = CreateProcessW(NULL, cmd, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
DWORD e = GetLastError();
CloseHandle(in_r);
CloseHandle(out_w);
if (nul != INVALID_HANDLE_VALUE) CloseHandle(nul);
ReleaseSRWLockExclusive(&spawn);
if (!ok) {
CloseHandle(in_w), CloseHandle(out_r);
if (err) *err = e;
return NULL;
}
CloseHandle(pi.hThread);
// Every child goes into one job that ends its processes when its last handle closes, which is when this process
// ends: a translator stuck on a program would otherwise outlive a game that exited or crashed before stopping it.
// The handle is never closed by hand. A failure leaves the child as it was before.
static HANDLE job;
static INIT_ONCE job_once = INIT_ONCE_STATIC_INIT;
BOOL pending;
if (InitOnceBeginInitialize(&job_once, 0, &pending, NULL) && pending) {
HANDLE j = CreateJobObjectW(NULL, NULL);
JOBOBJECT_EXTENDED_LIMIT_INFORMATION li = {0};
li.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (j && !SetInformationJobObject(j, JobObjectExtendedLimitInformation, &li, sizeof li)) CloseHandle(j), j = NULL;
job = j;
InitOnceComplete(&job_once, 0, NULL);
}
if (job) AssignProcessToJobObject(job, pi.hProcess);
PlatChild *c = malloc(sizeof *c);
if (!c) {
TerminateProcess(pi.hProcess, 1);
CloseHandle(pi.hProcess), CloseHandle(in_w), CloseHandle(out_r);
return NULL;
}
c->proc = pi.hProcess, c->in = in_w, c->out = out_r;
return c;
}
void plat_child_stop(PlatChild *c) {
if (!c) return;
CloseHandle(c->in);
CloseHandle(c->out);
TerminateProcess(c->proc, 1);
CloseHandle(c->proc);
free(c);
}
void plat_child_lower(PlatChild *c) {
if (c) SetPriorityClass(c->proc, IDLE_PRIORITY_CLASS);
}
int plat_child_write(PlatChild *c, const void *data, size_t n) {
DWORD put = 0;
return WriteFile(c->in, data, (DWORD)n, &put, NULL) && put == n;
}
// A child that stays alive but never answers (a translator stuck on one program) used to block the reader for good,
// and with it the draw or dispatch waiting for that shader: after PLAT_CHILD_READ_MS without a byte the read fails
// with -2, apart from the -1 of a broken pipe, and the caller stops the child without sending the request again
// (gfx_shader.c svc_request). The pipe is anonymous, so readiness is polled.
long plat_child_read(PlatChild *c, void *buf, size_t cap) {
ULONGLONG t0 = GetTickCount64();
for (unsigned spins = 0;; spins++) {
DWORD avail = 0;
if (!PeekNamedPipe(c->out, NULL, 0, NULL, &avail, NULL)) return -1; // broken: the child exited
if (avail) break;
if (GetTickCount64() - t0 > PLAT_CHILD_READ_MS) return -2;
Sleep(spins < 50 ? 0 : 1);
}
DWORD got = 0;
if (!ReadFile(c->out, buf, (DWORD)cap, &got, NULL)) return -1;
return (long)got;
}
// ---- local IPC (platform.h): Discord's Rich Presence named pipe
struct PlatIpc { HANDLE h; };
PlatIpc *plat_ipc_connect(const char *name) {
wchar_t wname[128], path[152];
if (!widen(name, wname, 128)) return NULL;
swprintf(path, 152, L"\\\\.\\pipe\\%ls", wname);
HANDLE h = CreateFileW(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (h == INVALID_HANDLE_VALUE) return NULL; // no listener, or the pipe is busy: the caller retries later
PlatIpc *p = malloc(sizeof *p);
if (!p) {
CloseHandle(h);
return NULL;
}
p->h = h;
return p;
}
int plat_ipc_read(PlatIpc *p, void *buf, size_t n, uint32_t ms) {
uint64_t deadline = plat_now_ms() + ms;
for (;;) {
DWORD have = 0;
if (!PeekNamedPipe(p->h, NULL, 0, NULL, &have, NULL)) return -1; // the writer closed the pipe
if (have) {
DWORD got = 0;
if (!ReadFile(p->h, buf, have < n ? have : (DWORD)n, &got, NULL) || !got) return -1;
return (int)got;
}
if (plat_now_ms() >= deadline) return 0;
plat_sleep_ms(5);
}
}
int plat_ipc_write(PlatIpc *p, const void *buf, size_t n) {
const uint8_t *at = buf;
while (n) {
DWORD put = 0;
if (!WriteFile(p->h, at, n > (1u << 30) ? 1u << 30 : (DWORD)n, &put, NULL) || !put) return 0;
at += put, n -= put;
}
return 1;
}
void plat_ipc_close(PlatIpc *p) {
if (!p) return;
CloseHandle(p->h);
free(p);
}
// ---- UDP sockets (platform.h). Winsock starts at the first call that needs it.
#ifndef SIO_UDP_CONNRESET
#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#endif
#ifndef WSA_FLAG_NO_HANDLE_INHERIT
#define WSA_FLAG_NO_HANDLE_INHERIT 0x80
#endif
struct PlatUdp { SOCKET s; int v6; };
static PlatOnce wsa_once = PLAT_ONCE_INIT;
static int wsa_ok;
static void wsa_start(void *arg) {
WSADATA d;
(void)arg;
wsa_ok = WSAStartup(MAKEWORD(2, 2), &d) == 0;
}
static int net_ready(void) {
plat_once(&wsa_once, wsa_start, NULL);
return wsa_ok;
}
static void addr_from_sockaddr(PlatAddr *out, const SOCKADDR_STORAGE *ss) {
uint8_t ip[16] = {0};
if (ss->ss_family == AF_INET6) {
const SOCKADDR_IN6 *a = (const SOCKADDR_IN6 *)ss;
memcpy(ip, &a->sin6_addr, 16);
plat_addr_set(out, ip, ntohs(a->sin6_port), a->sin6_scope_id);
} else {
const SOCKADDR_IN *a = (const SOCKADDR_IN *)ss;
ip[10] = ip[11] = 0xff;
memcpy(ip + 12, &a->sin_addr, 4);
plat_addr_set(out, ip, ntohs(a->sin_port), 0);
}
}
// The socket address of a for a socket of family v6 (1: AF_INET6, 0: AF_INET); 0 when an IPv4 socket cannot reach it.
static int addr_to_sockaddr(const PlatAddr *a, int v6, SOCKADDR_STORAGE *ss, int *len) {
uint8_t ip[16];
uint32_t scope;
plat_addr_get(a, ip, &scope);
memset(ss, 0, sizeof *ss);
if (v6) {
SOCKADDR_IN6 *s = (SOCKADDR_IN6 *)ss;
s->sin6_family = AF_INET6;
s->sin6_port = htons(plat_addr_port(a));
s->sin6_scope_id = scope;
memcpy(&s->sin6_addr, ip, 16);
*len = (int)sizeof *s;
return 1;
}
static const uint8_t mapped[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff};
if (memcmp(ip, mapped, sizeof mapped)) return 0;
SOCKADDR_IN *s = (SOCKADDR_IN *)ss;
s->sin_family = AF_INET;
s->sin_port = htons(plat_addr_port(a));
memcpy(&s->sin_addr, ip + 12, 4);
*len = (int)sizeof *s;
return 1;
}
int plat_net_resolve(const char *host, uint16_t port, PlatAddr *out) {
wchar_t whost[256];
if (!host || !*host || !net_ready() || !widen(host, whost, 256)) return 0;
ADDRINFOW hints = {.ai_family = AF_UNSPEC, .ai_socktype = SOCK_DGRAM}, *res = NULL, *pick = NULL;
if (GetAddrInfoW(whost, NULL, &hints, &res) != 0) return 0;
for (ADDRINFOW *r = res; r; r = r->ai_next) {
if (r->ai_addrlen > sizeof(SOCKADDR_STORAGE) || (r->ai_family != AF_INET && r->ai_family != AF_INET6)) continue;
if (!pick || (r->ai_family == AF_INET && pick->ai_family != AF_INET)) pick = r; // IPv4 first
}
int ok = pick != NULL;
if (ok) {
SOCKADDR_STORAGE ss;
memset(&ss, 0, sizeof ss);
memcpy(&ss, pick->ai_addr, pick->ai_addrlen);
addr_from_sockaddr(out, &ss);
uint8_t ip[16];
uint32_t scope;
plat_addr_get(out, ip, &scope);
plat_addr_set(out, ip, port, scope);
}
FreeAddrInfoW(res);
return ok;
}
// A non-blocking UDP socket of family af that no child process inherits, with Windows' report of an ICMP port-unreachable
// as a failed receive (WSAECONNRESET on an unconnected socket) switched off: a peer that is gone is not an error here.
static SOCKET udp_socket(int af) {
SOCKET s = WSASocketW(af, SOCK_DGRAM, IPPROTO_UDP, NULL, 0, WSA_FLAG_OVERLAPPED | WSA_FLAG_NO_HANDLE_INHERIT);
if (s == INVALID_SOCKET) return s;
u_long nonblocking = 1;
BOOL report = FALSE;
DWORD got = 0;
if (ioctlsocket(s, FIONBIO, &nonblocking) != 0) {
closesocket(s);
return INVALID_SOCKET;
}
WSAIoctl(s, SIO_UDP_CONNRESET, &report, sizeof report, NULL, 0, &got, NULL, NULL);
return s;
}
PlatUdp *plat_udp_open(uint16_t port, int dual_stack) {
if (!net_ready()) return NULL;
int v6 = dual_stack;
SOCKET s = v6 ? udp_socket(AF_INET6) : INVALID_SOCKET;
if (s != INVALID_SOCKET) {
DWORD only_v6 = 0; // Windows defaults to IPv6 only
setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (const char *)&only_v6, sizeof only_v6);
SOCKADDR_IN6 a;
memset(&a, 0, sizeof a); // in6addr_any
a.sin6_family = AF_INET6;
a.sin6_port = htons(port);
if (bind(s, (SOCKADDR *)&a, sizeof a) != 0) closesocket(s), s = INVALID_SOCKET;
}
if (s == INVALID_SOCKET) { // IPv4 only, asked for or all the machine has
v6 = 0;
s = udp_socket(AF_INET);
if (s == INVALID_SOCKET) return NULL;
SOCKADDR_IN a;
memset(&a, 0, sizeof a);
a.sin_family = AF_INET;
a.sin_port = htons(port);
a.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(s, (SOCKADDR *)&a, sizeof a) != 0) {
closesocket(s);
return NULL;
}
}
PlatUdp *u = malloc(sizeof *u);
if (!u) {
closesocket(s);
return NULL;
}
u->s = s, u->v6 = v6;
return u;
}
uint16_t plat_udp_port(const PlatUdp *u) {
SOCKADDR_STORAGE ss;
int len = (int)sizeof ss;
if (!u || getsockname(u->s, (SOCKADDR *)&ss, &len) != 0) return 0;
PlatAddr a;
addr_from_sockaddr(&a, &ss);
return plat_addr_port(&a);
}
int plat_udp_send(PlatUdp *u, const PlatAddr *to, const void *p, size_t n) {
SOCKADDR_STORAGE ss;
int len;
if (!u || n > 65507 || !addr_to_sockaddr(to, u->v6, &ss, &len)) return 0;
if (sendto(u->s, (const char *)p, (int)n, 0, (SOCKADDR *)&ss, len) != SOCKET_ERROR) return 1;
// a full buffer, a missing route or an ICMP error is a lost datagram; a datagram too long or a socket that is not
// ours to use can never be sent
int e = WSAGetLastError();
return e != WSAEMSGSIZE && e != WSAENOTSOCK && e != WSAEINVAL && e != WSAEAFNOSUPPORT;
}
int plat_udp_recv(PlatUdp *u, void *buf, size_t cap, PlatAddr *from, uint32_t ms) {
if (!u) return -1;
WSAPOLLFD pf = {.fd = u->s, .events = POLLRDNORM};
int r = WSAPoll(&pf, 1, ms > (uint32_t)INT_MAX ? INT_MAX : (int)ms);
if (r == SOCKET_ERROR) return WSAGetLastError() == WSAEINTR ? 0 : -1;
if (r == 0) return 0;
SOCKADDR_STORAGE ss;
int len = (int)sizeof ss;
int k = recvfrom(u->s, (char *)buf, cap > (size_t)INT_MAX ? INT_MAX : (int)cap, 0, (SOCKADDR *)&ss, &len);
if (k == SOCKET_ERROR) {
int e = WSAGetLastError();
if (e == WSAEMSGSIZE) { // longer than buf: Windows reports it as an error but has filled buf
k = cap > (size_t)INT_MAX ? INT_MAX : (int)cap;
} else {
return e == WSAEWOULDBLOCK || e == WSAEINTR || e == WSAECONNRESET || e == WSAENETRESET || e == WSAEHOSTUNREACH ||
e == WSAENETUNREACH
? 0 : -1;
}
}
if (from) addr_from_sockaddr(from, &ss);
return k;
}
void plat_udp_close(PlatUdp *u) {
if (!u) return;
closesocket(u->s);
free(u);
}
// ---- restart
// NX2NAVI_RESTART_WAIT names the process that called plat_restart, as its id and creation time: 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; no handle is inherited, so none of them stays open in the new one either.
#define RESTART_WAIT "NX2NAVI_RESTART_WAIT"
static uint64_t process_created(HANDLE h) {
FILETIME c, e, k, u;
return GetProcessTimes(h, &c, &e, &k, &u) ? ft64(c) : 0;
}
int plat_restart(void) {
char wait[48];
snprintf(wait, sizeof wait, "%lu.%llu", GetCurrentProcessId(), (unsigned long long)process_created(GetCurrentProcess()));
plat_setenv(RESTART_WAIT, wait);
STARTUPINFOW si = {.cb = sizeof si};
PROCESS_INFORMATION pi;
wchar_t *cmd = _wcsdup(GetCommandLineW()); // CreateProcessW may write its command line
BOOL ok = cmd && CreateProcessW(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
free(cmd);
plat_setenv(RESTART_WAIT, NULL);
if (!ok) return 0;
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 1;
}
// One argument of a command line as CommandLineToArgvW reads it back: in quotes, a backslash run doubled before a quote
// and at the end, a quote escaped.
static size_t quote_arg(const wchar_t *a, wchar_t *out, size_t cap, size_t at) {
#define PUT(c) do { if (at + 1 < cap) out[at] = (c); at++; } while (0)
PUT(L'"');
for (const wchar_t *p = a;; p++) {
size_t slashes = 0;
while (*p == L'\\') p++, slashes++;
size_t times = !*p || *p == L'"' ? 2 : 1;
for (size_t k = 0; k < slashes * times; k++) PUT(L'\\');
if (!*p) break;
if (*p == L'"') PUT(L'\\');
PUT(*p);
}
PUT(L'"');
PUT(L' ');
#undef PUT
return at;
}
int plat_spawn(const char *const *argv, const char *cwd) {
enum { CMD = 32768 };
wchar_t *cmd = calloc(CMD, sizeof *cmd), *arg = malloc(WPATH * sizeof *arg), *dir = malloc(WPATH * sizeof *dir);
int ok = cmd && arg && dir && argv[0] && widen(cwd, dir, WPATH);
size_t at = 0;
for (int i = 0; ok && argv[i]; i++) {
ok = widen(argv[i], arg, WPATH);
if (ok) at = quote_arg(arg, cmd, CMD, at);
}
ok = ok && at < CMD && at > 0;
if (ok) cmd[at - 1] = 0; // the last separator
STARTUPINFOW si = {.cb = sizeof si};
PROCESS_INFORMATION pi;
ok = ok && CreateProcessW(NULL, cmd, NULL, NULL, FALSE, DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP, NULL, dir, &si,
&pi);
if (ok) {
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
free(cmd);
free(arg);
free(dir);
return ok;
}
int plat_restart_wait(uint32_t timeout_ms) {
char v[48];
if (!plat_getenv(RESTART_WAIT, v, sizeof v)) return 0;
plat_setenv(RESTART_WAIT, NULL); // the processes this one starts do not wait again
unsigned long pid = 0;
unsigned long long created = 0;
if (sscanf(v, "%lu.%llu", &pid, &created) != 2) return 0;
HANDLE h = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!h) return 0; // already gone
int r = process_created(h) != created ? 0 : WaitForSingleObject(h, timeout_ms) == WAIT_OBJECT_0 ? 1 : -1;
CloseHandle(h);
return r;
}