diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03981458..5122ba47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,11 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: true -# Three jobs: +# Jobs: +# +# linux-build — ubuntu, system GCC. Actually compiles + links (unlike +# validate). Surfaces GCC-on-Linux strictness gaps MinGW +# misses. Not a runtime claim; not a required check yet. # # validate — ubuntu, configure-only. Byte-compiles the tooling, # regenerates the US ROM-asset symbol file, and CMake- @@ -108,6 +112,47 @@ jobs: - name: CMake configure (ntsc-final) run: cmake -S . -B build-ntsc -DROMID=ntsc-final + # Linux (Ubuntu, system GCC) compile job. Unlike `validate` this actually + # runs `cmake --build`, so it surfaces the GCC-on-Linux strictness gaps that + # MinGW does not flag. The port has never been run on Linux — a green build + # here is a "does it compile + link" signal only, not a runtime claim. + # Not a required check yet; flip it to required once it is reliably green. + # + # Toolchain: ubuntu-24.04 + gcc-14. The port is developed on MinGW GCC 16; + # pinning a recent GCC here keeps the first bring-up focused on real + # portability gaps rather than a decade of accumulated old-GCC pedantry. + # (A "does it build on the GCC a typical distro ships" pass can come later.) + linux-build: + runs-on: ubuntu-24.04 + env: + CC: gcc-14 + CXX: g++-14 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential gcc-14 g++-14 cmake python3 \ + libsdl2-dev zlib1g-dev libgl1-mesa-dev + + - name: Regenerate ROM-asset symbol file (US) + run: python3 scripts/gen_romassets.py u + + - name: Configure (ntsc-final) + run: cmake -S . -B build-linux -DROMID=ntsc-final + -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 + + - name: Build (ntsc-final) + run: cmake --build build-linux -j"$(nproc)" + + - name: Show binary + run: | + ls -la build-linux/ge007* || true + file build-linux/ge007* || true + # Real Windows (MSYS2 MINGW64) build — the only platform the port is # developed and tested on. This compiles and LINKS the full executable. # diff --git a/CMakeLists.txt b/CMakeLists.txt index 50dd3e56..eaee9923 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,11 +52,25 @@ include(TargetArch) include(FindPkgConfig) set(CMAKE_C_STANDARD 11) -set(CMAKE_C_EXTENSIONS OFF) +# GNU extensions ON: strict -std=c11/-std=c++20 define __STRICT_ANSI__, which +# disables glibc's _DEFAULT_SOURCE and hides POSIX symbols the port needs +# (clock_gettime/CLOCK_REALTIME in port/src/libultra.c, mmap flags, etc.). +# -std=gnu11/gnu++20 is GCC's own default and what MinGW was already using. +set(CMAKE_C_EXTENSIONS ON) set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_EXTENSIONS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +# Opt-in ccache launcher. The game sources are frozen (1:1 with the decomp), +# so ccache makes branch-switch / worktree rebuilds nearly free. No-op when +# ccache is not installed. +find_program(CCACHE_PROGRAM ccache) +if(CCACHE_PROGRAM) + set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") + set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") + message(STATUS "ccache enabled: ${CCACHE_PROGRAM}") +endif() + # Default build type if(NOT CMAKE_BUILD_TYPE AND NOT EXISTS ${CMAKE_BINARY_DIR}/CMakeCache.txt) set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "" FORCE) @@ -148,6 +162,15 @@ configure_file( "${CMAKE_SOURCE_DIR}/port/include/hoststring.h.in" "${CMAKE_BINARY_DIR}/port/include/hoststring.h" @ONLY) +# Host path for port/shim/sched.h. The decomp's src/sched.h is on +# 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) +configure_file( + "${CMAKE_SOURCE_DIR}/port/include/hostsched.h.in" + "${CMAKE_BINARY_DIR}/port/include/hostsched.h" @ONLY) + # Host path for port/shim/stdlib.h. The N64 include/stdlib.h # typedefs lldiv_t/ldiv_t, which conflict with the host's when both land in # one C++ TU (fast3d): MinGW's own sec_api/stdlib_s.h does `#include @@ -160,7 +183,17 @@ configure_file( # Host path for port/shim/stddef.h (same reason as stdlib: the N64 # stub shadows the host header in C++ TUs, hiding max_align_t from cstddef). -get_filename_component(PORT_HOST_STDDDEF_H "${_PORT_GCC_DIR}/../include/stddef.h" ABSOLUTE) +# stddef.h is compiler-internal, not in bin/../include: on a native Linux GCC +# it lives under lib/gcc///include. Ask the compiler where it is; +# fall back to the MinGW bin/../include layout. +execute_process( + COMMAND "${CMAKE_C_COMPILER}" -print-file-name=include/stddef.h + OUTPUT_VARIABLE _PORT_GCC_STDDEF OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) +if(_PORT_GCC_STDDEF AND EXISTS "${_PORT_GCC_STDDEF}") + get_filename_component(PORT_HOST_STDDDEF_H "${_PORT_GCC_STDDEF}" ABSOLUTE) +else() + get_filename_component(PORT_HOST_STDDDEF_H "${_PORT_GCC_DIR}/../include/stddef.h" ABSOLUTE) +endif() configure_file( "${CMAKE_SOURCE_DIR}/port/include/hoststddef.h.in" "${CMAKE_BINARY_DIR}/port/include/hoststddef.h" @ONLY) @@ -223,6 +256,25 @@ add_compile_options( add_compile_options($<$:-Wno-pointer-sign>) add_compile_options($<$:-Wno-unused-function>) +# The decomp's `#define inherits struct` idiom (src/bondtypes.h) makes the base +# struct an unnamed tagged member and accesses its fields directly +# (obj->type where type lives in the embedded PropRecord). GCC targeting +# *-w64-mingw32 enables this via -fms-extensions implicitly; a native Linux/ +# macOS GCC does not, so `'ObjectRecord' has no member named 'type'`. Enable +# the Plan 9 extensions explicitly there (superset of -fms-extensions: also the +# automatic base-pointer conversion the idiom relies on). C only — the decomp +# is C; fast3d C++ does not use `inherits`. +if(NOT MINGW) + add_compile_options($<$:-fplan9-extensions>) + # GCC 14 promoted several long-standing C leniencies to hard errors + # (int<->pointer for a known prototype, incompatible-pointer arg types, + # implicit int/function). The WIP decomp still trips these (e.g. + # append_text_picked_up's vestigial u8* params in propobj.c). -fpermissive + # downgrades them to warnings — the C counterpart of the -Wno-error lines + # above, and the documented path for building older C against GCC 14+. + add_compile_options($<$:-fpermissive>) +endif() + # ASLR must be OFF: dram_syms.s defines absolute symbols (cfb_16 @ 0x70000000, # _bssSegmentEnd @ 0x70050000) that the game references through pointer-typed # externs (src/bondgame.h: `extern u32 *_bssSegmentEnd`). On x86-64/PE, taking @@ -233,6 +285,27 @@ add_compile_options($<$:-Wno-unused-function>) # base at startup. if(MINGW) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--disable-dynamicbase") +elseif(UNIX AND NOT APPLE) + # Linux equivalent of the ASLR-off requirement above: dram_syms.s defines + # absolute symbols at 0x70000000 referenced through pointer-typed externs. A + # PIE build (GCC's default on modern distros) emits GOT/relative relocations + # for them — R_X86_64_32S link errors, or a runtime rebase to garbage. Build + # and link no-PIE so the image keeps its preferred load address. + add_compile_options(-fno-pie) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -no-pie") + # Move the image base to 0x20000000. The default no-PIE base 0x400000 puts + # .data/.bss symbols around 0x00500000-0x00600000, which COLLIDES with the + # N64 address ranges fast3d's seg_addr() decodes: a game-built display list + # that embeds `&symbol` (via the osVirtualToPhysical identity shim) hands + # seg_addr a value < 0x800000, which it treats as a K0-physical DRAM offset + # and remaps to 0x80xxxxxx (zeroed DRAM mirror) -> "unknown GBI opcode 0x00" + # -> black screen. 0x20000000 sits in the gap between the segmented-address + # space (< 0x10000000), the cart map (0x10000000), the D131 module range + # ([0x40000000,0x70000000)) and DRAM (>= 0x70000000); seg_addr passes such + # addresses through untouched. Still < 2GiB so -mcmodel=small / R_X86_64_32 + # 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") endif() # port/shim first: PC-only shadow headers (bondtypes.h, bondconstants.h, diff --git a/build-pc.sh b/build-pc.sh index 8f71030f..34a6388c 100644 --- a/build-pc.sh +++ b/build-pc.sh @@ -22,7 +22,7 @@ set -euo pipefail ROMID="${1:-ntsc-final}" -BUILD_DIR="build-pc" +BUILD_DIR="${BUILD_DIR:-build-pc}" echo "==> Configuring PC port (ROMID=${ROMID})" cmake -S . -B "${BUILD_DIR}" -DROMID="${ROMID}" diff --git a/docs/dev/LINUX-PORT.md b/docs/dev/LINUX-PORT.md new file mode 100644 index 00000000..9586411f --- /dev/null +++ b/docs/dev/LINUX-PORT.md @@ -0,0 +1,91 @@ +# Linux port — status & gap analysis + +Session M-38 (2026-09-02). Goal-2 on the roadmap (after the Windows alpha / +release artifact). This is a scoping + first-patch pass; **nothing here has run +on a Linux machine yet.** + +## Method + +Read `build-pc.sh`, `CMakeLists.txt`, `docs/building.md`, and grepped +`port/src`, `port/fast3d`, `port/include` for Windows-only headers / Win32 APIs +/ unguarded `_WIN32` branches. A local Qwen `triage` pass seeded the hunt; every +claim below was re-checked against the real files (the 27B was wrong on two of +three headline items — see "Ruled out"). + +## What is already Linux-ready (verified by reading) + +- **`port/include/platform.h`** — sets `PLATFORM_LINUX` / `PLATFORM_MACOS` / + `PLATFORM_WINDOWS` cleanly. +- **`CMakeLists.txt`** — `find_package(SDL2 REQUIRED)`, `find_package(ZLIB + REQUIRED)`, and `else()` branches already present for the non-Windows case: + `GL_LIBRARY = GL` (l.291), `EXTRA_LIBRARIES = stdc++ m dl pthread` (l.304), + `elseif(UNIX)` platform block (l.86). `WIN32_EXECUTABLE` and the MinGW zlib + fallback are correctly `if(WIN32)` / `if(MINGW)` guarded. +- **`build-pc.sh`** — already documents the Linux apt deps + (`cmake libsdl2-dev zlib1g-dev libgl1-mesa-dev`) and is a plain bash script, + no MSYS-isms in the executable body. +- **`port/src/system.c`** — `sysSleep()` has a `nanosleep()` `#else` branch + (l.76-82); `` is `PLATFORM_WINDOWS`-guarded. +- **`port/src/libultra.c`** — the OS-thread kernel has a real pthreads branch + next to the Win32 `_beginthreadex` one. +- **`port/src/crash.c`** — `MessageBoxW` / `SetThreadPriority` / `__debugbreak` + all `_WIN32`-guarded, with `PLATFORM_LINUX` counterparts. +- **`port/src/video.c`** — `GE_MKDIR` has both `_mkdir` and `mkdir(p,0777)`. +- No `__declspec`, `#pragma comment`, or hardcoded `\`-path / drive-letter + usage anywhere in `port/`. + +## Ruled out (Qwen triage false positives) + +- ~~`system.c:75` `Sleep()` has no POSIX branch~~ — it does (`nanosleep`). +- ~~CMake needs `else()` branches for zlib/pthread on Linux~~ — already there. + +## The one real code gap — FIXED this session (verification owed) + +**`port/src/romdata.c`** — the fixed-address cart map (`0x10000000`, so absolute +ROM-asset symbols are live host pointers) had a **Windows-only** implementation +(`VirtualAlloc`); the `#else` branch was a stub that logged "not implemented on +this platform" and fell straight to the heap-copy fallback. In heap-copy mode +anything that dereferences a cart address *directly* (not via +`romdataGetRom()` / the PI shims) reads wrong memory — so Linux would run, if at +all, in the same degraded mode Windows only hits when `VirtualAlloc` loses the +address race. + +**Change (M-38):** +- Factored the post-map work (memcpy + `pcmodelsLoadSidecars` + + `pccgLoadSidecars` + D55 RLE-header fixup) into `romdataFinishCartMap()`, + shared by both paths so they cannot drift. +- POSIX `#else` now does an anonymous `mmap((void*)CART_BASE, len, + PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED_NOREPLACE, -1, 0)`. + `MAP_FIXED_NOREPLACE` (Linux 4.17+) fails rather than clobbering an existing + mapping; where the macro is absent the plain hint is advisory and the + `at == CART_BASE` check catches a relocated result. On any failure it + `munmap`s and falls through to the identical heap-copy path as before. +- `romdataDestroy()` `munmap`s on POSIX (tracked length in `mappedLen`). + +**Owed verification (needs a Linux box):** +1. Compiles under GCC on Linux (`` resolves; no decomp-include-path + shadowing surprise). +2. `mmap` actually lands at `0x10000000` on a stock ASLR Linux — `0x10000000` + (256 MB) is low but usually free; if the kernel routinely refuses it, + consider linking the binary no-PIE or reserving the range via a linker + script so the loader keeps it clear. +3. `-level_09` boots crash-free with the mapped path taken (check the log says + "mapped at 0x10000000 (cart base)", not "using heap copy"). +4. Whether the heap-copy fallback is actually survivable on Linux if step 2 + fails — audit for direct `0x1xxxxxxx` derefs outside the PI shims. + +## Remaining Linux work after the romdata patch lands + +| # | Item | Size | Note | +|---|------|------|------| +| 1 | Run the Ubuntu **`validate`** CI job → upgrade it to a real **compile** job | S | `.github/workflows/ci.yml` already configures on Ubuntu; flipping it to `cmake --build` surfaces the real GCC-strictness gaps cheaply | +| 2 | First actual Linux runtime bring-up (`-level_09`) | M | expect a short tail of unguarded calls / struct-layout asserts GCC-on-Linux vs MinGW | +| 3 | `docs/building.md` — promote Linux from "untested" to "supported" once 1+2 pass | S | | +| 4 | SDL2 / GL context creation on X11 + Wayland | M | fast3d is GL 3.3 core; PD port runs on Linux so the path is known-good | +| 5 | Package: an AppImage or tarball equivalent of `bundle-win.sh` | M | reuse `prepare-assets.py` verbatim — it is already OS-agnostic | + +## Recommended next step + +Land the romdata patch behind CI (item 1) — turn the Ubuntu `validate` job into +a compile job in the same PR so the "does it build on Linux" question is +answered by the bots, then do the runtime bring-up on a real machine. diff --git a/docs/img/bugs/1 fps.PNG b/docs/img/bugs/1 fps.PNG new file mode 100644 index 00000000..9606bd02 Binary files /dev/null and b/docs/img/bugs/1 fps.PNG differ diff --git a/include/PR/os.h b/include/PR/os.h index 0a84cd5d..6df1d845 100644 --- a/include/PR/os.h +++ b/include/PR/os.h @@ -980,9 +980,16 @@ extern void osAckRamromWrite(void); /* byte string operations */ +#if defined(PORT) && !defined(_WIN32) +/* On the Linux/macOS port these resolve to the host libc; glibc + * declares them with size_t sizes, which conflict with the N64 `int` + * prototypes below (GCC 14 hard error). Use the host declarations. */ +#include +#else extern void bcopy(const void *, void *, int); extern int bcmp(const void *, const void *, int); extern void bzero(void *, int); +#endif /* Miscellaneous operations */ diff --git a/port/fast3d/gfx_opengl.cpp b/port/fast3d/gfx_opengl.cpp index 66e5aea1..532a2a69 100644 --- a/port/fast3d/gfx_opengl.cpp +++ b/port/fast3d/gfx_opengl.cpp @@ -1083,7 +1083,15 @@ extern "C" bool gfx_opengl_dump_bound_fbo(uint32_t width, uint32_t height, const size_t n = (size_t)width * height * 3; uint8_t* pixels = (uint8_t*)malloc(n); if (!pixels) return false; + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glReadBuffer(GL_BACK); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glFinish(); glReadPixels(0, 0, (GLsizei)width, (GLsizei)height, GL_RGB, GL_UNSIGNED_BYTE, pixels); + { + GLenum e = glGetError(); + if (e) sysLogPrintf(LOG_WARNING, "dump_bound_fbo: glReadPixels err 0x%x (%ux%u)", (unsigned)e, width, height); + } FILE* f = fopen(path, "wb"); if (!f) { free(pixels); return false; } fprintf(f, "P6\n%u %u\n255\n", width, height); @@ -1199,7 +1207,11 @@ bool gfx_opengl_start_draw_to_framebuffer(int fb_id, float noise_scale) { if (noise_scale != 0.0f) { current_noise_scale = 1.0f / noise_scale; } - glBindFramebuffer(GL_FRAMEBUFFER, fb.fbo); + /* fb_id 0 is the on-screen target: bind the window's default + * framebuffer (GL name 0), not framebuffers[0].fbo (a generated but + * attachment-less FBO -> incomplete -> nothing renders on a strict + * GL implementation like Mesa/llvmpipe). */ + glBindFramebuffer(GL_FRAMEBUFFER, fb_id == 0 ? 0 : fb.fbo); current_framebuffer = fb_id; return true; } else { diff --git a/port/fast3d/gfx_sdl2.cpp b/port/fast3d/gfx_sdl2.cpp index 8bdac3e5..ce1a7650 100644 --- a/port/fast3d/gfx_sdl2.cpp +++ b/port/fast3d/gfx_sdl2.cpp @@ -427,10 +427,19 @@ static inline void sync_framerate_with_timer(void) { previous_time = t; } +/* Optional hook run with the freshly-composited frame still in the back + * buffer, immediately before the swap. video.c uses it for GE_PCDUMP / + * screenshots: reading the back buffer AFTER SDL_GL_SwapWindow is undefined + * on buffer-exchange drivers (Mesa, WSLg) and yields a black capture. */ +extern "C" void (*gfx_pre_swap_hook)(void) = NULL; + static void gfx_sdl_swap_buffers_begin(void) { if (target_fps) { sync_framerate_with_timer(); } + if (gfx_pre_swap_hook) { + gfx_pre_swap_hook(); + } SDL_GL_SwapWindow(wnd); } diff --git a/port/include/hostsched.h.in b/port/include/hostsched.h.in new file mode 100644 index 00000000..6d15a3f1 --- /dev/null +++ b/port/include/hostsched.h.in @@ -0,0 +1,9 @@ +#ifndef PORT_HOSTSCHED_H +#define PORT_HOSTSCHED_H +/* Generated by CMake from hostsched.h.in — do not edit. + * Includes the HOST by absolute path so that port/shim/sched.h can + * give C++ TUs (fast3d, libstdc++'s /gthr headers) the real + * — cpu_set_t, sched_yield — instead of the decomp's src/sched.h, + * which sits on the include path and would otherwise shadow it. */ +#include "@PORT_HOST_SCHED_H@" +#endif diff --git a/port/include/pc_protos.h b/port/include/pc_protos.h index 15587143..6d4096e8 100644 --- a/port/include/pc_protos.h +++ b/port/include/pc_protos.h @@ -201,6 +201,11 @@ void fileUpdateSelectedBondInSave(); s32 fogGetPropDistColor(); void fogLoadLevelEnvironment(); void free(); +/* D38: the N64 stub declares neither, and unlike MinGW a strict + * host GCC does not leak them — so getenv()'s 64-bit pointer return is + * truncated to int (crashed configGetFrameDump on Linux). */ +char * getenv(); +void * realloc(); int frontGetPlayersFavoriteWeaponInHand(); void generate_player_thrown_grenade(); void generate_player_thrown_knife(); @@ -389,9 +394,10 @@ s32 sizepropdef(); Gfx * skyRender(); void skySetStageNum(); void skyTick(); -/* Exact match of GCC's built-in (and MSVCRT) prototype; size_t is - * `unsigned long long` on this MinGW toolchain. */ -int snprintf(char *, unsigned long long, const char *, ...); +/* Match the C library prototype. size_t (from above) is + * `unsigned long long` on MinGW but `unsigned long` on Linux/glibc — spelling + * it `size_t` keeps this compatible with both libcs' . */ +int snprintf(char *, size_t, const char *, ...); PropRecord * something_with_generating_object(); void speedgraphInit(); void speedgraphMarkerUpdate(); @@ -446,7 +452,9 @@ s32 texLoadFromGdl(); s32 texReadUncompressed(); void texSelect(); s32 texShrinkNonPaletted(); -long long time(); +#if defined(_WIN32) +long long time(); /* MinGW: is shadowed by the N64 stub here; POSIX gets the real one */ +#endif const char * tokenFind(); s32 tokenReadIo(); void transform3Dto2DWithZScaling(); diff --git a/port/shim/sched.h b/port/shim/sched.h new file mode 100644 index 00000000..256c003d --- /dev/null +++ b/port/shim/sched.h @@ -0,0 +1,18 @@ +/* + * PC port shim for . + * + * The decomp ships src/sched.h (the N64 OSSc* scheduler client interface), + * which is on the compiler include path. libstdc++'s / gthr headers + * (pulled into the fast3d C++ TUs) do `#include ` and need the HOST + * header — cpu_set_t, sched_yield — so C++ TUs must be routed to the real + * by absolute path (generated hostsched.h, same trick as + * port/shim/string.h). C TUs keep resolving to the decomp's own + * src/sched.h, exactly as on the console. + * + * Inert in the N64 build (port/shim is not on its include path). + */ +#if defined(__cplusplus) +#include "hostsched.h" +#else +#include_next +#endif diff --git a/port/shim/stdarg.h b/port/shim/stdarg.h index 962bd243..5cc58941 100644 --- a/port/shim/stdarg.h +++ b/port/shim/stdarg.h @@ -4,16 +4,35 @@ * The N64 decomp's include/stdarg.h does `#include ` (the console * dev-kit convenience: one header pulls in all of libultra). On the PC that * means any HOST header that internally includes (SDL_stdinc.h, - * parts of MinGW's own headers, ...) drags PR/os.h into the middle of host - * header parsing. If MinGW's has already run (it defines the - * `errno` macro), PR/os.h's controller structs — which have fields literally - * named `errno` — fail to parse ("field '_errno' declared as a function"). - * - * Parse the N64 header with the errno macro suppressed; restore it after. - * Inert in the N64 build (port/shim is not on its include path). + * parts of the CRT's own headers, glibc's , ...) drags PR/os.h into + * the middle of host header parsing. */ +#if defined(_WIN32) +/* + * MinGW: parse the N64 header with the errno macro suppressed (PR/os.h has + * fields literally named `errno`); restore it after. MinGW's CRT headers do + * not need __gnuc_va_list. + */ #pragma push_macro("errno") #undef errno #include "include/stdarg.h" #pragma pop_macro("errno") + +#else +/* + * Linux/macOS: do NOT pull into system-header parsing. glibc's + * needs both va_list and the __gnuc_va_list alias that GCC's real + * would provide; the decomp header defines neither. Provide the + * whole set straight from the always-available compiler builtins. + */ +#ifndef _PORT_SHIM_STDARG_H +#define _PORT_SHIM_STDARG_H +typedef __builtin_va_list va_list; +typedef __builtin_va_list __gnuc_va_list; +#define va_start(ap, last) __builtin_va_start(ap, last) +#define va_end(ap) __builtin_va_end(ap) +#define va_arg(ap, type) __builtin_va_arg(ap, type) +#define va_copy(dst, src) __builtin_va_copy(dst, src) +#endif +#endif diff --git a/port/shim/stddef.h b/port/shim/stddef.h index fe0d0a40..c36a0361 100644 --- a/port/shim/stddef.h +++ b/port/shim/stddef.h @@ -25,6 +25,41 @@ #undef __need_NULL #undef __need_wint_t #include "hoststddef.h" -#else +#elif defined(_WIN32) +/* + * C TUs on MinGW: keep the empty N64 include/stddef.h stub. MinGW's CRT + * headers leak size_t/ptrdiff_t transitively, and its own drags in + * the `#define errno (*_errno())` CRT macro, which collides with PR/os.h's + * `u8 errno;` struct field — so routing to the host header is not an option + * here. + */ #include "include/stddef.h" +#else +/* + * C TUs on Linux/macOS: the N64 include/stddef.h stub defines nothing, glibc + * only defines these in the compiler's shadowed builtin , and the + * MinGW hoststddef trick (bin/../include/stddef.h) does not exist on Linux. + * Provide the standard types straight from the always-predefined GCC/Clang + * builtins — no header, so nothing to collide with. C11 permits the benign + * identical redefinition if a real is ever reached another way. + */ +#ifndef _PORT_SHIM_STDDEF_TYPES +#define _PORT_SHIM_STDDEF_TYPES +typedef __SIZE_TYPE__ size_t; +typedef __PTRDIFF_TYPE__ ptrdiff_t; +/* SDL's headers (SDL_stdinc.h, SDL_hidapi.h) and glibc's + * reference wchar_t, which in C is a library typedef owned by the compiler's + * shadowed . Provide it from the builtin, guarded by the same + * macro GCC/glibc use so a real reached later is a no-op. */ +#ifndef _WCHAR_T +#define _WCHAR_T +typedef __WCHAR_TYPE__ wchar_t; +#endif +#ifndef NULL +#define NULL ((void *)0) +#endif +#ifndef offsetof +#define offsetof(t, m) __builtin_offsetof(t, m) +#endif +#endif #endif diff --git a/port/shim/stdlib.h b/port/shim/stdlib.h index 868956c1..5512a05f 100644 --- a/port/shim/stdlib.h +++ b/port/shim/stdlib.h @@ -17,4 +17,15 @@ #include "hoststdlib.h" #else #include "include/stdlib.h" +#if !defined(_WIN32) +/* The N64 stub declares only lldiv_t/ldiv_t + lldiv/ldiv. MinGW's other CRT + * headers leak getenv() etc.; a strict host GCC (Linux/macOS) does not, so + * getenv()'s 64-bit pointer return is assumed int and truncated (crashed + * configGetFrameDump). Declare the pointer-returning stragglers the port + + * game code use that nothing else declares. K&R form = compatible with any + * real prototype. (malloc/calloc/free are in port/include/pc_protos.h and + * a few port TUs' own local decls — not repeated here to avoid conflicts.) */ +extern char *getenv(); +extern void *realloc(); +#endif #endif diff --git a/port/src/crash.c b/port/src/crash.c index c064ddf4..aced06e4 100644 --- a/port/src/crash.c +++ b/port/src/crash.c @@ -402,6 +402,39 @@ static void crashHandler(int sig, siginfo_t *siginfo, void *ctx) sysFatalError("Crash!\n\n%s", msg); } +/* D38: called by the kernel heartbeat watchdog (port/src/libultra.c) when a + * hang is detected. A full cross-thread backtrace on POSIX would need a + * signal-based stack-walk of every peer thread; for now log the live thread + * roster plus the calling (watchdog) thread's own backtrace, which is enough + * to see the watchdog fired and which threads were still up. */ +void crashDumpThreads(const unsigned long *tids, const char **names, int count) +{ + char msg[CRASH_MAX_MSG + 1] = { 0 }; + unsigned msglen = 0; + + CRASH_MSG("THREAD DUMP (%d live):\n", count); + for (int i = 0; i < count; ++i) { + CRASH_MSG(" tid=0x%lx %s\n", tids ? tids[i] : 0UL, + (names && names[i]) ? names[i] : "?"); + } + CRASH_MSG("\nWATCHDOG THREAD BACKTRACE:\n"); + { + void *frames[CRASH_MAX_FRAMES] = { NULL }; + const int nframes = backtrace(frames, CRASH_MAX_FRAMES); + char **strings = (nframes > 0) ? backtrace_symbols(frames, nframes) : NULL; + for (int i = 0; i < nframes; ++i) { + CRASH_MSG("#%02d: %s\n", i, + (strings && strings[i]) ? strings[i] : "?"); + } + free(strings); + } + sysLogPrintf(LOG_ERROR, "%s", msg); + { + FILE *f = fopen(CRASH_LOG_FNAME, "ab"); + if (f) { fprintf(f, "%s", msg); fclose(f); } + } +} + #endif /* PLATFORM_WINDOWS / PLATFORM_LINUX */ int g_CrashEnabled = 0; diff --git a/port/src/libultra.c b/port/src/libultra.c index 3a20d356..d4fe542f 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -173,8 +173,14 @@ static uint64_t portNextTimerUs(void); static void portHeartbeatCheck(void) { + /* A real hang is a permanent stall; transient >3 s gaps are normal on a + * slow/software-GL/debugger setup (level loads, gdb pauses, SIGTERM + * teardown) and were spamming ge007.crash.log with dozens of thread + * dumps. Use a longer window and stop after a few reports. */ + static int fired = 0; uint64_t now = sysGetMicroseconds(); - if (now - g_lastFrameUs > 3000000 && now - g_lastHeartbeatUs > 2000000) { + if (fired < 3 && now - g_lastFrameUs > 8000000 && now - g_lastHeartbeatUs > 5000000) { + fired++; g_lastHeartbeatUs = now; sysLogPrintf(LOG_ERROR, "kernel heartbeat: no frame rendered for %llu ms (frames=%d); state:", @@ -334,6 +340,29 @@ void osCreateThread(OSThread *t, OSId id, void (*entry)(void *), void *arg, t->state = OS_STATE_STOPPED; } +#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. */ +static void *portAllocLowStack(size_t sz) +{ +#if defined(MAP_32BIT) + void *p = mmap(NULL, sz, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT +#if defined(MAP_STACK) + | MAP_STACK +#endif + , -1, 0); + if (p != MAP_FAILED) + return p; +#endif + return NULL; +} +#endif + void osStartThread(OSThread *t) { PortThread *pt = portFind(t); @@ -344,7 +373,15 @@ void osStartThread(OSThread *t) pthread_attr_t attr; int rc; pthread_attr_init(&attr); +#if !defined(_WIN32) + void *lowStack = portAllocLowStack(PORT_THREAD_STACK); + if (lowStack) + pthread_attr_setstack(&attr, lowStack, PORT_THREAD_STACK); + else + pthread_attr_setstacksize(&attr, PORT_THREAD_STACK); +#else pthread_attr_setstacksize(&attr, PORT_THREAD_STACK); +#endif rc = pthread_create(&pt->th, &attr, portThreadWrapper, pt); pthread_attr_destroy(&attr); if (rc != 0) { @@ -765,6 +802,7 @@ static int dramHostAddrValid(uintptr_t addr, u32 size) * buffers (e.g. ramrom_data_target), stack compbuffers, sidecar images. * Truncated wild addresses (0x40xxxxxx from s32 pointer math) are not * committed, so VirtualQuery still catches them. */ +#if defined(PLATFORM_WINDOWS) { MEMORY_BASIC_INFORMATION mbi; if (VirtualQuery((LPCVOID)addr, &mbi, sizeof(mbi)) == sizeof(mbi) && @@ -774,6 +812,13 @@ static int dramHostAddrValid(uintptr_t addr, u32 size) return 1; } return 0; +#else + /* No cheap committed-memory probe on POSIX; this is a TEMP D60 diagnostic. + * Be permissive so legitimate .bss/stack/sidecar DMA targets are not + * flagged as fatal (a genuinely wild target still faults in the memcpy). */ + (void)size; + return 1; +#endif } static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size) @@ -817,6 +862,7 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size) wp += snprintf(wp, win + sizeof(win) - (wp - win), " %p", (void *)sp[i]); } +#if defined(PLATFORM_WINDOWS) { PVOID tlow = NULL, thigh = NULL; GetCurrentThreadStackLimits(&tlow, &thigh); @@ -826,6 +872,12 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size) ((uintptr_t)dstVA >= (uintptr_t)tlow && (uintptr_t)dstVA < (uintptr_t)thigh)); } +#else + /* TEMP D60 diagnostic: no portable committed-stack query without + * _GNU_SOURCE (pthread_getattr_np); the raw stack window above and + * the crash handler's register dump are enough to symbolicate. */ + sysLogPrintf(LOG_ERROR, "D60 thread stack: (n/a on POSIX)\n"); +#endif sysLogPrintf(LOG_ERROR, "D60 BAD DMA TARGET dst=%p src=0x%08X size=0x%X " "stack@rbp:%s\n", diff --git a/port/src/main.c b/port/src/main.c index 878a947c..4b92576e 100644 --- a/port/src/main.c +++ b/port/src/main.c @@ -115,6 +115,7 @@ int main(int argc, char **argv) * loader loads at 0x140000000 and those relocations are no-ops; if we * ever got relocated, every such slot would be silently corrupted. * Fail loudly instead. */ +#if defined(PLATFORM_WINDOWS) if (sysImageBase() != 0x140000000ul) { sysLogPrintf(LOG_ERROR, "image loaded at %p, expected preferred base 0x140000000; " @@ -122,6 +123,11 @@ int main(int argc, char **argv) (void *)sysImageBase()); return 1; } +#else + /* Linux/ELF no-PIE: dram_syms.s absolute symbols resolve to their literal + * values independent of the image base (no .refptr indirection), so the + * load address is not constrained. sysImageBase() is a stub here anyway. */ +#endif /* 2b. Reserve the N64-DRAM region: s32-safe view @ 0x70000000 (cfb_16, * mempools) + KSEG0 mirror @ 0x80000000 (see port/src/dram.c). */ diff --git a/port/src/n64stubs.c b/port/src/n64stubs.c index d85aa77c..dfc01bae 100644 --- a/port/src/n64stubs.c +++ b/port/src/n64stubs.c @@ -106,10 +106,14 @@ u8 (*tlbmanageGetTlbAllocatedBlock(void))[TLB_BLOCK_SIZE] return (u8 (*)[TLB_BLOCK_SIZE])0x70700000; } -/* --- K&R libc helpers (IDO provided these; the host libc does not) ------- */ -/* Signatures mirror include/PR/os.h:983 / include/bstring.h exactly. */ +/* --- K&R libc helpers (IDO provided these; MinGW's libc does not) -------- */ +/* Signatures mirror include/PR/os.h:983 / include/bstring.h exactly. + * glibc still exports bcopy/bzero (declared in with size_t), so + * on the POSIX port we use those and must NOT redefine them here. */ +#if defined(_WIN32) void bcopy(const void *src, void *dst, int n) { memmove(dst, src, (size_t)n); } void bzero(void *s, int n) { memset(s, 0, (size_t)n); } +#endif /* --- libm internals (IDO's libm exposed these; MinGW's does not) --------- */ /* Quiet NaN float, used by gu/cosf.c and game/zlib.c. The VALUE is the same */ diff --git a/port/src/pccg.c b/port/src/pccg.c index 39c1a1d3..7ae501fa 100644 --- a/port/src/pccg.c +++ b/port/src/pccg.c @@ -22,7 +22,7 @@ #include extern void *malloc(unsigned long long size); -extern int snprintf(char *str, unsigned long long maxsize, const char *format, ...); +extern int snprintf(char *str, size_t maxsize, const char *format, ...); #include "platform.h" #include "system.h" diff --git a/port/src/pcmodels.c b/port/src/pcmodels.c index 247a8694..045e4a16 100644 --- a/port/src/pcmodels.c +++ b/port/src/pcmodels.c @@ -24,7 +24,7 @@ /* D38: / resolve to the decomp's N64 stubs via the * include path; declare the host functions this file uses. */ extern void *malloc(unsigned long long size); -extern int snprintf(char *str, unsigned long long maxsize, const char *format, ...); +extern int snprintf(char *str, size_t maxsize, const char *format, ...); #include "platform.h" #include "system.h" diff --git a/port/src/romdata.c b/port/src/romdata.c index dac3561a..b1be80bb 100644 --- a/port/src/romdata.c +++ b/port/src/romdata.c @@ -23,7 +23,7 @@ /* D38: / resolve to the decomp's N64 stubs via the include * path; declare the host functions this file uses. */ extern void *malloc(unsigned long long size); -extern int snprintf(char *str, unsigned long long maxsize, const char *format, ...); +extern int snprintf(char *str, size_t maxsize, const char *format, ...); #include "platform.h" @@ -32,6 +32,11 @@ extern int snprintf(char *str, unsigned long long maxsize, const char *format, . #define WIN32_LEAN_AND_MEAN #endif #include +#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 #endif #include "system.h" @@ -50,6 +55,9 @@ extern int snprintf(char *str, unsigned long long maxsize, const char *format, . static u8 *rom = NULL; /* heap copy (fallback path) */ static u32 romSize = 0; static int mappedAtCartBase = 0; +#if !defined(PLATFORM_WINDOWS) +static unsigned long long mappedLen = 0; /* munmap() needs the length */ +#endif /* D34 (docs/internals.md): PE image base, low 32 bits zero. On N64 the * animation_data segment links at VMA 0, so game code computes record @@ -105,6 +113,35 @@ static int romHeaderValid(const u8 *h, char *err, size_t errsz) static u32 romdataBswap32(u32 v); /* defined below; used by the D55 fixup */ +/* The caller has placed a writable region of (romSize + sidecars) bytes at + * CART_BASE. Populate it, load the sidecars, apply the D55 RLE-header fixup, + * and mark the ROM live. Shared by the Windows (VirtualAlloc) and POSIX (mmap) + * fixed-address paths so the post-map logic can never drift between them. */ +static int romdataFinishCartMap(const char *tok, u8 *img, + u32 sideTotal, u32 cgTotal) +{ + memcpy((void *)(uintptr_t)CART_BASE, 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()); + + /* 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 + * copy by swapping it in place (no-op if it already reads as LE). */ + { + extern void *unknown2; /* absolute cart address */ + u32 *hdr = (u32 *)&unknown2; + if ((u16)*hdr > 512 || (u16)(*hdr >> 16) > 512) + *hdr = romdataBswap32(*hdr); + } + return 0; +} + int romdataInit(void) { char err[128] = ""; @@ -120,10 +157,11 @@ int romdataInit(void) uintptr_t p = (uintptr_t)&probe; uintptr_t hi = p & ~(uintptr_t)0xFFFFFFFFu; if (hi == 0) { - sysLogPrintf(LOG_ERROR, "romdataInit: image base 0x%llX is not " - "4GiB-aligned; ANIM_DATA_* offsets would be wrong", - (unsigned long long)p); - return -1; + /* Low image base (Linux no-PIE loads at 0x400000). The ANIM_DATA_* + * lvalues are only ever address-taken — `(s32)&ANIM_DATA_x` yields + * the offset, never a dereference — so g_pc_animdata_base just has + * to be some 4GiB-aligned address whose low 32 bits are zero. */ + hi = (uintptr_t)0x100000000ull; } g_pc_animdata_base = (u8 *)hi; } @@ -168,51 +206,44 @@ int romdataInit(void) * sidecar extension. */ u32 cgTotal = pccgReserveSize(img); - /* Map at the cart address so absolute asset symbols are live. */ + /* Map at the cart address so absolute asset symbols are live. On + * failure both paths fall through to the heap copy below (degraded: + * anything that dereferences a cart address directly, rather than via + * romdataGetRom()/the PI shims, will read wrong memory). */ + { + u32 maplen = romSize + sideTotal + cgTotal; #if defined(PLATFORM_WINDOWS) - void *at = VirtualAlloc((LPVOID)CART_BASE, romSize + sideTotal + cgTotal, - MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); - if (at == (void *)(uintptr_t)CART_BASE) { - memcpy(at, 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()); - - /* D55 (docs/internals.md): the RLE folder-menu background at - * `unknown2` (romassets_.s) is stored in the ROM with a - * big-endian w/h header (01 B8 01 2B = 440x299), but - * rle_expand_8bit() reads it little-endian. The N64 build embeds - * this asset into .data via assets/romfiles2.s from an extracted - * .bin whose header is byte-swapped (title2.c hardcodes 440x299 - * row rendering, and this is the only decodable 440x299 RLE - * stream in the ROM), so make the cart copy match it: swap the - * w/h word in place. The guard makes the fixup a no-op if a - * region's copy already reads as a plausible LE size. */ - { - extern void *unknown2; /* absolute cart address */ - u32 *hdr = (u32 *)&unknown2; - if ((u16)*hdr > 512 || (u16)(*hdr >> 16) > 512) - *hdr = romdataBswap32(*hdr); - } - return 0; - } - if (at) - VirtualFree(at, 0, MEM_RELEASE); - sysLogPrintf(LOG_WARNING, "romdataInit: could not reserve 0x%08X; " - "using heap copy — direct ROM reads will fail", - CART_BASE); + void *at = VirtualAlloc((LPVOID)CART_BASE, maplen, + MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + if (at == (void *)(uintptr_t)CART_BASE) + return romdataFinishCartMap(tok, img, sideTotal, cgTotal); + if (at) + VirtualFree(at, 0, MEM_RELEASE); + sysLogPrintf(LOG_WARNING, "romdataInit: could not reserve 0x%08X; " + "using heap copy — direct ROM reads will fail", + CART_BASE); #else - /* Best effort: fixed-hint mmap. If the hint is refused we fall back - * to a heap copy (same limitation as above). */ - sysLogPrintf(LOG_WARNING, "romdataInit: fixed-address mapping not " - "implemented on this platform; using heap copy"); + /* 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. */ + int flags = MAP_PRIVATE | MAP_ANONYMOUS; +#ifdef MAP_FIXED_NOREPLACE + flags |= MAP_FIXED_NOREPLACE; #endif + void *at = mmap((void *)(uintptr_t)CART_BASE, maplen, + PROT_READ | PROT_WRITE, flags, -1, 0); + if (at != MAP_FAILED && at == (void *)(uintptr_t)CART_BASE) { + mappedLen = maplen; + return romdataFinishCartMap(tok, img, sideTotal, cgTotal); + } + 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); +#endif + } rom = img; return 0; @@ -225,12 +256,15 @@ int romdataInit(void) void romdataDestroy(void) { -#if defined(PLATFORM_WINDOWS) if (mappedAtCartBase) { +#if defined(PLATFORM_WINDOWS) VirtualFree((LPVOID)CART_BASE, 0, MEM_RELEASE); +#else + munmap((void *)(uintptr_t)CART_BASE, (size_t)mappedLen); + mappedLen = 0; +#endif mappedAtCartBase = 0; } -#endif free(rom); rom = NULL; romSize = 0; diff --git a/port/src/video.c b/port/src/video.c index 4bb774ba..9e87305f 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -100,6 +100,10 @@ static u32 frames = 0; /* Set by the host event pump (F12), consumed on the render thread in * videoEndFrame where a GL context is current. */ static volatile int screenshotReq = 0; + +/* Pre-swap capture hook (defined below, registered in videoInit). */ +static void videoPreSwapCapture(void); +extern void (*gfx_pre_swap_hook)(void); static double fpsWindowStart = 0.0; static int fpsNumFrames = 0; static float vidAvgFPS = 0.f; @@ -172,6 +176,8 @@ int videoInit(void) * still needs a current context on this thread. */ gfx_sdl_release_context(); + gfx_pre_swap_hook = videoPreSwapCapture; + initDone = 1; sysLogPrintf(LOG_INFO, "video: %dx%d window (native %dx%d)", (int)gfx_current_dimensions.width, (int)gfx_current_dimensions.height, @@ -289,18 +295,13 @@ void videoSubmitCommands(Gfx *cmds) gfx_run(cmds); } -void videoEndFrame(void) +/* Runs from gfx_sdl_swap_buffers_begin with the composited frame still in the + * back buffer, just before SDL_GL_SwapWindow. Reading the back buffer after + * the swap is undefined on buffer-exchange drivers (Mesa/WSLg) -> black. */ +static void videoPreSwapCapture(void) { - if (!initDone) { - return; - } - gfx_end_frame(); - - /* TEMP D70 (env-gated, debug-only, env-gated): GE_PCDUMP="first-last" - * or "first-last:step" dumps the presented frame as ./ppm/frame_NNNNNN.ppm - * (one frame behind real time — read happens after SwapWindow). Used to - * pixel-verify the intro's 3D content. Also honours [Debug] FrameDump in - * ge007.ini (env var wins). */ + /* GE_PCDUMP="first-last" / "first-last:step" -> ./ppm/frame_NNNNNN.ppm. + * Also honours [Debug] FrameDump in ge007.ini (env var wins). */ const char *pcdump = configGetFrameDump(); if (pcdump) { static int lo = -1, hi = 0, step = 1; @@ -335,6 +336,14 @@ void videoEndFrame(void) sysLogPrintf(LOG_WARNING, "video: screenshot failed"); } } +} + +void videoEndFrame(void) +{ + if (!initDone) { + return; + } + gfx_end_frame(); ++frames; ++fpsNumFrames; diff --git a/src/game/image.c b/src/game/image.c index 86cca08e..56daa0d9 100644 --- a/src/game/image.c +++ b/src/game/image.c @@ -2435,7 +2435,14 @@ void texLoad(s32 *updateword, struct texpool *pool) if (tex == NULL) { +#ifdef PORT + /* compbuffer is a stack array; (u32) truncates the 64-bit host stack + * address (glibc pthread stacks live above 4GiB). Align at full + * pointer width -- same result on N64, correct on the port. */ + alignedcompbuffer = (u8 *) (((uintptr_t)compbuffer + 0xF) & ~(uintptr_t)0xF); +#else alignedcompbuffer = (u8 *) (((u32)compbuffer + 0xF) >> 4 << 4); +#endif if (alignedcompbuffer); if (tex); diff --git a/src/game/propobj.c b/src/game/propobj.c index 43a1a701..d7de6fef 100644 --- a/src/game/propobj.c +++ b/src/game/propobj.c @@ -9921,7 +9921,14 @@ void sub_GAME_7F04F244(PropRecord* prop, rect4f** polygon, s32* edges, f32* top, } +#ifdef PORT +/* GCC 14 rejects the vestigial u8* params (enum/int -> pointer is a hard + error, not downgraded by -fpermissive). param2/param3 are unused; match + the two callsites' actual types. Codegen-neutral. */ +void append_text_picked_up(u8 *buffer, AMMOTYPE param2, u32 param3) +#else void append_text_picked_up(u8 *buffer,u8 * param2,u8 * param3) +#endif { u8 *str; diff --git a/src/game/vtxstore.c b/src/game/vtxstore.c index fca37a65..f3052be0 100644 --- a/src/game/vtxstore.c +++ b/src/game/vtxstore.c @@ -159,7 +159,7 @@ void sub_GAME_7F09BAC4(s32 find, s32 replacement) { while (var_a1 != NULL) { val = var_a1->Opcode & 0xFF; if (val == 0x18) { - temp_v0_2 = modelGetNodeRwData(var_v0->chrflags, var_a1); + temp_v0_2 = modelGetNodeRwData((Model*)var_v0->chrflags, var_a1); if (find == *temp_v0_2) { *temp_v0_2 = replacement; }