ba74bbd5aa
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>
179 lines
7.2 KiB
Python
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))
|