diff --git a/Standalone/Research/WorldAssemblerM3/Linux118/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux118/REPORT.md new file mode 100644 index 0000000..8575aae --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux118/REPORT.md @@ -0,0 +1,58 @@ +# Linux118 — passage tile cost + +## Result + +The L115 output is exact: owner4 hashes match at W1/W8/W12 under Clang and GCC. Walk audits passed, and the fast tier passed with 259 checks plus 5 diagnostic-only baseline comparisons. + +The near-ready target was not met. The final candidate is 17.2–25.7% slower than L109 across the eight W8/W12 cells; whole-view readiness is 9.4–13.9% slower. Both designs leave readiness close to L115. + +## Field tags + +One W1 probe per stratum, summed over LOD0–2 passage-feature tiles. `SmoothMinDistance` time is estimated from 1/128 sampled calls. Point time contains `CachedColumn`; cache read time is inside `CachedColumn`, so those columns overlap. + +| Stratum | Feature tiles | Prepare ms | Smooth-min calls / ms est. | Point / CachedColumn ms est. | Shared reads ms est. (initial / locked) | +| --- | ---: | ---: | ---: | ---: | ---: | +| S0 | 158 | 467 | 7.14M / 137 | 404 / 226 | 24 / 4 | +| S1 | 195 | 919 | 15.60M / 289 | 0 / 0 | 0 / 0 | +| S2 | 187 | 848 | 13.93M / 249 | 0 / 0 | 0 / 0 | +| S3 | 182 | 567 | 2.62M / 49 | 0 / 0 | 0 / 0 | + +The Heightfield2D/cache tags occur in S0; the other three strata do not use that path in these views. + +## Designs and passage counters + +Design 1 added a conservative segment-to-sample lower bound in `prepareSweptTubeBase`. It skips distance, floor, and shell work when a segment cannot improve the sample. Hashes stayed exact. It pruned 17–18% of candidate pairs, but the readiness matrix remained near L115. + +Design 2 deferred floor-profile interpolation in exact fallbacks until a segment could improve the nearest route or floor result. It also stayed hash-exact, with only a small timing change. Restricting fallbacks to prepared feature spans changed hashes, so that restriction was removed. + +W8 passage-feature tile totals, averaged over two profiles per stratum. L118 pairs show evaluated plus bound-pruned counts; their sum equals L115's candidate-pair total. + +| Stratum | Sweep pairs: L109 → L115; L118 evaluated + pruned | Fallbacks: L109 → L115 → L118 | Visits: L109 → L115 → L118 | +| --- | ---: | ---: | ---: | +| S0 | 3.36M → 7.56M; 6.25M + 1.31M | 0.241M → 0.413M → 0.413M | 1.19M → 3.41M → 3.41M | +| S1 | 6.19M → 16.22M; 13.25M + 2.97M | 0.369M → 0.719M → 0.719M | 1.91M → 7.60M → 7.59M | +| S2 | 6.15M → 15.52M; 12.69M + 2.83M | 0.315M → 0.634M → 0.634M | 1.66M → 6.88M → 6.87M | +| S3 | 4.09M → 9.63M; 7.98M + 1.64M | 0.074M → 0.131M → 0.131M | 0.48M → 1.41M → 1.41M | + +## Readiness + +Five interleaved measured runs per version, stratum, and worker count, after one warm-up per version. Values are median seconds: near LOD0–2 / whole view. + +| Stratum | Workers | Near L109 → L115 → L118 (Δ vs L109) | Whole L109 → L115 → L118 (Δ vs L109) | +| --- | ---: | ---: | ---: | +| S0 | 8 | 0.771 → 0.941 → 0.939 (+21.8%) | 1.731 → 1.968 → 1.938 (+12.0%) | +| S0 | 12 | 0.781 → 0.927 → 0.932 (+19.4%) | 1.716 → 1.878 → 1.897 (+10.6%) | +| S1 | 8 | 0.964 → 1.235 → 1.212 (+25.7%) | 1.886 → 2.122 → 2.088 (+10.7%) | +| S1 | 12 | 0.908 → 1.166 → 1.136 (+25.1%) | 1.754 → 2.022 → 1.997 (+13.8%) | +| S2 | 8 | 0.991 → 1.257 → 1.241 (+25.2%) | 1.863 → 2.136 → 2.121 (+13.8%) | +| S2 | 12 | 0.929 → 1.172 → 1.143 (+23.1%) | 1.736 → 1.989 → 1.976 (+13.8%) | +| S3 | 8 | 0.830 → 0.981 → 0.997 (+20.1%) | 1.726 → 1.918 → 1.912 (+10.8%) | +| S3 | 12 | 0.780 → 0.933 → 0.914 (+17.2%) | 1.625 → 1.795 → 1.778 (+9.4%) | + +The timing gate recorded one-minute load 0.65 and 99.2% CPU idle. Each headless run used CPUs 0–11, the 6 GiB address-space limit, and a 1,740 s timeout. Design 1 timings are preserved in `Saved/Linux/l118/timing-design1/`; final timings are in `Saved/Linux/l118/timing/`. + +## Unreal and workspace + +The post-timing Linux `VFHostEditor` build passed (27/27 actions, 71.13 s) with `-NoUBA -MaxParallelActions=4`; it compiled `VFCore_Core_src_VoxelForgeCore_cpp.cpp`. Unreal ran without `prlimit`. Three existing RealtimeMesh deprecation warnings remain. + +Work stayed on `wip/l111-halfpipe-spline` at `c2b8d39`. No commit, push, or stash was made. Logs and summaries are under `Saved/Linux/l118/`. diff --git a/Standalone/Research/WorldAssemblerM3/Linux118/RunL118HashMatrix.py b/Standalone/Research/WorldAssemblerM3/Linux118/RunL118HashMatrix.py new file mode 100644 index 0000000..b21c686 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux118/RunL118HashMatrix.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Compare L118 owner4 hashes to L115 at W1/W8/W12 under Clang and GCC.""" +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/l118/hashes" +DATA = ROOT / "Standalone/data/owner_world.json" +BASELINE = ROOT / "Saved/Linux/l115/hashes/summary.json" +BINARIES = { + "clang": ROOT / "Saved/Linux/l118/bin/VoxelForgeCoreHeadless-l118-clang", + "gcc": ROOT / "Saved/Linux/l118/bin/VoxelForgeCoreHeadless-l118-gcc", +} +HASH = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=(\S+)$", re.M) + + +def run(workers: int, compiler: str) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + label = f"owner4-w{workers}-{compiler}" + log = OUT / f"{label}.log" + layout = OUT / f"{label}.layout.json" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARIES[compiler]), str(DATA), + "--workers=" + str(workers), "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--force-generate", "--clip-radius=3", + "--max-clip-level=8", "--max-level=8", "--stream-hash", + f"--resolved-layout-output={layout}", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + hashes = HASH.findall(content) + if result.returncode or len(hashes) != 12: + raise SystemExit(f"hash run failed or incomplete: {label}; exit={result.returncode} " + f"hashes={len(hashes)} log={log}") + row = {"workers": workers, "compiler": compiler, "exit_code": result.returncode, + "hashes": hashes, + "source_stamp": (STAMP.search(content).group(1) if STAMP.search(content) else None), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT))} + print("[L118HashRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + if not DATA.is_file() or not BASELINE.is_file() or any(not path.is_file() + for path in BINARIES.values()): + raise SystemExit("missing L118 hash input or L115 reference") + reference = json.loads(BASELINE.read_text(encoding="utf-8"))["runs"] + reference = [row for row in reference if row["dataset"] == "owner4" + and row["recipe"] == "surface-world"] + rows = [run(workers, compiler) for workers in (1, 8, 12) + for compiler in ("clang", "gcc")] + comparisons = [] + for row in rows: + matches = [old for old in reference if old["workers"] == row["workers"] + and old["compiler"] == row["compiler"]] + if len(matches) != 1: + raise SystemExit(f"expected one L115 reference for W{row['workers']}/{row['compiler']}") + match = row["hashes"] == matches[0]["hashes"] + comparisons.append({"workers": row["workers"], "compiler": row["compiler"], + "matches_l115": match, "l115_log": matches[0]["log"], + "l118_log": row["log"]}) + if not match: + raise SystemExit(f"L118 hash mismatch versus L115 at W{row['workers']}/{row['compiler']}") + summary = {"runs": rows, "comparisons": comparisons, + "l115_hashes_identical": len({tuple(r["hashes"]) for r in reference}) == 1, + "candidate_matches_l115_all_workers_and_compilers": True} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux118/RunL118PassageCounters.py b/Standalone/Research/WorldAssemblerM3/Linux118/RunL118PassageCounters.py new file mode 100644 index 0000000..bf31819 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux118/RunL118PassageCounters.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Collect two W8 passage-tile counter profiles per owner stratum.""" +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/l118/passage-counters" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARY = ROOT / "Saved/Linux/l118/bin/VoxelForgeCoreHeadless-l118-clang" +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), +} +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") + + +def rows(path: Path, tag: str) -> dict[str, dict[str, str]]: + result = {} + for line in path.read_text(encoding="utf-8", errors="replace").splitlines(): + if line.startswith(tag + " "): + field = dict(FIELDS.findall(line)) + result[field["key"]] = field + return result + + +def run(strate: int, replicate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + label = f"s{strate}-w8-profile{replicate}" + log = OUT / f"{label}.log" + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARY), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--profile-passage-composer", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + if result.returncode: + raise SystemExit(f"passage-counter profile failed: {log}; exit={result.returncode}") + composer = rows(log, "[PassageComposerTile]") + stages = rows(log, "[CpuTileStages]") + selected = [] + for key, comp in composer.items(): + lod = int(key.rsplit("lod", 1)[1].rstrip(")")) + if lod > 2 or int(comp["features"]) == 0: + continue + stage = stages.get(key) + if stage is None: + raise SystemExit(f"missing CpuTileStages record for {label} {key}") + fallbacks = [int(value) for value in stage["sweep_exact_fallback_by_caller"].split(",")] + selected.append({ + "features": int(comp["features"]), + "sweep_pairs": int(comp["sweep_pairs"]), + "sweep_bound_pruned": int(comp.get("sweep_bound_pruned", "0")), + "exact_fallbacks": sum(fallbacks), + "segment_visits": int(comp["carve_segment_visits"]), + }) + total = {name: sum(item[name] for item in selected) + for name in ("sweep_pairs", "sweep_bound_pruned", "exact_fallbacks", "segment_visits")} + row = {"strate": strate, "workers": 8, "replicate": replicate, + "passage_tiles_lod02": len(selected), "totals": total, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT))} + print("[L118PassageCounters] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + if not DATA.is_file() or not BINARY.is_file(): + raise SystemExit("missing owner data or L118 Clang binary") + result = [run(strate, replicate) for strate in range(4) for replicate in (1, 2)] + summary = [] + for strate in range(4): + group = [item for item in result if item["strate"] == strate] + summary.append({"strate": strate, "workers": 8, "replicates": len(group), + "passage_tiles_lod02_median": sorted( + row["passage_tiles_lod02"] for row in group)[len(group) // 2], + "totals_mean": {name: sum(row["totals"][name] for row in group) / len(group) + for name in group[0]["totals"]}}) + (OUT / "summary.json").write_text( + json.dumps({"runs": result, "summary": summary}, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux118/RunL118TagProbe.py b/Standalone/Research/WorldAssemblerM3/Linux118/RunL118TagProbe.py new file mode 100644 index 0000000..e94caca --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux118/RunL118TagProbe.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Collect opt-in L118 field timing tags for one W1 view per owner stratum.""" +from __future__ import annotations + +import json +import os +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l118/tag-probe" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARY = ROOT / "Saved/Linux/l118/bin/VoxelForgeCoreHeadless-l118-clang" +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), +} +TAGS = re.compile(r"^\[L118FieldTags\] (.*)$", re.M) +PASSAGE = re.compile(r"^\[PassageComposerTile\] (.*)$", re.M) +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") + + +def run(strate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + label = f"s{strate}-w1-tags" + log = OUT / f"{label}.log" + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARY), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=1", "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--profile-passage-composer", + ] + started = time.monotonic() + env = dict(os.environ, VOXELFORGE_L118_TAG_TIMING="1") + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, env=env, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + tags = [dict(FIELDS.findall(match)) for match in TAGS.findall(content)] + passage = [dict(FIELDS.findall(match)) for match in PASSAGE.findall(content)] + if result.returncode or not tags or not passage: + raise SystemExit(f"tag probe failed or emitted no tags: {log}; exit={result.returncode}") + feature_keys = { + row["key"] for row in passage + if int(row["key"].rsplit("lod", 1)[1].rstrip(")")) <= 2 + and int(row["features"]) > 0 + } + near = [row for row in tags if row["key"] in feature_keys] + totals = { + "passage_feature_tiles": len(near), + "sweep_prepare_ms": sum(float(row["sweep_prepare_ms"]) for row in near), + "smoothmin_calls": sum(int(row["smoothmin_calls"]) for row in near), + "smoothmin_ms_est": sum(float(row["smoothmin_ms_est"]) for row in near), + "point_ms_est": sum(float(row["point_ms_est"]) for row in near), + "cached_column_ms_est": sum(float(row["cached_column_ms_est"]) for row in near), + "shared_initial_read_ms_est": sum( + float(row["shared_initial_read_ms_est"]) for row in near), + "shared_locked_read_ms_est": sum( + float(row["shared_locked_read_ms_est"]) for row in near), + } + row = {"strate": strate, "workers": 1, "elapsed_s": round(time.monotonic() - started, 3), + "tagged_tiles": len(tags), "near_lod02_feature_tiles": totals, + "log": str(log.relative_to(ROOT))} + print("[L118TagProbe] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + if not DATA.is_file() or not BINARY.is_file(): + raise SystemExit("missing owner data or L118 Clang binary") + rows = [run(strate) for strate in range(4)] + (OUT / "summary.json").write_text(json.dumps(rows, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux118/RunL118Timing.py b/Standalone/Research/WorldAssemblerM3/Linux118/RunL118Timing.py new file mode 100644 index 0000000..b4777b6 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux118/RunL118Timing.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Five interleaved L80 readiness runs for L109, L115, and L118.""" +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/l118/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "L109": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang", + "L115": ROOT / "Saved/Linux/l115/bin/VoxelForgeCoreHeadless-l115-clang", + "L118": ROOT / "Saved/Linux/l118/bin/VoxelForgeCoreHeadless-l118-clang", +} +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, +) + + +def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + label = f"s{strate}-w{workers}-{version}-{phase}{replicate}" + log = OUT / f"{label}.log" + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARIES[version]), 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: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline_match = PIPELINE.search(content) + near_match = NEAR.search(content) + if result.returncode or pipeline_match is None or near_match is None: + raise SystemExit(f"readiness run failed or trace missing: {log}; exit={result.returncode}") + pipeline = dict(FIELDS.findall(pipeline_match.group(1))) + row = { + "strate": strate, "workers": workers, "version": version, + "phase": phase, "replicate": replicate, "exit_code": result.returncode, + "wall_s": round(time.monotonic() - started, 3), + "lod02_ready_s": float(near_match.group(3)), + "whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")), + "whole_tiles": int(pipeline["whole_tiles"].split("/")[0]), + "log": str(log.relative_to(ROOT)), + } + print("[L118TimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def cpu_idle_percent(interval_s: float = 2.0) -> float: + def totals() -> tuple[int, int]: + fields = Path("/proc/stat").read_text(encoding="utf-8").splitlines()[0].split() + values = [int(value) for value in fields[1:]] + return sum(values), values[3] + (values[4] if len(values) > 4 else 0) + + total_before, idle_before = totals() + time.sleep(interval_s) + total_after, idle_after = totals() + total_delta = max(1, total_after - total_before) + return 100.0 * (idle_after - idle_before) / total_delta + + +def main() -> int: + if not DATA.is_file() or any(not binary.is_file() for binary in BINARIES.values()): + raise SystemExit("missing L109/L115/L118 timing input") + OUT.mkdir(parents=True, exist_ok=True) + load_1m = os.getloadavg()[0] + idle_pct = cpu_idle_percent() + uptime = subprocess.run(["uptime"], cwd=ROOT, text=True, capture_output=True, + check=True).stdout.strip() + top = subprocess.run(["ps", "-eo", "pid,pcpu,comm", "--sort=-pcpu"], cwd=ROOT, + text=True, capture_output=True, check=True).stdout + (OUT / "machine-load-before.txt").write_text( + f"uptime: {uptime}\n1-minute load: {load_1m:.2f}\n" + f"2-second CPU idle: {idle_pct:.1f}%\ntop CPU users:\n{top}", + encoding="utf-8") + print(f"[L118TimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle_pct:.1f}% " + f"uptime={uptime}", flush=True) + if load_1m >= 0.9: + raise SystemExit(f"timing refused: 1-minute load {load_1m:.2f} is not below 0.9") + if idle_pct < 97.0: + raise SystemExit(f"timing refused: CPU idle {idle_pct:.1f}% is below 97%") + rows = [] + versions = ("L109", "L115", "L118") + for strate in range(4): + for workers in (8, 12): + for version in versions: + run(strate, workers, version, "warmup", 0) + for replicate in range(1, 6): + offset = (replicate - 1) % len(versions) + order = versions[offset:] + versions[:offset] + 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") + summary = [] + for strate in range(4): + for workers in (8, 12): + item = {"strate": strate, "workers": workers, "replicates": 5} + medians = {} + for version in versions: + group = [row for row in rows if row["strate"] == strate + and row["workers"] == workers and row["version"] == version] + medians[version] = {} + for key in ("lod02_ready_s", "whole_view_ready_s"): + values = [row[key] for row in group] + medians[version][key] = statistics.median(values) + item[f"{version}_{key}_values"] = values + item[f"{version}_{key}_median"] = medians[version][key] + for comparison in ("L109", "L115"): + for key in ("lod02_ready_s", "whole_view_ready_s"): + item[f"L118_vs_{comparison}_{key}_delta_pct"] = round( + 100.0 * (medians["L118"][key] / medians[comparison][key] - 1.0), 2) + summary.append(item) + (OUT / "summary.json").write_text( + json.dumps({"runs": rows, "summary": summary}, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux118/l118-designs.patch b/Standalone/Research/WorldAssemblerM3/Linux118/l118-designs.patch new file mode 100644 index 0000000..be89dca --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux118/l118-designs.patch @@ -0,0 +1,557 @@ +diff --git a/Standalone/Core/include/VoxelForgeCore/SharedColumnCache.h b/Standalone/Core/include/VoxelForgeCore/SharedColumnCache.h +index cbb0885..3784247 100644 +--- a/Standalone/Core/include/VoxelForgeCore/SharedColumnCache.h ++++ b/Standalone/Core/include/VoxelForgeCore/SharedColumnCache.h +@@ -2,6 +2,7 @@ + + #include "VoxelForgeCore/CpuCycleCounter.h" + ++#include + #include + #include + #include +@@ -24,12 +25,15 @@ namespace VoxelForgeCoreShared + std::uint64_t lockWaitNanoseconds = 0; + std::uint64_t sampledInitialReadCycles = 0; + std::uint64_t sampledInitialReads = 0; ++ std::uint64_t sampledInitialReadNanoseconds = 0; + std::uint64_t sampledLockedReadCycles = 0; + std::uint64_t sampledLockedReads = 0; ++ std::uint64_t sampledLockedReadNanoseconds = 0; + std::uint64_t sampledLockCycles = 0; + std::uint64_t sampledLocks = 0; + std::uint64_t sampledBuildCycles = 0; + std::uint64_t sampledBuilds = 0; ++ bool captureNanoseconds = false; + }; + + // Exact input identity for a pure XY column value. LOD is deliberately not part of this key: +@@ -140,12 +144,19 @@ namespace VoxelForgeCoreShared + static thread_local std::uint64_t timingSequence=0; + const bool sampleCycles=metrics&&((++timingSequence&127u)==0); + const std::uint64_t initialReadStart=sampleCycles?ReadCpuCycles():0; ++ const auto initialReadStartNs=sampleCycles&&metrics->captureNanoseconds ++ ?std::chrono::steady_clock::now():std::chrono::steady_clock::time_point{}; + const bool initialHit=TryReadSet(set, keyWords, tag, value); + if(sampleCycles) + { + metrics->sampledInitialReadCycles+=AdjustedCpuCycleDelta( + initialReadStart,ReadCpuCycles()); + ++metrics->sampledInitialReads; ++ if(metrics->captureNanoseconds) ++ metrics->sampledInitialReadNanoseconds+=static_cast( ++ std::max(0,std::chrono::duration_cast< ++ std::chrono::nanoseconds>(std::chrono::steady_clock::now() ++ -initialReadStartNs).count())); + } + if (initialHit) + { +@@ -156,12 +167,19 @@ namespace VoxelForgeCoreShared + + FSpinGuard lock(set.Writer, metrics,sampleCycles); + const std::uint64_t lockedReadStart=sampleCycles?ReadCpuCycles():0; ++ const auto lockedReadStartNs=sampleCycles&&metrics->captureNanoseconds ++ ?std::chrono::steady_clock::now():std::chrono::steady_clock::time_point{}; + const bool lockedHit=TryReadSet(set, keyWords, tag, value); + if(sampleCycles) + { + metrics->sampledLockedReadCycles+=AdjustedCpuCycleDelta( + lockedReadStart,ReadCpuCycles()); + ++metrics->sampledLockedReads; ++ if(metrics->captureNanoseconds) ++ metrics->sampledLockedReadNanoseconds+=static_cast( ++ std::max(0,std::chrono::duration_cast< ++ std::chrono::nanoseconds>(std::chrono::steady_clock::now() ++ -lockedReadStartNs).count())); + } + if (lockedHit) + { +diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +index ac27df1..ad6e8c2 100644 +--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h ++++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +@@ -147,6 +147,7 @@ namespace VoxelForgeCore + // by the opt-in passage composer profile, except the mesh composition itself. + std::uint64_t passageSweepRouteSegmentCandidates = 0; + std::uint64_t passageSweepSegmentSamplePairs = 0; ++ std::uint64_t passageSweepBoundPrunedPairs = 0; + std::uint64_t passageSweepOutsideInfluencePairs = 0; + std::uint64_t passageSweepMinimumUpdates = 0; + std::uint64_t passageSweepBaseInfluenceSamples = 0; +@@ -158,6 +159,19 @@ namespace VoxelForgeCore + std::uint64_t passageSweepBaseInfluenceHits = 0; + std::uint64_t passageSweepOutOfInfluenceHits = 0; + std::uint64_t passageSweepMissingSamples = 0; ++ double passageSweepPrepareMilliseconds = 0.0; ++ std::uint64_t passageSmoothMinCalls = 0; ++ std::uint64_t passageSmoothMinSampledNanoseconds = 0; ++ std::uint64_t passageSmoothMinSamples = 0; ++ std::uint64_t heightfieldPointSampledNanoseconds = 0; ++ std::uint64_t heightfieldPointSamples = 0; ++ std::uint64_t heightfieldCachedColumnSampledNanoseconds = 0; ++ std::uint64_t heightfieldCachedColumnSamples = 0; ++ std::uint64_t heightfieldSharedInitialReadSampledNanoseconds = 0; ++ std::uint64_t heightfieldSharedInitialReadSamples = 0; ++ std::uint64_t heightfieldSharedLockedReadSampledNanoseconds = 0; ++ std::uint64_t heightfieldSharedLockedReadSamples = 0; ++ bool l118FieldTagsCaptured = false; + std::array passageSweepMissingLookupsByCaller{}; + std::array passageSweepExactRouteFallbacksByCaller{}; + std::array passageSweepFeatureBoundSkipsByCaller{}; +@@ -1394,6 +1408,16 @@ namespace VoxelForgeCore + std::uint64_t sharedColumnHits = 0; + std::uint64_t columnEvaluations = 0; + double columnComputeMilliseconds = 0.0; ++ // L118 opt-in, one-in-128 sampled timings. Nanoseconds are accumulated only for ++ // sampled calls; report estimates multiply by 128. ++ std::uint64_t sampledPointNanoseconds = 0; ++ std::uint64_t sampledPointCalls = 0; ++ std::uint64_t sampledCachedColumnNanoseconds = 0; ++ std::uint64_t sampledCachedColumnCalls = 0; ++ std::uint64_t sampledSharedInitialReadNanoseconds = 0; ++ std::uint64_t sampledSharedInitialReads = 0; ++ std::uint64_t sampledSharedLockedReadNanoseconds = 0; ++ std::uint64_t sampledSharedLockedReads = 0; + bool captureCoordinates = false; + bool coordinateCaptureComplete = true; + std::vector evaluatedCoordinates; +diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h +index c266bd8..4782980 100644 +--- a/Standalone/Core/src/ShapeWorldInternal.h ++++ b/Standalone/Core/src/ShapeWorldInternal.h +@@ -72,6 +72,14 @@ static bool VF_IsL98PassagePrepareTraceEnabled() + return bEnabled; + } + ++static bool VF_IsL118TagTimingEnabled() ++{ ++ static const bool bEnabled=VF_IsL88DiagnosticSwitchEnabled( ++ "VOXELFORGE_L118_TAG_TIMING"); ++ return bEnabled; ++} ++static thread_local std::uint64_t VF_L118SmoothMinTimingSequence=0; ++ + static double VF_L93PassageCarveReachMetres() + { + // The generated world uses a fixed bounded tube influence. An explicit diagnostic +@@ -289,6 +297,8 @@ public: + std::vector passages; + std::atomic lookups{0},cacheHits{0},baseInfluenceHits{0}, + outOfInfluenceHits{0},missingSamples{0}; ++ std::atomic smoothMinCalls{0},smoothMinSampledNanoseconds{0}, ++ smoothMinSamples{0}; + std::array,PassageDensityQueryCallerCount> + missingLookupsByCaller{},exactRouteFallbacksByCaller{}, + featureBoundSkipsByCaller{},tileFreeSkipsByCaller{}, +@@ -376,6 +386,17 @@ public: + const FPassageSweepSample* sweptSample(std::size_t passageIndex, + double qx,double qy,double qz) const; + bool hasSweptTubeBase() const noexcept{return passageSweepData_!=nullptr;} ++ void recordSmoothMinTiming(std::uint64_t nanoseconds,bool sampled) const noexcept ++ { ++ if(!profileStats_||!passageSweepData_)return; ++ passageSweepData_->smoothMinCalls.fetch_add(1,std::memory_order_relaxed); ++ if(sampled) ++ { ++ passageSweepData_->smoothMinSampledNanoseconds.fetch_add( ++ nanoseconds,std::memory_order_relaxed); ++ passageSweepData_->smoothMinSamples.fetch_add(1,std::memory_order_relaxed); ++ } ++ } + void copySweptTubeRuntimeCounts() const noexcept + { + if(!ownsPassageSweepDiagnostics_||!profileStats_||!passageSweepData_)return; +@@ -389,6 +410,13 @@ public: + passageSweepData_->outOfInfluenceHits.load(std::memory_order_relaxed); + profileStats_->passageSweepMissingSamples=passageSweepData_->missingSamples.load( + std::memory_order_relaxed); ++ profileStats_->passageSmoothMinCalls=passageSweepData_->smoothMinCalls.load( ++ std::memory_order_relaxed); ++ profileStats_->passageSmoothMinSampledNanoseconds= ++ passageSweepData_->smoothMinSampledNanoseconds.load( ++ std::memory_order_relaxed); ++ profileStats_->passageSmoothMinSamples=passageSweepData_->smoothMinSamples.load( ++ std::memory_order_relaxed); + for(std::size_t caller=0;callerpassageSweepMissingLookupsByCaller[caller]= +@@ -1298,12 +1326,19 @@ public: + const double ex=qx-cx,ey=qy-cy,ez=qz-cz; + const double roundDistance=DeterministicSqrt( + ex*ex+ey*ey+ez*ez)-radius; ++ const double floorCenterDistanceSq=ex*ex+ey*ey+ez*ez; ++ const bool floorWins=floorCenterDistanceSq1.0e-12 + ?(((qx-segment.a[0])*segment.delta[0] + +(qy-segment.a[1])*segment.delta[1]) + /segment.horizontalLengthSq):t; + const double floorTClamped=std::clamp(floorT,0.0,1.0); +- const double floorCenterDistanceSq=ex*ex+ey*ey+ez*ez; + const double floorRadius=segment.radiusA + +floorTClamped*segment.radiusDelta; + const double floorDepth=PassageFlatFloorDepthSubvoxels( +@@ -1311,13 +1346,9 @@ public: + const double floorCenterZ=PassageFlatFloorCenterZSubvoxels( + constants,segments,segmentIndex,floorT,segment, + qx,qy,floorDepth,qPerMetre); +- const double flatDistance=PassageFlatBottomDistance( +- static_cast(roundDistance),floorCenterZ,qz,floorDepth,qPerMetre); + // The profile uses the closest route projection's arc length. Tube + // radius selects the bore shape only; it never selects floor height. +- if(floorCenterDistanceSq(roundDistance),floorCenterZ,qz,floorDepth, ++ qPerMetre); + bestDistance=roundDistance; + nearest.signedDistance=flatDistance; + nearest.roundSignedDistance=roundDistance; +@@ -6929,12 +6961,31 @@ private: + static float SmoothMinDistance(float A,float B,float Width) + { + FScopedLegacySmoothMinTimer SmoothMinTimer; ++ const auto* tagScope=VF_IsL118TagTimingEnabled() ++ ?ActivePassageTileQueries:nullptr; ++ const bool tag=tagScope&&tagScope->profileStats() ++ &&tagScope->hasSweptTubeBase(); ++ const bool sampled=tag&&((++VF_L118SmoothMinTimingSequence&127u)==0); ++ const auto tagStart=sampled?std::chrono::steady_clock::now() ++ :std::chrono::steady_clock::time_point{}; ++ const auto recordTag=[&]() ++ { ++ if(!tag)return; ++ const std::uint64_t elapsed=sampled?static_cast(std::max(0, ++ std::chrono::duration_cast( ++ std::chrono::steady_clock::now()-tagStart).count())):0; ++ tagScope->recordSmoothMinTiming(elapsed,sampled); ++ }; + if(Width<=0.0f) + { +- return std::min(A,B); ++ const float result=std::min(A,B); ++ recordTag(); ++ return result; + } + const float H=std::max(Width-std::abs(A-B),0.0f)/Width; +- return std::min(A,B)-(H*H*H)*(Width/6.0f); ++ const float result=std::min(A,B)-(H*H*H)*(Width/6.0f); ++ recordTag(); ++ return result; + } + static float SmoothMaxDistance(float A,float B,float Width) + {return -SmoothMinDistance(-A,-B,Width);} +@@ -11464,6 +11515,9 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa + const double wallReliefShell=0.41*qPerMetre; + const double floorDetailShell=1.0*qPerMetre; + const bool profile=profileStats_!=nullptr; ++ const bool tagTiming=profile&&VF_IsL118TagTimingEnabled(); ++ const auto tagStart=tagTiming?std::chrono::steady_clock::now() ++ :std::chrono::steady_clock::time_point{}; + if(profile) + { + profileStats_->passageSweepRouteSegmentCandidates=routeSegmentCandidateCount_; +@@ -11527,6 +11581,31 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa + ||qy>segment.sweepMaximumSubvoxels[1] + ||qzsegment.sweepMaximumSubvoxels[2])continue; ++ const std::size_t sampleIndex=(static_cast(gz) ++ *static_cast(sweep->dimension)+static_cast(gy)) ++ *static_cast(sweep->dimension)+static_cast(gx); ++ auto& sample=passageSamples.samples[sampleIndex]; ++ const double boundDx=std::max({std::min(segment.a[0],segment.b[0])-qx, ++ 0.0,qx-std::max(segment.a[0],segment.b[0])}); ++ const double boundDy=std::max({std::min(segment.a[1],segment.b[1])-qy, ++ 0.0,qy-std::max(segment.a[1],segment.b[1])}); ++ const double boundDz=std::max({std::min(segment.a[2],segment.b[2])-qz, ++ 0.0,qz-std::max(segment.a[2],segment.b[2])}); ++ const double centerDistanceLowerBoundSq=boundDx*boundDx ++ +boundDy*boundDy+boundDz*boundDz; ++ const double roundDistanceLowerBound=FStandaloneShapeWorld::DeterministicSqrt( ++ centerDistanceLowerBoundSq)-std::max(segment.radiusA,segment.radiusB); ++ const bool couldImproveRound=!sample.valid ++ ||roundDistanceLowerBound<=sample.roundSignedDistance; ++ const bool couldImproveFloor=centerDistanceLowerBoundSq ++ <=sample.floorCenterDistanceSq; ++ const bool couldContributeShell=roundDistanceLowerBound<=shellThickness; ++ if(!couldImproveRound&&!couldImproveFloor&&!couldContributeShell) ++ { ++ if(profile)++profileStats_->passageSweepBoundPrunedPairs; ++ continue; ++ } ++ if(profile)++profileStats_->passageSweepSegmentSamplePairs; + const double projectionXY=(qx-segment.a[0])*segment.delta[0] + +(qy-segment.a[1])*segment.delta[1]; + const double t=std::clamp((projectionXY +@@ -11552,15 +11631,10 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa + qx,qy,floorDepth,qPerMetre); + const double flatDistance=FStandaloneShapeWorld::PassageFlatBottomDistance( + static_cast(signedDistance),floorCenterZ,qz,floorDepth,qPerMetre); +- if(profile)++profileStats_->passageSweepSegmentSamplePairs; + if(signedDistance>sweep->influenceSubvoxels) + { + if(profile)++profileStats_->passageSweepOutsideInfluencePairs; + } +- const std::size_t sampleIndex=(static_cast(gz) +- *static_cast(sweep->dimension)+static_cast(gy)) +- *static_cast(sweep->dimension)+static_cast(gx); +- auto& sample=passageSamples.samples[sampleIndex]; + if(floorCenterDistanceSq(std::count(anyPassageInfluence.begin(), + anyPassageInfluence.end(),static_cast(0))); + } ++ if(tagTiming) ++ profileStats_->passageSweepPrepareMilliseconds= ++ std::chrono::duration( ++ std::chrono::steady_clock::now()-tagStart).count(); + passageSweepData_=std::move(sweep); + } + +diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp +index b139ec4..a25049d 100644 +--- a/Standalone/Core/src/VoxelForgeCore.cpp ++++ b/Standalone/Core/src/VoxelForgeCore.cpp +@@ -2444,6 +2444,28 @@ namespace VoxelForgeCore + FStandaloneShapeWorld::SetThreadDensityQueryProfileEnabled(options.instrumentTileDrain); + VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats( + options.captureColumnCoordinates); ++ const auto copyL118FieldTags=[&]() ++ { ++ if(!options.profilePassageComposer ++ ||!world_assembly_detail::L118FieldTagTimingEnabled())return; ++ const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats(); ++ out.diagnostics.l118FieldTagsCaptured=true; ++ out.diagnostics.heightfieldPointSampledNanoseconds= ++ Columns.sampledPointNanoseconds; ++ out.diagnostics.heightfieldPointSamples=Columns.sampledPointCalls; ++ out.diagnostics.heightfieldCachedColumnSampledNanoseconds= ++ Columns.sampledCachedColumnNanoseconds; ++ out.diagnostics.heightfieldCachedColumnSamples= ++ Columns.sampledCachedColumnCalls; ++ out.diagnostics.heightfieldSharedInitialReadSampledNanoseconds= ++ Columns.sampledSharedInitialReadNanoseconds; ++ out.diagnostics.heightfieldSharedInitialReadSamples= ++ Columns.sampledSharedInitialReads; ++ out.diagnostics.heightfieldSharedLockedReadSampledNanoseconds= ++ Columns.sampledSharedLockedReadNanoseconds; ++ out.diagnostics.heightfieldSharedLockedReadSamples= ++ Columns.sampledSharedLockedReads; ++ }; + if(options.captureColumnCoordinates) + { + constexpr std::int64_t SubvoxelsPerVoxel=WorldAssemblySubvoxelsPerVoxel; +@@ -2530,6 +2552,7 @@ namespace VoxelForgeCore + out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0, + Counts.Milliseconds-classificationColumnComputeMilliseconds); + out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds; ++ copyL118FieldTags(); + #if defined(VF_L30_ENABLE_COST_COUNTERS) + PrintL30CostCounters(Counts); + #endif +@@ -3128,6 +3151,7 @@ namespace VoxelForgeCore + out.diagnostics.columnEvaluations=Columns.columnEvaluations; + out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds; + out.diagnostics.columnEvaluationExclusiveMilliseconds=Columns.columnComputeMilliseconds; ++ copyL118FieldTags(); + out.diagnostics.classificationMilliseconds=Counts.Milliseconds; + out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0, + Counts.Milliseconds-classificationColumnComputeMilliseconds); +diff --git a/Standalone/Core/src/WorldAssemblerInternal.h b/Standalone/Core/src/WorldAssemblerInternal.h +index 07b3c9b..dd7c1d6 100644 +--- a/Standalone/Core/src/WorldAssemblerInternal.h ++++ b/Standalone/Core/src/WorldAssemblerInternal.h +@@ -10,6 +10,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -1798,11 +1799,49 @@ namespace world_assembly_detail + inline thread_local VoxelForgeCoreThreadLocal::FLazy< + std::array> HeightfieldColumnCacheNextWay; + inline thread_local HeightfieldColumnCacheStats HeightfieldColumnStats{}; ++ inline thread_local VoxelForgeCoreShared::FColumnCacheMetrics ++ HeightfieldSharedColumnCacheMetrics{}; ++ inline thread_local std::uint64_t HeightfieldPointTimingSequence=0; ++ inline thread_local std::uint64_t HeightfieldCachedColumnTimingSequence=0; + inline thread_local bool HeightfieldColumnCacheLegacyDirectMapped=false; + inline thread_local std::array HeightfieldColumnCaptureBounds{}; + inline thread_local bool HeightfieldColumnCaptureBoundsValid=false; + inline std::atomic HeightfieldColumnCaptureSequence{0}; + ++ inline bool L118FieldTagTimingEnabled() noexcept ++ { ++ static const bool enabled=[] ++ { ++ const char* value=std::getenv("VOXELFORGE_L118_TAG_TIMING"); ++ return value&&value[0]=='1'&&value[1]=='\0'; ++ }(); ++ return enabled; ++ } ++ ++ struct FScopedHeightfieldSampledTimer ++ { ++ FScopedHeightfieldSampledTimer(std::uint64_t& nanoseconds, ++ std::uint64_t& samples,std::uint64_t& sequence) noexcept ++ :Nanoseconds(nanoseconds),Samples(samples) ++ { ++ if(!L118FieldTagTimingEnabled()||((++sequence&127u)!=0))return; ++ Start=std::chrono::steady_clock::now(); ++ bTimed=true; ++ } ++ ~FScopedHeightfieldSampledTimer() ++ { ++ if(!bTimed)return; ++ Nanoseconds+=static_cast(std::max(0, ++ std::chrono::duration_cast( ++ std::chrono::steady_clock::now()-Start).count())); ++ ++Samples; ++ } ++ std::uint64_t& Nanoseconds; ++ std::uint64_t& Samples; ++ std::chrono::steady_clock::time_point Start{}; ++ bool bTimed=false; ++ }; ++ + inline std::size_t HeightfieldColumnCacheSet(const WorldAssemblyRecipeV1& recipe, + const WorldAssemblyStratumPlanV1& plan,std::int64_t x,std::int64_t y) noexcept + { +@@ -1989,6 +2028,10 @@ namespace world_assembly_detail + inline HeightfieldPointSample CachedColumn(const WorldAssemblyRecipeV1& recipe, + const WorldAssemblyStratumPlanV1& plan,std::int64_t x,std::int64_t y) + { ++ FScopedHeightfieldSampledTimer CachedColumnTimer( ++ HeightfieldColumnStats.sampledCachedColumnNanoseconds, ++ HeightfieldColumnStats.sampledCachedColumnCalls, ++ HeightfieldCachedColumnTimingSequence); + const std::int64_t InterpolationStride=HeightfieldColumnInterpolationStrideSubvoxels; + if(InterpolationStride>0) + { +@@ -2105,7 +2148,10 @@ namespace world_assembly_detail + static_cast(reinterpret_cast(&plan)), + plan.contentHash,x,y}; + bool computed=false; +- column=shared->getOrCompute(key,compute,computed); ++ auto* metrics=L118FieldTagTimingEnabled() ++ ?&HeightfieldSharedColumnCacheMetrics:nullptr; ++ if(metrics)metrics->captureNanoseconds=true; ++ column=shared->getOrCompute(key,compute,computed,metrics); + if(!computed)++HeightfieldColumnStats.sharedColumnHits; + } + else column=compute(); +@@ -2576,6 +2622,10 @@ WorldAssemblyFeatureTileListV1 Heightfield2D::buildFeatureTileList( + HeightfieldPointSample Heightfield2D::point(std::int64_t x, std::int64_t y, + std::int64_t z) const noexcept + { ++ world_assembly_detail::FScopedHeightfieldSampledTimer PointTimer( ++ world_assembly_detail::HeightfieldColumnStats.sampledPointNanoseconds, ++ world_assembly_detail::HeightfieldColumnStats.sampledPointCalls, ++ world_assembly_detail::HeightfieldPointTimingSequence); + HeightfieldPointSample out; + if (!isValid() || world_assembly_detail::OutsideRange(x) + || world_assembly_detail::OutsideRange(y) +@@ -2635,6 +2685,17 @@ void Heightfield2D::resetThreadColumnCacheStats(bool captureCoordinates, bool cl + Stats.sharedColumnHits=0; + Stats.columnEvaluations=0; + Stats.columnComputeMilliseconds=0.0; ++ Stats.sampledPointNanoseconds=0; ++ Stats.sampledPointCalls=0; ++ Stats.sampledCachedColumnNanoseconds=0; ++ Stats.sampledCachedColumnCalls=0; ++ Stats.sampledSharedInitialReadNanoseconds=0; ++ Stats.sampledSharedInitialReads=0; ++ Stats.sampledSharedLockedReadNanoseconds=0; ++ Stats.sampledSharedLockedReads=0; ++ world_assembly_detail::HeightfieldSharedColumnCacheMetrics={}; ++ world_assembly_detail::HeightfieldPointTimingSequence=0; ++ world_assembly_detail::HeightfieldCachedColumnTimingSequence=0; + Stats.captureCoordinates=captureCoordinates; + Stats.coordinateCaptureComplete=true; + Stats.evaluatedCoordinates.clear(); +@@ -2670,7 +2731,15 @@ std::int64_t Heightfield2D::threadColumnInterpolationStrideSubvoxels() noexcept + + + const HeightfieldColumnCacheStats& Heightfield2D::threadColumnCacheStats() noexcept +-{ return world_assembly_detail::HeightfieldColumnStats; } ++{ ++ auto& Stats=world_assembly_detail::HeightfieldColumnStats; ++ const auto& Shared=world_assembly_detail::HeightfieldSharedColumnCacheMetrics; ++ Stats.sampledSharedInitialReadNanoseconds=Shared.sampledInitialReadNanoseconds; ++ Stats.sampledSharedInitialReads=Shared.sampledInitialReads; ++ Stats.sampledSharedLockedReadNanoseconds=Shared.sampledLockedReadNanoseconds; ++ Stats.sampledSharedLockedReads=Shared.sampledLockedReads; ++ return Stats; ++} + + HeightfieldBoxClass Heightfield2D::classifyLatticeBox(std::int64_t minimumX, + std::int64_t minimumY,std::int64_t minimumZ,std::int64_t maximumX, +diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h +index 2964e86..231362b 100644 +--- a/Standalone/Source/StreamingWorld.h ++++ b/Standalone/Source/StreamingWorld.h +@@ -1580,7 +1580,7 @@ private: + static_cast(D.islandSharedCacheConflictReplacements), + static_cast(D.islandSharedCacheLockWaitNanoseconds)/1000.0); + if(D.passageComposerProfiled) +- std::printf("[PassageComposerTile] key=(%d,%d,%d,lod%d) worker_ms=%.3f samples=%llu native_field_calls=%llu features=%llu route_candidates=%llu boundary_band_queries=%llu queries=%llu passage_iterations=%llu carve_segment_visits=%llu relief_field_calls=%llu sweep_segments=%llu sweep_pairs=%llu sweep_outside_pairs=%llu sweep_min_updates=%llu sweep_base_samples=%llu sweep_detail_samples=%llu sweep_grid_samples=%llu sweep_no_influence_samples=%llu sweep_lookups=%llu sweep_cache_hits=%llu sweep_base_hits=%llu sweep_outside_hits=%llu sweep_missing=%llu\n", ++ std::printf("[PassageComposerTile] key=(%d,%d,%d,lod%d) worker_ms=%.3f samples=%llu native_field_calls=%llu features=%llu route_candidates=%llu boundary_band_queries=%llu queries=%llu passage_iterations=%llu carve_segment_visits=%llu relief_field_calls=%llu sweep_segments=%llu sweep_pairs=%llu sweep_bound_pruned=%llu sweep_outside_pairs=%llu sweep_min_updates=%llu sweep_base_samples=%llu sweep_detail_samples=%llu sweep_grid_samples=%llu sweep_no_influence_samples=%llu sweep_lookups=%llu sweep_cache_hits=%llu sweep_base_hits=%llu sweep_outside_hits=%llu sweep_missing=%llu\n", + Event.Key.Coord.X,Event.Key.Coord.Y,Event.Key.Coord.Z,Event.Key.Level, + Event.WorkerMilliseconds,static_cast(Event.DensitySamples), + static_cast(D.fieldCalls), +@@ -1593,6 +1593,7 @@ private: + static_cast(D.passageReliefFieldCalls), + static_cast(D.passageSweepRouteSegmentCandidates), + static_cast(D.passageSweepSegmentSamplePairs), ++ static_cast(D.passageSweepBoundPrunedPairs), + static_cast(D.passageSweepOutsideInfluencePairs), + static_cast(D.passageSweepMinimumUpdates), + static_cast(D.passageSweepBaseInfluenceSamples), +@@ -1604,6 +1605,24 @@ private: + static_cast(D.passageSweepBaseInfluenceHits), + static_cast(D.passageSweepOutOfInfluenceHits), + static_cast(D.passageSweepMissingSamples)); ++ if(D.l118FieldTagsCaptured) ++ std::printf("[L118FieldTags] key=(%d,%d,%d,lod%d) sweep_prepare_ms=%.3f smoothmin_calls=%llu smoothmin_samples=%llu smoothmin_ms_est=%.3f point_samples=%llu point_ms_est=%.3f cached_column_samples=%llu cached_column_ms_est=%.3f shared_initial_read_samples=%llu shared_initial_read_ms_est=%.3f shared_locked_read_samples=%llu shared_locked_read_ms_est=%.3f sample_period=128 overlap=(point_contains_cached_column,cached_column_contains_shared_reads)\n", ++ Event.Key.Coord.X,Event.Key.Coord.Y,Event.Key.Coord.Z,Event.Key.Level, ++ D.passageSweepPrepareMilliseconds, ++ static_cast(D.passageSmoothMinCalls), ++ static_cast(D.passageSmoothMinSamples), ++ D.passageSmoothMinSamples ++ ?static_cast(D.passageSmoothMinSampledNanoseconds)*128.0/1.0e6:0.0, ++ static_cast(D.heightfieldPointSamples), ++ static_cast(D.heightfieldPointSampledNanoseconds)*128.0/1.0e6, ++ static_cast(D.heightfieldCachedColumnSamples), ++ static_cast(D.heightfieldCachedColumnSampledNanoseconds)*128.0/1.0e6, ++ static_cast(D.heightfieldSharedInitialReadSamples), ++ static_cast(D.heightfieldSharedInitialReadSampledNanoseconds) ++ *128.0/1.0e6, ++ static_cast(D.heightfieldSharedLockedReadSamples), ++ static_cast(D.heightfieldSharedLockedReadSampledNanoseconds) ++ *128.0/1.0e6); + } + void RecordTimelineEvent(FStandaloneCpuTileTimelineEvent::EType Type, + const FVoxelTileKey& Key,int32 WorkerId,size_t QueueDepth,size_t ActiveWorkers,