Merge pull request #34 from jkdansereau/docs/d172-0xb9-withdrawn

fix(D172): particle magenta/cyan — gfx_lod_tile_offset collapses non-LOD 2-texture combines
This commit is contained in:
James Dansereau authored and GitHub committed 2026-09-08 23:40:52 -04:00
commit 0b2da7bc2d
6 files changed
+177 -39

No files matched your search

+106 -36
View File
@@ -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)
@@ -45,50 +70,92 @@ 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
<particles> -> drawn with STALE combine mode + stale tile/timg state
Decode of the record word `w0 = 0xB900031D`, `w1 = <varies>`:
`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.
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. Cycle-type is ALSO ruled out — the real record, and where the bug is (M-82)
The actual particle records are in **`assets/oddtextures.c`**
(`globalDL_0x078` … , 17 of them). One record verbatim:
```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(),
```
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 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.
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.
**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.
## 5. Verification (cheap, in-game)
### Probes shipped (M-82, `#ifdef PORT`, `GE_D172=1`, inert when unset)
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.)
- `gfx_pc.cpp` G_SETCOMBINE case — logs each distinct combine + active
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)
## 6. Fix path (next session — needs a display)
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. 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
@@ -98,3 +165,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.
+1
View File
@@ -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=<n>` 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) |
+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). | **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: 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). |
+1
View File
@@ -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"
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
45 D175
46 D176
47 D177
48 D172
49 D174
50 D183
51 D173
+1
View File
@@ -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 |
+67 -2
View File
@@ -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) {
@@ -1691,6 +1700,40 @@ 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] && !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);
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
struct ShaderProgram* prg = comb->prg[tm];
if (prg == NULL) {
comb->prg[tm] = prg =
@@ -2784,6 +2827,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)),