#!/usr/bin/env python3 """Compare owner canonical hashes across Clang/GCC and W1/W8/W12.""" from __future__ import annotations import json import re import subprocess import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parents[4] OUT = ROOT / "Saved/Linux/l173/canonical-hashes" DATASETS = { "owner4": ROOT / "Standalone/data/owner_world.json", "owner2": ROOT / "Standalone/data/regression/owner_world_2strate_l61.json", } RECIPES = {"owner4": ("surface-world",), "owner2": ("surface-world", "floating-islands")} BINARIES = { "clang": ROOT / "Saved/Linux/l173/fast/bin/VoxelForgeCoreHeadless-fast-clang", "gcc": ROOT / "Saved/Linux/l173/bin/VoxelForgeCoreHeadless-l173-gcc", } BASELINES = { ("owner2", "surface-world"): ( "owner-hash-surface-world-style1-v227.baseline", "owner-hash-surface-world-style1-v237.baseline", "owner-hash-surface-world-style1-v239.baseline"), ("owner2", "floating-islands"): ( "owner-hash-floating-islands-style2-v227.baseline", "owner-hash-floating-islands-style2-v237.baseline", "owner-hash-floating-islands-style2-v239.baseline"), ("owner4", "surface-world"): ( "owner-hash-owner4-surface-world-style1-v227.baseline", "owner-hash-owner4-surface-world-style1-v239.baseline"), } BASELINE_ROOT = ROOT / "Standalone/Tools/verify/baselines/owner-hash/canonical" HASH_RE = re.compile( r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) def main() -> int: OUT.mkdir(parents=True, exist_ok=True) stamp = subprocess.check_output( [sys.executable, str(ROOT / "Standalone/Tools/verify/source_stamp.py"), "stamp", "--root", str(ROOT)], text=True).strip() baseline_hashes = {} for key, filenames in BASELINES.items(): baseline_hashes[key] = {} for filename in filenames: path = BASELINE_ROOT / filename if path.is_file(): baseline_hashes[key][filename] = HASH_RE.findall( path.read_text(encoding="utf-8", errors="replace")) rows = [] groups = {} partial = OUT / "runs-partial.json" for dataset, recipes in RECIPES.items(): for recipe in recipes: signatures = [] for workers in (1, 8, 12): for compiler in ("clang", "gcc"): binary = BINARIES[compiler] label = f"{dataset}-{recipe}-{compiler}-w{workers}" log = OUT / f"{label}.log" layout = OUT / f"{label}.layout.json" command = [ "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "timeout", "1740s", str(binary), str(DATASETS[dataset]), "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", "--force-generate", f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8", "--max-level=8", f"--recipe={recipe}", "--stream-hash", f"--resolved-layout-output={layout}", ] # Rebuild owner4's Core plan from the same exported seed. Its checked-in # Unreal snapshot currently fails canonical import after the planner merge. if dataset == "owner4": command.append("--seed=0") started = time.monotonic() code: int | str try: with log.open("w", encoding="utf-8") as output: proc = subprocess.run(command, cwd=ROOT, stdout=output, stderr=subprocess.STDOUT, check=False, timeout=1740) code = proc.returncode except subprocess.TimeoutExpired: code = "OUTER_TIMEOUT" output = log.read_text(encoding="utf-8", errors="replace") \ if log.is_file() else "" hashes = HASH_RE.findall(output) stamp_match = STAMP_RE.search(output) run_ok = (code == 0 and len(hashes) == 12 and stamp_match is not None and stamp_match.group(1) == stamp) row = { "dataset": dataset, "recipe": recipe, "compiler": compiler, "workers": workers, "seed_override": 0 if dataset == "owner4" else None, "exit_code": code, "run_passed": run_ok, "hashes": hashes, "source_stamp": stamp_match.group(1) if stamp_match else None, "elapsed_s": round(time.monotonic() - started, 3), "log": str(log.relative_to(ROOT)), } rows.append(row) signatures.append(row) print("[L173CanonicalHashRun] " + json.dumps(row, sort_keys=True), flush=True) partial.write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8") expected = baseline_hashes[(dataset, recipe)] first = signatures[0]["hashes"] if signatures else [] deterministic = (len(signatures) == 6 and all(row["run_passed"] and row["hashes"] == first for row in signatures)) baseline_matches = { name: bool(first) and first == values for name, values in expected.items() } groups[f"{dataset}/{recipe}"] = { "all_compilers_workers_match": deterministic, "hash_rows": len(first), "baseline_matches": baseline_matches, "source_stamp": stamp, } print("[L173CanonicalHashGroup] " + json.dumps( {"group": f"{dataset}/{recipe}", **groups[f"{dataset}/{recipe}"]}, sort_keys=True), flush=True) passed = (len(rows) == 18 and all(row["run_passed"] for row in rows) and all(group["all_compilers_workers_match"] for group in groups.values())) summary = {"status": "PASS" if passed else "FAIL", "source_stamp": stamp, "runs": rows, "groups": groups} (OUT / "summary.json").write_text( json.dumps(summary, indent=2) + "\n", encoding="utf-8") print(f"[L173CanonicalHashSummary] status={summary['status']} runs={len(rows)} " f"groups={len(groups)}", flush=True) return 0 if passed else 1 if __name__ == "__main__": raise SystemExit(main())