#!/usr/bin/env python3 """Fail closed if a runtime-CLUT fit has timed out a required fabric branch.""" from __future__ import annotations import argparse import re import sys from pathlib import Path DEFAULT_OUTPUT = Path( "synth/de25_nano/top_psmct32_raster_demo/output_files" ) REVISION = "de25_nano_psmct32_raster_demo_top" def read(path: Path) -> str: try: return path.read_text(encoding="utf-8", errors="replace") except OSError as exc: raise SystemExit(f"FAIL: cannot read {path}: {exc}") from exc def summary_value(text: str, label: str) -> int: match = re.search(rf"^{re.escape(label)}\s*:\s*([0-9,]+)\s*/", text, re.M) if not match: raise SystemExit(f"FAIL: missing '{label}' in fit summary") return int(match.group(1).replace(",", "")) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT) args = parser.parse_args() fit_summary = args.output_dir / f"{REVISION}.fit.summary" fit_report = args.output_dir / f"{REVISION}.fit.rpt" sta_summary = args.output_dir / f"{REVISION}.sta.summary" summary = read(fit_summary) fit = read(fit_report) sta = read(sta_summary) failures: list[str] = [] if "Fitter Status : Successful" not in summary: failures.append("fitter did not report success") alms = summary_value(summary, "Logic utilization (in ALMs)") rams = summary_value(summary, "Total RAM Blocks") # A 640x480 persistent-Z runtime scene normally consumes roughly 79% ALMs # and 74% M20Ks. These deliberately loose floors catch path pruning, not # healthy placement variation. if alms < 20_000: failures.append(f"only {alms:,} ALMs; runtime raster fabric appears pruned") if rams < 150: failures.append(f"only {rams} RAM blocks; runtime texture/Z fabric appears pruned") for name in ("u_demo|g_feeder", "u_texcache|tex_mem", "u_zc_emit|u_req"): if name not in fit: failures.append(f"required fitted hierarchy missing: {name}") for warning in ( "feeder staging RAM dest EMPTY", "tex_mem dst matched 0 keepers", ): if warning in fit: failures.append(f"runtime SDC endpoint missing: {warning}") emif_setup = re.search( r"Type\s*:\s*Setup 'u_emif_lpddr4b\|iopll\|iopll_0_outclk0'\s*\n" r"Slack\s*:\s*([+-]?[0-9.]+)\s*\n" r"TNS\s*:\s*([+-]?[0-9.]+)", sta, ) if not emif_setup: failures.append("missing 310 MHz EMIF setup summary") else: setup_slack, setup_tns = map(float, emif_setup.groups()) if setup_slack < 0 or setup_tns < 0: failures.append( f"310 MHz setup fails: slack {setup_slack:+.3f} ns, TNS {setup_tns:+.3f} ns" ) if failures: print("RUNTIME FABRIC FIT: FAIL") for failure in failures: print(f"- {failure}") return 1 print( "RUNTIME FABRIC FIT: PASS " f"({alms:,} ALMs, {rams} RAM blocks, feeder/texture/Z present, 310 MHz setup met)" ) return 0 if __name__ == "__main__": sys.exit(main())