From 75f39ebe137fb5678f3b38b367048c5612e8b5b1 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:12:43 -0400 Subject: [PATCH 1/8] =?UTF-8?q?docs(D172):=20withdraw=200xB9=20root=20caus?= =?UTF-8?q?e=20=E2=80=94=20it's=20G=5FSETOTHERMODE=5FL,=20already=20handle?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0xB9 == G_SETOTHERMODE_L in this non-F3DEX2 build (G_IMMFIRST-6); fast3d handles it as gsDPSetRenderMode. -level_09 sustained-fire run: zero D146 aborts, zero warnings. Re-scoped to the 2-cycle/cycletype hypothesis (records never set G_MDSFT_CYCLETYPE -> use_2cyc==0 on PC -> TEXEL1 garbage -> magenta/cyan). --- docs/dev/D172-PARTICLE-COLOR-RESEARCH.md | 86 ++++++++++++++---------- docs/dev/GRAPHICS-BACKLOG.md | 2 +- 2 files changed, 53 insertions(+), 35 deletions(-) diff --git a/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md b/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md index 3df88f4f..e97f68ee 100644 --- a/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md +++ b/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md @@ -45,50 +45,65 @@ bit layout: - The generated GLSL combiner implements the RDP formula with the usual special cases; `SHADER_COMBINED` correctly resolves to the previous cycle's running value. -## 4. Root-cause candidate: the 0xB9 word aborts the sub-DL (D146 path) +## 4. The 0xB9 root-cause candidate is WITHDRAWN (M-82, 2026-09-08) -fast3d's SP walker has **no case for opcode 0xB9** (full case list audited: MTX/MOVEMEM/ -VTX/DL/COL/syncs/all DP immediates/EXT commands — nothing in the 0x80–0xBF group). Its -`default:` (`gfx_pc.cpp:2920`, the D146 guard) **ends the whole (sub-)display list** on an -unknown opcode. +The earlier audit claimed fast3d has "no case for opcode 0xB9" and that the D146 +`default:` guard therefore aborts every particle sub-DL. **That is wrong.** -Consequence for every particle record replay: +This build does **not** define `F3DEX_GBI_2` (verified: no `-DF3DEX_GBI_2` in +`CMakeLists.txt`, no `#define` in `include/PR/gbi.h` or anywhere in tree). In the +non-F3DEX2 branch of `gbi.h`, `G_SETOTHERMODE_L = G_IMMFIRST-6 = -71`, and +`(uint8_t)(-71) == 0xB9`. So `gfx_pc.cpp:2731` `case (uint8_t)G_SETOTHERMODE_L:` +**is** `case 0xB9:`. -``` -RDPPIPESYNC -> ok (no-op) -0xB9 -> D146 default -> RETURN (sub-DL dead) -G_SETCOMBINE -> never applied -G_SETTIMG -> never applied - -> drawn with STALE combine mode + stale tile/timg state -``` +Decode of the record word `w0 = 0xB900031D`, `w1 = `: +`gfx_sp_set_other_mode(C0(8,8)=3, C0(0,8)=29, w1)` → sets `other_mode_l` bits +3..31. `sft=3 == G_MDSFT_RENDERMODE`, `len=29` — this is exactly +`gsDPSetRenderMode(w1)`. `w1` values (`0x0C184B50`, `0x00504B50`, …) are +render-mode / blender words. fast3d applies it correctly. -Stale CC/tile state = arbitrary colors = the magenta/cyan symptom. On N64, gmain.s -obviously handles 0xB9 (the game works there), so this is a fast3d gap, not a ROM quirk. +So the particle sub-DL runs to completion on PC: `E7` RDPPIPESYNC (handled), +`B9` SetRenderMode (handled), `FC` SETCOMBINE (handled), `FD` SETTIMG (handled). +**No D146 abort. No stale-state fallthrough.** The wrong-colour bug is elsewhere. -Secondary (only matters if 0xB9 turns out to be a no-op): particle 2-cycle rendering -requires `G_CYC_2CYCLE` in other_mode_h (`gfx_pc.cpp:1523`); the records never set -other-modes, so particles inherit ambient state — same on N64, but worth confirming if -the primary fix doesn't resolve it. +## 5. Where the bug actually is — re-scoped (M-82) -## 5. Verification (cheap, in-game) +The dominant combine (14/17 records, §2) is a **2-cycle** mode +(`cycle 1 RGB: (COMBINED - 0)·SHADE + COMB_A`). But the records set render mode +(`0xB9`) and **never set the cycle-type** field (`G_MDSFT_CYCLETYPE`, in +`other_mode_h`). On N64 the ambient `other_mode_h` at particle-draw time is +2-cycle (set by whatever DL ran before); fast3d's ambient `other_mode_h` at that +point is very likely **1-cycle**, so `use_2cyc` (`gfx_pc.cpp:1521`) is false and +fast3d renders only cycle-0 of the combine: +`(TEXEL0 - 0)·TEXEL1 + COMBINED_A`. With no second tile loaded, **TEXEL1 is +garbage** → arbitrary colour → the magenta/cyan symptom. This is the "secondary" +note from the old §4, now promoted to the primary hypothesis. -The D146 guard logs `D146: unknown GBI opcode 0x%02x ...` rate-limited to **20** messages. -Run any level, shoot things / trigger flares, watch console output at startup for -`unknown GBI opcode 0xb9`. If present → root cause confirmed. (The budget fills in the -first frame or two of particles, so check early output.) +### Verification + +1. **DONE (M-82).** `-level_09` (bunker1), `GE_INPUTSCRIPT` sustained-fire, + ~3600 frames, `d172_probe.log`: **zero `D146: unknown GBI opcode` lines, + zero ERROR/WARN of any kind.** The particle sub-DL does not abort on 0xB9. + §4 root cause is dead. (Headless, so this does not by itself confirm the + §5 2-cycle hypothesis — that still needs the probe below.) +2. Add an env-gated probe at `gfx_sp_tri*` when `rdp.combine_mode` matches a + particle record's key: log `use_2cyc`, `other_mode_h & CYCLETYPE`, the CC + key. Expect `use_2cyc == 0`. ## 6. Fix path (next session) -1. **Decode 0xB9's semantics.** `ginit.s` (dispatch-table init) is *not in this repo* - (`gmain.s:10` includes it; file absent). Options: (a) locate ginit.s in the original - source / another checkout; (b) trace gmain.s's 0x80–0xBF group handler by hand; - (c) infer from data — w0 is constant `B900031D` across all records, w1 varies - (`0C184B50`, `00504B50`, …); if it's an other-modes/cycle-type setter, w1 bits should - match G_MDSFT_* fields. -2. Implement the handler in fast3d (port-only). Re-run: blood/flares should go dark red; - D146 spam for 0xb9 stops. -3. If colors still wrong after that: log `use_2cyc` + combine key on particle triangles - (env-gated) and check ambient G_CYC_2CYCLE (§4 secondary). +1. Run the §5 verification. If `use_2cyc == 0` on particle tris is confirmed: +2. **Port-only fix, `src/game/explosion.c` under `#ifdef PORT`** (same + hardware-idiom exception class as the existing `G_TRI4` / dynamic-light + `#ifdef PORT`s): emit an explicit `gsDPSetCycleType(G_CYC_2CYCLE)` (or the + raw `G_SETOTHERMODE_H` immediate) into / ahead of the particle record replay + so PC does not depend on ambient cycle-type. `#else` = N64 stream verbatim. + Alternative (pure port): in `gfx_pc.cpp`, when a SETCOMBINE carries a + 2-cycle-only combine (cycle-1 slots non-trivial) force `use_2cyc` — riskier, + broader blast radius. +3. If colour still wrong after 2-cycle is forced: check `texSelect` actually + binds TEXEL1's tile for these records, and re-decode the combine (the §2 + decode was not re-verified this pass). ## Files @@ -98,3 +113,6 @@ first frame or two of particles, so check early output.) SETCOMBINE decode (2786), combine store (2270), cc generation (312+), 2CYCLE flag (1523) - `rsp/graphics/gmain.s` — N64 ucode ground truth for 0xB9 (dispatch table at dmem 0xbc; ginit.s missing from repo) + +Note (M-82): 0xB9 is `G_SETOTHERMODE_L` in this non-F3DEX2 build and is already +handled by fast3d — `gmain.s` / `ginit.s` are NOT needed to resolve D172. diff --git a/docs/dev/GRAPHICS-BACKLOG.md b/docs/dev/GRAPHICS-BACKLOG.md index 1081fd74..15921e03 100644 --- a/docs/dev/GRAPHICS-BACKLOG.md +++ b/docs/dev/GRAPHICS-BACKLOG.md @@ -18,7 +18,7 @@ once the game is playable start-to-finish without crashes. | **D149** | Front-end MISSION COMPLETE / mode-select 3D models (dossier, wallets) render as garbled full-screen geometry or not at all — one of the compiled sub-DLs (seg5+0x9ee4) is malformed, D146 now skips it instead of aborting. | D75 family. §F D146. | | **D170** | Named-NPC "flee" scripted behavior looks wrong on PC: on Silo, Ourumov breaks into a full run the instant he alerts ("Ourumov: Kill him!") rather than the N64 walk/retreat; developer reports Trevelyan behaves the same way on Cradle. **M-49 full playthrough: escalated — Cradle (`-level_51`) is "bugged out completely" from Trevelyan's behavior; the final level's scripted progression breaks. Now folded into / superseded by §F D193 (NPC AI locomotion too slow).** | **Observed, not investigated** (M-34, from a Silo playtest capture; user bug report — no fix wanted yet). Could be a genuine ailist / `chrTick` divergence, OR the D155/D156 wall-clock `deltaFrames` locomotion-anim speed-up (a catch-up spike playing the walk cycle at 2–3×). **Verify first:** capture the same beat against N64 reference footage before assuming it diverges — Ourumov may be scripted to flee. If it's real, suspect the frametiming clamp (§F D155/D156) or the Cradle-family AI path (§F D123). | | **D171** | Silo missile-shaft: a large translucent grey triangle sweeps across the lower frame while panning down the shaft (~3 s of the capture). Looks like a stray poly / near-plane clip fan, same family as D106 (portal near-plane min>max). | **Observed, not investigated** (M-34, Silo capture). Cosmetic, intermittent, tied to camera angle in the open vertical shaft. | -| **D172** | Bullet-impact / blood particle spray renders magenta or cyan instead of dark red (visible on guards hit at close range on Silo). | **Root-cause candidate found, static research done** (`docs/dev/D172-PARTICLE-COLOR-RESEARCH.md`). Particles get CC/tile state from 15 ROM records replayed via `gSPDisplayList` (`explosion.c:894`); each record contains an opcode **0xB9** that fast3d does not handle → the D146 unknown-opcode guard kills the sub-DL before the record's `G_SETCOMBINE`+`G_SETTIMG` apply → particles draw with stale state = magenta/cyan. SETCOMBINE decode itself verified correct. Verify in-game: watch console for `D146: unknown GBI opcode 0xb9` while shooting (rate-limited to 20). Fix: decode 0xB9 semantics (ginit.s missing from repo — trace gmain.s 0x80–0xBF group or infer from data) + implement in fast3d. | +| **D172** | Bullet-impact / blood particle spray renders magenta or cyan instead of dark red (visible on guards hit at close range on Silo). | **M-82: 0xB9 root cause WITHDRAWN** (`docs/dev/D172-PARTICLE-COLOR-RESEARCH.md` §4). 0xB9 = `G_SETOTHERMODE_L` in this non-F3DEX2 build (`= G_IMMFIRST-6`), already handled by fast3d as `gsDPSetRenderMode`; `-level_09` sustained-fire run logged **zero D146 aborts**. New primary hypothesis: the dominant particle combine is **2-cycle** but the records never set `G_MDSFT_CYCLETYPE`, so on PC `use_2cyc==0` → only cycle-0 runs → TEXEL1 garbage → magenta/cyan. Next: env-gated `use_2cyc`/CC-key probe on particle tris; fix = emit explicit `gsDPSetCycleType(G_CYC_2CYCLE)` in `explosion.c` under `#ifdef PORT`. | | **D178** | Objectives not listed correctly on the pre-mission briefing screen (the mission overview shown before starting a level). | **M-36 triage: the objective lines are BLANK, not wrong.** `GE_STARTMENU=10` Dam briefing renders the header fine ("OHMSS / Agent: James Bond / Mission 1: Arkangelsk / Part i: Dam / PRIMARY OBJECTIVES:") then just the bullet "a." with no text and no b/c/d. → **D143/D151 blank-text family** (`langGet()` → NULL → silent blank), *not* a propDef walk / difficulty decode. **Family B** in `docs/dev/M-36-TRIAGE.md` — find which lang bank the front-end briefing flow fails to load. Re-confirm the exact shape in a *real* briefing (the `GE_STARTMENU` boot skips real save state). Close relatives: D151 (watch objective text blank — type-35/23 `text` slot u16→u32 fix, M-28; interactive re-verify was still pending), D143 (briefing/objective text blank via `langGet` NULL), D157 (objective difficulty byte offset — could make difficulty-gated objectives appear/disappear wrongly). The pre-start briefing is a *different surface* from the in-level watch, so D151's fix may not cover it. **Next:** ask user which level + what exactly shows vs expected; compare against `docs/dev/LEVEL-OBJECTIVES.md` (regenerated post-D157); if lines are blank → D143/D151 family (`langGet` NULL guard = silent blank), if wrong objectives listed → propDef type-23 record walk / difficulty decode. | | **D175** | In-game stutter / brief hang during normal play — observed opening a door on Runway (mission 3, `-level_35`) and also on Surface. No backtrace captured (gdb attach missed the window; it self-recovered). | **Observed, not investigated** (user QA report). Matches known transient-hang classes rather than looking new: D155 (post-slow-load catch-up spiral — clamped), D156 (anim NaN loop — guarded, prints one-shot stderr diag if it fires), D134 (SP/DP task-done event swallowed — fixed M-22, Runway was an original victim), D147/D152 (audio `s_imLock` steal = ~2 s hiccup). Door-open specifically = new room first visible → one-off texture-import/frame spike (likely benign). **Next:** reproduce under `tools_pc/debug.ps1` with the box idle; check console for the D156 stderr diag; if it recurs, attach gdb mid-hang and pattern-match `bt` (import_texture = benign slow frame; model.c:3131 spin = D156; pthread_mutex_lock on s_imLock = D147/D152). | | **D176** | Surface exterior renders wrong (two co-located defects). **(a)** Sky is solid **black** — the skybox / sky gradient does not draw at all (reference: warm sunset cloud gradient). **(b)** The perimeter **tree-line backdrop renders as grey TV-static / comb interlacing** instead of snow-laden pines — the geometry is present and roughly the right silhouette but every tree texture is scrambled noise. Ground, tunnel and the dish/bunker building draw fine. | **Observed, screenshots committed** (M-35, user QA; `docs/img/bugs/d176-surface-exterior-our-build.png` vs `-reference.png`). Level is the shared Surface exterior — user reached it Dam→Surface on Agent so this is **Surface (i) `-level_36`** (confirm it also affects `-level_43`). **(a)** black sky: **M-36 partial (findings.md "D176(a)").** Env data is correct (`Sky.Clouds=1`, `CloudRGB=240,120,30`, `SkyImageId=0`); `skyRender` runs the cloud path and classifies corners correctly (switch case 12), but `skyRenderTri`/`skyRenderFull` emit nothing — not even the `env->RGB` fill shows. Not root-caused: next is probing `sub_GAME_7F097388` projected coords, verifying `skywaterimages[0]` resolves on PC (`image_bank.c` GIMG offset `0xFB4`), and a fast3d sky-tri trace. `GE_D176=1` probe shipped. **M-37: ROOT-CAUSED** — `skyRenderTri`/`skyRenderFull` build geometry *exclusively* as `G_RDPHALF_*` immediates, which `gfx_pc.cpp` deliberately no-ops (identical no-op in the PD port); fix = decode the RDPHALF tri-raster stream in fast3d (~1 session, genuine new code). **M-42: `-level_22` (Statue) confirmed a clean headless repro** — bare boot, frame ~360, upper half pure black; use it to verify the decoder. **M-47 + read-only consolidation:** Path B draft exists (`scratchpad/sky.patch`, `skyPortRenderPoly` PORT hook — geometry+texture reach GL, two unit defects open) and the full RDPHALF wire format is now decoded (S15.1 half-packing, 8-component perspective-interpolated vertex vectors); next session is mechanical per `docs/dev/D176a-SKY-NOTES.md`. `-level_36`/`-43` intro cams face down (not a sky test without `GE_INPUTSCRIPT`). **M-82: PR #18 rebased onto main + Defect-1 fix applied** — cloud tile is `s_skywaterimages[0]` = 64×64 IA8; the patch's `tc = S·32` scale is correct (matches GE's `width<<5` idiom, `glass2.c:678`), the bug was `(s16)` overflow on the ±20k-texel horizon S/T; fix = per-primitive phase-fold to the 64-texel period (`sky.c` `skyPortRenderPoly`, `#ifdef PORT`). Builds clean, no crash (bunker1/statue/surface1 verify PASS). **Unverified visually** — owed: one `-level_22` screenshot (see `D176a-SKY-NOTES.md` "verification ask"). Defect 2 (horizon-band coverage / straddling tris) still open → adaptive tessellation. **(b)** tree/rock static: **M-36 triage — NOT the D159 family.** `texSwapAltRowBytes` is a full `#ifdef PORT` no-op (`image.c:2199`), it cannot cause this. Real cause: `port/fast3d/gfx_pc.cpp` `import_texture_*` assume `line_size_bytes == full_image_line_size_bytes` (`SUPPORT_CHECK`; rgba16's is commented out *"trips ... with a garbage size"* `:658`) → non-PoT / sub-rect tiles get a flat linear read → diagonal row shear = grey static. **Family A** in `docs/dev/M-36-TRIAGE.md`. Fix: honour `line_size_bytes` as the real source pitch. Same defect as D182(2). | From ecd77dea79321f2968cb1967ea22b0b9068d1342 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:21:02 -0400 Subject: [PATCH 2/8] diag(D172): rule out cycle-type too; ship GE_D172 probes Real particle records (assets/oddtextures.c globalDL_0x078..) DO set gsDPSetCycleType(G_CYC_2CYCLE); GE_D172=1 probe confirms fast3d applies it (particle combine fc26a004 logged at cycletype=2CYC). Both the 0xB9-abort and the cycle-type hypotheses are now dead. Bug is the G_CC_INTERFERENCE two-tile combine / TEXEL1 path (IA8 smoke x RGBA16 fire @ tmem 0x188). Probes (#ifdef PORT, GE_D172=1, inert otherwise): - gfx_pc.cpp G_SETCOMBINE: distinct combine + active cycletype - gfx_pc.cpp gfx_sp_tri1: tris with cyc2 combine drawn at cycletype!=2CYC Needs a display capture to go further (headless can't see particle colour). --- docs/dev/D172-PARTICLE-COLOR-RESEARCH.md | 86 +++++++++++++++--------- docs/dev/GRAPHICS-BACKLOG.md | 2 +- port/fast3d/gfx_pc.cpp | 41 +++++++++++ 3 files changed, 95 insertions(+), 34 deletions(-) diff --git a/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md b/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md index e97f68ee..3b8c7087 100644 --- a/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md +++ b/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md @@ -66,44 +66,64 @@ So the particle sub-DL runs to completion on PC: `E7` RDPPIPESYNC (handled), `B9` SetRenderMode (handled), `FC` SETCOMBINE (handled), `FD` SETTIMG (handled). **No D146 abort. No stale-state fallthrough.** The wrong-colour bug is elsewhere. -## 5. Where the bug actually is — re-scoped (M-82) +## 5. Cycle-type is ALSO ruled out — the real record, and where the bug is (M-82) -The dominant combine (14/17 records, §2) is a **2-cycle** mode -(`cycle 1 RGB: (COMBINED - 0)·SHADE + COMB_A`). But the records set render mode -(`0xB9`) and **never set the cycle-type** field (`G_MDSFT_CYCLETYPE`, in -`other_mode_h`). On N64 the ambient `other_mode_h` at particle-draw time is -2-cycle (set by whatever DL ran before); fast3d's ambient `other_mode_h` at that -point is very likely **1-cycle**, so `use_2cyc` (`gfx_pc.cpp:1521`) is false and -fast3d renders only cycle-0 of the combine: -`(TEXEL0 - 0)·TEXEL1 + COMBINED_A`. With no second tile loaded, **TEXEL1 is -garbage** → arbitrary colour → the magenta/cyan symptom. This is the "secondary" -note from the old §4, now promoted to the primary hypothesis. +The actual particle records are in **`assets/oddtextures.c`** +(`globalDL_0x078` … , 17 of them). One record verbatim: -### Verification +```c +gsDPPipeSync(), +gsDPSetCycleType(G_CYC_2CYCLE), // <-- record DOES set 2-cycle +gsDPSetRenderMode(G_RM_PASS, G_RM_ZB_CLD_SURF2), +gsDPSetTextureLOD(G_TL_TILE), +gsDPSetCombineMode(G_CC_INTERFERENCE, G_CC_MODULATEIA2), +gsSPTexture(..., G_TX_RENDERTILE, G_ON), + // tile 0: IMAGE_SMOKE_0 IA16 image -> loaded/rendered as IA8, 56x56 + // tile 1: IMAGE_FIRE_0 RGBA16, 16x14, loaded to tmem 0x188 +gsDPSetTextureLUT(G_TT_NONE), +gsDPPipeSync(), +gsSPEndDisplayList(), +``` -1. **DONE (M-82).** `-level_09` (bunker1), `GE_INPUTSCRIPT` sustained-fire, - ~3600 frames, `d172_probe.log`: **zero `D146: unknown GBI opcode` lines, - zero ERROR/WARN of any kind.** The particle sub-DL does not abort on 0xB9. - §4 root cause is dead. (Headless, so this does not by itself confirm the - §5 2-cycle hypothesis — that still needs the probe below.) -2. Add an env-gated probe at `gfx_sp_tri*` when `rdp.combine_mode` matches a - particle record's key: log `use_2cyc`, `other_mode_h & CYCLETYPE`, the CC - key. Expect `use_2cyc == 0`. +So the record **explicitly sets `G_CYC_2CYCLE`** (opcode `0xBA` = +`G_SETOTHERMODE_H` in this build), and fast3d handles `0xBA` correctly: +`gfx_pc.cpp:2734` → `gfx_sp_set_other_mode(52, 2, w1<<32)` sets `other_mode_h` +cycletype = 2CYC. **Confirmed by probe (M-82):** the `GE_D172=1` SETCOMBINE +probe logged the particle combine `w0=fc26a004` (= `G_CC_INTERFERENCE`/ +`G_CC_MODULATEIA2`) with **`cycletype=1 (2CYC)`** every time. The cycle-type +hypothesis is dead too. -## 6. Fix path (next session) +**The bug is the `G_CC_INTERFERENCE` two-tile combine / TEXEL1 path.** +`G_CC_INTERFERENCE` cycle-0 = `TEXEL0 * TEXEL1`; the record binds **two +different tiles of two different formats** (tile 0 IA8 smoke, tile 1 RGBA16 +fire at tmem 0x188). Magenta/cyan = a TEXEL1 sampling/decode fault: wrong tmem +offset for tile 1, RGBA16 (5551) channel/endian misread, or fast3d treating +tile 1 as an LOD mip of tile 0 under `G_TL_TILE`. This needs a visual +render-iterate pass (headless can't see particle colour) — see §6. -1. Run the §5 verification. If `use_2cyc == 0` on particle tris is confirmed: -2. **Port-only fix, `src/game/explosion.c` under `#ifdef PORT`** (same - hardware-idiom exception class as the existing `G_TRI4` / dynamic-light - `#ifdef PORT`s): emit an explicit `gsDPSetCycleType(G_CYC_2CYCLE)` (or the - raw `G_SETOTHERMODE_H` immediate) into / ahead of the particle record replay - so PC does not depend on ambient cycle-type. `#else` = N64 stream verbatim. - Alternative (pure port): in `gfx_pc.cpp`, when a SETCOMBINE carries a - 2-cycle-only combine (cycle-1 slots non-trivial) force `use_2cyc` — riskier, - broader blast radius. -3. If colour still wrong after 2-cycle is forced: check `texSelect` actually - binds TEXEL1's tile for these records, and re-decode the combine (the §2 - decode was not re-verified this pass). +### Probes shipped (M-82, `#ifdef PORT`, `GE_D172=1`, inert when unset) + +- `gfx_pc.cpp` G_SETCOMBINE case — logs each distinct combine + active + cycletype (dedup, 64 max). +- `gfx_pc.cpp` `gfx_sp_tri1` — flags tris with a non-trivial cyc2 combine + drawn while cycletype != 2CYC (over-fires on trivial passthrough cyc2 — + read `combine_mode` values, don't trust the count). + +## 6. Fix path (next session — needs a display) + +1. Get a `-level_XX` capture with visible particle spray (Silo guard kill at + close range per the original report, or any wall-impact spark). Note the + colour. +2. Env-gated probe in `gfx_pc.cpp` `import_texture` / the TEXEL1 bind path: + for tile 1 when combine == the particle key, log format/size/tmem-addr/ + line_size and the first few decoded texels. Compare against the RGBA16 + `IMAGE_FIRE_0` bytes in the extracted asset. +3. Likely fixes, in order of suspicion: (a) tile-1 tmem address 0x188 not + honoured → TEXEL1 reads tile-0 bytes; (b) RGBA16→RGBA32 decode for the + second texunit; (c) `G_TL_TILE` LOD path picking the wrong tile for TEXEL1. +4. Re-decode `G_CC_INTERFERENCE` / `G_CC_MODULATEIA2` as fast3d generates them + (`gfx_cc.cpp`) and confirm the GLSL matches `TEXEL0*TEXEL1` then + `COMBINED*SHADE`. ## Files diff --git a/docs/dev/GRAPHICS-BACKLOG.md b/docs/dev/GRAPHICS-BACKLOG.md index 15921e03..7ea14f96 100644 --- a/docs/dev/GRAPHICS-BACKLOG.md +++ b/docs/dev/GRAPHICS-BACKLOG.md @@ -18,7 +18,7 @@ once the game is playable start-to-finish without crashes. | **D149** | Front-end MISSION COMPLETE / mode-select 3D models (dossier, wallets) render as garbled full-screen geometry or not at all — one of the compiled sub-DLs (seg5+0x9ee4) is malformed, D146 now skips it instead of aborting. | D75 family. §F D146. | | **D170** | Named-NPC "flee" scripted behavior looks wrong on PC: on Silo, Ourumov breaks into a full run the instant he alerts ("Ourumov: Kill him!") rather than the N64 walk/retreat; developer reports Trevelyan behaves the same way on Cradle. **M-49 full playthrough: escalated — Cradle (`-level_51`) is "bugged out completely" from Trevelyan's behavior; the final level's scripted progression breaks. Now folded into / superseded by §F D193 (NPC AI locomotion too slow).** | **Observed, not investigated** (M-34, from a Silo playtest capture; user bug report — no fix wanted yet). Could be a genuine ailist / `chrTick` divergence, OR the D155/D156 wall-clock `deltaFrames` locomotion-anim speed-up (a catch-up spike playing the walk cycle at 2–3×). **Verify first:** capture the same beat against N64 reference footage before assuming it diverges — Ourumov may be scripted to flee. If it's real, suspect the frametiming clamp (§F D155/D156) or the Cradle-family AI path (§F D123). | | **D171** | Silo missile-shaft: a large translucent grey triangle sweeps across the lower frame while panning down the shaft (~3 s of the capture). Looks like a stray poly / near-plane clip fan, same family as D106 (portal near-plane min>max). | **Observed, not investigated** (M-34, Silo capture). Cosmetic, intermittent, tied to camera angle in the open vertical shaft. | -| **D172** | Bullet-impact / blood particle spray renders magenta or cyan instead of dark red (visible on guards hit at close range on Silo). | **M-82: 0xB9 root cause WITHDRAWN** (`docs/dev/D172-PARTICLE-COLOR-RESEARCH.md` §4). 0xB9 = `G_SETOTHERMODE_L` in this non-F3DEX2 build (`= G_IMMFIRST-6`), already handled by fast3d as `gsDPSetRenderMode`; `-level_09` sustained-fire run logged **zero D146 aborts**. New primary hypothesis: the dominant particle combine is **2-cycle** but the records never set `G_MDSFT_CYCLETYPE`, so on PC `use_2cyc==0` → only cycle-0 runs → TEXEL1 garbage → magenta/cyan. Next: env-gated `use_2cyc`/CC-key probe on particle tris; fix = emit explicit `gsDPSetCycleType(G_CYC_2CYCLE)` in `explosion.c` under `#ifdef PORT`. | +| **D172** | Bullet-impact / blood particle spray renders magenta or cyan instead of dark red (visible on guards hit at close range on Silo). | **M-82: 0xB9 root cause WITHDRAWN** (`docs/dev/D172-PARTICLE-COLOR-RESEARCH.md` §4). 0xB9 = `G_SETOTHERMODE_L` in this non-F3DEX2 build (`= G_IMMFIRST-6`), already handled by fast3d as `gsDPSetRenderMode`; `-level_09` sustained-fire run logged **zero D146 aborts**. **Cycle-type also ruled out (M-82):** the real records (`assets/oddtextures.c` `globalDL_0x078…`) *do* `gsDPSetCycleType(G_CYC_2CYCLE)`, and a `GE_D172=1` probe confirms fast3d applies it (particle combine `fc26a004` logged at `cycletype=2CYC`). Bug is the **`G_CC_INTERFERENCE` two-tile combine / TEXEL1 path** — record binds tile 0 (IA8 smoke) × tile 1 (RGBA16 fire @ tmem 0x188); magenta/cyan = TEXEL1 tmem-offset / RGBA16 decode / LOD-tile fault. Needs a display capture. Probes shipped (`GE_D172=1`, inert). Detail: `D172-PARTICLE-COLOR-RESEARCH.md` §5–6. | | **D178** | Objectives not listed correctly on the pre-mission briefing screen (the mission overview shown before starting a level). | **M-36 triage: the objective lines are BLANK, not wrong.** `GE_STARTMENU=10` Dam briefing renders the header fine ("OHMSS / Agent: James Bond / Mission 1: Arkangelsk / Part i: Dam / PRIMARY OBJECTIVES:") then just the bullet "a." with no text and no b/c/d. → **D143/D151 blank-text family** (`langGet()` → NULL → silent blank), *not* a propDef walk / difficulty decode. **Family B** in `docs/dev/M-36-TRIAGE.md` — find which lang bank the front-end briefing flow fails to load. Re-confirm the exact shape in a *real* briefing (the `GE_STARTMENU` boot skips real save state). Close relatives: D151 (watch objective text blank — type-35/23 `text` slot u16→u32 fix, M-28; interactive re-verify was still pending), D143 (briefing/objective text blank via `langGet` NULL), D157 (objective difficulty byte offset — could make difficulty-gated objectives appear/disappear wrongly). The pre-start briefing is a *different surface* from the in-level watch, so D151's fix may not cover it. **Next:** ask user which level + what exactly shows vs expected; compare against `docs/dev/LEVEL-OBJECTIVES.md` (regenerated post-D157); if lines are blank → D143/D151 family (`langGet` NULL guard = silent blank), if wrong objectives listed → propDef type-23 record walk / difficulty decode. | | **D175** | In-game stutter / brief hang during normal play — observed opening a door on Runway (mission 3, `-level_35`) and also on Surface. No backtrace captured (gdb attach missed the window; it self-recovered). | **Observed, not investigated** (user QA report). Matches known transient-hang classes rather than looking new: D155 (post-slow-load catch-up spiral — clamped), D156 (anim NaN loop — guarded, prints one-shot stderr diag if it fires), D134 (SP/DP task-done event swallowed — fixed M-22, Runway was an original victim), D147/D152 (audio `s_imLock` steal = ~2 s hiccup). Door-open specifically = new room first visible → one-off texture-import/frame spike (likely benign). **Next:** reproduce under `tools_pc/debug.ps1` with the box idle; check console for the D156 stderr diag; if it recurs, attach gdb mid-hang and pattern-match `bt` (import_texture = benign slow frame; model.c:3131 spin = D156; pthread_mutex_lock on s_imLock = D147/D152). | | **D176** | Surface exterior renders wrong (two co-located defects). **(a)** Sky is solid **black** — the skybox / sky gradient does not draw at all (reference: warm sunset cloud gradient). **(b)** The perimeter **tree-line backdrop renders as grey TV-static / comb interlacing** instead of snow-laden pines — the geometry is present and roughly the right silhouette but every tree texture is scrambled noise. Ground, tunnel and the dish/bunker building draw fine. | **Observed, screenshots committed** (M-35, user QA; `docs/img/bugs/d176-surface-exterior-our-build.png` vs `-reference.png`). Level is the shared Surface exterior — user reached it Dam→Surface on Agent so this is **Surface (i) `-level_36`** (confirm it also affects `-level_43`). **(a)** black sky: **M-36 partial (findings.md "D176(a)").** Env data is correct (`Sky.Clouds=1`, `CloudRGB=240,120,30`, `SkyImageId=0`); `skyRender` runs the cloud path and classifies corners correctly (switch case 12), but `skyRenderTri`/`skyRenderFull` emit nothing — not even the `env->RGB` fill shows. Not root-caused: next is probing `sub_GAME_7F097388` projected coords, verifying `skywaterimages[0]` resolves on PC (`image_bank.c` GIMG offset `0xFB4`), and a fast3d sky-tri trace. `GE_D176=1` probe shipped. **M-37: ROOT-CAUSED** — `skyRenderTri`/`skyRenderFull` build geometry *exclusively* as `G_RDPHALF_*` immediates, which `gfx_pc.cpp` deliberately no-ops (identical no-op in the PD port); fix = decode the RDPHALF tri-raster stream in fast3d (~1 session, genuine new code). **M-42: `-level_22` (Statue) confirmed a clean headless repro** — bare boot, frame ~360, upper half pure black; use it to verify the decoder. **M-47 + read-only consolidation:** Path B draft exists (`scratchpad/sky.patch`, `skyPortRenderPoly` PORT hook — geometry+texture reach GL, two unit defects open) and the full RDPHALF wire format is now decoded (S15.1 half-packing, 8-component perspective-interpolated vertex vectors); next session is mechanical per `docs/dev/D176a-SKY-NOTES.md`. `-level_36`/`-43` intro cams face down (not a sky test without `GE_INPUTSCRIPT`). **M-82: PR #18 rebased onto main + Defect-1 fix applied** — cloud tile is `s_skywaterimages[0]` = 64×64 IA8; the patch's `tc = S·32` scale is correct (matches GE's `width<<5` idiom, `glass2.c:678`), the bug was `(s16)` overflow on the ±20k-texel horizon S/T; fix = per-primitive phase-fold to the 64-texel period (`sky.c` `skyPortRenderPoly`, `#ifdef PORT`). Builds clean, no crash (bunker1/statue/surface1 verify PASS). **Unverified visually** — owed: one `-level_22` screenshot (see `D176a-SKY-NOTES.md` "verification ask"). Defect 2 (horizon-band coverage / straddling tris) still open → adaptive tessellation. **(b)** tree/rock static: **M-36 triage — NOT the D159 family.** `texSwapAltRowBytes` is a full `#ifdef PORT` no-op (`image.c:2199`), it cannot cause this. Real cause: `port/fast3d/gfx_pc.cpp` `import_texture_*` assume `line_size_bytes == full_image_line_size_bytes` (`SUPPORT_CHECK`; rgba16's is commented out *"trips ... with a garbage size"* `:658`) → non-PoT / sub-rect tiles get a flat linear read → diagonal row shear = grey static. **Family A** in `docs/dev/M-36-TRIAGE.md`. Fix: honour `line_size_bytes` as the real source pitch. Same defect as D182(2). | diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index b7e064bb..b2e48e45 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -1529,6 +1529,25 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo use_alpha = true; } +#ifdef PORT + /* D172 probe (env-gated, inert): flag triangles whose combine carries + * non-trivial cycle-2 terms while fast3d is NOT in 2-cycle mode - the + * suspected particle-colour bug (records set a 2-cyc combine but never + * set cycletype). rgb_cyc2 sits at bits 28..43 of combine_mode. */ + if (getenv("GE_D172")) { + uint32_t rgb_cyc2 = (uint32_t)((rdp.combine_mode >> 28) & 0xffff); + if (!use_2cyc && rgb_cyc2 != 0) { + static int d172t = 0; + if (d172t < 12) { + d172t++; + sysLogPrintf(LOG_NOTE, + "D172: tri with 2cyc combine but cycletype=1CYC combine_mode=%llx", + (unsigned long long)rdp.combine_mode); + } + } + } +#endif + if (use_alpha) { cc_options |= (uint64_t)SHADER_OPT_ALPHA; } @@ -2784,6 +2803,28 @@ static void gfx_run_dl(Gfx* cmd) { gfx_dp_set_grayscale_color(C1(24, 8), C1(16, 8), C1(8, 8), C1(0, 8)); break; case G_SETCOMBINE: +#ifdef PORT + /* D172 probe (env-gated, inert): log every SETCOMBINE with the + * cycle-type active at that moment. Particle records + * (explosion.c g_ExplosionDisplayLists[]) set a 2-cycle + * combine but never set cycletype - this tells us what + * cycletype fast3d has when they replay. */ + if (getenv("GE_D172")) { + static uint64_t d172seen[64]; + static int d172cnt = 0; + uint64_t key = ((uint64_t)(uint32_t)cmd->words.w0 << 32) | (uint32_t)cmd->words.w1; + bool dup = false; + for (int k = 0; k < d172cnt; k++) if (d172seen[k] == key) { dup = true; break; } + if (!dup && d172cnt < 64) { + d172seen[d172cnt++] = key; + uint32_t ct = (rdp.other_mode_h >> G_MDSFT_CYCLETYPE) & 3; + sysLogPrintf(LOG_NOTE, + "D172: SETCOMBINE w0=%08x w1=%08x cycletype=%u (%s)", + (uint32_t)cmd->words.w0, (uint32_t)cmd->words.w1, + ct, ct == 0 ? "1CYC" : ct == 1 ? "2CYC" : ct == 2 ? "COPY" : "FILL"); + } + } +#endif gfx_dp_set_combine_mode(color_comb(C0(20, 4), C1(28, 4), C0(15, 5), C1(15, 3)), alpha_comb(C0(12, 3), C1(12, 3), C0(9, 3), C1(9, 3)), color_comb(C0(5, 4), C1(24, 4), C0(0, 5), C1(6, 3)), From 65d3e025a5569fa5bb4faec740046cc1e30a54aa Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:43:15 -0400 Subject: [PATCH 3/8] diag(D172): replace weak tri probe with a decisive TEXEL1-bind dump The old 'cyc2 combine at cycletype=1CYC' probe over-fired on trivial passthrough cyc2 terms. Replace with one that fires only when a 2-cycle triangle actually consumes TEXEL1 (the G_CC_INTERFERENCE particle case) and dumps both texunits' tile/tmem/fmt/siz. One Silo capture then shows whether TEXEL1 resolves to the fire tile (tmem 392, RGBA16) or something wrong. Build clean; bunker1 verify PASS. --- docs/dev/D172-PARTICLE-COLOR-RESEARCH.md | 15 ++++++--- port/fast3d/gfx_pc.cpp | 42 +++++++++++++----------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md b/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md index 3b8c7087..508b8e3a 100644 --- a/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md +++ b/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md @@ -104,10 +104,17 @@ render-iterate pass (headless can't see particle colour) — see §6. ### Probes shipped (M-82, `#ifdef PORT`, `GE_D172=1`, inert when unset) - `gfx_pc.cpp` G_SETCOMBINE case — logs each distinct combine + active - cycletype (dedup, 64 max). -- `gfx_pc.cpp` `gfx_sp_tri1` — flags tris with a non-trivial cyc2 combine - drawn while cycletype != 2CYC (over-fires on trivial passthrough cyc2 — - read `combine_mode` values, don't trust the count). + cycletype (dedup, 64 max). Confirmed the particle combine `fc26a004` runs + at `cycletype=2CYC`. +- `gfx_pc.cpp` `gfx_sp_tri1` (after the texunit loop) — whenever a **2-cycle + triangle actually consumes TEXEL1**, dumps `combine_mode` + both texunits' + `tile / tmem / fmt / siz`. This is the decisive probe: a Silo close-range + guard kill with `GE_D172=1` should log + `T0[... fmt=3(IA) siz=1(8b)] T1[tmem=392 fmt=0(RGBA) siz=2(16b)]`. + If `T1.tmem != 392`, or `T1.fmt/siz` are wrong, the TEXEL1 bind is the bug. + If they're all correct, the fault is in `import_texture_rgba16` for the + second texunit or the `G_CC_INTERFERENCE` CC generation — pair with + `GE_DTEX=1` / `GE_TEXRAW=1` to dump the fire tile's bytes. ## 6. Fix path (next session — needs a display) diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index b2e48e45..e46f0133 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -1529,25 +1529,6 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo use_alpha = true; } -#ifdef PORT - /* D172 probe (env-gated, inert): flag triangles whose combine carries - * non-trivial cycle-2 terms while fast3d is NOT in 2-cycle mode - the - * suspected particle-colour bug (records set a 2-cyc combine but never - * set cycletype). rgb_cyc2 sits at bits 28..43 of combine_mode. */ - if (getenv("GE_D172")) { - uint32_t rgb_cyc2 = (uint32_t)((rdp.combine_mode >> 28) & 0xffff); - if (!use_2cyc && rgb_cyc2 != 0) { - static int d172t = 0; - if (d172t < 12) { - d172t++; - sysLogPrintf(LOG_NOTE, - "D172: tri with 2cyc combine but cycletype=1CYC combine_mode=%llx", - (unsigned long long)rdp.combine_mode); - } - } - } -#endif - if (use_alpha) { cc_options |= (uint64_t)SHADER_OPT_ALPHA; } @@ -1710,6 +1691,29 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo } } +#ifdef PORT + /* D172 probe (env-gated, inert): the magenta/cyan blood/spark bug. The + * particle records (assets/oddtextures.c globalDL_0x078..) draw a 2-cycle + * G_CC_INTERFERENCE combine (TEXEL0*TEXEL1) binding two tiles of two + * formats - tile 0 IA8 smoke, tile 1 RGBA16 fire @ tmem 0x188. Dump both + * texunits' tile state whenever a 2-cycle tri actually consumes TEXEL1, so + * one Silo capture pins whether TEXEL1 resolves to the right tmem/format. */ + if (getenv("GE_D172") && use_2cyc && comb->used_textures[1]) { + static int d172x = 0; + if (d172x < 16) { + d172x++; + const uint32_t t0 = rdp.first_tile_index + gfx_lod_tile_offset(0); + const uint32_t t1 = rdp.first_tile_index + gfx_lod_tile_offset(1); + sysLogPrintf(LOG_NOTE, + "D172: 2cyc+TEXEL1 tri combine=%llx " + "T0[tile=%u tmem=%u fmt=%u siz=%u] T1[tile=%u tmem=%u fmt=%u siz=%u]", + (unsigned long long)rdp.combine_mode, + t0, rdp.texture_tile[t0].tmem, rdp.texture_tile[t0].fmt, rdp.texture_tile[t0].siz, + t1, rdp.texture_tile[t1].tmem, rdp.texture_tile[t1].fmt, rdp.texture_tile[t1].siz); + } + } +#endif + struct ShaderProgram* prg = comb->prg[tm]; if (prg == NULL) { comb->prg[tm] = prg = From 4f2585245d3fec41687986bce36c4dcfd28e5196 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:22:32 -0400 Subject: [PATCH 4/8] =?UTF-8?q?diag(D172):=20sharper=20probe=20=E2=80=94?= =?UTF-8?q?=20filter=20to=20real=20multitex,=20log=20tex=5Flod=20+=20sampl?= =?UTF-8?q?ed=20vs=20configured=20tile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 16-line budget filled during the Silo intro with LOD-bilerp tris (TEXEL1 = mip level, T0==T1 is correct there). New probe: only fires when the DL configured tile first+1 at a DISTINCT tmem (genuine 2nd material), logs tex_lod + which tile each texunit actually SAMPLES vs what was configured, flags TEXEL1==TEXEL0. Budget 40. --- port/fast3d/gfx_pc.cpp | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index e46f0133..d77efcb9 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -1699,17 +1699,28 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo * texunits' tile state whenever a 2-cycle tri actually consumes TEXEL1, so * one Silo capture pins whether TEXEL1 resolves to the right tmem/format. */ if (getenv("GE_D172") && use_2cyc && comb->used_textures[1]) { - static int d172x = 0; - if (d172x < 16) { - d172x++; - const uint32_t t0 = rdp.first_tile_index + gfx_lod_tile_offset(0); - const uint32_t t1 = rdp.first_tile_index + gfx_lod_tile_offset(1); - sysLogPrintf(LOG_NOTE, - "D172: 2cyc+TEXEL1 tri combine=%llx " - "T0[tile=%u tmem=%u fmt=%u siz=%u] T1[tile=%u tmem=%u fmt=%u siz=%u]", - (unsigned long long)rdp.combine_mode, - t0, rdp.texture_tile[t0].tmem, rdp.texture_tile[t0].fmt, rdp.texture_tile[t0].siz, - t1, rdp.texture_tile[t1].tmem, rdp.texture_tile[t1].fmt, rdp.texture_tile[t1].siz); + const uint32_t fi = rdp.first_tile_index; + /* the tile fast3d will actually SAMPLE for each texunit */ + const uint32_t s0 = fi + gfx_lod_tile_offset(0); + const uint32_t s1 = fi + gfx_lod_tile_offset(1); + /* the tile the DL actually CONFIGURED for texunit 1 */ + const uint32_t c1 = fi + 1; + /* only interesting when the DL set up a genuinely distinct 2nd tile + * (different tmem) -- filters out mip/LOD-bilerp false positives */ + if (rdp.texture_tile[c1].tmem != rdp.texture_tile[fi].tmem) { + static int d172x = 0; + if (d172x < 40) { + d172x++; + sysLogPrintf(LOG_NOTE, + "D172: multitex tri combine=%llx tex_lod=%d first=%u | " + "cfg1[tile=%u tmem=%u fmt=%u siz=%u] | " + "SAMPLED0=tile%u(tmem=%u) SAMPLED1=tile%u(tmem=%u fmt=%u)%s", + (unsigned long long)rdp.combine_mode, (int)rdp.tex_lod, fi, + c1, rdp.texture_tile[c1].tmem, rdp.texture_tile[c1].fmt, rdp.texture_tile[c1].siz, + s0, rdp.texture_tile[s0].tmem, + s1, rdp.texture_tile[s1].tmem, rdp.texture_tile[s1].fmt, + (s1 == s0) ? " <-- TEXEL1 == TEXEL0 (BUG)" : ""); + } } } #endif From 3e2f62b0a3a4d5208f41815078c51ae9f1aadc31 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:25:26 -0400 Subject: [PATCH 5/8] =?UTF-8?q?diag(D172):=20probe=20only=20non-LOD=20mult?= =?UTF-8?q?itex=20(tex=5Flod=3D0)=20=E2=80=94=20the=20particle=20case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- port/fast3d/gfx_pc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index d77efcb9..839a4cf1 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -1698,7 +1698,7 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo * formats - tile 0 IA8 smoke, tile 1 RGBA16 fire @ tmem 0x188. Dump both * texunits' tile state whenever a 2-cycle tri actually consumes TEXEL1, so * one Silo capture pins whether TEXEL1 resolves to the right tmem/format. */ - if (getenv("GE_D172") && use_2cyc && comb->used_textures[1]) { + if (getenv("GE_D172") && use_2cyc && comb->used_textures[1] && !rdp.tex_lod) { const uint32_t fi = rdp.first_tile_index; /* the tile fast3d will actually SAMPLE for each texunit */ const uint32_t s0 = fi + gfx_lod_tile_offset(0); From 109d37a6c21fc51a4bd4f87b3b0e15078996cf90 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:30:45 -0400 Subject: [PATCH 6/8] fix(D172): stop gfx_lod_tile_offset collapsing non-LOD 2-texture combines ROOT CAUSE FOUND (GE_D172 probe, user Silo firefight capture): particle draws (G_CC_INTERFERENCE = TEXEL0*TEXEL1) configure tile 0 = IA8 smoke @ tmem 0 and tile 1 = RGBA16 fire @ tmem 0x188, but gfx_lod_tile_offset() returns 0 unconditionally on the !gfx_detail_textures_enabled path (a D107 blurry-ceiling fix), so texunit 1 samples the smoke texture too. smoke * smoke -> the magenta/cyan blood/spark colour. Probe evidence, 40/40 particle tris: cfg1[tile=1 tmem=392 fmt=RGBA siz=16b] SAMPLED1=tile0(tmem=0 fmt=IA) Fix: only fold texunit i to the base tile when LOD is actually active (rdp.tex_lod). D107's mip case (tex_lod=1) is unchanged; genuine non-LOD multitexture now gets tile i. bunker1 verify PASS (worst_cell 11.3, the D107 repro level, no regression). Needs an in-game eyeball: blood/sparks should read dark red, not magenta. --- port/fast3d/gfx_pc.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index 839a4cf1..df76f5b3 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -1432,8 +1432,17 @@ static inline int gfx_lod_tile_offset(const int i) { // mip (tile 1), whose single-LOADBLOCK TMEM slot fast3d never registers // -> a magnified crop of the base image (the "blurry blob" ceilings / // wall panels in BUNKER1). GE loads the whole mip chain at TMEM 0, so - // the base render tile is the only correctly-loaded level: always use it. - return 0; + // for an LOD texture the base render tile is the only correctly-loaded + // level: use it. + // + // D172: but this must NOT collapse a genuine non-LOD two-texture combine. + // The explosion/blood/spark particle records (assets/oddtextures.c + // globalDL_0x078..) set G_TL_TILE (no LOD) + G_CC_INTERFERENCE + // (TEXEL0 * TEXEL1) and bind tile 0 = IA8 smoke @ TMEM 0, tile 1 = RGBA16 + // fire @ TMEM 0x188. Returning 0 here fed TEXEL1 the smoke texture too + // (smoke * smoke) -> the magenta/cyan particle colour. Only fold to the + // base tile when LOD is actually active. + return rdp.tex_lod ? 0 : i; } static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bool is_rect) { From d23befb08fa78af0903a0211a63b429b1cd1d611 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:31:47 -0400 Subject: [PATCH 7/8] =?UTF-8?q?docs(D172):=20ROOT=20CAUSE=20FOUND=20+=20FI?= =?UTF-8?q?XED=20=E2=80=94=20gfx=5Flod=5Ftile=5Foffset=20collapse;=20findi?= =?UTF-8?q?ngs=20D172=20row=20+=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/dev/D172-PARTICLE-COLOR-RESEARCH.md | 29 ++++++++++++++++++++++-- docs/dev/GRAPHICS-BACKLOG.md | 2 +- docs/dev/findings-index.csv | 1 + docs/dev/findings.md | 1 + 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md b/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md index 508b8e3a..541410df 100644 --- a/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md +++ b/docs/dev/D172-PARTICLE-COLOR-RESEARCH.md @@ -1,7 +1,32 @@ -# D172 — particle/flares wrong color (magenta/cyan blood): static research + root-cause candidate +# D172 — particle/flares wrong color (magenta/cyan blood): ROOT CAUSE FOUND + FIXED (M-82) Symptom (user-reported, GRAPHICS-BACKLOG line 31): bullet impacts / flares / blood spatter -render magenta/cyan instead of dark red. Static audit done; one strong root-cause candidate +render magenta/cyan instead of dark red. + +## TL;DR (M-82) + +**Root cause:** `gfx_lod_tile_offset()` (`gfx_pc.cpp`) returns `0` unconditionally on +the `!gfx_detail_textures_enabled` path (a D107 blurry-ceiling fix — and +`port/src/video.c:225` sets that flag `false` at startup). Particle draws use +`G_CC_INTERFERENCE` (`TEXEL0 * TEXEL1`) with tile 0 = IA8 smoke @ TMEM 0 and +tile 1 = RGBA16 fire @ TMEM 0x188 — but with the offset pinned to 0, **texunit 1 +samples the smoke texture too**. `smoke * smoke` through the rest of the combine += the magenta/cyan. Confirmed by the `GE_D172` probe on a user Silo firefight: +40/40 particle tris logged `cfg1[tile=1 tmem=392 fmt=RGBA]` but +`SAMPLED1=tile0(tmem=0 fmt=IA)`. + +**Fix (`gfx_pc.cpp` `gfx_lod_tile_offset`):** `return rdp.tex_lod ? 0 : i;` — +fold to the base tile only when LOD is actually active (D107's mip case, +`tex_lod=1`, unchanged); a genuine non-LOD 2-texture combine now gets tile `i`. +`bunker1` verify PASS (the D107 repro level). Committed on PR #34. **Owed: an +in-game eyeball that blood/sparks now read dark red.** + +The 0xB9 / cycle-type false trails and the probe design are kept below for the +record. + +--- + +Original note (superseded): Static audit done; one strong root-cause candidate with a cheap in-game verification. **No code changed.** ## 1. How particles get their color state (mechanism) diff --git a/docs/dev/GRAPHICS-BACKLOG.md b/docs/dev/GRAPHICS-BACKLOG.md index 7ea14f96..d9dcd721 100644 --- a/docs/dev/GRAPHICS-BACKLOG.md +++ b/docs/dev/GRAPHICS-BACKLOG.md @@ -18,7 +18,7 @@ once the game is playable start-to-finish without crashes. | **D149** | Front-end MISSION COMPLETE / mode-select 3D models (dossier, wallets) render as garbled full-screen geometry or not at all — one of the compiled sub-DLs (seg5+0x9ee4) is malformed, D146 now skips it instead of aborting. | D75 family. §F D146. | | **D170** | Named-NPC "flee" scripted behavior looks wrong on PC: on Silo, Ourumov breaks into a full run the instant he alerts ("Ourumov: Kill him!") rather than the N64 walk/retreat; developer reports Trevelyan behaves the same way on Cradle. **M-49 full playthrough: escalated — Cradle (`-level_51`) is "bugged out completely" from Trevelyan's behavior; the final level's scripted progression breaks. Now folded into / superseded by §F D193 (NPC AI locomotion too slow).** | **Observed, not investigated** (M-34, from a Silo playtest capture; user bug report — no fix wanted yet). Could be a genuine ailist / `chrTick` divergence, OR the D155/D156 wall-clock `deltaFrames` locomotion-anim speed-up (a catch-up spike playing the walk cycle at 2–3×). **Verify first:** capture the same beat against N64 reference footage before assuming it diverges — Ourumov may be scripted to flee. If it's real, suspect the frametiming clamp (§F D155/D156) or the Cradle-family AI path (§F D123). | | **D171** | Silo missile-shaft: a large translucent grey triangle sweeps across the lower frame while panning down the shaft (~3 s of the capture). Looks like a stray poly / near-plane clip fan, same family as D106 (portal near-plane min>max). | **Observed, not investigated** (M-34, Silo capture). Cosmetic, intermittent, tied to camera angle in the open vertical shaft. | -| **D172** | Bullet-impact / blood particle spray renders magenta or cyan instead of dark red (visible on guards hit at close range on Silo). | **M-82: 0xB9 root cause WITHDRAWN** (`docs/dev/D172-PARTICLE-COLOR-RESEARCH.md` §4). 0xB9 = `G_SETOTHERMODE_L` in this non-F3DEX2 build (`= G_IMMFIRST-6`), already handled by fast3d as `gsDPSetRenderMode`; `-level_09` sustained-fire run logged **zero D146 aborts**. **Cycle-type also ruled out (M-82):** the real records (`assets/oddtextures.c` `globalDL_0x078…`) *do* `gsDPSetCycleType(G_CYC_2CYCLE)`, and a `GE_D172=1` probe confirms fast3d applies it (particle combine `fc26a004` logged at `cycletype=2CYC`). Bug is the **`G_CC_INTERFERENCE` two-tile combine / TEXEL1 path** — record binds tile 0 (IA8 smoke) × tile 1 (RGBA16 fire @ tmem 0x188); magenta/cyan = TEXEL1 tmem-offset / RGBA16 decode / LOD-tile fault. Needs a display capture. Probes shipped (`GE_D172=1`, inert). Detail: `D172-PARTICLE-COLOR-RESEARCH.md` §5–6. | +| **D172** | Bullet-impact / blood particle spray renders magenta or cyan instead of dark red (visible on guards hit at close range on Silo). | **M-82: ROOT CAUSE FOUND + FIXED (PR #34).** `GE_D172` probe on a user Silo firefight: 40/40 particle tris configure tile 1 = RGBA16 fire @ tmem 392 but fast3d **samples tile 0 (smoke) for both texunits** — `gfx_lod_tile_offset()` returns 0 unconditionally (D107 blurry-ceiling fix; `video.c:225` disables `gfx_detail_textures_enabled`). `G_CC_INTERFERENCE` = `smoke*smoke` → magenta/cyan. Fix: `return rdp.tex_lod ? 0 : i` — only fold to base tile for real LOD/mip draws. (0xB9 + cycle-type were both false trails, ruled out earlier in M-82.) **Owed: in-game eyeball — blood should be dark red.** Detail: `D172-PARTICLE-COLOR-RESEARCH.md`. | | **D178** | Objectives not listed correctly on the pre-mission briefing screen (the mission overview shown before starting a level). | **M-36 triage: the objective lines are BLANK, not wrong.** `GE_STARTMENU=10` Dam briefing renders the header fine ("OHMSS / Agent: James Bond / Mission 1: Arkangelsk / Part i: Dam / PRIMARY OBJECTIVES:") then just the bullet "a." with no text and no b/c/d. → **D143/D151 blank-text family** (`langGet()` → NULL → silent blank), *not* a propDef walk / difficulty decode. **Family B** in `docs/dev/M-36-TRIAGE.md` — find which lang bank the front-end briefing flow fails to load. Re-confirm the exact shape in a *real* briefing (the `GE_STARTMENU` boot skips real save state). Close relatives: D151 (watch objective text blank — type-35/23 `text` slot u16→u32 fix, M-28; interactive re-verify was still pending), D143 (briefing/objective text blank via `langGet` NULL), D157 (objective difficulty byte offset — could make difficulty-gated objectives appear/disappear wrongly). The pre-start briefing is a *different surface* from the in-level watch, so D151's fix may not cover it. **Next:** ask user which level + what exactly shows vs expected; compare against `docs/dev/LEVEL-OBJECTIVES.md` (regenerated post-D157); if lines are blank → D143/D151 family (`langGet` NULL guard = silent blank), if wrong objectives listed → propDef type-23 record walk / difficulty decode. | | **D175** | In-game stutter / brief hang during normal play — observed opening a door on Runway (mission 3, `-level_35`) and also on Surface. No backtrace captured (gdb attach missed the window; it self-recovered). | **Observed, not investigated** (user QA report). Matches known transient-hang classes rather than looking new: D155 (post-slow-load catch-up spiral — clamped), D156 (anim NaN loop — guarded, prints one-shot stderr diag if it fires), D134 (SP/DP task-done event swallowed — fixed M-22, Runway was an original victim), D147/D152 (audio `s_imLock` steal = ~2 s hiccup). Door-open specifically = new room first visible → one-off texture-import/frame spike (likely benign). **Next:** reproduce under `tools_pc/debug.ps1` with the box idle; check console for the D156 stderr diag; if it recurs, attach gdb mid-hang and pattern-match `bt` (import_texture = benign slow frame; model.c:3131 spin = D156; pthread_mutex_lock on s_imLock = D147/D152). | | **D176** | Surface exterior renders wrong (two co-located defects). **(a)** Sky is solid **black** — the skybox / sky gradient does not draw at all (reference: warm sunset cloud gradient). **(b)** The perimeter **tree-line backdrop renders as grey TV-static / comb interlacing** instead of snow-laden pines — the geometry is present and roughly the right silhouette but every tree texture is scrambled noise. Ground, tunnel and the dish/bunker building draw fine. | **Observed, screenshots committed** (M-35, user QA; `docs/img/bugs/d176-surface-exterior-our-build.png` vs `-reference.png`). Level is the shared Surface exterior — user reached it Dam→Surface on Agent so this is **Surface (i) `-level_36`** (confirm it also affects `-level_43`). **(a)** black sky: **M-36 partial (findings.md "D176(a)").** Env data is correct (`Sky.Clouds=1`, `CloudRGB=240,120,30`, `SkyImageId=0`); `skyRender` runs the cloud path and classifies corners correctly (switch case 12), but `skyRenderTri`/`skyRenderFull` emit nothing — not even the `env->RGB` fill shows. Not root-caused: next is probing `sub_GAME_7F097388` projected coords, verifying `skywaterimages[0]` resolves on PC (`image_bank.c` GIMG offset `0xFB4`), and a fast3d sky-tri trace. `GE_D176=1` probe shipped. **M-37: ROOT-CAUSED** — `skyRenderTri`/`skyRenderFull` build geometry *exclusively* as `G_RDPHALF_*` immediates, which `gfx_pc.cpp` deliberately no-ops (identical no-op in the PD port); fix = decode the RDPHALF tri-raster stream in fast3d (~1 session, genuine new code). **M-42: `-level_22` (Statue) confirmed a clean headless repro** — bare boot, frame ~360, upper half pure black; use it to verify the decoder. **M-47 + read-only consolidation:** Path B draft exists (`scratchpad/sky.patch`, `skyPortRenderPoly` PORT hook — geometry+texture reach GL, two unit defects open) and the full RDPHALF wire format is now decoded (S15.1 half-packing, 8-component perspective-interpolated vertex vectors); next session is mechanical per `docs/dev/D176a-SKY-NOTES.md`. `-level_36`/`-43` intro cams face down (not a sky test without `GE_INPUTSCRIPT`). **M-82: PR #18 rebased onto main + Defect-1 fix applied** — cloud tile is `s_skywaterimages[0]` = 64×64 IA8; the patch's `tc = S·32` scale is correct (matches GE's `width<<5` idiom, `glass2.c:678`), the bug was `(s16)` overflow on the ±20k-texel horizon S/T; fix = per-primitive phase-fold to the 64-texel period (`sky.c` `skyPortRenderPoly`, `#ifdef PORT`). Builds clean, no crash (bunker1/statue/surface1 verify PASS). **Unverified visually** — owed: one `-level_22` screenshot (see `D176a-SKY-NOTES.md` "verification ask"). Defect 2 (horizon-band coverage / straddling tris) still open → adaptive tessellation. **(b)** tree/rock static: **M-36 triage — NOT the D159 family.** `texSwapAltRowBytes` is a full `#ifdef PORT` no-op (`image.c:2199`), it cannot cause this. Real cause: `port/fast3d/gfx_pc.cpp` `import_texture_*` assume `line_size_bytes == full_image_line_size_bytes` (`SUPPORT_CHECK`; rgba16's is commented out *"trips ... with a garbage size"* `:658`) → non-PoT / sub-rect tiles get a flat linear read → diagonal row shear = grey static. **Family A** in `docs/dev/M-36-TRIAGE.md`. Fix: honour `line_size_bytes` as the real source pitch. Same defect as D182(2). | diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index e15bfaa4..9db80291 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -45,6 +45,7 @@ D178,FIXED,Pre-mission briefing screen:,FIXED (M-36) — high confidence D175,?,In-game stutter / brief hang during normal play (user QA report):,"Observed, not investigated" D176,OPEN,"Surface exterior renders wrong (-level_36), two independent defects.","(a) root-caused, fix owed / (b) open" D177,FIXED,Ladders non-functional —,FIXED (M-36) — high confidence; interactive climb test owed +D172,FIXED,"Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED).",FIXED (port/fast3d/gfx_pc.cpp gfx_lod_tile_offset): D174,?,"""No blood effect"" (user QA report).",Observed; first step = BUNKER1 firefight with regenerated sidecars D183,OPEN,"M-36 Family A (""texture line/pitch shear"") is DISPROVEN for the Surface repro (-level_36).",Hypothesis DISPROVEN + diagnostics shipped; D176(b)/D182(2) still OPEN D173,?,Third-person Bond model spawns too high (user QA report):,"Observed, not investigated" diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 4d8eabb4..824ecd9c 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -419,6 +419,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D175 | **In-game stutter / brief hang during normal play** (user QA report): opening a door on Runway (`-level_35`, mission 3) and also observed on Surface. Self-recovered; no backtrace captured. Likely one of the known transient-hang classes (D155 catch-up spiral / D156 anim NaN loop / D134 task-done event / D147-D152 audio-lock steal) or a benign new-room texture-import frame spike on door open. See `GRAPHICS-BACKLOG.md` D175 for the gdb pattern-match sheet. | Observed, not investigated | | D176 | **Surface exterior renders wrong (`-level_36`), two independent defects.** **(a)** sky solid black — env data is correct (`Clouds=1`, warm `CloudRGB`), the cloud-sky path runs, but `skyRenderTri`/`skyRenderFull` emit only `G_RDPHALF_*` immediates which `gfx_pc.cpp:2901` deliberately no-ops → **root-caused M-37** (see "D176(a) — M-37 UPDATE"); fix = decode the RDPHALF sky-tri stream in fast3d. **(b)** cliff/rock walls = grey diagonal static — NOT a texture decode bug (D183 disproved the shear hypothesis, 0/166 loads strided); re-scope from ROM ground truth of the wall texnum (M-37: inconclusive, leaning tiling-density). See `GRAPHICS-BACKLOG.md` D176. | (a) root-caused, fix owed / (b) open | | D177 | **Ladders non-functional — progression blocker (user QA report) — FIXED (M-36).** Not the input path and not the ladder state machine: `MoveBond` gates the ladder-collision path on `stanGetLocusCount(&curLocus)`, which was pinned at 0 on PC. `stanCheckLinkedSpecialTile` writes the LADDER signal via raw `outFlags[1] = 1` into a `struct StandTileLocusCallbackRecord` whose first member `s32 *rooms` is pointer-widened on PC → `[1]` is the high half of `rooms`, and `count` (moved +4→+8) is never written. Compounded by `curLocus` being declared as the 8-byte placeholder `move_bond_temp_struct` (too small for the widened record) and a `(s32)coords` pointer-truncation AV waiting in `stanGetMoveBondCollisionTiles`. **Fix:** `#ifdef PORT` — write record fields by name, declare `curLocus` as the real struct, `PORT_PTRADD` for the truncating cast (`src/game/stan.c`, `src/game/bondview2.c`; no game logic). Full detail: §F "D177". | **FIXED (M-36)** — high confidence; interactive climb test owed | +| D172 | **Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED).** Particle billboards (`explosion.c` `explosionRenderPart`, `gSPVertex`+`gSP2Triangles`) take their CC/tile state from ROM state records replayed just before them (`g_ExplosionDisplayLists[]` = `assets/oddtextures.c` `globalDL_0x078..`). The dominant record sets `gsDPSetCycleType(G_CYC_2CYCLE)` + `gsDPSetTextureLOD(G_TL_TILE)` + `gsDPSetCombineMode(G_CC_INTERFERENCE, G_CC_MODULATEIA2)` and binds **two** tiles: tile 0 = IA8 "smoke" @ TMEM 0, tile 1 = RGBA16 "fire" @ TMEM 0x188. `G_CC_INTERFERENCE` cycle-0 = `TEXEL0 * TEXEL1`. **fast3d's `gfx_lod_tile_offset()` returns `0` unconditionally** on the `!gfx_detail_textures_enabled` branch — a D107 fix for GE's mip textures (whole chain loaded in one LOADBLOCK at TMEM 0; `port/src/video.c:225` sets the flag `false`). So texunit 1's sampled tile = `first_tile_index + 0` = tile 0 → **TEXEL1 samples the smoke texture, not fire** → `smoke * smoke` → wrong colour. Two earlier M-82 candidates were false trails: (1) "0xB9 unhandled → D146 abort" — 0xB9 is `G_SETOTHERMODE_L` in this non-F3DEX2 build, handled; zero D146 aborts in a sustained-fire run. (2) "records never set 2-cycle" — they do (`0xBA`), and fast3d applies it (probe-confirmed `cycletype=2CYC`). **Probe evidence (`GE_D172=1`, user Silo firefight, `d172_silo.log`):** 40/40 particle tris → `cfg1[tile=1 tmem=392 fmt=RGBA siz=16b]` but `SAMPLED1 = tile0 (tmem=0 fmt=IA)`, `tex_lod=0`. | **FIXED (`port/fast3d/gfx_pc.cpp` `gfx_lod_tile_offset`): `return rdp.tex_lod ? 0 : i;`** — fold to the base tile only when LOD is actually active. D107's mip case (`tex_lod=1`, blurry ceilings) unchanged; a genuine non-LOD 2-texture combine now samples tile `i`. `bunker1` verify PASS (the D107 repro level), no framediff regression. PR #34. **Owed: in-game eyeball — blood/sparks should read dark red.** Env-gated `GE_D172` probes left in tree (`#ifdef PORT`, inert). | | D174 | **"No blood effect" (user QA report).** Likely the unverified D120 decal fix: `d43_emit.py` now emits the opcode-0x18 `PointUsage[]` chain, but it was never interactively verified and requires a full sidecar regen to take effect (`debug.ps1` does not regen). Spray path is D172 (draws, wrong colour) — total absence would be new. See `GRAPHICS-BACKLOG.md` D174. | Observed; first step = BUNKER1 firefight with regenerated sidecars | | D183 | **M-36 Family A ("texture line/pitch shear") is DISPROVEN for the Surface repro (`-level_36`).** The importers' pitch assumption is real but never fires there; the Surface cliff "grey static" is not a shear. Full evidence + what the walls actually are: §F "D183" below. A defensive, provably-no-op de-stride landed in `import_texture()` anyway (covers the `gfx_dp_load_tile` case the `SUPPORT_CHECK`s assert against), plus `GE_DTEX` STRIDED marker + `GE_TEXRAW` raw-source dump. | Hypothesis DISPROVEN + diagnostics shipped; D176(b)/D182(2) still OPEN | | D173 | **Third-person Bond model spawns too high** (user QA report): the player-representing figure at level start floats well above the ground "a lot of the time" while the actual player spawn is correct; same on the Cuba end credits — JB ~6 ft in the air above Natalya. D75 animated-model family but a *position* defect, not absence (M-33: gun-barrel Bond renders fine). Suspects: intro/credits puppet chr spawn position vs model base transform; anim root-motion accumulation (`modelSetAnimFrame2WithChrStuff`, cf. D156); packed-float `coord3d` decode in intro setup. "Player spawning OK" localises it to the render-side model, not `g_CurrentPlayer`. See `GRAPHICS-BACKLOG.md` D173. | Observed, not investigated | From 65b7516434d8edff8978e89ca1d04ff0a105b2e4 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:37:49 -0400 Subject: [PATCH 8/8] docs(D172): add GE_D172 to GE-ENV-PROBES.md (validate gate) --- docs/dev/GE-ENV-PROBES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index 543504d1..4ee525cd 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -45,6 +45,7 @@ Two classes: | `GE_D193` | `src/game/frametiming.c` (`waitForNextFrame`, `#ifdef PORT`) | Per-wall-second line: `render=` rendered frames, `sim=` sim ticks delivered (Σ of the `deltaFrames`/`g_ClockTimer` fed downstream), `clamped=` frames whose catch-up hit `FRAMETIMING_PORT_MAX_CATCHUP`, `maxraw=` largest pre-clamp delta. Healthy steady state = `render=30 sim=60 clamped=0 maxraw=2` (measured M-80, all levels idle). Use to check whether the wall-clock timer under-advances under real AI load (D193). Cheap; safe for a full playtest. | **live** (D193) | | `GE_D193A` | `src/game/chr.c` (`chrTick` → `d193aSample`, plus `d193aMoved` in `chrUpdateAnim`; `#ifdef PORT`) | Per-wall-second per-ticked-chr locomotion telemetry (D193/D209): `chr=` chrnum, `act=` ACT_TYPE, `speed=` world-units travelled/s, `gtd=` g_GlobalTimerDelta, `tier=` `act_gopos.unk59` SPEED tier (0 walk / 1 run / 2 sprint; `-1` when not ACT_GOPOS), `gspd=` `act_gopos.speed` (**a turn rate, not a travel rate** — 0 means running straight), `typ=` PROP_TYPE (3=CHR, 6=VIEWER), `tick=`/`move=`/`frz=` chrTick entries vs `chrUpdateAnim` calls vs freeze-gated (move « tick = the chr is being visibility-culled out of its own locomotion; `move=0` at high `speed` = WAYMODE_MAGIC), `hid=` CHRFLAG_HIDDEN, then the root-motion scalars `scale` / `xlscale` / `animrate` / `playspeed` / `mspeed` (model->speed) / `endframe` / `anim` ptr / `aidx=` that pointer reverse-resolved to its `animation_table_ptrs1` index (40 walking, 41 sprinting, 42 running, 107 walking_unarmed — map via `src/game/initanitable.c`). `GE_D193A_CHR=` restricts to one chrnum. **This probe found D209**: Cradle showed `tier=1` (RUN) with `aidx=40/107` (walk anims) on every sample. | **live** (D193/D209) | | `GE_D193B` | `src/game/model.c` (`sub_GAME_7F06D3F4`, `#ifdef PORT`) | Rate-limited (1/97) raw anim root-motion decode trace (D193): `joint=` / `flip=` / `base=` mtx channel index / `frame=` / `tmp=` decoded s16 translation triple from the anim bitstream / `angle=` / `anim` ptr / `bd=`+`bs=` the D32 u32 bitDescriptors/bitStream addresses / `njoints`. M-80 Facility: `base=0`, `angle=0` **always** (root-motion facing delta never non-zero — top lead), `tmp` ≈ (±small, ~1086 hip-height, ±small). Compare `tmp`/`angle` magnitudes against a clean N64 decomp build of the same anim+frame. | **live** (D193) | +| `GE_D172` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1` — SETCOMBINE dedup log + non-LOD multitex tri dump; `#ifdef PORT`) | Particle magenta/cyan colour (D172). Logs each distinct SETCOMBINE + active cycletype, and — for a 2-cycle tri that consumes TEXEL1 with `tex_lod=0` and a distinct 2nd tile — the tile fast3d *samples* for each texunit vs what the DL *configured*. **Found D172**: 40/40 particle tris configured tile 1 @ tmem 392 (fire) but sampled tile 0 (smoke) → `smoke*smoke`. FIXED (`gfx_lod_tile_offset` `return rdp.tex_lod ? 0 : i`). Probe kept inert. | **live** (D172 FIXED, probe kept) | | `GE_DTEX` | `port/fast3d/gfx_pc.cpp:1001` | Per-`import_texture` param log (dims / line / size / lod / gen_mipmaps). Used to root-cause RC2 and D159. Kept as an inert diagnostic. | **live** (RC2/RC3 not finished) | | `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp:1020`, `port/fast3d/gfx_opengl.cpp:700` | PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` line. Root-caused D161 (Depot ceiling). Kept as an inert diagnostic. | **live** (RC3 not finished) | | `GE_D116` | `port/fast3d/gfx_pc.cpp:1710,2362`, `src/game/textrelated.c:265,541` (cached `ge_d116`) | HUD/text "X-mirror" (D114/D116) vbo/uv trace. **D114/D116 CLOSED — NOT A BUG (M-33/D168): the captures were upside-down, not mirrored.** Probe is now dead scaffolding — strip on the next cleanup pass. | dead (D114/D116 closed) |