mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
GE's texLoadFromGdl picks a handler per G_NOOP texture reference from w0 & 7. TEXTURETYPE_LOD/MIPMAP (texHandleType0/Type2) issue ONE load block at TMEM 0, so tiles 0..n are LOD levels of that one image. TEXTURETYPE_DETAIL (texHandleType1, src/game/tex.c) issues TWO: the detail texture at TMEM 0 and the real base image at TMEM texGetSizeInBytes(detail,0) on tiles 1+. gfx_lod_tile_offset folded every G_TL_LOD draw to tile 0 (D107 -- correct for the first two, wrong for DETAIL), so the port drew the detail texture and never imported the base image at all. All four Surface 1 treeline cards (tex1198-tex1201, RGBA5551 64x17 cut-outs) are DETAIL bindings sharing one detail texture, tex2465: an opaque 32x32 IA8 noise tile. That is the same texture D267/D276 measured at 1023/1024 texels opaque and called the mystery "noise painter" -- hence 20+ passes chasing decode, alpha, mip and geometry theories for what was a binding bug. Fix (port layer only): under G_TL_LOD, if tile fi+1 differs from tile fi in BOTH TMEM address and fmt/siz, sample tile 1. Asymmetric on purpose -- LOD levels of one image always share a format, so a mip chain can never trip it, and a detail pair that happened to share a format just keeps the old behaviour. Config Video.DetailBaseTile, default on. Verified: Surface 1 idle spawn goes from an opaque tiled noise wall to discrete snow-covered pines with ground/snow unchanged; Surface 2 shows a discrete night treeline; Depot renders clean; bunker1 golden framediff is bit-identical with the fix on and off. Offline scan of every level's bg: DETAIL bindings exist only in sevx (84), depo (39), sevb (4), sev (4), so every other level is provably untouched. User play-test confirmed. Also refuted and removed pass 25's fix direction: cut-out mip suppression changed nothing, and its alpha census showed the wall is painted by a fully opaque texture -- retiring the mip / 1-bit-alpha / XLU-smear branch. The GE_D236P25 probe is gone with it. tools_pc/gen_env_probes.py: stop counting a "GE_FOO[" log-line TAG in the doc as a documented env var. GE_TEXDUMP_ADDR prints "GE_TEXA[...]", which is not the name of any getenv(), so the check reported a permanent phantom GONE. Generalised as docs/porting-notes.md D12; full trail in findings.md D236 pass 26. Carries the pending D75/M-197 sidecar-converter fix and the env-gated D236/D75 probes, which the doc gates require to land alongside their rows.
226 lines
8.5 KiB
Python
226 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""D236 pass 22: evaluate N64 render-mode words offline, straight from the repo.
|
|
|
|
Two jobs:
|
|
|
|
1. `decode(word)` -- turn a raw `G_SETOTHERMODE_L` value (the `oml` the live
|
|
`GE_D2xx` probes print) into the flag names and blender equation it stands
|
|
for, and name it if it matches a `G_RM_*` composite in `include/PR/gbi.h`.
|
|
|
|
2. `applied_luts()` -- evaluate the `DL_LUT_*` tables in `src/game/bg.c` into
|
|
plain (from -> to) word maps, for the four LUT indices `bg.c` actually
|
|
applies at room load (1/5/6/7). This is what lets a *static* census of a
|
|
bg file's authored render modes be compared against the words a *live*
|
|
probe logs, which are the post-rewrite ones.
|
|
|
|
Everything is parsed from the real headers/sources rather than hardcoded, so
|
|
it cannot drift from the build. No C compiler required.
|
|
"""
|
|
import os
|
|
import re
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
GBI_H = os.path.join(ROOT, "include", "PR", "gbi.h")
|
|
BG_C = os.path.join(ROOT, "src", "game", "bg.c")
|
|
|
|
# Order matters for readable decoding: mask-style fields first.
|
|
_FIELDS = [
|
|
("CVG_DST", 0x300, {0x000: "CVG_DST_CLAMP", 0x100: "CVG_DST_WRAP",
|
|
0x200: "CVG_DST_FULL", 0x300: "CVG_DST_SAVE"}),
|
|
("ZMODE", 0xC00, {0x000: "ZMODE_OPA", 0x400: "ZMODE_INTER",
|
|
0x800: "ZMODE_XLU", 0xC00: "ZMODE_DEC"}),
|
|
]
|
|
_BITS = [(0x8, "AA_EN"), (0x10, "Z_CMP"), (0x20, "Z_UPD"), (0x40, "IM_RD"),
|
|
(0x80, "CLR_ON_CVG"), (0x1000, "CVG_X_ALPHA"),
|
|
(0x2000, "ALPHA_CVG_SEL"), (0x4000, "FORCE_BL")]
|
|
|
|
_BL_P = {0: "CLR_IN", 1: "CLR_MEM", 2: "CLR_BL", 3: "CLR_FOG"}
|
|
_BL_A = {0: "A_IN/1MA", 1: "A_FOG/A_MEM", 2: "A_SHADE/1", 3: "0"}
|
|
|
|
|
|
def _flag_defines():
|
|
"""Simple `#define NAME <int literal>` pairs from gbi.h."""
|
|
out = {}
|
|
for line in open(GBI_H, encoding="utf-8", errors="replace"):
|
|
m = re.match(r"#define\s+(\w+)\s+(0x[0-9a-fA-F]+|\d+)\s*(/\*.*)?$", line)
|
|
if m:
|
|
out[m.group(1)] = int(m.group(2), 0)
|
|
return out
|
|
|
|
|
|
def _macro_bodies():
|
|
"""`#define NAME(args) body` and `#define NAME body` with continuations."""
|
|
src = open(GBI_H, encoding="utf-8", errors="replace").read()
|
|
src = re.sub(r"/\*.*?\*/", " ", src, flags=re.S)
|
|
src = src.replace("\\\n", " ")
|
|
fn, obj = {}, {}
|
|
for line in src.split("\n"):
|
|
m = re.match(r"#define\s+(\w+)\(([^)]*)\)\s+(.*)$", line)
|
|
if m:
|
|
fn[m.group(1)] = ([a.strip() for a in m.group(2).split(",")], m.group(3).strip())
|
|
continue
|
|
m = re.match(r"#define\s+(\w+)\s+(.+)$", line)
|
|
if m:
|
|
obj[m.group(1)] = m.group(2).strip()
|
|
return fn, obj
|
|
|
|
|
|
class RenderModes(object):
|
|
def __init__(self):
|
|
self.flags = _flag_defines()
|
|
self.fn, self.obj = _macro_bodies()
|
|
self.by_name = {}
|
|
for name in self.obj:
|
|
if not name.startswith("G_RM_"):
|
|
continue
|
|
try:
|
|
v = self.eval_name(name)
|
|
except Exception:
|
|
continue
|
|
if v is not None:
|
|
self.by_name[name] = v & 0xFFFFFFFF
|
|
self.by_value = {}
|
|
for n, v in self.by_name.items():
|
|
self.by_value.setdefault(v, []).append(n)
|
|
|
|
def _expand(self, text, depth=0):
|
|
if depth > 12:
|
|
raise ValueError("macro recursion")
|
|
# GBL_c1/GBL_c2 are the only function-like macros we need.
|
|
def gbl(m):
|
|
which, args = m.group(1), [a.strip() for a in m.group(2).split(",")]
|
|
vals = [self._value(a) for a in args]
|
|
sh = (30, 26, 22, 18) if which == "1" else (28, 24, 20, 16)
|
|
return "(%d)" % sum(v << s for v, s in zip(vals, sh))
|
|
prev = None
|
|
while prev != text:
|
|
prev = text
|
|
text = re.sub(r"GBL_c([12])\s*\(([^()]*)\)", gbl, text)
|
|
return text
|
|
|
|
def _value(self, tok):
|
|
tok = tok.strip()
|
|
if re.match(r"^(0x[0-9a-fA-F]+|\d+)$", tok):
|
|
return int(tok, 0)
|
|
if tok in self.flags:
|
|
return self.flags[tok]
|
|
if tok in self.obj:
|
|
return self.eval_name(tok)
|
|
raise ValueError("unknown token %r" % tok)
|
|
|
|
def eval_name(self, name, _depth=0):
|
|
if _depth > 12:
|
|
raise ValueError("recursion")
|
|
body = self.obj[name]
|
|
# G_RM_FOO -> RM_FOO(1) / G_RM_FOO2 -> RM_FOO(2)
|
|
m = re.match(r"^(RM_\w+)\((\d)\)$", body)
|
|
if m:
|
|
macro, clk = m.group(1), m.group(2)
|
|
if macro not in self.fn:
|
|
return None
|
|
_args, mbody = self.fn[macro]
|
|
mbody = mbody.replace("GBL_c##clk", "GBL_c%s" % clk)
|
|
return self._eval_expr(mbody)
|
|
return self._eval_expr(body)
|
|
|
|
def _eval_expr(self, expr):
|
|
expr = self._expand(expr)
|
|
expr = re.sub(r"\b([A-Za-z_]\w*)\b",
|
|
lambda m: str(self._value(m.group(1))), expr)
|
|
return eval(expr, {"__builtins__": {}}, {}) & 0xFFFFFFFF # noqa: S307
|
|
|
|
def decode(self, word):
|
|
word &= 0xFFFFFFFF
|
|
low = word & 0xFFFF
|
|
parts = [n for b, n in _BITS if low & b]
|
|
for _label, mask, table in _FIELDS:
|
|
parts.append(table[low & mask])
|
|
c1 = "(%s*%s + %s*%s)" % (_BL_P[(word >> 30) & 3], _BL_A[(word >> 26) & 3],
|
|
_BL_P[(word >> 22) & 3], _BL_A[(word >> 18) & 3])
|
|
c2 = "(%s*%s + %s*%s)" % (_BL_P[(word >> 28) & 3], _BL_A[(word >> 24) & 3],
|
|
_BL_P[(word >> 20) & 3], _BL_A[(word >> 16) & 3])
|
|
names = self.by_value.get(word, [])
|
|
return {"word": word, "flags": parts, "cyc1": c1, "cyc2": c2, "names": names}
|
|
|
|
def name_of(self, word):
|
|
n = self.by_value.get(word & 0xFFFFFFFF, [])
|
|
return "/".join(n) if n else ""
|
|
|
|
|
|
# ---------------------------------------------------------------- bg.c LUTs
|
|
|
|
LUT_NAMES = {1: "DL_LUT_PRIMARY_ADDFOG", 5: "DL_LUT_SECONDARY_ADDFOG",
|
|
6: "DL_LUT_PRIMARY", 7: "DL_LUT_SECONDARY"}
|
|
|
|
|
|
def _lut_entries(body, rm):
|
|
"""Evaluate a DL_LUT_* initializer body into a flat list of (w0, w1)."""
|
|
body = re.sub(r"//[^\n]*", " ", body)
|
|
body = re.sub(r"/\*.*?\*/", " ", body, flags=re.S)
|
|
words = []
|
|
for tok in re.finditer(r"gsDPSetRenderMode\s*\(([^,]+),([^)]+)\)|(0x[0-9a-fA-F]+|\b0\b)",
|
|
body):
|
|
if tok.group(1):
|
|
a = rm._eval_expr(tok.group(1).strip())
|
|
b = rm._eval_expr(tok.group(2).strip())
|
|
words.append((0xB900031D, (a | b) & 0xFFFFFFFF))
|
|
else:
|
|
words.append(("RAW", int(tok.group(3), 0)))
|
|
# Flatten raw scalar runs into (w0, w1) pairs the same way C brace-elision
|
|
# fills Gfx.words.w0/w1.
|
|
out, pend = [], []
|
|
for kind, val in words:
|
|
if kind == "RAW":
|
|
pend.append(val)
|
|
if len(pend) == 2:
|
|
out.append((pend[0], pend[1]))
|
|
pend = []
|
|
else:
|
|
if pend:
|
|
out.append((pend[0], 0))
|
|
pend = []
|
|
out.append((kind, val))
|
|
if pend:
|
|
out.append((pend[0], 0))
|
|
return out
|
|
|
|
|
|
def applied_luts(rm=None):
|
|
"""{lut_index: {from_word: to_word}} for the LUTs bg.c actually applies."""
|
|
rm = rm or RenderModes()
|
|
src = open(BG_C, encoding="utf-8", errors="replace").read()
|
|
maps = {}
|
|
for idx, name in LUT_NAMES.items():
|
|
m = re.search(r"Gfx\s+%s\[\]\s*=\s*\{(.*?)\n\};" % re.escape(name), src, re.S)
|
|
if not m:
|
|
continue
|
|
ents = _lut_entries(m.group(1), rm)
|
|
pairs = {}
|
|
# (match, replacement) pairs, terminated by an entry with w0 == 0.
|
|
i = 0
|
|
while i + 1 < len(ents) and ents[i][0] != 0:
|
|
(aw0, aw1), (bw0, bw1) = ents[i], ents[i + 1]
|
|
if aw0 == 0xB900031D and bw0 == 0xB900031D:
|
|
pairs[aw1] = bw1
|
|
i += 2
|
|
maps[idx] = pairs
|
|
return maps
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
rm = RenderModes()
|
|
if len(sys.argv) > 1:
|
|
for a in sys.argv[1:]:
|
|
d = rm.decode(int(a, 16))
|
|
print("0x%08x %s" % (d["word"], "/".join(d["names"]) or "(no G_RM_* match)"))
|
|
print(" flags: %s" % " | ".join(d["flags"]))
|
|
print(" cyc1 %s cyc2 %s" % (d["cyc1"], d["cyc2"]))
|
|
else:
|
|
print("known G_RM_* composites: %d" % len(rm.by_name))
|
|
for idx, pairs in sorted(applied_luts(rm).items()):
|
|
print("\n%s (index %d): %d rewrite pairs" % (LUT_NAMES[idx], idx, len(pairs)))
|
|
for a, b in sorted(pairs.items()):
|
|
print(" 0x%08x %-26s -> 0x%08x %s"
|
|
% (a, rm.name_of(a), b, rm.name_of(b)))
|