forked from Ports/goldeneye-pc-port
docs(findings): migrate fat §F rows D244-D270 to tail sections (ctx-opt P1, batch 7/8)
20 rows: topic/status cells moved verbatim to new '## Dxx' sections at file tail; table rows now carry a one-line summary + pointer. CSV regenerated, label set unchanged (171). Nothing deleted.
This commit is contained in:
1 parent
ce4ec5a387
commit
63b4d79ce6
2 files changed
+145
-25
No files matched your search
@@ -39,7 +39,7 @@ D229,FIXED,"Water on IsWater levels renders green and pulses blue<->green on an
|
||||
D230,OPEN,In-level music quality:,"OPEN — static-analysis pass done (M-108), not runtime-confirmed."
|
||||
D233,FIXED,All levels (intermittent):,"FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_tri1) —…"
|
||||
D234,FIXED,"Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).",VISUALLY VERIFIED FIXED (M-108) —…
|
||||
D244,OPEN,Self-hosted CI runner (corpyvt6agent01) link failure:,"OPEN, not yet fixed. Next:"
|
||||
D244,OPEN,Self-hosted CI runner (corpyvt6agent01) link failure:,"OPEN, not yet fixed. Next…"
|
||||
D228,FIXED,AK47 (and other) weapon models still show WHITE missing-texture patches after D217's texpool-alignment fix —,"FIXED (port/fast3d/gfx_pc.cpp, palette_to_rgba32) —"
|
||||
D204,FIXED,PC audio ran ~2 % below real time permanently:,"FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —"
|
||||
D203,?,"Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle:",CLOSED — root cause + fix in D253.
|
||||
@@ -140,11 +140,11 @@ D240,OPEN,"Player gunshot SFX timing/cadence reportedly does not match the N64 o
|
||||
D241,OPEN,Player gunshot SFX goes silent under some conditions —,"OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug."
|
||||
D242,?,Player-death sequence UX regression vs N64 original:,NOT A PORT-LOGIC BUG as far as static analysis can tell.
|
||||
D243,OPEN,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):",OPEN — M-109 re-analysis of the same session's raw logs corrects the timing/pattern claims above; the D134/pendingGfx race hypothesis is likely wrong.
|
||||
D245,OPEN,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"OPEN — REOPENED (M-116 live playtest, same session):"
|
||||
D245,OPEN,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"OPEN — REOPENED (M-116 live playtest, same session)…"
|
||||
D246,OPEN,"Tiny pixel strips at the left and right edges of the screen, ""like overscan"" —","OPEN — one candidate ruled out (M-116 follow-up), root cause not found."
|
||||
D247,RESOLVED,Black bars top and bottom of screen —,"NOT A BUG — confirmed faithful, unmodified N64 decomp behaviour, no code change made."
|
||||
D248,FIXED,"Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes).","FIXED, root-caused to a genuine pointer-width ABI bug in src/sched.c (compiled for the PC build, not excluded)."
|
||||
D249,OPEN,"LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV —",OPEN — not investigated.
|
||||
D249,OPEN,"LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV —",OPEN — not investigated.…
|
||||
D250,?,FPS is inconsistent/low on many levels since the port started actually running at 60fps —,
|
||||
D251,FIXED,F10 options overlay QoL pass:,"FIXED (port-only, port/src/optionsoverlay.c)."
|
||||
D252,?,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).",
|
||||
@@ -153,12 +153,12 @@ D254,FIXED,Linux crash handler destroyed its own logs:,FIXED — field-verified.
|
||||
D255,?,"Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle, D253 fix confirmed working —",
|
||||
D256,FIXED,Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing data/ —,FIXED (path half) — save-flow detail still owed from the user.
|
||||
D257,FIXED,New Game.AllUnlocked option (F10; default flipped OFF for v0.2.0 —,FIXED — verified by user playtest (PR #79 merged); default flipped OFF for v0.2.0 (D259).
|
||||
D259,FIXED,Game.AllUnlocked (D257) breaks audio on a fresh save:,FIXED in v0.2.1 — the deferred fix was applied:
|
||||
D259,FIXED,Game.AllUnlocked (D257) breaks audio on a fresh save:,FIXED in v0.2.1 —…
|
||||
D260,FIXED,v0.2.1 (user request):,"FIXED — build green; one-line branch swap, trivially reviewable."
|
||||
D261,FIXED,v0.2.1 (user request:,FIXED — feel-check received; W/S sensitivity follow-up logged above.
|
||||
D262,FIXED,v0.2.1 (user report):,"FIXED (primary cause, D264) —"
|
||||
D264,FIXED,"D262's root cause (ABI/layout class, cf.","FIXED — build green, probes removed; visual confirmation pending."
|
||||
D263,RESOLVED,v0.2.1 (user report):,"RESOLVED (not a bug), but refined (v0.2.1 further user testing, 2026-09-15) —"
|
||||
D263,RESOLVED,v0.2.1 (user report):,"RESOLVED (not a bug), but refined (v0.2.1 further user testing, 2026-09-15) —…"
|
||||
D265,?,"D236 investigation (port-layer, port/fast3d/gfx_pc.cpp):",PARKED — reverted to baseline; evidence + suspect list above.
|
||||
D258,OPEN,PAL/JP asset conversion is broken at the source-data level —,"OPEN — repair path verified, not applied (out of v0.2.0 scope)."
|
||||
D284,FIXED,"port/src/random.c's MIPS64 dsll32/dsrl32 shift helpers were backwards (modelled as 32-bit shifts, not ""shift by n+32 on the full 64-bit register""), desyncing t…","FIXED, 21-level sweep clean, no crashes —"
|
||||
|
||||
|
+140
-20
@@ -412,7 +412,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D230 | **In-level music quality: a bass instrument sounds corrupted on PC, correct on N64 reference (M-105, user reports; repro refined M-106).** — full `## D230` entry at file tail | **OPEN — static-analysis pass done (M-108), not runtime-confirmed.** Repro… |
|
||||
| D233 | **All levels (intermittent): indoor level geometry invisible near doors — you can see through walls/floors into rooms you can't see into, while world objects…** — full `## D233` entry at file tail | **FIXED (port/fast3d/gfx_pc.cpp, `gfx_sp_tri1`) —… |
|
||||
| D234 | **Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).** — full `## D234` entry at file tail | **VISUALLY VERIFIED FIXED (M-108) —… |
|
||||
| D244 | **Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the reinstalled box, run `34707798707`).** `src/game/objective_status.c:162` uses `#pragma weak objectiveGetStatus_WEAK = get_status_of_objective` (a decomp ground-truth alias, referenced 2-arg from `chrai.c:2538` — comment calls it "horrible hack to get ai matching"). The box's `g++`/`ld` reported as `x86_64-w64-mingw32/16.1.0`; a minimal local repro of the identical `#pragma weak` cross-TU function alias **links cleanly** on this session's own machine (`mingw-w64-x86_64-gcc 16.2.0-3`, `binutils 2.47-3`). Not a game-logic bug (the alias is unmodified decomp code, ground truth) and not a CMakeLists/link-flag issue (no `--gc-sections`/LTO in play) — the leading, unverified explanation is the self-hosted box's MSYS2 packages predate a PE/COFF weak-external fix or behavior change versus current MSYS2, consistent with rollout step 2's earlier finding that the box's mingw64 package set was stale/incomplete at initial setup. | **OPEN, not yet fixed.** Next: on the box, `pacman -Syu` (or at minimum `pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-binutils`) to bring the toolchain current, then re-dispatch `selfhosted.yml`. If updating doesn't clear it, the fallback is a narrow `#ifdef PORT` replacement of the alias with an explicit thin wrapper function in `objective_status.c` (ABI-preserving, no logic change) — but try the toolchain update first since the local repro suggests this is a version/environment gap, not a fundamental PE/COFF limitation. |
|
||||
| D244 | **Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the…** — full `## D244` entry at file tail | **OPEN, not yet fixed.** Next… |
|
||||
| D228 | **AK47 (and other) weapon models still show WHITE missing-texture patches after D217's texpool-alignment fix — a separate failure mode from the green wrong-palette defect (M-97, user playtest of PR #47, `-level_34` FACILITY).** Split out of D217 defect ①, exactly as the M-90 write-up anticipated ("do not close D217 defect ① on this merge — reopen/retitle as the AK47 + NPC-world-model white-texture residual"). **Playtest result that motivates the split:** with PR #47's `texInitPool` 8-byte arena alignment in, the *green* symptom is GONE — PP7/PPK grip and hammer render correct, Bond's watch face correct, no green cast on hands or the 3rd-person world model. But **NPC-held AK47s still render with white patches**, i.e. tiles sampling a missing/undecoded texture rather than a wrong-but-present palette. White ≠ wrong colour: a wrong palette binds a real TLUT with wrong contents (D217's mechanism, now fixed), whereas white is the signature of a texture that never got decoded/uploaded at all, or a tile descriptor pointing at an empty/cleared texpool slot. The M-87 matrix described these as distinct from the start ("AK47 viewmodel has white (missing-texture) patches on the magazine + barrel" vs. the green/blue colour corruption) — this playtest confirms they have distinct root causes, since fixing one left the other untouched. | **FIXED (`port/fast3d/gfx_pc.cpp`, `palette_to_rgba32`) — a real byte-order bug, not a texpool/decode-never-ran issue as originally guessed.** `GE_TEXDUMP` on Facility (`-level_34`) found ~14% of LOD-textured uploads (fmt=CI, `palette_fmt=G_TT_IA16`) rendering solid near-white; a `GE_D228` correlation probe ruled out the texpool/RC2-clip hypothesis (the decoded size exactly matched the tile's base image every time — the data reaches `import_texture` fine, sized correctly). The actual bug: `palette_to_rgba32`'s `G_TT_IA16` branch read `intensity = palentry & 0xff` (low byte) and `alpha = palentry >> 8` (high byte) — backwards relative to the already-correct direct-IA16 decode two functions above it in the same file (`import_texture_ia16`: `intensity = addr[2*i]` (first/high byte after `gfx_dp_load_tlut`'s `PD_BE16` swap), `alpha = addr[2*i+1]`). A dim, fully-opaque palette entry (e.g. intensity=0x58, alpha=0xff) decoded backwards as bright and mostly-transparent (intensity=0xff, alpha=0x58) — exactly the white-patch symptom, on any CI-format material whose TLUT happens to be IA16 rather than RGBA16 (weapon metal/grip materials commonly are, since IA is a natural fit for grayscale metal with a separate opacity channel). Fix: swap the two lines to match the working convention. **User-verified live:** AK47 and other metallic guns render correctly (no more white patches). Verified crash-free with a `-level_34` headless run before the live check; a full 21-level sweep is still owed (not run this session — the user was using their own game instance concurrently, `verify.sh`/`taskkill` would have killed it). D217 · D161 lineage — same general "CI/LUT format-assumption" bug class, distinct specific bug. |
|
||||
| D204 | **PC audio ran ~2 % below real time permanently: GE's AI feedback loop (`src/audi.c:531`) has a ~3 ms setpoint that PC scheduling jitter clears, so the SDL queue starves and the device pads playback with silence (FIXED, measured).** `amMain` wakes at 30 Hz (`sched.c:334` forwards every 2nd retrace to the audio client) and asks for `g_MinFrameSize`=720 samples per block = 21600/s against a 22050 Hz device — structurally 2 % short, relying on 784-sample top-up blocks that `audi.c` only requests once the reported AI length falls under ~69 frames (3 ms). Fine on N64 (double-buffered AI, exact VI interrupt); on PC the queue empties before the loop reacts, and the padded silence is unrecoverable time. **Fix (F5, port-only):** `audioGetAiLengthBytes()` subtracts a 1024-frame (~46 ms) cushion before reporting, so the loop tops up while slack remains; `audi.c`'s control law untouched (rule #2 clean). Measured A/B in one binary via `GE_D204_OLD`: **rt 0.980 → 1.000, q min 0 → 368, drop 0.** Also: F1 correct AI_LEN_REG single-buffer semantics (robustness, measures as a no-op), F3 `Audio.QueueLimit` 8192→2880 + non-silent drops (NB: an existing `ge007.ini` pins it), F4 oversize invariant guard, and `GE_D204=1` cheap audio-health monitor (`rt`/`q`/`drop`/`max`, safe to leave on for a full playtest). **Falsified in-session, do not re-open:** the u32 wrap at `audi.c:531` is real (high-water 2064 frames > the 789 threshold) but harmless — `frameSamples` is `s16` (`audi.c:145`), so it lands negative and audi.c's own lower clamp catches it; max block stays 3136 B vs the 3156 B allocation, no heap overrun. **Corrects two M-63 claims:** `soundIndex=109` is guards' return fire (count was double-logged; the "32.6 ms cadence" is just the 2880-byte block quantum every `dumppos` is rounded to) — closed negative; and M-63's "13 % of real time" was `GE_MIXERTRACE`'s own 25 MB unbuffered log starving the audio thread (same repro without it: 58.0 s / 60 s). Does NOT explain D202. | FIXED (`port/src/audio.c`, `port/src/libultra.c`, `port/include/audio.h`) — measured; a by-ear pass on a real playthrough still owed |
|
||||
| D203 | **Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle: Facility (`-level_34`) crashes "after loading in as James Bond" (user bug report).** — full `## D203` entry at file tail | CLOSED — root cause + fix in D253. |
|
||||
@@ -480,7 +480,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D150 | watch OBJECTIVES / BRIEF page crash: `strcat(buf, langGet(id))` with `langGet` → NULL (unloaded bank) → NULL deref; `str.c` str* builtins elide a plain param NULL check, so guard via asm-laundered `GE_IS_NULL` | FIXED (`src/str.c` `#ifdef PORT`); interactive re-verify pending |
|
||||
| D260–D265 | v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → root-caused to D264 (fixed at Gfx level, visual check pending) · D263 Facility Ourumov/Trevelyan beat — **resolved: not a bug**, proximity/LOS-triggered as on N64; full chain incl. execution shot verified firing on PC (teleport probe + user playtest) · D264 watch preview renderdata.flags zeroed by cross-global template read on PC (ABI/layout class; fixed) · D236/D265 Surface 1 tree "wall": IA16 importer dimensions transposed (fixed); decode attempt reverted after raw-byte proof that byte-pair is the authored image; root cause = `import_texture_ia4` decoding RDP I2:A2 as I3:A1, inverting the tree strip's alpha ramp → opaque repeating silhouettes; IA4 I3:A1→I2:A2 decode fix applied and user re-tested — **still a wall; reverted, parked** with evidence + suspect list in D265 | partial — see rows |
|
||||
| D266–D269 | D236/D265 wall, second pass: **the wall is CI8 32×32, not IA** (D269) — band-gated probe finds two draw classes per room stream (OPA oml=0xc4112078 + XLU oml=0xc41049d8), both BILERP/TEXTLOD/2-cycle/**FOG-on**; cards are soft gradient blobs, stripe period ≈ one card per visible room. D266: re-applied the reverted I2:A2 IA4 decode + replaced the hardcoded 0.19 edge quantize with AC-aware handling (uncommitted; census shows zero IA4 draws at boot). D267: IA8 dump infra — the three IA8 images are noise/mast sprites, not the wall. D268: force-alpha A/B proves the blend pipeline works. Ranked suspects now: fog range/application (D218 family) > TEXTLOD/mip sampling > draw density/culling > palette | OPEN — probes live in tree (`GE_D266`/`GE_D266W`/`GE_D267`/`GE_D268_FORCEA`), fixes uncommitted |
|
||||
| D270 | **Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.** D236 wall, between-pass instrumentation toolkit for the fog hypothesis (D269 ranked suspect #1) and general A/B isolation: `gfx_sp_tri1` per-vertex fog min/max attribution; `GE_D270SKIP`/`GE_D270SKIPOML`/`GE_D270KEEPOML`/`GE_D270KILLALL` (drop-by-oml/fmt A/B rules); `GE_D270PIX` (single-pixel draw-order attribution); `GE_D270FOG` (per-frame fog-color-change log); and the `GE_TEXRAW` dump cap raise 400→600 to reach the wall cards' upload index. No independent conclusion of its own — superseded by D271's culling fix and D272's later per-card audit, both of which reused this toolkit's skip/keep hooks. | dead-ended into D271/D272; infrastructure kept in tree |
|
||||
| D270 | **Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.** — full `## D270` entry at file tail | dead-ended into D271/D272; infrastructure kept in tree |
|
||||
| D271 | **D236 wall, third pass — suspect #3 (draw density/culling) CONFIRMED as a real over-draw, and the D106 guard was the cause.** At Surface 1's boot camera, 11 portals around room 13 straddle the near plane every frame; their z==0 clip points project to ±1e20–1e26-scale FINITE screen coords. The original D106 guard (`lim=1e5`) mapped any such bound to a full-screen box — but on N64 there is no guard: those finite values flow into `bgRectIntersect` (pure comparisons), which either clamps to the parent box (portal covers view → keep) or yields an empty box (portal projects off-screen → **cull**). 9 of the 11 portals clamp empty on N64; PC was force-drawing those rooms. Fix: guard `lim` 1e5→1e38 (catches only NaN/inf, which this projection can't reach — max ~1e26); finite min>max still hits the original `degenerate` check (N64's own path, D106's real regression case unchanged). Verified: `d106Fallback` 396–781/600 ticks → **0** (Surface 1 + BUNKER1); roomsDrawn 17–18→10–13 at the boot view; per-portal oor-finite dumps confirm every culled portal's finite portion is far off-screen (N64 clamps identically); BUNKER1 room drops traced to the same N64-matching empty clamp. No crashes in long unattended runs. **USER RE-TEST (2026-09-14): NEGATIVE — trees still render as a big wall; user's read: "rather than the texture being mapped correctly"** → suspect #3 was a real over-draw but NOT the wall's root cause; fix kept for N64 fidelity. D236 thread continues — top suspect now per-card S/T / UV windowing (D265 note: all captured cards sampled the same u/v window) | FIXED-but-insufficient (`src/game/bg.c` `#ifdef PORT`, uncommitted) — probes live (`GE_D271`, extended `GE_D104`); wall still open, see D236 |
|
||||
| D276 | **D236 wall, eighth pass — the true painter of the visible weave, at confirmed weave pixels, is a fully-opaque IA8 blocky-noise texture sampled with a fixed, identical 2×-wrapped UV window and low fog — NOT either of D269's named CI8 classes.** Multi-point pixel attribution + per-vertex fog (D275's `GE_D270PIX` list extension) at 4 pixels picked directly off the reproduced weave screenshot found the D269 OPA/XLU classes absent at 2 of them; the dominant front-most class there (`oml` low bits coincide with the XLU class but `fmt=3`/IA, not CI8) decodes to blocky gray static, matches D267's already-dumped-but-dismissed "seq 113" texture, and reproduces D265's original "all cards sample u:0..64/v:0..16" observation to the exact texel — that lead was reframed away from prematurely in D269's second pass. A third class (`0x0c184b50`, CI8) is present everywhere with erratic z/w (`-0.04..0.99`) and high fog when it does land normally — flagged as the possible real "tree" layer, with its own likely occlusion/billboard bug, meaning D236 may have two separate root causes, not one. | REFRAMED — `oml=0xc81049d8`/`fmt=3` is the primary suspect now, not D269's CI8 pair; `0x0c184b50` unattributed; wall still open, see D236 |
|
||||
| D279 | **Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in `scratch/` (`d279_run.log`, `d279_imgmap_run.log`, `d279_g110.png`, `d279_g220*.png`, `d279_gray_1500.png`) but no written conclusion; the logs carry no D279-tagged lines, so its exact probe/finding cannot be reconstructed from the tree.** Next available label after it: D280. | undocumented — artifacts only |
|
||||
@@ -536,29 +536,29 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D241 | **Player gunshot SFX goes silent under some conditions — reported on Bunker with the alarm sounding, "or some other factor" — but only for PP7 and AK47, Klobb…** — full `## D241` entry at file tail | **OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug.** Recommend folding this repro into D207 (or closing D241 as a duplicate once D207 is reopened) rather than tracking separately.… |
|
||||
| D242 | **Player-death sequence UX regression vs N64 original: PC requires an LMB click to proceed to the mission-failed/file-select screen; user recalls N64 auto-plays…** — full `## D242` entry at file tail | **NOT A PORT-LOGIC BUG as far as static analysis can tell.** Confidence HIGH that the LMB-click skip path is faithful original behavior (Z_TRIG-mapped, exists unmodified in `bondview2.c`), not a defect to remove.… |
|
||||
| D243 | **Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject): the camera violently shakes/"spazzes" during each of the scripted camera-angle…** — full `## D243` entry at file tail | **OPEN — M-109 re-analysis of the same session's raw logs corrects the timing/pattern claims above; the D134/pendingGfx race hypothesis is likely wrong.** Confidence MEDIUM on the correction, LOW-MEDIUM on the new lead.… |
|
||||
| D245 | **IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) — "looks almost exactly like skybox" (M-116, user QA report, post-D229-fix build).** The symptom is D227's exact signature class (s16 `Vtx.tc` overflow → rasteriser interpolates between wrapped coordinates → two differently-wrong screen regions + a seam at the wrap boundary; on the sky quad the split ran on the 0–3 diagonal). Water draws through the same port path: `src/game/sky.c` water block (~905–956) — `texSelect(&skywaterimages[WaterImageId])` + `sub_GAME_7F09343C` (two-texunit cross-fade; both tiles re-declared RGBA/16b @ tmem 0, the draw D229 fixed) then a `skyRenderTri` fan over `sp274[]` under `skyPortBeginFan(sp274, s1, FALSE)` — and **the `FALSE` is a documented, deliberate tolerance of exactly this overflow**: the comment above `skyPortBeginFan` (sky.c ~1738) records that the IsWater quad is excluded from D227's tile-shift safety valve because both texunits read the same vertex tc, so a tc rescale would need matched shifts on BOTH tiles and "shifting both makes them sample identically, which flattens the cross-fade"; the 2026-09 decision was that "the water quad keeps the plain 1:1 tc and simply tolerates the overflow on its horizon vertex, exactly as it did before D227." The mechanism is real: `unk20/unk24` are S10.5 units reaching tens of thousands near the horizon; the fold rebases to a multiple of `SKY_TC_WRAP` (2048) but with k=0 an s16 tc holds only ±32767, so any wider span wraps. It was invisible before D229 because every water texel was green mottle garbage anyway; with correct blue texels the wrapped region now reads as a visibly different pattern + seam, and it tracks the view because the fan's S/T depend on camera orientation. (On N64 this draw is a verbatim RDP `G_TRI_SHADE_TXTR` coefficient block with no s16 tc — the overflow constraint is a port artifact of re-emitting as `gSPVertex` batches, same class as D227.) | **OPEN — REOPENED (M-116 live playtest, same session): the `allowShift=TRUE` mechanistic fix below did NOT resolve the symptom.** User report on Frigate post-fix: water still "glitches, kinda goes back and forth between two graphics too quickly," and still "changes depending on your view unlike the sky" (i.e. the sky, using the identical `skyPortBeginFan`/`skyPortEmitTileShift` mechanism post-D227, no longer has this problem, but water still does after getting the same treatment). This means the `allowShift=FALSE→TRUE` change either isn't the actual fix, or there's a second mechanism specific to water's two-texunit cross-fade that the tc-shift alone doesn't address — most likely candidate: the shift only rescales the *shared* vertex tc uniformly, but the tile-0-vs-tile-1 sample-point difference is set by each tile's own `uls`/`ult` (RDP tile-space, not vertex-tc-space) — if per-frame `k` changes granularity of the *quantized* shared coordinate before each tile's independent `uls`/`ult` subtraction, the two tiles' effective sampled texel could drift relative to each other frame-to-frame in a way the static `GE_D227V` trace (single frame, single camera angle) never exercised. Needs a fresh live investigation, not a repeat of the same static-trace approach — a scripted or live multi-frame capture while rotating view, watching both tiles' *decoded* texel positions (not just vertex tc) frame over frame. Do not re-apply the same `allowShift=TRUE` reasoning without new evidence; the superseded mechanism write-up below is unconfirmed-by-playtest and may be wrong. The code change (`allowShift=TRUE` on the water quad) is left in place — it's not shown to be actively harmful, and reverting it isn't obviously better than leaving it, pending the fresh investigation. **Superseded analysis (unconfirmed by playtest, kept for reference):** ~~FIXED (port-layer, `src/game/sky.c`, `#ifdef PORT`) — confirmed the `allowShift=FALSE` tolerance was the cause, and the "flattens the cross-fade" objection was wrong.~~ Changed the water quad's call from `skyPortBeginFan(sp274, s1, FALSE)` to `..., TRUE)`, so it now gets D227's tc-shift safety valve like every other sky fan. Why the old objection doesn't hold: `skyPortEmitTileShift` replays **both** tiles' captured `SETTILE` commands with the identical `k`, so both texunits' decoded S/T are rescaled by the same `2^k` *before* each tile's own (unshifted, tile-native) `uls`/`ult` offset is applied by the RDP — the offset between tile 0 and tile 1's sample points (what drives the cross-fade) is preserved, not flattened, because the shift and the offset act at different stages. **Verified mechanistically:** `GE_D227V=1` on Frigate (`-level_26`) shows a water fan vertex spanning S=-166..299317, T=-6451..288410 in S10.5 units — tens of thousands of texels, the same order of magnitude as D227's pre-fix sky overflow — now getting `kS=4 kT=4` (previously forced to 0), decoding to small in-range tc valuesLine truncated
|
||||
| D246 | **Tiny pixel strips at the left and right edges of the screen, "like overscan" — look like raw pixels, not 3D graphics (M-116, user QA report).** Uninvestigated. The port renders at native 640×480 (`GE_NATIVE_W/H`; `gfx_current_native_aspect` set in `port/src/video.c:253,570`) and scales to the window on present, so the leading candidate is a scaling/blit rounding artifact: non-integer scale factors leaving 1–2 columns of uncleared backbuffer content at the edges (or an off-by-one in the blit rect). Secondary candidates: an RDP fill-rect/scissor off-by-one in a game DL or the F10 overlay hook (`gfx_run()` seam, D184), or GL edge filtering on the scaled texture. | **OPEN — one candidate ruled out (M-116 follow-up), root cause not found.** Step (1) from the original plan is done: a `GE_PCDUMP` capture (Frigate, native 640×483 buffer, both against the existing save and a fresh EEPROM) shows clean, continuous scene content in the leftmost/rightmost columns — no dead/black/garbage strip at either edge. That means the defect is **not** baked into the native render itself (rules out an RDP fill-rect/scissor off-by-one in a game DL, and rules out D227/D245-style tc issues) — it must be introduced later, specifically in the SDL-window presentation path that `GE_PCDUMP` bypasses entirely (`gfx_run()` reads back the game's own render target directly, never the actual window backbuffer the user sees). Re-examined `gfx_adjust_viewport_or_scissor` (`gfx_pc.cpp:2174`): its floor/ceil rounding is already deliberately anti-gap (comment: "prevents sub-pixel gaps at viewport edges"), so a plain rounding bug there is a weaker lead than it looked; more likely candidates now are the `game_renders_to_framebuffer`-path MSAA resolve `glBlitFramebuffer` (`gfx_opengl.cpp:1252`, only exercised when `Video.MSAA>1` or the window size actually differs from the game's target size mid-frame — a path `GE_PCDUMP`'s headless capture doesn't exercise the same way a live resizable window does) or a genuine sub-pixel/DPI scaling remainder specific to the user's actual window dimensions. **Next, still needs the user (no live window access in this environment):** (1) confirm current `Video.MSAA` setting and window size/aspect at the time the strips were seen; (2) if `MSAA>1`, retest with `Video.MSAA=1` to see if the strips disappear (isolates the resolve-blit path); (3) report whether the strip width changes with window size (fixed 1px = blit-rect off-by-one; scales with window = scale-factor remainder). Cross-ref D247 (separate, now closed — don't conflate the two; D247's letterbox is confirmed-intended and top/bottom only, D246 is left/right and still unexplained). **M-141: Steam Deck playtest confirmation, new platform data point** — user reports "some artifacts left and right of screen, only a sliver... like the fullscreen isn't quite going to full screen" specifically in **Full Screen mode on Steam Deck**. Same symptom description as the original M-116 report (Windows). Checked: the Deck's own first-run preset (`videoApplySteamOSDefaults()`, `port/src/video.c:255`) sets `cfgMSAA = 4` by default — exactly the condition this row already flagged as the leading remaining suspect (the MSAA-resolve `glBlitFramebuffer` path, step 2 of the still-open bisection ask). Next: have the user set `Video.MSAA=1` via F10 and retest on the Deck specifically — if the strips vanish, this row's root cause is confirmed without needing any more code tracing. |
|
||||
| D247 | **Black bars top and bottom of screen — possible aspect-ratio defect (M-116, user QA report).** Uninvestigated. Context: the port currently renders native 4:3 (640×480); widescreen is a *parked proposal* (`docs/dev/WIDESCREEN-FOV-PLAN.md` — PD-parity native 16:9 render, needs rule-#2 sign-off for its game-code HUD wave), so if the user's window/display is wider than 4:3 and the present path letterboxes to preserve aspect, the bars are current intended behaviour rather than a bug. If they appear in a 4:3 window too, it is a real defect (framebuffer sized wrong vs window, `videoCreateFramebuffer(w,h,upscale,autoresize)` → `gfx_create_framebuffer`). | **NOT A BUG — confirmed faithful, unmodified N64 decomp behaviour, no code change made.** `GE_PCDUMP` captures (both against the pre-existing save and a fresh-EEPROM run, to rule out a stale `Screen` option from earlier test sessions) show the exact same inset on Frigate: real scene content occupies rows ~20–459 of a 640×483 capture (the 483 vs 480 is a separate, harmless 3-row capture-tool rounding quirk, not investigated further), i.e. roughly 220/240 of the vertical field — and `src/game/fr.h:34` defines `VIEWPORT_HEIGHT_DEFAULT_NTSC 220` (240 for the full field), consumed unmodified by `bondviewGetCurrentPlayerViewportHeight()` (`bondview2.c:7858`) as the **default in-game viewport height whenever `PLAYER_OPTION_SCREEN` is `SCREEN_SIZE_FULLSCREEN`** (the decomp's own default value, `options.c:128`) — `VIEWPORT_HEIGHT_FULLSCREEN` (304, no inset) is a red herring name; it's only reachable via the `cameraBufferToggle` branch, not normal solo play. This is GoldenEye 007's well-documented original TV-safe-area letterboxing, baked into ground-truth decomp constants with no `#ifdef PORT` anywhere near it — the N64 game genuinely doesn't fill the full field during normal gameplay. Ruled out a stale/leftover EEPROM `Screen=Wide/Cinema` setting from earlier QoL-testing sessions as an alternative explanation: a fresh EEPROM (default options) shows the identical inset. **No fix needed; this row closes.** If the user's window shows noticeably *thicker* bars than this ~8–9%-per-edge margin, or bars appear on the *sides* instead of top/bottom, that would be a genuinely different (and real) symptom worth reopening under a new label — but "thin bars top and bottom, present at any window size" is correct, original behaviour. Cross-ref D226 (separate, already-tracked HUD-scale QoL ask — unrelated mechanism, don't conflate). |
|
||||
| D248 | **Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes).** The port's pacemaker (`port/src/libultra.c` `portTickThread`) posts a VI-retrace event at a true 60Hz (NTSC) cadence, so the halving had to be somewhere downstream of that. | **FIXED, root-caused to a genuine pointer-width ABI bug in `src/sched.c` (compiled for the PC build, not excluded).** `osScAddClient()` (`src/sched.c:187-199`) stashes a per-client "present every retrace vs. every other retrace" flag by writing into the *next array slot's* `.next` field (`c[1].next = next`) rather than adding a real struct member — `gfxClient` (`src/init.c:246`) is registered with `next=NULL` (send every retrace → 60Hz) and the audio client `g_AudioClient[0]` (`src/audi.c:441`) with a nonzero sentinel `1` (send every other retrace → 30Hz, its intended cadence). `__scHandleRetrace` (`src/sched.c:334`) reads that flag back with a hardcoded byte offset, `*((s32*)client + 2)` (2×`sizeof(s32)` = 8 bytes). On the N64 (32-bit pointers) `sizeof(OSScClient)` is exactly 8 bytes, so offset 8 correctly lands on `client[1].next` — matching the write. On this 64-bit port `sizeof(OSScClient)` is 16 bytes, so the same hardcoded 8-byte offset instead reads the low 32 bits of `client->msgQ` itself (always a real, non-NULL pointer) — the gfx client's flag reads as permanently "nonzero" and silently falls onto the audio client's every-other-frame path, halving the game's real render/tick cadence to 30Hz although the retrace pacemaker itself was always ticking at the correct 60Hz. Same D122/D126/D132/D209 bug class (32→64-bit pointer-width struct-size growth desyncing a hand-rolled offset), just in the scheduler instead of a ROM-serialized struct. **Fix (`#ifdef PORT`, N64 line kept verbatim under `#else`):** read the real field by indexing the struct (`client[1].next == 0`) instead of hardcoding a byte offset — this naturally scales with pointer width on either platform and is the direct decompilation of what `osScAddClient`'s write side already does. **Verified:** `Video.DisplayFPS=1` overlay (`port/src/optionsoverlay.c`, D213) read a rock-stable "30 FPS" on `-level_09` (BUNKER1) pre-fix and "60 FPS" post-fix, same build/scene, screenshots captured via `GE_PCDUMP`. Rebuilt clean (`ninja -j1`, pre-existing unrelated warnings only); `-level_09` and `-level_20` both ran 12-30s post-fix with no crash (`ge007.crash.log` absent). No game-code files touched beyond this one mechanical read (`src/sched.c` is compiled for the PC target per `CMakeLists.txt`'s `SRC_ENGINE` list, not one of the excluded os/io files) and the audio client's own flag/path is completely untouched, so `#ifdef PORT`-gated audio pacing (D204 tempo work) is unaffected. **Note for `docs/dev/UNLOCKED-FPS-PLAN.md`:** this settles that plan's Phase 0 open question — the ~30fps default was a real port bug, not original N64 behavior or an artifact of retrace-rate tracking sim speed; the plan's default-preset baseline should be re-measured at the corrected 60fps before Phase 1 pacing work resumes. Not yet done: a full `verify.sh sweep` across all 21 levels (only `-level_09`/`-level_20` spot-checked this session) and a live human playtest for feel (input latency, animation smoothness, any level-specific perf regression now that render work happens twice as often per second). |
|
||||
| D249 | **LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV — far-away geometry has visible culling issues (M-119, user QA report).** Distinct from **D222** (which is specifically high `Video.FovScale` breaking *near-screen-edge* culling on NPCs/objects — not implicated here, default FOV) and from **D218** (the fog/far-clip *tint* boundary at wide FOV — also not the same symptom). This report is at the stock FOV, on the game's two biggest/most-open outdoor-scale levels, affecting *far* geometry specifically — points at either (a) a portal/room-visibility BFS cutting off too aggressively at range on large multi-room layouts (same neighbourhood as D233/D234's near-camera portal-cull bugs — worth checking whether D233's `gfx_sp_tri1` w<0 trivial-reject fix, which only guarded *behind-camera* vertices, has a sibling far-plane/depth-range case), or (b) GE's own N64 LOD/detail-swap system (mip-tile / simple-model substitution at distance — see D218's still-open question of "does GE have a separate geometry/object LOD-swap distinct from `Visibility.FarFog`") not being ported faithfully, popping or dropping geometry instead of swapping detail level. **Second, separate ask bundled in the same report:** the user would like LOD **improved** in general (presumably distance-based detail transitions look rougher/more abrupt on PC than remembered) but explicitly wants this **gated behind a toggle** so the original N64-accurate LOD behaviour stays selectable ("in case you want old school graphics") — i.e. this is a QoL feature request layered on top of a correctness bug, not one fix. | OPEN — not investigated. Next: reproduce on Streets (`-level_20`) and Egyptian (`-level_08`, confirm level id) at default `Video.FovScale`; `GE_PCDUMP` a long sightline where geometry visibly drops/pops, cross-referencing whatever governs GE's LOD-tile selection (`gfx_lod_tile_offset` family, already touched by D172/D219) and the portal/room-visibility walk (D233/D234 neighbourhood) to determine which of (a)/(b) is actually firing before designing a fix or the requested toggle. |
|
||||
| D245 | **IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) — "looks almost exactly like skybox" (M-116, user…** — full `## D245` entry at file tail | **OPEN — REOPENED (M-116 live playtest, same session)… |
|
||||
| D246 | **Tiny pixel strips at the left and right edges of the screen, "like overscan" — look like raw pixels, not 3D graphics (M-116, user QA report).** — full `## D246` entry at file tail | **OPEN — one candidate ruled out (M-116 follow-up), root cause not found.** Step (1) from the original plan is done… |
|
||||
| D247 | **Black bars top and bottom of screen — possible aspect-ratio defect (M-116, user QA report).** — full `## D247` entry at file tail | **NOT A BUG — confirmed faithful, unmodified N64 decomp behaviour, no code change made.** `GE_PCDUMP` captures (both against the pre-existing save and a fresh-EEPROM run, to rule out a stale `Screen` option from earlier test sessions) show the exact same inset on Frigate… |
|
||||
| D248 | **Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes).** — full `## D248` entry at file tail | **FIXED, root-caused to a genuine pointer-width ABI bug in `src/sched.c` (compiled for the PC build, not excluded).** `osScAddClient()` (`src/sched.c:187-199`) stashes a per-client "present every retrace vs.… |
|
||||
| D249 | **LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV — far-away geometry has visible culling issues (M-119…** — full `## D249` entry at file tail | OPEN — not investigated.… |
|
||||
| D250 | **FPS is inconsistent/low on many levels since the port started actually running at 60fps — drops to 20–40fps intermittently, described as worse than before (M-119, user QA report, follow-on from D248).** Very likely a direct consequence of **D248** shipping: the port's render+tick loop had been silently running at half rate (30Hz) for the entire life of the project (D248, M-118) — the *real* per-level CPU/GPU cost of rendering and simulating this port was always this high, it was simply invisible while D248 halved the work being asked of the hardware every second. D248's own write-up flagged this as an open risk ("any level-specific perf regression now that render work happens twice as often per second") and explicitly left a live playtest / perf pass as un-done. Not yet investigated which side (CPU sim, CPU fast3d command generation, or GPU draw) is the bottleneck, or whether it's uniform or level-specific (the user's report implies specific/intermittent, which fits a per-scene cost — draw-call count, overdraw, or a specific expensive subsystem — rather than a flat doubled baseline cost). **M-120: partial progress, real bottleneck not yet caught.** User report sharpened: ~45fps at 720p on a self-described high-end PC, present even outside of dedicated stress levels — worse than the coarse "many levels, intermittent" framing suggested. Two things ruled out this session: (1) **build flags are not the cause** — `build-pc/CMakeCache.txt` confirms `CMAKE_BUILD_TYPE=RelWithDebInfo` (`-O2 -DNDEBUG`) is the actual default (`CMakeLists.txt:75-76`, unconditional unless a cache already exists), matching what `./build-pc.sh` produces with no extra flags — a debug/`-O0` build is not the explanation. (2) **idle-load is not the bottleneck, at least not in this dev box:** `GE_D193=1` on `-level_09` (BUNKER1, small level) idles at a rock-steady `render=60 sim=60`, frame-render time ~2.5 µs·10^3 (2.5 ms); `-level_29` (STREETS, one of D249's flagged large/open levels) idles at `render=55-60 sim=60-62`, frame-render time 6-7 ms — both comfortably under the 16.7 ms/60fps budget with no catch-up clamp hits. **This does not reproduce the user's 45fps** — either the real cost only shows up under actual gameplay load (AI/combat/particles active, not an empty idle scene — matches D193's own "player fine, AI-driven cost is different" pattern) or it's resolution/GPU-fill-rate-bound in a way this box's small idle test window doesn't exercise (this session could not drive real keyboard/mouse input into the game to generate a loaded scene — no interactive input tooling available headlessly). Also spotted and ruled out in passing: the `D51 vi post` debug log firing every render frame in `port/src/libultra.c:781` is throttled to ~once/sec (`% 60 == 1`), not a per-frame cost — not the culprit, though it's a `TEMP D51` instrument that's been live since early porting and could be removed as cleanup. **M-120 continued, live session with the user driving the game directly — the sim/render split is now confirmed live, and several strong candidates are ruled out, but the actual cost is still unlocated.** Reproduced directly on Dam (`-level_33`): `sim` holds a healthy 59-67/s throughout, `render` sags to 45-50/s, and the CPU-side `frame rendered in Nus` cost (`gfx_run`'s software-RSP interpretation, `port/src/libultra.c:1252`) stays only 6-10ms the whole time — comfortably under the 16.7ms/60fps budget. That leaves a genuine, unexplained ~10-15ms/frame, and a live A/B pass ruled out every obvious GPU-side suspect one at a time, each via the F10 in-game overlay while watching `GE_D193`: **MSAA 2→1 and Anisotropy 16→1 — no change.** **FovScale 125→100 — no change.** **VSync 1→0 (true uncapped rendering) — render still pinned at ~60 max, never higher, and the bad-scene numbers were unchanged** (also tells us the ~60fps ceiling itself is not a vsync artifact — something else already caps the loop at 60, vsync or not). **Resolution 1280×960 fullscreen → 640×480 windowed — no change** (rules out GPU fill-rate/pixel-count entirely; the cost does not scale with pixels at all). **Found and fixed a real, separate bug in passing (did not turn out to be the active cause here):** `sync_framerate_with_timer()` (`port/fast3d/gfx_sdl2.cpp`) computes the remaining frame budget in 100ns units (`qpc_to_100ns`) and passes it straight to `sysSleep()`, which takes **microseconds** — a genuine 10x oversleep whenever that branch fires. Fixed (`left / 10`). But this path only runs when `target_fps` is nonzero (tied to `Video.FpsCap`), and the user's test config had `FpsCap=0`/`VSync=0` — confirmed live that the bug fix made no difference to the reported drops, because the buggy code path was never executing in that config. Still worth landing as a correctness fix for whenever `FpsCap`/`VSync` *is* active. **Added `GE_D250`** (`src/boss.c`, wraps `lvlRender()`, the DL-building/culling/scene-traversal steLine truncated
|
||||
| D251 | **F10 options overlay QoL pass: (a) bottom rows drawn off-screen in the in-game 2D space — the layout assumed a ~320×240 field and hard-fit all rows, but `viSetXY` gives 440×330 on front-end screens (`src/game/front.c:8618`) vs 320×240 in-game (`front.c:8628`), so with 17 rows × 15u pitch + 2 header rows the last rows land at y≈282, off-screen whenever the panel is opened in-level; (b) MSAA row only cycled forward — `rowAdjust()`'s `ROW_MSAA` case clamped its index to [0,3] instead of wrapping, so left-click stuck it at 8x with no way down via clicks; (c) manual % rows (`Video.DrawDistance`/`Video.LodDistance`) showed unconditionally even while their `*AutoFov` toggles were on, where the percent is inert/misleading. | **FIXED (port-only, `port/src/optionsoverlay.c`).** (a) The panel now computes how many rows fit the *actual* current 2D height (`maxVisibleRows()` from `viGetY()`) and scrolls: a visible-row window (`s_scroll`) follows the selection, driven by wheel (`optionsOverlayScroll`) and arrow/D-pad at the list edges; layout, mouse hit-testing (`overlayRowAtY` now returns a visible position) and keyboard nav all index the visible list (`s_visIdx`/`s_visN`, `s_sel` is now a visible-list index), so it fits both 240- and 330-tall spaces with no per-space tuning. (b) `ROW_MSAA` wraps `(idx + dir + 4) % 4` — OFF→2x→4x→8x→OFF in both directions, matching the already-wrapping `ROW_ENUM`/`ROW_RES`; sliders keep clamping (standard for drag-able values). (c) New `Row.hiddenIfOn`/`hidePtr` fields: a row is hidden while the named option is nonzero — `Draw distance %` hides under `Video.DrawDistanceAutoFov`, `LOD distance %` under `Video.LodDistanceAutoFov`; rebuilt every frame in `overlayUpdateVisible()` (selection clamped into range when its row vanishes). Hint line updated to "click / drag / arrows / wheel". **Verified:** compiles clean in the full 241-target build; final link of this session was blocked only by a running game instance holding the exe (re-link pending). Not yet eyeballed live: scroll feel at 320×240, hover/scroll interaction, and the auto/% row appear-vanish transitions. Cross-ref D213 (FPS readout in the same file), D218/D249 (the auto-coupling these rows expose). **Follow-up after live test:** (d) list navigation (wheel + arrows) wrapped at both ends, so scrolling past the bottom made the highlight "repeat" from the top — now clamped at both edges like a normal PC settings list; (e) new `Input.SensLink` toggle (default ON, registered in `port/src/input.c`) links the aim/turn sensitivity knobs: moving either rescales the other to hold the stock 38 (`Input.AimModeSens`) : 50 (`Input.MouseTurnSpeed`) ratio, via a re-entrancy-guarded pair-write in `rowSet()`; (f) the "Mouse aim speed" row now edits `Input.AimModeSens` (the knob the default GEPD aim path actually uses — it previously edited `Input.MouseAimSpeed`, which only feeds the legacy velocity-stick fallback and was inert under default settings). **Second follow-up:** hover-to-highlight removed entirely -- moving the mouse over the panel no longer moves the selection or scrolls; near a list edge the hover->select->scroll-shift->new-row-under-cursor feedback loop made the bottom of the list twitch/echo (the persistent "duplicate row" report). Selection now moves only by explicit click (which also selects the clicked row), wheel, or arrows. **Reassessment of the 4K-fullscreen ghost-row bug:** the presentation-UV-wrap theory is DEAD — `gfxFramebuffer` has zero readers (the textured-quad present never happens); presentation is a full-frame `glBlitFramebuffer` from the same-size MSAA FBO to the default framebuffer (fb index 0 is reserved for the window, `gfx_opengl_init`), which has no UVs and cannot wrap. The overlay DL draws each visible row exactly once, in-bounds, with its own scissor. Local repro (maximized 16:9 window + MSAA=4 + `GE_OPTIONSOVERLAY=1` + `GE_PCDUMP`) shows NO ghost — the FBO/MSAA path alone is not the trigger; the remaining variable is true fullscreen on the user's 3072×1728 display (driver/display-level suspect). Next: user bisection — Fullscreen ON + MSAA OFF (restart) → does the ghost persist? **User follow-up (logged, not fixed):** the 4K-fullscreen ghost is *intermittent* across launches, and the user suspects the `hiddenIfOn` appear/vanish transitions of the Draw/LOD distance % rows (visible-list resize while the panel is open) — unverified; note `overlayUpdateVisible()`+`overlayUpdateScroll()` are re-run together in both emit and input each frame, so a stale-state window would have to come from elsewhere (e.g. the one frame a row count changes). MSAA-off bisection still pending. User: in the sessions seen so far the ghost is present on *first open* of F10 (not only after an auto-toggle flip) — weakens but does not kill the appear/vanish theory (toggles had flipped in earlier sessions before a restart). **v0.2.0-pre playtest (user, latest build):** the symptom has changedLine truncated
|
||||
| D251 | ****F10 options overlay QoL pass:** — full `## D251` entry at file tail | **FIXED (port-only, `port/src/optionsoverlay.c`).** (a) The panel now computes how many rows fit the *actual* current 2D height (`maxVisibleRows()` from `viGetY()`) and scrolls… |
|
||||
| D252 | **Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).** Shooting repeatedly makes the bullet-impact "sparks" slowly progress through the full hue wheel (~10 shots to sweep it); other lingering particle effects — smoke / explosion residue — show the same rainbow colouration instead of their intended grey/orange palette. **Mechanism context (verified this session):** particle tris carry no per-primitive material; their colour comes entirely from the ROM-resident state records `globalDL_0x078 … globalDL_0xa50` (`assets/oddtextures.c`, 15 of them) replayed per particle batch (`explosion.c:894`). Each record binds a **distinct** texture pair — `IMAGE_SMOKE_N` (IA, tile 0 @ TMEM 0) × `IMAGE_FIRE_N` (RGBA16, tile 1 @ TMEM 0x188) under `G_CC_INTERFERENCE` (TEXEL0·TEXEL1), and `explosionRenderPart` uses a fixed all-white default vertex (`explosion.c:215`) — so the intended per-stage colour is whatever those 15 fire/smoke pairs encode, and particles **age through the records** (`var_s2` 14→0 at `frame/flareanimspeed`), so a per-texture decode error surfaces exactly as a hue sweep over time / across shots. **Ruled out this session:** (a) the M-82/D172 tile-offset fix is correctly configured for this path — `gsDPSetTextureLOD(G_TL_TILE)` does NOT set the `G_TL_LOD` bit, so `rdp.tex_lod` stays false (`gfx_pc.cpp:2860`) and `gfx_lod_tile_offset` returns `i`, i.e. TEXEL1 genuinely samples tile 1; (b) the RGBA16/5551 texel decode in `import_texture_rgba16` is correct (big-endian, R=bits15-11 G=10-6 B=5-1 A=bit0). **Prime suspect: the width/height derivation in `import_texture_rgba16`** — `width = rdp.texture_tile[tile].line_size_bytes / 2`, `height = size_bytes / line_size_bytes`. The fire tiles are 16 px wide with a MASK_16 TMEM line (64 bytes), so `width` computes 32 (true: 16) and, with `size_bytes` = the LOADBLOCK's 16×14×2 = 448, `height` computes 7 (true: 14) — the GL texture would be built from the right pixels at the wrong dimensions, scrambling each of the 15 fire sprites into a different wrong-hued smear; that fits "slow rainbow progression" precisely (each aging stage = a different scrambled sprite). **Not yet confirmed** — needs one imported fire tile dumped and compared against the raw `IMAGE_FIRE_N` bytes (`GE_TEXDUMP`/`GE_PCDUMP`, or a targeted probe at the `upload_texture` call). **Per user instruction: documented only, NOT fixed for v0.2.0-pre** — ships as a known issue; pick up afterwards (cheap if the suspect holds: derive width/height from the LOADBLOCK's dtx/dlh rather than the tile line size, or special-case non-32-texel-wide RGBA16 tiles). D172/M-82 lineage. **M-141 update: the width/height suspect is REFUTED by the authored asset data, not just re-tested — the prior "prime suspect" reasoning assumed `line_size_bytes` derives from the tile's `MASK_16` wrap period (64 bytes ⇒ 32 texels), but `assets/oddtextures.c`'s actual `gsDPSetTile` for the RGBA16 **render** tile (tile 1, e.g. `globalDL_0x078:50`) sets `line=4` explicitly (independent of the mask), giving `line_size_bytes = 4*8 = 32` bytes ⇒ `width = 32/2 = 16` texels — and `size_bytes` from the paired `gsDPLoadBlock(...CALC_LRS(16,14,G_IM_SIZ_16b)...)` is 16×14×2 = 448 bytes ⇒ `height = 448/32 = 14`. `import_texture_rgba16`'s existing `width = line_size_bytes/2` / `height = size_bytes/line_size_bytes` formula therefore computes the CORRECT 16×14 for every fire tile, cross-confirmed by the pre-existing `is_fire_shape = (width==16 && height==14)` filter already hardcoded into `gfx_opengl.cpp`'s `GE_TEXDUMP` dumper (D219-era) — that filter only makes sense if 16×14 is what the importer actually produces at runtime. **Do not chase width/height again.** Also checked and rejected: the render tile's `gsDPSetTileSize(1,...,CALC_TILESIZE(56),CALC_TILESIZE(56))` sets a 56×56 texel-coordinate WINDOW for UV-wrapping the 16×14 content against the co-rendered 56×56 smoke texture (`G_CC_INTERFERENCE`, TEXEL0=smoke/TEXEL1=fire) — this is not a usable content-dimension source and switching to it (the naive "use SETTILESIZE instead of line_size_bytes" fix) would have made the bug worse, not better. **GEVR-TRIAGE item E (byte-group-order hypothesis) also does not apply new information** — the original session's decode-order check (still correct: `addr[2i]<<8|addr[2i+1]`, matches the already-fixed D228 IA16 sibling) stands; no runtime capture this session found a byte-order anomaly either (see below). **Root cause is now genuinely open, not "unconfirmed suspect"** — both named hypotheses are dead. Next investigator: the TMEM slot the fire render tile always loads into (`tmem=0x188`/392, `rdp.loaded_texture[]` indexed by tmem, hard-coded identically across all 15 `globalDL_0x078..0x9a8` records) is a plausible next avenue — 15 *different* ROM source images share one fixed TMEM address and one GL-cache key derived from `{texture_addr, fmt, siz, size_bytes, .Line truncated
|
||||
| D253 | **Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck — `stanCheckLinkedSpecialTile` stores to a garbage `outFlags` pointer (D203 root cause).** The B-callback typedef `standTileLocusCallback_B_t` (`bondtypes.h:573`) declares the middle three params as `f32 f32 f32`, so on x86-64 SysV the caller (`sub_GAME_7F0B1DDC`, `stan.c:2228`) passes the 6th arg (`record`) in **`%rdx`** (3rd integer slot; the floats take xmm0–2). But the actual implementation `stanCheckLinkedSpecialTile` was decompiled with an all-`s32` prototype, so it reads `outFlags` from **`%r9`** (6th integer slot) — a stale scratch value left by earlier callees. On the N64 (MIPS o32) both prototypes agree: everything flows through GPRs a0–a5 in order, 6th arg = a5 either way, so the original is fine and the decompilation looked consistent. The mismatch stays dormant until a tile actually hits the FORCECROUCH or LADDER branch and *stores* through `outFlags` → SEGV; that's why it was level-specific (Facility's opening crouch tile) and input-dependent, and why idle repros were negative. The sibling B-callback `stanLocusEdgeIsAboveY` already had the correct float prototype, which is why only this path was booby-trapped. **Fix (ABI-class, semantics-preserving):** declare the three middle params of `stanCheckLinkedSpecialTile` as `f32` in both the forward decl and definition (`stan.c:152`, `stan.c:2407`) to match the typedef; the body never uses them, and on N64 o32 f32 args occupy the same GPRs, so N64 codegen is unaffected. **Verified:** gdb run on WSL (gcc 15) — pre-fix, SIGSEGV at `stan.c:2441` with `r9=0x3` and valid `record=0x409fea00` in the caller frame; post-fix, the same walk route completes with no crash. Side benefit: the FORCECROUCH/LADDER branches were effectively dead-on-arrival on PC (any hit = crash); they now function. **Also observed post-fix (separate, WSLg-only):** a segfault *inside libgallium* during `gfx_opengl_upload_texture` (32×48 RGBA + mipmaps) while walking further — GL-driver-side under WSLg's Mesa; not reproducible on Windows; park as a new finding if it recurs on real hardware. | FIXED — Deck-verified: user got past the crouch tile with the re-cut bundle (a *new*, later crash is D255). |
|
||||
| D254 | **Linux crash handler destroyed its own logs: `ge007.crash.log` only ever contained the SIGABRT block, never the original SEGV.** The POSIX `crashHandler` (`port/src/crash.c`) finished by calling `sysFatalError()` → `abort()` → SIGABRT, which re-entered the *same* handler (SIGABRT was one of the installed signals); the second pass reopened the log with `"wb"` and overwrote the first pass's SEGV block — faulting PC, signal, everything. The Deck firefight crash (D255) came back as a bare `SIGNAL: 6` abort record for exactly this reason, forcing offline disassembly of the CI binary to recover what the log should have said. **Fix (port-only):** re-entrancy guard (`inCrashHandler`, second pass `_exit(128+sig)` immediately) + terminate directly with `_exit()` instead of `sysFatalError()` (the Windows path already terminated directly for the same reason) + the Linux block now dumps the faulting context: `FAULT ADDR` (`si_addr`) and the full general register set (Rax/Rcx/Rdx/Rsi/Rdi/R8–R11, Rsp/Rbp) from the `ucontext`, so a field crash now names the bad pointer on its face. **Field-verified 2026-09-13:** the D255 Deck crash came back as `SIGNAL: 11` + `FAULT ADDR: 0x10` + full register set — exactly the format designed here; it directly identified the NULL-model case (see D255). | FIXED — field-verified. |
|
||||
| D253 | **Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck — `stanCheckLinkedSpecialTile` stores to a garbage…** — full `## D253` entry at file tail | FIXED — Deck-verified: user got past the crouch tile with the re-cut bundle (a *new*, later crash is D255). |
|
||||
| D254 | **Linux crash handler destroyed its own logs: `ge007.crash.log` only ever contained the SIGABRT block, never the original SEGV.** — full `## D254` entry at file tail | FIXED — field-verified. |
|
||||
| D255 | **Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle, D253 fix confirmed working — user got past the opening crouch tile).** Crash during gunplay; abort-unwind backtrace points at `sub_GAME_7F09BAC4` (`vtxstore.c:158`) ← `sub_GAME_7F09BBBC` (`:220`) ← `lvlManageMpGame` (`lv.c:1302`) ← `bossMainloop` (`boss.c:567`). Disassembling the exact CI binary at the fault PC (`0x200db135`) gives `mov 0x14(%rax),%edi; mov 0x10(%rdi),%rax`: the function walks the active PropRecord list keeping `type == 1` — which is **PROP_TYPE_OBJ, not CHR** (the decompiler cast to the union's first member and misnamed the field `ChrRecord.chrflags`; it is actually `ObjectRecord.model` at `0x14`) — then dereferences `model->obj`. Two port-relevant wrinkles: (a) the load is **32-bit zero-extended** because the decomp types the field through the `CHRFLAG` enum, so a model pointer ≥ 4 GB would truncate to garbage; (b) the loop skips records whose `model->obj` is NULL but *dereferences* `model` unconditionally, so an active object with a NULL/freed `model` faults — plausible in a firefight where destructibles (crates etc.) die. Not reproduced on WSL after equivalent play. **Register capture (D254 format, 2026-09-13):** two more Deck crashes — Bunker mid-firefight, then Frigate with ~3 NPCs alive; log: `SIGNAL: 11 / FAULT ADDR: 0x10 / Rax=0x701e5f54 Rdi=(nil) / PC 0x200db135`. `Rdi` = the record's model value = **exactly NULL** → case (b) confirmed: an active PROP_TYPE_OBJ record with a NULL `model`, dereferenced by the unconditional `mov 0x10(%rdi),%rax`; truncation (a) and stale/garbage pointers are ruled out. Faulting PC matches the recorded disassembly (`sub_GAME_7F09BAC4` = `vtxstore_fix_refs`). Grep-verified: nothing in game code ever writes NULL into `ObjectRecord.model` — `clear_model_obj()` nulls `model->obj` only, and `objInit`/spawn paths only activate a record once model ≠ NULL — so the NULL comes from either a fresh/uninitialized record slot being activated or a destroy-path ordering race (model freed/nulled while the record is still active this tick). **Fix constraint:** any guard lands in game code (`vtxstore.c`) → needs rule-#2 sign-off; `port/` cannot intercept this dereference. Next: (1) decision on whether a NULL-model skip in `vtxstore_fix_refs` qualifies as an ABI-class exception (semantics-preserving defensive guard, PD analogue check); (2) if yes, add it plus a `#ifdef PORT` trace at the deactivation/model-free sites to name the path that leaves the record model-less. **Corrected, deterministic repro (v0.2.1 user testing, 2026-09-15, superseding the initial report): the trigger is Facility's computer terminals — the ones you ACTIVATE (interact with, not shoot/destroy) to open a door for level progression.** User confirms this is consistent/reliably reproducible on those specific terminals, on Steam Deck **and** Linux generally (not Deck-hardware-specific). This is a much stronger lead than the original "sometimes during firefights" — a terminal activation deactivating/freeing its associated object model while the record briefly stays active this tick is fully consistent with the destroy-path-ordering theory above, just via a scripted trigger instead of combat destruction, and gives a deterministic repro instead of a probabilistic one. **Next session should reproduce via terminal activation specifically**, not generic combat — much cheaper to instrument. **M-140: added a diagnostic probe (`GE_D255`, off by default, `#ifdef PORT`) — `vtxstore.c`'s `sub_GAME_7F09BAC4` now skips-and-logs instead of dereferencing when it finds a NULL model on an active OBJ record (identity + state fields), and `propobj.c`'s `objFree` logs every free call for correlation. Attempted a live Windows repro this session (5 terminal activations across 3 Facility launches, one direct `-level_34` boot each time): zero probe hits, zero crashes — Windows does not reproduce this, consistent with the existing WSL-negative result. Two negatives (WSL, native Windows) against one positive (Linux/Steam Deck) points at something genuinely platform-specific (allocator/timing), not a Windows-reachable path — don't spend further Windows session time chasing a live repro.** The probes are harmless (env-gated, zero behavior change when unset) and committed so a Linux/Deck session can set `GE_D255=1` and get the trace directly. **User report, 2026-09-15 (same day, later, real Steam Deck hardware on the published v0.2.1 release — unverified by this session, logging as reported, not re-asserting the activation theory as wrong):** Facility crashes **consistently on terminals being destroyed** (shot), not (only) on activation. Frigate crashed once during this same testing pass, specifics not recorded. Shooting terminals elsewhere/other attempts did not reliably reproduce. This is closer to the *original* pre-M-139 framing (combat/destructibles triggering the NULL-model race) than the M-139 "corrected, acLine truncated
|
||||
|
||||
**ROOT-CAUSED AND FIXED, same day, live Deck session over SSH (M-140).** The `objFree`/destroy-path-race theory above was a red herring — set up a real Deck test (SSH access, key-based auth, WSL-cross-compiled local Linux build deployed and run live against the user's own prepared ROM assets), reproduced the crash directly, and symbolized it on-device against the build's own debug info. **First capture with the `GE_D255` probe active:** the probe's `ObjectRecord.model == NULL` guard did NOT fire, yet the game crashed anyway at `vtxstore.c:199` (`var_v1->RootNode`), `FAULT ADDR: 0x10`, `Rdi=(nil)` — a *different* pointer than the one the probe was checking. **Actual bug:** `sub_GAME_7F09BAC4` (`vtxstore_fix_refs`) reads the record's `Model*` through `((Model*)((ChrRecord*)prop->chr)->chrflags)` — a union pun onto `ChrRecord.chrflags`, which is typed `CHRFLAG`, a **4-byte enum**. On 32-bit N64 this is bit-exact (both the enum and a pointer are 4 bytes, so casting the enum's value to `Model*` reproduces the real stored bytes). On 64-bit PC, `Model*` is 8 bytes but the enum read is still only 4 — the cast reads just the pointer's low 32 bits as an `int` and zero-extends it back to a 64-bit address, discarding the real high bits entirely. This is the **exact D3x pointer-width ABI class** documented in AGENTS.md/porting-notes — a struct field misread across the 32→64-bit pointer-width transition — and it was *already flagged* as suspect (a) in this very row's first paragraph, back in the original v0.2.0-pre investigation, then wrongly deprioritized in favor of suspect (b) (the NULL-model theory) once a live register capture happened to show `Rdi=(nil)` and looked like it confirmed (b). It didn't: `(nil)` is exactly what a 32-bit-truncated read produces whenever the real pointer's low 32 bits happen to be zero — a coincidence that pointed the whole investigation at the wrong field for two sessions. **Proof it was the field, not the object:** this session's `GE_D255` probe read the SAME record through the *correctly*-typed `ObjectRecord.model` field (`Model*`, 8 bytes) and it was **always a valid, non-NULL pointer** — the object's model was never actually missing. **Fix (ABI-class, semantics-preserving, `#ifdef PORT`; `src/game/vtxstore.c`, `sub_GAME_7F09BAC4`):** read the pointer through `ObjectRecord.model` (the same union memory, correctly typed as `Model*`) instead of through the mistyped `ChrRecord.chrflags` alias, at both use sites in the function (the `RootNode` walk and the `modelGetNodeRwData` call). Identical bytes and identical result on N64 (both fields are 4 bytes there); correct on 64-bit PC. The earlier `GE_D255` diagnostic skip-guard (which checked the wrong field and would never have actually fired) is removed — it's no longer needed, the record's model was never really missing. **Verified live, in order:** (1) diagnostic build with the guard checking BOTH fields caught the exact moment of divergence — `ObjectRecord.model` valid, `ChrRecord.chrflags`-cast pointer producing the crash — nailing the mechanism beyond doubt; (2) the real fix (no guard, just the corrected field) held through the *entire* Facility level including multiple terminal destructions (the most consistent prior repro) and through Caverns' radio-objective terminals (the second confirmed level) with zero crashes, live on the user's own Steam Deck hardware over the ROM/assets they already had prepared. Windows rebuild + full 21-level `verify.sh sweep` also run this session as the standard regression gate. | **FIXED** — root-caused (D3x-class 32-bit pointer truncation via a mistyped enum-to-pointer cast, not a destroy-path race) and verified live on real Steam Deck hardware across both confirmed-repro levels (Facility, Caverns), zero crashes; Windows sweep clean. The separate audio-thread crash (D285) is untouched by this fix. |
|
||||
| D256 | **Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing `data/` — reported as "saves do not work at all" on Steam Deck.** `romdataInit` finds the ROM in `$S/` (CWD `data/`) *or* `$E/` (exe dir) *or* `./`, but `sysResolvePath`'s POSIX `$S` branch (used by `ge007.eep` and `ge007.ini`) only ever produced a CWD-relative path — its "fallback" was `./data/`, identical to the primary. Launch from any other CWD (Steam non-steam-game shortcut, terminal in `~`): ROM loads via the exe-dir candidate, game runs, but every EEPROM/config write hits `fopen(..., "wb")` on a nonexistent `./data/` and fails with a stderr-only "cannot write" — saves never persist, and **every launch also starts with default settings** (F10 changes don't stick), which presents as "draw distance/LOD is low" on levels like Dam where the user had raised `Video.DrawDistance` on another machine. The Windows branch already did CWD-else-exedir; the POSIX branch now mirrors it (`readlink("/proc/self/exe")`) and creates the directory best-effort so first-run writes don't fail. **Verified on WSL:** with exe+`data/` in `/tmp/fb2` and CWD=`/`, config/eeprom/ROM all resolve into `/tmp/fb2/data/` and a 25 s run completes clean; the pre-fix binary in the same layout fails to find the ROM at all. **Caveat:** if the user's Deck launch *was* from the bundle dir (CWD had `data/`), this fix does not explain their save failure — need the save-flow detail (does the memory-card screen appear? slot confirm? "saved" message?) before closing the report. Side note found while testing: a `data/` without the generated sidecars (`pccg-*/`, `pcmodels-*/` from `tools_pc/d69_emit.py`/`d43_emit.py`) starts fine but faults in `load_bg_file` (`bg.c:865`) on level load — the reserve functions warn "stage loads will fault" and return 0; expected degraded mode, not a new bug. | FIXED (path half) — save-flow detail still owed from the user. |
|
||||
| D257 | **New `Game.AllUnlocked` option (F10; default flipped OFF for v0.2.0 — see D259): skip N64 progression entirely — all 21 solo levels selectable at every difficulty, 007 mode available, and the cheat menu fully populated. Port-layer only; no game-code edits.** Three mechanisms, all in `port/`: (1) `main.c` seeds the two live `LEFTOVERDEBUG` RAM flags after `configLoad()` — `debug_enable_all_levels_flag` (all levels) and `debug_007_unlock_flag` (007 mode; both in `src/game/debugmenu_handler.c`, same class as the existing `GE_UNLOCK_ALL` getenv hook in the getter); (2) the eep shim (`libultra.c geEepromPatchAllCheats`) patches the save block at read time — special-casing `fileValidateSaves`'s exact pattern (block 4, 5×96 bytes) — setting all 20 progression-gated cheat-unlock bits per slot (`unlocked_cheats_1/2/3`; the cheat menu row on mode-select only renders when ≥1 is set, `front.c interface_menu06_modesel`) **and** filling every *empty* stage/difficulty completion-time field with the max time 0x3FF (existing player times preserved); both are then CRC'd per slot with the game's own `fileGenerateCRC` or `fileValidateSaves` would wipe the slots. (3) `optionsoverlay.c` row so users can toggle OFF for faithful progression. **Why the completion-time half is required (found by user repro):** when ANY single-player cheat is active, `g_AppendCheatSinglePlayer` makes mission-select (`get_highest_unlocked_difficulty_for_level`, front.c) accept only levels with status `STAGESTATUS_COMPLETED (3)`, not merely `UNLOCKED (1)` — AllUnlocked's debug-flag path returns 1, so enabling a cheat locked every level ("could only do Silo"). Reading the save as a fully-completed campaign satisfies both branches. Time bit-math mirrors `fileGet/SetSaveStageDifficultyTime` verbatim (10-bit fields, difficulties 0–2; 007 times are virtual). **Pitfall hit during implementation:** the first mirror of `save_data` used `times[80]` instead of `(SP_LEVEL_MAX-1)*4 = 76`, making `sizeof` 100≠96 so the block-read size check silently never matched — now enforced by a compile-time `ge_save_slot_size_check`. **Semantics:** unlocked cheats/completions persist to `ge007.eep` on any subsequent save write (self-consistent; mirrors N64 where completing a level permanently unlocks its cheat). No *active* cheats are enabled (invincibility/all-guns stay OFF); weapons remain per-mission pickups. **Verified:** fresh eep — all 5 slots validate `stored==calc` under `GE_SAVELOG=1`; user playtest: all levels selectable at each difficulty, 007 mode present, cheat menu populated, cheats toggle and work (time-modifying cheats untested by user), mission select intact with all cheats ON. | FIXED — verified by user playtest (PR #79 merged); default flipped OFF for v0.2.0 (D259). |
|
||||
| D259 | **`Game.AllUnlocked` (D257) breaks audio on a fresh save: with it ON, music and SFX load at volume 0 (silence). v0.2.0 ships with `AllUnlocked` default OFF.** User-observed during v0.2.0 artifact testing ("all unlocked seems to break sound"). **Mechanism:** a fresh `ge007.eep` is zero-filled, so every save slot is all-zero. On N64 such a slot fails `fileValidateSaves`' CRC check and is rewritten as `BLANKSAVEDATA` (`src/bondconstants.h:2148`), whose `music_vol`/`sfx_vol` are `0xFF` (max). With AllUnlocked ON, `geEepromPatchAllCheats` (`port/src/libultra.c`) patches the zeroed slots (cheat bits + completion times) and recomputes a **valid** per-slot CRC, so `fileValidateSaves` accepts them as-is and never calls `fileResetSave`. The volume bytes therefore stay 0, and `fileLoadSettingsForFolder` (`src/game/file2.c:1289-1290`) loads them verbatim: `set_mTrack2Vol((save->music_vol << 7) | (save->music_vol >> 1))` = 0 → silence. With AllUnlocked OFF the zeroed slots fail CRC and reset to `BLANKSAVEDATA`, which is why sound works in the default configuration. **Disposition for v0.2.0:** flip `portAllUnlocked` default to 0 (`port/src/video.c`; `SkipIntro` was already 0) — faithful N64 progression out of the box, F10 *All unlocked* still enables the goodie. The quirk then only affects users who deliberately toggle it ON with a fresh save (recoverable: raising the in-game volume writes a nonzero slot, after which the patch leaves volumes alone). **Deferred fix candidate (port-only, ~6 lines):** in `geEepromPatchAllCheats`, detect all-zero slots and seed `music_vol`/`sfx_vol = 0xFF` before the CRC recompute, emulating what `fileResetSave` would have done; real (nonzero) saves incl. a user-muted one stay untouched. | **FIXED in v0.2.1** — the deferred fix was applied: `geEepromPatchAllCheats` now detects all-zero slots and seeds `music_vol`/`sfx_vol = 0xFF` before the CRC recompute, emulating `fileResetSave`; real saves (incl. a deliberately muted one) stay untouched. **By-ear confirmed by the user** (v0.2.1 test pass: fresh save, AllUnlocked ON — audio plays correctly). |
|
||||
| D260 | **v0.2.1 (user request): mouse-wheel weapon-select direction swapped.** Wheel-up now selects the *previous* weapon and wheel-down the *next*, matching the in-game wheel menu's on-screen layout (the port had them inverted vs the user's expectation). Port-only: `inputPostWheel()` (`port/src/input.c`) — the notches>0 / notches<0 branches were swapped so scroll-up emits the back-notch and scroll-down the forward-notch. N64 build untouched; no game-code edits. | FIXED — build green; one-line branch swap, trivially reviewable. |
|
||||
| D261 | **v0.2.1 (user request: "fix watch menu sensitivity for wasd/controller buttons"): holding W/S (or a fully-pressed stick axis) on the watch inventory page crawled one item per tap — no repeat.** Root cause is the interaction of two existing behaviours: keyboard W/S map to `stickY = ±STICK_MAX (0x50)` and SDL pads peg at 0x80, both *outside* the smooth-scroll band (0x1f..0x45) that D118d intentionally left for the port — so a held press only ever hit the latched single-step path (`watch_stick_y_pressed_*`, one step per press edge, no repeat). On N64 the same stick level was the *fast-scroll* band (one step per frame), which D118d removed because digital sources peg there and it became runaway scroll. **Fix (port-only, `#ifdef PORT` in `src/game/options.c`):** a bounded auto-repeat helper `geWatchStickFastStep()` — while the stick is held past the smooth band (≥ 0x46 up / ≤ −0x45 down), step one extra item every `GE_WATCH_NAV_RATE` (6) frames after an initial `GE_WATCH_NAV_DELAY` (15)-frame hold; the first press still goes through the untouched latched path, so a tap behaves exactly as before, and the smooth band + D-pad/C-button edge-repeat are unchanged. Called from `game_options_inventory_navigation()` only. **Verified:** build green; headless smoke (`GE_INPUTSCRIPT` long-W-hold on the inventory page) runs clean. Feel-check (repeat rate) owed to the user — both constants are at the top of the helper for a one-number tune. **User feel-check (v0.2.1 test pass): repeat "feels fine for now", but W/S is still too sensitive overall — desired behaviour is tactile one-press-one-item with no risk of scrolling the whole list on an over-held key; candidate tweaks (not yet done): longer `GE_WATCH_NAV_DELAY`, slower `GE_WATCH_NAV_RATE`, or gating the auto-repeat off for keyboard taps entirely (repeat only for analogue stick). Logged, no code change. | FIXED — feel-check received; W/S sensitivity follow-up logged above. |
|
||||
| D262 | **v0.2.1 (user report): "3D models missing on watch menu screen item select" — the selected-item 3D preview does not appear on the watch inventory page.** User visually confirmed this session: no 3D model on the item-select page where one should be (default selection is *unarmed*, which has no model — a weapon must be scrolled to with S/DOWN). Headless repro (`GE_INPUTSCRIPT` pulse train + `GE_PCDUMP`, Dam `-level_33`): watch can't open during the intro freeze (`bondviewFrozenMoveBond` zeroes input while `g_CameraMode == CAMERAMODE_INTRO`; freeze ends ~frame 1400–1500 on Dam, and controller reads run faster than sim ticks, so scripts are timed by trial); once open + paged to INVENTORY (R_JPAD), the watch UI renders but the centre of the face is empty for every item. **Root cause found and fixed as D264** (`renderdata.flags == 0` in the watch subdraw pass — the cross-global ModelRenderData template read zeroes on PC; `subdraw()` then gated *every* geometry node, emitting exactly 1 word instead of the model's commands). Residual: with flags restored, the preview emits matrix/light-setup words (71 for item 5) but dump frames still show no visible centre geometry — a possible second layer (LOD/distance gating or fast3d rasterisation of the sub-DLs) remains unverified; user visual re-check pending. | FIXED (primary cause, D264) — pixel-level confirmation pending user run. |
|
||||
| D264 | **D262's root cause (ABI/layout class, cf. D140/D52): the watch item preview's `ModelRenderData` template is read across two adjacent globals that no longer sit adjacent on PC.** `set_enviro_fog_for_items_in_solo_watch_menu()` (`src/game/gunfire.c`) starts with `ModelRenderData renderdata = *((ModelRenderData *) (&D_80035D00));` — a 64-byte copy that on N64 spans `D_80035D00` (a zeroed u32, .bss) *and* `D_80035D04[] = {1, 3, 0…}` (.data): the effective template is the one documented in the comment above `D_80035D00` in `gun.c` — `basemtx=NULL, zbufferenabled=TRUE, flags=3, rest zero` (`CULLMODE_BOTH` is 0). The PC link places .bss and .data in separate sections, so the read is all zeros: **`flags==0`**, and every geometry emitter in the subdraw node dispatch gates on `renderdata->flags & 1/2` (e.g. `modelRenderNodeDl`) — so `subdraw()` emitted exactly one word (`gSPSegment`) per preview, no matter the item. Probes this session: entry gates all pass (hand has item, model header non-NULL), node tree present (~400 nodes, isLoaded=1), world-path geometry nodes carry valid built DLs — but in the watch pass `flags=0` on every node. **Fix (`#ifdef PORT`, values identical to the N64 ROM bytes):** explicit template — `ModelRenderData renderdata = {0}; renderdata.zbufferenabled = TRUE; renderdata.flags = 3;`. Verified at Gfx level: preview emission went 1 → 71 words (item 5) / 81 (mid-scroll) / 9 (item 47, knife-class). Note for follow-up: the 71-word dump is a repeating {matrix/segment/light} unit with no `gSPDisplayList` in the first 24 words — if the user's re-check still shows no visible model, next suspects are LOD/distance gating of the local watch-space matrices (`modelUpdateDistanceRelations`, cf. 7c48f664) or fast3d handling of the preview sub-DLs (segment 3 / render modes). | FIXED — build green, probes removed; visual confirmation pending. |
|
||||
| D263 | **v0.2.1 (user report): Facility — "Ourumov not shooting Trevelyan"; the scripted opening beat never triggers. RESOLVED: not a port bug — the beat is proximity/LOS-triggered exactly as on N64, and the full chain (incl. the execution shot) was verified firing end-to-end on PC.** The triage suspects (tick pacing, camera-mode gate, silent anim lookup) were all ruled out at runtime. **The beat, fully mapped** (static decode of `UsetuparkZ.c` — remember every DBYTE/TBYTE in the dump is bswap16/32'd relative to game interpretation): Trevelyan = guard chr 67 (body 9 = Trevelyan_006, pad 131) on ai_18: walks to pad 124, wanders, then loops a sees-Bond check; **only when Bond is in his vision** (range <10k, facing ±110°, fog, random gate ≈1/49/tick, LOS + *same stand tile* via `chrCanSeeBond`) does he jump to ai_19: runs at Bond, alarm + dialogue timers, sets an objective bit; BG list ai_41 (ID 0x1000, auto-started) polls that bit and spawns **Ourumov (body 7) at pad 88 with ai_23** plus a 6-soldier squad; ai_23 renumbers him to chrnum 78 and arms him → ai_22: runs to pad 123, waits for Trevelyan within 0x1400 of it, ~95 s dialogue cascade, then the **execution shot** (`HitChrWithItem` from 78 on 67). It is a multi-minute sequence that *starts* only when the player walks Bond west and down a floor into Trevelyan's room — Bond spawns 8900 units away on the upper floor with LOS blocked (probed directly: `stanTestLineUnobstructed` = obstructed, different stan tiles), identical geometry to N64. **Runtime verification** (`-level_34`, `#ifdef PORT` probes in the AI executor/vision path, all since removed): all 15 BG lists + guards start; Trevelyan alive on ai_18; every vision gate passes except the geometric ones at spawn distance; player movement confirmed working (note for future headless tests: under the port's default SOLITARE/natural-pitch scheme, walking is C-button/D-pad fed — W→C-up — and the analog stick carries pitch, so `GE_INPUTSCRIPT` SUP/SDOWN tokens do *not* walk Bond there; use UP/DOWN pulses). **Decisive test:** a temporary PORT-only teleport probe dropped Bond 65 units from Trevelyan at t≈12 s → seconds later: vision fires (chr 67 → ai_19), objective bit set, `SpawnChr body=7 pad=88` + squad, chrnum→78 on ai_23; ~2½ min later **`HitChrWithItem from_chr=78 target=67 part=8 item=6`** — Ourumov executes Trevelyan. Most likely explanation of the report: the tester never walked Bond into Trevelyan's sightline (or pre-D253 crash masked it). No game-code or port-code change required. **Kept from this session (test-harness only):** `port/src/input.c` — `GE_INPUTSCRIPT` analog-stick tokens were applied *after* the stick was already written out, so SUP/SDOWN/SLEFT/SRIGHT were silently discarded; the script override now runs before the clamp/write. | RESOLVED (not a bug), but refined (v0.2.1 further user testing, 2026-09-15) — beat verified firing end-to-end on PC incl. execution shot; **user playtest confirmed the sequence plays normally.** **Refinement: the exact trigger behavior is significantly sensitive to Bond's precise location and does not fire 1:1 identically to the N64 original across different approach paths/positions** — consistent with the beat's own documented sensitivity (range/facing/LOS/same-stand-tile + a ≈1/49-per-tick random gate, see the mapped mechanism above), so this is likely the same finicky-trigger behavior rather than a new bug, but not confirmed byte-identical to N64 for every approach. **User confirms this does NOT block level progression** either way — logged for awareness, not tracked as an open defect. Probes removed; input-script stick fix kept. **M-141: re-confirmed by user, one more mechanism now documented — the standoff has a hard TIMEOUT fallback, not just the shot-miss/objective-bit gates.** User reported (misread at first as a new freeze) reaching the Ourumov/Trevelyan standoff, waiting without shooting or triggering anything else, and after a couple of minutes Ourumov delivered his lines and executed Trevelyan on his own — **this is by design**: `ai_22`'s label `0x05` OR-gate (four early-exit conditions: two objective bits, a "Bond's shot missed" check, and a third objective bit set by an unrelated guard's surrender elsewhere in the level) is not the only path forward — falling through the whole dialogue-timer cascade (`label 0x42`'s chain of `if_local_timer_greater_than` checks) reaches `label 0x13`, which unconditionally fires `chr_hit_body_part_with_item_damage(0x43, 0x08, 0x06)` (the execution) once enough real time has elapsed, no player action required. Matches this row's own earlier "~2½ min later" measurement. **Do not re-flag "stuck standoff" as a bug without confirming the user waited the full timeout window first.** |
|
||||
| D265 | **D236 investigation (port-layer, `port/fast3d/gfx_pc.cpp`): the tree cards are IA8/IA4, not IA16 — and `import_texture_ia4` decoded the RDP's I2:A2 texel as I3:A1, inverting the alpha ramp. Fixed; awaiting user visual re-test.** The earlier sessions misidentified the format: a live tri-time tile census (probe in `gfx_sp_tri1`, since removed) shows level_36's tree-card classes are (a) **IA8, line=64B → 64×16** (texdump r107), drawn OPA/bilerp/1-cycle/AC_NONE — *genuinely fully opaque* (A nibble F on 1023/1024 texels; opaque on N64 too) — and (b) **IA4, line=64B → 128×16** (texdump r117), drawn ZMODE_XLU/point/2-cycle with m2a=CLR_MEM/m2b=1MA, i.e. texel-alpha blended. There are no 32-wide IA tiles at tri time; the prior "32×32 tile" reading was wrong, and the whole earlier IA16 byte-pair/I11:A5 debate (r107) was analyzing an IA8 image as byte-pairs. **The wall's root cause:** r117 decoded as I2:A2 is a strip of ~13 discrete conifer silhouettes (trunk+canopy shapes visible in the ASCII render, `scratch/r117_ia4_alpha.png`) with a 3-level alpha ramp {0: 95% of texels, 85, 170} — faint dithered distant trees on N64. But `import_texture_ia4` read each nibble as intensity=`part>>1` (3-bit, SCALE_3_8) + alpha=`part&1`: A2=1 canopy pixels became **fully opaque**, A2=2 edge dithering became **invisible** → opaque repeating tree silhouettes = the "wall". The bug is inherited verbatim from the PD port (`pd_port/port/fast3d/gfx_pc.cpp` has the identical I3:A1 read — latent there, fatal for GE's tree strip). **Fix (applied):** `intensity = part>>2`, `alpha = part&3`, both via new `SCALE_2_8`; this is the hardware-correct RDP IA4 layout. Also cleaned up: the ia16 importer comment now states plain I8:A8 (the byte-pair read *is* correct IA16; the old "r107 proof" was misidentified data), and the earlier part-1 dimension fix stays (harmless/correct for real IA16 tiles). **Caveats/owed:** (1) user visual re-test of Surface 1 trees — headless A/B was in flight when work halted; a cross-run region diff was inconclusive (camera drift between captures); (2) the decode change affects *every* IA4 tile game-wide — regression-sweep other levels for dithered/alpha IA4 art that now renders with correct 3-level alpha; (3) if a hard repeating band remains after the fix, it is the opaque IA8 strip (a), which matches hardware and D236 needs re-scoping. **OUTCOME (user re-test): NEGATIVE — trees still render as the wall with the I2:A2 decode applied; the change was fully reverted (`git checkout` of `port/fast3d/gfx_pc.cpp`, back to the original PD-derived importers) and work is parked.** The census/RM/texture data above stands as evidence for the next attempt: the decode was *necessary* (I2:A2 is the hardware layout and r117 is unambiguously an authored tree strip under it) but *not sufficient* — remaining suspects, in order: (i) fast3d's blending of these XLU/2-cycle cards (texel alpha reaching the GL blend at all — the `texture_edge`/`SHADER_OPT_TEXTURE_EDGE` path forces `a>0.19 ? 1 : discard`, which for a 3-level ramp quantizes to opaque/discard, i.e. still hard-edged), (ii) per-card UV windowing/placement (all captured cards sampled the same u:0..64/v:0..16 window — confirm against N64), (iii) fog/distance. Note for the next session: verify what the N64 actually shows in that region before more port-side changes (the opaque IA8 strip is faithful to hardware either way). | PARKED — reverted to baseline; evidence + suspect list above. |
|
||||
| D258 | **PAL/JP asset conversion is broken at the source-data level — `scripts/filelist.e.csv` and `filelist.j.csv` carry naming defects, so `gen_romassets.py e/j` (and the d43/d69/d88 emitters' `find_row`) cannot resolve PAL/JP builds. v0.2.0's drop-in ROM feature therefore ships NTSC-U only.** Discovered while scoping issue #6 (drop-in ROM). **PAL:** 31 of the 512 model rows in `filelist.e.csv` have truncated/renamed basenames (an extraction artifact — e.g. `Pbridge_console` ×6 where the resource table says `Pbridge_console1aZ…3bZ`, `Pchrgrenadelauncher` for `PchrgrenadelaunchZ`). **JP:** one font row is mislabeled — `fontBankGothic_fontchartable.bin` @3044276 is named `fontZurichBold_fontchartable.bin` (a duplicate label). Both were proven without a PAL/JP ROM: (a) greedy two-pointer sequence alignment of the 512-row `file_resource_table.inc.c` model order against each CSV's model rows gives an exact 1:1 match (481 by-name + 31 positional for PAL), offsets strictly non-decreasing, and zero size anomalies vs US (all within [0.5×, 2×]); (b) JP font row proven by the contiguity invariant — Gothic kerning @3043600+676 = 3044276 exactly hits the mislabeled row, whose size (8716) equals the US Gothic chartable, while Zurich's own chain is contiguous one row later. **Consequence:** no PAL or JP PC exe has ever been buildable from this tree (latent — only NTSC was ever exercised); the CSV `file` column is a label only (emitters look up by basename, never read the path), so the repair is a pure data fix: rename the 31 PAL model rows + 1 JP font row to the exact table symbols. **Scope decision (v0.2.0):** ship drop-in ROM support for NTSC-U only — `prepare-assets.py`/the frozen `ge007-convert` gate non-US ROMs with a clear message after SHA-1 region detection; the CSV repair + multi-region matrix + per-region exes are deferred to the Part B follow-up. | OPEN — repair path verified, not applied (out of v0.2.0 scope). |
|
||||
| D256 | **Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing `data/` — reported as "saves do not…** — full `## D256` entry at file tail | FIXED (path half) — save-flow detail still owed from the user. |
|
||||
| D257 | **New `Game.AllUnlocked` option (F10; default flipped OFF for v0.2.0 — see D259): skip N64 progression entirely — all 21 solo levels selectable at every…** — full `## D257` entry at file tail | FIXED — verified by user playtest (PR #79 merged); default flipped OFF for v0.2.0 (D259). |
|
||||
| D259 | **`Game.AllUnlocked` (D257) breaks audio on a fresh save: with it ON, music and SFX load at volume 0 (silence). v0.2.0 ships with `AllUnlocked` default OFF.** — full `## D259` entry at file tail | **FIXED in v0.2.1** —… |
|
||||
| D260 | **v0.2.1 (user request): mouse-wheel weapon-select direction swapped.** — full `## D260` entry at file tail | FIXED — build green; one-line branch swap, trivially reviewable. |
|
||||
| D261 | **v0.2.1 (user request: "fix watch menu sensitivity for wasd/controller buttons"): holding W/S (or a fully-pressed stick axis) on the watch inventory page…** — full `## D261` entry at file tail | FIXED — feel-check received; W/S sensitivity follow-up logged above. |
|
||||
| D262 | **v0.2.1 (user report): "3D models missing on watch menu screen item select" — the selected-item 3D preview does not appear on the watch inventory page.** — full `## D262` entry at file tail | FIXED (primary cause, D264) — pixel-level confirmation pending user run. |
|
||||
| D264 | **D262's root cause (ABI/layout class, cf. D140/D52): the watch item preview's `ModelRenderData` template is read across two adjacent globals that no longer sit…** — full `## D264` entry at file tail | FIXED — build green, probes removed; visual confirmation pending. |
|
||||
| D263 | **v0.2.1 (user report): Facility — "Ourumov not shooting Trevelyan"; the scripted opening beat never triggers. RESOLVED: not a port bug — the beat is…** — full `## D263` entry at file tail | RESOLVED (not a bug), but refined (v0.2.1 further user testing, 2026-09-15) —… |
|
||||
| D265 | **D236 investigation (port-layer, `port/fast3d/gfx_pc.cpp`): the tree cards are IA8/IA4, not IA16 — and `import_texture_ia4` decoded the RDP's I2:A2 texel as…** — full `## D265` entry at file tail | PARKED — reverted to baseline; evidence + suspect list above. |
|
||||
| D258 | **PAL/JP asset conversion is broken at the source-data level — `scripts/filelist.e.csv` and `filelist.j.csv` carry naming defects, so `gen_romassets.py e/j` (and…** — full `## D258` entry at file tail | OPEN — repair path verified, not applied (out of v0.2.0 scope). |
|
||||
| D284 | **`port/src/random.c`'s MIPS64 `dsll32`/`dsrl32` shift helpers were backwards (modelled as 32-bit shifts, not "shift by n+32 on the full 64-bit register"), desyncing the PC PRNG stream from the N64's from the very first draw, in every release to date (M-138 security/fidelity review; fix decided M-139b).** Fix: `dsll32(x,n) = x << (n+32)`, `dsrl32(x,n) = x >> (n+32)`, no masking — matches real MIPS64 semantics and `src/random.s`/`src/game/chrObjRandom.s` line-by-line. Verified by hand trace + an independent standalone C reproduction (both diverge from the buggy stream from draw 1). Full body below. | FIXED, 21-level sweep clean, no crashes — RNG stream changes from every prior build; existing replays will diverge, savegames unaffected |
|
||||
| D285 | **A real Steam Deck SIGSEGV caught during D255 live-testing (v0.2.1 published binary, symbolized on-device via `addr2line` against its own debug symbols) is a SEPARATE bug from D255** — `FAULT ADDR: 0x62` inside the **audio thread** (`sndHandleEvent`, `src/snd.c:366`, via `amMain`/`portThreadWrapper`), not D255's `0x10`/`vtxstore_fix_refs` on the render/game-logic thread. Root cause (static, not yet gdb-confirmed): the pool-exhaustion preempt-scan in `sndHandleEvent` reads/walks the shared `D_800243E4` active-`ALSoundState` list with **no `osSetIntMask` lock at all** (checked: absent from the function's entire body), while the list's insert path (`sndSetupSound`, reached from 15 different `src/game/*.c` files' direct `sndPlaySfx` calls, e.g. `explosion.c` — i.e. from whatever thread the caller runs on) **does** lock it — an asymmetric-locking gap, the same D4/D147/D152 "N64 interrupts-off is not a real lock on PC" class. Fits the repro pattern (explosion SFX bursts = high-contention window; Frigate's lower SFX load not reproducing it fits a timing race, not a deterministic trigger). Full body below. | **FIXED (M-141, `src/snd.c`)** — `osSetIntMask` lock added around the preempt-scan, matching `sndSetupSound`'s existing unconditional (no `#ifdef PORT`) pattern in the same file; build green, `-level_09`/`-level_20` unregressed (1400+ frames, no crash, no D152 lock-stuck warnings). Not yet gdb/live-confirmed against a real reproduced crash — see M-141 body. Do not conflate with D255 — different thread, different subsystem. |
|
||||
| D286 | **User-confirmed real (not system-side) ~8s frame freeze on Caverns, Steam Deck, caught during the same live session as D255/D285 — self-recovered, not a crash.** The game's own hang watchdog (`portHeartbeatCheck`) fired once (no frame for ~8s, `frames=46404`), dumped all-thread state via `crashDumpThreads`, then rendering resumed normally and the session continued to a clean close. Not tied to an explosion/terminal (rules out D255/D285). Not root-caused — no distinguishing log line precedes it; leading unconfirmed guess is a heavy room/asset stream-in stall (Caverns is a large level, cross-ref D250's FPS lineage, though the symptom shape differs). Also fixed in passing: the heartbeat log printed a raw microsecond value with a `ms` label (cosmetic, `port/src/libultra.c`). Full body below. | OPEN — one confirmed occurrence, no repro trigger identified, not root-caused; distinct from D255/D285. Do not chase a repro without a better lead. |
|
||||
@@ -10710,3 +10710,123 @@ D152 addendum (M-31): static audit of every compiled-audio `osSetIntMask(OS_IM_N
|
||||
|
||||
**Status (from §F table):** **OPEN — M-109 re-analysis of the same session's raw logs corrects the timing/pattern claims above; the D134/pendingGfx race hypothesis is likely wrong.** Confidence MEDIUM on the correction, LOW-MEDIUM on the new lead. Re-reading `data/playtest_d160.log`/`_run2.log` at the `g_framesRendered` level (not just grepping for `D146`) shows the D146 burst fires **~1600-3900 frames (tens of real seconds) *after*** `bossReturnTitleStage`/the `camera_switch(7)` calls, not during them, in **both** runs — "right in the camera-cut window" and "run 1 only" are both wrong as stated above. This also weakens the D134/`pendingGfx` citation: `osSpTaskStartGo` runs `gfx_run()` synchronously and inline (`port/src/libultra.c:1232-1258`), so nothing from the cutscene's own DL submission is still in flight by the time any teardown code runs — the port doesn't have the async-RSP race the hypothesis above assumes. **New lead:** whatever screen renders in that multi-thousand-frame gap after mission-complete is the more likely site — probably `MENU_MISSION_COMPLETE` (`front.c:7267`, `constructor_menu0D_missioncomplete`) or an adjacent briefing/file-select screen in that chain, same general class as D75 (front-end model absence) / D141 (menu bad-pointer bug), not a cutscene-render race. Of the 4 repeating fault addresses, 3 read as literal zero words and only 1 matches the `0xfafbfbfc…` pattern, arguing against a deliberate poison-fill (none found anywhere in `port/`) and for either genuinely-unwritten memory or a decode landing inside unrelated valid data. Next: an env-gated `front.c` menu-transition trace (`MENU_*` id + `g_framesRendered` on every transition after `bossReturnTitleStage`) plus a `GE_PCDUMP` window over frames ~4500-8000 (not the camera-cut window) to name the exact screen/segment. Cross-ref D160 (this supersedes its stale "cutscene skipped" framing with a "cutscene renders corrupted" one, now further corrected to "corruption isn't in the cutscene render at all") and D173 (Cradle's Bond-floating/spinning cutscene bug — same subsystem family, unconfirmed shared root cause). **M-120: the `GE_D243` trace above was BUILT (statically, i.e. written) on branch `investigate/d243-cutscene-race` (`764a1f26`, pushed, not merged) — `src/game/front.c` only, `#ifdef PORT`, logs the target `MENU_*` name + a local per-tick counter on every `menu_init()` state transition. NOT build-verified: two independent sessions (the dispatched subagent, then this session re-verifying in a fresh worktree) both hit the same reproducible failure — `cmake`'s C-compiler ABI check fails with "Cannot create temporary file in C:\Windows\: Permission denied" when gcc is invoked *through* CMake/ninja, even though a bare `gcc -c` invocation from the same shell, same session, succeeds and correctly resolves its temp dir to `%TEMP%`. This is a genuine, reproducible environment issue distinct from the diff's content (static review of the diff found nothing wrong: enum cases all verified to exist in `bondconstants.h`, brace/`#ifdef` balance checked, follows the existing `GE_D160`/`GE_D178` env-gated trace idiom used elsewhere in the same file) — worth a session with a working toolchain investigating the CMake/ninja-vs-bare-gcc environment discrepancy itself as its own item, separate from D243. Next: build `investigate/d243-cutscene-race` on a session where `./build-pc.sh` actually completes, then run the `-level_33` Dam repro with `GE_D243=1` (and the existing `GE_D160`/`GE_PCDUMP` mechanisms) to correlate the trace against the D146 burst's frame range. **M-141: the M-120 toolchain blocker is EXPLAINED and worked around, GE_D243 is now build-verified.** The "Cannot create temporary file in C:\Windows\" failure isn't specific to CMake/ninja vs. bare `gcc -c` — it's this Claude Code session's `Bash` tool shell (Git-for-Windows Bash, not a real MSYS2 MINGW64 window) not exposing a usable `TMP`/`TEMP` to child processes the way an interactive MSYS2 shell does; a bare `gcc -c` from the same shell happened to not hit the temp-file path M-120 tested, which is why it looked clean while the full build wasn't. Fix: explicitly `export TMP=... TEMP=...` (Windows-style path) alongside the already-known `PATH=/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH` fix, both before invoking `./build-pc.sh`/`ninja`/`cmake` — now also recorded in `porting-notes.md` §E as a general session-start gotcha, not just a D243-specific note. Cherry-picked the `GE_D243` trace commit (`764a1f26`) cleanly onto `release/v0.3.0` (branch `investigate/d243-fresh`), fixed one thing the original diff got wrong per this project's own D250 rule (`menu_init()` runs ~once/frame during front-end states — the diff's three `getenv("GE_D243")` calls were uncached; now a single cached `static int s_d243` check), and confirmed it builds clean. **Smoke-tested, not fully exercised:** a bare `-level_33` boot runs 1100+ frames with zero crashes and (correctly) zerLine truncated
|
||||
|
||||
## D244 — Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the…
|
||||
|
||||
**Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the reinstalled box, run `34707798707`).** `src/game/objective_status.c:162` uses `#pragma weak objectiveGetStatus_WEAK = get_status_of_objective` (a decomp ground-truth alias, referenced 2-arg from `chrai.c:2538` — comment calls it "horrible hack to get ai matching"). The box's `g++`/`ld` reported as `x86_64-w64-mingw32/16.1.0`; a minimal local repro of the identical `#pragma weak` cross-TU function alias **links cleanly** on this session's own machine (`mingw-w64-x86_64-gcc 16.2.0-3`, `binutils 2.47-3`). Not a game-logic bug (the alias is unmodified decomp code, ground truth) and not a CMakeLists/link-flag issue (no `--gc-sections`/LTO in play) — the leading, unverified explanation is the self-hosted box's MSYS2 packages predate a PE/COFF weak-external fix or behavior change versus current MSYS2, consistent with rollout step 2's earlier finding that the box's mingw64 package set was stale/incomplete at initial setup.
|
||||
|
||||
**Status (from §F table):** **OPEN, not yet fixed.** Next: on the box, `pacman -Syu` (or at minimum `pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-binutils`) to bring the toolchain current, then re-dispatch `selfhosted.yml`. If updating doesn't clear it, the fallback is a narrow `#ifdef PORT` replacement of the alias with an explicit thin wrapper function in `objective_status.c` (ABI-preserving, no logic change) — but try the toolchain update first since the local repro suggests this is a version/environment gap, not a fundamental PE/COFF limitation.
|
||||
|
||||
## D270 — Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.
|
||||
|
||||
**Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.** D236 wall, between-pass instrumentation toolkit for the fog hypothesis (D269 ranked suspect #1) and general A/B isolation: `gfx_sp_tri1` per-vertex fog min/max attribution; `GE_D270SKIP`/`GE_D270SKIPOML`/`GE_D270KEEPOML`/`GE_D270KILLALL` (drop-by-oml/fmt A/B rules); `GE_D270PIX` (single-pixel draw-order attribution); `GE_D270FOG` (per-frame fog-color-change log); and the `GE_TEXRAW` dump cap raise 400→600 to reach the wall cards' upload index. No independent conclusion of its own — superseded by D271's culling fix and D272's later per-card audit, both of which reused this toolkit's skip/keep hooks.
|
||||
|
||||
**Status (from §F table):** dead-ended into D271/D272; infrastructure kept in tree
|
||||
|
||||
## D245 — IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) — "looks almost exactly like skybox" (M-116, user…
|
||||
|
||||
**IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) — "looks almost exactly like skybox" (M-116, user QA report, post-D229-fix build).** The symptom is D227's exact signature class (s16 `Vtx.tc` overflow → rasteriser interpolates between wrapped coordinates → two differently-wrong screen regions + a seam at the wrap boundary; on the sky quad the split ran on the 0–3 diagonal). Water draws through the same port path: `src/game/sky.c` water block (~905–956) — `texSelect(&skywaterimages[WaterImageId])` + `sub_GAME_7F09343C` (two-texunit cross-fade; both tiles re-declared RGBA/16b @ tmem 0, the draw D229 fixed) then a `skyRenderTri` fan over `sp274[]` under `skyPortBeginFan(sp274, s1, FALSE)` — and **the `FALSE` is a documented, deliberate tolerance of exactly this overflow**: the comment above `skyPortBeginFan` (sky.c ~1738) records that the IsWater quad is excluded from D227's tile-shift safety valve because both texunits read the same vertex tc, so a tc rescale would need matched shifts on BOTH tiles and "shifting both makes them sample identically, which flattens the cross-fade"; the 2026-09 decision was that "the water quad keeps the plain 1:1 tc and simply tolerates the overflow on its horizon vertex, exactly as it did before D227." The mechanism is real: `unk20/unk24` are S10.5 units reaching tens of thousands near the horizon; the fold rebases to a multiple of `SKY_TC_WRAP` (2048) but with k=0 an s16 tc holds only ±32767, so any wider span wraps. It was invisible before D229 because every water texel was green mottle garbage anyway; with correct blue texels the wrapped region now reads as a visibly different pattern + seam, and it tracks the view because the fan's S/T depend on camera orientation. (On N64 this draw is a verbatim RDP `G_TRI_SHADE_TXTR` coefficient block with no s16 tc — the overflow constraint is a port artifact of re-emitting as `gSPVertex` batches, same class as D227.)
|
||||
|
||||
**Status (from §F table):** **OPEN — REOPENED (M-116 live playtest, same session): the `allowShift=TRUE` mechanistic fix below did NOT resolve the symptom.** User report on Frigate post-fix: water still "glitches, kinda goes back and forth between two graphics too quickly," and still "changes depending on your view unlike the sky" (i.e. the sky, using the identical `skyPortBeginFan`/`skyPortEmitTileShift` mechanism post-D227, no longer has this problem, but water still does after getting the same treatment). This means the `allowShift=FALSE→TRUE` change either isn't the actual fix, or there's a second mechanism specific to water's two-texunit cross-fade that the tc-shift alone doesn't address — most likely candidate: the shift only rescales the *shared* vertex tc uniformly, but the tile-0-vs-tile-1 sample-point difference is set by each tile's own `uls`/`ult` (RDP tile-space, not vertex-tc-space) — if per-frame `k` changes granularity of the *quantized* shared coordinate before each tile's independent `uls`/`ult` subtraction, the two tiles' effective sampled texel could drift relative to each other frame-to-frame in a way the static `GE_D227V` trace (single frame, single camera angle) never exercised. Needs a fresh live investigation, not a repeat of the same static-trace approach — a scripted or live multi-frame capture while rotating view, watching both tiles' *decoded* texel positions (not just vertex tc) frame over frame. Do not re-apply the same `allowShift=TRUE` reasoning without new evidence; the superseded mechanism write-up below is unconfirmed-by-playtest and may be wrong. The code change (`allowShift=TRUE` on the water quad) is left in place — it's not shown to be actively harmful, and reverting it isn't obviously better than leaving it, pending the fresh investigation. **Superseded analysis (unconfirmed by playtest, kept for reference):** ~~FIXED (port-layer, `src/game/sky.c`, `#ifdef PORT`) — confirmed the `allowShift=FALSE` tolerance was the cause, and the "flattens the cross-fade" objection was wrong.~~ Changed the water quad's call from `skyPortBeginFan(sp274, s1, FALSE)` to `..., TRUE)`, so it now gets D227's tc-shift safety valve like every other sky fan. Why the old objection doesn't hold: `skyPortEmitTileShift` replays **both** tiles' captured `SETTILE` commands with the identical `k`, so both texunits' decoded S/T are rescaled by the same `2^k` *before* each tile's own (unshifted, tile-native) `uls`/`ult` offset is applied by the RDP — the offset between tile 0 and tile 1's sample points (what drives the cross-fade) is preserved, not flattened, because the shift and the offset act at different stages. **Verified mechanistically:** `GE_D227V=1` on Frigate (`-level_26`) shows a water fan vertex spanning S=-166..299317, T=-6451..288410 in S10.5 units — tens of thousands of texels, the same order of magnitude as D227's pre-fix sky overflow — now getting `kS=4 kT=4` (previously forced to 0), decoding to small in-range tc values (100s, not wrapped). **Verified no regression:** `verify.sh frigate` and `verify.sh "surface 2"` (the two `IsWater` levels) both PASS crash-free post-fix; non-water sky levels (`statue`/`-level_22`) unaffected since only the water call site changed. Screenshot comparison at a fixed scripted-intro camera angle (frame 500, both builds) showed no regression, but that specific angle didn't happen to frame the overflowing part of the fan either before or after — the sinf()-driven cross-fade also makes frame-for-frame screenshot diffs across separate runs noisy (each run's shimmer phase differs), so a same-frame pixel diff wasn't a clean confirmation tool here; the `GE_D227V` trace is the real evidence — **but it turned out not to be evidence the fix actually worked in live play.** Cross-ref D227 (mechanism), D229 (made this visible).
|
||||
|
||||
## D246 — Tiny pixel strips at the left and right edges of the screen, "like overscan" — look like raw pixels, not 3D graphics (M-116, user QA report).
|
||||
|
||||
**Tiny pixel strips at the left and right edges of the screen, "like overscan" — look like raw pixels, not 3D graphics (M-116, user QA report).** Uninvestigated. The port renders at native 640×480 (`GE_NATIVE_W/H`; `gfx_current_native_aspect` set in `port/src/video.c:253,570`) and scales to the window on present, so the leading candidate is a scaling/blit rounding artifact: non-integer scale factors leaving 1–2 columns of uncleared backbuffer content at the edges (or an off-by-one in the blit rect). Secondary candidates: an RDP fill-rect/scissor off-by-one in a game DL or the F10 overlay hook (`gfx_run()` seam, D184), or GL edge filtering on the scaled texture.
|
||||
|
||||
**Status (from §F table):** **OPEN — one candidate ruled out (M-116 follow-up), root cause not found.** Step (1) from the original plan is done: a `GE_PCDUMP` capture (Frigate, native 640×483 buffer, both against the existing save and a fresh EEPROM) shows clean, continuous scene content in the leftmost/rightmost columns — no dead/black/garbage strip at either edge. That means the defect is **not** baked into the native render itself (rules out an RDP fill-rect/scissor off-by-one in a game DL, and rules out D227/D245-style tc issues) — it must be introduced later, specifically in the SDL-window presentation path that `GE_PCDUMP` bypasses entirely (`gfx_run()` reads back the game's own render target directly, never the actual window backbuffer the user sees). Re-examined `gfx_adjust_viewport_or_scissor` (`gfx_pc.cpp:2174`): its floor/ceil rounding is already deliberately anti-gap (comment: "prevents sub-pixel gaps at viewport edges"), so a plain rounding bug there is a weaker lead than it looked; more likely candidates now are the `game_renders_to_framebuffer`-path MSAA resolve `glBlitFramebuffer` (`gfx_opengl.cpp:1252`, only exercised when `Video.MSAA>1` or the window size actually differs from the game's target size mid-frame — a path `GE_PCDUMP`'s headless capture doesn't exercise the same way a live resizable window does) or a genuine sub-pixel/DPI scaling remainder specific to the user's actual window dimensions. **Next, still needs the user (no live window access in this environment):** (1) confirm current `Video.MSAA` setting and window size/aspect at the time the strips were seen; (2) if `MSAA>1`, retest with `Video.MSAA=1` to see if the strips disappear (isolates the resolve-blit path); (3) report whether the strip width changes with window size (fixed 1px = blit-rect off-by-one; scales with window = scale-factor remainder). Cross-ref D247 (separate, now closed — don't conflate the two; D247's letterbox is confirmed-intended and top/bottom only, D246 is left/right and still unexplained). **M-141: Steam Deck playtest confirmation, new platform data point** — user reports "some artifacts left and right of screen, only a sliver... like the fullscreen isn't quite going to full screen" specifically in **Full Screen mode on Steam Deck**. Same symptom description as the original M-116 report (Windows). Checked: the Deck's own first-run preset (`videoApplySteamOSDefaults()`, `port/src/video.c:255`) sets `cfgMSAA = 4` by default — exactly the condition this row already flagged as the leading remaining suspect (the MSAA-resolve `glBlitFramebuffer` path, step 2 of the still-open bisection ask). Next: have the user set `Video.MSAA=1` via F10 and retest on the Deck specifically — if the strips vanish, this row's root cause is confirmed without needing any more code tracing.
|
||||
|
||||
## D247 — Black bars top and bottom of screen — possible aspect-ratio defect (M-116, user QA report).
|
||||
|
||||
**Black bars top and bottom of screen — possible aspect-ratio defect (M-116, user QA report).** Uninvestigated. Context: the port currently renders native 4:3 (640×480); widescreen is a *parked proposal* (`docs/dev/WIDESCREEN-FOV-PLAN.md` — PD-parity native 16:9 render, needs rule-#2 sign-off for its game-code HUD wave), so if the user's window/display is wider than 4:3 and the present path letterboxes to preserve aspect, the bars are current intended behaviour rather than a bug. If they appear in a 4:3 window too, it is a real defect (framebuffer sized wrong vs window, `videoCreateFramebuffer(w,h,upscale,autoresize)` → `gfx_create_framebuffer`).
|
||||
|
||||
**Status (from §F table):** **NOT A BUG — confirmed faithful, unmodified N64 decomp behaviour, no code change made.** `GE_PCDUMP` captures (both against the pre-existing save and a fresh-EEPROM run, to rule out a stale `Screen` option from earlier test sessions) show the exact same inset on Frigate: real scene content occupies rows ~20–459 of a 640×483 capture (the 483 vs 480 is a separate, harmless 3-row capture-tool rounding quirk, not investigated further), i.e. roughly 220/240 of the vertical field — and `src/game/fr.h:34` defines `VIEWPORT_HEIGHT_DEFAULT_NTSC 220` (240 for the full field), consumed unmodified by `bondviewGetCurrentPlayerViewportHeight()` (`bondview2.c:7858`) as the **default in-game viewport height whenever `PLAYER_OPTION_SCREEN` is `SCREEN_SIZE_FULLSCREEN`** (the decomp's own default value, `options.c:128`) — `VIEWPORT_HEIGHT_FULLSCREEN` (304, no inset) is a red herring name; it's only reachable via the `cameraBufferToggle` branch, not normal solo play. This is GoldenEye 007's well-documented original TV-safe-area letterboxing, baked into ground-truth decomp constants with no `#ifdef PORT` anywhere near it — the N64 game genuinely doesn't fill the full field during normal gameplay. Ruled out a stale/leftover EEPROM `Screen=Wide/Cinema` setting from earlier QoL-testing sessions as an alternative explanation: a fresh EEPROM (default options) shows the identical inset. **No fix needed; this row closes.** If the user's window shows noticeably *thicker* bars than this ~8–9%-per-edge margin, or bars appear on the *sides* instead of top/bottom, that would be a genuinely different (and real) symptom worth reopening under a new label — but "thin bars top and bottom, present at any window size" is correct, original behaviour. Cross-ref D226 (separate, already-tracked HUD-scale QoL ask — unrelated mechanism, don't conflate).
|
||||
|
||||
## D248 — Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes).
|
||||
|
||||
**Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes).** The port's pacemaker (`port/src/libultra.c` `portTickThread`) posts a VI-retrace event at a true 60Hz (NTSC) cadence, so the halving had to be somewhere downstream of that.
|
||||
|
||||
**Status (from §F table):** **FIXED, root-caused to a genuine pointer-width ABI bug in `src/sched.c` (compiled for the PC build, not excluded).** `osScAddClient()` (`src/sched.c:187-199`) stashes a per-client "present every retrace vs. every other retrace" flag by writing into the *next array slot's* `.next` field (`c[1].next = next`) rather than adding a real struct member — `gfxClient` (`src/init.c:246`) is registered with `next=NULL` (send every retrace → 60Hz) and the audio client `g_AudioClient[0]` (`src/audi.c:441`) with a nonzero sentinel `1` (send every other retrace → 30Hz, its intended cadence). `__scHandleRetrace` (`src/sched.c:334`) reads that flag back with a hardcoded byte offset, `*((s32*)client + 2)` (2×`sizeof(s32)` = 8 bytes). On the N64 (32-bit pointers) `sizeof(OSScClient)` is exactly 8 bytes, so offset 8 correctly lands on `client[1].next` — matching the write. On this 64-bit port `sizeof(OSScClient)` is 16 bytes, so the same hardcoded 8-byte offset instead reads the low 32 bits of `client->msgQ` itself (always a real, non-NULL pointer) — the gfx client's flag reads as permanently "nonzero" and silently falls onto the audio client's every-other-frame path, halving the game's real render/tick cadence to 30Hz although the retrace pacemaker itself was always ticking at the correct 60Hz. Same D122/D126/D132/D209 bug class (32→64-bit pointer-width struct-size growth desyncing a hand-rolled offset), just in the scheduler instead of a ROM-serialized struct. **Fix (`#ifdef PORT`, N64 line kept verbatim under `#else`):** read the real field by indexing the struct (`client[1].next == 0`) instead of hardcoding a byte offset — this naturally scales with pointer width on either platform and is the direct decompilation of what `osScAddClient`'s write side already does. **Verified:** `Video.DisplayFPS=1` overlay (`port/src/optionsoverlay.c`, D213) read a rock-stable "30 FPS" on `-level_09` (BUNKER1) pre-fix and "60 FPS" post-fix, same build/scene, screenshots captured via `GE_PCDUMP`. Rebuilt clean (`ninja -j1`, pre-existing unrelated warnings only); `-level_09` and `-level_20` both ran 12-30s post-fix with no crash (`ge007.crash.log` absent). No game-code files touched beyond this one mechanical read (`src/sched.c` is compiled for the PC target per `CMakeLists.txt`'s `SRC_ENGINE` list, not one of the excluded os/io files) and the audio client's own flag/path is completely untouched, so `#ifdef PORT`-gated audio pacing (D204 tempo work) is unaffected. **Note for `docs/dev/UNLOCKED-FPS-PLAN.md`:** this settles that plan's Phase 0 open question — the ~30fps default was a real port bug, not original N64 behavior or an artifact of retrace-rate tracking sim speed; the plan's default-preset baseline should be re-measured at the corrected 60fps before Phase 1 pacing work resumes. Not yet done: a full `verify.sh sweep` across all 21 levels (only `-level_09`/`-level_20` spot-checked this session) and a live human playtest for feel (input latency, animation smoothness, any level-specific perf regression now that render work happens twice as often per second).
|
||||
|
||||
## D249 — LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV — far-away geometry has visible culling issues (M-119…
|
||||
|
||||
**LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV — far-away geometry has visible culling issues (M-119, user QA report).** Distinct from **D222** (which is specifically high `Video.FovScale` breaking *near-screen-edge* culling on NPCs/objects — not implicated here, default FOV) and from **D218** (the fog/far-clip *tint* boundary at wide FOV — also not the same symptom). This report is at the stock FOV, on the game's two biggest/most-open outdoor-scale levels, affecting *far* geometry specifically — points at either (a) a portal/room-visibility BFS cutting off too aggressively at range on large multi-room layouts (same neighbourhood as D233/D234's near-camera portal-cull bugs — worth checking whether D233's `gfx_sp_tri1` w<0 trivial-reject fix, which only guarded *behind-camera* vertices, has a sibling far-plane/depth-range case), or (b) GE's own N64 LOD/detail-swap system (mip-tile / simple-model substitution at distance — see D218's still-open question of "does GE have a separate geometry/object LOD-swap distinct from `Visibility.FarFog`") not being ported faithfully, popping or dropping geometry instead of swapping detail level. **Second, separate ask bundled in the same report:** the user would like LOD **improved** in general (presumably distance-based detail transitions look rougher/more abrupt on PC than remembered) but explicitly wants this **gated behind a toggle** so the original N64-accurate LOD behaviour stays selectable ("in case you want old school graphics") — i.e. this is a QoL feature request layered on top of a correctness bug, not one fix.
|
||||
|
||||
**Status (from §F table):** OPEN — not investigated. Next: reproduce on Streets (`-level_20`) and Egyptian (`-level_08`, confirm level id) at default `Video.FovScale`; `GE_PCDUMP` a long sightline where geometry visibly drops/pops, cross-referencing whatever governs GE's LOD-tile selection (`gfx_lod_tile_offset` family, already touched by D172/D219) and the portal/room-visibility walk (D233/D234 neighbourhood) to determine which of (a)/(b) is actually firing before designing a fix or the requested toggle.
|
||||
|
||||
## D251 — **F10 options overlay QoL pass:
|
||||
|
||||
**F10 options overlay QoL pass: (a) bottom rows drawn off-screen in the in-game 2D space — the layout assumed a ~320×240 field and hard-fit all rows, but `viSetXY` gives 440×330 on front-end screens (`src/game/front.c:8618`) vs 320×240 in-game (`front.c:8628`), so with 17 rows × 15u pitch + 2 header rows the last rows land at y≈282, off-screen whenever the panel is opened in-level; (b) MSAA row only cycled forward — `rowAdjust()`'s `ROW_MSAA` case clamped its index to [0,3] instead of wrapping, so left-click stuck it at 8x with no way down via clicks; (c) manual % rows (`Video.DrawDistance`/`Video.LodDistance`) showed unconditionally even while their `*AutoFov` toggles were on, where the percent is inert/misleading.
|
||||
|
||||
**Status (from §F table):** **FIXED (port-only, `port/src/optionsoverlay.c`).** (a) The panel now computes how many rows fit the *actual* current 2D height (`maxVisibleRows()` from `viGetY()`) and scrolls: a visible-row window (`s_scroll`) follows the selection, driven by wheel (`optionsOverlayScroll`) and arrow/D-pad at the list edges; layout, mouse hit-testing (`overlayRowAtY` now returns a visible position) and keyboard nav all index the visible list (`s_visIdx`/`s_visN`, `s_sel` is now a visible-list index), so it fits both 240- and 330-tall spaces with no per-space tuning. (b) `ROW_MSAA` wraps `(idx + dir + 4) % 4` — OFF→2x→4x→8x→OFF in both directions, matching the already-wrapping `ROW_ENUM`/`ROW_RES`; sliders keep clamping (standard for drag-able values). (c) New `Row.hiddenIfOn`/`hidePtr` fields: a row is hidden while the named option is nonzero — `Draw distance %` hides under `Video.DrawDistanceAutoFov`, `LOD distance %` under `Video.LodDistanceAutoFov`; rebuilt every frame in `overlayUpdateVisible()` (selection clamped into range when its row vanishes). Hint line updated to "click / drag / arrows / wheel". **Verified:** compiles clean in the full 241-target build; final link of this session was blocked only by a running game instance holding the exe (re-link pending). Not yet eyeballed live: scroll feel at 320×240, hover/scroll interaction, and the auto/% row appear-vanish transitions. Cross-ref D213 (FPS readout in the same file), D218/D249 (the auto-coupling these rows expose). **Follow-up after live test:** (d) list navigation (wheel + arrows) wrapped at both ends, so scrolling past the bottom made the highlight "repeat" from the top — now clamped at both edges like a normal PC settings list; (e) new `Input.SensLink` toggle (default ON, registered in `port/src/input.c`) links the aim/turn sensitivity knobs: moving either rescales the other to hold the stock 38 (`Input.AimModeSens`) : 50 (`Input.MouseTurnSpeed`) ratio, via a re-entrancy-guarded pair-write in `rowSet()`; (f) the "Mouse aim speed" row now edits `Input.AimModeSens` (the knob the default GEPD aim path actually uses — it previously edited `Input.MouseAimSpeed`, which only feeds the legacy velocity-stick fallback and was inert under default settings). **Second follow-up:** hover-to-highlight removed entirely -- moving the mouse over the panel no longer moves the selection or scrolls; near a list edge the hover->select->scroll-shift->new-row-under-cursor feedback loop made the bottom of the list twitch/echo (the persistent "duplicate row" report). Selection now moves only by explicit click (which also selects the clicked row), wheel, or arrows. **Reassessment of the 4K-fullscreen ghost-row bug:** the presentation-UV-wrap theory is DEAD — `gfxFramebuffer` has zero readers (the textured-quad present never happens); presentation is a full-frame `glBlitFramebuffer` from the same-size MSAA FBO to the default framebuffer (fb index 0 is reserved for the window, `gfx_opengl_init`), which has no UVs and cannot wrap. The overlay DL draws each visible row exactly once, in-bounds, with its own scissor. Local repro (maximized 16:9 window + MSAA=4 + `GE_OPTIONSOVERLAY=1` + `GE_PCDUMP`) shows NO ghost — the FBO/MSAA path alone is not the trigger; the remaining variable is true fullscreen on the user's 3072×1728 display (driver/display-level suspect). Next: user bisection — Fullscreen ON + MSAA OFF (restart) → does the ghost persist? **User follow-up (logged, not fixed):** the 4K-fullscreen ghost is *intermittent* across launches, and the user suspects the `hiddenIfOn` appear/vanish transitions of the Draw/LOD distance % rows (visible-list resize while the panel is open) — unverified; note `overlayUpdateVisible()`+`overlayUpdateScroll()` are re-run together in both emit and input each frame, so a stale-state window would have to come from elsewhere (e.g. the one frame a row count changes). MSAA-off bisection still pending. User: in the sessions seen so far the ghost is present on *first open* of F10 (not only after an auto-toggle flip) — weakens but does not kill the appear/vanish theory (toggles had flipped in earlier sessions before a restart). **v0.2.0-pre playtest (user, latest build):** the symptom has changed — the panel's *bottom row now deterministically duplicates whatever item is currently selected* (e.g. MSAA appears both on its own row and again at the bottom), not just an intermittent 4K/MSAA ghost of the top row; suggests the visible-row window can contain the selected row twice (off-by-one in `overlayUpdateScroll`/`maxVisibleRows` bookkeeping) — investigate post-release. **QoL follow-up:** make the auto-linked % rows visually subordinate to their toggle (indent and/or dimmer value colour) so the pairing is obvious in the panel; hint line also reworded to "select: scroll/click, change: mouse/arrows".
|
||||
|
||||
## D253 — Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck — `stanCheckLinkedSpecialTile` stores to a garbage…
|
||||
|
||||
**Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck — `stanCheckLinkedSpecialTile` stores to a garbage `outFlags` pointer (D203 root cause).** The B-callback typedef `standTileLocusCallback_B_t` (`bondtypes.h:573`) declares the middle three params as `f32 f32 f32`, so on x86-64 SysV the caller (`sub_GAME_7F0B1DDC`, `stan.c:2228`) passes the 6th arg (`record`) in **`%rdx`** (3rd integer slot; the floats take xmm0–2). But the actual implementation `stanCheckLinkedSpecialTile` was decompiled with an all-`s32` prototype, so it reads `outFlags` from **`%r9`** (6th integer slot) — a stale scratch value left by earlier callees. On the N64 (MIPS o32) both prototypes agree: everything flows through GPRs a0–a5 in order, 6th arg = a5 either way, so the original is fine and the decompilation looked consistent. The mismatch stays dormant until a tile actually hits the FORCECROUCH or LADDER branch and *stores* through `outFlags` → SEGV; that's why it was level-specific (Facility's opening crouch tile) and input-dependent, and why idle repros were negative. The sibling B-callback `stanLocusEdgeIsAboveY` already had the correct float prototype, which is why only this path was booby-trapped. **Fix (ABI-class, semantics-preserving):** declare the three middle params of `stanCheckLinkedSpecialTile` as `f32` in both the forward decl and definition (`stan.c:152`, `stan.c:2407`) to match the typedef; the body never uses them, and on N64 o32 f32 args occupy the same GPRs, so N64 codegen is unaffected. **Verified:** gdb run on WSL (gcc 15) — pre-fix, SIGSEGV at `stan.c:2441` with `r9=0x3` and valid `record=0x409fea00` in the caller frame; post-fix, the same walk route completes with no crash. Side benefit: the FORCECROUCH/LADDER branches were effectively dead-on-arrival on PC (any hit = crash); they now function. **Also observed post-fix (separate, WSLg-only):** a segfault *inside libgallium* during `gfx_opengl_upload_texture` (32×48 RGBA + mipmaps) while walking further — GL-driver-side under WSLg's Mesa; not reproducible on Windows; park as a new finding if it recurs on real hardware.
|
||||
|
||||
**Status (from §F table):** FIXED — Deck-verified: user got past the crouch tile with the re-cut bundle (a *new*, later crash is D255).
|
||||
|
||||
## D254 — Linux crash handler destroyed its own logs: `ge007.crash.log` only ever contained the SIGABRT block, never the original SEGV.
|
||||
|
||||
**Linux crash handler destroyed its own logs: `ge007.crash.log` only ever contained the SIGABRT block, never the original SEGV.** The POSIX `crashHandler` (`port/src/crash.c`) finished by calling `sysFatalError()` → `abort()` → SIGABRT, which re-entered the *same* handler (SIGABRT was one of the installed signals); the second pass reopened the log with `"wb"` and overwrote the first pass's SEGV block — faulting PC, signal, everything. The Deck firefight crash (D255) came back as a bare `SIGNAL: 6` abort record for exactly this reason, forcing offline disassembly of the CI binary to recover what the log should have said. **Fix (port-only):** re-entrancy guard (`inCrashHandler`, second pass `_exit(128+sig)` immediately) + terminate directly with `_exit()` instead of `sysFatalError()` (the Windows path already terminated directly for the same reason) + the Linux block now dumps the faulting context: `FAULT ADDR` (`si_addr`) and the full general register set (Rax/Rcx/Rdx/Rsi/Rdi/R8–R11, Rsp/Rbp) from the `ucontext`, so a field crash now names the bad pointer on its face. **Field-verified 2026-09-13:** the D255 Deck crash came back as `SIGNAL: 11` + `FAULT ADDR: 0x10` + full register set — exactly the format designed here; it directly identified the NULL-model case (see D255).
|
||||
|
||||
**Status (from §F table):** FIXED — field-verified.
|
||||
|
||||
## D256 — Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing `data/` — reported as "saves do not…
|
||||
|
||||
**Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing `data/` — reported as "saves do not work at all" on Steam Deck.** `romdataInit` finds the ROM in `$S/` (CWD `data/`) *or* `$E/` (exe dir) *or* `./`, but `sysResolvePath`'s POSIX `$S` branch (used by `ge007.eep` and `ge007.ini`) only ever produced a CWD-relative path — its "fallback" was `./data/`, identical to the primary. Launch from any other CWD (Steam non-steam-game shortcut, terminal in `~`): ROM loads via the exe-dir candidate, game runs, but every EEPROM/config write hits `fopen(..., "wb")` on a nonexistent `./data/` and fails with a stderr-only "cannot write" — saves never persist, and **every launch also starts with default settings** (F10 changes don't stick), which presents as "draw distance/LOD is low" on levels like Dam where the user had raised `Video.DrawDistance` on another machine. The Windows branch already did CWD-else-exedir; the POSIX branch now mirrors it (`readlink("/proc/self/exe")`) and creates the directory best-effort so first-run writes don't fail. **Verified on WSL:** with exe+`data/` in `/tmp/fb2` and CWD=`/`, config/eeprom/ROM all resolve into `/tmp/fb2/data/` and a 25 s run completes clean; the pre-fix binary in the same layout fails to find the ROM at all. **Caveat:** if the user's Deck launch *was* from the bundle dir (CWD had `data/`), this fix does not explain their save failure — need the save-flow detail (does the memory-card screen appear? slot confirm? "saved" message?) before closing the report. Side note found while testing: a `data/` without the generated sidecars (`pccg-*/`, `pcmodels-*/` from `tools_pc/d69_emit.py`/`d43_emit.py`) starts fine but faults in `load_bg_file` (`bg.c:865`) on level load — the reserve functions warn "stage loads will fault" and return 0; expected degraded mode, not a new bug.
|
||||
|
||||
**Status (from §F table):** FIXED (path half) — save-flow detail still owed from the user.
|
||||
|
||||
## D257 — New `Game.AllUnlocked` option (F10; default flipped OFF for v0.2.0 — see D259): skip N64 progression entirely — all 21 solo levels selectable at every…
|
||||
|
||||
**New `Game.AllUnlocked` option (F10; default flipped OFF for v0.2.0 — see D259): skip N64 progression entirely — all 21 solo levels selectable at every difficulty, 007 mode available, and the cheat menu fully populated. Port-layer only; no game-code edits.** Three mechanisms, all in `port/`: (1) `main.c` seeds the two live `LEFTOVERDEBUG` RAM flags after `configLoad()` — `debug_enable_all_levels_flag` (all levels) and `debug_007_unlock_flag` (007 mode; both in `src/game/debugmenu_handler.c`, same class as the existing `GE_UNLOCK_ALL` getenv hook in the getter); (2) the eep shim (`libultra.c geEepromPatchAllCheats`) patches the save block at read time — special-casing `fileValidateSaves`'s exact pattern (block 4, 5×96 bytes) — setting all 20 progression-gated cheat-unlock bits per slot (`unlocked_cheats_1/2/3`; the cheat menu row on mode-select only renders when ≥1 is set, `front.c interface_menu06_modesel`) **and** filling every *empty* stage/difficulty completion-time field with the max time 0x3FF (existing player times preserved); both are then CRC'd per slot with the game's own `fileGenerateCRC` or `fileValidateSaves` would wipe the slots. (3) `optionsoverlay.c` row so users can toggle OFF for faithful progression. **Why the completion-time half is required (found by user repro):** when ANY single-player cheat is active, `g_AppendCheatSinglePlayer` makes mission-select (`get_highest_unlocked_difficulty_for_level`, front.c) accept only levels with status `STAGESTATUS_COMPLETED (3)`, not merely `UNLOCKED (1)` — AllUnlocked's debug-flag path returns 1, so enabling a cheat locked every level ("could only do Silo"). Reading the save as a fully-completed campaign satisfies both branches. Time bit-math mirrors `fileGet/SetSaveStageDifficultyTime` verbatim (10-bit fields, difficulties 0–2; 007 times are virtual). **Pitfall hit during implementation:** the first mirror of `save_data` used `times[80]` instead of `(SP_LEVEL_MAX-1)*4 = 76`, making `sizeof` 100≠96 so the block-read size check silently never matched — now enforced by a compile-time `ge_save_slot_size_check`. **Semantics:** unlocked cheats/completions persist to `ge007.eep` on any subsequent save write (self-consistent; mirrors N64 where completing a level permanently unlocks its cheat). No *active* cheats are enabled (invincibility/all-guns stay OFF); weapons remain per-mission pickups. **Verified:** fresh eep — all 5 slots validate `stored==calc` under `GE_SAVELOG=1`; user playtest: all levels selectable at each difficulty, 007 mode present, cheat menu populated, cheats toggle and work (time-modifying cheats untested by user), mission select intact with all cheats ON.
|
||||
|
||||
**Status (from §F table):** FIXED — verified by user playtest (PR #79 merged); default flipped OFF for v0.2.0 (D259).
|
||||
|
||||
## D259 — `Game.AllUnlocked` (D257) breaks audio on a fresh save: with it ON, music and SFX load at volume 0 (silence). v0.2.0 ships with `AllUnlocked` default OFF.
|
||||
|
||||
**`Game.AllUnlocked` (D257) breaks audio on a fresh save: with it ON, music and SFX load at volume 0 (silence). v0.2.0 ships with `AllUnlocked` default OFF.** User-observed during v0.2.0 artifact testing ("all unlocked seems to break sound"). **Mechanism:** a fresh `ge007.eep` is zero-filled, so every save slot is all-zero. On N64 such a slot fails `fileValidateSaves`' CRC check and is rewritten as `BLANKSAVEDATA` (`src/bondconstants.h:2148`), whose `music_vol`/`sfx_vol` are `0xFF` (max). With AllUnlocked ON, `geEepromPatchAllCheats` (`port/src/libultra.c`) patches the zeroed slots (cheat bits + completion times) and recomputes a **valid** per-slot CRC, so `fileValidateSaves` accepts them as-is and never calls `fileResetSave`. The volume bytes therefore stay 0, and `fileLoadSettingsForFolder` (`src/game/file2.c:1289-1290`) loads them verbatim: `set_mTrack2Vol((save->music_vol << 7) | (save->music_vol >> 1))` = 0 → silence. With AllUnlocked OFF the zeroed slots fail CRC and reset to `BLANKSAVEDATA`, which is why sound works in the default configuration. **Disposition for v0.2.0:** flip `portAllUnlocked` default to 0 (`port/src/video.c`; `SkipIntro` was already 0) — faithful N64 progression out of the box, F10 *All unlocked* still enables the goodie. The quirk then only affects users who deliberately toggle it ON with a fresh save (recoverable: raising the in-game volume writes a nonzero slot, after which the patch leaves volumes alone). **Deferred fix candidate (port-only, ~6 lines):** in `geEepromPatchAllCheats`, detect all-zero slots and seed `music_vol`/`sfx_vol = 0xFF` before the CRC recompute, emulating what `fileResetSave` would have done; real (nonzero) saves incl. a user-muted one stay untouched.
|
||||
|
||||
**Status (from §F table):** **FIXED in v0.2.1** — the deferred fix was applied: `geEepromPatchAllCheats` now detects all-zero slots and seeds `music_vol`/`sfx_vol = 0xFF` before the CRC recompute, emulating `fileResetSave`; real saves (incl. a deliberately muted one) stay untouched. **By-ear confirmed by the user** (v0.2.1 test pass: fresh save, AllUnlocked ON — audio plays correctly).
|
||||
|
||||
## D260 — v0.2.1 (user request): mouse-wheel weapon-select direction swapped.
|
||||
|
||||
**v0.2.1 (user request): mouse-wheel weapon-select direction swapped.** Wheel-up now selects the *previous* weapon and wheel-down the *next*, matching the in-game wheel menu's on-screen layout (the port had them inverted vs the user's expectation). Port-only: `inputPostWheel()` (`port/src/input.c`) — the notches>0 / notches<0 branches were swapped so scroll-up emits the back-notch and scroll-down the forward-notch. N64 build untouched; no game-code edits.
|
||||
|
||||
**Status (from §F table):** FIXED — build green; one-line branch swap, trivially reviewable.
|
||||
|
||||
## D261 — v0.2.1 (user request: "fix watch menu sensitivity for wasd/controller buttons"): holding W/S (or a fully-pressed stick axis) on the watch inventory page…
|
||||
|
||||
**v0.2.1 (user request: "fix watch menu sensitivity for wasd/controller buttons"): holding W/S (or a fully-pressed stick axis) on the watch inventory page crawled one item per tap — no repeat.** Root cause is the interaction of two existing behaviours: keyboard W/S map to `stickY = ±STICK_MAX (0x50)` and SDL pads peg at 0x80, both *outside* the smooth-scroll band (0x1f..0x45) that D118d intentionally left for the port — so a held press only ever hit the latched single-step path (`watch_stick_y_pressed_*`, one step per press edge, no repeat). On N64 the same stick level was the *fast-scroll* band (one step per frame), which D118d removed because digital sources peg there and it became runaway scroll. **Fix (port-only, `#ifdef PORT` in `src/game/options.c`):** a bounded auto-repeat helper `geWatchStickFastStep()` — while the stick is held past the smooth band (≥ 0x46 up / ≤ −0x45 down), step one extra item every `GE_WATCH_NAV_RATE` (6) frames after an initial `GE_WATCH_NAV_DELAY` (15)-frame hold; the first press still goes through the untouched latched path, so a tap behaves exactly as before, and the smooth band + D-pad/C-button edge-repeat are unchanged. Called from `game_options_inventory_navigation()` only. **Verified:** build green; headless smoke (`GE_INPUTSCRIPT` long-W-hold on the inventory page) runs clean. Feel-check (repeat rate) owed to the user — both constants are at the top of the helper for a one-number tune. **User feel-check (v0.2.1 test pass): repeat "feels fine for now", but W/S is still too sensitive overall — desired behaviour is tactile one-press-one-item with no risk of scrolling the whole list on an over-held key; candidate tweaks (not yet done): longer `GE_WATCH_NAV_DELAY`, slower `GE_WATCH_NAV_RATE`, or gating the auto-repeat off for keyboard taps entirely (repeat only for analogue stick). Logged, no code change.
|
||||
|
||||
**Status (from §F table):** FIXED — feel-check received; W/S sensitivity follow-up logged above.
|
||||
|
||||
## D262 — v0.2.1 (user report): "3D models missing on watch menu screen item select" — the selected-item 3D preview does not appear on the watch inventory page.
|
||||
|
||||
**v0.2.1 (user report): "3D models missing on watch menu screen item select" — the selected-item 3D preview does not appear on the watch inventory page.** User visually confirmed this session: no 3D model on the item-select page where one should be (default selection is *unarmed*, which has no model — a weapon must be scrolled to with S/DOWN). Headless repro (`GE_INPUTSCRIPT` pulse train + `GE_PCDUMP`, Dam `-level_33`): watch can't open during the intro freeze (`bondviewFrozenMoveBond` zeroes input while `g_CameraMode == CAMERAMODE_INTRO`; freeze ends ~frame 1400–1500 on Dam, and controller reads run faster than sim ticks, so scripts are timed by trial); once open + paged to INVENTORY (R_JPAD), the watch UI renders but the centre of the face is empty for every item. **Root cause found and fixed as D264** (`renderdata.flags == 0` in the watch subdraw pass — the cross-global ModelRenderData template read zeroes on PC; `subdraw()` then gated *every* geometry node, emitting exactly 1 word instead of the model's commands). Residual: with flags restored, the preview emits matrix/light-setup words (71 for item 5) but dump frames still show no visible centre geometry — a possible second layer (LOD/distance gating or fast3d rasterisation of the sub-DLs) remains unverified; user visual re-check pending.
|
||||
|
||||
**Status (from §F table):** FIXED (primary cause, D264) — pixel-level confirmation pending user run.
|
||||
|
||||
## D264 — D262's root cause (ABI/layout class, cf. D140/D52): the watch item preview's `ModelRenderData` template is read across two adjacent globals that no longer sit…
|
||||
|
||||
**D262's root cause (ABI/layout class, cf. D140/D52): the watch item preview's `ModelRenderData` template is read across two adjacent globals that no longer sit adjacent on PC.** `set_enviro_fog_for_items_in_solo_watch_menu()` (`src/game/gunfire.c`) starts with `ModelRenderData renderdata = *((ModelRenderData *) (&D_80035D00));` — a 64-byte copy that on N64 spans `D_80035D00` (a zeroed u32, .bss) *and* `D_80035D04[] = {1, 3, 0…}` (.data): the effective template is the one documented in the comment above `D_80035D00` in `gun.c` — `basemtx=NULL, zbufferenabled=TRUE, flags=3, rest zero` (`CULLMODE_BOTH` is 0). The PC link places .bss and .data in separate sections, so the read is all zeros: **`flags==0`**, and every geometry emitter in the subdraw node dispatch gates on `renderdata->flags & 1/2` (e.g. `modelRenderNodeDl`) — so `subdraw()` emitted exactly one word (`gSPSegment`) per preview, no matter the item. Probes this session: entry gates all pass (hand has item, model header non-NULL), node tree present (~400 nodes, isLoaded=1), world-path geometry nodes carry valid built DLs — but in the watch pass `flags=0` on every node. **Fix (`#ifdef PORT`, values identical to the N64 ROM bytes):** explicit template — `ModelRenderData renderdata = {0}; renderdata.zbufferenabled = TRUE; renderdata.flags = 3;`. Verified at Gfx level: preview emission went 1 → 71 words (item 5) / 81 (mid-scroll) / 9 (item 47, knife-class). Note for follow-up: the 71-word dump is a repeating {matrix/segment/light} unit with no `gSPDisplayList` in the first 24 words — if the user's re-check still shows no visible model, next suspects are LOD/distance gating of the local watch-space matrices (`modelUpdateDistanceRelations`, cf. 7c48f664) or fast3d handling of the preview sub-DLs (segment 3 / render modes).
|
||||
|
||||
**Status (from §F table):** FIXED — build green, probes removed; visual confirmation pending.
|
||||
|
||||
## D263 — v0.2.1 (user report): Facility — "Ourumov not shooting Trevelyan"; the scripted opening beat never triggers. RESOLVED: not a port bug — the beat is…
|
||||
|
||||
**v0.2.1 (user report): Facility — "Ourumov not shooting Trevelyan"; the scripted opening beat never triggers. RESOLVED: not a port bug — the beat is proximity/LOS-triggered exactly as on N64, and the full chain (incl. the execution shot) was verified firing end-to-end on PC.** The triage suspects (tick pacing, camera-mode gate, silent anim lookup) were all ruled out at runtime. **The beat, fully mapped** (static decode of `UsetuparkZ.c` — remember every DBYTE/TBYTE in the dump is bswap16/32'd relative to game interpretation): Trevelyan = guard chr 67 (body 9 = Trevelyan_006, pad 131) on ai_18: walks to pad 124, wanders, then loops a sees-Bond check; **only when Bond is in his vision** (range <10k, facing ±110°, fog, random gate ≈1/49/tick, LOS + *same stand tile* via `chrCanSeeBond`) does he jump to ai_19: runs at Bond, alarm + dialogue timers, sets an objective bit; BG list ai_41 (ID 0x1000, auto-started) polls that bit and spawns **Ourumov (body 7) at pad 88 with ai_23** plus a 6-soldier squad; ai_23 renumbers him to chrnum 78 and arms him → ai_22: runs to pad 123, waits for Trevelyan within 0x1400 of it, ~95 s dialogue cascade, then the **execution shot** (`HitChrWithItem` from 78 on 67). It is a multi-minute sequence that *starts* only when the player walks Bond west and down a floor into Trevelyan's room — Bond spawns 8900 units away on the upper floor with LOS blocked (probed directly: `stanTestLineUnobstructed` = obstructed, different stan tiles), identical geometry to N64. **Runtime verification** (`-level_34`, `#ifdef PORT` probes in the AI executor/vision path, all since removed): all 15 BG lists + guards start; Trevelyan alive on ai_18; every vision gate passes except the geometric ones at spawn distance; player movement confirmed working (note for future headless tests: under the port's default SOLITARE/natural-pitch scheme, walking is C-button/D-pad fed — W→C-up — and the analog stick carries pitch, so `GE_INPUTSCRIPT` SUP/SDOWN tokens do *not* walk Bond there; use UP/DOWN pulses). **Decisive test:** a temporary PORT-only teleport probe dropped Bond 65 units from Trevelyan at t≈12 s → seconds later: vision fires (chr 67 → ai_19), objective bit set, `SpawnChr body=7 pad=88` + squad, chrnum→78 on ai_23; ~2½ min later **`HitChrWithItem from_chr=78 target=67 part=8 item=6`** — Ourumov executes Trevelyan. Most likely explanation of the report: the tester never walked Bond into Trevelyan's sightline (or pre-D253 crash masked it). No game-code or port-code change required. **Kept from this session (test-harness only):** `port/src/input.c` — `GE_INPUTSCRIPT` analog-stick tokens were applied *after* the stick was already written out, so SUP/SDOWN/SLEFT/SRIGHT were silently discarded; the script override now runs before the clamp/write.
|
||||
|
||||
**Status (from §F table):** RESOLVED (not a bug), but refined (v0.2.1 further user testing, 2026-09-15) — beat verified firing end-to-end on PC incl. execution shot; **user playtest confirmed the sequence plays normally.** **Refinement: the exact trigger behavior is significantly sensitive to Bond's precise location and does not fire 1:1 identically to the N64 original across different approach paths/positions** — consistent with the beat's own documented sensitivity (range/facing/LOS/same-stand-tile + a ≈1/49-per-tick random gate, see the mapped mechanism above), so this is likely the same finicky-trigger behavior rather than a new bug, but not confirmed byte-identical to N64 for every approach. **User confirms this does NOT block level progression** either way — logged for awareness, not tracked as an open defect. Probes removed; input-script stick fix kept. **M-141: re-confirmed by user, one more mechanism now documented — the standoff has a hard TIMEOUT fallback, not just the shot-miss/objective-bit gates.** User reported (misread at first as a new freeze) reaching the Ourumov/Trevelyan standoff, waiting without shooting or triggering anything else, and after a couple of minutes Ourumov delivered his lines and executed Trevelyan on his own — **this is by design**: `ai_22`'s label `0x05` OR-gate (four early-exit conditions: two objective bits, a "Bond's shot missed" check, and a third objective bit set by an unrelated guard's surrender elsewhere in the level) is not the only path forward — falling through the whole dialogue-timer cascade (`label 0x42`'s chain of `if_local_timer_greater_than` checks) reaches `label 0x13`, which unconditionally fires `chr_hit_body_part_with_item_damage(0x43, 0x08, 0x06)` (the execution) once enough real time has elapsed, no player action required. Matches this row's own earlier "~2½ min later" measurement. **Do not re-flag "stuck standoff" as a bug without confirming the user waited the full timeout window first.**
|
||||
|
||||
## D265 — D236 investigation (port-layer, `port/fast3d/gfx_pc.cpp`): the tree cards are IA8/IA4, not IA16 — and `import_texture_ia4` decoded the RDP's I2:A2 texel as…
|
||||
|
||||
**D236 investigation (port-layer, `port/fast3d/gfx_pc.cpp`): the tree cards are IA8/IA4, not IA16 — and `import_texture_ia4` decoded the RDP's I2:A2 texel as I3:A1, inverting the alpha ramp. Fixed; awaiting user visual re-test.** The earlier sessions misidentified the format: a live tri-time tile census (probe in `gfx_sp_tri1`, since removed) shows level_36's tree-card classes are (a) **IA8, line=64B → 64×16** (texdump r107), drawn OPA/bilerp/1-cycle/AC_NONE — *genuinely fully opaque* (A nibble F on 1023/1024 texels; opaque on N64 too) — and (b) **IA4, line=64B → 128×16** (texdump r117), drawn ZMODE_XLU/point/2-cycle with m2a=CLR_MEM/m2b=1MA, i.e. texel-alpha blended. There are no 32-wide IA tiles at tri time; the prior "32×32 tile" reading was wrong, and the whole earlier IA16 byte-pair/I11:A5 debate (r107) was analyzing an IA8 image as byte-pairs. **The wall's root cause:** r117 decoded as I2:A2 is a strip of ~13 discrete conifer silhouettes (trunk+canopy shapes visible in the ASCII render, `scratch/r117_ia4_alpha.png`) with a 3-level alpha ramp {0: 95% of texels, 85, 170} — faint dithered distant trees on N64. But `import_texture_ia4` read each nibble as intensity=`part>>1` (3-bit, SCALE_3_8) + alpha=`part&1`: A2=1 canopy pixels became **fully opaque**, A2=2 edge dithering became **invisible** → opaque repeating tree silhouettes = the "wall". The bug is inherited verbatim from the PD port (`pd_port/port/fast3d/gfx_pc.cpp` has the identical I3:A1 read — latent there, fatal for GE's tree strip). **Fix (applied):** `intensity = part>>2`, `alpha = part&3`, both via new `SCALE_2_8`; this is the hardware-correct RDP IA4 layout. Also cleaned up: the ia16 importer comment now states plain I8:A8 (the byte-pair read *is* correct IA16; the old "r107 proof" was misidentified data), and the earlier part-1 dimension fix stays (harmless/correct for real IA16 tiles). **Caveats/owed:** (1) user visual re-test of Surface 1 trees — headless A/B was in flight when work halted; a cross-run region diff was inconclusive (camera drift between captures); (2) the decode change affects *every* IA4 tile game-wide — regression-sweep other levels for dithered/alpha IA4 art that now renders with correct 3-level alpha; (3) if a hard repeating band remains after the fix, it is the opaque IA8 strip (a), which matches hardware and D236 needs re-scoping. **OUTCOME (user re-test): NEGATIVE — trees still render as the wall with the I2:A2 decode applied; the change was fully reverted (`git checkout` of `port/fast3d/gfx_pc.cpp`, back to the original PD-derived importers) and work is parked.** The census/RM/texture data above stands as evidence for the next attempt: the decode was *necessary* (I2:A2 is the hardware layout and r117 is unambiguously an authored tree strip under it) but *not sufficient* — remaining suspects, in order: (i) fast3d's blending of these XLU/2-cycle cards (texel alpha reaching the GL blend at all — the `texture_edge`/`SHADER_OPT_TEXTURE_EDGE` path forces `a>0.19 ? 1 : discard`, which for a 3-level ramp quantizes to opaque/discard, i.e. still hard-edged), (ii) per-card UV windowing/placement (all captured cards sampled the same u:0..64/v:0..16 window — confirm against N64), (iii) fog/distance. Note for the next session: verify what the N64 actually shows in that region before more port-side changes (the opaque IA8 strip is faithful to hardware either way).
|
||||
|
||||
**Status (from §F table):** PARKED — reverted to baseline; evidence + suspect list above.
|
||||
|
||||
## D258 — PAL/JP asset conversion is broken at the source-data level — `scripts/filelist.e.csv` and `filelist.j.csv` carry naming defects, so `gen_romassets.py e/j` (and…
|
||||
|
||||
**PAL/JP asset conversion is broken at the source-data level — `scripts/filelist.e.csv` and `filelist.j.csv` carry naming defects, so `gen_romassets.py e/j` (and the d43/d69/d88 emitters' `find_row`) cannot resolve PAL/JP builds. v0.2.0's drop-in ROM feature therefore ships NTSC-U only.** Discovered while scoping issue #6 (drop-in ROM). **PAL:** 31 of the 512 model rows in `filelist.e.csv` have truncated/renamed basenames (an extraction artifact — e.g. `Pbridge_console` ×6 where the resource table says `Pbridge_console1aZ…3bZ`, `Pchrgrenadelauncher` for `PchrgrenadelaunchZ`). **JP:** one font row is mislabeled — `fontBankGothic_fontchartable.bin` @3044276 is named `fontZurichBold_fontchartable.bin` (a duplicate label). Both were proven without a PAL/JP ROM: (a) greedy two-pointer sequence alignment of the 512-row `file_resource_table.inc.c` model order against each CSV's model rows gives an exact 1:1 match (481 by-name + 31 positional for PAL), offsets strictly non-decreasing, and zero size anomalies vs US (all within [0.5×, 2×]); (b) JP font row proven by the contiguity invariant — Gothic kerning @3043600+676 = 3044276 exactly hits the mislabeled row, whose size (8716) equals the US Gothic chartable, while Zurich's own chain is contiguous one row later. **Consequence:** no PAL or JP PC exe has ever been buildable from this tree (latent — only NTSC was ever exercised); the CSV `file` column is a label only (emitters look up by basename, never read the path), so the repair is a pure data fix: rename the 31 PAL model rows + 1 JP font row to the exact table symbols. **Scope decision (v0.2.0):** ship drop-in ROM support for NTSC-U only — `prepare-assets.py`/the frozen `ge007-convert` gate non-US ROMs with a clear message after SHA-1 region detection; the CSV repair + multi-region matrix + per-region exes are deferred to the Part B follow-up.
|
||||
|
||||
**Status (from §F table):** OPEN — repair path verified, not applied (out of v0.2.0 scope).
|
||||
|
||||
Reference in new issue
Block a user