diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 08d1c0a..4fa4862 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -1313,11 +1313,7 @@ public: const bool passageTargetsNativeCaveField=queryStratum&&queryStratum->Resolved &&queryStratum->Resolved->passageCapabilities.hasNativeCaveField &&passage.lowerStrateIndex==queryStratum->StrateIndex; - const bool passageTargetsPlannedTunnelNetwork=queryStratum - &&queryStratum->Resolved - &&queryStratum->Resolved->generatorKind=="TunnelNetwork" - &&queryStratum->Resolved->recipeId=="Core.Caves.PrimitiveField" - &&queryStratum->Resolved->recipeVersion==1; + const bool passageTargetsPlannedTunnelNetwork=plannedTunnelStratum; const double mouthShellBlendMetres=passageTargetsPlannedTunnelNetwork ?2.0:MouthShellBlendMetres; // Legacy native cave fields own the floor and mouth carve on their @@ -1348,6 +1344,20 @@ public: nearest.floorT=swept->floorT; nearest.valid=true; nearest.swept=swept; + if(!swept->hasExactContribution) + { + // The prepared sentinel is farther from the route than the + // maximum body, relief, shell, and blend support. Preserve its + // exact flat distance in the passage union; its wall and floor + // shell bounds are positive, so the rest of the composer has + // no effect for this passage sample. + const double outsideDistance=PassageFlatBottomDistance( + static_cast(swept->roundSignedDistance), + swept->floorCenterZSubvoxels,qz, + swept->floorDepthSubvoxels,qPerMetre); + unionDistance=std::min(unionDistance,outsideDistance); + continue; + } } else { diff --git a/Standalone/Research/WorldAssemblerM3/Linux138/AnalyzeL138PrechangeCacheCounts.py b/Standalone/Research/WorldAssemblerM3/Linux138/AnalyzeL138PrechangeCacheCounts.py new file mode 100644 index 0000000..9c07ed2 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux138/AnalyzeL138PrechangeCacheCounts.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Summarize per-tile cache counters from the five L137 paired timing runs.""" +from __future__ import annotations + +import json +import re +import statistics +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +SOURCE = ROOT / "Saved/Linux/l137/timing/final-design1" +OUT = ROOT / "Saved/Linux/l138/prechange/column-cache-counts.json" +STAMPS = { + "assembler-world": "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621", + "l137": "7913d019d968de2dbaea2282b90fa8ff41b53f996c022e9e6fed92712cb40ecf", +} +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +METRICS = ( + "column_point_calls", "column_local_hits", "column_local_misses", + "column_conflict_misses", "column_shared_hits", "column_formula_evaluations", +) + + +def read_run(strate: int, workers: int, version: str, replicate: int) -> dict: + path = SOURCE / f"s{strate}-w{workers}-{version}-measured{replicate}.log" + if not path.is_file(): + raise SystemExit(f"missing pre-change L137 run: {path}") + text = path.read_text(encoding="utf-8", errors="replace") + stamp = re.search(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", text, re.M) + if stamp is None or stamp.group(1) != STAMPS[version]: + raise SystemExit(f"unexpected source stamp in {path}") + rows = {} + for line in text.splitlines(): + if not line.startswith("[WorldAssemblerLODCostDetail] "): + continue + fields = dict(FIELDS.findall(line)) + rows[int(fields["lod"])] = fields + if set(rows) != set(range(9)): + raise SystemExit(f"incomplete LOD counters in {path}: {sorted(rows)}") + return {"path": str(path.relative_to(ROOT)), "rows": rows} + + +def summarize(runs: list[dict], lods: range) -> dict: + per_run = [] + for run in runs: + selected = [run["rows"][lod] for lod in lods] + tile_count = sum(int(row["tiles"]) for row in selected) + per_run.append({ + "tiles": tile_count, + **{metric: sum(int(row[metric]) for row in selected) / tile_count + for metric in METRICS}, + }) + return { + "tiles_per_run": statistics.mean(row["tiles"] for row in per_run), + "mean_per_tile": { + metric: statistics.mean(row[metric] for row in per_run) + for metric in METRICS + }, + "run_range_per_tile": { + metric: [min(row[metric] for row in per_run), + max(row[metric] for row in per_run)] + for metric in METRICS + }, + } + + +def main() -> int: + result = { + "source": "five measured L137 L80 runs per version, worker count, and stratum", + "pre_change_stamps": STAMPS, + "cells": [], + } + for strate in range(4): + for workers in (8, 12): + versions = { + version: [read_run(strate, workers, version, replicate) + for replicate in range(1, 6)] + for version in STAMPS + } + item = {"strate": strate, "workers": workers} + for window, lods in (("near_lod0_2", range(3)), ("whole_lod0_8", range(9))): + item[window] = { + version: summarize(runs, lods) + for version, runs in versions.items() + } + item[window]["candidate_minus_baseline_per_tile"] = { + metric: (item[window]["l137"]["mean_per_tile"][metric] + - item[window]["assembler-world"]["mean_per_tile"][metric]) + for metric in METRICS + } + item["logs"] = { + version: [run["path"] for run in runs] + for version, runs in versions.items() + } + result["cells"].append(item) + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + print(f"[L138PrechangeCacheCounts] cells={len(result['cells'])} output={OUT.relative_to(ROOT)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux138/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux138/REPORT.md new file mode 100644 index 0000000..4167d0e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux138/REPORT.md @@ -0,0 +1,49 @@ +# Linux138 — final cave planner speed pass + +Source stamp: `9aade4e6cb33e86c36b89440644d9ef5d440288fb6ff0af03af128291c95f93f`. + +## Result + +The suspected column-cache regression is not present in the measured counters. Calls and formula evaluations per tile match `assembler/world`; the small miss deltas change sign between W8 and W12. The L77 `CachedColumn` and shared `TryRead` increases were normalized sample-share changes, not evidence of more cache calls. + +I removed repeated route identity checks and added a fast path for prepared sweep samples that are stamped outside all passage influence. The path preserves the sample's flat distance in the union and skips wall/floor shell work whose bounds are positive. The speed bar is still missed on several near-ready cells, especially Vertical Shafts. No version bump or baseline update was needed: output audits remained green. + +## Before-change cache census + +Means per near-ready LOD0–2 tile, averaged across the five measured L137 L80 runs per worker count. Each cell is `assembler/world / L137 candidate`. + +| Stratum / workers | Point calls | Local hits | Local misses | Shared hits | Formula evaluations | +|---|---:|---:|---:|---:|---:| +| Surface World / W8 | 6529.395 / 6529.396 | 5712.251 / 5720.303 | 817.144 / 809.092 | 570.207 / 562.158 | 246.937 / 246.934 | +| Surface World / W12 | 6529.395 / 6529.396 | 5642.386 / 5641.558 | 887.009 / 887.838 | 639.976 / 640.851 | 247.033 / 246.987 | + +Floating Islands, Tunnel Network, and Vertical Shafts had zero generic heightfield column calls and cache hits/misses per near-ready tile in both binaries. Floating Islands' separate local/shared cache counters were also zero in its profiled LOD0–2 tiles. The shared `TryRead` path is reached on local misses: W8 had about 8 fewer misses per tile on the candidate, while W12 had 0.83 more. Cache code, key, and window are unchanged versus `assembler/world` in `WorldAssemblerInternal.h`, `SharedColumnCache.h`, and `FloatingIslandsInternal.h`. + +L77 reports each function's share of sampled worker ticks. A higher share can result when other work takes a smaller share; it is not a cache-call counter. The before-change census is in `Saved/Linux/l138/prechange/column-cache-counts.json`; the separate Floating Islands counters are in `Saved/Linux/l138/prechange/island-cache/summary.json`. + +## Change and timing + +`ShapeWorldInternal.h` now reuses the already-computed Tunnel Network stratum predicate in the passage loop. For a swept sample with no exact contribution, it adds the same flat outside distance to the union, then skips relief, mouth, wall, and shell calculations. The influence bound is 1.053 m (0.75 m shell + 0.21 m relief + 0.083 m corner support + 0.01 m guard); the stamped sentinel is one subvoxel beyond it. + +Final L80 used one warm-up and five interleaved runs per binary, at W8/W12 across all four strata. The quiet-host gate passed at load 1.34 and 98.9% CPU idle. Deltas are candidate medians versus `assembler/world`; the L137 A/A noise floor is ±1%. + +| Stratum | Near-ready W8 / W12 | Whole-view W8 / W12 | +|---|---:|---:| +| Surface World | +0.34% / +1.72% | −0.48% / −0.93% | +| Floating Islands | −4.34% / +2.87% | −3.11% / −1.50% | +| Tunnel Network | −2.54% / +2.86% | −3.96% / −1.73% | +| Vertical Shafts | +4.66% / +3.25% | −1.73% / −2.06% | + +Only Surface World W8 is within ±1% near-ready. The per-LOD worker totals show LOD2 down 24–39% on Floating Islands, Tunnel Network, and Vertical Shafts, but Vertical Shafts LOD0/1 are up 5–22%; that remaining work keeps its near-ready medians outside the bar. Tunnel Network W8 is the closest of its two near-ready cells. + +## Checks + +- Fast tier: PASS, 273 checks, 286.0 s. +- Composed sweep: PASS, 16/16 seeds, with passage, seam, shell, mouth-profile, naturalness, ripple, and blocked-route controls. +- Clang/GCC W1/W8/W12: PASS, all 13 determinism hash lines match. +- Linux `VFHostEditor` Development: PASS, 5 actions, 38.19 s with `-NoUBA -MaxParallelActions=4`; no `prlimit`. +- The full verification tier and Windows UE build were not run. No Unreal `Source/` files changed. + +The first fast-tier invocation used the verifier's default `l42` output directory and refused its stale binary. I reran it with `--out Saved/Linux/l138`; the final-source run passed. + +Build, fast-tier, seed, determinism, cache-census, and timing logs are under `Saved/Linux/l138/`; the final timing matrix is `Saved/Linux/l138/timing/summary.json`. Changes are uncommitted. diff --git a/Standalone/Research/WorldAssemblerM3/Linux138/RunL138Determinism.py b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138Determinism.py new file mode 100644 index 0000000..cf53a60 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138Determinism.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Compare L138 Clang/GCC composed hashes at W1, W8, and W12.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l138/determinism" +DATA = ROOT / "Standalone/data/regression/owner_world_2strate_l61.json" +BINARIES = { + "clang": ROOT / "Saved/Linux/l138/bin/VoxelForgeCoreHeadless-l138-clang", + "gcc": ROOT / "Saved/Linux/l138/bin/VoxelForgeCoreHeadless-l138-gcc", +} +STAMP = "9aade4e6cb33e86c36b89440644d9ef5d440288fb6ff0af03af128291c95f93f" +CAMERA = "-4.688214,38.662384,-505.781342,-28.446,0" +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|MaterialHash|LODHash|ExactQueryHash)\].*$", + re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def main() -> int: + if any(not path.is_file() for path in (DATA, *BINARIES.values())): + raise SystemExit("missing L138 determinism binary or owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + rows = [] + reference: tuple[str, ...] | None = None + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + name = f"determinism-{compiler}-w{workers}" + log = OUT / f"{name}.log" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=0", + "--max-level=0", f"--camera={CAMERA}", "--stream-hash-tiles", + "--force-generate", "--recipe=surface-world", "--render-distance-chunks=0", + "--landmark-chamber-demo", "--ore-veins-demo"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + hashes = tuple(HASH_RE.findall(content)) + passed = (proc.returncode == 0 and stamp is not None + and stamp.group(1) == STAMP and len(hashes) >= 4) + if reference is None and passed: + reference = hashes + elif reference is not None: + passed = passed and hashes == reference + row = {"compiler": compiler, "workers": workers, "exit": proc.returncode, + "stamp": stamp.group(1) if stamp else None, "hash_lines": len(hashes), + "hashes_match_first": hashes == reference, "passed": bool(passed), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT))} + rows.append(row) + print("[L138DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True) + if not passed: + (OUT / "summary.json").write_text(json.dumps( + {"status": "FAIL", "stamp": STAMP, "runs": rows}, indent=2) + "\n") + raise SystemExit(f"determinism mismatch; see {log}") + (OUT / "summary.json").write_text(json.dumps( + {"status": "PASS", "stamp": STAMP, "runs": rows}, indent=2) + "\n") + print(f"[L138DeterminismSummary] runs={len(rows)} stamp={STAMP} status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux138/RunL138FinalTiming.py b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138FinalTiming.py new file mode 100644 index 0000000..96946c6 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138FinalTiming.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Run five interleaved L80 timings for assembler/world and the L138 candidate.""" +from __future__ import annotations + +import json +import os +import re +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l138/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ( + ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-a", + "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621"), + "l138": ( + ROOT / "Saved/Linux/l138/bin/VoxelForgeCoreHeadless-l138-clang", + "9aade4e6cb33e86c36b89440644d9ef5d440288fb6ff0af03af128291c95f93f"), +} +CAMERAS = { + 0: (8.0, 16.0, -372.2626953125), + 1: (25.8369140625, 3.19921875, -737.0), + 2: (19.166015625, -14.2001953125, -753.3740234375), + 3: (-20.0625, -5.068359375, -900.353515625), +} +PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M) +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +NEAR = re.compile( + r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$", + re.M) +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + binary, expected_stamp = BINARIES[version] + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + label = f"s{strate}-w{workers}-{version}-{phase}{replicate}" + log = OUT / f"{label}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline, near, stamp = PIPELINE.search(content), NEAR.search(content), STAMP.search(content) + if (proc.returncode or pipeline is None or near is None or stamp is None + or stamp.group(1) != expected_stamp): + raise SystemExit(f"L80 timing failed or stamp mismatch: {log}; exit={proc.returncode}") + fields = dict(FIELDS.findall(pipeline.group(1))) + row = { + "strate": strate, "workers": workers, "version": version, + "phase": phase, "replicate": replicate, "source_stamp": stamp.group(1), + "lod02_ready_s": float(near.group(3)), + "whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")), + "whole_tiles": int(fields["whole_tiles"].split("/")[0]), + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), + } + print("[L138FinalTimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def quiet_machine() -> tuple[float, float]: + load_1m = os.getloadavg()[0] + + def totals() -> tuple[int, int]: + fields = Path("/proc/stat").read_text().splitlines()[0].split() + values = [int(value) for value in fields[1:]] + return sum(values), values[3] + values[4] + + before = totals() + time.sleep(2.0) + after = totals() + idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0]) + return load_1m, idle + + +def main() -> int: + if any(not path.is_file() for path in [DATA, *(entry[0] for entry in BINARIES.values())]): + raise SystemExit("missing L138 timing input") + OUT.mkdir(parents=True, exist_ok=True) + load_1m, idle = quiet_machine() + (OUT / "machine-load-before.txt").write_text( + f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n") + print(f"[L138FinalTimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True) + if load_1m >= 1.5 or idle < 97.0: + raise SystemExit("timing refused: machine is above the L80 quiet-load gate") + + rows = [] + for strate in range(4): + for workers in (8, 12): + for version in BINARIES: + run(strate, workers, version, "warmup", 0) + for replicate in range(1, 6): + order = tuple(BINARIES) if replicate % 2 else tuple(reversed(BINARIES)) + for version in order: + rows.append(run(strate, workers, version, "measured", replicate)) + (OUT / "runs-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + + measured = [row for row in rows if row["phase"] == "measured"] + summary = [] + for strate in range(4): + for workers in (8, 12): + group = [row for row in measured + if row["strate"] == strate and row["workers"] == workers] + entry = {"strate": strate, "workers": workers, "replicates_per_version": 5} + medians = { + version: { + key: statistics.median(row[key] for row in group + if row["version"] == version) + for key in ("lod02_ready_s", "whole_view_ready_s") + } + for version in BINARIES + } + entry["medians"] = medians + for key in ("lod02_ready_s", "whole_view_ready_s"): + entry[f"l138_vs_assembler-world_{key}_delta_pct"] = round( + 100.0 * (medians["l138"][key] / medians["assembler-world"][key] - 1.0), 2) + entry[f"{key}_paired_deltas_pct"] = [ + round(100.0 * ( + next(row[key] for row in group + if row["version"] == "l138" and row["replicate"] == replicate) + / next(row[key] for row in group + if row["version"] == "assembler-world" + and row["replicate"] == replicate) - 1.0), 3) + for replicate in range(1, 6) + ] + summary.append(entry) + (OUT / "summary.json").write_text(json.dumps({ + "a_a_noise_floor_pct": 1.0, + "timing_protocol": "one warmup per binary, five interleaved runs, W8/W12, all four strata", + "runs": rows, + "summary": summary, + }, indent=2) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux138/RunL138PrechangeIslandCacheCensus.py b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138PrechangeIslandCacheCensus.py new file mode 100644 index 0000000..040c361 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138PrechangeIslandCacheCensus.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Count pre-change Floating Islands cache calls from L137 binaries.""" +from __future__ import annotations + +import json +import re +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l138/prechange/island-cache" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ( + ROOT / "Saved/Linux/l137/profile-bin/VoxelForgeCoreHeadless-l137-profile-base", + "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621"), + "l137": ( + ROOT / "Saved/Linux/l137/profile-bin/VoxelForgeCoreHeadless-l137-profile-head", + "dcf2ffa55fd6d62b3c7faf76229c3b99c635868dc1b694fc6e8f39e738775a4e"), +} +CAMERAS = { + 1: (25.8369140625, 3.19921875, -737.0), +} +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +TILE = re.compile(r"^\[CpuTileStages\] key=\([^)]*,lod(\d+)\).*", re.M) +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +CACHE_FIELDS = ( + "fi_local_hit", "fi_local_miss", "fi_local_conflicts", + "fi_shared_hit", "fi_shared_miss", "fi_shared_computes", + "fi_shared_conflicts", +) + + +def main() -> int: + OUT.mkdir(parents=True, exist_ok=True) + rows = [] + for workers in (8, 12): + for version, (binary, expected_stamp) in BINARIES.items(): + camera = ",".join(f"{value:.12f}" for value in CAMERAS[1]) + ",0,0" + log = OUT / f"s1-w{workers}-{version}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8", + "--max-level=8", f"--camera={camera}", "--profile-lod-work", + "--profile-passage-composer", "--stream-scheduler-policy=3", + ] + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + text = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP.search(text) + if proc.returncode or not stamp or stamp.group(1) != expected_stamp: + raise SystemExit(f"pre-change cache census failed: {log}") + tiles = [] + for line in text.splitlines(): + match = TILE.match(line) + if not match or int(match.group(1)) > 2: + continue + fields = dict(FIELDS.findall(line)) + tiles.append({key: int(fields.get(key, "0")) for key in CACHE_FIELDS}) + if not tiles: + raise SystemExit(f"no LOD0-2 tile-cache counters in {log}") + total = {key: sum(tile[key] for tile in tiles) for key in CACHE_FIELDS} + row = { + "strate": 1, "workers": workers, "version": version, + "source_stamp": stamp.group(1), "tiles": len(tiles), + "totals": total, + "per_tile": {key: value / len(tiles) for key, value in total.items()}, + "logs": str(log.relative_to(ROOT)), + } + rows.append(row) + print("[L138PrechangeIslandCache] " + json.dumps(row, sort_keys=True), flush=True) + (OUT / "summary.json").write_text(json.dumps(rows, indent=2) + "\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux138/RunL138SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138SeedSweep.py new file mode 100644 index 0000000..a6ce234 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138SeedSweep.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Run the 16-seed composed passage and seam audits on the L138 candidate.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l138/seeds" +BIN = ROOT / "Saved/Linux/l138/bin/VoxelForgeCoreHeadless-l138-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "9aade4e6cb33e86c36b89440644d9ef5d440288fb6ff0af03af128291c95f93f" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +REQUIRED = { + "passage": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "route_negative_control": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} + + +def main() -> int: + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L138 Clang binary or owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + results = [] + for seed in range(16): + log = OUT / f"seed{seed}.log" + layout = OUT / f"seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + "--force-generate", f"--seed={seed}", "--recipe=surface-world", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + blocked = re.findall(r"^\[ResolvedPassageBlockedControl\].*detected=true$", + content, re.M) + checks["three_blocked_controls"] = len(blocked) == 3 + passed = proc.returncode == 0 and all(checks.values()) + row = {"seed": seed, "exit": proc.returncode, "passed": passed, + "checks": checks, "blocked_controls": len(blocked), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L138SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + (OUT / "runs-partial.json").write_text(json.dumps(results, indent=2) + "\n") + if not passed: + raise SystemExit(f"seed {seed} failed composed audits; see {log}") + summary = {"source_stamp": STAMP, "seeds": list(range(16)), + "passed": len(results), "failed": 0, "results": results} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + print("[L138SeedSweepSummary] passed=16/16 status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())