wip: L175 fix tile seams - passage-affected X8 lanes use the composed path (v241, RED)

Seam audit: 0 mismatches over 16 seeds (99,269 pairs, 121.6M shared samples),
injected control detected. Step-2 overlap/turn failures remain. Mainline has the
same dispatch structure (not changed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011g9Ez42ZNqQGjigc4FWDih
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-11-04 14:05:01 +01:00
1 parent 0ff8d98786
commit 72f2609507
10 files changed
+605 -6

No files matched your search

@@ -516,7 +516,8 @@ namespace VoxelForgeCore
// v238 plans one resolved passage spline with explicit landing chambers. // v238 plans one resolved passage spline with explicit landing chambers.
// v239 plans their floor planes as the passage's continuous floor profile. // v239 plans their floor planes as the passage's continuous floor profile.
// v240 adds level end runs and rejects passage tube overlaps and hairpins. // v240 adds level end runs and rejects passage tube overlaps and hairpins.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 240; // v241 keeps passage composition sample-local and out of base-only X8 lanes.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 241;
enum class FieldChannel : std::uint32_t enum class FieldChannel : std::uint32_t
{ {
+85 -1
View File
@@ -228,6 +228,17 @@ public:
FSurfaceWorldFieldAdapter,FFloatingIslandFieldAdapter, FSurfaceWorldFieldAdapter,FFloatingIslandFieldAdapter,
FCavesFieldAdapter,FLegacyGeneratorFieldAdapter>; FCavesFieldAdapter,FLegacyGeneratorFieldAdapter>;
inline static thread_local std::uint32_t PassageX8OutsideProofDepth=0;
class FScopedPassageX8OutsideProof
{
public:
FScopedPassageX8OutsideProof(){++PassageX8OutsideProofDepth;}
~FScopedPassageX8OutsideProof(){--PassageX8OutsideProofDepth;}
FScopedPassageX8OutsideProof(const FScopedPassageX8OutsideProof&)=delete;
FScopedPassageX8OutsideProof& operator=(const FScopedPassageX8OutsideProof&)=delete;
static bool IsActive() noexcept{return PassageX8OutsideProofDepth!=0;}
};
struct FBoundStratumField struct FBoundStratumField
{ {
enum class EKind : uint8 { Legacy, SurfaceWorld, FloatingIslands, Caves }; enum class EKind : uint8 { Legacy, SurfaceWorld, FloatingIslands, Caves };
@@ -1298,6 +1309,14 @@ public:
{ {
const std::size_t passageIndex=passageTileScopeMatches const std::size_t passageIndex=passageTileScopeMatches
?(*routePassageIndices)[routeOffset]:routeOffset; ?(*routePassageIndices)[routeOffset]:routeOffset;
// A passage can be in this tile's list because a different route segment
// overlaps the tile. Check the sample before its landing chamber is composed.
if(passageTileScopeMatches
&&!passageTileQueries->routeFeatureMayAffect(passageIndex,qx,qy,qz))
{
passageTileQueries->recordFeatureBoundSkip();
continue;
}
const auto& passage=ResolvedLayout->passages[passageIndex]; const auto& passage=ResolvedLayout->passages[passageIndex];
if(passage.controlPoints.size()<2)continue; if(passage.controlPoints.size()<2)continue;
const bool surfaceDepartureLanding=queryStratum&&queryStratum->Resolved const bool surfaceDepartureLanding=queryStratum&&queryStratum->Resolved
@@ -3710,7 +3729,50 @@ public:
static_cast<uint64>(Count)); static_cast<uint64>(Count));
FScopedRoute l77Route(ERoute::SharedSource); FScopedRoute l77Route(ERoute::SharedSource);
FScopedPhase l77Phase(EPhase::BaseField); FScopedPhase l77Phase(EPhase::BaseField);
SharedDensitySource->DensityAtLatticeX8(XStart,XStep,Y,Z,Count,OutDensity); const int32 ActiveCount=std::clamp(Count,0,8);
const auto* TileQueries=FScopedPassageTileQueries::CurrentFor(*this);
const auto& Source=*SharedDensitySource;
const bool bOldFastEligible=Source.EditBrushes.empty()
&&Y>=Source.Min[1]&&Y<=Source.Max[1]
&&Z>=Source.Min[2]&&Z<=Source.Max[2]
&&XStart>=Source.Min[0]
&&XStart+static_cast<int64>(ActiveCount-1)*XStep<=Source.Max[0];
const bool bHasResolvedPassages=ResolvedLayout
&&!ResolvedLayout->passages.empty();
std::array<bool,8> LaneMayBeAffected{};
const auto mayBeAffected=[&](int32 Lane)
{
if(!TileQueries)return bHasResolvedPassages;
const double Q=static_cast<double>(
VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
const double X=static_cast<double>(XStart+Lane*XStep)*Q;
const double QY=static_cast<double>(Y)*Q;
const double QZ=static_cast<double>(Z)*Q;
for(const std::size_t passageIndex:TileQueries->routePassageIndices())
if(TileQueries->routeFeatureMayAffect(passageIndex,X,QY,QZ))
return true;
return false;
};
for(int32 Lane=0;Lane<ActiveCount;++Lane)
LaneMayBeAffected[static_cast<std::size_t>(Lane)]=mayBeAffected(Lane);
// The shared X8 source contains only the base field. Route-bound lanes
// use the composed scalar path; only unaffected runs delegate to X8.
for(int32 Lane=0;Lane<ActiveCount;)
{
if(LaneMayBeAffected[static_cast<std::size_t>(Lane)])
{
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
++Lane;
continue;
}
const int32 RunStart=Lane;
while(Lane<ActiveCount
&&!LaneMayBeAffected[static_cast<std::size_t>(Lane)])++Lane;
FScopedPassageX8OutsideProof outsideProof;
SharedDensitySource->DensityAtLatticeX8(
XStart+RunStart*XStep,XStep,Y,Z,Lane-RunStart,
OutDensity+RunStart);
}
return; return;
} }
Count=std::clamp(Count,0,8); Count=std::clamp(Count,0,8);
@@ -3782,6 +3844,10 @@ public:
&&Z>=Min[2]&&Z<=Max[2] &&Z>=Min[2]&&Z<=Max[2]
&&XStart>=Min[0] &&XStart>=Min[0]
&&XStart+static_cast<int64>(Count-1)*XStep<=Max[0]; &&XStart+static_cast<int64>(Count-1)*XStep<=Max[0];
const bool bOldFastEligible=bFast;
const auto* TileQueries=FScopedPassageTileQueries::CurrentFor(*this);
if(!TileQueries&&ResolvedLayout&&!ResolvedLayout->passages.empty()
&&!FScopedPassageX8OutsideProof::IsActive())bFast=false;
if(!bFast) if(!bFast)
{ {
for(int32 Lane=0;Lane<Count;++Lane) for(int32 Lane=0;Lane<Count;++Lane)
@@ -3802,6 +3868,24 @@ public:
constexpr float ProxyBandEdge=DetailSupremum+0.125f; constexpr float ProxyBandEdge=DetailSupremum+0.125f;
for(int32 Lane=0;Lane<Count;++Lane) for(int32 Lane=0;Lane<Count;++Lane)
{ {
bool bPassageMayAffectLane=false;
if(TileQueries)
{
const double Q=static_cast<double>(
VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
const double SampleX=static_cast<double>(XStart+Lane*XStep)*Q;
const double SampleY=static_cast<double>(Y)*Q;
const double SampleZ=static_cast<double>(Z)*Q;
for(const std::size_t passageIndex:TileQueries->routePassageIndices())
if(TileQueries->routeFeatureMayAffect(passageIndex,
SampleX,SampleY,SampleZ))
{bPassageMayAffectLane=true;break;}
}
if(bPassageMayAffectLane)
{
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
continue;
}
const size_t Column=ColumnStart+static_cast<size_t>(Lane*XStep); const size_t Column=ColumnStart+static_cast<size_t>(Lane*XStep);
const size_t Word=Column*WordsPerZ+WordOffset; const size_t Word=Column*WordsPerZ+WordOffset;
const bool bRock=(RockWords[Word]&Mask)!=0; const bool bRock=(RockWords[Word]&Mask)!=0;
@@ -0,0 +1,150 @@
# L175 — passage tile seams
## Cause
Tile B took the packed base-only X8 path because its fast predicate checked only
that edits were empty and the requested X/Y/Z samples were inside the cached
base-field bounds. It did not ask whether any lane was inside a passage feature
bound. The measured trace for tile `(0,1,-32)`, at the shared `z=-2048` plane,
has `fast=1`, all base-bound checks true, `route_candidates=2`, and
`feature_aabb=1`; it then reports `path=packed_x8_base_only` with `-4.125`, while
the composed scalar result is `+0.781364381`. The corresponding seam sample is
`(16.5, 28.5, -512 m)`, a `4.90636438` density delta. See
`Saved/Linux/l175/dispatch-trace/seed0-seam-only-trace2.log`.
The bound and edge hypotheses did not fit the trace. Route influence reach is
`0.75 m shell + 0.21 m wall relief + 0.50/6 m corner support + 0.01 m guard =
1.053333… m`; route boxes add the maximum tube radius and a 0.01-core-voxel
rounding margin. Landing chamber bounds include their full radius, floor
gradient, clear height, and reach. Point and tile-box comparisons are inclusive.
The tile query covers `origin..origin+extent` plus the one-step normal stencil,
also inclusive. Those bounds are in core-voxel/subvoxel units. No unit, LOD-step,
or shared-edge omission was found.
A second composition-order issue was measured: a tile can list a passage because
another segment overlaps it, then compose that passage's landing chamber before
checking whether the current sample lies in the route feature AABB. Different
candidate lists could therefore alter samples outside the route segment. This
was fixed with the dispatch issue.
The reach ablation (`reach-proof-seed0-final.log`) covered 512 tiles and
21,952,000 samples at each of 1.5, 2.0, 2.5, and 4.0 m. All four comparisons
passed, with 150,096 shared crossings and no changed samples, crossings, mesh
edges, or normals. This supports the source-derived bound; the direct dispatch
trace identifies the root cause.
## Fix
In the shared layer, X8 lanes inside a passage route AABB now use the fully
composed scalar path. Only contiguous runs proven outside all route AABBs are
delegated to packed X8. The packed source also fails closed to scalar when it
has resolved passages but no tile-query proof. Before passage composition, a
sample outside a listed passage's route AABB now skips that passage entirely,
including its landing chamber.
The L175 seed-0 counter measured 23 route-bound LOD1 dispatch scopes. The old
predicate would admit all 23 to packed X8. With the fix, 22 scopes retain at
least one proven-unaffected packed run; 16 scopes send affected lanes through
scalar composition, and one scope has no unaffected packed run. These are
scopes where the X8 dispatcher ran, not a census of every generated tile.
`WorldAssemblyEvaluatorVersion` is now 241. Clang and GCC builds both use source
stamp `4d2512ec24add53e06effe1bd82dab5efafa9c450c815d3fc933c58e43327342`.
Across 18 canonical runs, all compiler/worker hashes matched for owner4
surface-world, owner2 surface-world, and owner2 floating-islands at W1/W8/W12.
Each owner2 v241 baseline changes 3 canonical rows from v240; both new baseline
files and the `verify.py` references are updated.
## 16-seed seam results
The Tunnel Network and Vertical Shafts route audits ran for seeds 0–15. Across
all seeds: **99,269 adjacent pairs, 121,604,525 shared samples, and 0 natural
mismatches**. The injected control still works: it creates one marked mismatch
and reports `injected=DETECTED` (`injected-seed0-final.log`).
| Seed | Pairs | Shared samples | Mismatches | Seam coverage | TN route | VS route |
|---:|---:|---:|---:|:---:|:---:|:---:|
| 0 | 7,579 | 9,284,275 | 0 | PASS | PASS | PASS |
| 1 | 6,417 | 7,860,825 | 0 | PASS | PASS | PASS |
| 2 | 5,273 | 6,459,425 | 0 | Coverage only | FAIL | PASS |
| 3 | 4,612 | 5,649,700 | 0 | PASS | PASS | PASS |
| 4 | 5,092 | 6,237,700 | 0 | PASS | PASS | PASS |
| 5 | 5,501 | 6,738,725 | 0 | Coverage only | PASS | PASS |
| 6 | 7,236 | 8,864,100 | 0 | Coverage only | FAIL | PASS |
| 7 | 6,912 | 8,467,200 | 0 | PASS | PASS | PASS |
| 8 | 6,537 | 8,007,825 | 0 | PASS | PASS | PASS |
| 9 | 7,080 | 8,673,000 | 0 | PASS | PASS | PASS |
| 10 | 7,715 | 9,450,875 | 0 | PASS | PASS | PASS |
| 11 | 5,933 | 7,267,925 | 0 | PASS | PASS | PASS |
| 12 | 6,703 | 8,211,175 | 0 | Coverage only | PASS | PASS |
| 13 | 5,916 | 7,247,100 | 0 | PASS | FAIL | PASS |
| 14 | 4,720 | 5,782,000 | 0 | PASS | PASS | PASS |
| 15 | 6,043 | 7,402,675 | 0 | Coverage only | PASS | PASS |
The strict seam audit status is PASS for 11/16 seeds. Seeds 2, 5, 6, 12, and
15 fail only its per-disc coverage requirement: one disc has LOD0 samples but
zero LOD1 samples on shared faces. All six end runs have both LODs covered in
each seed; there are no exempt pairs. The measured result is that no LOD1
shared-face sample was tagged to those discs. The likely explanation is that
the discs do not cross a sampled LOD1 seam; that is an inference from the
coverage output. This is missing feature-on-seam coverage, not a measured
density mismatch. The audit was left strict. Tunnel Network route gates pass
13/16 and Vertical Shafts pass 16/16. Overall sweep rows remain failed by the
existing passage, naturalness, self-overlap, route, and count gates; no step-2,
step-3, or LayerPlanner work was changed.
## Mainline `assembler/world` verdict
**Yes, the mainline at `e3359d2` has the same unsafe dispatch structure.** Its
packed X8 predicate checks cached base bounds without checking passage
influence, and passage composition happens later. The untouched setup stopped
before the seam audit (`grade_rejected=3460`, `sweepable_candidates=0`). In the
throwaway `/tmp/l175-mainline` build only, a scratch environment gate selected
the best of 148 candidates while explicitly skipping grade, capsule, and
support checks. With that setup bypass, the bounded LOD0 seam audit examined
129 candidate tile pairs, compared 49 shared pairs/60,025 samples, and found
0 mismatches. This limited sample does not show the mainline is safe; it did not
exercise the original 16-seed sweep.
The minimal mainline fix to schedule is the same shared-layer change: test each
X8 lane against passage bounds before delegating, and skip landing-chamber
composition for samples outside the passage feature bound. No `assembler/world`
or mainline source was edited.
## Timings and remaining checks
The L80 comparison used one warm-up and five interleaved measured runs for each
binary, on all four owner-layer camera positions at W8 and W12 (96 runs total).
The window was Nov 4, 11:05–11:32 local; pre-run load was 0.42 and CPU idle was
99.4%. The stale prebuilt v240 binary failed its snapshot identity check, so the
baseline was rebuilt from committed L174 `0ff8d98` in
`Saved/Linux/l175/base-v240-src` (stamp `bf2d3bd4…`). Both versions use
`--seed=0` to build the same plan locally.
| Layer | Workers | LOD0–2 median v240 → v241 | Change | Whole-view median v240 → v241 | Change |
|---:|---:|---:|---:|---:|---:|
| 0 | 8 | 16.350 → 17.249 s | +5.50% | 17.361 → 18.241 s | +5.07% |
| 0 | 12 | 16.075 → 16.093 s | +0.11% | 17.037 → 17.046 s | +0.05% |
| 1 | 8 | 15.762 → 15.787 s | +0.16% | 16.676 → 16.645 s | −0.19% |
| 1 | 12 | 17.220 → 17.258 s | +0.22% | 18.048 → 18.083 s | +0.19% |
| 2 | 8 | 15.696 → 15.862 s | +1.06% | 16.517 → 16.655 s | +0.84% |
| 2 | 12 | 17.051 → 17.055 s | +0.03% | 17.803 → 17.802 s | −0.01% |
| 3 | 8 | 15.733 → 17.124 s | +8.84% | 16.549 → 17.959 s | +8.52% |
| 3 | 12 | 17.150 → 16.611 s | −3.15% | 17.949 → 17.402 s | −3.05% |
The timing result is inconclusive. A same-binary A/A control at layer 0/W8,
after one warm-up and 10 measured runs, ranged 15.560–17.747 s for LOD0–2
readiness and 16.619–18.844 s for whole-view readiness (Nov 4, 11:34–11:37).
That 13–14% same-binary spread is as large as or larger than the base/v241
median differences. The raw timing matrix and A/A logs are in
`Saved/Linux/l175/timing/`.
`verify.py fast` passed `streaming-admission-order-mid-pass-refill`, then
stopped on the expected first step-2 owner audit (`owner-floating-islands-style2-clang`).
That run reports one unsupported upper landing probe, 11 tube overlap pairs,
and one end-turn violation; source stamp, recipe, and style checks passed. The
canonical hash gate was not reached. Final-source Clang/GCC builds and hashes,
the 16-seed sweeps, injection control, and `git diff --check` ran. No Unreal
build or Windows compile was run.
Changed `Source/` files: **none**.
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""Capture v241 canonical owner 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/l175/hash-cross-compiler"
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 / "Standalone/bin/linux/VoxelForgeCoreHeadless-l175-final-clang",
"gcc": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l175-final-gcc",
}
BASELINE_ROOT = ROOT / "Standalone/Tools/verify/baselines/owner-hash/canonical"
BASELINES = {
("owner2", "surface-world"): (
"owner-hash-surface-world-style1-v240.baseline",
"owner-hash-surface-world-style1-v241.baseline"),
("owner2", "floating-islands"): (
"owner-hash-floating-islands-style2-v240.baseline",
"owner-hash-floating-islands-style2-v241.baseline"),
}
CANONICAL_RE = re.compile(
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
ALL_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 run(dataset: str, recipe: str, compiler: str, workers: int,
expected_stamp: str) -> dict:
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(BINARIES[compiler]), 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}",
]
if dataset == "owner4":
command.append("--seed=0")
started = time.monotonic()
try:
with log.open("w", encoding="utf-8") as output:
result = subprocess.run(command, cwd=ROOT, stdout=output,
stderr=subprocess.STDOUT, check=False, timeout=1740)
code: int | str = result.returncode
except subprocess.TimeoutExpired:
code = "OUTER_TIMEOUT"
content = log.read_text(encoding="utf-8", errors="replace") if log.is_file() else ""
hashes = ALL_HASH_RE.findall(content)
canonical = CANONICAL_RE.findall(content)
stamp = STAMP_RE.search(content)
row = {
"dataset": dataset, "recipe": recipe, "compiler": compiler,
"workers": workers, "exit_code": code,
"run_passed": code == 0 and len(canonical) == 12 and len(hashes) == 13
and stamp is not None and stamp.group(1) == expected_stamp,
"canonical_hashes": canonical, "all_pipeline_hashes": hashes,
"source_stamp": stamp.group(1) if stamp else None,
"elapsed_s": round(time.monotonic() - started, 3),
"log": str(log.relative_to(ROOT)),
}
print("[L175CanonicalHashRun] " + json.dumps(row, sort_keys=True), flush=True)
return row
def main() -> int:
if any(not path.is_file() for path in (*DATASETS.values(), *BINARIES.values())):
raise SystemExit("missing v241 owner input or final compiler binary")
OUT.mkdir(parents=True, exist_ok=True)
expected_stamp = subprocess.check_output([
sys.executable, str(ROOT / "Standalone/Tools/verify/source_stamp.py"),
"stamp", "--root", str(ROOT)], text=True).strip()
rows = []
groups = {}
partial = OUT / "runs-partial.json"
for dataset, recipes in RECIPES.items():
for recipe in recipes:
current = []
for workers in (1, 8, 12):
for compiler in ("clang", "gcc"):
row = run(dataset, recipe, compiler, workers, expected_stamp)
rows.append(row)
current.append(row)
partial.write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8")
deterministic = (len(current) == 6 and all(row["run_passed"] for row in current)
and all(row["all_pipeline_hashes"] == current[0]["all_pipeline_hashes"]
for row in current))
group = f"{dataset}/{recipe}"
groups[group] = {"all_compilers_workers_match": deterministic,
"canonical_rows": len(current[0]["canonical_hashes"]) if current else 0,
"pipeline_rows": len(current[0]["all_pipeline_hashes"]) if current else 0}
print("[L175CanonicalHashGroup] " + json.dumps(
{"group": group, **groups[group]}, 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())
if passed:
for key, (old_name, new_name) in BASELINES.items():
canonical = next(row["canonical_hashes"] for row in rows
if (row["dataset"], row["recipe"], row["compiler"], row["workers"])
== (*key, "clang", 12))
(BASELINE_ROOT / new_name).write_text("\n".join(canonical) + "\n",
encoding="utf-8")
old = CANONICAL_RE.findall((BASELINE_ROOT / old_name).read_text(
encoding="utf-8", errors="replace"))
groups[f"{key[0]}/{key[1]}"]["changed_v240_rows"] = sum(
a != b for a, b in zip(old, canonical))
summary = {"status": "PASS" if passed else "FAIL",
"source_stamp": expected_stamp, "runs": rows, "groups": groups}
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
encoding="utf-8")
print(f"[L175CanonicalHashSummary] status={summary['status']} runs={len(rows)} "
f"groups={len(groups)}", flush=True)
return 0 if passed else 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""Five interleaved L80 readiness measurements per owner layer and worker count."""
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/l175/timing"
DATA = ROOT / "Standalone/data/owner_world.json"
BINARIES = {
"base": ROOT / "Saved/Linux/l175/base-v240-src/Standalone/bin/linux/VoxelForgeCoreHeadless-l175-v240-base",
"l175": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l175-final-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 = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
FIELDS_RE = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
NEAR_RE = re.compile(
r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) "
r"cold_ready_s=([0-9.]+)$", re.M)
def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict:
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
label = f"strate{strate}-w{workers}-{version}-{phase}{replicate}"
log = OUT / f"{label}.log"
command = [
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
"timeout", "1740s", str(BINARIES[version]), str(DATA),
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
"--seed=0",
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, near = PIPELINE_RE.search(content), NEAR_RE.search(content)
if result.returncode or pipeline is None or near is None:
raise RuntimeError(f"timing run failed: {log}; exit={result.returncode}")
fields = dict(FIELDS_RE.findall(pipeline.group(1)))
row = {
"strate": strate, "workers": workers, "version": version,
"phase": phase, "replicate": replicate,
"lod02_ready_s": float(near.group(3)),
"whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")),
"wall_s": round(time.monotonic() - started, 3),
"log": str(log.relative_to(ROOT)),
}
print("[L175TimingRun] " + 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] if len(values) > 4 else 0)
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, *BINARIES.values())):
raise SystemExit("missing owner input or timing binary")
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"[L175TimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True)
if load_1m >= 1.5:
raise SystemExit("timing refused: load is at or above the L80 quiet-load limit")
rows = []
for strate in range(4):
for workers in (8, 12):
for version in BINARIES:
rows.append(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):
old = [row for row in measured if row["strate"] == strate
and row["workers"] == workers and row["version"] == "base"]
new = [row for row in measured if row["strate"] == strate
and row["workers"] == workers and row["version"] == "l175"]
item = {"strate": strate, "workers": workers, "replicates_per_version": 5}
for key in ("lod02_ready_s", "whole_view_ready_s"):
old_med = statistics.median(row[key] for row in old)
new_med = statistics.median(row[key] for row in new)
item[f"base_{key}_median"] = old_med
item[f"l175_{key}_median"] = new_med
item[f"delta_pct_{key}"] = round(100.0 * (new_med / old_med - 1.0), 2)
item[f"base_{key}_values"] = [row[key] for row in old]
item[f"l175_{key}_values"] = [row[key] for row in new]
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())
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Measure same-binary timing spread for one L80 owner view and worker count."""
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/l175/timing/aa-control"
DATA = ROOT / "Standalone/data/owner_world.json"
BINARY = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l175-final-clang"
CAMERA = "8.000000000000,16.000000000000,-372.262695312500,0,0"
PIPELINE_RE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
FIELDS_RE = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
NEAR_RE = re.compile(
r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) "
r"cold_ready_s=([0-9.]+)$", re.M)
def run(label: str, phase: str, replicate: int) -> dict:
log = OUT / f"{label}-{phase}{replicate}.log"
command = [
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
"timeout", "1740s", str(BINARY), str(DATA),
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
"--seed=0", "--workers=8", "--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, near = PIPELINE_RE.search(content), NEAR_RE.search(content)
if result.returncode or pipeline is None or near is None:
raise RuntimeError(f"A/A timing run failed: {log}; exit={result.returncode}")
fields = dict(FIELDS_RE.findall(pipeline.group(1)))
row = {
"label": label, "phase": phase, "replicate": replicate,
"lod02_ready_s": float(near.group(3)),
"whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")),
"wall_s": round(time.monotonic() - started, 3),
"log": str(log.relative_to(ROOT)),
}
print("[L175TimingAA] " + 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 input or final Clang binary")
OUT.mkdir(parents=True, exist_ok=True)
rows = [run("warmup", "warmup", 0)]
for replicate in range(1, 11):
rows.append(run("same-binary", "measured", replicate))
(OUT / "runs-partial.json").write_text(
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
summary = {}
for key in ("lod02_ready_s", "whole_view_ready_s"):
values = [row[key] for row in rows if row["phase"] == "measured"]
summary[key] = {"values": values, "min": min(values), "max": max(values)}
(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())
@@ -15,3 +15,5 @@ verification gates. The gates now read this tracked directory instead of
level-end-run and passage-overlap changes. level-end-run and passage-overlap changes.
- `owner-hash/canonical/owner-hash-*-v240.baseline`: L174 canonical owner hashes after - `owner-hash/canonical/owner-hash-*-v240.baseline`: L174 canonical owner hashes after
Design50 steps 1–2; step 0 retained the v239 geometry and hashes. Design50 steps 1–2; step 0 retained the v239 geometry and hashes.
- `owner-hash/canonical/owner-hash-*-v241.baseline`: L175 canonical owner hashes after
passage composition was made consistent across tile seams.
@@ -0,0 +1,12 @@
[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173
[PipelineWholeViewHash] tiles=2370 density=0x661b0ed5e4c0295c mesh=0xaf52ebbe42f68030 payload=0x3391b41e1c0542f5 topology=0x85deb8c9f8e0676e
[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a
[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630
[PipelineLODHash] lod=2 tiles=320 density=0xf5dcecf9f4b95e55 mesh=0xf9d0320de1246253 payload=0x3390ce42cba05749 topology=0xc536d6371958f5dd
[PipelineLODHash] lod=3 tiles=208 density=0x25c95480ee134e4c mesh=0xcc56bbe02e024573 payload=0xa54a32c0bd0b2a26 topology=0xf4508c7b069f9875
[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494
[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573
[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618
[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b
[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04
[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries
@@ -0,0 +1,12 @@
[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae
[PipelineWholeViewHash] tiles=2370 density=0xcea15df21f3de284 mesh=0x7f3af937f66b4120 payload=0x0df48eeb9c361f1e topology=0x187ec3b5fee288a7
[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3
[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100
[PipelineLODHash] lod=2 tiles=320 density=0x3d02f2de40ac17eb mesh=0xeb3260907c45cbea payload=0x742d68cef4e33d90 topology=0x94a5c298e959adab
[PipelineLODHash] lod=3 tiles=208 density=0x4a840012d34cc75b mesh=0xd5672aef7e6c7166 payload=0x8f4b7d83b106ab63 topology=0xe7759acb1434b631
[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08
[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89
[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64
[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72
[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335
[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries
+4 -4
View File
@@ -125,11 +125,11 @@ RECIPE_RE = re.compile(
def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]: def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]:
if recipe == "surface-world" and style == 1: if recipe == "surface-world" and style == 1:
return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v240.baseline", return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v241.baseline",
"L174-v240-Design50-steps1-2") "L175-v241-passage-tile-seam-fix")
if recipe == "floating-islands" and style == 2: if recipe == "floating-islands" and style == 2:
return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v240.baseline", return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v241.baseline",
"L174-v240-Design50-steps1-2") "L175-v241-passage-tile-seam-fix")
raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}") raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}")