forked from Ports/goldeneye-pc-port
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.
251 lines
9.5 KiB
Python
251 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
"""D236 pass 22: offline census of a level's bg room display lists.
|
|
|
|
Decodes `bg/bg_<level>_all_p.seg` straight out of the ROM and walks every
|
|
room's primary and secondary GDL, reporting the distinct render-mode
|
|
(`G_SETOTHERMODE_L`, i.e. the `oml` words the live `GE_D2xx` probes log) and
|
|
texture-image classes each room draws. This answers "which bg draw class
|
|
carries the trees, and which room draws it" without building or running the
|
|
game.
|
|
|
|
Pipeline (mirrors `bg.c` exactly):
|
|
* the .seg file is stored UNCOMPRESSED in ROM (obLoadBGFileBytesAtOffset
|
|
random-accesses it by byte offset); only the per-room blobs inside it are
|
|
deflated;
|
|
* word[1] of the decompressed file is the segment offset of the
|
|
`bg_room_data` array (`bg.c:865`), 24 bytes/record:
|
|
u32 pPointTableBin, u32 pPriMappingBin, u32 pSecMappingBin, coord3d pos;
|
|
* a room's compressed DL size is the distance to the *next* non-zero
|
|
mapping-bin offset, taking primary and secondary as one interleaved
|
|
stream (`bg.c:944-990` + `getPri/SecMappingBinCount`);
|
|
* each blob is itself raw-deflate behind its own 2-byte header
|
|
(`bgDecompress` -> `decompressdata`).
|
|
|
|
The words reported are the ones **as authored in the file**, i.e. BEFORE
|
|
`bgApplyDynamicCCRMLUT` rewrites them at room load. The `->` column shows
|
|
what the applied LUT turns each one into, so the output can be compared
|
|
directly against what the runtime probes log. Only LUTs 1/5/6/7 are ever
|
|
applied (`bg.c:2797/2804/2812/2819`); see D236 pass 21.
|
|
|
|
Usage: python tools_pc/bg_gdl_census.py [sevx] [ntsc-final]
|
|
"""
|
|
import collections
|
|
import contextlib
|
|
import io
|
|
import os
|
|
import struct
|
|
import sys
|
|
import zlib
|
|
|
|
import rendermode
|
|
import texheader
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
sys.path.insert(0, os.path.join(ROOT, "tools_pc"))
|
|
|
|
G_SETOTHERMODE_L = 0xB9
|
|
G_SETTIMG = 0xFD
|
|
G_NOOP = 0xC0
|
|
G_ENDDL = 0xB8
|
|
G_DL = 0xDE
|
|
# gsDPSetRenderMode's w0: SETOTHERMODE_L, shift 3, length 29 -> the render-mode
|
|
# field. Other 0xB9 commands (alpha compare, z source) use different shifts
|
|
# and are counted separately.
|
|
RENDERMODE_W0 = 0xB900031D
|
|
|
|
IM_FMT = {0: "RGBA", 1: "YUV", 2: "CI", 3: "IA", 4: "I"}
|
|
IM_SIZ = {0: "4b", 1: "8b", 2: "16b", 3: "32b"}
|
|
|
|
|
|
def load_rom_file(name, region):
|
|
"""Decompress one ROM file table entry by name."""
|
|
sys.argv = ["", region]
|
|
buf = io.StringIO()
|
|
with contextlib.redirect_stdout(buf):
|
|
import d43_emit as m
|
|
key, row = m.find_row(name)
|
|
if not key:
|
|
raise SystemExit("not in ROM file table: %s" % name)
|
|
addr, size = row
|
|
raw = m.rom[addr:addr + size]
|
|
# Model files are raw-deflate behind a 2-byte 0x1172 header, but the bg
|
|
# .seg files are stored UNCOMPRESSED in ROM -- they have to be, because
|
|
# obLoadBGFileBytesAtOffset() random-accesses them by byte offset to pull
|
|
# one room's blob at a time. Only the per-room blobs inside are deflated.
|
|
try:
|
|
return zlib.decompress(raw[2:], -15)
|
|
except zlib.error:
|
|
return raw
|
|
|
|
|
|
def seg_off(v):
|
|
"""Segment-0x0F address -> file offset (bg.c's BG_SEG_TO_PTR)."""
|
|
return v & 0x00FFFFFF
|
|
|
|
|
|
def read_rooms(data):
|
|
"""Parse the bg_room_data array: [(point, pri, sec, pos), ...]."""
|
|
list_off = seg_off(struct.unpack_from(">I", data, 4)[0])
|
|
rooms = []
|
|
off = list_off
|
|
while off + 24 <= len(data):
|
|
point, pri, sec = struct.unpack_from(">III", data, off)
|
|
pos = struct.unpack_from(">fff", data, off + 12)
|
|
rooms.append((point, pri, sec, pos))
|
|
# The array is terminated by the sentinel record whose pPriMappingBin
|
|
# is 0 AND which is not just an interior room with no primary DL, so
|
|
# walk until all three offsets are zero.
|
|
if len(rooms) > 1 and point == 0 and pri == 0 and sec == 0:
|
|
break
|
|
off += 24
|
|
return rooms, list_off
|
|
|
|
|
|
def next_nonzero(rooms, start, field):
|
|
i = start
|
|
while i < len(rooms) and rooms[i][field] == 0:
|
|
i += 1
|
|
return i if i < len(rooms) else None
|
|
|
|
|
|
def blob_sizes(rooms, i):
|
|
"""csize_primary / csize_secondary for room i, per bg.c:944-990."""
|
|
_point, pri, sec, _pos = rooms[i]
|
|
csz_pri = csz_sec = 0
|
|
if pri:
|
|
pidx = next_nonzero(rooms, i + 1, 1)
|
|
sidx = next_nonzero(rooms, i, 2)
|
|
if pidx is not None and (sidx is None or pidx <= sidx):
|
|
csz_pri = seg_off(rooms[pidx][1]) - seg_off(pri)
|
|
elif sidx is not None:
|
|
csz_pri = seg_off(rooms[sidx][2]) - seg_off(pri)
|
|
if sec:
|
|
pidx = next_nonzero(rooms, i + 1, 1)
|
|
sidx = next_nonzero(rooms, i + 1, 2)
|
|
if pidx is not None and (sidx is None or pidx <= sidx):
|
|
csz_sec = seg_off(rooms[pidx][1]) - seg_off(sec)
|
|
elif sidx is not None:
|
|
csz_sec = seg_off(rooms[sidx][2]) - seg_off(sec)
|
|
return csz_pri, csz_sec
|
|
|
|
|
|
def inflate_blob(data, off, size):
|
|
if size <= 2:
|
|
return None
|
|
try:
|
|
return zlib.decompress(data[off + 2:off + size], -15)
|
|
except zlib.error as e:
|
|
return ("ERR", str(e))
|
|
|
|
|
|
def walk_gdl(gdl):
|
|
"""Walk an 8-byte big-endian N64 Gfx stream.
|
|
|
|
Returns (rendermodes, textures, opcodes, ncmd) where rendermodes maps an
|
|
oml word to how many draw-setup commands used it, and textures maps
|
|
(fmt, siz) to the set of image addresses set while it was active.
|
|
"""
|
|
rms = collections.Counter()
|
|
texs = collections.Counter()
|
|
ops = collections.Counter()
|
|
pairs = collections.Counter()
|
|
cur_rm = None
|
|
n = len(gdl) // 8
|
|
for k in range(n):
|
|
w0, w1 = struct.unpack_from(">II", gdl, k * 8)
|
|
op = w0 >> 24
|
|
ops[op] += 1
|
|
if op == G_SETOTHERMODE_L and w0 == RENDERMODE_W0:
|
|
cur_rm = w1
|
|
rms[w1] += 1
|
|
elif op == G_NOOP:
|
|
# bg room DLs never carry G_SETTIMG -- they reference textures by
|
|
# NUMBER in a G_NOOP, which texLoadFromGdl() resolves into real
|
|
# texture commands at room load (tex.c:779, `case G_NOOP:
|
|
# texnum = in->words.w1 & 0xfff`).
|
|
texs[w1 & 0xFFF] += 1
|
|
pairs[(cur_rm, w1 & 0xFFF)] += 1
|
|
return rms, texs, ops, pairs, n
|
|
|
|
|
|
def report(level, region):
|
|
name = "bg_%s_all_p" % level # file-table basename (no dir/.seg)
|
|
data = load_rom_file(name, region)
|
|
print("%s: %d bytes decompressed" % (name, len(data)))
|
|
rooms, list_off = read_rooms(data)
|
|
print("room table at file offset 0x%x, %d records\n" % (list_off, len(rooms)))
|
|
|
|
all_rms = collections.Counter()
|
|
all_pairs = collections.Counter()
|
|
rm_rooms = collections.defaultdict(set)
|
|
tex_rooms = collections.defaultdict(set)
|
|
bad = 0
|
|
for i in range(1, len(rooms)):
|
|
csz_pri, csz_sec = blob_sizes(rooms, i)
|
|
for label, off_raw, csz in (("pri", rooms[i][1], csz_pri),
|
|
("sec", rooms[i][2], csz_sec)):
|
|
if not off_raw or csz <= 0:
|
|
continue
|
|
out = inflate_blob(data, seg_off(off_raw), csz)
|
|
if out is None:
|
|
continue
|
|
if isinstance(out, tuple):
|
|
bad += 1
|
|
print("room %3d %s: INFLATE FAILED at 0x%x size %d (%s)"
|
|
% (i, label, seg_off(off_raw), csz, out[1]))
|
|
continue
|
|
rms, texs, ops, pairs, ncmd = walk_gdl(out)
|
|
all_rms.update(rms)
|
|
all_pairs.update(pairs)
|
|
for w in rms:
|
|
rm_rooms[w].add(i)
|
|
for t in texs:
|
|
tex_rooms[t].add(i)
|
|
print("room %3d %s: csize=%-6d usize=%-6d cmds=%-5d rm=%d tex=%d %s"
|
|
% (i, label, csz, len(out), ncmd, len(rms), sum(texs.values()),
|
|
" ".join("0x%08x" % w for w in sorted(rms))))
|
|
|
|
print("\n%s" % ("=" * 78))
|
|
rm = rendermode.RenderModes()
|
|
luts = rendermode.applied_luts(rm)
|
|
print("RENDER-MODE CENSUS (as authored in the file, pre-LUT)")
|
|
print(" the '->' column is what bgApplyDynamicCCRMLUT rewrites the word to at")
|
|
print(" room load when fog is on (LUT 1 primary / LUT 5 secondary) -- i.e. the")
|
|
print(" value a live GE_D2xx probe would log. With fog OFF, LUTs 6/7 rewrite")
|
|
print(" COMBINE modes only, so render modes stay exactly as authored.")
|
|
print("=" * 78)
|
|
for w, n in all_rms.most_common():
|
|
rr = sorted(rm_rooms[w])
|
|
to = luts.get(1, {}).get(w) or luts.get(5, {}).get(w)
|
|
print(" 0x%08x -> %-12s %4d setups %3d rooms rooms=%s"
|
|
% (w, ("0x%08x" % to) if to else "(unchanged)", n, len(rr),
|
|
",".join(map(str, rr[:14])) + ("..." if len(rr) > 14 else "")))
|
|
print(" %s" % " | ".join(rm.decode(w)["flags"]))
|
|
|
|
print("\n%s" % ("=" * 78))
|
|
print("TEXTURES PER RENDER-MODE CLASS (texture numbers from G_NOOP w1)")
|
|
print(" ROM header decoded offline; 'live' is what a runtime probe prints.")
|
|
print("=" * 78)
|
|
rom, trows = texheader.load(region)
|
|
bytex = collections.defaultdict(collections.Counter)
|
|
for (w, t), _n in all_pairs.items():
|
|
bytex[w][t] = len(tex_rooms[t])
|
|
for w, _n in all_rms.most_common():
|
|
print(" rm=0x%08x" % w)
|
|
for t, nrooms in bytex[w].most_common():
|
|
try:
|
|
desc = texheader.describe(t, rom, trows)
|
|
except Exception:
|
|
desc = "tex%-5d (not in image list)" % t
|
|
print(" %2d rooms %s" % (nrooms, desc))
|
|
|
|
if bad:
|
|
print("\n%d blob(s) failed to inflate -- size derivation may be off for those."
|
|
% bad)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
lvl = sys.argv[1] if len(sys.argv) > 1 else "sevx"
|
|
reg = sys.argv[2] if len(sys.argv) > 2 else "ntsc-final"
|
|
report(lvl, reg)
|