diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index a0a852bc..859085ca 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -68,6 +68,8 @@ Two classes: | `GE_D245_FIXEDSHIFT` | `src/game/sky.c:946` (`#ifdef PORT`) | D245 test override: pins the water tc shift `k` to the given integer (0..SKY_TC_MAX_SHIFT) instead of the computed per-vertex value, to A/B the allowShift=TRUE behavior. Off by default (unset = current behavior). | **live, test-only** (D245 OPEN) | | `GE_D252` | `port/fast3d/gfx_pc.cpp:733` (`import_texture`, cached `s_d252`) | D252 rainbow-particle diag: one-shot per (address,size) import log of the tile state (tmem/fmt/siz/addr/size/line) for small textures so a suspect particle tile can be checked against its source bytes by hand. Remove once D252 is resolved. | **live** (D252 OPEN — non-reproducible) | | `GE_D235` | `src/boss.c` (per-stage `while (!done)` block, after `memaReset`; `#ifdef PORT`) | D235 evidence probe: logs how many GL-texture-cache entries the per-stage `videoResetTextureCache()` call purges at each level load. A cold boot into a level should log 0; a death/restart (or any transition from a level that uploaded textures) logs >0 — direct evidence of the stale-entry mechanism. The clear itself is **not** gated on this var (it's the fix, see below); the var only controls the log line. | **live** (D235) | +| `GE_D235_OFF` | `src/boss.c` (same block) | **TEMP** A/B switch: skips the per-stage `videoResetTextureCache()`, i.e. reverts the D235 fix for one run. For comparing a restart with and without the fix. | **TEMP** (D235; remove when D235 closes) | +| `GE_D235_RELOAD=` | `src/boss.c` (same block + `bossMainloop` retrace branch) | **TEMP** reload harness: once the title stage loads after the first level (e.g. after a scripted watch Abort), wait `` retraces, then re-enter that level via `bossSetLoadedStage()`, the front end's own mission-start call. A headless death/restart without the pointer-menu walk. Pair with `GE_INPUTSCRIPT="800:START;1200:A;1300:RIGHT;1400:A"` (abort) on `-level_XX`. | **TEMP** (D235; remove when D235 closes) | | `GE_D75V` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex`, end of the copy/transform loop; cached env check) | D75 (Nintendo logo) per-batch vertex probe: for each `G_VTX` batch in frames `lo..hi`, logs frame#, source pointer, n_vertices, original dest_index, raw ob[0..2] of the first vertex, min/max of the transformed x/y/z/w across the batch, and the MP translation row. Used to prove the transform is correct (M-195) and that every batch encodes v0=0 (slot-overwrite hypothesis). Value form `"lo-hi"` (e.g. `300-900` = the logo screen); unset = off. | **live** (D75) | | `GE_D75D` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` G_VTX case + `gfx_sp_tri1` rejection/emit gates; cached env check) | D75 (Nintendo logo) per-frame draw-path probe (M-196): for frames `lo..hi`, logs one line/frame with the V/T token stream (`seq`), max triangle vertex index, cumulative vertices loaded, and per-triangle accounting — trivial-reject / backface-cull counts, how many tris reach GL, how many of those are on-screen (NDC bbox ⊂ [−1,1]²), invisible-flag count, vertex-color min/max, first oml word, depth z/w range + NaN count, texture-sampler presence (`utex` = GL shader, `cutex` = combiner), combine mode, prim/env color. Used to REFUTE the slot-overwrite hypothesis (DL is streamed, max index 15) and narrow the bug to a texture-modulate material rendering black. M-197 added logo-mode `lg_*` fields (sampled only on tris with the logo combine word — the frame-level `utex`/`cutex` are first-tri-wins and were the source of the "combiner claims 0" false anomaly): `lg_utex`/`lg_tex` + tile-0 descriptor + texture buffer address/id/size for the logo material. Value form `"lo-hi"` (logo window ≈ `244-320` on a bare front-end boot); unset = off. Companion one-shot `D75DCC:` log in `gfx_generate_cc` fires when `combine_mode==0x009ffe4f19ffe4f1` to decode the logo's combine slots. | **live** (D75) | diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 00c1fe6e..6c3cfbd9 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -131,7 +131,7 @@ D130,RESOLVED,"crash class C2 (Facility -level_34, Runway -level_35):",resolved D164,FIXED,"Disclaimer / legal screen only draws the first line (D76 root cause, M-31).",FIXED — corrected 2026-09-18: D165,FIXED,"Front-end menu cursor doesn't feel like a mouse pointer (M-31, user bug B5 follow-up).","FIXED (port/src/input.c; Input.MenuPointerMode, Input.HipfirePitchSpeed added)." D166,FIXED,Hipfire mouse pitch is digital while yaw is analog —,FIXED (port/src/input.c).… -D235,OPEN,Wrong textures load after dying/restarting a level —,OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.… +D235,OPEN,Wrong textures load after dying/restarting a level —,"OPEN — fix wired, headless reload clean (2026-09-23); bug NOT reproduced even with the fix off (GE_D235_OFF)." D236,CLOSED,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).","CLOSED (M-198, pass 26, 425237f9; probes stripped 0503d335):" D237,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only D238,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only (port/src/input.c + config.c) diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 8206502c..0c04a178 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -538,7 +538,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D166 | **Hipfire mouse pitch is digital while yaw is analog — inconsistent feel (user bug B7 / D118a, "cheaper alternative").** — full `## D166` entry at file tail | FIXED (`port/src/input.c`).… | | 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 + VISUALLY VERIFIED (M-107 fix, M-108 verify).** See the other D233 row for full detail. | | 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), mechanism unexplained.** See the other D234 row for detail. | -| D235 | **Wrong textures load after dying/restarting a level — not Facility-specific, seen on multiple levels (M-106, user QA report; clarified same session).** — full `## D235` entry at file tail | OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.… | +| D235 | **Wrong textures load after dying/restarting a level — not Facility-specific, seen on multiple levels (M-106, user QA report; clarified same session).** — full `## D235` entry at file tail | OPEN — fix wired, headless reload clean (2026-09-23); bug NOT reproduced even with the fix off (GE_D235_OFF). Needs the original die → mission select → restart trigger in a playtest to close. | | D236 | **Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).** — full `## D236` entry at file tail | **CLOSED (M-198, pass 26, `425237f9`; probes stripped `0503d335`): root cause = `TEXTURETYPE_DETAIL` tile binding — fast3d drew the detail tile (opaque IA8 noise) instead of the base image; `gfx_lod_tile_offset` now samples tile 1 when the pair differs (`Video.DetailBaseTile`, default on). Live-verified Surface 1/2 + Depot; awaiting user play-test sign-off.** Historical trail — pass 24 (2026-09-21, static) DECODED THE TREE TEXTURE** (new `tools_pc/texdecode.py` ports image.c's LOOKUP/HUFFLOOKUP/RLELOOKUP codecs). **`tex1198` LOD 0 = 64×17 RGBA5551, 44.2% opaque, and its 1-bit alpha renders as an unmistakable treeline silhouette** — pass 23's identification confirmed by decode, not inference. **Rare authored a six-level LOD pyramid that PRESERVES the silhouette and its alpha coverage (44.2% → 43.1% → 38.8%), each level a separately-authored image with its own compmethod.** Since `XLU_SURF` has no alpha test (pass 22), averaging that 0/1 alpha into fractional mip alpha would smear the minified treeline into exactly the reported band — so 'authored LOD chain replaced by driver-generated mips' is now the leading mechanism, with hard ground truth behind it. Corrects pass 23: only `tex1198` is a treeline (`tex1199` 89.4% opaque denser foliage; `tex1200`/`tex1201` 100% opaque detail textures sharing the class). Two decoder bugs found while porting (missing per-image byte-align, `image.c:1096`; a GUESSED rather than ported RLE fudge) — both produced confident wrong pictures. **Pass 25 (live) answered all three questions: (1) YES 64×17 RGBA16 intact, pool ≡ offline decode; (2) YES alpha survives (44.2% opaque); (3) NO — no code path uploads authored chains as GL mips.** New: `tex1198`–`1201` are **never drawn from an idle spawn** (zero RGBA16 imports in 4800 frames) — the spawn wall is painted by other classes (cf D276/D280); the drawn tree-class card is **`tex149` IA8 32×64 hard cutout, byte-exact import, no authored chain**. Mip mechanism located: `texGetDepthAndSize` sizes LOADBLOCKs from the header `lods` field even for single-image textures (live A/B: 2808 B vs 2048 B real data), and the port's RC2 clip + `glGenerateMipmap`/trilinear (observed live) box-filters hard 0/F alpha into fractional mips → XLU smear. Pass 26 fix directions in entry (cheapest: no mip gen for no-chain textures). | | D237 | **QoL ask (M-106, user QA): F10 options overlay — make it scrollable with the mouse wheel, expose more settings into it (e.g. screen-flash / no-hit-flash…** — full `## D237` entry at file tail | OPEN — not designed; port-only | | D238 | **QoL ask (M-106, user QA): mouse sensitivity rework toward normal PC-shooter feel.** — full `## D238` entry at file tail | OPEN — not designed; port-only (`port/src/input.c` + `config.c`) | @@ -10744,6 +10744,13 @@ D243 (cutscene shake/duplication, its own CONSOLIDATED NEXT STEPS block appears **M-193 (v0.4.0 Phase 1, 2026-09-21): port-side GL texture cache found — the M-108 refutation stands for the *game-side* CI cache, but the port has its own cache with the exact same structural hazard, and it was never invalidated on stage transitions.** The PC renderer keeps a `TextureCacheMap` (`port/fast3d/gfx_pc.cpp`) mapping `(source address, palette addrs, fmt, siz, palette_index, size_bytes, palette_hash)` → uploaded GL texture, with **no content check** — a hit trusts the key. Stage textures' source addresses live in the `MEMPOOL_STAGE` arena, which the per-stage `while (!done)` block in `boss.c` resets via `mempResetBank`/`memaReset` on *every* transition (death/restart included — M-108's call-graph trace already established both paths re-run that block). After the reset, any cache entry keyed into that arena is stale; if the texpool places a different image at an address that held another image in the previous run of the level, the lookup hits and returns the old pixels = "wrong textures after dying/restarting", never on a cold boot (empty cache). The N64 has no such cache (the RSP re-reads DRAM every frame), so this is a pure port-side defect — fixable in `port/`-adjacent code with no rule-2 sign-off. **Fix applied:** the orphaned `videoResetTextureCache()` (`port/src/video.c`, zero callers) is now wired into that per-stage block right after `memaReset`/`reset_play_data_ptrs` (`#ifdef PORT`) — a full clear on every stage load. Re-uploads are pixel-identical (same content → same GL upload), cost is one-time during the loading screen; ROM-segment textures (menu/HUD) just re-upload too, harmlessly. `GE_D235=1` logs the purged entry count per transition (cold boot = 0, death/restart > 0) as direct mechanism evidence. **Remaining:** live death/restart confirmation at playtest (wrong textures gone?) + a `GE_D235` capture showing >0 on a restart; if symptoms persist after this, the next suspect is intra-level texpool eviction reusing addresses mid-play (nothing emits `G_INVALTEXCACHE_EXT`, so per-address staleness is possible there too — the handler exists at `gfx_pc.cpp:3624` but has no emitter). +**v0.4.0 verification pass (2026-09-23, native Windows, headless): fix wired and clean on reload; the bug itself did NOT reproduce, even with the fix disabled. Needs the user's original trigger to close.** Harness: `-level_09`, scripted watch Abort → title stage → new TEMP `GE_D235_RELOAD=` re-enters Bunker 1 via `bossSetLoadedStage()` (the front end's mission-start call) after a front-end dwell; new TEMP `GE_D235_OFF=1` skips the reset for an A/B. Results: +- Fix ON: cold boot purges 0, the abort into the title purges 527–833, and re-entry purges 63 (front-end entries). The reloaded level renders correctly; settled gameplay matches the fix-OFF run 7/9 frames, and the 2 fails are intro-fade/camera timing (D117), not textures. +- Fix OFF, identical load order (intro both times, 300-frame legal-screen dwell): 588–788 stale entries survive into the reload; **all textures correct**. +- Fix OFF, divergent load order (first load skips the intro, so only the shaft is seen; the reload plays the full control-room flyby): 589 stale entries survive; **all textures correct** at full resolution (`scratch/d235_1750.png`). + +So on this path, stale keys either miss or hit the same image. Either GE's placement of Bunker 1's bg textures is order-independent here, or the key (`palette_hash`, `size_bytes`, fmt) disambiguates. The fix stays correct by construction: an address-keyed cache with no content check outlives an arena reset, and clearing it at every stage load costs only re-uploads during the loading screen. What's untested is the reported trigger: a real **death → mission failed → mission select → restart** (the front end's image-heavy briefing/folder screens and 3D models go through the stage arena in between) on the level where it was seen (Facility first). **Human test:** reproduce with `GE_D235_OFF=1` (expect wrong textures) and without (expect clean); note the level and surfaces. Confidence that the fix addresses the report: medium, since the mechanism is real but the trigger is not captured. + ## D236 — Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report). **Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).** Eight passes so far (D265, D266–D270, D271, D272, D273, D274, D275–D276). Ruled out: IA4/IA8 decode (as originally scoped), the blend pipeline, fog combine (as originally scoped), portal-culling over-draw (D271, real bug, fixed, not the cause), converter/UV-conversion bugs (D272, code-read clean), the CI palette bit-layout (D273/D274, tested twice against real content), "mips absent" (D274, confirmed present/enabled), "identical UV window" *for the CI8 classes* (D272). **Current leading picture (D276, M-134): the actual visible weave at confirmed weave pixels is painted by a different, previously-dismissed class — an IA8 blocky-noise texture (`oml=0xc81049d8`/`fmt=3`) sampled with a fixed, byte-identical 2×-wrapped UV window and low fog, reproducing D265's original "same window" observation exactly (D269's CI8-classes framing had reframed away from that lead prematurely).** A third, unattributed CI8 class (`0x0c184b50`) may be the reference's actual discrete-tree layer and shows an erratic z/w suggesting its own occlusion/billboard bug — D236 may split into two root causes. **Ninth pass (D280, M-135): fog math verified N64-faithful (±2 truncation over 2505 verts), the IA8 noise class visually confirmed as the weave's painter (HF 14.8→2.9 when collapsed), `0xc8102078` ruled out (terrain, 1.4M tris), and the depth-race read inverted — `0x0c184b50` tree cards are IN FRONT of the noise quad (z/w smaller=closer) yet still don't visibly win; under deterministic `-level_36` boot they barely instantiate at all (~4 tris vs 720 in a free run). No quick win: remaining work is CPU-side instantiation/room-streaming, not a port fix.** **Tenth pass (M-155): that CPU-side-instantiation framing itself is REFUTED — three independent deterministic re-runs of the same `-level_36` boot (one with sustained camera rotation) proved the class's per-frame count is byte-identical, not run-to-run dependent; the earlier "~4 vs 720" comparison was an outdoor-vs-indoor room mismatch (the boot's scripted intro walks the camera out of the tree-bearing room at a fixed frame), not nondeterminism. No fix; the original wall-vs-discrete-sprites symptom is unresolved and untouched by this pass.** Distinct from D176(b)'s painted tree-line backdrop. Logged in `GRAPHICS-BACKLOG.md`. diff --git a/src/boss.c b/src/boss.c index 1243d2c5..5700d52a 100644 --- a/src/boss.c +++ b/src/boss.c @@ -146,6 +146,9 @@ struct memallocstring memallocstringtable[] = { }; s32 g_MainStageNum = LEVELID_NONE; +#ifdef PORT +static s32 g_d235ReloadStage = -1, g_d235ReloadWait = 0; /* TEMP D235 verification */ +#endif s32 g_BossIsDebugMenuOpen = FALSE; OSScMsg g_bossGfxDoneMsg = { OS_SC_DONE_MSG }; @@ -521,7 +524,32 @@ void bossMainloop(void) if (ge_d235) osSyncPrintf("D235: stage=%d texcache entries purged at load=%d\n", (s32)g_StageNum, gfx_texture_cache_count()); - videoResetTextureCache(); + if (!getenv("GE_D235_OFF")) /* TEMP: A/B switch for verification */ + videoResetTextureCache(); + + /* TEMP (D235 verification, remove with the probe): + * GE_D235_RELOAD= re-enters the first level once, + * retraces after the title stage loads following it (e.g. after a + * watch Abort) -- so the front end has rendered its own textures + * into the reset stage arena first, like a real death/restart -- + * via the same bossSetLoadedStage() call the front end's mission + * start makes (front.c). Inert when unset. */ + { + static int s_reload = -2, s_firstLevel = -1; + if (s_reload == -2) { + const char *e = getenv("GE_D235_RELOAD"); + s_reload = e ? atoi(e) : -1; + } + if (s_reload >= 0) { + if (g_StageNum != LEVELID_TITLE && s_firstLevel < 0) + s_firstLevel = g_StageNum; + else if (g_StageNum == LEVELID_TITLE && s_firstLevel >= 0 && + g_d235ReloadStage < 0 && g_d235ReloadWait == 0) { + g_d235ReloadStage = s_firstLevel; + g_d235ReloadWait = s_reload + 1; + } + } + } } #endif @@ -579,6 +607,13 @@ void bossMainloop(void) { if (g_MainStageNum < 0 && pendingGfx < 2U) { +#ifdef PORT + /* TEMP D235 verification (see GE_D235_RELOAD above) */ + if (g_d235ReloadWait > 0 && --g_d235ReloadWait == 0) { + osSyncPrintf("D235: RELOAD re-entering stage %d\n", g_d235ReloadStage); + bossSetLoadedStage(g_d235ReloadStage); + } +#endif if (get_is_ramrom_flag()) { iterate_ramrom_entries_handle_camera_out();