Files
goldeneye-pc-port/tools_pc/rendermode.py
T
James Dansereau 425237f93f D236 (M-198): fix Surface 1/2 tree cards; root cause was a DETAIL tile binding
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.
2026-09-21 22:48:10 -04:00

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)))