Update the public-facing docs to the M-49 state (v0.1.0 alpha, first
full-campaign playtest: 19/21 missions completable, D191 crashes Bunker ii
+ Statue, D193 AI pacing is the top flaw).
- README: lead with what it is + status, research angle second; new
Quick start section; Status/Download rewritten for the alpha + dual
platform; "By the numbers" refreshed (~3 weeks, ~320 commits, ~190
findings, ~49 sessions, 69 PORT files).
- .github/release-notes.md: v0.1.0 title + known-issues rewrite.
- .github/social-preview.html: status/stat lines (regen + re-upload the
PNG separately).
- docs/index.md, docs/dev/LEVEL-STATUS.md: M-49 status.
- docs/dev/findings.md, docs/dev/GRAPHICS-BACKLOG.md: D191-D197 +
D148/D170/D173 M-49 escalations.
Hygiene:
- Fix the `&` literal in internals.md H1.
- Repoint two ../README.md links that 404 on the Pages site.
- Standardise British -> American spelling across the public docs
(behaviour->behavior, colour->color, catalogue->catalog,
centre->center, etc.). Deep internal logs left untouched.
- .gitignore: document the local-notes convention (docs/HANDOFF.md,
docs/BACKLOG.md, and a new docs/dev/notes/ drop for roadmaps and
session plans) so it survives a fresh clone.
* fix(linux): survive the decomp's latent stack-buffer overruns (D189)
The decompiled game code has a class of fixed-size stack-buffer over/under-
runs that write a few words into adjacent stack scratch:
* stan.c sub_GAME_7F0B1DDC — `StandTile *tileStack[39]` with a bail check
of `if (cat >= 41)` that only runs once per outer iteration, so `cat`
reaches 40 + pointCount and the append writes several slots past the end.
BUNKER1 (-level_09) prop room-list setup legitimately visits ~40 tiles.
* bondview2.c bondviewCalcIntroSwirlCamera — `f32 pointbuf[10]` indexed
`&pointbuf[i*3]` for i in [-1..2] then written [0..5], i.e. [-3 .. 11].
On the N64 and on the MinGW/Windows build (neither uses a stack protector)
these are harmless in practice — the Windows build is stable over thousands
of frames. Only Ubuntu's gcc default (`-fstack-protector-strong`) turns them
into a fatal `__stack_chk_fail`, at BUNKER1 stage load and again at the intro
swirl camera (~frame 5).
Two-part fix:
* `-fno-stack-protector` globally (CMakeLists) — matches the N64 and MinGW
builds so the same code runs the same way. A hardened Linux build would
need the whole overrun class swept first.
* `StandTile *tileStack[64]` under `#ifdef PORT` (stan.c) — the one case
that is cleanly understood and bounded (`pointCount` is 4-bit, so 40+15
is the true worst case). Pure local scratch, never escapes, no behaviour
change. N64 keeps `[39]` under `#else`.
* stan.c:2225 link-resolve also switched to the D177 PORT_PTRADD macro for
idiom consistency (the blob maps below 4 GiB so the bare cast is
currently lossless; #else is byte-identical).
Stacked on the D188 xprintf fix. Verified: Windows -level_09 framediff 3/3
within golden threshold (all changes are #ifdef PORT or no-op on MinGW);
Linux -O2 -level_09 now runs to frame 2100+ with no crash.
* docs: D189 update (stack-protector / latent overrun class) + porting-notes D8
* docs: D190 — Linux -level_45 objective.c load crash (open); D189 sweep 5/6
* fix(linux): thread a va_list* through _Printf (x86-64 SysV ABI)
On the x86-64 System V ABI `va_list` is an array type, so a by-value
`va_list` parameter decays to a pointer. `_Printf()` was passing `&args`
(the address of that local pointer, not the argument list) to `_Putfld()`,
which does `va_arg(*args, ...)` — so `_Putfld` walked garbage and
segfaulted. Latent at `-Og`, fatal at `-O2` (PR #9), which is why the
Linux release build crashed at boot:
bossInitMainthreadData (boss.c:247, the D121 per-level -m token sprintf)
-> sprintf("-level_%c%c %s", ...) -> _Putfld -> SIGSEGV @ xprintf.c
Same UB class as D187. Windows x64's `va_list` is a scalar (`char*`), so
`&args` happened to work there — the Windows build is unaffected.
Fix: under `#ifdef PORT`, `_Printf` takes `va_list *` and passes it
straight to `_Putfld` (which already expects `va_list *`); `src/sprintf.c`
(the only PC caller — `rmon.c` is excluded from the build) passes `&args`
of a real `va_list` object. N64 build untouched (`#ifdef PORT`, and it
byte-matches the ROM).
Verified:
- Windows: build links, `-level_09` framediff 3/3 within golden threshold.
- Linux -O2: boot crash gone, now advances past bossInitMainthreadData
into stage load (uncovers a separate pre-existing stan.c stack overflow
during BUNKER1 prop setup — tracked separately).
* docs: D188 (xprintf va_list ABI) + D189 (stan.c stack-smash) findings + porting-notes D7
The HUD weapon/ammo-type icon (D187, PR #19) and blank briefing objective
text (D178, cd1ed574) are both fixed and on main. Remove them from the
release-notes and bundled-README known-issues blurbs so a v0.1.0 tag ships
an accurate list. Black sky and the front-end 3D models (Nintendo logo,
MISSION COMPLETE / mode-select) are still open and stay listed.
A stock MSYS2 MINGW64 shell ships neither `zip` nor `unzip`, so
`tools_pc/bundle-win.sh` failed at the packaging step for a local release
build (the M-45 handoff's documented v0.1.0 cut path). CI is unaffected
(it `pacman -S zip unzip`, so the `zip` branch is still taken).
Fallback order: zip -> 7z on PATH -> "C:\Program Files\7-Zip\7z.exe" ->
powershell Compress-Archive. Verified end-to-end: bundle builds, unpacks
to a clean dir, prepare-assets runs, ge007.x86_64.exe boots -level_09 to
frame 2100+ crash-free.
The HUD weapon/ammo-type icon (bottom-right, and bottom-left when
dual-wielding) stopped drawing ~2026-09-02; the ammo *numbers* still
render. Root cause: set_rgba_redirect_generate_microcode (gunfire.c, the
sole icon emitter) is non-void but has no return statement (the decomp
banner flags it). At -Og the tail call's result stayed in $v0 and it
worked; commit 7549570b restored -O2 for playtest builds, where the
undefined return register makes callers resume the DL from the icon's
start offset and overwrite the icon commands with the following
microcode_constructor / gunDrawHudInteger output.
Fix: add the return under AVOID_UB (already -D'd in the PC build;
CMakeLists.txt:207), N64 byte-match path unchanged under #else.
Codegen-correctness only, same class as the existing AVOID_UB edits.
Owed: build (-O2) + -level_09 capture diff vs docs/img/bunker1-2.png.
A/B: Debug build (-Og) shows the icon, -O2 build without this fix does not.
- tools_pc/bundle-linux.sh: package a Linux build (exe + README + licenses +
prepare-assets) as a .tar.gz. Mirrors bundle-win.sh's ROM / size guards and
prepare-assets assembly. No shared libs bundled — the generated README tells
the user to install SDL2 / zlib / libGL from their distro.
- tools_pc/dist/README.md.in: made OS-neutral via @PLATFORM@ / @EXE@ / @DEPS@ /
@LICENSE_EXTRA@ tokens; refreshed the known-issues list (dropped the stale
"ladders don't work" line — D177 fixed M-36; added black sky + missing HUD
weapon icon).
- .github/workflows/ci.yml: linux-build now packages + uploads the Linux
bundle; the release job attaches both platforms to the draft pre-release.
- .github/release-notes.md: rewritten for the two-platform NTSC-U alpha with
the prepare-assets step and current known issues.
Alpha scope: NTSC-U only, one-time prepare-assets step (Part A launcher and
Part B native converter from release-dropin-rom-plan.md remain follow-ups).
Claude-Session: https://claude.ai/code/session_01V4pK3pT83YnPsBp3Ey4RP2
Drop `paths-ignore` from the `pull_request:` trigger. With it, a docs-only
PR never triggered the workflow, so the required `validate` / `windows-build`
checks stayed "expected" forever and the PR could not be merged without an
owner bypass (M-44 hit this on #14/#15).
The workflow now always runs on PRs; the `changes` job diffs the PR against
its base sha (mirroring the existing branch-push logic against
`github.event.before`) and, for a docs-only change, reports the build jobs
as skipped — which branch protection accepts as success.
Claude-Session: https://claude.ai/code/session_01V4pK3pT83YnPsBp3Ey4RP2
gbi.h's gDP* DL macros use _SHIFTL/_SHIFTR without defining them; game TUs
pull them in via <ultra64.h>. This is the first port/src TU to build a
display list, and on GCC/ld the missing macro became an undefined symbol
(MinGW's include chain happens to provide it). Define the two pure macros
locally, verbatim from PR/mbi.h -- including mbi.h itself drags in N64 OS
headers that shadow libc.
- Fix invisible text: drawText passed the measured text w/h into textRender's
width/height args, which are the on-screen CLIP rect (not the text size), so
every glyph failed the clip test. Pass the full 2D viewport like bondview2.c's
debug-text path.
- Mouse UI: hover-to-highlight; left-click in the value column toggles/cycles;
drag sliders; red X close box (clicking a row's label only focuses it, so
selecting can't flip a toggle). inputSuspendForOverlay() forces the OS cursor
free + visible while the overlay owns the mouse.
- New rows: Fullscreen (live) and Resolution (windowed presets filtered to the
desktop, live resize + recenter). SDL window ops are posted from the
scheduler thread and drained host-side in videoPumpEvents.
- Layout recomputed for the wide BankGothic font: right-aligned values, wider
row pitch, all 13 rows fit the ~240-unit 2D space; nothing clips.
Port-layer only. Links clean; runtime-verified on a real display (front end +
-level_09): layout, hover/click/drag, live VSync/filter/fullscreen/resolution.
The M-37 scratch note (D176a-sky-rootcause.md) was never committed. Re-derive
the complete format from src/game/sky.c and include/PR/gbi.h:
- skyRenderTri/skyRenderFull emit a verbatim N64 RDP triangle command
(2-word header + 3 edge-slope pairs + 16-word shade block + 16-word
texture block) chopped into gImmp1(G_RDPHALF_1/_CONT) pairs, terminated
by G_RDPHALF_2. GE's RSP ucode DMAs it straight to the RDP.
- sub_GAME_7F097388 already yields complete screen-space verts; the
projection math is endian-clean and is not the defect.
Two implementation paths documented with a recommendation (Path B: an
#ifdef PORT emit swap in sky.c, first; Path A: an RDP-tri decoder in
gfx_pc.cpp, as the long-term answer). Word layout cross-checked against
gbi.h packing conventions.
sync_framerate_with_timer() runs inline on the scheduler thread
(osSpTaskStartGo -> gfx_run -> swap_buffers_begin), so a low frame cap
blocks VI-retrace delivery and drags every game thread down to the cap
rate rather than just dropping presented frames. There is no N64 analogue
(RSP/RDP/VI are separate silicon; the CPU never waits on them).
Clamp 0 < FpsCap < 30 to 0 (uncapped) with a warning, at both:
- videoInit, so a bad ge007.ini value (e.g. dinged to 10 via the F10
options overlay) is normalised and persists sane on next configSave();
- gfx_sdl_set_target_fps, the fast3d chokepoint, which also covers the
overlay's live set path.
Caps >= 30 are unchanged. Default (FpsCap = 0) is byte-identical to
golden (-level_09 framediff 3/3). The real fix — moving frame pacing off
the scheduler thread — is documented as owed in findings.md D186.
- findings.md / GRAPHICS-BACKLOG.md: -level_22 (Statue, night) is a clean
no-input headless repro of the D176(a) black sky — bare boot, frame ~360,
upper half pure black. Best verification target for the RDPHALF sky-tri
decoder. Note -level_36/-43 intro cams face the terrain (not a sky test
without GE_INPUTSCRIPT); -level_29 top-of-frame is dim, not black.
- GRAPHICS-BACKLOG "D74 wrap-block": struck through — the dead-code / OOB
in-vertex-loop wrap block was reworked and hoisted to a per-texunit
pre-wrap window behind Video.WrapFix at M-30 (RC3/D167). No longer latent.
building.md listed only d43 + d69; d88 (per-level Usetup*Z stage setup,
appended to the pccg dir) was omitted, so a source build per these
instructions produced a game that crashed on first level load.
The non-Debug branch unconditionally appended -Og, which (last -O wins)
overrode RelWithDebInfo's -O2 for every TU. -Og is a ~10% frame-time
regression on the software RSP/rasteriser hot path and is not what a
release/playtest build wants. Restrict -Og to explicit Debug builds.
Note: does NOT fix the ~1 fps issue under investigation — the software
renderer is ~950 ms/frame at both -Og and -O2 on this box; this is just
the correct default.
portHeartbeatCheck fired on any >3s frame gap and re-fired every 2s,
spamming ge007.crash.log with thread dumps during gdb pauses, level
loads, and SIGTERM teardown on a slow/software-GL setup. Widen to 8s and
stop after 3 reports. A genuine hang is permanent so it still gets caught.
CMakeLists.txt: CMAKE_{C,CXX}_COMPILER_LAUNCHER=ccache when found (no-op
otherwise); game sources are frozen so branch-switch rebuilds get near-free.
build-pc.sh: BUILD_DIR=${BUILD_DIR:-build-pc} so parallel agents/worktrees
can use separate build dirs.
- gfx_opengl_start_draw_to_framebuffer: fb_id 0 now binds GL framebuffer 0
(the window) instead of framebuffers[0].fbo, a generated attachment-less
FBO. Harmless on Windows (verified: -level_09 renders, mean RGB ~80);
strict GL (Mesa) treats the incomplete FBO as an error and drops draws.
- GE_PCDUMP / F12 screenshots move to a pre-swap hook (gfx_pre_swap_hook,
called from gfx_sdl_swap_buffers_begin before SDL_GL_SwapWindow). Reading
the back buffer AFTER the swap is undefined on buffer-exchange drivers
(Mesa/WSLg) -> black capture. Also: read GL_BACK explicitly, glFinish
first, GL_PACK_ALIGNMENT=1 (639*3 is not 4-aligned -> row shear), and log
any glReadPixels error.
Windows -level_09 verified: renders, no crash, capture non-black.
The no-PIE default base 0x400000 puts .data/.bss around 0x00500000-
0x00600000, colliding with the address space fast3d's seg_addr() decodes.
A game-built display list embeds &symbol (osVirtualToPhysical identity
shim); seg_addr sees w1 < 0x800000, treats it as a K0-physical DRAM
offset and remaps to 0x80xxxxxx (zeroed DRAM mirror) -> 'unknown GBI
opcode 0x00 at 0x80575d40' twice per frame -> DL walk aborts.
Move the image to 0x20000000 -- the gap between segmented addrs
(<0x10000000), the cart map (0x10000000), the D131 module range
([0x40000000,0x70000000)) and DRAM (>=0x70000000). seg_addr passes
0x20xxxxxx through untouched. Still <2GiB so -mcmodel=small relocations
are fine (0x140000000 needed mcmodel=large). ELF analogue of the Windows
0x140000000 base. D146 opcode-0x00 errors: 2/frame -> 0.
The N64 <stdlib.h> stub declares neither. MinGW's other CRT headers leak
them; a strict host GCC (Linux/macOS, -std=gnu11) does not, so getenv()'s
64-bit char* return is assumed int and truncated -> configGetFrameDump
(GE_PCDUMP path) dereferenced a low-32 pointer and segfaulted. Add K&R
decls: pc_protos.h (D38 mechanism -> game TUs via <ultra64.h>) and the
port/shim/stdlib.h non-Windows branch (-> port TUs that #include
<stdlib.h>). K&R form stays compatible with the real prototypes; malloc/
calloc/free are already covered elsewhere so not repeated.
First runtime bring-up on Linux (Ubuntu, gcc-14). -level_09 now renders
past frame 1200 crash-free with a clean shutdown, matching Windows.
- port/src/romdata.c: no-PIE loads the image at 0x400000, so the D34
'clear low 32 bits of a symbol address' gives 0 -> false 'image base not
4GiB-aligned' abort. The ANIM_DATA_* lvalues are only ever address-taken
(never dereferenced), so fall back to a fixed 4GiB-aligned sentinel
(0x100000000) for g_pc_animdata_base.
- port/src/main.c: the 'image must load at 0x140000000' assertion is
PE-specific (.refptr base relocations); ELF no-PIE resolves dram_syms.s
absolute symbols literally regardless of load address. Guard it to
PLATFORM_WINDOWS.
- port/src/libultra.c: mmap game-thread stacks with MAP_32BIT so they sit
in the low 2GiB. The decomp truncates stack-buffer pointers to u32 (e.g.
in texLoad); glibc pthread stacks live above 4GiB and
those truncations corrupt. One fix covers the whole idiom class. Windows
pthread stacks are already low.
- src/game/image.c: belt-and-braces #ifdef PORT -- align compbuffer at
uintptr_t width (identical result on N64).
- CMakeLists.txt: drop the -Ttext-segment experiment (needs -mcmodel=large;
breaks R_X86_64_32 relocs) -- comment only.
Windows: build 240/241, -level_09 crash-free past frame 1440.
crashDumpThreads was defined only in the PLATFORM_WINDOWS block; the
kernel-heartbeat watchdog in port/src/libultra.c references it
unconditionally -> undefined reference at link on Linux. Add a
PLATFORM_LINUX implementation: log the live thread roster + the watchdog
thread's own backtrace (a full cross-thread stackwalk needs signal-based
peer unwinding -- deferred). Linux-only block; Windows unaffected.
The decomp's src/sched.h (N64 OSSc* scheduler) is on the include path and
shadows the system header. libstdc++'s <thread>/<mutex> gthr headers in the
fast3d C++ TUs #include <sched.h> and need the real one (cpu_set_t,
sched_yield) -- with -std=gnu++20 libstdc++ defines _GNU_SOURCE so
<pthread.h> also references cpu_set_t. New shim: C++ -> generated
hostsched.h (absolute path, hoststring pattern); C -> #include_next so
src/sched.h still wins as before. Windows build verified green (240/241).
<stddef.h> is compiler-internal (lib/gcc/<triple>/<ver>/include), not in
bin/../include on a native Linux GCC, so the MinGW-layout guess produced
/usr/include/stddef.h which does not exist -> fatal error in every fast3d
C++ TU. Ask the compiler with -print-file-name=include/stddef.h and fall
back to the old path. MinGW resolves to its own lib/gcc internal copy;
Windows build verified green (240/241).
- CMakeLists: CMAKE_C_EXTENSIONS/CXX_EXTENSIONS ON. Strict -std=c11/c++20
define __STRICT_ANSI__, which disables glibc _DEFAULT_SOURCE and hides
clock_gettime(CLOCK_REALTIME) etc. that port/src/libultra.c needs.
-std=gnu11/gnu++20 is GCC's default and what MinGW already used.
- port/src/{pccg,pcmodels,romdata}.c: local 'extern int snprintf(...,
unsigned long long maxsize, ...)' decls conflict with glibc's size_t one
(same bug already fixed in pc_protos.h in M-39); spell it size_t.
- libultra.c: drop the _GNU_SOURCE define (pulls cpu_set_t refs into
<pthread.h> without <sched.h>); the D60 POSIX stack-bounds diagnostic
now just prints n/a rather than needing pthread_getattr_np.
Windows: full rebuild 240/241, -level_09 crash-free past frame 1740.
- pc_protos.h: the vestigial 'long long time()' prototype conflicts with
glibc's time_t time(time_t*); no game code calls time(), guard it to
_WIN32 (MinGW shadows <time.h> with the N64 stub; POSIX gets the real one).
- n64stubs.c: don't redefine bcopy/bzero on POSIX -- glibc exports them
(via <strings.h>, size_t sizes); the local int-signature definitions
conflict. _WIN32-only now.
- libultra.c: #define _GNU_SOURCE on POSIX for clock_gettime(CLOCK_*) and
pthread_getattr_np; guard the D60 VirtualQuery committed-memory probe and
GetCurrentThreadStackLimits behind PLATFORM_WINDOWS with a
pthread_getattr_np-based stack-bounds fallback for the diagnostic path.
Windows build verified green.