fast3d/D217: key CI texture cache on palette content, not just TLUT address (#49)

GE assembles weapon and character model display lists in scratch arena RAM
and reissues gDPLoadTLUT from a repeated source address with different
palette *content* between materials and frames. The CI TextureCacheKey
keyed only on {texel addr, palette_addrs[0/1], palette_index, size_bytes}
with no content check, so gfx_texture_cache_lookup() (a plain map.find)
could return a stale HIT whose GL texture was decoded against a different
palette. Proven with an env-gated probe on -level_34: the same
{addr, paladdr} pair re-imported many times per frame with materially
different rdp.palette content, all taking HITs.

Fix: FNV-1a hash of the 512-byte rdp.palette, recomputed only in
gfx_dp_load_tlut() (rare vs draws -> no per-texel or per-draw cost), added
as a trailing field of the CI cache key. Non-CI keys carry 0. Cannot
produce a worse decode than the stale HIT it replaces; worst case is a few
extra texture uploads per frame (~2 measured on -level_34).

Partial for D217: this removes a real stale-palette-HIT class (the D217
finding's own "test a palette-content hash" next-step) but does NOT resolve
the visible green PP7 grip. That has an upstream cause - the grip's count=1
model TLUT is fed different source bytes per level (Bunker 0x0001 -> black,
Facility 0x1A00 -> green) for the same tile; fast3d faithfully stores what
it is given. See scratchpad D217-report.md. Full 21-level world-CI
regression sweep still owed before merge.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
This commit is contained in:
James Dansereau authored and GitHub committed 2026-09-11 18:17:29 -04:00
1 parent 50d73c519c
commit f800275247
2 files changed
+29 -2

No files matched your search

+20 -2
View File
@@ -156,6 +156,7 @@ static struct RDP {
uint16_t palette[256];
const uint8_t* palette_addrs[2];
uint32_t palette_fmt;
uint32_t palette_hash; /* D217: FNV-1a of palette[], refreshed in gfx_dp_load_tlut */
struct {
const uint8_t* addr;
uint8_t siz;
@@ -988,9 +989,9 @@ static void import_texture(int i, int tile, bool importReplacement) {
TextureCacheKey key;
if (fmt == G_IM_FMT_CI) {
key = { orig_addr, { rdp.palette_addrs[0], rdp.palette_addrs[1] }, fmt, siz, palette_index,
loaded_texture.size_bytes };
loaded_texture.size_bytes, rdp.palette_hash }; // D217: key on palette content
} else {
key = { orig_addr, {}, fmt, siz, palette_index, loaded_texture.size_bytes };
key = { orig_addr, {}, fmt, siz, palette_index, loaded_texture.size_bytes, 0u };
}
if (gfx_texture_cache_lookup(i, key)) {
@@ -2233,6 +2234,23 @@ static void gfx_dp_load_tlut(uint8_t tile, uint32_t uls, uint32_t ult, uint32_t
*dst++ = PD_BE16(*src++);
}
/* D217: refresh the palette-content hash that keys the CI texture cache.
* GE reissues gDPLoadTLUT from a repeated scratch source address with
* different content between weapon / character model materials; the CI
* TextureCacheKey keys on the source *address*, so without a content hash a
* later material can take a stale cache HIT decoded against an earlier
* palette. gDPLoadTLUT is rare relative to draws, so hashing the whole
* 512-byte table here keeps the per-texel path untouched. */
{
uint32_t h = 2166136261u;
const uint8_t *pb = (const uint8_t *)rdp.palette;
for (uint32_t k = 0; k < sizeof(rdp.palette); ++k) {
h ^= pb[k];
h *= 16777619u;
}
rdp.palette_hash = h;
}
rdp.textures_changed[0] = rdp.textures_changed[1] = true;
}