Files
retroDE_ps2/tools/merge_runtime_sched.py
thejayman77 ba74bbd5aa Snapshot: fog implementation + fidelity tooling baseline (pre bilinear-clamp fix)
Per-vertex GS fog end-to-end (gs_stub emit incl. persp_emit5, gs_prim_list_feeder
XYZ2->XYZF2 on PRIM.FGE, gs_make_sh3_scheduler_fixture.py F/FGE packing), new fog
TBs, fidelity attribution tooling. Functional baseline before removing the dead
bilinear lerp8 clamps (Codex: 161-node comb loop -> -0.042ns setup fail).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 19:56:46 -04:00

179 lines
7.2 KiB
Python

#!/usr/bin/env python3
"""Concatenate runtime-CLUT scheduler fixtures without regenerating accepted epochs.
Usage: merge_runtime_sched.py [--reference-inputs] OUTPUT_TAG INPUT_TAG[@START:END] [...]
All inputs must use the same framebuffer and fixed 512x512 cache geometry. The
tool renumbers epochs and emits the descriptor/parameter headers consumed by
the shared RTL testbench and host. By default it copies generated assets under
OUTPUT_TAG. With --reference-inputs, the host descriptor retains the original
asset names (so repeated resident textures are staged only once), while cheap
local hard-link aliases satisfy the shared RTL testbench's epoch-local naming
contract.
"""
import os
import re
import shutil
import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
DATA = os.path.join(ROOT, "sim", "data", "top_psmct32_raster_demo")
def parse_table(tag):
path = os.path.join(DATA, f"sh3_{tag}_epochs.txt")
meta = None
rows = []
with open(path) as f:
for line in f:
if line.startswith("META "):
meta = line.strip()
elif line and line[0].isdigit():
fields = line.split()
if len(fields) != 14:
raise ValueError(f"{path}: expected 14 fields, got {len(fields)}")
rows.append(fields)
if meta is None:
raise ValueError(f"{path}: no META row")
return meta, rows
def parse_spec(spec):
if "@" not in spec:
tag=spec; bounds=None
else:
tag,bounds=spec.rsplit("@",1)
meta,rows=parse_table(tag)
if bounds is not None:
a,b=bounds.split(":",1)
start=int(a) if a else 0; end=int(b) if b else len(rows)
if start<0 or end<start or end>len(rows):
raise ValueError(f"{spec}: invalid row slice for {len(rows)} rows")
rows=rows[start:end]
return tag,meta,rows
def meta_dict(line):
p = line.split()[1:]
return dict(zip(p[0::2], p[1::2]))
def link_or_copy(src, dst):
"""Create a local testbench alias without duplicating large texture files."""
try:
os.unlink(dst)
except FileNotFoundError:
pass
try:
os.link(src, dst)
except OSError:
shutil.copyfile(src, dst)
def write_zero_pal(dst):
"""Satisfy the shared RTL harness for direct-color epochs.
Host descriptors keep '-' so no runtime CLUT upload occurs. The generic
testbench still opens an epoch-local palette filename before TEX0.PSM is
known; a deterministic unused zero table avoids a misleading readmem
warning without changing the fixture contract.
"""
with open(dst, "w") as f:
f.write("// unused direct-color palette placeholder\n")
for _ in range(256):
f.write("00000000\n")
def main(argv):
reference_inputs = "--reference-inputs" in argv
args = [x for x in argv[1:] if x != "--reference-inputs"]
if len(args) < 3:
print(__doc__.strip())
return 2
out, tags = args[0], args[1:]
parsed = [(spec, *parse_spec(spec)) for spec in tags]
metas = [meta_dict(x[2]) for x in parsed]
fixed = ("fbpxw", "fbh", "fbwords", "lpddr_tex", "tex_words", "n_beats")
for key in fixed:
vals = {m[key] for m in metas}
if len(vals) != 1:
raise ValueError(f"input fixtures disagree on {key}: {sorted(vals)}")
out_rows = []
nk = 0
for spec, tag, _meta, rows in parsed:
for old in rows:
oldk = int(old[0])
tex_src = os.path.join(DATA, old[4])
list_src = os.path.join(DATA, old[8])
has_pal = old[12] != "-"
pal_src = os.path.join(DATA, old[12]) if has_pal else None
tex_alias = f"sh3_{out}{nk}_tex_lpddr.mem"
list_alias = f"feeder_sh3_{out}{nk}.mem"
pal_alias = f"sh3_{out}{nk}_pal.mem"
if reference_inputs:
tex_name, list_name, pal_name = old[4], old[8], old[12]
assets = [(tex_src, tex_alias), (list_src, list_alias)]
if has_pal:
assets.append((pal_src, pal_alias))
for src, name in assets:
link_or_copy(src, os.path.join(DATA, name))
if not has_pal:
write_zero_pal(os.path.join(DATA, pal_alias))
else:
tex_name, list_name = tex_alias, list_alias
pal_name = pal_alias if has_pal else "-"
assets = [(tex_src, tex_name), (list_src, list_name)]
if has_pal:
assets.append((pal_src, pal_name))
for src, name in assets:
shutil.copyfile(src, os.path.join(DATA, name))
if not has_pal:
write_zero_pal(os.path.join(DATA, pal_alias))
row = list(old)
row[0] = str(nk)
row[4], row[8], row[12] = tex_name, list_name, pal_name
# A sliced input can begin with reuse=1 even though the preceding
# output row came from another fixture and holds another texture.
# Never carry that stale residency promise across a merge seam.
if row[11] == "1" and (not out_rows or out_rows[-1][7] != row[7]):
row[11] = "0"
out_rows.append(row)
nk += 1
m = metas[0]
with open(os.path.join(DATA, f"sh3_{out}_epochs.txt"), "w") as f:
f.write(f"# merged runtime-CLUT scheduler fixture: {tags}\n")
f.write("# k idx tbp cbp_reloc tex_file lpddr size crc list_file words records reuse pal_file pal_sum32\n")
f.write("META n_epochs %d fbpxw %s fbh %s fbwords %s lpddr_tex %s tex_words %s n_beats %s\n" %
(nk, m["fbpxw"], m["fbh"], m["fbwords"], m["lpddr_tex"], m["tex_words"], m["n_beats"]))
for row in out_rows:
f.write(" ".join(row) + "\n")
first_tag=parsed[0][1]
base_params = open(os.path.join(DATA, f"sh3_{first_tag}_params.vh")).read()
globals_only = re.split(r"localparam int\s+EP0_NTRIS", base_params, maxsplit=1)[0]
globals_only = re.sub(r"(localparam int N_EPOCHS\s*=\s*)\d+", rf"\g<1>{nk}", globals_only)
with open(os.path.join(DATA, f"sh3_{out}_params.vh"), "w") as f:
f.write(f"// merged runtime scheduler params: {tags}\n")
f.write(globals_only)
for k, row in enumerate(out_rows):
f.write(f"localparam int EP{k}_NTRIS = {row[10]};\n")
f.write(f"localparam int EP{k}_CBP = {row[3]};\n")
f.write(f"localparam [31:0] EP{k}_CRC = 32'h{int(row[7], 16):08x};\n")
f.write(f"localparam bit EP{k}_REUSE = 1'b{row[11]};\n")
with open(os.path.join(DATA, f"sh3_{out}_epoch_table.vh"), "w") as f:
f.write(f"// merged runtime scheduler table: {tags}\n")
for k in range(nk):
f.write(f"EP_CRC[{k}] = EP{k}_CRC; EP_REC[{k}] = EP{k}_NTRIS; EP_REUSE[{k}] = EP{k}_REUSE;\n")
for stem in ("bios", "payload"):
shutil.copyfile(os.path.join(DATA, f"{stem}_sh3_{first_tag}.mem"),
os.path.join(DATA, f"{stem}_sh3_{out}.mem"))
print(f"[merge_sched] {tags} -> {out}: {nk} epochs ({'referenced inputs' if reference_inputs else 'copied assets'})")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))