From 357f2f24d6914205b99da2bf6ea62ea153884fb9 Mon Sep 17 00:00:00 2001 From: ytsodacan <103301261+ytsodacan@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:16:23 +0000 Subject: [PATCH 01/12] Add macOS support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- builder/macos/hookshot-launch | 15 +++ host/hos_main.c | 18 ++- host/hos_setjmp.c | 10 ++ host/posix/prof_stub.c | 3 + host/posix/psapi.h | 14 +++ host/posix/win32_posix.c | 218 +++++++++++++++++++++++++++++++--- host/posix/windows.h | 4 + nx2/build.py | 66 +++++----- nx2/make_game.py | 30 +++-- readme.md | 21 +++- 10 files changed, 338 insertions(+), 61 deletions(-) create mode 100755 builder/macos/hookshot-launch create mode 100644 host/posix/prof_stub.c diff --git a/builder/macos/hookshot-launch b/builder/macos/hookshot-launch new file mode 100755 index 00000000..ebd6d295 --- /dev/null +++ b/builder/macos/hookshot-launch @@ -0,0 +1,15 @@ +#!/bin/sh +set -eu + +folder=${1:-} +if [ -z "$folder" ]; then + folder="${HOME}/Games/Hookshot" +fi + +if [ ! -f "$folder/hookshot" ] || [ ! -f "$folder/image.bin" ]; then + echo "Hookshot: no built game in $folder" >&2 + exit 1 +fi + +cd "$folder" +exec ./hookshot diff --git a/host/hos_main.c b/host/hos_main.c index 74e3b041..edd11310 100644 --- a/host/hos_main.c +++ b/host/hos_main.c @@ -129,13 +129,20 @@ static int reserve_guest(void) { step: MAP_NORESERVE). Decommitting is madvise (host/posix/win32_posix.c VirtualFree): the pages read as zero. */ void hos_commit_init(void); hos_commit_init(); /* (the crash report) */ - if (mmap((void *)A64_HOST_BASE, GUEST_SPACE, PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE | MAP_NORESERVE, -1, 0) != (void *)A64_HOST_BASE) + int map_flags = MAP_PRIVATE | MAP_ANONYMOUS; +#ifdef __linux__ + map_flags |= MAP_FIXED_NOREPLACE | MAP_NORESERVE; +#else + map_flags |= MAP_FIXED; +#endif + if (mmap((void *)A64_HOST_BASE, GUEST_SPACE, PROT_READ | PROT_WRITE, map_flags, -1, 0) != (void *)A64_HOST_BASE) return 0; /* (issue 312) no transparent huge pages in the guest window: the renderer imports its pools as userptr memory, and every collapse khugepaged makes in them changes their mappings, which the kernel answers by pinning the pages again at the next submission: ~250 ms a submit on RADV while the GPU sat idle. HOS_THP=1: as before */ +#ifdef __linux__ if (!getenv("HOS_THP")) madvise((void *)A64_HOST_BASE, GUEST_SPACE, MADV_NOHUGEPAGE); +#endif return 1; #endif } @@ -163,7 +170,7 @@ static void call_guest(Ctx *c, uint64_t addr, uint64_t sp, const char *what) { void hos_update_check(void); /* (host/hos_update.c) */ #ifndef _WIN32 -/* (linux) Saves live in $XDG_DATA_HOME/hookshot (~/.local/share/hookshot), so a rebuilt game folder still finds them. +/* POSIX saves live outside the rebuilt game folder, so a rebuilt game still finds them. A user/ folder beside the executable from before is moved there the first time, entry by entry. Anything that cannot be moved keeps the old place. --user still wins. */ static int is_dir(const char *p) { @@ -222,12 +229,17 @@ static void delete_tree(const char *p) { static const char *saves_dir(const char *beside) { static char dir[MAX_PATH]; const char *xdg = getenv("XDG_DATA_HOME"), *home = getenv("HOME"); +#ifdef __APPLE__ + if (home && home[0] == '/') snprintf(dir, sizeof dir, "%s/Library/Application Support", home); +#else if (xdg && xdg[0] == '/') snprintf(dir, sizeof dir, "%s", xdg); else if (home && home[0] == '/') { snprintf(dir, sizeof dir, "%s/.local", home); CreateDirectoryA(dir, NULL); snprintf(dir, sizeof dir, "%s/.local/share", home); } else return beside; +#endif + if (!home && !xdg) return beside; CreateDirectoryA(dir, NULL); snprintf(dir + strlen(dir), sizeof dir - strlen(dir), "/hookshot"); CreateDirectoryA(dir, NULL); diff --git a/host/hos_setjmp.c b/host/hos_setjmp.c index ec01bff4..b1c02e5b 100644 --- a/host/hos_setjmp.c +++ b/host/hos_setjmp.c @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -100,6 +101,15 @@ __attribute__((naked, noreturn)) static void recomp_sj_longjmp(void **buf, int v "jmpq *72(%rcx)\n\t"); } +#elif defined(__aarch64__) +__attribute__((returns_twice)) int recomp_sj_setjmp(void **buf) { + return setjmp(*(jmp_buf *)buf); +} + +__attribute__((noreturn)) static void recomp_sj_longjmp(void **buf, int value) { + longjmp(*(jmp_buf *)buf, value); +} + #else /* (linux) the System V ABI's: the buffer as above, its callee-saved registers only (rbx rbp r12-r15: rdi, rsi and the xmm registers are the caller's to keep there), buf in rdi and the value in esi */ diff --git a/host/posix/prof_stub.c b/host/posix/prof_stub.c new file mode 100644 index 00000000..4d1693ef --- /dev/null +++ b/host/posix/prof_stub.c @@ -0,0 +1,3 @@ +#include "../hos.h" + +void hos_prof_start(void) {} diff --git a/host/posix/psapi.h b/host/posix/psapi.h index 6e3479eb..42504a1e 100644 --- a/host/posix/psapi.h +++ b/host/posix/psapi.h @@ -3,6 +3,9 @@ #ifndef HOOKSHOT_POSIX_PSAPI_H #define HOOKSHOT_POSIX_PSAPI_H #include +#ifdef __APPLE__ +#include +#endif typedef struct { DWORD cb, PageFaultCount; SIZE_T PeakWorkingSetSize, WorkingSetSize, QuotaPeakPagedPoolUsage, QuotaPagedPoolUsage, QuotaPeakNonPagedPoolUsage, @@ -15,6 +18,16 @@ typedef struct { } PROCESS_MEMORY_COUNTERS_EX; static inline BOOL K32GetProcessMemoryInfo(HANDLE p, PROCESS_MEMORY_COUNTERS *out, DWORD cb) { (void)p; +#ifdef __APPLE__ + mach_task_basic_info_data_t info; + mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; + if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &count) != KERN_SUCCESS) return FALSE; + memset(out, 0, cb); + out->cb = cb, out->WorkingSetSize = info.resident_size, out->PeakWorkingSetSize = info.resident_size, + out->PagefileUsage = info.virtual_size - info.resident_size; + if (cb >= sizeof(PROCESS_MEMORY_COUNTERS_EX)) ((PROCESS_MEMORY_COUNTERS_EX *)out)->PrivateUsage = out->PagefileUsage; + return TRUE; +#else FILE *f = fopen("/proc/self/status", "r"); if (!f) return FALSE; SIZE_T rss = 0, hwm = 0, anon = 0; @@ -30,6 +43,7 @@ static inline BOOL K32GetProcessMemoryInfo(HANDLE p, PROCESS_MEMORY_COUNTERS *ou out->cb = cb, out->WorkingSetSize = rss, out->PeakWorkingSetSize = hwm, out->PagefileUsage = anon; if (cb >= sizeof(PROCESS_MEMORY_COUNTERS_EX)) ((PROCESS_MEMORY_COUNTERS_EX *)out)->PrivateUsage = anon; return TRUE; +#endif } #define GetProcessMemoryInfo K32GetProcessMemoryInfo #endif diff --git a/host/posix/win32_posix.c b/host/posix/win32_posix.c index 8284b9a0..a88a1162 100644 --- a/host/posix/win32_posix.c +++ b/host/posix/win32_posix.c @@ -1,4 +1,4 @@ -/* (linux) The Win32 calls of host/posix/windows.h, on POSIX. Built into the executable and, on its own, into +/* The Win32 calls of host/posix/windows.h, on POSIX. Built into the executable and, on its own, into hookshot_vk.so (each its own copy, -Bsymbolic: the executable exports nothing of it). */ #define _GNU_SOURCE #include @@ -9,12 +9,17 @@ #include #include #include -#include #include #include -#include #include #include +#ifdef __linux__ +#include +#include +#else +#include +#include +#endif /* the guest window (third_party/a64-kit/rt/a64.h A64_HOST_BASE, host/hos.h HOS_GUEST_SPACE): one mapping, all of it readable and writable, which VirtualQuery reports as committed without asking the kernel */ @@ -61,7 +66,9 @@ static struct timespec abs_after_ms(DWORD ms) { static void monitor_init(pthread_mutex_t *mu, pthread_cond_t *cv) { pthread_condattr_t a; pthread_condattr_init(&a); +#ifdef __linux__ pthread_condattr_setclock(&a, CLOCK_MONOTONIC); +#endif pthread_cond_init(cv, &a); pthread_condattr_destroy(&a); pthread_mutex_init(mu, NULL); @@ -79,26 +86,66 @@ static void monitor_init(pthread_mutex_t *mu, pthread_cond_t *cv) { /* ---- futexes: condition variables, WaitOnAddress ---- */ static long futex(volatile uint32_t *a, int op, uint32_t v, const struct timespec *ts) { +#ifdef __linux__ return syscall(SYS_futex, a, op, v, ts, NULL, 0); +#else + (void)a, (void)op, (void)v, (void)ts; + return -1; +#endif } static struct timespec rel_ms(DWORD ms) { struct timespec ts = {(time_t)(ms / 1000), (long)(ms % 1000) * 1000000L}; return ts; } +#ifndef __linux__ +static pthread_mutex_t posix_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t posix_cv = PTHREAD_COND_INITIALIZER; +#endif void WakeConditionVariable(CONDITION_VARIABLE *cv) { __atomic_add_fetch(&cv->seq, 1, __ATOMIC_SEQ_CST); +#ifdef __linux__ futex(&cv->seq, FUTEX_WAKE_PRIVATE, 1, NULL); +#else + (void)cv; + pthread_mutex_lock(&posix_mu); + pthread_cond_signal(&posix_cv); + pthread_mutex_unlock(&posix_mu); +#endif } void WakeAllConditionVariable(CONDITION_VARIABLE *cv) { __atomic_add_fetch(&cv->seq, 1, __ATOMIC_SEQ_CST); +#ifdef __linux__ futex(&cv->seq, FUTEX_WAKE_PRIVATE, INT_MAX, NULL); +#else + (void)cv; + pthread_mutex_lock(&posix_mu); + pthread_cond_broadcast(&posix_cv); + pthread_mutex_unlock(&posix_mu); +#endif } static BOOL cv_wait(CONDITION_VARIABLE *cv, DWORD ms, void (*unlock)(void *), void (*relock)(void *, ULONG), void *l, ULONG flags) { const uint32_t seq = __atomic_load_n(&cv->seq, __ATOMIC_SEQ_CST); unlock(l); +#ifdef __linux__ struct timespec ts = rel_ms(ms); const long r = futex(&cv->seq, FUTEX_WAIT_PRIVATE, seq, ms == INFINITE ? NULL : &ts); const int timed_out = r == -1 && errno == ETIMEDOUT; +#else + pthread_mutex_lock(&posix_mu); + int timed_out = 0; + if (ms == INFINITE) { + while (__atomic_load_n(&cv->seq, __ATOMIC_SEQ_CST) == seq) pthread_cond_wait(&posix_cv, &posix_mu); + } else { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += ms / 1000; + ts.tv_nsec += (long)(ms % 1000) * 1000000L; + if (ts.tv_nsec >= 1000000000L) ts.tv_sec++, ts.tv_nsec -= 1000000000L; + while (__atomic_load_n(&cv->seq, __ATOMIC_SEQ_CST) == seq && + pthread_cond_timedwait(&posix_cv, &posix_mu, &ts) != 0) timed_out = 1; + } + pthread_mutex_unlock(&posix_mu); +#endif relock(l, flags); if (timed_out) { last_error = ERROR_TIMEOUT; return FALSE; } return TRUE; @@ -125,6 +172,7 @@ void InitializeCriticalSection(CRITICAL_SECTION *cs) { } /* (any size: the futex is the aligned 32-bit word holding the address, woken by any change near it) */ BOOL WaitOnAddress(volatile void *addr, void *compare, SIZE_T size, DWORD ms) { +#ifdef __linux__ volatile uint32_t *w = (volatile uint32_t *)((uintptr_t)addr & ~(uintptr_t)3); const uint32_t seen = *w; if (memcmp((const void *)addr, compare, size)) return TRUE; @@ -132,9 +180,41 @@ BOOL WaitOnAddress(volatile void *addr, void *compare, SIZE_T size, DWORD ms) { const long r = futex(w, FUTEX_WAIT_PRIVATE, seen, ms == INFINITE ? NULL : &ts); if (r == -1 && errno == ETIMEDOUT) { last_error = ERROR_TIMEOUT; return FALSE; } return TRUE; +#else + pthread_mutex_lock(&posix_mu); + struct timespec now; + clock_gettime(CLOCK_REALTIME, &now); + const int64_t until = ms == INFINITE ? INT64_MAX : + (int64_t)now.tv_sec * 1000000000LL + now.tv_nsec + (int64_t)ms * 1000000; + while (!memcmp((const void *)addr, compare, size)) { + if (ms == 0 || now_ns() >= until) { + pthread_mutex_unlock(&posix_mu); + last_error = ERROR_TIMEOUT; + return FALSE; + } + struct timespec ts = {(time_t)(until / 1000000000), (long)(until % 1000000000)}; + if (pthread_cond_timedwait(&posix_cv, &posix_mu, &ts) == ETIMEDOUT) break; + } + pthread_mutex_unlock(&posix_mu); + return TRUE; +#endif +} +void WakeByAddressSingle(void *addr) { +#ifdef __linux__ + futex((volatile uint32_t *)((uintptr_t)addr & ~(uintptr_t)3), FUTEX_WAKE_PRIVATE, INT_MAX, NULL); +#else + (void)addr; + pthread_cond_signal(&posix_cv); +#endif +} +void WakeByAddressAll(void *addr) { +#ifdef __linux__ + futex((volatile uint32_t *)((uintptr_t)addr & ~(uintptr_t)3), FUTEX_WAKE_PRIVATE, INT_MAX, NULL); +#else + (void)addr; + pthread_cond_broadcast(&posix_cv); +#endif } -void WakeByAddressSingle(void *addr) { futex((volatile uint32_t *)((uintptr_t)addr & ~(uintptr_t)3), FUTEX_WAKE_PRIVATE, INT_MAX, NULL); } -void WakeByAddressAll(void *addr) { futex((volatile uint32_t *)((uintptr_t)addr & ~(uintptr_t)3), FUTEX_WAKE_PRIVATE, INT_MAX, NULL); } /* ---- time ---- */ void Sleep(DWORD ms) { @@ -178,6 +258,15 @@ typedef struct { char name[64]; } HThread; static __thread HThread *self_thread; +static pid_t posix_thread_id(void) { +#ifdef __linux__ + return (pid_t)syscall(SYS_gettid); +#else + uint64_t id = 0; + pthread_threadid_np(NULL, &id); + return (pid_t)id; +#endif +} static void thread_unref(HThread *t) { if (__atomic_sub_fetch(&t->refs, 1, __ATOMIC_ACQ_REL) == 0) { pthread_mutex_destroy(&t->mu); @@ -195,7 +284,7 @@ static HThread *thread_self(void) { if (!self_thread) { /* (a thread this file did not start: the main thread, a library's) */ HThread *t = thread_new(); t->th = pthread_self(); - t->tid = (pid_t)syscall(SYS_gettid); + t->tid = posix_thread_id(); t->refs = 1; /* (kept for the thread's life) */ self_thread = t; } @@ -219,7 +308,7 @@ static void thread_finish(DWORD code) { static void *thread_entry(void *p) { HThread *t = (HThread *)p; self_thread = t; - t->tid = (pid_t)syscall(SYS_gettid); + t->tid = posix_thread_id(); thread_finish(t->fn(t->arg)); return NULL; } @@ -282,6 +371,7 @@ int GetThreadPriority(HANDLE h) { return t ? t->priority : 0; } DWORD_PTR SetThreadAffinityMask(HANDLE h, DWORD_PTR mask) { +#ifdef __linux__ HThread *t = thread_of(h); if (!t || !mask) { last_error = ERROR_INVALID_PARAMETER; return 0; } cpu_set_t old, want; @@ -293,8 +383,14 @@ DWORD_PTR SetThreadAffinityMask(HANDLE h, DWORD_PTR mask) { DWORD_PTR r = 0; for (int i = 0; i < 64; i++) if (CPU_ISSET(i, &old)) r |= (DWORD_PTR)1 << i; return r ? r : 1; +#else + (void)h, (void)mask; + last_error = ERROR_NOT_SUPPORTED; + return 0; +#endif } BOOL GetProcessAffinityMask(HANDLE p, DWORD_PTR *proc, DWORD_PTR *sys) { +#ifdef __linux__ (void)p; cpu_set_t s; CPU_ZERO(&s); @@ -305,6 +401,13 @@ BOOL GetProcessAffinityMask(HANDLE p, DWORD_PTR *proc, DWORD_PTR *sys) { *proc = m; *sys = n >= 64 ? ~(DWORD_PTR)0 : (((DWORD_PTR)1 << n) - 1); return TRUE; +#else + (void)p; + long n = sysconf(_SC_NPROCESSORS_ONLN); + *proc = n >= 64 ? ~(DWORD_PTR)0 : (((DWORD_PTR)1 << n) - 1); + *sys = *proc; + return TRUE; +#endif } int WideCharToMultiByte(UINT cp, DWORD flags, LPCWSTR w, int n, LPSTR s, int cap, LPCSTR def, BOOL *used) { (void)cp, (void)flags, (void)def; @@ -354,7 +457,11 @@ HRESULT SetThreadDescription(HANDLE h, PCWSTR name) { t->name[sizeof t->name - 1] = 0; char shortname[16]; snprintf(shortname, sizeof shortname, "%s", t->name); /* (the kernel keeps 15 characters) */ +#ifdef __linux__ pthread_setname_np(t->th, shortname); +#else + if (t->th == pthread_self()) pthread_setname_np(shortname); +#endif return S_OK; } HRESULT GetThreadDescription(HANDLE h, PWSTR *name) { @@ -374,7 +481,13 @@ BOOL GetThreadTimes(HANDLE h, FILETIME *create, FILETIME *exit_, FILETIME *kerne HThread *t = thread_of(h); clockid_t clk; struct timespec ts; - if (!t || t->done || pthread_getcpuclockid(t->th, &clk) || clock_gettime(clk, &ts)) { last_error = ERROR_INVALID_HANDLE; return FALSE; } + if (!t || t->done +#ifdef __linux__ + || pthread_getcpuclockid(t->th, &clk) || clock_gettime(clk, &ts) +#else + || clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts) +#endif + ) { last_error = ERROR_INVALID_HANDLE; return FALSE; } filetime_ns(create, 0), filetime_ns(exit_, 0), filetime_ns(kernel, 0); filetime_ns(user, (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec); return TRUE; @@ -478,7 +591,12 @@ DWORD WaitForSingleObject(HANDLE h, DWORD ms) { } else if (is(h, H_TIMER)) { const int64_t due = ((HTimer *)h)->due; struct timespec ts = {(time_t)(due / 1000000000LL), (long)(due % 1000000000LL)}; - while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL) == EINTR) {} + for (;;) { + const int64_t left = (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec - now_ns(); + if (left <= 0) break; + struct timespec nap = {(time_t)(left / 1000000000LL), (long)(left % 1000000000LL)}; + nanosleep(&nap, NULL); + } r = WAIT_OBJECT_0; } else { last_error = ERROR_INVALID_HANDLE; @@ -534,7 +652,14 @@ void *CreateFiberEx(SIZE_T commit, SIZE_T reserve, DWORD flags, LPFIBER_START_RO size_t size = reserve > commit ? reserve : commit; if (size < (1u << 20)) size = 1u << 20; size = (size + 0xFFFF) & ~(size_t)0xFFFF; - void *stack = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_STACK, -1, 0); + int map_flags = MAP_PRIVATE | MAP_ANONYMOUS; +#ifdef __linux__ + map_flags |= MAP_NORESERVE; +#ifdef MAP_STACK + map_flags |= MAP_STACK; +#endif +#endif + void *stack = mmap(NULL, size, PROT_READ | PROT_WRITE, map_flags, -1, 0); if (stack == MAP_FAILED) { last_error = ERROR_NOT_ENOUGH_MEMORY; return NULL; } mprotect(stack, 4096, PROT_NONE); /* (a guard page at the bottom) */ Fiber *f = (Fiber *)calloc(1, sizeof *f); @@ -606,7 +731,11 @@ BOOL GetLogicalProcessorInformation(SYSTEM_LOGICAL_PROCESSOR_INFORMATION *info, } void GetSystemInfo(SYSTEM_INFO *si) { memset(si, 0, sizeof *si); - si->wProcessorArchitecture = 9; /* (PROCESSOR_ARCHITECTURE_AMD64) */ +#if defined(__aarch64__) + si->wProcessorArchitecture = 12; /* PROCESSOR_ARCHITECTURE_ARM64 */ +#else + si->wProcessorArchitecture = 9; /* PROCESSOR_ARCHITECTURE_AMD64 */ +#endif si->dwPageSize = (DWORD)sysconf(_SC_PAGESIZE); si->dwAllocationGranularity = 0x10000; si->dwNumberOfProcessors = (DWORD)sysconf(_SC_NPROCESSORS_ONLN); @@ -646,7 +775,12 @@ void *VirtualAlloc(void *addr, SIZE_T size, DWORD type, DWORD protect) { return addr; } const int prot = (type & MEM_COMMIT) ? prot_of(protect) : PROT_NONE; - void *p = mmap(addr, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | (addr ? MAP_FIXED_NOREPLACE : 0), -1, 0); + int flags = MAP_PRIVATE | MAP_ANONYMOUS; +#ifdef __linux__ + flags |= MAP_NORESERVE; + if (addr) flags |= MAP_FIXED_NOREPLACE; +#endif + void *p = mmap(addr, size, prot, flags, -1, 0); if (p == MAP_FAILED || (addr && p != addr)) { if (p != MAP_FAILED) munmap(p, size); last_error = ERROR_INVALID_PARAMETER; @@ -724,12 +858,24 @@ SIZE_T VirtualQuery(const void *addr, MEMORY_BASIC_INFORMATION *mbi, SIZE_T size /* ---- modules ---- */ static void exe_path(char *out, size_t cap) { +#ifdef __linux__ const ssize_t n = readlink("/proc/self/exe", out, cap - 1); out[n > 0 ? n : 0] = 0; +#else + uint32_t n = (uint32_t)cap; + if (_NSGetExecutablePath(out, &n) != 0) out[0] = 0; +#endif } /* a Windows library name, as the Linux one: SDL2.dll -> libSDL2-2.0.so.0, vulkan-1.dll -> libvulkan.so.1, x.dll -> x.so */ -static const char *linux_name(const char *base) { - static const char *map[][2] = {{"sdl2.dll", "libSDL2-2.0.so.0"}, {"vulkan-1.dll", "libvulkan.so.1"}}; +static const char *posix_name(const char *base) { + static const char *map[][2] = { + {"sdl2.dll", "libSDL2-2.0.0.dylib"}, + {"vulkan-1.dll", "libvulkan.dylib"} + }; +#ifdef __linux__ + map[0][1] = "libSDL2-2.0.so.0"; + map[1][1] = "libvulkan.so.1"; +#endif for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) if (!strcasecmp(base, map[i][0])) return map[i][1]; return NULL; @@ -741,7 +887,7 @@ HMODULE LoadLibraryA(LPCSTR name) { for (char *p = path; *p; p++) if (*p == '\\') *p = '/'; char *slash = strrchr(path, '/'); char *base = slash ? slash + 1 : path; - const char *mapped = linux_name(base); + const char *mapped = posix_name(base); if (mapped) { snprintf(base, sizeof path - (size_t)(base - path), "%s", mapped); } else { @@ -752,6 +898,9 @@ HMODULE LoadLibraryA(LPCSTR name) { } } HMODULE m = dlopen(path, RTLD_NOW | RTLD_LOCAL); +#ifdef __APPLE__ + if (!m && !strcasecmp(base, "libvulkan.dylib")) m = dlopen("libMoltenVK.dylib", RTLD_NOW | RTLD_LOCAL); +#endif if (!m) last_error = ERROR_FILE_NOT_FOUND; return m; } @@ -1070,20 +1219,53 @@ static struct sigaction previous[32]; static pthread_mutex_t handlers_mu = PTHREAD_MUTEX_INITIALIZER; static void context_from(CONTEXT *c, const ucontext_t *uc) { +#ifdef __APPLE__ +#if defined(__aarch64__) + const uint64_t *x = uc->uc_mcontext->__ss.__x; + c->Rax = x[0], c->Rcx = x[1], c->Rdx = x[2], c->Rbx = x[3]; + c->Rsp = uc->uc_mcontext->__ss.__sp, c->Rbp = x[29], c->Rsi = x[4], c->Rdi = x[5]; + c->R8 = x[6], c->R9 = x[7], c->R10 = x[8], c->R11 = x[9], c->R12 = x[10], c->R13 = x[11], c->R14 = x[12], c->R15 = x[13]; + c->Rip = uc->uc_mcontext->__ss.__pc, c->EFlags = uc->uc_mcontext->__ss.__cpsr; +#else + const __darwin_x86_thread_state64_t *s = &uc->uc_mcontext->__ss; + c->Rax = s->__rax, c->Rcx = s->__rcx, c->Rdx = s->__rdx, c->Rbx = s->__rbx; + c->Rsp = s->__rsp, c->Rbp = s->__rbp, c->Rsi = s->__rsi, c->Rdi = s->__rdi; + c->R8 = s->__r8, c->R9 = s->__r9, c->R10 = s->__r10, c->R11 = s->__r11, c->R12 = s->__r12, c->R13 = s->__r13; + c->R14 = s->__r14, c->R15 = s->__r15, c->Rip = s->__rip, c->EFlags = s->__rflags; +#endif +#else const greg_t *g = uc->uc_mcontext.gregs; c->Rax = (DWORD64)g[REG_RAX], c->Rcx = (DWORD64)g[REG_RCX], c->Rdx = (DWORD64)g[REG_RDX], c->Rbx = (DWORD64)g[REG_RBX]; c->Rsp = (DWORD64)g[REG_RSP], c->Rbp = (DWORD64)g[REG_RBP], c->Rsi = (DWORD64)g[REG_RSI], c->Rdi = (DWORD64)g[REG_RDI]; c->R8 = (DWORD64)g[REG_R8], c->R9 = (DWORD64)g[REG_R9], c->R10 = (DWORD64)g[REG_R10], c->R11 = (DWORD64)g[REG_R11]; c->R12 = (DWORD64)g[REG_R12], c->R13 = (DWORD64)g[REG_R13], c->R14 = (DWORD64)g[REG_R14], c->R15 = (DWORD64)g[REG_R15]; c->Rip = (DWORD64)g[REG_RIP], c->EFlags = (DWORD)g[REG_EFL], c->ContextFlags = CONTEXT_FULL; +#endif + c->ContextFlags = CONTEXT_FULL; } static void context_to(ucontext_t *uc, const CONTEXT *c) { +#ifdef __APPLE__ +#if defined(__aarch64__) + uint64_t *x = uc->uc_mcontext->__ss.__x; + x[0] = c->Rax, x[1] = c->Rcx, x[2] = c->Rdx, x[3] = c->Rbx; + uc->uc_mcontext->__ss.__sp = c->Rsp, x[29] = c->Rbp, x[4] = c->Rsi, x[5] = c->Rdi; + x[6] = c->R8, x[7] = c->R9, x[8] = c->R10, x[9] = c->R11, x[10] = c->R12, x[11] = c->R13, x[12] = c->R14, x[13] = c->R15; + uc->uc_mcontext->__ss.__pc = c->Rip, uc->uc_mcontext->__ss.__cpsr = c->EFlags; +#else + __darwin_x86_thread_state64_t *s = &uc->uc_mcontext->__ss; + s->__rax = c->Rax, s->__rcx = c->Rcx, s->__rdx = c->Rdx, s->__rbx = c->Rbx; + s->__rsp = c->Rsp, s->__rbp = c->Rbp, s->__rsi = c->Rsi, s->__rdi = c->Rdi; + s->__r8 = c->R8, s->__r9 = c->R9, s->__r10 = c->R10, s->__r11 = c->R11, s->__r12 = c->R12, s->__r13 = c->R13; + s->__r14 = c->R14, s->__r15 = c->R15, s->__rip = c->Rip, s->__rflags = c->EFlags; +#endif +#else greg_t *g = uc->uc_mcontext.gregs; g[REG_RAX] = (greg_t)c->Rax, g[REG_RCX] = (greg_t)c->Rcx, g[REG_RDX] = (greg_t)c->Rdx, g[REG_RBX] = (greg_t)c->Rbx; g[REG_RSP] = (greg_t)c->Rsp, g[REG_RBP] = (greg_t)c->Rbp, g[REG_RSI] = (greg_t)c->Rsi, g[REG_RDI] = (greg_t)c->Rdi; g[REG_R8] = (greg_t)c->R8, g[REG_R9] = (greg_t)c->R9, g[REG_R10] = (greg_t)c->R10, g[REG_R11] = (greg_t)c->R11; g[REG_R12] = (greg_t)c->R12, g[REG_R13] = (greg_t)c->R13, g[REG_R14] = (greg_t)c->R14, g[REG_R15] = (greg_t)c->R15; g[REG_RIP] = (greg_t)c->Rip; +#endif } static void on_signal(int sig, siginfo_t *si, void *uctx) { ucontext_t *uc = (ucontext_t *)uctx; @@ -1097,7 +1279,11 @@ static void on_signal(int sig, siginfo_t *si, void *uctx) { rec.ExceptionAddress = (void *)(uintptr_t)ctx.Rip; if (sig == SIGSEGV || sig == SIGBUS) { rec.NumberParameters = 2; - rec.ExceptionInformation[0] = (uc->uc_mcontext.gregs[REG_ERR] & 2) ? 1 : 0; /* (1: a write) */ +#ifdef __linux__ + rec.ExceptionInformation[0] = (uc->uc_mcontext.gregs[REG_ERR] & 2) ? 1 : 0; +#else + rec.ExceptionInformation[0] = 0; +#endif rec.ExceptionInformation[1] = (ULONG_PTR)si->si_addr; } EXCEPTION_POINTERS ep = {&rec, &ctx}; diff --git a/host/posix/windows.h b/host/posix/windows.h index b351be6f..1ab77f27 100644 --- a/host/posix/windows.h +++ b/host/posix/windows.h @@ -147,7 +147,11 @@ void SetLastError(DWORD e); #define InterlockedCompareExchangePointer InterlockedCompareExchange #define MemoryBarrier() __atomic_thread_fence(__ATOMIC_SEQ_CST) #define _ReadWriteBarrier() __atomic_signal_fence(__ATOMIC_SEQ_CST) +#ifdef __x86_64__ #define YieldProcessor() __builtin_ia32_pause() +#else +#define YieldProcessor() __asm__ volatile("yield") +#endif /* ---- locks: SRW locks are POSIX rwlocks (zero-initialised is valid, as on Windows); condition variables a futex word, so they wait with either an SRW lock or a critical section ---- */ diff --git a/nx2/build.py b/nx2/build.py index 13974c5b..0727f508 100644 --- a/nx2/build.py +++ b/nx2/build.py @@ -48,11 +48,13 @@ # (host/posix: its windows.h is found first), the window and sound on SDL2. HOOKSHOT_SYSROOT=: compile and link # against that root (the AppImage's: glibc 2.35, so the game runs on older systems than the one that built it). # HOOKSHOT_VK_PREBUILT=: the Vulkan back end as built already (the AppImage's), not compiled here. -LINUX = os.name != "nt" +LINUX = sys.platform.startswith("linux") +MACOS = sys.platform == "darwin" +POSIX_BUILD = LINUX or MACOS POSIX = os.path.join(kit.HERE, "..", "host", "posix") SYSROOT = os.environ.get("HOOKSHOT_SYSROOT", "") -EXE = "hookshot" if LINUX else "hookshot.exe" -VK_LIB = "hookshot_vk.so" if LINUX else "hookshot_vk.dll" +EXE = "hookshot" if POSIX_BUILD else "hookshot.exe" +VK_LIB = "hookshot_vk.dylib" if MACOS else ("hookshot_vk.so" if LINUX else "hookshot_vk.dll") def target(): @@ -86,7 +88,7 @@ def HOT_ORDER(): return [toolchain.link_opt("/order:@" + p.replace("\\", "/")), toolchain.link_opt("/ignore:4037")] -HOST_LIBS = ["-lpthread", "-ldl", "-lm"] if LINUX else \ +HOST_LIBS = (["-lpthread", "-ldl", "-lm"] if LINUX else ["-lpthread", "-lm"]) if POSIX_BUILD else \ ["-lwinhttp", "-luser32", "-lwinmm", "-ladvapi32", "-lole32", "-lshell32", "-lgdi32", "-ldbghelp"] GPU = os.path.join(kit.HERE, "..", "gpu") # FFmpeg (libavcodec's VP8/VP9 decoders, swscale) for nn::codec's video decoders (host/nn_video.c): the static build @@ -198,7 +200,7 @@ def ffmpeg_linux(): return None -if LINUX: +if POSIX_BUILD: FFMPEG_LX = ffmpeg_linux() FFMPEG = None @@ -209,7 +211,7 @@ def ffmpeg_version_key(): incs = [] if FFMPEG: incs.append(os.path.join(FFMPEG, "include")) - if LINUX and FFMPEG_LX: + if POSIX_BUILD and FFMPEG_LX: incs += [f[2:] for f in FFMPEG_LX[0] if f.startswith("-I")] incs.append("/usr/include") for inc in incs: @@ -245,6 +247,9 @@ def host_march(): if pf(40): return "x86-64-v3" return "x86-64-v2" if pf(38) else "x86-64" + if MACOS: + import platform + return "apple-m1" if platform.machine() in ("arm64", "aarch64") else "x86-64-v3" flags = open("/proc/cpuinfo").read().split("flags", 1)[1].split(chr(10), 1)[0].split() if all(f in flags for f in want): return "x86-64-v3" @@ -256,13 +261,14 @@ def host_march(): MARCH = os.environ.get("RX_MARCH") or host_march() if MARCH != "x86-64-v3": print("build: this CPU has no AVX2: the game is built for %s (a little slower)" % MARCH, flush=True) -CPU = ["-march=" + MARCH, "-ffp-contract=off"] +CPU = [("-mcpu=" if MACOS and MARCH == "apple-m1" else "-march=") + MARCH, "-ffp-contract=off"] +MCX16 = [] if MACOS and MARCH == "apple-m1" else ["-mcx16"] def flags(gen, rt64, opt, polls): return target() + CPU + [ "-I" + rt64, "-I" + gen, "-I" + os.path.join(kit.KIT, "rt"), "-I" + NX2_RT, "-Wno-everything", - "-fno-strict-aliasing", "-mcx16", "-" + opt, "-g0", "-mllvm", "-rotation-max-header-size=0", + "-fno-strict-aliasing", *MCX16, "-" + opt, "-g0", "-mllvm", "-rotation-max-header-size=0", "-ffunction-sections", "-mllvm", "-capture-tracking-max-uses-to-explore=5000"] + \ (["-DA64_POLLING"] if polls else []) + WRITE_TRACK @@ -385,9 +391,10 @@ def main(): # its own stand-ins, host/posix/linux_stubs.c) have_wgpu = os.path.isfile(os.path.join(WGPU, "include", "webgpu", "webgpu.h")) host_srcs = sorted(os.path.join(HOST, f) for f in os.listdir(HOST) if f.endswith(".c") and (f != "pgo_rt.c" or PGO == "gen") - and not (LINUX and f in ("display.c", "mf_av1.c", "rx_nowgpu.c")) and (f != "rx_nowgpu.c" or not have_wgpu)) - if LINUX: # (the window on SDL2, the Win32 calls on POSIX, no Media Foundation and no WebGPU renderer) - host_srcs += [os.path.join(POSIX, f) for f in ("win32_posix.c", "display_sdl.c", "linux_stubs.c", "prof.c")] + and not (POSIX_BUILD and f in ("display.c", "mf_av1.c", "rx_nowgpu.c")) and (f != "rx_nowgpu.c" or not have_wgpu)) + if POSIX_BUILD: # (the window on SDL2, the Win32 calls on POSIX, no Media Foundation and no WebGPU renderer) + host_srcs += [os.path.join(POSIX, f) for f in + ("win32_posix.c", "display_sdl.c", "linux_stubs.c", "prof.c" if LINUX else "prof_stub.c")] else: srcs += [os.path.join(NX2_RT, "eden_shim.c"), write_info(out)] if os.environ.get("RX_VK_ONLY"): # (no lift needed: the back end alone) @@ -407,18 +414,18 @@ def main(): hot_fl if os.path.basename(s).startswith("code_hot") else fl, shim_deps if os.path.dirname(s) == NX2_RT else deps) for s in srcs] # the runtime: ordinary C at -O2, with its own headers (any host/*.h change rebuilds all of it) - host_fl = target() + CPU + (["-I" + POSIX, "-D_GNU_SOURCE"] if LINUX else []) + \ - ["-I" + HOST, "-I" + rt64, "-I" + gen, "-Wno-everything", "-O2", "-g0", "-mcx16", "-DA64_POLLING"] + WRITE_TRACK + host_fl = target() + CPU + (["-I" + POSIX] + (["-D_GNU_SOURCE"] if LINUX else []) if POSIX_BUILD else []) + \ + ["-I" + HOST, "-I" + rt64, "-I" + gen, "-Wno-everything", "-O2", "-g0", *MCX16, "-DA64_POLLING"] + WRITE_TRACK host_deps = deps + b"".join(open(os.path.join(HOST, f), "rb").read() for f in sorted(os.listdir(HOST)) if f.endswith(".h")) - if LINUX: + if POSIX_BUILD: host_deps += b"".join(open(os.path.join(POSIX, f), "rb").read() for f in sorted(os.listdir(POSIX)) if f.endswith(".h")) host_deps += open(os.path.join(GPU, "vk", "vk_api.h"), "rb").read() host_deps += ffmpeg_version_key() jobs += [(cc, s, os.path.join(obj_dir, "host_" + os.path.basename(s)[:-2] + ".o"), host_fl + ["-I" + os.path.join(OPUS, "include")] + (["-I" + os.path.join(FFMPEG, "include")] if FFMPEG else []) + - (FFMPEG_LX[0] if LINUX and FFMPEG_LX else []) + + (FFMPEG_LX[0] if POSIX_BUILD and FFMPEG_LX else []) + ([stamp_def] if os.path.basename(s) == "hos_main.c" else []), host_deps) for s in host_srcs] # libopus 1.5.2 (third_party/opus, from TOTKPort): nn::codec's hardware Opus decoder (host/nn_codec.c) if host_srcs: @@ -428,8 +435,8 @@ def main(): jobs.append((cc, os.path.join(OPUS, rel), os.path.join(obj_dir, "opus_" + rel.replace("/", "_")[:-2] + ".o"), oflags, b"opus-1.5.2")) # the renderer (C++20, NDEBUG: no asserts in it or suyu), when a wgpu-native release is there - wgpu = a.standalone and not a.only and not LINUX and os.path.isfile(os.path.join(WGPU, "include", "webgpu", "webgpu.h")) - if a.standalone and not a.only and not wgpu and not LINUX: + wgpu = a.standalone and not a.only and not POSIX_BUILD and os.path.isfile(os.path.join(WGPU, "include", "webgpu", "webgpu.h")) + if a.standalone and not a.only and not wgpu and not POSIX_BUILD: print("no wgpu-native at %s (WGPU_NATIVE): building without the renderer" % WGPU) if wgpu: cxx = toolchain.need(toolchain.find("clang++")) @@ -457,7 +464,7 @@ def main(): for f in sorted(translator) + [os.path.join(vk, n) for n in ("vk_shader.cpp", "vk_state.h", "vk_api.h")]: with open(f, "rb") as src: stamp.update(os.path.relpath(f, vk).replace("\\", "/").encode() + src.read().replace(bytes((13, 10)), bytes((10,)))) - vflags = target() + CPU + (["-fPIC", "-I" + POSIX, "-D_GNU_SOURCE"] if LINUX else []) + [ # (hookshot: the replay is the frame's limit now) + vflags = target() + CPU + (["-fPIC", "-I" + POSIX] + (["-D_GNU_SOURCE"] if LINUX else []) if POSIX_BUILD else []) + [ # (hookshot: the replay is the frame's limit now) "-DHOOKSHOT_SHADER_STAMP=0x%sull" % stamp.hexdigest()[:16], # (what the code takes for granted, which newer C++ libraries bring in by themselves and older ones do not: issue 48) "-include", "algorithm", "-include", "cstdio", "-include", "cstdlib", "-include", "cstring", "-include", "functional", @@ -485,8 +492,8 @@ def main(): vk_jobs.append((cxx, src, os.path.join(obj_dir, "vk_" + name + ".o"), vflags + ([stamp_def] if os.path.basename(src) == "vk_menu.cpp" else []), vk_lib_deps if lib else vdeps)) - if LINUX: # (its own copy of the Win32 calls on POSIX, and the bit of GDI the Insert menu draws text with) - cflags = target() + CPU + ["-fPIC", "-O2", "-g0", "-Wno-everything", "-D_GNU_SOURCE", "-I" + POSIX, + if POSIX_BUILD: # (its own copy of the Win32 calls on POSIX, and the bit of GDI the Insert menu draws text with) + cflags = target() + CPU + ["-fPIC", "-O2", "-g0", "-Wno-everything"] + (["-D_GNU_SOURCE"] if LINUX else []) + ["-I" + POSIX, "-I" + os.path.join(kit.HERE, "..", "third_party", "stb")] pdeps = tree_headers(POSIX) for f in ("win32_posix.c", "gdi_posix.c"): @@ -518,12 +525,13 @@ def main(): lib = os.path.join(out, "hookshot_game.lib") subprocess.run([toolchain.lld_link(), "/lib", "/out:" + lib, "@" + rsp], check=True) print("wrote %s (%d objects, %d MB)" % (lib, len(objs), os.path.getsize(lib) >> 20)) - if a.standalone and not a.only and not vk_only and LINUX: + if a.standalone and not a.only and not vk_only and POSIX_BUILD: exe = os.path.join(out, EXE) # (+ hookshot.map: crash offsets -> f_) t = time.time() # (--export-dynamic-symbol: the window's state, which the Vulkan back end's own Win32 calls read by name) - r = subprocess.run([cc] + target() + ["-fuse-ld=lld", "-o", exe, "-Wl,-Map=" + exe + ".map", "-Wl,--gc-sections", - "-Wl,--export-dynamic-symbol=hos_posix_*", "@" + rsp] + HOT_ORDER() + + r = subprocess.run([cc] + target() + (["-fuse-ld=lld"] if LINUX else []) + + ["-o", exe, "-Wl,-Map=" + exe + ".map", "-Wl,--gc-sections", + "-Wl,--export-dynamic-symbol=hos_posix_*" if LINUX else "-Wl,-export_dynamic", "@" + rsp] + HOT_ORDER() + (["-fprofile-instr-generate"] if PGO == "gen" else []) + (FFMPEG_LX[1] if FFMPEG_LX else []) + HOST_LIBS, capture_output=True, text=True) @@ -533,10 +541,10 @@ def main(): print("\n".join(undefined[:60]) if undefined else (r.stdout + r.stderr)[-3000:]) return 1 print("linked %s: %d MB (%.0fs)" % (exe, os.path.getsize(exe) >> 20, time.time() - t)) - sdl = os.environ.get("HOOKSHOT_SDL2", "") # (libSDL2-2.0.so.0 beside it, when the AppImage brings one) + sdl = os.environ.get("HOOKSHOT_SDL2", "") if sdl and os.path.exists(sdl): import shutil - shutil.copyfile(sdl, os.path.join(out, "libSDL2-2.0.so.0")) + shutil.copyfile(sdl, os.path.join(out, os.path.basename(sdl))) elif a.standalone and not a.only and not vk_only: exe = os.path.join(out, "hookshot.exe") # (+ hookshot.map: crash offsets -> f_) t = time.time() @@ -582,10 +590,12 @@ def main(): vrsp = os.path.join(out, "vk_objs.rsp") open(vrsp, "w", encoding="utf-8").write("\n".join('"%s"' % j[2].replace("\\", "/") for j in vk_jobs)) t = time.time() - if LINUX: # (its C++ runtime inside: the game folder runs where the AppImage's sysroot is older than the system) + if POSIX_BUILD: link = [toolchain.need(toolchain.find("clang++"))] + target() + [ - "-fuse-ld=lld", "-shared", "-o", vdll, "-Wl,-Map=" + vdll + ".map", "-Wl,-Bsymbolic", "-Wl,--gc-sections", - "-static-libstdc++", "-static-libgcc", "@" + vrsp, "-ldl", "-lpthread", "-lm"] + *(["-fuse-ld=lld"] if LINUX else []), "-dynamiclib" if MACOS else "-shared", "-o", vdll, + "-Wl,-Map=" + vdll + ".map", "-Wl,--gc-sections", + *(["-Wl,-Bsymbolic", "-static-libstdc++", "-static-libgcc"] if LINUX else []), + "@" + vrsp, *HOST_LIBS] else: link = [cc] + toolchain.native_target() + toolchain.native_link() + \ ["-shared", "-o", vdll, toolchain.link_opt("/map:" + vdll[:-4] + ".map"), "@" + vrsp, "-luser32", "-lgdi32"] + (["-lc++", "-lunwind"] if toolchain.mingw() else []) diff --git a/nx2/make_game.py b/nx2/make_game.py index 7b23ae33..31b0e5c0 100644 --- a/nx2/make_game.py +++ b/nx2/make_game.py @@ -27,8 +27,12 @@ ROOT = os.path.dirname(HERE) PY = [sys.executable, "-I", "-u", "-X", "utf8"] # (utf8: every step's output in UTF-8 whatever the console's code page: lift.py crashed on a letter, #180) # (-u: the steps' output as it comes, not in 8 KB blocks minutes apart) BUILD_ID = "e0f3ccfa" # (the version the patches, the hot list and the profile were made from) -LINUX = os.name != "nt" -BESIDE = ["hookshot", "hookshot_vk.so", "libSDL2-2.0.so.0"] if LINUX else ["hookshot.exe", "hookshot_vk.dll", "SDL2.dll", "SDL3.dll", "wgpu_native.dll"] +LINUX = sys.platform.startswith("linux") +MACOS = sys.platform == "darwin" +POSIX = LINUX or MACOS +BESIDE = (["hookshot", "hookshot_vk.dylib", "libSDL2-2.0.0.dylib"] if MACOS else + ["hookshot", "hookshot_vk.so", "libSDL2-2.0.so.0"] if LINUX else + ["hookshot.exe", "hookshot_vk.dll", "SDL2.dll", "SDL3.dll", "wgpu_native.dll"]) LANGUAGES = [("en-US", "English (US)"), ("en-GB", "English (UK)"), ("fr", "French (France)"), ("fr-CA", "French (Canada)"), ("de", "German"), ("it", "Italian"), ("es", "Spanish (Spain)"), ("es-419", "Spanish (Latin America)"), ("pt-BR", "Portuguese (Brazil)"), ("nl", "Dutch"), ("pl", "Polish"), ("ru", "Russian"), ("ja", "Japanese"), ("ko", "Korean"), ("zh-Hans", "Chinese (Simplified)"), @@ -245,7 +249,7 @@ def check_cpp_library(toolchain): def check_linux_compiler(): - """(linux) A C compiler that builds and links a program here (the AppImage's own clang and sysroot, else the system's): + """A C compiler that builds and links a program here (the AppImage's own clang and sysroot on Linux): found now, not 40 minutes in""" sys.path.insert(0, os.path.join(ROOT, "third_party", "a64-kit", "tools")) import toolchain @@ -257,13 +261,15 @@ def check_linux_compiler(): src = os.path.join(d, "t.c") with open(src, "w") as f: f.write(chr(10).join(("#include ", "#include ", "int main(void) { return 0; }", ""))) - r = subprocess.run([cc] + (["--sysroot=" + sysroot] if sysroot else []) + ["-fuse-ld=lld", src, "-o", os.path.join(d, "t")], + r = subprocess.run([cc] + (["--sysroot=" + sysroot] if sysroot else []) + (["-fuse-ld=lld"] if LINUX else []) + + [src, "-o", os.path.join(d, "t")], capture_output=True, text=True, encoding="utf-8", errors="replace") if r.returncode: print(r.stdout + r.stderr, flush=True) errors = [l for l in r.stderr.splitlines() if "error" in l] - fail("The compiler could not build a test program: %s (install clang, lld and your system's C library headers: " - "libc6-dev on Debian and Ubuntu, glibc-devel on Fedora)" % (errors[0] if errors else "no message")) + fail("The compiler could not build a test program: %s (install clang%s and your system's C library headers: " + "libc6-dev on Debian and Ubuntu, glibc-devel on Fedora)" + % (errors[0] if errors else "no message", ", lld" if LINUX else "")) def free_memory_gb(): @@ -304,7 +310,7 @@ def default_jobs(): def bundled_mingw(): """(as TotK Recomp's installer) llvm-mingw in the tools folder: its clang, headers and libraries build the game, and nothing of Visual Studio or the Windows SDK is needed. HOOKSHOT_MINGW set already wins; HOOKSHOT_MSVC=1: the Visual Studio route""" - if LINUX or os.environ.get("HOOKSHOT_MSVC") == "1": + if POSIX or os.environ.get("HOOKSHOT_MSVC") == "1": return os.environ.get("HOOKSHOT_MINGW", "") if not os.environ.get("HOOKSHOT_MINGW"): here = os.path.join(ROOT, "llvm-mingw") @@ -342,7 +348,7 @@ def check_mingw(): def check_windows_sdk(): """The compiler is bundled; Microsoft's C library and Windows SDK may not be, by their licences. Found now, not 40 minutes in.""" - if LINUX: + if POSIX: return check_linux_compiler() if bundled_mingw(): return check_mingw() @@ -462,7 +468,7 @@ def folder_language(out): def profile_readable(prof): """(linux) the profile is made by a recent clang (the Windows tools'): an older one, the system's, cannot read it and stops the build. Then the game is built without it (a little slower in play), not at all""" - if not LINUX: + if not POSIX: return True sys.path.insert(0, os.path.join(ROOT, "third_party", "a64-kit", "tools")) import toolchain @@ -506,7 +512,7 @@ def main(): p.add_argument("--exefs") p.add_argument("--out") p.add_argument("--work", default=os.path.join(os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache"), "hookshot-builder") - if LINUX else os.path.join(os.environ.get("LOCALAPPDATA", ROOT), "HookshotBuilder")) + if POSIX else os.path.join(os.environ.get("LOCALAPPDATA", ROOT), "HookshotBuilder")) # (HOOKSHOT_JOBS: fewer compilers on a PC short of memory; the AppImage's builds pass no --jobs) p.add_argument("--jobs", type=int, default=int(os.environ.get("HOOKSHOT_JOBS") or 0) or default_jobs()) p.add_argument("--language", default="en-US", help="the game's language: a code from LANGUAGES, written to language.txt") @@ -530,7 +536,7 @@ def main(): os.makedirs(out, exist_ok=True) # (linux) the compiler probe waits until the compile. The game-free cache test stops at the lift, and a missing # compiler is still said before the long compile, not after the package has been read - if not LINUX: + if not POSIX: check_windows_sdk() drop_stale_objects(work) exefs = os.path.join(work, "exefs") @@ -610,7 +616,7 @@ def main(): if os.path.isfile(os.path.join(work, "sa", "gen", "code_hot.c")): run(PY + ["nx2/split_hot.py", os.path.join(work, "sa", "gen")], env) # (eight files side by side: ~6 minutes, not 79) - if LINUX: + if POSIX: check_linux_compiler() say("stage", "compile", "Compiling") done = {"n": 0, "of": 1} diff --git a/readme.md b/readme.md index 46273af5..dd1c52c2 100644 --- a/readme.md +++ b/readme.md @@ -9,7 +9,7 @@
-

