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).
This commit is contained in:
James Dansereau committed 2026-09-08 23:12:09 -04:00
1 parent 809560ca1e
commit 9215fde4fb
3 files changed
+95 -34

No files matched your search

+53 -33
View File
@@ -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
+1 -1
View File
@@ -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). |
+41
View File
@@ -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)),