diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 2fcb0d8..526c5b9 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -179,6 +179,11 @@ public: std::size_t segmentIndex=0; std::array minimumSubvoxels{},maximumSubvoxels{}; }; + struct FSegmentRangeCache + { + std::ptrdiff_t first=-1,end=-1; + std::vector activeNodes; + }; FScopedPassageTileQueries(const FStandaloneShapeWorld& shape, const std::array& minimumSubvoxels, @@ -201,6 +206,7 @@ public: passageColumns_=passageRoutes_; passageMouthSupports_=passageRoutes_; passageMouthSupportSegments_.resize(passageRoutes_.size()); + passageSegmentRangeCaches_.resize(passageRoutes_.size()); if(shape.ResolvedLayout&&!shape.ResolvedLayout->passages.empty()) { constexpr double Q=static_cast( @@ -281,6 +287,22 @@ public: return passageIndex* segmentRangeNodeMask(std::size_t passageIndex, + std::ptrdiff_t first,std::ptrdiff_t end,int priority) const + { + if(passageIndex>=passageSegmentRangeCaches_.size() + ||priority<0||priority>=4)return nullptr; + FSegmentRangeCache& cache=passageSegmentRangeCaches_[passageIndex][ + static_cast(priority)]; + if(cache.first!=first||cache.end!=end) + { + cache.first=first; + cache.end=end; + cache.activeNodes=shape_->BuildPassageSegmentRangeNodeMask( + passageIndex,first,end); + } + return &cache.activeNodes; + } bool hasMouthSupport(std::size_t passageIndex) const {return passageIndex cavernFeatureIndices_; std::vector passageRoutes_,passageColumns_,passageMouthSupports_; std::vector> passageMouthSupportSegments_; + mutable std::vector> passageSegmentRangeCaches_; std::vector upperMouthSupportPassages_; std::size_t routeSegmentCandidateCount_=0; bool routeFeatureOverlap_=false,columnZOverlap_=false; @@ -1513,17 +1536,46 @@ public: ?std::clamp(-(qz-(nearestA.positionSubvoxels[2]+bestT*( static_cast(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2]))) /distanceToCenter,0.0,1.0):0.0; - const float relief=VF_FieldPassageWallRelief(static_cast(X), - static_cast(Y),static_cast(Z), - static_cast(Owner?Owner->Seed:0)); - const float mouthSurfaceRelief=VF_FieldSurfaceRelief(static_cast(X)*1.5f, - static_cast(Y)*1.5f,static_cast(Z)*1.5f, - static_cast(Owner?Owner->Seed:0)^0x4D4F5554u); - const float mouthTerrainRelief=mouthSurfaceRelief; - const float mouthTreadFieldRelief=VF_FieldSurfaceRelief(static_cast(X)*0.75f, - static_cast(Y)*0.75f,static_cast(Z)*0.75f, - static_cast(Owner?Owner->Seed:0)^0x54485244u); - if(passageProfile)passageProfile->passageReliefFieldCalls+=3; + const std::uint32_t passageReliefSeed=static_cast(Owner?Owner->Seed:0); + float cachedWallRelief=0.0f,cachedMouthSurfaceRelief=0.0f, + cachedMouthTreadFieldRelief=0.0f; + bool hasCachedWallRelief=false,hasCachedMouthSurfaceRelief=false, + hasCachedMouthTreadFieldRelief=false; + const auto getWallRelief=[&]() + { + if(!hasCachedWallRelief) + { + cachedWallRelief=VF_FieldPassageWallRelief(static_cast(X), + static_cast(Y),static_cast(Z),passageReliefSeed); + hasCachedWallRelief=true; + if(passageProfile)++passageProfile->passageReliefFieldCalls; + } + return cachedWallRelief; + }; + const auto getMouthSurfaceRelief=[&]() + { + if(!hasCachedMouthSurfaceRelief) + { + cachedMouthSurfaceRelief=VF_FieldSurfaceRelief( + static_cast(X)*1.5f,static_cast(Y)*1.5f, + static_cast(Z)*1.5f,passageReliefSeed^0x4D4F5554u); + hasCachedMouthSurfaceRelief=true; + if(passageProfile)++passageProfile->passageReliefFieldCalls; + } + return cachedMouthSurfaceRelief; + }; + const auto getMouthTreadFieldRelief=[&]() + { + if(!hasCachedMouthTreadFieldRelief) + { + cachedMouthTreadFieldRelief=VF_FieldSurfaceRelief( + static_cast(X)*0.75f,static_cast(Y)*0.75f, + static_cast(Z)*0.75f,passageReliefSeed^0x54485244u); + hasCachedMouthTreadFieldRelief=true; + if(passageProfile)++passageProfile->passageReliefFieldCalls; + } + return cachedMouthTreadFieldRelief; + }; const auto routeSegmentLengthMetres=[&](std::ptrdiff_t routeIndex) { const auto& before=passage.controlPoints[static_cast(routeIndex)]; @@ -1639,21 +1691,33 @@ public: if(passageProfile)++passageProfile->passageSmoothstepEvaluations; const double floorReliefWeight=floorReliefT*floorReliefT *(3.0-2.0*floorReliefT); + const double wallOpening=1.0-verticalWallAlignment; + const double wallReliefWeight=0.05+0.95*wallOpening; + const bool shortSpanCarve=IsPassageShortSpanSegment(passageIndex, + static_cast(bestIndex)); + float carveInputAfterBridge=composed; + if(shortSpanCarve) + carveInputAfterBridge=std::min(composed,static_cast( + PassageBridgeSignedDistance(passageIndex, + static_cast(bestIndex),X,Y,Z)/Q)); + const double maximumWallReliefOffset=0.21*qPerMetre*wallReliefWeight; + const double maximumMouthFloorReliefOffset=upperMouthFloorFade + ?0.20*qPerMetre*mouthFloorBlend*floorReliefWeight:0.0; + const double maximumCarvedDensity=-(bestSignedDistance + -maximumWallReliefOffset-maximumMouthFloorReliefOffset)/Q; + const bool wallReliefCanChangeCarve=!std::isfinite(maximumCarvedDensity) + ||maximumCarvedDensity>static_cast(carveInputAfterBridge); + const float relief=wallReliefCanChangeCarve?getWallRelief():0.0f; + const float mouthTerrainRelief=upperMouthFloorFade + ?getMouthSurfaceRelief():0.0f; const double floorReliefOffset=std::clamp( static_cast(mouthTerrainRelief),-1.0,1.0) *0.20*qPerMetre*mouthFloorBlend*floorReliefWeight *(upperMouthFloorFade?1.0:0.0); - const double wallOpening=1.0-verticalWallAlignment; - const double wallReliefWeight=0.05+0.95*wallOpening; const double roughSignedDistance=bestSignedDistance +static_cast(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight +floorReliefOffset; - if(IsPassageShortSpanSegment(passageIndex,static_cast(bestIndex))) - { - const double bridgeDistance=PassageBridgeSignedDistance(passageIndex, - static_cast(bestIndex),X,Y,Z); - composed=std::min(composed,static_cast(bridgeDistance/Q)); - } + if(shortSpanCarve)composed=carveInputAfterBridge; // Leave native crater ground at the lip. The bore opens through the walls // immediately, while its floor cut eases in along the passage centerline. double mouthCarveFade=1.0; @@ -1894,7 +1958,7 @@ public: double supportHalfWidth=std::min(radius, supportHalfWidthMetres*qPerMetre); const double shoulderWarp=mouthTread - ?std::clamp(static_cast(mouthSurfaceRelief),-1.0,1.0) + ?std::clamp(static_cast(getMouthSurfaceRelief()),-1.0,1.0) *0.12*qPerMetre:0.0; supportHalfWidth=mouthTread ?std::clamp(supportHalfWidth+shoulderWarp, @@ -1950,7 +2014,7 @@ public: -0.80*qPerMetre,0.80*qPerMetre); const double floorQ=baseFloorQ +(mouthTread?std::clamp(nativeTreadOffset - +std::clamp(static_cast(mouthTreadFieldRelief),-1.0,1.0) + +std::clamp(static_cast(getMouthTreadFieldRelief()),-1.0,1.0) *0.40*qPerMetre,-0.80*qPerMetre,0.0):0.0); const double floorVoxels=floorQ/Q; const bool signedDistanceSupport=sparseRouteSupport||mouthTread; @@ -2073,7 +2137,7 @@ public: if(nativeRestoreCapsuleClear) { const double nativeSurfaceDetail=std::clamp( - static_cast(mouthTerrainRelief),-1.0,1.0) + static_cast(getMouthSurfaceRelief()),-1.0,1.0) *0.18*qPerMetre/Q; const float nativeGroundWithRelief=static_cast( (supportNeedsDeferral?std::max(-1.0f,density):density) @@ -4836,6 +4900,42 @@ private: return nodeIndex; } + std::vector BuildPassageSegmentRangeNodeMask( + std::size_t passageIndex,std::ptrdiff_t first,std::ptrdiff_t end) const + { + std::vector active; + if(first>=end||passageIndex>=PassageNearestNodes.size() + ||PassageNearestNodes[passageIndex].empty())return active; + const auto& nodes=PassageNearestNodes[passageIndex]; + active.assign(nodes.size(),0); + const auto visit=[&](auto&& self,int nodeIndex)->bool + { + if(nodeIndex<0)return false; + const std::size_t index=static_cast(nodeIndex); + const auto& node=nodes[index]; + if(static_cast(node.maximumSegment)(node.minimumSegment)>=end)return false; + bool hasCandidate=false; + if(node.left<0) + { + for(const std::size_t segment:node.segments) + hasCandidate=hasCandidate + ||(static_cast(segment)>=first + &&static_cast(segment)(hasCandidate); + return hasCandidate; + }; + visit(visit,0); + return active; + } + int BuildPassageProofNode(std::size_t first,std::size_t end) { FPassageProofNode node;node.first=first;node.end=end; @@ -5410,35 +5510,53 @@ private: template void VisitNearestPassageSpatialNode(std::size_t passageIndex,int nodeIndex, std::size_t first,std::size_t end,double qx,double qy,double qz,int priority, - double& bestDistance,FVisit& visit) const + const std::vector* activeNodes,double& bestDistance, + FVisit& visit) const { if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return; const auto& nodes=PassageNearestNodes[passageIndex]; - const auto& node=nodes[static_cast(nodeIndex)]; - if(node.maximumSegment=end)return; - const bool allSegmentsInRange=first<=node.minimumSegment - &&node.maximumSegment=end)return; + const std::size_t nodeOffset=static_cast(nodeIndex); + if(activeNodes&&(nodeOffset>=activeNodes->size()||!(*activeNodes)[nodeOffset]))return; + const auto& node=nodes[nodeOffset]; + if(!activeNodes&&(node.maximumSegment=end))return; + auto candidate=node.segments.begin(); + if(!activeNodes) + { + const bool allSegmentsInRange=first<=node.minimumSegment + &&node.maximumSegment=end)return; + } const double lowerBound=PassageNearestNodeLowerBound(node,qx,qy,qz); if(lowerBound>bestDistance)return; if(node.left<0) { - for(auto it=candidate;it!=node.segments.end()&&*it=end)continue; visit(*it,priority); + } return; } - const double leftBound=PassageNearestNodeLowerBound( - nodes[static_cast(node.left)],qx,qy,qz); - const double rightBound=PassageNearestNodeLowerBound( - nodes[static_cast(node.right)],qx,qy,qz); + const bool leftActive=!activeNodes + ||(static_cast(node.left)size() + &&(*activeNodes)[static_cast(node.left)]); + const bool rightActive=!activeNodes + ||(static_cast(node.right)size() + &&(*activeNodes)[static_cast(node.right)]); + const double leftBound=leftActive?PassageNearestNodeLowerBound( + nodes[static_cast(node.left)],qx,qy,qz) + :std::numeric_limits::infinity(); + const double rightBound=rightActive?PassageNearestNodeLowerBound( + nodes[static_cast(node.right)],qx,qy,qz) + :std::numeric_limits::infinity(); const int nearChild=leftBound<=rightBound?node.left:node.right; const int farChild=leftBound<=rightBound?node.right:node.left; VisitNearestPassageSpatialNode(passageIndex,nearChild,first,end,qx,qy,qz, - priority,bestDistance,visit); + priority,activeNodes,bestDistance,visit); VisitNearestPassageSpatialNode(passageIndex,farChild,first,end,qx,qy,qz, - priority,bestDistance,visit); + priority,activeNodes,bestDistance,visit); } template @@ -5449,8 +5567,12 @@ private: if(first>last||passageIndex>=PassageNearestNodes.size() ||PassageNearestNodes[passageIndex].empty())return; auto callback=std::forward(visit); + const auto* tileQueries=ActivePassageTileQueries; + const auto* activeNodes=tileQueries&&tileQueries->matches(*this) + ?tileQueries->segmentRangeNodeMask(passageIndex,first,last+1,priority):nullptr; VisitNearestPassageSpatialNode(passageIndex,0,static_cast(first), - static_cast(last+1),qx,qy,qz,priority,bestDistance,callback); + static_cast(last+1),qx,qy,qz,priority,activeNodes, + bestDistance,callback); } bool PassagePotentialWithinNode(std::size_t passageIndex,int nodeIndex, @@ -12312,6 +12434,11 @@ private: static_cast(Stats.WalkSamples)); return Stats.WalkFailures==0; } + // An imported Unreal-resolved layout is the canonical route source. An empty + // passage list is meaningful for blocked controls, so never fall back to the + // regenerated legacy owner route in that case. Recipe overrides without an + // exported Core plan retain their generated owner route audit. + if(Owner&&Owner->bHasUnrealResolvedCorePlan&&ResolvedLayout)return true; constexpr double SampleStep=0.5; uint32 PassageIndex=0; for(const FVoxelPassage& P:Owner->Manager->GetPassages()) diff --git a/Standalone/Core/src/StandaloneSnapshotAdapter.h b/Standalone/Core/src/StandaloneSnapshotAdapter.h index f631508..7385d87 100644 --- a/Standalone/Core/src/StandaloneSnapshotAdapter.h +++ b/Standalone/Core/src/StandaloneSnapshotAdapter.h @@ -11,5 +11,6 @@ namespace VoxelForgeCore // the same command-line overrides as the viewer's reference loader. ResolvedWorldSnapshot MakeStandaloneSnapshotFromParsedJson(const VFJson::Value& jsonRoot, std::int32_t seed,std::int32_t clipRadius,std::int32_t maxLodOverride, - bool roundTunnels,bool enhancedTunnelWalls,std::string& error); + bool roundTunnels,bool enhancedTunnelWalls,std::string& error, + bool hasUnrealResolvedCorePlan=false); } diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index ad27766..dc77689 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -926,10 +926,12 @@ namespace VoxelForgeCore ResolvedWorldSnapshot MakeStandaloneSnapshotFromParsedJson(const VFJson::Value& jsonRoot, std::int32_t seed,std::int32_t clipRadius,std::int32_t maxLodOverride, - bool roundTunnels,bool enhancedTunnelWalls,std::string& error) + bool roundTunnels,bool enhancedTunnelWalls,std::string& error, + bool hasUnrealResolvedCorePlan) { auto owner=std::make_shared(); if(!StandaloneOwnerLoader::Resolve(jsonRoot,*owner,error))return {}; + owner->bHasUnrealResolvedCorePlan=hasUnrealResolvedCorePlan; std::shared_ptr worldAssembly; if(jsonRoot.Get("world_assembly").IsObject()) { diff --git a/Standalone/Research/WorldAssemblerM3/Linux82/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux82/REPORT.md new file mode 100644 index 0000000..3a29d15 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux82/REPORT.md @@ -0,0 +1,50 @@ +# Linux82 — samples near passages + +## Changes + +- Kept the 3D relief definition. The wall relief noise is skipped only when its hard-bounded maximum contribution (including mouth-floor relief and the bridge input) cannot change the final `max`. Mouth surface and tread noise are evaluated only on paths that use them. This is exact; no 2D relief approximation or world change was made. +- Cached exact active BVH-node masks per tile, passage, segment range, and priority. Repeated samples reuse the mask and skip the per-sample `lower_bound`; candidate segment ordering and distance tests are unchanged. +- The full-view audit now imports the exported Core plan and resolved layout without a recipe override. Snapshot creation preserves that export provenance so generated recipe overrides keep their own route audit. + +## Passage census + +Mean near-LOD passage share of weighted sampler ticks; L81 has two exact-mode runs and L82 has three. + +| World | L81 exact | L82 exact | Change | +|---|---:|---:|---:| +| FI | 64.56% | 58.67% | −5.89 pp | +| TN | 65.35% | 58.40% | −6.95 pp | + +## Cold readiness + +Three interleaved measured pairs per stratum; medians in seconds. + +| Stratum | LOD0–2 L81 → L82 | Whole view L81 → L82 | +|---|---:|---:| +| SW | 1.158 → 1.110 | 2.525 → 2.414 | +| FI | 2.864 → 2.505 | 4.240 → 3.797 | +| TN | 2.879 → 2.535 | 4.042 → 3.632 | +| VS | 1.232 → 1.224 | 2.261 → 2.227 | + +All 960 LOD0–1 tile hashes per stratum matched L81 for density, mesh, payload, and topology; whole signatures matched with zero mismatches. The exact path kept evaluator version 214 and made no world-data change. + +## TN comparison + +Five interleaved TN-only LOD0–2 pairs (`max-level=2`, default scheduling on both binaries): `90378a5` median 3.078 s; `9cb5e53` median 2.747 s. All five paired deltas favored `9cb5e53` (median −0.281 s). The 3.18→3.51 s slowdown was not reproduced in the requested comparison; this A/B does not isolate an L80-to-L81 pair. + +## Audit and verification + +- Canonical full-view walkability: 21,425/21,425 samples passed; all three resolved passage audits passed. The stale 26-point root route was not used. +- Fast tier: PASS, 298.1 s. +- Linux `VFHostEditor` build after timing: PASS, 27/27 actions in 70.98 s with `-NoUBA -MaxParallelActions=4`; log rotation was redirected to `/tmp` after the default config path proved read-only. Three existing RealtimeMesh deprecation warnings. +- Final source stamp: `99fcbd2fd46e0bb3e250f3ad67e98ade0545e419193f987550356015714c6f1b`. + +## Failed attempts + +- The first TN harness expected the newer near-LOD marker, which `90378a5` does not emit. It was changed to use whole-view readiness with only LOD0–2 enabled; all five pairs then completed. +- The first full-view audit used `--recipe=surface-world`, which rebuilt a recipe snapshot and hit the stale root route (65/5,310). Removing the override imported the canonical export and passed. +- The first fast-tier launch lacked its round-specific binary. A later diagnostic exposed lost export provenance in snapshot creation; that was threaded through, and the final fast tier passed. +- The first UE build could not rotate a log in the read-only user config directory. Retrying with `XDG_CONFIG_HOME` under `/tmp` succeeded. + +Logs and summaries are under `Saved/Linux/l82/`. No commit, push, or stash was made. +All headless runs used the requested CPU affinity and 6 GiB address-space cap; UBT used the CPU affinity without the cap. No run reached 30 minutes. diff --git a/Standalone/Research/WorldAssemblerM3/Linux82/RunL82Census.py b/Standalone/Research/WorldAssemblerM3/Linux82/RunL82Census.py new file mode 100644 index 0000000..8c7cad5 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux82/RunL82Census.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Repeat the L77 FI/TN phase census on the L82 passage implementation.""" +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/l82/census" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARY = ROOT / "Saved/Linux/l82/profile-bin/VoxelForgeCoreHeadless-clang19" +CAMERAS = { + "FI": (25.8369140625, 3.19921875, -737.0), + "TN": (19.166015625, -14.2001953125, -753.3740234375), +} +SAMPLE = re.compile(r"^\[L77Sample\] .*?lod=(\d+) .*?phase=(\S+) .*?weight=(\d+)$", re.M) +SAMPLER = re.compile(r"^\[L77Sampler\] action=stop .*?samples=(\d+).*?status=PASS$", re.M) + + +def run(world: str, rep: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"l82-phase-{world.lower()}-r{rep}.log" + camera = ",".join(f"{value:.12f}" for value in CAMERAS[world]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "env", "VOXELFORGE_L81_PASSAGE_BOUND_PADDING_VOXELS=0", "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", "--linux77-profile", + ] + start = 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) + output = log.read_text(encoding="utf-8", errors="replace") + matches = [(int(lod), phase, int(weight)) for lod, phase, weight in SAMPLE.findall(output)] + sampler = SAMPLER.search(output) + if result.returncode or not matches or sampler is None: + raise RuntimeError(f"census failed: exit={result.returncode} {log}") + near = [row for row in matches if row[0] <= 2] + passage = sum(weight for _, phase, weight in near if phase == "passages") + total = sum(weight for _, _, weight in near) + row = { + "world": world, "replicate": rep, "exit_code": result.returncode, + "wall_s": round(time.monotonic() - start, 3), + "sampler_samples": int(sampler.group(1)), "near_lod_samples": len(near), + "near_lod_weighted_ticks": total, "near_lod_passage_weighted_ticks": passage, + "near_lod_passage_share_pct": 100.0 * passage / total if total else 0.0, + "log": str(log.relative_to(ROOT)), + } + print("[L82CensusRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + if not BINARY.is_file() or not DATA.is_file(): + raise SystemExit(f"missing input: {BINARY} or {DATA}") + rows = [run(world, rep) for world in CAMERAS for rep in (1, 2, 3)] + by_world = {} + for world in CAMERAS: + values = [row["near_lod_passage_share_pct"] for row in rows if row["world"] == world] + by_world[world] = {"runs_pct": values, "mean_pct": sum(values) / len(values)} + (OUT / "summary.json").write_text(json.dumps( + {"runs": rows, "near_lod_passage_share": by_world}, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux82/RunL82Hashes.py b/Standalone/Research/WorldAssemblerM3/Linux82/RunL82Hashes.py new file mode 100644 index 0000000..da821b1 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux82/RunL82Hashes.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Compare L81 and L82 per-tile hashes over each four-strate LOD0-1 view.""" +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/l82/hash-check" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "L81": ROOT / "Saved/Linux/l81/bin/VoxelForgeCoreHeadless-l81-clang", + "L82": ROOT / "Saved/Linux/l82/bin/VoxelForgeCoreHeadless-clang19", +} +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), +} +TILE_HASH = re.compile( + r"^\[PipelineTileHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) " + r"density=(0x[0-9a-f]+) mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) " + r"topology=(0x[0-9a-f]+)$", re.M) +WHOLE_HASH = re.compile( + r"^\[PipelineWholeViewHash\] tiles=(\d+) density=(0x[0-9a-f]+) " + r"mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) topology=(0x[0-9a-f]+)$", re.M) + + +def run(strate: int, version: str) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"{version.lower()}-s{strate}.log" + camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "env", "VOXELFORGE_L81_PASSAGE_BOUND_PADDING_VOXELS=0", "timeout", "1740s", + str(BINARIES[version]), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=1", f"--camera={camera}", + "--stream-hash-tiles", + ] + start = 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) + output = log.read_text(encoding="utf-8", errors="replace") + hashes = { + (int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))): m.group(5, 6, 7, 8) + for m in TILE_HASH.finditer(output) + } + whole = WHOLE_HASH.search(output) + if result.returncode or whole is None or len(hashes) != 960: + raise RuntimeError(f"hash run failed or incomplete: exit={result.returncode} {log}") + row = { + "version": version, "strate": strate, "exit_code": result.returncode, + "wall_s": round(time.monotonic() - start, 3), "tile_hash_count": len(hashes), + "whole_hash": whole.group(1, 2, 3, 4, 5), "hashes": hashes, + "log": str(log.relative_to(ROOT)), + } + print("[L82HashRun] " + json.dumps( + {k: v for k, v in row.items() if k != "hashes"}, sort_keys=True), flush=True) + return row + + +def main() -> int: + for binary in (*BINARIES.values(), DATA): + if not binary.is_file(): + raise SystemExit(f"missing input: {binary}") + rows = [] + comparisons = [] + for strate in range(4): + pair = {version: run(strate, version) for version in ("L81", "L82")} + a, b = pair["L81"], pair["L82"] + mismatch_keys = sorted(key for key in set(a["hashes"]) | set(b["hashes"]) + if a["hashes"].get(key) != b["hashes"].get(key)) + comparison = { + "strate": strate, "tiles": len(a["hashes"]), + "tile_hashes_identical": not mismatch_keys, + "whole_hash_identical": a["whole_hash"] == b["whole_hash"], + "mismatch_count": len(mismatch_keys), + "first_mismatches": mismatch_keys[:8], + "l81_whole_hash": a["whole_hash"], "l82_whole_hash": b["whole_hash"], + } + comparisons.append(comparison) + rows.extend({k: v for k, v in run_data.items() if k != "hashes"} + for run_data in pair.values()) + print("[L82HashComparison] " + json.dumps(comparison, sort_keys=True), flush=True) + summary = {"comparisons": comparisons, "runs": rows} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + return 0 if all(item["tile_hashes_identical"] and item["whole_hash_identical"] + for item in comparisons) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux82/RunL82OwnerPassageAudit.py b/Standalone/Research/WorldAssemblerM3/Linux82/RunL82OwnerPassageAudit.py new file mode 100644 index 0000000..bd325ef --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux82/RunL82OwnerPassageAudit.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Run the full-view surface-world passage validation against resolved routes.""" +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/l82/audits" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARY = ROOT / "Saved/Linux/l82/bin/VoxelForgeCoreHeadless-clang19" +RECIPE = re.compile(r"^\[WorldAssemblerRecipeAudit\] requested=surface-world " + r"actual=surface-world .*status=PASS$", re.M) +WALK = re.compile(r"^\[Walkability\] PASS source=resolved_core_layout (\d+)/(\d+) " + r"compiled Core passage centerline samples remain open$", re.M) +AUDIT = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M) +SUMMARY = re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M) + + +def main() -> int: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / "owner-surface-world-full.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "env", "VOXELFORGE_L81_PASSAGE_BOUND_PADDING_VOXELS=0", "timeout", "1740s", + str(BINARY), str(DATA), "--workers=12", "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=3", + "--max-clip-level=8", "--max-level=-1", + "--audit-resolved-layout-passages", + ] + start = 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) + output = log.read_text(encoding="utf-8", errors="replace") + walk = WALK.search(output) + rows = { + "exit_code": result.returncode, + "wall_s": round(time.monotonic() - start, 3), + "recipe_pass": RECIPE.search(output) is not None, + "canonical_walkability_pass": walk is not None, + "canonical_center_samples": int(walk.group(2)) if walk else 0, + "resolved_audit_passages": len(AUDIT.findall(output)), + "resolved_audit_summary_pass": SUMMARY.search(output) is not None, + "legacy_root_route_failure": "[WalkabilityRoute] passage=" in output, + "log": str(log.relative_to(ROOT)), + } + (OUT / "summary.json").write_text(json.dumps(rows, indent=2) + "\n", + encoding="utf-8") + print("[L82OwnerPassageAudit] " + json.dumps(rows, sort_keys=True), flush=True) + return 0 if (result.returncode == 0 and rows["recipe_pass"] + and rows["canonical_walkability_pass"] + and rows["resolved_audit_passages"] > 0 + and rows["resolved_audit_summary_pass"] + and not rows["legacy_root_route_failure"]) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux82/RunL82Readiness.py b/Standalone/Research/WorldAssemblerM3/Linux82/RunL82Readiness.py new file mode 100644 index 0000000..9136f9b --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux82/RunL82Readiness.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Three interleaved L80-protocol readiness pairs per stratum, L81 vs L82.""" +from __future__ import annotations + +import json +import re +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l82/readiness" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "L81": ROOT / "Saved/Linux/l81/bin/VoxelForgeCoreHeadless-l81-clang", + "L82": ROOT / "Saved/Linux/l82/bin/VoxelForgeCoreHeadless-clang19", +} +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+) " + r"cold_ready_s=([0-9.]+)$", re.M) + + +def run(strate: int, version: str, phase: str, rep: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"s{strate}-{version.lower()}-{phase}{rep}.log" + camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "env", "VOXELFORGE_L81_PASSAGE_BOUND_PADDING_VOXELS=0", "timeout", "1740s", + str(BINARIES[version]), 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", "--stream-scheduler-policy=3", + ] + start = 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) + output = log.read_text(encoding="utf-8", errors="replace") + pipeline_match, near_match = PIPELINE.search(output), NEAR.search(output) + if result.returncode or pipeline_match is None or near_match is None: + raise RuntimeError(f"readiness run failed: exit={result.returncode} {log}") + pipeline = dict(FIELDS.findall(pipeline_match.group(1))) + row = { + "strate": strate, "version": version, "phase": phase, "replicate": rep, + "exit_code": result.returncode, "wall_s": round(time.monotonic() - start, 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("[L82ReadinessRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + for binary in (*BINARIES.values(), DATA): + if not binary.is_file(): + raise SystemExit(f"missing input: {binary}") + for strate in range(4): + for version in ("L81", "L82"): + run(strate, version, "warmup", 0) + rows = [] + orders = (("L81", "L82"), ("L82", "L81"), ("L81", "L82")) + for strate in range(4): + for rep, order in enumerate(orders, 1): + for version in order: + rows.append(run(strate, version, "measured", rep)) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n", + encoding="utf-8") + summary = [] + for strate in range(4): + group = [row for row in rows if row["strate"] == strate] + item = {"strate": strate, "measured_pairs": 3} + for version in ("L81", "L82"): + subset = [row for row in group if row["version"] == version] + for metric in ("lod02_ready_s", "whole_view_ready_s"): + values = [row[metric] for row in subset] + item[f"{version}_{metric}"] = values + item[f"{version}_{metric}_median"] = statistics.median(values) + 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/Linux82/RunL82TNSlowdown.py b/Standalone/Research/WorldAssemblerM3/Linux82/RunL82TNSlowdown.py new file mode 100644 index 0000000..c3d5804 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux82/RunL82TNSlowdown.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Five interleaved TN LOD0-2 readiness pairs: 90378a5 vs 9cb5e53.""" +from __future__ import annotations + +import json +import re +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l82/tn-regression" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "90378a5": ROOT / "Saved/Linux/l76/verify/bin/VoxelForgeCoreHeadless-verify-clang", + "9cb5e53": ROOT / "Saved/Linux/l81/bin/VoxelForgeCoreHeadless-l81-clang", +} +CAMERA = (19.166015625, -14.2001953125, -753.3740234375) +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+) " + r"cold_ready_s=([0-9.]+)$", re.M) + + +def run(version: str, rep: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"{version}-r{rep}.log" + camera = ",".join(f"{v:.12f}" for v in CAMERA) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "env", "VOXELFORGE_L81_PASSAGE_BOUND_PADDING_VOXELS=0", "timeout", "1740s", + str(BINARIES[version]), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=2", f"--camera={camera}", + "--profile-lod-work", + ] + start = 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) + output = log.read_text(encoding="utf-8", errors="replace") + pipeline_match, near_match = PIPELINE.search(output), NEAR.search(output) + if result.returncode or pipeline_match is None: + raise RuntimeError(f"TN run failed: exit={result.returncode} {log}") + pipeline = dict(FIELDS.findall(pipeline_match.group(1))) + whole_tiles = int(pipeline["whole_tiles"].split("/")[0]) + if near_match is not None: + lod02_ready_s = float(near_match.group(3)) + lod02_tiles = int(near_match.group(1)) + else: + # 90378a5 predates the explicit near-LOD milestone. With max-level=2, + # the complete 0-2 view is the whole-view milestone. + lod02_ready_s = float(pipeline["whole_view_ready"].removesuffix("s")) + lod02_tiles = whole_tiles + if lod02_tiles != 1344 or whole_tiles != 1344: + raise RuntimeError(f"TN tile scope mismatch: lod02={lod02_tiles} whole={whole_tiles} {log}") + row = { + "version": version, "pair": rep, "exit_code": result.returncode, + "wall_s": round(time.monotonic() - start, 3), + "lod02_ready_s": lod02_ready_s, + "whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")), + "whole_tiles": whole_tiles, + "log": str(log.relative_to(ROOT)), + } + print("[L82TNRegressionRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + for path in (*BINARIES.values(), DATA): + if not path.is_file(): + raise SystemExit(f"missing input: {path}") + rows = [] + for rep in range(1, 6): + order = ("90378a5", "9cb5e53") if rep % 2 else ("9cb5e53", "90378a5") + for version in order: + rows.append(run(version, rep)) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n", + encoding="utf-8") + by_pair = {rep: {row["version"]: row for row in rows if row["pair"] == rep} + for rep in range(1, 6)} + paired_ready_delta = [by_pair[rep]["9cb5e53"]["lod02_ready_s"] + - by_pair[rep]["90378a5"]["lod02_ready_s"] + for rep in range(1, 6)] + paired_whole_delta = [by_pair[rep]["9cb5e53"]["whole_view_ready_s"] + - by_pair[rep]["90378a5"]["whole_view_ready_s"] + for rep in range(1, 6)] + summary = { + "pairs": 5, + "90378a5_lod02_median_s": statistics.median( + row["lod02_ready_s"] for row in rows if row["version"] == "90378a5"), + "9cb5e53_lod02_median_s": statistics.median( + row["lod02_ready_s"] for row in rows if row["version"] == "9cb5e53"), + "paired_lod02_delta_s": paired_ready_delta, + "paired_lod02_delta_median_s": statistics.median(paired_ready_delta), + "90378a5_whole_median_s": statistics.median( + row["whole_view_ready_s"] for row in rows if row["version"] == "90378a5"), + "9cb5e53_whole_median_s": statistics.median( + row["whole_view_ready_s"] for row in rows if row["version"] == "9cb5e53"), + "paired_whole_delta_s": paired_whole_delta, + "paired_whole_delta_median_s": statistics.median(paired_whole_delta), + "scheduling": "both binaries used their default scheduler settings; 90378a5 predates the policy flag", + "runs": rows, + } + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + print("[L82TNRegressionSummary] " + json.dumps( + {k: v for k, v in summary.items() if k != "runs"}, sort_keys=True), flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 71e2a7f..85029b0 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -2242,7 +2242,7 @@ int main(int argc,char** argv) std::chrono::duration(Clock::now()-planStart).count()); auto snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(ownerJsonRoot,seed, - options.ClipRadius,options.MaxClipLevel,false,false,error); + options.ClipRadius,options.MaxClipLevel,false,false,error,useExportedCorePlan); if(!snapshot) {std::fprintf(stderr,"Shape interval build failed: %s\n",error.c_str());return 2;}