Merge pull request #8 from jkdansereau/feat/linux-ci-compile

Linux: CI compile job + romdata POSIX mmap
This commit is contained in:
James Dansereau authored and GitHub committed 2026-09-03 00:17:20 -04:00
commit 3b7989944c
25 files changed
+573 -84

No files matched your search

+46 -1
View File
@@ -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.
#
+76 -3
View File
@@ -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 <sched.h> path for port/shim/sched.h. The decomp's src/sched.h is on
# the include path and shadows the system header; libstdc++'s <thread>/gthr
# headers in the fast3d C++ TUs need the real <sched.h> (cpu_set_t,
# sched_yield). Both MinGW and Linux keep <sched.h> 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 <stdlib.h> 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 <stddef.h> 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/<triple>/<ver>/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($<$<COMPILE_LANGUAGE:C>:-Wno-pointer-sign>)
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-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($<$<COMPILE_LANGUAGE:C>:-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($<$<COMPILE_LANGUAGE:C>:-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($<$<COMPILE_LANGUAGE:CXX>:-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,
+1 -1
View File
@@ -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}"
+91
View File
@@ -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); `<windows.h>` 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 (`<sys/mman.h>` 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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

+7
View File
@@ -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 <strings.h>
* declares them with size_t sizes, which conflict with the N64 `int`
* prototypes below (GCC 14 hard error). Use the host declarations. */
#include <strings.h>
#else
extern void bcopy(const void *, void *, int);
extern int bcmp(const void *, const void *, int);
extern void bzero(void *, int);
#endif
/* Miscellaneous operations */
+13 -1
View File
@@ -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 {
+9
View File
@@ -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);
}
+9
View File
@@ -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 <sched.h> by absolute path so that port/shim/sched.h can
* give C++ TUs (fast3d, libstdc++'s <thread>/gthr headers) the real
* <sched.h> — 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
+12 -4
View File
@@ -201,6 +201,11 @@ void fileUpdateSelectedBondInSave();
s32 fogGetPropDistColor();
void fogLoadLevelEnvironment();
void free();
/* D38: the N64 <stdlib.h> 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 <PR/ultratypes.h> above) is
* `unsigned long long` on MinGW but `unsigned long` on Linux/glibc — spelling
* it `size_t` keeps this compatible with both libcs' <stdio.h>. */
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: <time.h> is shadowed by the N64 stub here; POSIX gets the real one */
#endif
const char * tokenFind();
s32 tokenReadIo();
void transform3Dto2DWithZScaling();
+18
View File
@@ -0,0 +1,18 @@
/*
* PC port shim for <sched.h>.
*
* The decomp ships src/sched.h (the N64 OSSc* scheduler client interface),
* which is on the compiler include path. libstdc++'s <thread> / gthr headers
* (pulled into the fast3d C++ TUs) do `#include <sched.h>` and need the HOST
* header — cpu_set_t, sched_yield — so C++ TUs must be routed to the real
* <sched.h> by absolute path (generated hostsched.h, same trick as
* port/shim/string.h). C TUs keep resolving <sched.h> 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 <sched.h>
#endif
+26 -7
View File
@@ -4,16 +4,35 @@
* The N64 decomp's include/stdarg.h does `#include <ultra64.h>` (the console
* dev-kit convenience: one header pulls in all of libultra). On the PC that
* means any HOST header that internally includes <stdarg.h> (SDL_stdinc.h,
* parts of MinGW's own headers, ...) drags PR/os.h into the middle of host
* header parsing. If MinGW's <errno.h> 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 <stdio.h>, ...) 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 <ultra64.h> into system-header parsing. glibc's
* <stdio.h> needs both va_list and the __gnuc_va_list alias that GCC's real
* <stdarg.h> 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
+36 -1
View File
@@ -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 <stddef.h> 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 <stddef.h>, 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 <stddef.h> 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 <bits/wchar2.h>
* reference wchar_t, which in C is a library typedef owned by the compiler's
* shadowed <stddef.h>. Provide it from the builtin, guarded by the same
* macro GCC/glibc use so a real <stddef.h> 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
+11
View File
@@ -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
+33
View File
@@ -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;
+53 -1
View File
@@ -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 <sys/mman.h>
/* 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",
+6
View File
@@ -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). */
+6 -2
View File
@@ -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 <strings.h> 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 */
+1 -1
View File
@@ -22,7 +22,7 @@
#include <string.h>
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"
+1 -1
View File
@@ -24,7 +24,7 @@
/* D38: <stdlib.h>/<string.h> 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"
+83 -49
View File
@@ -23,7 +23,7 @@
/* D38: <stdio.h>/<stdlib.h> 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 <windows.h>
#else
/* POSIX: mmap the ROM at the fixed cart address (mirrors the VirtualAlloc
* path). <sys/mman.h> is a host header the decomp include path does not
* shadow. */
#include <sys/mman.h>
#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_<r>.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;
+20 -11
View File
@@ -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;
+7
View File
@@ -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);
+7
View File
@@ -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;
+1 -1
View File
@@ -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;
}