mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-24 00:02:45 +02:00
4b51ded7d1991097ad1841f000b38e14a50bca09
36
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4b51ded7d1 |
docs: SEO round 2 (Bing) - demote per-page h1 to h2 site-wide, re-trim meta description, bump README/Pages to v0.2.1
Bing Webmaster re-flagged the site after the first SEO pass ( |
||
|
|
436484a926 |
docs: final v0.2.0 pre-tag pass
- Drop-in ROM wording everywhere (README quick start, Pages landing, release notes, bundled README): first launch auto-generates the derived assets via the frozen converter; no Python/toolchain needed. PAL/JP marked not-yet-supported with issue #85 pointers. - building.md: Linux build is CI-compiled and ships in the bundle (was 'never built or run on Linux'); bundle note now describes the frozen ge007-convert auto-spawn; PAL/JP sidecar caveat (D258). - Status rewording across README/Pages/release notes: drop the 'first open-source port to our knowledge' claim; cutscene glitch description aligned to 'mostly with James Bond'; index.md status no longer contradicts the open D255 Deck crash. - Em-dash cleanup across all GitHub Pages docs (index, building, internals, porting-notes, dev-process, agentic-development, release notes); internal dev logs untouched. |
||
|
|
70bc9d65b5 |
D250: getenv() caching fix -- root cause of the Dam fps drop (plus sysSleep unit fix, GE_D250 probe, perf.ps1) (#75)
* D250: fix sysSleep unit-conversion bug in frame-pacing; add GE_D250 lvlRender probe port/fast3d/gfx_sdl2.cpp sync_framerate_with_timer() computed the remaining frame budget in 100ns units (qpc_to_100ns) and passed it straight into sysSleep(), which takes microseconds -- a real 10x oversleep whenever that branch fires. Genuine bug, but only active when target_fps is nonzero (tied to Input/Video FpsCap); confirmed via live testing that it is NOT the active cause of the user's reported ~45-50fps with FpsCap=0/VSync=0, where this pacing path never runs. Added GE_D250 (src/boss.c, #ifdef PORT) to isolate lvlRender()'s DL-building cost (culling/animation/scene traversal) from gfx_run's already-measured software-RSP interpretation cost. Live test on Dam shows lvlRender averaging ~0.5-1.5ms -- also not the bottleneck. Documented in docs/dev/GE-ENV-PROBES.md (GE_D250 row). findings.md D250 update to follow after rebase. Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog * docs(D250): live A/B session -- MSAA/aniso/FOV/resolution/vsync all ruled out Live session with the user driving the game directly, confirmed the sim-healthy/render-sagging pattern from earlier idle testing reproduces under real play on Dam. One-at-a-time A/B via the F10 overlay ruled out MSAA, anisotropy, FOV, resolution (1280x960 fullscreen vs 640x480 windowed), and VSync (render never exceeds ~60 even uncapped) as the cause -- the cost is resolution/GPU-setting-independent. gfx_run (~6-10ms) + lvlRender (~0.5-1.5ms, via the new GE_D250 probe) together don't account for the real ~20-22ms/frame. Leading remaining suspect: the emulated N64 thread-handoff/scheduler round-trip (gesched.c/sched.c) -- real OS context-switch/wake latency standing in for what was free hardware handoff on N64. Needs a real profiler (Visual Studio / WPR) attached during a live bad-fps moment to confirm; ad-hoc per-call timers have now ruled out the two most obvious CPU call sites without finding the cost. Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog * D250: fix uncached getenv() in fast3d hot paths -- root cause of the fps drop A live WPR/xperf CPU trace (tools_pc/perf.ps1, new sister script to debug.ps1) with Microsoft public symbols resolved found the dominant render thread spending only 5.2% of its CPU time in the game's own code -- 43% was in msvcrt.dll, and getenv()/_getenv_helper_nolock alone accounted for ~50% of the thread's total CPU time. Root cause: port/fast3d/gfx_pc.cpp's import_texture() (runs on every texture bind) and several per-triangle/per-G_SETCOMBINE debug-probe checks (GE_DTEX, GE_TEXDUMP, GE_TEXRAW, GE_D229 x2, GE_D172 x2, plus GE_TEXDUMP/GE_PCDUMP in gfx_opengl.cpp) called getenv() uncached on every call, unlike the safe static-cached pattern already used elsewhere in this codebase. On a texture-heavy scene that's tens of thousands of locked, locale-aware Windows CRT environment lookups per second -- invisible to both of the timers added earlier this session (gfx_run's own cost measurement and the new GE_D250 lvlRender probe), since neither isolates CRT/library time from the game's own code. Fix: cache every getenv() site in gfx_pc.cpp/gfx_opengl.cpp with the established static-int pattern. Narrow, mechanical, no logic change. Verified: the exact idle-Dam test that was pinned at 45-50fps before the fix now holds a rock-steady 60-61fps, zero dips. Also explains why the earlier MSAA/aniso/FOV/resolution/vsync A/B pass all showed "no change" -- the getenv() cost was a large per-call constant that swamped any real, smaller resolution-dependent GPU cost at every setting tried. Residual, separate issue found once this was fixed: still runs badly at 1440p+ (640x480 now holds a clean 60fps) -- likely the real GPU fill-rate cost the getenv() bug was masking, now exposed on its own. Not yet investigated. docs/dev/findings.md D250 updated with the full root-cause writeup. docs/porting-notes.md gets a new lesson under section E (cache getenv() in any hot-path probe). tools_pc/perf.ps1 automates the trace-and-analyze workflow for next time. Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog * docs(D250): correct the 1440p+ follow-up -- it was a stale-build false alarm User re-tested 1440p/4K on the actual getenv-fix build and confirmed it holds up fine too. The "still runs badly at 1440p+" report was from before the fix was live, same stale-build class as the DLL-load false alarm earlier in the session. D250 is fully resolved across all resolutions tested, not just 640x480. Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog * docs: add missing GE_D219RAW row to GE-ENV-PROBES.md (CI validate fix) Pre-existing gap unrelated to this branch's own changes -- gen_env_probes.py --check caught it now since it runs on every PR. GE_D219RAW has had live getenv() sites in src/game/image.c since the D219 investigation but was never added to the probe doc. |
||
|
|
52477b44af |
D248: fix port-wide 30fps halving -- pointer-width scheduler ABI bug
osScAddClient() stashes a per-client "every retrace vs every other retrace" flag in the next array slot's .next field (c[1].next = next); __scHandleRetrace read it back via a hardcoded 8-byte offset that only lands on the right memory when OSScClient is 8 bytes (32-bit pointers). On this 64-bit port the struct is 16 bytes, so the read landed inside the gfx client's own msgQ pointer (always non-NULL) instead, silently collapsing the gfx client onto the audio client's 30Hz path even though the VI-retrace pacemaker itself was always correctly ticking at 60Hz. Fix indexes the struct (client[1].next) instead of hand-rolling the byte offset, so it scales with pointer width on either platform -- same class as D122/D126/D132/D209, just in the scheduler instead of a ROM-serialized struct. Verified via the Video.DisplayFPS overlay: 30 FPS pre-fix, 60 FPS post-fix, same build/scene (-level_09). -level_09/-level_20 both crash-free post-fix. |
||
|
|
dac4ae7c0c |
D228: fix white NPC-weapon-texture patches -- IA16 palette intensity/alpha byte-order bug
GE_TEXDUMP on Facility (-level_34) found ~14% of LOD-textured CI uploads whose palette is IA16 (intensity-alpha, not RGBA16) rendering solid near-white. A GE_D228 correlation probe ruled out the texpool/RC2-clip hypothesis this row started with -- decoded size exactly matched the base tile every time, so the data reaches import_texture fine. The real bug: palette_to_rgba32's G_TT_IA16 branch read intensity = palentry & 0xff (low byte), alpha = palentry >> 8 (high byte) -- backwards relative to the already-correct direct-IA16 decode (import_texture_ia16) a few lines above it in the same file, which reads intensity from the first byte (high, after gfx_dp_load_tlut's PD_BE16 swap) and alpha from the second. A dim, fully-opaque palette entry (e.g. intensity=0x58, alpha=0xff) decoded backwards as bright and mostly-transparent -- the white-patch symptom. Fix: swap the two lines to match the working convention. User-verified live: AK47 and other metallic guns render correctly. Headless -level_34 run crash-free pre-verification; a full sweep is still owed (the user's own game instance was running concurrently this session, so verify.sh/taskkill were avoided). Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog |
||
|
|
a088bb51e7 |
D195: restore G_TEXTURE_GEN envmap UV generation -- fixes black reflective materials game-wide
GE uses real N64 RSP environment-mapping (G_TEXTURE_GEN) for every shiny/reflective material: gold/silver weapon skins, chrome trim, and Control's reflective console glass. An earlier session (D72.1) removed the PD-inherited envmap UV-generation block wholesale from gfx_sp_vertex to fix the Rareware logo, reasoning "GE never generates texture coordinates from vertex normals" -- true for the logo (lit, but doesn't set G_TEXTURE_GEN) but false in general. Every envmap draw was left sampling whatever raw tc[] happened to be authored (near 0), landing on a fixed corner texel of the reflection texture regardless of view angle -- reading as solid black for a typically dark-edged reflection map. Restored the envmap UV generation (ported from the reference Perfect Dark PC port's already-correct implementation), gated specifically on rsp.geometry_mode & G_TEXTURE_GEN so D72.1's actual fix is untouched (the logo never sets this bit) while every other envmap draw gets real reflection coordinates back. User-verified live: console glass and gold/silver guns are now visibly reflective instead of flat black. The remaining "not translucent" complaint on the glass is confirmed NOT a bug -- it was never see-through on the original N64 either (reflective by design, not transparent). Full 21-level sweep crash-free post-fix. Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog |
||
|
|
1f7f804733 |
D247: close as NOT A BUG -- confirmed faithful N64 TV-safe letterbox; D246 narrowed
Display pipeline pass on the two remaining M-116 QA reports. D247 (black bars top/bottom): traced to bondviewGetCurrentPlayerViewportHeight() returning VIEWPORT_HEIGHT_DEFAULT (220 of 240 NTSC scanlines, fr.h) for the decomp's own default SCREEN_SIZE_FULLSCREEN option -- unmodified ground-truth code, no #ifdef PORT anywhere near it. Verified against both the existing save and a fresh EEPROM (rules out a stale Screen=Wide/Cinema setting from earlier sessions) -- same inset either way. This is GoldenEye's real TV-safe-area letterbox, present on original hardware. No code change. D246 (left/right edge strips): GE_PCDUMP captures of the native render show clean edge columns, ruling out an RDP/game-DL scissor bug and the D227/D245 tc-overflow class -- the defect must be introduced later, in the SDL window presentation path GE_PCDUMP bypasses (MSAA resolve blit or a live-window scale remainder). Still needs the user's window size/MSAA setting to chase further; documented as a narrowed OPEN with concrete next steps. New porting-notes.md entry: check the decomp's own constants before assuming a rendering "defect" is a port bug. Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog |
||
|
|
9b0acccd1d |
D229: fix green/pulsating IsWater water -- fast3d wasn't expanding CI8 indices
texSelect loads the water image as ZLIB+CI8 (raw 8-bit palette indices); GE's custom RSP ucode expands them to RGBA16 in TMEM at load time, so a later RGBA/16b tile re-declaration over the same slot samples real blue pixels. fast3d has no such expansion and read the raw index byte-pairs as 16-bit texels (g dominates -> green mottle); the "pulsing" was always the pre-existing PRIM_LOD_FRAC sinf() cross-fade between two mis-decoded tiles. Track each TMEM load's source G_IM_FMT in LoadedTexture.src_fmt; in import_texture, an RGBA/16b tile over a CI-sourced slot now routes through the CI8 palette import path instead. Fires only on the genuine format disagreement, so real RGBA16 textures are untouched. Verified live on Frigate (-level_26): water renders blue, user confirms "looks better". Sweep + Surface 2 by-eye still owed. Also files D245 (water seam), D246 (edge strips), D247 (black bars) from the same playtest session. Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog |
||
|
|
9137f8f44b | docs(D219): flag the unverified 16-bit texnums in porting-notes for future triage | ||
|
|
b1cf0eac4c |
D219: scope the fire-particle byte-order fix by pixel format; resolved
M-115: re-apply M-113's store-time bswap scoped to the 16-bit GBI-size family only (PORT_PIXEL16 = bswap16 under #ifdef PORT; PORT_PIXEL32 stays identity). The fast3d importers disagree on pool byte order: import_texture_rgba16/_ia16 read manually big-endian bytes, while import_texture_rgba32 does PD_BE32() on a native load -- M-113's blanket swap hit both and regressed muzzle flash/ammo HUD (M-114). Format is the structural discriminator, not compmethod (fire frames span LOOKUP/RLE/HUFFMANBLUR/RLELOOKUP) or texnum range. Offline census of all 2698 ROM images (scratch/d219_headers.py / d219_census.txt): 94 non-zlib wide-pixel; the 16-bit family is IMAGE_FIRE_0..14 + texnum 1198-1201/2430/ 2510-2523, every ammo/flare/crosshair/muzzle-flash image is RGBA32. Runtime-verified on Dam (scripted fire): all 15 frames decode format=1, pool dumps warm R-dominant (mean opaque RGB 255,238,139). RGBA32 pool bytes bit-identical to the M-114-revert build. User playtest confirmed; D219 closed. GE_D219RAW probes generalized (no texnum filter), inert. |
||
|
|
7e99344741 |
docs(D148/D160/D173): re-confirm propDef stride ruled-out; rule out PadRecord.stan and act_anim aliasing for puppet-position bug (#61)
Investigation pass on the cutscene-reliability cluster (D148/D160/D173), part of the 1.0 acceptance-bar #4 push. No build/runtime toolchain was available in this worktree (no gcc/cmake on PATH, MSYS2 MINGW64 not active), so this is a static-analysis-only pass; no code changes. D160 (Dam end-cutscene / D148 label): independently re-derived (not just re-cited) that hypothesis (a), a propDef sizepropdef() stride desync, is ruled out — CutsceneRecord (propDef type 46) is all-scalar, 28B, zero pointer/union fields, so the D122/D126/D132/D209 pointer- width-growth class cannot apply. Confirms the existing M-31 audit recorded in porting-notes.md. Hypotheses (b)/(c) (AI trigger flow / invisible puppet render) remain unconfirmed — still need the shipped GE_D160=1 trace driven live. D173 (third-person Bond puppet floats / spins in cutscenes): checked the two most D209-shaped candidates for a puppet Y-position aliasing bug and ruled out both. (1) PadRecord.stan is a ROM-serialized pointer field zeroed by tools_pc/d88_emit.py's emit_pad() with a "dead-on-load" comment; verified against the N64 source asset tables (UsetupdamZ.c) that every PadRecord literal initializes stan to 0 on N64 too, so the PC zeroing is byte-identical to ground truth, not a converter bug. (2) The anim-root-motion Y-accumulation chain was already exhaustively audited during D193/D209 (M-80/M-81) and the padding[]-union sweep that found D209/D210 explicitly cleared act_anim/act_dead as layout-stable. Root cause not found; the D209-style struct-aliasing pattern does not appear to be the shared explanation here, unlike the hope stated in the dispatch brief. Flagged next steps: a live Cradle capture, and locating the actual two-actor Cuba end-credits code path (not found this session). Added a generalizable porting-notes.md quirk: before assuming a zeroed ROM-serialized pointer-width tail slot is a converter miss, check the N64 source literal — some fields are legitimately always NULL on both platforms. Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG |
||
|
|
650ac57fc6 |
fix(D233): fast3d trivial-reject drops visible triangles with a behind-camera vertex (#63)
gfx_sp_tri1's CPU-side trivial-reject AND-test trusted per-vertex outcodes (x<-w / x>w / y<-w / y>w / z>w) that are only valid half-space tests when w>0. A vertex that has crossed behind the camera plane (w<0 -- common when the camera is close to a large room-structure polygon, e.g. walls/ceiling trusses near a doorway) flips the comparison sense, so its outcode can come out wrong. When that spurious bit happens to match the other two genuinely correct vertices' bits, the AND-reduction fired and dropped the whole triangle before it ever reached OpenGL -- even though GL's own homogeneous clipper (which handles w<0 correctly) would have rendered the visible part of it. This reads exactly like D233 (indoor geometry intermittently vanishing near doors while props/furniture keep drawing): large near-camera polygons are far more likely to have a vertex cross behind the eye than small self-contained prop meshes, and the backface-cull code six lines below already special-cases this same w-sign hazard for its own cross-product sign -- the trivial-reject block above it never got the same treatment. Fix: skip the trivial-reject AND-test whenever any of the triangle's three vertices has w<0, deferring to GL's clipper instead. This can only ever send MORE triangles to the GPU than before, never fewer, so it cannot make anything newly invisible. Also investigated D234 (Dam guard towers, only the first renders) under the same M-106 hypothesis that it shares a root cause with D233/D222 -- did not find fast3d-side evidence for it; left open in findings.md with the ruled-out mechanisms and a next-step recommendation. D222 (FovScale breaks culling) is a separate, already-characterised game-logic bug in src/game/fr.c/bondview2.c, out of this fast3d-only fix's scope. Build verified green (MSYS2 mingw64 cmake+ninja, full link succeeds). Could not visually verify -- this worktree has no ROM/data and no interactive display; see docs/dev/findings.md D233 entry for the exact human verification steps (GE_PCDUMP on a doorway-heavy interior). docs/dev/findings.md: D233 (FIXED, high confidence on mechanism/unverified visually) + D234 (OPEN, not root-caused) entries added. docs/porting-notes.md: generalised the w<0 trivial-reject hazard as a recurring class for any future CPU-side "skip this triangle" heuristic. docs/dev/findings-index.csv regenerated. Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG |
||
|
|
646798be56 |
docs(M-102): fix findings-index false-positive, release <version> bug, land parked site cleanup (#53)
* docs(M-102): fix findings-index false-positive, release <version> bug, land parked site cleanup
Docs triage pass (docs/dev/notes/PROMPT-docs-triage-session.md).
- gen_findings_index.py: D194's row was silently bucketed FIXED (an open
design item) because it had no distinct status cell, so the generator's
fallback keyword search matched "fixed" somewhere inside 6,500 chars of
narrative -- the same misleading-index failure mode that cost a session
on D227 (see docs/dev/findings.md SS H "D227 -- M-98"/"M-99"). Gave D194
a real status cell. Also extended LEAD_RE/bucket() with 5 verdict tokens
already in use in the doc (ANALYZED, GUARDED, DIAGNOSTIC SHIPPED,
ROOT-CAUSED, NOT A BUG) and added a leading verdict word to 4 more rows
that had a status cell but no recognized token -- fixes all 10 rows the
generator previously bucketed "?" (D175 D174 D173 D220 D160 D133 D136
D156 D153, plus D194). findings-index.csv regenerated; --check passes;
diff reviewed, only these 10 rows moved.
- ci.yml: release-notes.md's <version> placeholders were never substituted
before `gh release create --notes-file`, so every published release body
(including the live v0.1.0 release) ships the literal text. Now sed'd to
the tag first.
- Landed docs/dev/notes/UNLANDED-docs-site-cleanup-steps3-5.{md,patch}
(parked M-101, never pushed, branch deleted): deletes the orphaned
docs/StructureGuide.md (0 tracked references), adds title/description
frontmatter to 5 published Pages docs, rewrites docs/index.md's stale
"Phase 2 of 4 / no audio yet / 19-of-21" status text to match the
current README. The 2 files whose hunks no longer applied via `git am`
(docs/index.md, docs/dev/agentic-development.md) were redone by hand
against current main.
- docs/BACKLOG.md (local, gitignored): retargeted a dead
docs/PORT-LEARNINGS.md reference to docs/porting-notes.md.
Larger triage items (findings.md monster-row convention, full
status-accuracy sweep, duplicate-defect clustering, dev-notes plan
triage, HANDOFF rotation policy) scoped but not started -- see
docs/dev/notes/FOLLOWUP-docs-triage-session.md (gitignored).
Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
* docs/index.md: fix stale in-level-music claim before merge
This PR predates the M-102c/M-105 correction (already landed in the
README via PR #55): in-level music is NOT absent, it plays with some
tracks having wrong-sounding instruments / occasional garbling (D77
fixed, D230 filed for the residual). This branch's own rewritten Status
paragraph still said music 'doesn't yet' play -- fixing it to match
before merge so main doesn't briefly regress to a stale claim this
session already corrected elsewhere.
|
||
|
|
741dfe78b9 |
fix(D227/D176a): sky texcoords are S10.5, not texels — fixes seam, over-tiling and scroll speed (#43)
* fix(D176a): sky scroll/tiling too fast -- perspective-correct tc interpolation skyPortRenderPoly (Path B, D176(a) black-sky fix) placed sky quad verts under a plain w=1 ortho projection, so fast3d's GPU pipeline interpolated texture coords linearly in screen space across the primitive. The source S/T data (unk20/unk24) is computed upstream from a world-space projection without a perspective divide -- exactly like the real N64 RDP's G_TRI_SHADE_TXTR coefficient block, which relies on real perspective- correct interpolation to divide it back out per pixel. A single sky quad's near (top) and horizon-grazing (bottom) vertices can differ up to ~38x in camera-space w. Under linear interpolation that smears the entire S/T delta evenly across every pixel instead of concentrating it near the horizon, producing a uniformly dense aliased 'hatching' over the whole sky -- reported as 'scrolls too fast / tiles too visibly'. Fix: build a custom projection matrix that gives each vertex its real camera-space w (already computed upstream, SkyRelated38.unk0c) while keeping the same screen-space x,y, so the GPU's normal perspective divide recovers the intended position but interpolates tc (and colour) with real 1/w weighting -- the same pipeline every other textured draw in the game already depends on. No port/fast3d changes; #ifdef PORT only, N64 path untouched. Verified: tools_pc/verify.sh bunker1 PASS (no regression on a non-sky level); -level_22 (Statue) and -level_41 (Cradle) GE_PCDUMP captures show the expected before/after change (uniform parallel aliasing -> a proper perspective-converging fan), and Cradle's sky is no longer black at default FOV. Defect 2 (horizon-band tessellation) and the FOV-coupled black-sky-past-frustum symptom are unchanged / still open. docs/dev/findings.md: new D176(a) subsection + summary-row update. docs/porting-notes.md: generalized the screen-space-overlay quirk to call out the w=1-vs-real-w interpolation pitfall. * chore(D176a): regen findings index * docs(D176a/D227): M-93 - human playtest of PR #43 confirms Cradle black-sky fix + partial Statue scroll improvement; new split-sky residual filed as D227 * docs(D227): M-94 - visual evidence + a concrete mechanistic hypothesis for the sky seam (per-primitive wScale quantization), not yet fixed Headless -level_22 capture (this shell can do offscreen GE_PCDUMP capture via the game binary directly, run from the repo root with the MSYS2 mingw64 PATH set -- the earlier GUI-launch flakiness in this session was specific to a live window, not this path) shows a real, static-frame crease in the sky's cloud texture. Screenshot committed: docs/img/bugs/d227-sky-seam-statue.png. Hypothesis: skyPortRenderPoly (D176(a)'s fix) computes wScale per call, and skyRenderTri/skyRenderFull issue up to 5 separate calls per frame (one per triangle in the sky fan). The position math is exact for any wScale (re-verified algebraically), but Vtx.ob[] is s16, so two triangles sharing an edge can quantize a logically-shared vertex to two slightly different values depending on which triangle's independently- chosen wScale computed it -- crack at the shared edge, reads as "two regions with a different pattern." Not yet confirmed (didn't isolate camera motion from sky-scroll motion to test the "moves at a different rate" half of the report) and not fixed -- documented the fix direction (one wScale shared across the whole fan, not recomputed per triangle) for whoever picks this up next. Cleaned up capture scratch files after pulling the one evidence shot. * docs(D227): M-94 addendum - static-camera test confirms the sky genuinely animates over time, independent of camera motion * fix(D227): M-95 - one shared wScale across the whole sky fan instead of one per triangle, closes the visible seam skyRenderTri/skyRenderFull draw up to 5 triangles per frame from a shared pool of up to 5 vertices (built once, then fan-triangulated across 1-3 calls). D176(a)'s fix had each call compute its own wScale from only its own 2-4 verts; two triangles sharing a vertex could each pick a different wScale, and since Vtx.ob[] is s16, quantize that logically-shared vertex to two different values depending on which triangle's call computed it -- a crack at the shared edge (D227, the "two sky halves" report). Fix: skyPortBeginFan()/skyPortEndFan() (new, #ifdef PORT) compute one wScale across the whole fan and skyPortRenderPoly uses it when set, falling back to the old per-call computation otherwise. N64 path unchanged (these are PORT-only additions around existing #ifdef PORT code, no #else branch touched). Verified: - Clean build, no new/duplicate symbols. - Full verify.sh sweep: 21/21 PASS, both with and without this change. - Same-frame before/after screenshots at -level_22 frame 345 (docs/img/bugs/d227-sky-seam-statue{,-fixed}.png): visible kink in the cloud streaks in the before shot, gone in the after shot, same exact frame. - bunker1's nonzero framediff worst_cell is unaffected by this change (reproduced identically pre-fix) -- confirmed pre-existing stale golden-baseline issue, unrelated to sky.c. - Cradle's black-rectangle artifact is also confirmed pre-existing (reproduced identically pre-fix) -- the separate, already-documented FOV-coupled black-sky-past-frustum-edge symptom (D218/D222 family), not touched by this fix. Not re-verified: whether the "moves at a different rate" half of the original report is also resolved, and the aspect-ratio-artifact alternate theory was never independently ruled out. Human eyeball still owed before merge, same as every other rendering fix landed this session without interactive display access. Also generalized the quirk (per-call vs per-shared-vertex-group scale quantization) to docs/porting-notes.md alongside the existing D176(a) entry. * docs(D227): M-96 - human playtest on Statue: seam/crack STILL PRESENT despite M-95 fix The M-95 wScale-sharing fix only verified the static per-primitive quantization theory (single-frame screenshot diff); it explicitly left the 'two halves move at different rates' temporal theory from the original report unverified. This playtest is that missing verification, and it says the seam is still visible in live play. D227 NOT resolved. PR #43 stays in draft pending investigation of the temporal/rate theory -- do not merge on the M-95 fix alone. * docs(D227): M-96 addendum - N64 sky should be near-static, this build scrolls extremely fast New data point from the same Statue playtest session, separate from the seam/crack: the user reports the intended N64 scroll rate is near-static, stronger than M-93's 'still reads too fast'. Bears on D176(a)'s fix magnitude itself, not just the D227 seam -- flagged as a possible shared root cause (wrong per-frame tc delta could drive both symptoms), not yet investigated. * fix(D227): M-98 - share sky S/T fold across whole fan, not per-triangle skyPortBeginFan() (M-95) shared wScale across a whole sky fan so shared vertices quantize position consistently, but skyPortRenderPoly's S/T fold (foldS/foldT -- floorf(minS/64)*64, keeps texel coords in s16 range) was still computed per-call, from only that triangle's own 2-4 verts. Same bug class as the wScale fix, one field over, never fixed -- which is why M-96's human playtest still saw the seam after M-95. Two triangles in one fan can floor a shared vertex's S/T to different multiples of 64 texels; nominally invisible (GL_REPEAT period 64), but each triangle bakes (S-fold)*32 into its own s16 Vtx buffer, so two folds 64 texels apart produce tc values 2048 apart -- a real crack at the shared edge. Fix: extend skyPortBeginFan()/skyPortEndFan() to also compute one shared fold across the whole fan, threaded into skyPortRenderPoly the same way as wScale. #ifdef PORT only, no game-logic change. Also measured (per M-96/M-97's ask): cross-correlated 11 consecutive frames at a static camera -> ~7 px/tick horizontal scroll at 640px capture width, a full screen-width sweep every ~1.5s. Confirms "extremely fast" quantitatively; root cause of that magnitude is not yet found (accumulator + vertex geometry are unmodified N64 logic, so if it's genuinely wrong the bug is in how the Path B substitution maps texel-ticks to screen pixels, not the tick rate itself -- next step spec'd in findings.md). Verified: build clean; -level_22 frame 345 (same frame M-95's before/after used) shows no visible crease vs. the pre-M-95 reference screenshot; verify.sh bunker1 PASS, worst_cell unchanged (known stale golden baseline, not a regression). Human eyeball still owed on both the seam-close and the scroll-speed question before PR #43 leaves draft. * docs(D176a): M-98 - Cradle side of PR #43 checked; tower starburst is Defect 2 Checked the Cradle side of the D227 fold-sharing fix (M-97 left this outstanding). No regression -- verify.sh cradle PASS -- but found a much more visible pre-existing artifact while there: looking up the suspension tower near spawn, roughly half the sky renders as a sharp radiating starburst instead of clouds. Confirmed pre-existing (pixel-identical with sky.c reverted to pre-M-98) and confirmed static, not the D227 "moves at different rates" symptom (pixel-identical 30 ticks apart at a fixed camera). This is the already-known, already-open D176(a) Defect 2 (horizon-band tessellation) -- the code's own fold-logic comment already predicts exactly this failure shape for a triangle spanning >1000 texels near the horizon, which looking straight up the tower produces. Not fixed (tessellation is separate, bigger work); logged with a concrete repro + screenshot so it's no longer "inconclusive." * docs(D227): M-99 - human playtest negative (seam persists, view-angle-dependent); headless repro blocked Human playtest after M-98's fold-sharing fix: the seam is still present on Statue and now confirmed to move/change with mouse/view position -- also reported still bad on Cradle and present on Surface. M-98's fix was real but is not the dominant mechanism live play is hitting, same pattern as M-95 before it. Ruled out this pass: the cross-quad water/cloud texture-scale mismatch theory, on the two IsWater==0 levels actually tested (Statue, Cradle) -- untested on any IsWater==1 level, which is now the top ranked candidate for next session. Blocked on: headless camera-pan repro via GE_INPUTSCRIPT does not work -- confirmed neither analog-stick nor C-button tokens rotate the camera, so this needs either a human playtest or a debug hook into the real mouselook path before it can be chased further headlessly. Full ranked candidate list + environment notes for next session in findings.md and (session-local) docs/HANDOFF.md. * docs(D227): M-100 - root cause PROVEN: s16 texture-coordinate overflow The sky seam is not a crack, not a quantization mismatch, and not view-dependence. One sky quad spans ~30,000 texels in S and T, but Vtx.tc is S10.5 in an s16 = +/-1024 texels, so 7 of its 8 texture coordinates overflow and wrap to arbitrary values. The fold cannot help against a span 29x the representable range. Unblocked by a user-supplied emulator-vs-ours screenshot pair on Surface: the real symptom is a radial starburst, not a crack, which is a different mechanism from what M-94..M-98 were chasing. M-95 (shared wScale) and M-98 (shared S/T fold) each fixed a real second-order bug; neither could touch this one. Both stay - per-fan consistency is exactly what will make subdivision seamless. Measured with a new env-gated GE_D227V probe (GE_D176 style) on the Cradle frame-350 camera. All four w are positive, so no projective pole. The quad is split gSP2Triangles(0,1,3, 0,3,2) on the 0-3 diagonal, and vertices 0 and 3 are exactly the two that keep one coordinate in range - hence "two halves with different patterns, split by a line", the original D227 report verbatim. Apparent view-dependence needs no view-dependent mechanism: S/T depend on view, so the wrap point moves. Confirmed by a diagnostic build: baking tc at 1-texel instead of 1/32-texel granularity (32x range, deliberately wrong scale) makes the starburst and the split completely disappear. No second defect behind it. D227 == D176(a) "Defect 2" == M-98's "Cradle tower starburst". M-98 misfiled that capture as unrelated, which is what made D227 look headlessly-unreproducible in M-99 - it reproduces statically. Fix not implemented: subdivide until span < ~960 texels/primitive. The subdivision math and cut placement are worked out in the finding (S is linear in w, so cut uniformly in w), but the vertex budget is an open decision - dynAllocateVertices is an unchecked 50 KB/frame pool and pure tessellation wants ~40% of it. * docs(D227): M-100 addendum - RDP tile shift (option D) dominates; fast3d already implements it shifts/shiftt is the RDP's native precision-vs-range knob and gfx_pc.cpp implements it fully including the left-shift case. Emitting tc at 32/2^k units with shifts = 16-k buys 2^k range with no tessellation, no fast3d change and no vertex-budget cost. k=5 covers the measured 29,830-texel span outright. * fix(D227): M-100b - sky texcoords are S10.5, not texels; drop M-82's stray *32 Fixes the sky seam/starburst, the over-tiling, and the "scrolls too fast" complaint - all three were the same bug. M-82 ("D176(a) Defect 1") read unk20/unk24 as texel counts and baked tc as (S - fold) * 32 to convert texels into tc's S10.5 units. They are not texel counts. skyRender sets them as worldpos * 0.1f and the N64 path hands them to the RDP coefficient block unscaled (skyRenderTri: S * unk34 * sp368), so they are already in the RDP's native S10.5 1/32-texel units. That stray *32 made every sky texture coordinate 32x too large, producing three symptoms previously tracked as separate bugs: 1. D227 - spans of ~30,000 texels against an s16 tc holding +/-1024, so 7 of a quad's 8 coordinates overflowed and wrapped to arbitrary values. The quad splits on the 0-3 diagonal and verts 0 and 3 are exactly the two that keep one coordinate in range, hence "two halves with different patterns" - the original D227 report verbatim. 2. Over-tiling - ~466 repeats of the 64-texel cloud per quad, not ~15. 3. M-96's "the N64 sky is nearly static, ours races", measured at ~7 px/tick in M-98. g_SkyCloudOffset advances 1 unit/tick = 1/32 texel, not 1 texel. This is why M-98's re-derivation of the tick path kept coming back clean: the accumulator was never wrong, its unit was. Fix: tc = (S - fold) 1:1, with the fold rebased to a whole tile period (SKY_TC_WRAP = 64 texels * 32 = 2048 units). The ~30,000-unit span then fits an s16 unaided - which is also why the original N64 code needs no tessellation here: it never had a range problem to solve. Kept: M-95's shared wScale and M-98's shared fold (both real bugs; per-fan consistency keeps a fan's shared vertices bit-identical). Also kept as a safety valve, since only ~4% of s16 headroom remains on the observed span: skyPortPickShift/skyPortCaptureTile/skyPortEmitTileShift, which trade tc precision for range via the RDP's own tile shifts/shiftt (port/fast3d already implements it, so no fast3d change). skyPortCaptureTile replays the exact G_SETTILE texSelect emitted with only the shift fields patched, so it cannot drift from texSelect's format/line/mask derivation. Also adds the env-gated GE_D227V per-vertex probe (GE_D176 style) that measured all of this. Verified: -level_41 Cradle frame 350 (the M-98 starburst repro) now shows soft cloud bands at the correct scale with no starburst, diagonal split or moire; GE_D227V confirms kS=kT=0 and max tc 31,513 of 32,767. -level_22 Statue frame 345 clean. verify.sh bunker1 PASS (worst_cell 10.32, the known stale-golden-baseline number). Human play-test still owed. * D229: file green/pulsating water; decouple D227's tc safety valve from it New finding D229: on IsWater levels (Frigate, Surface 2) the water renders green and pulses blue<->green on an interval. Pre-existing, proven by A/B: built plain main with none of the D227 work and captured -level_26 frame 340 - same green, only the tiling density differs. D229 is independent of D227. The gun half of the same report was NOT a bug: the branch was 2 commits behind main and those 2 commits were exactly the D217 fixes (#47 texpool alignment, #49 CI cache palette-content key). After merging main the user confirms the gun is good on Frigate. Mechanism so far: the water quad is sky.c's IsWater block1 (Frigate confirmed IsWater=1 via GE_D176, which also settles M-99's ranked item 1). sub_GAME_7F09343C binds tile 0 and tile 1 to the SAME tmem, offsets tile 1 via gDPSetTileSize(1, 90, 150, 0, 0), and cross-fades TEXEL0/TEXEL1 with a PRIM_LOD_FRAC driven by sinf(). That sine is the reported pulsing, so one texunit resolves blue and the other green. Ruled out the shade path by measurement (it spans dark blue to pale yellow, never green; sampled pixels have R and B crushed). Not yet root-caused. Code change here is only the decoupling: skyPortBeginFan takes allowShift and the water quad passes FALSE, leaving that path byte-identical to its pre-D227 behaviour. The water quad is a two-texunit draw sharing one set of vertex tc, so a tc rescale needs a matching shift on both tiles - and shifting both makes them sample identically, which flattens the cross-fade. An intermediate build that did so turned the water from pulsating into constantly green, observed live. Also fixes skyPortCaptureTile to capture after all tile setup (not just after texSelect) and to handle both tiles, so a future shift cannot clobber a later re-declaration. Cradle sky re-verified unaffected (frame 350 still correct). bunker1 golden framediff reports REGRESSION at 55.02, but plain main scores WORSE at 73.24 - the baseline is stale relative to main's D217 texture fixes, the re-baseline owed since M-83/M-84. Not caused by this branch. * docs(D227/D229): regenerate findings index; mark D227 FIXED The generated findings-index.csv was hand-edited during the main merge and when D229 was filed, so CI's 'Check generated dev indexes are current' failed on PR #43. Regenerated with tools_pc/gen_findings_index.py. Also updates the D227 row's status to lead with FIXED (M-100b) rather than the M-94 'not yet fixed' text, so the index reflects the user-confirmed fix. * docs: register GE_D227V in the env-probe index CI's 'Check generated dev indexes are current' also runs gen_env_probes.py --check, which flags any GE_* probe with live sites that has no row in docs/dev/GE-ENV-PROBES.md. GE_D227V (added in M-100 to measure the sky texcoord overflow) had none. |
||
|
|
4d45e78176 |
docs: Pages site cleanup step 1 - curated publish set + cross-link fixes (#51)
* docs: step 1 of the docs/site cleanup - whitelist Pages site config, fix cross-links to excluded docs, drop game-behavior-reference index bullet Publish only the curated set (index/building/internals/porting-notes/ dev-process/agentic-development); everything else under docs/ stays in the repo but out of the Pages build and sitemap. Cross-links from the published pages to excluded docs now point at GitHub blob URLs so they still resolve. No content rewrite yet -- steps 2-5 (README update, per- page descriptions, light copy passes, repo hygiene) remain. Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG * docs(dev-process): add §6 -- keep PR/git-history noise low Squash-merge single-purpose/docs PRs, batch same-session docs updates into one branch instead of one-PR-per-finding, keep unrelated housekeeping off active branches, declare stacked-PR dependencies, and triage the open-PR list every session -- close stale PRs (superseded by newer main content) rather than force-merging or endlessly rebasing them. Prompted by this session's PR #26: real conflicts against main because D191/D193/D149 were all fixed/closed since it was opened; closed rather than rebased. |
||
|
|
199182d1d9 |
fix(D77): in-level music silent — missing return in sub_GAME_7F0C0BF0 (D6-class UB)
Root cause: src/game/mp_music.c's sub_GAME_7F0C0BF0() is a one-line wrapper around get_mTrack2Vol() with no return statement. On N64/IDO the value happened to already sit in $v0; at the PC release build's -O2 the return register is genuinely undefined (docs/porting-notes.md D6 class, same as D187). Every in-level music trigger goes through set_missionstate()'s MISSION_STATE_1/4 cases (the only path that starts a level's background track), which feeds this garbage/zero value straight into musicTrack1ApplySeqpVol()/musicTrack3ApplySeqpVol() right before musicTrack1Play()/musicTrack3Play() — silencing the whole track for its entire duration while note-on processing continues normally. Front-end/menu music bypasses set_missionstate() entirely (front.c calls musicTrack1Play() directly), which is why it was never affected. Fix: return the tail call under #ifdef AVOID_UB, #else keeps the N64 body verbatim. One line, no game-logic change. Diagnostic: added a temporary, cheap (~1/s, gated on GE_AUDIOTRACE) [AUDIOLVL] peak/RMS probe of the exact buffer audioSetNextBuffer() hands to SDL (port/src/audio.c), interleaved into the same audiotrace.log as the existing csplayer.c [MUSICNOTE] probe via the shared geTracePrintf lock. Before the fix: 173 [MUSICNOTE] events over a 28s BUNKER1 capture, but every single [AUDIOLVL] reading (29 samples spanning the full run) read peak=0 rms=0.0. After the fix: same note count, every [AUDIOLVL] reading sustained non-zero (peak ~2700-10300, rms ~800-2900). tools_pc/verify.sh bunker1: PASS, no visual regression. Docs: docs/dev/findings.md D77 updated from stale "nothing reaches PC speakers" framing to the actual root cause + verification; new D6 instance logged in docs/porting-notes.md. Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG |
||
|
|
d285a30725 |
docs(D217): M-88 - defect 1 is NOT a tools_pc/d43_emit.py sidecar bug
Dispatched to root-cause + fix D217 defect (1) (wrong palette bytes on model-DL CI textures) confined to tools_pc/d43_*.py, working theory: a 4-byte/2-entry model-TLUT source-pointer offset in the sidecar. Investigation refutes that theory: traced the full texture reference chain (ModelFileTextures TextureID -> Globalimagetable -> runtime texInflateZlib/NonZlib decode -> texWriteLoadToTmemAddr -> fast3d gfx_dp_load_tlut) and confirmed tools_pc/d43_emit.py never touches texture or palette content for model files - it only converts geometry/node/GDL structure and passes TextureID through unchanged. Reproduced the exact reported bytes with the existing GE_TEXDUMP env probe (no code changes): BUNKER1 pal[0]=0001 (black, correct) vs FACILITY pal[0]=1a00 (green, matches the originally reported defect) for the same tile=1x1 CI4 grip texture. Narrowed the likely real mechanism to port/fast3d/gfx_pc.cpp (gfx_dp_load_tlut palette-address pitch math) interacting with src/game/tex.c (texWriteLoadToTmemAddr's literal width=1 SetTextureImage) - both out of this dispatch's file scope, so no fix applied per the brief's own instruction to stop and write up rather than edit out-of-scope code. No tools_pc/*.py files modified. docs-only commit. Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG |
||
|
|
6b66c9ddc9 |
docs(D217/D211): M-86 — D217 root cause characterised; D211 aim-zoom verified
D217: two defects found. (1) The visible green grip/hand is upstream of fast3d — the grip's count=1 model G_LOADTLUT is fed different source bytes per level (Facility 1a 00 -> 0x1A00 green; Bunker 00 01 -> black); every larger model TLUT carries a 2-entry leading zero pad, so the port's model-TLUT source pointer is likely 4 bytes low (or a d43_emit.py blob offset). fast3d byteswap is correct (proven by Bunker). (2) A real CI texture-cache collision (no palette-content key) — fix landed on branch fix/d217-model-texture-palette f6fc0399 (palette FNV-1a hash in the CI key); owes a 21-level sweep; does not fix (1). porting-notes + graphics- backlog updated. D211: aim-zoom compose verified correct by construction (single post-lerp multiply at the fr.c chokepoint; zoom ratio preserved). Only a low-risk cosmetic eyeball remains (viewmodel/frustum-cull use unscaled fovy). Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG |
||
|
|
9a6017a784 |
D176(a): emit screen-space sky geometry on PC (Path B)
skyRenderTri/skyRenderFull emit the sky as a verbatim N64 RDP triangle command chopped into G_RDPHALF_* immediates, which fast3d deliberately no-ops -> Surface/Statue/Streets skies render solid black. Behind #ifdef PORT, substitute a screen-space gSPVertex + gSP1Triangle/ gSP2Triangles emit built from the already-projected SkyRelated38 verts (new helper skyPortRenderPoly, ortho matrices via dynAllocate*). N64 path unchanged under #else. Pure RSP-idiom substitution, no behavioural change. Links 243/243. Render check owed on a display: -level_22 frame ~360. findings.md D176(a) M-46; porting-notes.mdD. |
||
|
|
b47eea65e3 |
fix(D210): chrToPatrol leaves act_patrol.lastvisible60 uninitialised on PC
Sibling of D209 (A1 raw-byte union-alias class). chrToPatrol writes act_init.padding[0x13] (union byte 0x4c) = -1, which is act_patrol.lastvisible60 on N64. act_patrol starts with a patrol_path* that widens 4->8 on x86-64, so 0x4c now lands in waydata and lastvisible60 (@0x50) stays uninitialised. The patrol 'haven't seen the player recently' gate (chraction.c:~9347) then reads garbage on the first ticks after entering PATROL. Fix: write the named field under #ifdef PORT; N64 line verbatim under #else. ABI/pointer-width only, no game-logic change. Found by the post-D209 A1 sweep; the only new instance (act_anim/act_dead padding aliases are pointer-free -> layout-stable). Build clean; bunker1 + facility verify PASS (both patrol-heavy), no crash. findings D210 + porting-notes A1 + findings-index regen. |
||
|
|
a63efe184b |
fix(D209): AI locomotion locked to walk anim — union byte alias shifts at 64-bit
Root cause of D193 ("NPC AI locomotion is too slow", the user's biggest
playtest complaint; broke Cradle completability).
get_sound_at_range() (chraction.c:3573) is the locomotion-animation
selector: it picks ANIM_DATA_sprinting/running/walking from an arg1 speed
tier. Its only caller passed that tier as self->act_ubytes.padding[45] — a
raw byte alias into the ChrRecord action union rather than the named field.
On N64 offset 45 is exactly act_gopos.unk59:
act_gopos = {coord3d targetpos@0, StandTile *target@12,
waypoint *target_path@16, waypoint *waypoints[6]@20,
u8 curindex@44, u8 unk59@45}
At 64-bit the three pointer members widen 4->8B (target@16, target_path@24,
waypoints@32), pushing unk59 to 81 while the literal 45 lands inside
waypoints[1] (40..47) — byte 5 of a heap pointer. The arena is low-4GB
(0x00000000_70xxxxxx), so that byte is always 0x00: arg1 reads 0 forever
and every AI chr in the game was bound to ANIM_DATA_walking /
ANIM_DATA_walking_unarmed regardless of what the ailist commanded. GE
character travel is animation-root-motion driven, so all AI moved at walk
pace on every level. Bond is unaffected (explicit physics, no root motion),
which is the reported "player fine / all AI slow" split.
Fix: PORT-only read of the named field; N64 line kept verbatim under #else.
ABI/pointer-width only, no game-logic change (rule-2 narrow exception, D3x).
Both callers reach the site with actiontype == ACT_GOPOS, so it is the same
byte N64 reads.
Evidence (Cradle -level_41, GE_D193A extended with tier/aidx): all six chrs
report ACT_GOPOS tier=1 SPEED_RUN on 420/440 samples, while the live
model->anim reverse-resolves to animation_table_ptrs1 index 40
(PTR_ANIM_walking) for Trevelyan and 107 (PTR_ANIM_walking_unarmed) for the
guards on every sample; 42 (running) and 41 (sprinting) never appear.
Travel sat in a flat 140-155 u/s band for all of them.
User playtest-verified on the fix build: Trevelyan and the guards run.
Also:
- GE_D193A extended with tier / gspd / typ / tick / move / frz / hid / aidx
(aidx reverse-resolves model->anim against animation_table_ptrs1).
- bondtypes.h: unk59 was commented "guess: room" — it is the SPEED tier.
- porting-notes.md A1: new bug class — raw-byte aliases into a union read a
plausible constant rather than garbage, so they misbehave silently.
- findings.md: D209 added; D193 closed against it.
|
||
|
|
cf477c20cf |
docs(D191): findings + porting-notes + regen findings-index
Playtest-verified (Bunker ii + Statue). 21/21 solo levels crash-free. Regenerated findings-index.csv (the validate CI check requires it current). |
||
|
|
9bf0c49151 |
fix(audio): D206 — every SFX resolved one bank slot too high
GE's sndPlaySfx reads the on-disk sound bank through the cast alias
struct ALInstrumentAlt_s { s32 unk0, unk4, unk8; ALSound *soundArray[1]; }.
On N64 (4-byte pointers) soundArray sits at struct offset 12 and
deliberately aliases the on-disk ALInstrument's bendRange/soundCount
words, so soundArray[N] == on-disk table entry [N-1] — GE's SFX_IDs are
1-based into the table by design (ID 0 = NOTHING_SFX, short-circuited).
On PC the 8-byte pointer + 8-byte alignment push soundArray to offset 16,
and the converted bank (romdata.c afFixupInst) is packed to match, so
every SFX_ID resolved one bank slot too high.
Root cause of D202's silenced-PPK 'slap' (id 46 -> PUNCH1 instead of
GUN_SILPPK_A) and D205 (3) armour + (4) melee->Klobb. M-61's rejected
'index 45, +0.999' was correct all along.
Fix: src/snd.c:1001, #ifndef PORT / #else -> soundArray[soundIndex - 1],
N64 line kept verbatim. ABI/layout only (D3x class), no game-logic change.
Also serves the retrigger chain (same 1-based space).
Verified: build clean; BUNKER1 GE_AUDIOTRACE 60s crash-free, all ~90
distinct requested indices resolve to rom_sfx_decode.py slot N-1 (0/90 at
N); -level_09 golden framediff 3/3. User playtest: 'most audio is good now.'
findings.md D206 + D205 (M-73/M-74) + D207; porting-notes.md §E.
|
||
|
|
5c3944582b |
docs: v0.1.0 alpha framing + hygiene pass (#25)
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. |
||
|
|
86440e3b58 |
fix(linux): survive the decomp's latent stack-buffer overruns (D189) (#24)
* 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
|
||
|
|
1261389a5c |
fix(linux): thread a va_list* through _Printf (x86-64 SysV ABI) (#22)
* 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 |
||
|
|
ac7223959f |
docs(hud): D187 finding + porting-notes D6 (missing-return / -O2 class)
M-47 root-cause + interactive-console verification for the missing HUD ammo-type icon: framediff -level_09 frame 320 worst-cell dmean 21.8 -> 4.0 with the one-line AVOID_UB return; orange clip glyph confirmed present again. porting-notes D6 covers the non-void-no-return / latent-until-O2 pattern. |
||
|
|
ccff1374b5 | Merge branch 'main' into feat/f10-options-overlay | ||
|
|
7863ec096f |
port: D186 — refuse a sub-30 Video.FpsCap instead of throttling the sim
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.
|
||
|
|
2f5d7f9063 |
port: D184 — F10 port-layer options overlay (approach C)
New port/src/optionsoverlay.c + port/include/optionsoverlay.h: an immediate-mode options overlay drawn as its own fast3d 2D display list, appended after the game DL. Implements docs/dev/OPTIONS-MENU-PLAN.md §2/§3. - config.c/h: configForEachOption() + configSetOptionMeta() label/step/enum side table; CONFIG_OPT_* enum. config.c stays free of specific-key knowledge. - gfx_pc.cpp gfx_run(): one-line hook runs optionsOverlayEmit() after the game DL, before gfx_flush. Returns NULL when closed -> zero bytes appended, golden dumps byte-identical. - video.c: F10 toggles (videoPumpEvents, next to F12); ESC closes; wheel scrolls; videoRequestLiveConfig()/liveCfgDirty re-applies VSync/FpsCap/ TextureFilter at frame start. - input.c: while open, controller 0 returns a neutral pad and nav (arrows/enter/wheel/mouse) routes to optionsOverlayHandleInput() — mirrors the D180 WI-1 swallow pattern. - v1 rows: VSync, FpsCap, MSAA(restart), TextureFilter, MouseAimSpeed, MouseTurnSpeed, MouseInvertY, MouseCaptureMode, ScreenShakeIntensity. Live-apply where possible; configSave() on close. - GE_OPTIONSOVERLAY=1 auto-opens at boot (env-gated diagnostic, left in). Port-layer only, no src/ edits, no #ifdef PORT in game code. Build links 241/241. Runtime framediff/soak/eyeball owed on an interactive machine (this headless env kills the GUI process early and writes 0-byte PPMs). |
||
|
|
293e7436d3 |
port: D177 — ladders non-functional (progression blocker), pointer-width ABI
MoveBond gates the entire ladder-collision path on stanGetLocusCount(&curLocus), which was pinned at 0 on PC. Three compounding pointer-width bugs in the stan navigation layer (same class as D79/D90), no game logic touched: 1. stanCheckLinkedSpecialTile receives the caller's struct StandTileLocusCallbackRecord typed as `s32 *outFlags` and writes the LADDER signal with raw `outFlags[1] = 1`. The record's first member is `s32 *rooms` — 8 bytes on PC — so `[1]` is its high half and `count` (moved +4 -> +8) is never written. stanGetLocusCount() reads ->count, stays 0, ladder path never runs. (FORCECROUCH survived by luck: LE low half of rooms is at +0.) Fix: #ifdef PORT cast to the real struct, write ->rooms / ->count by name. 2. curLocus was declared `struct move_bond_temp_struct` — an 8-byte "placeholder while matching" that equals the N64 record size but is too small for the widened PC record, so stan's field writes overflowed the local. Fix: #ifdef PORT declare it as the real struct StandTileLocusCallbackRecord. 3. stanGetMoveBondCollisionTiles would AV on the first climb: `(coord3d *)((s32)coords + off)` truncates the 64-bit &bondCollision stack pointer. Fix: PORT_PTRADD (uintptr_t arithmetic) added to stan.c. All #ifdef PORT; N64 path kept verbatim under #else. Candidate root-cause from an M-36 subagent (parts 1/3); part 2 found on review. Verified: build links clean; -level_09 framediff 3/3 (frame_200 run variance only); -level_36 (Surface, mandatory ladders) boots crash-free; interactive climb confirmed working in-game by the owner. findings.md D177 (§F + §H), porting-notes.md §A. |
||
|
|
4b71435d73 |
port: D183 — fast3d source-pitch de-stride + texture diagnostics (M-36 Family A)
Family A of the M-36 triage (Surface "grey static" D176b, file-select interlacing D182-2). The assigned hypothesis — import_texture_* shear when line_size_bytes != full_image_line_size_bytes — is DISPROVEN for the Surface repro: - Only gfx_dp_load_tile produces a strided load; GE loads every texture via gDPLoadBlock (tex.c texWriteLoadToTmem*), which sets line == full. Measured: 0/64 GE_DTEX + 0/166 GE_TEXDUMP imports strided on -level_36; 0 in the front end either. - Independent offline pitch scan (new GE_TEXRAW dump) shows the importers pick exactly the pitch the source is stored at. The Surface cliff texture is a 32x32 IA8 whose ROM bytes are genuinely a noise field — decoded faithfully, tiled ~1 texel/px. D176(b) is likely a tiling-density / UV / shift-LOD issue (RC3/D167 family) or a wrong-texture bind, not a decode bug. Full evidence: findings.md D183. D176(b)/D182(2) stay OPEN. - Re-confirmed D159's texSwapAltRowBytes no-op is correct (re-applying it makes the dumps worse). Shipped anyway (golden-safe, reversible): - import_texture() compacts strided rows into a contiguous buffer before the format dispatch, so every importer sees line == full — the behaviour the seven SUPPORT_CHECK(full == line) asserts only assert (and assert compiles out under NDEBUG). Provably inert when full == line (all current loads). GE_TEXPITCH=0 restores the old flat read. - GE_DTEX gains line=/full=/STRIDED markers; GE_TEXRAW=1 dumps raw importer-input bytes for offline decode-vs-source analysis. Verified: build 240/240; -level_09 framediff 3/3; GE_STARTMENU=10 briefing crash-free, renders. Investigation: subagent (worktree). Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT |
||
|
|
cd1ed57449 |
port: D178 — byte-swap the raw briefing segment (blank objectives / briefing text)
Family B of the M-36 triage. The Ubrief*Z segment (assets/obseg/brief/) is
a raw 48-byte ROM image of struct BriefStruct { u16 brief[4]; struct { u16
textid; u16 enabled_difficulty; } objective[10]; }, loaded by front.c
load_briefing_text_for_stage() via _fileNameLoadToAddr() with no converter
and no BE->LE fixup. On the LE host all 24 u16 read byte-swapped:
- textid 0x2C04 (getStringID(LDAM,4)) -> 0x042C -> langGet() resolves an
unloaded bank -> NULL -> blank objective/briefing text. This is the whole
of the D143 "briefing text blank" residual (the bank loaded fine; the ids
into it were swapped).
- enabled_difficulty 0x0001 -> 0x0100 = 256, so `selected_difficulty >=
enabled_difficulty` is always false -> difficulty-gated objectives vanish
(on Agent only the one AGENT-difficulty line survived -> lone "a." bullet).
Fix: romdataFixupBriefing() in port/src/romdata.c, called from a #ifdef PORT
block in load_briefing_text_for_stage() right after the load — same shape as
langFixupLoadedBank(). BE-serialized-struct reconciliation (AGENTS.md #2),
semantics-preserving, N64 build untouched. GE_D178=1 dumps raw + fixed words.
Verified: Dam briefing renders all four objectives at 00 Agent with correct
difficulty gating and "007: James Bond" header; -level_09 framediff 3/3;
GE_STARTMENU=7/=13 crash-free; build links clean 240/240.
Investigation + patch: subagent (worktree). Suspect 2 (Gfx-width temp_s0
arithmetic) was ruled out — benign 512-byte gap, langLoadToBank records the
bank base itself. Follow-ups noted in findings.md: annotate D143 as
superseded; latent 64->32 pointer truncation at front.c:6569 / :2202.
Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
|
||
|
|
4ca11e4386 |
Prepare repository for public release
- README: attract-mode stills + demo GIF as the hero row, status/warning callouts, metrics table, "Contents" index, doc-index table; simplified the two-agent workflow diagram (LR -> TD, fewer edges) for GitHub web - Table-of-contents indexes added to internals.md, porting-notes.md, agentic-development.md, AUDIO-PLAN.md - CI: add Windows/MSYS2 build job, minimal token (contents: read), persist-credentials: false, concurrency group; run on main + master. validate job regenerates the US ROM-asset symbol file only and configures ntsc-final (PAL/JP need a region ROM; no ROM is used in CI) - .github: SECURITY.md, issue forms + chooser config, PR template, labels.yml, dependabot (Actions, monthly) - Do not track the proprietary SGI IRIX IDO toolchain or SGIImageViewer.exe; document in SetupGuide.md / building.md how to obtain IDO for N64 ROM builds (the PC port does not need it) - Stop tracking two committed *.pyc files; drop stale screenshots and a redundant .gitignore line; misc doc cross-reference fixes Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT |
||
|
|
203549f57e |
D168: fix vertically-flipped GE_PCDUMP captures; close D114/D116
gfx_opengl_dump_bound_fbo wrote glReadPixels output (GL bottom-left origin) straight into a top-row-first P6 PPM, so every GE_PCDUMP / F12 capture was upside-down. This is the entire source of the D114/D116 "HUD/text X-mirror" - four sessions chasing a fast3d mirror that never existed, misreading inverted screenshots of asymmetric content. The developer confirms the game renders correctly on real hardware. - writer now emits rows top-to-bottom - tools_pc/golden/*.png flipped in place to match + golden/README.md - D114/D116 reclassified as capture-orientation artifacts (not deleted); D75 "logo mispositioned" / RC1 "photo 180 deg" flagged for re-judging - annotations in findings.md, GRAPHICS-BACKLOG.md, TEXTURE-GLITCH-ANALYSIS.md, porting-notes.md D2, GE-ENV-PROBES.md Not runtime-verified (migrated tree has no ROM/toolchain) - needs a fresh capture to confirm text reads normally. Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V |
||
|
|
52539d10de |
Prepare for public release
- README, LICENSE (MIT for the port layer), NOTICE, CONTRIBUTING. - Restructure docs: internals.md, porting-notes.md, dev-process.md + dev/agentic-development.md (the research write-up), dev/ (raw record). - Replace decomp status-badge CI with a PC-port validate workflow. - Remove session/agent scaffolding and stale planning docs from the tree. - Add .gitattributes (normalize line endings to LF); add screenshots. The pre-2026-08 history is the GoldenEye 007 decompilation by Larry Ficken (KholdFuzion) and contributors, preserved unchanged. The PC-port work from 2026-08-17 on is mine. |