The Legend of Zelda: Ocarina of Time (Nintendo Switch 2) running natively on Windows & Linux, with no emulator. +

The Legend of Zelda: Ocarina of Time (Nintendo Switch 2) running natively on Windows, Linux & macOS, with no emulator.

@@ -112,6 +112,22 @@ builds without it), Python 3 with `lz4`, `numpy`, `capstone`, `unicorn` and `zst with Vulkan-Headers, SPIRV-Headers and VulkanMemoryAllocator in CPM's layout (`scripts/linux/deps.sh` makes one). `scripts/linux/deps.sh` and `scripts/linux/appimage.sh` make the AppImage. +### On macOS + +macOS builds are supported on Intel and Apple silicon through the POSIX backend. Install Python 3, LLVM/Clang, FFmpeg, +SDL2 and a Vulkan loader with MoltenVK (Homebrew provides `python`, `llvm`, `ffmpeg`, `sdl2` and `molten-vk`), then set +`HOOKSHOT_FFMPEG` and `HOOKSHOT_SDL2` if they are not found by `pkg-config` or beside the output folder. + +Build from the command line with the same options as Linux: + +```sh +python3 -I nx2/make_game.py --nup ~/your-game.nup --out ~/Games/Hookshot --language en-US +./builder/macos/hookshot-launch ~/Games/Hookshot +``` + +The game executable is `hookshot`, and the renderer is `hookshot_vk.dylib`. A Vulkan 1.3-capable MoltenVK device is +required; macOS does not support the Linux AppImage, so its compiler and libraries must be installed locally. + ### Updating to a newer release A new release is not a patch to your game folder: the game is built again from your copy, with the new code. Your @@ -124,6 +140,7 @@ saves and settings are kept. What changed in each release is in [changelog.txt]( - **Hookshot Builder:** download the new HookshotBuilder zip and extract it to a new folder (with its `tools` folder). The old builder folder can then be deleted. - **Linux:** download the new AppImage and make it executable. The old one can be deleted, the game is not removed with it. + - **macOS:** pull the new source and build again with the same command. The game folder and saves are kept. - **Command line:** `git pull` in your clone, and take the `tools` folder's contents (see [What you need](#what-you-need)) from the new HookshotBuilder zip, as they can change between releases. 4. Build again with the same `.nup` (or `romfs` and `exefs` folders) and **the same game folder** as before. With the @@ -135,7 +152,7 @@ What a new build keeps and what it replaces in the game folder: | Kept | Replaced | |---|---| -| Your saves (`user`, or `~/.local/share/hookshot` on Linux) | `hookshot.exe` (`hookshot` on Linux) and its files | +| Your saves (`user`, or `~/.local/share/hookshot` on Linux, or `~/Library/Application Support/Hookshot` on macOS) | `hookshot.exe` (`hookshot` on Linux/macOS) and its files | | `hookshot_options.ini`, the in-game menu's choices | `image.bin` and the shader list | | `hookshot_env.txt` | `language.txt`, written again with the language you choose | From e24575d00f185c8c7316d5ef3258ebcb04687fc7 Mon Sep 17 00:00:00 2001 From: ytsodacan <103301261+ytsodacan@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:18:33 +0000 Subject: [PATCH 02/12] Add macOS CI workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/macos.yml | 76 +++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .github/workflows/macos.yml diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 00000000..2f51d7b3 --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,76 @@ +name: macOS checks + +on: + push: + branches: [main] + paths: + - host/** + - nx2/** + - third_party/a64-kit/** + - tests/** + - readme.md + - .github/workflows/macos.yml + pull_request: + branches: [main] + paths: + - host/** + - nx2/** + - third_party/a64-kit/** + - tests/** + - readme.md + - .github/workflows/macos.yml + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: macos-${{ github.ref }} + cancel-in-progress: true + +jobs: + checks: + runs-on: macos-14 + timeout-minutes: 20 + steps: + - name: Check out source + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: '3.12' + + - name: Install macOS dependencies + run: | + brew install ffmpeg llvm molten-vk sdl2 + python -m pip install --disable-pip-version-check \ + capstone lz4 numpy "unicorn>=2.1,<2.2" zstandard + + - name: Check macOS platform selection + run: | + python - <<'PY' + import importlib + import sys + sys.platform = "darwin" + build = importlib.import_module("nx2.build") + game = importlib.import_module("nx2.make_game") + assert build.POSIX_BUILD and build.MACOS and build.EXE == "hookshot" + assert build.VK_LIB == "hookshot_vk.dylib" + assert game.BESIDE[0] == "hookshot" + assert "hookshot_vk.dylib" in game.BESIDE + PY + + - name: Check Python sources + run: | + python -m py_compile nx2/build.py nx2/make_game.py + python -I -B nx2/test_nup_paths.py + python -I -B tests/test_make_game_cache.py + python -I -B tests/test_cpm_discovery.py + + - name: Check POSIX compatibility layer + run: | + "$(brew --prefix llvm)/bin/clang" -fsyntax-only -std=gnu11 \ + -Ihost/posix host/posix/win32_posix.c From bd808d22e953145ab1c65de2e2bc7dfa410966e6 Mon Sep 17 00:00:00 2001 From: ytsodacan <103301261+ytsodacan@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:21:10 +0000 Subject: [PATCH 03/12] Build macOS renderer in CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/macos.yml | 43 ++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 2f51d7b3..ef7e9a7d 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,4 +1,4 @@ -name: macOS checks +name: macOS build on: push: @@ -29,9 +29,9 @@ concurrency: cancel-in-progress: true jobs: - checks: + build: runs-on: macos-14 - timeout-minutes: 20 + timeout-minutes: 90 steps: - name: Check out source uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 @@ -70,7 +70,44 @@ jobs: python -I -B tests/test_make_game_cache.py python -I -B tests/test_cpm_discovery.py + - name: Prepare Vulkan headers + run: | + mkdir -p "$RUNNER_TEMP/cpm" + fetch() { + local repo="$1" tag="$2" name="$3" + curl --fail --location --retry 3 \ + "https://github.com/${repo}/archive/refs/tags/${tag}.tar.gz" \ + -o "$RUNNER_TEMP/${name}.tar.gz" + mkdir -p "$RUNNER_TEMP/cpm/${name}/${tag}" + tar -xzf "$RUNNER_TEMP/${name}.tar.gz" \ + -C "$RUNNER_TEMP/cpm/${name}/${tag}" --strip-components=1 + } + fetch KhronosGroup/Vulkan-Headers v1.4.345 vulkanheaders + fetch KhronosGroup/SPIRV-Headers vulkan-sdk-1.4.341.0 spirv-headers + fetch GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator v3.3.0 vulkanmemoryallocator + + - name: Build macOS Vulkan renderer + env: + PATH: /opt/homebrew/opt/llvm/bin:/usr/local/opt/llvm/bin:/usr/bin:/bin + RX_CPM: ${{ runner.temp }}/cpm + RX_VK_ONLY: '1' + RX_RELEASE: '1' + run: | + python -I nx2/build.py --standalone --jobs "$(sysctl -n hw.ncpu)" + test -f work/sa/build/hookshot_vk.dylib + file work/sa/build/hookshot_vk.dylib + otool -L work/sa/build/hookshot_vk.dylib + - name: Check POSIX compatibility layer run: | "$(brew --prefix llvm)/bin/clang" -fsyntax-only -std=gnu11 \ -Ihost/posix host/posix/win32_posix.c + + - name: Upload macOS build + uses: actions/upload-artifact@v4 + with: + name: Hookshot-macos-${{ runner.arch }} + path: | + work/sa/build/hookshot_vk.dylib + work/sa/build/hookshot_vk.dylib.map + if-no-files-found: error From 8ff1013284ee848693f1b99d98f40443c1ef3c83 Mon Sep 17 00:00:00 2001 From: ytsodacan <103301261+ytsodacan@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:22:20 +0000 Subject: [PATCH 04/12] Use supported macOS CI runner Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index ef7e9a7d..ee6019dd 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -30,7 +30,7 @@ concurrency: jobs: build: - runs-on: macos-14 + runs-on: macos-15 timeout-minutes: 90 steps: - name: Check out source From 97e1d157592a236c0c3b14fb4f0d777c490fe7c2 Mon Sep 17 00:00:00 2001 From: ytsodacan <103301261+ytsodacan@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:24:25 +0000 Subject: [PATCH 05/12] Skip Windows FFmpeg assertion on macOS Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_cpm_discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cpm_discovery.py b/tests/test_cpm_discovery.py index 38aeee0a..a54a80a2 100644 --- a/tests/test_cpm_discovery.py +++ b/tests/test_cpm_discovery.py @@ -65,7 +65,7 @@ def assert_packages(self, build, expected): "bundled FFmpeg was not discovered") self.assertEqual(build.cpm_package("vulkanheaders"), expected["vulkanheaders"], "bundled Vulkan headers were not discovered") - if not build.LINUX: + if not build.LINUX and not build.MACOS: self.assertEqual(build.FFMPEG, expected["ffmpeg"], "Windows import did not select FFmpeg") def test_portable_package_without_rx_cpm(self): From 7d7a8a58162e4c95073f9ddcbba2ffa0b25f3387 Mon Sep 17 00:00:00 2001 From: ytsodacan <103301261+ytsodacan@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:26:44 +0000 Subject: [PATCH 06/12] Fix macOS build CPU detection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/macos.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index ee6019dd..4c502d8e 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -88,12 +88,12 @@ jobs: - name: Build macOS Vulkan renderer env: - PATH: /opt/homebrew/opt/llvm/bin:/usr/local/opt/llvm/bin:/usr/bin:/bin + PATH: /opt/homebrew/opt/llvm/bin:/usr/local/opt/llvm/bin:/usr/bin:/bin:/usr/sbin:/sbin RX_CPM: ${{ runner.temp }}/cpm RX_VK_ONLY: '1' RX_RELEASE: '1' run: | - python -I nx2/build.py --standalone --jobs "$(sysctl -n hw.ncpu)" + python -I nx2/build.py --standalone --jobs "$(python -c 'import os; print(os.cpu_count() or 1)')" test -f work/sa/build/hookshot_vk.dylib file work/sa/build/hookshot_vk.dylib otool -L work/sa/build/hookshot_vk.dylib From 207b9fa522df13aed1ba023389683d2b9a9f0b99 Mon Sep 17 00:00:00 2001 From: ytsodacan <103301261+ytsodacan@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:28:02 +0000 Subject: [PATCH 07/12] Show progress during macOS build Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/macos.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 4c502d8e..f7d72dc7 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -93,7 +93,15 @@ jobs: RX_VK_ONLY: '1' RX_RELEASE: '1' run: | - python -I nx2/build.py --standalone --jobs "$(python -c 'import os; print(os.cpu_count() or 1)')" + jobs="$(python -c 'import os; print(os.cpu_count() or 1)')" + echo "Starting macOS Vulkan renderer build with ${jobs} jobs" + python -I nx2/build.py --standalone --jobs "$jobs" & + build_pid=$! + while kill -0 "$build_pid" 2>/dev/null; do + echo "macOS renderer build still running ($(date -u '+%Y-%m-%dT%H:%M:%SZ'))" + sleep 60 + done + wait "$build_pid" test -f work/sa/build/hookshot_vk.dylib file work/sa/build/hookshot_vk.dylib otool -L work/sa/build/hookshot_vk.dylib From 670ccc4a74a3779dd961eebc705da2332ded4ccf Mon Sep 17 00:00:00 2001 From: ytsodacan <103301261+ytsodacan@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:32:41 +0000 Subject: [PATCH 08/12] Fix macOS POSIX renderer compilation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- host/posix/win32_posix.c | 10 +++++++++- host/posix/windows.h | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/host/posix/win32_posix.c b/host/posix/win32_posix.c index a88a1162..772d286e 100644 --- a/host/posix/win32_posix.c +++ b/host/posix/win32_posix.c @@ -12,6 +12,11 @@ #include #include #include +#ifdef __APPLE__ +#ifndef _XOPEN_SOURCE +#define _XOPEN_SOURCE 700 +#endif +#endif #include #ifdef __linux__ #include @@ -934,10 +939,13 @@ DWORD GetModuleFileNameA(HMODULE m, LPSTR out, DWORD cap) { else if (!realpath(di.dli_fname, path)) snprintf(path, sizeof path, "%s", di.dli_fname); if (!path[0]) exe_path(path, sizeof path); - } else { + } +#ifdef __linux__ + else { void *lm = NULL; /* (a handle from LoadLibraryA) */ if (dlinfo(m, RTLD_DI_LINKMAP, &lm) == 0 && lm) snprintf(path, sizeof path, "%s", ((const char **)lm)[1]); } +#endif if (!cap) return 0; const size_t n = strlen(path); const size_t k = n < cap - 1 ? n : cap - 1; diff --git a/host/posix/windows.h b/host/posix/windows.h index 1ab77f27..58d0276a 100644 --- a/host/posix/windows.h +++ b/host/posix/windows.h @@ -28,6 +28,7 @@ #ifdef __cplusplus extern "C" { #endif +extern char **environ; /* ---- types ---- */ typedef int BOOL; From 4ef123a5bc8e71d7d1327ad72425ff5ff0cd120c Mon Sep 17 00:00:00 2001 From: ytsodacan <103301261+ytsodacan@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:37:22 +0000 Subject: [PATCH 09/12] Use Darwin linker flags for macOS Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nx2/build.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nx2/build.py b/nx2/build.py index 0727f508..bdb56706 100644 --- a/nx2/build.py +++ b/nx2/build.py @@ -530,7 +530,8 @@ def main(): t = time.time() # (--export-dynamic-symbol: the window's state, which the Vulkan back end's own Win32 calls read by name) r = subprocess.run([cc] + target() + (["-fuse-ld=lld"] if LINUX else []) + - ["-o", exe, "-Wl,-Map=" + exe + ".map", "-Wl,--gc-sections", + ["-o", exe, "-Wl,-Map=" + exe + ".map" if LINUX else "-Wl,-map," + exe + ".map", + "-Wl,--gc-sections" if LINUX else "-Wl,-dead_strip", "-Wl,--export-dynamic-symbol=hos_posix_*" if LINUX else "-Wl,-export_dynamic", "@" + rsp] + HOT_ORDER() + (["-fprofile-instr-generate"] if PGO == "gen" else []) + (FFMPEG_LX[1] if FFMPEG_LX else []) + HOST_LIBS, @@ -593,7 +594,8 @@ def main(): if POSIX_BUILD: link = [toolchain.need(toolchain.find("clang++"))] + target() + [ *(["-fuse-ld=lld"] if LINUX else []), "-dynamiclib" if MACOS else "-shared", "-o", vdll, - "-Wl,-Map=" + vdll + ".map", "-Wl,--gc-sections", + "-Wl,-Map=" + vdll + ".map" if LINUX else "-Wl,-map," + vdll + ".map", + "-Wl,--gc-sections" if LINUX else "-Wl,-dead_strip", *(["-Wl,-Bsymbolic", "-static-libstdc++", "-static-libgcc"] if LINUX else []), "@" + vrsp, *HOST_LIBS] else: From 4a75d420804eb53bd3e209cb53e6a8bee3fd862f Mon Sep 17 00:00:00 2001 From: ytsodacan <103301261+ytsodacan@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:41:53 +0000 Subject: [PATCH 10/12] Use fsync on macOS Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- host/posix/win32_posix.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/host/posix/win32_posix.c b/host/posix/win32_posix.c index 772d286e..dd2f4105 100644 --- a/host/posix/win32_posix.c +++ b/host/posix/win32_posix.c @@ -1066,7 +1066,11 @@ BOOL SetEndOfFile(HANDLE h) { } BOOL FlushFileBuffers(HANDLE h) { if (!is(h, H_FILE)) { last_error = ERROR_INVALID_HANDLE; return FALSE; } +#ifdef __APPLE__ + return fsync(((HFile *)h)->fd) == 0 ? TRUE : fail_errno(); +#else return fdatasync(((HFile *)h)->fd) == 0 || errno == EINVAL ? TRUE : fail_errno(); +#endif } DWORD GetFileType(HANDLE h) { struct stat st; From 765e9f9a7768799677a27dc855919847274bbcc2 Mon Sep 17 00:00:00 2001 From: ytsodacan <103301261+ytsodacan@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:47:32 +0000 Subject: [PATCH 11/12] Clarify macOS renderer artifact Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/macos.yml | 17 +++++++++++++++-- readme.md | 3 +++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index f7d72dc7..10553417 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -111,11 +111,24 @@ jobs: "$(brew --prefix llvm)/bin/clang" -fsyntax-only -std=gnu11 \ -Ihost/posix host/posix/win32_posix.c - - name: Upload macOS build + - name: Describe renderer artifact + run: | + cat > work/sa/build/README.txt <<'EOF' + This is Hookshot's macOS Vulkan renderer library, not a standalone app. + + Do not execute hookshot_vk.dylib directly. To build and run the game, + provide your own game dump and follow the macOS instructions in readme.md: + + python3 -I nx2/make_game.py --nup ~/your-game.nup --out ~/Games/Hookshot --language en-US + ./builder/macos/hookshot-launch ~/Games/Hookshot + EOF + + - name: Upload macOS renderer uses: actions/upload-artifact@v4 with: - name: Hookshot-macos-${{ runner.arch }} + name: Hookshot-macos-renderer-arm64 path: | work/sa/build/hookshot_vk.dylib work/sa/build/hookshot_vk.dylib.map + work/sa/build/README.txt if-no-files-found: error diff --git a/readme.md b/readme.md index dd1c52c2..23c35750 100644 --- a/readme.md +++ b/readme.md @@ -128,6 +128,9 @@ python3 -I nx2/make_game.py --nup ~/your-game.nup --out ~/Games/Hookshot --langu The game executable is `hookshot`, and the renderer is `hookshot_vk.dylib`. A Vulkan 1.3-capable MoltenVK device is required; macOS does not support the Linux AppImage, so its compiler and libraries must be installed locally. +The GitHub Actions artifact named `Hookshot-macos-renderer-arm64` contains only the renderer library for CI validation. +It is not executable by itself and does not include a game dump; use `make_game.py` above to create a playable game folder. + ### Updating to a newer release A new release is not a patch to your game folder: the game is built again from your copy, with the new code. Your From 02578e138628c6e8cb67257b40f8298cda725758 Mon Sep 17 00:00:00 2001 From: SillyProotSoda <103301261+ytsodacan@users.noreply.github.com> Date: Sun, 8 Nov 2026 16:53:47 +1100 Subject: [PATCH 12/12] Add files via upload --- .github/workflows/builder-windows.yml | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/builder-windows.yml diff --git a/.github/workflows/builder-windows.yml b/.github/workflows/builder-windows.yml new file mode 100644 index 00000000..ceb524a4 --- /dev/null +++ b/.github/workflows/builder-windows.yml @@ -0,0 +1,47 @@ +# Builds Hookshot Builder (WPF, .NET Framework 4.8) on Windows, reports the size of HookshotBuilder.exe, and starts it once to +# catch a crash on startup (a XAML or resource error). No game data is involved. +name: Builder (Windows) + +on: + push: + paths: ['builder/HookshotBuilder/**', 'builder/HookshotBuilder.sln', '.github/workflows/builder-windows.yml'] + pull_request: + paths: ['builder/HookshotBuilder/**', 'builder/HookshotBuilder.sln'] + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Build + run: dotnet build builder/HookshotBuilder.sln -c Release -o out + + - name: Size and contents + shell: pwsh + run: | + Get-ChildItem out | Select-Object Name, Length | Format-Table | Out-String | Write-Host + $exe = Get-Item out/HookshotBuilder.exe + $kb = [math]::Round($exe.Length / 1KB) + "HookshotBuilder.exe: $kb KB" | Tee-Object -Append $env:GITHUB_STEP_SUMMARY + # nothing but the exe and its config beside it: no .NET runtime, no extra DLLs + $extra = Get-ChildItem out -File | Where-Object { $_.Name -notin 'HookshotBuilder.exe', 'HookshotBuilder.exe.config' } + if ($extra) { $extra | ForEach-Object { Write-Host "::error::unexpected file $($_.Name)" }; exit 1 } + if ($exe.Length -gt 3MB) { Write-Host "::error::HookshotBuilder.exe is $kb KB (over 3 MB)"; exit 1 } + + - name: Starts without crashing + shell: pwsh + run: | + $p = Start-Process -FilePath out/HookshotBuilder.exe -PassThru + Start-Sleep -Seconds 10 + if ($p.HasExited) { Write-Host "::error::HookshotBuilder.exe exited with code $($p.ExitCode) within 10 seconds"; exit 1 } + Stop-Process -Id $p.Id -Force + "Started and ran for 10 seconds without exiting." | Tee-Object -Append $env:GITHUB_STEP_SUMMARY + + - uses: actions/upload-artifact@v4 + with: + name: HookshotBuilder + path: | + out/HookshotBuilder.exe + out/HookshotBuilder.exe.config