Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/builder-windows.yml
Original file line number Diff line number Diff line change
@@ -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
134 changes: 134 additions & 0 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
name: macOS build

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:
build:
runs-on: macos-15
timeout-minutes: 90
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: 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:/usr/sbin:/sbin
RX_CPM: ${{ runner.temp }}/cpm
RX_VK_ONLY: '1'
RX_RELEASE: '1'
run: |
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

- name: Check POSIX compatibility layer
run: |
"$(brew --prefix llvm)/bin/clang" -fsyntax-only -std=gnu11 \
-Ihost/posix host/posix/win32_posix.c

- 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-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
15 changes: 15 additions & 0 deletions builder/macos/hookshot-launch
Original file line number Diff line number Diff line change
@@ -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
18 changes: 15 additions & 3 deletions host/hos_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions host/hos_setjmp.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <setjmp.h>
#include <windows.h>
#include <string.h>

Expand Down Expand Up @@ -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 */
Expand Down
3 changes: 3 additions & 0 deletions host/posix/prof_stub.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#include "../hos.h"

void hos_prof_start(void) {}
14 changes: 14 additions & 0 deletions host/posix/psapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
#ifndef HOOKSHOT_POSIX_PSAPI_H
#define HOOKSHOT_POSIX_PSAPI_H
#include <windows.h>
#ifdef __APPLE__
#include <mach/mach.h>
#endif
typedef struct {
DWORD cb, PageFaultCount;
SIZE_T PeakWorkingSetSize, WorkingSetSize, QuotaPeakPagedPoolUsage, QuotaPagedPoolUsage, QuotaPeakNonPagedPoolUsage,
Expand All @@ -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;
Expand All @@ -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
Loading