diff --git a/docs/TEXTURE-GLITCH-ANALYSIS.md b/docs/TEXTURE-GLITCH-ANALYSIS.md index 404c3927..e48bad55 100644 --- a/docs/TEXTURE-GLITCH-ANALYSIS.md +++ b/docs/TEXTURE-GLITCH-ANALYSIS.md @@ -63,19 +63,24 @@ `G_TX_WRAP == 0` (`include/PR/gbi.h:391`) — dead code (already parked in `docs/GRAPHICS-BACKLOG.md`). -### RC4 — Palette off-by-one (subtle global hue shift) +### RC4 — Palette off-by-one — **RETRACTED (M-30): the current code is correct.** -`port/fast3d/gfx_pc.cpp:835`: +The "N64 RGBA16 spec" line below is **ARGB1555**, which is *not* what the N64 uses. +N64 `G_IM_FMT_RGBA` / `G_IM_SIZ_16b` is **RGBA5551**: `RRRRR GGGGG BBBBB A` +(R = bits 15-11, G = 10-6, B = 5-1, A = bit 0). ```c -// fast3d (current): r = v>>11; g = (v>>6)&0x1f; b = (v>>1)&0x1f; a = v&1 -// N64 RGBA16 spec: r = (v>>10)&0x1f; g = (v>>5)&0x1f; b = v&0x1f; a = v>>15 +// fast3d (current, CORRECT): r = v>>11; g = (v>>6)&0x1f; b = (v>>1)&0x1f; a = v&1 +// the "spec" line here was wrong: r = (v>>10)&0x1f; g = (v>>5)&0x1f; b = v&0x1f; a = v>>15 ``` -Channels are read one bit off (alpha treated as LSB). Grays (r≈g≈b) survive nearly intact — -which is why BUNKER1 looks "mostly right" (it's gray/brown) — but saturated hues shift. -Format mapping itself was verified correct: `TEXFORMAT_RGBA16_CI8/CI4` → `G_TT_RGBA16`, -`IA16_CI8/CI4` → `G_TT_IA16` (`src/game/image.c:71–104`). +Both `palette_to_rgba32` (`gfx_pc.cpp:~835`) and `import_texture_rgba16` +(`~651`) use the same RGBA5551 decode and agree with each other. The palette is +`PD_BE16`-swapped at load (`gfx_dp_load_tlut`), so `palentry` is already +correctly ordered. **Applying the "fix" would rotate every channel and move +alpha to the wrong bit — do not do it.** If a hue problem is ever confirmed +in-game, it is upstream (TLUT byte order at the converter, or a wrong +`rdp.palette_fmt`), not this function. ## 3. Depot-specific analysis ("why blue ground, only there?") diff --git a/tools_pc/d43_emit.py b/tools_pc/d43_emit.py index 5ee5b44e..35a6962b 100644 --- a/tools_pc/d43_emit.py +++ b/tools_pc/d43_emit.py @@ -341,6 +341,7 @@ def process(name): vtx_regions.append((cvo, ncv)); dstpos = add_region(cvo, 16 * ncv, dstpos) if nv and puo: dstpos = add_region(puo, 2 * nv, dstpos) + op24_pointusage[puo] = nv elif op == 22: nv = struct.unpack_from(">i", src, data)[0]; vo = be32o(src, data + 4) if nv and vo: @@ -629,6 +630,16 @@ def process(name): else: emit_main_vtx(vo, nv, npos) + # D120: PointUsage[] -- an `s16` next-index chain (numVertices entries, + # negative-terminated). Plain byteswap, no remap. + for puo, punv in op24_pointusage.items(): + pnpos = remap(puo) + if pnpos is None: + errors.append(f"{name}: PointUsage {puo:#x} not in region map") + continue + for k in range(punv): + put_s16(pnpos + 2 * k, be16(src, puo + 2 * k)) + # GDLs (pack order: unvisited first, then visit order), 16B LE slots for g in all_gdls: end = gdl_end(src, D, g) @@ -675,6 +686,11 @@ def process(name): # collision-vertex set (filled during layout; emit pass needs it) op24_is_collision = {} +# D120: opcode-0x18 PointUsage arrays -> {old_off: numVertices}. Reserved in +# layout (add_region) but the emit pass must byteswap the s16 index chain or the +# runtime blood-decal walk (chr.c PointUsage[index]) cycles (guarded, not fixed, +# until now). +op24_pointusage = {} def validate(name, src, D, NS, NT, nodes, R0, placed, all_gdls, buf, D_PC, regions, node_newoff, rec_newoff, gdl_newoff, remap, inv_remap): @@ -764,6 +780,7 @@ stats["table_models"] = len(model_names) for name in model_names: # reset per-file collision set before layout (populated below) op24_is_collision.clear() + op24_pointusage.clear() process(name) if errors: