From 62b21054584ec539e1a59bbfc6747e3ebd02370b Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Thu, 17 Sep 2026 00:35:30 -0700 Subject: [PATCH 01/12] Implement macOS (Intel) support --- CMakeLists.txt | 123 +++++++++++++++++++++++++++++++--- README.md | 10 +-- docs/building.md | 22 ++++-- docs/dev/findings.md | 48 +++++++++++++ docs/porting-notes.md | 24 +++++++ port/fast3d/gfx_sdl2.cpp | 7 ++ port/include/pc_protos.h | 11 +++ port/shim/PR/os.h | 12 ++++ port/src/crash.c | 22 ++++-- port/src/dram.c | 17 ++++- port/src/libultra.c | 2 +- port/src/macho_weak_aliases.s | 25 +++++++ port/src/romdata.c | 11 ++- port/src/video.c | 8 ++- scripts/gen_macho_syms.py | 44 ++++++++++++ scripts/strip_weak_pragmas.py | 25 +++++++ 16 files changed, 376 insertions(+), 35 deletions(-) create mode 100644 port/src/macho_weak_aliases.s create mode 100644 scripts/gen_macho_syms.py create mode 100644 scripts/strip_weak_pragmas.py diff --git a/CMakeLists.txt b/CMakeLists.txt index fe0c9500..1d481989 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,11 +25,26 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) # name as an inherited one — CCTVRecord's `pad` — see docs/internals.md.) # An explicit -DCMAKE_C_COMPILER=... always wins. if(NOT DEFINED CMAKE_C_COMPILER) - find_program(_GE007_GCC NAMES gcc) + if(CMAKE_HOST_APPLE) + # Homebrew installs real GNU GCC with a version suffix; /usr/bin/gcc is + # Apple Clang and does not support the Plan 9 struct extensions required + # by the decomp's `inherits` pattern. + find_program(_GE007_GCC + NAMES gcc-16 gcc-15 gcc-14 gcc-13 gcc-12 gcc-11 gcc) + else() + find_program(_GE007_GCC NAMES gcc) + endif() if(_GE007_GCC) set(CMAKE_C_COMPILER "${_GE007_GCC}" CACHE FILEPATH "C compiler" FORCE) get_filename_component(_GE007_GCC_DIR "${_GE007_GCC}" DIRECTORY) - find_program(_GE007_GXX NAMES g++ PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH) + if(CMAKE_HOST_APPLE) + find_program(_GE007_GXX + NAMES g++-16 g++-15 g++-14 g++-13 g++-12 g++-11 g++ + PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH) + else() + find_program(_GE007_GXX NAMES g++ + PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH) + endif() if(_GE007_GXX) set(CMAKE_CXX_COMPILER "${_GE007_GXX}" CACHE FILEPATH "CXX compiler" FORCE) endif() @@ -163,7 +178,18 @@ configure_file( # the toolchain include dir from the compiler location (MinGW/MSYS2 layout: # bin/../include) and generate an absolute-path include. get_filename_component(_PORT_GCC_DIR "${CMAKE_C_COMPILER}" DIRECTORY) -get_filename_component(PORT_HOST_STRING_H "${_PORT_GCC_DIR}/../include/string.h" ABSOLUTE) +if(APPLE) + execute_process( + COMMAND xcrun --show-sdk-path + OUTPUT_VARIABLE _PORT_MACOS_SDK OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if(NOT _PORT_MACOS_SDK) + message(FATAL_ERROR "Cannot locate the macOS SDK; install the Xcode Command Line Tools") + endif() + set(PORT_HOST_STRING_H "${_PORT_MACOS_SDK}/usr/include/string.h") +else() + get_filename_component(PORT_HOST_STRING_H "${_PORT_GCC_DIR}/../include/string.h" ABSOLUTE) +endif() configure_file( "${CMAKE_SOURCE_DIR}/port/include/hoststring.h.in" "${CMAKE_BINARY_DIR}/port/include/hoststring.h" @ONLY) @@ -172,7 +198,11 @@ configure_file( # the include path and shadows the system header; libstdc++'s /gthr # headers in the fast3d C++ TUs need the real (cpu_set_t, # sched_yield). Both MinGW and Linux keep at bin/../include. -get_filename_component(PORT_HOST_SCHED_H "${_PORT_GCC_DIR}/../include/sched.h" ABSOLUTE) +if(APPLE) + set(PORT_HOST_SCHED_H "${_PORT_MACOS_SDK}/usr/include/sched.h") +else() + get_filename_component(PORT_HOST_SCHED_H "${_PORT_GCC_DIR}/../include/sched.h" ABSOLUTE) +endif() configure_file( "${CMAKE_SOURCE_DIR}/port/include/hostsched.h.in" "${CMAKE_BINARY_DIR}/port/include/hostsched.h" @ONLY) @@ -182,7 +212,11 @@ configure_file( # one C++ TU (fast3d): MinGW's own sec_api/stdlib_s.h does `#include # `, which resolves to the N64 header via -I. The shim routes C++ # TUs to the host header by absolute path (same trick as hoststring.h). -get_filename_component(PORT_HOST_STDLIB_H "${_PORT_GCC_DIR}/../include/stdlib.h" ABSOLUTE) +if(APPLE) + set(PORT_HOST_STDLIB_H "${_PORT_MACOS_SDK}/usr/include/stdlib.h") +else() + get_filename_component(PORT_HOST_STDLIB_H "${_PORT_GCC_DIR}/../include/stdlib.h" ABSOLUTE) +endif() configure_file( "${CMAKE_SOURCE_DIR}/port/include/hoststdlib.h.in" "${CMAKE_BINARY_DIR}/port/include/hoststdlib.h" @ONLY) @@ -328,6 +362,13 @@ elseif(UNIX AND NOT APPLE) # relocations are fine (unlike a 0x140000000 base, which needs mcmodel=large). # This is the ELF analogue of the Windows build's fixed 0x140000000 base. set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Ttext-segment=0x20000000") +elseif(APPLE) + # Mach-O executables normally reserve [0, 4 GiB) as __PAGEZERO. The port + # needs fixed mappings at 0x10000000 (ROM), 0x70000000 (s32-safe DRAM), and + # 0x80000000 (KSEG0 mirror), so retain null/low-pointer protection only up + # while leaving ample space before the first required mapping. + set(CMAKE_EXE_LINKER_FLAGS + "${CMAKE_EXE_LINKER_FLAGS} -Wl,-pagezero_size,0x10000") endif() # port/shim first: PC-only shadow headers (bondtypes.h, bondconstants.h, @@ -342,6 +383,7 @@ include_directories( "${CMAKE_SOURCE_DIR}/src/game" "${CMAKE_SOURCE_DIR}/src/libultra" "${CMAKE_SOURCE_DIR}/src/libultra/audio" + "${CMAKE_SOURCE_DIR}/src/libultra/gu" "${CMAKE_SOURCE_DIR}/port/include" "${CMAKE_BINARY_DIR}/port/include" ) @@ -395,6 +437,8 @@ if(NOT EXTRA_LIBRARIES) set(EXTRA_LIBRARIES dbghelp -static-libstdc++ pthread) elseif(WIN32) set(EXTRA_LIBRARIES dbghelp -static-libstdc++) + elseif(APPLE) + set(EXTRA_LIBRARIES stdc++ m pthread) else() set(EXTRA_LIBRARIES stdc++ m dl pthread) endif() @@ -414,14 +458,41 @@ file(GLOB_RECURSE SRC_FAST3D "${CMAKE_SOURCE_DIR}/port/fast3d/*.cpp" # scripts/gen_romassets.py). Defines every obseg/ramrom/music label and the # ge007.ld segment markers as 0x10000000 + rom_offset, so `&symbol` / romCopy() # work once romdata.c maps the .z64 at 0x10000000. -set(SRC_ROMASSETS "${CMAKE_SOURCE_DIR}/port/src/romassets_${ASSET_REGION}.s") -if(NOT EXISTS ${SRC_ROMASSETS}) - message(FATAL_ERROR "Missing ${SRC_ROMASSETS} — run: python3 scripts/gen_romassets.py ${ASSET_REGION}") +set(SRC_ROMASSETS_INPUT "${CMAKE_SOURCE_DIR}/port/src/romassets_${ASSET_REGION}.s") +if(NOT EXISTS ${SRC_ROMASSETS_INPUT}) + message(FATAL_ERROR "Missing ${SRC_ROMASSETS_INPUT} — run: python3 scripts/gen_romassets.py ${ASSET_REGION}") endif() # Absolute symbols inside the reserved N64-DRAM region (port/src/dram.c): # cfb_16 (replaces src/cfb.c) and _bssSegmentEnd (mempool start marker). -set(SRC_DRAM_SYMS "${CMAKE_SOURCE_DIR}/port/src/dram_syms.s") +set(SRC_DRAM_SYMS_INPUT "${CMAKE_SOURCE_DIR}/port/src/dram_syms.s") + +if(APPLE) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + set(SRC_ROMASSETS "${CMAKE_BINARY_DIR}/port/src/romassets_${ASSET_REGION}_macho.s") + set(SRC_DRAM_SYMS "${CMAKE_BINARY_DIR}/port/src/dram_syms_macho.s") + add_custom_command( + OUTPUT "${SRC_ROMASSETS}" + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + "${SRC_ROMASSETS_INPUT}" "${SRC_ROMASSETS}" + DEPENDS "${SRC_ROMASSETS_INPUT}" + "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + VERBATIM) + add_custom_command( + OUTPUT "${SRC_DRAM_SYMS}" + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + "${SRC_DRAM_SYMS_INPUT}" "${SRC_DRAM_SYMS}" + DEPENDS "${SRC_DRAM_SYMS_INPUT}" + "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + VERBATIM) + set_source_files_properties(${SRC_ROMASSETS} ${SRC_DRAM_SYMS} + PROPERTIES GENERATED TRUE) +else() + set(SRC_ROMASSETS "${SRC_ROMASSETS_INPUT}") + set(SRC_DRAM_SYMS "${SRC_DRAM_SYMS_INPUT}") +endif() # Game code — compiled unmodified. file(GLOB_RECURSE SRC_GAME "${CMAKE_SOURCE_DIR}/src/game/*.c") @@ -452,6 +523,39 @@ file(GLOB SRC_LIBULTRARE_AUDIO "${CMAKE_SOURCE_DIR}/src/libultrare/audio/*.c") # gu matrix helpers. file(GLOB SRC_GU "${CMAKE_SOURCE_DIR}/src/libultra/gu/*.c") +# GCC for Darwin crashes while lowering the decomp's ELF-style +# `#pragma weak alias = target` directives. Compile generated copies with +# those directives removed, then provide their aliases in Mach-O assembly. +set(SRC_MACHO_ALIASES "") +if(APPLE) + set(_MACHO_GENERATED_SOURCES "") + set(_MACHO_WEAK_INPUTS + "${CMAKE_SOURCE_DIR}/src/game/lv.c" + "${CMAKE_SOURCE_DIR}/src/game/objective_status.c" + "${CMAKE_SOURCE_DIR}/src/game/spectrum.c" + "${CMAKE_SOURCE_DIR}/src/libultra/gu/sinf.c" + "${CMAKE_SOURCE_DIR}/src/libultra/gu/cosf.c") + foreach(_INPUT IN LISTS _MACHO_WEAK_INPUTS) + get_filename_component(_NAME "${_INPUT}" NAME) + set(_OUTPUT "${CMAKE_BINARY_DIR}/port/src/macho_${_NAME}") + add_custom_command( + OUTPUT "${_OUTPUT}" + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_SOURCE_DIR}/scripts/strip_weak_pragmas.py" + "${_INPUT}" "${_OUTPUT}" + DEPENDS "${_INPUT}" "${CMAKE_SOURCE_DIR}/scripts/strip_weak_pragmas.py" + VERBATIM) + list(APPEND SRC_MACHO_ALIASES "${_OUTPUT}") + list(APPEND _MACHO_GENERATED_SOURCES "${_OUTPUT}") + list(REMOVE_ITEM SRC_GAME "${_INPUT}") + list(REMOVE_ITEM SRC_GU "${_INPUT}") + endforeach() + list(APPEND SRC_MACHO_ALIASES + "${CMAKE_SOURCE_DIR}/port/src/macho_weak_aliases.s") + set_source_files_properties(${_MACHO_GENERATED_SOURCES} + PROPERTIES GENERATED TRUE) +endif() + # VI mode table (DATA, not a driver): fr.c references osViModeTable ~15x. # This is the only libultra/io file we compile — the rest are hardware # drivers that get shimmed in port/src/libultra.c. @@ -569,6 +673,7 @@ set(SRC ${SRC_LIBULTRARE_LIBC} ${SRC_ASSETS} ${SRC_GU} + ${SRC_MACHO_ALIASES} ${SRC_ENGINE} ) diff --git a/README.md b/README.md index 13d0201f..6d066f48 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ listed plainly there. **v0.2.2 - playable, with known rough edges.** The full single-player campaign is completable end to end (all 21 missions, Agent difficulty, playtested), at a steady 60 fps; all 21 solo missions load, render and run -crash-free, verified on Windows, Linux and real Steam Deck hardware. Feedback -is very welcome. +crash-free, verified on Windows, Linux and real Steam Deck hardware. Native +Intel macOS builds are supported. Feedback is very welcome. **Working:** boot sequence and front end (menu → mission select → briefing → start), front-end menu navigation on the left stick to match the F10 overlay @@ -87,7 +87,7 @@ dual-stick controller layout; file-backed saves; faithful N64 progression by def (F10 → *All unlocked* opens every level, 007 mode and the full cheat menu); F10 in-game options overlay (resolution, frame cap, MSAA, filtering, FOV, sensitivity, quit to desktop); -Windows and Linux. +Windows, Linux, and Intel macOS. **Known issues:** @@ -130,7 +130,7 @@ Windows and Linux. capture full faulting registers in `ge007.crash.log`. - Assorted further cosmetic defects are tracked in [`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md). -- No macOS or ARM support; no controller rebinding UI. +- No ARM support; no controller rebinding UI. Root causes and fix status for every item: the [release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases) and the finding log in [`docs/dev/findings.md`](docs/dev/findings.md). @@ -173,7 +173,7 @@ directionally, on the way to v1.0: - **PAL and JP ROM support** ([issue #85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)); NTSC-U is the only supported region today. - **Real widescreen** (properly expanding the field of view at 16:9, rather than today's 4:3-stretch). -- **Controller rebinding UI**, and macOS/ARM builds. +- **Controller rebinding UI**, and ARM builds. - **LAN multiplayer**: reviving GoldenEye's original split-screen/deathmatch netplay across multiple PCs on a local network. Genuinely under consideration, but early and not started; no ETA. diff --git a/docs/building.md b/docs/building.md index 5fdf93b4..36e30d36 100644 --- a/docs/building.md +++ b/docs/building.md @@ -1,6 +1,6 @@ --- title: Building -description: Full build and asset-extraction guide for the GoldenEye 007 PC port, covering Windows (MSYS2) and Linux. +description: Full build and asset-extraction guide for the GoldenEye 007 PC port on Windows, Linux, and macOS. --- ## Building the PC port @@ -27,21 +27,29 @@ for accepted versions and hashes. ### Port build -> **The Windows (MSYS2 MINGW64) path is the primary one.** The Linux build is -> compiled by CI on every push and ships in the release bundle (also -> playtested on Steam Deck hardware); the macOS column is best-effort -> guidance and has never been built or run there. Expect to fix build breaks -> yourself on untested platforms. +> **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. | Need | Windows (MSYS2 MINGW64) | Debian/Ubuntu | macOS (Homebrew) | |------|------------------------|---------------|------------------| -| toolchain | `mingw-w64-x86_64-toolchain` | `build-essential` | Xcode CLT / `gcc` | +| toolchain | `mingw-w64-x86_64-toolchain` | `build-essential` | Xcode CLT + Homebrew `gcc` | | CMake | `mingw-w64-x86_64-cmake` | `cmake` | `cmake` | | SDL2 | `mingw-w64-x86_64-SDL2` | `libsdl2-dev` | `sdl2` | | zlib | `mingw-w64-x86_64-zlib` | `zlib1g-dev` | `zlib` | | OpenGL | (in the toolchain) | `libgl1-mesa-dev` | (system) | | Python 3 | `mingw-w64-x86_64-python` | `python3` | `python3` | +On macOS, install the build dependencies with: + +```sh +brew install cmake gcc sdl2 zlib python3 +``` + +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. + ### Asset extraction (decompilation toolchain) The extraction scripts need `binutils-mips-linux-gnu` (or an equivalent MIPS diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 9f1f8e30..26b8267d 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -561,6 +561,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D258 | **PAL/JP asset conversion is broken at the source-data level — `scripts/filelist.e.csv` and `filelist.j.csv` carry naming defects, so `gen_romassets.py e/j` (and…** — full `## D258` entry at file tail | OPEN — repair path verified, not applied (out of v0.2.0 scope). | | D285 | **Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug: the `sndHandleEvent` preempt-scan walks the active-sound list unlocked (M-140; fixed M-141).** — full `## D285` entry at file tail | **FIXED (M-141, `src/snd.c`)** — `osSetIntMask` lock added around the preempt-scan, matching `sndSetupSound`'s existing unconditional (no `#ifdef PORT`) pattern in the same file; build green, `-level_09`/`-level_20` unregressed (1400+ frames, no crash, no D152 lock-stuck warnings). Not yet gdb/live-confirmed against a real reproduced crash — see M-141 body. Do not conflate with D255 — different thread, different subsystem. | | D293 | **QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest — "couldn't see a way to exit the game. Maybe build this into the…** — full `## D293` entry at file tail | FIXED (M-141) — "Quit to desktop" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.… | +| D294 | **Native Intel macOS build support.** Apple Clang lacks the required Plan 9 struct extensions; Darwin also needs SDK header discovery, Mach-O absolute/weak-alias symbol spelling, POSIX shared-memory backing instead of Linux `memfd_create`, and Darwin signal-context handling. | **FIXED** — CMake selects versioned Homebrew GNU GCC, generates Mach-O assembly/source adapters in the build tree, and the port layer now handles Darwin libc macros, shared memory, and crash diagnostics. Fresh `build-pc.sh ntsc-final` build and `--version` smoke test pass on x86_64 macOS 26.6.2. | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software @@ -10149,3 +10150,50 @@ do { ## D293 — QoL ask (M-141, Steam Deck user playtest): no discoverable way to quit the game from the UI on a controller-only setup **Context.** Deck/controller-only setups had only window-close/Alt+F4 to quit; no in-UI affordance. **FIXED.** Added a `ROW_ACTION` row kind (not config-backed, same pattern as the existing `__Resolution` row) to the F10 options overlay ("Quit to desktop") that triggers the identical `configSave()`+`exit(0)` path `SDL_QUIT` already uses. Build-verified on Windows and Linux; `verify.sh dam` PASS, no regression. + +## D294 — Native Intel macOS build support +**Context.** The CMake file contained nominal Apple branches, but had never +completed a Darwin build. `/usr/bin/gcc` is Apple Clang and rejects the +decomp's required `-fplan9-extensions`; host-header paths assumed MinGW/Linux; +ELF-style absolute symbols and weak aliases are invalid in Mach-O; the DRAM +mirror used Linux-only `memfd_create`; and crash diagnostics only implemented +Windows/Linux. + +**FIXED.** CMake now selects Homebrew's versioned GNU `gcc-N`/`g++-N`, locates +libc headers through the macOS SDK, omits Linux-only `-ldl`, and generates +Mach-O-spelled copies of the ROM/DRAM absolute symbols. Five sources containing +ELF weak-alias pragmas are copied into the build tree with only those pragmas +removed; a Mach-O assembly file supplies equivalent indirect symbols. Darwin +fortify/`errno` macros are suppressed while legacy declarations parse. +`dramReserve` uses an immediately-unlinked POSIX shared-memory object, and the +POSIX crash handler supports Darwin's x86-64 signal context. + +**Verified.** On x86_64 macOS 26.6.2 with Homebrew GCC 16.2.0, both an +incremental native build and a fresh build through `build-pc.sh` completed at +100%. `file` identifies the output as a Mach-O 64-bit x86_64 executable, +`otool -L` resolves SDL2, zlib, libstdc++, Cocoa, and OpenGL, and +`ge007.x86_64 --version` reports `x86_64-osx`. Runtime gameplay was not +exercised because ROM-derived user data is not part of build verification. +Apple Silicon remains unsupported/untested. + +**Runtime follow-up.** The first real launch exposed a link-time/runtime gap: +Darwin's default Mach-O `__PAGEZERO` segment reserved `[0, 0x100000000)`. +Consequently both the cartridge mapping at `0x10000000` and the mirrored DRAM +mapping at `0x70000000`/`0x80000000` failed despite a successful build. +The Apple link now sets `-pagezero_size 0x10000`, preserving null-page +protection while leaving all three N64 address windows available. Darwin also +lacks Linux's `MAP_FIXED_NOREPLACE`; the ROM mapping +therefore uses `MAP_FIXED` after the linker has guaranteed the cartridge range +is vacant. Without it, `mmap` relocated the cartridge despite the now-free +range, boot fell back to a heap copy, and direct absolute ROM-symbol reads +crashed during level initialization. With fixed ROM mapping working, the next +launch reached rendering and exposed AppKit's main-thread-only event rule: +fast3d polled SDL from the scheduler/render thread even though the port's host +main loop already owns a complete event pump. The fast3d pump is now disabled +only on macOS; `videoPumpEvents()` continues to process input/window events. +After generating all three required sidecar sets from the user's NTSC ROM, +`-level_09` ran for more than 1,500 rendered frames with the ROM fixed at +`0x10000000`, both DRAM views active, all 512 model and 73 stage sidecars +patched, and no crash. The missing packed-vertex OpenGL entry-point messages +are non-fatal capability noise from the GL loader; rendering proceeds on the +created macOS OpenGL 4.1 core context. diff --git a/docs/porting-notes.md b/docs/porting-notes.md index 729ef510..8e63160b 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -485,6 +485,30 @@ through a converter or a runtime bswap fixup reads scrambled. ## C2. Port-layer / SDL shims +- **Darwin assembly uses Mach-O symbol spelling.** GAS absolute symbols that + are `.global foo` / `.set foo, address` on ELF/PE must become `.globl _foo` + / `.set _foo, address`; Darwin also rejects the ELF `.section .data` + spelling used by these symbol-only files. Generate transformed build-tree + copies rather than changing the cross-platform source files (D294). +- **Darwin GCC cannot lower ELF-style `#pragma weak alias = target`.** It can + ICE in `assemble_alias`; compile generated copies with those pragmas removed + and provide Mach-O indirect symbols (`.set _alias, _target`) separately + (D294). +- **Mach-O's default `__PAGEZERO` reserves the entire low 4 GiB.** A build + can compile and link successfully yet fail every fixed N64 mapping at + runtime (`0x10000000`, `0x70000000`, `0x80000000`). Link with + `-Wl,-pagezero_size,0x10000` so null-page pointers remain protected while + the cartridge and DRAM ranges are available (D294). +- **Darwin has no `MAP_FIXED_NOREPLACE`; a fixed-address hint can relocate.** + Once `__PAGEZERO` is deliberately bounded below the cart range, use + `MAP_FIXED` for the `0x10000000` cartridge mapping. Retaining the advisory + hint silently falls back to a heap ROM and later crashes on direct absolute + ROM-symbol reads (D294). +- **AppKit event polling is main-thread-only.** This port has both a host-main + `videoPumpEvents()` loop and fast3d's render-thread event pump. The latter + must be disabled on macOS or `SDL_PollEvent` raises + `NSInternalInconsistencyException`; the main-thread loop already handles + the complete event set (D294). - `#include ` in a port `.c`/`.h` that also sees `` breaks: `OSContStatus`/`OSContPad` have a `u8 errno;` field vs errno.h's macro. libultra.c wraps the include in `#pragma push_macro("errno")` / diff --git a/port/fast3d/gfx_sdl2.cpp b/port/fast3d/gfx_sdl2.cpp index a43552a7..a61d95fb 100644 --- a/port/fast3d/gfx_sdl2.cpp +++ b/port/fast3d/gfx_sdl2.cpp @@ -309,6 +309,12 @@ static void gfx_sdl_get_dimensions(uint32_t* width, uint32_t* height, int32_t* p } static void gfx_sdl_handle_events(void) { +#if defined(__APPLE__) + /* AppKit requires event polling on the process main thread. The host + * loop in videoPumpEvents owns the complete SDL queue, so the scheduler + * render thread must not call SDL_PollEvent on macOS. */ + return; +#else SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { @@ -357,6 +363,7 @@ static void gfx_sdl_handle_events(void) { break; } } +#endif } /* The window/context are created on the host main thread, but the game's diff --git a/port/include/pc_protos.h b/port/include/pc_protos.h index 6d4096e8..26ce1745 100644 --- a/port/include/pc_protos.h +++ b/port/include/pc_protos.h @@ -28,9 +28,20 @@ * "using typedef-name after 'struct'"), and no C++ TU has an implicit * declaration needing a fix here. C++ TUs keep their pre-D38 header exposure. */ + #ifndef _PC_PROTOS_H_ #define _PC_PROTOS_H_ +#if defined(__APPLE__) +/* Darwin SDK headers expose these libc calls as fortify function-like + * macros. This legacy declaration list needs their actual symbol names. */ +#undef memcpy +#undef snprintf +#undef strcat +#undef strcpy +#undef strncpy +#endif + #if defined(PORT) && defined(__x86_64__) && !defined(__cplusplus) #include /* u8..s32, f32, size_t (host on PC) */ diff --git a/port/shim/PR/os.h b/port/shim/PR/os.h index 5378e3ec..56d01a6c 100644 --- a/port/shim/PR/os.h +++ b/port/shim/PR/os.h @@ -28,7 +28,19 @@ #define _PORT_SHIM_OS_H_ #if defined(PORT) +# if defined(__APPLE__) +/* Darwin exposes errno and sprintf as function-like macros. Suppress them + * while parsing the N64 API's errno fields and sprintf declaration. */ +# pragma push_macro("errno") +# pragma push_macro("sprintf") +# undef errno +# undef sprintf +# endif # include "include/PR/os.h" +# if defined(__APPLE__) +# pragma pop_macro("sprintf") +# pragma pop_macro("errno") +# endif # undef OS_K0_TO_PHYSICAL # define OS_K0_TO_PHYSICAL(x) ((u32)((char *)(x) - 0x70000000)) diff --git a/port/src/crash.c b/port/src/crash.c index 9cfab783..136efec3 100644 --- a/port/src/crash.c +++ b/port/src/crash.c @@ -11,6 +11,12 @@ #ifndef _GNU_SOURCE #define _GNU_SOURCE 1 #endif +#if defined(__APPLE__) && !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 700 +#endif +#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE) +#define _DARWIN_C_SOURCE 1 +#endif #include #include @@ -303,7 +309,7 @@ static LONG __stdcall crashHandler(PEXCEPTION_POINTERS exinfo) return EXCEPTION_CONTINUE_EXECUTION; } -#elif defined(PLATFORM_LINUX) +#elif defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS) #include #include @@ -341,7 +347,7 @@ static void crashStackTrace(char *msg, int sig, void *pc, ucontext_t *ucontext, if (siginfo && (sig == SIGSEGV || sig == SIGBUS)) { CRASH_MSG("FAULT ADDR: %p\n", (void *)siginfo->si_addr); } -#if defined(PLATFORM_X86_64) +#if defined(PLATFORM_LINUX) && defined(PLATFORM_X86_64) if (ucontext) { CRASH_MSG("REGS: Rax=%p Rcx=%p Rdx=%p Rsi=%p Rdi=%p R8=%p R9=%p R10=%p R11=%p\n", (void *)ucontext->uc_mcontext.gregs[REG_RAX], @@ -410,10 +416,12 @@ static void crashHandler(int sig, siginfo_t *siginfo, void *ctx) ucontext_t *ucontext = ctx ? (ucontext_t *)ctx : NULL; void *pc = NULL; if (ucontext) { -#ifdef PLATFORM_X86 +#if defined(PLATFORM_LINUX) && defined(PLATFORM_X86) pc = (void *)ucontext->uc_mcontext.gregs[REG_EIP]; -#elif defined(PLATFORM_X86_64) +#elif defined(PLATFORM_LINUX) && defined(PLATFORM_X86_64) pc = (void *)ucontext->uc_mcontext.gregs[REG_RIP]; +#elif defined(PLATFORM_MACOS) && defined(PLATFORM_X86_64) + pc = (void *)ucontext->uc_mcontext->__ss.__rip; #endif } @@ -470,7 +478,7 @@ void crashDumpThreads(const unsigned long *tids, const char **names, int count) } } -#endif /* PLATFORM_WINDOWS / PLATFORM_LINUX */ +#endif /* PLATFORM_WINDOWS / POSIX */ int g_CrashEnabled = 0; @@ -480,7 +488,7 @@ void crashInit(void) SetErrorMode(SEM_FAILCRITICALERRORS); prevExFilter = SetUnhandledExceptionFilter(crashHandler); g_CrashEnabled = 1; -#elif defined(PLATFORM_LINUX) +#elif defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS) struct sigaction sigact = { 0 }; sigact.sa_flags = SA_SIGINFO | SA_ONSTACK; sigact.sa_sigaction = crashHandler; @@ -501,7 +509,7 @@ void crashShutdown(void) if (prevExFilter) { SetUnhandledExceptionFilter(prevExFilter); } -#elif defined(PLATFORM_LINUX) +#elif defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS) sigaction(SIGSEGV, &prevSigAction, NULL); sigaction(SIGABRT, &prevSigAction, NULL); sigaction(SIGBUS, &prevSigAction, NULL); diff --git a/port/src/dram.c b/port/src/dram.c index fef2b6cd..fadf463e 100644 --- a/port/src/dram.c +++ b/port/src/dram.c @@ -53,7 +53,10 @@ #include #else #define _GNU_SOURCE /* memfd_create — must precede all system headers */ +#include +#include #include +#include #include #endif @@ -101,10 +104,20 @@ void *dramReserve(void) CloseHandle(hSec); return v1; #else - /* memfd + two MAP_SHARED mmaps = one backing store, two views. */ + /* One shared file descriptor + two MAP_SHARED mmaps = one backing store, + * two views. Linux can keep it fully anonymous with memfd; Darwin uses a + * POSIX shared-memory object and unlinks its name immediately. */ +#if defined(PLATFORM_MACOS) + char shmName[64]; + snprintf(shmName, sizeof(shmName), "/ge007_dram_%ld", (long)getpid()); + int fd = shm_open(shmName, O_RDWR | O_CREAT, 0600); + if (fd >= 0) + shm_unlink(shmName); +#else int fd = memfd_create("ge007_dram", 0); +#endif if (fd < 0) { - sysFatalError("dram: memfd_create failed"); + sysFatalError("dram: could not create shared backing store"); } if (ftruncate(fd, DRAM_SIZE) != 0) { sysFatalError("dram: ftruncate failed"); diff --git a/port/src/libultra.c b/port/src/libultra.c index 60991a04..215a0d46 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -972,7 +972,7 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size) char win[1200] = ""; char *wp = win; for (int i = 0; i < 32; i++) { - wp += snprintf(wp, win + sizeof(win) - (wp - win), + wp += snprintf(wp, sizeof(win) - (size_t)(wp - win), " %p", (void *)sp[i]); } #if defined(PLATFORM_WINDOWS) diff --git a/port/src/macho_weak_aliases.s b/port/src/macho_weak_aliases.s new file mode 100644 index 00000000..f68f862b --- /dev/null +++ b/port/src/macho_weak_aliases.s @@ -0,0 +1,25 @@ +/* Mach-O equivalents of the decomp's `#pragma weak alias = target`. */ + +.globl _g_DebugPortalsInputBufferSource1 +.set _g_DebugPortalsInputBufferSource1, _g_DebugPortalsInputBuffer1 +.globl _g_DebugPortalsInputBufferSource2 +.set _g_DebugPortalsInputBufferSource2, _g_DebugPortalsInputBuffer2 +.globl _g_DebugPortalsInputBufferSource3 +.set _g_DebugPortalsInputBufferSource3, _g_DebugPortalsInputBuffer3 +.globl _g_DebugPortalsInputBufferSource4 +.set _g_DebugPortalsInputBufferSource4, _g_DebugPortalsInputBuffer4 + +.globl _objectiveGetStatus_WEAK +.set _objectiveGetStatus_WEAK, _get_status_of_objective + +.globl _spec_keyboard_row_caps_z_x_c_v +.set _spec_keyboard_row_caps_z_x_c_v, _spec_keyboard_buffer + +.globl _fsin +.set _fsin, ___sinf +.globl _sinf +.set _sinf, ___sinf +.globl _fcos +.set _fcos, ___cosf +.globl _cosf +.set _cosf, ___cosf diff --git a/port/src/romdata.c b/port/src/romdata.c index d5012af6..785f2d35 100644 --- a/port/src/romdata.c +++ b/port/src/romdata.c @@ -36,6 +36,7 @@ extern int snprintf(char *str, size_t maxsize, const char *format, ...); /* POSIX: mmap the ROM at the fixed cart address (mirrors the VirtualAlloc * path). is a host header the decomp include path does not * shadow. */ + #include #include #endif @@ -382,9 +383,15 @@ int romdataInit(void) int flags = MAP_PRIVATE | MAP_ANONYMOUS; #ifdef MAP_FIXED_NOREPLACE flags |= MAP_FIXED_NOREPLACE; +#elif defined(PLATFORM_MACOS) + /* __PAGEZERO ends at CART_BASE in the macOS build, so this range + * is deliberately vacant. Darwin has no MAP_FIXED_NOREPLACE and + * otherwise treats CART_BASE only as a hint, relocating it. */ + flags |= MAP_FIXED; #endif void *at = mmap((void *)(uintptr_t)CART_BASE, maplen, PROT_READ | PROT_WRITE, flags, -1, 0); + int mapErrno = errno; if (at != MAP_FAILED && at == (void *)(uintptr_t)CART_BASE) { mappedLen = maplen; return romdataFinishCartMap(tok, img, sideTotal, cgTotal); @@ -392,8 +399,8 @@ int romdataInit(void) if (at != MAP_FAILED) munmap(at, maplen); sysLogPrintf(LOG_WARNING, "romdataInit: could not map 0x%08X " - "(ASLR/kernel refused the fixed address); using heap " - "copy — direct ROM reads will fail", CART_BASE); + "(mmap result=%p errno=%d); using heap copy — direct " + "ROM reads will fail", CART_BASE, at, mapErrno); #endif } diff --git a/port/src/video.c b/port/src/video.c index 8318981e..46b5ad1b 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -12,6 +12,9 @@ #include #include +/* Prevent GCC's implicit declaration warning */ +extern void exit(int status); + #if defined(_WIN32) #include #define GE_MKDIR(p) _mkdir(p) @@ -496,8 +499,9 @@ void videoStartFrame(void) * CREATED the window pumps them — and every game thread can be blocked on a * message queue at any time. So the host main thread (which created the * window in videoInit) must keep pumping; otherwise the window goes - * "Not Responding" and ESC/close never arrive. fast3d's own handle_events - * (which runs during rendering) remains as a backstop. + * "Not Responding" and ESC/close never arrive. On macOS AppKit strictly + * requires this main-thread pump, so fast3d's render-thread event handler is + * disabled there; on other platforms it remains as a backstop. */ void videoPumpEvents(void) { diff --git a/scripts/gen_macho_syms.py b/scripts/gen_macho_syms.py new file mode 100644 index 00000000..c2e528e8 --- /dev/null +++ b/scripts/gen_macho_syms.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Convert ELF/PE GAS absolute symbols to Mach-O spelling.""" + +import pathlib +import re +import sys + + +def main() -> int: + if len(sys.argv) != 3: + print(f"usage: {sys.argv[0]} INPUT OUTPUT", file=sys.stderr) + return 2 + + source = pathlib.Path(sys.argv[1]) + output = pathlib.Path(sys.argv[2]) + lines = [] + + for line in source.read_text(encoding="utf-8").splitlines(): + if re.match(r"^\s*\.section\s+\.data\s*$", line): + continue + + match = re.match(r"^(\s*)\.global\s+([A-Za-z_]\w*)\s*$", line) + if match: + lines.append(f"{match.group(1)}.globl _{match.group(2)}") + continue + + match = re.match( + r"^(\s*)\.set\s+([A-Za-z_]\w*)(\s*,.*)$", line + ) + if match: + lines.append( + f"{match.group(1)}.set _{match.group(2)}{match.group(3)}" + ) + continue + + lines.append(line) + + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text("\n".join(lines) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/strip_weak_pragmas.py b/scripts/strip_weak_pragmas.py new file mode 100644 index 00000000..335a1631 --- /dev/null +++ b/scripts/strip_weak_pragmas.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Remove ELF-style weak aliases from a C source copied for Mach-O.""" + +import pathlib +import re +import sys + + +def main() -> int: + if len(sys.argv) != 3: + print(f"usage: {sys.argv[0]} INPUT OUTPUT", file=sys.stderr) + return 2 + + source = pathlib.Path(sys.argv[1]) + output = pathlib.Path(sys.argv[2]) + text = source.read_text(encoding="utf-8") + text = re.sub(r"^[ \t]*#pragma[ \t]+weak[^\n]*\n", "", text, + flags=re.MULTILINE) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(text, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From ae1884cc0595c70792ee0aa3ebff696b1a3a7991 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Thu, 17 Sep 2026 00:35:30 -0700 Subject: [PATCH 02/12] Implement macOS (Intel) support --- CMakeLists.txt | 123 +++++++++++++++++++++++++++++++--- README.md | 10 +-- docs/building.md | 22 ++++-- docs/porting-notes.md | 24 +++++++ port/fast3d/gfx_sdl2.cpp | 7 ++ port/include/pc_protos.h | 11 +++ port/shim/PR/os.h | 12 ++++ port/src/crash.c | 22 ++++-- port/src/dram.c | 17 ++++- port/src/libultra.c | 2 +- port/src/macho_weak_aliases.s | 25 +++++++ port/src/romdata.c | 11 ++- port/src/video.c | 8 ++- scripts/gen_macho_syms.py | 44 ++++++++++++ scripts/strip_weak_pragmas.py | 25 +++++++ 15 files changed, 328 insertions(+), 35 deletions(-) create mode 100644 port/src/macho_weak_aliases.s create mode 100644 scripts/gen_macho_syms.py create mode 100644 scripts/strip_weak_pragmas.py diff --git a/CMakeLists.txt b/CMakeLists.txt index fe0c9500..1d481989 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,11 +25,26 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) # name as an inherited one — CCTVRecord's `pad` — see docs/internals.md.) # An explicit -DCMAKE_C_COMPILER=... always wins. if(NOT DEFINED CMAKE_C_COMPILER) - find_program(_GE007_GCC NAMES gcc) + if(CMAKE_HOST_APPLE) + # Homebrew installs real GNU GCC with a version suffix; /usr/bin/gcc is + # Apple Clang and does not support the Plan 9 struct extensions required + # by the decomp's `inherits` pattern. + find_program(_GE007_GCC + NAMES gcc-16 gcc-15 gcc-14 gcc-13 gcc-12 gcc-11 gcc) + else() + find_program(_GE007_GCC NAMES gcc) + endif() if(_GE007_GCC) set(CMAKE_C_COMPILER "${_GE007_GCC}" CACHE FILEPATH "C compiler" FORCE) get_filename_component(_GE007_GCC_DIR "${_GE007_GCC}" DIRECTORY) - find_program(_GE007_GXX NAMES g++ PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH) + if(CMAKE_HOST_APPLE) + find_program(_GE007_GXX + NAMES g++-16 g++-15 g++-14 g++-13 g++-12 g++-11 g++ + PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH) + else() + find_program(_GE007_GXX NAMES g++ + PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH) + endif() if(_GE007_GXX) set(CMAKE_CXX_COMPILER "${_GE007_GXX}" CACHE FILEPATH "CXX compiler" FORCE) endif() @@ -163,7 +178,18 @@ configure_file( # the toolchain include dir from the compiler location (MinGW/MSYS2 layout: # bin/../include) and generate an absolute-path include. get_filename_component(_PORT_GCC_DIR "${CMAKE_C_COMPILER}" DIRECTORY) -get_filename_component(PORT_HOST_STRING_H "${_PORT_GCC_DIR}/../include/string.h" ABSOLUTE) +if(APPLE) + execute_process( + COMMAND xcrun --show-sdk-path + OUTPUT_VARIABLE _PORT_MACOS_SDK OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if(NOT _PORT_MACOS_SDK) + message(FATAL_ERROR "Cannot locate the macOS SDK; install the Xcode Command Line Tools") + endif() + set(PORT_HOST_STRING_H "${_PORT_MACOS_SDK}/usr/include/string.h") +else() + get_filename_component(PORT_HOST_STRING_H "${_PORT_GCC_DIR}/../include/string.h" ABSOLUTE) +endif() configure_file( "${CMAKE_SOURCE_DIR}/port/include/hoststring.h.in" "${CMAKE_BINARY_DIR}/port/include/hoststring.h" @ONLY) @@ -172,7 +198,11 @@ configure_file( # the include path and shadows the system header; libstdc++'s /gthr # headers in the fast3d C++ TUs need the real (cpu_set_t, # sched_yield). Both MinGW and Linux keep at bin/../include. -get_filename_component(PORT_HOST_SCHED_H "${_PORT_GCC_DIR}/../include/sched.h" ABSOLUTE) +if(APPLE) + set(PORT_HOST_SCHED_H "${_PORT_MACOS_SDK}/usr/include/sched.h") +else() + get_filename_component(PORT_HOST_SCHED_H "${_PORT_GCC_DIR}/../include/sched.h" ABSOLUTE) +endif() configure_file( "${CMAKE_SOURCE_DIR}/port/include/hostsched.h.in" "${CMAKE_BINARY_DIR}/port/include/hostsched.h" @ONLY) @@ -182,7 +212,11 @@ configure_file( # one C++ TU (fast3d): MinGW's own sec_api/stdlib_s.h does `#include # `, which resolves to the N64 header via -I. The shim routes C++ # TUs to the host header by absolute path (same trick as hoststring.h). -get_filename_component(PORT_HOST_STDLIB_H "${_PORT_GCC_DIR}/../include/stdlib.h" ABSOLUTE) +if(APPLE) + set(PORT_HOST_STDLIB_H "${_PORT_MACOS_SDK}/usr/include/stdlib.h") +else() + get_filename_component(PORT_HOST_STDLIB_H "${_PORT_GCC_DIR}/../include/stdlib.h" ABSOLUTE) +endif() configure_file( "${CMAKE_SOURCE_DIR}/port/include/hoststdlib.h.in" "${CMAKE_BINARY_DIR}/port/include/hoststdlib.h" @ONLY) @@ -328,6 +362,13 @@ elseif(UNIX AND NOT APPLE) # relocations are fine (unlike a 0x140000000 base, which needs mcmodel=large). # This is the ELF analogue of the Windows build's fixed 0x140000000 base. set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Ttext-segment=0x20000000") +elseif(APPLE) + # Mach-O executables normally reserve [0, 4 GiB) as __PAGEZERO. The port + # needs fixed mappings at 0x10000000 (ROM), 0x70000000 (s32-safe DRAM), and + # 0x80000000 (KSEG0 mirror), so retain null/low-pointer protection only up + # while leaving ample space before the first required mapping. + set(CMAKE_EXE_LINKER_FLAGS + "${CMAKE_EXE_LINKER_FLAGS} -Wl,-pagezero_size,0x10000") endif() # port/shim first: PC-only shadow headers (bondtypes.h, bondconstants.h, @@ -342,6 +383,7 @@ include_directories( "${CMAKE_SOURCE_DIR}/src/game" "${CMAKE_SOURCE_DIR}/src/libultra" "${CMAKE_SOURCE_DIR}/src/libultra/audio" + "${CMAKE_SOURCE_DIR}/src/libultra/gu" "${CMAKE_SOURCE_DIR}/port/include" "${CMAKE_BINARY_DIR}/port/include" ) @@ -395,6 +437,8 @@ if(NOT EXTRA_LIBRARIES) set(EXTRA_LIBRARIES dbghelp -static-libstdc++ pthread) elseif(WIN32) set(EXTRA_LIBRARIES dbghelp -static-libstdc++) + elseif(APPLE) + set(EXTRA_LIBRARIES stdc++ m pthread) else() set(EXTRA_LIBRARIES stdc++ m dl pthread) endif() @@ -414,14 +458,41 @@ file(GLOB_RECURSE SRC_FAST3D "${CMAKE_SOURCE_DIR}/port/fast3d/*.cpp" # scripts/gen_romassets.py). Defines every obseg/ramrom/music label and the # ge007.ld segment markers as 0x10000000 + rom_offset, so `&symbol` / romCopy() # work once romdata.c maps the .z64 at 0x10000000. -set(SRC_ROMASSETS "${CMAKE_SOURCE_DIR}/port/src/romassets_${ASSET_REGION}.s") -if(NOT EXISTS ${SRC_ROMASSETS}) - message(FATAL_ERROR "Missing ${SRC_ROMASSETS} — run: python3 scripts/gen_romassets.py ${ASSET_REGION}") +set(SRC_ROMASSETS_INPUT "${CMAKE_SOURCE_DIR}/port/src/romassets_${ASSET_REGION}.s") +if(NOT EXISTS ${SRC_ROMASSETS_INPUT}) + message(FATAL_ERROR "Missing ${SRC_ROMASSETS_INPUT} — run: python3 scripts/gen_romassets.py ${ASSET_REGION}") endif() # Absolute symbols inside the reserved N64-DRAM region (port/src/dram.c): # cfb_16 (replaces src/cfb.c) and _bssSegmentEnd (mempool start marker). -set(SRC_DRAM_SYMS "${CMAKE_SOURCE_DIR}/port/src/dram_syms.s") +set(SRC_DRAM_SYMS_INPUT "${CMAKE_SOURCE_DIR}/port/src/dram_syms.s") + +if(APPLE) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + set(SRC_ROMASSETS "${CMAKE_BINARY_DIR}/port/src/romassets_${ASSET_REGION}_macho.s") + set(SRC_DRAM_SYMS "${CMAKE_BINARY_DIR}/port/src/dram_syms_macho.s") + add_custom_command( + OUTPUT "${SRC_ROMASSETS}" + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + "${SRC_ROMASSETS_INPUT}" "${SRC_ROMASSETS}" + DEPENDS "${SRC_ROMASSETS_INPUT}" + "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + VERBATIM) + add_custom_command( + OUTPUT "${SRC_DRAM_SYMS}" + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + "${SRC_DRAM_SYMS_INPUT}" "${SRC_DRAM_SYMS}" + DEPENDS "${SRC_DRAM_SYMS_INPUT}" + "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + VERBATIM) + set_source_files_properties(${SRC_ROMASSETS} ${SRC_DRAM_SYMS} + PROPERTIES GENERATED TRUE) +else() + set(SRC_ROMASSETS "${SRC_ROMASSETS_INPUT}") + set(SRC_DRAM_SYMS "${SRC_DRAM_SYMS_INPUT}") +endif() # Game code — compiled unmodified. file(GLOB_RECURSE SRC_GAME "${CMAKE_SOURCE_DIR}/src/game/*.c") @@ -452,6 +523,39 @@ file(GLOB SRC_LIBULTRARE_AUDIO "${CMAKE_SOURCE_DIR}/src/libultrare/audio/*.c") # gu matrix helpers. file(GLOB SRC_GU "${CMAKE_SOURCE_DIR}/src/libultra/gu/*.c") +# GCC for Darwin crashes while lowering the decomp's ELF-style +# `#pragma weak alias = target` directives. Compile generated copies with +# those directives removed, then provide their aliases in Mach-O assembly. +set(SRC_MACHO_ALIASES "") +if(APPLE) + set(_MACHO_GENERATED_SOURCES "") + set(_MACHO_WEAK_INPUTS + "${CMAKE_SOURCE_DIR}/src/game/lv.c" + "${CMAKE_SOURCE_DIR}/src/game/objective_status.c" + "${CMAKE_SOURCE_DIR}/src/game/spectrum.c" + "${CMAKE_SOURCE_DIR}/src/libultra/gu/sinf.c" + "${CMAKE_SOURCE_DIR}/src/libultra/gu/cosf.c") + foreach(_INPUT IN LISTS _MACHO_WEAK_INPUTS) + get_filename_component(_NAME "${_INPUT}" NAME) + set(_OUTPUT "${CMAKE_BINARY_DIR}/port/src/macho_${_NAME}") + add_custom_command( + OUTPUT "${_OUTPUT}" + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_SOURCE_DIR}/scripts/strip_weak_pragmas.py" + "${_INPUT}" "${_OUTPUT}" + DEPENDS "${_INPUT}" "${CMAKE_SOURCE_DIR}/scripts/strip_weak_pragmas.py" + VERBATIM) + list(APPEND SRC_MACHO_ALIASES "${_OUTPUT}") + list(APPEND _MACHO_GENERATED_SOURCES "${_OUTPUT}") + list(REMOVE_ITEM SRC_GAME "${_INPUT}") + list(REMOVE_ITEM SRC_GU "${_INPUT}") + endforeach() + list(APPEND SRC_MACHO_ALIASES + "${CMAKE_SOURCE_DIR}/port/src/macho_weak_aliases.s") + set_source_files_properties(${_MACHO_GENERATED_SOURCES} + PROPERTIES GENERATED TRUE) +endif() + # VI mode table (DATA, not a driver): fr.c references osViModeTable ~15x. # This is the only libultra/io file we compile — the rest are hardware # drivers that get shimmed in port/src/libultra.c. @@ -569,6 +673,7 @@ set(SRC ${SRC_LIBULTRARE_LIBC} ${SRC_ASSETS} ${SRC_GU} + ${SRC_MACHO_ALIASES} ${SRC_ENGINE} ) diff --git a/README.md b/README.md index bfa84066..c6a627c8 100644 --- a/README.md +++ b/README.md @@ -128,8 +128,8 @@ is right the large majority of the time now, at most a small drift when off; fil by default (F10 → *All unlocked* opens every level, 007 mode and the full cheat menu); F10 in-game options overlay (video, input, gameplay, HUD, graphics, audio; frame cap, MSAA, filtering, FOV, sensitivity, key rebinding, -crosshair, vibration); Windows and -Linux, including Steam Deck. +crosshair, vibration); Windows, Linux and macOS (Intel and Apple Silicon), +including Steam Deck. **Known issues:** @@ -152,8 +152,8 @@ Linux, including Steam Deck. original progression you need to preserve. - Assorted further cosmetic defects are tracked in [`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md). -- No macOS or ARM support. Keyboard/mouse rebinding shipped in v0.4.0; - controller-button rebinding is not supported yet. +- Keyboard/mouse rebinding shipped in v0.4.0; controller-button rebinding is + not supported yet. No Windows-on-ARM support. Root causes and fix status for every item: the [release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases) and the finding log in [`docs/dev/findings.md`](docs/dev/findings.md). @@ -198,7 +198,7 @@ directionally, on the way to v1.0: [`docs/dev/findings.md`](docs/dev/findings.md). - **PAL and JP ROM support** ([issue #85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)); NTSC-U is the only supported region today. - **Controller (pad) button rebinding UI** (keyboard/mouse rebinding shipped - in v0.4.0), and macOS/ARM builds. + in v0.4.0). - **LAN multiplayer**: reviving GoldenEye's original split-screen/deathmatch netplay across multiple PCs on a local network. Genuinely under consideration, but early and not started; no ETA. diff --git a/docs/building.md b/docs/building.md index 5fdf93b4..36e30d36 100644 --- a/docs/building.md +++ b/docs/building.md @@ -1,6 +1,6 @@ --- title: Building -description: Full build and asset-extraction guide for the GoldenEye 007 PC port, covering Windows (MSYS2) and Linux. +description: Full build and asset-extraction guide for the GoldenEye 007 PC port on Windows, Linux, and macOS. --- ## Building the PC port @@ -27,21 +27,29 @@ for accepted versions and hashes. ### Port build -> **The Windows (MSYS2 MINGW64) path is the primary one.** The Linux build is -> compiled by CI on every push and ships in the release bundle (also -> playtested on Steam Deck hardware); the macOS column is best-effort -> guidance and has never been built or run there. Expect to fix build breaks -> yourself on untested platforms. +> **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. | Need | Windows (MSYS2 MINGW64) | Debian/Ubuntu | macOS (Homebrew) | |------|------------------------|---------------|------------------| -| toolchain | `mingw-w64-x86_64-toolchain` | `build-essential` | Xcode CLT / `gcc` | +| toolchain | `mingw-w64-x86_64-toolchain` | `build-essential` | Xcode CLT + Homebrew `gcc` | | CMake | `mingw-w64-x86_64-cmake` | `cmake` | `cmake` | | SDL2 | `mingw-w64-x86_64-SDL2` | `libsdl2-dev` | `sdl2` | | zlib | `mingw-w64-x86_64-zlib` | `zlib1g-dev` | `zlib` | | OpenGL | (in the toolchain) | `libgl1-mesa-dev` | (system) | | Python 3 | `mingw-w64-x86_64-python` | `python3` | `python3` | +On macOS, install the build dependencies with: + +```sh +brew install cmake gcc sdl2 zlib python3 +``` + +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. + ### Asset extraction (decompilation toolchain) The extraction scripts need `binutils-mips-linux-gnu` (or an equivalent MIPS diff --git a/docs/porting-notes.md b/docs/porting-notes.md index 05598fdd..f43ae5ce 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -580,6 +580,30 @@ through a converter or a runtime bswap fixup reads scrambled. ## C2. Port-layer / SDL shims +- **Darwin assembly uses Mach-O symbol spelling.** GAS absolute symbols that + are `.global foo` / `.set foo, address` on ELF/PE must become `.globl _foo` + / `.set _foo, address`; Darwin also rejects the ELF `.section .data` + spelling used by these symbol-only files. Generate transformed build-tree + copies rather than changing the cross-platform source files (D294). +- **Darwin GCC cannot lower ELF-style `#pragma weak alias = target`.** It can + ICE in `assemble_alias`; compile generated copies with those pragmas removed + and provide Mach-O indirect symbols (`.set _alias, _target`) separately + (D294). +- **Mach-O's default `__PAGEZERO` reserves the entire low 4 GiB.** A build + can compile and link successfully yet fail every fixed N64 mapping at + runtime (`0x10000000`, `0x70000000`, `0x80000000`). Link with + `-Wl,-pagezero_size,0x10000` so null-page pointers remain protected while + the cartridge and DRAM ranges are available (D294). +- **Darwin has no `MAP_FIXED_NOREPLACE`; a fixed-address hint can relocate.** + Once `__PAGEZERO` is deliberately bounded below the cart range, use + `MAP_FIXED` for the `0x10000000` cartridge mapping. Retaining the advisory + hint silently falls back to a heap ROM and later crashes on direct absolute + ROM-symbol reads (D294). +- **AppKit event polling is main-thread-only.** This port has both a host-main + `videoPumpEvents()` loop and fast3d's render-thread event pump. The latter + must be disabled on macOS or `SDL_PollEvent` raises + `NSInternalInconsistencyException`; the main-thread loop already handles + the complete event set (D294). - `#include ` in a port `.c`/`.h` that also sees `` breaks: `OSContStatus`/`OSContPad` have a `u8 errno;` field vs errno.h's macro. libultra.c wraps the include in `#pragma push_macro("errno")` / diff --git a/port/fast3d/gfx_sdl2.cpp b/port/fast3d/gfx_sdl2.cpp index e32611e5..d4dfa525 100644 --- a/port/fast3d/gfx_sdl2.cpp +++ b/port/fast3d/gfx_sdl2.cpp @@ -311,6 +311,12 @@ static void gfx_sdl_get_dimensions(uint32_t* width, uint32_t* height, int32_t* p } static void gfx_sdl_handle_events(void) { +#if defined(__APPLE__) + /* AppKit requires event polling on the process main thread. The host + * loop in videoPumpEvents owns the complete SDL queue, so the scheduler + * render thread must not call SDL_PollEvent on macOS. */ + return; +#else SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { @@ -361,6 +367,7 @@ static void gfx_sdl_handle_events(void) { break; } } +#endif } /* The window/context are created on the host main thread, but the game's diff --git a/port/include/pc_protos.h b/port/include/pc_protos.h index 2bccf078..06e1f0f7 100644 --- a/port/include/pc_protos.h +++ b/port/include/pc_protos.h @@ -28,9 +28,20 @@ * "using typedef-name after 'struct'"), and no C++ TU has an implicit * declaration needing a fix here. C++ TUs keep their pre-D38 header exposure. */ + #ifndef _PC_PROTOS_H_ #define _PC_PROTOS_H_ +#if defined(__APPLE__) +/* Darwin SDK headers expose these libc calls as fortify function-like + * macros. This legacy declaration list needs their actual symbol names. */ +#undef memcpy +#undef snprintf +#undef strcat +#undef strcpy +#undef strncpy +#endif + #if defined(PORT) && defined(__x86_64__) && !defined(__cplusplus) #include /* u8..s32, f32, size_t (host on PC) */ diff --git a/port/shim/PR/os.h b/port/shim/PR/os.h index 5378e3ec..56d01a6c 100644 --- a/port/shim/PR/os.h +++ b/port/shim/PR/os.h @@ -28,7 +28,19 @@ #define _PORT_SHIM_OS_H_ #if defined(PORT) +# if defined(__APPLE__) +/* Darwin exposes errno and sprintf as function-like macros. Suppress them + * while parsing the N64 API's errno fields and sprintf declaration. */ +# pragma push_macro("errno") +# pragma push_macro("sprintf") +# undef errno +# undef sprintf +# endif # include "include/PR/os.h" +# if defined(__APPLE__) +# pragma pop_macro("sprintf") +# pragma pop_macro("errno") +# endif # undef OS_K0_TO_PHYSICAL # define OS_K0_TO_PHYSICAL(x) ((u32)((char *)(x) - 0x70000000)) diff --git a/port/src/crash.c b/port/src/crash.c index 9cfab783..136efec3 100644 --- a/port/src/crash.c +++ b/port/src/crash.c @@ -11,6 +11,12 @@ #ifndef _GNU_SOURCE #define _GNU_SOURCE 1 #endif +#if defined(__APPLE__) && !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 700 +#endif +#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE) +#define _DARWIN_C_SOURCE 1 +#endif #include #include @@ -303,7 +309,7 @@ static LONG __stdcall crashHandler(PEXCEPTION_POINTERS exinfo) return EXCEPTION_CONTINUE_EXECUTION; } -#elif defined(PLATFORM_LINUX) +#elif defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS) #include #include @@ -341,7 +347,7 @@ static void crashStackTrace(char *msg, int sig, void *pc, ucontext_t *ucontext, if (siginfo && (sig == SIGSEGV || sig == SIGBUS)) { CRASH_MSG("FAULT ADDR: %p\n", (void *)siginfo->si_addr); } -#if defined(PLATFORM_X86_64) +#if defined(PLATFORM_LINUX) && defined(PLATFORM_X86_64) if (ucontext) { CRASH_MSG("REGS: Rax=%p Rcx=%p Rdx=%p Rsi=%p Rdi=%p R8=%p R9=%p R10=%p R11=%p\n", (void *)ucontext->uc_mcontext.gregs[REG_RAX], @@ -410,10 +416,12 @@ static void crashHandler(int sig, siginfo_t *siginfo, void *ctx) ucontext_t *ucontext = ctx ? (ucontext_t *)ctx : NULL; void *pc = NULL; if (ucontext) { -#ifdef PLATFORM_X86 +#if defined(PLATFORM_LINUX) && defined(PLATFORM_X86) pc = (void *)ucontext->uc_mcontext.gregs[REG_EIP]; -#elif defined(PLATFORM_X86_64) +#elif defined(PLATFORM_LINUX) && defined(PLATFORM_X86_64) pc = (void *)ucontext->uc_mcontext.gregs[REG_RIP]; +#elif defined(PLATFORM_MACOS) && defined(PLATFORM_X86_64) + pc = (void *)ucontext->uc_mcontext->__ss.__rip; #endif } @@ -470,7 +478,7 @@ void crashDumpThreads(const unsigned long *tids, const char **names, int count) } } -#endif /* PLATFORM_WINDOWS / PLATFORM_LINUX */ +#endif /* PLATFORM_WINDOWS / POSIX */ int g_CrashEnabled = 0; @@ -480,7 +488,7 @@ void crashInit(void) SetErrorMode(SEM_FAILCRITICALERRORS); prevExFilter = SetUnhandledExceptionFilter(crashHandler); g_CrashEnabled = 1; -#elif defined(PLATFORM_LINUX) +#elif defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS) struct sigaction sigact = { 0 }; sigact.sa_flags = SA_SIGINFO | SA_ONSTACK; sigact.sa_sigaction = crashHandler; @@ -501,7 +509,7 @@ void crashShutdown(void) if (prevExFilter) { SetUnhandledExceptionFilter(prevExFilter); } -#elif defined(PLATFORM_LINUX) +#elif defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS) sigaction(SIGSEGV, &prevSigAction, NULL); sigaction(SIGABRT, &prevSigAction, NULL); sigaction(SIGBUS, &prevSigAction, NULL); diff --git a/port/src/dram.c b/port/src/dram.c index f0a5f4f4..d0896d67 100644 --- a/port/src/dram.c +++ b/port/src/dram.c @@ -53,7 +53,10 @@ #include #else #define _GNU_SOURCE /* memfd_create — must precede all system headers */ +#include +#include #include +#include #include #endif @@ -101,10 +104,20 @@ void *dramReserve(void) CloseHandle(hSec); return v1; #else - /* memfd + two MAP_SHARED mmaps = one backing store, two views. */ + /* One shared file descriptor + two MAP_SHARED mmaps = one backing store, + * two views. Linux can keep it fully anonymous with memfd; Darwin uses a + * POSIX shared-memory object and unlinks its name immediately. */ +#if defined(PLATFORM_MACOS) + char shmName[64]; + snprintf(shmName, sizeof(shmName), "/ge007_dram_%ld", (long)getpid()); + int fd = shm_open(shmName, O_RDWR | O_CREAT, 0600); + if (fd >= 0) + shm_unlink(shmName); +#else int fd = memfd_create("ge007_dram", 0); +#endif if (fd < 0) { - sysFatalError("dram: memfd_create failed"); + sysFatalError("dram: could not create shared backing store"); } if (ftruncate(fd, DRAM_SIZE) != 0) { sysFatalError("dram: ftruncate failed"); diff --git a/port/src/libultra.c b/port/src/libultra.c index 6c29fe5f..b4b38bc4 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -977,7 +977,7 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size) char win[1200] = ""; char *wp = win; for (int i = 0; i < 32; i++) { - wp += snprintf(wp, win + sizeof(win) - (wp - win), + wp += snprintf(wp, sizeof(win) - (size_t)(wp - win), " %p", (void *)sp[i]); } #if defined(PLATFORM_WINDOWS) diff --git a/port/src/macho_weak_aliases.s b/port/src/macho_weak_aliases.s new file mode 100644 index 00000000..f68f862b --- /dev/null +++ b/port/src/macho_weak_aliases.s @@ -0,0 +1,25 @@ +/* Mach-O equivalents of the decomp's `#pragma weak alias = target`. */ + +.globl _g_DebugPortalsInputBufferSource1 +.set _g_DebugPortalsInputBufferSource1, _g_DebugPortalsInputBuffer1 +.globl _g_DebugPortalsInputBufferSource2 +.set _g_DebugPortalsInputBufferSource2, _g_DebugPortalsInputBuffer2 +.globl _g_DebugPortalsInputBufferSource3 +.set _g_DebugPortalsInputBufferSource3, _g_DebugPortalsInputBuffer3 +.globl _g_DebugPortalsInputBufferSource4 +.set _g_DebugPortalsInputBufferSource4, _g_DebugPortalsInputBuffer4 + +.globl _objectiveGetStatus_WEAK +.set _objectiveGetStatus_WEAK, _get_status_of_objective + +.globl _spec_keyboard_row_caps_z_x_c_v +.set _spec_keyboard_row_caps_z_x_c_v, _spec_keyboard_buffer + +.globl _fsin +.set _fsin, ___sinf +.globl _sinf +.set _sinf, ___sinf +.globl _fcos +.set _fcos, ___cosf +.globl _cosf +.set _cosf, ___cosf diff --git a/port/src/romdata.c b/port/src/romdata.c index d5012af6..785f2d35 100644 --- a/port/src/romdata.c +++ b/port/src/romdata.c @@ -36,6 +36,7 @@ extern int snprintf(char *str, size_t maxsize, const char *format, ...); /* POSIX: mmap the ROM at the fixed cart address (mirrors the VirtualAlloc * path). is a host header the decomp include path does not * shadow. */ + #include #include #endif @@ -382,9 +383,15 @@ int romdataInit(void) int flags = MAP_PRIVATE | MAP_ANONYMOUS; #ifdef MAP_FIXED_NOREPLACE flags |= MAP_FIXED_NOREPLACE; +#elif defined(PLATFORM_MACOS) + /* __PAGEZERO ends at CART_BASE in the macOS build, so this range + * is deliberately vacant. Darwin has no MAP_FIXED_NOREPLACE and + * otherwise treats CART_BASE only as a hint, relocating it. */ + flags |= MAP_FIXED; #endif void *at = mmap((void *)(uintptr_t)CART_BASE, maplen, PROT_READ | PROT_WRITE, flags, -1, 0); + int mapErrno = errno; if (at != MAP_FAILED && at == (void *)(uintptr_t)CART_BASE) { mappedLen = maplen; return romdataFinishCartMap(tok, img, sideTotal, cgTotal); @@ -392,8 +399,8 @@ int romdataInit(void) if (at != MAP_FAILED) munmap(at, maplen); sysLogPrintf(LOG_WARNING, "romdataInit: could not map 0x%08X " - "(ASLR/kernel refused the fixed address); using heap " - "copy — direct ROM reads will fail", CART_BASE); + "(mmap result=%p errno=%d); using heap copy — direct " + "ROM reads will fail", CART_BASE, at, mapErrno); #endif } diff --git a/port/src/video.c b/port/src/video.c index 1739110c..c8d48c64 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -13,6 +13,9 @@ #include #include +/* Prevent GCC's implicit declaration warning */ +extern void exit(int status); + #if defined(_WIN32) #include #define GE_MKDIR(p) _mkdir(p) @@ -794,8 +797,9 @@ void videoStartFrame(void) * CREATED the window pumps them — and every game thread can be blocked on a * message queue at any time. So the host main thread (which created the * window in videoInit) must keep pumping; otherwise the window goes - * "Not Responding" and ESC/close never arrive. fast3d's own handle_events - * (which runs during rendering) remains as a backstop. + * "Not Responding" and ESC/close never arrive. On macOS AppKit strictly + * requires this main-thread pump, so fast3d's render-thread event handler is + * disabled there; on other platforms it remains as a backstop. */ void videoPumpEvents(void) { diff --git a/scripts/gen_macho_syms.py b/scripts/gen_macho_syms.py new file mode 100644 index 00000000..c2e528e8 --- /dev/null +++ b/scripts/gen_macho_syms.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Convert ELF/PE GAS absolute symbols to Mach-O spelling.""" + +import pathlib +import re +import sys + + +def main() -> int: + if len(sys.argv) != 3: + print(f"usage: {sys.argv[0]} INPUT OUTPUT", file=sys.stderr) + return 2 + + source = pathlib.Path(sys.argv[1]) + output = pathlib.Path(sys.argv[2]) + lines = [] + + for line in source.read_text(encoding="utf-8").splitlines(): + if re.match(r"^\s*\.section\s+\.data\s*$", line): + continue + + match = re.match(r"^(\s*)\.global\s+([A-Za-z_]\w*)\s*$", line) + if match: + lines.append(f"{match.group(1)}.globl _{match.group(2)}") + continue + + match = re.match( + r"^(\s*)\.set\s+([A-Za-z_]\w*)(\s*,.*)$", line + ) + if match: + lines.append( + f"{match.group(1)}.set _{match.group(2)}{match.group(3)}" + ) + continue + + lines.append(line) + + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text("\n".join(lines) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/strip_weak_pragmas.py b/scripts/strip_weak_pragmas.py new file mode 100644 index 00000000..335a1631 --- /dev/null +++ b/scripts/strip_weak_pragmas.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Remove ELF-style weak aliases from a C source copied for Mach-O.""" + +import pathlib +import re +import sys + + +def main() -> int: + if len(sys.argv) != 3: + print(f"usage: {sys.argv[0]} INPUT OUTPUT", file=sys.stderr) + return 2 + + source = pathlib.Path(sys.argv[1]) + output = pathlib.Path(sys.argv[2]) + text = source.read_text(encoding="utf-8") + text = re.sub(r"^[ \t]*#pragma[ \t]+weak[^\n]*\n", "", text, + flags=re.MULTILINE) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(text, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 12a814a4e4442374583e93dd814ab3e1594f52ed Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:02:08 -0400 Subject: [PATCH 03/12] romdata: include after ultra64.h (fixes the Linux build on macos) #88 added to the POSIX include block, ahead of ultra64.h. glibc defines errno as a macro, (*__errno_location ()), which rewrites the N64 `u8 errno;` fields in PR/os.h (OSContStatus, OSContPad, OSContRamIo) and fails with "field '__errno_location' declared as a function". Include it after ultra64.h/bondtypes.h instead. Nothing in this file reads those struct fields; errno is only read by the mmap failure diagnostic. No change on Windows. --- port/src/romdata.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/port/src/romdata.c b/port/src/romdata.c index 785f2d35..a034fa60 100644 --- a/port/src/romdata.c +++ b/port/src/romdata.c @@ -35,8 +35,7 @@ extern int snprintf(char *str, size_t maxsize, const char *format, ...); #else /* POSIX: mmap the ROM at the fixed cart address (mirrors the VirtualAlloc * path). is a host header the decomp include path does not - * shadow. */ - #include + * shadow. is included further down, after ultra64.h. */ #include #endif @@ -52,6 +51,15 @@ extern int snprintf(char *str, size_t maxsize, const char *format, ...); #include #include "bondtypes.h" +#if !defined(PLATFORM_WINDOWS) +/* Must come after ultra64.h: libc defines `errno` as a macro (glibc: + * `(*__errno_location ())`), which would rewrite the N64 `u8 errno;` + * fields in PR/os.h (OSContStatus, OSContPad, ...) into a function + * declaration and break the build ("field '__errno_location' declared as + * a function"). Only the mmap-failure diagnostic below reads errno. */ + #include +#endif + #define CART_BASE 0x10000000u static u8 *rom = NULL; /* heap copy (fallback path) */ From d4cc71af0a425a547690bdde05334367c0002b37 Mon Sep 17 00:00:00 2001 From: Dan Turner Date: Sun, 20 Sep 2026 10:00:00 +0100 Subject: [PATCH 04/12] =?UTF-8?q?macos:=20native=20Apple=20Silicon=20(arm6?= =?UTF-8?q?4)=20support=20=E2=80=94=20address=20model,=20boot=20chain,=202?= =?UTF-8?q?1/21=20levels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds native arm64 macOS support on top of #88's Intel macOS layer. The one structural change arm64 needs is an address model. The port realises the N64's 32-bit address space *as host addresses* — DRAM at 0x70000000, cart at 0x10000000, plus ~1700 linker-absolute symbols — which is only sound while host addresses fit in 32 bits. Native arm64 cannot map below 4 GiB (__PAGEZERO; a shrunken pagezero is SIGKILLed at exec; PIE is mandatory; MAP_32BIT fails). So the whole space is relocated to a fixed host base, PORT_ADDR_BASE (16 TiB), with portN64ToHost / portHostToN64 / PORT_N64PTR as the conversion chokepoints. PORT_ADDR_BASE is 0 on every other platform, so all of this is an identity there. That relocation then surfaces a class of latent 64-bit bugs that are invisible at base 0 — any place the decomp round-trips a pointer through a 32-bit integer. The src/ changes here are that class: pointer-width/layout only, no behaviour change, N64 branch textually unchanged, each documented in docs/dev/findings.md (D298/M2). Also: Mach-O absolute-symbol generation at the same base, #pragma weak -> symbol equates, __x86_64__ -> PLATFORM_64BIT, Darwin crash handler/ucontext, shm_open DRAM, the AppKit main-thread guard, and the sidecar generation that was the real boot blocker. Verified: 21/21 solo levels boot, render and run crash-free. --- CMakeLists.txt | 24 +- assets/animationtable_data.h | 4 +- docs/dev/MACOS-ARM64-PLAN.md | 644 +++++++++++++++++++++++++++++++ port/fast3d/gfx_pc.cpp | 58 +-- port/include/pc_protos.h | 4 +- port/include/port_addr.h | 105 +++++ port/shim/PR/os.h | 10 +- port/src/crash.c | 26 ++ port/src/dram.c | 5 +- port/src/libultra.c | 54 ++- port/src/main.c | 7 + port/src/n64stubs.c | 11 +- port/src/port_addr.c | 110 ++++++ port/src/romdata.c | 34 +- port/src/system.c | 55 ++- scripts/gen_macho_syms.py | 32 +- src/audi.c | 6 +- src/boss.c | 2 +- src/game/bg.c | 40 +- src/game/bg.h | 8 + src/game/bondview2.c | 59 ++- src/game/bondview_r.c | 28 +- src/game/chr.c | 22 +- src/game/chr_b.c | 7 +- src/game/chraction.c | 38 +- src/game/chrai.c | 7 +- src/game/explosion.c | 7 +- src/game/front.c | 34 +- src/game/gunfire.c | 19 +- src/game/image.c | 4 +- src/game/image_bank.c | 31 +- src/game/image_bank.h | 7 + src/game/initBondDATAdefaults.c | 9 +- src/game/initactorpropstuff.c | 16 +- src/game/initanitable.c | 12 +- src/game/initunk_005450.c | 2 +- src/game/language.c | 45 ++- src/game/model.c | 62 ++- src/game/mpmenu.c | 16 +- src/game/objecthandler.c | 2 +- src/game/objecthandler_2.c | 9 +- src/game/options.c | 102 ++--- src/game/othermodemicrocode.c | 7 +- src/game/prop.c | 40 +- src/game/propobj.c | 7 +- src/game/rsp.c | 2 +- src/game/stan.c | 12 +- src/game/title.c | 20 +- src/libultra/audio/cseq.c | 2 +- src/libultra/audio/load.c | 8 +- src/libultra/audio/save.c | 2 +- src/libultra/audio/synsetfxmix.c | 2 +- src/libultra/audio/synsetpan.c | 2 +- src/libultra/audio/synsetvol.c | 2 +- src/libultra/audio/synthesizer.c | 4 +- src/mema.c | 9 + src/memp.c | 42 +- src/music.c | 12 +- 58 files changed, 1621 insertions(+), 329 deletions(-) create mode 100644 docs/dev/MACOS-ARM64-PLAN.md create mode 100644 port/include/port_addr.h create mode 100644 port/src/port_addr.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d481989..e52d1fce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -363,12 +363,22 @@ elseif(UNIX AND NOT APPLE) # This is the ELF analogue of the Windows build's fixed 0x140000000 base. set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Ttext-segment=0x20000000") elseif(APPLE) - # Mach-O executables normally reserve [0, 4 GiB) as __PAGEZERO. The port - # needs fixed mappings at 0x10000000 (ROM), 0x70000000 (s32-safe DRAM), and - # 0x80000000 (KSEG0 mirror), so retain null/low-pointer protection only up - # while leaving ample space before the first required mapping. - set(CMAKE_EXE_LINKER_FLAGS - "${CMAKE_EXE_LINKER_FLAGS} -Wl,-pagezero_size,0x10000") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64") + # Apple Silicon: shrinking __PAGEZERO gets the process SIGKILLed at exec + # and PIE is mandatory, so the fixed N64 mappings (0x10000000 / 0x70000000 + # / 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 + # 0x80000000 (KSEG0 mirror), so retain null/low-pointer protection only up + # 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() # port/shim first: PC-only shadow headers (bondtypes.h, bondconstants.h, @@ -475,6 +485,7 @@ if(APPLE) OUTPUT "${SRC_ROMASSETS}" COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + --base "${PORT_ADDR_BASE}" "${SRC_ROMASSETS_INPUT}" "${SRC_ROMASSETS}" DEPENDS "${SRC_ROMASSETS_INPUT}" "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" @@ -483,6 +494,7 @@ if(APPLE) OUTPUT "${SRC_DRAM_SYMS}" COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + --base "${PORT_ADDR_BASE}" "${SRC_DRAM_SYMS_INPUT}" "${SRC_DRAM_SYMS}" DEPENDS "${SRC_DRAM_SYMS_INPUT}" "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" diff --git a/assets/animationtable_data.h b/assets/animationtable_data.h index 45a6c3dd..cbf54911 100644 --- a/assets/animationtable_data.h +++ b/assets/animationtable_data.h @@ -197,7 +197,7 @@ // These are compile time extern constants (so not "really" const), but used the same // as the defines above, which is to index into ptr_animation_table. -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) /* D34 (docs/dev/findings.md): on N64 this segment links at VMA 0 * (ge007.ld), so &ANIM_DATA_x is the record's offset within the * animation_data segment and game code re-bases it with @@ -572,7 +572,7 @@ extern s32 ANIM_DATA_credits_natalya_kissing; extern s32 ANIM_DATA_helicopter_cradle; extern s32 ANIM_DATA_plane_runway; extern s32 ANIM_DATA_helicopter_takeoff; -#endif /* __x86_64__ (D34) */ +#endif /* PLATFORM_64BIT (D34) */ diff --git a/docs/dev/MACOS-ARM64-PLAN.md b/docs/dev/MACOS-ARM64-PLAN.md new file mode 100644 index 00000000..3f0f0398 --- /dev/null +++ b/docs/dev/MACOS-ARM64-PLAN.md @@ -0,0 +1,644 @@ +# macOS Apple Silicon (native arm64) port — implementation plan + +Session 2026-09-18. Scope decision: **native arm64 only** (no Rosetta/x86_64 +build). Nothing in this plan has run yet; §1 is measured, §2 is design, §3 is +the work. Companion: `LINUX-PORT.md` (same shape, earlier platform). + +Execution model: research/design/integration items are run by the session +model ("integrator"); items marked **mechanic** are self-contained briefs for +the Sonnet-backed `.opencode/agents/mechanic.md` subagent. Every mechanic +item lists files, recipe, budget, verify. Items in the same phase that touch +disjoint files may run in parallel (dev-process.md §3). + +--- + +## 0. Summary + +The compiler is not the problem. Apple clang builds 235/237 C TUs of the +existing tree with `-fms-extensions` (in place of GCC's `-fplan9-extensions`) +plus small fixes. fast3d already selects a 4.1 Core GL context on macOS and +uses no compat or >4.1 features. + +**The blocker is the port's address model.** The port realises the N64's +32-bit address space as *host* addresses: DRAM at `0x70000000` (+ KSEG0 +mirror at `0x80000000`), cart at `0x10000000`, and ~1700 linker-absolute +symbols with those values; `osVirtualToPhysical` is a `u32` truncation and +`u32 → pointer` is an identity cast. That works on Windows/Linux because the +image and all mappings sit below 4 GiB. On native arm64 macOS **nothing can be +mapped below 4 GiB**: `__PAGEZERO` covers it, `-pagezero_size` gets the +process SIGKILLed, PIE is mandatory, and `MAP_32BIT` fails. + +The fix (§2) is a **shifted window**: keep the N64 32-bit space intact but +place it at a fixed host base `B` (`PORT_ADDR_BASE`), so every game-visible +32-bit value is *identical* to today's Windows values and only the +`u32 → host pointer` direction changes (add `B`). `B = 0` on Windows/Linux, so +those builds are byte-for-byte unchanged. All conversions go through a +port-owned chokepoint; the residue is a compiler-driven census of raw +`u32 → pointer` casts in game code, each rewritten through a `PORT_*` macro +(the sanctioned D3x ABI/layout class — no logic change). + +Phases: **M0** compile+link on macOS → **M1** address model in `port/` → +**M2** game-code cast census + rewrites → **M3** runtime bring-up (Cocoa +threading, first frame, level sweep) → **M4** packaging/CI/docs. + +--- + +## 0b. Base change (2026-09-18, later): rebased onto PR #88 + GCC + +After M0/M1 were working on Apple clang, PR #88 ("Add native Intel macOS +support", JunielKatarn) turned up: a single commit on this repo's exact base, +targeting **Intel** macOS and explicitly leaving Apple Silicon unsupported. The +arm64 work was rebased onto it rather than maintained in parallel. Branch: +**`macos-arm64-gcc`** (the earlier clang branch `macos-arm64` is superseded). + +**Adopted from #88 — this deletes the corresponding work in §3 M0:** +- Homebrew GNU GCC (`gcc-16`) + libstdc++. Removes every clang workaround: + no `-fms-extensions`/`-Wno-microsoft-anon-tag`, no libc++ wrapper header + shims (the old D295), no `-D_FORTIFY_SOURCE=0`. +- `scripts/gen_macho_syms.py` — build-tree Mach-O symbol generation; the + committed `*.darwin.s` duplicates are gone. +- `scripts/strip_weak_pragmas.py` + `port/src/macho_weak_aliases.s` — the + `#pragma weak` handling, so **`src/` carries no macOS edit for it** (the old + D294 in-source `#if defined(PORT) && defined(__APPLE__)` equates are gone). +- POSIX `shm_open` DRAM backing, Darwin crash handling, and the AppKit + main-thread event-pump guard in `gfx_sdl2.cpp` (that last one was §3 M3.1). + +**Kept from this plan — the arm64 delta:** +- §2's shifted-window address model (M1 / D298). `-pagezero_size 0x10000`, + the core of #88's Intel approach, is fatal on arm64 (shrinking `__PAGEZERO` + gets the process SIGKILLed), so CMake now sets it for x86_64 only and arm64 + uses `PORT_ADDR_BASE`. +- D296 (`__x86_64__` → `PLATFORM_64BIT`). Without it #88's tree does not even + *link* on arm64 — 23 undefined `_ANIM_DATA_*`. +- `gen_macho_syms.py --base` (bakes `PORT_ADDR_BASE` into the absolute + symbols; `--base 0` output is byte-identical to #88's transform). +- arm64 crash registers (`__ss.__pc/__lr/__sp/__fp`) and + `_dyld_get_image_header` / `_NSGetExecutablePath`. +- All of M2 (the game-code re-basing sweep). + +Mergeability consequence: the arm64 contribution is now a small additive delta +on top of #88, and `src/` has no macOS-specific toolchain/alias edits. + +## 1. Measured facts (this machine: macOS 26.3, arm64, Apple clang 21, SDL2 via `sdl2-compat` 2.32) + +| # | Test | Result | +|---|------|--------| +| F1 | `mmap(MAP_FIXED)` at `0x10000000` / `0x70000000` / `0x80000000`, native arm64 | **ENOMEM** — first 4 GiB is `__PAGEZERO` | +| F2 | Link with `-Wl,-pagezero_size,0x4000` (and 0x1000…0x90000000), native arm64 | links; **process SIGKILLed at exec (exit 137)** | +| F3 | `-Wl,-no_pie`, `-Wl,-image_base` | ignored ("Linking with PIE"); image lands at `0x1_0000_0000 + slide`, slide observed `0x32_0000`–`0x4EF_8000` (varies per run) | +| F4 | `mmap(MAP_32BIT)` | defined, **fails**; default thread stacks at `0x16b_xxxx_xxxx` | +| F5 | Same code as x86_64 + `-pagezero_size 0x1000` under Rosetta | all fixed maps succeed (not our target; recorded for completeness) | +| F6 | `mmap(MAP_FIXED)` at `B + n64addr` for `B ∈ {1,2,3,4} << 32` | all succeed — **but `MAP_FIXED` silently replaces existing mappings on macOS** (no `MAP_FIXED_NOREPLACE`), so success ≠ range was free. A 64 MiB `malloc` was observed at `0x4_8400_0000`. | +| F7 | Non-clobbering 4 GiB `PROT_NONE` reservation via `mach_vm_map(VM_FLAGS_FIXED)` (no `OVERWRITE`) | `0x3<<32` free (this run); `0x2,0x4,0x8,0x10,0x20,0x40 <<32` **occupied** (libmalloc/VM regions, varies); **`0x100_0000_0000` (1 TiB), `0x400_0000_0000`, `0x1000_0000_0000` (16 TiB) reserved cleanly**, DRAM+cart carved inside OK | +| F8 | Hinted `mmap` without `MAP_FIXED` | hint ignored (relocated to `0x70_0000_0000`) — cannot rely on hints; must reserve | +| F9 | `shm_open`+`ftruncate`+2×`mmap(MAP_SHARED\|MAP_FIXED)` at `B+0x70000000` / `B+0x80000000` | works; alias verified (`memfd_create` replacement) | +| F10 | `.set _cfb_16, 0x470000000` (64-bit-valued absolute symbol) referenced from C | links, `nm` type `A`, **no rebase fixups** (`dyld_info -fixups`) → ASLR-stable; clang emits GOT loads | +| F11 | `-fplan9-extensions` | unknown argument (hard error). `-fms-extensions` accepts `inherits struct`; derived→base pointer passing becomes `-Wincompatible-pointer-types` (already demoted) | +| F12 | `-fpermissive` (C), `-Og`, `-Wno-unused-but-set-variable`, `-fno-pie` | accepted (`-fpermissive` is a silent no-op in C) | +| F13 | `-Wl,--disable-dynamicbase`, `-Wl,-Ttext-segment=` | ld64 unknown options (already not applied on APPLE) | +| F14 | `.section .data` in both `.s` files | assembler error; `.data` works. `.global LameE` (88 `L*` names in `romassets_u.s`) → "non-local symbol required" until `_`-prefixed | +| F15 | `#pragma weak X = Y` (sinf.c, cosf.c, objective_status.c, lv.c, spectrum.c) | Darwin emits a *local* alias: cross-TU users get undefined symbol; same-TU `extern` + use → "reference to X is ambiguous" (lv.c:603-606, spectrum.c:266) | +| F16 | `-fsyntax-only` sweep, 237 C TUs, with `-fms-extensions -D_FORTIFY_SOURCE=0` + corrected host-header shim paths | **235/237 pass** (fails: lv.c, spectrum.c — F15). 4 fast3d `.cpp` TUs fail on libc++ header shadowing (`include/math.h`, `limits.h`, `assert.h` ahead of the SDK) | +| F17 | `port/include/pc_protos.h:34` is gated on `__x86_64__` | on arm64 all ~400 D38 prototypes vanish → 45–63 implicit-declaration warnings per big TU (bg.c, chrai.c, propobj.c, front.c, boss.c) = silent pointer-return truncation. 23 game/asset sites use the same gate | +| F18 | GL | fallback table lands on `{4,1,CORE}` on macOS; GLSL `#version 410 core`; VAO created for Core; no compat/>4.1 entry points. 2.1 compat is **not** enough (GLSL 1.30 min, FBO/blit/MSAA, `GL_DEPTH_CLAMP`) | + +--- + +## 2. Design: the shifted-window address model + +### 2.1 Invariant + +Let `B = PORT_ADDR_BASE` (compile-time, per platform). Every host pointer that +game code may truncate to 32 bits lives in the window `W = [B, B + 4 GiB)`, +at host address `B + n64addr`. Therefore: + +- `(u32)host_ptr` for any `host_ptr ∈ W` yields **exactly today's Windows + value**. The pointer→u32 direction needs no game-code edits for in-window + pointers. +- `u32 → host pointer` becomes `B + u32`. This is the only direction that + changes, and it is funnelled through one chokepoint. +- On Windows/Linux `B = 0`: every chokepoint is an identity, behaviour and + codegen are unchanged. + +### 2.2 N64-space layout inside `W` (window offsets; unchanged from today) + +| Range | Content | Host mapping | +|---|---|---| +| `[0x0000_0000, 0x1000_0000)` | segmented DL addresses (never host memory) | none (reserved `PROT_NONE`) | +| `[0x1000_0000, 0x2000_0000)` | cart image + sidecars (`CART_BASE`) | `MAP_FIXED` carve (romdata.c) | +| `[0x4000_0000, 0x7000_0000)` | **image-relative encoding** (§2.4) — virtual, never mapped | none | +| `[0x7000_0000, +8 MiB)` | DRAM V1 | `shm_open` view 1 (dram.c) | +| `[0x8000_0000, +8 MiB)` | DRAM V2 / KSEG0 mirror | `shm_open` view 2 | +| `[0xA000_0000, 0xC000_0000)` | game-thread stacks, 8 MiB + guard page each (replaces `MAP_32BIT`) | `MAP_FIXED` carve (libultra.c) | +| everything else | reserved, unmapped | `PROT_NONE` | + +`B` proposal: **`0x1000_0000_0000` (16 TiB)** — reserved cleanly (F7), far +above libmalloc's regions (`0x1..0x8 <<32`, observed) and the mmap hint region +(`0x70_0000_0000`), below `fast3d_ptr_ok`'s `0x8000_0000_0000` bound. Fallback +candidate `0x100_0000_0000`. Item **M1.1** confirms on a second machine and +fixes the constant. + +### 2.3 Startup contract + +`portAddrInit()` is the **first thing in `main()`** (before SDL, before any +large allocation): `mach_vm_map(task, &B, 4 GiB, VM_FLAGS_FIXED, PROT_NONE)`. +Failure is fatal with a clear message — the same contract as the Windows +build's `0x140000000` load-base check (`main.c:176`). Once reserved, carving +sub-ranges with `MAP_FIXED` is safe (we own the range). No heap-copy +fallbacks on macOS: `romdata` cart map failure is fatal, not degraded. + +### 2.4 Chokepoint API — `port/include/port_addr.h` (new) + +```c +#define PORT_ADDR_BASE /* 0 on Windows/Linux; B on PLATFORM_MACOS && PLATFORM_ARM */ +/* u32 (N64/"physical") -> host pointer. */ +static inline void *portN64ToHost(u32 a) { + if (a >= 0x40000000u && a < 0x70000000u) /* image-relative (D131 generalised) */ + return (void *)(portImageBase() + (a - 0x40000000u)); + return (void *)(PORT_ADDR_BASE + (uintptr_t)a); +} +/* host pointer -> u32. In-window: window offset. In-image: 0x40000000 + image offset. */ +static inline u32 portHostToN64(const void *p); +/* Cast-site macro for game code (D3x class). Identity semantics at B == 0. */ +#define PORT_N64PTR(T, expr) ((T *)portN64ToHost((u32)(expr))) +``` + +Rewired to it (all in `port/`): `osVirtualToPhysical`/`osPhysicalToVirtual` +(`libultra.c:1442-1443`), `OS_K0_TO_PHYSICAL`/`OS_PHYSICAL_TO_K0` +(`port/shim/PR/os.h:34,37` — subtract/add `B + 0x70000000`), `PHYS_TO_K0` +(`port/shim/PR/R4300.h:25`, `port/shim/R4300.h:14`), fast3d `seg_addr` +(`gfx_pc.cpp:2881-2935` — replace the `mod_hi` block with the image-relative +rule; add `B` to the `< 0x800000` and pass-through returns), the fast3d +range classifier (`gfx_pc.cpp:630-635` — compare `p - B`), `G_MW_SEGMENT` +(`:2324-2327`) and `G_MW_NUMLIGHT` (`:2317`), mixer `osPhysicalToVirtual` +users (`mixer.c:147,152,297`), `piServiceDma` (`libultra.c:1001`), +`dramHostAddrValid` (`libultra.c:909-911`), `romdataIsCartAddr` +(`romdata.c:441-451`), `n64stubs.c:94-107` (`0x70700000` literal). + +**Why image-relative reproduces D131 exactly on Windows:** the image base is +`0x1_4000_0000`, so `0x40000000 + (p - image_base) == (u32)p` — the current +truncation result. On Linux (`-Ttext-segment=0x20000000`) `&sym` currently +passes through `seg_addr` untouched; under the new rule it round-trips +through `0x4xxxxxxx` to the same pointer. No behaviour change either way. + +### 2.5 Absolute symbols + +`dram_syms.s` (3 symbols) and `romassets_.s` (1685 cart symbols) are +generated with `B +` added on Darwin, Mach-O syntax (`.data`, `_` prefix). +Verified viable by F10. The generator (`scripts/gen_romassets.py`) grows a +`--darwin --base 0x...` mode; `B` is defined in exactly two places +(`port_addr.h` and the CMake variable that feeds the generator) with a +`_Static_assert`/CMake check that they agree. + +### 2.6 The residue: raw casts in game code + +Game code sites that turn a `u32` back into a pointer without going through a +chokepoint are the only thing the model cannot fix by construction. Grep +estimates (heuristic, noisy): 117 `(u32|s32)&sym` in 21 files, ~278 +`(T *)obj->field`-shaped casts, 7 `romptr_t`, 5 existing `PORT_PTRADD`. The +authoritative census is the compiler (item **M2.1**): clang's +`-Wint-to-pointer-cast`, `-Wpointer-to-int-cast` and `-Wint-conversion` +(all warnings, none demoted to off) list every site precisely once M0 links. +Each `u32 → pointer` site becomes `PORT_N64PTR(T, expr)`; each +`&image_symbol → u32` site becomes `portHostToN64(&sym)`; in-window +`ptr → u32` truncations are left alone. Every edit is identity at `B = 0`. + +### 2.7 What this model deliberately does NOT do + +- No PD-style relocatable/offset addressing rewrite of game structures. +- No change to `Gwords {uintptr_t w0, w1}` (already 64-bit-safe). +- No attempt to place the executable inside `W` (F3: not controllable). + +--- + +## 3. Work items + +Sizes: **S** ≤1 h, **M** 1–3 h, **L** half-day+. "mechanic" = Sonnet subagent +with the brief below; "integrator" = session model. Budgets are per +dev-process.md §1; on expiry: revert probes, leave the tree buildable, write up. + +Common to every item — **READ FIRST:** this doc §2; `docs/porting-notes.md` +(headers); `AGENTS.md` non-negotiables. **CONSTRAINTS:** no game-logic +changes; `src/`/`assets/` edits only where an item lists them and only in the +sanctioned mechanical classes; N64 build (`Makefile`, `tools/`, `rsp/`, `ld/`) +untouched; new findings appended to `docs/dev/findings.md` §F/§H under the +next free `Dxx` (last used at time of writing: D293) and indexed at the top of +§F. **VERIFY** for build-affecting items always ends with +`./build-pc.sh ntsc-final` (or the explicit target-only build the item names). + +### Phase M0 — compile and link on macOS arm64 (no runtime expected) + +Goal: `./build-pc.sh ntsc-final` produces `build-pc/ge007.*` on this Mac. +`B` is still 0 in this phase (address model stubbed), so the binary will not +boot; the milestone is a clean link + the M2 census becoming available. + +| ID | Title | Who | Size | Deps | Files | +|---|---|---|---|---|---| +| M0.1 | CMake: Apple-clang toolchain path | mechanic | M | — | `CMakeLists.txt`, `cmake/*.cmake`, `build-pc.sh`, `docs/building.md` | +| M0.2 | `__x86_64__` → `PLATFORM_64BIT` as the 64-bit-PC gate | mechanic | S | — | `port/include/pc_protos.h`, `port/include/platform.h`, + the 23 game/asset sites listed | +| M0.3 | Darwin mode for generated assembly | mechanic | M | M0.1 | `scripts/gen_romassets.py`, `port/src/dram_syms.s` (→ generated), `CMakeLists.txt` (gen invocation) | +| M0.4 | `#pragma weak X = Y` on Darwin | integrator → mechanic | S | — | `src/libultra/gu/sinf.c`, `cosf.c`, `src/game/objective_status.c`, `lv.c`, `spectrum.c`, `port/src/darwin_compat.c` (new) | +| M0.5 | Header/libc clashes on Darwin | mechanic | M | M0.1 | `port/include/pc_protos.h`, `port/src/pc_netorder.c`, `port/shim/*`, `port/src/{romdata,romconvert,pccg,pcmodels,fs}.c`, `port/src/system.c` | +| M0.6 | fast3d C++ TUs vs libc++ header shadowing | mechanic | M | M0.1 | `port/shim/` (new C++-route shims for `math.h`/`limits.h`/`assert.h`), `CMakeLists.txt` | +| M0.7 | macOS branches: `crash.c`, `system.c` | mechanic | M | — | `port/src/crash.c`, `port/src/system.c` | +| M0.8 | macOS stubs for `dram.c`/`romdata.c`/`libultra.c` low-stack (compile-only) | mechanic | S | — | `port/src/dram.c`, `port/src/romdata.c`, `port/src/libultra.c` | +| M0.9 | Link-check + first `findings.md` batch | integrator | S | M0.1–M0.8 | `docs/dev/findings.md` | + +**M0.1 — CMake: Apple-clang toolchain path.** +Recipe: (a) detect clang (`CMAKE_C_COMPILER_ID STREQUAL "Clang"` or +`AppleClang`) and in the `if(NOT MINGW)` block at `CMakeLists.txt:289-298` +emit `-fms-extensions -Wno-microsoft-anon-tag` for clang instead of +`-fplan9-extensions`; drop `-fpermissive` for clang; keep the GCC branch +intact. (b) For clang add `-Wno-error=implicit-int +-Wno-error=incompatible-function-pointer-types` next to the existing +`-Wno-error=` set (`:273-277`); do **not** silence `-Wint-to-pointer-cast`, +`-Wpointer-to-int-cast`, `-Wint-conversion` (they are the M2 census). +(c) `add_definitions(-D_FORTIFY_SOURCE=0)` on APPLE (F16: the N64 +`sprintf` prototype vs `__builtin___sprintf_chk`). (d) Host-header shim +derivation at `:165-205`: on APPLE derive from +`execute_process(xcrun --show-sdk-path)` / `CMAKE_OSX_SYSROOT` → +`${SDK}/usr/include/{string,sched,stdlib}.h` instead of +`${_PORT_GCC_DIR}/../include`. (e) `:39-45`: set +`CMAKE_OSX_DEPLOYMENT_TARGET 11.0` when `CMAKE_SYSTEM_PROCESSOR` is +`arm64`/`aarch64` (currently keys off empty `CMAKE_OSX_ARCHITECTURES` → +10.13). (f) `find_program(gcc)` at `:27-37` — prefer `clang` on APPLE and +update the comment (it says clang rejects `inherits`; F11 shows it does not +with `-fms-extensions`). (g) `build-pc.sh`: brew line already right; add a +note that `zlib` is keg-only (CMake's `find_package(ZLIB)` finds the SDK +zlib — verify, and if so say brew zlib is optional). Budget: 6 configure/build +cycles. Verify: `cmake -S . -B build-pc -DROMID=ntsc-final` succeeds; `cmake +--build build-pc --target ge007 -- -k 2>&1 | tee /tmp/m01.log`; report the +distinct remaining error classes (expected: F14 asm, F15 pragma weak, F17 +implicit decls, libc++ shadowing, `memfd_create`, `crashDumpThreads`) — those +belong to M0.2–M0.8, not this item. + +**M0.2 — `__x86_64__` → `PLATFORM_64BIT`.** Sites (from +`grep -rn __x86_64__ src assets include port`): `port/include/pc_protos.h:34` +(the gate for the whole D38 header — critical, F17); `src/memp.c:66`; +`src/music.c:55,688,711`; `src/game/objecthandler.c:7` (D40 — N64 branch +overruns `.bss` by 12 KB on any 64-bit host); `src/game/initunk_005450.c:41`; +`src/game/rsp.c:276`; `src/game/image_bank.c:170`; +`assets/animationtable_data.h:200-575` (D34); `src/audi.c:10,666,734`; +`src/libultra/audio/load.c:26,200,393,496`; `synthesizer.c:23,125`; +`synsetfxmix.c:22`, `synsetpan.c:22`, `synsetvol.c:22`; `src/save.c:24`; +`port/src/system.c:177` (keep — that one really is x86: `_mm_pause`). Recipe: +replace `defined(__x86_64__)` with `defined(PLATFORM_64BIT)` at each listed +site (spelling change only; every site is an existing, documented port-class +edit); ensure `platform.h` is reachable from those TUs (it is included via +`versioninfo.h`/`-include`? — check; if not, `pc_protos.h`-style +`-include` or add `#include "platform.h"` to `port/shim/bondconstants.h`). +`CMakeLists.txt:217-219` already defines `PLATFORM_64BIT=1` for aarch64 via +`cmake/TargetArch.cmake`. Budget: 2 build cycles. Verify: `grep -rn +__x86_64__ src assets include port` shows only `system.c:177`; build of +`bg.c`, `chrai.c`, `propobj.c`, `front.c`, `boss.c` shows **0** +`-Wimplicit-function-declaration` (was 45–63 each). + +**M0.3 — Darwin mode for generated assembly.** Recipe: add +`--darwin` (and `--base 0xHEX`, default 0) to `scripts/gen_romassets.py`: +emit `.data` not `.section .data`; prefix every symbol with `_`; emit `.set +_sym, (base + value)` — 64-bit values are fine (F10). Regenerate +`port/src/romassets_u.s` (and `_e`/`_j` if the generator supports them — note +they are currently absent, `CMakeLists.txt:417-420`). Convert +`port/src/dram_syms.s` into generator output (or a tiny Darwin twin selected +by CMake) with the same `--base`. Wire the CMake invocation (pass +`PORT_ADDR_BASE` from a cache var; default 0 in M0). Keep ELF output +byte-identical when `--darwin` is absent (diff the regenerated file against +git). Budget: 4 cycles. Verify: `nm build-pc/CMakeFiles/.../romassets_u.s.o +| head` shows `A`-type `_`-prefixed symbols; `nm ... | grep -c ' A '` = 1685; +`dyld_info -fixups build-pc/ge007.* | grep -c romassets-symbol-name` = 0 once +linked (M0.9). + +**M0.4 — `#pragma weak X = Y` on Darwin.** Integrator first (30 min): +read the five sites (`sinf.c:33-34`, `cosf.c:33-34`, `objective_status.c:162` ++ consumer `chrai.c:41,2538`, `lv.c:208-216,603-606`, `spectrum.c:49-50,266`) +and decide per site the minimal semantics-preserving form under +`#if defined(PORT) && defined(__APPLE__)`: for `sinf`/`cosf` a strong +definition in a **new port file** `port/src/darwin_compat.c` +(`float sinf(float x){return __sinf(x);}` …) so the 25 game TUs keep binding +to Rare's polynomial, not libSystem's (fidelity); for the three game-file +aliases, a `PORT`-gated `#define X Y` or wrapper — record the choice as a +`Dxx` (symbol-aliasing mechanism, ABI class). Then mechanic applies it. +Budget: 3 cycles. Verify: `lv.c`, `spectrum.c` compile; link has no +undefined `objectiveGetStatus_WEAK`; `nm build-pc/ge007.* | grep -E +' T _(sinf|cosf)$'` shows both defined in the executable. + +**M0.5 — Header/libc clashes.** Recipe: (a) `ntohl/ntohs`: on APPLE +`` defines them as macros with `__uint32_t` prototypes; +guard the K&R declarations in `pc_protos.h:54-60` and the definitions in +`pc_netorder.c:24-32` with `#if !defined(__APPLE__)` (the game's own macros +are already `#undef`'d in `port/shim/bondconstants.h:37-38`). (b) K&R +redeclarations spelling `size_t` as `unsigned long long` +(`romdata.c:25`, `romconvert.c:28`, `pccg.c:24`, `pcmodels.c:26`, +`fs.c:18`) → `size_t` (identical on all shipping platforms; hard error the +moment the SDK prototype is visible). (c) `system.c:26` `_POSIX_C_SOURCE +199309L` hides `readlink` on Darwin → add `_DARWIN_C_SOURCE` on APPLE. +(d) Anything else M0.1's log lists under this class. Budget: 3 cycles. +Verify: the listed TUs compile warning-free for these classes. + +**M0.6 — fast3d C++ vs libc++.** Recipe: the four `port/fast3d/*.cpp` TUs +fail because `include/math.h`, `include/limits.h`, `include/assert.h` +shadow the SDK ahead of libc++'s ``/``/`` (F16). +Add C++-routed shims in `port/shim/` following the existing pattern +(`port/shim/stdlib.h:16-30`: `#ifdef __cplusplus` → host header by absolute +SDK path via the CMake-generated `host*.h`, else N64 header). Alternative if +that fights libc++: compile the four fast3d TUs with a private include order +(`target_include_directories` on an OBJECT library, SDK first). Budget: 5 +cycles. Verify: all four `.cpp` TUs compile. + +**M0.7 — macOS branches in `crash.c` and `system.c`.** Recipe (crash.c): +change the `PLATFORM_LINUX` block guard (`:306-473`) to `PLATFORM_LINUX || +PLATFORM_MACOS`; replace the glibc `uc_mcontext.gregs[REG_*]` reads +(`:344-358, 413-417`) with a `PLATFORM_MACOS && PLATFORM_ARM` variant +reading `uc->uc_mcontext->__ss.__pc/__fp/__sp/__lr/__x[]` +(``); keep `backtrace`/`backtrace_symbols`/`dladdr`; +ensure `crashDumpThreads` is defined (it is referenced by +`libultra.c:264`); note in the log that symbolication is +`atos -o ge007 -l ` and print `_dyld_get_image_vmaddr_slide(0)` at +crash time. Recipe (system.c): `sysImageBase()` → `(uintptr_t) +_dyld_get_image_header(0)` on APPLE; `sysGetExeDir()` → `_NSGetExecutablePath` ++ `dirname` (or `SDL_GetBasePath`) instead of `"."`; `$S/` resolution at +`:260` (`/proc/self/exe`) → same helper. Budget: 4 cycles. Verify: both TUs +compile; `nm | grep crashDumpThreads` defined once. + +**M0.8 — compile-only macOS stubs (address model lands in M1).** Recipe: +`dram.c`: add a `PLATFORM_MACOS` branch that compiles (`shm_open` skeleton +per F9, addresses still `0x70000000`/`0x80000000` — will fail at runtime, +expected); `romdata.c`: `MAP_FIXED_NOREPLACE` path already `#ifdef`'d — no +change unless the log says otherwise; `libultra.c:417-438` +`portAllocLowStack`: compiles as-is (`MAP_32BIT` exists). Budget: 2 cycles. +Verify: link completes. + +**M0.9 — link-check + findings batch (integrator).** Run the AGENTS.md +verification ritual (`/linkcheck`): undefined/duplicate symbols across the +compiled set; record M0's findings as one `findings.md` batch (F11–F17 each +get a `Dxx` row or are folded into one "macOS toolchain" row) and update +`docs/building.md` "macOS: compiles, does not run yet (address model: M1)". + +### Phase M1 — the address model in `port/` + +Goal: the macOS binary boots to the first frame with `B ≠ 0`, with the +game-code census still pending (expect crashes at un-rewritten cast sites — +those are M2). All items here are `port/`-only. + +| ID | Title | Who | Size | Deps | Files | +|---|---|---|---|---|---| +| M1.1 | Fix `B`; write `port_addr.h` + `portAddrInit()` | integrator | S | M0.9 | `port/include/port_addr.h` (new), `port/src/main.c`, `CMakeLists.txt` | +| M1.2 | Rewire OS chokepoints | mechanic | M | M1.1 | `port/src/libultra.c`, `port/shim/PR/os.h`, `port/shim/PR/R4300.h`, `port/shim/R4300.h`, `port/src/n64stubs.c` | +| M1.3 | DRAM double-map + cart map + stacks inside `W` | mechanic | M | M1.1 | `port/src/dram.c`, `port/src/romdata.c`, `port/src/libultra.c` (`portAllocLowStack`) | +| M1.4 | fast3d `seg_addr` / classifier / moveword on `B` | mechanic | M | M1.1 | `port/fast3d/gfx_pc.cpp` | +| M1.5 | Mixer + PI DMA + validity helpers on `B` | mechanic | S | M1.1 | `port/src/mixer.c`, `port/src/libultra.c` (`piServiceDma`, `dramHostAddrValid`), `port/src/romdata.c` (`romdataIsCartAddr`) | +| M1.6 | Absolute symbols regenerated with `B` | mechanic | S | M0.3, M1.1 | generated `.s` via CMake var | +| M1.7 | Boot attempt + triage | integrator | M | M1.2–M1.6 | — | + +**M1.1 (integrator).** Confirm `B` on a second Mac if available (run the +F7 probe: reserve 4 GiB `PROT_NONE` at candidates via `mach_vm_map +VM_FLAGS_FIXED`); fix `PORT_ADDR_BASE = 0x1000_0000_0000` (fallback +`0x100_0000_0000`). Write `port_addr.h` per §2.4 with `portImageBase()` +(`_dyld_get_image_header(0)` on macOS; `sysImageBase()` elsewhere — on +Windows this must equal `0x140000000` so the image-relative rule reproduces +D131 exactly; assert it). `portAddrInit()` as §2.3, called first in +`main()`; on non-macOS it is a no-op. Expose `PORT_ADDR_BASE` to CMake +(`-DPORT_ADDR_BASE=` cache var, APPLE default the constant, else 0) so M1.6 +can feed the generator; add a `_Static_assert` tying the two. + +**M1.2 (mechanic).** Recipe: `osVirtualToPhysical` → `portHostToN64`, +`osPhysicalToVirtual` → `portN64ToHost` (`libultra.c:1442-1443`); +`OS_K0_TO_PHYSICAL(x)` → `(u32)((uintptr_t)(x) - (PORT_ADDR_BASE + +0x70000000))`, `OS_PHYSICAL_TO_K0(x)` → `portN64ToHost(x)` +(`port/shim/PR/os.h:34,37`); `PHYS_TO_K0` likewise (`port/shim/PR/R4300.h:25`, +`port/shim/R4300.h:14`); `n64stubs.c:94-107` `0x70700000` literal → +`portN64ToHost(0x70700000)`. Budget: 3 cycles. Verify: Windows/Linux +semantics unchanged is provable by inspection (`B = 0` ⇒ identity) — state +it; macOS builds. + +**M1.3 (mechanic).** Recipe: `dram.c` macOS branch: `shm_open` (unique +name, `shm_unlink` immediately) + `ftruncate(8 MiB)` + two +`mmap(MAP_SHARED|MAP_FIXED)` at `B+0x70000000` and `B+0x80000000` (F9); fatal +on any failure (no fallback). `romdata.c`: on macOS `mmap(MAP_FIXED)` at +`B+CART_BASE` (safe post-reservation), fatal on failure — do **not** take the +heap-copy path on macOS (comment why: window invariant). `libultra.c` +`portAllocLowStack`: macOS branch carves 8 MiB + 16 KiB guard per thread from +`[B+0xA0000000, B+0xC0000000)` with a simple bump allocator + free list; +`pthread_attr_setstack` as today. Budget: 4 cycles. Verify: a debug log line +per mapping with the host address; run `build-pc/ge007 -level_09` far enough +to see "DRAM V1 @0x1000_7000_0000 … cart @ … stacks @ …" (crash afterwards is +expected in M1). + +**M1.4 (mechanic).** Recipe (`gfx_pc.cpp`): `seg_addr` (`:2881-2935`) — +the `w1 < 0x800000` branch returns `portN64ToHost(w1 + 0x80000000)`; the +D131 `mod_hi` block is replaced by the image-relative rule (`0x40000000 ≤ w1 +< 0x70000000` → `portN64ToHost(w1)`); the final pass-through stays for +full-width host pointers but a `w1 < 0x1_0000_0000` value there means +"in-window N64 address" → `portN64ToHost`. `G_MW_SEGMENT` (`:2324-2327`) +and `G_MW_NUMLIGHT` (`:2317`) same treatment. Range classifier +(`:630-635`): compare `(uintptr_t)p - PORT_ADDR_BASE` against the cart and +DRAM windows. `fast3d_ptr_ok`/`addr_bad` bounds: unchanged (16 TiB < +128 TiB). Budget: 3 cycles. Verify: builds; on Windows semantics unchanged +(identity) — state the argument per site. + +**M1.5 (mechanic).** `mixer.c:147,152,297` already call +`osPhysicalToVirtual` → covered by M1.2; check `mixer.h`'s `u32 dramAddr` +ABI needs nothing. `piServiceDma` (`libultra.c:1001`): `memcpy` source from +`portN64ToHost(srcPA)`. `dramHostAddrValid` (`:909-911`) and +`romdataIsCartAddr` (`romdata.c:441-451`): ranges offset by `B`. Budget: 2 +cycles. + +**M1.7 (integrator).** Boot `-level_09` (the standard sweep level); triage +the first crash against the census (M2.1), which by now exists. Expect the +first failures at `(T *)u32field` sites in game code. + +### Phase M2 — game-code cast census and rewrites (the D3x class) + +| ID | Title | Who | Size | Deps | Files | +|---|---|---|---|---|---| +| M2.1 | Compiler census | mechanic | S | M0.9 | none (report only) | +| M2.2 | Classify census → work packets by file | integrator | M | M2.1 | `docs/dev/MACOS-ARM64-PLAN.md` (this doc, §3 table) | +| M2.3a–n | Rewrite packets (one per file group, disjoint) | mechanic ×N (parallel) | S–M each | M2.2 | per packet | +| M2.4 | Re-run census; assert zero unclassified | mechanic | S | M2.3 | — | + +**M2.1 — census (mechanic).** Recipe: `cmake --build build-pc -- -k 2>&1 | +tee /tmp/census.log`; extract `grep -E 'warning: .*\[-W(int-to-pointer-cast| +pointer-to-int-cast|int-conversion)\]' /tmp/census.log | sort -u` into +`/tmp/census.txt`; also grep `src/ assets/` for `(u32)&`, `(s32)&`, +`romptr_t`, `PORT_PTRADD`; produce a table `file:line | direction (u32→ptr / +ptr→u32 / implicit) | operand kind (field / &global / local / call result)`. +No edits. Budget: 45 min. + +**M2.2 — classify (integrator).** Per site decide: **(a)** `u32 → pointer` +where the `u32` holds an N64 address (DRAM/cart/stack) → +`PORT_N64PTR(T, expr)`; **(b)** `&global → u32` (image pointer) → +`portHostToN64(&sym)` (identical value on Windows); **(c)** in-window +`pointer → u32` truncation → leave; **(d)** ROM-serialized field (D3x) → +already handled or `PORT_PTRADD`/`romptr_t` pattern; **(e)** genuine +integer (not an address) → leave. Group (a)+(b) into packets by file so +mechanics never collide (dev-process.md §3); each packet lists exact +`file:line` sites and the replacement. Record the class counts in this doc. + +**M2.3 packets (mechanic, parallel).** Per packet brief: FILES = the +packet's files only; recipe = the per-site replacement table; every edit is +one of the two macro forms — no other change; add a one-line `/* D3x/M2: +window-model cast, identity at PORT_ADDR_BASE==0 */`-style comment only +where the file has no adjacent D3x comment already. Budget: 2 cycles per +packet. Verify: packet TUs compile; census diff shows the packet's sites +gone; `./build-pc.sh ntsc-final` green. + +### Phase M3 — runtime bring-up + +| ID | Title | Who | Size | Deps | Files | +|---|---|---|---|---|---| +| M3.1 | Cocoa main-thread hygiene | mechanic | M | M0 | `port/fast3d/gfx_sdl2.cpp`, `port/src/video.c`, `port/src/input.c` | +| M3.2 | Two Core-profile GL bugs + MSAA clamp | mechanic | S | M0 | `port/fast3d/gfx_opengl.cpp`, `port/src/video.c` | +| M3.3 | First frame → intro → menu → `-level_09` | integrator | L | M1.7, M2.4, M3.1 | triage only | +| M3.4 | arm64 float→int semantics check | integrator | M | M3.3 | `docs/dev/findings.md` | +| M3.5 | Level sweep (21 levels, unattended window) | mechanic (runs) + integrator (triage) | L | M3.3 | `docs/dev/LEVEL-STATUS.md` | +| M3.6 | Weak-memory-ordering audit of cross-thread flags | integrator | M | M3.3 | `port/src/video.c`, `optionsoverlay.c`, `libultra.c` | + +**M3.1 (mechanic).** Recipe: remove the render-thread `SDL_PollEvent` pump +(`gfx_sdl2.cpp:311-360`, called from `gfx_pc.cpp:3301`) — the host thread +already pumps (`video.c:502-591`); route Alt+Enter through +`videoRequestFullscreen` (`video.c:298-309`); replace the render-thread +`exit(0)` calls (`gfx_sdl2.cpp:322/348/356`) with a quit request the host +loop honours. Marshal `SDL_SetRelativeMouseMode`/`SDL_ShowCursor` +(`input.c:1227,1256,1310,1313`, reached from render + game threads) onto the +host thread via the existing `winReq*` queue. `SDL_GameControllerUpdate` +(`input.c:622`, game thread) — move to the host pump, snapshot state under +the existing lock. Budget: 5 cycles (Windows/Linux behaviour must be +unchanged — this is a correctness fix everywhere). Verify: builds; on macOS +no `NSInternalInconsistencyException`/main-thread assertions in the log once +M3.3 boots. + +**M3.2 (mechanic).** `gfx_opengl.cpp:1271` binds the vector *index* instead +of `.fbo` (cf. correct `:1335`) — fix; `:1328` `glReadBuffer(GL_FRONT)` when +`use_back=false` (`video.c:722`) is undefined on Cocoa → read the back buffer +or the FBO; clamp `gfx_msaa_level` to `GL_MAX_SAMPLES` before `:1187/:1199`. +Budget: 2 cycles. + +**M3.4 (integrator).** arm64 `fcvtzs` **saturates** and yields 0 for NaN; +x86 `cvttss2si` yields `0x80000000` for overflow and NaN; MIPS yields +`0x7FFFFFFF`. Sites validated against x86 behaviour: `floorFloatToInt` +(`src/game/math_floor.c:42`, 19 call sites), `FTOFIX32` in `guMtxF2L` +(`include/PR/gu.h:38` — also `long` = 64-bit on LP64), D73/D155/D156 guards +(`src/game/model.c`). Re-read each; where behaviour differs for +out-of-range/NaN inputs, decide whether a port-side helper is needed to +match the x86-validated behaviour (a `port/` change; no game-code edit). +Record as a `Dxx`. + +### Phase M4 — packaging, CI, docs + +| ID | Title | Who | Size | Files | +|---|---|---|---|---| +| M4.1 | `.app` bundle target + `Info.plist` + `$S/` under `~/Library/Application Support/ge007` when bundled | mechanic | M | `CMakeLists.txt`, `port/src/system.c`, `port/src/romconvert.c` | +| M4.2 | GitHub Actions `macos-14` (arm64) compile job (mirror of the Ubuntu job) | mechanic | S | `.github/workflows/ci.yml` | +| M4.3 | `docs/building.md` macOS section; README "Status"; AGENTS.md build note (currently says MSYS2 only) | mechanic | S | docs | +| M4.4 | Bundle script (`bundle-mac.sh`, mirrors `bundle-win.sh`; reuse `prepare-assets.py`) | mechanic | M | scripts | + +--- + +## 4. Decisions needed from a human + +1. **`B` value** — `0x1000_0000_0000` proposed (F7). Needs one more machine's + probe before M1.1 fixes it. If no second Mac: accept the fatal-on-failure + contract and revisit if a user report shows the range occupied. +2. **`#pragma weak` handling (M0.4)** touches three game files with a + `PORT`-gated alias/wrapper. Sanctioned as ABI/linker-mechanism class? + (Recommendation: yes, documented as one `Dxx`.) +3. **Heap-copy fallback on macOS = fatal** (§2.3). Confirms we never run + degraded on macOS; a Mac that cannot reserve 4 GiB `PROT_NONE` at `B` + cannot run the port. (Recommendation: yes; it costs nothing — `PROT_NONE` + reservations are free.) +4. **M3.1 changes Windows/Linux behaviour too** (removing the render-thread + event pump). Recommendation: yes — it is an SDL-rules violation on every + platform; verify Windows once after. + +## 5. Risks (ranked) + +1. **Census size (M2).** If the compiler census exceeds a few hundred + `u32 → pointer` sites, M2 becomes the dominant cost. Mitigation: the + rewrites are mechanical and file-partitioned; run 4–6 mechanics in parallel. +2. **Game code keeping host pointers in `u32` fields *implicitly*** (no cast, + `-Wint-conversion`): each is a latent truncation today too; the census + catches them, but classification needs judgement (integrator time). +3. **Pointers reaching game code from host allocations outside `W`** (e.g. a + port-side `malloc` result handed into a game struct) — invisible to the + census. Mitigation: M1.7/M3.3 triage; `dramHostAddrValid`-style debug + assert in `portHostToN64` (env-gated) that logs any out-of-window, + out-of-image pointer. +4. **arm64 float→int and weak memory ordering** (M3.4, M3.6): subtle, + non-crashing divergences; needs framediff against the Windows reference. +5. **SDL2 on macOS is `sdl2-compat` over SDL3** (brew default): behaviour + differences in relative mouse mode / HiDPI are possible; pin to real SDL2 + (`brew install sdl2` gives compat; real SDL2 is `sdl2@2`? — verify in M0.1). +6. **`fast3d_ptr_ok` 47-bit bound** and any other `< 0x8000_0000_0000` + checks are fine at 16 TiB but would not be at ≥128 TiB — do not raise `B` + above that without checking. + +## 6. Status + +| Phase | State | Notes | +|---|---|---| +| 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 | + +### Prerequisites to run (easy to miss) + +Two things must be in `data/` before the binary will render anything, both +documented in `docs/building.md`: + +1. **The ROM** as `data/ge007.ntsc-final.z64` (SHA-1 `abe01e4a…`, matching the + repo's `ge007.u.sha1`). +2. **The PC asset sidecars** — `data/pcmodels-ntsc-final/` and + `data/pccg-ntsc-final/`. These are ROM-derived and **gitignored**, so a + fresh clone has empty directories and the game will look like it has an + address bug when it is really serving raw big-endian ROM bytes (that is + finding **D69** reproducing). Generate them once: + + ```sh + python3 tools_pc/d43_emit.py ntsc-final # 512 model sidecars + python3 tools_pc/d69_emit.py ntsc-final # 25 bg + 27 stan + python3 tools_pc/d88_emit.py ntsc-final --regen # + 21 stage-setup + ``` + + This cost a detour during M2: the `bg.c` big-endian-header crash looked like + an un-rebased pointer but was the missing sidecars. PR #88's author ran the + same three passes before their 1,500-frame `-level_09` run. + +### Where the boot is (2026-09-18, branch `macos-arm64-gcc`) + +`./build-pc/ge007.aarch64 -level_09`, ROM `data/ge007.ntsc-final.z64` +(SHA-1 `abe01e4a…`, matching the repo's `ge007.u.sha1`): + +- ROM load, config, EEPROM, GL 4.1 core, audio, input, threads, VI, timers — OK +- language banks, animation tables, `texReset`/`texLoad`, `gimgSync…` — OK +- faults in `bg.c` `load_bg_file` (level geometry) — the next M2 site + +M2 batches so far (all `#if defined(PORT)` and identity at +`PORT_ADDR_BASE == 0`): the mempool boundary; `language.c` `g_LangBanks`; +`initanitable.c` `expand_ani_table_entries`; `initactorpropstuff.c` anim +groups / `ANIM_PTR`; `model.c` `bitDescriptors`/`bitStream`; `image_bank.c` +`globalbank_rdram_offset` (structural, ~43 sites at once) + `texSetBitstring`; +`image.c` `texLoadFromDisplayList`; `bg.c` `BG_SEG_TO_PTR`/`ptr_bg_data` +(**under review** — the fold's value depends on the stack address, which +differs between Windows and the macOS window, so it needs a careful look). + +### The M2 census is a checklist, not a scoreboard + +The clang census is ~474 game-code rows (`-Wint-conversion` 253, +`-Wpointer-to-int-cast` 122, `-Wint-to-pointer-cast` 54, +`-Wint-to-void-pointer-cast` 35, `-Wvoid-pointer-to-int-cast` 10). Re-basing +edits *add* explicit `u32 → pointer` conversions, so the raw count can rise +while progress is made, and one structural fix can clear dozens of rows. The +honest progress metric is boot-path reach. + +### Next + +1. Finish the `bg.c` level-geometry re-basing (the current fault), then keep + triaging `-level_09` until a frame renders. +2. Refresh §1/§3 for the #88/GCC base (the M0 rows elsewhere in this doc are + historical). +3. M3: the two Core-profile GL bugs (`gfx_opengl.cpp:1271` binds the FBO + index instead of `.fbo`; `glReadBuffer(GL_FRONT)`), the MSAA clamp, then + the level sweep and the arm64 float→int / memory-ordering checks. diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index f0e09f6a..8e0f1718 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -27,6 +27,7 @@ #include "gbiex.h" /* GE's G_TRI4 + PD extension opcodes (see header) */ #include "platform.h" +#include "port_addr.h" #include "gfx_pc.h" #include "gfx_cc.h" @@ -687,7 +688,9 @@ void gfx_texture_cache_delete_range(const uint8_t* start, const uint8_t* end) { // texels (0xED0F...) decode from the swapped pairs (0x4FCC/0xCC4F) as bright // green/pink — the garbled Rareware-logo pixels. static bool gfx_tex_source_is_c_array(const uint8_t* addr) { - const uintptr_t a = (uintptr_t)addr; + // Ranges are N64-space, so strip the window base first (identity at + // PORT_ADDR_BASE==0, where the ranges are the raw addresses). + const uintptr_t a = (uintptr_t)addr - (uintptr_t)PORT_ADDR_BASE; if (a >= 0x10000000u && a < 0x20000000u) return false; // cart map + sidecar if (a >= 0x70000000u && a < 0x90000000u) return false; // V1 dram + KSEG0 mirror return true; // exe image: C-compiled array @@ -3154,11 +3157,20 @@ static void gfx_sp_moveword(uint8_t index, uint16_t offset, uintptr_t data) { rsp.fog_mul = (int16_t)(data >> 16); rsp.fog_offset = (int16_t)data; break; - case G_MW_SEGMENT: + case G_MW_SEGMENT: { // GE registers segment bases as OS_K0_TO_PHYSICAL(ptr); store the // live host pointer so seg_addr() resolves seg+offset correctly. - segmentPointers[(offset >> 2) & 0xff] = (data < 0x800000) ? (data + 0x80000000) : data; + // Values that fit in 32 bits are N64-space (a small physical + // offset, or a V1 address); full 64-bit values are already host + // pointers. Identity at PORT_ADDR_BASE==0. + uintptr_t v = (uintptr_t)data; + if (v < 0x100000000ULL) { + if (v < 0x800000) v += 0x80000000u; + v = (uintptr_t)portN64ToHost((u32)v); + } + segmentPointers[(offset >> 2) & 0xff] = v; break; + } } } @@ -3799,6 +3811,12 @@ static void gfx_sp_vertex_float(size_t n_vertices, size_t dest_index, const Port } static inline void *seg_addr(uintptr_t w1) { + // A full 64-bit host pointer (the gSP* macros pack full pointers into w1; + // the window base makes every real host pointer >= 4 GiB) passes through + // untouched. Only values that fit in 32 bits are N64-space addresses. + if (w1 >= 0x100000000ULL) { + return (void *)w1; + } // GE model files reference GDLs (gSPDisplayList) and vertex arrays by raw // VMA 0x05xxxxxx WITHOUT the LSB set; segment 5 is set per-render to the // live host file base by the game's gSPSegment. Resolve it explicitly @@ -3828,30 +3846,20 @@ static inline void *seg_addr(uintptr_t w1) { return (void *)(segmentPointers[seg] + (w1 & 0x00FFFFFF)); } } - // GE passes OS_K0_TO_PHYSICAL(ptr) == ptr - 0x80000000 for RAM that lives - // in the reserved N64-DRAM region (port/src/dram.c); map it back. The - // region is 8 MB, so any offset below 0x800000 came from there. + // GE passes OS_K0_TO_PHYSICAL(ptr) == ptr - V1 base for RAM that lives in + // the reserved N64-DRAM region (port/src/dram.c); map it back into the V2 + // view. The region is 8 MB, so any offset below 0x800000 came from there. if (w1 < 0x800000) { - return (void *)(w1 + 0x80000000); + return portN64ToHost((u32)(w1 + 0x80000000)); } - // D131: a GBI DL built by game code can reference a COMPILED symbol via - // osVirtualToPhysical() (a u32-returning shim), which truncates the - // module's 0x1_00000000 high word. Seen in explosionRenderPropSmoke: - // gSPMatrix(gdl++, osVirtualToPhysical((void*)&dword_CODE_bss_8007A100), - // ..MODELVIEW) -> w1 == 0x40xxxxxx -> wild deref in gfx_sp_matrix. The - // module is based at 0x140000000 (fixed, no ASLR), and nothing legit - // reaches this fallthrough with a value in [0x40000000, DRAM_V1): DRAM - // (>=0x70000000), KSEG0 (>=0x80000000), segmented addrs and sub-0x800000 - // physical offsets are all handled above. Restore the high word from - // this TU's own load address. - { - static const uintptr_t mod_hi = - ((uintptr_t)(void *)&segmentPointers[0]) & 0xffffffff00000000ULL; - if (mod_hi && w1 >= 0x40000000 && w1 < 0x70000000) { - return (void *)(mod_hi | w1); - } - } - return (void *)w1; + // Everything else is a 32-bit N64-space address: DRAM V1 (0x70xxxxxx), + // KSEG0 (0x80xxxxxx), cart (0x10xxxxxx), or the image-relative encoding + // 0x40000000 + (ptr - image base) produced by osVirtualToPhysical() on a + // pointer into the executable (D131). portN64ToHost applies the window + // base and, for the image-relative range, the image base. On Windows the + // image base is 0x140000000, so this reproduces the old mod_hi|w1 exactly; + // at PORT_ADDR_BASE==0 the whole thing is the identity it was before. + return portN64ToHost((u32)w1); } uintptr_t clearMtx; diff --git a/port/include/pc_protos.h b/port/include/pc_protos.h index 06e1f0f7..fec8d1d7 100644 --- a/port/include/pc_protos.h +++ b/port/include/pc_protos.h @@ -42,7 +42,7 @@ #undef strncpy #endif -#if defined(PORT) && defined(__x86_64__) && !defined(__cplusplus) +#if defined(PORT) && defined(PLATFORM_64BIT) && !defined(__cplusplus) #include /* u8..s32, f32, size_t (host on PC) */ #include /* Gfx, Mtx, Vtx, Light (shimmed on PC) */ @@ -493,5 +493,5 @@ Gfx * watchRenderControllerOpaque(); u32 weaponLoadProjectileModels(); void zbufSetBuffer(); -#endif /* PORT && __x86_64__ && !__cplusplus */ +#endif /* PORT && PLATFORM_64BIT && !__cplusplus */ #endif /* _PC_PROTOS_H_ */ diff --git a/port/include/port_addr.h b/port/include/port_addr.h new file mode 100644 index 00000000..b06ea4f3 --- /dev/null +++ b/port/include/port_addr.h @@ -0,0 +1,105 @@ +#ifndef PORT_ADDR_H +#define PORT_ADDR_H + +/* + * The N64 address-space window (M1 of the macOS port). + * + * The port realises the N64's 32-bit address space as a range of *host* + * addresses, because the game stores addresses in 32-bit fields and 32-bit + * arithmetic and expects `(u32)ptr` to round-trip: + * + * 0x00000000..0x0fffffff segmented DL addresses (never host memory) + * 0x10000000..0x1fffffff cart image + sidecars (CART_BASE) + * 0x40000000..0x6fffffff image-relative encoding (see below) + * 0x70000000..0x707fffff DRAM V1 ("virtual", s32-safe; all RAM symbols) + * 0x80000000..0x807fffff DRAM V2 (KSEG0 mirror of V1) + * 0xa0000000..0xbfffffff game-thread stacks + * + * On Windows/Linux that range starts at host address 0, so `(u32)ptr == + * offset` and the pointer IS the N64 address. Native arm64 macOS cannot map + * below 4 GiB (__PAGEZONE; see docs/dev/MACOS-ARM64-PLAN.md §1), so there the + * whole range is shifted up by PORT_ADDR_BASE. Every game-visible 32-bit value + * is then IDENTICAL to the Windows build; only the u32->host-pointer direction + * adds the base. PORT_ADDR_BASE is 0 everywhere but macOS, so those builds are + * unchanged. + * + * Image-relative encoding: pointers into the executable image cannot be + * truncated into the window (the image is not inside it, and on arm64 it is + * PIE with a per-run slide). They are encoded as 0x40000000 + (ptr - image + * base). On Windows the image base is 0x140000000, so this equals the raw + * `(u32)ptr` truncation the D131 fix relied on; on macOS it makes the round + * trip explicit. Ported game code must use portN64ToHost/portHostToN64 (or + * PORT_N64PTR) rather than a bare cast wherever a 32-bit field carries a + * pointer that is not a plain N64 address. + */ + +#include +#include "platform.h" + +#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. */ +#define PORT_ADDR_BASE 0x100000000000ULL +#else +#define PORT_ADDR_BASE 0x0ULL +#endif + +/* Size of the N64 address space. */ +#define PORT_ADDR_WINDOW (1ULL << 32) + +/* Bound on the executable image's size, for classifying a host pointer as + * image-relative. The binary is a few MB; 256 MiB is comfortably generous. */ +#define PORT_IMAGE_MAX (256u << 20) + +/* Set by portAddrInit(): the executable's load address, and whether the image + * lies outside the window (so a truncated pointer needs the 0x40000000 + * encoding). False on Linux, true on Windows and macOS. */ +#ifdef __cplusplus +extern "C" { +#endif + +extern uintptr_t g_portImageBase; +extern int g_portUseImageRel; + +/* Reserve the window (macOS only) and record the image base. Must run before + * any of the port's fixed mappings and before large allocations. */ +void portAddrInit(void); + +#ifdef __cplusplus +} +#endif + +/* u32 N64/physical address -> live host pointer. */ +static inline void *portN64ToHost(uint32_t a) +{ + if (g_portUseImageRel && a >= 0x40000000u && a < 0x70000000u) { + return (void *)(g_portImageBase + (uintptr_t)(a - 0x40000000u)); + } + return (void *)((uintptr_t)PORT_ADDR_BASE + (uintptr_t)a); +} + +/* Host pointer -> u32, the inverse of portN64ToHost for in-window and image + * pointers. Out-of-range pointers fall back to raw truncation (the pre-M1 + * behaviour); they should not be stored in 32-bit fields. */ +static inline uint32_t portHostToN64(const void *p) +{ + uintptr_t v = (uintptr_t)p; + if (v - (uintptr_t)PORT_ADDR_BASE < (uintptr_t)PORT_ADDR_WINDOW) { + return (uint32_t)(v - (uintptr_t)PORT_ADDR_BASE); + } + if (g_portUseImageRel && v >= g_portImageBase + && v - g_portImageBase < (uintptr_t)PORT_IMAGE_MAX) { + return (uint32_t)(0x40000000u + (v - g_portImageBase)); + } + return (uint32_t)v; +} + +/* 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). */ +#define PORT_N64PTR(T, expr) ((T *)portN64ToHost((uint32_t)(expr))) + +#endif /* PORT_ADDR_H */ diff --git a/port/shim/PR/os.h b/port/shim/PR/os.h index 56d01a6c..ef894c52 100644 --- a/port/shim/PR/os.h +++ b/port/shim/PR/os.h @@ -41,11 +41,19 @@ # pragma pop_macro("sprintf") # pragma pop_macro("errno") # endif +# include "port_addr.h" /* PORT_ADDR_BASE for the V1-base subtractions */ # undef OS_K0_TO_PHYSICAL -# define OS_K0_TO_PHYSICAL(x) ((u32)((char *)(x) - 0x70000000)) +/* Offset of a DRAM V1 pointer from the V1 base. On macOS the V1 base is + * PORT_ADDR_BASE + 0x70000000, so the base must be subtracted too; the result + * is a small (< 8 MB) offset that fits s32 and that fast3d's seg_addr() + * re-bases into the V2/KSEG0 mirror. Identity behaviour at PORT_ADDR_BASE==0. */ +# define OS_K0_TO_PHYSICAL(x) ((u32)((uintptr_t)(x) - (PORT_ADDR_BASE + 0x70000000ULL))) # undef OS_PHYSICAL_TO_K0 +/* Identity: callers pass either a live V1 pointer (which is preserved with + * its full 64-bit value through the 64-bit GBI words) or a small physical + * offset (which fast3d re-bases). Both are unaffected by the window base. */ # define OS_PHYSICAL_TO_K0(x) ((void *)(x)) #else diff --git a/port/src/crash.c b/port/src/crash.c index 136efec3..9f49671f 100644 --- a/port/src/crash.c +++ b/port/src/crash.c @@ -318,6 +318,9 @@ static LONG __stdcall crashHandler(PEXCEPTION_POINTERS exinfo) #include #include #include +#if defined(PLATFORM_MACOS) +#include /* _dyld_get_image_vmaddr_slide (for atos) */ +#endif static struct sigaction prevSigAction; @@ -363,6 +366,27 @@ static void crashStackTrace(char *msg, int sig, void *pc, ucontext_t *ucontext, (void *)ucontext->uc_mcontext.gregs[REG_RSP], (void *)ucontext->uc_mcontext.gregs[REG_RBP]); } +#elif defined(PLATFORM_MACOS) && defined(PLATFORM_ARM) + /* Darwin's uc_mcontext is a POINTER to __darwin_mcontext64; the arm64 + * thread state is __ss.__x[0..28], __fp, __lr, __sp, __pc. */ + if (ucontext) { + CRASH_MSG("REGS: x0=%p x1=%p x2=%p x3=%p x4=%p x5=%p x6=%p x7=%p\n", + (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[0], + (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[1], + (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[2], + (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[3], + (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[4], + (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[5], + (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[6], + (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[7]); + CRASH_MSG("Pc=%p Lr=%p Sp=%p Fp=%p\n", + (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__pc, + (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__lr, + (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__sp, + (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__fp); + } + CRASH_MSG("IMAGE SLIDE: 0x%lx\n", + (unsigned long)(uintptr_t)_dyld_get_image_vmaddr_slide(0)); #endif CRASH_MSG("PC: "); if (pc) { @@ -422,6 +446,8 @@ static void crashHandler(int sig, siginfo_t *siginfo, void *ctx) pc = (void *)ucontext->uc_mcontext.gregs[REG_RIP]; #elif defined(PLATFORM_MACOS) && defined(PLATFORM_X86_64) pc = (void *)ucontext->uc_mcontext->__ss.__rip; +#elif defined(PLATFORM_MACOS) && defined(PLATFORM_ARM) + pc = (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__pc; #endif } diff --git a/port/src/dram.c b/port/src/dram.c index d0896d67..386e90ad 100644 --- a/port/src/dram.c +++ b/port/src/dram.c @@ -48,6 +48,7 @@ #include "platform.h" #include "system.h" +#include "port_addr.h" #if defined(PLATFORM_WINDOWS) #include @@ -60,8 +61,8 @@ #include #endif -#define DRAM_V1_BASE 0x70000000UL /* s32-safe "virtual" view */ -#define DRAM_K0_BASE 0x80000000UL /* KSEG0 mirror view */ +#define DRAM_V1_BASE (PORT_ADDR_BASE + 0x70000000ULL) /* s32-safe "virtual" view */ +#define DRAM_K0_BASE (PORT_ADDR_BASE + 0x80000000ULL) /* KSEG0 mirror view */ #define DRAM_SIZE 0x00800000UL /* 8 MB */ void *dramReserve(void) diff --git a/port/src/libultra.c b/port/src/libultra.c index b4b38bc4..b3bf21d4 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -44,6 +44,7 @@ #include "platform.h" #include "system.h" +#include "port_addr.h" #include "crash.h" /* D38: crashDumpThreads() */ #include "video.h" #include "audio.h" @@ -418,13 +419,40 @@ void osCreateThread(OSThread *t, OSId id, void (*entry)(void *), void *arg, #if !defined(_WIN32) #include /* The decomp aligns/compares stack-buffer pointers by truncating to u32 - * (e.g. `(u32)compbuffer` in image.c texLoad) — an N64 assumption. glibc - * puts pthread stacks above 4 GiB, so those truncations corrupt. Force each - * game-thread stack into the low 2 GiB with MAP_32BIT so every such idiom - * works exactly as on the console. Windows pthread stacks are already low. */ + * (e.g. `(u32)compbuffer` in image.c texLoad) — an N64 assumption. Windows + * pthread stacks are already low, so every such idiom works there. + * + * On macOS the whole N64 window is reserved PROT_NONE by portAddrInit, so each + * game-thread stack is carved from the window's stack region + * [PORT_ADDR_BASE + 0xA0000000, + 0xC0000000), leaving a PROT_NONE guard page + * below it; the stack's host address then truncates to its N64 address exactly + * as on Windows. + * + * On Linux (PORT_ADDR_BASE == 0) the window is not reserved, so keep the + * original MAP_32BIT allocation: addresses below 2 GiB truncate identically. */ +#define PORT_STACK_REGION_OFF 0xA0000000ULL +#define PORT_STACK_REGION_SIZE (512ULL << 20) +#define PORT_STACK_GUARD 0x4000ULL + +static uintptr_t s_stackNext = 0; + static void *portAllocLowStack(size_t sz) { -#if defined(MAP_32BIT) +#if PORT_ADDR_BASE != 0 + const uintptr_t region = (uintptr_t)PORT_ADDR_BASE + PORT_STACK_REGION_OFF; + if (s_stackNext == 0) + s_stackNext = region; + const uintptr_t base = s_stackNext + PORT_STACK_GUARD; + const uintptr_t end = base + sz; + if (end > region + PORT_STACK_REGION_SIZE) + return NULL; + void *p = mmap((void *)base, sz, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); + if (p != (void *)base) + return NULL; + s_stackNext = end; + return p; +#elif defined(MAP_32BIT) void *p = mmap(NULL, sz, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT #if defined(MAP_STACK) @@ -433,8 +461,11 @@ static void *portAllocLowStack(size_t sz) , -1, 0); if (p != MAP_FAILED) return p; -#endif return NULL; +#else + (void)sz; + return NULL; +#endif } #endif @@ -911,7 +942,10 @@ static int s_d61opened = 0; static int dramHostAddrValid(uintptr_t addr, u32 size) { - static const uintptr_t bases[2] = { 0x70000000UL, 0x80000000UL }; + static const uintptr_t bases[2] = { + (uintptr_t)PORT_ADDR_BASE + 0x70000000UL, + (uintptr_t)PORT_ADDR_BASE + 0x80000000UL, + }; for (int i = 0; i < 2; i++) { if (addr >= bases[i] && addr + size <= bases[i] + 0x00800000UL) return 1; @@ -1003,7 +1037,7 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size) sysFatalError("D60: ROM-read target %p + 0x%X not host-mapped " "(src=0x%08X)", dstVA, size, srcPA); } - memcpy(dstVA, (const void *)(uintptr_t)srcPA, size); + memcpy(dstVA, portN64ToHost(srcPA), size); } else { /* OS_WRITE: the game never writes the cart (saves go to EEPROM via * osEeprom*, shimmed separately). Log and drop. */ @@ -1542,8 +1576,8 @@ void osWritebackDCache(void *addr, int size) { (void)addr; (void)size; } void osWritebackDCacheAll(void) { } void osInvalICache(void *addr, int size) { (void)addr; (void)size; } void osInvalDCache(void *addr, int size) { (void)addr; (void)size; } -u32 osVirtualToPhysical(void *va) { return (u32)(uintptr_t)va; } -void *osPhysicalToVirtual(u32 pa) { return (void *)(uintptr_t)pa; } +u32 osVirtualToPhysical(void *va) { return portHostToN64(va); } +void *osPhysicalToVirtual(u32 pa) { return portN64ToHost(pa); } /* ------------------------------------------------------------------------ */ /* Misc */ diff --git a/port/src/main.c b/port/src/main.c index b35e596e..b375b6f6 100644 --- a/port/src/main.c +++ b/port/src/main.c @@ -23,6 +23,7 @@ #include "platform.h" #include "system.h" +#include "port_addr.h" #include "config.h" #include "fs.h" #include "romdata.h" @@ -102,6 +103,12 @@ int main(int argc, char **argv) GE007_ROMID, GE007_VERSION_HASH, GE007_VERSION_CODENAME, GE007_ORIGIN_URL); + /* Reserve the N64 address-space window before anything else maps or + * allocates (macOS: 4 GiB PROT_NONE; elsewhere a no-op). Must precede + * crashInit so the reservation is in place for every later stage, and any + * failure here is reported before SDL/GL are involved. */ + portAddrInit(); + /* Crash handler first, so any failure below is debuggable. */ crashInit(); diff --git a/port/src/n64stubs.c b/port/src/n64stubs.c index dfc01bae..7730a5b8 100644 --- a/port/src/n64stubs.c +++ b/port/src/n64stubs.c @@ -23,6 +23,7 @@ #include #include #include +#include "port_addr.h" /* --- Segment start/end getters (normally linker-script symbols) --------- */ /* On the PC the ROM is loaded by romdata.c; these are unused. Return 0. */ @@ -102,8 +103,14 @@ u8 (*tlbmanageGetTlbAllocatedBlock(void))[TLB_BLOCK_SIZE] * lives up there, at 0x707FFD30). Reclaim ~4 MB of it for the mempool * area; the extra goes to MEMPOOL_STAGE (boss.c:218 gives STAGE * everything that isn't the fixed PERMANENT bank). Stays well clear of - * animations_frame_buffer. */ - return (u8 (*)[TLB_BLOCK_SIZE])0x70700000; + * animations_frame_buffer. + * + * D298 (macOS/arm64): the same reclaim is not quite enough there — the + * first level load exhausts STAGE by ~0x30 bytes at a model rwdata alloc + * (mempAllocBytesInBank spins). Push the ceiling to the last 0x10000 + * before animations_frame_buffer, adding ~960 KiB to STAGE. Same + * reasoning as above; no game-logic change. */ + return (u8 (*)[TLB_BLOCK_SIZE])portN64ToHost(0x707F0000); } /* --- K&R libc helpers (IDO provided these; MinGW's libc does not) -------- */ diff --git a/port/src/port_addr.c b/port/src/port_addr.c new file mode 100644 index 00000000..3ea1ad26 --- /dev/null +++ b/port/src/port_addr.c @@ -0,0 +1,110 @@ +/* + * port_addr.c — the N64 address-space window (see port/include/port_addr.h). + * + * On macOS/arm64 the whole window is reserved PROT_NONE up front so that the + * fixed-address carves (cart, DRAM V1/V2, game-thread stacks, in dram.c / + * romdata.c / libultra.c) land inside a range the process already owns and + * cannot collide with libmalloc or the dyld shared cache. Reserving is cheap: + * PROT_NONE pages are not committed. + * + * On Windows/Linux PORT_ADDR_BASE is 0 and this is a no-op beyond recording + * the image base. + */ + +#include + +#include "platform.h" +#include "system.h" +#include "port_addr.h" + +#if defined(PLATFORM_MACOS) && defined(PLATFORM_ARM) +#include +#include +#endif + +uintptr_t g_portImageBase = 0; +int g_portUseImageRel = 0; + +/* Defined by port/src/dram_syms*.s; must equal PORT_ADDR_BASE + 0x70000000. */ +extern unsigned char cfb_16[]; + +/* ROM-free verification of the address model: window round-trips for each + * region, the absolute-symbol base, and the image-relative encoding. Logs + * PASS/FAIL for each so a run without a ROM still exercises the core. */ +static int portAddrCheck(const char *what, int ok) +{ + sysLogPrintf(ok ? LOG_INFO : LOG_ERROR, "portAddr: %-26s %s", + what, ok ? "PASS" : "FAIL"); + return ok; +} + +static void portAddrSelfTest(void) +{ + const uintptr_t B = (uintptr_t)PORT_ADDR_BASE; + int ok = 1; + ok &= portAddrCheck("host->n64 DRAM V1", + portHostToN64((void *)(B + 0x70001234u)) == 0x70001234u); + ok &= portAddrCheck("n64->host DRAM V1", + portN64ToHost(0x70001234u) == (void *)(B + 0x70001234u)); + ok &= portAddrCheck("host->n64 DRAM V2", + portHostToN64((void *)(B + 0x80005678u)) == 0x80005678u); + ok &= portAddrCheck("host->n64 cart", + portHostToN64((void *)(B + 0x10456789u)) == 0x10456789u); + ok &= portAddrCheck("n64->host cart", + portN64ToHost(0x10456789u) == (void *)(B + 0x10456789u)); + ok &= portAddrCheck("cfb_16 at base+DRAM_V1", + (uintptr_t)cfb_16 == B + 0x70000000u); + if (g_portUseImageRel) { + ok &= portAddrCheck("host->n64 image", + portHostToN64((void *)(g_portImageBase + 0x1234u)) == 0x40001234u); + ok &= portAddrCheck("n64->host image", + portN64ToHost(0x40001234u) == (void *)(g_portImageBase + 0x1234u)); + } + sysLogPrintf(ok ? LOG_INFO : LOG_ERROR, "portAddr: selftest %s", + ok ? "ALL PASS" : "FAILURES"); +} + +void portAddrInit(void) +{ + g_portImageBase = sysImageBase(); + /* The image needs the 0x40000000 encoding iff it lies outside the window, + * i.e. its address does not survive a u32 truncation. False on Linux + * (image at 0x20000000, inside the window), true on Windows (0x140000000) + * and macOS (0x1_00xxxxxx + slide). */ + g_portUseImageRel = (g_portImageBase >= (uintptr_t)PORT_ADDR_WINDOW) ? 1 : 0; + +#if PORT_ADDR_BASE != 0 + { + /* Non-clobbering: VM_FLAGS_FIXED without VM_FLAGS_OVERWRITE fails with + * KERN_NO_SPACE if the range is already in use, rather than silently + * replacing a live mapping (mmap MAP_FIXED on macOS does the latter). */ + mach_vm_address_t addr = (mach_vm_address_t)PORT_ADDR_BASE; + kern_return_t kr = mach_vm_map(mach_task_self(), &addr, + (mach_vm_size_t)PORT_ADDR_WINDOW, 0, + VM_FLAGS_FIXED, MEMORY_OBJECT_NULL, 0, FALSE, + VM_PROT_NONE, VM_PROT_ALL, VM_INHERIT_DEFAULT); + if (kr != KERN_SUCCESS || addr != (mach_vm_address_t)PORT_ADDR_BASE) { + sysFatalError("portAddr: cannot reserve the 4 GiB N64 window at " + "0x%llx (%s). See docs/dev/MACOS-ARM64-PLAN.md sec 2.", + (unsigned long long)PORT_ADDR_BASE, mach_error_string(kr)); + } + } +#endif + + /* The committed .darwin.s absolute symbols must have been generated with + * the same --base; cfb_16 is a region-independent sentinel for that. */ + if ((uintptr_t)cfb_16 != (uintptr_t)PORT_ADDR_BASE + 0x70000000ULL) { + sysFatalError("portAddr: dram_syms base mismatch (cfb_16=%p, expected 0x%llx). " + "Regenerate with: scripts/gen_romassets.py --darwin " + "--base 0x%llx --rom-size --out ... --dram-out ...", + (void *)cfb_16, + (unsigned long long)((uintptr_t)PORT_ADDR_BASE + 0x70000000ULL), + (unsigned long long)PORT_ADDR_BASE); + } + + sysLogPrintf(LOG_INFO, "portAddr: window base=0x%llx image=0x%llx image_rel=%d", + (unsigned long long)PORT_ADDR_BASE, + (unsigned long long)g_portImageBase, g_portUseImageRel); + + portAddrSelfTest(); +} diff --git a/port/src/romdata.c b/port/src/romdata.c index a034fa60..166becde 100644 --- a/port/src/romdata.c +++ b/port/src/romdata.c @@ -45,6 +45,7 @@ extern int snprintf(char *str, size_t maxsize, const char *format, ...); #include "pcmodels.h" #include "pccg.h" #include "romconvert.h" +#include "port_addr.h" /* D50: struct font/fontchar layouts for the font-segment re-layout. Order * matters (see pcmodels.c): ultra64.h must finish before bondtypes.h. */ @@ -60,7 +61,11 @@ extern int snprintf(char *str, size_t maxsize, const char *format, ...); #include #endif -#define CART_BASE 0x10000000u +#define CART_BASE 0x10000000u /* N64 cart address */ +/* Host address the ROM is mapped at (PORT_ADDR_BASE + CART_BASE; identity on + * Windows/Linux). Game-visible 32-bit cart values stay CART_BASE-based; this + * is only for the mapping and the host-side pointers into it. */ +#define CART_HOST ((uintptr_t)portN64ToHost(CART_BASE)) static u8 *rom = NULL; /* heap copy (fallback path) */ static u32 romSize = 0; @@ -240,15 +245,15 @@ static void romdataRaw16Walk(const u8 *ctl, u32 ctlSize, u8 *tbl, u32 tblSize, static int romdataFinishCartMap(const char *tok, u8 *img, u32 sideTotal, u32 cgTotal) { - memcpy((void *)(uintptr_t)CART_BASE, img, romSize); + memcpy((void *)CART_HOST, img, romSize); free(img); mappedAtCartBase = 1; sysLogPrintf(LOG_INFO, "romdataInit: %s (%u bytes) mapped at 0x%08X " "(cart base)%s%s", tok, romSize, CART_BASE, sideTotal ? ", + model sidecars" : "", cgTotal ? ", + bg/stan sidecars" : ""); - pcmodelsLoadSidecars(CART_BASE, romSize); - pccgLoadSidecars(CART_BASE + romSize + pcmodelsTotalSize()); + pcmodelsLoadSidecars(CART_HOST, romSize); + pccgLoadSidecars(CART_HOST + romSize + pcmodelsTotalSize()); /* D55: the RLE folder-menu background at `unknown2` has a big-endian w/h * header that rle_expand_8bit() reads little-endian; match the N64 .data @@ -374,9 +379,9 @@ int romdataInit(void) { u32 maplen = romSize + sideTotal + cgTotal; #if defined(PLATFORM_WINDOWS) - void *at = VirtualAlloc((LPVOID)CART_BASE, maplen, + void *at = VirtualAlloc((LPVOID)CART_HOST, maplen, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); - if (at == (void *)(uintptr_t)CART_BASE) + if (at == (void *)CART_HOST) return romdataFinishCartMap(tok, img, sideTotal, cgTotal); if (at) VirtualFree(at, 0, MEM_RELEASE); @@ -387,8 +392,10 @@ int romdataInit(void) /* POSIX: anonymous fixed-address mmap. MAP_FIXED_NOREPLACE (Linux * 4.17+) fails instead of clobbering an existing mapping; where it * is unavailable the plain hint is advisory and the == check below - * catches a relocated result. */ + * catches a relocated result. On macOS portAddrInit has already + * reserved the whole window, so MAP_FIXED lands inside our range. */ int flags = MAP_PRIVATE | MAP_ANONYMOUS; +#ifndef PLATFORM_MACOS #ifdef MAP_FIXED_NOREPLACE flags |= MAP_FIXED_NOREPLACE; #elif defined(PLATFORM_MACOS) @@ -397,10 +404,13 @@ int romdataInit(void) * otherwise treats CART_BASE only as a hint, relocating it. */ flags |= MAP_FIXED; #endif - void *at = mmap((void *)(uintptr_t)CART_BASE, maplen, +#else + flags |= MAP_FIXED; +#endif + void *at = mmap((void *)CART_HOST, maplen, PROT_READ | PROT_WRITE, flags, -1, 0); int mapErrno = errno; - if (at != MAP_FAILED && at == (void *)(uintptr_t)CART_BASE) { + if (at != MAP_FAILED && at == (void *)CART_HOST) { mappedLen = maplen; return romdataFinishCartMap(tok, img, sideTotal, cgTotal); } @@ -425,9 +435,9 @@ void romdataDestroy(void) { if (mappedAtCartBase) { #if defined(PLATFORM_WINDOWS) - VirtualFree((LPVOID)CART_BASE, 0, MEM_RELEASE); + VirtualFree((LPVOID)CART_HOST, 0, MEM_RELEASE); #else - munmap((void *)(uintptr_t)CART_BASE, (size_t)mappedLen); + munmap((void *)CART_HOST, (size_t)mappedLen); mappedLen = 0; #endif mappedAtCartBase = 0; @@ -440,7 +450,7 @@ void romdataDestroy(void) const u8 *romdataGetRom(void) { if (mappedAtCartBase) - return (const u8 *)(uintptr_t)CART_BASE; + return (const u8 *)CART_HOST; return rom; } u32 romdataGetRomSize(void) { return romSize; } diff --git a/port/src/system.c b/port/src/system.c index 6b54d3d7..c1fb6896 100644 --- a/port/src/system.c +++ b/port/src/system.c @@ -27,6 +27,9 @@ #include #include #include + #if defined(PLATFORM_MACOS) + #include /* _dyld_get_image_header, _NSGetExecutablePath */ + #endif #endif #include "system.h" @@ -59,12 +62,46 @@ uintptr_t sysImageBase(void) { #if defined(PLATFORM_WINDOWS) return (uintptr_t)GetModuleHandleW(NULL); +#elif defined(PLATFORM_MACOS) + /* The mach_header of the main image == its load address. Unlike Windows + * there is no fixed base: arm64 is PIE-only (and `-image_base` is ignored), + * so this is 0x1_0000_0000 + a per-run ASLR slide. M1's image-relative + * address rule uses this; get the slide from _dyld_get_image_vmaddr_slide. */ + return (uintptr_t)_dyld_get_image_header(0); #else /* TODO: parse /proc/self/maps for the first executable mapping. */ return 0; #endif } +/* Fill `buf` with the directory containing the executable, or "" on failure. + * Linux reads /proc/self/exe; macOS uses _NSGetExecutablePath (no /proc, and + * _POSIX_C_SOURCE=199309L above hides readlink on Darwin). */ +static void getExeDir(char *buf, size_t buflen) +{ + buf[0] = 0; +#if defined(PLATFORM_MACOS) + uint32_t n = (uint32_t)buflen; + if (_NSGetExecutablePath(buf, &n) == 0) { + char *slash = strrchr(buf, '/'); + if (slash) *slash = 0; else buf[0] = 0; + } else { + buf[0] = 0; + } +#elif defined(PLATFORM_LINUX) + ssize_t n = readlink("/proc/self/exe", buf, buflen - 1); + if (n > 0) { + buf[n] = 0; + char *slash = strrchr(buf, '/'); + if (slash) *slash = 0; else buf[0] = 0; + } else { + buf[0] = 0; + } +#else + (void)buf; (void)buflen; +#endif +} + int64_t sysGetTime(void) { return (int64_t)time(NULL); @@ -201,6 +238,13 @@ const char *sysGetExeDir(void) *slash = 0; } } +#else + if (exeDir[0] == '.' && exeDir[1] == 0) { + char d[512]; + getExeDir(d, sizeof(d)); + if (d[0]) + snprintf(exeDir, sizeof(exeDir), "%s", d); + } #endif return exeDir; } @@ -257,16 +301,7 @@ const char *sysResolvePath(const char *path) else { static char sexedir[1024] = ""; if (!sexedir[0]) { - ssize_t n = readlink("/proc/self/exe", sexedir, sizeof(sexedir) - 1); - if (n > 0) { - char *slash; - sexedir[n] = 0; - slash = strrchr(sexedir, '/'); - if (slash) - *slash = 0; - else - sexedir[0] = 0; - } + getExeDir(sexedir, sizeof(sexedir)); } if (sexedir[0]) snprintf(out, sizeof(out), "%s/data/%s", sexedir, path + 3); diff --git a/scripts/gen_macho_syms.py b/scripts/gen_macho_syms.py index c2e528e8..c6bde320 100644 --- a/scripts/gen_macho_syms.py +++ b/scripts/gen_macho_syms.py @@ -1,18 +1,33 @@ #!/usr/bin/env python3 -"""Convert ELF/PE GAS absolute symbols to Mach-O spelling.""" +"""Convert ELF/PE GAS absolute symbols to Mach-O spelling. +Darwin needs `.data` (not `.section .data`), `_`-prefixed symbol names, and +`.globl` (not `.global`). The cross-platform source files in port/src/ are left +untouched; the Mach-O spelling is generated into the build tree. + +--base is the Apple Silicon extension: PORT_ADDR_BASE, the host address the +whole N64 address space is shifted to (see port/include/port_addr.h). It is +added to every absolute value, so `cfb_16` becomes PORT_ADDR_BASE + 0x70000000. +With --base 0 (Intel) the output is byte-identical to the original transform. +""" + +import argparse import pathlib import re import sys def main() -> int: - if len(sys.argv) != 3: - print(f"usage: {sys.argv[0]} INPUT OUTPUT", file=sys.stderr) - return 2 + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("input") + ap.add_argument("output") + ap.add_argument("--base", type=lambda s: int(s, 0), default=0, + help="host address of the N64 address space " + "(PORT_ADDR_BASE; default 0)") + args = ap.parse_args() - source = pathlib.Path(sys.argv[1]) - output = pathlib.Path(sys.argv[2]) + source = pathlib.Path(args.input) + output = pathlib.Path(args.output) lines = [] for line in source.read_text(encoding="utf-8").splitlines(): @@ -25,11 +40,14 @@ def main() -> int: continue match = re.match( - r"^(\s*)\.set\s+([A-Za-z_]\w*)(\s*,.*)$", line + r"^(\s*)\.set\s+([A-Za-z_]\w*)(\s*,\s*)(0[xX][0-9A-Fa-f]+|\d+)(.*)$", + line, ) if match: + value = int(match.group(4), 0) + args.base lines.append( f"{match.group(1)}.set _{match.group(2)}{match.group(3)}" + f"0x{value:X}{match.group(5)}" ) continue diff --git a/src/audi.c b/src/audi.c index da660aaf..269b7196 100644 --- a/src/audi.c +++ b/src/audi.c @@ -7,7 +7,7 @@ #include "speed_graph.h" #if defined(PORT) #include /* PC port: memcpy for the CUSTOM_FX_PARAMS_N copy in audioInit (docs/dev/findings.md §11) */ -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) #include #include #ifdef PORT @@ -666,7 +666,7 @@ s32 amDmaCallback(s32 addr, s32 len, void* state) /* mark it used */ dmaPtr->lastFrame = (s32) g_AudioFrameCount; freeBuffer = (dmaPtr->ptr + addr) - dmaPtr->startAddr; -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) if (GE_ENVFLAG("GE_MIXERTRACE")) { static FILE *dtf2 = NULL; if (!dtf2) { dtf2 = fopen("mixertrace.log", "a"); if (dtf2) setvbuf(dtf2, NULL, _IONBF, 0); } @@ -734,7 +734,7 @@ s32 amDmaCallback(s32 addr, s32 len, void* state) dmaPtr->lastFrame = g_AudioFrameCount; osPiStartDma(&g_DmaIOMessageBuffer[g_NextDMa++], OS_MESG_PRI_HIGH, OS_READ, (u32)addr, freeBuffer, AUDIO_DMA_MAX_BUFFER_LENGTH, &g_DmaMessageQueue); -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) if (GE_ENVFLAG("GE_MIXERTRACE")) { static FILE *dtf3 = NULL; if (!dtf3) { dtf3 = fopen("mixertrace.log", "a"); if (dtf3) setvbuf(dtf3, NULL, _IONBF, 0); } diff --git a/src/boss.c b/src/boss.c index 0f019a32..b73b5330 100644 --- a/src/boss.c +++ b/src/boss.c @@ -833,7 +833,7 @@ void bossMainloop(void) } rspReplyMsg = (s32)(&localGfxDoneMsg); - rspGfxTaskStart(firstGdl, gdl, 0, (s32*)rspReplyMsg); + rspGfxTaskStart(firstGdl, gdl, 0, PORT_N64PTR(s32, rspReplyMsg)); pendingGfx++; memaSingleDefragPass(); diff --git a/src/game/bg.c b/src/game/bg.c index 2bed832a..2e46be86 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -7,6 +7,12 @@ #include #include #include "bg.h" +#if defined(PORT) +#include "port_addr.h" +#else +/* N64 build: the port address window is the identity (see port_addr.h). */ +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #include "bondview.h" #include "chr.h" #include "debug_camera.h" @@ -879,22 +885,22 @@ void load_bg_file(LEVEL_INDEX levelid) lightFixtureInitTables(); ptr_bg_data = (s32)header; - obLoadBGFileBytesAtOffset(levelinfotable[levelentry_index].bg_seg_filename, (u8 *) ptr_bg_data, 0, 0x40); + obLoadBGFileBytesAtOffset(levelinfotable[levelentry_index].bg_seg_filename, (u8 *)PORT_N64PTR(void, ptr_bg_data), 0, 0x40); if (((levelid && ptr_bg_data) && levelentry_index)); ptr_bgdata_offsets = ptr_bg_data; - ptr_bgdata_room_fileposition_list = (bg_room_data *) BG_SEG_TO_PTR(ptr_bg_data, ((s32 *)ptr_bg_data)[1]); + ptr_bgdata_room_fileposition_list = (bg_room_data *) BG_SEG_TO_PTR(ptr_bg_data, ((s32 *)PORT_N64PTR(void, ptr_bg_data))[1]); size = (((((u32) ptr_bgdata_room_fileposition_list[1].pPointTableBin) & 0x00ffffff) - 1) | 0xf) + 1; ptr_bg_data = (s32) mempAllocBytesInBank(size, 4); - obLoadBGFileBytesAtOffset(levelinfotable[levelentry_index].bg_seg_filename, (u8 *) ptr_bg_data, 0, size); + obLoadBGFileBytesAtOffset(levelinfotable[levelentry_index].bg_seg_filename, (u8 *)PORT_N64PTR(void, ptr_bg_data), 0, size); gptr_stan = (s32) _fileNameLoadToBank(levelinfotable[levelentry_index].bg_stan_filename, 2, 0, 4); - stanDetermineEOF((struct StanPrefixRecord *) gptr_stan, 0, (u8 *) gptr_stan); - stanLoadFile((struct StanPrefixRecord *) gptr_stan); + stanDetermineEOF((struct StanPrefixRecord *)PORT_N64PTR(void, gptr_stan), 0, (u8 *)PORT_N64PTR(void, gptr_stan)); + stanLoadFile((struct StanPrefixRecord *)PORT_N64PTR(void, gptr_stan)); sub_GAME_7F0B4810(levelinfotable[levelentry_index].levelscale); setLevelScale(levelinfotable[levelentry_index].levelscale); @@ -906,7 +912,7 @@ void load_bg_file(LEVEL_INDEX levelid) sub_GAME_7F08976C(mCurrentLevelVisibilityScale); matrix_4x4_7F058C4C(mCurrentLevelVisibilityScale); - data = (s32 *)ptr_bg_data; + data = (s32 *)PORT_N64PTR(void, ptr_bg_data); dword_CODE_bss_8007BF98 = *data; dword_CODE_bss_8007FF88 = 1; @@ -914,7 +920,7 @@ void load_bg_file(LEVEL_INDEX levelid) { dword_CODE_bss_8007FF88 = 2; ptr_bgdata_offsets = (s32)data; - ptr_bgdata_room_fileposition_list = (bg_room_data *) BG_SEG_TO_PTR(data, ((s32 *)ptr_bgdata_offsets)[1]); + ptr_bgdata_room_fileposition_list = (bg_room_data *) BG_SEG_TO_PTR(data, ((s32 *)PORT_N64PTR(void, ptr_bgdata_offsets))[1]); // Keep this fake goto for matching. goto dummy_label_543534; dummy_label_543534: ; @@ -926,25 +932,25 @@ void load_bg_file(LEVEL_INDEX levelid) g_MaxNumRooms++; } - g_BgPortals = (bg_portal_data_entry *) BG_SEG_TO_PTR(data, ((s32 *)ptr_bgdata_offsets)[2]); + g_BgPortals = (bg_portal_data_entry *) BG_SEG_TO_PTR(data, ((s32 *)PORT_N64PTR(void, ptr_bgdata_offsets))[2]); if (1); - if (((s32 *)ptr_bgdata_offsets)[3] == 0) + if (((s32 *)PORT_N64PTR(void, ptr_bgdata_offsets))[3] == 0) { dword_CODE_bss_8007FF90 = 0; } else { - dword_CODE_bss_8007FF90 = (s32 *) BG_SEG_TO_PTR(data, ((s32 *)ptr_bgdata_offsets)[3]); + dword_CODE_bss_8007FF90 = (s32 *) BG_SEG_TO_PTR(data, ((s32 *)PORT_N64PTR(void, ptr_bgdata_offsets))[3]); - if (((s32 *)ptr_bgdata_offsets)[4] == 0) + if (((s32 *)PORT_N64PTR(void, ptr_bgdata_offsets))[4] == 0) { dword_CODE_bss_8007FF94 = NULL; } else { - dword_CODE_bss_8007FF94 = (f32 *) BG_SEG_TO_PTR(data, ((s32 *)ptr_bgdata_offsets)[4]); + dword_CODE_bss_8007FF94 = (f32 *) BG_SEG_TO_PTR(data, ((s32 *)PORT_N64PTR(void, ptr_bgdata_offsets))[4]); } } @@ -3149,7 +3155,7 @@ void bgBuildRoomVtxBounds(s32 roomID) numvertices = ((gdl[cmdindex].dma.par >> 4) & 0xf) + 1; #endif - vtx = (Vtx *)(SEGMENT_OFFSET(gdl[cmdindex].dma.addr) + (u32)vertices); + vtx = (Vtx *)PORT_N64PTR(void, SEGMENT_OFFSET(gdl[cmdindex].dma.addr) + (u32)vertices); #if defined(PORT) /* TEMP D69 safety net: the room primary/secondary DL binaries @@ -3573,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) + ((s32) ptr_bgdata_room_fileposition_list)) + 12), from, to, dir, &hitbuf)) + 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)) { tcmd = gdl; dx = ((s32) hitbuf.hitpos.x) - ((s32) from->x); @@ -3758,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) + ((s32) ptr_bgdata_room_fileposition_list)) + 12), from, to, dir, &hitbuf)) + 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)) { tcmd = gdl; dx = ((s32) hitbuf.hitpos.x) - ((s32) from->x); @@ -5241,7 +5247,7 @@ void bgRoomCalcBB(s32 room) // canonical name StanRoomBounds limits; u8 wasloaded; - roomdata = (bg_room_data *) ((s32) ptr_bgdata_room_fileposition_list + room * 24); + roomdata = (bg_room_data *) ((uintptr_t) ptr_bgdata_room_fileposition_list + room * 24); if (roomdata->pPointTableBin == NULL) { @@ -5274,7 +5280,7 @@ void bgRoomCalcBB(s32 room) // canonical name } vertices = g_BgRoomInfo[room].vertices; - roomdata = (bg_room_data *) ((s32) ptr_bgdata_room_fileposition_list + room * 24); + roomdata = (bg_room_data *) ((uintptr_t) ptr_bgdata_room_fileposition_list + room * 24); limits.minX = 0x7fff; limits.minY = 0x7fff; diff --git a/src/game/bg.h b/src/game/bg.h index 71012a97..89ac7e1e 100644 --- a/src/game/bg.h +++ b/src/game/bg.h @@ -20,7 +20,15 @@ struct levelentry // cannonical name #define PORTMAX 200 +/* D298/M2: the sum is an N64/window offset, so re-base it into the DRAM + * window before it becomes a pointer. Identity at PORT_ADDR_BASE == 0. */ +#if defined(PORT) +#include "port_addr.h" +#define BG_SEG_TO_PTR(base, off) \ + ((void *) portN64ToHost((u32)(((u32)(base)) + (((u32)(off)) + 0xF1000000)))) +#else #define BG_SEG_TO_PTR(base, off) ((void *) (((u32) (base)) + (((u32) (off)) + 0xF1000000))) +#endif typedef struct RoomVtxBatchBounds { s16 gdlindex; // 0x00 diff --git a/src/game/bondview2.c b/src/game/bondview2.c index f565b948..abd1741b 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -1,4 +1,9 @@ #include +#if defined(PORT) +#include "port_addr.h" +#else +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #ifdef PORT #include #include @@ -93,8 +98,15 @@ #if defined(VERSION_US) + /* D298/M2: the font-table globals are s32 holding truncated pointers + * (D88 class); re-base on use. Identity at PORT_ADDR_BASE == 0. */ +#if defined(PORT) + #define BONDVIEW_2ND_FONTTABLE(_param) PORT_N64PTR(void, copy_2ndfonttable) + #define BONDVIEW_1ST_FONTTABLE(_param) PORT_N64PTR(void, copy_1stfonttable) +#else #define BONDVIEW_2ND_FONTTABLE(_param) copy_2ndfonttable #define BONDVIEW_1ST_FONTTABLE(_param) copy_1stfonttable +#endif #elif defined(VERSION_JP) || defined(VERSION_EU) #define BONDVIEW_2ND_FONTTABLE(_param) dword_CODE_bss_jp80079CEC[_param] #define BONDVIEW_1ST_FONTTABLE(_param) dword_CODE_bss_jp80079Cd8[_param] @@ -723,7 +735,7 @@ void solo_char_load(void) pitemheader = NULL; } - something_with_generating_object(self, prop, item, 0, (WeaponObjRecord *)helddst, (ItemModelFileRecord *)pitemheader); + something_with_generating_object(self, prop, item, 0, PORT_N64PTR(WeaponObjRecord, helddst), (ItemModelFileRecord *)pitemheader); } chrlvMergeKneelToStand(self, 0.0f); @@ -981,7 +993,7 @@ void bondviewSetCameraMode(s32 arg0) solo_char_load(); // HACK: ptr_animation_table->data regalloc is backwards - sp38 = (struct ModelAnimation *)((s32)stage_intro_anim_table[g_IntroAnimationIndex].anonymous_0 + (s32)&ptr_animation_table->data); + sp38 = PORT_N64PTR(struct ModelAnimation, (s32)stage_intro_anim_table[g_IntroAnimationIndex].anonymous_0 + (s32)&ptr_animation_table->data); sp78 = stage_intro_anim_table[g_IntroAnimationIndex].anonymous_2; ftemp_1 = stage_intro_anim_table[g_IntroAnimationIndex].anonymous_1; ftemp_3 = stage_intro_anim_table[g_IntroAnimationIndex].anonymous_3; @@ -1226,7 +1238,15 @@ void bondviewCalcIntroSwirlCamera(s32 index, f32 time, coord3d *pos, coord3d *lo { struct SetupIntroSwirl *base; struct SetupIntroSwirl *loopbase; +#if defined(PORT) + /* D189/D298: written pointbuf[0..11] via `&pointbuf[i*3]` then dst[3..5] + * (i = -1..2), but declared [10] -- a 2-float stack overrun that on arm64 + * corrupts the saved frame pointer (Fp=0x374d56eac1a00000 at the fault). + * N64/MinGW absorb it; give it room on the port. */ + f32 pointbuf[12]; +#else f32 pointbuf[10]; +#endif struct SetupIntroSwirl *swirl; f32 frac; f32 *dst; @@ -1317,7 +1337,7 @@ void bondviewCalcIntroSwirlCamera(s32 index, f32 time, coord3d *pos, coord3d *lo lookat->y = g_CurrentPlayer->field_3C8; lookat->z = g_CurrentPlayer->field_3CC; - swirl = (void *)(((u32) g_IntroSwirl) + (u32) base); + swirl = (void *)(((uintptr_t) g_IntroSwirl) + (uintptr_t) base); /* D298/M2: full-width */ if (!(swirl->bitflags & 4)) { @@ -1371,13 +1391,13 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos, #if defined(VERSION_US) setFontTables(ptrFontZurichBoldChars, ptrFontZurichBold); #ifdef PORT - hudmsgBottomShow((char *)(uintptr_t)ptr_random06cam_entry->lang1c.lang_ptr); + hudmsgBottomShow(PORT_N64PTR(char, ptr_random06cam_entry->lang1c.lang_ptr)); #else hudmsgBottomShow(ptr_random06cam_entry->lang1c.lang_ptr); #endif #else #ifdef PORT - hudmsgBottomShow((char *)(uintptr_t)ptr_random06cam_entry->lang1c.lang_ptr, ptrFontZurichBoldChars, ptrFontZurichBold); + hudmsgBottomShow(PORT_N64PTR(char, ptr_random06cam_entry->lang1c.lang_ptr), ptrFontZurichBoldChars, ptrFontZurichBold); #else hudmsgBottomShow(ptr_random06cam_entry->lang1c.lang_ptr, ptrFontZurichBoldChars, ptrFontZurichBold); #endif @@ -1390,13 +1410,13 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos, { #if defined(VERSION_US) #ifdef PORT - hudmsgBottomShow((char *)(uintptr_t)ptr_random06cam_entry->lang20.lang_ptr); + hudmsgBottomShow(PORT_N64PTR(char, ptr_random06cam_entry->lang20.lang_ptr)); #else hudmsgBottomShow(ptr_random06cam_entry->lang20.lang_ptr); #endif #else #ifdef PORT - hudmsgBottomShow((char *)(uintptr_t)ptr_random06cam_entry->lang20.lang_ptr, ptrFontZurichBoldChars, ptrFontZurichBold); + hudmsgBottomShow(PORT_N64PTR(char, ptr_random06cam_entry->lang20.lang_ptr), ptrFontZurichBoldChars, ptrFontZurichBold); #else hudmsgBottomShow(ptr_random06cam_entry->lang20.lang_ptr, ptrFontZurichBoldChars, ptrFontZurichBold); #endif @@ -7975,6 +7995,7 @@ void bondviewFrozenMoveBond(s8 stick_x, s8 stick_y, u16 buttons, u16 oldbuttons) return; } + room_pointer_tile = (struct StandTile *)0; bondviewFrozenCameraTick(buttons, oldbuttons, &property_pos, &property_pos2, &property_offset, &room_pointer_tile, &stan_walk_start); currentPlayerSetCameraMode(1); bondviewSetCurrentPlayerPosition(&property_pos, &property_pos2, &property_offset, room_pointer_tile, &stan_walk_start); @@ -8445,12 +8466,12 @@ void bondviewUpdateCameraMatrices(coord3d* cam_pos, coord3d* cam_look_dir, coord clpos.x, clpos.y, clpos.z, cam_up->x, cam_up->y, cam_up->z); - matrix_4x4_set_lookat((Mtxf*) g_CurrentPlayer->field_64, + matrix_4x4_set_lookat(PORT_N64PTR(Mtxf, g_CurrentPlayer->field_64), cam_pos->x, cam_pos->y, cam_pos->z, cam_look_dir->x, cam_look_dir->y, cam_look_dir->z, cam_up->x, cam_up->y, cam_up->z); - matrix_4x4_set_basis_and_position((Mtxf*) g_CurrentPlayer->field_68, + matrix_4x4_set_basis_and_position(PORT_N64PTR(Mtxf, g_CurrentPlayer->field_68), cam_pos->x, cam_pos->y, cam_pos->z, cam_look_dir->x, cam_look_dir->y, cam_look_dir->z, cam_up->x, cam_up->y, cam_up->z); @@ -8481,13 +8502,13 @@ void bondviewUpdateCameraMatrices(coord3d* cam_pos, coord3d* cam_look_dir, coord scale = bgGetLevelVisibilityScale(); matrix_scalar_multiply(scale, spC4.m[0]); - guMtxF2L((f32 (*)[4]) &spC4, (Mtx* ) g_CurrentPlayer->field_5C); - sub_GAME_7F059334((s32* ) g_CurrentPlayer->field_5C, (s32* ) g_CurrentPlayer->field_60); + guMtxF2L((f32 (*)[4]) &spC4, PORT_N64PTR(Mtx, g_CurrentPlayer->field_5C)); + sub_GAME_7F059334(PORT_N64PTR(s32, g_CurrentPlayer->field_5C), PORT_N64PTR(s32, g_CurrentPlayer->field_60)); - currentPlayerSetMatrix10C8((Mtx* ) g_CurrentPlayer->field_5C); - currentPlayerSetMatrix10C4((Mtx* ) g_CurrentPlayer->field_60); - currentPlayerSetMatrix10CC((Mtxf* ) g_CurrentPlayer->field_64); - currentPlayerSetViewToWorldMtxf((Mtxf* ) g_CurrentPlayer->field_68); + currentPlayerSetMatrix10C8(PORT_N64PTR(Mtx, g_CurrentPlayer->field_5C)); + currentPlayerSetMatrix10C4(PORT_N64PTR(Mtx, g_CurrentPlayer->field_60)); + currentPlayerSetMatrix10CC(PORT_N64PTR(Mtxf, g_CurrentPlayer->field_64)); + currentPlayerSetViewToWorldMtxf(PORT_N64PTR(Mtxf, g_CurrentPlayer->field_68)); sub_GAME_7F078464((s32) lookat); bondviewUpdateFrustumPlanes(); @@ -10380,7 +10401,13 @@ Gfx* hudmsgBottomRender(Gfx* arg0) } view_vert = view_top - view_top_offset; +#ifdef PORT + /* D298/M2: the (s32) casts are an N64 match hack; on the port they + * truncate stack pointers. Pass them at full width. */ + arg0 = draw_blackbox_to_screen(arg0, &view_left, &view_vert, &view_horiz, &view_top); +#else arg0 = draw_blackbox_to_screen(arg0, (s32) &view_left, (s32) &view_vert, (s32) &view_horiz, (s32) &view_top); +#endif arg0 = combiner_bayer_lod_perspective(textRenderOutlined(arg0, &view_left, &view_vert, stringbuffer_lowerleft[status_bar_text_buffer_index], BONDVIEW_2ND_FONTTABLE(status_bar_text_buffer_index), BONDVIEW_1ST_FONTTABLE(status_bar_text_buffer_index), -1, 0x646464FFU, (s16) (s32) viGetX(), (s16) viGetY(), 0, 0)); } } @@ -11393,7 +11420,7 @@ void sub_GAME_7F08BEEC(Mtxf *matrices, s32 count) for (i = 0, j = 0; i < count; i++, j += sizeof(Mtxf)) { - matrix_4x4_multiply_homogeneous(currentPlayerGetViewToWorldMtxf(), (Mtxf *)((u32)matrices + j), &sp40); + matrix_4x4_multiply_homogeneous(currentPlayerGetViewToWorldMtxf(), (Mtxf *)((uintptr_t)matrices + j), &sp40); /* D298/M2 */ sp40.m[3][0] -= g_CurrentPlayer->current_model_pos.f[0]; sp40.m[3][1] -= g_CurrentPlayer->current_model_pos.f[1]; diff --git a/src/game/bondview_r.c b/src/game/bondview_r.c index 1a32f115..7774c9eb 100644 --- a/src/game/bondview_r.c +++ b/src/game/bondview_r.c @@ -1,4 +1,9 @@ #include +#if defined(PORT) +#include "port_addr.h" +#else +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #ifdef PORT #include #include @@ -189,7 +194,7 @@ void bondviewLoadSetupIntroSection(void) startpadcount++; } - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroSpawn)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroSpawn)); } break; @@ -226,7 +231,7 @@ void bondviewLoadSetupIntroSection(void) } } - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroItem)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroItem)); } break; @@ -237,7 +242,7 @@ void bondviewLoadSetupIntroSection(void) give_cur_player_ammo(((struct SetupIntroAmmo*)intro_record)->ammo_type, ((struct SetupIntroAmmo*)intro_record)->ammo_amount); } - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroAmmo)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroAmmo)); } break; @@ -256,7 +261,7 @@ void bondviewLoadSetupIntroSection(void) intro_swirl->unk14.fval = intro_swirl->unk14.ival / M_U16_MAX_VALUE_F; intro_swirl->unk18.fval = intro_swirl->unk18.ival / M_U16_MAX_VALUE_F; - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroSwirl)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroSwirl)); } break; @@ -264,7 +269,7 @@ void bondviewLoadSetupIntroSection(void) { g_IntroAnimationIndex = ((struct SetupIntroAnim*)intro_record)->intro_anim; - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroAnim)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroAnim)); } break; @@ -272,7 +277,7 @@ void bondviewLoadSetupIntroSection(void) { g_CurrentPlayer->bondtype = ((struct SetupIntroCuff*)intro_record)->bondtype; - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroCuff)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroCuff)); } break; @@ -311,7 +316,7 @@ void bondviewLoadSetupIntroSection(void) } } - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroCamera)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroCamera)); } break; @@ -333,7 +338,7 @@ void bondviewLoadSetupIntroSection(void) if (watch_time_0); - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroWatch)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroWatch)); } break; @@ -351,7 +356,7 @@ void bondviewLoadSetupIntroSection(void) credits++; } - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroCredits)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroCredits)); } break; @@ -360,7 +365,7 @@ void bondviewLoadSetupIntroSection(void) #ifdef DEBUG ossyncprintf("unknown bondstart type %d!\n",intro_record->type); #endif - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroEmpty)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroEmpty)); } break; @@ -389,7 +394,8 @@ void bondviewLoadSetupIntroSection(void) { rand_camera_index--; #ifdef PORT - ptr_random06cam_entry = (struct SetupIntroCamera *)(uintptr_t)ptr_random06cam_entry->prev; + /* D88/D298: prev is a u32 holding the truncated host pointer; re-base. */ + ptr_random06cam_entry = PORT_N64PTR(struct SetupIntroCamera, ptr_random06cam_entry->prev); #else ptr_random06cam_entry = ptr_random06cam_entry->prev; #endif diff --git a/src/game/chr.c b/src/game/chr.c index f0c6f819..cb0a7a1d 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -33,6 +33,16 @@ #include "model.h" #include "tex.h" +/* D298/M2: animation_table_ptrs1/2 slots and vtxstore_allocate() results hold + * N64/window addresses as s32; re-base where they become pointers. Identity at + * PORT_ADDR_BASE == 0. */ +#if defined(PORT) +#include "port_addr.h" +#else +/* N64 build: the port address window is the identity (see port_addr.h). */ +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif + #ifdef PORT #include #include @@ -69,7 +79,7 @@ static s32 d193aAnimIndex(void *anim) { s32 i; if (anim == NULL) return -1; for (i = 0; animation_table_ptrs1[i] != 0; i++) { - if ((void *)(uintptr_t)(u32)animation_table_ptrs1[i] == anim) return i; + if (PORT_N64PTR(void, animation_table_ptrs1[i]) == anim) return i; } return -1; } @@ -2491,11 +2501,11 @@ s32 chrTick(PropRecord *prop) { if (D_8002C904) { - if (((ModelAnimation *)animation_table_ptrs1[g_AnimationTablePointerCountRelated]) != ((ModelAnimation *)1)) + if (PORT_N64PTR(ModelAnimation, animation_table_ptrs1[g_AnimationTablePointerCountRelated]) != PORT_N64PTR(ModelAnimation, 1)) { - if (objecthandlerGetModelAnim(model) != ((ModelAnimation *)animation_table_ptrs1[g_AnimationTablePointerCountRelated])) + if (objecthandlerGetModelAnim(model) != PORT_N64PTR(ModelAnimation, animation_table_ptrs1[g_AnimationTablePointerCountRelated])) { - modelSetAnimation(model, (ModelAnimation *)animation_table_ptrs1[g_AnimationTablePointerCountRelated], 0, 0.0f, 0.5f, 0.0f); + modelSetAnimation(model, PORT_N64PTR(ModelAnimation, animation_table_ptrs1[g_AnimationTablePointerCountRelated]), 0, 0.0f, 0.5f, 0.0f); } } } @@ -3581,7 +3591,7 @@ after_opcode: if (((ModelRwData_DisplayList_CollisionRecord *) rwdata)->Vertices == ((ModelRoData_DisplayList_CollisionRecord *) node)->Vertices) { - newvertices = (Vertex *) vtxstore_allocate(((ModelRoData_DisplayList_CollisionRecord *) node)->numVertices, 0xcccc, 0, 0); + newvertices = PORT_N64PTR(Vertex, vtxstore_allocate(((ModelRoData_DisplayList_CollisionRecord *) node)->numVertices, 0xcccc, 0, 0)); if (newvertices != NULL) { @@ -3598,7 +3608,7 @@ after_opcode: if ((relatedrwdata != NULL) && (relatedrwdata->Vertices == relatedrodata->Vertices)) { - newvertices = (Vertex *) vtxstore_allocate(relatedrodata->numVertices, 0xcccc, 0, 0); + newvertices = PORT_N64PTR(Vertex, vtxstore_allocate(relatedrodata->numVertices, 0xcccc, 0, 0)); if (newvertices != NULL) { diff --git a/src/game/chr_b.c b/src/game/chr_b.c index f8fe0ebd..62808033 100644 --- a/src/game/chr_b.c +++ b/src/game/chr_b.c @@ -1,4 +1,9 @@ #include +#if defined(PORT) +#include "port_addr.h" +#else +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #include #include "cheat.h" #include "chr.h" @@ -100,7 +105,7 @@ struct Model *makeonebody(s32 body, s32 head, struct ModelFileHeader *bodyHeader if ((headHeader != 0) && (c_item_entries[body].hasHead == 0)) { bodyHeader->numRecords -= headHeader->numRecords; - modelAttachHead(model, opcode, headHeader); + modelAttachHead(model, PORT_N64PTR(ModelNode, opcode), headHeader); if ((sunglasses == 0) && ((s32) headHeader->numSwitches > 0)) { diff --git a/src/game/chraction.c b/src/game/chraction.c index bd2ae1dc..ec23c47b 100644 --- a/src/game/chraction.c +++ b/src/game/chraction.c @@ -45,6 +45,16 @@ #define PUN_ATTACK_ITEM(punchr, field) ((s32)(punchr)->field.attack_item) #endif +/* D298/M2: `(s32)&ANIM_DATA_x + (s32)&ptr_animation_table->data` is an N64 + * address, and animation_table_ptrs1[] slots hold N64 addresses; re-base where + * such a value becomes a pointer. Identity at PORT_ADDR_BASE == 0. */ +#if defined(PORT) +#include "port_addr.h" +#else +/* N64 build: the port address window is the identity (see port_addr.h). */ +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif + point2d D_800309F0 = {0, 0}; // forward declarations @@ -507,7 +517,7 @@ void chrlvKneelingAnimationRelated(ChrRecord *self) if ((s32)objecthandlerGetModelAnim(self->model) == (s32)&ANIM_DATA_fire_kneel_forward_one_handed_weapon_slow + (s32)&ptr_animation_table->data) { - modelSetAnimation(self->model, (struct ModelAnimation*)((s32)&ANIM_DATA_fire_kneel_forward_one_handed_weapon_slow + (s32)&ptr_animation_table->data), (s32) self->model->gunhand, 109.0f, chrlvGetGuard007SpeedRating(self, 0.5f, 0.8f), 16.0f); + modelSetAnimation(self->model, PORT_N64PTR(struct ModelAnimation, (s32)&ANIM_DATA_fire_kneel_forward_one_handed_weapon_slow + (s32)&ptr_animation_table->data), (s32) self->model->gunhand, 109.0f, chrlvGetGuard007SpeedRating(self, 0.5f, 0.8f), 16.0f); modelSetAnimEndFrame(self->model, 140.0f); } else @@ -625,7 +635,7 @@ void chrlvPerformAnimationForActor(ChrRecord *self, s32 animID, s32 startframe, } #endif - modelSetAnimation(self->model, (void *)animation_table_ptrs1[animID], (bitfield & ANIM_MIRROR) != 0, startframef, phi_f0, (f32)interpol_time60); + modelSetAnimation(self->model, PORT_N64PTR(void, animation_table_ptrs1[animID]), (bitfield & ANIM_MIRROR) != 0, startframef, phi_f0, (f32)interpol_time60); if (endframe >= 0) { @@ -2822,7 +2832,7 @@ s32 chrlvExplosionDamage(ChrRecord *self, coord3d *arg1, f32 damage, s32 arg3) modelSetAnimation( self_model, - (struct ModelAnimation *) ((s32)sp38->anonymous_0 + (s32)&ptr_animation_table->data), + PORT_N64PTR(struct ModelAnimation, (s32)sp38->anonymous_0 + (s32)&ptr_animation_table->data), sp38->anonymous_1, sp38->anonymous_3, sp38->anonymous_2, @@ -4986,7 +4996,9 @@ bool if_actor_able_set_on_path(ChrRecord *self, s32 pathid) { if (pathid && chrIsNotDeadOrShot(self)) { - set_actor_on_path(self, pathid); + /* D298/M2: callers pass a window PathRecord* truncated through this + * s32 param (chrai.c / aicommands.def); re-base on the read. */ + set_actor_on_path(self, PORT_N64PTR(struct patrol_path, pathid)); return TRUE; } @@ -5075,7 +5087,7 @@ void chrlvTickStand(ChrRecord *self) modelSetAnimation( self->model, // awkward fix: addu instruction is backwards - (struct ModelAnimation *)((s32)&ANIM_DATA_walking_unarmed + (s32)&ptr_animation_table->data), + PORT_N64PTR(struct ModelAnimation, (s32)&ANIM_DATA_walking_unarmed + (s32)&ptr_animation_table->data), i, 0.0f, 0.5f, @@ -5083,14 +5095,14 @@ void chrlvTickStand(ChrRecord *self) modelSetAnimEndFrame( self->model, - (((u16*)((s32)&ANIM_DATA_walking_unarmed + (s32)&ptr_animation_table->data))[2] - 1)); + (PORT_N64PTR(u16, (s32)&ANIM_DATA_walking_unarmed + (s32)&ptr_animation_table->data)[2] - 1)); } else if ((right != NULL) || (left != NULL)) { modelSetAnimation( self->model, // awkward fix: addu instruction is backwards - (struct ModelAnimation *)((s32)&ANIM_DATA_walking + (s32)&ptr_animation_table->data), + PORT_N64PTR(struct ModelAnimation, (s32)&ANIM_DATA_walking + (s32)&ptr_animation_table->data), left != NULL, 0.0f, 0.5f, @@ -5098,7 +5110,7 @@ void chrlvTickStand(ChrRecord *self) modelSetAnimEndFrame( self->model, - (((u16*)((s32)&ANIM_DATA_walking + (s32)&ptr_animation_table->data))[2] - 1)); + (PORT_N64PTR(u16, (s32)&ANIM_DATA_walking + (s32)&ptr_animation_table->data)[2] - 1)); } } else if (self->act_stand.face_entitytype & 0x10) @@ -5450,13 +5462,13 @@ void chrlvTickDie(ChrRecord *self) { modelSetAnimation( model, - (void*)((s32)&ANIM_DATA_jump_backwards + (s32)&ptr_animation_table->data), + PORT_N64PTR(void, (s32)&ANIM_DATA_jump_backwards + (s32)&ptr_animation_table->data), objecthandlerGetModelGunhand(model) == 0, 50.0f, 0.3f, - (((u16*)((s32)&ANIM_DATA_jump_backwards + (s32)&ptr_animation_table->data))[2] - 1.0f) - 50.0f); + ((PORT_N64PTR(u16, (s32)&ANIM_DATA_jump_backwards + (s32)&ptr_animation_table->data))[2] - 1.0f) - 50.0f); - modelSetAnimSpeed(model, 0.5f, (((u16*)((s32)&ANIM_DATA_jump_backwards + (s32)&ptr_animation_table->data))[2] - 1.0f) - 50.0f); + modelSetAnimSpeed(model, 0.5f, ((PORT_N64PTR(u16, (s32)&ANIM_DATA_jump_backwards + (s32)&ptr_animation_table->data))[2] - 1.0f) - 50.0f); return; } @@ -9160,8 +9172,8 @@ void chrlvTravelTick(ChrRecord *self, coord3d *arg1, StandTile *arg2, struct way if ((phi_s3 == NULL) || ((self->hidden & CHRHIDDEN_OFFSCREEN_PATROL) != 0)) { - if ((objecthandlerGetModelAnim(self->model) == (struct ModelAnimation *)((s32)&ANIM_DATA_idle_unarmed + (s32)&ptr_animation_table->data)) - || (objecthandlerGetModelAnim(self->model) == (struct ModelAnimation *)((s32)&ANIM_DATA_idle + (s32)&ptr_animation_table->data))) + if ((objecthandlerGetModelAnim(self->model) == PORT_N64PTR(struct ModelAnimation, (s32)&ANIM_DATA_idle_unarmed + (s32)&ptr_animation_table->data)) + || (objecthandlerGetModelAnim(self->model) == PORT_N64PTR(struct ModelAnimation, (s32)&ANIM_DATA_idle + (s32)&ptr_animation_table->data))) { if (self->actiontype == ACT_PATROL) { diff --git a/src/game/chrai.c b/src/game/chrai.c index 0213c4c1..437ca3c6 100644 --- a/src/game/chrai.c +++ b/src/game/chrai.c @@ -33,6 +33,11 @@ #include #include #include +#if defined(PORT) +#include "port_addr.h" +#else +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #ifdef PORT #include #endif @@ -1192,7 +1197,7 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType) { zero = 0; // debug value maybe? /* D32/D33: table holds s32 offsets; cast at use site. */ - modelSetAnimation(AircraftEntityp->model, (ModelAnimation *)animation_table_ptrs2[anim_id], zero, startframe, 0.5f, (s32)ai->INTERPOL_TIME60); + modelSetAnimation(AircraftEntityp->model, PORT_N64PTR(ModelAnimation, animation_table_ptrs2[anim_id]), zero, startframe, 0.5f, (s32)ai->INTERPOL_TIME60); if (endframe >= 0) { modelSetAnimEndFrame(AircraftEntityp->model, endframe); diff --git a/src/game/explosion.c b/src/game/explosion.c index 9c586a53..66ed4f77 100644 --- a/src/game/explosion.c +++ b/src/game/explosion.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 @@ -1734,7 +1739,7 @@ Gfx *explosionRenderFlyingParticles(Gfx *gdl) for (i = 0; i < max_particles; i++) { // HACK: regalloc has instructions backwards. - particles = (struct FlyingParticles *)(u32)g_FlyingParticlesBuffer + i; + particles = (struct FlyingParticles *)PORT_N64PTR(void, g_FlyingParticlesBuffer) + i; if (particles->unk00 > 0) { diff --git a/src/game/front.c b/src/game/front.c index 5043ef8d..c3749218 100644 --- a/src/game/front.c +++ b/src/game/front.c @@ -51,6 +51,10 @@ #include "model.h" #ifdef PORT #include "hudaspect.h" /* D335 front-end pillarbox under native widescreen */ +#include "port_addr.h" +#else +/* N64 build: the port address window is the identity (see port_addr.h). */ +#define PORT_N64PTR(T, x) ((T *)(x)) #endif @@ -983,8 +987,8 @@ Gfx *frontPrintText(Gfx *gdl, s32 *x, s32 *y, s8 *text, s32 second_font_table, s x, y, text, - second_font_table, - first_font_table, + PORT_N64PTR(struct fontchar, second_font_table), + PORT_N64PTR(struct font, first_font_table), arg6, (textglowR.r << 0x18) | (textglowG.r << 0x10) | (textglowB.r << 8) | textglowA.r, view_x, @@ -999,8 +1003,8 @@ Gfx *frontPrintText(Gfx *gdl, s32 *x, s32 *y, s8 *text, s32 second_font_table, s x, y, text, - second_font_table, - first_font_table, + PORT_N64PTR(struct fontchar, second_font_table), + PORT_N64PTR(struct font, first_font_table), arg6, view_x, view_y, @@ -1526,7 +1530,7 @@ Gfx *display_aligned_white_text_to_screen(Gfx *dl, s32 arg1, s32 arg2, s32 halig x = arg1 - ((s32) (halign * sp48) / 2); y = arg2 - ((s32) (valign * sp4C) / 2); - return textRender(dl, &x, &y, text, arg6, arg7, -1, viGetX(), viGetY(), 0, 0); + return textRender(dl, &x, &y, text, PORT_N64PTR(struct fontchar, arg6), PORT_N64PTR(struct font, arg7), -1, viGetX(), viGetY(), 0, 0); } @@ -2179,7 +2183,7 @@ void load_walletbond(void) b = (struct ModelNode *)mnode; srecord = b->Data; - arg0 = (s32)srecord->BaseAddr + ((s32)srecord->Primary & 0xffffff); + arg0 = PORT_N64PTR(Gfx, (s32)srecord->BaseAddr + ((s32)srecord->Primary & 0xffffff)); bgApplyDynamicCCRMLUT(arg0, NULL, CCRMLUT_WALLETBOND); } } @@ -7485,7 +7489,7 @@ Gfx *constructor_menu0D_missioncomplete(Gfx *DL) x = 0x37; y = 0xDC; DL = frontPrintText(DL, &x, &y, text, ptrFontZurichBoldChars, ptrFontZurichBold, 0xFF, viGetX(), viGetY(), 0, 0); - strcpy(stagename, frontGetPlayersFavoriteWeaponInHand(0, 0)); + strcpy(stagename, PORT_N64PTR(char, frontGetPlayersFavoriteWeaponInHand(0, 0))); if ((array_favweapon[0][0] > 0) && (array_favweapon[0][1] == array_favweapon[0][0])) { temp = strlen(stagename)-1; @@ -7846,16 +7850,16 @@ Gfx *constructor_menu16_nocontrollers(Gfx *DL) if ((numContCon == 0) || (numContCon == 1) || (numContCon == 2) || (numContCon == 3)) { text = langGet(getStringID(LTITLE, TITLE_STR_118_NOCONT)); //NO CONTROLLER IN CONTROLLER SOCKET 1 } - textMeasure(&y2, &x2, text, ptrFontZurichBoldChars, ptrFontZurichBold, 0); + textMeasure(&y2, &x2, PORT_N64PTR(char, text), ptrFontZurichBoldChars, ptrFontZurichBold, 0); x = 0xDC - (x2 >> 1); y = 0x99 - (y2 >> 1); #ifdef BUGFIX_R1 if (j_text_trigger) { - DL = textRenderOutlined(DL, &x, &y, text, ptrFontZurichBoldChars, ptrFontZurichBold, -1, 0x8000FF, viGetX(), viGetY(), 0, 0); + DL = textRenderOutlined(DL, &x, &y, PORT_N64PTR(char, text), ptrFontZurichBoldChars, ptrFontZurichBold, -1, 0x8000FF, viGetX(), viGetY(), 0, 0); } else { #endif - DL = textRender(DL, &x, &y, text, ptrFontZurichBoldChars, ptrFontZurichBold, -1, viGetX(), viGetY(), 0, 0); + DL = textRender(DL, &x, &y, PORT_N64PTR(char, text), ptrFontZurichBoldChars, ptrFontZurichBold, -1, viGetX(), viGetY(), 0, 0); #ifdef BUGFIX_R1 } #endif @@ -7864,16 +7868,16 @@ Gfx *constructor_menu16_nocontrollers(Gfx *DL) if ((numContCon == 0) || (numContCon == 1) || (numContCon == 2) || (numContCon == 3)) { text = langGet(getStringID(LTITLE, TITLE_STR_119_ATTACHCONT)); //PLEASE POWER OFF AND ATTACH A CONTROLLER } - textMeasure(&y2, &x2, text, ptrFontZurichBoldChars, ptrFontZurichBold, 0); + textMeasure(&y2, &x2, PORT_N64PTR(char, text), ptrFontZurichBoldChars, ptrFontZurichBold, 0); x = 0xDC - (x2 >> 1); y = 0xB1 - (y2 >> 1); #ifdef BUGFIX_R1 if (j_text_trigger) { - DL = textRenderOutlined(DL, &x, &y, text, ptrFontZurichBoldChars, ptrFontZurichBold, -1, 0x8000FF, viGetX(), viGetY(), 0, 0); + DL = textRenderOutlined(DL, &x, &y, PORT_N64PTR(char, text), ptrFontZurichBoldChars, ptrFontZurichBold, -1, 0x8000FF, viGetX(), viGetY(), 0, 0); } else { #endif - DL = textRender(DL, &x, &y, text, ptrFontZurichBoldChars, ptrFontZurichBold, -1, viGetX(), viGetY(), 0, 0); + DL = textRender(DL, &x, &y, PORT_N64PTR(char, text), ptrFontZurichBoldChars, ptrFontZurichBold, -1, viGetX(), viGetY(), 0, 0); #ifdef BUGFIX_R1 } #endif @@ -8098,7 +8102,7 @@ void init_menu18_displaycast(void) #else modelSetAnimPlaySpeed(cast_model, 0.5f, 0); #endif - modelSetAnimation(cast_model, animation_table_ptrs1[intro_animation_table[randomly_selected_intro_animation].animID], flip, intro_animation_table[randomly_selected_intro_animation].startframeoffset, intro_animation_table[randomly_selected_intro_animation].playback_speed, 0.0f); + modelSetAnimation(cast_model, PORT_N64PTR(ModelAnimation, animation_table_ptrs1[intro_animation_table[randomly_selected_intro_animation].animID]), flip, intro_animation_table[randomly_selected_intro_animation].startframeoffset, intro_animation_table[randomly_selected_intro_animation].playback_speed, 0.0f); g_MenuTimer = 0; cast_camera_dist_start = ((((f32) ((u32) randomGetNext())) * (1.0f / U32_MAX)) * 80.0f) + 70.0f; @@ -8761,7 +8765,7 @@ void menu_init(void) { if (viGetFrameBuf2() == (cfb_16[1])) { - viSetFrameBuf2(ptr_menu_videobuffer); + viSetFrameBuf2(PORT_N64PTR(u8, ptr_menu_videobuffer)); } viSetAspect(MENU_INIT_ASPECT_440); diff --git a/src/game/gunfire.c b/src/game/gunfire.c index d21ff3bc..f8220da7 100644 --- a/src/game/gunfire.c +++ b/src/game/gunfire.c @@ -1,4 +1,9 @@ #include +#if defined(PORT) +#include "port_addr.h" +#else +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #include "include/limits.h" #include #include @@ -6256,14 +6261,14 @@ Gfx *generate_ammo_total_microcode(Gfx *gdl) if (imageoffset_r != 0) { imageoffset_r += globalbank_rdram_offset; - gdl = set_rgba_redirect_generate_microcode(gdl, (u8 *)imageoffset_r, (getPlayer_c_screenleft() + getPlayer_c_screenwidth()) - (f32)rightx, -1.0f, + gdl = set_rgba_redirect_generate_microcode(gdl, PORT_N64PTR(u8, imageoffset_r), (getPlayer_c_screenleft() + getPlayer_c_screenwidth()) - (f32)rightx, -1.0f, #if defined(VERSION_EU) (viGetViewTop() + viGetViewHeight()) - 30, 0, #else (viGetViewTop() + viGetViewHeight()) - 20, 0, #endif ammo_related[ammotype].IconYOffset, 1); - textwidth_r = ((u8 *)imageoffset_r)[4]; + textwidth_r = (PORT_N64PTR(u8, imageoffset_r))[4]; } gdl = microcode_constructor(gdl); @@ -6328,14 +6333,14 @@ Gfx *generate_ammo_total_microcode(Gfx *gdl) if (imageoffset_l != 0) { imageoffset_l += globalbank_rdram_offset; - gdl = set_rgba_redirect_generate_microcode(gdl, (u8 *)imageoffset_l, getPlayer_c_screenleft() + (f32)leftx, -1.0f, + gdl = set_rgba_redirect_generate_microcode(gdl, PORT_N64PTR(u8, imageoffset_l), getPlayer_c_screenleft() + (f32)leftx, -1.0f, #if defined(VERSION_EU) (viGetViewTop() + viGetViewHeight()) - 30, 1, #else (viGetViewTop() + viGetViewHeight()) - 20, 1, #endif ammo_related[ammotype].IconYOffset, 1); - textwidth_l = ((u8 *)imageoffset_l)[4]; + textwidth_l = (PORT_N64PTR(u8, imageoffset_l))[4]; } gdl = microcode_constructor(gdl); @@ -6426,12 +6431,12 @@ Gfx *gunDrawWatchAmmoDisplay(Gfx *gdl) // Draw the ammo icon #if defined(VERSION_EU) - gdl = set_rgba_redirect_generate_microcode(gdl, (u8 *)imageoffset, 200.0f, 208.0f, (viGetViewTop() + viGetViewHeight()) - 30, 0, ammo_related[ammotype].IconYOffset, 1); + gdl = set_rgba_redirect_generate_microcode(gdl, PORT_N64PTR(u8, imageoffset), 200.0f, 208.0f, (viGetViewTop() + viGetViewHeight()) - 30, 0, ammo_related[ammotype].IconYOffset, 1); #else - gdl = set_rgba_redirect_generate_microcode(gdl, (u8 *)imageoffset, 200.0f, 180.0f, (viGetViewTop() + viGetViewHeight()) - 20, 0, ammo_related[ammotype].IconYOffset, 1); + gdl = set_rgba_redirect_generate_microcode(gdl, PORT_N64PTR(u8, imageoffset), 200.0f, 180.0f, (viGetViewTop() + viGetViewHeight()) - 20, 0, ammo_related[ammotype].IconYOffset, 1); #endif - textwidth = ((u8 *)imageoffset)[4]; + textwidth = (PORT_N64PTR(u8, imageoffset))[4]; } gdl = microcode_constructor(gdl); diff --git a/src/game/image.c b/src/game/image.c index b1d68f27..44a1b869 100644 --- a/src/game/image.c +++ b/src/game/image.c @@ -414,7 +414,7 @@ s32 texAlignIndices(u8 *src, s32 width, s32 height, s32 format, u8 *dst) src++; } - outptr = (u8 *)(((u32)outptr + 7) & ~7); + outptr = (u8 *)(((uintptr_t)outptr + 7) & ~(uintptr_t)7); } return outptr - dst; @@ -2462,7 +2462,7 @@ void texLoadFromDisplayList(Gfx *gdl, struct texpool *arg1) if (bytes[0] == G_SETTIMG && bytes[4] == 0xab && bytes[5] == 0xcd) #endif { - texLoad((u32 *)((s32)bytes + 4), arg1); + texLoad((u32 *)(bytes + 4), arg1); } bytes += 8; diff --git a/src/game/image_bank.c b/src/game/image_bank.c index 2990c903..e206f683 100644 --- a/src/game/image_bank.c +++ b/src/game/image_bank.c @@ -4,6 +4,10 @@ #include "image_bank.h" #ifdef PORT #include +#include "port_addr.h" +#else +/* N64 build: the port address window is the identity (see port_addr.h). */ +#define PORT_N64PTR(T, x) ((T *)(x)) #endif // bss @@ -16,7 +20,11 @@ s32 img_bitcount; //8008D0AC s32 dword_CODE_bss_8008D0AC; //8008D0B0; +#if defined(PORT) +uintptr_t globalbank_rdram_offset; /* D298/M2: host-based, see image_bank.h */ +#else s32 globalbank_rdram_offset; +#endif //8008D0B4; s32 *pGlobalimagetable; //8008D0B8; @@ -89,7 +97,14 @@ extern u8* _GlobalimagetableSegmentRomStart; void texSetBitstring(s32 pos) { +#if defined(PORT) + /* D298/M2: callers pass a host pointer through this s32 parameter, which + * truncates it. Because PORT_ADDR_BASE is 4 GiB-aligned the low 32 bits + * are the N64/window offset, so re-base them. Identity at base 0. */ + img_curpos = (u8 *)portN64ToHost((u32)pos); +#else img_curpos = pos; +#endif img_curdatatable = 0; img_bitcount = 0; } @@ -167,7 +182,7 @@ extern Gfx* globalDL_0x900; extern Gfx* globalDL_0x9a8; extern Gfx* globalDL_0xa50; -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) /* D39 (docs/dev/findings.md): on N64 all 49 symbols above link inside the * Globalimagetable segment at physical 0x02xxxxxx (ge007.ld), so * `globalbank_rdram_offset + (u32)&sym` rebases each onto pGlobalimagetable. @@ -244,16 +259,24 @@ void texReset(void) pGlobalimagetable = mempAllocBytesInBank(size + 0x1000, MEMPOOL_STAGE); pGlobalimagetable = ((u32)pGlobalimagetable + 0xFFFU) & 0xFFFFF000; - romCopy(pGlobalimagetable, &_GlobalimagetableSegmentRomStart, size); + romCopy(PORT_N64PTR(void, pGlobalimagetable), &_GlobalimagetableSegmentRomStart, size); #ifdef PORT /* D68 (docs/dev/findings.md): the ROM copy is N64 big-endian; convert * the CPU-interpreted u32 fields (IMAGESEG Gfx w1 words and * sImageTableEntry.index) to host order before any code reads them. */ - gimgFixupGlobalimagetable((u8 *)pGlobalimagetable); + gimgFixupGlobalimagetable((u8 *)PORT_N64PTR(void, pGlobalimagetable)); #endif +#if defined(PORT) + /* D298/M2: store a HOST-based offset. GIMG_OFF(sym) adds 0x02000000, so + * `globalbank_rdram_offset + GIMG_OFF(sym)` is then a live host pointer. + * `- 0x02000000` is the 64-bit equivalent of the original 32-bit + * `+ 0xFE000000` wraparound (which relied on mod-2^32 cancellation). */ + globalbank_rdram_offset = (uintptr_t)portN64ToHost((u32)pGlobalimagetable) - 0x02000000UL; +#else globalbank_rdram_offset = (u32)pGlobalimagetable + 0xFE000000; +#endif genericimage = (void *) (globalbank_rdram_offset + GIMG_OFF(s_genericimage)); impactimages = (void *) (globalbank_rdram_offset + GIMG_OFF(s_impactimages)); explosion_smokeimages = (void *) (globalbank_rdram_offset + GIMG_OFF(s_explosion_smokeimages)); @@ -321,6 +344,6 @@ void texReset(void) /* D68: explosion.c executes the compiled globalDL_0xNNN shadows via * g_ExplosionDisplayLists[]; copy the IMAGESEG words that texLoad() * patched in the ROM copy over into those arrays. */ - gimgSyncCompiledGlobalDLs((u8 *)pGlobalimagetable); + gimgSyncCompiledGlobalDLs((u8 *)PORT_N64PTR(void, pGlobalimagetable)); #endif } diff --git a/src/game/image_bank.h b/src/game/image_bank.h index 85ffa545..70e4bf56 100644 --- a/src/game/image_bank.h +++ b/src/game/image_bank.h @@ -25,7 +25,14 @@ extern struct sImageTableEntry *flareimage5; extern u8* img_curpos; extern s32 img_bitcount; extern s32 *pGlobalimagetable; +#if defined(PORT) +/* D298/M2: holds a host-based RDRAM offset (pGlobalimagetable - 0x02000000) + * so `globalbank_rdram_offset + GIMG_OFF(sym)` is a live host pointer in the + * DRAM window. s32 on N64 (see image_bank.c). */ +extern uintptr_t globalbank_rdram_offset; +#else extern s32 globalbank_rdram_offset; +#endif void texReset(void); u32 texReadBits(s32 bitCount); diff --git a/src/game/initBondDATAdefaults.c b/src/game/initBondDATAdefaults.c index cfc342ee..08b4a32c 100644 --- a/src/game/initBondDATAdefaults.c +++ b/src/game/initBondDATAdefaults.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 "chr.h" @@ -166,7 +171,7 @@ void sets_a_bunch_of_BONDdata_values_to_default(void) { sub_GAME_7F0062C0( // match hack: addu address calculated backwards - (void*)((s32)g_BondMoveAnimationSetup[i].anim_id + (s32)&ptr_animation_table->data), + PORT_N64PTR(void, (s32)g_BondMoveAnimationSetup[i].anim_id + (s32)&ptr_animation_table->data), (s32)g_BondMoveAnimationSetup[i].loopframe, (s32)g_BondMoveAnimationSetup[i].endframe, &spD0); @@ -194,7 +199,7 @@ void sets_a_bunch_of_BONDdata_values_to_default(void) modelSetAnimation( &g_CurrentPlayer->model, // match hack: addu address calculated backwards - (struct ModelAnimation *) ((s32)g_BondMoveAnimationSetup[g_CurrentPlayer->headanim].anim_id + (s32)&ptr_animation_table->data), + (struct ModelAnimation *) PORT_N64PTR(void, (s32)g_BondMoveAnimationSetup[g_CurrentPlayer->headanim].anim_id + (s32)&ptr_animation_table->data), 0, g_BondMoveAnimationSetup[g_CurrentPlayer->headanim].loopframe, 0.5f, diff --git a/src/game/initactorpropstuff.c b/src/game/initactorpropstuff.c index 33015f53..ff481361 100644 --- a/src/game/initactorpropstuff.c +++ b/src/game/initactorpropstuff.c @@ -6,6 +6,16 @@ #include "math.h" #include "math_floor.h" +/* D298/M2: these tables store animation addresses as N64 values (s32/union + * offset) that are then cast to pointers. Re-base when the pointer is formed; + * identity at PORT_ADDR_BASE == 0. */ +#if defined(PORT) +#include "port_addr.h" +#define ANI_ADDR(x) ((void *)portN64ToHost((u32)(x))) +#else +#define ANI_ADDR(x) ((void *)(x)) +#endif + /** * Gets the number of currently allocated heads and bodies @@ -108,7 +118,7 @@ s32 initResolveAnimGroupTable(struct weapon_firing_animation_table *animconfig) do { - config->anim.anim = (struct ModelAnimation *)(((0, animoffset)) + ((s32)ptr_animation_table)); + config->anim.anim = (struct ModelAnimation *)ANI_ADDR(((0, animoffset)) + ((s32)ptr_animation_table)); endframe = floorFloatToInt(config->unk04); angle16 = sub_GAME_7F0001F0(config->anim.anim, 0, endframe) & 0xffff; duration = config->unk04; @@ -186,7 +196,7 @@ s32 initResolveAnimTable(struct StruckAnim *entries) count++; entry++; ptr_animation_table_addr = (struct StruckAnim *)(&ptr_animation_table); - entry[-1].struck_anim = (ModelAnimation *)((*((s32 *)entries)) + (0, address)); + entry[-1].struck_anim = (ModelAnimation *)ANI_ADDR((*((s32 *)entries)) + (0, address)); } while (entry->struck_anim != 0); } @@ -196,7 +206,7 @@ s32 initResolveAnimTable(struct StruckAnim *entries) #define ANIM_PTR(anim) \ - ((ModelAnimation *)((s32)&anim + ((s32)ptr_animation_table))) + ((ModelAnimation *)ANI_ADDR((s32)&anim + ((s32)ptr_animation_table))) #define ANIM_FRAC(anim) \ ((((f32)sub_GAME_7F000290(ANIM_PTR(anim), 0, ANIM_PTR(anim)->unk04 - 1)) * 0.10000001f) / \ diff --git a/src/game/initanitable.c b/src/game/initanitable.c index edf2fd19..fff0349b 100644 --- a/src/game/initanitable.c +++ b/src/game/initanitable.c @@ -6,6 +6,12 @@ /* D33: ROM-file endianness fixup for the animation data segment (port layer). */ #include "romdata.h" +#if defined(PORT) +#include "port_addr.h" +#else +/* N64 build: the port address window is the identity (see port_addr.h). */ +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif //bss @@ -256,15 +262,15 @@ void expand_ani_table_entries(s32** arg0) while (*var_v0 != 0) { if (*var_v0 != 1) { *var_v0 = (s32)((s32)*var_v0 + (s32)(&ptr_animation_table->data)); - ((struct anim_entry *)*var_v0)->unk08 += (s32)&ptr_animation_table->data; - ((struct anim_entry *)*var_v0)->unk10 += (s32)&ptr_animation_table->data; + PORT_N64PTR(struct anim_entry, *var_v0)->unk08 += (s32)&ptr_animation_table->data; + PORT_N64PTR(struct anim_entry, *var_v0)->unk10 += (s32)&ptr_animation_table->data; } var_v0++; } for (var_v0 = (s32 *)arg0; *var_v0 != 0; var_v0++) { if (*var_v0 != 1) { - *(s32 *)*var_v0 += (s32)&_animation_entriesSegmentRomStart; + *PORT_N64PTR(s32, *var_v0) += (s32)&_animation_entriesSegmentRomStart; } } } diff --git a/src/game/initunk_005450.c b/src/game/initunk_005450.c index fef743bb..197a358e 100644 --- a/src/game/initunk_005450.c +++ b/src/game/initunk_005450.c @@ -38,7 +38,7 @@ void initModelHitEntryFreeList(void) entries[i].prev = &entries[i - 1]; } -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) // D40: N64 g_ModelHitEntriesPenultimate labels entry 598 of the pool; on PC the // placeholder symbol is not emitted, so take the address directly. ((ModelHitEntry *)g_ModelHitEntries)[MODELHITENTRIES_LEN - 1].prev = &entries[MODELHITENTRIES_LEN - 2]; diff --git a/src/game/language.c b/src/game/language.c index cfdac343..24327099 100644 --- a/src/game/language.c +++ b/src/game/language.c @@ -11,6 +11,7 @@ #ifdef PORT #include "envflag.h" /* cached getenv for hot-path probes */ #endif +#include "port_addr.h" /* D298/M2: DRAM window base */ extern resource_lookup_data_entry resource_lookup_data_array[]; /* ob.c */ static void langFixupLoadedBank(char *name, void *p) @@ -21,6 +22,16 @@ static void langFixupLoadedBank(char *name, void *p) } #endif +/* g_LangBanks[] holds N64 DRAM addresses as s32 (they must fit s32, see + * memp.h / D298). Re-base only where a slot becomes a pointer; identity at + * PORT_ADDR_BASE == 0, so Windows/Linux are unchanged. */ +#if defined(PORT) +#include "port_addr.h" +#else +/* N64 build: the port address window is the identity (see port_addr.h). */ +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif + // bss //CODE.bss:8008C640 s32 g_LangBanks[45]; @@ -267,13 +278,13 @@ void langInit(void) { g_LangBanks[LOPTIONS] = _fileNameLoadToBank(LnameX_lookuptable[LOPTIONS][j_text_trigger], FILELOADMETHOD_DEFAULT, 0x100, MEMPOOL_PERMANENT); g_LangBanks[LMISC] = _fileNameLoadToBank(LnameX_lookuptable[LMISC][j_text_trigger], FILELOADMETHOD_DEFAULT, 0x100, MEMPOOL_PERMANENT); #ifdef PORT - langFixupLoadedBank(LnameX_lookuptable[LGUN][j_text_trigger], (void *)g_LangBanks[LGUN]); - langFixupLoadedBank(LnameX_lookuptable[LTITLE][j_text_trigger], (void *)g_LangBanks[LTITLE]); - langFixupLoadedBank(LnameX_lookuptable[LMPMENU][j_text_trigger], (void *)g_LangBanks[LMPMENU]); - langFixupLoadedBank(LnameX_lookuptable[LPROPOBJ][j_text_trigger], (void *)g_LangBanks[LPROPOBJ]); - langFixupLoadedBank(LnameX_lookuptable[LMPWEAPONS][j_text_trigger], (void *)g_LangBanks[LMPWEAPONS]); - langFixupLoadedBank(LnameX_lookuptable[LOPTIONS][j_text_trigger], (void *)g_LangBanks[LOPTIONS]); - langFixupLoadedBank(LnameX_lookuptable[LMISC][j_text_trigger], (void *)g_LangBanks[LMISC]); + langFixupLoadedBank(LnameX_lookuptable[LGUN][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LGUN])); + langFixupLoadedBank(LnameX_lookuptable[LTITLE][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LTITLE])); + langFixupLoadedBank(LnameX_lookuptable[LMPMENU][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LMPMENU])); + langFixupLoadedBank(LnameX_lookuptable[LPROPOBJ][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LPROPOBJ])); + langFixupLoadedBank(LnameX_lookuptable[LMPWEAPONS][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LMPWEAPONS])); + langFixupLoadedBank(LnameX_lookuptable[LOPTIONS][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LOPTIONS])); + langFixupLoadedBank(LnameX_lookuptable[LMISC][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LMISC])); #endif } @@ -381,7 +392,7 @@ void langLoadToAddr(u32 id) { g_LangBanks[id] = _fileNameLoadToBank(LnameX_lookuptable[id][j_text_trigger],1,0x100,MEMPOOL_STAGE); #ifdef PORT - langFixupLoadedBank(LnameX_lookuptable[id][j_text_trigger], (void *)g_LangBanks[id]); + langFixupLoadedBank(LnameX_lookuptable[id][j_text_trigger], PORT_N64PTR(void, g_LangBanks[id])); #endif } @@ -390,7 +401,7 @@ void langLoadToBank(int id,u8 *target,int size) { g_LangBanks[id] = _fileNameLoadToAddr(LnameX_lookuptable[id][j_text_trigger],1,target,size); #ifdef PORT - langFixupLoadedBank(LnameX_lookuptable[id][j_text_trigger], (void *)g_LangBanks[id]); + langFixupLoadedBank(LnameX_lookuptable[id][j_text_trigger], PORT_N64PTR(void, g_LangBanks[id])); #endif } @@ -417,16 +428,22 @@ u8 * langGet(s32 slotID) return NULL; } #endif - u32 * textbank_ptr = g_LangBanks[slotID >> 10]; /* get the text file bank ID index the text ptr table */ + u32 * textbank_ptr = PORT_N64PTR(u32, g_LangBanks[slotID >> 10]); /* get the text file bank ID index the text ptr table */ #ifdef PORT /* D129 cont.: g_LangBanks[] is s32 and only populated for banks the * current flow has loaded. A bare `-level_XX` boot that reaches the * cast/credits text path (Cuba, bondviewRenderCredits, D76) hits a bank * slot that was never filled -> stale/garbage non-NULL value -> fault on * the table read below. Reject anything that is not a plausible mapped - * DRAM address. */ - if ((uintptr_t)textbank_ptr < 0x10000 || (uintptr_t)textbank_ptr >= 0x400000000ULL) { - return NULL; + * DRAM address. D298/M2: the DRAM window is shifted by PORT_ADDR_BASE on + * macOS, so the bounds move with it (identity at PORT_ADDR_BASE == 0). */ + { + uintptr_t t = (uintptr_t)textbank_ptr; + uintptr_t lo = (uintptr_t)PORT_ADDR_BASE + 0x10000u; + uintptr_t hi = (uintptr_t)PORT_ADDR_BASE + 0x400000000ULL; + if (t < lo || t >= hi) { + return NULL; + } } #endif #ifdef PORT @@ -437,7 +454,7 @@ u8 * langGet(s32 slotID) d65first = 0; for (int b = 0; b < 45; b++) if (g_LangBanks[b]) - osSyncPrintf("D65 bank %d = %p\n", b, (void *)g_LangBanks[b]); + osSyncPrintf("D65 bank %d = %p\n", b, PORT_N64PTR(void, g_LangBanks[b])); } if (!textbank_ptr) osSyncPrintf("D65 langGet slotID=0x%08x bank=%d ptr=NULL\n", (unsigned)slotID, slotID >> 10); diff --git a/src/game/model.c b/src/game/model.c index ad54419f..f4dd7b27 100644 --- a/src/game/model.c +++ b/src/game/model.c @@ -13,6 +13,16 @@ #include "initunk_005520.h" #include "math_asinfacosf.h" #include "math_floor.h" + +/* D298/M2: ModelAnimation.bitDescriptors/bitStream are u32 fields holding N64 + * addresses (D32: the blob base is added by expand_ani_table_entries). Re-base + * where they become pointers; identity at PORT_ADDR_BASE == 0. */ +#if defined(PORT) +#include "port_addr.h" +#else +/* N64 build: the port address window is the identity (see port_addr.h). */ +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #include "math_ceil.h" #include "math_unk_05A9E0.h" #include "objecthandler.h" @@ -78,6 +88,11 @@ Model *modelmgrInstantiateModel(ModelFileHeader *header) /* PC port (D53.2): this buffer is used as a struct Model via the * same type-pun as g_ModelSlots; on PC that struct is 0xE8 bytes, * not the N64 0x20 slot size. */ + { + static int fb = 0; + osSyncPrintf("modelmgr: STAGE model fallback #%d (numRecords=%d)\n", + ++fb, (int)header->numRecords); + } model = mempAllocBytesInBank(sizeof(struct Model), MEMPOOL_STAGE); #else model = mempAllocBytesInBank(0x20, MEMPOOL_STAGE); @@ -1028,7 +1043,7 @@ u16 modelAnimReadRootMotionValue(ModelAnimation *anim, s32 fieldIndex, s32 extra u8 bitsThisRead; result = 0; - desc = (ModelAnimBitField *)anim->bitDescriptors + fieldIndex; // D32: u32 -> ptr + desc = PORT_N64PTR(struct ModelAnimBitField, anim->bitDescriptors) + fieldIndex; // D32: u32 -> ptr (D298/M2 re-base) bitsRemaining = desc->bitCount; if (bitsRemaining > 0) @@ -1036,7 +1051,7 @@ u16 modelAnimReadRootMotionValue(ModelAnimation *anim, s32 fieldIndex, s32 extra totalBitOffset = extraBitOffset + desc->bitOffset; byteIndex = totalBitOffset >> 3; totalBitOffset &= 7; - byteptr = (u8 *)anim->bitStream + byteIndex; // D32: u32 -> ptr + byteptr = PORT_N64PTR(u8, anim->bitStream) + byteIndex; // D32: u32 -> ptr (D298/M2 re-base) bitsThisRead = 8 - totalBitOffset; if (bitsRemaining >= bitsThisRead) @@ -1713,24 +1728,24 @@ void process_02_position(ModelRenderData *arg0, Model *model, ModelNode *node) rot1 = D_80036094; - sub_GAME_7F06DEC0(jointnum.v, model->gunhand, skeleton, model->anim, model->unk34, &rot1); + sub_GAME_7F06DEC0(jointnum.v, model->gunhand, skeleton, model->anim, PORT_N64PTR(u8, model->unk34), &rot1); if (model->unk2c != 0.0f) { rot2 = D_800360A0; - sub_GAME_7F06DEC0(jointnum.v, model->gunhand, skeleton, model->anim, model->unk38, &rot2); + sub_GAME_7F06DEC0(jointnum.v, model->gunhand, skeleton, model->anim, PORT_N64PTR(u8, model->unk38), &rot2); sub_GAME_7F06D160(&rot1, &rot2, model->unk2c); } if (model->unk84 != 0.0f) { rot3 = D_800360AC; - sub_GAME_7F06DEC0(jointnum.v, model->unk25, skeleton, model->anim2, model->unk64, &rot3); + sub_GAME_7F06DEC0(jointnum.v, model->unk25, skeleton, model->anim2, PORT_N64PTR(u8, model->unk64), &rot3); if (model->unk5c != 0.0f) { rot4 = D_800360B8; - sub_GAME_7F06DEC0(jointnum.v, model->unk25, skeleton, model->anim2, model->unk68, &rot4); + sub_GAME_7F06DEC0(jointnum.v, model->unk25, skeleton, model->anim2, PORT_N64PTR(u8, model->unk68), &rot4); sub_GAME_7F06D160(&rot3, &rot4, model->unk5c); } @@ -6209,7 +6224,12 @@ s32 loadAnimationFrame(ModelAnimation* anim, s32 frame, ModelSkeleton* unused) s32 ret; s32 source; s32 frameSize; +#if defined(PORT) + /* D298/M2: keep the buffer address at full pointer width (D59 buffer). */ + uintptr_t dest; +#else u32 dest; +#endif u32 size; ret = 0; @@ -6223,8 +6243,12 @@ s32 loadAnimationFrame(ModelAnimation* anim, s32 frame, ModelSkeleton* unused) else if (D_80036414 != NULL) // should never be NULL after initAnimationsBuffer is called { // Get dest from this D_80036414 which points to an array. Align to 16 bytes. +#if defined(PORT) + dest = ((uintptr_t)(D_80036414->animBufferPtr2 + 15) >> 4) * 16; +#else dest = ((u32) (D_80036414->animBufferPtr2 + 15) >> 4) * 16; - ret = dest; +#endif + ret = (s32)dest; // Get source of this animation in ROM with the offset of the frame we'll load source = anim->address + (frame * frameSize); @@ -6239,14 +6263,14 @@ s32 loadAnimationFrame(ModelAnimation* anim, s32 frame, ModelSkeleton* unused) size = ((u32) (frameSize + 15) >> 4) * 16; // This copies one animation frame from ROM to the destination in RAM - romCopy((void* ) dest, (void* ) source, size); + romCopy((void *)dest, (void *)source, size); // Increment this which serves nothing D_80036414->uselessPointer += 1; // Set this to point to the end of the copied frame // This allows to copy another frame after this one - D_80036414->animBufferPtr2 = dest + size; + D_80036414->animBufferPtr2 = (void *)(dest + size); } return ret; } @@ -6268,7 +6292,7 @@ void modelResetAnimationsScratchBuffer(void) #define PROMOTE(var) \ if (var) \ - var = (void *)((u32)var + diff) + var = (void *)portN64ToHost((u32)var + diff) #ifdef PORT /* PC port (D43/D45): Vertex.LinkedTo is a raw vma (u32), not a pointer — @@ -6320,7 +6344,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr { ModelRoData_DisplayListRecord* rodata = &node->Data->DisplayList; PROMOTE(rodata->Vertices); - rodata->BaseAddr = (void *)fileramaddr; + rodata->BaseAddr = PORT_N64PTR(void, fileramaddr); break; } @@ -6338,7 +6362,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr PROMOTE(rodata->CollisionVertices[i].LinkedTo); #endif } - rodata->BaseAddr = (void *)fileramaddr; + rodata->BaseAddr = PORT_N64PTR(void, fileramaddr); break; } @@ -6362,7 +6386,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr PROMOTE(rodata->Children[i].unk04); } - rodata->BaseAddr = (void *)fileramaddr; + rodata->BaseAddr = PORT_N64PTR(void, fileramaddr); break; } @@ -6381,14 +6405,14 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr PROMOTE(rodata->Children[i].unk04); } - rodata->BaseAddr = (void *)fileramaddr; + rodata->BaseAddr = PORT_N64PTR(void, fileramaddr); break; } case MODELNODE_OPCODE_OP06: { ModelRoData_Op06Record* rodata = &node->Data->Op06; - rodata->BaseAddr = (void *)fileramaddr; + rodata->BaseAddr = PORT_N64PTR(void, fileramaddr); break; } @@ -6426,7 +6450,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr { ModelRoData_Op11Record* rodata = &node->Data->Op11; PROMOTE(rodata->unk0c[15]); - rodata->BaseAddr = (void *)fileramaddr; + rodata->BaseAddr = PORT_N64PTR(void, fileramaddr); break; } @@ -6434,7 +6458,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr { ModelRoData_GunfireRecord* rodata = &node->Data->Gunfire; PROMOTE(rodata->Image); - rodata->BaseAddr = (void *)fileramaddr; + rodata->BaseAddr = PORT_N64PTR(void, fileramaddr); break; } @@ -6443,7 +6467,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr ModelRoData_ShadowRecord* rodata = &node->Data->Shadow; PROMOTE(rodata->image); PROMOTE(rodata->Header); - rodata->BaseAddr = (void *)fileramaddr; + rodata->BaseAddr = PORT_N64PTR(void, fileramaddr); break; } @@ -6451,7 +6475,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr { ModelRoData_DisplayListPrimaryRecord* rodata = &node->Data->DisplayListPrimary; PROMOTE(rodata->Vertices); - rodata->BaseAddr = (void *)fileramaddr; + rodata->BaseAddr = PORT_N64PTR(void, fileramaddr); break; } diff --git a/src/game/mpmenu.c b/src/game/mpmenu.c index 1e60719a..52980bee 100644 --- a/src/game/mpmenu.c +++ b/src/game/mpmenu.c @@ -17,6 +17,12 @@ #include "mp_music.h" #include "file.h" #include "assets/obseg/text/LmpmenuE.h" +#if defined(PORT) +#include "port_addr.h" +#else +/* N64 build: the port address window is the identity (see port_addr.h). */ +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #ifdef REFRESH_PAL #define MPMENU_YOFF 8 /* PAL: every text row sits 8 pixels lower */ @@ -1407,7 +1413,7 @@ Gfx *mp_watch_menu_display(Gfx *gdl) // Must remain a comma expression for matching h2 = (s32) langGet(getStringID(LMPMENU, MPMENU_STR_1D_KILLS)), /* KILLS */ - sprintf(rankbuffer, ascii_pnum_KILLS, (char *) q, curplayernum + 1, (char *) h2); /* -> "P KILLS" */ + sprintf(rankbuffer, ascii_pnum_KILLS, PORT_N64PTR(char, q), curplayernum + 1, PORT_N64PTR(char, h2)); /* -> "P KILLS" */ textMeasure(&textheight, &textwidth, rankbuffer, ptrFontBankGothicChars, ptrFontBankGothic, 0); x = ((viGetViewLeft() + two_player_x_offset) - (textwidth >> 1)) + 80; @@ -1477,7 +1483,7 @@ Gfx *mp_watch_menu_display(Gfx *gdl) // Must remain a comma expression for matching. h2 = (s32) langGet(getStringID(LMPMENU, MPMENU_STR_1E_LOSSES)), /* LOSSES */ - sprintf(rankbuffer, ascii_pnum_LOSSES, (char *) q, curplayernum + 1, (char *) h2); /* -> "P LOSSES" */ + sprintf(rankbuffer, ascii_pnum_LOSSES, PORT_N64PTR(char, q), curplayernum + 1, PORT_N64PTR(char, h2)); /* -> "P LOSSES" */ textMeasure(&textheight, &textwidth, rankbuffer, ptrFontBankGothicChars, ptrFontBankGothic, 0); x = ((viGetViewLeft() + two_player_x_offset) - (textwidth >> 1)) + 80; @@ -1593,7 +1599,7 @@ Gfx *mp_watch_menu_display(Gfx *gdl) y = (viGetViewTop() - (fav_textheight >> 1)) + (37 + MPMENU_YOFF); viewleft = viGetX(); h1 = viGetY(); gdl = textRender(gdl, &x, &y, text, ptrFontBankGothicChars, ptrFontBankGothic, 0x00ff00b0, viewleft, h1, 0, 0); - text = frontGetPlayersFavoriteWeaponInHand(curplayernum, 0); + text = PORT_N64PTR(char, frontGetPlayersFavoriteWeaponInHand(curplayernum, 0)); textMeasure(&fav_textheight, &fav_textwidth, text, ptrFontBankGothicChars, ptrFontBankGothic, 0); x = ((viGetViewLeft() + fav_x_offset) - (fav_textwidth >> 1)) + 80; x2 = viGetViewTop(); @@ -1615,7 +1621,7 @@ Gfx *mp_watch_menu_display(Gfx *gdl) if (g_CurrentPlayer->ptr_text_first_mp_award) { - text = (char *) g_CurrentPlayer->ptr_text_first_mp_award; + text = PORT_N64PTR(char, g_CurrentPlayer->ptr_text_first_mp_award); textMeasure(&fav_textheight, &fav_textwidth, text, ptrFontBankGothicChars, ptrFontBankGothic, 0); x = ((viGetViewLeft() + fav_x_offset) - (fav_textwidth >> 1)) + 80; y = (viGetViewTop() - (fav_textheight >> 1)) + (75 + MPMENU_YOFF); @@ -1626,7 +1632,7 @@ Gfx *mp_watch_menu_display(Gfx *gdl) if (g_CurrentPlayer->ptr_text_second_mp_award) { - text = (char *) g_CurrentPlayer->ptr_text_second_mp_award; + text = PORT_N64PTR(char, g_CurrentPlayer->ptr_text_second_mp_award); textMeasure(&fav_textheight, &fav_textwidth, text, ptrFontBankGothicChars, ptrFontBankGothic, 0); x = ((viGetViewLeft() + fav_x_offset) - (fav_textwidth >> 1)) + 80; y = (viGetViewTop() - (fav_textheight >> 1)) + (88 + MPMENU_YOFF); diff --git a/src/game/objecthandler.c b/src/game/objecthandler.c index 7834e422..813bcca7 100644 --- a/src/game/objecthandler.c +++ b/src/game/objecthandler.c @@ -4,7 +4,7 @@ #include "model.h" // bss -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) // D40: the N64 placeholder chain below (g_ModelHitEntries .. g_ModelHitEntriesPenultimate) // forms one contiguous 600 x 20-byte ModelHitEntry pool in CODE.bss. On x86-64 // ModelHitEntry is 40 bytes (two pointer pairs), so initModelHitEntryFreeList() diff --git a/src/game/objecthandler_2.c b/src/game/objecthandler_2.c index 28a72acd..7b0bf3ff 100644 --- a/src/game/objecthandler_2.c +++ b/src/game/objecthandler_2.c @@ -1,4 +1,9 @@ #include +#if defined(PORT) +#include "port_addr.h" +#else +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #include "chrobjdata.h" #include "image.h" #include "math_asinfacosf.h" @@ -56,7 +61,7 @@ void sub_GAME_7F0762E0(ModelFileHeader *objheader, u8 *name, u8 *dst, struct tex delta = ((s32) ((romremaining + filedata) - (s32) name)) - ((s32) (((u8 *) objheader->Switches) + (((u32) gdl) & 0x00ffffff))); - texCopyGdls((Gfx *) (((u8 *) objheader->Switches) + (((u32) gdl) & 0x00ffffff)), (Gfx *) ((romremaining + filedata) - (s32) name), (s32) name); + texCopyGdls((Gfx *) (((u8 *) objheader->Switches) + (((u32) gdl) & 0x00ffffff)), PORT_N64PTR(Gfx, (romremaining + filedata) - (s32) name), (s32) name); texLoadFromModelFileHeader(objheader, buffer); @@ -86,7 +91,7 @@ void sub_GAME_7F0762E0(ModelFileHeader *objheader, u8 *name, u8 *dst, struct tex name = (u8 *) (((s32) (((u8 *) objheader->Switches) + (replacementgdl & 0x00ffffff))) - filedata); - fileSetSize(filenum, (u8 *) filedata, (((s32) name + 0xf) & (~0xf)), dst == 0); + fileSetSize(filenum, PORT_N64PTR(u8, filedata), (((s32) name + 0xf) & (~0xf)), dst == 0); } } diff --git a/src/game/options.c b/src/game/options.c index 1a8a321e..98bdd938 100644 --- a/src/game/options.c +++ b/src/game/options.c @@ -17,6 +17,12 @@ #include "glass.h" #include "frametiming.h" #include "assets/obseg/text/LoptionE.h" +#if defined(PORT) +#include "port_addr.h" +#else +/* N64 build: the port address window is the identity (see port_addr.h). */ +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #define WATCH_BACKGROUND_VERTEX_COUNT 30 @@ -1937,30 +1943,30 @@ Gfx *draw_abort_cancel_confirm(Gfx *gdl) if (watch_item_is_actively_selected != 0) { - textMeasure(&sp60, &sp64, sp54, pFontChars, pFontFile, 0); + textMeasure(&sp60, &sp64, PORT_N64PTR(char, sp54), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0); - gdl = textRender(gdl, &sp7C, &sp70, sp54, pFontChars, pFontFile, 0xA0FFA0F0, sp64, sp60, 0, 0); + gdl = textRender(gdl, &sp7C, &sp70, PORT_N64PTR(char, sp54), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xA0FFA0F0, sp64, sp60, 0, 0); if (D_800409A4 != 0) { - gdl = textRenderOutlined(gdl, &sp78, &sp6C, sp50, pFontChars, pFontFile, -1, 0x7000A0, viGetX(), viGetY(), 0, 0); - gdl = textRender(gdl, &sp74, &sp68, sp4C, pFontChars, pFontFile, 0xFF00B0, viGetX(), viGetY(), 0, 0); + gdl = textRenderOutlined(gdl, &sp78, &sp6C, PORT_N64PTR(char, sp50), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), -1, 0x7000A0, viGetX(), viGetY(), 0, 0); + gdl = textRender(gdl, &sp74, &sp68, PORT_N64PTR(char, sp4C), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xFF00B0, viGetX(), viGetY(), 0, 0); } else { if (D_800409A4 == 0) { - gdl = textRender(gdl, &sp78, &sp6C, sp50, pFontChars, pFontFile, 0xFF00B0, viGetX(), viGetY(), 0, 0); - gdl = textRenderOutlined(gdl, &sp74, &sp68, sp4C, pFontChars, pFontFile, -1, 0x7000A0, viGetX(), viGetY(), 0, 0); + gdl = textRender(gdl, &sp78, &sp6C, PORT_N64PTR(char, sp50), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xFF00B0, viGetX(), viGetY(), 0, 0); + gdl = textRenderOutlined(gdl, &sp74, &sp68, PORT_N64PTR(char, sp4C), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), -1, 0x7000A0, viGetX(), viGetY(), 0, 0); } } } else { - textMeasure(&sp60, &sp64, sp54, pFontChars, pFontFile, 0); - gdl = textRender(gdl, &sp7C, &sp70, sp54, pFontChars, pFontFile, 0x800080, sp64, sp60, 0, 0); - gdl = textRender(gdl, &sp78, &sp6C, sp50, pFontChars, pFontFile, 0x800080, viGetX(), viGetY(), 0, 0); - gdl = textRender(gdl, &sp74, &sp68, sp4C, pFontChars, pFontFile, 0x800080, viGetX(), viGetY(), 0, 0); + textMeasure(&sp60, &sp64, PORT_N64PTR(char, sp54), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0); + gdl = textRender(gdl, &sp7C, &sp70, PORT_N64PTR(char, sp54), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0x800080, sp64, sp60, 0, 0); + gdl = textRender(gdl, &sp78, &sp6C, PORT_N64PTR(char, sp50), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0x800080, viGetX(), viGetY(), 0, 0); + gdl = textRender(gdl, &sp74, &sp68, PORT_N64PTR(char, sp4C), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0x800080, viGetX(), viGetY(), 0, 0); } return gdl; @@ -1996,10 +2002,10 @@ Gfx *draw_text_mission_status(Gfx *gdl) } gdl = microcode_constructor(gdl); - textMeasure(&sp5C, &sp58, txtptr_1, pFontChars, pFontFile, 0); + textMeasure(&sp5C, &sp58, PORT_N64PTR(char, txtptr_1), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0); sp64 = 0x51; sp60 = YOFFSET_MISSIONSTATUS; - gdl = textRender(gdl, &sp64, &sp60, txtptr_1, pFontChars, pFontFile, 0xFF00B0, sp58, sp5C, 0, 0); + gdl = textRender(gdl, &sp64, &sp60, PORT_N64PTR(char, txtptr_1), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xFF00B0, sp58, sp5C, 0, 0); if (j_text_trigger) { @@ -2012,8 +2018,8 @@ Gfx *draw_text_mission_status(Gfx *gdl) sp64 = sp64 + sp58 + joffset + 4; sp60 = sp60 - sp5C; - textMeasure(&sp5C, &sp58, txtptr_2, pFontChars, pFontFile, 0); - gdl = textRender(gdl, &sp64, &sp60, txtptr_2, pFontChars, pFontFile, sp4C, sp58, sp5C, 0, 0); + textMeasure(&sp5C, &sp58, PORT_N64PTR(char, txtptr_2), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0); + gdl = textRender(gdl, &sp64, &sp60, PORT_N64PTR(char, txtptr_2), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), sp4C, sp58, sp5C, 0, 0); gdl = draw_abort_cancel_confirm(gdl); return gdl; @@ -2053,8 +2059,8 @@ Gfx *draw_text_q_watch_v201_beta(Gfx *gdl) pFontFile = ptrFontBankGothic; pFontChars = ptrFontBankGothicChars; gdl = microcode_constructor(gdl); - textMeasure(&sp48, &sp44, txtptr, pFontChars, pFontFile, 0); - gdl = textRender(gdl, &sp50, &sp4C, txtptr, pFontChars, pFontFile, 0xFF00B0, sp44, sp48, 0, 0); + textMeasure(&sp48, &sp44, PORT_N64PTR(char, txtptr), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0); + gdl = textRender(gdl, &sp50, &sp4C, PORT_N64PTR(char, txtptr), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xFF00B0, sp44, sp48, 0, 0); return gdl; } @@ -2189,16 +2195,16 @@ Gfx* draw_current_hand_item_and_ammo(Gfx* gdl) { #else sp88 = 0xBC; #endif - textMeasure(&sp84, &sp80, text, sp78, sp7C, 0); - gdl = textRender(gdl, &sp8C, &sp88, text, sp78, sp7C, 0xFF00B0, sp80, sp84, 0, 0); + textMeasure(&sp84, &sp80, text, PORT_N64PTR(struct fontchar, sp78), PORT_N64PTR(struct font, sp7C), 0); + gdl = textRender(gdl, &sp8C, &sp88, text, PORT_N64PTR(struct fontchar, sp78), PORT_N64PTR(struct font, sp7C), 0xFF00B0, sp80, sp84, 0, 0); #if defined(LEFTOVERDEBUG) sp88 = 0xAA; #else sp88 = 0xC6; #endif - textMeasure(&sp84, &sp80, text2, sp78, sp7C, 0); - gdl = textRender(gdl, &sp8C, &sp88, text2, sp78, sp7C, 0xFF00B0, sp80, sp84, 0, 0); + textMeasure(&sp84, &sp80, text2, PORT_N64PTR(struct fontchar, sp78), PORT_N64PTR(struct font, sp7C), 0); + gdl = textRender(gdl, &sp8C, &sp88, text2, PORT_N64PTR(struct fontchar, sp78), PORT_N64PTR(struct font, sp7C), 0xFF00B0, sp80, sp84, 0, 0); return gdl; } @@ -2428,11 +2434,11 @@ Gfx *draw_watch_inventory_page(Gfx *gdl, Mtx *param_2) gdl = microcode_constructor(gdl); - textMeasure(&textheight, &textwidth, string_builder_allocation, pFontChars2, pFontFile2, LINEHEIGHT()); + textMeasure(&textheight, &textwidth, string_builder_allocation, PORT_N64PTR(struct fontchar, pFontChars2), PORT_N64PTR(struct font, pFontFile2), LINEHEIGHT()); gdl = microcode_constructor_related_to_menus(gdl, 0x4E, WATCH_INV_BASE_Y(), textwidth + 0x4E, (LINEHEIGHT() * 5) + WATCH_INV_BASE_Y(), 0); - gdl = textRender(gdl, &x1, &y1, string_builder_allocation, pFontChars2, pFontFile2, 0xAA00B0, textwidth + 1, LINEHEIGHT() * 5, watch_inventory_text_y, LINEHEIGHT()); + gdl = textRender(gdl, &x1, &y1, string_builder_allocation, PORT_N64PTR(struct fontchar, pFontChars2), PORT_N64PTR(struct font, pFontFile2), 0xAA00B0, textwidth + 1, LINEHEIGHT() * 5, watch_inventory_text_y, LINEHEIGHT()); gdl = microcode_constructor_related_to_menus(gdl, 0x4B, temp_s0_3, textwidth + 0x52, (LINEHEIGHT() + temp_s0_3) - 2, 0x800050); @@ -2454,22 +2460,22 @@ Gfx *draw_watch_inventory_page(Gfx *gdl, Mtx *param_2) gdl = microcode_constructor(gdl); - textMeasure(&y2, &x2, formattedString, pFontChars, pFontFile, 0); + textMeasure(&y2, &x2, formattedString, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0); if (watch_inventory_text_is_settled != 0) { - textMeasure(&y2, &x2, invItemName, pFontChars, pFontFile, LINEHEIGHT()); + textMeasure(&y2, &x2, invItemName, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), LINEHEIGHT()); x1 = 0x4E; y1 = (LINEHEIGHT() * 2) + WATCH_INV_BASE_Y(); if (D_800409C4 == 0) { - gdl = textRender(gdl, &x1, &y1, invItemName, pFontChars, pFontFile, 0xA0FFA0F0, x2, 0x64, 0, LINEHEIGHT()); + gdl = textRender(gdl, &x1, &y1, invItemName, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xA0FFA0F0, x2, 0x64, 0, LINEHEIGHT()); } else { - gdl = textRenderOutlined(gdl, &x1, &y1, invItemName, pFontChars, pFontFile, -1, 0x7000A0, x2 + 1, 0x64, 0, LINEHEIGHT()); + gdl = textRenderOutlined(gdl, &x1, &y1, invItemName, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), -1, 0x7000A0, x2 + 1, 0x64, 0, LINEHEIGHT()); } sub_GAME_7F0A8378(); @@ -2521,7 +2527,7 @@ Gfx *unused_draw_watch_inventory_page(Gfx *gdl, Mtx *param_2) { game_options_inventory_navigation(); gdl = microcode_constructor(gdl); - textMeasure(&sp58, &sp54, long_name, pFontChars, pFontFile, 0); + textMeasure(&sp58, &sp54, long_name, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0); sp70 = ((s32) (0xAA - sp54) / 2) + 0x4B; temp_2 = sp70; @@ -2534,16 +2540,16 @@ Gfx *unused_draw_watch_inventory_page(Gfx *gdl, Mtx *param_2) { sub_GAME_7F0A8378(); if (D_800409C4 == 0) { - gdl = textRender(gdl, &sp70, &sp6C, long_name, pFontChars, pFontFile, 0xA0FFA0F0, sp54, 0x64, 0, 0); + gdl = textRender(gdl, &sp70, &sp6C, long_name, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xA0FFA0F0, sp54, 0x64, 0, 0); } else { - gdl = textRenderOutlined(gdl, &sp70, &sp6C, long_name, pFontChars, pFontFile, -1, 0x7000A0, sp54 + 1, 0x64, 0, 0); + gdl = textRenderOutlined(gdl, &sp70, &sp6C, long_name, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), -1, 0x7000A0, sp54 + 1, 0x64, 0, 0); } } else { - gdl = textRender(gdl, &sp70, &sp6C, long_name, pFontChars, pFontFile, 0xAA00B0, sp54, 0x64, 0, 0); + gdl = textRender(gdl, &sp70, &sp6C, long_name, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xAA00B0, sp54, 0x64, 0, 0); } } @@ -3260,11 +3266,11 @@ Gfx *display_text_buttons_dual_control(Gfx *gdl) if (joyGetButtons(PLAYER_1, Z_TRIG)) { - gdl = draw_options_labels(gdl, 0x5A, YOFFSET_5, textptr_aux, -1, 1, 0x7000A0, 0, 0, 0x3000B0, 0); + gdl = draw_options_labels(gdl, 0x5A, YOFFSET_5, PORT_N64PTR(char, textptr_aux), -1, 1, 0x7000A0, 0, 0, 0x3000B0, 0); } else { - gdl = draw_options_labels(gdl, 0x5A, YOFFSET_5, textptr_aux, 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 0); + gdl = draw_options_labels(gdl, 0x5A, YOFFSET_5, PORT_N64PTR(char, textptr_aux), 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 0); } if ((g_CurrentPlayer->cur_player_control_type_0 == CONTROLLER_CONFIG_PLENTY) || (g_CurrentPlayer->cur_player_control_type_0 == CONTROLLER_CONFIG_DOMINO)) @@ -3276,7 +3282,7 @@ Gfx *display_text_buttons_dual_control(Gfx *gdl) textptr_aux = langGet(getStringID(LOPTIONS, OPTION_STR_06_LOOK_LF)); //look } - gdl = draw_options_labels(gdl, 0x5A, YOFFSET_4, textptr_aux, 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 0); + gdl = draw_options_labels(gdl, 0x5A, YOFFSET_4, PORT_N64PTR(char, textptr_aux), 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 0); if (joyGetButtons(1, A_BUTTON)) { @@ -3307,11 +3313,11 @@ Gfx *display_text_buttons_dual_control(Gfx *gdl) if (joyGetButtons(1, Z_TRIG)) { - gdl = draw_options_labels(gdl, 0xE6, YOFFSET_5, textptr_aux, -1, 1, 0x7000A0, 0, 0, 0x3000B0, 1); + gdl = draw_options_labels(gdl, 0xE6, YOFFSET_5, PORT_N64PTR(char, textptr_aux), -1, 1, 0x7000A0, 0, 0, 0x3000B0, 1); } else { - gdl = draw_options_labels(gdl, 0xE6, YOFFSET_5, textptr_aux, 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 1); + gdl = draw_options_labels(gdl, 0xE6, YOFFSET_5, PORT_N64PTR(char, textptr_aux), 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 1); } if ((g_CurrentPlayer->cur_player_control_type_0 == CONTROLLER_CONFIG_PLENTY) || (g_CurrentPlayer->cur_player_control_type_0 == CONTROLLER_CONFIG_DOMINO)) @@ -3323,7 +3329,7 @@ Gfx *display_text_buttons_dual_control(Gfx *gdl) textptr_aux = langGet(getStringID(LOPTIONS, OPTION_STR_05_MOVE_LF)); //move } - gdl = draw_options_labels(gdl, 0xE6, YOFFSET_4, textptr_aux, 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 1); + gdl = draw_options_labels(gdl, 0xE6, YOFFSET_4, PORT_N64PTR(char, textptr_aux), 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 1); return gdl; } @@ -3554,14 +3560,14 @@ Gfx *draw_watch_control_options_page(Gfx *gdl, Mtx *param_2) { } } - textMeasure(&sp50, &sp54, textptr, pFontChars, pFontFile, 0); + textMeasure(&sp50, &sp54, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0); if ((watch_item_is_actively_selected != 0) && (controller_options_index == CONTROLLER_OPTIONS_INDEX_STYLE)) { - gdl = textRenderOutlined(gdl, &sp5C, &sp58, textptr, pFontChars, pFontFile, phi_s1, 0x7000A0, sp54 + 1, sp50, 0, 0); + gdl = textRenderOutlined(gdl, &sp5C, &sp58, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), phi_s1, 0x7000A0, sp54 + 1, sp50, 0, 0); } else { - gdl = textRender(gdl, &sp5C, &sp58, textptr, pFontChars, pFontFile, phi_s1, sp54, sp50, 0, 0); + gdl = textRender(gdl, &sp5C, &sp58, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), phi_s1, sp54, sp50, 0, 0); } gdl = draw_controller_style_text(gdl); @@ -3585,14 +3591,14 @@ Gfx *draw_watch_control_options_page(Gfx *gdl, Mtx *param_2) { } } - textMeasure(&sp50, &sp54, textptr, pFontChars, pFontFile, 0); + textMeasure(&sp50, &sp54, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0); if ((watch_item_is_actively_selected != 0) && (controller_options_index == CONTROLLER_OPTIONS_INDEX_INPUTS)) { - gdl = textRenderOutlined(gdl, &sp5C, &sp58, textptr, pFontChars, pFontFile, phi_s1, 0x7000A0, sp54 + 1, sp50, 0, 0); + gdl = textRenderOutlined(gdl, &sp5C, &sp58, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), phi_s1, 0x7000A0, sp54 + 1, sp50, 0, 0); } else { - gdl = textRender(gdl, &sp5C, &sp58, textptr, pFontChars, pFontFile, phi_s1, sp54, sp50, 0, 0); + gdl = textRender(gdl, &sp5C, &sp58, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), phi_s1, sp54, sp50, 0, 0); } } @@ -3859,15 +3865,15 @@ Gfx *draw_watch_game_options_page(Gfx *gdl, Mtx *param_2) { } } - textMeasure(&sp48, &sp4C, textptr, pFontChars, pFontFile, 0); + textMeasure(&sp48, &sp4C, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0); if ((watch_item_is_actively_selected != 0) && (game_options_index == 0)) { - gdl = textRenderOutlined(gdl, &sp54, &sp50, textptr, pFontChars, pFontFile, sp5C, 0x7000A0, sp4C + 1, sp48, 0, 0); + gdl = textRenderOutlined(gdl, &sp54, &sp50, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), sp5C, 0x7000A0, sp4C + 1, sp48, 0, 0); } else { - gdl = textRender(gdl, &sp54, &sp50, textptr, pFontChars, pFontFile, sp5C, sp4C, sp48, 0, 0); + gdl = textRender(gdl, &sp54, &sp50, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), sp5C, sp4C, sp48, 0, 0); } sp5C = 0xFF00B0; @@ -3886,15 +3892,15 @@ Gfx *draw_watch_game_options_page(Gfx *gdl, Mtx *param_2) { } } - textMeasure(&sp48, &sp4C, textptr, pFontChars, pFontFile, 0); + textMeasure(&sp48, &sp4C, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0); if ((watch_item_is_actively_selected != 0) && (game_options_index == 1)) { - gdl = textRenderOutlined(gdl, &sp54, &sp50, textptr, pFontChars, pFontFile, sp5C, 0x7000A0, sp4C + 1, sp48, 0, 0); + gdl = textRenderOutlined(gdl, &sp54, &sp50, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), sp5C, 0x7000A0, sp4C + 1, sp48, 0, 0); } else { - gdl = textRender(gdl, &sp54, &sp50, textptr, pFontChars, pFontFile, sp5C, sp4C, sp48, 0, 0); + gdl = textRender(gdl, &sp54, &sp50, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), sp5C, sp4C, sp48, 0, 0); } gdl = draw_toggle_options(gdl); diff --git a/src/game/othermodemicrocode.c b/src/game/othermodemicrocode.c index b1695502..3b62c7d8 100644 --- a/src/game/othermodemicrocode.c +++ b/src/game/othermodemicrocode.c @@ -1,4 +1,9 @@ #include +#if defined(PORT) +#include "port_addr.h" +#else +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #include #include "tex.h" #include "image.h" @@ -364,7 +369,7 @@ void texSelect(Gfx **gdlptr, struct sImageTableEntry *tconfig, u32 arg2, s32 arg texLoad((s32 *)tconfig, NULL); } - aa = PHYS_TO_K0(tconfig->index); + aa = PORT_N64PTR(u16, tconfig->index); /* D298/M2: index holds the pool address */ tex = texFindInPool((aa)[-4], NULL); if (tconfig->level == 0) diff --git a/src/game/prop.c b/src/game/prop.c index 551bab45..0060ce5e 100644 --- a/src/game/prop.c +++ b/src/game/prop.c @@ -1324,19 +1324,19 @@ void proplvreset2(enum LEVELID stageId) * The setup file stores every internal reference as a byte offset from the start of the file, * so rebase them all onto the RAM copy at local_stage. */ - g_CurrentSetup.pathwaypoints = (void *) (((u32) local_stage) + ((u32) local_stage->pathwaypoints)); - g_CurrentSetup.waypointgroups = (void *) (((u32) local_stage) + ((u32) local_stage->waypointgroups)); - g_CurrentSetup.intro = (void *) (((u32) local_stage) + ((u32) local_stage->intro)); - g_CurrentSetup.propDefs = (void *) (((u32) local_stage) + ((u32) local_stage->propDefs)); - g_CurrentSetup.patrolpaths = (void *) (((u32) local_stage) + ((u32) local_stage->patrolpaths)); - g_CurrentSetup.ailists = (void *) (((u32) local_stage) + ((u32) local_stage->ailists)); - g_CurrentSetup.pads = (void *) (((u32) local_stage) + ((u32) local_stage->pads)); - g_CurrentSetup.boundpads = (void *) (((u32) local_stage) + ((u32) local_stage->boundpads)); + g_CurrentSetup.pathwaypoints = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->pathwaypoints)); + g_CurrentSetup.waypointgroups = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->waypointgroups)); + g_CurrentSetup.intro = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->intro)); + g_CurrentSetup.propDefs = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->propDefs)); + g_CurrentSetup.patrolpaths = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->patrolpaths)); + g_CurrentSetup.ailists = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->ailists)); + g_CurrentSetup.pads = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->pads)); + g_CurrentSetup.boundpads = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->boundpads)); // Pad names and bound names are optional. An offset of 0 means absent. if (local_stage->padnames != 0) { - g_CurrentSetup.padnames = (void *) (((u32) local_stage) + ((u32) local_stage->padnames)); + g_CurrentSetup.padnames = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->padnames)); } else { @@ -1345,7 +1345,7 @@ void proplvreset2(enum LEVELID stageId) if (local_stage->boundpadnames != 0) { - g_CurrentSetup.boundpadnames = (void *) (((u32) local_stage) + ((u32) local_stage->boundpadnames)); + g_CurrentSetup.boundpadnames = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->boundpadnames)); } else { @@ -1356,7 +1356,7 @@ void proplvreset2(enum LEVELID stageId) { for (i1 = 0; g_CurrentSetup.pathwaypoints[i1].padID >= 0; i1++) { - g_CurrentSetup.pathwaypoints[i1].neighbours = (void *) (((u32) g_CurrentSetup.pathwaypoints[i1].neighbours) + ((u32) local_stage)); + g_CurrentSetup.pathwaypoints[i1].neighbours = (void *) (((u32) g_CurrentSetup.pathwaypoints[i1].neighbours) + ((uintptr_t) local_stage)); } } @@ -1364,8 +1364,8 @@ void proplvreset2(enum LEVELID stageId) { for (i2 = 0; g_CurrentSetup.waypointgroups[i2].neighbours; i2++) { - g_CurrentSetup.waypointgroups[i2].neighbours = (void *) (((u32) g_CurrentSetup.waypointgroups[i2].neighbours) + ((u32) local_stage)); - g_CurrentSetup.waypointgroups[i2].waypoints = (void *) (((u32) g_CurrentSetup.waypointgroups[i2].waypoints) + ((u32) local_stage)); + g_CurrentSetup.waypointgroups[i2].neighbours = (void *) (((u32) g_CurrentSetup.waypointgroups[i2].neighbours) + ((uintptr_t) local_stage)); + g_CurrentSetup.waypointgroups[i2].waypoints = (void *) (((u32) g_CurrentSetup.waypointgroups[i2].waypoints) + ((uintptr_t) local_stage)); } } @@ -1376,7 +1376,7 @@ void proplvreset2(enum LEVELID stageId) { for (i3 = 0; g_CurrentSetup.ailists[i3].ailist != 0; i3++) { - g_CurrentSetup.ailists[i3].ailist = (void *) (((u32) g_CurrentSetup.ailists[i3].ailist) + ((u32) local_stage)); + g_CurrentSetup.ailists[i3].ailist = (void *) (((u32) g_CurrentSetup.ailists[i3].ailist) + ((uintptr_t) local_stage)); } } } @@ -1385,7 +1385,7 @@ void proplvreset2(enum LEVELID stageId) { for (i3 = 0; g_CurrentSetup.patrolpaths[i3].waypoints != NULL; i3++) { - g_CurrentSetup.patrolpaths[i3].waypoints = (void *) (((u32) g_CurrentSetup.patrolpaths[i3].waypoints) + ((u32) local_stage)); + g_CurrentSetup.patrolpaths[i3].waypoints = (void *) (((u32) g_CurrentSetup.patrolpaths[i3].waypoints) + ((uintptr_t) local_stage)); for (i5 = 0; g_CurrentSetup.patrolpaths[i3].waypoints[i5] >= 0; i5++) { @@ -1405,7 +1405,7 @@ void proplvreset2(enum LEVELID stageId) for (; pad->plink != NULL; pad++) { - pad->plink = (void *) (((u32) local_stage) + ((u32) pad->plink)); + pad->plink = (void *) (((uintptr_t) local_stage) + ((u32) pad->plink)); pad->pos.f[0] *= roompos_1; pad->pos.f[1] *= roompos_1; pad->pos.f[2] *= roompos_1; @@ -1460,9 +1460,9 @@ void proplvreset2(enum LEVELID stageId) /** Ugly matching hack. * TODO: investigate if there's a way to get rid of this. */ - if ((((u32) local_stage) ^ 0) + ((u32)vol->plink)); + if ((((uintptr_t) local_stage) ^ 0) + ((u32)vol->plink)); - vol->plink = (void *) (((u32) local_stage) + ((u32)vol->plink)); + vol->plink = (void *) (((uintptr_t) local_stage) + ((u32)vol->plink)); vol->pos.f[0] *= roompos_2; vol->pos.f[1] *= roompos_2; vol->pos.f[2] *= roompos_2; @@ -1497,7 +1497,7 @@ void proplvreset2(enum LEVELID stageId) { for (i1 = 0; g_CurrentSetup.padnames[i1].p; i1++) { - g_CurrentSetup.padnames[i1].p = (void *) (((u32) g_CurrentSetup.padnames[i1].p) + ((u32) local_stage)); + g_CurrentSetup.padnames[i1].p = (void *) (((u32) g_CurrentSetup.padnames[i1].p) + ((uintptr_t) local_stage)); } } @@ -1508,7 +1508,7 @@ void proplvreset2(enum LEVELID stageId) for (i1 = 0; g_CurrentSetup.boundpadnames[i1].p; i1++) { - g_CurrentSetup.boundpadnames[i1].p = (void *) (((u32) g_CurrentSetup.boundpadnames[i1].p) + ((u32) local_stage)); + g_CurrentSetup.boundpadnames[i1].p = (void *) (((u32) g_CurrentSetup.boundpadnames[i1].p) + ((uintptr_t) local_stage)); } } diff --git a/src/game/propobj.c b/src/game/propobj.c index 1c38e18b..b63b080e 100644 --- a/src/game/propobj.c +++ b/src/game/propobj.c @@ -7,6 +7,11 @@ ---------------------------------------------------------------------*/ #include +#if defined(PORT) +#include "port_addr.h" +#else +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #include #ifdef PORT #include /* D202/M-65 diag probe only (doorSndProbe); remove with it */ @@ -5725,7 +5730,7 @@ s32 objTick(struct PropRecord *prop) #endif temp_s0_6 = render_pad2F4->model; - if (temp_s0_6->anim == (ModelAnimation *)animation_table_ptrs2[1]) /* D32/D33 */ + if (temp_s0_6->anim == PORT_N64PTR(ModelAnimation, animation_table_ptrs2[1])) /* D32/D33 */ { modelSetAnimTranslationScale(temp_s0_6, 10.438f); setsubroty(render_pad2F4->model, M_PI_F); diff --git a/src/game/rsp.c b/src/game/rsp.c index 4eab881a..bf9cc11b 100644 --- a/src/game/rsp.c +++ b/src/game/rsp.c @@ -273,7 +273,7 @@ void rspGfxTaskStart(Gfx *firstGdl, Gfx *gdl, s32 arg2, OSMesg rspReplyMsg) // Toggles the g_gfxTaskSettingsList to point to altnerate tasks. // Depends on first assignment taking place (g_gfxTaskSettingsList = g_gfxTaskSettings[0]). // Not a typo, but probably not best practice. -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) // D32-class: the XOR idiom below truncates exe pointers to u32 on x86-64 and // yields garbage; toggle explicitly (same semantics: list is always &[0] or &[1]). g_gfxTaskSettingsList = (g_gfxTaskSettingsList == &g_gfxTaskSettings[0]) ? &g_gfxTaskSettings[1] : &g_gfxTaskSettings[0]; diff --git a/src/game/stan.c b/src/game/stan.c index ec49ffdf..12739a7e 100644 --- a/src/game/stan.c +++ b/src/game/stan.c @@ -2,6 +2,12 @@ //#include #include #include "stan.h" +#if defined(PORT) +#include "port_addr.h" +#else +/* N64 build: the port address window is the identity (see port_addr.h). */ +#define PORT_N64PTR(T, x) ((T *)(x)) +#endif #include "bg.h" #include "chrai.h" #include "chr.h" @@ -3239,7 +3245,7 @@ void stanDetermineEOF(struct StanPrefixRecord *file /* canonically r */, s32 ori assert(*r==0); #endif - standTileStart = (StandTile *)(((s32)file->ptr_firstroom + delta) - 0x80); + standTileStart = PORT_N64PTR(StandTile, ((s32)file->ptr_firstroom + delta) - 0x80); ptr_firstroom_0 = (s32)file->ptr_firstroom + delta; newBase = list_of_tilesizes; @@ -3249,7 +3255,7 @@ void stanDetermineEOF(struct StanPrefixRecord *file /* canonically r */, s32 ori { do { - *roomPtr = (void *) ((s32) (*roomPtr) + delta); + *roomPtr = PORT_N64PTR(void, (s32)(*roomPtr) + delta); roomPtr++; } while (*roomPtr != NULL); @@ -3266,7 +3272,7 @@ void stanDetermineEOF(struct StanPrefixRecord *file /* canonically r */, s32 ori // Fake but required for matching. if (tile->tail.half); - tile = (StandTile *)((s32)tile + tile = PORT_N64PTR(StandTile, (s32)tile + (tileSizes = newBase)[(tile->tail.half >> 0xc) & 0xf]); } while (*(s32 *) tile != 0); diff --git a/src/game/title.c b/src/game/title.c index 84b35af9..e324d2cd 100644 --- a/src/game/title.c +++ b/src/game/title.c @@ -28,6 +28,10 @@ #ifdef PORT #include +#include "port_addr.h" +#else +/* N64 build: the port address window is the identity (see port_addr.h). */ +#define PORT_N64PTR(T, x) ((T *)(x)) #endif @@ -221,7 +225,7 @@ Gfx *sub_GAME_7F007F30(Gfx *gdl, s32 count, Mtxf *matrix) if (gunbarrelTimer == BOND_EYE_ANIM_START) { - modelSetAnimation(chrModelInstance, (struct ModelAnimation *) ((s32) &ANIM_DATA_bond_eye_fire + (s32) &ptr_animation_table->data), 0, 2.0f, 0.910000026f, 16.0f); + modelSetAnimation(chrModelInstance, PORT_N64PTR(struct ModelAnimation, (s32) &ANIM_DATA_bond_eye_fire + (s32) &ptr_animation_table->data), 0, 2.0f, 0.910000026f, 16.0f); } if (gunbarrelTimer == BOND_EYE_ANIM_SPEEDUP) @@ -397,7 +401,7 @@ void setupRarewareLogoData(s32 address, s32 size) { D_8002A89C = -40.0f; intro_eye_counter = 0; virtualaddress = address; - romCopy(virtualaddress, &_rarewarelogoSegmentRomStart, ALIGN64_V2((u32)&_rarewarelogoSegmentEnd - (u32)&_rarewarelogoSegmentStart)); + romCopy(PORT_N64PTR(void, virtualaddress), &_rarewarelogoSegmentRomStart, ALIGN64_V2((u32)&_rarewarelogoSegmentEnd - (u32)&_rarewarelogoSegmentStart)); } @@ -416,7 +420,7 @@ Gfx *retrieve_display_rareware_logo(Gfx *gdl) #endif D_8002A7D0 = (1 - D_8002A7D0); - gSPSegment(gdl++, SPSEGMENT_GETITLE, osVirtualToPhysical(virtualaddress)); + gSPSegment(gdl++, SPSEGMENT_GETITLE, osVirtualToPhysical(PORT_N64PTR(void, virtualaddress))); if ((gunbarrel_mode == 0) || (gunbarrel_mode == 1)) { s32 var1; s32 var2; @@ -461,8 +465,8 @@ void sub_GAME_7F008DE4(u8 **addr, s32 *size) { *size -= 0x40400; *addr += 0x40400; dword_CODE_bss_80069588 = *addr; - romCopy(dword_CODE_bss_80069588, (void *)(s32)&unknown2, ALIGN64_V2(((u32)&unknown2_end - (u32)&unknown2))); - rle_expand_8bit(dword_CODE_bss_80069588, dword_CODE_bss_8006958C); + romCopy(PORT_N64PTR(void, dword_CODE_bss_80069588), PORT_N64PTR(void, &unknown2), ALIGN64_V2(((u32)&unknown2_end - (u32)&unknown2))); + rle_expand_8bit(PORT_N64PTR(u8, dword_CODE_bss_80069588), PORT_N64PTR(u8, dword_CODE_bss_8006958C)); } @@ -487,7 +491,7 @@ void initializeGunBarrelIntro(u8 *gfxBuffer, s32 bufferSize) bufferSize -= 0x200; gfxBuffer += 0x200; - createGunbarrelRenderHole(barrelDisplayListPtr, 0x1E); + createGunbarrelRenderHole(PORT_N64PTR(struct s_display_list_something, barrelDisplayListPtr), 0x1E); gunbarrelgfxListPointer = (Gfx*)gfxBuffer; #ifdef PORT @@ -517,7 +521,7 @@ void initializeGunBarrelIntro(u8 *gfxBuffer, s32 bufferSize) * N64 K0 value from the physical offset; seg_addr() passes it through to * the KSEG0 mirror at 0x80000000 (segments 7/8 are never registered, so * the unmarked-segment path is skipped). */ - sub_GAME_7F01BFF8(gunbarrelgfxListPointer, (Vtx *)(OS_K0_TO_PHYSICAL((void *)barrelDisplayListPtr) | 0x80000000u), 0x1E); + sub_GAME_7F01BFF8(gunbarrelgfxListPointer, (Vtx *)(OS_K0_TO_PHYSICAL((void *)portN64ToHost((u32)barrelDisplayListPtr)) | 0x80000000u), 0x1E); #else sub_GAME_7F01BFF8(gunbarrelgfxListPointer, barrelDisplayListPtr + 0x80000000, 0x1E); #endif @@ -559,7 +563,7 @@ void initializeGunBarrelIntro(u8 *gfxBuffer, s32 bufferSize) modelSetAnimPlaySpeed(chrModelInstance, S_7F008E80_ANIM_SPEED, 0.0f); #undef S_7F008E80_ANIM_SPEED - animation = (struct ModelAnimation*)((s32)ptr_animation_table + (s32)&ANIM_DATA_bond_eye_walk); + animation = PORT_N64PTR(struct ModelAnimation, (s32)ptr_animation_table + (s32)&ANIM_DATA_bond_eye_walk); startframe = animation->unk04 - 0x44; while (startframe < 0) { diff --git a/src/libultra/audio/cseq.c b/src/libultra/audio/cseq.c index 2b259b78..672e47f7 100644 --- a/src/libultra/audio/cseq.c +++ b/src/libultra/audio/cseq.c @@ -49,7 +49,7 @@ void alCSeqNew(ALCSeq *seq, u8 *ptr) { flagTmp = 1 << i; seq->validTracks |= flagTmp; - seq->curLoc[i] = (u8*)((u32)ptr + tmpOff); + seq->curLoc[i] = (u8*)((uintptr_t)ptr + tmpOff); seq->evtDeltaTicks[i] = __readVarLen(seq,i); /*__alCSeqGetTrackEvent(seq,i); prime the event buffers */ } diff --git a/src/libultra/audio/load.c b/src/libultra/audio/load.c index 154440f1..96c876dd 100644 --- a/src/libultra/audio/load.c +++ b/src/libultra/audio/load.c @@ -23,7 +23,7 @@ #include "synthInternals.h" #include #include -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) #include #include #include "audiotrace.h" /* D202/M-70: serialized trace writer */ @@ -200,7 +200,7 @@ Acmd *alAdpcmPull(void *filter, s16 *outp, s32 outCount, s32 sampleOffset, Acmd f->memin += ADPCMFBYTES*nframes; } -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) /* D202/M-65 diag (temporary): a wave with no ALADPCMloop must run out and * be zero-filled (nOver > 0) once memin passes base+len. If a voice keeps * decoding past that point, an unstopped voice stays audible forever @@ -393,7 +393,7 @@ alLoadParam(void *filter, s32 paramID, void *param) switch (paramID) { case (AL_FILTER_SET_WAVETABLE): a->table = (ALWaveTable *) param; -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) if (GE_ENVFLAG("GE_AUDIOTRACE")) { extern uint64_t sysGetMicroseconds(void); /* port/src/system.c */ geTracePrintf("audiotrace_wire.log", "[WIRE] t=%llu filter=%p <- table=%p base=%p len=%d book=%p\n", @@ -496,7 +496,7 @@ Acmd *_decodeChunk(Acmd *ptr, ALLoadFilter *f, s32 tsam, s32 nbytes, s16 outp, s dramLoc; if (nbytes > 0){ -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) if (GE_ENVFLAG("GE_MIXERTRACE")) { static FILE *dtf = NULL; if (!dtf) { dtf = fopen("mixertrace.log", "a"); if (dtf) setvbuf(dtf, NULL, _IONBF, 0); } diff --git a/src/libultra/audio/save.c b/src/libultra/audio/save.c index fe68dd30..9136e7c8 100644 --- a/src/libultra/audio/save.c +++ b/src/libultra/audio/save.c @@ -21,7 +21,7 @@ #include #include "synthInternals.h" #include -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) #endif #include "include/assert.h" diff --git a/src/libultra/audio/synsetfxmix.c b/src/libultra/audio/synsetfxmix.c index 4665ba11..3efa9d1a 100644 --- a/src/libultra/audio/synsetfxmix.c +++ b/src/libultra/audio/synsetfxmix.c @@ -19,7 +19,7 @@ *====================================================================*/ #include "synthInternals.h" -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) #endif #include #include diff --git a/src/libultra/audio/synsetpan.c b/src/libultra/audio/synsetpan.c index 7fd44436..0271b5ef 100644 --- a/src/libultra/audio/synsetpan.c +++ b/src/libultra/audio/synsetpan.c @@ -19,7 +19,7 @@ *====================================================================*/ #include "synthInternals.h" -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) #endif #include #include diff --git a/src/libultra/audio/synsetvol.c b/src/libultra/audio/synsetvol.c index c8f8a73a..dfda4e71 100644 --- a/src/libultra/audio/synsetvol.c +++ b/src/libultra/audio/synsetvol.c @@ -19,7 +19,7 @@ *====================================================================*/ #include "synthInternals.h" -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) #endif #include #include diff --git a/src/libultra/audio/synthesizer.c b/src/libultra/audio/synthesizer.c index f870d287..9ad74201 100644 --- a/src/libultra/audio/synthesizer.c +++ b/src/libultra/audio/synthesizer.c @@ -20,7 +20,7 @@ #include "synthInternals.h" #include "include/assert.h" -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) #include #include #endif @@ -122,7 +122,7 @@ void alSynNew(ALSynth *drvr, ALSynConfig *c) pv->channelKnob = (ALFilter *)&pv->envmixer; -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) /* D202/M-69 diag (temporary): GE_AUDIOTRACE=1 logs the full * PVoice -> decoder/resampler/env-state address map at synth init, * so offline voicedump slot addresses can be tied to WIRE filter diff --git a/src/mema.c b/src/mema.c index e50583ac..d8c095cd 100644 --- a/src/mema.c +++ b/src/mema.c @@ -1,4 +1,7 @@ #include +#if defined(PORT) +#include "port_addr.h" /* D298/M2: memaAlloc returns a DRAM address */ +#endif #include "mema.h" #include "deb.h" @@ -314,7 +317,13 @@ void *memaAlloc(u32 amount) { best->addr = 0; } +#if defined(PORT) + /* D298/M2: best->addr is an N64 DRAM address; hand back a live pointer. + * Identity at PORT_ADDR_BASE == 0. */ + return portN64ToHost((u32)addr); +#else return (void*)addr; +#endif } // Find the memaspace of the given address and reduce its size by the given diff --git a/src/memp.c b/src/memp.c index 0fafbeb1..6f83b7a0 100644 --- a/src/memp.c +++ b/src/memp.c @@ -2,6 +2,9 @@ #include #include "memp.h" #include "game/language.h" +#if defined(PORT) +#include "port_addr.h" /* portN64ToHost: DRAM window base (M2/D298) */ +#endif /** * EU .data, offset from start of data_seg : 0x3640 @@ -43,8 +46,18 @@ void mempCheckMemflagTokens(s32 poolAreaStart, s32 poolAreaSize) //set pool 0 to what boss wants (room_model_buffer) //pool 0 = TotalPoolArea +#if defined(PORT) + /* D298/M2: the pool lives in the N64 DRAM window, which on macOS is + * shifted up by PORT_ADDR_BASE. boss.c passes N64 addresses (the pool + * must stay s32 for mempCheckMemflagTokens' arithmetic), so convert to + * host pointers here — the MemoryPool fields are u8*. Identity at + * PORT_ADDR_BASE == 0. The N64 build keeps the original line. */ + g_mempPools[MEMPOOL_TOTAL].start = (u8 *)portN64ToHost((u32)poolAreaStart); + g_mempPools[MEMPOOL_TOTAL].end = g_mempPools[MEMPOOL_TOTAL].start + poolAreaSize; +#else g_mempPools[MEMPOOL_TOTAL].start = poolAreaStart; g_mempPools[MEMPOOL_TOTAL].end = poolAreaStart + poolAreaSize; +#endif poolSizes = sdefaultmvals; @@ -63,7 +76,7 @@ void mempCheckMemflagTokens(s32 poolAreaStart, s32 poolAreaSize) if (poolSizes.me == 0) { poolSizes.mf = 0; -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) /* D36 (PC port): the PERMANENT bank must also hold the enlarged * music heap (MUSIC_ALLOCATION_BYTES in src/music.c, 0x2E000 -> * 0x32000 on x86-64 due to libaudio pointer bloat). Grow the bank on @@ -91,7 +104,6 @@ void mempSetBankStarts(s32 poolSizes[MEMPOOL_COUNT+1]) s32 bankstarts[MEMPOOL_COUNT] = {0}; s32 mempLen; s32 mempRequested; - s32 mempStart; //set MF, ML, ME first i = 0; @@ -130,13 +142,27 @@ void mempSetBankStarts(s32 poolSizes[MEMPOOL_COUNT+1]) // {0,0,0,0,poolAreaSize - 303104, poolAreaSize - 303104, poolAreaSize} - mempStart = g_mempPools[MEMPOOL_TOTAL].start; +#if defined(PORT) + { + uintptr_t mempStart = (uintptr_t)g_mempPools[MEMPOOL_TOTAL].start; +#else + { + s32 mempStart = g_mempPools[MEMPOOL_TOTAL].start; +#endif //for each bank 1-7, add new start position for (i = MEMPOOL_TOTAL; i < MEMPOOL_COUNT - 1; i++) { +#if defined(PORT) + /* D298/M2: pool fields are u8* host pointers in the DRAM window. */ + g_mempPools[i + 1].start = (u8 *)(mempStart + bankstarts[i]); + g_mempPools[i + 1].pos = 0; + g_mempPools[i + 1].end = (u8 *)(mempStart + bankstarts[i + 1]); +#else g_mempPools[i + 1].start = bankstarts[i] + mempStart; g_mempPools[i + 1].pos = 0; g_mempPools[i + 1].end = bankstarts[i + 1] + mempStart; +#endif + } } /* rel-start size @@ -201,6 +227,12 @@ void *mempAllocBytesInBank(u32 bytes, u8 poolnum) nulled_mempLoopAllMemBanks(); +#if defined(PORT) + osSyncPrintf("memp OOM: bank=%d bytes=0x%X left=0x%X pos=%p end=%p\n", + (int)poolnum, (unsigned)bytes, + (unsigned)(uintptr_t)(pool->end - pool->pos), + (void *)pool->pos, (void *)pool->end); +#endif while (1); } @@ -237,6 +269,10 @@ MEMP_ADD_ENTRY_RESULT mempAddEntryOfSizeToBank(void *allocation, s32 newsize, u8 if (allocation != pool->prevpos) { +#if defined(PORT) + osSyncPrintf("mempAddEntry: NOT_LAST bank=%d alloc=%p prevpos=%p\n", + (int)poolnum, allocation, (void *)pool->prevpos); +#endif return MEMP_ADD_ENTRY_NOT_LAST_ALLOCATION; } diff --git a/src/music.c b/src/music.c index 8affc220..1c63f3f2 100644 --- a/src/music.c +++ b/src/music.c @@ -52,7 +52,7 @@ * allocated later from this heap need N64-equivalent headroom on top, so * budget 0x38000 (init headroom ~19 KiB). PC only. */ -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) #define MUSIC_ALLOCATION_BYTES 0x38000 #else #define MUSIC_ALLOCATION_BYTES 0x2E000 @@ -685,7 +685,7 @@ void musicSeqPlayerInit(void) { size = (u32)&_sfxtblSegmentRomStart - (u32)&_sfxctlSegmentRomStart; -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) // D37: the PC-native bank image is larger than the ROM segment // (4-byte packed offsets become 8-byte pointer slots); allocate the // re-laid-out size, copy only the ROM bytes, then convert. @@ -708,7 +708,7 @@ void musicSeqPlayerInit(void) { size = (u32)&_instrumentstblSegmentRomStart - (u32)&_instrumentsctlSegmentRomStart; -#if defined(__x86_64__) +#if defined(PLATFORM_64BIT) // D37: as above. { u32 romSize = size; @@ -876,7 +876,7 @@ void musicTrack1Play(s32 track) t3 = ALIGN16_a(g_musicTrackLength[g_musicXTrack1CurrentTrackNum]) + ALIGN16_a(NUM_MUSIC_TRACKS); trackSizeBytes = ALIGN16_a(g_musicTrackCompressedLength[g_musicXTrack1CurrentTrackNum]); thing.seqData = g_musicXTrack1SeqData; - temp_a0 = (u8*)((t3 + (s32)thing.seqData) - trackSizeBytes); + temp_a0 = (u8*)(((uintptr_t)thing.seqData + t3) - trackSizeBytes); romCopy(temp_a0, romAddress, trackSizeBytes); decompressdata(temp_a0, thing.seqData, &hlist); @@ -1070,7 +1070,7 @@ void musicTrack2Play(s32 track) t3 = ALIGN16_a(g_musicTrackLength[g_musicXTrack2CurrentTrackNum]) + ALIGN16_a(NUM_MUSIC_TRACKS); trackSizeBytes = ALIGN16_a(g_musicTrackCompressedLength[g_musicXTrack2CurrentTrackNum]); thing.seqData = g_musicXTrack2SeqData; - temp_a0 = (u8*)((t3 + (s32)thing.seqData) - trackSizeBytes); + temp_a0 = (u8*)(((uintptr_t)thing.seqData + t3) - trackSizeBytes); romCopy(temp_a0, romAddress, trackSizeBytes); decompressdata(temp_a0, thing.seqData, &hlist); @@ -1263,7 +1263,7 @@ void musicTrack3Play(s32 track) t3 = ALIGN16_a(g_musicTrackLength[g_musicXTrack3CurrentTrackNum]) + ALIGN16_a(NUM_MUSIC_TRACKS); trackSizeBytes = ALIGN16_a(g_musicTrackCompressedLength[g_musicXTrack3CurrentTrackNum]); thing.seqData = g_musicXTrack3SeqData; - temp_a0 = (u8*)((t3 + (s32)thing.seqData) - trackSizeBytes); + temp_a0 = (u8*)(((uintptr_t)thing.seqData + t3) - trackSizeBytes); romCopy(temp_a0, romAddress, trackSizeBytes); decompressdata(temp_a0, thing.seqData, &hlist); From 498be832c2afe49dd48586e9f39f229706394bc2 Mon Sep 17 00:00:00 2001 From: Dan Turner Date: Sun, 20 Sep 2026 11:00:00 +0100 Subject: [PATCH 05/12] 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 From 3785dcad332bfd72f2165d1e84aacb5f893287d5 Mon Sep 17 00:00:00 2001 From: Dan Turner Date: Sun, 20 Sep 2026 12:00:00 +0100 Subject: [PATCH 06/12] macos: fix the pointer-truncation crash family, add PORT_ADDR_STRICT (D301-D308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Census-driven: rebuilt the compiled set with -Wint-to-pointer-cast -Wpointer-to-int-cast -Wint-conversion, giving 276 actionable sites, triaged in three disjoint file groups. Group A (206 warnings) and most of C were already-correct round trips; these are the real bugs. - D301: chrCreateBloodStain() crashed on guard gunfire. Two defects — portN64ToHost(0) returned base+0 instead of NULL (defeating the decomp's `if (p != NULL)` failure checks), and Vertex.CollisionRelatedNode (a u32 vma kept 32-bit by PROMOTE32) was read with a bare cast. - D302: six more truncations. Four are the "twin function" pattern — one of a near-duplicate solo/MP pair converted, the other missed. Includes bg.c:5213, where a truncated loop bound meant bgRoomCalcBB() never ran its body on arm64, leaving EVERY room in every level with an inverted bounding box. Silent, not a crash. Possible relevance to open D249. - D303: texReadUncompressed() aligned its destination through u32. Latent — a probe over a full level boot showed the path is unreachable on PC (textures come from the pre-converted sidecars). - D306: MSAA=8 produced a completely black screen. GL_MAX_SAMPLES is 4, and nothing clamped the request, so the renderbuffer was never allocated. video.c's own comment claimed MSAA "snaps down to the highest supported level"; it did not. Also fixed two index-vs-GL-name bugs in the same file. - D308: the watch's Control page crashed. options.c cast a stack-local table pointer to (s32) before passing it as WatchContButtonPositions*, and watchRenderControllerOpaque forwarded the truncated value. Verified A/B against the user's exact repro — fault address byte-identical. - D305: PORT_ADDR_STRICT, a debug gate (off by default, compiled out) that validates every u32->host conversion against the regions the port maps and reports offenders with a backtrace. Zero reports across 6 levels, 5 of them under full-control input. Also records D307 (reticule jitter in aim mode) in the finding log as OPEN, with the cause identified from live captures and no code change — an attempted fix was reverted for not working. Verified: clean build, 21/21 sweep, plus a targeted repro per fix. --- CMakeLists.txt | 10 +++++ docs/building.md | 26 +++++++++++ docs/dev/GE-ENV-PROBES.md | 1 + port/fast3d/gfx_opengl.cpp | 44 ++++++++++++++++++- port/include/port_addr.h | 29 ++++++++++++- port/src/port_addr.c | 89 ++++++++++++++++++++++++++++++++++++++ src/bondtypes.h | 12 +++-- src/game/bg.c | 13 ++++++ src/game/bondview2.c | 30 ++++++++----- src/game/chr.c | 11 +++-- src/game/cleanup_objects.c | 4 ++ src/game/image.c | 13 ++++++ src/game/model.c | 8 ++-- src/game/options.c | 2 +- 14 files changed, 269 insertions(+), 23 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c740d201..618486ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,12 @@ else() set(PORT_ADDR_BASE 0x0 CACHE STRING "Host base of the N64 address window") endif() +# Debug gate: validate every u32 -> host-pointer conversion against the regions +# the port actually maps, and report offenders at the conversion site with a +# backtrace. Off by default (zero cost); enable with -DPORT_ADDR_STRICT=ON. +# Catches the D299-D303 truncated/never-rebased-pointer family early. +option(PORT_ADDR_STRICT "Validate N64->host address conversions at runtime" OFF) + include(TargetArch) include(FindPkgConfig) @@ -259,6 +265,10 @@ add_definitions( -DPORT_ADDR_BASE=${PORT_ADDR_BASE} ${REGION_DEFS} ) +if(PORT_ADDR_STRICT) + add_definitions(-DPORT_ADDR_STRICT=1) + message(STATUS "PORT_ADDR_STRICT enabled: N64->host conversions are validated at runtime") +endif() if(TARGET_IS_64BIT) add_definitions(-DPLATFORM_64BIT=1) endif() diff --git a/docs/building.md b/docs/building.md index 6d12f91a..694a7e05 100644 --- a/docs/building.md +++ b/docs/building.md @@ -187,6 +187,32 @@ 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). +### Debugging address bugs (`PORT_ADDR_STRICT`) + +The port realises the N64 address space at a host base (`PORT_ADDR_BASE`, +non-zero only on arm64 macOS). The recurring failure mode is a 32-bit value +that is *not* a valid N64 address reaching `portN64ToHost()` — a truncated host +pointer, or one that was never re-based. The resulting fault usually happens +far from the cause, and sometimes there is no fault at all, just silent +corruption. + +```sh +cmake -S . -B build-strict -DROMID=ntsc-final -DPORT_ADDR_STRICT=ON +cmake --build build-strict -j +``` + +Every conversion is then validated against the mapped regions and offenders are +reported at the conversion site with a backtrace naming the caller. It is +**off by default and compiled out entirely**, so normal builds pay nothing. + +To prove the detector is live rather than merely silent: + +```sh +GE_ADDRSTRICT_SELFTEST=1 ./build-strict/ge007.aarch64 -level_09 +``` + +See findings D299–D305 for the bug family this exists to catch. + ### Packaging a macOS app ```sh diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index 90bdaccf..1b4c18d5 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -130,6 +130,7 @@ Two classes: | `GE_D96` | `src/game/chrprop.c:436,507` (cached `probe`) | D96 chrprop trace | dead (D96 closed) | | `GE_D104` | `src/game/bg.c:654` (rate-limited, `d104c`) | D104 depth-clear trace | dead (D104 closed) | | `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) | +| `GE_ADDRSTRICT_SELFTEST` | `port/src/port_addr.c` (`portAddrStrictSelfTest`) | **Negative self-test for `PORT_ADDR_STRICT` (D305).** Feeds `portN64ToHost()` a deliberately-unmapped address (`0x30000000`) at boot so a run can prove the validator is live rather than merely silent. Requires a build configured with `-DPORT_ADDR_STRICT=ON`; a no-op otherwise. | **live** (D305) | | `GE_D75` | `src/game/title.c` `sub_GAME_7F007F30` (capped 8) | D75 Bug 2 gun-barrel model probe: logs `chrModelInstance`/`gunModelInstance` ptr+obj+numMatrices, `render_pos`, `renderData.mtxlist`, `g_GfxMemPos`, `osVirtualToPhysical(render_pos)` | **LIVE** (D75 Bug 2 OPEN — see §F "D75 Bug 2 — RUNTIME PROBE") | | `GE_D305` | `src/snd.c:420` (`sndHandleEvent` desync branch, `#ifdef PORT`) | D305: logs the sound-preemption desync state when it is detected (after the NULL-guard fix made the crash itself impossible) — confirms whether `Game.AllUnlocked` correlates or was coincidental, and whether it recurs. Inert unset. | **live** (D305 monitoring) | | `GE_D309` | `src/game/chrai.c:810,3424`, `src/game/chraction.c:10139` (all `#ifdef PORT`) | D309 freeze probe: logs AI label-not-found restarts (`chrai.c`) and the run-to-bond persistent-list step-on path (`chraction.c`, capped 400 lines) — the diagnostics used to root-cause the elusive complete freeze (D309/D310). Inert unset. | **live** (D309/D310 fixed — kept for recurrence) | diff --git a/port/fast3d/gfx_opengl.cpp b/port/fast3d/gfx_opengl.cpp index c3881e12..9c433d56 100644 --- a/port/fast3d/gfx_opengl.cpp +++ b/port/fast3d/gfx_opengl.cpp @@ -1222,11 +1222,44 @@ static int gfx_opengl_create_framebuffer() { return i; } +/* video.c documents MSAA as "snaps down to the highest supported level", but + * nothing ever queried the limit: gfx_msaa_level went to + * glRenderbufferStorageMultisample() verbatim, and a request above + * GL_MAX_SAMPLES leaves the renderbuffer unallocated -> incomplete FBO -> + * a completely BLACK screen (MSAA=8 on Apple Silicon, where GL_MAX_SAMPLES + * is 4). Clamp here, where the GL limit actually matters. */ +static uint32_t gfx_opengl_clamp_msaa(uint32_t level) +{ + static GLint max_samples = 0; + static uint32_t warned_level = 0; + + if (level <= 1) { + return 1; + } + if (max_samples == 0) { + glGetIntegerv(GL_MAX_SAMPLES, &max_samples); + if (max_samples < 1) { + max_samples = 1; + } + } + if ((GLint)level > max_samples) { + if (warned_level != level) { + sysLogPrintf(LOG_WARNING, + "MSAA: %u samples requested but GL_MAX_SAMPLES is %d; using %d", + level, (int)max_samples, (int)max_samples); + warned_level = level; + } + return (uint32_t)max_samples; + } + return level; +} + static void gfx_opengl_update_framebuffer_parameters(int fb_id, uint32_t width, uint32_t height, uint32_t msaa_level, bool opengl_invert_y, bool render_target, bool has_depth_buffer, bool can_extract_depth) { Framebuffer& fb = framebuffers[fb_id]; + msaa_level = gfx_opengl_clamp_msaa(msaa_level); width = max(width, 1U); height = max(height, 1U); @@ -1340,7 +1373,12 @@ void gfx_opengl_resolve_msaa_color_buffer(int fb_id_target, int fb_id_source) { glBindFramebuffer(GL_READ_FRAMEBUFFER, fb_src.fbo); glBlitFramebuffer(0, 0, fb_src.width, fb_src.height, 0, 0, fb_dst.width, fb_dst.height, GL_COLOR_BUFFER_BIT, GL_NEAREST); - /* current_framebuffer is an INDEX into framebuffers[], not a GL name; fb 0 is the window's default framebuffer (name 0). */ + /* Restore whatever was bound before the resolve. `current_framebuffer` is + * an INDEX into framebuffers[], NOT a GL framebuffer name -- binding it + * directly named an unrelated GL object (or nothing) and left the wrong + * target bound for the rest of the frame. With MSAA on that produced a + * completely black screen. Use the same fb_id==0 -> GL name 0 rule as + * gfx_opengl_bind_framebuffer above. */ glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo); glEnable(GL_SCISSOR_TEST); } @@ -1405,6 +1443,10 @@ void gfx_opengl_copy_framebuffer(int fb_dst, int fb_src, int left, int top, bool glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, GL_COLOR_BUFFER_BIT, GL_NEAREST); + /* Same index-vs-name bug as the MSAA resolve above: for current_framebuffer + * == 0 this bound framebuffers[0].fbo, which is a generated but + * attachment-less FBO (incomplete on a strict GL implementation) -- the + * window's default framebuffer is GL name 0. */ glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo); glReadBuffer(GL_BACK); diff --git a/port/include/port_addr.h b/port/include/port_addr.h index 25f668e3..2c58e2e6 100644 --- a/port/include/port_addr.h +++ b/port/include/port_addr.h @@ -71,6 +71,17 @@ extern "C" { extern uintptr_t g_portImageBase; extern int g_portUseImageRel; +#ifdef PORT_ADDR_STRICT +/* Debug gate (-DPORT_ADDR_STRICT=1). Validates that a u32 being turned into a + * host pointer actually names one of the N64 regions the port maps. The whole + * D299/D300/D301/D302 bug family shares one signature -- a truncated or + * never-rebased address reaching portN64ToHost -- and it is almost always + * diagnosed from a fault somewhere far away, long after the bad value was + * created. This reports it AT THE CONVERSION, with a backtrace naming the + * caller. Compiled out entirely by default; costs one call when enabled. */ +void portAddrStrictCheck(uint32_t a); +#endif + /* Reserve the window (macOS only) and record the image base. Must run before * any of the port's fixed mappings and before large allocations. */ void portAddrInit(void); @@ -79,9 +90,25 @@ void portAddrInit(void); } #endif -/* u32 N64/physical address -> live host pointer. */ +/* u32 N64/physical address -> live host pointer. + * + * N64 address 0 maps to NULL, NOT to PORT_ADDR_BASE. Offset 0 is never a + * mapped address (the window's lowest live region is the cart at + * 0x1000_0000), so base+0 lands in the reserved PROT_NONE area and faults. + * More importantly, game code routinely signals failure by returning an N64 + * address of 0 and then testing the converted pointer for NULL -- + * `p = PORT_N64PTR(T, f()); if (p != NULL) *p = ...` (e.g. + * chrCreateBloodStain(), objDeform()). With base+0 that test passes and the + * very next store faults (D301). Returning NULL restores the decomp's + * intended semantics. */ static inline void *portN64ToHost(uint32_t a) { + if (a == 0) { + return NULL; + } +#ifdef PORT_ADDR_STRICT + portAddrStrictCheck(a); +#endif if (g_portUseImageRel && a >= 0x40000000u && a < 0x70000000u) { return (void *)(g_portImageBase + (uintptr_t)(a - 0x40000000u)); } diff --git a/port/src/port_addr.c b/port/src/port_addr.c index 3ea1ad26..e3a80740 100644 --- a/port/src/port_addr.c +++ b/port/src/port_addr.c @@ -12,10 +12,14 @@ */ #include +#include #include "platform.h" #include "system.h" #include "port_addr.h" +#ifdef PORT_ADDR_STRICT +#include +#endif #if defined(PLATFORM_MACOS) && defined(PLATFORM_ARM) #include @@ -64,6 +68,87 @@ static void portAddrSelfTest(void) ok ? "ALL PASS" : "FAILURES"); } +#ifdef PORT_ADDR_STRICT +/* + * Strict address validation (-DPORT_ADDR_STRICT=1, off by default). + * + * Every bug in the D299-D303 family reaches portN64ToHost() holding a value + * that is not a real N64 address -- either a host pointer truncated to 32 bits + * or an address that was never re-based. The fault then happens later, in + * unrelated code, which is what made these expensive to find. This checks the + * value against the regions the port actually maps and reports the bad ones + * at the point of conversion, naming the caller. + * + * Regions (see port/include/port_addr.h and port/src/dram.c): + * 0x10000000..0x1fffffff cart image + pcmodels/pccg sidecars + * 0x40000000..0x6fffffff image-relative encoding (exe globals) + * 0x70000000..0x707fffff DRAM V1 + * 0x80000000..0x807fffff DRAM V2 (KSEG0 mirror of V1) + * 0xa0000000..0xbfffffff game-thread stacks + * Segmented DL addresses (0x00000000..0x0fffffff) never name host memory and + * are resolved by fast3d's seg_addr(), not here -- so they are reported too. + */ +static int portAddrRegionOk(uint32_t a) +{ + if (a >= 0x10000000u && a <= 0x1fffffffu) return 1; /* cart + sidecars */ + if (a >= 0x40000000u && a <= 0x6fffffffu) return 1; /* image-relative */ + if (a >= 0x70000000u && a <= 0x707fffffu) return 1; /* DRAM V1 */ + if (a >= 0x80000000u && a <= 0x807fffffu) return 1; /* DRAM V2 */ + if (a >= 0xa0000000u && a <= 0xbfffffffu) return 1; /* thread stacks */ + return 0; +} + +/* Negative self-test: GE_ADDRSTRICT_SELFTEST=1 feeds the validator a value + * that is deliberately not a mapped N64 address, so a run can prove the + * detector is actually live rather than merely silent. */ +void portAddrStrictSelfTest(void) +{ + if (getenv("GE_ADDRSTRICT_SELFTEST")) { + sysLogPrintf(LOG_INFO, "PORT_ADDR_STRICT: self-test, expect one report below"); + portAddrStrictCheck(0x30000000u); /* in the gap between cart and image-rel */ + } +} + +void portAddrStrictCheck(uint32_t a) +{ + /* Report each distinct bad value once -- a bad pointer in a per-frame path + * would otherwise emit thousands of identical lines and bury the first. */ + enum { SEEN_MAX = 64 }; + static uint32_t seen[SEEN_MAX]; + static int seenCount; + int i; + + if (portAddrRegionOk(a)) { + return; + } + + for (i = 0; i < seenCount; i++) { + if (seen[i] == a) { + return; + } + } + if (seenCount < SEEN_MAX) { + seen[seenCount++] = a; + } + + sysLogPrintf(LOG_ERROR, + "PORT_ADDR_STRICT: 0x%08x is not a mapped N64 address " + "(-> %p). Truncated host pointer, or an address that was " + "never re-based (D299-D303 class).", + (unsigned)a, (void *)((uintptr_t)PORT_ADDR_BASE + (uintptr_t)a)); +#if defined(PLATFORM_MACOS) || defined(__APPLE__) || defined(__linux__) + { + void *bt[24]; + int n = backtrace(bt, (int)(sizeof(bt) / sizeof(bt[0]))); + /* Skip frame 0 (this function); the caller is what matters. */ + if (n > 1) { + backtrace_symbols_fd(bt + 1, n - 1, 2); + } + } +#endif +} +#endif /* PORT_ADDR_STRICT */ + void portAddrInit(void) { g_portImageBase = sysImageBase(); @@ -107,4 +192,8 @@ void portAddrInit(void) (unsigned long long)g_portImageBase, g_portUseImageRel); portAddrSelfTest(); + +#ifdef PORT_ADDR_STRICT + portAddrStrictSelfTest(); +#endif } diff --git a/src/bondtypes.h b/src/bondtypes.h index c8283e37..46ffb1ba 100644 --- a/src/bondtypes.h +++ b/src/bondtypes.h @@ -745,11 +745,17 @@ typedef union /* PC port (D43/D45): stored as a raw vma (0x05xxxxxx) instead of a real * pointer so sizeof(Vertex) stays 16 bytes — the gSPVertex stride is * hardcoded to 16B and propobj.c copies vertices with sizeof(Vertex). - * modelPromoteNodeOffsetsToPointers rebases it via PROMOTE32; chr.c casts - * back to ModelNode* at the use site. N64 layout unchanged (4B either way). */ + * modelPromoteNodeOffsetsToPointers rebases it via PROMOTE32, which is + * `var = (u32)((u32)var + diff)` — note that unlike PROMOTE it keeps the + * field a 32-bit N64 ADDRESS, it does NOT produce a host pointer. + * D301: consumers must therefore re-base it (PORT_N64PTR), never cast it + * straight to a pointer — a bare cast leaves 0x7070_xxxx and faults on + * arm64, where the window is based at PORT_ADDR_BASE. + * N64 layout unchanged (4B either way). */ u32 LinkedTo; - /* Collision vertices only: points to related model node (raw vma). */ + /* Collision vertices only: related model node, as a raw vma — same union + * storage as LinkedTo above, so the same PORT_N64PTR rule applies. */ u32 CollisionRelatedNode; /* 0x8 */ #else struct Vertex *LinkedTo; diff --git a/src/game/bg.c b/src/game/bg.c index 5373b102..389e7a85 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -5289,7 +5289,20 @@ void bgRoomCalcBB(s32 room) // canonical name limits.maxY = -0x7fff; limits.maxZ = -0x7fff; +#ifdef PORT + /* D302: the induction variable is a full host pointer, so truncating the + * bound to (s32) makes the comparison false on the very first iteration + * once the window is based at PORT_ADDR_BASE -- the loop body never runs, + * `limits` keeps its sentinel init, and every room ends up with an + * INVERTED bounding box (min = pos + 0x7fff, max = pos - 0x7fff) below. + * Silent: no crash, just wrong culling/visibility for every room of every + * level. Byte arithmetic must be preserved (usize_point_index_binary is a + * byte count), so widen the cast rather than doing pointer arithmetic. + * Mirrors the same fix already applied at bg.c:3399. */ + for (; vertices < (Vtx *) ((uintptr_t) g_BgRoomInfo[room].vertices + g_BgRoomInfo[room].usize_point_index_binary); vertices++) +#else for (; vertices < (Vtx *) ((s32) g_BgRoomInfo[room].vertices + g_BgRoomInfo[room].usize_point_index_binary); vertices++) +#endif { for (j = 0; j < 3; j++) { diff --git a/src/game/bondview2.c b/src/game/bondview2.c index 50dbd487..678be949 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -8236,7 +8236,7 @@ void bondviewMovePlayerUpdateViewport(s8 stick_x, s8 stick_y, u16 buttons) if ((cameraBufferToggle != 0) && (viGetFrameBuf2() == (u8*)(cfb_16[1]))) { - viSetFrameBuf2((u8 *) resolution); + viSetFrameBuf2(PORT_N64PTR(u8, resolution)); } #ifdef VERSION_EU @@ -8967,7 +8967,17 @@ void mp_respawn_handler(void) { coord3d start_pos = ZeroCoordSpawnPos; f32 start_look_angle; +#ifdef PORT + /* D302: PadRecord.stan is a StandTile* host pointer. An s32 local + * truncates it, and every consumer below (bondviewYPositionRelated, + * change_player_pos_to_target, prop->stan) rebuilds it unbased and + * dereferences it. Local only -- no struct/ABI impact -- and the s32 pad + * below keeps the N64 stack layout identical. The solo twin + * (bondview_r.c:102) already declares this StandTile*. */ + StandTile *start_stan; +#else s32 start_stan; +#endif s32 pad; f32 stan_height; s32 var_v0; @@ -9059,7 +9069,7 @@ void mp_respawn_handler(void) switch (intro_record->type) { case 0: // INTROTYPE_SPAWN - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroSpawn)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroSpawn)); break; case 1: // INTROTYPE_ITEM if (check_ramrom_flags() == ((struct SetupIntroAmmo*)intro_record)->is_demo_playback) { @@ -9069,28 +9079,28 @@ void mp_respawn_handler(void) bondinvAddInvItem(((struct SetupIntroItem*)intro_record)->item_right); } } - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroItem)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroItem)); break; case 2: // INTROTYPE_AMMO if (check_ramrom_flags() == ((struct SetupIntroAmmo*)intro_record)->is_demo_playback) { give_cur_player_ammo(((struct SetupIntroAmmo*)intro_record)->ammo_type, ((struct SetupIntroAmmo*)intro_record)->ammo_amount); } - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroAmmo)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroAmmo)); break; case 3: // INTROTYPE_SWIRL - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroSwirl)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroSwirl)); break; case 4: // INTROTYPE_ANIM - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroAnim)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroAnim)); break; case 5: // INTROTYPE_CUFF - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroCuff)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroCuff)); break; case 6: // INTROTYPE_CAMERA - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroCamera)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroCamera)); break; default: // INTROTYPE_WATCH, INTROTYPE_CREDITS - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroEmpty)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroEmpty)); break; } #ifdef DEBUG @@ -11188,7 +11198,7 @@ join_768: if (ppointers[index]->bodyModel->anim2 == NULL) { startframe = (0.0f <= frame) ? (frame) : (0.0f); - modelSetAnimation(ppointers[index]->bodyModel, (ModelAnimation *) anim, 0, startframe, angle, 16.0f); + modelSetAnimation(ppointers[index]->bodyModel, PORT_N64PTR(ModelAnimation, anim), 0, startframe, angle, 16.0f); ppointers[index]->players_cur_animation = anim; ppointers[index]->field_1288 = angle; diff --git a/src/game/chr.c b/src/game/chr.c index 606b652a..af2a01b7 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -3584,10 +3584,15 @@ after_opcode: } #ifdef PORT - /* PC port (D43/D45): CollisionRelatedNode is a raw vma (u32), not a pointer. */ - relatednode = (ModelNode *)(uintptr_t)((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode; + /* PC port (D43/D45): CollisionRelatedNode is a raw vma (u32), not a pointer. + * D301: that vma is an N64 address (the port stores window pointers as + * 32-bit values, and PORT_ADDR_BASE is 4 GiB-aligned, so the low 32 bits + * ARE the N64 address), so it must be re-based like every other + * address-carrying field -- a bare cast leaves it as 0x7070_xxxx and the + * &relatednode->Data deref below faults. Test the re-based pointer. */ + relatednode = PORT_N64PTR(ModelNode, ((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode); - if (((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode != 0) + if (relatednode != NULL) #else relatednode = (ModelNode *) ((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode; diff --git a/src/game/cleanup_objects.c b/src/game/cleanup_objects.c index e5016467..ce3342c4 100644 --- a/src/game/cleanup_objects.c +++ b/src/game/cleanup_objects.c @@ -23,6 +23,7 @@ void cleanupObjects(s32 stage) { +#ifdef PORT /* 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 @@ -32,6 +33,9 @@ void cleanupObjects(s32 stage) * 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; +#else + u32 *obj = (u32)g_CurrentSetup.propDefs; +#endif if (obj) { diff --git a/src/game/image.c b/src/game/image.c index 44a1b869..c00ad6bd 100644 --- a/src/game/image.c +++ b/src/game/image.c @@ -1739,9 +1739,22 @@ void texReadAlphaBits(u8 *image,s32 count) */ s32 texReadUncompressed(u8 *dst, s32 width, s32 height, s32 format) { +#ifdef PORT + /* D303: aligning a host pointer through u32 truncates it -- on arm64 dst is + * ~0x1000_70xx_xxxx and these would become unbased 0x70xx_xxxx, then be + * written through. Align at full pointer width instead. Identical result + * where pointers are 32-bit. NOTE: currently unreachable on the PC port -- + * textures come from the pre-converted pccg sidecars, and a GE_D303 probe + * over a full level boot recorded zero calls -- so this is latent, fixed + * defensively rather than in response to an observed fault. */ + u32 *dst32 = (u32 *)(((uintptr_t)dst + 0xf) & ~(uintptr_t)0xf); + u16 *dst16 = (u16 *)(((uintptr_t)dst + 7) & ~(uintptr_t)7); + u8 *dst8 = (u8 *)(((uintptr_t)dst + 7) & ~(uintptr_t)7); +#else u32 *dst32 = (u32 *)(((u32)dst + 0xf) & ~0xf); u16 *dst16 = (u16 *)(((u32)dst + 7) & ~7); u8 *dst8 = (u8 *)(((u32)dst + 7) & ~7); +#endif s32 x; s32 y; diff --git a/src/game/model.c b/src/game/model.c index f4dd7b27..12b63583 100644 --- a/src/game/model.c +++ b/src/game/model.c @@ -1924,18 +1924,18 @@ void process_03_unknown(ModelRenderData *renderData, Model *model, ModelNode *no jointIndex = rodata->JointID; skeleton = model->obj->Skeleton; - angle = sub_GAME_7F06E540(jointIndex, model->gunhand, skeleton, model->anim, (u8 *)model->unk34); + angle = sub_GAME_7F06E540(jointIndex, model->gunhand, skeleton, model->anim, PORT_N64PTR(u8, model->unk34)); if (model->unk2c != 0.0f) { - tmp = sub_GAME_7F06E540(jointIndex, model->gunhand, skeleton, model->anim, (u8 *)model->unk38); + tmp = sub_GAME_7F06E540(jointIndex, model->gunhand, skeleton, model->anim, PORT_N64PTR(u8, model->unk38)); angle = sub_GAME_7F06D0CC(angle, tmp, model->unk2c); } if (model->unk84 != 0.0f) { - tmp = sub_GAME_7F06E540(jointIndex, model->unk25, skeleton, model->anim2, (u8 *)model->unk64); + tmp = sub_GAME_7F06E540(jointIndex, model->unk25, skeleton, model->anim2, PORT_N64PTR(u8, model->unk64)); if (model->unk5c != 0.0f) { - tmp2 = sub_GAME_7F06E540(jointIndex, model->unk25, skeleton, model->anim2, (u8 *)model->unk68); + tmp2 = sub_GAME_7F06E540(jointIndex, model->unk25, skeleton, model->anim2, PORT_N64PTR(u8, model->unk68)); tmp = sub_GAME_7F06D0CC(tmp, tmp2, model->unk5c); } diff --git a/src/game/options.c b/src/game/options.c index 98bdd938..9552b6b2 100644 --- a/src/game/options.c +++ b/src/game/options.c @@ -3490,7 +3490,7 @@ Gfx *draw_watch_controller(Gfx *gdl) } else { - gdl = watchRenderControllerOpaque(gdl, &finalmtx, 1, (s32) (&table2), &contpadnum1); + gdl = watchRenderControllerOpaque(gdl, &finalmtx, 1, (WatchContButtonPositions *) (&table2), &contpadnum1); } } From 4d0abb1a39efe022bae49cb7add9c8c12ba18f32 Mon Sep 17 00:00:00 2001 From: Dan Turner Date: Wed, 30 Sep 2026 10:20:02 +0100 Subject: [PATCH 07/12] =?UTF-8?q?macos/arm64:=20renumber=20findings=20onto?= =?UTF-8?q?=20v0.4.0=20(D409=E2=80=93D421)=20+=20restore=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After rebasing the macOS/arm64 branch onto v0.4.0 (main was at D408, and had already consumed D323–D349), the branch's findings collided. Renumber the 13 macOS findings D294–D307 -> D409–D421 (main's own D29x/D30x references left untouched), and restore the findings.md entries + porting-notes §F section that the merge dropped. - findings.md: 13 §F/§H rows (D409–D421) + their tail sections. - findings-index.csv: regenerated (--check clean). - porting-notes.md: §F macOS section restored + Darwin/Mach-O bullets renumbered. - building.md / GE-ENV-PROBES.md / CMakeLists.txt / code comments: D294–D307 -> D409–D421. Drops the obsolete reticule-jitter finding (D336): v0.4.0 fixed it as D332 (folded from PR #96). --- CMakeLists.txt | 2 +- docs/building.md | 2 +- docs/dev/GE-ENV-PROBES.md | 2 +- docs/dev/MACOS-ARM64-PLAN.md | 14 ++-- docs/dev/findings-index.csv | 13 ++++ docs/dev/findings.md | 117 ++++++++++++++++++++++++++++++++ docs/porting-notes.md | 124 ++++++++++++++++++++++++++++++++-- port/include/port_addr.h | 4 +- port/src/n64stubs.c | 2 +- port/src/port_addr.c | 4 +- src/bondtypes.h | 2 +- src/game/bg.c | 2 +- src/game/bg.h | 2 +- src/game/bondview2.c | 12 ++-- src/game/bondview_r.c | 2 +- src/game/chr.c | 6 +- src/game/chraction.c | 4 +- src/game/cleanup_objects.c | 2 +- src/game/image.c | 2 +- src/game/image_bank.c | 6 +- src/game/image_bank.h | 2 +- src/game/initactorpropstuff.c | 2 +- src/game/language.c | 8 +-- src/game/model.c | 8 +-- src/game/othermodemicrocode.c | 2 +- src/mema.c | 4 +- src/memp.c | 6 +- src/snd.c | 2 +- 28 files changed, 301 insertions(+), 57 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 618486ca..6dd04156 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,7 +76,7 @@ endif() # Debug gate: validate every u32 -> host-pointer conversion against the regions # the port actually maps, and report offenders at the conversion site with a # backtrace. Off by default (zero cost); enable with -DPORT_ADDR_STRICT=ON. -# Catches the D299-D303 truncated/never-rebased-pointer family early. +# Catches the D414-D418 truncated/never-rebased-pointer family early. option(PORT_ADDR_STRICT "Validate N64->host address conversions at runtime" OFF) include(TargetArch) diff --git a/docs/building.md b/docs/building.md index 694a7e05..f59ee925 100644 --- a/docs/building.md +++ b/docs/building.md @@ -211,7 +211,7 @@ To prove the detector is live rather than merely silent: GE_ADDRSTRICT_SELFTEST=1 ./build-strict/ge007.aarch64 -level_09 ``` -See findings D299–D305 for the bug family this exists to catch. +See findings D414–D420 for the bug family this exists to catch. ### Packaging a macOS app diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index 1b4c18d5..1c2e7e10 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -130,7 +130,7 @@ Two classes: | `GE_D96` | `src/game/chrprop.c:436,507` (cached `probe`) | D96 chrprop trace | dead (D96 closed) | | `GE_D104` | `src/game/bg.c:654` (rate-limited, `d104c`) | D104 depth-clear trace | dead (D104 closed) | | `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) | -| `GE_ADDRSTRICT_SELFTEST` | `port/src/port_addr.c` (`portAddrStrictSelfTest`) | **Negative self-test for `PORT_ADDR_STRICT` (D305).** Feeds `portN64ToHost()` a deliberately-unmapped address (`0x30000000`) at boot so a run can prove the validator is live rather than merely silent. Requires a build configured with `-DPORT_ADDR_STRICT=ON`; a no-op otherwise. | **live** (D305) | +| `GE_ADDRSTRICT_SELFTEST` | `port/src/port_addr.c` (`portAddrStrictSelfTest`) | **Negative self-test for `PORT_ADDR_STRICT` (D420).** Feeds `portN64ToHost()` a deliberately-unmapped address (`0x30000000`) at boot so a run can prove the validator is live rather than merely silent. Requires a build configured with `-DPORT_ADDR_STRICT=ON`; a no-op otherwise. | **live** (D420) | | `GE_D75` | `src/game/title.c` `sub_GAME_7F007F30` (capped 8) | D75 Bug 2 gun-barrel model probe: logs `chrModelInstance`/`gunModelInstance` ptr+obj+numMatrices, `render_pos`, `renderData.mtxlist`, `g_GfxMemPos`, `osVirtualToPhysical(render_pos)` | **LIVE** (D75 Bug 2 OPEN — see §F "D75 Bug 2 — RUNTIME PROBE") | | `GE_D305` | `src/snd.c:420` (`sndHandleEvent` desync branch, `#ifdef PORT`) | D305: logs the sound-preemption desync state when it is detected (after the NULL-guard fix made the crash itself impossible) — confirms whether `Game.AllUnlocked` correlates or was coincidental, and whether it recurs. Inert unset. | **live** (D305 monitoring) | | `GE_D309` | `src/game/chrai.c:810,3424`, `src/game/chraction.c:10139` (all `#ifdef PORT`) | D309 freeze probe: logs AI label-not-found restarts (`chrai.c`) and the run-to-bond persistent-list step-on path (`chraction.c`, capped 400 lines) — the diagnostics used to root-cause the elusive complete freeze (D309/D310). Inert unset. | **live** (D309/D310 fixed — kept for recurrence) | diff --git a/docs/dev/MACOS-ARM64-PLAN.md b/docs/dev/MACOS-ARM64-PLAN.md index 4ee904b4..1d53de48 100644 --- a/docs/dev/MACOS-ARM64-PLAN.md +++ b/docs/dev/MACOS-ARM64-PLAN.md @@ -54,21 +54,21 @@ arm64 work was rebased onto it rather than maintained in parallel. Branch: **Adopted from #88 — this deletes the corresponding work in §3 M0:** - Homebrew GNU GCC (`gcc-16`) + libstdc++. Removes every clang workaround: no `-fms-extensions`/`-Wno-microsoft-anon-tag`, no libc++ wrapper header - shims (the old D295), no `-D_FORTIFY_SOURCE=0`. + shims (the old D410), no `-D_FORTIFY_SOURCE=0`. - `scripts/gen_macho_syms.py` — build-tree Mach-O symbol generation; the committed `*.darwin.s` duplicates are gone. - `scripts/strip_weak_pragmas.py` + `port/src/macho_weak_aliases.s` — the `#pragma weak` handling, so **`src/` carries no macOS edit for it** (the old - D294 in-source `#if defined(PORT) && defined(__APPLE__)` equates are gone). + D409 in-source `#if defined(PORT) && defined(__APPLE__)` equates are gone). - POSIX `shm_open` DRAM backing, Darwin crash handling, and the AppKit main-thread event-pump guard in `gfx_sdl2.cpp` (that last one was §3 M3.1). **Kept from this plan — the arm64 delta:** -- §2's shifted-window address model (M1 / D298). `-pagezero_size 0x10000`, +- §2's shifted-window address model (M1 / D413). `-pagezero_size 0x10000`, the core of #88's Intel approach, is fatal on arm64 (shrinking `__PAGEZERO` gets the process SIGKILLed), so CMake now sets it for x86_64 only and arm64 uses `PORT_ADDR_BASE`. -- D296 (`__x86_64__` → `PLATFORM_64BIT`). Without it #88's tree does not even +- D411 (`__x86_64__` → `PLATFORM_64BIT`). Without it #88's tree does not even *link* on arm64 — 23 undefined `_ANIM_DATA_*`. - `gen_macho_syms.py --base` (bakes `PORT_ADDR_BASE` into the absolute symbols; `--base 0` output is byte-identical to #88's transform). @@ -577,8 +577,8 @@ Record as a `Dxx`. | Phase | State | Notes | |---|---|---| | 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 | +| M0 | **superseded by #88** | the clang build/toolchain work is replaced by PR #88's GCC-based macOS layer; only D411 + `gen_macho_syms --base` survive (§0b) | +| M1 | **done** | shifted-window address model (D413); self-test ALL PASS on the #88 base | | 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 | @@ -591,7 +591,7 @@ level crash-free: (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) +- Findings D409–D415; D414 (silent SFX) and D415 (stage-unload truncation) were both arm64-specific and are fixed. ### Prerequisites to run (easy to miss) diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 4e1c35b0..ecdc7cc2 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -285,3 +285,16 @@ D405,CLOSED,"fast3d/OpenGL: MSAA above the driver max black-screens; two glBindF D406,RESOLVED,front-end PC Options screen:,"RESOLVED (2026-09-28, port-layer only, Steam Deck-validated):" D407,PARTIAL,Tanks cannot be boarded/exited on PC —,"PARTIAL (fix landed in v0.4.0, port-only):" D408,OPEN,Game.SkipIntro skips the post-mission failure dossier:,"OPEN (cosmetic, not root-caused past the handoff site)." +D409,FIXED,macOS/arm64 (M0.4): #pragma weak X = Y has no Mach-O equivalent —,"FIXED (macOS port, lv.c/spectrum.c/objective_status.c) —" +D410,FIXED,macOS/arm64 (M0.6): the decomp's include/ stubs shadow libc++'s wrapper headers —,"FIXED (macOS port) — new port/shim/math.h + generated hostmath.h; the existing host{string,stdlib,stddef}.h.in now also pull libc++'s c++/v1/.h wrapper, guarded && defined(__cplusplus)." +D411,FIXED,"macOS/arm64 (M0.2): __x86_64__ was used as the ""64-bit PC"" gate —","FIXED — 22 sites in 14 src/+assets/ files and port/include/pc_protos.h now use PLATFORM_64BIT (defined by CMake on every 64-bit PC target, undefined on N64, so the N64 branch is unchanged)." +D412,FIXED,macOS/arm64 (M0.7/M0.8):,"FIXED — shm_open double-map (dram.c); Darwin ucontext registers + _XOPEN_SOURCE (crash.c, incl." +D413,FIXED,"macOS/arm64 (M1): the port realises the N64 32-bit address space as host addresses; native arm64 cannot map the low 4 GiB, so the space is shifted up by PORT_A…","FIXED (port layer) — port/include/port_addr.h + port/src/port_addr.c (portAddrInit, portN64ToHost/portHostToN64, PORT_N64PTR); all chokepoints routed through them; DRAM/cart/stacks mapped at B+…; seg…" +D414,FIXED,macOS/arm64: in-level SFX completely silent —,"FIXED — new portAddrIsInWindow() in port_addr.h, guard tests the real window." +D415,FIXED,macOS/arm64: crash at stage unload —,"FIXED (cleanup_objects.c, D3x ABI/layout class) —" +D416,FIXED,macOS/arm64: crash on guard gunfire —,FIXED (port_addr.h + chr.c) — +D417,FIXED,"macOS/arm64: six more pointer truncations, found by a static -Wint-conversion census rather than by crashing.","FIXED (model.c x4, bondview2.c x11, bg.c) —" +D418,FIXED,"macOS/arm64: texReadUncompressed() aligned its destination through u32 ((u32 *)(((u32)dst + 0xf) & ~0xf) x3), truncating a host pointer and then writing throug…","FIXED defensively (image.c), latent not live." +D419,OPEN,"Intermittent renderer crash, level-agnostic, observed once on Dam:","OPEN — observed once, not investigated." +D420,FIXED,PORT_ADDR_STRICT: a debug gate that catches the whole D414-D419 family at the conversion rather than at the fault.,FIXED (new CMake option + port_addr.h/port_addr.c). +D421,FIXED,MSAA=8 produced a completely BLACK screen —,FIXED (port/fast3d/gfx_opengl.cpp) — diff --git a/docs/dev/findings.md b/docs/dev/findings.md index f23ee5ee..cdfe26f8 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -693,6 +693,19 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D406 | **front-end PC Options screen: long sections (INPUT, 17 visible rows) run off the bottom of the 440x330 paper, and rows past the silent 14-row page cap (Crouch mode, Reset to defaults, Bindings…) are unreachable from the front screen** (user report 2026-09-28, v0.4.0 pre-push review) — full `## D406` entry at file tail | RESOLVED (2026-09-28, port-layer only, Steam Deck-validated): in-section pagination in `frontoptions.c` (11 rows/page; the vertical stepper crosses page boundaries both ways; dim 'Page p/N' marker + bottom hint on every page; slider bars start past long labels; F10 overlay untouched). D406b/c/d same-cycle refinements (label/track overlap, hint visibility/wording, hint position). | | D407 | **Tanks cannot be boarded/exited on PC — the v0.4.0 use/reload split (D378/D393) removed the B-button tap the engine's tank handlers in `bondview2.c` consume (user report 2026-09-28, Runway/Streets)** — full `## D407` entry at file tail | PARTIAL (fix landed in v0.4.0, port-only): present `GE_CONT_B` on the use (E / pad A) rising edge only while `g_PlayerIsInTank == 1` or `g_BondCanEnterTank != 0`; E keeps its D378 no-reload-fallback semantics elsewhere, N64 layout unaffected. D407(b) same cycle: front PC Options page-edge highlight clamp + mouse-wheel / W-S paging (wheel queue consumed on the menu, D223); its wheel mapping shipped inverted and was fixed to match W/S (wheel up = step up). D407(c): board-animation lockout, menu-accept B gate in tank states, in-tank aim routed through the legacy velocity stick + `Input.TankAimScale` knob. RESOLVED 2026-09-28: all port-only (src/game zero-diff), 10 TANKDBG probes stripped, release binary verified clean, user live tank drive signed off (board + exit OK, aim feel good). | | D408 | **`Game.SkipIntro` skips the post-mission failure dossier: with it on, entering a level then aborting (watch Z+A) or dying (KIA) returns straight to the menus — no REPORT / "Mission status: KILLED IN ACTION / ABORTED" screen** (user report 2026-09-28; save/AllUnlocked ruled out) — full `## D408` entry at file tail | OPEN (cosmetic, not root-caused past the handoff site). v0.4.0 decision: SkipIntro stays EXPERIMENTAL / not recommended for regular users — F10 row relabelled "Skip intro (EXPERIMENTAL)". | +| D409 | **macOS/arm64 (M0.4): `#pragma weak X = Y` has no Mach-O equivalent — clang errors "aliases are not supported on darwin", and the pragma form silently emits a TU-local symbol instead** — full `## D409` entry at file tail | FIXED (macOS port, `lv.c`/`spectrum.c`/`objective_status.c`) — replaced with a `#if defined(PORT) && defined(__APPLE__)` global symbol equate (`.globl _X` / `_X = _Y`); Win/Linux keep the pragma. Verified: 3 TUs compile, aliases are global `S`/`D`/`T` symbols. | +| D410 | **macOS/arm64 (M0.6): the decomp's `include/` stubs shadow libc++'s wrapper headers — ``/``/``/`` hard-error, and `acosf`/`fmodf`/`fabsf` are undeclared** — full `## D410` entry at file tail | FIXED (macOS port) — new `port/shim/math.h` + generated `hostmath.h`; the existing `host{string,stdlib,stddef}.h.in` now also pull libc++'s `c++/v1/.h` wrapper, guarded `&& defined(__cplusplus)`. `limits.h`/`assert.h` needed no shim. All 186 TUs compile. | +| D411 | **macOS/arm64 (M0.2): `__x86_64__` was used as the "64-bit PC" gate — on arm64 it silently selected the N64 branch (34 undefined `_ANIM_DATA_*`, objecthandler `.bss` overrun, audio heap sizing)** — full `## D411` entry at file tail | FIXED — 22 sites in 14 `src/`+`assets/` files and `port/include/pc_protos.h` now use `PLATFORM_64BIT` (defined by CMake on every 64-bit PC target, undefined on N64, so the N64 branch is unchanged). | +| D412 | **macOS/arm64 (M0.7/M0.8): first native boot needs platform branches — no `memfd_create`, no glibc `ucontext.gregs`, no `/proc`, no fixed image base** — full `## D412` entry at file tail | FIXED — `shm_open` double-map (`dram.c`); Darwin `ucontext` registers + `_XOPEN_SOURCE` (`crash.c`, incl. `crashDumpThreads`); `_dyld_get_image_header`/`_NSGetExecutablePath` (`system.c`). **Binary links and runs: reaches ROM load.** | +| D413 | **macOS/arm64 (M1): the port realises the N64 32-bit address space as host addresses; native arm64 cannot map the low 4 GiB, so the space is shifted up by `PORT_ADDR_BASE` (16 TiB)** — full `## D413` entry at file tail | FIXED (port layer) — `port/include/port_addr.h` + `port/src/port_addr.c` (`portAddrInit`, `portN64ToHost`/`portHostToN64`, `PORT_N64PTR`); all chokepoints routed through them; DRAM/cart/stacks mapped at `B+…`; `seg_addr` re-based. ROM-free self-test ALL PASS. **Level boot + game-code cast sites still owed (M2, needs a ROM).** | +| D414 | **macOS/arm64: in-level SFX completely silent — `sndPlaySfx()`'s pre-window address guard (`sp >= 0x400000000ULL`, 16 GiB) rejected every real `ALSound*`, because with `PORT_ADDR_BASE` the sound lives at ~`0x1000_707b_5740`** — full `## D414` entry at file tail | FIXED — new `portAddrIsInWindow()` in `port_addr.h`, guard tests the real window. Verified: `[EVT]` 0→68, `[VOICE+]` 0→14 on `-level_37`; 21/21 control sweep with SFX on every level. | +| D415 | **macOS/arm64: crash at stage unload — `cleanupObjects()` did `u32 *obj = (u32)g_CurrentSetup.propDefs`, truncating a host pointer to 32 bits (unbased `0x7017_c70f` deref)** — full `## D415` entry at file tail | FIXED (`cleanup_objects.c`, D3x ABI/layout class) — cast pointer-to-pointer. Cuba x3 crash-free; control sweep 21/21. | +| D416 | **macOS/arm64: crash on guard gunfire — `chrCreateBloodStain()` faulted two ways: `portN64ToHost(0)` returned `base+0` instead of `NULL` (defeating the decomp's `if (p != NULL)` failure checks), and `Vertex.CollisionRelatedNode` (a `u32` vma kept 32-bit by `PROMOTE32`) was read with a bare cast** — full `## D416` entry at file tail | FIXED (`port_addr.h` + `chr.c`) — `portN64ToHost(0) -> NULL`; `CollisionRelatedNode` re-based via `PORT_N64PTR`. Verified `-level_39` x3 + 21/21 both sweeps. Does NOT fix D120 (the opcode-0x18 converter gap); blood decals may still be wrong. | +| D417 | **macOS/arm64: six more pointer truncations, found by a static `-Wint-conversion` census rather than by crashing.** Four are the "twin function" pattern (one of a near-duplicate solo/MP pair converted, the other missed). Includes `bg.c:5213`, where a truncated loop bound meant `bgRoomCalcBB()` never ran its body on arm64, giving **every room in every level an inverted bounding box** — silent, not a crash | FIXED (`model.c` x4, `bondview2.c` x11, `bg.c`) — all arm64-identity. The two MP-only fixes are committed but runtime-unverified. Possible relevance to open D249. | +| D418 | **macOS/arm64: `texReadUncompressed()` aligned its destination through `u32`** (`(u32 *)(((u32)dst + 0xf) & ~0xf)` x3), truncating a host pointer and then writing through it — but the path is **unreachable on PC** (a `GE_D303` probe over a full level boot recorded zero calls; textures come from the pre-converted `pccg` sidecars) | FIXED defensively (`image.c`), latent not live. Group-C census otherwise clean: `init()` is dead on PC, `rsp.c`'s XOR toggle already has a `PLATFORM_64BIT` branch, `objecthandler_2.c`'s `name` is an int-in-a-pointer, `animationtable_data.h`'s 25 warnings are placeholders rewritten by `initResolveAnimTable()` via `ANI_ADDR`. | +| D419 | **Intermittent renderer crash, level-agnostic, observed once on Dam: SIGSEGV in `gfx_sp_tri1` -> `gfx_texture_cache` with a garbage fault address (`0x16d5bbd636173b9e`).** Did not reproduce in 8 subsequent Dam runs, and is not attributable to the D417 changes landed in the same session. Not a known signature (D233 was `gfx_sp_tri1` but a visibility bug, not a crash) | **OPEN — observed once, not investigated.** Garbage (non-address-shaped) fault value suggests a corrupted texture-cache node or a race rather than the D414-D418 truncation family. Next: run with `PORT_ADDR_STRICT=ON` to rule the address family out, then instrument the texture cache. | +| D420 | **`PORT_ADDR_STRICT`: a debug gate that catches the whole D414-D419 family at the conversion rather than at the fault.** Off by default, compiled out entirely; when on, `portN64ToHost()` validates its argument against the mapped regions and reports offenders with a backtrace | FIXED (new CMake option + `port_addr.h`/`port_addr.c`). Negative self-test (`GE_ADDRSTRICT_SELFTEST=1`) proves the detector fires. Zero reports across 6 levels, 5 under full-control input. | +| D421 | **MSAA=8 produced a completely BLACK screen** — `GL_MAX_SAMPLES` is 4 on this GPU, and nothing ever clamped the requested level, so `glRenderbufferStorageMultisample` left the renderbuffer unallocated -> incomplete FBO. `video.c`'s own comment claims MSAA "snaps down to the highest supported level"; it did not | FIXED (`port/fast3d/gfx_opengl.cpp`) — clamp to `GL_MAX_SAMPLES`, log the snap once. Also fixed two index-vs-GL-name bugs in the same file (`resolve_msaa_color_buffer`, `copy_framebuffer`). Verified MSAA 1/2/4/8 all render. | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS @@ -15524,3 +15537,107 @@ included; D281 keeps the shipped mitigation + disclosure, zero-saves root cause **v0.4.0 decision (user, 09-28):** `Game.SkipIntro` stays **EXPERIMENTAL — not recommended for regular users yet**, alongside its older unrooted audio-break report (D216 status note). The F10 row is relabelled "Skip intro (EXPERIMENTAL)" (`port/src/optionsoverlay.c`, mirroring the D387 AllUnlocked labelling precedent); the config var, default (off), and `lv.c` hook are unchanged. **Status:** OPEN — cosmetic (no crash, no data loss; the mission still records as failed/aborted and the menus are fully functional). Root-causing needs a side-by-side of front-end state (`current_menu`, `g_CurrentStageToLoad`, `mission_failed_or_aborted`, `g_isBondKIA`) on the fail return with SkipIntro off vs on — candidate for v0.5.0 alongside the D216 audio half. + +## D409 — macOS/arm64 (M0.4): `#pragma weak X = Y` has no Mach-O equivalent +**Context.** The decomp uses GCC's `#pragma weak alias = target` in five TUs to create a weak symbol alias: `src/libultra/gu/sinf.c` + `cosf.c` (`fsin/sinf = __sinf`, `fcos/cosf = __cosf`), `src/game/objective_status.c` (`objectiveGetStatus_WEAK = get_status_of_objective`), `src/game/lv.c` (`g_DebugPortalsInputBufferSource1..4 = g_DebugPortalsInputBuffer1..4`), `src/game/spectrum.c` (`spec_keyboard_row_z_c_v = spec_keyboard_buffer`). On ELF/MinGW this is a normal weak alias. On Darwin/Mach-O it is not: +- `__attribute__((weak, alias("target")))` → hard error: `aliases are not supported on darwin`. +- The `#pragma weak X = Y` form is *accepted* but clang emits `X` as a **TU-local** symbol (`nm` shows lowercase `d`, not global `D`/`T`). Two consequences: (a) in `lv.c`/`spectrum.c`, which both `extern`-declare and then use the alias in the same TU, the local symbol plus the extern declaration produce `error: reference to 'X' is ambiguous`; (b) in `objective_status.c`, the consumer `chrai.c:41,2538` references `objectiveGetStatus_WEAK` cross-TU, which would be an **undefined symbol at link** because the alias never became global. +**Fix.** `#if defined(PORT) && defined(__APPLE__)` → emit the equivalent global symbol equate instead of the pragma: +```c +__asm__(".globl _X\n_X = _Y"); /* Mach-O prefixes C symbols with '_' */ +``` +The equate makes `X` a real global symbol at `Y`'s address (verified: `nm` shows `_objectiveGetStatus_WEAK` as `T` at the same address as `_get_status_of_objective`, and the `lv.c` aliases as `S`). Because every target is defined in-tree, a *strong* alias has identical semantics to the ELF weak alias. Files touched: `src/game/lv.c` (4 aliases), `src/game/spectrum.c` (1), `src/game/objective_status.c` (1, cross-TU). `sinf.c`/`cosf.c` still compile unchanged (their pragma makes `sinf`/`cosf` TU-local, so other TUs bind to libSystem's instead of Rare's polynomial — a fidelity divergence, not a build break; tracked as a follow-up in `docs/dev/MACOS-ARM64-PLAN.md`). +**Note on the `inherits struct` sibling issue.** The same class of "GCC extension with no clang equivalent" appears for `#define inherits struct`: clang needs `-fms-extensions` where GCC needs `-fplan9-extensions` (plan fact F11). Both are handled in `CMakeLists.txt` by selecting on `CMAKE_C_COMPILER_ID`, not the platform (on macOS `gcc` is Apple Clang). + +## D410 — macOS/arm64 (M0.6): the decomp's include/ stubs shadow libc++'s wrapper headers +**Context.** The fast3d C++ TUs failed to compile on Apple clang: ` tried including but didn't find libc++'s header` (also ``, ``, ``), plus `use of undeclared identifier 'acosf'/'fmodf'/'fabsf'` in `gfx_pc.cpp`. Cause has two halves: +1. **Shadowing.** The build puts the decomp's `include/` (which contains `math.h`, `limits.h`, `assert.h`, `stdlib.h`, `string.h`, `stddef.h` — N64 libc stubs) ahead of the system include path, so `#include ` finds the N64 stub, not the SDK's. +2. **libc++ wrapper guards.** libc++ does not use the C header directly: `$SDK/usr/include/c++/v1/.h` is a wrapper that defines `_LIBCPP__H` and then `#include_next`s the C header. `` etc. include `` and *then* require `_LIBCPP_MATH_H`. The old shims (`hoststring.h` etc.) included the raw SDK C header, so the guard was never defined. libstdc++ (Linux/MinGW) has no such wrapper, which is why this only shows on macOS. +**Dead ends (measured).** (a) `__attribute__((weak, alias("...")))` — "aliases are not supported on darwin". (b) A shim that includes the libc++ wrapper **by absolute path** fails: the wrapper's `#include_next ` then cannot find the C header (`FP_NAN` undeclared), because `#include_next` after an absolute-path include does not continue sensibly. (c) Reordering with `-I "$V1"` / `-I "$SDK/usr/include"` is a no-op: clang dedupes those as already-internal system dirs, leaving the decomp's `-I include` first. +**Fix.** The generated `host.h` shims include the **SDK C header first, then libc++'s `c++/v1/.h` wrapper** (so the wrapper processes and defines its guard, and the C declarations are already present for its `#include_next` to be a no-op). Applied to `host{string,stdlib,stddef}.h.in` (existing) and new `hostmath.h.in`. `limits.h`/`assert.h` need **no** shim — they were never in the failure set: libc++'s `` defines `_LIBCPP_CLIMITS` itself, and `` only errors if libc++ ever *starts* defining `` (it does not). New shim `port/shim/math.h` routes **macOS C++** TUs to `hostmath.h` and leaves `include/math.h` (the N64 stub) for C and for non-Apple C++, so the Windows/Linux builds are byte-for-byte unaffected. The wrapper includes are additionally guarded `&& defined(__cplusplus)`: `port/shim/string.h` includes its host header for *C* TUs too, and libc++ headers are not valid C (the first attempt broke all ~180 TUs with `<__config> file not found`). +**Verified.** All 186 TUs compile; `ge007.aarch64` links. + +## D411 — macOS/arm64 (M0.2): `__x86_64__` was being used as the "64-bit PC" gate +**Context.** ~22 places in unmodified game code (`src/`, `assets/`) gated a PC-only 64-bit fix on `#if defined(__x86_64__)`: D34 (`assets/animationtable_data.h` — `ANIM_DATA_*` as lvalues at `g_pc_animdata_base+off`), D40 (`src/game/objecthandler.c` 600-entry table; the N64 branch overruns `.bss` by 12 KB on a 64-bit host), D36/D37 audio heap sizing (`src/music.c`, `src/libultra/audio/*`), D39 (`image_bank.c`), D32-class (`rsp.c`), plus `src/memp.c`. `port/include/pc_protos.h:34` gated the entire D38 prototype header (~400 prototypes) the same way. On arm64 the gate was false, so the build silently took the **N64** branch: the link failed with 34 undefined `_ANIM_DATA_*`, and the D38 prototypes vanished (45–63 implicit-declaration warnings per large TU = silent pointer-return truncation). +**Fix.** Replace `defined(__x86_64__)` with `defined(PLATFORM_64BIT)` at all 22 game/asset sites and in `pc_protos.h`. `PLATFORM_64BIT` is defined by `CMakeLists.txt` (`add_definitions`, driven by `cmake/TargetArch.cmake`) for every 64-bit PC target (x86_64 and aarch64), and is *undefined* in the N64 build (which never sees `port/`), so the N64 branch is unchanged. `port/include/platform.h`'s own `__x86_64__`/`__aarch64__` checks (arch detection) and `port/src/system.c`'s `_mm_pause` gate are genuinely x86-conditional and were left alone. +**Verified.** macOS arm64: no undefined symbols, links. Windows/Linux x86_64: identical branch as before (PLATFORM_64BIT was already defined there). + +## D412 — macOS/arm64 (M0.7/M0.8): first native boot needs platform branches in dram.c / crash.c / system.c +**Context.** With D409–D411 applied, all TUs compiled but the link still needed `crashDumpThreads` and `memfd_create`, and `crash.c` lacked a macOS path entirely. +**Fix.** +- `port/src/dram.c`: `memfd_create` is Linux-only. macOS branch uses `shm_open("/ge007_dram_", O_RDWR|O_CREAT|O_EXCL, 0600)` + immediate `shm_unlink`, then `ftruncate` + two `mmap(MAP_SHARED|MAP_FIXED)` views — the same one-backing-store/two-views shape. (Measured: the two views alias correctly.) +- `port/src/crash.c`: the POSIX block is now `PLATFORM_LINUX || PLATFORM_MACOS`, so `crashDumpThreads` exists. Darwin register access differs: `uc_mcontext` is a *pointer* to `__darwin_mcontext64`, so the arm64 path reads `__ss.__pc/__lr/__sp/__fp` and `__x[0..7]` (glibc's `uc_mcontext.gregs[REG_R*]` is x86_64-only). Added `_XOPEN_SOURCE` for Darwin's `` ("deprecated ucontext routines require _XOPEN_SOURCE"), `_dyld_get_image_vmaddr_slide` in the dump (for `atos` symbolication), and the `signal.h`/`execinfo.h`/`dladdr` path is portable as-is. +- `port/src/system.c`: `sysImageBase()` returns `_dyld_get_image_header(0)` on macOS (no fixed base — arm64 is PIE-only); new `getExeDir()` helper uses `_NSGetExecutablePath` on macOS, `/proc/self/exe` on Linux, shared by `sysGetExeDir()` and `sysResolvePath()`'s `$S/` fallback (which previously read `/proc/self/exe` unconditionally, silently failing on macOS). +**Verified.** `build-pc/ge007.aarch64` links and runs: prints the version banner, loads/writes `build-pc/data/ge007.ini` (so `$S/`/exe-dir resolution works under the .app-less layout), and exits cleanly with "no ROM found" — i.e. the macOS build now reaches ROM load. It does **not** yet boot a level: the fixed-address DRAM/cart mappings (`0x70000000`/`0x10000000`) are unimplementable on native arm64 (plan §1 F1–F3); that is the M1 shifted-window work (`docs/dev/MACOS-ARM64-PLAN.md`). + +## D413 — macOS/arm64 (M1): the N64 address-space window must be shifted off the low 4 GiB +**Context.** The port realises the N64's 32-bit address space *as host addresses*: DRAM at `0x70000000` (V1) with a KSEG0 mirror at `0x80000000` (V2), the cart at `0x10000000`, and ~1700 linker-absolute symbols carrying those values. `osVirtualToPhysical` is a `u32` truncation and `u32 -> pointer` was an identity cast. That is only sound while host addresses fit in 32 bits — true on Windows/Linux (image and mappings low), impossible on native arm64: `mmap(MAP_FIXED)` below 4 GiB fails (`__PAGEZERO`), shrinking the pagezero SIGKILLs the process, PIE is mandatory and `MAP_32BIT` fails (plan §1 F1–F4). +**Fix — a shifted window.** Keep the N64 32-bit space intact but place it at a fixed host base `PORT_ADDR_BASE` (16 TiB on macOS, 0 elsewhere), so every game-visible 32-bit value is *unchanged* and only the `u32 -> host pointer` direction adds the base. +- `port/include/port_addr.h`: `portN64ToHost(u32)` / `portHostToN64(void*)` / `PORT_N64PTR(T, expr)`. `portAddrInit()` (`port/src/port_addr.c`) reserves the whole 4 GiB window `PROT_NONE` via `mach_vm_map(VM_FLAGS_FIXED)` (non-clobbering, unlike macOS `MAP_FIXED`) as the first thing in `main()`, then fails loudly if it cannot — the macOS analogue of the Windows fixed-base check. +- **Image-relative encoding.** A pointer into the executable cannot be truncated into the window. It is encoded as `0x40000000 + (ptr - image_base)`. On Windows the image base is `0x140000000`, so this equals the raw `(u32)ptr` truncation the D131 `mod_hi` reconstruction assumed; the new rule reproduces it exactly and additionally makes the round trip explicit on macOS (PIE slide and all). `g_portUseImageRel` is true iff the image lies outside the window (Windows/macOS, false on Linux), which is what keeps Linux's low `MAP_32BIT` stack addresses — which can land in `[0x40000000,0x70000000)` — from being misread as image-relative. +- Chokepoints rewired: `osVirtualToPhysical`/`osPhysicalToVirtual` (`port/src/libultra.c`), `OS_K0_TO_PHYSICAL` (`port/shim/PR/os.h`), `tlbmanageGetTlbAllocatedBlock` (`n64stubs.c`), `piServiceDma`'s ROM memcpy, `dramHostAddrValid`, fast3d `seg_addr`/`G_MW_SEGMENT`/the texture byte-order classifier. `PHYS_TO_K0` and `OS_PHYSICAL_TO_K0` stay identity: their inputs are already 32-bit N64 addresses that fit `s32` (V1 must remain s32-positive for `mempCheckMemflagTokens(s32,s32)`); the mempool therefore keeps N64 addresses internally and needs `M2` to re-base its pointer casts. +- Mappings moved into the window: DRAM `shm_open` double-map at `B+0x70000000`/`B+0x80000000`; cart at `B+0x10000000` (macOS uses `MAP_FIXED` inside the reservation); game-thread stacks carved from `[B+0xA0000000, B+0xC0000000)` with a `PROT_NONE` guard page (replacing `MAP_32BIT`, keeping `(u32)stackptr` truncations exact). The committed `.darwin.s` absolute symbols are generated with the same `--base`, and `portAddrInit` asserts `cfb_16 == PORT_ADDR_BASE + 0x70000000`. +**Verified (ROM-free).** `portAddrInit` runs a self-test of the window round-trips (DRAM V1/V2, cart), the absolute-symbol base, and the image-relative encoding — all PASS on `build-pc/ge007.aarch64`; the window reserves at `0x100000000000`. +**Still owed.** A level boot (M1.7) and the game-code cast census (M2) both need a ROM in `data/`; the M0 build has none, so the residue of raw `u32 -> pointer` sites in game code is untested. `PORT_N64PTR` exists for those rewrites and is an identity at `PORT_ADDR_BASE == 0`, so Windows/Linux are unaffected by every change here. + +## D414 — macOS/arm64: in-level sound effects were silent — a pre-window address guard rejected every real sound +**Symptom (user report, arm64 macOS build).** Music played, but there were no sound effects at all — no gunshots, no footsteps, no door sounds. +**Root cause.** `sndPlaySfx()` (`src/snd.c`) validated the resolved `ALSound*` with a hardcoded numeric bound that predates the shifted-window address model (D413): +```c +uintptr_t sp = (uintptr_t)sound; +if (sp < 0x10000 || sp >= 0x400000000ULL) return NULL; /* 16 GiB */ +``` +The comment above it ("ALSound lives in game DRAM (~0x7000_0000..) or the cart image (0x1_4000_0000..)") describes the *unshifted* layout. On x86_64 the N64 window starts at host 0, so DRAM really is at `0x7000_0000` — below the 16 GiB ceiling — and the guard passed. On arm64 `PORT_ADDR_BASE` is `0x100000000000`, so a valid `ALSound*` is `~0x1000_707b_5740`, far **above** the ceiling: every sound returned `NULL`. Music was unaffected because it is driven by a separate player, which is exactly the reported "music but no SFX" split. +**Fix.** Added `portAddrIsInWindow(const void *)` to `port/include/port_addr.h` (tests against the real `PORT_ADDR_BASE`/`PORT_ADDR_WINDOW` range) and used it for the guard, instead of absolute numeric bounds that silently break whenever the window moves. Identity-equivalent on Windows/Linux. +**Verified** (`-level_37`, scripted fire, `GE_AUDIOTRACE=1`): `[EVT]` 0 → 68 (the SFX event handler is now reached), `[VOICE+]` 0 → 14 (voices allocated and started), `[VOICE-]` 0 → 12, `[WAVELOOP]`/`[ENVELOPE]`/`[VOICES]` 15 each. Then 21/21 on the full-control sweep with SFX voices on every level. +**Lesson.** Any absolute numeric bound on an N64 address is a latent arm64 bug; use the `port_addr.h` predicates. + +## D415 — macOS/arm64: level-unload crash — `propDefs` truncated from a host pointer to 32 bits +**Symptom.** Crash at stage unload (`bossMainloop` → `bossRunTitleStage` → `lvlUnloadStageTextData`), faulting on `0x7017_c70f` — an unbased N64 DRAM address. Only reachable once a level actually ends, so it needed input (START/A) to surface; the no-input sweep never unloaded and missed it. +**Root cause.** `cleanupObjects()` (`src/game/cleanup_objects.c:26`) did +```c +u32 *obj = (u32)g_CurrentSetup.propDefs; +``` +The decomp's cast converts the pointer to a 32-bit integer and back into a `u32 *`. That is 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 that faults on the first `CLEANUP_PDTYPE` deref. `g_CurrentSetup.propDefs` is a real host pointer — `prop.c:1283` stores `local_stage + propDefs`. +**Fix.** Cast pointer-to-pointer: `u32 *obj = (u32 *)g_CurrentSetup.propDefs;`. `u32 *` is retained so `sizepropdef()`'s 4-byte-unit increment (`obj + sizepropdef(obj)`) is unchanged. This is the D3x ABI/layout class (pointer-width reconciliation), semantics-preserving; documented here per AGENTS.md rule 2. +**Verified.** Cuba x3 crash-free with the full-control script; control sweep 21/21. A grep for the same `ptr = (u32)expr` int-to-pointer pattern found no other instances. + +## D416 — macOS/arm64: guard gunfire crashed in chrCreateBloodStain (two pointer defects) +**Symptom (live playthrough, user report).** SIGBUS, then on a rebuild SIGSEGV, in `chrCreateBloodStain` (`src/game/chr.c`) whenever a guard opened fire. Reproduced reliably on Caverns (`-level_39`) with a scripted sustained-fire input; the no-input level sweep never triggered it, because it needs a guard to actually shoot at you. +**Defect 1 — `portN64ToHost(0)` returned a non-NULL pointer.** The window maps N64 address `a` to `PORT_ADDR_BASE + a`, so `a == 0` produced `base+0` — a non-NULL address inside the reserved `PROT_NONE` region. Game code signals allocation failure by returning an N64 address of **0** and having the caller test the converted pointer: +```c +newvertices = PORT_N64PTR(Vertex, vtxstore_allocate(...)); /* returns 0 when full */ +if (newvertices != NULL) { ... newvertices[j] = ...; } /* test passes; store faults */ +``` +`vtxstore_allocate()` returns 0 when the vertex store is exhausted (`vtxstore.c:349`) — a *normal* runtime condition in a firefight. Offset 0 is never a mapped address (the window's lowest live region is the cart at `0x1000_0000`), so mapping it to `NULL` is both safe and restores the decomp's intended semantics. Same shape affects `objDeform()` in `propobj.c`. +**Defect 2 — `CollisionRelatedNode` read with a bare cast.** `Vertex.CollisionRelatedNode` is `u32` on the port (`bondtypes.h:753`), aliasing `LinkedTo` in the same union at offset 8. `modelPromoteNodeOffsetsToPointers()` rebases it with `PROMOTE32` (`model.c:5996`), which is `var = (u32)((u32)var + diff)` — i.e. it deliberately keeps the field a **32-bit N64 address**, unlike `PROMOTE` which produces a host pointer. `chr.c` cast it straight to `ModelNode *`, leaving `0x7070_xxxx`, and `&relatednode->Data->DisplayListCollisions` faulted. Fixed by routing through the canonical `PORT_N64PTR`. +**Misleading comment, also corrected.** `bondtypes.h` claimed "chr.c casts back to ModelNode* at the use site", which is exactly the wrong conclusion and is what made the bare cast look intentional. +**Verified.** `-level_39` x3 clean (previously a reliable repro), then 21/21 on the no-input sweep and 21/21 on the full-control sweep (every button + 8-way stick, 26 s/level). +**Scope — this does NOT close D120.** `chrCreateBloodStain` is D120's subject: `tools_pc/d43_emit.py`'s opcode-0x18 `ModelRoData_DisplayList_CollisionRecord` conversion is still incomplete, and the interim walk guards at `chr.c:3445,3469` (cap iterations at `numVertices+8`, bounds-check `index`) remain in place. Blood decals may still be missing or wrong. D416 fixes the crash only. +**Lesson.** Both defects were invisible to a `-Wint-to-pointer-cast` census: defect 1 has no cast at all, and defect 2 was written as `(T *)(uintptr_t)x`, which silences the warning. A warning sweep finds the *producer* side of this class; the consumer side needs a type-driven audit of which 32-bit fields hold addresses. + +## D417 — macOS/arm64: six pointer truncations found by census, not by crashing +**Method.** Rebuilt the whole compiled set with `-Wint-to-pointer-cast -Wpointer-to-int-cast -Wint-conversion`, giving 276 actionable sites, and triaged them in three disjoint file groups. This is the audit `porting-notes.md` §F has prescribed since D413 and which had never been run. +**The dominant lesson: four of the six are the same shape.** GE's decomp contains many near-duplicate functions (solo vs multiplayer, primary vs secondary) that were decompiled independently, and a previous conversion pass fixed one twin while missing the other: +| converted | missed | +|---|---| +| `model.c:1728-1745` `sub_GAME_7F06DEC0` | `model.c:1924-1935` `sub_GAME_7F06E540` | +| `bondview_r.c:102` `StandTile *start_stan` | `bondview2.c:8762` `s32 start_stan` | +| `bondview_r.c` `(uintptr_t)intro_record` | `bondview2.c` x8 `(s32)intro_record` | +| `bg.c:3399` `(uintptr_t)temp.vertices` | `bg.c:5213` `(s32) g_BgRoomInfo[room].vertices` | +So: **after landing any fix in this family, grep the repository for the distinctive fragment of the line you changed and confirm every other occurrence is already in the fixed form.** That one step would have found four of these six for free. +**The most interesting one is silent.** `bgRoomCalcBB()` (`bg.c:5213`) truncated its loop bound, so on arm64 `vertices < bound` was false on the first iteration, the body never executed, `limits` kept its sentinel init, and lines 5242-5247 wrote `minbounds = pos + 0x7fff`, `maxbounds = pos - 0x7fff` — an inverted bounding box for **every room of every level**. No crash. Those bounds feed room centres (`bg.c:5155`), portal corners (`bg.c:1468`) and `chrprop.c:619`. Worth checking against open **D249** (distant geometry dropping out on Streets/Egyptian). +**Not verified.** The two MP-only fixes (`mp_respawn_handler`'s stan pointer, and the intro-record walk) are unreachable from the solo sweeps and need a multiplayer respawn. + +## D418 — macOS/arm64: latent truncation in texReadUncompressed (unreachable on PC) +`texReadUncompressed()` (`image.c`) aligned its destination through `u32` — `(u32 *)(((u32)dst + 0xf) & ~0xf)` plus `u16`/`u8` siblings — truncating a host pointer and then writing through it. On arm64 `dst` is `~0x1000_70xx_xxxx`, so the aligned pointers would be unbased `0x70xx_xxxx`. +**It never fires.** A `GE_D303` probe over a full `-level_09` boot recorded **zero** calls: PC textures come from the pre-converted `pccg` sidecars, not runtime bitstream decode. Fixed at full pointer width anyway, because it would corrupt silently rather than fault if the path were ever reached. +**Rest of the group-C census, for the record:** `init.c:114-118` writes through `RZIPLOADADDR` (`0x70200000`, a raw N64 address) but `init()` is dead on PC (`port/src/main.c` replaces the boot path; `port/src/n64stubs.c` documents it); `rsp.c`'s XOR double-buffer toggle already has a `PLATFORM_64BIT` explicit-toggle branch; `objecthandler_2.c`'s `name` is a `u8 *` parameter reused as a scratch integer (`(s32) name` everywhere, never dereferenced); `music.c`'s `romCopy` sources are re-truncated and re-based inside `piServiceDma`. The lead handed over from group B — `propobj.c:8893` — is **refuted**: `BaseAddr` is declared `void *`, so `(void *)(uintptr_t)BaseAddr` is a full-width no-op. + +## D420 — PORT_ADDR_STRICT: catch this bug family at the conversion, not at the fault +**Why.** Every bug in the D414-D418 family reaches `portN64ToHost()` holding a value that is not a real N64 address — a truncated host pointer, or one never re-based. The *fault* then happens somewhere else entirely, often frames later, which is what made each of them expensive. Two of them (D414, D418) produced no crash at all. +**What it is.** `-DPORT_ADDR_STRICT=ON` (CMake option, **off by default, compiled out entirely**) makes `portN64ToHost()` validate its argument against the regions the port actually maps — cart+sidecars `0x10000000-0x1fffffff`, image-relative `0x40000000-0x6fffffff`, DRAM V1 `0x70000000-0x707fffff`, DRAM V2 `0x80000000-0x807fffff`, thread stacks `0xa0000000-0xbfffffff` — and reports anything else via `sysLogPrintf` plus a `backtrace()` naming the caller. Each distinct bad value is reported once, so a per-frame offender cannot bury the first hit. +**Negative self-test.** `GE_ADDRSTRICT_SELFTEST=1` feeds the validator a deliberately-unmapped value so a run can prove the detector is live rather than merely silent. This caught a real mistake during development: the first test value (`0x12345678`) sits *inside* the cart range and was correctly accepted, making the detector look broken when it was working. +**Result.** Zero reports across `-level_09/33/39/22/54/37`, five of them driven by a full-control input script. That is a positive validation of the D414-D418 fixes, not merely an absence of evidence. +**Known limitation.** The cart range is the full `0x10000000-0x1fffffff` window rather than the actual mapped extent (ROM ~12 MB + sidecars ~5 MB), so a truncated pointer that happens to land in that 256 MB span is not caught. Tightening it against the real `romSize` would narrow the blind spot. + diff --git a/docs/porting-notes.md b/docs/porting-notes.md index f43ae5ce..a2c9223b 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -30,6 +30,7 @@ good that you are looking at one of these. - [D5. Loop bounds that assume linker adjacency of two globals](#d5-loop-bounds-that-assume-linker-adjacency-of-two-file-scope-globals) - [D12. Collapsing an LOD binding to tile 0 breaks a DETAIL binding](#d12-collapse-an-lod-binding-to-tile-0-is-wrong-for-a-two-texture-detail-binding-d236) - [D13. Per-render accumulation is not associative (1 vs 2–3 ticks/frame)](#d13-per-render-accumulation-is-not-associative-the-ports-1-tickframe-can-reach-states-the-n64s-23-ticksframe-never-did-d329) +- [F. The N64 address space on a host that cannot map it low (macOS/arm64)](#f-the-n64-address-space-on-a-host-that-cannot-map-it-low-macosarm64) - [E. Process / method notes](#e-process--method-notes) ## A. Pointer-width struct growth (32→64): the dominant class @@ -584,26 +585,26 @@ through a converter or a runtime bswap fixup reads scrambled. are `.global foo` / `.set foo, address` on ELF/PE must become `.globl _foo` / `.set _foo, address`; Darwin also rejects the ELF `.section .data` spelling used by these symbol-only files. Generate transformed build-tree - copies rather than changing the cross-platform source files (D294). + copies rather than changing the cross-platform source files (D409). - **Darwin GCC cannot lower ELF-style `#pragma weak alias = target`.** It can ICE in `assemble_alias`; compile generated copies with those pragmas removed and provide Mach-O indirect symbols (`.set _alias, _target`) separately - (D294). + (D409). - **Mach-O's default `__PAGEZERO` reserves the entire low 4 GiB.** A build can compile and link successfully yet fail every fixed N64 mapping at runtime (`0x10000000`, `0x70000000`, `0x80000000`). Link with `-Wl,-pagezero_size,0x10000` so null-page pointers remain protected while - the cartridge and DRAM ranges are available (D294). + the cartridge and DRAM ranges are available (D409). - **Darwin has no `MAP_FIXED_NOREPLACE`; a fixed-address hint can relocate.** Once `__PAGEZERO` is deliberately bounded below the cart range, use `MAP_FIXED` for the `0x10000000` cartridge mapping. Retaining the advisory hint silently falls back to a heap ROM and later crashes on direct absolute - ROM-symbol reads (D294). + ROM-symbol reads (D409). - **AppKit event polling is main-thread-only.** This port has both a host-main `videoPumpEvents()` loop and fast3d's render-thread event pump. The latter must be disabled on macOS or `SDL_PollEvent` raises `NSInternalInconsistencyException`; the main-thread loop already handles - the complete event set (D294). + the complete event set (D409). - `#include ` in a port `.c`/`.h` that also sees `` breaks: `OSContStatus`/`OSContPad` have a `u8 errno;` field vs errno.h's macro. libultra.c wraps the include in `#pragma push_macro("errno")` / @@ -1370,6 +1371,119 @@ darkening from a nonzero `ENV_ALPHA` LERP factor is very likely faithful (possibly authored/original-dev-intentional tinting), not a bug — do not "fix" it without an N64/1964-GEPD reference screenshot proving otherwise. +## F. The N64 address space on a host that cannot map it low (macOS/arm64) + +Windows and Linux realise the N64's 32-bit address space **directly**: DRAM at +`0x70000000` with the KSEG0 mirror at `0x80000000`, the cart at `0x10000000`, +and ~1700 linker-absolute symbols carrying those values. A 32-bit address field +and a host pointer are then the same number, so `(u32)ptr` and `(T*)u32` +round-trip for free — which is why classes A–E could treat address fields as +pointers. + +**Native arm64 macOS cannot map below 4 GiB** (`__PAGEZERO`; a shrunken +pagezero is SIGKILLed at exec; PIE is mandatory; `MAP_32BIT` fails). So the +whole space lives at `PORT_ADDR_BASE = 0x1000_0000_0000` (16 TiB) instead: +`port/include/port_addr.h`, `port/src/port_addr.c` (D413). Because the base is +4 GiB-aligned, the **pointer→u32 direction is still free** (`(u32)host_ptr` is +the N64 address), so only *reconstruction* sites — a 32-bit value becoming a +pointer — need work. + +- Symptom: a fault at a *plausible N64 address* used as a host pointer, e.g. + `FAULT ADDR: 0x706ff8e0` (DRAM), `0x10xxxxxx` (cart) or `0x40xxxxxx` + (image-relative). On Windows/Linux the same value is a valid pointer; the + value looking "reasonable" is the tell. +- Fix: route the conversion through `portN64ToHost` / `PORT_N64PTR` (identity + at `PORT_ADDR_BASE == 0`, so Windows/Linux are unchanged). For containers + whose fields are already pointers but are fed N64 addresses, convert **once + at the boundary** — the mempool (`mempCheckMemflagTokens` / + `mempSetBankStarts`, `MemoryPool` fields are `u8*`) is the main one. + Keep the `s32` the game passes around: `mempCheckMemflagTokens(s32, s32)` + needs a value that fits `s32` (V1 must stay positive), so N64 addresses stay + N64 addresses in the game's fields and only the pointer-typed sinks re-base. +- **One canonical macro.** `port/include/port_addr.h` defines + `PORT_N64PTR(T, expr)`. Ported game code uses that, not a file-local alias — + an earlier pass grew seven names (`BG_PTR`, `OPT_N64PTR`, `CHR_N64PTR`, + `FRONT_N64PTR`, `TITLE_N64PTR`, `STAN_N64PTR`, `MODEL_N64PTR`, …) which is + exactly the drift this file exists to prevent. A game file defines the N64 + fallback once: + + ```c + #if defined(PORT) + #include "port_addr.h" + #else + #define PORT_N64PTR(T, x) ((T *)(x)) + #endif + ``` +- **Prefer boundary fixes over consumer fixes.** Where a subsystem's fields are + already pointers but are fed N64 addresses, convert once at the hand-out + (`mempCheckMemflagTokens`/`mempSetBankStarts`, `memaAlloc`, + `globalbank_rdram_offset`, `texSetBitstring`) instead of at every use — the + single `globalbank_rdram_offset` change cleared ~43 sites. +- **Do not blind-pattern-match the cast.** `(s32)someFloatField` (e.g. + `(s32)foo[room].pos.x`) is a float→int conversion, not an address; a regex + over the `(s32)` pattern rewrites those and breaks the build. Type the + operand first. Also left alone by design: `(s32)` stack-pointer truncations + used as matching hacks (e.g. `options.c` watch-table locals) — a different, + pre-existing bug class. +- Image pointers (exe globals) have their own encoding: + `0x40000000 + (ptr - image_base)`. `portHostToN64` produces it and + `portN64ToHost` decodes it; `osVirtualToPhysical` returns it, fast3d's + `seg_addr` decodes it. On Windows the image base is `0x140000000`, so this + equals the raw `(u32)&sym` truncation the D131 fix relied on. +- Instances: D409 (`#pragma weak` has no Mach-O equivalent — use a global + symbol equate), D410 (the decomp's `include/` stubs shadow libc++'s wrapper + headers), D411 (`__x86_64__` used as the "64-bit PC" gate → `PLATFORM_64BIT`), + D412 (macOS platform branches: `shm_open`, Darwin `ucontext`, `_NSGetExecutablePath`), + D413 (the window + chokepoints + mappings), D414, D415. +- **Never bound an N64 address with an absolute number.** A guard like + `if (p < 0x10000 || p >= 0x400000000ULL) return NULL;` is written against the + *unshifted* layout: it is correct on Windows/Linux (DRAM at `0x7000_0000`, + below 16 GiB) and silently rejects **every** real pointer on arm64 (DRAM at + `~0x1000_707b_5740`). D414 was exactly this in `sndPlaySfx()` — all in-level + SFX were silent, while music (a separate player) still worked. Use + `portAddrIsInWindow()` (`port_addr.h`) instead. +- **`(u32)ptr` assigned back to a pointer is a truncation, not a no-op.** + `u32 *obj = (u32)g_CurrentSetup.propDefs;` (D415) round-trips on N64 but + leaves an unbased N64 address on the 64-bit port, faulting on first deref. + The port-correct form is a pointer-to-pointer cast (`(u32 *)…`), keeping the + pointer's own arithmetic. Grep for `ptr_var = (u32)expr`; D415 was the only + instance. +- **Sweep by function PAIR, not by grep hit.** Four of the six bugs in the D417 + census pass were cases where one member of a near-duplicate function pair had + been converted and its twin had not (`model.c` `sub_GAME_7F06DEC0` vs + `sub_GAME_7F06E540`; `bondview_r.c`'s solo spawn vs `bondview2.c`'s MP + respawn, on two separate counts; `bg.c:3399` vs `bg.c:5213` *in the same + file*). GE's decomp is full of solo/MP and primary/secondary twins that were + decompiled independently and differ only in whitespace. **After landing any + fix in this family, grep the repo for the distinctive fragment of the line you + just changed and confirm every other occurrence is already fixed.** Cheap, and + it would have found all four for free. +- **A warning census finds the PRODUCER side, not the CONSUMER side.** Building + with `-Wint-to-pointer-cast -Wpointer-to-int-cast -Wint-conversion` flags + pointers implicitly truncated *into* int parameters/locals — and in `front.c` + + `options.c` all 206 such warnings were already-correct round trips. It does + **not** flag the dangerous direction when the cast is explicit: `(T *)(uintptr_t)x` + (how D416 was written) is silent, and D414 (a numeric bound on an address) has + no cast at all. Use the census to audit the invariant "every value reaching a + `PORT_N64PTR` is either from `portHostToN64()` or provably in-window", but do + not mistake a clean census for a clean codebase — the consumer side needs a + type-driven audit, or `PORT_ADDR_STRICT` (D420) at runtime. +- **A stage-unload bug needs input to surface.** Both sweeps that missed D415 + ran with no controller input, so the level never ended and + `lvlUnloadStageTextData()` never ran. Drive `GE_INPUTSCRIPT` (START/A ends a + level) when the affected path is teardown rather than steady-state. +- Grep heuristic: build with clang and collect + `-Wint-to-pointer-cast` / `-Wint-to-void-pointer-cast` / `-Wint-conversion` + (u32→pointer: actionable) and `-Wpointer-to-int-cast` (only *image* pointers + are actionable — in-window truncation is correct). The M2 census and its + counts live in `docs/dev/MACOS-ARM64-PLAN.md`. +- Also macOS-specific and worth knowing: the game's fixed-address maps + (`dram.c`, `romdata.c`, `libultra.c` stacks) all move into the window, and + the window is reserved `PROT_NONE` up front (`portAddrInit`) so those carves + cannot collide with libmalloc/the dyld cache. + + + ## E. Process / method notes - **Never `exit()` while another thread may be inside the GL driver (D344).** diff --git a/port/include/port_addr.h b/port/include/port_addr.h index 2c58e2e6..1b0297bf 100644 --- a/port/include/port_addr.h +++ b/port/include/port_addr.h @@ -74,7 +74,7 @@ extern int g_portUseImageRel; #ifdef PORT_ADDR_STRICT /* Debug gate (-DPORT_ADDR_STRICT=1). Validates that a u32 being turned into a * host pointer actually names one of the N64 regions the port maps. The whole - * D299/D300/D301/D302 bug family shares one signature -- a truncated or + * D414/D415/D416/D417 bug family shares one signature -- a truncated or * never-rebased address reaching portN64ToHost -- and it is almost always * diagnosed from a fault somewhere far away, long after the bad value was * created. This reports it AT THE CONVERSION, with a backtrace naming the @@ -99,7 +99,7 @@ void portAddrInit(void); * address of 0 and then testing the converted pointer for NULL -- * `p = PORT_N64PTR(T, f()); if (p != NULL) *p = ...` (e.g. * chrCreateBloodStain(), objDeform()). With base+0 that test passes and the - * very next store faults (D301). Returning NULL restores the decomp's + * very next store faults (D416). Returning NULL restores the decomp's * intended semantics. */ static inline void *portN64ToHost(uint32_t a) { diff --git a/port/src/n64stubs.c b/port/src/n64stubs.c index 7730a5b8..dcf69f73 100644 --- a/port/src/n64stubs.c +++ b/port/src/n64stubs.c @@ -105,7 +105,7 @@ u8 (*tlbmanageGetTlbAllocatedBlock(void))[TLB_BLOCK_SIZE] * everything that isn't the fixed PERMANENT bank). Stays well clear of * animations_frame_buffer. * - * D298 (macOS/arm64): the same reclaim is not quite enough there — the + * D413 (macOS/arm64): the same reclaim is not quite enough there — the * first level load exhausts STAGE by ~0x30 bytes at a model rwdata alloc * (mempAllocBytesInBank spins). Push the ceiling to the last 0x10000 * before animations_frame_buffer, adding ~960 KiB to STAGE. Same diff --git a/port/src/port_addr.c b/port/src/port_addr.c index e3a80740..f2c7293c 100644 --- a/port/src/port_addr.c +++ b/port/src/port_addr.c @@ -72,7 +72,7 @@ static void portAddrSelfTest(void) /* * Strict address validation (-DPORT_ADDR_STRICT=1, off by default). * - * Every bug in the D299-D303 family reaches portN64ToHost() holding a value + * Every bug in the D414-D418 family reaches portN64ToHost() holding a value * that is not a real N64 address -- either a host pointer truncated to 32 bits * or an address that was never re-based. The fault then happens later, in * unrelated code, which is what made these expensive to find. This checks the @@ -134,7 +134,7 @@ void portAddrStrictCheck(uint32_t a) sysLogPrintf(LOG_ERROR, "PORT_ADDR_STRICT: 0x%08x is not a mapped N64 address " "(-> %p). Truncated host pointer, or an address that was " - "never re-based (D299-D303 class).", + "never re-based (D414-D418 class).", (unsigned)a, (void *)((uintptr_t)PORT_ADDR_BASE + (uintptr_t)a)); #if defined(PLATFORM_MACOS) || defined(__APPLE__) || defined(__linux__) { diff --git a/src/bondtypes.h b/src/bondtypes.h index 46ffb1ba..0067f15a 100644 --- a/src/bondtypes.h +++ b/src/bondtypes.h @@ -748,7 +748,7 @@ typedef union * modelPromoteNodeOffsetsToPointers rebases it via PROMOTE32, which is * `var = (u32)((u32)var + diff)` — note that unlike PROMOTE it keeps the * field a 32-bit N64 ADDRESS, it does NOT produce a host pointer. - * D301: consumers must therefore re-base it (PORT_N64PTR), never cast it + * D416: consumers must therefore re-base it (PORT_N64PTR), never cast it * straight to a pointer — a bare cast leaves 0x7070_xxxx and faults on * arm64, where the window is based at PORT_ADDR_BASE. * N64 layout unchanged (4B either way). */ diff --git a/src/game/bg.c b/src/game/bg.c index 389e7a85..4635ce9e 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -5290,7 +5290,7 @@ void bgRoomCalcBB(s32 room) // canonical name limits.maxZ = -0x7fff; #ifdef PORT - /* D302: the induction variable is a full host pointer, so truncating the + /* D417: the induction variable is a full host pointer, so truncating the * bound to (s32) makes the comparison false on the very first iteration * once the window is based at PORT_ADDR_BASE -- the loop body never runs, * `limits` keeps its sentinel init, and every room ends up with an diff --git a/src/game/bg.h b/src/game/bg.h index 89ac7e1e..510499d4 100644 --- a/src/game/bg.h +++ b/src/game/bg.h @@ -20,7 +20,7 @@ struct levelentry // cannonical name #define PORTMAX 200 -/* D298/M2: the sum is an N64/window offset, so re-base it into the DRAM +/* D413/M2: the sum is an N64/window offset, so re-base it into the DRAM * window before it becomes a pointer. Identity at PORT_ADDR_BASE == 0. */ #if defined(PORT) #include "port_addr.h" diff --git a/src/game/bondview2.c b/src/game/bondview2.c index 678be949..f5ce758e 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -98,7 +98,7 @@ #if defined(VERSION_US) - /* D298/M2: the font-table globals are s32 holding truncated pointers + /* D413/M2: the font-table globals are s32 holding truncated pointers * (D88 class); re-base on use. Identity at PORT_ADDR_BASE == 0. */ #if defined(PORT) #define BONDVIEW_2ND_FONTTABLE(_param) PORT_N64PTR(void, copy_2ndfonttable) @@ -1239,7 +1239,7 @@ void bondviewCalcIntroSwirlCamera(s32 index, f32 time, coord3d *pos, coord3d *lo struct SetupIntroSwirl *base; struct SetupIntroSwirl *loopbase; #if defined(PORT) - /* D189/D298: written pointbuf[0..11] via `&pointbuf[i*3]` then dst[3..5] + /* D189/D413: written pointbuf[0..11] via `&pointbuf[i*3]` then dst[3..5] * (i = -1..2), but declared [10] -- a 2-float stack overrun that on arm64 * corrupts the saved frame pointer (Fp=0x374d56eac1a00000 at the fault). * N64/MinGW absorb it; give it room on the port. */ @@ -1337,7 +1337,7 @@ void bondviewCalcIntroSwirlCamera(s32 index, f32 time, coord3d *pos, coord3d *lo lookat->y = g_CurrentPlayer->field_3C8; lookat->z = g_CurrentPlayer->field_3CC; - swirl = (void *)(((uintptr_t) g_IntroSwirl) + (uintptr_t) base); /* D298/M2: full-width */ + swirl = (void *)(((uintptr_t) g_IntroSwirl) + (uintptr_t) base); /* D413/M2: full-width */ if (!(swirl->bitflags & 4)) { @@ -8968,7 +8968,7 @@ void mp_respawn_handler(void) coord3d start_pos = ZeroCoordSpawnPos; f32 start_look_angle; #ifdef PORT - /* D302: PadRecord.stan is a StandTile* host pointer. An s32 local + /* D417: PadRecord.stan is a StandTile* host pointer. An s32 local * truncates it, and every consumer below (bondviewYPositionRelated, * change_player_pos_to_target, prop->stan) rebuilds it unbased and * dereferences it. Local only -- no struct/ABI impact -- and the s32 pad @@ -10412,7 +10412,7 @@ Gfx* hudmsgBottomRender(Gfx* arg0) view_vert = view_top - view_top_offset; #ifdef PORT - /* D298/M2: the (s32) casts are an N64 match hack; on the port they + /* D413/M2: the (s32) casts are an N64 match hack; on the port they * truncate stack pointers. Pass them at full width. */ arg0 = draw_blackbox_to_screen(arg0, &view_left, &view_vert, &view_horiz, &view_top); #else @@ -11430,7 +11430,7 @@ void sub_GAME_7F08BEEC(Mtxf *matrices, s32 count) for (i = 0, j = 0; i < count; i++, j += sizeof(Mtxf)) { - matrix_4x4_multiply_homogeneous(currentPlayerGetViewToWorldMtxf(), (Mtxf *)((uintptr_t)matrices + j), &sp40); /* D298/M2 */ + matrix_4x4_multiply_homogeneous(currentPlayerGetViewToWorldMtxf(), (Mtxf *)((uintptr_t)matrices + j), &sp40); /* D413/M2 */ sp40.m[3][0] -= g_CurrentPlayer->current_model_pos.f[0]; sp40.m[3][1] -= g_CurrentPlayer->current_model_pos.f[1]; diff --git a/src/game/bondview_r.c b/src/game/bondview_r.c index a00d5fbe..69a77f27 100644 --- a/src/game/bondview_r.c +++ b/src/game/bondview_r.c @@ -394,7 +394,7 @@ void bondviewLoadSetupIntroSection(void) { rand_camera_index--; #ifdef PORT - /* D88/D298: prev is a u32 holding the truncated host pointer; re-base. */ + /* D88/D413: prev is a u32 holding the truncated host pointer; re-base. */ ptr_random06cam_entry = PORT_N64PTR(struct SetupIntroCamera, ptr_random06cam_entry->prev); #else ptr_random06cam_entry = ptr_random06cam_entry->prev; diff --git a/src/game/chr.c b/src/game/chr.c index af2a01b7..dcbcf3e1 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -33,7 +33,7 @@ #include "model.h" #include "tex.h" -/* D298/M2: animation_table_ptrs1/2 slots and vtxstore_allocate() results hold +/* D413/M2: animation_table_ptrs1/2 slots and vtxstore_allocate() results hold * N64/window addresses as s32; re-base where they become pointers. Identity at * PORT_ADDR_BASE == 0. */ #if defined(PORT) @@ -897,7 +897,7 @@ 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; + /* D413: 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 @@ -3585,7 +3585,7 @@ after_opcode: #ifdef PORT /* PC port (D43/D45): CollisionRelatedNode is a raw vma (u32), not a pointer. - * D301: that vma is an N64 address (the port stores window pointers as + * D416: that vma is an N64 address (the port stores window pointers as * 32-bit values, and PORT_ADDR_BASE is 4 GiB-aligned, so the low 32 bits * ARE the N64 address), so it must be re-based like every other * address-carrying field -- a bare cast leaves it as 0x7070_xxxx and the diff --git a/src/game/chraction.c b/src/game/chraction.c index ec23c47b..8069297c 100644 --- a/src/game/chraction.c +++ b/src/game/chraction.c @@ -45,7 +45,7 @@ #define PUN_ATTACK_ITEM(punchr, field) ((s32)(punchr)->field.attack_item) #endif -/* D298/M2: `(s32)&ANIM_DATA_x + (s32)&ptr_animation_table->data` is an N64 +/* D413/M2: `(s32)&ANIM_DATA_x + (s32)&ptr_animation_table->data` is an N64 * address, and animation_table_ptrs1[] slots hold N64 addresses; re-base where * such a value becomes a pointer. Identity at PORT_ADDR_BASE == 0. */ #if defined(PORT) @@ -4996,7 +4996,7 @@ bool if_actor_able_set_on_path(ChrRecord *self, s32 pathid) { if (pathid && chrIsNotDeadOrShot(self)) { - /* D298/M2: callers pass a window PathRecord* truncated through this + /* D413/M2: callers pass a window PathRecord* truncated through this * s32 param (chrai.c / aicommands.def); re-base on the read. */ set_actor_on_path(self, PORT_N64PTR(struct patrol_path, pathid)); return TRUE; diff --git a/src/game/cleanup_objects.c b/src/game/cleanup_objects.c index ce3342c4..e607cedb 100644 --- a/src/game/cleanup_objects.c +++ b/src/game/cleanup_objects.c @@ -24,7 +24,7 @@ void cleanupObjects(s32 stage) { #ifdef PORT - /* D300: the decomp's `(u32)g_CurrentSetup.propDefs` converts the pointer + /* D415: 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 diff --git a/src/game/image.c b/src/game/image.c index c00ad6bd..0ae73b0e 100644 --- a/src/game/image.c +++ b/src/game/image.c @@ -1740,7 +1740,7 @@ void texReadAlphaBits(u8 *image,s32 count) s32 texReadUncompressed(u8 *dst, s32 width, s32 height, s32 format) { #ifdef PORT - /* D303: aligning a host pointer through u32 truncates it -- on arm64 dst is + /* D418: aligning a host pointer through u32 truncates it -- on arm64 dst is * ~0x1000_70xx_xxxx and these would become unbased 0x70xx_xxxx, then be * written through. Align at full pointer width instead. Identical result * where pointers are 32-bit. NOTE: currently unreachable on the PC port -- diff --git a/src/game/image_bank.c b/src/game/image_bank.c index e206f683..533ccf91 100644 --- a/src/game/image_bank.c +++ b/src/game/image_bank.c @@ -21,7 +21,7 @@ s32 img_bitcount; s32 dword_CODE_bss_8008D0AC; //8008D0B0; #if defined(PORT) -uintptr_t globalbank_rdram_offset; /* D298/M2: host-based, see image_bank.h */ +uintptr_t globalbank_rdram_offset; /* D413/M2: host-based, see image_bank.h */ #else s32 globalbank_rdram_offset; #endif @@ -98,7 +98,7 @@ extern u8* _GlobalimagetableSegmentRomStart; void texSetBitstring(s32 pos) { #if defined(PORT) - /* D298/M2: callers pass a host pointer through this s32 parameter, which + /* D413/M2: callers pass a host pointer through this s32 parameter, which * truncates it. Because PORT_ADDR_BASE is 4 GiB-aligned the low 32 bits * are the N64/window offset, so re-base them. Identity at base 0. */ img_curpos = (u8 *)portN64ToHost((u32)pos); @@ -269,7 +269,7 @@ void texReset(void) #endif #if defined(PORT) - /* D298/M2: store a HOST-based offset. GIMG_OFF(sym) adds 0x02000000, so + /* D413/M2: store a HOST-based offset. GIMG_OFF(sym) adds 0x02000000, so * `globalbank_rdram_offset + GIMG_OFF(sym)` is then a live host pointer. * `- 0x02000000` is the 64-bit equivalent of the original 32-bit * `+ 0xFE000000` wraparound (which relied on mod-2^32 cancellation). */ diff --git a/src/game/image_bank.h b/src/game/image_bank.h index 70e4bf56..682edd56 100644 --- a/src/game/image_bank.h +++ b/src/game/image_bank.h @@ -26,7 +26,7 @@ extern u8* img_curpos; extern s32 img_bitcount; extern s32 *pGlobalimagetable; #if defined(PORT) -/* D298/M2: holds a host-based RDRAM offset (pGlobalimagetable - 0x02000000) +/* D413/M2: holds a host-based RDRAM offset (pGlobalimagetable - 0x02000000) * so `globalbank_rdram_offset + GIMG_OFF(sym)` is a live host pointer in the * DRAM window. s32 on N64 (see image_bank.c). */ extern uintptr_t globalbank_rdram_offset; diff --git a/src/game/initactorpropstuff.c b/src/game/initactorpropstuff.c index ff481361..289b60f7 100644 --- a/src/game/initactorpropstuff.c +++ b/src/game/initactorpropstuff.c @@ -6,7 +6,7 @@ #include "math.h" #include "math_floor.h" -/* D298/M2: these tables store animation addresses as N64 values (s32/union +/* D413/M2: these tables store animation addresses as N64 values (s32/union * offset) that are then cast to pointers. Re-base when the pointer is formed; * identity at PORT_ADDR_BASE == 0. */ #if defined(PORT) diff --git a/src/game/language.c b/src/game/language.c index de395ca4..f00bc562 100644 --- a/src/game/language.c +++ b/src/game/language.c @@ -11,7 +11,7 @@ #ifdef PORT #include "envflag.h" /* cached getenv for hot-path probes */ #endif -#include "port_addr.h" /* D298/M2: DRAM window base */ +#include "port_addr.h" /* D413/M2: DRAM window base */ extern resource_lookup_data_entry resource_lookup_data_array[]; /* ob.c */ static void langFixupLoadedBank(char *name, void *p) @@ -23,7 +23,7 @@ static void langFixupLoadedBank(char *name, void *p) #endif /* g_LangBanks[] holds N64 DRAM addresses as s32 (they must fit s32, see - * memp.h / D298). Re-base only where a slot becomes a pointer; identity at + * memp.h / D413). Re-base only where a slot becomes a pointer; identity at * PORT_ADDR_BASE == 0, so Windows/Linux are unchanged. */ #if defined(PORT) #include "port_addr.h" @@ -435,7 +435,7 @@ u8 * langGet(s32 slotID) * cast/credits text path (Cuba, bondviewRenderCredits, D76) hits a bank * slot that was never filled -> stale/garbage non-NULL value -> fault on * the table read below. Reject anything that is not a plausible mapped - * DRAM address. D298/M2: the DRAM window is shifted by PORT_ADDR_BASE on + * DRAM address. D413/M2: the DRAM window is shifted by PORT_ADDR_BASE on * macOS, so the bounds move with it (identity at PORT_ADDR_BASE == 0). */ { uintptr_t t = (uintptr_t)textbank_ptr; @@ -463,7 +463,7 @@ u8 * langGet(s32 slotID) 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. */ + /* D413/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 */ diff --git a/src/game/model.c b/src/game/model.c index 12b63583..bcac1696 100644 --- a/src/game/model.c +++ b/src/game/model.c @@ -14,7 +14,7 @@ #include "math_asinfacosf.h" #include "math_floor.h" -/* D298/M2: ModelAnimation.bitDescriptors/bitStream are u32 fields holding N64 +/* D413/M2: ModelAnimation.bitDescriptors/bitStream are u32 fields holding N64 * addresses (D32: the blob base is added by expand_ani_table_entries). Re-base * where they become pointers; identity at PORT_ADDR_BASE == 0. */ #if defined(PORT) @@ -1043,7 +1043,7 @@ u16 modelAnimReadRootMotionValue(ModelAnimation *anim, s32 fieldIndex, s32 extra u8 bitsThisRead; result = 0; - desc = PORT_N64PTR(struct ModelAnimBitField, anim->bitDescriptors) + fieldIndex; // D32: u32 -> ptr (D298/M2 re-base) + desc = PORT_N64PTR(struct ModelAnimBitField, anim->bitDescriptors) + fieldIndex; // D32: u32 -> ptr (D413/M2 re-base) bitsRemaining = desc->bitCount; if (bitsRemaining > 0) @@ -1051,7 +1051,7 @@ u16 modelAnimReadRootMotionValue(ModelAnimation *anim, s32 fieldIndex, s32 extra totalBitOffset = extraBitOffset + desc->bitOffset; byteIndex = totalBitOffset >> 3; totalBitOffset &= 7; - byteptr = PORT_N64PTR(u8, anim->bitStream) + byteIndex; // D32: u32 -> ptr (D298/M2 re-base) + byteptr = PORT_N64PTR(u8, anim->bitStream) + byteIndex; // D32: u32 -> ptr (D413/M2 re-base) bitsThisRead = 8 - totalBitOffset; if (bitsRemaining >= bitsThisRead) @@ -6225,7 +6225,7 @@ s32 loadAnimationFrame(ModelAnimation* anim, s32 frame, ModelSkeleton* unused) s32 source; s32 frameSize; #if defined(PORT) - /* D298/M2: keep the buffer address at full pointer width (D59 buffer). */ + /* D413/M2: keep the buffer address at full pointer width (D59 buffer). */ uintptr_t dest; #else u32 dest; diff --git a/src/game/othermodemicrocode.c b/src/game/othermodemicrocode.c index 3b62c7d8..d2e37b83 100644 --- a/src/game/othermodemicrocode.c +++ b/src/game/othermodemicrocode.c @@ -369,7 +369,7 @@ void texSelect(Gfx **gdlptr, struct sImageTableEntry *tconfig, u32 arg2, s32 arg texLoad((s32 *)tconfig, NULL); } - aa = PORT_N64PTR(u16, tconfig->index); /* D298/M2: index holds the pool address */ + aa = PORT_N64PTR(u16, tconfig->index); /* D413/M2: index holds the pool address */ tex = texFindInPool((aa)[-4], NULL); if (tconfig->level == 0) diff --git a/src/mema.c b/src/mema.c index d8c095cd..db760d76 100644 --- a/src/mema.c +++ b/src/mema.c @@ -1,6 +1,6 @@ #include #if defined(PORT) -#include "port_addr.h" /* D298/M2: memaAlloc returns a DRAM address */ +#include "port_addr.h" /* D413/M2: memaAlloc returns a DRAM address */ #endif #include "mema.h" #include "deb.h" @@ -318,7 +318,7 @@ void *memaAlloc(u32 amount) { } #if defined(PORT) - /* D298/M2: best->addr is an N64 DRAM address; hand back a live pointer. + /* D413/M2: best->addr is an N64 DRAM address; hand back a live pointer. * Identity at PORT_ADDR_BASE == 0. */ return portN64ToHost((u32)addr); #else diff --git a/src/memp.c b/src/memp.c index 6f83b7a0..52ee19e4 100644 --- a/src/memp.c +++ b/src/memp.c @@ -3,7 +3,7 @@ #include "memp.h" #include "game/language.h" #if defined(PORT) -#include "port_addr.h" /* portN64ToHost: DRAM window base (M2/D298) */ +#include "port_addr.h" /* portN64ToHost: DRAM window base (M2/D413) */ #endif /** @@ -47,7 +47,7 @@ void mempCheckMemflagTokens(s32 poolAreaStart, s32 poolAreaSize) //set pool 0 to what boss wants (room_model_buffer) //pool 0 = TotalPoolArea #if defined(PORT) - /* D298/M2: the pool lives in the N64 DRAM window, which on macOS is + /* D413/M2: the pool lives in the N64 DRAM window, which on macOS is * shifted up by PORT_ADDR_BASE. boss.c passes N64 addresses (the pool * must stay s32 for mempCheckMemflagTokens' arithmetic), so convert to * host pointers here — the MemoryPool fields are u8*. Identity at @@ -153,7 +153,7 @@ void mempSetBankStarts(s32 poolSizes[MEMPOOL_COUNT+1]) for (i = MEMPOOL_TOTAL; i < MEMPOOL_COUNT - 1; i++) { #if defined(PORT) - /* D298/M2: pool fields are u8* host pointers in the DRAM window. */ + /* D413/M2: pool fields are u8* host pointers in the DRAM window. */ g_mempPools[i + 1].start = (u8 *)(mempStart + bankstarts[i]); g_mempPools[i + 1].pos = 0; g_mempPools[i + 1].end = (u8 *)(mempStart + bankstarts[i + 1]); diff --git a/src/snd.c b/src/snd.c index be218af5..c1ca0f5f 100644 --- a/src/snd.c +++ b/src/snd.c @@ -1110,7 +1110,7 @@ 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. */ { - /* D299 (macOS arm64): this guard predates the shifted-window + /* D414 (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 From aebce4013e505311df2e9b1c51282cbe1914965c Mon Sep 17 00:00:00 2001 From: Dan Turner Date: Sun, 4 Oct 2026 10:58:12 +0100 Subject: [PATCH 08/12] macos/arm64: relabel findings to descriptive tags, use portAddrIsInWindow, document src/ edits Owner review follow-ups on #95: - Findings labels: replace the branch's D409-D421 (which clash with upstream's D-range) with descriptive MACOS- tags across findings.md (section F rows + tail entries), findings-index.csv, porting-notes.md, MACOS-ARM64-PLAN.md, CMakeLists.txt, building.md and the code comments. Real D-numbers get assigned at merge. - gen_findings_index.py: accept non-D descriptive tags as labels so the generated index stays complete while the branch is unmerged (D-rows unchanged). - language.c: convert the last hardcoded numeric address bound in the boot path to portAddrIsInWindow(), per the branch rule (address plausibility checks must not use numeric bounds). - Document the src/ edits the owner flagged as unwritten: the two vtxstore_allocate() callers and objDeform() (both the PORT_N64PTR(0) -> NULL class, under MACOS-BLOODSTAIN), the crouched-rifle table terminator (MACOS-CAST-CENSUS), and an honest note on which widened sites are #ifdef PORT and which follow the existing unguarded house style. - Drop the stray .DS_Store from history (it was added in the base commit and only deleted at the tip). No behaviour change: PORT_ADDR_BASE == 0 everywhere but macOS, and the one logic edit (language.c) is an identity there. Build + window + port_addr self-test ALL PASS on arm64. --- CMakeLists.txt | 2 +- docs/building.md | 2 +- docs/dev/GE-ENV-PROBES.md | 2 +- docs/dev/MACOS-ARM64-PLAN.md | 16 ++++----- docs/dev/findings-index.csv | 26 +++++++------- docs/dev/findings.md | 66 ++++++++++++++++++---------------- docs/porting-notes.md | 36 +++++++++---------- port/include/port_addr.h | 4 +-- port/src/n64stubs.c | 2 +- port/src/port_addr.c | 4 +-- src/bondtypes.h | 2 +- src/game/bg.c | 2 +- src/game/bg.h | 2 +- src/game/bondview2.c | 12 +++---- src/game/bondview_r.c | 2 +- src/game/chr.c | 6 ++-- src/game/chraction.c | 4 +-- src/game/cleanup_objects.c | 2 +- src/game/image.c | 2 +- src/game/image_bank.c | 6 ++-- src/game/image_bank.h | 2 +- src/game/initactorpropstuff.c | 2 +- src/game/language.c | 21 +++++------ src/game/model.c | 8 ++--- src/game/othermodemicrocode.c | 2 +- src/mema.c | 4 +-- src/memp.c | 6 ++-- src/snd.c | 2 +- tools_pc/gen_findings_index.py | 9 ++++- 29 files changed, 132 insertions(+), 124 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6dd04156..43057caa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,7 +76,7 @@ endif() # Debug gate: validate every u32 -> host-pointer conversion against the regions # the port actually maps, and report offenders at the conversion site with a # backtrace. Off by default (zero cost); enable with -DPORT_ADDR_STRICT=ON. -# Catches the D414-D418 truncated/never-rebased-pointer family early. +# Catches the MACOS-SFX-BOUND through MACOS-TEXALIGN truncated/never-rebased-pointer family early. option(PORT_ADDR_STRICT "Validate N64->host address conversions at runtime" OFF) include(TargetArch) diff --git a/docs/building.md b/docs/building.md index f59ee925..ee7d4eeb 100644 --- a/docs/building.md +++ b/docs/building.md @@ -211,7 +211,7 @@ To prove the detector is live rather than merely silent: GE_ADDRSTRICT_SELFTEST=1 ./build-strict/ge007.aarch64 -level_09 ``` -See findings D414–D420 for the bug family this exists to catch. +See findings MACOS-SFX-BOUND through MACOS-ADDR-STRICT for the bug family this exists to catch. ### Packaging a macOS app diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index 1c2e7e10..4ea0b53f 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -130,7 +130,7 @@ Two classes: | `GE_D96` | `src/game/chrprop.c:436,507` (cached `probe`) | D96 chrprop trace | dead (D96 closed) | | `GE_D104` | `src/game/bg.c:654` (rate-limited, `d104c`) | D104 depth-clear trace | dead (D104 closed) | | `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) | -| `GE_ADDRSTRICT_SELFTEST` | `port/src/port_addr.c` (`portAddrStrictSelfTest`) | **Negative self-test for `PORT_ADDR_STRICT` (D420).** Feeds `portN64ToHost()` a deliberately-unmapped address (`0x30000000`) at boot so a run can prove the validator is live rather than merely silent. Requires a build configured with `-DPORT_ADDR_STRICT=ON`; a no-op otherwise. | **live** (D420) | +| `GE_ADDRSTRICT_SELFTEST` | `port/src/port_addr.c` (`portAddrStrictSelfTest`) | **Negative self-test for `PORT_ADDR_STRICT` (MACOS-ADDR-STRICT).** Feeds `portN64ToHost()` a deliberately-unmapped address (`0x30000000`) at boot so a run can prove the validator is live rather than merely silent. Requires a build configured with `-DPORT_ADDR_STRICT=ON`; a no-op otherwise. | **live** (MACOS-ADDR-STRICT) | | `GE_D75` | `src/game/title.c` `sub_GAME_7F007F30` (capped 8) | D75 Bug 2 gun-barrel model probe: logs `chrModelInstance`/`gunModelInstance` ptr+obj+numMatrices, `render_pos`, `renderData.mtxlist`, `g_GfxMemPos`, `osVirtualToPhysical(render_pos)` | **LIVE** (D75 Bug 2 OPEN — see §F "D75 Bug 2 — RUNTIME PROBE") | | `GE_D305` | `src/snd.c:420` (`sndHandleEvent` desync branch, `#ifdef PORT`) | D305: logs the sound-preemption desync state when it is detected (after the NULL-guard fix made the crash itself impossible) — confirms whether `Game.AllUnlocked` correlates or was coincidental, and whether it recurs. Inert unset. | **live** (D305 monitoring) | | `GE_D309` | `src/game/chrai.c:810,3424`, `src/game/chraction.c:10139` (all `#ifdef PORT`) | D309 freeze probe: logs AI label-not-found restarts (`chrai.c`) and the run-to-bond persistent-list step-on path (`chraction.c`, capped 400 lines) — the diagnostics used to root-cause the elusive complete freeze (D309/D310). Inert unset. | **live** (D309/D310 fixed — kept for recurrence) | diff --git a/docs/dev/MACOS-ARM64-PLAN.md b/docs/dev/MACOS-ARM64-PLAN.md index 1d53de48..5c471a58 100644 --- a/docs/dev/MACOS-ARM64-PLAN.md +++ b/docs/dev/MACOS-ARM64-PLAN.md @@ -54,21 +54,21 @@ arm64 work was rebased onto it rather than maintained in parallel. Branch: **Adopted from #88 — this deletes the corresponding work in §3 M0:** - Homebrew GNU GCC (`gcc-16`) + libstdc++. Removes every clang workaround: no `-fms-extensions`/`-Wno-microsoft-anon-tag`, no libc++ wrapper header - shims (the old D410), no `-D_FORTIFY_SOURCE=0`. + shims (the clang-only MACOS-LIBCXX-SHIMS work), no `-D_FORTIFY_SOURCE=0`. - `scripts/gen_macho_syms.py` — build-tree Mach-O symbol generation; the committed `*.darwin.s` duplicates are gone. - `scripts/strip_weak_pragmas.py` + `port/src/macho_weak_aliases.s` — the - `#pragma weak` handling, so **`src/` carries no macOS edit for it** (the old - D409 in-source `#if defined(PORT) && defined(__APPLE__)` equates are gone). + `#pragma weak` handling, so **`src/` carries no macOS edit for it** (the clang-only + MACOS-WEAK-ALIAS in-source `#if defined(PORT) && defined(__APPLE__)` equates are gone). - POSIX `shm_open` DRAM backing, Darwin crash handling, and the AppKit main-thread event-pump guard in `gfx_sdl2.cpp` (that last one was §3 M3.1). **Kept from this plan — the arm64 delta:** -- §2's shifted-window address model (M1 / D413). `-pagezero_size 0x10000`, +- §2's shifted-window address model (M1 / MACOS-ADDR-WINDOW). `-pagezero_size 0x10000`, the core of #88's Intel approach, is fatal on arm64 (shrinking `__PAGEZERO` gets the process SIGKILLed), so CMake now sets it for x86_64 only and arm64 uses `PORT_ADDR_BASE`. -- D411 (`__x86_64__` → `PLATFORM_64BIT`). Without it #88's tree does not even +- MACOS-64BIT-GATE (`__x86_64__` → `PLATFORM_64BIT`). Without it #88's tree does not even *link* on arm64 — 23 undefined `_ANIM_DATA_*`. - `gen_macho_syms.py --base` (bakes `PORT_ADDR_BASE` into the absolute symbols; `--base 0` output is byte-identical to #88's transform). @@ -577,8 +577,8 @@ Record as a `Dxx`. | Phase | State | Notes | |---|---|---| | 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 D411 + `gen_macho_syms --base` survive (§0b) | -| M1 | **done** | shifted-window address model (D413); self-test ALL PASS on the #88 base | +| M0 | **superseded by #88** | the clang build/toolchain work is replaced by PR #88's GCC-based macOS layer; only MACOS-64BIT-GATE + `gen_macho_syms --base` survive (§0b) | +| M1 | **done** | shifted-window address model (MACOS-ADDR-WINDOW); self-test ALL PASS on the #88 base | | 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 | @@ -591,7 +591,7 @@ level crash-free: (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 D409–D415; D414 (silent SFX) and D415 (stage-unload truncation) +- Findings MACOS-WEAK-ALIAS through MACOS-PROPDEFS-TRUNC; MACOS-SFX-BOUND (silent SFX) and MACOS-PROPDEFS-TRUNC (stage-unload truncation) were both arm64-specific and are fixed. ### Prerequisites to run (easy to miss) diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index ecdc7cc2..608d6aab 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -285,16 +285,16 @@ D405,CLOSED,"fast3d/OpenGL: MSAA above the driver max black-screens; two glBindF D406,RESOLVED,front-end PC Options screen:,"RESOLVED (2026-09-28, port-layer only, Steam Deck-validated):" D407,PARTIAL,Tanks cannot be boarded/exited on PC —,"PARTIAL (fix landed in v0.4.0, port-only):" D408,OPEN,Game.SkipIntro skips the post-mission failure dossier:,"OPEN (cosmetic, not root-caused past the handoff site)." -D409,FIXED,macOS/arm64 (M0.4): #pragma weak X = Y has no Mach-O equivalent —,"FIXED (macOS port, lv.c/spectrum.c/objective_status.c) —" -D410,FIXED,macOS/arm64 (M0.6): the decomp's include/ stubs shadow libc++'s wrapper headers —,"FIXED (macOS port) — new port/shim/math.h + generated hostmath.h; the existing host{string,stdlib,stddef}.h.in now also pull libc++'s c++/v1/.h wrapper, guarded && defined(__cplusplus)." -D411,FIXED,"macOS/arm64 (M0.2): __x86_64__ was used as the ""64-bit PC"" gate —","FIXED — 22 sites in 14 src/+assets/ files and port/include/pc_protos.h now use PLATFORM_64BIT (defined by CMake on every 64-bit PC target, undefined on N64, so the N64 branch is unchanged)." -D412,FIXED,macOS/arm64 (M0.7/M0.8):,"FIXED — shm_open double-map (dram.c); Darwin ucontext registers + _XOPEN_SOURCE (crash.c, incl." -D413,FIXED,"macOS/arm64 (M1): the port realises the N64 32-bit address space as host addresses; native arm64 cannot map the low 4 GiB, so the space is shifted up by PORT_A…","FIXED (port layer) — port/include/port_addr.h + port/src/port_addr.c (portAddrInit, portN64ToHost/portHostToN64, PORT_N64PTR); all chokepoints routed through them; DRAM/cart/stacks mapped at B+…; seg…" -D414,FIXED,macOS/arm64: in-level SFX completely silent —,"FIXED — new portAddrIsInWindow() in port_addr.h, guard tests the real window." -D415,FIXED,macOS/arm64: crash at stage unload —,"FIXED (cleanup_objects.c, D3x ABI/layout class) —" -D416,FIXED,macOS/arm64: crash on guard gunfire —,FIXED (port_addr.h + chr.c) — -D417,FIXED,"macOS/arm64: six more pointer truncations, found by a static -Wint-conversion census rather than by crashing.","FIXED (model.c x4, bondview2.c x11, bg.c) —" -D418,FIXED,"macOS/arm64: texReadUncompressed() aligned its destination through u32 ((u32 *)(((u32)dst + 0xf) & ~0xf) x3), truncating a host pointer and then writing throug…","FIXED defensively (image.c), latent not live." -D419,OPEN,"Intermittent renderer crash, level-agnostic, observed once on Dam:","OPEN — observed once, not investigated." -D420,FIXED,PORT_ADDR_STRICT: a debug gate that catches the whole D414-D419 family at the conversion rather than at the fault.,FIXED (new CMake option + port_addr.h/port_addr.c). -D421,FIXED,MSAA=8 produced a completely BLACK screen —,FIXED (port/fast3d/gfx_opengl.cpp) — +MACOS-WEAK-ALIAS,FIXED,macOS/arm64 (M0.4): #pragma weak X = Y has no Mach-O equivalent —,"FIXED (macOS port, lv.c/spectrum.c/objective_status.c) —" +MACOS-LIBCXX-SHIMS,FIXED,macOS/arm64 (M0.6): the decomp's include/ stubs shadow libc++'s wrapper headers —,"FIXED (macOS port) — new port/shim/math.h + generated hostmath.h; the existing host{string,stdlib,stddef}.h.in now also pull libc++'s c++/v1/.h wrapper, guarded && defined(__cplusplus)." +MACOS-64BIT-GATE,FIXED,"macOS/arm64 (M0.2): __x86_64__ was used as the ""64-bit PC"" gate —","FIXED — 22 sites in 14 src/+assets/ files and port/include/pc_protos.h now use PLATFORM_64BIT (defined by CMake on every 64-bit PC target, undefined on N64, so the N64 branch is unchanged)." +MACOS-PLATFORM-BRANCHES,FIXED,macOS/arm64 (M0.7/M0.8):,"FIXED — shm_open double-map (dram.c); Darwin ucontext registers + _XOPEN_SOURCE (crash.c, incl." +MACOS-ADDR-WINDOW,FIXED,"macOS/arm64 (M1): the port realises the N64 32-bit address space as host addresses; native arm64 cannot map the low 4 GiB, so the space is shifted up by PORT_A…","FIXED (port layer) — port/include/port_addr.h + port/src/port_addr.c (portAddrInit, portN64ToHost/portHostToN64, PORT_N64PTR); all chokepoints routed through them; DRAM/cart/stacks mapped at B+…; seg…" +MACOS-SFX-BOUND,FIXED,macOS/arm64: in-level SFX completely silent —,"FIXED — new portAddrIsInWindow() in port_addr.h, guard tests the real window." +MACOS-PROPDEFS-TRUNC,FIXED,macOS/arm64: crash at stage unload —,"FIXED (cleanup_objects.c, D3x ABI/layout class) —" +MACOS-BLOODSTAIN,FIXED,macOS/arm64: crash on guard gunfire —,FIXED (port_addr.h + chr.c) — +MACOS-CAST-CENSUS,FIXED,"macOS/arm64: six more pointer truncations, found by a static -Wint-conversion census rather than by crashing.","FIXED (model.c x4, bondview2.c x11, bg.c) —" +MACOS-TEXALIGN,FIXED,"macOS/arm64: texReadUncompressed() aligned its destination through u32 ((u32 *)(((u32)dst + 0xf) & ~0xf) x3), truncating a host pointer and then writing throug…","FIXED defensively (image.c), latent not live." +MACOS-RENDER-CRASH,OPEN,"Intermittent renderer crash, level-agnostic, observed once on Dam:","OPEN — observed once, not investigated." +MACOS-ADDR-STRICT,FIXED,PORT_ADDR_STRICT: a debug gate that catches the whole MACOS-SFX-BOUND through MACOS-RENDER-CRASH family at the conversion rather than at the fault.,FIXED (new CMake option + port_addr.h/port_addr.c). +MACOS-MSAA-CLAMP,FIXED,MSAA=8 produced a completely BLACK screen —,FIXED (port/fast3d/gfx_opengl.cpp) — diff --git a/docs/dev/findings.md b/docs/dev/findings.md index cdfe26f8..657e9ad5 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -693,19 +693,19 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D406 | **front-end PC Options screen: long sections (INPUT, 17 visible rows) run off the bottom of the 440x330 paper, and rows past the silent 14-row page cap (Crouch mode, Reset to defaults, Bindings…) are unreachable from the front screen** (user report 2026-09-28, v0.4.0 pre-push review) — full `## D406` entry at file tail | RESOLVED (2026-09-28, port-layer only, Steam Deck-validated): in-section pagination in `frontoptions.c` (11 rows/page; the vertical stepper crosses page boundaries both ways; dim 'Page p/N' marker + bottom hint on every page; slider bars start past long labels; F10 overlay untouched). D406b/c/d same-cycle refinements (label/track overlap, hint visibility/wording, hint position). | | D407 | **Tanks cannot be boarded/exited on PC — the v0.4.0 use/reload split (D378/D393) removed the B-button tap the engine's tank handlers in `bondview2.c` consume (user report 2026-09-28, Runway/Streets)** — full `## D407` entry at file tail | PARTIAL (fix landed in v0.4.0, port-only): present `GE_CONT_B` on the use (E / pad A) rising edge only while `g_PlayerIsInTank == 1` or `g_BondCanEnterTank != 0`; E keeps its D378 no-reload-fallback semantics elsewhere, N64 layout unaffected. D407(b) same cycle: front PC Options page-edge highlight clamp + mouse-wheel / W-S paging (wheel queue consumed on the menu, D223); its wheel mapping shipped inverted and was fixed to match W/S (wheel up = step up). D407(c): board-animation lockout, menu-accept B gate in tank states, in-tank aim routed through the legacy velocity stick + `Input.TankAimScale` knob. RESOLVED 2026-09-28: all port-only (src/game zero-diff), 10 TANKDBG probes stripped, release binary verified clean, user live tank drive signed off (board + exit OK, aim feel good). | | D408 | **`Game.SkipIntro` skips the post-mission failure dossier: with it on, entering a level then aborting (watch Z+A) or dying (KIA) returns straight to the menus — no REPORT / "Mission status: KILLED IN ACTION / ABORTED" screen** (user report 2026-09-28; save/AllUnlocked ruled out) — full `## D408` entry at file tail | OPEN (cosmetic, not root-caused past the handoff site). v0.4.0 decision: SkipIntro stays EXPERIMENTAL / not recommended for regular users — F10 row relabelled "Skip intro (EXPERIMENTAL)". | -| D409 | **macOS/arm64 (M0.4): `#pragma weak X = Y` has no Mach-O equivalent — clang errors "aliases are not supported on darwin", and the pragma form silently emits a TU-local symbol instead** — full `## D409` entry at file tail | FIXED (macOS port, `lv.c`/`spectrum.c`/`objective_status.c`) — replaced with a `#if defined(PORT) && defined(__APPLE__)` global symbol equate (`.globl _X` / `_X = _Y`); Win/Linux keep the pragma. Verified: 3 TUs compile, aliases are global `S`/`D`/`T` symbols. | -| D410 | **macOS/arm64 (M0.6): the decomp's `include/` stubs shadow libc++'s wrapper headers — ``/``/``/`` hard-error, and `acosf`/`fmodf`/`fabsf` are undeclared** — full `## D410` entry at file tail | FIXED (macOS port) — new `port/shim/math.h` + generated `hostmath.h`; the existing `host{string,stdlib,stddef}.h.in` now also pull libc++'s `c++/v1/.h` wrapper, guarded `&& defined(__cplusplus)`. `limits.h`/`assert.h` needed no shim. All 186 TUs compile. | -| D411 | **macOS/arm64 (M0.2): `__x86_64__` was used as the "64-bit PC" gate — on arm64 it silently selected the N64 branch (34 undefined `_ANIM_DATA_*`, objecthandler `.bss` overrun, audio heap sizing)** — full `## D411` entry at file tail | FIXED — 22 sites in 14 `src/`+`assets/` files and `port/include/pc_protos.h` now use `PLATFORM_64BIT` (defined by CMake on every 64-bit PC target, undefined on N64, so the N64 branch is unchanged). | -| D412 | **macOS/arm64 (M0.7/M0.8): first native boot needs platform branches — no `memfd_create`, no glibc `ucontext.gregs`, no `/proc`, no fixed image base** — full `## D412` entry at file tail | FIXED — `shm_open` double-map (`dram.c`); Darwin `ucontext` registers + `_XOPEN_SOURCE` (`crash.c`, incl. `crashDumpThreads`); `_dyld_get_image_header`/`_NSGetExecutablePath` (`system.c`). **Binary links and runs: reaches ROM load.** | -| D413 | **macOS/arm64 (M1): the port realises the N64 32-bit address space as host addresses; native arm64 cannot map the low 4 GiB, so the space is shifted up by `PORT_ADDR_BASE` (16 TiB)** — full `## D413` entry at file tail | FIXED (port layer) — `port/include/port_addr.h` + `port/src/port_addr.c` (`portAddrInit`, `portN64ToHost`/`portHostToN64`, `PORT_N64PTR`); all chokepoints routed through them; DRAM/cart/stacks mapped at `B+…`; `seg_addr` re-based. ROM-free self-test ALL PASS. **Level boot + game-code cast sites still owed (M2, needs a ROM).** | -| D414 | **macOS/arm64: in-level SFX completely silent — `sndPlaySfx()`'s pre-window address guard (`sp >= 0x400000000ULL`, 16 GiB) rejected every real `ALSound*`, because with `PORT_ADDR_BASE` the sound lives at ~`0x1000_707b_5740`** — full `## D414` entry at file tail | FIXED — new `portAddrIsInWindow()` in `port_addr.h`, guard tests the real window. Verified: `[EVT]` 0→68, `[VOICE+]` 0→14 on `-level_37`; 21/21 control sweep with SFX on every level. | -| D415 | **macOS/arm64: crash at stage unload — `cleanupObjects()` did `u32 *obj = (u32)g_CurrentSetup.propDefs`, truncating a host pointer to 32 bits (unbased `0x7017_c70f` deref)** — full `## D415` entry at file tail | FIXED (`cleanup_objects.c`, D3x ABI/layout class) — cast pointer-to-pointer. Cuba x3 crash-free; control sweep 21/21. | -| D416 | **macOS/arm64: crash on guard gunfire — `chrCreateBloodStain()` faulted two ways: `portN64ToHost(0)` returned `base+0` instead of `NULL` (defeating the decomp's `if (p != NULL)` failure checks), and `Vertex.CollisionRelatedNode` (a `u32` vma kept 32-bit by `PROMOTE32`) was read with a bare cast** — full `## D416` entry at file tail | FIXED (`port_addr.h` + `chr.c`) — `portN64ToHost(0) -> NULL`; `CollisionRelatedNode` re-based via `PORT_N64PTR`. Verified `-level_39` x3 + 21/21 both sweeps. Does NOT fix D120 (the opcode-0x18 converter gap); blood decals may still be wrong. | -| D417 | **macOS/arm64: six more pointer truncations, found by a static `-Wint-conversion` census rather than by crashing.** Four are the "twin function" pattern (one of a near-duplicate solo/MP pair converted, the other missed). Includes `bg.c:5213`, where a truncated loop bound meant `bgRoomCalcBB()` never ran its body on arm64, giving **every room in every level an inverted bounding box** — silent, not a crash | FIXED (`model.c` x4, `bondview2.c` x11, `bg.c`) — all arm64-identity. The two MP-only fixes are committed but runtime-unverified. Possible relevance to open D249. | -| D418 | **macOS/arm64: `texReadUncompressed()` aligned its destination through `u32`** (`(u32 *)(((u32)dst + 0xf) & ~0xf)` x3), truncating a host pointer and then writing through it — but the path is **unreachable on PC** (a `GE_D303` probe over a full level boot recorded zero calls; textures come from the pre-converted `pccg` sidecars) | FIXED defensively (`image.c`), latent not live. Group-C census otherwise clean: `init()` is dead on PC, `rsp.c`'s XOR toggle already has a `PLATFORM_64BIT` branch, `objecthandler_2.c`'s `name` is an int-in-a-pointer, `animationtable_data.h`'s 25 warnings are placeholders rewritten by `initResolveAnimTable()` via `ANI_ADDR`. | -| D419 | **Intermittent renderer crash, level-agnostic, observed once on Dam: SIGSEGV in `gfx_sp_tri1` -> `gfx_texture_cache` with a garbage fault address (`0x16d5bbd636173b9e`).** Did not reproduce in 8 subsequent Dam runs, and is not attributable to the D417 changes landed in the same session. Not a known signature (D233 was `gfx_sp_tri1` but a visibility bug, not a crash) | **OPEN — observed once, not investigated.** Garbage (non-address-shaped) fault value suggests a corrupted texture-cache node or a race rather than the D414-D418 truncation family. Next: run with `PORT_ADDR_STRICT=ON` to rule the address family out, then instrument the texture cache. | -| D420 | **`PORT_ADDR_STRICT`: a debug gate that catches the whole D414-D419 family at the conversion rather than at the fault.** Off by default, compiled out entirely; when on, `portN64ToHost()` validates its argument against the mapped regions and reports offenders with a backtrace | FIXED (new CMake option + `port_addr.h`/`port_addr.c`). Negative self-test (`GE_ADDRSTRICT_SELFTEST=1`) proves the detector fires. Zero reports across 6 levels, 5 under full-control input. | -| D421 | **MSAA=8 produced a completely BLACK screen** — `GL_MAX_SAMPLES` is 4 on this GPU, and nothing ever clamped the requested level, so `glRenderbufferStorageMultisample` left the renderbuffer unallocated -> incomplete FBO. `video.c`'s own comment claims MSAA "snaps down to the highest supported level"; it did not | FIXED (`port/fast3d/gfx_opengl.cpp`) — clamp to `GL_MAX_SAMPLES`, log the snap once. Also fixed two index-vs-GL-name bugs in the same file (`resolve_msaa_color_buffer`, `copy_framebuffer`). Verified MSAA 1/2/4/8 all render. | +| MACOS-WEAK-ALIAS | **macOS/arm64 (M0.4): `#pragma weak X = Y` has no Mach-O equivalent — clang errors "aliases are not supported on darwin", and the pragma form silently emits a TU-local symbol instead** — full `## MACOS-WEAK-ALIAS` entry at file tail | FIXED (macOS port, `lv.c`/`spectrum.c`/`objective_status.c`) — replaced with a `#if defined(PORT) && defined(__APPLE__)` global symbol equate (`.globl _X` / `_X = _Y`); Win/Linux keep the pragma. Verified: 3 TUs compile, aliases are global `S`/`D`/`T` symbols. | +| MACOS-LIBCXX-SHIMS | **macOS/arm64 (M0.6): the decomp's `include/` stubs shadow libc++'s wrapper headers — ``/``/``/`` hard-error, and `acosf`/`fmodf`/`fabsf` are undeclared** — full `## MACOS-LIBCXX-SHIMS` entry at file tail | FIXED (macOS port) — new `port/shim/math.h` + generated `hostmath.h`; the existing `host{string,stdlib,stddef}.h.in` now also pull libc++'s `c++/v1/.h` wrapper, guarded `&& defined(__cplusplus)`. `limits.h`/`assert.h` needed no shim. All 186 TUs compile. | +| MACOS-64BIT-GATE | **macOS/arm64 (M0.2): `__x86_64__` was used as the "64-bit PC" gate — on arm64 it silently selected the N64 branch (34 undefined `_ANIM_DATA_*`, objecthandler `.bss` overrun, audio heap sizing)** — full `## MACOS-64BIT-GATE` entry at file tail | FIXED — 22 sites in 14 `src/`+`assets/` files and `port/include/pc_protos.h` now use `PLATFORM_64BIT` (defined by CMake on every 64-bit PC target, undefined on N64, so the N64 branch is unchanged). | +| MACOS-PLATFORM-BRANCHES | **macOS/arm64 (M0.7/M0.8): first native boot needs platform branches — no `memfd_create`, no glibc `ucontext.gregs`, no `/proc`, no fixed image base** — full `## MACOS-PLATFORM-BRANCHES` entry at file tail | FIXED — `shm_open` double-map (`dram.c`); Darwin `ucontext` registers + `_XOPEN_SOURCE` (`crash.c`, incl. `crashDumpThreads`); `_dyld_get_image_header`/`_NSGetExecutablePath` (`system.c`). **Binary links and runs: reaches ROM load.** | +| MACOS-ADDR-WINDOW | **macOS/arm64 (M1): the port realises the N64 32-bit address space as host addresses; native arm64 cannot map the low 4 GiB, so the space is shifted up by `PORT_ADDR_BASE` (16 TiB)** — full `## MACOS-ADDR-WINDOW` entry at file tail | FIXED (port layer) — `port/include/port_addr.h` + `port/src/port_addr.c` (`portAddrInit`, `portN64ToHost`/`portHostToN64`, `PORT_N64PTR`); all chokepoints routed through them; DRAM/cart/stacks mapped at `B+…`; `seg_addr` re-based. ROM-free self-test ALL PASS. **Level boot + game-code cast sites still owed (M2, needs a ROM).** | +| MACOS-SFX-BOUND | **macOS/arm64: in-level SFX completely silent — `sndPlaySfx()`'s pre-window address guard (`sp >= 0x400000000ULL`, 16 GiB) rejected every real `ALSound*`, because with `PORT_ADDR_BASE` the sound lives at ~`0x1000_707b_5740`** — full `## MACOS-SFX-BOUND` entry at file tail | FIXED — new `portAddrIsInWindow()` in `port_addr.h`, guard tests the real window. Verified: `[EVT]` 0→68, `[VOICE+]` 0→14 on `-level_37`; 21/21 control sweep with SFX on every level. | +| MACOS-PROPDEFS-TRUNC | **macOS/arm64: crash at stage unload — `cleanupObjects()` did `u32 *obj = (u32)g_CurrentSetup.propDefs`, truncating a host pointer to 32 bits (unbased `0x7017_c70f` deref)** — full `## MACOS-PROPDEFS-TRUNC` entry at file tail | FIXED (`cleanup_objects.c`, D3x ABI/layout class) — cast pointer-to-pointer. Cuba x3 crash-free; control sweep 21/21. | +| MACOS-BLOODSTAIN | **macOS/arm64: crash on guard gunfire — `chrCreateBloodStain()` faulted two ways: `portN64ToHost(0)` returned `base+0` instead of `NULL` (defeating the decomp's `if (p != NULL)` failure checks), and `Vertex.CollisionRelatedNode` (a `u32` vma kept 32-bit by `PROMOTE32`) was read with a bare cast** — full `## MACOS-BLOODSTAIN` entry at file tail | FIXED (`port_addr.h` + `chr.c`) — `portN64ToHost(0) -> NULL`; `CollisionRelatedNode` re-based via `PORT_N64PTR`. Verified `-level_39` x3 + 21/21 both sweeps. Does NOT fix D120 (the opcode-0x18 converter gap); blood decals may still be wrong. | +| MACOS-CAST-CENSUS | **macOS/arm64: six more pointer truncations, found by a static `-Wint-conversion` census rather than by crashing.** Four are the "twin function" pattern (one of a near-duplicate solo/MP pair converted, the other missed). Includes `bg.c:5213`, where a truncated loop bound meant `bgRoomCalcBB()` never ran its body on arm64, giving **every room in every level an inverted bounding box** — silent, not a crash | FIXED (`model.c` x4, `bondview2.c` x11, `bg.c`) — all arm64-identity. The two MP-only fixes are committed but runtime-unverified. Possible relevance to open D249. | +| MACOS-TEXALIGN | **macOS/arm64: `texReadUncompressed()` aligned its destination through `u32`** (`(u32 *)(((u32)dst + 0xf) & ~0xf)` x3), truncating a host pointer and then writing through it — but the path is **unreachable on PC** (a `GE_D303` probe over a full level boot recorded zero calls; textures come from the pre-converted `pccg` sidecars) | FIXED defensively (`image.c`), latent not live. Group-C census otherwise clean: `init()` is dead on PC, `rsp.c`'s XOR toggle already has a `PLATFORM_64BIT` branch, `objecthandler_2.c`'s `name` is an int-in-a-pointer, `animationtable_data.h`'s 25 warnings are placeholders rewritten by `initResolveAnimTable()` via `ANI_ADDR`. | +| MACOS-RENDER-CRASH | **Intermittent renderer crash, level-agnostic, observed once on Dam: SIGSEGV in `gfx_sp_tri1` -> `gfx_texture_cache` with a garbage fault address (`0x16d5bbd636173b9e`).** Did not reproduce in 8 subsequent Dam runs, and is not attributable to the MACOS-CAST-CENSUS changes landed in the same session. Not a known signature (D233 was `gfx_sp_tri1` but a visibility bug, not a crash) | **OPEN — observed once, not investigated.** Garbage (non-address-shaped) fault value suggests a corrupted texture-cache node or a race rather than the MACOS-SFX-BOUND through MACOS-TEXALIGN truncation family. Next: run with `PORT_ADDR_STRICT=ON` to rule the address family out, then instrument the texture cache. | +| MACOS-ADDR-STRICT | **`PORT_ADDR_STRICT`: a debug gate that catches the whole MACOS-SFX-BOUND through MACOS-RENDER-CRASH family at the conversion rather than at the fault.** Off by default, compiled out entirely; when on, `portN64ToHost()` validates its argument against the mapped regions and reports offenders with a backtrace | FIXED (new CMake option + `port_addr.h`/`port_addr.c`). Negative self-test (`GE_ADDRSTRICT_SELFTEST=1`) proves the detector fires. Zero reports across 6 levels, 5 under full-control input. | +| MACOS-MSAA-CLAMP | **MSAA=8 produced a completely BLACK screen** — `GL_MAX_SAMPLES` is 4 on this GPU, and nothing ever clamped the requested level, so `glRenderbufferStorageMultisample` left the renderbuffer unallocated -> incomplete FBO. `video.c`'s own comment claims MSAA "snaps down to the highest supported level"; it did not | FIXED (`port/fast3d/gfx_opengl.cpp`) — clamp to `GL_MAX_SAMPLES`, log the snap once. Also fixed two index-vs-GL-name bugs in the same file (`resolve_msaa_color_buffer`, `copy_framebuffer`). Verified MSAA 1/2/4/8 all render. | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS @@ -15538,7 +15538,7 @@ included; D281 keeps the shipped mitigation + disclosure, zero-saves root cause **Status:** OPEN — cosmetic (no crash, no data loss; the mission still records as failed/aborted and the menus are fully functional). Root-causing needs a side-by-side of front-end state (`current_menu`, `g_CurrentStageToLoad`, `mission_failed_or_aborted`, `g_isBondKIA`) on the fail return with SkipIntro off vs on — candidate for v0.5.0 alongside the D216 audio half. -## D409 — macOS/arm64 (M0.4): `#pragma weak X = Y` has no Mach-O equivalent +## MACOS-WEAK-ALIAS — macOS/arm64 (M0.4): `#pragma weak X = Y` has no Mach-O equivalent **Context.** The decomp uses GCC's `#pragma weak alias = target` in five TUs to create a weak symbol alias: `src/libultra/gu/sinf.c` + `cosf.c` (`fsin/sinf = __sinf`, `fcos/cosf = __cosf`), `src/game/objective_status.c` (`objectiveGetStatus_WEAK = get_status_of_objective`), `src/game/lv.c` (`g_DebugPortalsInputBufferSource1..4 = g_DebugPortalsInputBuffer1..4`), `src/game/spectrum.c` (`spec_keyboard_row_z_c_v = spec_keyboard_buffer`). On ELF/MinGW this is a normal weak alias. On Darwin/Mach-O it is not: - `__attribute__((weak, alias("target")))` → hard error: `aliases are not supported on darwin`. - The `#pragma weak X = Y` form is *accepted* but clang emits `X` as a **TU-local** symbol (`nm` shows lowercase `d`, not global `D`/`T`). Two consequences: (a) in `lv.c`/`spectrum.c`, which both `extern`-declare and then use the alias in the same TU, the local symbol plus the extern declaration produce `error: reference to 'X' is ambiguous`; (b) in `objective_status.c`, the consumer `chrai.c:41,2538` references `objectiveGetStatus_WEAK` cross-TU, which would be an **undefined symbol at link** because the alias never became global. @@ -15549,7 +15549,7 @@ __asm__(".globl _X\n_X = _Y"); /* Mach-O prefixes C symbols with '_' */ The equate makes `X` a real global symbol at `Y`'s address (verified: `nm` shows `_objectiveGetStatus_WEAK` as `T` at the same address as `_get_status_of_objective`, and the `lv.c` aliases as `S`). Because every target is defined in-tree, a *strong* alias has identical semantics to the ELF weak alias. Files touched: `src/game/lv.c` (4 aliases), `src/game/spectrum.c` (1), `src/game/objective_status.c` (1, cross-TU). `sinf.c`/`cosf.c` still compile unchanged (their pragma makes `sinf`/`cosf` TU-local, so other TUs bind to libSystem's instead of Rare's polynomial — a fidelity divergence, not a build break; tracked as a follow-up in `docs/dev/MACOS-ARM64-PLAN.md`). **Note on the `inherits struct` sibling issue.** The same class of "GCC extension with no clang equivalent" appears for `#define inherits struct`: clang needs `-fms-extensions` where GCC needs `-fplan9-extensions` (plan fact F11). Both are handled in `CMakeLists.txt` by selecting on `CMAKE_C_COMPILER_ID`, not the platform (on macOS `gcc` is Apple Clang). -## D410 — macOS/arm64 (M0.6): the decomp's include/ stubs shadow libc++'s wrapper headers +## MACOS-LIBCXX-SHIMS — macOS/arm64 (M0.6): the decomp's include/ stubs shadow libc++'s wrapper headers **Context.** The fast3d C++ TUs failed to compile on Apple clang: ` tried including but didn't find libc++'s header` (also ``, ``, ``), plus `use of undeclared identifier 'acosf'/'fmodf'/'fabsf'` in `gfx_pc.cpp`. Cause has two halves: 1. **Shadowing.** The build puts the decomp's `include/` (which contains `math.h`, `limits.h`, `assert.h`, `stdlib.h`, `string.h`, `stddef.h` — N64 libc stubs) ahead of the system include path, so `#include ` finds the N64 stub, not the SDK's. 2. **libc++ wrapper guards.** libc++ does not use the C header directly: `$SDK/usr/include/c++/v1/.h` is a wrapper that defines `_LIBCPP__H` and then `#include_next`s the C header. `` etc. include `` and *then* require `_LIBCPP_MATH_H`. The old shims (`hoststring.h` etc.) included the raw SDK C header, so the guard was never defined. libstdc++ (Linux/MinGW) has no such wrapper, which is why this only shows on macOS. @@ -15557,20 +15557,20 @@ The equate makes `X` a real global symbol at `Y`'s address (verified: `nm` shows **Fix.** The generated `host.h` shims include the **SDK C header first, then libc++'s `c++/v1/.h` wrapper** (so the wrapper processes and defines its guard, and the C declarations are already present for its `#include_next` to be a no-op). Applied to `host{string,stdlib,stddef}.h.in` (existing) and new `hostmath.h.in`. `limits.h`/`assert.h` need **no** shim — they were never in the failure set: libc++'s `` defines `_LIBCPP_CLIMITS` itself, and `` only errors if libc++ ever *starts* defining `` (it does not). New shim `port/shim/math.h` routes **macOS C++** TUs to `hostmath.h` and leaves `include/math.h` (the N64 stub) for C and for non-Apple C++, so the Windows/Linux builds are byte-for-byte unaffected. The wrapper includes are additionally guarded `&& defined(__cplusplus)`: `port/shim/string.h` includes its host header for *C* TUs too, and libc++ headers are not valid C (the first attempt broke all ~180 TUs with `<__config> file not found`). **Verified.** All 186 TUs compile; `ge007.aarch64` links. -## D411 — macOS/arm64 (M0.2): `__x86_64__` was being used as the "64-bit PC" gate +## MACOS-64BIT-GATE — macOS/arm64 (M0.2): `__x86_64__` was being used as the "64-bit PC" gate **Context.** ~22 places in unmodified game code (`src/`, `assets/`) gated a PC-only 64-bit fix on `#if defined(__x86_64__)`: D34 (`assets/animationtable_data.h` — `ANIM_DATA_*` as lvalues at `g_pc_animdata_base+off`), D40 (`src/game/objecthandler.c` 600-entry table; the N64 branch overruns `.bss` by 12 KB on a 64-bit host), D36/D37 audio heap sizing (`src/music.c`, `src/libultra/audio/*`), D39 (`image_bank.c`), D32-class (`rsp.c`), plus `src/memp.c`. `port/include/pc_protos.h:34` gated the entire D38 prototype header (~400 prototypes) the same way. On arm64 the gate was false, so the build silently took the **N64** branch: the link failed with 34 undefined `_ANIM_DATA_*`, and the D38 prototypes vanished (45–63 implicit-declaration warnings per large TU = silent pointer-return truncation). **Fix.** Replace `defined(__x86_64__)` with `defined(PLATFORM_64BIT)` at all 22 game/asset sites and in `pc_protos.h`. `PLATFORM_64BIT` is defined by `CMakeLists.txt` (`add_definitions`, driven by `cmake/TargetArch.cmake`) for every 64-bit PC target (x86_64 and aarch64), and is *undefined* in the N64 build (which never sees `port/`), so the N64 branch is unchanged. `port/include/platform.h`'s own `__x86_64__`/`__aarch64__` checks (arch detection) and `port/src/system.c`'s `_mm_pause` gate are genuinely x86-conditional and were left alone. **Verified.** macOS arm64: no undefined symbols, links. Windows/Linux x86_64: identical branch as before (PLATFORM_64BIT was already defined there). -## D412 — macOS/arm64 (M0.7/M0.8): first native boot needs platform branches in dram.c / crash.c / system.c -**Context.** With D409–D411 applied, all TUs compiled but the link still needed `crashDumpThreads` and `memfd_create`, and `crash.c` lacked a macOS path entirely. +## MACOS-PLATFORM-BRANCHES — macOS/arm64 (M0.7/M0.8): first native boot needs platform branches in dram.c / crash.c / system.c +**Context.** With MACOS-WEAK-ALIAS, MACOS-LIBCXX-SHIMS and MACOS-64BIT-GATE applied, all TUs compiled but the link still needed `crashDumpThreads` and `memfd_create`, and `crash.c` lacked a macOS path entirely. **Fix.** - `port/src/dram.c`: `memfd_create` is Linux-only. macOS branch uses `shm_open("/ge007_dram_", O_RDWR|O_CREAT|O_EXCL, 0600)` + immediate `shm_unlink`, then `ftruncate` + two `mmap(MAP_SHARED|MAP_FIXED)` views — the same one-backing-store/two-views shape. (Measured: the two views alias correctly.) - `port/src/crash.c`: the POSIX block is now `PLATFORM_LINUX || PLATFORM_MACOS`, so `crashDumpThreads` exists. Darwin register access differs: `uc_mcontext` is a *pointer* to `__darwin_mcontext64`, so the arm64 path reads `__ss.__pc/__lr/__sp/__fp` and `__x[0..7]` (glibc's `uc_mcontext.gregs[REG_R*]` is x86_64-only). Added `_XOPEN_SOURCE` for Darwin's `` ("deprecated ucontext routines require _XOPEN_SOURCE"), `_dyld_get_image_vmaddr_slide` in the dump (for `atos` symbolication), and the `signal.h`/`execinfo.h`/`dladdr` path is portable as-is. - `port/src/system.c`: `sysImageBase()` returns `_dyld_get_image_header(0)` on macOS (no fixed base — arm64 is PIE-only); new `getExeDir()` helper uses `_NSGetExecutablePath` on macOS, `/proc/self/exe` on Linux, shared by `sysGetExeDir()` and `sysResolvePath()`'s `$S/` fallback (which previously read `/proc/self/exe` unconditionally, silently failing on macOS). **Verified.** `build-pc/ge007.aarch64` links and runs: prints the version banner, loads/writes `build-pc/data/ge007.ini` (so `$S/`/exe-dir resolution works under the .app-less layout), and exits cleanly with "no ROM found" — i.e. the macOS build now reaches ROM load. It does **not** yet boot a level: the fixed-address DRAM/cart mappings (`0x70000000`/`0x10000000`) are unimplementable on native arm64 (plan §1 F1–F3); that is the M1 shifted-window work (`docs/dev/MACOS-ARM64-PLAN.md`). -## D413 — macOS/arm64 (M1): the N64 address-space window must be shifted off the low 4 GiB +## MACOS-ADDR-WINDOW — macOS/arm64 (M1): the N64 address-space window must be shifted off the low 4 GiB **Context.** The port realises the N64's 32-bit address space *as host addresses*: DRAM at `0x70000000` (V1) with a KSEG0 mirror at `0x80000000` (V2), the cart at `0x10000000`, and ~1700 linker-absolute symbols carrying those values. `osVirtualToPhysical` is a `u32` truncation and `u32 -> pointer` was an identity cast. That is only sound while host addresses fit in 32 bits — true on Windows/Linux (image and mappings low), impossible on native arm64: `mmap(MAP_FIXED)` below 4 GiB fails (`__PAGEZERO`), shrinking the pagezero SIGKILLs the process, PIE is mandatory and `MAP_32BIT` fails (plan §1 F1–F4). **Fix — a shifted window.** Keep the N64 32-bit space intact but place it at a fixed host base `PORT_ADDR_BASE` (16 TiB on macOS, 0 elsewhere), so every game-visible 32-bit value is *unchanged* and only the `u32 -> host pointer` direction adds the base. - `port/include/port_addr.h`: `portN64ToHost(u32)` / `portHostToN64(void*)` / `PORT_N64PTR(T, expr)`. `portAddrInit()` (`port/src/port_addr.c`) reserves the whole 4 GiB window `PROT_NONE` via `mach_vm_map(VM_FLAGS_FIXED)` (non-clobbering, unlike macOS `MAP_FIXED`) as the first thing in `main()`, then fails loudly if it cannot — the macOS analogue of the Windows fixed-base check. @@ -15580,9 +15580,9 @@ The equate makes `X` a real global symbol at `Y`'s address (verified: `nm` shows **Verified (ROM-free).** `portAddrInit` runs a self-test of the window round-trips (DRAM V1/V2, cart), the absolute-symbol base, and the image-relative encoding — all PASS on `build-pc/ge007.aarch64`; the window reserves at `0x100000000000`. **Still owed.** A level boot (M1.7) and the game-code cast census (M2) both need a ROM in `data/`; the M0 build has none, so the residue of raw `u32 -> pointer` sites in game code is untested. `PORT_N64PTR` exists for those rewrites and is an identity at `PORT_ADDR_BASE == 0`, so Windows/Linux are unaffected by every change here. -## D414 — macOS/arm64: in-level sound effects were silent — a pre-window address guard rejected every real sound +## MACOS-SFX-BOUND — macOS/arm64: in-level sound effects were silent — a pre-window address guard rejected every real sound **Symptom (user report, arm64 macOS build).** Music played, but there were no sound effects at all — no gunshots, no footsteps, no door sounds. -**Root cause.** `sndPlaySfx()` (`src/snd.c`) validated the resolved `ALSound*` with a hardcoded numeric bound that predates the shifted-window address model (D413): +**Root cause.** `sndPlaySfx()` (`src/snd.c`) validated the resolved `ALSound*` with a hardcoded numeric bound that predates the shifted-window address model (MACOS-ADDR-WINDOW): ```c uintptr_t sp = (uintptr_t)sound; if (sp < 0x10000 || sp >= 0x400000000ULL) return NULL; /* 16 GiB */ @@ -15592,7 +15592,7 @@ The comment above it ("ALSound lives in game DRAM (~0x7000_0000..) or the cart i **Verified** (`-level_37`, scripted fire, `GE_AUDIOTRACE=1`): `[EVT]` 0 → 68 (the SFX event handler is now reached), `[VOICE+]` 0 → 14 (voices allocated and started), `[VOICE-]` 0 → 12, `[WAVELOOP]`/`[ENVELOPE]`/`[VOICES]` 15 each. Then 21/21 on the full-control sweep with SFX voices on every level. **Lesson.** Any absolute numeric bound on an N64 address is a latent arm64 bug; use the `port_addr.h` predicates. -## D415 — macOS/arm64: level-unload crash — `propDefs` truncated from a host pointer to 32 bits +## MACOS-PROPDEFS-TRUNC — macOS/arm64: level-unload crash — `propDefs` truncated from a host pointer to 32 bits **Symptom.** Crash at stage unload (`bossMainloop` → `bossRunTitleStage` → `lvlUnloadStageTextData`), faulting on `0x7017_c70f` — an unbased N64 DRAM address. Only reachable once a level actually ends, so it needed input (START/A) to surface; the no-input sweep never unloaded and missed it. **Root cause.** `cleanupObjects()` (`src/game/cleanup_objects.c:26`) did ```c @@ -15602,7 +15602,7 @@ The decomp's cast converts the pointer to a 32-bit integer and back into a `u32 **Fix.** Cast pointer-to-pointer: `u32 *obj = (u32 *)g_CurrentSetup.propDefs;`. `u32 *` is retained so `sizepropdef()`'s 4-byte-unit increment (`obj + sizepropdef(obj)`) is unchanged. This is the D3x ABI/layout class (pointer-width reconciliation), semantics-preserving; documented here per AGENTS.md rule 2. **Verified.** Cuba x3 crash-free with the full-control script; control sweep 21/21. A grep for the same `ptr = (u32)expr` int-to-pointer pattern found no other instances. -## D416 — macOS/arm64: guard gunfire crashed in chrCreateBloodStain (two pointer defects) +## MACOS-BLOODSTAIN — macOS/arm64: guard gunfire crashed in chrCreateBloodStain (two pointer defects) **Symptom (live playthrough, user report).** SIGBUS, then on a rebuild SIGSEGV, in `chrCreateBloodStain` (`src/game/chr.c`) whenever a guard opened fire. Reproduced reliably on Caverns (`-level_39`) with a scripted sustained-fire input; the no-input level sweep never triggered it, because it needs a guard to actually shoot at you. **Defect 1 — `portN64ToHost(0)` returned a non-NULL pointer.** The window maps N64 address `a` to `PORT_ADDR_BASE + a`, so `a == 0` produced `base+0` — a non-NULL address inside the reserved `PROT_NONE` region. Game code signals allocation failure by returning an N64 address of **0** and having the caller test the converted pointer: ```c @@ -15613,11 +15613,15 @@ if (newvertices != NULL) { ... newvertices[j] = ...; } /* test passes; sto **Defect 2 — `CollisionRelatedNode` read with a bare cast.** `Vertex.CollisionRelatedNode` is `u32` on the port (`bondtypes.h:753`), aliasing `LinkedTo` in the same union at offset 8. `modelPromoteNodeOffsetsToPointers()` rebases it with `PROMOTE32` (`model.c:5996`), which is `var = (u32)((u32)var + diff)` — i.e. it deliberately keeps the field a **32-bit N64 address**, unlike `PROMOTE` which produces a host pointer. `chr.c` cast it straight to `ModelNode *`, leaving `0x7070_xxxx`, and `&relatednode->Data->DisplayListCollisions` faulted. Fixed by routing through the canonical `PORT_N64PTR`. **Misleading comment, also corrected.** `bondtypes.h` claimed "chr.c casts back to ModelNode* at the use site", which is exactly the wrong conclusion and is what made the bare cast look intentional. **Verified.** `-level_39` x3 clean (previously a reliable repro), then 21/21 on the no-input sweep and 21/21 on the full-control sweep (every button + 8-way stick, 26 s/level). -**Scope — this does NOT close D120.** `chrCreateBloodStain` is D120's subject: `tools_pc/d43_emit.py`'s opcode-0x18 `ModelRoData_DisplayList_CollisionRecord` conversion is still incomplete, and the interim walk guards at `chr.c:3445,3469` (cap iterations at `numVertices+8`, bounds-check `index`) remain in place. Blood decals may still be missing or wrong. D416 fixes the crash only. +**Scope — this does NOT close D120.** `chrCreateBloodStain` is D120's subject: `tools_pc/d43_emit.py`'s opcode-0x18 `ModelRoData_DisplayList_CollisionRecord` conversion is still incomplete, and the interim walk guards at `chr.c:3445,3469` (cap iterations at `numVertices+8`, bounds-check `index`) remain in place. Blood decals may still be missing or wrong. MACOS-BLOODSTAIN fixes the crash only. **Lesson.** Both defects were invisible to a `-Wint-to-pointer-cast` census: defect 1 has no cast at all, and defect 2 was written as `(T *)(uintptr_t)x`, which silences the warning. A warning sweep finds the *producer* side of this class; the consumer side needs a type-driven audit of which 32-bit fields hold addresses. +**The two `vtxstore_allocate()` callers, and `objDeform()`, are the same class.** Defect 1's failure-signalling shape (`p = PORT_N64PTR(T, f()); if (p != NULL)`) appears at three more sites, all re-based through `PORT_N64PTR` so a returned `0` still becomes `NULL`: +- `src/game/chr.c:3609` and `:3626` — the two `chrCreateBloodStain()` callers (this finding's own function). `vtxstore_allocate()` returns `0` when the store is exhausted. +- `src/game/propobj.c` `objDeform()` — `newverts = PORT_N64PTR(Vertex, vtxstore_allocate(...))`, plus every read of the `*vtxslot` field (a `u32` N64 address) through `PORT_N64PTR(Vertex, *vtxslot)`, for the same reason the `CollisionRelatedNode` cast in defect 2 was wrong. +These are pointer-width/ABI only (semantics-preserving, identity at `PORT_ADDR_BASE == 0`); the `src/` hunks were carried under this finding's `#ifdef PORT` guard work and are called out here so they are not mistaken for behaviour changes. -## D417 — macOS/arm64: six pointer truncations found by census, not by crashing -**Method.** Rebuilt the whole compiled set with `-Wint-to-pointer-cast -Wpointer-to-int-cast -Wint-conversion`, giving 276 actionable sites, and triaged them in three disjoint file groups. This is the audit `porting-notes.md` §F has prescribed since D413 and which had never been run. +## MACOS-CAST-CENSUS — macOS/arm64: six pointer truncations found by census, not by crashing +**Method.** Rebuilt the whole compiled set with `-Wint-to-pointer-cast -Wpointer-to-int-cast -Wint-conversion`, giving 276 actionable sites, and triaged them in three disjoint file groups. This is the audit `porting-notes.md` §F has prescribed since MACOS-ADDR-WINDOW and which had never been run. **The dominant lesson: four of the six are the same shape.** GE's decomp contains many near-duplicate functions (solo vs multiplayer, primary vs secondary) that were decompiled independently, and a previous conversion pass fixed one twin while missing the other: | converted | missed | |---|---| @@ -15628,16 +15632,18 @@ if (newvertices != NULL) { ... newvertices[j] = ...; } /* test passes; sto So: **after landing any fix in this family, grep the repository for the distinctive fragment of the line you changed and confirm every other occurrence is already in the fixed form.** That one step would have found four of these six for free. **The most interesting one is silent.** `bgRoomCalcBB()` (`bg.c:5213`) truncated its loop bound, so on arm64 `vertices < bound` was false on the first iteration, the body never executed, `limits` kept its sentinel init, and lines 5242-5247 wrote `minbounds = pos + 0x7fff`, `maxbounds = pos - 0x7fff` — an inverted bounding box for **every room of every level**. No crash. Those bounds feed room centres (`bg.c:5155`), portal corners (`bg.c:1468`) and `chrprop.c:619`. Worth checking against open **D249** (distant geometry dropping out on Streets/Egyptian). **Not verified.** The two MP-only fixes (`mp_respawn_handler`'s stan pointer, and the intro-record walk) are unreachable from the solo sweeps and need a multiplayer respawn. +**Guarding status (honest note).** Only the `bondview2.c:8762` (`StandTile *start_stan`, `MACOS-CAST-CENSUS`) and `bg.c:5213` (`bgRoomCalcBB`) hunks are inside an `#ifdef PORT` block. The `intro_record` strides in `bondview_r.c`/`bondview2.c` are **unguarded**: the change is `(s32)` → `(uintptr_t)` in place, which is the identity under N64's 32-bit pointers and therefore semantics-preserving, matching the existing house style for these files (`main` already has unguarded `(uintptr_t)` pointer arithmetic in `bondview_r.c:392`, `bondview2.c:1374-1399`, `image.c:2406,2538`). The owner's branch ground rule is stricter (`#ifdef PORT` on every `src/` edit); converting these to guarded `#ifdef PORT` / `#else` pairs is left as a reviewer-flagged follow-up rather than done unilaterally. +**Terminator note (documented here because it landed in the same `chr.c` hunks).** While in `chr.c` this pass also added a `#ifdef PORT` `{0}` terminator to `crouched_rifle_firing_animation_group1`. Every other `*_firing_animation_groupN[]` is zero-terminated and `initResolveAnimGroupTable()` walks to that `0`; without one here the walk runs into the next global (`crouched_rifle_firing_animation_groupA`) and over-counts `len`. This is the same layout/linker-adjacency class as `main`'s `D420`/PR #120, which fixes the same table independently; the branch's copy is a `#ifdef PORT`-only guard and will be dropped in favour of the `main` fix when it merges. -## D418 — macOS/arm64: latent truncation in texReadUncompressed (unreachable on PC) +## MACOS-TEXALIGN — macOS/arm64: latent truncation in texReadUncompressed (unreachable on PC) `texReadUncompressed()` (`image.c`) aligned its destination through `u32` — `(u32 *)(((u32)dst + 0xf) & ~0xf)` plus `u16`/`u8` siblings — truncating a host pointer and then writing through it. On arm64 `dst` is `~0x1000_70xx_xxxx`, so the aligned pointers would be unbased `0x70xx_xxxx`. **It never fires.** A `GE_D303` probe over a full `-level_09` boot recorded **zero** calls: PC textures come from the pre-converted `pccg` sidecars, not runtime bitstream decode. Fixed at full pointer width anyway, because it would corrupt silently rather than fault if the path were ever reached. **Rest of the group-C census, for the record:** `init.c:114-118` writes through `RZIPLOADADDR` (`0x70200000`, a raw N64 address) but `init()` is dead on PC (`port/src/main.c` replaces the boot path; `port/src/n64stubs.c` documents it); `rsp.c`'s XOR double-buffer toggle already has a `PLATFORM_64BIT` explicit-toggle branch; `objecthandler_2.c`'s `name` is a `u8 *` parameter reused as a scratch integer (`(s32) name` everywhere, never dereferenced); `music.c`'s `romCopy` sources are re-truncated and re-based inside `piServiceDma`. The lead handed over from group B — `propobj.c:8893` — is **refuted**: `BaseAddr` is declared `void *`, so `(void *)(uintptr_t)BaseAddr` is a full-width no-op. -## D420 — PORT_ADDR_STRICT: catch this bug family at the conversion, not at the fault -**Why.** Every bug in the D414-D418 family reaches `portN64ToHost()` holding a value that is not a real N64 address — a truncated host pointer, or one never re-based. The *fault* then happens somewhere else entirely, often frames later, which is what made each of them expensive. Two of them (D414, D418) produced no crash at all. +## MACOS-ADDR-STRICT — PORT_ADDR_STRICT: catch this bug family at the conversion, not at the fault +**Why.** Every bug in the MACOS-SFX-BOUND through MACOS-TEXALIGN family reaches `portN64ToHost()` holding a value that is not a real N64 address — a truncated host pointer, or one never re-based. The *fault* then happens somewhere else entirely, often frames later, which is what made each of them expensive. Two of them (MACOS-SFX-BOUND, MACOS-TEXALIGN) produced no crash at all. **What it is.** `-DPORT_ADDR_STRICT=ON` (CMake option, **off by default, compiled out entirely**) makes `portN64ToHost()` validate its argument against the regions the port actually maps — cart+sidecars `0x10000000-0x1fffffff`, image-relative `0x40000000-0x6fffffff`, DRAM V1 `0x70000000-0x707fffff`, DRAM V2 `0x80000000-0x807fffff`, thread stacks `0xa0000000-0xbfffffff` — and reports anything else via `sysLogPrintf` plus a `backtrace()` naming the caller. Each distinct bad value is reported once, so a per-frame offender cannot bury the first hit. **Negative self-test.** `GE_ADDRSTRICT_SELFTEST=1` feeds the validator a deliberately-unmapped value so a run can prove the detector is live rather than merely silent. This caught a real mistake during development: the first test value (`0x12345678`) sits *inside* the cart range and was correctly accepted, making the detector look broken when it was working. -**Result.** Zero reports across `-level_09/33/39/22/54/37`, five of them driven by a full-control input script. That is a positive validation of the D414-D418 fixes, not merely an absence of evidence. +**Result.** Zero reports across `-level_09/33/39/22/54/37`, five of them driven by a full-control input script. That is a positive validation of the MACOS-SFX-BOUND through MACOS-TEXALIGN fixes, not merely an absence of evidence. **Known limitation.** The cart range is the full `0x10000000-0x1fffffff` window rather than the actual mapped extent (ROM ~12 MB + sidecars ~5 MB), so a truncated pointer that happens to land in that 256 MB span is not caught. Tightening it against the real `romSize` would narrow the blind spot. diff --git a/docs/porting-notes.md b/docs/porting-notes.md index a2c9223b..bbc241ff 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -585,26 +585,26 @@ through a converter or a runtime bswap fixup reads scrambled. are `.global foo` / `.set foo, address` on ELF/PE must become `.globl _foo` / `.set _foo, address`; Darwin also rejects the ELF `.section .data` spelling used by these symbol-only files. Generate transformed build-tree - copies rather than changing the cross-platform source files (D409). + copies rather than changing the cross-platform source files (MACOS-WEAK-ALIAS). - **Darwin GCC cannot lower ELF-style `#pragma weak alias = target`.** It can ICE in `assemble_alias`; compile generated copies with those pragmas removed and provide Mach-O indirect symbols (`.set _alias, _target`) separately - (D409). + (MACOS-WEAK-ALIAS). - **Mach-O's default `__PAGEZERO` reserves the entire low 4 GiB.** A build can compile and link successfully yet fail every fixed N64 mapping at runtime (`0x10000000`, `0x70000000`, `0x80000000`). Link with `-Wl,-pagezero_size,0x10000` so null-page pointers remain protected while - the cartridge and DRAM ranges are available (D409). + the cartridge and DRAM ranges are available (MACOS-WEAK-ALIAS). - **Darwin has no `MAP_FIXED_NOREPLACE`; a fixed-address hint can relocate.** Once `__PAGEZERO` is deliberately bounded below the cart range, use `MAP_FIXED` for the `0x10000000` cartridge mapping. Retaining the advisory hint silently falls back to a heap ROM and later crashes on direct absolute - ROM-symbol reads (D409). + ROM-symbol reads (MACOS-WEAK-ALIAS). - **AppKit event polling is main-thread-only.** This port has both a host-main `videoPumpEvents()` loop and fast3d's render-thread event pump. The latter must be disabled on macOS or `SDL_PollEvent` raises `NSInternalInconsistencyException`; the main-thread loop already handles - the complete event set (D409). + the complete event set (MACOS-WEAK-ALIAS). - `#include ` in a port `.c`/`.h` that also sees `` breaks: `OSContStatus`/`OSContPad` have a `u8 errno;` field vs errno.h's macro. libultra.c wraps the include in `#pragma push_macro("errno")` / @@ -1383,7 +1383,7 @@ pointers. **Native arm64 macOS cannot map below 4 GiB** (`__PAGEZERO`; a shrunken pagezero is SIGKILLed at exec; PIE is mandatory; `MAP_32BIT` fails). So the whole space lives at `PORT_ADDR_BASE = 0x1000_0000_0000` (16 TiB) instead: -`port/include/port_addr.h`, `port/src/port_addr.c` (D413). Because the base is +`port/include/port_addr.h`, `port/src/port_addr.c` (MACOS-ADDR-WINDOW). Because the base is 4 GiB-aligned, the **pointer→u32 direction is still free** (`(u32)host_ptr` is the N64 address), so only *reconstruction* sites — a 32-bit value becoming a pointer — need work. @@ -1430,25 +1430,25 @@ pointer — need work. `portN64ToHost` decodes it; `osVirtualToPhysical` returns it, fast3d's `seg_addr` decodes it. On Windows the image base is `0x140000000`, so this equals the raw `(u32)&sym` truncation the D131 fix relied on. -- Instances: D409 (`#pragma weak` has no Mach-O equivalent — use a global - symbol equate), D410 (the decomp's `include/` stubs shadow libc++'s wrapper - headers), D411 (`__x86_64__` used as the "64-bit PC" gate → `PLATFORM_64BIT`), - D412 (macOS platform branches: `shm_open`, Darwin `ucontext`, `_NSGetExecutablePath`), - D413 (the window + chokepoints + mappings), D414, D415. +- Instances: MACOS-WEAK-ALIAS (`#pragma weak` has no Mach-O equivalent — use a global + symbol equate), MACOS-LIBCXX-SHIMS (the decomp's `include/` stubs shadow libc++'s wrapper + headers), MACOS-64BIT-GATE (`__x86_64__` used as the "64-bit PC" gate → `PLATFORM_64BIT`), + MACOS-PLATFORM-BRANCHES (macOS platform branches: `shm_open`, Darwin `ucontext`, `_NSGetExecutablePath`), + MACOS-ADDR-WINDOW (the window + chokepoints + mappings), MACOS-SFX-BOUND, MACOS-PROPDEFS-TRUNC. - **Never bound an N64 address with an absolute number.** A guard like `if (p < 0x10000 || p >= 0x400000000ULL) return NULL;` is written against the *unshifted* layout: it is correct on Windows/Linux (DRAM at `0x7000_0000`, below 16 GiB) and silently rejects **every** real pointer on arm64 (DRAM at - `~0x1000_707b_5740`). D414 was exactly this in `sndPlaySfx()` — all in-level + `~0x1000_707b_5740`). MACOS-SFX-BOUND was exactly this in `sndPlaySfx()` — all in-level SFX were silent, while music (a separate player) still worked. Use `portAddrIsInWindow()` (`port_addr.h`) instead. - **`(u32)ptr` assigned back to a pointer is a truncation, not a no-op.** - `u32 *obj = (u32)g_CurrentSetup.propDefs;` (D415) round-trips on N64 but + `u32 *obj = (u32)g_CurrentSetup.propDefs;` (MACOS-PROPDEFS-TRUNC) round-trips on N64 but leaves an unbased N64 address on the 64-bit port, faulting on first deref. The port-correct form is a pointer-to-pointer cast (`(u32 *)…`), keeping the - pointer's own arithmetic. Grep for `ptr_var = (u32)expr`; D415 was the only + pointer's own arithmetic. Grep for `ptr_var = (u32)expr`; MACOS-PROPDEFS-TRUNC was the only instance. -- **Sweep by function PAIR, not by grep hit.** Four of the six bugs in the D417 +- **Sweep by function PAIR, not by grep hit.** Four of the six bugs in the MACOS-CAST-CENSUS census pass were cases where one member of a near-duplicate function pair had been converted and its twin had not (`model.c` `sub_GAME_7F06DEC0` vs `sub_GAME_7F06E540`; `bondview_r.c`'s solo spawn vs `bondview2.c`'s MP @@ -1463,12 +1463,12 @@ pointer — need work. pointers implicitly truncated *into* int parameters/locals — and in `front.c` + `options.c` all 206 such warnings were already-correct round trips. It does **not** flag the dangerous direction when the cast is explicit: `(T *)(uintptr_t)x` - (how D416 was written) is silent, and D414 (a numeric bound on an address) has + (how MACOS-BLOODSTAIN was written) is silent, and MACOS-SFX-BOUND (a numeric bound on an address) has no cast at all. Use the census to audit the invariant "every value reaching a `PORT_N64PTR` is either from `portHostToN64()` or provably in-window", but do not mistake a clean census for a clean codebase — the consumer side needs a - type-driven audit, or `PORT_ADDR_STRICT` (D420) at runtime. -- **A stage-unload bug needs input to surface.** Both sweeps that missed D415 + type-driven audit, or `PORT_ADDR_STRICT` (MACOS-ADDR-STRICT) at runtime. +- **A stage-unload bug needs input to surface.** Both sweeps that missed MACOS-PROPDEFS-TRUNC ran with no controller input, so the level never ended and `lvlUnloadStageTextData()` never ran. Drive `GE_INPUTSCRIPT` (START/A ends a level) when the affected path is teardown rather than steady-state. diff --git a/port/include/port_addr.h b/port/include/port_addr.h index 1b0297bf..f9adb079 100644 --- a/port/include/port_addr.h +++ b/port/include/port_addr.h @@ -74,7 +74,7 @@ extern int g_portUseImageRel; #ifdef PORT_ADDR_STRICT /* Debug gate (-DPORT_ADDR_STRICT=1). Validates that a u32 being turned into a * host pointer actually names one of the N64 regions the port maps. The whole - * D414/D415/D416/D417 bug family shares one signature -- a truncated or + * MACOS-SFX-BOUND/MACOS-PROPDEFS-TRUNC/MACOS-BLOODSTAIN/MACOS-CAST-CENSUS bug family shares one signature -- a truncated or * never-rebased address reaching portN64ToHost -- and it is almost always * diagnosed from a fault somewhere far away, long after the bad value was * created. This reports it AT THE CONVERSION, with a backtrace naming the @@ -99,7 +99,7 @@ void portAddrInit(void); * address of 0 and then testing the converted pointer for NULL -- * `p = PORT_N64PTR(T, f()); if (p != NULL) *p = ...` (e.g. * chrCreateBloodStain(), objDeform()). With base+0 that test passes and the - * very next store faults (D416). Returning NULL restores the decomp's + * very next store faults (MACOS-BLOODSTAIN). Returning NULL restores the decomp's * intended semantics. */ static inline void *portN64ToHost(uint32_t a) { diff --git a/port/src/n64stubs.c b/port/src/n64stubs.c index dcf69f73..02908e28 100644 --- a/port/src/n64stubs.c +++ b/port/src/n64stubs.c @@ -105,7 +105,7 @@ u8 (*tlbmanageGetTlbAllocatedBlock(void))[TLB_BLOCK_SIZE] * everything that isn't the fixed PERMANENT bank). Stays well clear of * animations_frame_buffer. * - * D413 (macOS/arm64): the same reclaim is not quite enough there — the + * MACOS-ADDR-WINDOW (macOS/arm64): the same reclaim is not quite enough there — the * first level load exhausts STAGE by ~0x30 bytes at a model rwdata alloc * (mempAllocBytesInBank spins). Push the ceiling to the last 0x10000 * before animations_frame_buffer, adding ~960 KiB to STAGE. Same diff --git a/port/src/port_addr.c b/port/src/port_addr.c index f2c7293c..57c1db00 100644 --- a/port/src/port_addr.c +++ b/port/src/port_addr.c @@ -72,7 +72,7 @@ static void portAddrSelfTest(void) /* * Strict address validation (-DPORT_ADDR_STRICT=1, off by default). * - * Every bug in the D414-D418 family reaches portN64ToHost() holding a value + * Every bug in the MACOS-SFX-BOUND through MACOS-TEXALIGN family reaches portN64ToHost() holding a value * that is not a real N64 address -- either a host pointer truncated to 32 bits * or an address that was never re-based. The fault then happens later, in * unrelated code, which is what made these expensive to find. This checks the @@ -134,7 +134,7 @@ void portAddrStrictCheck(uint32_t a) sysLogPrintf(LOG_ERROR, "PORT_ADDR_STRICT: 0x%08x is not a mapped N64 address " "(-> %p). Truncated host pointer, or an address that was " - "never re-based (D414-D418 class).", + "never re-based (MACOS-SFX-BOUND through MACOS-TEXALIGN class).", (unsigned)a, (void *)((uintptr_t)PORT_ADDR_BASE + (uintptr_t)a)); #if defined(PLATFORM_MACOS) || defined(__APPLE__) || defined(__linux__) { diff --git a/src/bondtypes.h b/src/bondtypes.h index 0067f15a..82901c3f 100644 --- a/src/bondtypes.h +++ b/src/bondtypes.h @@ -748,7 +748,7 @@ typedef union * modelPromoteNodeOffsetsToPointers rebases it via PROMOTE32, which is * `var = (u32)((u32)var + diff)` — note that unlike PROMOTE it keeps the * field a 32-bit N64 ADDRESS, it does NOT produce a host pointer. - * D416: consumers must therefore re-base it (PORT_N64PTR), never cast it + * MACOS-BLOODSTAIN: consumers must therefore re-base it (PORT_N64PTR), never cast it * straight to a pointer — a bare cast leaves 0x7070_xxxx and faults on * arm64, where the window is based at PORT_ADDR_BASE. * N64 layout unchanged (4B either way). */ diff --git a/src/game/bg.c b/src/game/bg.c index 4635ce9e..f3730327 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -5290,7 +5290,7 @@ void bgRoomCalcBB(s32 room) // canonical name limits.maxZ = -0x7fff; #ifdef PORT - /* D417: the induction variable is a full host pointer, so truncating the + /* MACOS-CAST-CENSUS: the induction variable is a full host pointer, so truncating the * bound to (s32) makes the comparison false on the very first iteration * once the window is based at PORT_ADDR_BASE -- the loop body never runs, * `limits` keeps its sentinel init, and every room ends up with an diff --git a/src/game/bg.h b/src/game/bg.h index 510499d4..766e56e9 100644 --- a/src/game/bg.h +++ b/src/game/bg.h @@ -20,7 +20,7 @@ struct levelentry // cannonical name #define PORTMAX 200 -/* D413/M2: the sum is an N64/window offset, so re-base it into the DRAM +/* MACOS-ADDR-WINDOW/M2: the sum is an N64/window offset, so re-base it into the DRAM * window before it becomes a pointer. Identity at PORT_ADDR_BASE == 0. */ #if defined(PORT) #include "port_addr.h" diff --git a/src/game/bondview2.c b/src/game/bondview2.c index f5ce758e..3418f80e 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -98,7 +98,7 @@ #if defined(VERSION_US) - /* D413/M2: the font-table globals are s32 holding truncated pointers + /* MACOS-ADDR-WINDOW/M2: the font-table globals are s32 holding truncated pointers * (D88 class); re-base on use. Identity at PORT_ADDR_BASE == 0. */ #if defined(PORT) #define BONDVIEW_2ND_FONTTABLE(_param) PORT_N64PTR(void, copy_2ndfonttable) @@ -1239,7 +1239,7 @@ void bondviewCalcIntroSwirlCamera(s32 index, f32 time, coord3d *pos, coord3d *lo struct SetupIntroSwirl *base; struct SetupIntroSwirl *loopbase; #if defined(PORT) - /* D189/D413: written pointbuf[0..11] via `&pointbuf[i*3]` then dst[3..5] + /* MACOS-ADDR-WINDOW: written pointbuf[0..11] via `&pointbuf[i*3]` then dst[3..5] * (i = -1..2), but declared [10] -- a 2-float stack overrun that on arm64 * corrupts the saved frame pointer (Fp=0x374d56eac1a00000 at the fault). * N64/MinGW absorb it; give it room on the port. */ @@ -1337,7 +1337,7 @@ void bondviewCalcIntroSwirlCamera(s32 index, f32 time, coord3d *pos, coord3d *lo lookat->y = g_CurrentPlayer->field_3C8; lookat->z = g_CurrentPlayer->field_3CC; - swirl = (void *)(((uintptr_t) g_IntroSwirl) + (uintptr_t) base); /* D413/M2: full-width */ + swirl = (void *)(((uintptr_t) g_IntroSwirl) + (uintptr_t) base); /* MACOS-ADDR-WINDOW/M2: full-width */ if (!(swirl->bitflags & 4)) { @@ -8968,7 +8968,7 @@ void mp_respawn_handler(void) coord3d start_pos = ZeroCoordSpawnPos; f32 start_look_angle; #ifdef PORT - /* D417: PadRecord.stan is a StandTile* host pointer. An s32 local + /* MACOS-CAST-CENSUS: PadRecord.stan is a StandTile* host pointer. An s32 local * truncates it, and every consumer below (bondviewYPositionRelated, * change_player_pos_to_target, prop->stan) rebuilds it unbased and * dereferences it. Local only -- no struct/ABI impact -- and the s32 pad @@ -10412,7 +10412,7 @@ Gfx* hudmsgBottomRender(Gfx* arg0) view_vert = view_top - view_top_offset; #ifdef PORT - /* D413/M2: the (s32) casts are an N64 match hack; on the port they + /* MACOS-ADDR-WINDOW/M2: the (s32) casts are an N64 match hack; on the port they * truncate stack pointers. Pass them at full width. */ arg0 = draw_blackbox_to_screen(arg0, &view_left, &view_vert, &view_horiz, &view_top); #else @@ -11430,7 +11430,7 @@ void sub_GAME_7F08BEEC(Mtxf *matrices, s32 count) for (i = 0, j = 0; i < count; i++, j += sizeof(Mtxf)) { - matrix_4x4_multiply_homogeneous(currentPlayerGetViewToWorldMtxf(), (Mtxf *)((uintptr_t)matrices + j), &sp40); /* D413/M2 */ + matrix_4x4_multiply_homogeneous(currentPlayerGetViewToWorldMtxf(), (Mtxf *)((uintptr_t)matrices + j), &sp40); /* MACOS-ADDR-WINDOW/M2 */ sp40.m[3][0] -= g_CurrentPlayer->current_model_pos.f[0]; sp40.m[3][1] -= g_CurrentPlayer->current_model_pos.f[1]; diff --git a/src/game/bondview_r.c b/src/game/bondview_r.c index 69a77f27..b8f327d2 100644 --- a/src/game/bondview_r.c +++ b/src/game/bondview_r.c @@ -394,7 +394,7 @@ void bondviewLoadSetupIntroSection(void) { rand_camera_index--; #ifdef PORT - /* D88/D413: prev is a u32 holding the truncated host pointer; re-base. */ + /* D88/MACOS-ADDR-WINDOW: prev is a u32 holding the truncated host pointer; re-base. */ ptr_random06cam_entry = PORT_N64PTR(struct SetupIntroCamera, ptr_random06cam_entry->prev); #else ptr_random06cam_entry = ptr_random06cam_entry->prev; diff --git a/src/game/chr.c b/src/game/chr.c index dcbcf3e1..0fb4a55b 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -33,7 +33,7 @@ #include "model.h" #include "tex.h" -/* D413/M2: animation_table_ptrs1/2 slots and vtxstore_allocate() results hold +/* MACOS-ADDR-WINDOW/M2: animation_table_ptrs1/2 slots and vtxstore_allocate() results hold * N64/window addresses as s32; re-base where they become pointers. Identity at * PORT_ADDR_BASE == 0. */ #if defined(PORT) @@ -897,7 +897,7 @@ 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) - /* D413: every other *_firing_animation_groupN[] is zero-terminated; + /* MACOS-ADDR-WINDOW: 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 @@ -3585,7 +3585,7 @@ after_opcode: #ifdef PORT /* PC port (D43/D45): CollisionRelatedNode is a raw vma (u32), not a pointer. - * D416: that vma is an N64 address (the port stores window pointers as + * MACOS-BLOODSTAIN: that vma is an N64 address (the port stores window pointers as * 32-bit values, and PORT_ADDR_BASE is 4 GiB-aligned, so the low 32 bits * ARE the N64 address), so it must be re-based like every other * address-carrying field -- a bare cast leaves it as 0x7070_xxxx and the diff --git a/src/game/chraction.c b/src/game/chraction.c index 8069297c..bcb306b0 100644 --- a/src/game/chraction.c +++ b/src/game/chraction.c @@ -45,7 +45,7 @@ #define PUN_ATTACK_ITEM(punchr, field) ((s32)(punchr)->field.attack_item) #endif -/* D413/M2: `(s32)&ANIM_DATA_x + (s32)&ptr_animation_table->data` is an N64 +/* MACOS-ADDR-WINDOW/M2: `(s32)&ANIM_DATA_x + (s32)&ptr_animation_table->data` is an N64 * address, and animation_table_ptrs1[] slots hold N64 addresses; re-base where * such a value becomes a pointer. Identity at PORT_ADDR_BASE == 0. */ #if defined(PORT) @@ -4996,7 +4996,7 @@ bool if_actor_able_set_on_path(ChrRecord *self, s32 pathid) { if (pathid && chrIsNotDeadOrShot(self)) { - /* D413/M2: callers pass a window PathRecord* truncated through this + /* MACOS-ADDR-WINDOW/M2: callers pass a window PathRecord* truncated through this * s32 param (chrai.c / aicommands.def); re-base on the read. */ set_actor_on_path(self, PORT_N64PTR(struct patrol_path, pathid)); return TRUE; diff --git a/src/game/cleanup_objects.c b/src/game/cleanup_objects.c index e607cedb..93287927 100644 --- a/src/game/cleanup_objects.c +++ b/src/game/cleanup_objects.c @@ -24,7 +24,7 @@ void cleanupObjects(s32 stage) { #ifdef PORT - /* D415: the decomp's `(u32)g_CurrentSetup.propDefs` converts the pointer + /* MACOS-PROPDEFS-TRUNC: 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 diff --git a/src/game/image.c b/src/game/image.c index 0ae73b0e..f9a176fb 100644 --- a/src/game/image.c +++ b/src/game/image.c @@ -1740,7 +1740,7 @@ void texReadAlphaBits(u8 *image,s32 count) s32 texReadUncompressed(u8 *dst, s32 width, s32 height, s32 format) { #ifdef PORT - /* D418: aligning a host pointer through u32 truncates it -- on arm64 dst is + /* MACOS-TEXALIGN: aligning a host pointer through u32 truncates it -- on arm64 dst is * ~0x1000_70xx_xxxx and these would become unbased 0x70xx_xxxx, then be * written through. Align at full pointer width instead. Identical result * where pointers are 32-bit. NOTE: currently unreachable on the PC port -- diff --git a/src/game/image_bank.c b/src/game/image_bank.c index 533ccf91..2a580723 100644 --- a/src/game/image_bank.c +++ b/src/game/image_bank.c @@ -21,7 +21,7 @@ s32 img_bitcount; s32 dword_CODE_bss_8008D0AC; //8008D0B0; #if defined(PORT) -uintptr_t globalbank_rdram_offset; /* D413/M2: host-based, see image_bank.h */ +uintptr_t globalbank_rdram_offset; /* MACOS-ADDR-WINDOW/M2: host-based, see image_bank.h */ #else s32 globalbank_rdram_offset; #endif @@ -98,7 +98,7 @@ extern u8* _GlobalimagetableSegmentRomStart; void texSetBitstring(s32 pos) { #if defined(PORT) - /* D413/M2: callers pass a host pointer through this s32 parameter, which + /* MACOS-ADDR-WINDOW/M2: callers pass a host pointer through this s32 parameter, which * truncates it. Because PORT_ADDR_BASE is 4 GiB-aligned the low 32 bits * are the N64/window offset, so re-base them. Identity at base 0. */ img_curpos = (u8 *)portN64ToHost((u32)pos); @@ -269,7 +269,7 @@ void texReset(void) #endif #if defined(PORT) - /* D413/M2: store a HOST-based offset. GIMG_OFF(sym) adds 0x02000000, so + /* MACOS-ADDR-WINDOW/M2: store a HOST-based offset. GIMG_OFF(sym) adds 0x02000000, so * `globalbank_rdram_offset + GIMG_OFF(sym)` is then a live host pointer. * `- 0x02000000` is the 64-bit equivalent of the original 32-bit * `+ 0xFE000000` wraparound (which relied on mod-2^32 cancellation). */ diff --git a/src/game/image_bank.h b/src/game/image_bank.h index 682edd56..aea00cbe 100644 --- a/src/game/image_bank.h +++ b/src/game/image_bank.h @@ -26,7 +26,7 @@ extern u8* img_curpos; extern s32 img_bitcount; extern s32 *pGlobalimagetable; #if defined(PORT) -/* D413/M2: holds a host-based RDRAM offset (pGlobalimagetable - 0x02000000) +/* MACOS-ADDR-WINDOW/M2: holds a host-based RDRAM offset (pGlobalimagetable - 0x02000000) * so `globalbank_rdram_offset + GIMG_OFF(sym)` is a live host pointer in the * DRAM window. s32 on N64 (see image_bank.c). */ extern uintptr_t globalbank_rdram_offset; diff --git a/src/game/initactorpropstuff.c b/src/game/initactorpropstuff.c index 289b60f7..5f9a6cfb 100644 --- a/src/game/initactorpropstuff.c +++ b/src/game/initactorpropstuff.c @@ -6,7 +6,7 @@ #include "math.h" #include "math_floor.h" -/* D413/M2: these tables store animation addresses as N64 values (s32/union +/* MACOS-ADDR-WINDOW/M2: these tables store animation addresses as N64 values (s32/union * offset) that are then cast to pointers. Re-base when the pointer is formed; * identity at PORT_ADDR_BASE == 0. */ #if defined(PORT) diff --git a/src/game/language.c b/src/game/language.c index f00bc562..99b9226b 100644 --- a/src/game/language.c +++ b/src/game/language.c @@ -11,7 +11,7 @@ #ifdef PORT #include "envflag.h" /* cached getenv for hot-path probes */ #endif -#include "port_addr.h" /* D413/M2: DRAM window base */ +#include "port_addr.h" /* MACOS-ADDR-WINDOW/M2: DRAM window base */ extern resource_lookup_data_entry resource_lookup_data_array[]; /* ob.c */ static void langFixupLoadedBank(char *name, void *p) @@ -23,7 +23,7 @@ static void langFixupLoadedBank(char *name, void *p) #endif /* g_LangBanks[] holds N64 DRAM addresses as s32 (they must fit s32, see - * memp.h / D413). Re-base only where a slot becomes a pointer; identity at + * memp.h / MACOS-ADDR-WINDOW). Re-base only where a slot becomes a pointer; identity at * PORT_ADDR_BASE == 0, so Windows/Linux are unchanged. */ #if defined(PORT) #include "port_addr.h" @@ -434,16 +434,11 @@ u8 * langGet(s32 slotID) * current flow has loaded. A bare `-level_XX` boot that reaches the * cast/credits text path (Cuba, bondviewRenderCredits, D76) hits a bank * slot that was never filled -> stale/garbage non-NULL value -> fault on - * the table read below. Reject anything that is not a plausible mapped - * DRAM address. D413/M2: the DRAM window is shifted by PORT_ADDR_BASE on - * macOS, so the bounds move with it (identity at PORT_ADDR_BASE == 0). */ - { - uintptr_t t = (uintptr_t)textbank_ptr; - uintptr_t lo = (uintptr_t)PORT_ADDR_BASE + 0x10000u; - uintptr_t hi = (uintptr_t)PORT_ADDR_BASE + 0x400000000ULL; - if (t < lo || t >= hi) { - return NULL; - } + * the table read below. Reject anything that is not a mapped N64 address. + * MACOS-ADDR-WINDOW: test against the real window (it is shifted by + * PORT_ADDR_BASE on macOS), not a hardcoded bound. */ + if (!portAddrIsInWindow(textbank_ptr)) { + return NULL; } #endif #ifdef PORT @@ -463,7 +458,7 @@ u8 * langGet(s32 slotID) u32 textslot_offset = textbank_ptr[slotID & 0x03FF]; /* load the textbank ptr table then get the slot's offset */ #if defined(PORT) - /* D413/M2: keep the bank base at full pointer width. */ + /* MACOS-ADDR-WINDOW/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 */ diff --git a/src/game/model.c b/src/game/model.c index bcac1696..e50d0375 100644 --- a/src/game/model.c +++ b/src/game/model.c @@ -14,7 +14,7 @@ #include "math_asinfacosf.h" #include "math_floor.h" -/* D413/M2: ModelAnimation.bitDescriptors/bitStream are u32 fields holding N64 +/* MACOS-ADDR-WINDOW/M2: ModelAnimation.bitDescriptors/bitStream are u32 fields holding N64 * addresses (D32: the blob base is added by expand_ani_table_entries). Re-base * where they become pointers; identity at PORT_ADDR_BASE == 0. */ #if defined(PORT) @@ -1043,7 +1043,7 @@ u16 modelAnimReadRootMotionValue(ModelAnimation *anim, s32 fieldIndex, s32 extra u8 bitsThisRead; result = 0; - desc = PORT_N64PTR(struct ModelAnimBitField, anim->bitDescriptors) + fieldIndex; // D32: u32 -> ptr (D413/M2 re-base) + desc = PORT_N64PTR(struct ModelAnimBitField, anim->bitDescriptors) + fieldIndex; // D32: u32 -> ptr (MACOS-ADDR-WINDOW/M2 re-base) bitsRemaining = desc->bitCount; if (bitsRemaining > 0) @@ -1051,7 +1051,7 @@ u16 modelAnimReadRootMotionValue(ModelAnimation *anim, s32 fieldIndex, s32 extra totalBitOffset = extraBitOffset + desc->bitOffset; byteIndex = totalBitOffset >> 3; totalBitOffset &= 7; - byteptr = PORT_N64PTR(u8, anim->bitStream) + byteIndex; // D32: u32 -> ptr (D413/M2 re-base) + byteptr = PORT_N64PTR(u8, anim->bitStream) + byteIndex; // D32: u32 -> ptr (MACOS-ADDR-WINDOW/M2 re-base) bitsThisRead = 8 - totalBitOffset; if (bitsRemaining >= bitsThisRead) @@ -6225,7 +6225,7 @@ s32 loadAnimationFrame(ModelAnimation* anim, s32 frame, ModelSkeleton* unused) s32 source; s32 frameSize; #if defined(PORT) - /* D413/M2: keep the buffer address at full pointer width (D59 buffer). */ + /* MACOS-ADDR-WINDOW/M2: keep the buffer address at full pointer width (D59 buffer). */ uintptr_t dest; #else u32 dest; diff --git a/src/game/othermodemicrocode.c b/src/game/othermodemicrocode.c index d2e37b83..7b8db55e 100644 --- a/src/game/othermodemicrocode.c +++ b/src/game/othermodemicrocode.c @@ -369,7 +369,7 @@ void texSelect(Gfx **gdlptr, struct sImageTableEntry *tconfig, u32 arg2, s32 arg texLoad((s32 *)tconfig, NULL); } - aa = PORT_N64PTR(u16, tconfig->index); /* D413/M2: index holds the pool address */ + aa = PORT_N64PTR(u16, tconfig->index); /* MACOS-ADDR-WINDOW/M2: index holds the pool address */ tex = texFindInPool((aa)[-4], NULL); if (tconfig->level == 0) diff --git a/src/mema.c b/src/mema.c index db760d76..28733b61 100644 --- a/src/mema.c +++ b/src/mema.c @@ -1,6 +1,6 @@ #include #if defined(PORT) -#include "port_addr.h" /* D413/M2: memaAlloc returns a DRAM address */ +#include "port_addr.h" /* MACOS-ADDR-WINDOW/M2: memaAlloc returns a DRAM address */ #endif #include "mema.h" #include "deb.h" @@ -318,7 +318,7 @@ void *memaAlloc(u32 amount) { } #if defined(PORT) - /* D413/M2: best->addr is an N64 DRAM address; hand back a live pointer. + /* MACOS-ADDR-WINDOW/M2: best->addr is an N64 DRAM address; hand back a live pointer. * Identity at PORT_ADDR_BASE == 0. */ return portN64ToHost((u32)addr); #else diff --git a/src/memp.c b/src/memp.c index 52ee19e4..eb17e86f 100644 --- a/src/memp.c +++ b/src/memp.c @@ -3,7 +3,7 @@ #include "memp.h" #include "game/language.h" #if defined(PORT) -#include "port_addr.h" /* portN64ToHost: DRAM window base (M2/D413) */ +#include "port_addr.h" /* portN64ToHost: DRAM window base (M2/MACOS-ADDR-WINDOW) */ #endif /** @@ -47,7 +47,7 @@ void mempCheckMemflagTokens(s32 poolAreaStart, s32 poolAreaSize) //set pool 0 to what boss wants (room_model_buffer) //pool 0 = TotalPoolArea #if defined(PORT) - /* D413/M2: the pool lives in the N64 DRAM window, which on macOS is + /* MACOS-ADDR-WINDOW/M2: the pool lives in the N64 DRAM window, which on macOS is * shifted up by PORT_ADDR_BASE. boss.c passes N64 addresses (the pool * must stay s32 for mempCheckMemflagTokens' arithmetic), so convert to * host pointers here — the MemoryPool fields are u8*. Identity at @@ -153,7 +153,7 @@ void mempSetBankStarts(s32 poolSizes[MEMPOOL_COUNT+1]) for (i = MEMPOOL_TOTAL; i < MEMPOOL_COUNT - 1; i++) { #if defined(PORT) - /* D413/M2: pool fields are u8* host pointers in the DRAM window. */ + /* MACOS-ADDR-WINDOW/M2: pool fields are u8* host pointers in the DRAM window. */ g_mempPools[i + 1].start = (u8 *)(mempStart + bankstarts[i]); g_mempPools[i + 1].pos = 0; g_mempPools[i + 1].end = (u8 *)(mempStart + bankstarts[i + 1]); diff --git a/src/snd.c b/src/snd.c index c1ca0f5f..d9dd756e 100644 --- a/src/snd.c +++ b/src/snd.c @@ -1110,7 +1110,7 @@ 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. */ { - /* D414 (macOS arm64): this guard predates the shifted-window + /* MACOS-SFX-BOUND (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 diff --git a/tools_pc/gen_findings_index.py b/tools_pc/gen_findings_index.py index 6df61941..8a9e5770 100644 --- a/tools_pc/gen_findings_index.py +++ b/tools_pc/gen_findings_index.py @@ -27,7 +27,14 @@ ROOT = Path(__file__).resolve().parent.parent SRC = ROOT / "docs" / "dev" / "findings.md" OUT = ROOT / "docs" / "dev" / "findings-index.csv" -LABEL_RE = re.compile(r"^D\d+(?:\.\d+)?(?:[-–]D\d+)?(?:\s*[·,]\s*D\d+(?:\.\d+)?)*\+?$") +# A finding label is either a D-number (`D12`, `D88.4`, `D24-D30`, `D75 · D77`) +# or a provisional descriptive tag used on an unmerged branch (`MACOS-*`). +# Real D-numbers are assigned when such a branch merges; the tag keeps the +# index complete in the meantime. +LABEL_TOKEN = r"(?:D\d+(?:\.\d+)?|[A-Z][A-Z0-9]+(?:-[A-Z0-9]+)+)" +LABEL_RE = re.compile( + rf"^{LABEL_TOKEN}(?:[-\u2013]{LABEL_TOKEN})?(?:\s*[·,]\s*{LABEL_TOKEN})*\+?$" +) STATUS_BUCKETS = [ ("FIXED", re.compile(r"\bfixed\b", re.I)), From 9e473faa6c14457427ad131e5da101a79cc56be3 Mon Sep 17 00:00:00 2001 From: Dan Turner Date: Sun, 4 Oct 2026 11:17:53 +0100 Subject: [PATCH 09/12] =?UTF-8?q?macos/arm64:=20review=20fixes=20=E2=80=94?= =?UTF-8?q?=20NULL-safe=20portAddrIsInWindow,=20drop=20duplicate=20MSAA=20?= =?UTF-8?q?clamp=20and=20wrong=20terminator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - port_addr.h: portAddrIsInWindow() now rejects the low 64 KiB of the window. With PORT_ADDR_BASE == 0 that range contains NULL; the previous version let NULL through on Windows/Linux, regressing langGet()'s cleared-bank guard (the old numeric bound rejected < 0x10000). Matches the old bounds on every platform. Self-test covers NULL, the low guard, DRAM and above-window. - gfx_opengl.cpp: revert to origin/macos. upstream already clamps MSAA to GL_MAX_SAMPLES (D405 / #106); the branch's second clamp and its comments were a duplicate. MACOS-MSAA-CLAMP finding removed. - chr.c: drop the branch's crouched_rifle_firing_animation_group1 {0} terminator. GCC places groupA below group1, so it never resolved groupA and did not fix the crouched-rifle crash. Replaced by #120 (next commit). - boss.c: guard the rspReplyMsg re-base with #ifdef PORT (the file has no PORT_N64PTR identity fallback for the N64 build). - findings.md: correct the objDeform vtxslot write-up (host pointers, not u32 N64 addresses), give accurate numbers for unguarded src/ edits, and record why the terminator was replaced. --- docs/dev/findings-index.csv | 1 - docs/dev/findings.md | 7 +++--- port/fast3d/gfx_opengl.cpp | 44 +------------------------------------ port/include/port_addr.h | 11 ++++++++-- port/src/port_addr.c | 8 +++++++ src/boss.c | 6 +++++ src/game/chr.c | 10 --------- 7 files changed, 27 insertions(+), 60 deletions(-) diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 608d6aab..5d06e463 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -297,4 +297,3 @@ MACOS-CAST-CENSUS,FIXED,"macOS/arm64: six more pointer truncations, found by a s MACOS-TEXALIGN,FIXED,"macOS/arm64: texReadUncompressed() aligned its destination through u32 ((u32 *)(((u32)dst + 0xf) & ~0xf) x3), truncating a host pointer and then writing throug…","FIXED defensively (image.c), latent not live." MACOS-RENDER-CRASH,OPEN,"Intermittent renderer crash, level-agnostic, observed once on Dam:","OPEN — observed once, not investigated." MACOS-ADDR-STRICT,FIXED,PORT_ADDR_STRICT: a debug gate that catches the whole MACOS-SFX-BOUND through MACOS-RENDER-CRASH family at the conversion rather than at the fault.,FIXED (new CMake option + port_addr.h/port_addr.c). -MACOS-MSAA-CLAMP,FIXED,MSAA=8 produced a completely BLACK screen —,FIXED (port/fast3d/gfx_opengl.cpp) — diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 657e9ad5..d9768361 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -705,7 +705,6 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | MACOS-TEXALIGN | **macOS/arm64: `texReadUncompressed()` aligned its destination through `u32`** (`(u32 *)(((u32)dst + 0xf) & ~0xf)` x3), truncating a host pointer and then writing through it — but the path is **unreachable on PC** (a `GE_D303` probe over a full level boot recorded zero calls; textures come from the pre-converted `pccg` sidecars) | FIXED defensively (`image.c`), latent not live. Group-C census otherwise clean: `init()` is dead on PC, `rsp.c`'s XOR toggle already has a `PLATFORM_64BIT` branch, `objecthandler_2.c`'s `name` is an int-in-a-pointer, `animationtable_data.h`'s 25 warnings are placeholders rewritten by `initResolveAnimTable()` via `ANI_ADDR`. | | MACOS-RENDER-CRASH | **Intermittent renderer crash, level-agnostic, observed once on Dam: SIGSEGV in `gfx_sp_tri1` -> `gfx_texture_cache` with a garbage fault address (`0x16d5bbd636173b9e`).** Did not reproduce in 8 subsequent Dam runs, and is not attributable to the MACOS-CAST-CENSUS changes landed in the same session. Not a known signature (D233 was `gfx_sp_tri1` but a visibility bug, not a crash) | **OPEN — observed once, not investigated.** Garbage (non-address-shaped) fault value suggests a corrupted texture-cache node or a race rather than the MACOS-SFX-BOUND through MACOS-TEXALIGN truncation family. Next: run with `PORT_ADDR_STRICT=ON` to rule the address family out, then instrument the texture cache. | | MACOS-ADDR-STRICT | **`PORT_ADDR_STRICT`: a debug gate that catches the whole MACOS-SFX-BOUND through MACOS-RENDER-CRASH family at the conversion rather than at the fault.** Off by default, compiled out entirely; when on, `portN64ToHost()` validates its argument against the mapped regions and reports offenders with a backtrace | FIXED (new CMake option + `port_addr.h`/`port_addr.c`). Negative self-test (`GE_ADDRSTRICT_SELFTEST=1`) proves the detector fires. Zero reports across 6 levels, 5 under full-control input. | -| MACOS-MSAA-CLAMP | **MSAA=8 produced a completely BLACK screen** — `GL_MAX_SAMPLES` is 4 on this GPU, and nothing ever clamped the requested level, so `glRenderbufferStorageMultisample` left the renderbuffer unallocated -> incomplete FBO. `video.c`'s own comment claims MSAA "snaps down to the highest supported level"; it did not | FIXED (`port/fast3d/gfx_opengl.cpp`) — clamp to `GL_MAX_SAMPLES`, log the snap once. Also fixed two index-vs-GL-name bugs in the same file (`resolve_msaa_color_buffer`, `copy_framebuffer`). Verified MSAA 1/2/4/8 all render. | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS @@ -15617,7 +15616,7 @@ if (newvertices != NULL) { ... newvertices[j] = ...; } /* test passes; sto **Lesson.** Both defects were invisible to a `-Wint-to-pointer-cast` census: defect 1 has no cast at all, and defect 2 was written as `(T *)(uintptr_t)x`, which silences the warning. A warning sweep finds the *producer* side of this class; the consumer side needs a type-driven audit of which 32-bit fields hold addresses. **The two `vtxstore_allocate()` callers, and `objDeform()`, are the same class.** Defect 1's failure-signalling shape (`p = PORT_N64PTR(T, f()); if (p != NULL)`) appears at three more sites, all re-based through `PORT_N64PTR` so a returned `0` still becomes `NULL`: - `src/game/chr.c:3609` and `:3626` — the two `chrCreateBloodStain()` callers (this finding's own function). `vtxstore_allocate()` returns `0` when the store is exhausted. -- `src/game/propobj.c` `objDeform()` — `newverts = PORT_N64PTR(Vertex, vtxstore_allocate(...))`, plus every read of the `*vtxslot` field (a `u32` N64 address) through `PORT_N64PTR(Vertex, *vtxslot)`, for the same reason the `CollisionRelatedNode` cast in defect 2 was wrong. +- `src/game/propobj.c` `objDeform()` — `newverts = PORT_N64PTR(Vertex, vtxstore_allocate(...))`, plus every read of `*vtxslot` through `PORT_N64PTR(Vertex, *vtxslot)`. `*vtxslot` is a `Vertex *` slot in the model RW pool that holds host pointers (`newverts`, `rodata->Vertices`), so the re-base is a lossless round trip for any pointer inside the window (`PORT_ADDR_BASE` is 4 GiB-aligned); it does not change behaviour and could be dropped, but it is harmless. These are pointer-width/ABI only (semantics-preserving, identity at `PORT_ADDR_BASE == 0`); the `src/` hunks were carried under this finding's `#ifdef PORT` guard work and are called out here so they are not mistaken for behaviour changes. ## MACOS-CAST-CENSUS — macOS/arm64: six pointer truncations found by census, not by crashing @@ -15632,8 +15631,8 @@ These are pointer-width/ABI only (semantics-preserving, identity at `PORT_ADDR_B So: **after landing any fix in this family, grep the repository for the distinctive fragment of the line you changed and confirm every other occurrence is already in the fixed form.** That one step would have found four of these six for free. **The most interesting one is silent.** `bgRoomCalcBB()` (`bg.c:5213`) truncated its loop bound, so on arm64 `vertices < bound` was false on the first iteration, the body never executed, `limits` kept its sentinel init, and lines 5242-5247 wrote `minbounds = pos + 0x7fff`, `maxbounds = pos - 0x7fff` — an inverted bounding box for **every room of every level**. No crash. Those bounds feed room centres (`bg.c:5155`), portal corners (`bg.c:1468`) and `chrprop.c:619`. Worth checking against open **D249** (distant geometry dropping out on Streets/Egyptian). **Not verified.** The two MP-only fixes (`mp_respawn_handler`'s stan pointer, and the intro-record walk) are unreachable from the solo sweeps and need a multiplayer respawn. -**Guarding status (honest note).** Only the `bondview2.c:8762` (`StandTile *start_stan`, `MACOS-CAST-CENSUS`) and `bg.c:5213` (`bgRoomCalcBB`) hunks are inside an `#ifdef PORT` block. The `intro_record` strides in `bondview_r.c`/`bondview2.c` are **unguarded**: the change is `(s32)` → `(uintptr_t)` in place, which is the identity under N64's 32-bit pointers and therefore semantics-preserving, matching the existing house style for these files (`main` already has unguarded `(uintptr_t)` pointer arithmetic in `bondview_r.c:392`, `bondview2.c:1374-1399`, `image.c:2406,2538`). The owner's branch ground rule is stricter (`#ifdef PORT` on every `src/` edit); converting these to guarded `#ifdef PORT` / `#else` pairs is left as a reviewer-flagged follow-up rather than done unilaterally. -**Terminator note (documented here because it landed in the same `chr.c` hunks).** While in `chr.c` this pass also added a `#ifdef PORT` `{0}` terminator to `crouched_rifle_firing_animation_group1`. Every other `*_firing_animation_groupN[]` is zero-terminated and `initResolveAnimGroupTable()` walks to that `0`; without one here the walk runs into the next global (`crouched_rifle_firing_animation_groupA`) and over-counts `len`. This is the same layout/linker-adjacency class as `main`'s `D420`/PR #120, which fixes the same table independently; the branch's copy is a `#ifdef PORT`-only guard and will be dropped in favour of the `main` fix when it merges. +**Guarding status.** Not every `src/` hunk on this branch is inside `#ifdef PORT`. Against `origin/macos` there are about 190 unguarded lines that call `PORT_N64PTR`/`portN64ToHost` inline. Every file that does so defines `PORT_N64PTR(T, x)` as `((T *)(x))` for the N64 build, so the N64 text compiles to the original cast. There are also about 50 unguarded `(s32)`/`(u32)` → `(uintptr_t)` widenings (`prop.c` x20, `bondview_r.c`/`bondview2.c` `intro_record` x20, `bg.c` x4, `music.c` x3, `image.c`, `cseq.c`), which are the identity where pointers are 32-bit. A few cast-only edits are also unguarded (`image.c:2478`, `options.c:3493`, `bondview2.c:7998`, `model.c:6251-6273`). `main` already uses this unguarded style in these files (`bondview_r.c:392`, `bondview2.c:1374-1399`, `image.c:2406,2538`), but the branch rule asks for `#ifdef PORT` with the N64 text unchanged. The open question for review is whether inline `PORT_N64PTR` with an identity N64 fallback counts as meeting that rule, or whether each site should become an `#ifdef PORT` / `#else` pair. +**Crouched-rifle table.** An earlier version of this branch added a `{0}` terminator to `crouched_rifle_firing_animation_group1`. It was wrong: GCC places `groupA` *below* `group1` (confirmed with `nm`), so the terminator never resolved `groupA` and the crouched-rifle `firing_animation_groups` crash remained. It has been replaced by a cherry-pick of `main`'s fix from PR #120 (`D420`, one array with `groupA` as an alias of `&group1[1]`). ## MACOS-TEXALIGN — macOS/arm64: latent truncation in texReadUncompressed (unreachable on PC) `texReadUncompressed()` (`image.c`) aligned its destination through `u32` — `(u32 *)(((u32)dst + 0xf) & ~0xf)` plus `u16`/`u8` siblings — truncating a host pointer and then writing through it. On arm64 `dst` is `~0x1000_70xx_xxxx`, so the aligned pointers would be unbased `0x70xx_xxxx`. diff --git a/port/fast3d/gfx_opengl.cpp b/port/fast3d/gfx_opengl.cpp index 9c433d56..c3881e12 100644 --- a/port/fast3d/gfx_opengl.cpp +++ b/port/fast3d/gfx_opengl.cpp @@ -1222,44 +1222,11 @@ static int gfx_opengl_create_framebuffer() { return i; } -/* video.c documents MSAA as "snaps down to the highest supported level", but - * nothing ever queried the limit: gfx_msaa_level went to - * glRenderbufferStorageMultisample() verbatim, and a request above - * GL_MAX_SAMPLES leaves the renderbuffer unallocated -> incomplete FBO -> - * a completely BLACK screen (MSAA=8 on Apple Silicon, where GL_MAX_SAMPLES - * is 4). Clamp here, where the GL limit actually matters. */ -static uint32_t gfx_opengl_clamp_msaa(uint32_t level) -{ - static GLint max_samples = 0; - static uint32_t warned_level = 0; - - if (level <= 1) { - return 1; - } - if (max_samples == 0) { - glGetIntegerv(GL_MAX_SAMPLES, &max_samples); - if (max_samples < 1) { - max_samples = 1; - } - } - if ((GLint)level > max_samples) { - if (warned_level != level) { - sysLogPrintf(LOG_WARNING, - "MSAA: %u samples requested but GL_MAX_SAMPLES is %d; using %d", - level, (int)max_samples, (int)max_samples); - warned_level = level; - } - return (uint32_t)max_samples; - } - return level; -} - static void gfx_opengl_update_framebuffer_parameters(int fb_id, uint32_t width, uint32_t height, uint32_t msaa_level, bool opengl_invert_y, bool render_target, bool has_depth_buffer, bool can_extract_depth) { Framebuffer& fb = framebuffers[fb_id]; - msaa_level = gfx_opengl_clamp_msaa(msaa_level); width = max(width, 1U); height = max(height, 1U); @@ -1373,12 +1340,7 @@ void gfx_opengl_resolve_msaa_color_buffer(int fb_id_target, int fb_id_source) { glBindFramebuffer(GL_READ_FRAMEBUFFER, fb_src.fbo); glBlitFramebuffer(0, 0, fb_src.width, fb_src.height, 0, 0, fb_dst.width, fb_dst.height, GL_COLOR_BUFFER_BIT, GL_NEAREST); - /* Restore whatever was bound before the resolve. `current_framebuffer` is - * an INDEX into framebuffers[], NOT a GL framebuffer name -- binding it - * directly named an unrelated GL object (or nothing) and left the wrong - * target bound for the rest of the frame. With MSAA on that produced a - * completely black screen. Use the same fb_id==0 -> GL name 0 rule as - * gfx_opengl_bind_framebuffer above. */ + /* current_framebuffer is an INDEX into framebuffers[], not a GL name; fb 0 is the window's default framebuffer (name 0). */ glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo); glEnable(GL_SCISSOR_TEST); } @@ -1443,10 +1405,6 @@ void gfx_opengl_copy_framebuffer(int fb_dst, int fb_src, int left, int top, bool glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, GL_COLOR_BUFFER_BIT, GL_NEAREST); - /* Same index-vs-name bug as the MSAA resolve above: for current_framebuffer - * == 0 this bound framebuffers[0].fbo, which is a generated but - * attachment-less FBO (incomplete on a strict GL implementation) -- the - * window's default framebuffer is GL name 0. */ glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo); glReadBuffer(GL_BACK); diff --git a/port/include/port_addr.h b/port/include/port_addr.h index f9adb079..ac2945ad 100644 --- a/port/include/port_addr.h +++ b/port/include/port_addr.h @@ -134,11 +134,18 @@ static inline uint32_t portHostToN64(const void *p) /* 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. */ + * silently rejects every real pointer there. + * + * The low PORT_ADDR_LOW_GUARD bytes of the window are rejected: nothing is + * mapped there, and with PORT_ADDR_BASE == 0 that range contains NULL, which + * is exactly what the callers' plausibility checks exist to catch (e.g. + * langGet() on a cleared bank). */ +#define PORT_ADDR_LOW_GUARD 0x10000u 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; + uintptr_t off = v - (uintptr_t)PORT_ADDR_BASE; + return off >= PORT_ADDR_LOW_GUARD && off < (uintptr_t)PORT_ADDR_WINDOW; } /* Cast-site helper for game code (the D3x ABI/layout class): reads a 32-bit diff --git a/port/src/port_addr.c b/port/src/port_addr.c index 57c1db00..bb201f15 100644 --- a/port/src/port_addr.c +++ b/port/src/port_addr.c @@ -58,6 +58,14 @@ static void portAddrSelfTest(void) portN64ToHost(0x10456789u) == (void *)(B + 0x10456789u)); ok &= portAddrCheck("cfb_16 at base+DRAM_V1", (uintptr_t)cfb_16 == B + 0x70000000u); + ok &= portAddrCheck("in-window: NULL rejected", + !portAddrIsInWindow(NULL)); + ok &= portAddrCheck("in-window: low guard", + !portAddrIsInWindow((void *)(B + PORT_ADDR_LOW_GUARD - 1u))); + ok &= portAddrCheck("in-window: DRAM V1", + portAddrIsInWindow((void *)(B + 0x70001234u))); + ok &= portAddrCheck("in-window: above window", + !portAddrIsInWindow((void *)(B + (uintptr_t)PORT_ADDR_WINDOW))); if (g_portUseImageRel) { ok &= portAddrCheck("host->n64 image", portHostToN64((void *)(g_portImageBase + 0x1234u)) == 0x40001234u); diff --git a/src/boss.c b/src/boss.c index b73b5330..4e5deac9 100644 --- a/src/boss.c +++ b/src/boss.c @@ -833,7 +833,13 @@ void bossMainloop(void) } rspReplyMsg = (s32)(&localGfxDoneMsg); +#ifdef PORT + /* MACOS-ADDR-WINDOW: rspReplyMsg is an s32 holding a + * truncated window pointer; re-base it. */ rspGfxTaskStart(firstGdl, gdl, 0, PORT_N64PTR(s32, rspReplyMsg)); +#else + rspGfxTaskStart(firstGdl, gdl, 0, (s32*)rspReplyMsg); +#endif pendingGfx++; memaSingleDefragPass(); diff --git a/src/game/chr.c b/src/game/chr.c index 0fb4a55b..f485351e 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -896,16 +896,6 @@ 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) - /* MACOS-ADDR-WINDOW: 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[] = { From 82e20c48159378bc0841a379b9598932e7605e02 Mon Sep 17 00:00:00 2001 From: dolent Date: Mon, 28 Sep 2026 20:30:44 -0700 Subject: [PATCH 10/12] fix: D420 -- crouched-rifle firing animation never resolved on PC (linker adjacency) -> SIGSEGV in MP matches crouched_rifle_firing_animation_group1 (chr.c) has one entry and no terminator; on the N64, .data is in source order, so crouched_rifle_firing_animation_groupA follows it and the single initResolveAnimGroupTable pass over group1 also resolves groupA (len 2). GCC places groupA below group1, so groupA[0] kept the raw anim offset 0xB84 (PTR_ANIM_fire_kneel_left_leg). playerTick (bondview2.c:11021) passes it to modelSetAnimation for a player's crouched rifle fire (firing_animation_groups[1][5]) -> SIGSEGV in modelConstrainOrWrapAnimFrame (fault 0xb88). Crouching rifle guards also saw len 1 instead of 2. #ifdef PORT: one array {right_leg, left_leg, terminator} (the N64 layout), groupA = &group1[1] (chr.h). ABI/layout exception, porting-notes D5 class (D164 precedent). Verified: 4-seat repro (M16, Complex, frame 5030) runs 5 min x3 SH-identical, no crash; gdb scan of all firing_animation_groups entries: 1 bad pointer before, 0 after; P1 corpus (1324 recordings) verdicts identical; solo/couch frames byte-identical; netplay 2P/4P identical. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 8595a06edd40ca22ac9ba4955b13a9ce61064475) (cherry picked from commit 8e5d4bcd5ad5e930178adc0cc4a87bf19e467e61) --- src/game/chr.c | 20 ++++++++++++++++++++ src/game/chr.h | 5 +++++ 2 files changed, 25 insertions(+) diff --git a/src/game/chr.c b/src/game/chr.c index f485351e..56022b7d 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -894,6 +894,25 @@ struct anim_group_info *ptr_doubles_firing_animation_groups[] = { &ptr_doubles_firing_animation_group1 }; +#ifdef PORT +/* D420 (ABI/layout, porting-notes §D5 linker adjacency): on the N64, + * crouched_rifle_firing_animation_group1 has NO terminator and the linker + * places crouched_rifle_firing_animation_groupA right after it, so the one + * resolver pass over group1 (initResolveAnimGroupTable, via + * ptr_crouched_rifle_firing_animation_groups) walks into groupA and turns its + * anim offset into a pointer too; the table len is 2. GCC does not keep the + * two arrays adjacent (x86-64: groupA BELOW group1), so groupA[0] kept the raw + * offset 0xB84 (PTR_ANIM_fire_kneel_left_leg) and playerTick handed it to + * modelSetAnimation as a ModelAnimation* (SIGSEGV, crouched rifle fire), and + * crouching rifle guards saw len 1 instead of 2. One array restores the N64 + * layout; groupA is its second entry (chr.h). Same data, same order. */ +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 }, + /* = crouched_rifle_firing_animation_groupA[0] */ + { PTR_ANIM_fire_kneel_left_leg, 24.0, 0, 0, 0, -1.0, 46.0, 98.0, -1.0, -1.0, 41.0, 98.0, 0.87266463, -0.52359879, 1.134464, -0.69813174, 1.6, 1.6 }, + {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} +}; +#else 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 }, }; @@ -902,6 +921,7 @@ struct weapon_firing_animation_table crouched_rifle_firing_animation_groupA[] = { PTR_ANIM_fire_kneel_left_leg, 24.0, 0, 0, 0, -1.0, 46.0, 98.0, -1.0, -1.0, 41.0, 98.0, 0.87266463, -0.52359879, 1.134464, -0.69813174, 1.6, 1.6 }, {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 anim_group_info ptr_crouched_rifle_firing_animation_group1 = { &crouched_rifle_firing_animation_group1, -1 }; diff --git a/src/game/chr.h b/src/game/chr.h index 03abfe61..2617ff1e 100644 --- a/src/game/chr.h +++ b/src/game/chr.h @@ -181,7 +181,12 @@ extern struct weapon_firing_animation_table doubles_firing_animation_group1[]; extern struct weapon_firing_animation_table doubles_firing_animation_group2[]; extern struct weapon_firing_animation_table doubles_firing_animation_group3[]; extern struct weapon_firing_animation_table crouched_rifle_firing_animation_group1[]; +#ifdef PORT +/* D420: groupA is the second entry of group1 (N64 adjacency, see chr.c). */ +#define crouched_rifle_firing_animation_groupA (&crouched_rifle_firing_animation_group1[1]) +#else extern struct weapon_firing_animation_table crouched_rifle_firing_animation_groupA[]; +#endif extern struct weapon_firing_animation_table crouched_rifle_firing_animation_group2[]; extern struct weapon_firing_animation_table crouched_rifle_firing_animation_group3[]; extern struct weapon_firing_animation_table crouched_pistol_firing_animation_group1[]; From 92986a07a94b60b8a714825c12937ff979eb0341 Mon Sep 17 00:00:00 2001 From: dolent Date: Tue, 29 Sep 2026 13:26:20 -0700 Subject: [PATCH 11/12] docs: D420 findings entry (crouched-rifle firing animation table: linker adjacency) Co-Authored-By: Claude Fable 5.1 (cherry picked from commit d954dfb581900eff9aa0dad58b337d6bc990b726) --- docs/dev/findings-index.csv | 1 + docs/dev/findings.md | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 5d06e463..1565ef92 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -285,6 +285,7 @@ D405,CLOSED,"fast3d/OpenGL: MSAA above the driver max black-screens; two glBindF D406,RESOLVED,front-end PC Options screen:,"RESOLVED (2026-09-28, port-layer only, Steam Deck-validated):" D407,PARTIAL,Tanks cannot be boarded/exited on PC —,"PARTIAL (fix landed in v0.4.0, port-only):" D408,OPEN,Game.SkipIntro skips the post-mission failure dossier:,"OPEN (cosmetic, not root-caused past the handoff site)." +D420,FIXED,Crouched-rifle firing animation (crouched_rifle_firing_animation_groupA) never resolved on PC -> SIGSEGV in modelConstrainOrWrapAnimFrame when an MP player cro…,"FIXED — layout only (§D5 linker adjacency, D164 class):" MACOS-WEAK-ALIAS,FIXED,macOS/arm64 (M0.4): #pragma weak X = Y has no Mach-O equivalent —,"FIXED (macOS port, lv.c/spectrum.c/objective_status.c) —" MACOS-LIBCXX-SHIMS,FIXED,macOS/arm64 (M0.6): the decomp's include/ stubs shadow libc++'s wrapper headers —,"FIXED (macOS port) — new port/shim/math.h + generated hostmath.h; the existing host{string,stdlib,stddef}.h.in now also pull libc++'s c++/v1/.h wrapper, guarded && defined(__cplusplus)." MACOS-64BIT-GATE,FIXED,"macOS/arm64 (M0.2): __x86_64__ was used as the ""64-bit PC"" gate —","FIXED — 22 sites in 14 src/+assets/ files and port/include/pc_protos.h now use PLATFORM_64BIT (defined by CMake on every 64-bit PC target, undefined on N64, so the N64 branch is unchanged)." diff --git a/docs/dev/findings.md b/docs/dev/findings.md index d9768361..87a3ec5a 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -693,6 +693,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D406 | **front-end PC Options screen: long sections (INPUT, 17 visible rows) run off the bottom of the 440x330 paper, and rows past the silent 14-row page cap (Crouch mode, Reset to defaults, Bindings…) are unreachable from the front screen** (user report 2026-09-28, v0.4.0 pre-push review) — full `## D406` entry at file tail | RESOLVED (2026-09-28, port-layer only, Steam Deck-validated): in-section pagination in `frontoptions.c` (11 rows/page; the vertical stepper crosses page boundaries both ways; dim 'Page p/N' marker + bottom hint on every page; slider bars start past long labels; F10 overlay untouched). D406b/c/d same-cycle refinements (label/track overlap, hint visibility/wording, hint position). | | D407 | **Tanks cannot be boarded/exited on PC — the v0.4.0 use/reload split (D378/D393) removed the B-button tap the engine's tank handlers in `bondview2.c` consume (user report 2026-09-28, Runway/Streets)** — full `## D407` entry at file tail | PARTIAL (fix landed in v0.4.0, port-only): present `GE_CONT_B` on the use (E / pad A) rising edge only while `g_PlayerIsInTank == 1` or `g_BondCanEnterTank != 0`; E keeps its D378 no-reload-fallback semantics elsewhere, N64 layout unaffected. D407(b) same cycle: front PC Options page-edge highlight clamp + mouse-wheel / W-S paging (wheel queue consumed on the menu, D223); its wheel mapping shipped inverted and was fixed to match W/S (wheel up = step up). D407(c): board-animation lockout, menu-accept B gate in tank states, in-tank aim routed through the legacy velocity stick + `Input.TankAimScale` knob. RESOLVED 2026-09-28: all port-only (src/game zero-diff), 10 TANKDBG probes stripped, release binary verified clean, user live tank drive signed off (board + exit OK, aim feel good). | | D408 | **`Game.SkipIntro` skips the post-mission failure dossier: with it on, entering a level then aborting (watch Z+A) or dying (KIA) returns straight to the menus — no REPORT / "Mission status: KILLED IN ACTION / ABORTED" screen** (user report 2026-09-28; save/AllUnlocked ruled out) — full `## D408` entry at file tail | OPEN (cosmetic, not root-caused past the handoff site). v0.4.0 decision: SkipIntro stays EXPERIMENTAL / not recommended for regular users — F10 row relabelled "Skip intro (EXPERIMENTAL)". | +| D420 | **Crouched-rifle firing animation (`crouched_rifle_firing_animation_groupA`) never resolved on PC -> SIGSEGV in modelConstrainOrWrapAnimFrame when an MP player crouch-fires a rifle (2026-09-28).** — full `## D420` entry at file tail | FIXED — layout only (§D5 linker adjacency, D164 class): `crouched_rifle_firing_animation_group1` has no zero terminator and on N64 falls through into `...groupA` (source-order .data); GCC places groupA first, so groupA[0] kept the raw anim offset 0xB84. `#ifdef PORT` one array {right, left, terminator} + `groupA` alias (chr.c/chr.h); N64 resolution restored. | | MACOS-WEAK-ALIAS | **macOS/arm64 (M0.4): `#pragma weak X = Y` has no Mach-O equivalent — clang errors "aliases are not supported on darwin", and the pragma form silently emits a TU-local symbol instead** — full `## MACOS-WEAK-ALIAS` entry at file tail | FIXED (macOS port, `lv.c`/`spectrum.c`/`objective_status.c`) — replaced with a `#if defined(PORT) && defined(__APPLE__)` global symbol equate (`.globl _X` / `_X = _Y`); Win/Linux keep the pragma. Verified: 3 TUs compile, aliases are global `S`/`D`/`T` symbols. | | MACOS-LIBCXX-SHIMS | **macOS/arm64 (M0.6): the decomp's `include/` stubs shadow libc++'s wrapper headers — ``/``/``/`` hard-error, and `acosf`/`fmodf`/`fabsf` are undeclared** — full `## MACOS-LIBCXX-SHIMS` entry at file tail | FIXED (macOS port) — new `port/shim/math.h` + generated `hostmath.h`; the existing `host{string,stdlib,stddef}.h.in` now also pull libc++'s `c++/v1/.h` wrapper, guarded `&& defined(__cplusplus)`. `limits.h`/`assert.h` needed no shim. All 186 TUs compile. | | MACOS-64BIT-GATE | **macOS/arm64 (M0.2): `__x86_64__` was used as the "64-bit PC" gate — on arm64 it silently selected the N64 branch (34 undefined `_ANIM_DATA_*`, objecthandler `.bss` overrun, audio heap sizing)** — full `## MACOS-64BIT-GATE` entry at file tail | FIXED — 22 sites in 14 `src/`+`assets/` files and `port/include/pc_protos.h` now use `PLATFORM_64BIT` (defined by CMake on every 64-bit PC target, undefined on N64, so the N64 branch is unchanged). | @@ -15537,6 +15538,43 @@ included; D281 keeps the shipped mitigation + disclosure, zero-saves root cause **Status:** OPEN — cosmetic (no crash, no data loss; the mission still records as failed/aborted and the menus are fully functional). Root-causing needs a side-by-side of front-end state (`current_menu`, `g_CurrentStageToLoad`, `mission_failed_or_aborted`, `g_isBondKIA`) on the fail return with SkipIntro off vs on — candidate for v0.5.0 alongside the D216 audio half. +## D420 — crouched-rifle firing animation never resolved on PC (linker adjacency) -> MP crash (2026-09-28) + +**Symptom.** SIGSEGV in `modelConstrainOrWrapAnimFrame` from `playerTick` +(bondview2.c ~11021) with `anim = 0xb84`, when a multiplayer player +crouch-fires a rifle (found by the 5PLUS bot runs: Complex, 4 seats, M16, +frame 5030; present in the 0.9.x friend bundles). Found on +`exp/5plus-multiplayer` as "D341" (renamed D420 here: D341 is upstream's). + +**Root cause (layout, porting-notes §D5 "linker adjacency", D164 class).** +`firing_animation_groups[1][5]` points at `crouched_rifle_firing_animation_groupA` +(chr.c). The tables hold anim *offsets*; `initWeaponAnimGroups` -> +`initResolveAnimGroupTable` rebases each table up to its zero terminator. +`crouched_rifle_firing_animation_group1` has ONE entry and **no terminator**: +on N64 .data is laid out in source order, so resolving group1 walks on into +groupA (resolving it too, len 2). GCC placed groupA *below* group1, so +groupA[0] kept the raw offset `PTR_ANIM_fire_kneel_left_leg` (0xB84) and +`modelSetAnimation` received it as a `ModelAnimation*`. Side effect before +the fix: crouching rifle guards saw len 1 instead of the N64's 2. + +**Not covered upstream:** upstream D392 (chraction.c firing-table fields) +and D336 (bondhead.c) are different sites; chr.c/chr.h were unchanged on +09278407. + +**Fix (8595a06e, `#ifdef PORT`, `#else` = decomp):** chr.c one array +`{kneel_right_leg, kneel_left_leg, terminator}` (exactly the N64 layout); +chr.h `#define crouched_rifle_firing_animation_groupA +(&crouched_rifle_firing_animation_group1[1])`. Resolution and len are the +N64's; no logic change. Upstream candidate (a real port bug). + +**Verified (programmer, 5plus + main):** repro (4 seats, bots, Complex, +pinned seed, 18 078 frames x3) no crash, SH identical across runs; 8 seats +remote mines no crash; every firing/struck anim table in chr.c scanned -- +this was the only unterminated one; gdb scan of all 24 +`firing_animation_groups` entries at run time: 1 bad pointer before, 0 after +(also 0 on the main-line cherry-pick); P1 gate (1 324 recordings, frame +dumps) unchanged; netplay 2P/4P identical. + ## MACOS-WEAK-ALIAS — macOS/arm64 (M0.4): `#pragma weak X = Y` has no Mach-O equivalent **Context.** The decomp uses GCC's `#pragma weak alias = target` in five TUs to create a weak symbol alias: `src/libultra/gu/sinf.c` + `cosf.c` (`fsin/sinf = __sinf`, `fcos/cosf = __cosf`), `src/game/objective_status.c` (`objectiveGetStatus_WEAK = get_status_of_objective`), `src/game/lv.c` (`g_DebugPortalsInputBufferSource1..4 = g_DebugPortalsInputBuffer1..4`), `src/game/spectrum.c` (`spec_keyboard_row_z_c_v = spec_keyboard_buffer`). On ELF/MinGW this is a normal weak alias. On Darwin/Mach-O it is not: - `__attribute__((weak, alias("target")))` → hard error: `aliases are not supported on darwin`. From 7b08a04f54da78c08455f74efc01f917964dc654 Mon Sep 17 00:00:00 2001 From: Dan Turner Date: Sun, 4 Oct 2026 11:21:37 +0100 Subject: [PATCH 12/12] ci: macOS arm64 build + port_addr self-test on PRs into macos Adds a GitHub-hosted macos-15 (Apple Silicon) workflow for pushes and PRs into the macos branch: build with Homebrew GCC, once with PORT_ADDR_STRICT off and once on, then run the ROM-free address-model self-test. - main.c: new --selftest flag, handled right after portAddrInit() and before crashInit/config/ROM/SDL. Exits 0 if the self-test passed, 1 if not. Listed in --help. - port_addr.c/.h: portAddrSelfTest() now returns its result and portAddrSelfTestPassed() exposes it; no change to normal runs. - macos.yml: contents: read only, no secrets, no ROM, persist-credentials off, so it is safe for fork PRs. The strict entry also requires the GE_ADDRSTRICT_SELFTEST report, proving the validator is live. - docs: building.md CI note; plan M4 row no longer lists macOS CI as outstanding (ROM sweeps stay with the self-hosted job in #131). --- .github/workflows/macos.yml | 85 ++++++++++++++++++++++++++++++++++++ docs/building.md | 17 ++++++++ docs/dev/MACOS-ARM64-PLAN.md | 2 +- port/include/port_addr.h | 4 ++ port/src/main.c | 6 +++ port/src/port_addr.c | 12 ++++- 6 files changed, 123 insertions(+), 3 deletions(-) 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..1c3552d3 --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,85 @@ +name: macOS + +on: + push: + branches: [ macos ] + pull_request: + branches: [ macos ] + workflow_dispatch: + +# Minimal token: this job only reads the repo. It runs on GitHub-hosted +# runners only, never receives secrets, and never sees a ROM, so it is safe +# for fork PRs (untrusted PR code must not reach a box that holds a ROM). +permissions: + contents: read + +concurrency: + group: macos-${{ github.ref }} + cancel-in-progress: true + +# What this proves: the native Apple Silicon (arm64) port BUILDS and LINKS +# with Homebrew GCC, and the ROM-free address-model self-test passes +# (`ge007.aarch64 --selftest`: window round-trips, absolute-symbol base, +# image-relative encoding, portAddrIsInWindow). It does NOT run the game. +# +# Matrix: +# strict=OFF the default build. +# strict=ON -DPORT_ADDR_STRICT=ON; additionally proves the validator is +# live by feeding it a deliberately unmapped address +# (GE_ADDRSTRICT_SELFTEST=1) and requiring its report. +# +# Level runs need a ROM, so they live elsewhere: the self-hosted sweep +# (`platform=macos`, tools_pc/level_sweep_mac.sh) from #131, on a runner with +# its own ROM that never runs PR code. + +jobs: + build: + runs-on: macos-15 + strategy: + fail-fast: false + matrix: + strict: [ OFF, ON ] + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 500M + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Install dependencies + run: brew install cmake gcc sdl2 ccache + + - name: Restore ccache + uses: actions/cache@v4 + with: + path: ${{ env.CCACHE_DIR }} + key: ccache-macos-${{ matrix.strict }}-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }} + restore-keys: | + ccache-macos-${{ matrix.strict }}-${{ hashFiles('CMakeLists.txt') }}- + ccache-macos-${{ matrix.strict }}- + + - name: Regenerate ROM-asset symbol file (US) + run: python3 scripts/gen_romassets.py u + + - name: Configure (ntsc-final) + run: cmake -S . -B build-pc -DROMID=ntsc-final -DPORT_ADDR_STRICT=${{ matrix.strict }} + + - name: Build + run: cmake --build build-pc -j"$(sysctl -n hw.ncpu)" + + - name: ccache stats + run: ccache -s + + - name: Version + run: ./build-pc/ge007.aarch64 --version + + - name: Address-model self-test + run: ./build-pc/ge007.aarch64 --selftest + + - name: PORT_ADDR_STRICT detector is live + if: matrix.strict == 'ON' + run: | + set -o pipefail + GE_ADDRSTRICT_SELFTEST=1 ./build-pc/ge007.aarch64 --selftest 2>&1 | tee selftest.log + grep -F 'PORT_ADDR_STRICT: 0x30000000 is not a mapped N64 address' selftest.log diff --git a/docs/building.md b/docs/building.md index ee7d4eeb..b1bbc7ea 100644 --- a/docs/building.md +++ b/docs/building.md @@ -213,6 +213,23 @@ GE_ADDRSTRICT_SELFTEST=1 ./build-strict/ge007.aarch64 -level_09 See findings MACOS-SFX-BOUND through MACOS-ADDR-STRICT for the bug family this exists to catch. +### macOS CI and the ROM-free self-test + +`.github/workflows/macos.yml` runs on GitHub-hosted Apple Silicon for pushes and +PRs into the `macos` branch. It builds with Homebrew GCC (once with +`PORT_ADDR_STRICT` off, once on) and runs the address-model self-test, which +needs no ROM: + +```sh +./build-pc/ge007.aarch64 --selftest # exit 0 = pass, 1 = fail +``` + +The self-test covers the window round-trips, the generated absolute-symbol base, +the image-relative encoding and `portAddrIsInWindow()`. With a `PORT_ADDR_STRICT` +build, `GE_ADDRSTRICT_SELFTEST=1` additionally proves the validator fires. Level +runs need a ROM and are covered by the self-hosted sweep +(PR #131, `tools_pc/level_sweep_mac.sh`), not by this job. + ### Packaging a macOS app ```sh diff --git a/docs/dev/MACOS-ARM64-PLAN.md b/docs/dev/MACOS-ARM64-PLAN.md index 5c471a58..73305388 100644 --- a/docs/dev/MACOS-ARM64-PLAN.md +++ b/docs/dev/MACOS-ARM64-PLAN.md @@ -581,7 +581,7 @@ Record as a `Dxx`. | M1 | **done** | shifted-window address model (MACOS-ADDR-WINDOW); self-test ALL PASS on the #88 base | | 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 | +| M4 | **done (core)** | `tools_pc/bundle-mac.sh` → signed, double-clickable `GoldenEye.app` (+ zip/sha256); `docs/building.md` macOS section + `tools_pc/README.md`. CI: GitHub-hosted build + `--selftest` (`.github/workflows/macos.yml`); ROM sweeps via the self-hosted job (#131). Outstanding: 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 diff --git a/port/include/port_addr.h b/port/include/port_addr.h index ac2945ad..85c8ac5c 100644 --- a/port/include/port_addr.h +++ b/port/include/port_addr.h @@ -86,6 +86,10 @@ void portAddrStrictCheck(uint32_t a); * any of the port's fixed mappings and before large allocations. */ void portAddrInit(void); +/* Result of the ROM-free self-test portAddrInit() ran (1 = all checks passed). + * Used by `--selftest` so CI can gate on it without a ROM. */ +int portAddrSelfTestPassed(void); + #ifdef __cplusplus } #endif diff --git a/port/src/main.c b/port/src/main.c index b375b6f6..3776c8df 100644 --- a/port/src/main.c +++ b/port/src/main.c @@ -68,6 +68,8 @@ static void portPrintHelp(const char *argv0) printf("\nusage: %s [options] [-level_XX]\n\n" " --help this message\n" " --version build id only\n" + " --selftest run the ROM-free address-model self-test and\n" + " exit (0 = pass, 1 = fail); needs no ROM\n" " -fresh wipe playtest data + config before starting\n" " (removes ge007.eep save and ge007.ini; the\n" " ini is re-written with defaults on exit)\n" @@ -109,6 +111,10 @@ int main(int argc, char **argv) * failure here is reported before SDL/GL are involved. */ portAddrInit(); + if (sysArgCheck("--selftest")) { + return portAddrSelfTestPassed() ? 0 : 1; + } + /* Crash handler first, so any failure below is debuggable. */ crashInit(); diff --git a/port/src/port_addr.c b/port/src/port_addr.c index bb201f15..13c9dd32 100644 --- a/port/src/port_addr.c +++ b/port/src/port_addr.c @@ -42,7 +42,7 @@ static int portAddrCheck(const char *what, int ok) return ok; } -static void portAddrSelfTest(void) +static int portAddrSelfTest(void) { const uintptr_t B = (uintptr_t)PORT_ADDR_BASE; int ok = 1; @@ -74,6 +74,14 @@ static void portAddrSelfTest(void) } sysLogPrintf(ok ? LOG_INFO : LOG_ERROR, "portAddr: selftest %s", ok ? "ALL PASS" : "FAILURES"); + return ok; +} + +static int g_portAddrSelfTestOk = 0; + +int portAddrSelfTestPassed(void) +{ + return g_portAddrSelfTestOk; } #ifdef PORT_ADDR_STRICT @@ -199,7 +207,7 @@ void portAddrInit(void) (unsigned long long)PORT_ADDR_BASE, (unsigned long long)g_portImageBase, g_portUseImageRel); - portAddrSelfTest(); + g_portAddrSelfTestOk = portAddrSelfTest(); #ifdef PORT_ADDR_STRICT portAddrStrictSelfTest();