From 498be832c2afe49dd48586e9f39f229706394bc2 Mon Sep 17 00:00:00 2001 From: Dan Turner Date: Sun, 20 Sep 2026 11:00:00 +0100 Subject: [PATCH] macos: packaging, sweep tooling, and two runtime fixes (D299, D300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tools_pc/bundle-mac.sh: a signed, double-clickable GoldenEye.app. Homebrew's libstdc++/libgcc_s and SDL2 are not system libraries, so they are bundled and rewritten to @executable_path/@loader_path. Note sdl2-compat (Homebrew's `sdl2`) finds its SDL3 backend through a RELATIVE rpath baked into the dylib, so SDL3 ships too and that rpath is repointed — copy it without that and sdl2-compat raises a modal error dialog from its initialiser and the app hangs before main(). prepare-assets/ goes in Contents/Resources/: codesign treats a directory under Contents/MacOS/ as nested code and fails the bundle. - tools_pc/level_sweep_mac.sh: per-level crash-log sweep, symbolicated with atos, with a rendered-pixel check so "runs but renders nothing" is caught. - D299: sndPlaySfx() rejected EVERY sound on arm64 — all in-level SFX were silent, while music played (separate player). Its guard predated the shifted window: `sp >= 0x400000000ULL` (16 GiB) is correct on x86_64 (DRAM at 0x70000000) and rejects everything on arm64 (DRAM at ~0x1000_707b_5740). Fixed with a portAddrIsInWindow() helper rather than absolute bounds. - D300: cleanupObjects() did `u32 *obj = (u32)g_CurrentSetup.propDefs`, truncating a host pointer to 32 bits. Crashed at stage unload — only reachable once a level actually ends. Verified: 21/21 on both the no-input and full-control sweeps; SFX voices allocate on every level. --- .gitignore | 7 + CMakeLists.txt | 17 ++- docs/building.md | 41 +++++- docs/dev/MACOS-ARM64-PLAN.md | 17 ++- port/include/port_addr.h | 23 +++- port/src/romconvert.c | 20 +++ src/game/bg.c | 4 +- src/game/bondhead.c | 7 +- src/game/bondview2.c | 2 +- src/game/bondview_r.c | 2 +- src/game/chr.c | 10 ++ src/game/cleanup_objects.c | 10 +- src/game/language.c | 5 + src/game/propobj.c | 46 +++---- src/snd.c | 15 ++- tools_pc/README.md | 3 +- tools_pc/bundle-mac.sh | 245 +++++++++++++++++++++++++++++++++++ tools_pc/level_sweep_mac.sh | 43 ++++++ 18 files changed, 474 insertions(+), 43 deletions(-) create mode 100755 tools_pc/bundle-mac.sh create mode 100755 tools_pc/level_sweep_mac.sh diff --git a/.gitignore b/.gitignore index 61e855ba..5bd508ef 100644 --- a/.gitignore +++ b/.gitignore @@ -200,3 +200,10 @@ tools_pc/sweep-captures/capture-*/ # referenced by no project tooling. Never commit. /Python/ docs/media/*.mp4 + +# Debug traces and capture artifacts written by the engine at runtime +# (GE_AUDIOTRACE, GE_MIXERTRACE, GE_AUDIODUMP, GE_PCDUMP, crash handler). +# These are run output, never source -- see dev-process.md "keep PR noise low". +*.log +audiodump.raw +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index e52d1fce..c740d201 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,6 +63,16 @@ endif() # (port/src/romassets_.s, see scripts/gen_romassets.py). project(ge007 C CXX ASM) +# Host base of the N64 address window (see port/include/port_addr.h). Must be +# set BEFORE add_definitions below. 0 on Windows/Linux; 16 TiB on arm64 macOS +# (overridable with -DPORT_ADDR_BASE=... for tooling, e.g. an ASan build whose +# shadow region overlaps 16 TiB). +if(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64") + set(PORT_ADDR_BASE 0x100000000000 CACHE STRING "Host base of the N64 address window (arm64 macOS)") +else() + set(PORT_ADDR_BASE 0x0 CACHE STRING "Host base of the N64 address window") +endif() + include(TargetArch) include(FindPkgConfig) @@ -246,6 +256,7 @@ add_definitions( -D_LANGUAGE_C=1 -DAVOID_UB=1 -DPAL=${VERSION_IS_PAL} + -DPORT_ADDR_BASE=${PORT_ADDR_BASE} ${REGION_DEFS} ) if(TARGET_IS_64BIT) @@ -271,7 +282,9 @@ add_compile_options( # runs the same way; the genuinely understood cases are still fixed properly # under #ifdef PORT (D189). A hardened Linux build would need the whole class # swept first — tracked in docs/dev/findings.md. -add_compile_options(-fno-stack-protector) +if(NOT GE_STACK_PROTECTOR_ALL) + add_compile_options(-fno-stack-protector) +endif() # Keep frame pointers: dbghelp's StackWalk64 (crash handler + thread dumps) # cannot read DWARF unwind info on MinGW, so the EBP chain is the only way to # get a real backtrace of a hung/crashed game thread. @@ -369,7 +382,6 @@ elseif(APPLE) # / 0x80000000) are impossible. The whole N64 address space is instead # shifted to PORT_ADDR_BASE and reserved PROT_NONE by portAddrInit() # (port/include/port_addr.h, docs/dev/MACOS-ARM64-PLAN.md). - set(PORT_ADDR_BASE 0x100000000000) else() # Intel: Mach-O normally reserves [0, 4 GiB) as __PAGEZERO. The port needs # fixed mappings at 0x10000000 (ROM), 0x70000000 (s32-safe DRAM), and @@ -377,7 +389,6 @@ elseif(APPLE) # while leaving ample space before the first required mapping. set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-pagezero_size,0x10000") - set(PORT_ADDR_BASE 0x0) endif() endif() diff --git a/docs/building.md b/docs/building.md index 36e30d36..6d12f91a 100644 --- a/docs/building.md +++ b/docs/building.md @@ -28,8 +28,9 @@ for accepted versions and hashes. ### Port build > **The Windows (MSYS2 MINGW64) path is the primary one.** Linux is compiled -> by CI and ships in the release bundle. Native Intel macOS builds are also -> supported; Apple Silicon remains untested. +> by CI and ships in the release bundle. macOS is supported natively on both +> Intel and Apple Silicon (arm64); see the note below the table for the +> arm64-specific points. | Need | Windows (MSYS2 MINGW64) | Debian/Ubuntu | macOS (Homebrew) | |------|------------------------|---------------|------------------| @@ -50,6 +51,20 @@ The project requires real GNU GCC for the decomp's Plan 9 struct extensions. Apple's `/usr/bin/gcc` is Clang and is not compatible. CMake automatically selects Homebrew's versioned `gcc-N`/`g++-N` executables. +**Apple Silicon (arm64).** Everything above applies as-is; `./build-pc.sh` +detects the architecture and produces `build-pc/ge007.aarch64`. Two arm64 +specifics are handled automatically: + +- Native arm64 cannot map the low 4 GiB (mandatory `__PAGEZERO`), so the N64 + address-space window is placed at a 16 TiB host base (`PORT_ADDR_BASE`, + `port/include/port_addr.h`). It is a CMake cache var + (`-DPORT_ADDR_BASE=...`) for tooling that needs it elsewhere; every + game-visible 32-bit value is identical to the other platforms. +- GCC's `libstdc++`/`libgcc_s` and SDL2 come from Homebrew and are **not** + system libraries, so a redistributable build must bundle them — use + `tools_pc/bundle-mac.sh` (below), which produces a signed, double-clickable + `GoldenEye.app`. + ### Asset extraction (decompilation toolchain) The extraction scripts need `binutils-mips-linux-gnu` (or an equivalent MIPS @@ -169,6 +184,28 @@ deterministic function of the ROM. Re-run after any change to `d43_emit.py` / The ROM in `data/` (from step 4) and the sidecars are both required at runtime. `ge007.ini` is written under `data/` on first launch. +On macOS the binary is `./build-pc/ge007.aarch64` (Apple Silicon) or +`./build-pc/ge007.x86_64` (Intel). + +### Packaging a macOS app + +```sh +tools_pc/bundle-mac.sh # -> dist/GoldenEye.app + a .zip + sha256 +``` + +Builds a double-clickable, ad-hoc-signed `GoldenEye.app` (the engine plus +bundled SDL2 / SDL3 / `libstdc++` / `libgcc_s`, README, licenses, and the +`prepare-assets` tool). Note that Homebrew's `sdl2` is **sdl2-compat**, a +shim over SDL3 that locates its backend through a *relative* rpath +(`@loader_path/../../../../opt/sdl3/lib`): copy it without SDL3 and +repointing that rpath, and it shows a modal error dialog from its +initializer and the app hangs before `main()`. The script does both. It contains **no ROM and no game data**: the player +drops their own ROM at +`GoldenEye.app/Contents/MacOS/data/ge007.ntsc-final.z64` and the app finds it +next to the executable. Because the app is ad-hoc signed rather than +notarised, first launch needs right-click → *Open* (or +`xattr -dr com.apple.quarantine GoldenEye.app`). + ### Useful flags / env | | | diff --git a/docs/dev/MACOS-ARM64-PLAN.md b/docs/dev/MACOS-ARM64-PLAN.md index 3f0f0398..4ee904b4 100644 --- a/docs/dev/MACOS-ARM64-PLAN.md +++ b/docs/dev/MACOS-ARM64-PLAN.md @@ -579,9 +579,20 @@ Record as a `Dxx`. | Research | done | §1 F1–F18, measured 2026-09-18 | | M0 | **superseded by #88** | the clang build/toolchain work is replaced by PR #88's GCC-based macOS layer; only D296 + `gen_macho_syms --base` survive (§0b) | | M1 | **done** | shifted-window address model (D298); self-test ALL PASS on the #88 base | -| M2 | **in progress** | game-code re-basing sweep; boot reaches the level stage load | -| M3 | partly inherited | #88 supplies the AppKit main-thread guard; the two Core-profile GL fixes and the level sweep remain | -| M4 | not started | packaging / CI / docs | +| M2 | **done** | game-code re-basing sweep complete; **21/21 solo levels boot, render and run crash-free**, including with full controller input | +| M3 | **done (core)** | #88's AppKit main-thread guard; `tools_pc/level_sweep_mac.sh` (crash + rendered-pixel check); the two Core-profile GL nits are cosmetic and parked in `docs/dev/GRAPHICS-BACKLOG.md` | +| M4 | **done (core)** | `tools_pc/bundle-mac.sh` → signed, double-clickable `GoldenEye.app` (+ zip/sha256); `docs/building.md` macOS section + `tools_pc/README.md`. Outstanding: CI for macOS, and a live run of the `.app` from Finder | + +**Verified state (2026-09-18).** `build-pc/ge007.aarch64` (Homebrew GCC 16.2) +opens a window, renders, plays music **and sound effects**, and runs every solo +level crash-free: + +- `tools_pc/level_sweep_mac.sh` — 21/21, all with non-clear pixels + (76–92%). +- Full-control sweep (`GE_INPUTSCRIPT`: every button + 8-way stick, 26 s per + level) — 21/21, with SFX voices allocating on every level. +- Findings D294–D300; D299 (silent SFX) and D300 (stage-unload truncation) + were both arm64-specific and are fixed. ### Prerequisites to run (easy to miss) diff --git a/port/include/port_addr.h b/port/include/port_addr.h index b06ea4f3..25f668e3 100644 --- a/port/include/port_addr.h +++ b/port/include/port_addr.h @@ -36,16 +36,23 @@ #include #include "platform.h" +#ifndef PORT_ADDR_BASE #if defined(PLATFORM_MACOS) && defined(PLATFORM_ARM) /* 16 TiB: reserved cleanly on Apple Silicon, far above libmalloc's regions and * the mmap hint area, and well below fast3d_ptr_ok()'s 128 TiB bound. The - * committed port/src/*.darwin.s files are generated with the SAME base - * (scripts/gen_romassets.py --darwin --base 0x100000000000); portAddrInit() - * checks the two agree at startup. */ + * generated Mach-O symbol files are produced with the SAME base (CMake's + * PORT_ADDR_BASE cache var, fed to scripts/gen_macho_syms.py --base); + * portAddrInit() checks the two agree at startup via cfb_16. + * + * Overridable from CMake (-DPORT_ADDR_BASE=...) for tooling that needs the + * window somewhere else -- e.g. an AddressSanitizer build, whose shadow + * region overlaps 16 TiB (it covers 0x027e00024000..0x10700001ffff, so a + * sanitizer build uses a LowMem base like 0x4000000000). */ #define PORT_ADDR_BASE 0x100000000000ULL #else #define PORT_ADDR_BASE 0x0ULL #endif +#endif /* Size of the N64 address space. */ #define PORT_ADDR_WINDOW (1ULL << 32) @@ -97,6 +104,16 @@ static inline uint32_t portHostToN64(const void *p) return (uint32_t)v; } +/* Is a host pointer inside the N64 address window (DRAM / cart / stacks)? + * Use this instead of hardcoded numeric bounds: the window is shifted by + * PORT_ADDR_BASE on arm64, so an absolute bound that is correct on x86_64 + * silently rejects every real pointer there. */ +static inline int portAddrIsInWindow(const void *p) +{ + uintptr_t v = (uintptr_t)p; + return v - (uintptr_t)PORT_ADDR_BASE < (uintptr_t)PORT_ADDR_WINDOW; +} + /* Cast-site helper for game code (the D3x ABI/layout class): reads a 32-bit * address out of a field/expression and yields a typed pointer. Identity on * Windows/Linux (PORT_ADDR_BASE == 0). */ diff --git a/port/src/romconvert.c b/port/src/romconvert.c index 72b7fd17..3a751af8 100644 --- a/port/src/romconvert.c +++ b/port/src/romconvert.c @@ -173,6 +173,26 @@ int romConvertEnsureSidecars(const unsigned char *romImg, const char *romRelPath break; } } + if (!convPath[0] && exedir[0]) { + /* Two layouts the "$E/" forms above miss. (1) On POSIX "$E/" resolves + * CWD-relative, not to the real exe dir, so a launch from another CWD + * needs the absolute form. (2) A macOS .app keeps prepare-assets/ in + * Contents/Resources/ — it cannot live in Contents/MacOS/, where + * codesign treats the directory as nested code and refuses to sign. */ + static const char *candExe[3] = { + "prepare-assets/ge007-convert", + "../Resources/prepare-assets/ge007-convert", + "ge007-convert", + }; + for (size_t i = 0; i < sizeof(candExe) / sizeof(candExe[0]); i++) { + char full[1024]; + snprintf(full, sizeof(full), "%s/%s", exedir, candExe[i]); + if (fsExists(full)) { + snprintf(convPath, sizeof(convPath), "%s", full); + break; + } + } + } if (!convPath[0]) { sysLogPrintf(LOG_ERROR, diff --git a/src/game/bg.c b/src/game/bg.c index 2e46be86..5373b102 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -3579,7 +3579,7 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV if (bgTestRayIntersectsBbox(from, dir, (s32 *) (&bboxMin), (s32 *) (&bboxMax))) { - if (intersectRayTriangle((Vertex *)((s32)vtxbase - (0 - (idx[0] << 4))), (Vertex *)((s32)vtxbase - (0 - (idx[1] << 4))), (Vertex *)((s32)vtxbase - (0 - (idx[2] << 4))), (coord3d *) (((roomnum * 24) + ((uintptr_t) ptr_bgdata_room_fileposition_list)) + 12), from, to, dir, &hitbuf)) + if (intersectRayTriangle((Vertex *)((uintptr_t)vtxbase - (0 - (idx[0] << 4))), (Vertex *)((uintptr_t)vtxbase - (0 - (idx[1] << 4))), (Vertex *)((uintptr_t)vtxbase - (0 - (idx[2] << 4))), (coord3d *) (((roomnum * 24) + ((uintptr_t) ptr_bgdata_room_fileposition_list)) + 12), from, to, dir, &hitbuf)) { tcmd = gdl; dx = ((s32) hitbuf.hitpos.x) - ((s32) from->x); @@ -3764,7 +3764,7 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV if (bgTestRayIntersectsBbox(from, dir, (s32 *) (&bboxMin2), (s32 *) (&bboxMax2))) { - if (intersectRayTriangle((Vertex *)((s32)vtxbase - (0 - (idx2[0] << 4))), (Vertex *)((s32)vtxbase - (0 - (idx2[1] << 4))), (Vertex *)((s32)vtxbase - (0 - (idx2[2] << 4))), (coord3d *) (((roomnum * 24) + ((uintptr_t) ptr_bgdata_room_fileposition_list)) + 12), from, to, dir, &hitbuf)) + if (intersectRayTriangle((Vertex *)((uintptr_t)vtxbase - (0 - (idx2[0] << 4))), (Vertex *)((uintptr_t)vtxbase - (0 - (idx2[1] << 4))), (Vertex *)((uintptr_t)vtxbase - (0 - (idx2[2] << 4))), (coord3d *) (((roomnum * 24) + ((uintptr_t) ptr_bgdata_room_fileposition_list)) + 12), from, to, dir, &hitbuf)) { tcmd = gdl; dx = ((s32) hitbuf.hitpos.x) - ((s32) from->x); diff --git a/src/game/bondhead.c b/src/game/bondhead.c index d4d47996..c14da996 100644 --- a/src/game/bondhead.c +++ b/src/game/bondhead.c @@ -1,4 +1,9 @@ #include +#if defined(PORT) +#include "port_addr.h" +#else +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #include #include #include @@ -438,7 +443,7 @@ void bheadAdjustAnimation(f32 speed) modelSetAnimation( &g_CurrentPlayer->model, // match hack: addu address backwards - (struct ModelAnimation *) ((s32)g_BondMoveAnimationSetup[i].anim_id + (s32)&ptr_animation_table->data), + PORT_N64PTR(struct ModelAnimation, (s32)g_BondMoveAnimationSetup[i].anim_id + (s32)&ptr_animation_table->data), (s32) BHEAD_FLIPFLAG, startframe, 0.5f, diff --git a/src/game/bondview2.c b/src/game/bondview2.c index abd1741b..50dbd487 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -4592,7 +4592,7 @@ void bondviewMoveAnimationTick(f32 speed, f32 speedforwards, f32 speedsideways) // HACK: ptr_animation_table dereference addition is backwards. // this should be: // ptr_animation_table->data[g_bondviewBondDeathAnimations[((u32) randomGetNext() % (u32) g_bondviewBondDeathAnimationsCount)]] - bheadStartDeathAnimation((struct ModelAnimation *) ((s32)g_bondviewBondDeathAnimations[((u32) randomGetNext() % (u32) g_bondviewBondDeathAnimationsCount)] + (s32)&ptr_animation_table->data[0]), randomGetNext() & 1, 0.0f, 1.0f); + bheadStartDeathAnimation(PORT_N64PTR(struct ModelAnimation, (s32)g_bondviewBondDeathAnimations[((u32) randomGetNext() % (u32) g_bondviewBondDeathAnimationsCount)] + (s32)&ptr_animation_table->data[0]), randomGetNext() & 1, 0.0f, 1.0f); g_CurrentPlayer->startnewbonddie = FALSE; } diff --git a/src/game/bondview_r.c b/src/game/bondview_r.c index 7774c9eb..a00d5fbe 100644 --- a/src/game/bondview_r.c +++ b/src/game/bondview_r.c @@ -347,7 +347,7 @@ void bondviewLoadSetupIntroSection(void) intro_credits = (struct SetupIntroCredits*)intro_record; // hack: bad address math - credits = (CreditsEntry*)((s32)g_ptrStageSetupFile + (s32)intro_credits->unk04); + credits = PORT_N64PTR(CreditsEntry, (s32)g_ptrStageSetupFile + (s32)intro_credits->unk04); credits_pointer = credits; // what is the point of this? diff --git a/src/game/chr.c b/src/game/chr.c index cb0a7a1d..606b652a 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -896,6 +896,16 @@ struct anim_group_info *ptr_doubles_firing_animation_groups[] = { struct weapon_firing_animation_table crouched_rifle_firing_animation_group1[] = { { PTR_ANIM_fire_kneel_right_leg, 27.0, 0, 0, 0, -1.0, 35.0, 75.0, -1.0, -1.0, 31.0, 75.0, 0.87266463, -0.69813174, 0.90757126, -0.69813174, 1.5, 1.5 }, +#if defined(PORT) + /* D298: every other *_firing_animation_groupN[] is zero-terminated; + * initResolveAnimGroupTable() walks to that 0, so without one here it runs + * into the next global (crouched_rifle_firing_animation_groupA) and + * over-counts `len`. On N64 the 72-byte stride happens to stop it; on PC + * the 80-byte stride runs several globals deep, so `len` allows a + * `next_anim` past the array and the guard-attack path reads a bogus + * animation. N64 build unchanged. */ + {0, 0.0, 0, 0, 0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0} +#endif }; struct weapon_firing_animation_table crouched_rifle_firing_animation_groupA[] = { diff --git a/src/game/cleanup_objects.c b/src/game/cleanup_objects.c index 9050721d..e5016467 100644 --- a/src/game/cleanup_objects.c +++ b/src/game/cleanup_objects.c @@ -23,7 +23,15 @@ void cleanupObjects(s32 stage) { - u32 *obj = (u32)g_CurrentSetup.propDefs; + /* D300: the decomp's `(u32)g_CurrentSetup.propDefs` converts the pointer + * to a 32-bit integer and back into a `u32 *` -- a no-op where pointers + * are 32-bit (N64), but on the 64-bit port it TRUNCATES the host pointer + * to its low 32 bits, leaving an unbased N64 address (0x7017_c70f on + * Cuba) that faults on the first CLEANUP_PDTYPE deref. propDefs is a real + * host pointer (prop.c:1283 stores `local_stage + propDefs`), so cast + * pointer-to-pointer and keep `u32 *` for sizepropdef()'s 4-byte-unit + * increment. Same class as D3x; semantics unchanged. */ + u32 *obj = (u32 *)g_CurrentSetup.propDefs; if (obj) { diff --git a/src/game/language.c b/src/game/language.c index 24327099..de395ca4 100644 --- a/src/game/language.c +++ b/src/game/language.c @@ -462,8 +462,13 @@ u8 * langGet(s32 slotID) #endif u32 textslot_offset = textbank_ptr[slotID & 0x03FF]; /* load the textbank ptr table then get the slot's offset */ +#if defined(PORT) + /* D298/M2: keep the bank base at full pointer width. */ + uintptr_t output_slot = (uintptr_t)textbank_ptr + textslot_offset; /* base + slot offset */ +#else u32 output_slot = textslot_offset; /* add the text slot offset to the base ptr to get the ptr to text file's slot */ output_slot += (u32)textbank_ptr; +#endif #ifdef DEBUG return (textslot_offset != 0) ? (u8 *)output_slot : "Sorry, string not loaded."; #endif diff --git a/src/game/propobj.c b/src/game/propobj.c index b63b080e..590f227f 100644 --- a/src/game/propobj.c +++ b/src/game/propobj.c @@ -7711,18 +7711,18 @@ void objDeform(ObjectRecord *obj, E_EXPLOSIONTYPE explosiontype) adjust_height = 1; } - newverts = vtxstore_allocate(rodata->numVertices, 0x0b0b, model->obj, objGetDestroyedLevel(obj)); + newverts = PORT_N64PTR(Vertex, vtxstore_allocate(rodata->numVertices, 0x0b0b, model->obj, objGetDestroyedLevel(obj))); if (newverts != NULL) { - if ((*vtxslot) != rodata->Vertices) + if (PORT_N64PTR(Vertex, *vtxslot) != rodata->Vertices) { for (i = 0, offset = 0; i < rodata->numVertices; i++, offset += sizeof(Vertex)) { - *((Word4 *) (((u8 *) newverts) + offset)) = *((Word4 *) (((u8 *) (*vtxslot)) + offset)); + *((Word4 *) (((u8 *) newverts) + offset)) = *((Word4 *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset)); } - sub_GAME_7F09C044(*vtxslot); + sub_GAME_7F09C044(PORT_N64PTR(Vertex, *vtxslot)); } else { @@ -7736,9 +7736,9 @@ void objDeform(ObjectRecord *obj, E_EXPLOSIONTYPE explosiontype) } else { - if ((*vtxslot) != rodata->Vertices) + if (PORT_N64PTR(Vertex, *vtxslot) != rodata->Vertices) { - sub_GAME_7F09C044(*vtxslot); + sub_GAME_7F09C044(PORT_N64PTR(Vertex, *vtxslot)); *vtxslot = rodata->Vertices; obj->runtime_bitflags |= 4; } @@ -7778,14 +7778,14 @@ void objDeform(ObjectRecord *obj, E_EXPLOSIONTYPE explosiontype) do { - if (((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.y < ymin) + if (((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.y < ymin) { - ymin = ((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.y; + ymin = ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.y; } - if (ymax < ((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.y) + if (ymax < ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.y) { - ymax = ((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.y; + ymax = ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.y; } i++; @@ -7845,7 +7845,7 @@ void objDeform(ObjectRecord *obj, E_EXPLOSIONTYPE explosiontype) { if (obj->mtx.m[1][1] >= 0.0f) { - if (((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.y >= ymid) + if (((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.y >= ymid) { if (adjust_height) { @@ -7865,7 +7865,7 @@ void objDeform(ObjectRecord *obj, E_EXPLOSIONTYPE explosiontype) chance = 90; } } - else if ((ymid ^ 0) >= ((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.y) + else if ((ymid ^ 0) >= ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.y) { if (adjust_height) { @@ -7894,10 +7894,10 @@ void objDeform(ObjectRecord *obj, E_EXPLOSIONTYPE explosiontype) { if (((s32) (chrObjRandomGetNext() % 100)) < chance) { - ((Vertex *) (((u8 *) (*vtxslot)) + offset))->r = 0; - ((Vertex *) (((u8 *) (*vtxslot)) + offset))->g = 0; - ((Vertex *) (((u8 *) (*vtxslot)) + offset))->b = 0; - ((Vertex *) (((u8 *) (*vtxslot)) + offset))->a = 255; + ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->r = 0; + ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->g = 0; + ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->b = 0; + ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->a = 255; } else #ifdef VERSION_EU @@ -7906,17 +7906,17 @@ void objDeform(ObjectRecord *obj, E_EXPLOSIONTYPE explosiontype) if ((explosiontype * 2) == EXPLOSION_MEDIUM) #endif { - ((Vertex *) (((u8 *) (*vtxslot)) + offset))->a = 0; + ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->a = 0; } - ((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.y = (((f32) (((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.y - ymin)) * yscale) + ((f32) ymin); - ((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.x += (chrObjRandomGetNext() % 80) - 40; - ((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.y += (chrObjRandomGetNext() % 80) - 40; - ((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.z += (chrObjRandomGetNext() % 80) - 40; + ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.y = (((f32) (((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.y - ymin)) * yscale) + ((f32) ymin); + ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.x += (chrObjRandomGetNext() % 80) - 40; + ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.y += (chrObjRandomGetNext() % 80) - 40; + ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.z += (chrObjRandomGetNext() % 80) - 40; - if (((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.y < ymin) + if (((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.y < ymin) { - ((Vertex *) (((u8 *) (*vtxslot)) + offset))->coord.y = ymin; + ((Vertex *) (((u8 *) PORT_N64PTR(Vertex, *vtxslot)) + offset))->coord.y = ymin; } i++; diff --git a/src/snd.c b/src/snd.c index 2fa125df..be218af5 100644 --- a/src/snd.c +++ b/src/snd.c @@ -2,6 +2,9 @@ #include #include #include "music.h" +#ifdef PORT +#include "port_addr.h" +#endif #include "snd.h" #ifdef PORT #include @@ -1107,8 +1110,16 @@ ALSoundState *sndPlaySfx(struct ALBankAlt_s *soundBank, s16 soundIndex, ALSoundS * (0x1_4000_0000..); anything else (e.g. 0x0000_5622_0001_0001) is a * byte-scrambled / OOB read. */ { - uintptr_t sp = (uintptr_t)sound; - if (sp < 0x10000 || sp >= 0x400000000ULL) { + /* D299 (macOS arm64): this guard predates the shifted-window + * address model (port/include/port_addr.h). With PORT_ADDR_BASE + * != 0 a valid ALSound* is ~0x1000_707b_5740 -- far ABOVE the old + * 16 GiB (0x400000000) ceiling -- so the hardcoded bound rejected + * every real sound and SFX were silent. (Music is a separate + * player, so only SFX were affected; on x86_64 the window starts + * at 0 and DRAM at 0x7000_0000, below the ceiling, which is why + * this never showed there.) Test the pointer against the actual + * window instead. */ + if (!portAddrIsInWindow(sound)) { return NULL; } } diff --git a/tools_pc/README.md b/tools_pc/README.md index 766374a1..ec81996d 100644 --- a/tools_pc/README.md +++ b/tools_pc/README.md @@ -34,7 +34,8 @@ investigation artifacts. | `playtest.sh` | Launch a level for `docs/dev/LEVEL-PLAYTEST.md` human validation (WS6). | | `audiodebug.ps1` | Audio counterpart to `debug.ps1` (that one is for crashes; this one is for audio behaviour). Drives the `GE_D204` health monitor + `GE_AUDIOTRACE`/`GE_AUDIODUMP` and prints a pass/fail verdict: real-time ratio, queue starvation, dropped blocks, oversized blocks, plus a soundIndex histogram. `-AB` runs the same binary twice (with and without `GE_D204_OLD`) and prints a before/after table — how D204 was measured. `-Play` for an instrumented interactive playtest (the D202 workflow), `-Soak` for a 5-min stability run, `-SyncData` to mirror `./data` first. | | `debug.ps1` / `repro_gdb.sh` / `attach_animgen.sh` | Launch (or attach to) the game under gdb so a crash always leaves a backtrace. | -| `bundle-win.sh` / `bundle-linux.sh` | Package a built tree as a distributable archive (exe + licenses + `prepare-assets/`). | +| `bundle-win.sh` / `bundle-linux.sh` / `bundle-mac.sh` | Package a built tree as a distributable archive (exe + licenses + `prepare-assets/`). `bundle-mac.sh` produces a signed, double-clickable `dist/GoldenEye.app` with SDL2 + libstdc++/libgcc_s bundled (Homebrew libs are not system libs) and `prepare-assets/` in `Contents/Resources/` — it cannot go in `Contents/MacOS/`, where codesign treats the directory as nested code and fails the bundle. | +| `level_sweep_mac.sh` | macOS/arm64 sweep: bare `-level_XX` boot of all 21 solo levels, crash-log detection (symbolicated via `atos`), and a `GE_PCDUMP` frame that must be non-near-black (`PIX_MIN`, default 2%), so "runs but renders nothing" is caught too. `SWEEP_SECS` (default 18). | | `romverify.c` | One-shot `.z64` integrity check against the repo's ground truths. | | `dump_objectives.py` | Dump per-level objectives + win/fail criteria from the ROM (playtest validation; Step 10 / N8 acceptance oracle). | | `disasm.py` | Minimal MIPS disassembler for the BE ROM (RAM-addr → file offset). | diff --git a/tools_pc/bundle-mac.sh b/tools_pc/bundle-mac.sh new file mode 100755 index 00000000..727fb50a --- /dev/null +++ b/tools_pc/bundle-mac.sh @@ -0,0 +1,245 @@ +#!/usr/bin/env bash +# +# Package a macOS (Apple Silicon / arm64) GoldenEye 007 PC port build as a +# double-clickable .app for distribution. +# +# Run from anywhere, AFTER building (./build-pc.sh or a cmake --build): +# +# tools_pc/bundle-mac.sh [VERSION] +# +# Produces, under dist/ : +# GoldenEye.app/ the app bundle (drop a ROM in to play) +# goldeneye-pc-port--macos-arm64.zip + .zip.sha256 +# +# The bundle contains ONLY: the engine executable, its two Homebrew-provided +# runtime libraries (SDL2, libstdc++/libgcc_s), a README, license texts, and +# the prepare-assets/ tool. It contains NO ROM and NO game assets. zlib, Cocoa +# and OpenGL come from macOS itself. +# +# The engine finds its data dir as `/data/` when there is no `data/` +# in the CWD (port/src/system.c sysResolvePath), so the user's ROM + generated +# sidecars live in GoldenEye.app/Contents/MacOS/data/. +# +# arm64 binaries must carry at least an ad-hoc signature to execute, and +# install_name_tool invalidates whatever signature the linker applied — so the +# script re-signs every binary (and the bundle) at the end. +# +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +VERSION="${1:-$(git describe --tags --always --dirty 2>/dev/null || echo 0.0.0-dev)}" +VERSION="${VERSION#v}" # strip a leading "v" +NAME="goldeneye-pc-port-${VERSION}-macos-arm64" +APP="dist/GoldenEye.app" +MACOS="$APP/Contents/MacOS" +RES="$APP/Contents/Resources" + +# Accept a build from either a local build-pc/ tree or a CI macos tree. +EXE="" +for cand in build-macos/ge007.aarch64 build-pc/ge007.aarch64 build-pc/ge007; do + if [ -f "$cand" ] && [ -x "$cand" ]; then EXE="$cand"; break; fi +done +[ -n "$EXE" ] || { echo "error: no arm64 ge007 binary found in build-macos/ or build-pc/ — build first" >&2; exit 1; } +file "$EXE" | grep -q 'arm64' || { echo "error: $EXE is not an arm64 binary" >&2; exit 1; } + +echo "==> Bundling $EXE -> $NAME" +rm -rf "$APP" "dist/$NAME" "dist/${NAME}.zip" "dist/${NAME}.zip.sha256" +mkdir -p "$MACOS" "$RES/licenses" + +cp "$EXE" "$MACOS/ge007" +chmod +x "$MACOS/ge007" +EXE_NAME="ge007" + +# --- bundle the Homebrew runtime deps -------------------------------- +# SDL2 and libstdc++ are the only non-system dynamic deps. Copy the exact +# files this exe was linked against next to it and rewrite the load commands +# to @executable_path so the bundled copies win, then make libstdc++ find its +# own libgcc_s via @loader_path. +copy_dep() { # -> echoes the bundled basename + local src="$1" base + base="$(basename "$src")" + if [ ! -f "$src" ]; then + echo "error: cannot locate $src (linked but not on disk)" >&2 + return 1 + fi + cp -L "$src" "$MACOS/$base" + chmod 644 "$MACOS/$base" + echo "$base" +} + +SDL2_SRC="$(otool -L "$MACOS/$EXE_NAME" | awk '/libSDL2/ {print $1; exit}')" +STDC_SRC="$(otool -L "$MACOS/$EXE_NAME" | awk '/libstdc\+\+/ {print $1; exit}')" +[ -n "$SDL2_SRC" ] || { echo "error: no libSDL2 in $EXE_NAME's load commands" >&2; exit 1; } +[ -n "$STDC_SRC" ] || { echo "error: no libstdc++ in $EXE_NAME's load commands" >&2; exit 1; } + +SDL2_BASE="$(copy_dep "$SDL2_SRC")" +STDC_BASE="$(copy_dep "$STDC_SRC")" +install_name_tool -change "$SDL2_SRC" "@executable_path/$SDL2_BASE" "$MACOS/$EXE_NAME" +install_name_tool -change "$STDC_SRC" "@executable_path/$STDC_BASE" "$MACOS/$EXE_NAME" + +# Homebrew's `sdl2` is sdl2-compat, a thin shim over SDL3 that finds its +# backend through a RELATIVE rpath baked into the dylib: +# @loader_path/../../../../opt/sdl3/lib +# That resolves correctly at /opt/homebrew/opt/sdl2-compat/lib/ and to +# nothing once the dylib is copied into the bundle -- sdl2-compat then shows +# a MODAL ERROR DIALOG from its initializer (dllinit -> error_dialog -> +# NSAlert runModal) and the app hangs before main(). Bundle SDL3 and repoint +# every Homebrew rpath at the bundle directory. +SDL3_SRC="$(dirname "$(readlink -f /opt/homebrew/opt/sdl3/lib/libSDL3.dylib 2>/dev/null || echo /opt/homebrew/opt/sdl3/lib/libSDL3.0.dylib)")" +if [ -f /opt/homebrew/opt/sdl3/lib/libSDL3.0.dylib ]; then + cp -L /opt/homebrew/opt/sdl3/lib/libSDL3.0.dylib "$MACOS/libSDL3.0.dylib" + chmod 644 "$MACOS/libSDL3.0.dylib" + ( cd "$MACOS" && ln -sf libSDL3.0.dylib libSDL3.dylib ) + echo " + libSDL3.0.dylib (+ libSDL3.dylib symlink; sdl2-compat backend)" +else + echo "error: SDL3 not found at /opt/homebrew/opt/sdl3/lib — required by sdl2-compat" >&2 + exit 1 +fi +# Repoint the bundled SDL2's Homebrew rpaths at @loader_path so the SDL3 +# dlopen finds the copy sitting next to it. +while IFS= read -r rp; do + case "$rp" in + /opt/*|@loader_path/../../..*) + install_name_tool -rpath "$rp" "@loader_path" "$MACOS/$SDL2_BASE" && \ + echo " ~ SDL2 rpath: $rp -> @loader_path" ;; + esac +done < <(otool -l "$MACOS/$SDL2_BASE" | awk '/LC_RPATH/{getline; getline; sub(/^ *path /,""); sub(/ \(offset.*/,""); print}') +install_name_tool -id "@rpath/$SDL2_BASE" "$MACOS/$SDL2_BASE" 2>/dev/null || true + +GCC_S_SRC="$(otool -L "$MACOS/$STDC_BASE" | awk '/libgcc_s/ {print $1; exit}')" +if [ -n "$GCC_S_SRC" ]; then + if [ ! -f "$GCC_S_SRC" ]; then + # libstdc++ records it as @rpath/... — resolve against the libstdc++ dir. + GCC_S_SRC="$(dirname "$STDC_SRC")/$(basename "$GCC_S_SRC")" + fi + GCC_S_BASE="$(copy_dep "$GCC_S_SRC")" + install_name_tool -change "@rpath/$GCC_S_BASE" "@loader_path/$GCC_S_BASE" "$MACOS/$STDC_BASE" 2>/dev/null || \ + install_name_tool -change "$GCC_S_SRC" "@loader_path/$GCC_S_BASE" "$MACOS/$STDC_BASE" + install_name_tool -id "@rpath/$STDC_BASE" "$MACOS/$STDC_BASE" 2>/dev/null || true + echo " + $STDC_BASE + $GCC_S_BASE (bundled)" +else + echo " + $STDC_BASE (bundled)" +fi +echo " + $SDL2_BASE (bundled)" + +# --- remaining deps should all be system ------------------------------ +echo " remaining dynamic dependencies (expected: libz + Apple frameworks):" +otool -L "$MACOS/$EXE_NAME" | tail -n +2 | grep -vE "@executable_path|@loader_path" | sed 's/^/ /' || true + +# --- Info.plist ------------------------------------------------------- +cat > "$APP/Contents/Info.plist" < + + + + CFBundleNameGoldenEye 007 + CFBundleDisplayNameGoldenEye 007 + CFBundleIdentifierorg.goldeneye.pcport + CFBundleExecutablege007 + CFBundlePackageTypeAPPL + CFBundleShortVersionString${VERSION} + CFBundleVersion${VERSION} + LSMinimumSystemVersion11.0 + NSHighResolutionCapable + NSSupportsAutomaticGraphicsSwitching + + +PLIST + +# --- docs + licenses -------------------------------------------------- +DEPS_BLOCK=$'## 1a. Runtime libraries\n\nNothing to install. SDL2 and the GCC C++ runtime (libstdc++, libgcc_s)\nare bundled inside the app; zlib, Cocoa and OpenGL come from macOS.\n\n**Gatekeeper:** the app is ad-hoc signed (not notarised). On first launch,\nright-click the app and choose *Open*, or run\n`xattr -dr com.apple.quarantine GoldenEye.app`. This is only needed once.\n' +DECK_BLOCK=$'## 4a. Game controller (options overlay)\n\nThe options overlay is fully gamepad-driven: it opens with **Select**, the\nD-pad or left stick (up/down) moves between options, **A** steps the selected\noption forward, **B** steps it back, and **Start** (or Select again) closes.\nToggles flip, resolution / MSAA / filtering cycle, sliders step in\nincrements. With a keyboard attached the same overlay is `F10` + arrows/Enter.\n\n' +# BSD sed/awk reject embedded newlines in -v, so the two multi-line blocks go +# in via sed's `r` (read-file) command at their markers. +DEPS_FILE="$(mktemp)"; DECK_FILE="$(mktemp)" +printf '%s\n' "$DEPS_BLOCK" > "$DEPS_FILE" +printf '%s\n' "$DECK_BLOCK" > "$DECK_FILE" +sed -e "s|@VERSION@|${VERSION}|g" \ + -e "s|@PLATFORM@|macOS arm64 (Apple Silicon)|g" \ + -e "s|@EXE@|GoldenEye.app|g" \ + -e "s|@LICENSE_EXTRA@||g" \ + -e "/@DEPS@/r $DEPS_FILE" -e '/@DEPS@/d' \ + -e "/@DECK@/r $DECK_FILE" -e '/@DECK@/d' \ + tools_pc/dist/README.md.in > "$RES/README.md" +rm -f "$DEPS_FILE" "$DECK_FILE" + +cp NOTICE "$RES/licenses/NOTICE" +cp LICENSE "$RES/licenses/LICENSE-port-MIT.txt" +[ -f port/fast3d/LICENSE.txt ] && cp port/fast3d/LICENSE.txt "$RES/licenses/LICENSE-fast3d.txt" + +# --- asset-prep tool (same assembly as the Linux/Windows bundles) ----- +# In Contents/Resources/, NOT Contents/MacOS/: codesign treats a directory +# under MacOS/ as nested code and refuses to sign the bundle if it holds +# non-Mach-O files (the vendored .inc.c headers). romconvert.c knows to look +# in ../Resources/prepare-assets/ on macOS. +PREP="$RES/prepare-assets" +mkdir -p "$PREP/vendor/scripts" "$PREP/vendor/assets/obseg" +cp tools_pc/dist/prepare-assets/prepare-assets.py "$PREP/" +cp tools_pc/d43_emit.py tools_pc/d69_emit.py "$PREP/" +cp tools_pc/d88_emit.py tools_pc/d88_propdefs.py "$PREP/" +cp scripts/filelist.u.csv "$PREP/vendor/scripts/" +cp assets/obseg/file_resource_table.inc.c "$PREP/vendor/assets/obseg/" +# BSD cp has no --parents; recreate each header's tree under vendor/ by hand. +find assets -iname 'modelfileheader.inc.c' -print | while IFS= read -r f; do + d="$PREP/vendor/$(dirname "$f")" + mkdir -p "$d" + cp "$f" "$d/" +done +nmh="$(find "$PREP/vendor/assets" -iname 'modelfileheader.inc.c' | wc -l)" +echo " + prepare-assets/ (emit scripts + $nmh model headers)" +[ "$nmh" -gt 400 ] || { echo "error: prepare-assets vendored only $nmh model headers — expected ~512" >&2; exit 1; } + +# Frozen first-run converter (drop-in ROM support). Same PyInstaller step as +# Linux/Windows, but a macOS freeze only runs on macOS: if it is not present, +# warn rather than fail — the app still runs with pre-generated sidecars. +CV="$REPO_ROOT/tools_pc/dist/prepare-assets/ge007-convert" +if [ -f "$CV" ]; then + cp "$CV" "$PREP/" + chmod 755 "$PREP/ge007-convert" + echo " + prepare-assets/ge007-convert ($(du -h "$CV" | cut -f1))" +else + echo " ! prepare-assets/ge007-convert not found — the app will only run" + echo " with pre-generated sidecars. Freeze it with:" + echo " python3 -m PyInstaller --onefile --name ge007-convert \\" + echo " --distpath tools_pc/dist/prepare-assets \\" + echo " tools_pc/dist/prepare-assets/prepare-assets.py" +fi + +# --- guard: no ROM / game data snuck in ------------------------------ +if find "$APP" -type f \( -iname '*.z64' -o -iname '*.n64' -o -iname '*.v64' \) | grep -q .; then + echo "error: bundle contains a ROM image — aborting" >&2 + exit 1 +fi +BYTES="$(du -sk "$APP" | cut -f1)" +LIMIT=$((120 * 1024)) # KiB +if [ "$BYTES" -gt "$LIMIT" ]; then + echo "error: bundle is ${BYTES} KiB (> 120 MB) — likely contains game data, aborting" >&2 + exit 1 +fi + +# --- ad-hoc sign (arm64 requires a signature; install_name_tool voids it) -- +if command -v codesign >/dev/null 2>&1; then + for lib in "$MACOS"/*.dylib; do + [ -f "$lib" ] && codesign --force --sign - "$lib" 2>/dev/null || true + done + codesign --force --sign - "$MACOS/$EXE_NAME" + # No --deep: it tries to sign the prepare-assets/ .c sources as nested code + # and fails. The only Mach-O in the bundle is the exe + the dylibs above. + codesign --force --sign - "$APP" + echo " + ad-hoc signed ($(codesign -dv "$APP" 2>&1 | awk -F= '/Signature/ {print $2}'))" +else + echo " ! codesign not found — the app will not launch on arm64" >&2 +fi + +# --- zip + checksum --------------------------------------------------- +( cd dist && rm -f "${NAME}.zip" "${NAME}.zip.sha256" \ + && zip -qry "${NAME}.zip" "GoldenEye.app" \ + && shasum -a 256 "${NAME}.zip" > "${NAME}.zip.sha256" ) + +echo "==> dist/${NAME}.zip ($(du -h "dist/${NAME}.zip" | cut -f1))" +cat "dist/${NAME}.zip.sha256" +echo "==> dist/GoldenEye.app — drop your ROM at:" +echo " GoldenEye.app/Contents/MacOS/data/ge007.ntsc-final.z64" +find "$APP" -type f | sed "s#^dist/##" | sort diff --git a/tools_pc/level_sweep_mac.sh b/tools_pc/level_sweep_mac.sh new file mode 100755 index 00000000..68821950 --- /dev/null +++ b/tools_pc/level_sweep_mac.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# macOS/arm64 level sweep: bare -level_XX boot per level. PASS = ran the whole +# window without a crash log AND a GE_PCDUMP frame that is not near-black +# (catches "runs but renders nothing" that a crash-only check would miss). +# Env: SWEEP_SECS (default 18), PIX_MIN (default 2, percent non-clear). +cd "$(dirname "$0")/.." +LV="Dam:33 Facility:34 Runway:35 Surface1:36 Bunker1:09 Silo:20 Frigate:26 Surface2:43 Bunker2:27 Statue:22 Archives:24 Streets:29 Depot:30 Train:25 Jungle:37 Control:23 Caverns:39 Cradle:41 Aztec:28 Egypt:32 Cuba:54" +OUT=/tmp/mac_sweep_results; : > $OUT +PIX_MIN=${PIX_MIN:-2} +mkdir -p ppm +for entry in $LV; do + name=${entry%%:*}; num=${entry##*:} + rm -f ge007.crash.log ppm/frame_000120.ppm + GE_PCDUMP="120-120:1" ./build-pc/ge007.aarch64 -level_$num > /tmp/sweeplogs/$name.log 2>&1 & + pid=$! + sleep ${SWEEP_SECS:-18} + kill $pid 2>/dev/null; sleep 1; kill -9 $pid 2>/dev/null; wait $pid 2>/dev/null + sleep 1 + frames=$(grep -cE 'frame [0-9]+ rendered' /tmp/sweeplogs/$name.log 2>/dev/null) + if [ -f ge007.crash.log ]; then + M=$(grep -oE 'MODULE: 0x[0-9a-f]+' ge007.crash.log|head -1|awk '{print $2}') + P=$(grep -oE '^PC: 0x[0-9a-f]+' ge007.crash.log|awk '{print $2}') + sym=$(atos -o build-pc/ge007.aarch64 -l "$M" "$P" 2>/dev/null | head -1) + cp ge007.crash.log /tmp/sweeplogs/$name.crash.log + echo "$name ($num): CRASH @ $sym" | tee -a $OUT + continue + fi + if [ -f ppm/frame_000120.ppm ]; then + pc=$(python3 tools_pc/pixcount.py ppm/frame_000120.ppm 2>/dev/null | grep -oE '[0-9]+\.[0-9]+%' | head -1) + pcn=${pc%\%} + if [ -z "$pcn" ]; then + echo "$name ($num): OK (frames=$frames, pixcount failed)" | tee -a $OUT + elif awk "BEGIN{exit !($pcn < $PIX_MIN)}"; then + echo "$name ($num): BLACK (frames=$frames, non-clear ${pc})" | tee -a $OUT + else + echo "$name ($num): OK (frames=$frames, non-clear ${pc})" | tee -a $OUT + fi + rm -f ppm/frame_000120.ppm + else + echo "$name ($num): OK (frames=$frames, no capture)" | tee -a $OUT + fi +done +echo "SWEEP DONE" | tee -a $OUT