Files
goldeneye-pc-port/tools_pc/gen_env_probes.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

102 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Drift check for docs/dev/GE-ENV-PROBES.md (speed-ups plan Step 4 / R9).
That doc is a hand-curated table of every `GE_*` env probe -- var, file:line,
what it does, live/dead. It was last snapshotted by a manual grep and drifts
as probes are added/removed. This script does NOT regenerate the prose (the
curation is the value); it re-greps the live `getenv("GE_...")` sites and
reports:
* NEW -- a var with live sites that the doc never names
* GONE -- a var the doc names that has no live site any more
* a fresh var -> file:line site map (paste into the doc's File:line cells)
Usage:
python tools_pc/gen_env_probes.py # print the drift report
python tools_pc/gen_env_probes.py --check # exit 1 if NEW or GONE
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DOC = ROOT / "docs" / "dev" / "GE-ENV-PROBES.md"
SCAN_DIRS = ["src", "port"]
GETENV_RE = re.compile(r'getenv\(\s*"(GE_[A-Z0-9_]+)"\s*\)')
def live_sites() -> dict[str, list[str]]:
sites: dict[str, list[str]] = {}
for d in SCAN_DIRS:
for path in sorted((ROOT / d).rglob("*")):
if path.suffix not in {".c", ".cpp", ".h", ".def", ".inc"}:
continue
try:
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
except OSError:
continue
for n, line in enumerate(lines, 1):
for m in GETENV_RE.finditer(line):
rel = path.relative_to(ROOT).as_posix()
sites.setdefault(m.group(1), []).append(f"{rel}:{n}")
return sites
def doc_vars() -> tuple[set[str], set[str]]:
"""Return (all vars named in the doc, vars whose row is an intentional tombstone)."""
if not DOC.exists():
return set(), set()
named, tombstone = set(), set()
for line in DOC.read_text(encoding="utf-8").splitlines():
# `GE_FOO[` in prose is a probe's LOG-LINE TAG, not an env var -- several
# probes print a tag that is not the name of any getenv() (e.g.
# GE_TEXDUMP_ADDR prints "GE_TEXA[...]"). Counting those as documented
# vars reports a permanent phantom GONE. A row for a real var always
# names it plainly in the table's first cell, so it is still picked up.
vs = [m.group(1) for m in re.finditer(r"(GE_[A-Z0-9_]+)(\[?)", line)
if not m.group(2)]
named.update(vs)
if "tombstone" in line.lower():
tombstone.update(vs)
return named, tombstone
def main() -> int:
check = "--check" in sys.argv[1:]
sites = live_sites()
documented, tombstoned = doc_vars()
live = set(sites)
new = sorted(live - documented)
gone = sorted(v for v in documented - live if v not in ({"GE_DETERM"} | tombstoned))
out = []
out.append(f"# GE_* probe drift vs {DOC.relative_to(ROOT).as_posix()}")
out.append(f"# {len(live)} vars with live sites, {len(documented)} named in the doc\n")
if new:
out.append("## NEW -- live sites, not in the doc (add a row):")
for v in new:
out.append(f" {v}")
for s in sites[v]:
out.append(f" {s}")
if gone:
out.append("\n## GONE -- named in the doc, no live site (mark removed):")
out += [f" {v}" for v in gone]
if not new and not gone:
out.append("doc is in sync with live getenv sites (site line numbers still drift -- see map below)")
out.append("\n## full live site map:")
for v in sorted(sites):
out.append(f" {v}: " + ", ".join(sites[v]))
print("\n".join(out))
if check and (new or gone):
sys.stderr.write(f"\nGE-ENV-PROBES.md drift: {len(new)} new, {len(gone)} gone\n")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())